summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:28:17 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:28:17 +0000
commit7a46c07230b8d8108c0e8e80df4522d0ac116538 (patch)
treed483300dab478b994fe199a5d19d18d74153718a
parentInitial commit. (diff)
downloadpipewire-7a46c07230b8d8108c0e8e80df4522d0ac116538.tar.xz
pipewire-7a46c07230b8d8108c0e8e80df4522d0ac116538.zip
Adding upstream version 0.3.65.upstream/0.3.65upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.cirrus.yml24
-rw-r--r--.codespell-ignore20
-rw-r--r--.editorconfig27
-rw-r--r--.gitignore41
-rw-r--r--.gitlab-ci.yml477
-rwxr-xr-x.gitlab/ci/check_missing_headers.sh28
-rw-r--r--.gitlab/issue_templates/.gitkeep0
-rw-r--r--.gitlab/issue_templates/bluetooth issue.md43
-rw-r--r--.gitlab/issue_templates/issue.md29
-rw-r--r--CODE_OF_CONDUCT.md77
-rw-r--r--COPYING26
-rw-r--r--INSTALL.md237
-rw-r--r--LICENSE11
-rw-r--r--Makefile.in78
-rw-r--r--NEWS4812
-rw-r--r--README.md218
-rwxr-xr-xautogen.sh18
-rw-r--r--doc/Doxyfile.in68
-rw-r--r--doc/api-tree.dox126
-rw-r--r--doc/api.dox89
-rw-r--r--doc/custom.css19
-rw-r--r--doc/dma-buf.dox163
-rw-r--r--doc/doxygen-awesome.css1364
-rw-r--r--doc/examples.dox.in9
-rw-r--r--doc/index.dox45
-rwxr-xr-xdoc/input-filter-h.sh32
-rwxr-xr-xdoc/input-filter.sh10
-rw-r--r--doc/manpage.dox.in5
-rw-r--r--doc/meson.build162
-rw-r--r--doc/overview.dox42
-rw-r--r--doc/pipewire-access.dox126
-rw-r--r--doc/pipewire-architecture.dox0
-rw-r--r--doc/pipewire-audio.dox127
-rw-r--r--doc/pipewire-daemon.dox175
-rw-r--r--doc/pipewire-design.dox70
-rw-r--r--doc/pipewire-library.dox240
-rw-r--r--doc/pipewire-midi.dox103
-rw-r--r--doc/pipewire-modules.dox84
-rw-r--r--doc/pipewire-objects-design.dox347
-rw-r--r--doc/pipewire-portal.dox215
-rw-r--r--doc/pipewire-session-manager.dox46
-rw-r--r--doc/pipewire-tools.dox.in7
-rw-r--r--doc/pipewire.dox26
-rw-r--r--doc/pulseaudio.dox69
-rw-r--r--doc/spa-buffer.dox71
-rw-r--r--doc/spa-design.dox35
-rw-r--r--doc/spa-index.dox88
-rw-r--r--doc/spa-plugins.dox360
-rw-r--r--doc/spa-pod.dox530
-rw-r--r--doc/tutorial.dox21
-rw-r--r--doc/tutorial1.c19
-rw-r--r--doc/tutorial1.dox47
-rw-r--r--doc/tutorial2.c56
-rw-r--r--doc/tutorial2.dox129
-rw-r--r--doc/tutorial3.c89
-rw-r--r--doc/tutorial3.dox119
-rw-r--r--doc/tutorial4.c112
-rw-r--r--doc/tutorial4.dox158
-rw-r--r--doc/tutorial5.c141
-rw-r--r--doc/tutorial5.dox223
-rw-r--r--doc/tutorial6.c97
-rw-r--r--doc/tutorial6.dox69
-rw-r--r--include/valgrind/memcheck.h302
-rw-r--r--include/valgrind/valgrind.h6647
-rw-r--r--man/meson.build44
-rw-r--r--man/pipewire-pulse.1.rst.in48
-rw-r--r--man/pipewire.1.rst.in54
-rw-r--r--man/pipewire.conf.5.rst.in113
-rw-r--r--man/pw-cat.1.rst.in174
-rw-r--r--man/pw-cli.1.rst.in190
-rw-r--r--man/pw-dot.1.rst.in65
-rw-r--r--man/pw-jack.1.rst.in65
-rw-r--r--man/pw-link.1.rst.in139
-rw-r--r--man/pw-metadata.1.rst.in67
-rw-r--r--man/pw-mididump.1.rst.in49
-rw-r--r--man/pw-mon.1.rst.in46
-rw-r--r--man/pw-profiler.1.rst.in57
-rw-r--r--man/pw-top.1.rst.in178
-rw-r--r--meson.build495
-rw-r--r--meson_options.txt283
-rw-r--r--pipewire-alsa/alsa-plugins/ctl_pipewire.c1496
-rw-r--r--pipewire-alsa/alsa-plugins/meson.build27
-rw-r--r--pipewire-alsa/alsa-plugins/pcm_pipewire.c1333
-rw-r--r--pipewire-alsa/conf/50-pipewire.conf106
-rw-r--r--pipewire-alsa/conf/99-pipewire-default.conf13
-rw-r--r--pipewire-alsa/conf/meson.build5
-rw-r--r--pipewire-alsa/tests/meson.build23
-rw-r--r--pipewire-alsa/tests/test-pipewire-alsa-stress.c151
-rw-r--r--pipewire-jack/examples/video-dsp-play.c203
-rw-r--r--pipewire-jack/jack/control.h658
-rw-r--r--pipewire-jack/jack/intclient.h130
-rw-r--r--pipewire-jack/jack/jack.h1477
-rw-r--r--pipewire-jack/jack/jslist.h293
-rw-r--r--pipewire-jack/jack/metadata.h322
-rw-r--r--pipewire-jack/jack/midiport.h197
-rw-r--r--pipewire-jack/jack/net.h429
-rw-r--r--pipewire-jack/jack/ringbuffer.h243
-rw-r--r--pipewire-jack/jack/session.h302
-rw-r--r--pipewire-jack/jack/statistics.h57
-rw-r--r--pipewire-jack/jack/systemdeps.h141
-rw-r--r--pipewire-jack/jack/thread.h160
-rw-r--r--pipewire-jack/jack/transport.h247
-rw-r--r--pipewire-jack/jack/types.h740
-rw-r--r--pipewire-jack/jack/uuid.h50
-rw-r--r--pipewire-jack/jack/weakjack.h125
-rw-r--r--pipewire-jack/jack/weakmacros.h97
-rw-r--r--pipewire-jack/meson.build5
-rw-r--r--pipewire-jack/src/control.c472
-rw-r--r--pipewire-jack/src/dummy.c39
-rw-r--r--pipewire-jack/src/export.c36
-rw-r--r--pipewire-jack/src/meson.build98
-rw-r--r--pipewire-jack/src/metadata.c421
-rw-r--r--pipewire-jack/src/net.c169
-rw-r--r--pipewire-jack/src/pipewire-jack-extensions.h50
-rw-r--r--pipewire-jack/src/pipewire-jack.c6509
-rwxr-xr-xpipewire-jack/src/pw-jack.in78
-rw-r--r--pipewire-jack/src/ringbuffer.c302
-rw-r--r--pipewire-jack/src/statistics.c66
-rw-r--r--pipewire-jack/src/uuid.c111
-rw-r--r--pipewire-v4l2/meson.build1
-rw-r--r--pipewire-v4l2/src/meson.build41
-rw-r--r--pipewire-v4l2/src/pipewire-v4l2.c2592
-rw-r--r--pipewire-v4l2/src/pipewire-v4l2.h38
-rwxr-xr-xpipewire-v4l2/src/pw-v4l2.in71
-rw-r--r--pipewire-v4l2/src/v4l2-func.c150
-rw-r--r--po/LINGUAS53
-rw-r--r--po/POTFILES.in25
-rw-r--r--po/POTFILES.skip1
-rw-r--r--po/af.po577
-rw-r--r--po/as.po619
-rw-r--r--po/be.po628
-rw-r--r--po/bg.po569
-rw-r--r--po/bn_IN.po618
-rw-r--r--po/ca.po619
-rw-r--r--po/cs.po731
-rw-r--r--po/da.po647
-rw-r--r--po/de.po618
-rw-r--r--po/de_CH.po638
-rw-r--r--po/el.po613
-rw-r--r--po/eo.po573
-rw-r--r--po/es.po621
-rw-r--r--po/fi.po650
-rw-r--r--po/fr.po623
-rw-r--r--po/gl.po707
-rw-r--r--po/gu.po622
-rw-r--r--po/he.po573
-rw-r--r--po/hi.po627
-rw-r--r--po/hr.po727
-rw-r--r--po/hu.po715
-rw-r--r--po/id.po671
-rw-r--r--po/it.po600
-rw-r--r--po/ja.po602
-rw-r--r--po/ka.po692
-rw-r--r--po/kk.po578
-rw-r--r--po/kn.po618
-rw-r--r--po/ko.po596
-rw-r--r--po/lt.po623
-rw-r--r--po/meson.build12
-rw-r--r--po/ml.po608
-rw-r--r--po/mr.po618
-rw-r--r--po/my.po595
-rw-r--r--po/nl.po605
-rw-r--r--po/nn.po638
-rw-r--r--po/oc.po617
-rw-r--r--po/or.po641
-rw-r--r--po/pa.po614
-rw-r--r--po/pipewire.pot619
-rw-r--r--po/pl.po718
-rw-r--r--po/pt.po677
-rw-r--r--po/pt_BR.po720
-rw-r--r--po/ro.po679
-rw-r--r--po/ru.po695
-rw-r--r--po/si.po571
-rw-r--r--po/sk.po587
-rw-r--r--po/sr.po641
-rw-r--r--po/sr@latin.po641
-rw-r--r--po/sv.po713
-rw-r--r--po/ta.po647
-rw-r--r--po/te.po624
-rw-r--r--po/tr.po691
-rw-r--r--po/uk.po714
-rw-r--r--po/zh_CN.po626
-rw-r--r--po/zh_TW.po585
-rwxr-xr-xpw-uninstalled.sh66
-rw-r--r--spa/examples/adapter-control.c771
-rw-r--r--spa/examples/example-control.c560
-rw-r--r--spa/examples/local-libcamera.c562
-rw-r--r--spa/examples/local-v4l2.c553
-rw-r--r--spa/examples/meson.build32
-rw-r--r--spa/include/meson.build18
-rw-r--r--spa/include/spa/buffer/alloc.h347
-rw-r--r--spa/include/spa/buffer/buffer.h131
-rw-r--r--spa/include/spa/buffer/meta.h192
-rw-r--r--spa/include/spa/buffer/type-info.h94
-rw-r--r--spa/include/spa/control/control.h62
-rw-r--r--spa/include/spa/control/type-info.h61
-rw-r--r--spa/include/spa/debug/buffer.h133
-rw-r--r--spa/include/spa/debug/context.h62
-rw-r--r--spa/include/spa/debug/dict.h62
-rw-r--r--spa/include/spa/debug/format.h234
-rw-r--r--spa/include/spa/debug/log.h103
-rw-r--r--spa/include/spa/debug/mem.h71
-rw-r--r--spa/include/spa/debug/node.h67
-rw-r--r--spa/include/spa/debug/pod.h227
-rw-r--r--spa/include/spa/debug/types.h127
-rw-r--r--spa/include/spa/graph/graph.h365
-rw-r--r--spa/include/spa/interfaces/audio/aec.h110
-rw-r--r--spa/include/spa/monitor/device.h307
-rw-r--r--spa/include/spa/monitor/event.h63
-rw-r--r--spa/include/spa/monitor/type-info.h67
-rw-r--r--spa/include/spa/monitor/utils.h106
-rw-r--r--spa/include/spa/node/command.h73
-rw-r--r--spa/include/spa/node/event.h64
-rw-r--r--spa/include/spa/node/io.h304
-rw-r--r--spa/include/spa/node/keys.h64
-rw-r--r--spa/include/spa/node/node.h680
-rw-r--r--spa/include/spa/node/type-info.h107
-rw-r--r--spa/include/spa/node/utils.h158
-rw-r--r--spa/include/spa/param/audio/aac-types.h58
-rw-r--r--spa/include/spa/param/audio/aac-utils.h89
-rw-r--r--spa/include/spa/param/audio/aac.h71
-rw-r--r--spa/include/spa/param/audio/alac-utils.h80
-rw-r--r--spa/include/spa/param/audio/alac.h49
-rw-r--r--spa/include/spa/param/audio/amr-types.h52
-rw-r--r--spa/include/spa/param/audio/amr-utils.h84
-rw-r--r--spa/include/spa/param/audio/amr.h56
-rw-r--r--spa/include/spa/param/audio/ape-utils.h80
-rw-r--r--spa/include/spa/param/audio/ape.h49
-rw-r--r--spa/include/spa/param/audio/compressed.h39
-rw-r--r--spa/include/spa/param/audio/dsd-utils.h100
-rw-r--r--spa/include/spa/param/audio/dsd.h81
-rw-r--r--spa/include/spa/param/audio/dsp-utils.h75
-rw-r--r--spa/include/spa/param/audio/dsp.h48
-rw-r--r--spa/include/spa/param/audio/flac-utils.h80
-rw-r--r--spa/include/spa/param/audio/flac.h49
-rw-r--r--spa/include/spa/param/audio/format-utils.h141
-rw-r--r--spa/include/spa/param/audio/format.h80
-rw-r--r--spa/include/spa/param/audio/iec958-types.h59
-rw-r--r--spa/include/spa/param/audio/iec958-utils.h79
-rw-r--r--spa/include/spa/param/audio/iec958.h69
-rw-r--r--spa/include/spa/param/audio/layout.h192
-rw-r--r--spa/include/spa/param/audio/mp3-types.h54
-rw-r--r--spa/include/spa/param/audio/mp3-utils.h80
-rw-r--r--spa/include/spa/param/audio/mp3.h57
-rw-r--r--spa/include/spa/param/audio/ra-utils.h80
-rw-r--r--spa/include/spa/param/audio/ra.h49
-rw-r--r--spa/include/spa/param/audio/raw-types.h278
-rw-r--r--spa/include/spa/param/audio/raw-utils.h96
-rw-r--r--spa/include/spa/param/audio/raw.h317
-rw-r--r--spa/include/spa/param/audio/type-info.h35
-rw-r--r--spa/include/spa/param/audio/vorbis-utils.h80
-rw-r--r--spa/include/spa/param/audio/vorbis.h49
-rw-r--r--spa/include/spa/param/audio/wma-types.h57
-rw-r--r--spa/include/spa/param/audio/wma-utils.h93
-rw-r--r--spa/include/spa/param/audio/wma.h67
-rw-r--r--spa/include/spa/param/bluetooth/audio.h74
-rw-r--r--spa/include/spa/param/bluetooth/type-info.h78
-rw-r--r--spa/include/spa/param/buffers-types.h90
-rw-r--r--spa/include/spa/param/buffers.h72
-rw-r--r--spa/include/spa/param/format-types.h191
-rw-r--r--spa/include/spa/param/format-utils.h58
-rw-r--r--spa/include/spa/param/format.h176
-rw-r--r--spa/include/spa/param/latency-types.h75
-rw-r--r--spa/include/spa/param/latency-utils.h177
-rw-r--r--spa/include/spa/param/latency.h89
-rw-r--r--spa/include/spa/param/param-types.h115
-rw-r--r--spa/include/spa/param/param.h107
-rw-r--r--spa/include/spa/param/port-config-types.h73
-rw-r--r--spa/include/spa/param/port-config.h66
-rw-r--r--spa/include/spa/param/profile-types.h65
-rw-r--r--spa/include/spa/param/profile.h72
-rw-r--r--spa/include/spa/param/profiler-types.h60
-rw-r--r--spa/include/spa/param/profiler.h97
-rw-r--r--spa/include/spa/param/props-types.h119
-rw-r--r--spa/include/spa/param/props.h132
-rw-r--r--spa/include/spa/param/route-types.h71
-rw-r--r--spa/include/spa/param/route.h69
-rw-r--r--spa/include/spa/param/type-info.h38
-rw-r--r--spa/include/spa/param/video/chroma.h64
-rw-r--r--spa/include/spa/param/video/color.h125
-rw-r--r--spa/include/spa/param/video/dsp-utils.h83
-rw-r--r--spa/include/spa/param/video/dsp.h55
-rw-r--r--spa/include/spa/param/video/encoded.h31
-rw-r--r--spa/include/spa/param/video/format-utils.h84
-rw-r--r--spa/include/spa/param/video/format.h61
-rw-r--r--spa/include/spa/param/video/h264-utils.h90
-rw-r--r--spa/include/spa/param/video/h264.h68
-rw-r--r--spa/include/spa/param/video/mjpg-utils.h82
-rw-r--r--spa/include/spa/param/video/mjpg.h53
-rw-r--r--spa/include/spa/param/video/multiview.h134
-rw-r--r--spa/include/spa/param/video/raw-types.h164
-rw-r--r--spa/include/spa/param/video/raw-utils.h135
-rw-r--r--spa/include/spa/param/video/raw.h221
-rw-r--r--spa/include/spa/param/video/type-info.h30
-rw-r--r--spa/include/spa/pod/builder.h701
-rw-r--r--spa/include/spa/pod/command.h69
-rw-r--r--spa/include/spa/pod/compare.h190
-rw-r--r--spa/include/spa/pod/dynamic.h81
-rw-r--r--spa/include/spa/pod/event.h68
-rw-r--r--spa/include/spa/pod/filter.h481
-rw-r--r--spa/include/spa/pod/iter.h475
-rw-r--r--spa/include/spa/pod/parser.h594
-rw-r--r--spa/include/spa/pod/pod.h246
-rw-r--r--spa/include/spa/pod/vararg.h113
-rw-r--r--spa/include/spa/support/cpu.h168
-rw-r--r--spa/include/spa/support/dbus.h167
-rw-r--r--spa/include/spa/support/i18n.h107
-rw-r--r--spa/include/spa/support/log-impl.h143
-rw-r--r--spa/include/spa/support/log.h321
-rw-r--r--spa/include/spa/support/loop.h327
-rw-r--r--spa/include/spa/support/plugin-loader.h101
-rw-r--r--spa/include/spa/support/plugin.h229
-rw-r--r--spa/include/spa/support/system.h165
-rw-r--r--spa/include/spa/support/thread.h149
-rw-r--r--spa/include/spa/utils/ansi.h114
-rw-r--r--spa/include/spa/utils/defs.h399
-rw-r--r--spa/include/spa/utils/dict.h120
-rw-r--r--spa/include/spa/utils/dll.h71
-rw-r--r--spa/include/spa/utils/enum-types.h65
-rw-r--r--spa/include/spa/utils/hook.h472
-rw-r--r--spa/include/spa/utils/json-pod.h177
-rw-r--r--spa/include/spa/utils/json.h485
-rw-r--r--spa/include/spa/utils/keys.h151
-rw-r--r--spa/include/spa/utils/list.h166
-rw-r--r--spa/include/spa/utils/names.h164
-rw-r--r--spa/include/spa/utils/result.h72
-rw-r--r--spa/include/spa/utils/ringbuffer.h188
-rw-r--r--spa/include/spa/utils/string.h414
-rw-r--r--spa/include/spa/utils/type-info.h118
-rw-r--r--spa/include/spa/utils/type.h153
-rw-r--r--spa/meson.build101
-rw-r--r--spa/plugins/aec/aec-null.c175
-rw-r--r--spa/plugins/aec/aec-webrtc.cpp276
-rw-r--r--spa/plugins/aec/meson.build16
-rw-r--r--spa/plugins/alsa/90-pipewire-alsa.rules214
-rw-r--r--spa/plugins/alsa/acp-tool.c786
-rw-r--r--spa/plugins/alsa/acp/acp.c1983
-rw-r--r--spa/plugins/alsa/acp/acp.h308
-rw-r--r--spa/plugins/alsa/acp/alsa-mixer.c5398
-rw-r--r--spa/plugins/alsa/acp/alsa-mixer.h459
-rw-r--r--spa/plugins/alsa/acp/alsa-ucm.c2420
-rw-r--r--spa/plugins/alsa/acp/alsa-ucm.h301
-rw-r--r--spa/plugins/alsa/acp/alsa-util.c1904
-rw-r--r--spa/plugins/alsa/acp/alsa-util.h176
-rw-r--r--spa/plugins/alsa/acp/array.h150
-rw-r--r--spa/plugins/alsa/acp/card.h75
-rw-r--r--spa/plugins/alsa/acp/channelmap.h476
-rw-r--r--spa/plugins/alsa/acp/compat.c210
-rw-r--r--spa/plugins/alsa/acp/compat.h656
-rw-r--r--spa/plugins/alsa/acp/conf-parser.c387
-rw-r--r--spa/plugins/alsa/acp/conf-parser.h88
-rw-r--r--spa/plugins/alsa/acp/device-port.h119
-rw-r--r--spa/plugins/alsa/acp/dynarray.h159
-rw-r--r--spa/plugins/alsa/acp/hashmap.h219
-rw-r--r--spa/plugins/alsa/acp/idxset.h200
-rw-r--r--spa/plugins/alsa/acp/llist.h109
-rw-r--r--spa/plugins/alsa/acp/meson.build22
-rw-r--r--spa/plugins/alsa/acp/proplist.h211
-rw-r--r--spa/plugins/alsa/acp/volume.h207
-rw-r--r--spa/plugins/alsa/alsa-acp-device.c1133
-rw-r--r--spa/plugins/alsa/alsa-compress-offload-sink.c1143
-rw-r--r--spa/plugins/alsa/alsa-pcm-device.c581
-rw-r--r--spa/plugins/alsa/alsa-pcm-sink.c1014
-rw-r--r--spa/plugins/alsa/alsa-pcm-source.c964
-rw-r--r--spa/plugins/alsa/alsa-pcm.c2696
-rw-r--r--spa/plugins/alsa/alsa-pcm.h374
-rw-r--r--spa/plugins/alsa/alsa-seq-bridge.c1006
-rw-r--r--spa/plugins/alsa/alsa-seq.c983
-rw-r--r--spa/plugins/alsa/alsa-seq.h199
-rw-r--r--spa/plugins/alsa/alsa-udev.c1014
-rw-r--r--spa/plugins/alsa/alsa.c80
-rw-r--r--spa/plugins/alsa/alsa.h39
-rw-r--r--spa/plugins/alsa/meson.build60
-rw-r--r--spa/plugins/alsa/mixer/meson.build7
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-aux.conf65
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf104
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-fm.conf65
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf104
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf102
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf114
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf133
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf154
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-linein.conf144
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf66
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-mic.conf141
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common60
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf107
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf65
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-video.conf64
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input.conf102
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input.conf.common289
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-chat.conf5
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf118
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-headphones.conf180
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-lineout.conf214
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-mono.conf99
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf187
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-speaker.conf246
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output.conf88
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output.conf.common199
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-0.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-1.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-10.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-2.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-3.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-4.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-5.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-6.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-7.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-8.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-9.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf20
-rw-r--r--spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf18
-rw-r--r--spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf27
-rw-r--r--spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf27
-rw-r--r--spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf34
-rw-r--r--spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf34
-rw-r--r--spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf34
-rw-r--r--spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf5
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/analog-only.conf102
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf93
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/audigy.conf94
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf66
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/default.conf580
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf55
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf153
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/force-speaker.conf152
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf35
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf36
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf38
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf86
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf90
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf161
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.conf110
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf84
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf130
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf53
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf91
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf80
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf112
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf58
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf42
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf23
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf75
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf64
-rw-r--r--spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0150
-rw-r--r--spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x24
-rw-r--r--spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3135
-rw-r--r--spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI4
-rw-r--r--spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD198162
-rw-r--r--spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A113
-rw-r--r--spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A128
-rw-r--r--spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer27
-rw-r--r--spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer37
-rw-r--r--spa/plugins/alsa/mixer/samples/USB Device 0x46d:0x9a4--USB Mixer5
-rw-r--r--spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888211
-rw-r--r--spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+160
-rw-r--r--spa/plugins/alsa/test-hw-params.c173
-rw-r--r--spa/plugins/alsa/test-timer.c310
-rw-r--r--spa/plugins/audioconvert/audioadapter.c1733
-rw-r--r--spa/plugins/audioconvert/audioconvert.c2945
-rw-r--r--spa/plugins/audioconvert/benchmark-fmt-ops.c323
-rw-r--r--spa/plugins/audioconvert/benchmark-resample.c204
-rw-r--r--spa/plugins/audioconvert/biquad.c111
-rw-r--r--spa/plugins/audioconvert/biquad.h45
-rw-r--r--spa/plugins/audioconvert/channelmix-ops-c.c533
-rw-r--r--spa/plugins/audioconvert/channelmix-ops-sse.c522
-rw-r--r--spa/plugins/audioconvert/channelmix-ops.c668
-rw-r--r--spa/plugins/audioconvert/channelmix-ops.h160
-rw-r--r--spa/plugins/audioconvert/crossover.c70
-rw-r--r--spa/plugins/audioconvert/crossover.h31
-rw-r--r--spa/plugins/audioconvert/delay.h72
-rw-r--r--spa/plugins/audioconvert/fmt-ops-avx2.c1043
-rw-r--r--spa/plugins/audioconvert/fmt-ops-c.c433
-rw-r--r--spa/plugins/audioconvert/fmt-ops-neon.c487
-rw-r--r--spa/plugins/audioconvert/fmt-ops-sse2.c1438
-rw-r--r--spa/plugins/audioconvert/fmt-ops-sse41.c86
-rw-r--r--spa/plugins/audioconvert/fmt-ops-ssse3.c111
-rw-r--r--spa/plugins/audioconvert/fmt-ops.c564
-rw-r--r--spa/plugins/audioconvert/fmt-ops.h488
-rw-r--r--spa/plugins/audioconvert/hilbert.h69
-rw-r--r--spa/plugins/audioconvert/law.h2163
-rw-r--r--spa/plugins/audioconvert/meson.build206
-rw-r--r--spa/plugins/audioconvert/peaks-ops-c.c50
-rw-r--r--spa/plugins/audioconvert/peaks-ops-sse.c122
-rw-r--r--spa/plugins/audioconvert/peaks-ops.c89
-rw-r--r--spa/plugins/audioconvert/peaks-ops.h72
-rw-r--r--spa/plugins/audioconvert/plugin.c50
-rw-r--r--spa/plugins/audioconvert/resample-native-avx.c94
-rw-r--r--spa/plugins/audioconvert/resample-native-c.c65
-rw-r--r--spa/plugins/audioconvert/resample-native-impl.h191
-rw-r--r--spa/plugins/audioconvert/resample-native-neon.c218
-rw-r--r--spa/plugins/audioconvert/resample-native-sse.c94
-rw-r--r--spa/plugins/audioconvert/resample-native-ssse3.c115
-rw-r--r--spa/plugins/audioconvert/resample-native.c400
-rw-r--r--spa/plugins/audioconvert/resample-peaks.c148
-rw-r--r--spa/plugins/audioconvert/resample.h69
-rw-r--r--spa/plugins/audioconvert/spa-resample.c341
-rw-r--r--spa/plugins/audioconvert/test-audioadapter.c302
-rw-r--r--spa/plugins/audioconvert/test-audioconvert.c1158
-rw-r--r--spa/plugins/audioconvert/test-channelmix.c374
-rw-r--r--spa/plugins/audioconvert/test-fmt-ops.c798
-rw-r--r--spa/plugins/audioconvert/test-helper.h97
-rw-r--r--spa/plugins/audioconvert/test-peaks.c128
-rw-r--r--spa/plugins/audioconvert/test-resample.c177
-rw-r--r--spa/plugins/audioconvert/test-source.c931
-rw-r--r--spa/plugins/audioconvert/volume-ops-c.c45
-rw-r--r--spa/plugins/audioconvert/volume-ops-sse.c66
-rw-r--r--spa/plugins/audioconvert/volume-ops.c84
-rw-r--r--spa/plugins/audioconvert/volume-ops.h68
-rw-r--r--spa/plugins/audiomixer/audiomixer.c990
-rw-r--r--spa/plugins/audiomixer/benchmark-mix-ops.c222
-rw-r--r--spa/plugins/audiomixer/meson.build126
-rw-r--r--spa/plugins/audiomixer/mix-ops-avx.c88
-rw-r--r--spa/plugins/audiomixer/mix-ops-c.c68
-rw-r--r--spa/plugins/audiomixer/mix-ops-sse.c87
-rw-r--r--spa/plugins/audiomixer/mix-ops-sse2.c87
-rw-r--r--spa/plugins/audiomixer/mix-ops.c136
-rw-r--r--spa/plugins/audiomixer/mix-ops.h169
-rw-r--r--spa/plugins/audiomixer/mixer-dsp.c927
-rw-r--r--spa/plugins/audiomixer/plugin.c50
-rw-r--r--spa/plugins/audiomixer/test-helper.h97
-rw-r--r--spa/plugins/audiomixer/test-mix-ops.c293
-rw-r--r--spa/plugins/audiotestsrc/audiotestsrc.c1159
-rw-r--r--spa/plugins/audiotestsrc/meson.build7
-rw-r--r--spa/plugins/audiotestsrc/plugin.c46
-rw-r--r--spa/plugins/audiotestsrc/render.c64
-rw-r--r--spa/plugins/avb/avb-pcm-sink.c910
-rw-r--r--spa/plugins/avb/avb-pcm-source.c910
-rw-r--r--spa/plugins/avb/avb-pcm.c1227
-rw-r--r--spa/plugins/avb/avb-pcm.h343
-rw-r--r--spa/plugins/avb/avb.c54
-rw-r--r--spa/plugins/avb/avb.h39
-rw-r--r--spa/plugins/avb/avbtp/packets.h220
-rw-r--r--spa/plugins/avb/meson.build14
-rw-r--r--spa/plugins/bluez5/README-MIDI.md24
-rw-r--r--spa/plugins/bluez5/README-OPUS-A2DP.md335
-rw-r--r--spa/plugins/bluez5/README-SBC-XQ.md54
-rw-r--r--spa/plugins/bluez5/a2dp-codec-aac.c661
-rw-r--r--spa/plugins/bluez5/a2dp-codec-aptx.c748
-rw-r--r--spa/plugins/bluez5/a2dp-codec-caps.h460
-rw-r--r--spa/plugins/bluez5/a2dp-codec-faststream.c640
-rw-r--r--spa/plugins/bluez5/a2dp-codec-lc3plus.c790
-rw-r--r--spa/plugins/bluez5/a2dp-codec-ldac.c604
-rw-r--r--spa/plugins/bluez5/a2dp-codec-opus.c1444
-rw-r--r--spa/plugins/bluez5/a2dp-codec-sbc.c689
-rw-r--r--spa/plugins/bluez5/backend-hsphfpd.c1588
-rw-r--r--spa/plugins/bluez5/backend-native.c2838
-rw-r--r--spa/plugins/bluez5/backend-ofono.c947
-rw-r--r--spa/plugins/bluez5/bap-codec-caps.h142
-rw-r--r--spa/plugins/bluez5/bap-codec-lc3.c859
-rw-r--r--spa/plugins/bluez5/bluez-hardware.conf103
-rw-r--r--spa/plugins/bluez5/bluez5-dbus.c5182
-rw-r--r--spa/plugins/bluez5/bluez5-device.c2398
-rw-r--r--spa/plugins/bluez5/codec-loader.c234
-rw-r--r--spa/plugins/bluez5/codec-loader.h39
-rw-r--r--spa/plugins/bluez5/dbus-monitor.c265
-rw-r--r--spa/plugins/bluez5/dbus-monitor.h83
-rw-r--r--spa/plugins/bluez5/decode-buffer.h486
-rw-r--r--spa/plugins/bluez5/defs.h822
-rw-r--r--spa/plugins/bluez5/hci.c93
-rw-r--r--spa/plugins/bluez5/media-codecs.c212
-rw-r--r--spa/plugins/bluez5/media-codecs.h178
-rw-r--r--spa/plugins/bluez5/media-sink.c1868
-rw-r--r--spa/plugins/bluez5/media-source.c1707
-rw-r--r--spa/plugins/bluez5/meson.build206
-rw-r--r--spa/plugins/bluez5/midi-enum.c887
-rw-r--r--spa/plugins/bluez5/midi-node.c2151
-rw-r--r--spa/plugins/bluez5/midi-parser.c295
-rw-r--r--spa/plugins/bluez5/midi-server.c581
-rw-r--r--spa/plugins/bluez5/midi.h142
-rw-r--r--spa/plugins/bluez5/modemmanager.c1249
-rw-r--r--spa/plugins/bluez5/modemmanager.h161
-rw-r--r--spa/plugins/bluez5/org.bluez.xml71
-rw-r--r--spa/plugins/bluez5/player.c428
-rw-r--r--spa/plugins/bluez5/player.h51
-rw-r--r--spa/plugins/bluez5/plugin.c83
-rw-r--r--spa/plugins/bluez5/quirks.c406
-rw-r--r--spa/plugins/bluez5/rtp.h74
-rw-r--r--spa/plugins/bluez5/sco-io.c289
-rw-r--r--spa/plugins/bluez5/sco-sink.c1517
-rw-r--r--spa/plugins/bluez5/sco-source.c1592
-rw-r--r--spa/plugins/bluez5/test-midi.c299
-rw-r--r--spa/plugins/bluez5/upower.c311
-rw-r--r--spa/plugins/bluez5/upower.h36
-rw-r--r--spa/plugins/control/meson.build10
-rw-r--r--spa/plugins/control/mixer.c869
-rw-r--r--spa/plugins/control/plugin.c46
-rw-r--r--spa/plugins/ffmpeg/ffmpeg-dec.c529
-rw-r--r--spa/plugins/ffmpeg/ffmpeg-enc.c507
-rw-r--r--spa/plugins/ffmpeg/ffmpeg.c160
-rw-r--r--spa/plugins/ffmpeg/ffmpeg.h20
-rw-r--r--spa/plugins/ffmpeg/meson.build9
-rw-r--r--spa/plugins/jack/jack-client.c113
-rw-r--r--spa/plugins/jack/jack-client.h84
-rw-r--r--spa/plugins/jack/jack-device.c457
-rw-r--r--spa/plugins/jack/jack-sink.c924
-rw-r--r--spa/plugins/jack/jack-source.c946
-rw-r--r--spa/plugins/jack/meson.build12
-rw-r--r--spa/plugins/jack/plugin.c54
-rw-r--r--spa/plugins/libcamera/libcamera-client.c247
-rw-r--r--spa/plugins/libcamera/libcamera-device.cpp332
-rw-r--r--spa/plugins/libcamera/libcamera-manager.cpp488
-rw-r--r--spa/plugins/libcamera/libcamera-manager.hpp29
-rw-r--r--spa/plugins/libcamera/libcamera-source.cpp1073
-rw-r--r--spa/plugins/libcamera/libcamera-utils.cpp990
-rw-r--r--spa/plugins/libcamera/libcamera.c57
-rw-r--r--spa/plugins/libcamera/libcamera.h51
-rw-r--r--spa/plugins/libcamera/meson.build17
-rw-r--r--spa/plugins/meson.build58
-rw-r--r--spa/plugins/support/cpu-arm.c137
-rw-r--r--spa/plugins/support/cpu-x86.c204
-rw-r--r--spa/plugins/support/cpu.c313
-rw-r--r--spa/plugins/support/dbus.c592
-rw-r--r--spa/plugins/support/evl-plugin.c47
-rw-r--r--spa/plugins/support/evl-system.c460
-rw-r--r--spa/plugins/support/journal.c341
-rw-r--r--spa/plugins/support/log-patterns.c108
-rw-r--r--spa/plugins/support/log-patterns.h13
-rw-r--r--spa/plugins/support/logger.c415
-rw-r--r--spa/plugins/support/loop.c1024
-rw-r--r--spa/plugins/support/meson.build70
-rw-r--r--spa/plugins/support/node-driver.c492
-rw-r--r--spa/plugins/support/null-audio-sink.c1001
-rw-r--r--spa/plugins/support/plugin.c67
-rw-r--r--spa/plugins/support/system.c384
-rw-r--r--spa/plugins/test/fakesink.c846
-rw-r--r--spa/plugins/test/fakesrc.c876
-rw-r--r--spa/plugins/test/meson.build7
-rw-r--r--spa/plugins/test/plugin.c50
-rw-r--r--spa/plugins/v4l2/meson.build10
-rw-r--r--spa/plugins/v4l2/v4l2-device.c291
-rw-r--r--spa/plugins/v4l2/v4l2-source.c1055
-rw-r--r--spa/plugins/v4l2/v4l2-udev.c754
-rw-r--r--spa/plugins/v4l2/v4l2-utils.c1743
-rw-r--r--spa/plugins/v4l2/v4l2.c59
-rw-r--r--spa/plugins/v4l2/v4l2.h49
-rw-r--r--spa/plugins/videoconvert/meson.build15
-rw-r--r--spa/plugins/videoconvert/plugin.c46
-rw-r--r--spa/plugins/videoconvert/videoadapter.c1671
-rw-r--r--spa/plugins/videotestsrc/draw.c288
-rw-r--r--spa/plugins/videotestsrc/meson.build7
-rw-r--r--spa/plugins/videotestsrc/plugin.c46
-rw-r--r--spa/plugins/videotestsrc/videotestsrc.c999
-rw-r--r--spa/plugins/volume/meson.build7
-rw-r--r--spa/plugins/volume/plugin.c46
-rw-r--r--spa/plugins/volume/volume.c894
-rw-r--r--spa/plugins/vulkan/meson.build12
-rw-r--r--spa/plugins/vulkan/plugin.c50
-rw-r--r--spa/plugins/vulkan/shaders/disk-intersection.comp143
-rw-r--r--spa/plugins/vulkan/shaders/filter-color.comp39
-rw-r--r--spa/plugins/vulkan/shaders/filter.comp44
-rw-r--r--spa/plugins/vulkan/shaders/filter.spvbin0 -> 4884 bytes
-rw-r--r--spa/plugins/vulkan/shaders/main.comp50
-rw-r--r--spa/plugins/vulkan/shaders/main.spvbin0 -> 9980 bytes
-rw-r--r--spa/plugins/vulkan/vulkan-compute-filter.c808
-rw-r--r--spa/plugins/vulkan/vulkan-compute-source.c1016
-rw-r--r--spa/plugins/vulkan/vulkan-utils.c758
-rw-r--r--spa/plugins/vulkan/vulkan-utils.h86
-rw-r--r--spa/tests/benchmark-dict.c145
-rw-r--r--spa/tests/benchmark-pod.c294
-rw-r--r--spa/tests/meson.build58
-rw-r--r--spa/tests/spa-include-test-template.c5
-rw-r--r--spa/tests/stress-ringbuffer.c147
-rw-r--r--spa/tools/meson.build11
-rw-r--r--spa/tools/spa-inspect.c319
-rw-r--r--spa/tools/spa-json-dump.c165
-rw-r--r--spa/tools/spa-monitor.c229
-rw-r--r--src/daemon/client-rt.conf.in114
-rw-r--r--src/daemon/client.conf.in85
-rw-r--r--src/daemon/filter-chain.conf.in62
-rw-r--r--src/daemon/filter-chain/demonic.conf63
-rw-r--r--src/daemon/filter-chain/meson.build19
-rw-r--r--src/daemon/filter-chain/sink-dolby-surround.conf46
-rw-r--r--src/daemon/filter-chain/sink-eq6.conf70
-rw-r--r--src/daemon/filter-chain/sink-make-LFE.conf56
-rw-r--r--src/daemon/filter-chain/sink-matrix-spatialiser.conf41
-rw-r--r--src/daemon/filter-chain/sink-mix-FL-FR.conf40
-rw-r--r--src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf177
-rw-r--r--src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf101
-rw-r--r--src/daemon/filter-chain/source-duplicate-FL.conf52
-rw-r--r--src/daemon/filter-chain/source-rnnoise.conf35
-rw-r--r--src/daemon/jack.conf.in128
-rw-r--r--src/daemon/meson.build141
-rw-r--r--src/daemon/minimal.conf.in352
-rw-r--r--src/daemon/pipewire-avb.conf.in73
-rw-r--r--src/daemon/pipewire-pulse.conf.in160
-rw-r--r--src/daemon/pipewire.c143
-rw-r--r--src/daemon/pipewire.conf.in259
-rw-r--r--src/daemon/pipewire.desktop.in9
-rw-r--r--src/daemon/systemd/meson.build6
-rw-r--r--src/daemon/systemd/system/meson.build15
-rw-r--r--src/daemon/systemd/system/pipewire.service.in35
-rw-r--r--src/daemon/systemd/system/pipewire.socket12
-rw-r--r--src/daemon/systemd/user/filter-chain.service.in21
-rw-r--r--src/daemon/systemd/user/meson.build27
-rw-r--r--src/daemon/systemd/user/pipewire-pulse.service.in36
-rw-r--r--src/daemon/systemd/user/pipewire-pulse.socket11
-rw-r--r--src/daemon/systemd/user/pipewire.service.in32
-rw-r--r--src/daemon/systemd/user/pipewire.socket9
-rw-r--r--src/examples/audio-capture.c209
-rw-r--r--src/examples/audio-dsp-filter.c180
-rw-r--r--src/examples/audio-dsp-src.c165
-rw-r--r--src/examples/audio-src.c186
-rw-r--r--src/examples/bluez-session.c400
-rw-r--r--src/examples/export-sink.c584
-rw-r--r--src/examples/export-source.c566
-rw-r--r--src/examples/export-spa-device.c144
-rw-r--r--src/examples/export-spa.c183
-rw-r--r--src/examples/local-v4l2.c469
-rw-r--r--src/examples/meson.build51
-rw-r--r--src/examples/sdl.h198
-rw-r--r--src/examples/video-dsp-play.c315
-rw-r--r--src/examples/video-play-fixate.c516
-rw-r--r--src/examples/video-play-pull.c588
-rw-r--r--src/examples/video-play-reneg.c438
-rw-r--r--src/examples/video-play.c529
-rw-r--r--src/examples/video-src-alloc.c464
-rw-r--r--src/examples/video-src-fixate.c602
-rw-r--r--src/examples/video-src-reneg.c509
-rw-r--r--src/examples/video-src.c357
-rw-r--r--src/gst/.editorconfig7
-rw-r--r--src/gst/gstpipewire.c69
-rw-r--r--src/gst/gstpipewireclock.c127
-rw-r--r--src/gst/gstpipewireclock.h71
-rw-r--r--src/gst/gstpipewirecore.c202
-rw-r--r--src/gst/gstpipewirecore.h60
-rw-r--r--src/gst/gstpipewiredeviceprovider.c754
-rw-r--r--src/gst/gstpipewiredeviceprovider.h110
-rw-r--r--src/gst/gstpipewireformat.c981
-rw-r--r--src/gst/gstpipewireformat.h42
-rw-r--r--src/gst/gstpipewirepool.c276
-rw-r--r--src/gst/gstpipewirepool.h92
-rw-r--r--src/gst/gstpipewiresink.c924
-rw-r--r--src/gst/gstpipewiresink.h110
-rw-r--r--src/gst/gstpipewiresrc.c1439
-rw-r--r--src/gst/gstpipewiresrc.h110
-rw-r--r--src/gst/meson.build33
-rw-r--r--src/meson.build15
-rw-r--r--src/modules/flatpak-utils.h156
-rw-r--r--src/modules/meson.build609
-rw-r--r--src/modules/module-access.c364
-rw-r--r--src/modules/module-adapter.c394
-rw-r--r--src/modules/module-adapter/adapter.c548
-rw-r--r--src/modules/module-adapter/adapter.h48
-rw-r--r--src/modules/module-avb.c129
-rw-r--r--src/modules/module-avb/aaf.h102
-rw-r--r--src/modules/module-avb/acmp.c476
-rw-r--r--src/modules/module-avb/acmp.h99
-rw-r--r--src/modules/module-avb/adp.c381
-rw-r--r--src/modules/module-avb/adp.h105
-rw-r--r--src/modules/module-avb/aecp-aem-descriptors.h247
-rw-r--r--src/modules/module-avb/aecp-aem.c285
-rw-r--r--src/modules/module-avb/aecp-aem.h345
-rw-r--r--src/modules/module-avb/aecp.c168
-rw-r--r--src/modules/module-avb/aecp.h60
-rw-r--r--src/modules/module-avb/avb.c106
-rw-r--r--src/modules/module-avb/avb.h44
-rw-r--r--src/modules/module-avb/avdecc.c335
-rw-r--r--src/modules/module-avb/descriptors.h274
-rw-r--r--src/modules/module-avb/iec61883.h110
-rw-r--r--src/modules/module-avb/internal.h166
-rw-r--r--src/modules/module-avb/maap.c469
-rw-r--r--src/modules/module-avb/maap.h70
-rw-r--r--src/modules/module-avb/mmrp.c233
-rw-r--r--src/modules/module-avb/mmrp.h68
-rw-r--r--src/modules/module-avb/mrp.c612
-rw-r--r--src/modules/module-avb/mrp.h181
-rw-r--r--src/modules/module-avb/msrp.c459
-rw-r--r--src/modules/module-avb/msrp.h134
-rw-r--r--src/modules/module-avb/mvrp.c297
-rw-r--r--src/modules/module-avb/mvrp.h62
-rw-r--r--src/modules/module-avb/packets.h101
-rw-r--r--src/modules/module-avb/srp.c59
-rw-r--r--src/modules/module-avb/srp.h32
-rw-r--r--src/modules/module-avb/stream.c589
-rw-r--r--src/modules/module-avb/stream.h104
-rw-r--r--src/modules/module-avb/utils.h86
-rw-r--r--src/modules/module-client-device.c232
-rw-r--r--src/modules/module-client-device/client-device.h44
-rw-r--r--src/modules/module-client-device/protocol-native.c556
-rw-r--r--src/modules/module-client-device/proxy-device.c90
-rw-r--r--src/modules/module-client-device/resource-device.c155
-rw-r--r--src/modules/module-client-node.c229
-rw-r--r--src/modules/module-client-node/client-node.c1777
-rw-r--r--src/modules/module-client-node/client-node.h60
-rw-r--r--src/modules/module-client-node/protocol-native.c1259
-rw-r--r--src/modules/module-client-node/remote-node.c1339
-rw-r--r--src/modules/module-client-node/v0/client-node.c1447
-rw-r--r--src/modules/module-client-node/v0/client-node.h101
-rw-r--r--src/modules/module-client-node/v0/ext-client-node.h414
-rw-r--r--src/modules/module-client-node/v0/protocol-native.c534
-rw-r--r--src/modules/module-client-node/v0/transport.c262
-rw-r--r--src/modules/module-client-node/v0/transport.h59
-rw-r--r--src/modules/module-combine-stream.c1063
-rw-r--r--src/modules/module-echo-cancel.c1350
-rw-r--r--src/modules/module-example-sink.c498
-rw-r--r--src/modules/module-example-source.c504
-rw-r--r--src/modules/module-fallback-sink.c473
-rw-r--r--src/modules/module-filter-chain.c2411
-rw-r--r--src/modules/module-filter-chain/biquad.c364
-rw-r--r--src/modules/module-filter-chain/biquad.h57
-rw-r--r--src/modules/module-filter-chain/builtin_plugin.c1035
-rw-r--r--src/modules/module-filter-chain/convolver.c425
-rw-r--r--src/modules/module-filter-chain/convolver.h34
-rw-r--r--src/modules/module-filter-chain/dsp-ops-avx.c85
-rw-r--r--src/modules/module-filter-chain/dsp-ops-c.c174
-rw-r--r--src/modules/module-filter-chain/dsp-ops-sse.c142
-rw-r--r--src/modules/module-filter-chain/dsp-ops.c114
-rw-r--r--src/modules/module-filter-chain/dsp-ops.h140
-rw-r--r--src/modules/module-filter-chain/ladspa.h603
-rw-r--r--src/modules/module-filter-chain/ladspa_plugin.c275
-rw-r--r--src/modules/module-filter-chain/lv2_plugin.c522
-rw-r--r--src/modules/module-filter-chain/pffft.c2381
-rw-r--r--src/modules/module-filter-chain/pffft.h181
-rw-r--r--src/modules/module-filter-chain/plugin.h112
-rw-r--r--src/modules/module-link-factory.c565
-rw-r--r--src/modules/module-loopback.c737
-rw-r--r--src/modules/module-metadata.c241
-rw-r--r--src/modules/module-metadata/metadata.c316
-rw-r--r--src/modules/module-metadata/protocol-native.c354
-rw-r--r--src/modules/module-metadata/proxy-metadata.c92
-rw-r--r--src/modules/module-pipe-tunnel.c730
-rw-r--r--src/modules/module-portal.c348
-rw-r--r--src/modules/module-profiler.c463
-rw-r--r--src/modules/module-profiler/protocol-native.c128
-rw-r--r--src/modules/module-protocol-native.c1528
-rw-r--r--src/modules/module-protocol-native/connection.c866
-rw-r--r--src/modules/module-protocol-native/connection.h112
-rw-r--r--src/modules/module-protocol-native/defs.h49
-rw-r--r--src/modules/module-protocol-native/local-socket.c169
-rw-r--r--src/modules/module-protocol-native/portal-screencast.c41
-rw-r--r--src/modules/module-protocol-native/protocol-footer.c152
-rw-r--r--src/modules/module-protocol-native/protocol-footer.h59
-rw-r--r--src/modules/module-protocol-native/protocol-native.c2236
-rw-r--r--src/modules/module-protocol-native/test-connection.c225
-rw-r--r--src/modules/module-protocol-native/v0/interfaces.h534
-rw-r--r--src/modules/module-protocol-native/v0/protocol-native.c1371
-rw-r--r--src/modules/module-protocol-native/v0/typemap.h282
-rw-r--r--src/modules/module-protocol-pulse.c387
-rw-r--r--src/modules/module-protocol-pulse/client.c390
-rw-r--r--src/modules/module-protocol-pulse/client.h136
-rw-r--r--src/modules/module-protocol-pulse/cmd.c136
-rw-r--r--src/modules/module-protocol-pulse/cmd.h32
-rw-r--r--src/modules/module-protocol-pulse/collect.c549
-rw-r--r--src/modules/module-protocol-pulse/collect.h166
-rw-r--r--src/modules/module-protocol-pulse/commands.h208
-rw-r--r--src/modules/module-protocol-pulse/dbus-name.c87
-rw-r--r--src/modules/module-protocol-pulse/dbus-name.h33
-rw-r--r--src/modules/module-protocol-pulse/defs.h265
-rw-r--r--src/modules/module-protocol-pulse/extension.c45
-rw-r--r--src/modules/module-protocol-pulse/extension.h47
-rw-r--r--src/modules/module-protocol-pulse/extensions/ext-device-manager.c8
-rw-r--r--src/modules/module-protocol-pulse/extensions/ext-device-restore.c340
-rw-r--r--src/modules/module-protocol-pulse/extensions/ext-stream-restore.c330
-rw-r--r--src/modules/module-protocol-pulse/extensions/registry.h13
-rw-r--r--src/modules/module-protocol-pulse/format.c861
-rw-r--r--src/modules/module-protocol-pulse/format.h233
-rw-r--r--src/modules/module-protocol-pulse/internal.h108
-rw-r--r--src/modules/module-protocol-pulse/log.h34
-rw-r--r--src/modules/module-protocol-pulse/manager.c1028
-rw-r--r--src/modules/module-protocol-pulse/manager.h145
-rw-r--r--src/modules/module-protocol-pulse/message-handler.c143
-rw-r--r--src/modules/module-protocol-pulse/message-handler.h8
-rw-r--r--src/modules/module-protocol-pulse/message.c879
-rw-r--r--src/modules/module-protocol-pulse/message.h75
-rw-r--r--src/modules/module-protocol-pulse/module.c333
-rw-r--r--src/modules/module-protocol-pulse/module.h94
-rw-r--r--src/modules/module-protocol-pulse/modules/module-always-sink.c122
-rw-r--r--src/modules/module-protocol-pulse/modules/module-combine-sink.c341
-rw-r--r--src/modules/module-protocol-pulse/modules/module-echo-cancel.c276
-rw-r--r--src/modules/module-protocol-pulse/modules/module-gsettings.c298
-rw-r--r--src/modules/module-protocol-pulse/modules/module-ladspa-sink.c257
-rw-r--r--src/modules/module-protocol-pulse/modules/module-ladspa-source.c265
-rw-r--r--src/modules/module-protocol-pulse/modules/module-loopback.c245
-rw-r--r--src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c127
-rw-r--r--src/modules/module-protocol-pulse/modules/module-null-sink.c228
-rw-r--r--src/modules/module-protocol-pulse/modules/module-pipe-sink.c210
-rw-r--r--src/modules/module-protocol-pulse/modules/module-pipe-source.c210
-rw-r--r--src/modules/module-protocol-pulse/modules/module-raop-discover.c112
-rw-r--r--src/modules/module-protocol-pulse/modules/module-remap-sink.c253
-rw-r--r--src/modules/module-protocol-pulse/modules/module-remap-source.c260
-rw-r--r--src/modules/module-protocol-pulse/modules/module-roc-sink-input.c201
-rw-r--r--src/modules/module-protocol-pulse/modules/module-roc-sink.c197
-rw-r--r--src/modules/module-protocol-pulse/modules/module-roc-source.c206
-rw-r--r--src/modules/module-protocol-pulse/modules/module-rtp-recv.c164
-rw-r--r--src/modules/module-protocol-pulse/modules/module-rtp-send.c224
-rw-r--r--src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c210
-rw-r--r--src/modules/module-protocol-pulse/modules/module-switch-on-connect.c291
-rw-r--r--src/modules/module-protocol-pulse/modules/module-tunnel-sink.c230
-rw-r--r--src/modules/module-protocol-pulse/modules/module-tunnel-source.c220
-rw-r--r--src/modules/module-protocol-pulse/modules/module-x11-bell.c130
-rw-r--r--src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c133
-rw-r--r--src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c746
-rw-r--r--src/modules/module-protocol-pulse/operation.c92
-rw-r--r--src/modules/module-protocol-pulse/operation.h50
-rw-r--r--src/modules/module-protocol-pulse/pending-sample.c50
-rw-r--r--src/modules/module-protocol-pulse/pending-sample.h48
-rw-r--r--src/modules/module-protocol-pulse/pulse-server.c5689
-rw-r--r--src/modules/module-protocol-pulse/pulse-server.h56
-rw-r--r--src/modules/module-protocol-pulse/quirks.c75
-rw-r--r--src/modules/module-protocol-pulse/quirks.h36
-rw-r--r--src/modules/module-protocol-pulse/remap.c58
-rw-r--r--src/modules/module-protocol-pulse/remap.h52
-rw-r--r--src/modules/module-protocol-pulse/reply.c84
-rw-r--r--src/modules/module-protocol-pulse/reply.h42
-rw-r--r--src/modules/module-protocol-pulse/sample-play.c211
-rw-r--r--src/modules/module-protocol-pulse/sample-play.h76
-rw-r--r--src/modules/module-protocol-pulse/sample.c50
-rw-r--r--src/modules/module-protocol-pulse/sample.h61
-rw-r--r--src/modules/module-protocol-pulse/server.c1087
-rw-r--r--src/modules/module-protocol-pulse/server.h60
-rw-r--r--src/modules/module-protocol-pulse/stream.c428
-rw-r--r--src/modules/module-protocol-pulse/stream.h141
-rw-r--r--src/modules/module-protocol-pulse/utils.c207
-rw-r--r--src/modules/module-protocol-pulse/utils.h40
-rw-r--r--src/modules/module-protocol-pulse/volume.c114
-rw-r--r--src/modules/module-protocol-pulse/volume.h85
-rw-r--r--src/modules/module-protocol-simple.c911
-rw-r--r--src/modules/module-pulse-tunnel.c1080
-rw-r--r--src/modules/module-raop-discover.c547
-rw-r--r--src/modules/module-raop-sink.c1848
-rw-r--r--src/modules/module-raop/rtsp-client.c631
-rw-r--r--src/modules/module-raop/rtsp-client.h92
-rw-r--r--src/modules/module-roc-sink.c514
-rw-r--r--src/modules/module-roc-source.c546
-rw-r--r--src/modules/module-roc/common.h71
-rw-r--r--src/modules/module-rt.c1095
-rw-r--r--src/modules/module-rtp-sink.c975
-rw-r--r--src/modules/module-rtp-source.c1200
-rw-r--r--src/modules/module-rtp/rtp.h78
-rw-r--r--src/modules/module-rtp/sap.h58
-rw-r--r--src/modules/module-session-manager.c74
-rw-r--r--src/modules/module-session-manager/client-endpoint/client-endpoint.c296
-rw-r--r--src/modules/module-session-manager/client-endpoint/client-endpoint.h62
-rw-r--r--src/modules/module-session-manager/client-endpoint/endpoint-stream.c352
-rw-r--r--src/modules/module-session-manager/client-endpoint/endpoint-stream.h64
-rw-r--r--src/modules/module-session-manager/client-endpoint/endpoint.c385
-rw-r--r--src/modules/module-session-manager/client-endpoint/endpoint.h61
-rw-r--r--src/modules/module-session-manager/client-session/client-session.c295
-rw-r--r--src/modules/module-session-manager/client-session/client-session.h62
-rw-r--r--src/modules/module-session-manager/client-session/endpoint-link.c369
-rw-r--r--src/modules/module-session-manager/client-session/endpoint-link.h65
-rw-r--r--src/modules/module-session-manager/client-session/session.c344
-rw-r--r--src/modules/module-session-manager/client-session/session.h62
-rw-r--r--src/modules/module-session-manager/endpoint-link.c590
-rw-r--r--src/modules/module-session-manager/endpoint-stream.c581
-rw-r--r--src/modules/module-session-manager/endpoint.c590
-rw-r--r--src/modules/module-session-manager/protocol-native.c3083
-rw-r--r--src/modules/module-session-manager/proxy-session-manager.c188
-rw-r--r--src/modules/module-session-manager/session.c578
-rw-r--r--src/modules/module-x11-bell.c364
-rw-r--r--src/modules/module-zeroconf-discover.c565
-rw-r--r--src/modules/module-zeroconf-discover/avahi-poll.c201
-rw-r--r--src/modules/module-zeroconf-discover/avahi-poll.h31
-rw-r--r--src/modules/spa/meson.build31
-rw-r--r--src/modules/spa/module-device-factory.c281
-rw-r--r--src/modules/spa/module-device.c127
-rw-r--r--src/modules/spa/module-node-factory.c280
-rw-r--r--src/modules/spa/module-node.c129
-rw-r--r--src/modules/spa/spa-device.c161
-rw-r--r--src/modules/spa/spa-device.h62
-rw-r--r--src/modules/spa/spa-node.c292
-rw-r--r--src/modules/spa/spa-node.h63
-rw-r--r--src/pipewire/array.h176
-rw-r--r--src/pipewire/buffers.c368
-rw-r--r--src/pipewire/buffers.h75
-rw-r--r--src/pipewire/client.h186
-rw-r--r--src/pipewire/conf.c1186
-rw-r--r--src/pipewire/conf.h49
-rw-r--r--src/pipewire/context.c1461
-rw-r--r--src/pipewire/context.h195
-rw-r--r--src/pipewire/control.c267
-rw-r--r--src/pipewire/control.h82
-rw-r--r--src/pipewire/core.c495
-rw-r--r--src/pipewire/core.h629
-rw-r--r--src/pipewire/data-loop.c292
-rw-r--r--src/pipewire/data-loop.h113
-rw-r--r--src/pipewire/device.h178
-rw-r--r--src/pipewire/extensions/client-node.h357
-rw-r--r--src/pipewire/extensions/meson.build21
-rw-r--r--src/pipewire/extensions/metadata.h112
-rw-r--r--src/pipewire/extensions/profiler.h95
-rw-r--r--src/pipewire/extensions/protocol-native.h105
-rw-r--r--src/pipewire/extensions/session-manager.h47
-rw-r--r--src/pipewire/extensions/session-manager/impl-interfaces.h298
-rw-r--r--src/pipewire/extensions/session-manager/interfaces.h479
-rw-r--r--src/pipewire/extensions/session-manager/introspect-funcs.h323
-rw-r--r--src/pipewire/extensions/session-manager/introspect.h130
-rw-r--r--src/pipewire/extensions/session-manager/keys.h67
-rw-r--r--src/pipewire/factory.h123
-rw-r--r--src/pipewire/filter.c1962
-rw-r--r--src/pipewire/filter.h250
-rw-r--r--src/pipewire/global.c430
-rw-r--r--src/pipewire/global.h165
-rw-r--r--src/pipewire/i18n.h56
-rw-r--r--src/pipewire/impl-client.c780
-rw-r--r--src/pipewire/impl-client.h185
-rw-r--r--src/pipewire/impl-core.c673
-rw-r--r--src/pipewire/impl-core.h104
-rw-r--r--src/pipewire/impl-device.c935
-rw-r--r--src/pipewire/impl-device.h116
-rw-r--r--src/pipewire/impl-factory.c301
-rw-r--r--src/pipewire/impl-factory.h131
-rw-r--r--src/pipewire/impl-link.c1508
-rw-r--r--src/pipewire/impl-link.h126
-rw-r--r--src/pipewire/impl-metadata.c627
-rw-r--r--src/pipewire/impl-metadata.h114
-rw-r--r--src/pipewire/impl-module.c427
-rw-r--r--src/pipewire/impl-module.h120
-rw-r--r--src/pipewire/impl-node.c2316
-rw-r--r--src/pipewire/impl-node.h190
-rw-r--r--src/pipewire/impl-port.c1614
-rw-r--r--src/pipewire/impl-port.h144
-rw-r--r--src/pipewire/impl.h62
-rw-r--r--src/pipewire/introspect.c590
-rw-r--r--src/pipewire/keys.h356
-rw-r--r--src/pipewire/link.h148
-rw-r--r--src/pipewire/log.c291
-rw-r--r--src/pipewire/log.h182
-rw-r--r--src/pipewire/loop.c162
-rw-r--r--src/pipewire/loop.h90
-rw-r--r--src/pipewire/main-loop.c157
-rw-r--r--src/pipewire/main-loop.h86
-rw-r--r--src/pipewire/map.h252
-rw-r--r--src/pipewire/mem.c808
-rw-r--r--src/pipewire/mem.h212
-rw-r--r--src/pipewire/meson.build138
-rw-r--r--src/pipewire/module.h121
-rw-r--r--src/pipewire/node.h216
-rw-r--r--src/pipewire/permission.h86
-rw-r--r--src/pipewire/pipewire.c871
-rw-r--r--src/pipewire/pipewire.h123
-rw-r--r--src/pipewire/port.h179
-rw-r--r--src/pipewire/private.h1310
-rw-r--r--src/pipewire/properties.c733
-rw-r--r--src/pipewire/properties.h199
-rw-r--r--src/pipewire/protocol.c188
-rw-r--r--src/pipewire/protocol.h162
-rw-r--r--src/pipewire/proxy.c374
-rw-r--r--src/pipewire/proxy.h219
-rw-r--r--src/pipewire/resource.c351
-rw-r--r--src/pipewire/resource.h174
-rw-r--r--src/pipewire/settings.c337
-rw-r--r--src/pipewire/stream.c2414
-rw-r--r--src/pipewire/stream.h529
-rw-r--r--src/pipewire/thread-loop.c469
-rw-r--r--src/pipewire/thread-loop.h175
-rw-r--r--src/pipewire/thread.c160
-rw-r--r--src/pipewire/thread.h65
-rw-r--r--src/pipewire/type.h65
-rw-r--r--src/pipewire/utils.c214
-rw-r--r--src/pipewire/utils.h111
-rw-r--r--src/pipewire/version.h.in68
-rw-r--r--src/pipewire/work-queue.c269
-rw-r--r--src/pipewire/work-queue.h71
-rw-r--r--src/tests/meson.build52
-rw-r--r--src/tests/test-cpp.cpp35
-rw-r--r--src/tests/test-endpoint.c469
-rw-r--r--src/tests/test-filter.c375
-rw-r--r--src/tests/test-interfaces.c382
-rw-r--r--src/tests/test-stream.c257
-rw-r--r--src/tools/dsffile.c266
-rw-r--r--src/tools/dsffile.h52
-rw-r--r--src/tools/meson.build88
-rw-r--r--src/tools/midifile.c740
-rw-r--r--src/tools/midifile.h64
-rw-r--r--src/tools/pw-cat.c1971
-rw-r--r--src/tools/pw-cli.c2375
-rw-r--r--src/tools/pw-dot.c1169
-rw-r--r--src/tools/pw-dump.c1664
-rw-r--r--src/tools/pw-link.c912
-rw-r--r--src/tools/pw-loopback.c284
-rw-r--r--src/tools/pw-metadata.c297
-rw-r--r--src/tools/pw-mididump.c233
-rw-r--r--src/tools/pw-mon.c875
-rw-r--r--src/tools/pw-profiler.c665
-rw-r--r--src/tools/pw-reserve.c253
-rw-r--r--src/tools/pw-top.c862
-rw-r--r--src/tools/reserve.c527
-rw-r--r--src/tools/reserve.h82
-rw-r--r--subprojects/media-session.wrap4
-rw-r--r--subprojects/wireplumber.wrap3
-rw-r--r--template.test.in3
-rw-r--r--test/meson.build156
-rw-r--r--test/pwtest-compat.c55
-rw-r--r--test/pwtest-implementation.h137
-rw-r--r--test/pwtest.c1486
-rw-r--r--test/pwtest.h577
-rw-r--r--test/test-array.c146
-rw-r--r--test/test-client.c70
-rw-r--r--test/test-config.c102
-rw-r--r--test/test-context.c277
-rw-r--r--test/test-example.c265
-rw-r--r--test/test-functional.c58
-rw-r--r--test/test-lib.c69
-rw-r--r--test/test-logger.c656
-rw-r--r--test/test-loop.c463
-rw-r--r--test/test-map.c242
-rw-r--r--test/test-properties.c629
-rw-r--r--test/test-pwtest.c55
-rw-r--r--test/test-spa-buffer.c150
-rw-r--r--test/test-spa-json.c333
-rw-r--r--test/test-spa-log.c213
-rw-r--r--test/test-spa-node.c251
-rw-r--r--test/test-spa-pod.c1706
-rw-r--r--test/test-spa-utils.c1043
-rw-r--r--test/test-support.c88
-rw-r--r--test/test-utils.c253
1109 files changed, 388937 insertions, 0 deletions
diff --git a/.cirrus.yml b/.cirrus.yml
new file mode 100644
index 0000000..d26c443
--- /dev/null
+++ b/.cirrus.yml
@@ -0,0 +1,24 @@
+task:
+ freebsd_instance:
+ matrix:
+ - image_family: freebsd-13-1-snap
+ env:
+ # /usr/ports/Mk/Uses/localbase.mk localbase:ldflags
+ LOCALBASE: /usr/local
+ CFLAGS: -isystem $LOCALBASE/include
+ CPPFLAGS: $CFLAGS
+ CXXFLAGS: $CFLAGS
+ LDFLAGS: -L$LOCALBASE/lib
+ deps_script:
+ - sed -i.bak -e 's/quarterly/latest/' /etc/pkg/FreeBSD.conf
+ - pkg install -y meson pkgconf git-lite dbus glib libepoll-shim libudev-devd vulkan-loader vulkan-headers v4l_compat gstreamer1 gstreamer1-plugins libinotify gettext libsndfile sdl2 alsa-lib
+ - sysrc dbus_enable=YES
+ - service dbus restart
+ build_script:
+ - mkdir build
+ - cd build
+ - meson setup -Dalsa=enabled -Draop=enabled -Dv4l2=enabled -Dpipewire-alsa=enabled -Dbluez5=disabled -Djack=disabled -Dpipewire-jack=enabled -Dpw-cat=enabled -Dpipewire-v4l2=disabled -Dsdl2=enabled -Dsystemd=disabled -Dsession-managers=media-session ..
+ - ninja
+ test_script:
+ - cd build
+ - ninja test
diff --git a/.codespell-ignore b/.codespell-ignore
new file mode 100644
index 0000000..6db5e19
--- /dev/null
+++ b/.codespell-ignore
@@ -0,0 +1,20 @@
+ba
+capela
+cas
+crasher
+datas
+endcode
+files'
+goin
+hda
+hist
+hve
+inport
+nd
+mmaped
+od
+ot
+parm
+sinc
+stdio
+uint \ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e3e33c6
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,27 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = tab
+indent_size = 8
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+# Use 2 spaces for meson files
+[*.build]
+indent_style = space
+indent_size = 2
+
+[*.yml]
+indent_style = space
+indent_size = 2
+
+[*.{conf,conf.in}]
+indent_style = space
+indent_size = 4
+
+[*.{xml,xml.in}]
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..57213aa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+.*
+.tarball-version
+.version
+.*.swp
+ABOUT-NLS
+*~
+*.tar.gz
+*.tar.xz
+*.o
+cscope.out
+cscope.in.out
+cscope.po.out
+Makefile
+subprojects/lua*
+subprojects/wireplumber
+subprojects/media-session
+subprojects/packagecache
+
+# Created by https://www.gitignore.io/api/vim
+
+### Vim ###
+# Swap
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+
+# Session
+Session.vim
+
+# Temporary
+.netrwhist
+*~
+# Auto-generated tag files
+tags
+# Persistent undo
+[._]*.un~
+
+
+# End of https://www.gitignore.io/api/vim
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..2ab6a08
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,477 @@
+stages:
+ - container
+ - container_coverity
+ - build
+ - analysis
+ - pages
+
+variables:
+ FDO_UPSTREAM_REPO: 'pipewire/pipewire'
+
+# ci-templates as of Jan 27th 2022
+.templates_sha: &templates_sha 0c312d9c7255f46e741d43bcd1930f09cd12efe7
+
+include:
+ - project: 'freedesktop/ci-templates'
+ ref: *templates_sha
+ file: '/templates/fedora.yml'
+ - project: 'freedesktop/ci-templates'
+ ref: *templates_sha
+ file: '/templates/ubuntu.yml'
+ - project: 'freedesktop/ci-templates'
+ ref: *templates_sha
+ file: '/templates/alpine.yml'
+
+.fedora:
+ variables:
+ # Update this tag when you want to trigger a rebuild
+ FDO_DISTRIBUTION_TAG: '2022-11-07.0'
+ FDO_DISTRIBUTION_VERSION: '35'
+ FDO_DISTRIBUTION_PACKAGES: >-
+ alsa-lib-devel
+ avahi-devel
+ bluez-libs-devel
+ clang
+ dbus-devel
+ doxygen
+ fdk-aac-free-devel
+ findutils
+ gcc
+ gcc-c++
+ git
+ glib-devel
+ graphviz
+ gstreamer1-devel
+ gstreamer1-plugins-base-devel
+ jack-audio-connection-kit-devel
+ libcanberra-devel
+ libldac-devel
+ libsndfile-devel
+ libusb-devel
+ lilv-devel
+ libv4l-devel
+ libva-devel
+ libX11-devel
+ ModemManager-devel
+ openssl-devel
+ pulseaudio-libs-devel
+ python3-docutils
+ sbc-devel
+ ShellCheck
+ SDL2-devel
+ systemd-devel
+ vulkan-loader-devel
+ webrtc-audio-processing-devel
+ which
+ valgrind
+ ninja-build
+ pkgconf
+ python3-pip
+ pulseaudio-utils
+ openal-soft
+ readline-devel
+ FDO_DISTRIBUTION_EXEC: >-
+ pip3 install meson
+
+.ubuntu:
+ variables:
+ # Update this tag when you want to trigger a rebuild
+ FDO_DISTRIBUTION_TAG: '2022-01-27.0'
+ FDO_DISTRIBUTION_VERSION: '20.04'
+ FDO_DISTRIBUTION_PACKAGES: >-
+ debhelper-compat
+ findutils
+ git
+ libasound2-dev
+ libavcodec-dev
+ libavfilter-dev
+ libavformat-dev
+ libdbus-1-dev
+ libglib2.0-dev
+ libgstreamer1.0-dev
+ libgstreamer-plugins-base1.0-dev
+ libsbc-dev
+ libsdl2-dev
+ libudev-dev
+ libva-dev
+ libv4l-dev
+ libx11-dev
+ ninja-build
+ pkg-config
+ python3-docutils
+ systemd
+ python3-pip
+ FDO_DISTRIBUTION_EXEC: >-
+ pip3 install meson
+
+.alpine:
+ variables:
+ # Update this tag when you want to trigger a rebuild
+ FDO_DISTRIBUTION_TAG: '2022-09-07.0'
+ FDO_DISTRIBUTION_VERSION: '3.15'
+ FDO_DISTRIBUTION_PACKAGES: >-
+ alsa-lib-dev
+ avahi-dev
+ bash
+ bluez-dev
+ gcc
+ g++
+ dbus-dev
+ doxygen
+ eudev-dev
+ fdk-aac-dev
+ git
+ glib-dev
+ graphviz
+ gst-plugins-base-dev
+ gstreamer-dev
+ jack-dev
+ libfreeaptx-dev
+ libusb-dev
+ libx11-dev
+ meson
+ modemmanager-dev
+ ncurses-dev
+ pulseaudio-dev
+ readline-dev
+ sbc-dev
+ vulkan-loader-dev
+ xmltoman
+
+.coverity:
+ variables:
+ FDO_REPO_SUFFIX: 'coverity'
+ FDO_BASE_IMAGE: registry.freedesktop.org/$FDO_UPSTREAM_REPO/fedora/$FDO_DISTRIBUTION_VERSION:$FDO_DISTRIBUTION_TAG
+ FDO_DISTRIBUTION_PACKAGES: >-
+ curl
+ FDO_DISTRIBUTION_EXEC: >-
+ mkdir -p /opt ;
+ cd /opt ;
+ curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/cxx/linux64
+ --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN ;
+ tar xf /tmp/cov-analysis-linux64.tgz ;
+ mv cov-analysis-linux64-* coverity ;
+ rm /tmp/cov-analysis-linux64.tgz
+ only:
+ variables:
+ - $COVERITY
+
+.not_coverity:
+ except:
+ variables:
+ - $COVERITY
+
+.build:
+ before_script:
+ # setup the environment
+ - export BUILD_ID="$CI_JOB_ID"
+ - export PREFIX="$PWD/prefix-$BUILD_ID"
+ - export BUILD_DIR="$PWD/build-$BUILD_ID"
+ - export XDG_RUNTIME_DIR="$(mktemp -p $PWD -d xdg-runtime-XXXXXX)"
+ - |
+ if [ -n "$FDO_CI_CONCURRENT" ]; then
+ NINJA_ARGS="-j$FDO_CI_CONCURRENT $NINJA_ARGS"
+ export NINJA_ARGS
+ fi
+ script:
+ - echo "Building with meson options $MESON_OPTIONS"
+ - meson "$BUILD_DIR" . --prefix="$PREFIX" $MESON_OPTIONS
+ - ninja $NINJA_ARGS -C "$BUILD_DIR"
+ - ninja $NINJA_ARGS -C "$BUILD_DIR" test
+ - ninja $NINJA_ARGS -C "$BUILD_DIR" install
+ artifacts:
+ name: pipewire-$CI_COMMIT_SHA
+ when: always
+ paths:
+ - build-*/meson-logs
+
+container_ubuntu:
+ extends:
+ - .ubuntu
+ - .fdo.container-build@ubuntu
+ stage: container
+ variables:
+ GIT_STRATEGY: none # no need to pull the whole tree for rebuilding the image
+
+container_fedora:
+ extends:
+ - .fedora
+ - .fdo.container-build@fedora
+ stage: container
+ variables:
+ GIT_STRATEGY: none # no need to pull the whole tree for rebuilding the image
+
+container_alpine:
+ extends:
+ - .alpine
+ - .fdo.container-build@alpine
+ stage: container
+ variables:
+ GIT_STRATEGY: none # no need to pull the whole tree for rebuilding the image
+
+container_coverity:
+ extends:
+ - .fedora
+ - .coverity
+ - .fdo.container-build@fedora
+ stage: container_coverity
+ variables:
+ GIT_STRATEGY: none
+
+build_on_ubuntu:
+ extends:
+ - .ubuntu
+ - .not_coverity
+ - .fdo.distribution-image@ubuntu
+ - .build
+ stage: build
+ variables:
+ MESON_OPTIONS: "-Dsession-managers=[]"
+
+.build_on_fedora:
+ extends:
+ - .fedora
+ - .not_coverity
+ - .fdo.distribution-image@fedora
+ - .build
+ stage: build
+
+build_on_fedora:
+ extends:
+ - .build_on_fedora
+ variables:
+ MESON_OPTIONS: >-
+ -Ddocs=enabled
+ -Dinstalled_tests=enabled
+ -Dsystemd-system-service=enabled
+ -Dbluez5-backend-hsphfpd=enabled
+ -Daudiotestsrc=enabled
+ -Dtest=enabled
+ -Dvideotestsrc=enabled
+ -Dvolume=enabled
+ -Dvulkan=enabled
+ -Dsdl2=enabled
+ -Dsndfile=enabled
+ -Dsession-managers=[]
+ artifacts:
+ name: pipewire-$CI_COMMIT_SHA
+ when: always
+ paths:
+ - build-*/meson-logs
+ - prefix-*
+
+build_on_alpine:
+ extends:
+ - .alpine
+ - .not_coverity
+ - .fdo.distribution-image@alpine
+ - .build
+ stage: build
+ variables:
+ MESON_OPTIONS: "-Dsession-managers=[]"
+
+# build with all auto() options enabled
+build_all:
+ extends:
+ - .build_on_fedora
+ variables:
+ # Fedora doesn't have libfreeaptx, lc3plus, lc3, or roc
+ # libcamera has no stable API, so let's not chase that target
+ MESON_OPTIONS: >-
+ -Dauto_features=enabled
+ -Dbluez5-codec-aptx=disabled
+ -Dbluez5-codec-lc3plus=disabled
+ -Dbluez5-codec-lc3=disabled
+ -Droc=disabled
+ -Dlibcamera=disabled
+ -Dsession-managers=[]
+ parallel:
+ matrix:
+ - CC: [gcc, clang]
+
+# build with all options on auto() or their default values
+build_with_no_commandline_options:
+ extends:
+ - .build_on_fedora
+ variables:
+ MESON_OPTIONS: "-Dsession-managers=[]"
+ parallel:
+ matrix:
+ - CC: [gcc, clang]
+
+# build with a set of options enabled or disabled
+build_with_custom_options:
+ extends:
+ - .build_on_fedora
+ parallel:
+ matrix:
+ - MESON_OPTION: [docs, installed_tests, systemd-system-service, bluez5-backend-hsphfpd,
+ audiotestsrc, test, videotestsrc, volume, vulkan, sdl2, sndfile]
+ MESON_OPTION_VALUE: [enabled, disabled]
+ script:
+ - echo "Building with -D$MESON_OPTION=$MESON_OPTION_VALUE"
+ - meson "$BUILD_DIR" . --prefix="$PREFIX" "-D$MESON_OPTION=$MESON_OPTION_VALUE" -Dsession-managers=[]
+ - ninja $NINJA_ARGS -C "$BUILD_DIR"
+ - ninja $NINJA_ARGS -C "$BUILD_DIR" test
+
+# A release build with NDEBUG, all options on auto() but tests explicitly
+# enabled. This should show issues with tests failing due to different
+# optimization or relying on assert.
+build_release:
+ extends:
+ - .build_on_fedora
+ variables:
+ MESON_OPTIONS: "-Dtest=enabled -Dbuildtype=release -Db_ndebug=true -Dsession-managers=[]"
+ parallel:
+ matrix:
+ - CC: [gcc, clang]
+
+build_session_managers:
+ extends:
+ - .build_on_fedora
+ script:
+ - echo "Building with meson options $MESON_OPTIONS"
+ - meson "$BUILD_DIR" . --prefix="$PREFIX" $MESON_OPTIONS
+ - ninja $NINJA_ARGS -C "$BUILD_DIR"
+ - ninja $NINJA_ARGS -C "$BUILD_DIR" install
+ variables:
+ MESON_OPTIONS: "-Dsession-managers=$SESSION_MANAGERS"
+ parallel:
+ matrix:
+ - SESSION_MANAGERS: ["[]", "wireplumber", "media-session", "media-session,wireplumber", "wireplumber,media-session" ]
+ allow_failure: true
+
+build_meson_prerelease:
+ extends:
+ - .build_on_fedora
+ script:
+ - pip3 install --upgrade --pre meson
+ - echo "Building with meson options $MESON_OPTIONS"
+ - meson "$BUILD_DIR" . --prefix="$PREFIX" $MESON_OPTIONS
+ - ninja $NINJA_ARGS -C "$BUILD_DIR"
+ - ninja $NINJA_ARGS -C "$BUILD_DIR" install
+ variables:
+ MESON_OPTIONS: "-Dsession-managers=wireplumber,media-session"
+ allow_failure: true
+
+build_meson_exact_release:
+ extends:
+ - .build_on_fedora
+ script:
+ - meson_version=$(head -n 5 meson.build | grep 'meson_version' | sed -e 's/.*\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/')
+ - echo "Requiring meson version $meson_version"
+ - test -n "$meson_version" || (echo "Meson version parser failed" && exit 1)
+ - pip3 uninstall --yes meson
+ - pip3 install "meson==$meson_version"
+ - echo "Building with meson options $MESON_OPTIONS"
+ - meson "$BUILD_DIR" . --prefix="$PREFIX" $MESON_OPTIONS
+ - ninja $NINJA_ARGS -C "$BUILD_DIR"
+ - ninja $NINJA_ARGS -C "$BUILD_DIR" install
+ variables:
+ MESON_OPTIONS: "-Dsession-managers=[]"
+
+valgrind:
+ extends:
+ - .build_on_fedora
+ script:
+ - echo "Building with meson options $MESON_OPTIONS"
+ - meson "$BUILD_DIR" . --prefix="$PREFIX" $MESON_OPTIONS
+ - meson test -C "$BUILD_DIR" --setup=valgrind
+ variables:
+ MESON_OPTIONS: "-Dsession-managers=[]"
+
+build_with_coverity:
+ extends:
+ - .fedora
+ - .coverity
+ - .fdo.suffixed-image@fedora
+ - .build
+ stage: analysis
+ script:
+ - export PATH=/opt/coverity/bin:$PATH
+ - meson "$BUILD_DIR" . --prefix="$PREFIX"
+ -Ddocs=disabled
+ -Dbluez5-backend-hsphfpd=enabled
+ -Daudiotestsrc=enabled
+ -Dtest=enabled
+ -Dvideotestsrc=enabled
+ -Dvolume=enabled
+ -Dvulkan=enabled
+ -Dsdl2=enabled
+ -Dsndfile=enabled
+ -Dsession-managers=[]
+ - cov-configure --config coverity_conf.xml
+ --comptype gcc --compiler cc --template
+ --xml-option=append_arg@C:--ppp_translator
+ --xml-option=append_arg@C:"replace/_sd_deprecated_\s+=/ ="
+ --xml-option=append_arg@C:--ppp_translator
+ --xml-option=append_arg@C:"replace/GLIB_(DEPRECATED|AVAILABLE)_ENUMERATOR_IN_\d_\d\d(_FOR\(\w+\)|)\s+=/ ="
+ --xml-option=append_arg@C:--ppp_translator
+ --xml-option=append_arg@C:"replace/(__has_builtin|_GLIBCXX_HAS_BUILTIN)\(\w+\)/1"
+ - cov-build --dir cov-int --config coverity_conf.xml ninja $NINJA_ARGS -C "$BUILD_DIR"
+ - tar czf cov-int.tar.gz cov-int
+ - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME
+ --form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL
+ --form file=@cov-int.tar.gz --form version="`git describe --tags`"
+ --form description="`git describe --tags` / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID "
+ artifacts:
+ name: pipewire-coverity-$CI_COMMIT_SHA
+ when: always
+ paths:
+ - build-*/meson-logs
+ - cov-int/build-log.txt
+
+shellcheck:
+ extends:
+ - .build_on_fedora
+ stage: analysis
+ script:
+ - shellcheck $(git grep -l "#\!/.*bin/.*sh")
+
+spellcheck:
+ extends:
+ - .build_on_fedora
+ stage: analysis
+ script:
+ - git ls-files | grep -v .gitlab-ci.yml | xargs -d '\n' sed -i 's/Pipewire/PipeWire/g'
+ - git diff --exit-code || (echo "Please fix the above spelling mistakes" && exit 1)
+
+doccheck:
+ extends:
+ - .build_on_fedora
+ stage: analysis
+ script:
+ # Check that each pipewire module has a \subpage entry
+ - git grep -h -o -e "\\\page page_module_\w\+" | cut -f2 -d' ' > pipewire_module_pages
+ - cat pipewire_module_pages
+ - |
+ for page in $(cat pipewire_module_pages); do
+ git grep -q -e "\\\subpage $page" || (echo "\\page $page is missing \\subpage entry in doc/pipewire-modules.dox" && false)
+ done
+
+check_missing_headers:
+ extends:
+ - .fedora
+ - .not_coverity
+ - .fdo.distribution-image@fedora
+ stage: analysis
+ dependencies:
+ - build_on_fedora
+ script:
+ - export PREFIX=`find -name prefix-*`
+ - ./.gitlab/ci/check_missing_headers.sh
+
+pages:
+ extends:
+ - .not_coverity
+ stage: pages
+ dependencies:
+ - build_on_fedora
+ script:
+ - mkdir public
+ - cp -R prefix-*/share/doc/pipewire/html/* public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/.gitlab/ci/check_missing_headers.sh b/.gitlab/ci/check_missing_headers.sh
new file mode 100755
index 0000000..25bcc1f
--- /dev/null
+++ b/.gitlab/ci/check_missing_headers.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+# This script will tell you if there are headers in the source tree
+# that have not been installed in $PREFIX
+
+LIST=""
+
+for i in $(find spa/include -name '*.h' | sed s#spa/include/##);
+do
+ [ -f "$PREFIX/include/spa-0.2/$i" ] || LIST="$i $LIST"
+done
+
+for i in $(find src/pipewire -name '*.h' -a -not -name '*private.h' | sed s#src/##);
+do
+ [ -f "$PREFIX/include/pipewire-0.3/$i" ] || LIST="$i $LIST"
+done
+
+for i in $LIST;
+do
+ echo "$i not installed"
+done
+
+if [ "$LIST" != "" ];
+then
+ exit 1
+fi
+
+exit 0
diff --git a/.gitlab/issue_templates/.gitkeep b/.gitlab/issue_templates/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.gitlab/issue_templates/.gitkeep
diff --git a/.gitlab/issue_templates/bluetooth issue.md b/.gitlab/issue_templates/bluetooth issue.md
new file mode 100644
index 0000000..a1010df
--- /dev/null
+++ b/.gitlab/issue_templates/bluetooth issue.md
@@ -0,0 +1,43 @@
+<!-- If you are filing this issue with a regular release please try master as it might already be fixed. -->
+
+<!-- If you can, test also with Pulseaudio and list `pulseaudio --version`. -->
+
+- PipeWire version (`pipewire --version`):
+- Distribution and distribution version (`PRETTY_NAME` from `/etc/os-release`):
+- Desktop Environment:
+- Kernel version (`uname -r`):
+- BlueZ version (`bluetoothctl --version`):
+- `lsusb`:
+```
+# paste the output of "lsusb" here
+```
+- Bluetooth devices:
+
+```
+# paste the output of "bluetoothctl devices" here
+```
+
+## Description of Problem:
+
+
+## How Reproducible:
+
+
+### Steps to Reproduce:
+
+
+ 1.
+ 2.
+ 3.
+
+
+### Actual Results:
+
+
+### Expected Results:
+
+
+# Additional Info (as attachments):
+
+ - `pw-dump > pw-dump.log`:
+ - Bluetooth debug log, see [here](https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Troubleshooting#bluetooth):
diff --git a/.gitlab/issue_templates/issue.md b/.gitlab/issue_templates/issue.md
new file mode 100644
index 0000000..b43446a
--- /dev/null
+++ b/.gitlab/issue_templates/issue.md
@@ -0,0 +1,29 @@
+<!-- If you are filing this issue with a regular release please try master as it might already be fixed. -->
+
+- PipeWire version (`pipewire --version`):
+- Distribution and distribution version (`PRETTY_NAME` from `/etc/os-release`):
+- Desktop Environment:
+- Kernel version (`uname -r`):
+
+## Description of Problem:
+
+
+## How Reproducible:
+
+
+### Steps to Reproduce:
+
+ 1.
+ 2.
+ 3.
+
+
+### Actual Results:
+
+
+### Expected Results:
+
+
+# Additional Info (as attachments):
+
+ - `pw-dump > pw-dump.log`:
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..3f6b27a
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,77 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to make participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies within all project spaces, and it also applies when
+an individual is representing the project or its community in public spaces.
+Examples of representing a project or community include using an official
+project e-mail address, posting via an official social media account, or acting
+as an appointed representative at an online or offline event. Representation of
+a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at pipewire-maintainers@lists.freedesktop.org. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
+
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..aa100ce
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,26 @@
+Copyright © 2018 Wim Taymans
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice (including the next
+paragraph) shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+---
+
+The above is the version of the MIT "Expat" License used by X.org:
+
+ http://cgit.freedesktop.org/xorg/xserver/tree/COPYING
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644
index 0000000..8969a81
--- /dev/null
+++ b/INSTALL.md
@@ -0,0 +1,237 @@
+## Building
+
+PipeWire uses a build tool called [*Meson*](https://mesonbuild.com) as a basis for its build
+process. It's a tool with some resemblance to Autotools and CMake. Meson
+again generates build files for a lower level build tool called [*Ninja*](https://ninja-build.org/),
+working in about the same level of abstraction as more familiar GNU Make
+does.
+
+Meson uses a user-specified build directory and all files produced by Meson
+are in that build directory. This build directory will be called `builddir`
+in this document.
+
+Generate the build files for Ninja:
+
+```
+$ meson setup builddir
+```
+
+For distribution-specific build dependencies, please check our
+[CI pipeline](https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/.gitlab-ci.yml)
+(search for `FDO_DISTRIBUTION_PACKAGES`). Note that some dependencies are
+optional and depend on options passed to meson.
+
+Once this is done, the next step is to review the build options:
+
+```
+$ meson configure builddir
+```
+
+Define the installation prefix:
+
+```
+$ meson configure builddir -Dprefix=/usr # Default: /usr/local
+```
+
+PipeWire specific build options are listed in the "Project options"
+section. They are defined in `meson_options.txt`.
+
+Finally, invoke the build:
+
+```
+$ meson compile -C builddir
+```
+
+Just to avoid any confusion: `autogen.sh` is a script invoked by *Jhbuild*,
+which orchestrates multi-component builds.
+
+## Running
+
+If you want to run PipeWire without installing it on your system, there is a
+script that you can run. This puts you in an environment in which PipeWire can
+be run from the build directory, and ALSA, PulseAudio and JACK applications
+will use the PipeWire emulation libraries automatically
+in this environment. You can get into this environment with:
+
+```
+$ ./pw-uninstalled.sh -b builddir
+```
+
+In most cases you would want to run the default pipewire daemon. Look
+below for how to make this daemon start automatically using systemd.
+If you want to run pipewire from the build directory, you can do this
+by doing:
+
+```
+cd builddir/
+make run
+```
+
+This will use the default config file to configure and start the daemon.
+The default config will also start `pipewire-media-session`, a default
+example media session and `pipewire-pulse`, a PulseAudio compatible server.
+
+You can also enable more debugging with the `PIPEWIRE_DEBUG` environment
+variable like so:
+
+```
+cd builddir/
+PIPEWIRE_DEBUG="D" make run
+```
+
+You might have to stop the pipewire service/socket that might have been
+started already, with:
+
+```
+systemctl --user stop pipewire.service \
+ pipewire.socket \
+ pipewire-media-session.service \
+ pipewire-pulse.service \
+ pipewire-pulse.socket
+```
+
+## Installing
+
+PipeWire comes with quite a bit of libraries and tools, run:
+
+```
+meson install -C builddir
+```
+
+to install everything onto the system into the specified prefix.
+Depending on the configured installation prefix, the above command
+may need to be run with elevated privileges (e.g. with `sudo`).
+Some additional steps will have to be performed to integrate
+with the distribution as shown below.
+
+### PipeWire daemon
+
+A correctly installed PipeWire system should have a pipewire
+process, a pipewire-media-session (or alternative) and an (optional)
+pipewire-pulse process running. PipeWire is usually started as a
+systemd unit using socket activation or as a service.
+
+Configuration of the PipeWire daemon can be found in
+`/usr/share/pipewire/pipewire.conf`. Please refer to the comments in the
+config file for more information about the configuration options.
+
+The daemon is started with:
+```
+systemctl --user start pipewire.service pipewire.socket
+```
+
+If you did not start the media-session in pipewire.conf, you will
+also need to start it like this:
+```
+systemctl --user start pipewire-media-session.service
+```
+To make it start on system startup:
+```
+systemctl --user enable pipewire-media-session.service
+```
+you can write ```enable --now``` to start service immediately.
+
+### ALSA plugin
+
+The ALSA plugin is usually installed in:
+
+On Fedora:
+```
+/usr/lib64/alsa-lib/libasound_module_pcm_pipewire.so
+```
+On Ubuntu:
+```
+/usr/lib/x86_64-linux-gnu/alsa-lib/libasound_module_pcm_pipewire.so
+```
+
+There is also a config file installed in:
+
+```
+/usr/share/alsa/alsa.conf.d/50-pipewire.conf
+```
+
+The plugin will be picked up by alsa when the following files
+are in `/etc/alsa/conf.d/`:
+
+```
+/etc/alsa/conf.d/50-pipewire.conf -> /usr/share/alsa/alsa.conf.d/50-pipewire.conf
+/etc/alsa/conf.d/99-pipewire-default.conf
+```
+
+With this setup, `aplay -l` should list a pipewire device that can be used as
+a regular alsa device for playback and record.
+
+### JACK emulation
+
+PipeWire reimplements the 3 libraries that JACK applications use to make
+them run on top of PipeWire.
+
+These libraries are found here:
+
+```
+/usr/lib64/pipewire-0.3/jack/libjacknet.so -> libjacknet.so.0
+/usr/lib64/pipewire-0.3/jack/libjacknet.so.0 -> libjacknet.so.0.304.0
+/usr/lib64/pipewire-0.3/jack/libjacknet.so.0.304.0
+/usr/lib64/pipewire-0.3/jack/libjackserver.so -> libjackserver.so.0
+/usr/lib64/pipewire-0.3/jack/libjackserver.so.0 -> libjackserver.so.0.304.0
+/usr/lib64/pipewire-0.3/jack/libjackserver.so.0.304.0
+/usr/lib64/pipewire-0.3/jack/libjack.so -> libjack.so.0
+/usr/lib64/pipewire-0.3/jack/libjack.so.0 -> libjack.so.0.304.0
+/usr/lib64/pipewire-0.3/jack/libjack.so.0.304.0
+
+```
+
+The provided `pw-jack` script uses `LD_LIBRARY_PATH` to set the library
+search path to these replacement libraries. This allows you to run
+jack apps on both the real JACK server or on PipeWire with the script.
+
+It is also possible to completely replace the JACK libraries by adding
+a file `pipewire-jack-x86_64.conf` to `/etc/ld.so.conf.d/` with
+contents like:
+
+```
+/usr/lib64/pipewire-0.3/jack/
+```
+
+Note that when JACK is replaced by PipeWire, the SPA JACK plugin (installed
+in `/usr/lib64/spa-0.2/jack/libspa-jack.so`) is not useful anymore and
+distributions should make them conflict.
+
+
+### PulseAudio replacement
+
+PipeWire reimplements the PulseAudio server protocol as a small service
+that runs on top of PipeWire.
+
+The binary is normally placed here:
+
+```
+/usr/bin/pipewire-pulse
+```
+
+The server can be started with provided systemd activation files or
+from PipeWire itself. (See `/usr/share/pipewire/pipewire.conf`)
+
+```
+systemctl --user start pipewire-pulse.service pipewire-pulse.socket
+```
+
+You can also start additional PulseAudio servers listening on other
+sockets with the `-a` option. See `pipewire-pulse -h` for more info.
+
+
+## Uninstalling
+
+To uninstall, run:
+
+```
+ninja -C builddir uninstall
+```
+
+Depending on the configured installation prefix, the above command
+may need to be run with elevated privileges (e.g. with `sudo`).
+
+Note that at the time of writing uninstallation only works with the
+same build directory that was used for installation. Meson stores the
+list of installed files in the build directory, and this list is
+necessary for uninstallation to work.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f4c692d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,11 @@
+All PipeWire source files are licensed under the MIT License.
+(see file COPYING for details)
+
+With the exception of:
+
+ libspa-alsa.so in spa/plugins/alsa, which contains LGPL code from
+ Pulseaudio and is thus licensed as LGPL.
+
+ libjackserver.so which links against the GPL2 jack/control.h, which
+ makes it GPL2
+
diff --git a/Makefile.in b/Makefile.in
new file mode 100644
index 0000000..67b199b
--- /dev/null
+++ b/Makefile.in
@@ -0,0 +1,78 @@
+VERSION = @VERSION@
+TAG = @TAG@
+SOURCE_ROOT = @SOURCE_ROOT@
+BUILD_ROOT = @BUILD_ROOT@
+
+all:
+ ninja -C $(BUILD_ROOT)
+
+install:
+ ninja -C $(BUILD_ROOT) install
+
+uninstall:
+ ninja -C $(BUILD_ROOT) uninstall
+
+clean:
+ ninja -C $(BUILD_ROOT) clean
+
+run: all
+ SPA_PLUGIN_DIR=$(BUILD_ROOT)/spa/plugins \
+ SPA_DATA_DIR=$(SOURCE_ROOT)/spa/plugins \
+ PIPEWIRE_MODULE_DIR=$(BUILD_ROOT)/src/modules \
+ PATH=$(BUILD_ROOT)/src/examples:$(PATH) \
+ PIPEWIRE_CONFIG_DIR=$(BUILD_ROOT)/src/daemon \
+ ACP_PATHS_DIR=$(SOURCE_ROOT)/spa/plugins/alsa/mixer/paths \
+ ACP_PROFILES_DIR=$(SOURCE_ROOT)/spa/plugins/alsa/mixer/profile-sets \
+ $(DBG) $(BUILD_ROOT)/src/daemon/pipewire-uninstalled
+
+run-pulse: all
+ SPA_PLUGIN_DIR=$(BUILD_ROOT)/spa/plugins \
+ SPA_DATA_DIR=$(SOURCE_ROOT)/spa/plugins \
+ PIPEWIRE_MODULE_DIR=$(BUILD_ROOT)/src/modules \
+ PIPEWIRE_CONFIG_DIR=$(BUILD_ROOT)/src/daemon \
+ ACP_PATHS_DIR=$(SOURCE_ROOT)/spa/plugins/alsa/mixer/paths \
+ ACP_PROFILES_DIR=$(SOURCE_ROOT)/spa/plugins/alsa/mixer/profile-sets \
+ $(DBG) $(BUILD_ROOT)/src/daemon/pipewire-pulse
+
+gdb:
+ $(MAKE) run DBG=gdb
+
+valgrind:
+ $(MAKE) run DBG="DISABLE_RTKIT=1 PIPEWIRE_DLCLOSE=false valgrind --trace-children=yes"
+
+test: all
+ ninja -C $(BUILD_ROOT) test
+
+benchmark: all
+ ninja -C $(BUILD_ROOT) benchmark
+
+monitor: all
+ SPA_PLUGIN_DIR=$(BUILD_ROOT)/spa/plugins \
+ SPA_DATA_DIR=$(SOURCE_ROOT)/spa/plugins \
+ PIPEWIRE_MODULE_DIR=$(BUILD_ROOT)/src/modules/ \
+ $(BUILD_ROOT)/src/tools/pw-mon
+
+cli: all
+ SPA_PLUGIN_DIR=$(BUILD_ROOT)/spa/plugins \
+ SPA_DATA_DIR=$(SOURCE_ROOT)/spa/plugins \
+ PIPEWIRE_MODULE_DIR=$(BUILD_ROOT)/src/modules/ \
+ $(BUILD_ROOT)/src/tools/pw-cli
+
+shell: all
+ ninja -C $(BUILD_ROOT) pw-uninstalled
+
+dist: all
+ git archive --prefix=pipewire-$(VERSION)/ -o pipewire-$(VERSION).tar.gz $(TAG)
+
+rpm: dist
+ rpmbuild -ta pipewire-$(VERSION).tar.gz
+
+publish: all
+ git branch -D gh-pages 2>/dev/null || true && \
+ git branch -D draft 2>/dev/null || true && \
+ git checkout -b draft && \
+ git add -f $(BUILD_ROOT)/doc/html && \
+ git commit -anm "Deploy on gh-pages" && \
+ git subtree split --prefix $(BUILD_ROOT)/doc/html -b gh-pages && \
+ git push --force origin gh-pages:gh-pages && \
+ git checkout work 2>/dev/null
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..c664c79
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,4812 @@
+# PipeWire 0.3.65 (2023-01-26)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Add back the deprecated symbols but make sure a deprecated warning is
+ emitted for them. This fixes compilation issues in bindings.
+ - Fix an error in the AVX code that could cause crackling in filter-chain
+ when using the mixer.
+ - The convolver in filter-chain can now select an IR from a list of IRs
+ that best matches the current samplerate. Also resampling of the IR
+ has been improved.
+ - A new native module-combine-stream was added. You can use this to create
+ a 5.1 device from 3 stereo soundcards, for example, or direct the output
+ to multiple sinks at once.
+ - Support for Bluetooth MIDI was added. This requires a wireplumber
+ addition as well.
+ - An ALSA plugin rule was added to tweak the buffer settings in Davinci
+ Resolve so that it now runs with acceptable latency. (#1697)
+ - Support for compress offload was added using tinycompress. This allows
+ compressed formats to be decoded in hardware using ALSA on some devices.
+ - Many more buffixes and improvements.
+
+
+## PipeWire
+ - Add back the deprecated symbols but make sure a deprecated warning is
+ emitted for them. (#2952)
+ - Fix a regression when running older servers and newer clients (such as
+ flatpaks on older server) where the server would run clients too soon,
+ causing crashes. (#2964)
+ - Ensure that environment variables override any config values.
+
+## Tools
+ - pw-cli has received some improvements in the output.
+ - pw-cat can now use ffmpeg to demux streams for compress offload.
+
+## modules
+ - The convolver IR volume is now preserved after resampling.
+ - Adapter ports can now have a custom prefix.
+ - module-rt now clamps the realtime priority to the user allowed one if
+ it is within an acceptable range. Before it would fall back to RTKit
+ immediately.
+ - The module-echo-cancel can now have per stream channel layouts which
+ makes it possible to link to specific audio ports on a device. (#2939)
+ - Fix an error in the AVX code that could cause crackling in filter-chain
+ when using the mixer. (#2965)
+ - The convolver in filter-chain can now select an IR from a list of IRs
+ that best matches the current sample-rate.
+ - module-pipe-* now better matches the pulseaudio properties. (#2973)
+ - A new combine-stream module was added to combine multiple sinks into
+ one sink. It is also possible to merge multiple sources into one.
+ - module-rtp-source now has match rules to select what SAP sessions
+ to stream from. There were also improvements to the buffering and
+ latency handling.
+ - module-rtp-sink now handles multicast loopback correctly.
+ - module-rtp-sink implements min-ptime and max-ptime to control the
+ send packet latency.
+
+## SPA
+ - A new modifier flag was added to the video format parser helper to
+ allow 0 (linear) as a valid modifier. (#2943)
+ - Params includes were reorganized to make it more scalable. Many compressed
+ audio formats were added.
+ - The alsa pcm plugin now handles invalid values from the driver
+ gracefully. (#2953)
+ - Fix some potential stuttering cause by wrong scaling and overflow
+ of the output buffers in audioconvert. (#2680)
+ - Debug output is now also sent to the log instead of stdout. (#2923)
+ - A debug context was added to debug macros to implement custom debug
+ handling. This is used to redirect the debug of pods to the debug log
+ instead of using some custom duplicated code.
+ - Fix some warnings for potentially undefined shifts in format
+ conversion.
+ - Support for compress offload was added using tinycompress. This is mostly
+ used on some embedded hardware where decoding of audio formats can be
+ done in hardware.
+
+## Bluetooth
+ - Some fixes for LE audio were added.
+ - Support for Bluetooth MIDI was added. This requires a wireplumber
+ addition as well.
+ - Reply OK to empty commands.
+ - Improve compatibility with some devices that send stray \n such as
+ the Sennheiser HD 350BT. (#2991)
+
+## pulse-server
+ - Devices with unsupported formats (by the pulseaudio API) are now also
+ listed in the pulseaudio API (with invalid formats).
+ - The native module-combine-stream is used for module-combine-sink.
+
+## JACK
+ - Make jack.merge-monitor default to true to better match the jack1/2
+ behaviour. Add an exception for mixxx, which is more usable with
+ unmerged monitors. (#1760)
+
+## ALSA
+ - The property handling in the ALSA plugin was improved. alsa.properties
+ and alsa.rules can now be added to the config file.
+ - A rule was added to tweak the buffer settings in Davinci Resolve so that
+ it can run with acceptable latency. (#1697)
+ - ALSA volume will now also use cubic volumes, like pulseaudio.
+ - The ALSA ctl plugin now also uses the client-rt.conf file.
+ - A new alsa.volume-method was added to configure cubic or linear volume.
+ This can be set per application using the rules.
+
+## GStreamer
+ - pipewiresrc will now advertize DMABUF support if the pipeline suports
+ this.
+ - pipewiresrc will now always be a live source unless told otherwise.
+
+Older versions:
+
+
+# PipeWire 0.3.64 (2023-01-12)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Clear old buffer memory on ports to fix some SIGBUS errors.
+ - It is now possible to assign custom port names to the ports from an
+ adapter. This feature is helpful to those who use a multichannel
+ interface with long-term connections. This way they can label each
+ port with its designation, such as an instrument name or anything else
+ to be displayed in a patchbay or DAW.
+ - Fix some issues with node suspend and quantum and rate calculations.
+ - Fix some regressions in pulse-tunnel and RTP-source adaptive resampling
+ that could cause synchronization problems.
+ - UCM devices now also have a Pro Audio profile.
+ - NODE_TARGET (with the object.id) is now deprecated, use TARGET_OBJECT
+ (with the object.serial, which is not reused and can avoid races).
+
+
+## PipeWire
+ - Clear all peer input port buffers when suspending. This fixes some
+ SIGBUS errors when some plugins were using old memory. (#2914)
+ - Fix a case where nodes that were not supposed to be suspended, were
+ kept suspended on a rate change. (#2929)
+ - Fix an error in the quantum and rate calculations that could cause
+ nodes to run with wrong quantum and rates when multiple rates were
+ allowed. (#2925)
+
+## Tools
+ - pw-dump will now sort dictionaries to make it easier to compare
+ different outputs.
+ - Improve output of pw-reserve.
+ - pw-loopback uses TARGET_OBJECT so you will need to use the serial
+ id (or better the name) as the target instead of the object id.
+
+## modules
+ - The filter-chain modules has seen some cleanups, refactoring and
+ optimizations in the various DSP functions.
+ - The ROC module now supports setting a custom samplerate.
+ - ROC 0.2.X is now required.
+ - The pulse tunnel and RTP source were not updating the rate field
+ correctly which could cause synchronization problems. (#2891)
+ - The filter-chain now supports an arbitrary number of control
+ properties. (#2933)
+ - It is now possible to assign custom port names to the ports from an
+ adapter with the PW_KEY_NODE_CHANNELNAMES.
+ - Support was added for capture and playback props in echo-cancel.
+ (#2939)
+
+## SPA
+ - The ACP code now has an option to set the probe samplerate. (#1599)
+ - UCM devices now also have a Pro Audio profile.
+ - Filtering of Step ranges is now implemented.
+
+## Pulse-Server
+ - The channel-map is now set correctly on the echo-cancel module.
+ - source_master and sink_master are now correctly handled in module
+ echo-cancel.
+ - Fix a regression in DRAIN where resuming after a DRAIN would fail.
+ This caused problems for espeak. (#2928)
+ - TARGET_OBJECT is now used to make it possible to use the indexes
+ as a target.
+ - ladspa-source and remap-source can now also link to monitors.
+
+## ALSA
+ - The ALSA plugin now handles the target.object correctly when set to
+ -1. (#2893)
+
+## V4L2
+ - The v4l2 replacement library now also follows symlinks.
+ - Support for getting and setting controls was added.
+ - Support for G_PARM was added.
+ - The environment variable PIPEWIRE_V4L2_TARGET can be used to force
+ an application onto a specific camera.
+
+## Bluetooth
+ - Fix compilation without ldac_abr.
+ - Fix a missing brace in CIND reply. This could cause some devices to
+ fail.
+ - Fix configuration of the initial latency.
+
+## GStreamer
+ - The device provider now supports setting an fd so that it can connect
+ to PipeWire sessions from the portal.
+ - DMABuf support was re-enabled in gstpipewiresrc.
+
+# PipeWire 0.3.63 (2022-12-15)
+
+This is a quick bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Fix a critical bug that causes audio distortion in some cases when using
+ AVX2.
+ - Fix a crash in mpv caused by deinit of PipeWire.
+ - Resample the convolver IR to match the graph samplerate for better
+ results.
+ - Many more small bugfixes and improvements.
+
+
+## PipeWire
+ - Fix a segfault in the PipeWire deinit code triggered by mpv in some
+ cases. (#2881)
+ - Fix docs about SPA_PLUGIN_DIR.
+ - Always dlclose by default (even under valgrind). Add an option with
+ PIPEWIRE_DLCLOSE to select alternative behaviour.
+ - Improve PIPEWIRE_DEBUG category handling.
+
+## modules
+ - Resample the IR for the convolver when the IR samplerate and graph rate
+ don't match.
+
+## SPA
+ - Handle spurious reads from timerfd gracefully.
+ - Fix potential stack-use-after-scope when starting Audacity.
+ - Fix distorted audio when using AVX2. (#2885)
+ - Remove fallback to default channel map in channelmix.
+ - Improve sorting of MIDI events, use the same order as Ardour. (#1816)
+ - Enable LFE downmixing by default. (#2425)
+ - Make IEC958/AC3 and IEC958/DTS work better by enforcing a fixed minimal
+ buffering for the encoder to avoid stuttering. (#2650)
+
+## Pulse-Server
+ - Add a new pulse.cmd config section to execute pulse commands, currently
+ only for loading modules. This removes the dependency on pactl.
+ - Improve debug of messages.
+
+# PipeWire 0.3.62 (2022-12-09)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - A regression in screensharing was fixed. It was caused by a race when
+ activating links and driver nodes.
+ - Video transform metadata was added so that cameras and screen sharing
+ can report the video orientation and transformations.
+ - Support for the PulseAudio module-gsettings was added to make paprefs
+ work.
+ - Support for bluetooth offloading was added. This allows for the bluetooth
+ reception, decoding and playback to happen completely in hardware.
+ This also requires some support in WirePlumber.
+ - Many bugfixes and improvements.
+
+
+## PipeWire
+ - More work on stopping nodes in a more controlled way.
+ - Fix a race in starting nodes and drivers. In some cases the driver
+ node would already be started while the link to the peer node was not
+ ready yet. This caused regressions in screen sharing. The driver is
+ now only started after all the followers and links completed.
+ - Fix a case where a slow capture stream would not recycle buffers
+ anymore and stall. (#2874)
+ - Fix a subtle bug in pw_loop_invoke that could cause callbacks to be
+ delayed and cause crashes in some cases.
+ - Fix a case where IPC was done from the data-thread and could cause
+ crashes.
+
+## Tools
+ - Silence some expected errors in the pw-top output.
+
+## modules
+ - The filter-chain has seen some optimizations in the copy plugin and
+ the convolver.
+ - The zeroconf plugin will now only unpublish services from the server
+ that was removed.
+ - Fix a potential crash when stopping pw-loopback.
+ - Some harmless errors were turned into info messages.
+ - Fix some cases where pw_stream methods were called from the data-thread
+ that could cause segfaults. (#2633)
+
+## SPA
+ - There is now a video transform metadata that indicates how a video
+ frame was transformed (rotated/flipped). libcamera and the GStreamer
+ elements now have support for this metadata.
+ - The SPA volume plugin is now disabled from the default build.
+ - Handle missing control info in libcamera.
+ - Handle errors from loop better, don't call the callbacks on errors.
+ - Somewhat improve performance in some audioconvert AVX2 code for format
+ conversion.
+ - Fix PortConfig and EnumPortConfig params in audioconvert and
+ audioadapter to reflect what is actually going on instead of using
+ hardcoded values.
+ - Pass ignore-dB property correctly in all cases.
+ - Probing is now done in 48KHz again. (#2857)
+
+## Pulse-server
+ - IPv4 addresses are now added first to the list and exposed first with
+ zeroconf discover.
+ - module-gsettings was added to make paprefs work.
+ - The pulse.idle.timeout option was disabled by default and only enabled
+ for selected apps (speech-dispatcher) because it caused some problems
+ for other apps. (#2880)
+
+## JACK
+ - Only process valid ports. Could fix some crashes. (#2863)
+
+## Bluetooth
+ - Support was added for offloading bluetooth handling. Some hardware can
+ receive, decode and play the bluetooth audio directly in hardware.
+
+# PipeWire 0.3.61 (2022-11-24)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Fix a bug in audioadapter that could cause crashes when switching
+ bluetooth profiles.
+ - Fix sound in QEMU, deadbeef and openal again.
+ - libcamera plugin fixes, dynamic add and remove should now work with
+ the next wireplumber version.
+ - Fix a regression in pw-midiplay where the first buffer would not
+ play and some events would be missing.
+ - The network module now doesn't export other network sources
+ anymore.
+ - pulse-server now detects clients that keep underrunning for a long time
+ and will pause them to save power.
+ - Many more bugfixes and improvements.
+
+
+## PipeWire
+ - Optimize away some useless graph recalculations.
+ - Increase alternative sample rates from 16 to 32.
+ - FreeBSD and musl build fixes.
+ - Silence some module loading errors when the error can be ignored.
+ - Fix initial buffer requested size for pw-stream when operating in
+ async mode. This also indirectly fixes the first buffer in
+ pw-midiplay. (#2843)
+
+## Modules
+ - Set the network property on pulse-tunnel streams so that they are
+ not exported anymore. (#2384)
+ - Filter-chain has optimized mix functions now.
+
+## SPA
+ - Handle some errors in libcamera better.
+ - Fix libcamera remove events. Fix the id allocation for devices.
+ - Fix a bug in audioadapter where it would not renegotiate after
+ a port reconfiguration, leading to crashes, especially when
+ automatically switching profiles in bluetooth. (#2764)
+ - Do ALSA probing in 44100Hz again. Some devices seem to fail
+ otherwise for some unknown reason. (#2718)
+ - Force playback start when the ALSA buffer is full. This fixes sound
+ in QEMU. (#2830)
+ - Support Digital 5.1 AC3 for Asus Xonar SE.
+ - Improve format renegotiation in audioadapter. This makes the ALSA
+ plugin work again for deadbeef. (#2832)
+ - Fix latency reporting on adapter DSP ports.
+
+## pulse-server
+ - Fix a bug where openal based applications would hang. (#2821)
+ - Improve zeroconf publish. Only publish on the address of the first
+ running server. This avoids duplicate entries for IPv4 and IPv6.
+ Add support for republish entries when new servers are started.
+ - Add a pulse.idle.timeout option (default to 5 seconds) to pause
+ streams that have been underrunning for this amount of time. Badly
+ behaving clients will then not keep the graph and device busy so
+ that devices can be suspended to save battery. This should give
+ better default behaviour with speech-dispatcher. (#2839)
+
+## JACK
+ - Add an option to configure the filter character.
+ - Fix connect_callbacks. It was only called once for output ports.
+ (#2841)
+ - Add option to set node.passive on jack clients. Make some quirks
+ for qsynth to make it suspend and fade out better.
+
+# PipeWire 0.3.60 (2022-11-10)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - The filter-chain now handles errors better and has fixes for many
+ crasher bugs.
+ - A new RTP module was added with a sender and receiver. It uses SAP
+ to announce and consume RTP streams and is compatible with the
+ PulseAudio RTP modules.
+ - Many small bluetooth improvements and fixes.
+ - The alsa plugin will now only start playback when there is data. This
+ results in better sync and lower latency between capture and playback.
+ - The v4l2 and libcamera plugins have seen a lot of improvements. They
+ support control properties now. Also pw-v4l2 has seen many improvements
+ and mostly passes the v4l2-compliance test now.
+ - Many more bugfixes and improvements.
+
+
+## PipeWire
+ - Code cleanups, compiler warning fixes.
+ - Add some extra checks to avoid scheduling an inactive node.
+ - Rework the sequence of events to start and stop nodes.
+ - Improve param enumeration.
+ - An option was added to give priority to the Buffer params of the
+ consumer. This makes it possible to use the default values of the
+ consumer (instead of the producer) when capturing from a source.
+ - The graph rate selection was improved to pick a rate closest to the
+ requested one (instead of picking the default).
+
+## Modules
+ - Fix some crashes in filter-chain. (#2737)
+ - X11 Bell module will now be loaded by default when available.
+ - A new RTP module was added with a sender and receiver. It uses SAP
+ to announce and consume RTP streams and is compatible with the
+ PulseAudio RTP modules.
+ - Improve RAOP compatibility.
+ - The echo-cancel module now uses the resampler prefill option to align
+ input and output samples without buffering. Better latency control
+ when starting and stopping has been implemented.
+ - The pulse tunnel will now write aligned samples to pulseaudio even
+ when the ringbuffer wraps around. This fixes playback issues with
+ multichannel sinks.
+ - Add a delay option to module-loopback using a ringbuffer.
+ - Implement echo-cancel params.
+ - The filter-chain module has better error reporting.
+ - The LADSPA search path was extended with some more common paths.
+ - The echo-canceler input can now also be a monitor of a sink. This
+ improves compatibility with some proton games that expect a real
+ sink instead of a virtual one.
+
+## Tools
+ - Better error reporting in pw-link.
+ - pw-top now also shows IEC958 passthrough formats and JPEG/H264 video
+ formats.
+ - pw-top refreshes the screen faster.
+ - pw-top now prints the state of the node and shows less info for
+ inactive nodes.
+ - pw-dump now uses the new seq field in the spa_param_info to discard
+ old param updates and avoid duplicate params in the output.
+
+## Bluetooth
+ - Add ModemManager support in the native backend.
+ - Clean up GetManagedObjects handling.
+ - Handle QoS from the endpoints in the codec.
+ - Increase the socket buffer to have more control over the rate and QoS.
+ - Simplify the packet flushing code.
+ - Stop processing nodes before destroying them.
+ - Fix timers when a source switches drivers.
+ - Codecs can now share endpoints. This reduces the amount of endpoints and
+ avoids problems with devices that can't handle a large amount of
+ codec endpoints.
+ - Report batery status to UPower for HFP AG.
+ - Fix bitpool increase.
+
+## SPA
+ - The audioresampler now avoids clicks and pops between activating and
+ deactivating the adaptive resampler when used by the stream API.
+ - Use default locale to parse float parameters.
+ - The upmix functions now have SSE optimizations.
+ - Avoid recalculating the complete channelmix setup when only the
+ volume changes.
+ - The alsa plugin will now only start playback when there is data. This
+ results in better sync and lower latency between capture and playback.
+ - The ALSA MIDI sequencer will now pull data from the graph even when it
+ did not output anything. Fixes some graph stalls with the sequencer in
+ some cases. (#2775)
+ - v4l2 and libcamera sources now recycle buffers when nothing is consuming
+ them. This avoids stalling the graph.
+ - libcamera now suggests a more appropriate frame size than the smallest
+ poster frame.
+ - Improve state changes in audioconvert. (#2764)
+ - A new seq field was added to spa_param_info to keep track of pending
+ param updates.
+ - Support speaker output only on RealTek ALC4080. (#2744)
+ - The v4l2 source now supports setting controls.
+ - The libcamera plugin now supports enumerating and setting controls.
+ - A new unit test for 6.1 channel mapping was added. (#2809) More debug
+ info was added to audioconvert for the channel matrix.
+ - Audioconvert will now also upmix a rear-center channel when needed.
+
+## pulse-server
+ - Add support for the RTP send and recv modules with the new native
+ RTP module.
+ - Add option to set latency for pulse-tunnel streams and
+ module-zeroconf-discover.
+ - The socket will now be given the same permissions as what pulseaudio
+ did (0777).
+ - Implement module-loopback latency_msec correctly with the new delay
+ parameter.
+ - sysfs.path is now filled with the same data as pulseaudio.
+ - The manager now uses the new seq field in the spa_param_info.
+ - Fix a bug where in some cases the read pointer would get out of sync
+ and cause too large requests. (#2799)
+
+## ALSA
+ - The alsa plugin now reuses the stream in prepare which results in
+ better performance.
+ - Some deadlocks have been fixed in the ALSA plugin.
+ - The ALSA plugin reports more accurate timing information in some cases.
+
+## V4l2
+ - The v4l2 compatibility layer has received a lot of updates.
+ - Improved node names and format enumeration.
+ - Support for multiple /dev/videoX devices, each mapped to a unique
+ PipeWire node.
+ - Passes the v4l2-compliance test now with both the v4l2 and libcamera
+ backend in PipeWire.
+ - Improved mmap support for inline buffer memory. This makes it possible to
+ consume PipeWire streams.
+ - Negotiation works more reliably now.
+
+## JACK
+ - Implement jack_acquire_real_time_scheduling() and
+ jack_drop_real_time_scheduling() by keeping the thread utils in a global
+ state.
+ - Fix jack_client_thread_id() to return NULL when the client is not active,
+ just like jack1 and jack2.
+ - An option was added to let the jack_set_buffer_size() function update the
+ global metadata. A quirk was added so that jack_bufsize uses this new feature
+ to make the buffer size settings persistent and global, just like jack.
+ - jack_port_register() and jack_port_unregister() can be called on an
+ active client so make this thread safe. (#2652)
+
+# PipeWire 0.3.59 (2022-09-30)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Fix possible wrong samplerate in loopback streams after suspend and
+ rate switch.
+ - module-filter-chain can now adapt to the graph samplerate.
+ - Fix some potential stuttering and crackling in pulse-server.
+ - Add Bluetooth LE support. This requires experimental kernel and bluez
+ support.
+ - The ALSA plugin has more options to control the buffer size. This can
+ be used to work around high latency in davinci resolve.
+ - Many bugfixes and improvements.
+
+
+## PipeWire
+ - Add audio capture example with volume meter.
+ - Fix a case where a rate switch would not suspend all the nodes of the
+ driver first. This could cause wrong samplerates in streams.
+ - Fix a case where a node would be Paused while still added to the
+ graph, causing potential crashes. (#2701)
+
+## Modules
+ - module-filter-chain and module-loopback now use the resample.prefill
+ option to avoid buffering extra samples and causing unwanted latency
+ when resampling is activated.
+ - module-filter-chain can now adapt to the graph samplerate.
+ - Improve module-raop to support the ALAC codec as raw PCM.
+ - Improve RTSP parsing to improve compatibility.
+
+## Tools
+ - Fix 100% CPU in pw-cli monitor mode. (#2709)
+ - spa-acp-tool can now be exited with ctrl-D.
+
+## SPA
+ - Various libcamera fixes and improvements.
+ - Set stride on audioconvert output buffers.
+ - Make sure we always place the last requested size from the resampler
+ on the buffers in pw-stream.
+ - Add resample.prefill option in the resampler to fill the history with
+ 0 so that we don't have smaller buffers at the start.
+ - Make sure that when an overflow corrupts a POD, that it will always
+ stay corrupted.
+ - Rate limit some ALSA warnings and reduce some unwanted warnings.
+ - Don't recalculate the audioconverter state for each pause/play. (#2701)
+ - Fix some POD parsing inconsistencies and potential overflows.
+ - Add support for Asus Xonar SE.
+ - Fix Flush command handling. It should not stop playback. (#2726)
+ - Refactor the peaks function and add some unit tests and optimizations.
+ - The channelmix has an optimized nXm converter and new unit tests.
+ - Normalization in the channelmixer was fixed.
+
+## pulse-server
+ - The requested latency of record streams was reduced to fix some
+ stuttering in Teamspeak. (#2702)
+ - Tweak the max amount of bytes sent to a client. (#2711) (#2715)
+ - Improve maxlength calculations, this fixes some crackling noise with
+ high samplerate and channel counts in some players (audacious).
+
+## Bluetooth
+ - Merge Bluetooth LE support.
+ - Make sure we are backward compatible with WirePlumber.
+ - Fix some HFP and HSP AT command parsing. (#2463)
+ - Use HFP by default over HSP.
+
+## ALSA
+ - Increase max number of periods.
+ - The parameters handling was improved. There is now an option to set the
+ buffer-bytes of the ALSA plugin.
+ - PIPEWIRE_ALSA can now be used as an environment variable to restrict the
+ plugin formats and buffer size.
+
+# PipeWire 0.3.58 (2022-09-15)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Fix a regression that could cause audio crackling.
+ - Fix a regression in RTKit because rlimit was not set correctly.
+ - JAVA sound applications will now alsa work with the pulseaudio-alsa plugin.
+ - pw-top will now show the negotiated formats of devices and streams.
+ - Fix some potential crashes when starting streams.
+ - The ALSA plugin has had improved timing reporting and poll descriptor
+ handling that should improve compatibility.
+ - Many more improvements and bugfixes.
+
+
+## PipeWire
+ - Avoid scheduling nodes before they are added to the graph. This could
+ avoid some crashes when scheduling nodes that were not completely
+ started yet. (#2677)
+
+## Tools
+ - pw-top now also shows the negotiated formats of streams and devices.
+ (#2566)
+ - pw-top prints microseconds as "us" now to avoid unicode problems.
+
+## Modules
+ - Fix compilation with newer lv2.
+ - Fix setting realtime priority with RTKit, it was not setting rlimit
+ correctly and RTKit would refuse to change the priority.
+ - Fix some playback problems with RAOP sink. (#2673)
+ - Filter chain will now warn when a non-existing control property is
+ used in the config file. (#2685)
+ - Filter chain can now handle control port names with ":" in the name.
+ (#2685)
+ - The echo-cancel module and interface now has activate/deactivate
+ functions to make it possible for plugins to reset their state.
+
+## SPA
+ - Make sure audioconvert uses the given channelmap and channels for the
+ volumes, even when not negotiated yet. This makes it possible to change
+ the volume before the node has been negotiated.
+ - Refactor the peaks resampler. Fix an error in the SSE code.
+ - Fix DSD min/max rates, avoid exposing impossible rates.
+ - Set monitor port buffer size correctly. This could cause some crackling
+ and hickups. (#2677)
+ - Make ALSA sequencer port names unique.
+
+## Pulse-server
+ - Rework the capture buffer attributes to better match pulseaudio. This
+ fixes a regression where opening pavucontrol could cause crackling.
+ (#2671)
+ - Implement TRIGGER and PREBUF methods.
+ - Handle clients that send more than the requested amount of data.
+ PipeWire will now also keep this as extra buffered data like PulseAudio.
+ This fixes JAVA sound applications when they are running on top of the
+ PulseAudio ALSA plugin. (#2626,#2674)
+ - Update the requested amount of bytes more like PulseAudio. Fixes
+ stuttering after resume with the GStreamer pulseaudio sink. (#2680)
+
+## ALSA Plugin
+ - More debug info was added. The time reporting was improved.
+ - The poll descriptor handling was improved, avoiding some spurious
+ wakeups. (#1697)
+
+
+# PipeWire 0.3.57 (2022-09-02)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Support masking of conf.d/ files. (#2629)
+ - Use org.freedesktop.portal.Realtime when available. This does the
+ correct PID/TID mappings to make realtime also work from flatpaks.
+ - Fix rate adjustement logic in pulse-tunnel. This would cause
+ increasing delays and hickups when using tunnels. (#2548)
+ - Add OPUS as a new vendor codec. Add OPUS-A2DP spec. PipeWire can now
+ send and reveive OPUS data over bluetooth.
+ - An AAC decoder was added so that PipeWire can now also function as
+ an A2DP AAC receiver.
+ - Fix some issues where the wrong samplerate was used. (#2614)
+ - Fix rate match for sources. This fixes an error where follower sources
+ would generate many resync warnings.
+ - Many more bugfixes and improvements.
+
+
+## PipeWire
+ - Support masking of conf.d/ files. (#2629)
+ - Add some more debug info to memfd.
+ - Improve data-loop invoke method. Also flush pending items. (#2631)
+ - Add a filter-chain systemd service file than can be used to start
+ custom filters placed in ~/.conf/pipewire/filter-chain.d/ (#2553)
+ - Improve triggered timestamps for remote nodes.
+ - Fix some potential cross compilation problems due to wrong
+ host_machine.
+ - Check return values of pw_getrandom().
+
+## Tools
+ - Updates to pw-cli manpages. (#2552)
+ - Remove the pw-cli dump command. It is mostly implemented as part of
+ wpctl status, pw-dump, pw-link, pw-top and others.
+ - Clean up resource in pw-cat correctly on errors. (#2651)
+
+## Modules
+ - Fix compilation of AVB on big-endian. Enable AVB only on Linux.
+ - Use org.freedesktop.portal.Realtime when available. This does the
+ correct PID/TID mappings to make realtime also work from flatpaks.
+ - Fix compilation of ROC module when headers are missing. (#2513)
+ - Improve some error cleanup paths in protocol-native. Improve connect
+ and disconnect.
+ - Fix a potential crash in FFT unload in filter-chain.
+ - Implement PIPEWIRE_NOTIFICATION_FD for notification when the socket
+ is ready.
+ - Try to use rtkit if set_nice() fails.
+ - Fix rate adjustement logic in pulse-tunnel. This would cause
+ increasing delays and hickups when using tunnels. (#2548)
+ - Handle disconnect in pulse-tunnel.
+
+## Bluetooth
+ - Add OPUS as a new vendor codec. Add OPUS-A2DP spec. PipeWire can now
+ send and reveive OPUS data over bluetooth.
+ - An AAC decoder was added so that PipeWire can now also function as
+ an A2DP AAC receiver.
+
+## SPA
+ - Tweak the resampler window function some more. (#2574)
+ - Improve format convert performance in some fallback cases.
+ - Fix rounding in format conversion on ARM NEON.
+ - Fix libcamera build error. (#2575)
+ - Fix some issues where the wrong samplerate was used. (#2614)
+ - Don't wait for more samples that can fit in the ringbuffer in ALSA.
+ - Improve buffer size handling in audioconvert, scale the buffers based
+ on the rate conversion and make things work with really large rate
+ conversions as well.
+ - Add more and better debug for ALSA devices.
+ - Improve channel mix: Filter FC and LFE when copying from a different
+ layout. Implement STEREO from FC. Avoid generating REAR from FC in PSD
+ mode.
+ - Fix rate match for sources. This fixes an error where follower sources
+ would generate many resync warnings.
+ - Improve ALSA format negotiation. If the ALSA node is not running and
+ there was a previously configured format, close and reopen the device
+ to enumerate and accept all possible formats again. (#2625).
+
+## ALSA
+ - The alsa plugin will now also save the volumes set with the control
+ API. This saves the volumes set with alsa-mixer, for example.
+
+## Pulse-server
+ - Flatpak apps with devices=all (Zoom) will now be granted Manager
+ permissions.
+ - Small tweaks to the amount of data sent to clients to work around an
+ issue in freerdp.
+
+## JACK
+ - Clean up the transport correctly when closing a client. (#2569)
+ - Match context properties in addition to node properties for the jack
+ client rules. (#2580)
+ - Make sure to return an error when disconnected from the server. (#2606)
+ - Fix thread cast problem in jack_client_thread_id().
+ - Increase jack_client_name_size() length and make sure we have space for
+ the \0 byte.
+ - JACK clients from the same application will be added to the same group
+ so that they share the quantum and rate.
+
+# PipeWire 0.3.56 (2022-07-19)
+
+This is a quick bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - A critical bug that could crash JACK apps was fixed.
+ - Some more regressions in audiomixer were fixed. This should fix crackling
+ and stuttering in some cases as well as some channel mapping regressions.
+ - A bug in the alsa plugin was fixed that could cause stuttering in VMs.
+ - Bluetooth sources should have improved latency and rate control.
+ - Many more bugfixes and improvements.
+
+
+## Modules
+ - An experimental AVB module was added. It can expose PipeWire as an AVB
+ entity and initiate (broken) streaming between entities.
+ - module-loopback now handles the cases where the input and output channels
+ are different without crashing or producing silence.
+ - The filter-chain module now correctly calculates the output size without
+ crashing in some cases. It also skips invalid ports instead of crashing.
+ - Handle and report pthread errors better.
+
+## SPA
+ - The resampler qualities were tweaked a little.
+ - A bug that would sometimes cut off the last part of a buffer was fixed in
+ the alsa plugin. This could cause broken audio in VMs. (#2536)
+ - Access to the alsa mixer and devices is now checked more thoroughly.
+ (#2534)
+ - The spa-resample tool can now also handle large downsampling rates without
+ crashing.
+ - Audioconverter now uses rounding for float to int conversions, which
+ reduces distortions. Compilation of the c functions was separated and uses
+ its own optimization flags now. Unit tests were added. (#2543)
+ - Noise shaping was improved in audioconvert. A new Wannamaker 3 tap shaper
+ was added.
+ - Audioconvert now uses a pattern for generating keep alive noise. This
+ should have much less energy and be even more inaudible. (#2540)
+ - A channel mapping bug was fixed in audioconvert. Unit tests were added.
+ - The dsp audio mixer would sometimes not mix enough and cause dropouts.
+ (#2525)
+
+## JACK
+ - A critical bug in the mixer was fixed. It would cause most JACK apps to
+ segfault at startup.
+
+## Bluetooth
+ - A new rate control algorithm was implemented for the sources.
+ - The media role on HSP/HFP streams is now fixed.
+
+## Pulse Server
+ - Add the resampler delay to delay reporting as well.
+
+
+# PipeWire 0.3.55 (2022-07-12)
+
+This is a quick bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Fix some more critical bugs in the new audioconvert and the queueing
+ in pw-stream that causes stuttering and hickups.
+ - HFP hardware volumes are now saved and restored.
+ - Format conversions and mixing was improved.
+ - Small bug fixes and improvements.
+
+## PipeWire
+ - The queueing in pw-stream was improved with support for buffer prefetch
+ in async mode.
+ - Add a pw-filter unit test.
+
+## tools
+ - pw-midiplay should now work again after improvements in pw-stream.
+
+## modules
+ - The RAOP module was improved to support auth_setup.
+ - The RAOP module should now handle timing packets better.
+ - Add some more filter-chain examples.
+ - The filter-chain now has a separate config file with the boilerplate
+ settings. The examples are now just config snippets that can be dropped
+ in .conf.d/ directories, such as the filter-chain.conf.d/ one.
+ - Start suggesting to use target.object instead of node.target in docs
+ and examples.
+
+## SPA
+ - Use the cosh window again for the resampler. It should now
+ give better resampler quality. (#2483)
+ - Rework the mixer functions. They were rewritten for higher precision and
+ better performance. Add unit tests and benchmarks.
+ - Improve format conversion for 32bits for avoid errors in clang because
+ of undefined behaviour at extreme ranges.
+ - Fix a bug in audioconvert where it would not consume the right
+ amount of samples when the resampler was disabled. This could cause
+ skipping and hickups. (#2519)
+ - Fix bug in audioconvert where it would try to convert the input samples
+ multiple times, causing strange artifacts when upmixing.
+ - Be more strict about valid JSON floats.
+ - device.vendor.id and device.product.id should now always show up in
+ 0xXXXX format and should not be converted to floats in pw-dump anymore.
+ - Add triangular dither, add unit tests for noise generation, add some
+ more optimizations.
+
+## Bluetooth
+ - HFP and A2DP now expose different routes and thus can have different
+ volumes.
+ - HW Volumes for HFP are now synced better. Volume changes from HW buttons
+ are now also saved.
+
+# PipeWire 0.3.54 (2022-07-07)
+
+This is a quick bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Some critical bugs in the new audioconvert were fixed. The old
+ adapter had internal buffering that was abused in some places.
+ - The bluetooth sources were rewritten using a ringbuffer to make them
+ more reliable to jitter and remove old audioconvert behaviour.
+ - Many improvements to the audio converter.
+ - Native DSD128 and up is now supported by pw-dsdplay.
+
+
+## tools
+ - Support DSD128 to DSD512 as well by scaling the amount of samples
+ to read per time slice.
+
+## SPA
+ - Format conversion is now generated with macros to remove duplication
+ of code.
+ - 24bits conversions were rewritten to use the generic conversion
+ functions.
+ - Temporary buffers in audioconvert are now made large enough in all
+ cases.
+ - Fix draining in audioconvert. This fixes speaker-test.
+ - Fix the channel remapping. (#2502, #2490)
+ - Audio conversion constants were tweaked to handle the maximum ranges
+ and provide lossless conversion between 24bits and floats.
+ - Vector code and C code are aligned and the unit tests are activated
+ again. A new lossless conversion test was added.
+ - Fix an underrun case where the adapter would not ask for more data.
+ - Fix PROP_INFO for audioconvert. (#2488)
+ - Use the blackman window again for the resampler, the cosh window has
+ some bugs that can cause distortion in some cases. (#2483)
+ - Add more unit tests for audioconvert. Add end-to-end conversion tests.
+ - Don't leak memory in format converter.
+
+## pulse-server
+ - Card properties are now also added to sinks and sources, just like
+ in pulseaudio.
+ - Increase the maxlength size to at least 4 times the fragsize to avoid
+ xruns.
+ - Fix a race when setting default devices.
+
+## Bluetooth
+ - The source was rewritten to use a ringbuffer. This avoids regressions
+ caused by audioconvert.
+
+# PipeWire 0.3.53 (2022-06-30)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - The 44.1KHz samplerate was removed again from the defaults, it caused
+ all kinds of problems with various hardware.
+ - The ALSA plugin should now be able to deal with unsupported samplerates
+ and fall back to the nearest supported one.
+ - The rlimits performance tuning wiki page was updated. Please check
+ you limits.conf file, the version on the wiki used to give all
+ processes a -19 nice level instead of just the pipewire daemon.
+ - The audioconvert plugin was rewritten to be more maintainable and
+ faster. It also gained support for control ports and dithering with
+ optional noise shaping.
+ - An impossible buffering situation is avoided in pulse-server that would
+ cause some applications (sunshine, ...) to stutter.
+
+
+## PipeWire
+ - 44.1KHz was removed from the allowed rates again. It caused all kinds
+ of regressions due to driver bugs and timing issues on HDMI.
+
+## modules
+ - filter-chain now does some more error checking and reporting to
+ avoid some crashes.
+ - filter-chain now supports more channel layouts for input and output
+ that does not need to match the plugin layout.
+ - Format parsing is now more consistent in the modules.
+
+## Tools
+ - pw-cli can now also work without readline support.
+ - pw-cat can now also read multichannel ulaw/alaw/u8/s8.
+
+## SPA
+ - The audioconvert plugin was rewritten. This should make it more
+ maintainable. It also fixed some issues such as CPU spikes in some
+ cases and crashes in others. The old plugins were removed, for a
+ code reduction of some 6000 lines.
+ - The audioconvert plugin now supports control ports, which can be
+ enabled on nodes in the session manager. This makes it possible to
+ control audioconvert properties using timed events or midi.
+ - NoteOn 0-velocity MIDI events are no longer filtered out. This is
+ a valid event, nodes that can't deal with it should fix it up
+ themselves. The JACK layer still filters out these events by default
+ but this can now be configured with a per-client property.
+ - The running status on midi events is now disabled to match what
+ JACK does.
+ - The ALSA plugin will now deal with driver bugs when a driver announces
+ support for a samplerate but then refuses to use it later.
+ - The ALSA plugin has been optimized a little for sample IO.
+ - V4L2 now doesn't error when there are no controls.
+ - Error handling was improved in the audio converter.
+ - The audioconvert plugin now supports rectangular dithering and
+ noise shaping.
+ - The audioconvert plugin can now insert additional inaudible noise
+ that can be used to keep some amplifiers alive. (#705)
+ - The audioconvert format conversion was changed so that it now produces
+ the full 32 bits range in the C fallback conversion code as well.
+ - The resampler window function was changed to a cosh() window
+ function. (#2483)
+ - Vendor and device id are now in hex.
+
+## pulse-server
+ - Tweak the record buffer attributes some more and make sure we don't
+ end up in impossible buffering situations. Fixes an issue with
+ distorted sound in sunshine. (#2447)
+ - Fix a potential crash when updating the client property list.
+ - Some properties on cards were aligned with pulseaudio.
+
+## Wiki
+ - Change "priority" to "nice" in the example limits.conf file. It was
+ giving a -19 nice level to all processes, not just the pipewire
+ daemon.
+
+# PipeWire 0.3.52 (2022-06-09)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Add 44.1KHz to allowed samplerates. The server can now switch by
+ default between 48KHz and 44.1KHz.
+ - Streams now allocate less resources.
+ - Fix some bugs that could make the server crash.
+ - Bluetooth now supports the LC3plus vendor codec.
+ - Many bugfixes and improvements.
+
+
+## PipeWire
+ - Add 44.1KHz to allowed samplerates.
+ - Avoid setting the locale.
+ - Avoid use-after-free when destroying a node from spa-node-factory.
+ - Avoid using reallocarray when not available.
+ - Set port alias is not otherwise set.
+
+## Modules
+ - Improve filter-chain parsing and error reporting. Handle empty
+ nodes. (#1950)
+ - Handle destroy of globals and factory in most modules. (#565)
+ - Add refcounts to client and resources to handle destroy of the
+ protocol. (#565)
+ - Handle global node.name in filter-chain and loopback again, use
+ it to construct unique stream names.
+ - Avoid a wrapped pw-node in the adapter. This reduces resources
+ allocated for streams.
+ - Fix a crash when module-x11-bell was unloaded. (#2392)
+ - Add a new module-pipe-tunnel that can write/read data from a
+ UNIX pipe.
+
+## Tools
+ - Fix DSD playback again in pw-cat.
+ - Add -n option to pw-loopback to set node names.
+ - Add -P option to pw-cat to pass properties to the stream.
+ - Support stdin/stdout in pw-cat. (#2387)
+ - pw-dump now also dumps object removal when monitoring. (#2426)
+
+## SPA
+ - Avoid duplicate param results in pw-dump for ports.
+ - Avoid endless loops in audioconvert for badly behaving client.
+ (#2359)
+ - Scale max-error in alsa based on quantum and avoid logging a warning
+ when starting.
+ - Improve debug of failed format conversion. (#2383)
+ - Handle offset in the audio dsp mixer inputs and clamp to the max
+ buffer size.
+ - Add option to disable locale support for JSON number conversion.
+ - Add support for Astro A20 Gen2.
+ - Fix some of the test sources, the flags were not set correctly.
+ - Add camera location as property in libcamera and let the session manager
+ Generate a localized description.
+ - Fix some crashes due to wrong vargar types in v4l2 controls. (#2400)
+ - Improve ALSA resync behaviour. (#2257)
+ - Add support for Komplete Audio 6 MK2.
+ - Improve loop cancel while iterating.
+ - Try not to mix surround channels and AUX channels. Make card with many
+ ports look better when not using the Pro Audio profile.
+ - Vulkan filters were added.
+
+## Bluetooth
+ - Add LC3plus vendor codec.
+ - Handle unsupported indicators better.
+ - Ensure multiple devices on an adapter use different codecs because one
+ endpoint can only be used by one device at a time.
+ - Fix bitpool control as a follower.
+ - Handle bluetooth errors better.
+ - Speed up bluetooth connection by only waiting for the profiles
+ supported by the adapter.
+ - The dummy AVRCP player is disabled by default because it seems to break
+ more devices than it fixes.
+
+## pulse-server
+ - Add initial stream latency property so that devices can be started
+ with a resonably accurate latency.
+ - Fix ringbuffer underrun case. (#2366)
+ - module-native-protocol-tcp now has a auth-anonymous option to give
+ full access to the clients.
+ - Report a node as being moved when it is still moving. This improves
+ compatibility with pasystray.
+ - Avoid overallocating message memory.
+ - Don't export NETWORK nodes in zeroconf. (#2384)
+ - Fix stride for TrueHD and DTSHD passthrough. (#2284)
+ - Make sure we don't send too small audio fragments. Fixes capture
+ from multiple tabs in Chrome. (#2418)
+ - Rework module handling some more.
+ - Use the new native module-pipe-tunnel for pipe-sink and pipe-source.
+ - Implement the STREAM_MOVED message when a stream got moved. (#2407)
+ - Fix a potential segfault when stopping the server and a TCP module
+ as still loaded.
+
+## ALSA
+ - Add support for updating sw_params at runtime, mostly the min-avail
+ param.
+ - Capture and playback nodes are now assumed to use a different clock and
+ will activate the adaptive resampler when linked. This assumption is
+ removed in Pro Audio mode. This provide a better experience out of the
+ box with most devices.
+
+## JACK
+ - Fix setting properties with PIPEWIRE_PROPS again.
+ - Don't use 64 bits atomic operations for sync_timeout. (#1867)
+ - Cleanup in error cases was improved, avoiding some crashes. (#2394)
+
+## GStreamer
+ - Fix pipewiresink in mode=provide. (#1980)
+ - Share memory into a new buffer in pipewiresrc to avoid buffer corruption.
+ - Fixes to the source and fd use.
+ - It is now possible to set client properties as well. (#1573)
+
+# PipeWire 0.3.51 (2022-04-28)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Improved graph reconfiguration.
+ - Extra configuration options for streams and filters with config
+ rules and environment variable.
+ - Improve module-pulse-tunnel latency, stability and error recovery.
+ - pw-top, pw-cli and pw-link improvements.
+ - Fix a channelmixer upmixing clipping issue.
+ - The ROC module has seen many improvements.
+ - Many more bugfixes and improvements.
+
+
+## PipeWire
+ - The graph reconfiguration code was reworked:
+ * Moved nodes will update the new driver quantum correctly. (#2293)
+ * Inactive nodes are ignored more.
+ * Nodes that require a driver are now not scheduled anymore when
+ they are passive (unused). (#2309)
+ * Improved performance, the graph is reconfigured with a minimal
+ amount of changes.
+ - Method and event argument names were improved.
+ - A linker garbage collection problem was fixed. (#2292)
+ - Properties on threads are now implemented. Use common code to
+ set thread name and add an option to set stack-size.
+ - Streams and filters always want a driver now. This makes it possible
+ to just link a playback stream to a capture stream without a driver
+ and have it work. (#1761).
+ - Streams and filters can now also have rules in the config file.
+ - Streams, filters, JACK, ALSA and v4l2 now support PIPEWIRE_PROPS
+ environment variable to override node properties.
+ - Add config section extensions. This provides a way for modules to
+ have specific config to override the default config.
+ - Handle realloc errors better.
+ - Improve stream and filter property updates.
+
+## Modules
+ - The pulse-tunnel modules has improved latency management and should
+ now work a lot better. (#2230)
+ - Module-loopback, module-echo-cancel, module-filter-chain unload the
+ module when a stream is destroyed. (#1754)
+ - Biquads in filter-chain now can have more gain (5->20 dB).
+ - Documentation updates. Most Wiki content was moved to the source code
+ inline comments.
+ - Filter-chain now has a builtin delay line filter. (#2320)
+ - Filter-chain can now parse the config key correctly in all cases.
+ - The ROC sink and source saw many improvements. roc-source is now a stream
+ by default that connects to the default sink. Both modules will try to set
+ a graph rate. Both modules have an option to select the FEC mode.
+ The ROC source has lower latency now. (#2331)
+ - Handle realloc errors better.
+
+## tools
+ - pw-cat does not have --list-targets anymore, use one of the more
+ advanced and less buggy tools such as wpctl or pw-cli to list
+ sinks and sources.
+ - pw-top has seen many improvements.
+ * It now has some timeouts to reset the node values to their default
+ state when unused.
+ * The man page was improved.
+ * Invalid timings and errors are displayed in a better way.
+ - pw-cli and pw-link can now create links between all ports of given nodes.
+ - pw-cat can now save to other file formats than wav, based on the extension
+ of the filename.
+
+## SPA
+ - The resampler now uses a different internal method for draining. It can
+ now also handle 0 size buffers as input without draining.
+ - The channelmixer now uses the front channel averages for FC and LFE.
+ This avoids clipping and results in much better upmixing.
+ - ALSA should now work again on 32 bits. (#2271)
+ - The JSON parser now converts escaped unicode correctly to UTF8.
+
+## bluetooth
+ - Codec switch improvements when the device is disconnected. (#2334)
+
+## pulse-server
+ - There is a new module-roc-sink-input module, the the PulseAudio equivalent.
+ - The ROC source and sink-input module now have a much lower latency.
+ - The ROC module now has an option to select FEC mode.
+ - Playback and record rate adjustements should work now. (#1159)
+
+## JACK
+ - Remove some useless pthread attributes. This makes JACK work in QEMU with
+ sandboxing enabled. (#2297)
+ - The buffer_size callback is now only called when something has changed
+ since the last process() callback or get_buffer_size() method. This
+ fixes a GStreamer issue and is more in line with what JACK does. (#2324)
+ - Fix a potential deadlock when the process thread is doing IPC and the
+ IPC thread is blocking on the data thread.
+ - Allocation errors in metadata are handled better.
+
+# PipeWire 0.3.50 (2022-04-13)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - pw-stream can now report more timing information and can suggest
+ the optimal number of samples to queue for playback.
+ - pw-dot now works again..
+ - module-pulse-tunnel latency was improved.
+ - WINE applications using the JACK backend should no longer crash.
+ - The channelmixer defaults are improved and the muffled sound when
+ playing back 5.1 and 7.1 material has been fixed.
+ - Many fixes and improvements.
+
+
+## PipeWire
+ - pw-stream now places a suggested amount of samples in the pw-buffer
+ for playback. This allows you to remove some places where
+ spa_io_rate_match was needed to get this information.
+ - pw-stream has new API to request a timing update. New fields are
+ added in the timing info, such as number of buffered samples in
+ the resampler and the number of queued and dequeued buffers.
+ - pw-stream has support for double controls now. More controls are
+ exposed such as the Rate control to do adaptive resampling.
+ - The thread-utils object was moved to the context to avoid some
+ concurrent use cases that caused crashes. (#2252)
+ - Deactivating an exported node/stream will now remove the node from
+ the data-thread immediately so that the process function will not
+ be called anymore and resources can be safely freed. This could
+ fix some of the last remaining crashes when streams are stopped.
+ - PipeWire will now fail to load a module that tries to register
+ the same export type twice instead of silently doing the wrong
+ thing. (#2281)
+
+## Modules
+ - Many modules now use the NODE_WANT_DRIVER instead of the
+ pipewire.dummy NODE_GROUP property. This makes it possible to use
+ them with any other driver and can avoid resampling in some
+ cases.
+ - module-pulse-tunnel now uses an adaptive resampler to keep the
+ latency under control. Latency should be much better than before
+ and stay constant even when there are network delays.
+ - There is now an option for packages to disable building the RTKit
+ module, which is still built by default for backwards compatibility
+ reasons.
+ - A leak was fixed in filter-chain. (#2220)
+ - Module node names are now made more unique with the pid.
+
+## tools
+ - pw-cat verbose output has been improved.
+ - pw-link now has a man page. (#2263)
+ - pw-reserve now has an -r option to make it issue a RequestRelease
+ command on the owner of the device. This makes it possible to ask
+ WirePlumber to close and release a device.
+ - Fix pw-dot again. It didn't work anymore because of stray done
+ events that were emitted to notify the client of object serials.
+ (#2253)
+
+## SPA
+ - The channelmixer now has PSD upmixing enabled again. We used the
+ simple upmixing in the previous release but that just sounds too
+ aweful to be a good default. (#861) and (#2219)
+ - The channelmixer will not upmix FC and LFE anymore when upmixing is
+ explicitly disabled. (#2266)
+ - The channelmixer will only lowpass filter FC and LFE channels when
+ they were upmixed. (#2280)
+ - The defaults of the channelmixer were tweaked a little. There is now
+ a little bit more bass in the LFE channel and more high fequencies
+ in the FC channel when upmixing. Also the channel widening has been
+ disabled by default.
+ - Locale independent float parsing now uses a more portable solution
+ with uselocale.
+ - ALSA will now only allocate a buffer size big enough to hold 4
+ times the quantum limit instead of as large as possible.
+
+## pulse-server
+ - Internal cleanups in handling of modules.
+ - A quirk to force s16 sample formats for teams-insider has been added.
+
+## JACK
+ - The data-loop is now started in activate and stopped in deactivate.
+ This makes the data-loop respect any custom thread functions you
+ configure. This also makes WINE apps using the JACK backend work.
+ (#1495).
+ - Port sorting was improved/fixed. (#2260)
+
+# PipeWire 0.3.49 (2022-03-29)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Sample rate switching should work again.
+ - pw-dot can now use the output of pw-dump to render a graph.
+ - Bluetooth A2DP streaming was improved that would reduce stuttering on
+ some devices.
+ - A JACK bug was fixed that would sometimes make it impossible to add more
+ tracks in Ardour. (#1714)
+ - Many bugfixes and improvements.
+
+## PipeWire
+ - Fix a potential crash when NULL params were configured.
+ - Add some simple functional tests to avoid some recent regressions. Improve
+ the test framework for this as well.
+ - Improvements to the poll loop to avoid some use-after-free scenarios.
+ - Fix samplerate switching again.
+ - setlocale is not called anymore from the pipewire library. This should be
+ called by the application. (#2223)
+ - pw_init() and pw_deinit() can now be nested and called multiple times.
+ - pw_stream will now report the resampler delay in the pw_time.queued field.
+
+## modules
+ - module-filter-chain now supports arbitrary many properties and will use
+ property hints to assign them the right type.
+ - The ROC modules now accept a sink/source_properties parameter.
+ - The module-rt can now also be built without RT-Kit support.
+ - module-echo-cancel can now use a fraction to specify the delay for more
+ precise control.
+
+## SPA
+ - The channelmixer will now do upmixing by default and will not use
+ normalization. It will also use a simple upmixing algorithm that duplicates
+ channels by default. A more interesting upmix method is also available (PSD)
+ but needs to be enabled manually. (#861)
+ - Add SSE optimized (de)interleave functions for 32 bits samples with and
+ without byteswap.
+ - JSON parsing of empty strings will now give an invalid number instead of
+ 0.
+ - JSON numbers are now parsed and serialized in a locale independent way so
+ that , and . are not mixed up.
+ - The resampler will now report the resample delay and queued samples as the
+ extra delay.
+
+## tools
+ - pw-cat will read more dsf files correctly and will not crash at the end.
+ - pw-top now has a man page.
+ - pw-dot can now use the output of pw-dump to render a graph.
+
+## bluetooth
+ - Improve interactions with oFono.
+ - Fix recovery with slow connections.
+ - Improve frame size of AptX-ll.
+ - A2DP can now use any quantum and will flush packets in smaller chunks
+ when needed to adapt. This improves stuttering in some cases.
+
+## pulse-server
+ - The server configuration can now be placed in pulse.properties section,
+ which also makes it possible to have custom overrides.
+ - Implement FIX_ flags for capture as well.
+ - Small fixes and improvements in module loading.
+
+## JACK
+ - Clear the last error before executing a new action or else we could end up
+ with error from a previous action. This causes some problems in Ardour where
+ adding a track would fail after some time. (#1714)
+
+
+# PipeWire 0.3.48 (2022-03-03)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Fix IEC958 passthrough again.
+ - Fix pulse-server crashes when playing a sample.
+ - Support for more a more advanced upmixing algorithm.
+ - filter-chain now supports arbitrary many ports.
+ - Fix multichannel support in WINE. (with new WirePlumber).
+ - Many bugfixes and improvements.
+
+
+## PipeWire
+ - The work queue is now created in the context so we can fail early and
+ avoid further error checking in various places.
+ - Fix a potential use after free with threaded loops.
+ - The protocol now has a message footer. This is used to pass around
+ global state such as the last registered object serial number. This can
+ be used to detect when a client tries to bind to old (but reused)
+ object ids. This avoids some races in the session manager but also
+ when binding objects.
+ - The zero-denormals CPU flag is now not touched anymore unless explicitly
+ selected by the user. Denormals are avoided in filter-chain now in
+ software. If the zero-denormals are now only configured in the data
+ thread. This should fix issues with luajit. (#2160)
+ - Configuration parsing will not actually fail on errors.
+ - pw-top now correctly clips unicode characters.
+ - Many places now use a dynamic POD builder to support arbitrary large
+ property sets.
+ - pw-stream now support PropInfo parameters so that they can announce
+ custom properties.
+ - Serial number are now also set on metadata and session-manager objects.
+
+## SPA
+ - audioadapter is now smarter when trying to fixate the format. It will
+ use the PortConfig format to fill in any wildcards. This results in
+ the least amount of conversions when the stream can handle it. It also
+ is part of a fix (also requires a session manager fix) for WINE
+ multichannel support. (#876).
+ - Fix 5.1 to 2 channels mixing. It was using the volume of the stereo
+ pair on all channels.
+ - Fix some weird volume issues when a source is capturing and
+ channelmixing.
+ - Add stereo to 7.1 upmixing.
+ - The channelmix parameters can be changed at runtime now.
+ - Many improvements to the upmixing algorithms. Rear channels are now
+ constructed from the ambient sound and can have delay and phase shift
+ applied to them to improve spacialization. The stereo channels can
+ be filtered so that the dialog is more concentrated in the center
+ channel. (#861)
+
+## modules
+ - Module X11 bell received cleanups and improvements.
+ - The module now has a private method to schedule unload later. This
+ simplifies cleanup in many modules.
+ - module-filter-chain now handles arbitrary many ports and control
+ ports. (#2179)
+ - Fix a bug in RAOP where it was reading from the wrong port. (#2183)
+
+## pulse-server
+ - Nodes with the DONT_MOVE property should fail with -EINVAL when
+ they are moved.
+ - Fix a segfault when playing a sample. (#2151)
+ - The _FIX flags in pulse-server also now ignore the configured
+ sample format, just like pulseaudio does. (#876)
+ - Fix IEC958 passthrough again. It got accidentally broken since
+ 0.3.45 with a fix for another issue. (#1442)
+ - Fix module-null-sink device.description. (#2166)
+
+## Bluetooth
+ - Don't try to connect HSP/HFP when no backend is available.
+
+
+# PipeWire 0.3.47 (2022-02-18)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+This is a quick emergency release to fix some severe
+problems with the previous release.
+
+## Highlights
+ - Fixes a bug in pulse-server that caused cached notifications
+ to play multiple times. (#2142)
+ - Removed check and warnings to catch leaked listeners on the
+ proxy. This might access invalid memory and cause infinite
+ loops in older wireplumber.
+
+# PipeWire 0.3.46 (2022-02-17)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Fix a critical bug in pipewire-pulse buffer size handling that made some
+ apps (MuseScore, ... ) stutter.
+ - Fix a critical bug where devices would not show when the kernel was
+ compiled without VERBOSE_PROCSFS.
+ - JACK clients will now use lock-quantum by default. This makes sure that
+ all dynamic quantum changes are disabled while a JACK app is running.
+ The only way to force a quantum chance is through a JACK app or with
+ the metadata.
+ - Almost all limits on number of ports, clients and nodes are removed.
+ - A Dummy fallback sink is now automatically created when there are no
+ other sinks. This avoids stalling browsers.
+ - Sound sharing with Zoom should work better. A new WirePlumber release
+ might be required.
+ - Many more fixes and improvements.
+
+
+## PipeWire
+ - Update docs with new config overrides.
+ - The rule matching logic was moved to config and code is now shared with
+ pulse-server and JACK.
+ - Add new Romanian translation.
+ - When a quantum is forced with metadata, any node that asked to lock-quantum
+ is ignored so that the quantum change can happen.
+ - Fix a bug where a mixer was removed twice, leading to potential memory
+ corruption.
+ - The port limits on nodes and filters are now removed. Some code was
+ simplified.
+ - Fix a potential leak because listeners where removed while they could be
+ emitted.
+ - Improve context.exec and avoid zombie processes.
+
+## Modules
+ - The RAOP module now has a default latency of 2 seconds, like PulseAudio.
+ - The echo-cancel module now uses the plugin loader to load the backends.
+ This makes it possible to add custom, out of tree, echo cancel plugins.
+
+## Tools
+ - Improve help of pw-link.
+ - Output to stdout and error to stderr. Use setlinebuf for stdout to improve
+ piping between apps. (#2110)
+
+## SPA
+ - Improve removing sources when dispatching. Also improve performance now
+ that a destroy loop can be removed. (#2114)
+ - Fix an fd leak in the logger when logging to a file.
+ - Improve loop enter/leave checks and support recursive loops.
+
+## pulse-server
+ - Clamp various buffer attributes to the max length. Fixes some issues
+ with various applications. (#2100)
+ - Module properties are now remapped correctly from their pulseaudio variant
+ to the PipeWire ones.
+ - Fix module index in introspect. Use the right index when loaded from our
+ internal modules. (#2101)
+ - Improve argument parsing and node.description. (#2086)
+ - The sink-index should now be filled in correctly when playing a sample.
+ (#2129)
+ - module-always-sink is now implemented and loaded by default. (#1838)
+ - Add support for loading some modules only once.
+ - Module load and unload now does extra sync to make it appear synchronous,
+ like in PulseAudio. This improves sounds sharing in Zoom.
+
+## ALSA
+ - Fix critial bug where alsa devices would not show when the kernel was
+ compiled without VERBOSE_PROCFS.
+ - Some corner cases were fixed in the ALSA timing code. When the capture node
+ is follower, it will now not try to read too much data and xrun but it will
+ instead produce a cycle of silence.
+ - Various fixes and improvements to make ALSA devices resync to the driver
+ more quickly and accurately.
+
+## JACK
+ - Add an option to name the defauld device as `system` to improve
+ compatibility with some applications,
+ - Use lock-quantum by default. This makes sure that all dynamic quantum
+ changes are disabled while a JACK app is running. The only way to force
+ a quantum chance is through a JACK app or with the metadata.
+ - It is now possible to do IPC calls from the data thread. Note that this
+ is a very bad idea but required for compatibility with JACK2.
+
+## GStreamer
+ - GStreamer sink will now set a default channelmap to make it possible to
+ remap to the channel layout of the device.
+
+# PipeWire 0.3.45 (2022-02-03)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Zoom, telegram and other apps should be able to play sound again.
+ - Implement a better way to force and lock JACK buffersize.
+ - Default sink and source names and properties are improved.
+ - The config loader can now load and merge fragments in conf.d directories
+ for easier user configuration of config files.
+ - Many small bug fixes and improvements.
+
+## PipeWire
+ - pw-cli can now also send Commands to nodes. This can be used to Suspend
+ a device, for example.
+ - The eventfd was removed from loops and invoke is now used to stop the loop,
+ this saves an fd.
+ - New Alpine CI target to test musl builds, various build fixes.
+ - Add force-quantum and force-rate properties.
+ - The config loader can now load and merge fragments in conf.d directories.
+ (#207)
+ - resource error methods can be called without a resource and then just
+ log an error message.
+ - link-factory can now also work from the config. (#2095)
+
+## modules
+ - module-simple-protocol has better argument parsing and can handle
+ channelmap now. (#2068) It's also possible to configure latency and
+ rate.
+ - The native protocol now does extra checks for invalid data. (#2070)
+
+## ALSA
+ - TI2902 chips as found in various Behringer cards should have inputs
+ again.
+ - Better handling of busy devices in udev, retry when the inotify close
+ event is emited.
+
+## SPA
+ - plugins now handle alignment properly and only expect the max alignment
+ required for the CPU. (#2074)
+
+## Bluetooth
+ - SBC-XQ is now enabled for the JBL Endurance RUN BT headset.
+ - Support for non-hexadecimal XAPL version strings to improve compatibility.
+ - Use HCI commands again to probe the adapter msbc capability. This improves
+ compatibility with some adapters. (#2030)
+ - Set the right startup volume.
+ - Better A2DP source idle handling.
+ - Fix a timer bug in SCO sink that could cause busy looping.
+
+## pulse-server
+ - A playback issue when the tlength > maxlength was fixed. (#2069) This
+ affected Zoom and other applications.
+ - The STREAM_BUFFER_ATTR command is now implemented.
+ - Module names are improved. (#2076)
+ - Many small fixes and improvements.
+ - Fix a pavucontrol crash with invalid channels. (#1442)
+
+## JACK
+ - Use the new force-quantum and force-rate properties in the JACK API to
+ switch quantum and ensure it can't change for the lifetime of the JACK
+ app. (#2079)
+
+# PipeWire 0.3.44 (2022-01-27)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - It is now possible to run a minimal PipeWire server without a session
+ manager, enough to run JACK clients.
+ - The maximum buffer size is now configurable and can be larger than
+ the previously hardcoded limit of 8192 samples. When using high sample
+ rates, the larger buffer size can avoid xruns.
+ - The default maximum latency was reduced from 170ms to 42ms. This should
+ improve overall latency for application that ask for a large latency,
+ such as notifications.
+ - Better JACK compatibility. Patchbays should now get less confused about
+ ports appearing and disappearing.
+ - Fix some bluetooth crashes.
+ - Fix some races in ALSA device detection.
+ - Many bug fixes and improvements all over the place.
+
+## PipeWire
+ - Bump the meson requirement to 0.59.0.
+ - pw-top now reports correct times for filter-chain and loopback.
+ - max-quantum is now also scaled with the rate. A new quantum-limit
+ property was added as a hard limit for the quantum. This makes it
+ possible to configure for larger than 8192 buffer sizes. Note
+ than many JACK applications have a hardcoded 8192 limit. (#1931)
+ - The max-quantum was reduced to 2048, This gives a 42ms default
+ latency. (#1908)
+ - pw-filter can now return a NULL buffer from _get_dsp_buffer().
+ - Add a PIPEWIRE_RATE and PIPEWIRE_QUANTUM env variable to set the
+ graph rate and the graph quantum and rate respectively.
+ - Fix a potential file descriptor leak in the connection.
+ - A new minimal.conf file was added to demonstrate a static setup
+ of a daemon that doesn't require a session manager and is able to
+ run JACK applicaions.
+ - Nice levels are now only changed on the servers, not the clients.
+ - Add an option to suspend nodes when idle.
+ - Make it possible to avoid quantum and rate changes with
+ pw-metadata. This is essential in a locked down system.
+ - Handle mixer port errors better and fail to create the link instead
+ of silently not working.
+ - Nodes that are moved to a driver now have all the linked nodes moved
+ as well. This makes it possible to run some graphs without a
+ driver, such as paplay -> zita-j2n.
+ - pw-cli and pw-dump can now also list objects by name, serial and
+ object.path using glob style pattern matching.
+
+
+## modules
+ - filter-chain can now also configure parameters by index.
+ - Fix the client name of module-protocol-simple. (#2017)
+ - module-rtkit was merged into module-rt. This makes it easier to
+ ship a default config that works on more systems by default.
+ - module-adapter can now configure the adapter node from the config.
+ Previously, this was a task only performed by the session manager.
+ - module-metadata can now also create metadata object from the
+ config file.
+ - The ROC module should now work again. (#2045)
+ - An X11-bell module was added to handle X11 bell events. (#1668)
+ - filter-chain and loopback modules now have better unique default
+ names for the streams, which makes it possible to save and restore
+ their volumes independently. (#1983)
+ - module-echo-cancel now has properties to control the delay and
+ buffer size.
+
+## ALSA
+ - The monitor names are now correctly parsed.
+ - The default period size for batch devices is limited now to avoid
+ large latency.
+ - The unused min/max-latency properties were removed.
+ - Internal latency is now also configurable with params at runtime.
+ - The udev rule for TI2902 was removed because it causes problems.
+ - Fix a race where some devices would sometimes be missing. (#2046)
+ - Add some more timeouts to work around a race in udev device
+ permission changes when switching VTs.
+
+## SPA
+ - Fix potential infinite loop in audioconvert.
+ - The spa-resample tools can now also use optimized implementations.
+ - Fix a potential crash in resampler. (#1994)
+ - audioconvert can now also handle F64 formats. (#1990)
+ - The channelmixer now does normalization by default to avoid clipping
+ when downmixing is active.
+ - The channelmixer will now generate LFE channels when the lfe_cutoff
+ frequency is set, even when upmix is disabled.
+ - The channelmixer will now always generate FC when the target has it.
+ - Adapter now reports latency correctly, even after linking the monitor
+ ports.
+ - Reduce memory usage and preallocated memory in some of the
+ audioconvert nodes.
+ - Many properties are now exposed in adapter, such as the resample
+ quality.
+ - The resampler and channelmixer can now be disabled.
+
+## V4L2
+ - pw-v4l2 now also works for ffplay. (#2029)
+ - Take product names from udev now that the kernel returns a generic
+ name.
+
+## JACK
+ - The jack pkgconfig file now has the `jack_implementation=pipewire`
+ variable to be able to distinguish jack implementations. (#1666)
+ - jconvolver now starts correctly again. (#1989)
+ - The object.serial is now used for the port_id. This makes it easier
+ to track old objects in the cache.
+ - Add a dummy jacknet implementation. (#2043)
+ - A bug in the port allocation was fixed that would make it impossible
+ to allocate ports at some point. (#1714)
+
+## Bluetooth
+ - Bluetooth profiles are now saved properly by the session manager.
+ - Improved profile detections, increased timeouts for slow devices.
+ - Implement HFP call indicator for improved compatibility.
+ - Handle the case where bluez does not set the adapter or address
+ properties on the device instead of crashing.
+ - Improved support for setting the profile from the session manager.
+
+## pulse-server
+ - Monitor sources now have the device.class=monitor for better
+ compatibility.
+ - Behaviour after seeking is improved. The algorithm for requesting
+ bytes from the client was simplified and improved. (#1981)
+ - module-ladspa-sink implements the control argument now. (#1987)
+ - A potential memory leak in the message queue was fixed. (#1840)
+ - Use the object.serial for the pulseaudio object index. The index is
+ not supposed to be reused and this would cause problems with some
+ clients.
+ - Servers should now again be able to listen in IPv4. (#2047)
+ - module-x11-bell was added. (#1668)
+ - There is now support for per-application quirks and properties in
+ the pipewire-pulse.conf file. Per-application latency and buffering
+ properties can also be configured.
+ - Fix a regression in telegram sounds not playing.
+
+# PipeWire 0.3.43 (2022-01-05)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Flatpak apps such as Ardour can now remove links again.
+ - Many fixes to pulse-server. Memory usage should be improved. Some
+ crashes are fixed. Underrun handling should work better. Better
+ compatibility with GStreamer based applications after seeking.
+ - Many of the samplerate and quantum changes bugs in previous releases
+ were fixed. This fixes some issues where the microphone would fail
+ to work.
+ - Many more small fixes and improvements all over the place.
+
+## PipeWire
+ - Quantum and rate changes are now applied immediately when the driver
+ is idle. This avoids setting the driver in the previous samplerate.
+ (#1913)
+ - Object destruction now does not need write permissions anymore. This
+ restriction needs some more work. (#1920)
+ - When we reposition, start a sync operation. This fixes a problem
+ in Ardour when seeking. (#1907)
+ - Require meson 0.56.0
+ - Make the align property in BUFFER_PARAM optional. We now only set this
+ if the plugin has specific requirements and we default to the CPU
+ largest alignment requirement.
+ - pw-record will now also list monitors and streams as possible targets.
+
+## modules
+ - Improve LV2 plugin support in filter-chain. Add support for Worker and
+ Options.
+ - The loopback module now has a unique media.name to make it possible
+ for the session manager to restore unique volume settings.
+
+## SPA
+ - Improve sample rate for EAC3 streams, some clients scale it while
+ others don't so use some heuristics to make things work better.
+ (#1902)
+ - Allocate ports dynamically in audioconvert. This avoids using larger
+ memory blocks of preallocated memory that confuses the allocator.
+ (#1840)
+
+## ALSA
+ - Merge the latest pulseaudio UCM improvements. (#1849)
+ - Fixes for selecting the sample rate. (#1892)
+ - Improve latency on USB devices by scaling the period size based on
+ the desired quantum when the device is opened.
+ - Add api.alsa.period-num to configure the amount of periods to use
+ in the device. Some devices have lower latency when a small value
+ is forced. (#1473)
+ - Allow multi-rate by default. In previous versions cards could only
+ be opened in one samplerate to avoid bugs in pre 5.16 kernels. This
+ however caused other problems so the default was removed.
+ - Fix a bug where a card was not freed correctly.
+ - Fix a bug in the alsa boundary check that could hang the alsa-plugin
+ for a long time.
+ - The ALSA plugin now has a parameter to configure the allowed
+ samplerates.
+
+## JACK
+ - Improve handling of monitor nodes.
+
+## Bluetooth
+ - Codecs now have a priority. This should improve codec selection.
+
+## pulse-server
+ - The stream FIX_ flags are now implemented. (#1912)
+ - Improve flushing and draining behaviour. Short samples will now
+ play correctly. (#1549)
+ - Fix a crash when enumerating the formats. (#1928)
+ - Track quantum changes and update the amount of required buffering
+ accordingly. This improves forced quantum handling. (#1930)
+ - Improve handling of channels > 32.
+ - Handle the case where a module is destroyed before it could be
+ completely loaded.
+ - Fixes some issues when samples were removed while they were playing.
+ (#1953)
+ - Fix some issues in module-zeroconf-publish.
+ - Fix some memory leaks in module-roc.
+ - Add command access control. This avoids execution of commands without
+ proper authentication.
+
+# PipeWire 0.3.42 (2021-12-16)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+This is a quick emergency release to fix some severe
+problems with the previous release.
+
+## Highlights
+ - Fixes a bug in pulse-server underrun handling that broke qemu
+ and orca.
+ - A fix was added to pulse-server to handle quantum changes
+ gracefully.
+ - Fix module-echo-cancel again.
+ - Fix a bug where the bluetooth headset capture was producing
+ noise.
+
+
+# PipeWire 0.3.41 (2021-12-13)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Improved compatibility for flatpaks. Flatpaks with newer PipeWire
+ version can connect to an older server in all cases.
+ - A new RAOP module was added to stream to Apple Airplay devices.
+ - OBS can now capture from the monitor devices again when using
+ WirePlumber.
+ - Improved JACK compatibility. Improved stability in Carla and Ardour
+ when changing buffer size. Improved latency calculations and
+ playback latency in Ardour.
+ - Improved pulse-server handling of underruns and buffer size changes.
+ - Many bugfixes and improvements.
+
+
+## PipeWire
+ - The systemd service files now have better names.
+ - client.access permission checks are improved.
+ - Fix some memory leaks in error paths.
+ - Objects now have a global serial number that is unique for the
+ lifetime of the server.
+ - Make clock.rate, clock.allowed-rates and clock.quantum
+ runtime tunable parameters with the settings metadata.
+ - Add some additional memory checks in client-node to avoid
+ sending invalid memory to clients. (#1859)
+ - Improve buffer memory allocation. If one of the nodes is a
+ remote node, ensure we only use memory that can be shared.
+ - Version checks when binding to objects is removed. This means
+ that newer clients can now bind to older servers, which is
+ a typical case for a flatpak.
+ - A bug in the latency calculations was fixed where it would in
+ some cases report the wrong minumum latency.
+
+## modules
+ - module-echo-cancel has voice-detection enabled now.
+ - module-raop-sink and module-raop-discover to stream audio to
+ an Apple Airplay device.
+ - module-filter-chain now has preliminary support for LV2
+ plugins.
+
+## SPA
+ - The audio resampler now has improved buffer size calculations.
+ In some cases it was too small and would cause distortions.
+ - More checks are done when doing volume changes so that the
+ channelmap is correct.
+ - Audioadapter now exposes most config options with params so that
+ they can be adjusted at runtime.
+ - The resampler can now calculate the expected input buffer size
+ before receiving the first buffer, which avoids some confusion
+ when starting streams.
+ - Support was added for some 10bit video formats.
+ - MONO channel handling was improved.
+ - Most plugins now set a clock name and this is configurable where
+ it makes sense. The clock.system.monotonic clock name is used
+ for most plugins that use the system clock for timing.
+
+## pulse-server
+ - implement module-raop-discover
+ - Use STREAM_CAPTURE_SINK property when capturing from a monitor
+ source to better inform the session manager. This fixes some
+ issues where OBS would capture from the microphone instead of
+ the output monitor.
+ - Limit the amount of cache messages to 16MB and don't add large
+ memory blocks to the cache. This should fix some excessive
+ memory usage that people reported.
+ - Fix a potential memory leak when cleaning up a client.
+ - Do some additional checks to avoid buffer overruns.
+ - Improve recovery from underruns better. (#1857) This improves
+ seeking in gnome-music.
+ - Improve recovery when the quantum is forced larger that the
+ stream configured latency.
+ - The prebuf state is now handled correctly.
+
+## JACK
+ - A per type object cache is now implemented. This ensures that
+ port objects remain valid for a longer time because many
+ JACK applications inspect objects after they are destroyed.
+ This improves catia/carla compatibility.
+ - Recompute the latencies when the buffer-size changes. Fix some
+ cases where we would end up with negative latencies.
+ - Handle regcomp errors to avoid some crashes later.
+ - Latency calculations are improved a lot.
+ - More care is taken to not call a process callback while a buffer
+ size change is pending. This fixes some crashes in Carla, which
+ expect that all clients are paused when one handles the buffersize
+ callback.
+ - Loopback links to a client are now handled correctly and without
+ latency. This fixes playback latency in ardour6 (#1839)
+
+## ALSA
+ - ALSA devices now keep track of the samplerate of the card and
+ ensure that all PCM use the same rate. This is a workaround for
+ a kernel bug that is fixed in 5.16.
+ - Refactor the ALSA plugin a little.
+ - The ALSA plugin now reports correct delay for a capture PCM. (#1697)
+ - The ALSA nodes now expose all config options with params that can be
+ changed at runtime.
+ - The ALSA node has a configurable clock name. Adaptive resampling to
+ match clock rates is avoided when the driver has the same clock
+ name as the ALSA node. This can be used to link alsa devices together
+ with a word clock.
+
+# PipeWire 0.3.40 (2021-11-11)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - Producers and consumers can now incrementally negotiate a format
+ by narrowing down the options. This can be used to select an optimal
+ combination of format and modifiers.
+ - Driver nodes such as the consumer of a headless compositor can now
+ throttle the speed based on a new trigger_done event.
+ - Headless compositors can now signal a damage event to consumers
+ to start the processing of the graph.
+ - Compatibility improvements in JACK.
+ - Draining and resuming is now working correctly in pulse and alsa.
+ - Many bugfixes and improvements.
+
+
+## PipeWire
+ - Many BSD fixes.
+ - clang compilation fixes.
+ - Fix map implementation on big-endian machines.
+ - Improve tracking of param changes in pw-stream.
+ - Add support for renegotiation. With this change, producer and
+ consumer can incrementally renegotiate a format until it is fixed.
+ This will be used to do complex negotiation of DRM modifiers.
+ (#1732).
+ - Add a trigger-done event in the stream. This can be used to know
+ when processing of the complete graph has finished after issuing
+ a trigger_process() and it can be used to throttle processing.
+ - Add a RequestProcess node event and command. This can be used by
+ non-driver nodes to suggest to a driver to start processing. One
+ case is where a compositor can emit this event as a result of a
+ screen update to let the headless compositor start an update.
+ - Fix zeroconf sample format.
+ - pw-mon outputs to stderr now and has colors.
+
+## SPA
+ - Fix compilation on ppc and armv7.
+ - Fix port type check for ALSA seq midi ports so that they are not
+ falsely listed as hardware.
+ - Fix crash when running SSE code on unsupported HW. (#1775)
+ - The libcamera plugin was rewritten. It now supports hotplug,
+ format enumeration and an easier to read codebase.
+ - Fix compatibility some more for cards with 64 channels.
+
+## pulse-server
+ - Flush data in pause in combine-sink to avoid stray audio fragments.
+ - Fix a race where not all objects were removed correctly.
+ - The latency calculations and setup was improved to more closely
+ match pulseaudio behaviour. PULSE_LATENCY_MSEC should now resemble
+ pulseaudio more closely. (#1769)
+ - The drained reply is now sent only once and new data will be
+ accepted once the drain completes.
+ - Fix a potential crasher bug where the stream started processing
+ before the setup was completed.
+ - The server will now drop the client connections when the pipewire
+ connection is lost.
+
+## JACK
+ - Rework the jack_port_get_buffer() method to return the same memory
+ when called multiple times during the process() callback. This makes
+ things work on a new Hydrogen. (#1748)
+ - Add an option to disable showing the monitor ports.
+ - JACK ports are now sorted per node/client and port_id. This should
+ more closely match JACK behaviour and avoid random port order.
+ (#1780)
+
+## v4l2
+ - Fix v4l2 LD_PRELOAD script.
+ - Make sure we destroy the proxy when the global is destroyed.
+
+## ALSA
+ - _prepare should exit the draining state. (#1467)
+ - Fix the precision of the _delay function by taking into account
+ the amount of queued samples are the correct samplerate.
+
+
+# PipeWire 0.3.39 (2021-10-21)
+
+This is a bugfix release that is API and ABI compatible with previous
+0.3.x releases.
+
+## Highlights
+ - media-session is now moved into a separate module to speed up its
+ deprecation in favour of WirePlumber.
+ - There is now an LD_PRELOAD v4l2 emulation library to run some existing
+ v4l2 applications on top of PipeWire.
+ - Filter-chains should now flush out remaining samples when paused. There
+ is now also the option to let a filter-chain drain so that long filters
+ such as reverbs can fade out properly.
+ - Stability and compatibility improvements in JACK apps.
+ - Better Bluetooth compatibility with more devices.
+ - libcamera plugin improvements.
+ - Many bugfixes and improvements all over the map.
+
+
+## PipeWire
+ - Fix compilation on ARM.
+ - Log topics are added to most modules.
+ - Documentation updates. Many improvements to the layout. Reorganisation
+ of the modules and groups.
+ - Share a work queue for all links and nodes. This removes the need for
+ a separate eventfd per link and per node.
+ - Catch errors in the map implementation.
+ - Add option to compile without dbus support.
+ - Fix biquad frequency. It was using the wrong sample rate.
+ - Fix a potential crash when destroying nodes, in some cases the node
+ would not be deactivated properly.
+ - Add some more helpers for dealing with properties and their values.
+ - Implement flush and reset on virtual sinks/sources.
+ - Make it possible to let virtual sinks/filter-chains run and drain
+ after being idle.
+ - Fix a bug where the quantum could exceed the maximum because it was
+ scaled with the sample rate.
+ - Fix channel_map parsing in module-zeroconf-discover so that the remote
+ channel map is used.
+ - pw-stream errors emitted on the proxy are reported but not fatal
+ any more. They are usually used by the session manager to signal status
+ to the client but otherwise does not really cause an error on the
+ client.
+ - Links now also store the output and input node id in the global
+ properties so that applications can parse and use them regardless of
+ how the link was made. (#1723)
+ - pw-stream and pw-filter now have an event to notify commands.
+ - The echo-cancel module can now operate on larger quantums.
+ - pw-cat now uses the right metadata to find the default devices in
+ --list-targets.
+
+## media-session
+ - Don't try to remix unpositioned streams when linking. This ensures
+ that linking to Pro-Audio nodes does not remix the stream channels
+ but links them as they are, one by one.
+ - media-session is now moved to a separate module to accelerate its
+ deprecation in favour of WirePlumber.
+
+## SPA
+ - Many libcamera improvements, handle MemFd buffers, handle errors
+ gracefully.
+ - Small improvements to make interface fall-backs easier to implement.
+ - Add support to enable flush-to-zero and denormals-are-zero to avoid
+ high CPU usage when dealing with denormals.
+ - AUX13 channels are no longer reported as AUX12. (#1727)
+ - Devices with more than 32 channels in Pro-Audio mode now only
+ uses AUX channels.
+ - Improve windowing function of the resampler to reduce aliasing and
+ improve the quality.
+
+## JACK
+ - Port connect callbacks will not only be emitted after the port
+ has negotiated buffers, which improves compatibility with
+ applications that try to use the port right after the callback
+ (jack_midi_latency_test).
+ - Fix crash when midi ports were removed and being monitored, like
+ in Ardour.
+
+## pulse-server
+ - The pulse tunnel will now use the specified format/rate/channels.
+ - Improve lookup of default source and fall back to the monitors when
+ no sources are available.
+ - Mark some nodes as network nodes so that we can set the NETWORK flag
+ correctly.
+
+## GStreamer
+ - The GStreamer element not releases the buffers in the stream again in
+ all cases so that they can be reused by other streams.
+
+## v4l2
+ - Add a v4l2 LD_PRELOAD library to emulate v4l2 system calls on top of
+ PipeWire. This is tested with firefox and GStreamer and is known to
+ not work with Chrome.
+
+## Bluetooth
+ - AAC compatibility improvements.
+ - Disable hardware volume for "Tribit MAXSound Plus" and
+ "SoundCore mini".
+ - Add quirk to disable faststream. Disable faststream on "FiiO BTR3".
+ - Add a dummy AVRCP player to improve compatibility with some devices.
+
+
+# PipeWire 0.3.38 (2021-09-30)
+
+This is a quick bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - Topic based logging was added to improve debugging.
+ - An off-by-one error was fixed in the audio resampler that could
+ cause distortion when downsampling.
+ - Various bluetooth compatibility improvements.
+ - More fixes and improvements.
+
+## PipeWire
+ - module-pulse-tunnel now has better default latency to make it work
+ better in more cases. There is also an option to configure the
+ desired latency.
+ - pw-cli now has readline support.
+ - Topic based logging was added. Log lines can now be filtered by
+ topic using wildcards. This should improve debugging.
+ - The systemd service files should now have better descriptions.
+ - Fix a crash in module-zeroconf-discover when unloading.
+ - Fix a crash in filter-chain when using unaligned memory.
+
+## ALSA
+ - Sync the udev rules and profiles with pulseaudio.
+ - Fix a memory leak.
+
+## SPA plugins
+ - An off-by-one error was fixed in the resampler that could cause
+ distortion when downsampling. (#1646)
+
+## Bluetooth
+ - Avoid probing the native backend because it might block for DBus
+ activation. This fixes some long startup times.
+ - Fix the kernel version check, 5.14.x kernels should also support
+ mSBC.
+ - Fix FastStream microphone support in more cases.
+ - Add workaround for Intel AX200.
+ - SCO sink should now also work in follower mode.
+
+## PulseAudio server
+ - Make the service file require a session manager.
+
+
+# PipeWire 0.3.37 (2021-09-23)
+
+This is a quick bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - Capture and playback is now avoided even more on unavailable
+ devices. This should fix some issues where an unusable microphone
+ was selected by default. It should now also again be possible
+ to select an unavailable device as the default.
+ - Native DSD audio playback is now supported. pw-cat can now also
+ play DSF files with the -d option.
+ - JACK stability improvements with buffer-size and samplerate
+ changes in some apps.
+ - Many cleanups and bugfixes all over the place.
+
+## PipeWire
+ - pw-metadata -d does not cause an infinite loop anymore. (#1622)
+ - Increase some plugin buffer sizes to fix some issues with many
+ channels. (#1620)
+ - Protect the global plugin list with a lock. Make sure pw_init()
+ is locked. Fixes some issues with concurrent ALSA plugin usage.
+
+## media-session
+ - Unavailable devices can be set as the default again. (#1624)
+ - Do a better check if a device has available routes and avoid
+ selecting devices with unavailable routes as default.
+ - Media-session was moved to its own directory. It used to live
+ in examples but it is past the example stage and it interferes
+ with the build options for the real examples.
+
+## Bluetooth
+ - The hardware quirk database is now loaded by the plugin instead of
+ the session manager. This makes it also work with wireplumber.
+
+## ALSA
+ - The ALSA mixer now handles device removal much better. (#1627)
+
+## libcamera
+ - Many fixes and improvement to the libcamera plugin. (#1513)
+
+## pulse-server
+ - Improve compatibility with pulseaudio module arguments.
+ - Parse channel_map arguments in module-loopback. (#1486)
+
+## JACK
+ - Delay emitting the samplerate and buffersize callbacks until the
+ client is active. This fixes some crashes with Carla and other
+ JACK apps.
+
+# PipeWire 0.3.36 (2021-09-16)
+
+This is a quick bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - A quick update with mostly only bugfixes and small improvements.
+ - Capture and playback is now avoided on unavailable devices. This
+ should fix some issues where an unusable microphone was selected
+ by default.
+ - MIDI output should not stop randomly now.
+ - The GStreamer elements are much improved, cheese should work
+ a lot better now.
+ - Virtual sinks and sources should now always show up immediately.
+ - JACK processing is now delayed until buffersize and samplerate
+ are emitted. This should improve stability of many JACK apps.
+ - JACK transport sync is now implemented correctly so that preroll
+ in bitwig works.
+
+## PipeWire
+ - The module dir environment variable can now contain multiple paths.
+ - Documentation now contains dot graphs of dependencies. (#1585)
+ - config min/max/default quantum values are now scaled with the
+ samplerate.
+ - A potential crash was fixed where destroyed memory was still used
+ by a node. This could cause crashes in cheese.
+
+## pipewire-media-session
+ - Only allow passthrough for passthrough formats (S/PDIF) for
+ now. (#1587)
+ - Improve bluetooth profile autoswitch.
+ - Don't try to route audio to nodes with unavailable routes.
+
+## ALSA
+ - Pass the right AES bits to the alsa device when opening an
+ S/PDIF stream.
+ - Fix a bug in the MIDI bridge port management logic. When a port
+ was added and immediately removed, output would stop.
+
+## GStreamer
+ - The GStreamer source now handles the flushing state correctly.
+ - All blocking operations now have a 30 seconds timeout, to avoid
+ infinite locks.
+
+## Plugins
+ - V4l2 Device formats and controls are now passed on the node, just
+ like with audio devices.
+ - audioconvert now also exposes the softMute property.
+
+## JACK
+ - Improve stability when changing buffer size and sample rate
+ dynamically by pausing the processing until the application has
+ handled the callback.
+ - Improve handling of timebase master. When the master was moved to
+ another driver, it did not attempt to become a new timebase
+ master on the new driver. (#1589)
+ - Implement transport sync to make preroll in bitwig work. (#1589)
+
+## pulse-server
+ - Fix an issue where virtual sinks/sources would not show up
+ immediately. (#1588)
+
+
+# PipeWire 0.3.35 (2021-09-09)
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - S/PDIF passthrough over optical or HDMI is now implemented.
+ - Some critical fixes to MIDI, draining of streams and various
+ modules.
+ - skypeforlinux should work better now after adding it to the
+ quirks database.
+ - Bluetooth codecs are now in separate plugins to make it easier
+ to ship them.
+
+## PipeWire
+ - Drain was fixed in pw-stream. In some cases it would not clear
+ the drain state correctly. Fixes the issue where speaker-test would
+ only play one channel.
+ - Loopback connections to a driver will now activate the driver. This
+ fixes an issue where MIDI connections between devices or some
+ applications (puredata) would not get any MIDI messages. (#1559)
+ - The audiomixer can now mix more formats. Together with the passthrough
+ improvements this can be used to avoid conversions to/from the DSP
+ format in some cases.
+ - Make sure we idle drivers when removing a node from it in all cases.
+ JACK clients could keep a driver node busy.
+ - Add new methods to accumulate object info. The old one was difficult
+ to use when applications need to accumulate multiple changes.
+ - A new interface to load modules has been added. Plugins can use this
+ to ask the host (PipeWire) to load spa plugins.
+ - Increase param buffer size to handle larger params. Nodes with a large
+ number of channels would sometimes not have properties. (#1574)
+ - Concurrent link negotiation that caused some links to not work,
+ is now avoided. This fixes monitor ports in Ardour6.
+ - Small tweaks to how the quantum and rate are handled when nodes move
+ between drivers. Make node.lock-quantum work with node.latency
+
+## PipeWire modules
+ - The convolver plugin in filter-chain has been optimized some more.
+ - The echo-cancel stream properties were improved so that it actually
+ can remember the streams it links to. (#1557)
+ - module-pulse-tunnel had the buffer attributes wrong and would cause
+ high latency with older pulseaudio servers. (#1434)
+ - module-roc had the properties configured wrongly, which would cause
+ it to not work at all in most cases. (#1538)
+ - There is now an example of a 7.1 virtual surround sink using the
+ hesuvi impulse responses.
+ - The convolver now supports dirac pulses as the IR.
+
+## ALSA
+ - UCM config is now cached per device, using up less memory. It also
+ temporarily works around a problem in alsa-lib that is now being
+ patched and rolled out. Should stop devices from disappearing when
+ logging out and back in. (#1553)
+ - Fix the MIDI clock rate matching. It was too sensitive to small
+ changes and would spiral out of control and break MIDI rather
+ quickly.
+
+## pipewire-media-session
+ - The media session can now save and restore IEC958 (S/PDIF) codecs
+ for the sinks.
+ - Passthrough of IEC958 (S/PDIF) content is now possible. If the client
+ and the sink contain a compatible set of codecs, an exclusive
+ connection can be made between client and sink to pass the encoded
+ S/PDIF content directly to the device.
+ - Use new introspection info update methods to suspend nodes in all
+ cases. Sometimes, nodes would fail to suspend because the state info
+ was not evaluated.
+ - The media session can now work in non-DSP mode, which will try to
+ avoid any audio conversions between client and device when possible.
+ But, this will also disable compatibility with JACK applications.
+
+## Bluetooth
+ - Bluetooth codecs are now compiled into separate plugins which are
+ dynamically loaded. This makes it possible to change the plugin
+ implementation or ship plugins separately without having to recompile
+ the bluetooth module.
+
+## PulseAudio server
+ - Delay stream create reply until the stream is linked to a sink/source.
+ - The device-restore extension is now implemented. This makes it possible
+ to configure the IEC958 (S/PDIF) codecs supported by the sink with
+ pavucontrol.
+ - skypeforlinux now uses the same quirks as teams to make the sinks
+ show up in all cases. This fixes the issue of not being able to hear
+ the remote end in skypeforlinux.
+
+## JACK
+ - Improve catia and carla compatibility by caching objects a little longer
+ after being removed. (#1531)
+ - JACK ports now notify the negotiated format correctly.
+ - A potential deadlock was fixed when multiple threads would perform a
+ call that would require a roundtrip.
+ - Improve bufsize callback, it should not be called right after doing
+ activate() but only when the buffersize changes later.
+ - Add tweak to disable the process lock. Some older apps might not
+ expect it. (#1576)
+
+## Docs
+ - man pages are now generated with rst2man.
+ - DMA-BUF docs were updated.
+ - Documentation updates.
+
+
+# PipeWire 0.3.34 (2021-08-26)
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+
+## Highlights
+ - Fixes some critical issues with previous release. Such as
+ devices not showing up and default devices being lost.
+ - Support for consumer driver streams to make the producer v-sync
+ to the consumer monitor in a headless compositor setup.
+ - Improvements to routing of streams.
+ - Bluetooth battery status support for head-set profile and
+ using Apple extensions. aptX-LL and FastStream codec support
+ was added.
+ - Internal latency of ALSA devices can now be configured.
+ - A fast convolver was added to the filter-chain to implement
+ virtual surround sinks or reverbs.
+
+## PipeWire
+ - Add support for streams that are driver nodes for the graph.
+ This was already possible for source streams but it is now
+ also possible for playback streams. This can be used to let
+ a producer v-sync to the consumer monitor in a headless
+ compositor setup. (#1484)
+ - State files are now stored in XDG_STATE_HOME instead of
+ XDG_CONFIG_HOME. They will still be loaded from the config home
+ if they are not in the new state home, to ease migration.
+ - Set a driver on inactive nodes to make transport work in xjadeo.
+ (#1491)
+ - Fix parsing of filter-chain controls.
+ - A new FFT based convolver was added to module-filter-chain. It
+ uses a 0-latency 2 stage convolver with small FFT for the head
+ and a large FFT for the tail of the convolution. A convolution
+ can be used to implement IR based reverbs, HRIR surround sound
+ or other convolution based operations. An example HRIR
+ virtual surround sound sink has been added as well.
+ - module-filter-chain was reworked a bit to support more config
+ options for the plugins.
+ - Endian conversion and alaw/ulaw formats are now supported for
+ streams.
+ - pw-cat will now suggest a samplerate for the graph.
+ - SPA_PLUGIN_DIR can now search in multiple paths separated with
+ a ':'.
+ - Passthrough mode has been worked on and has been partially
+ merged. S/PDIF definitions have been added and ALSA devices
+ updated to report and configure S/PDIF formats. The session
+ manager changes to fully configure and enable passthrough mode
+ will hopefully be merged next time.
+ - Fix a race in pw-stream where it would not always emit the
+ right events.
+
+## ALSA
+ - Fix volume changed check. It was checking against the wrong
+ value and this could cause rounding errors.
+ - The ALSA plugin now also uses RT scheduling.
+ - Fix the behringer UMC202 usb device id, it was using a generic
+ TI chip ID that caused problems.
+ - Fix USB devices that don't show up anymore. Use an ALSA
+ workaround to fix this. (#1478)
+ - Add a rule for the new firmware of Sennheiser GSX 1200.
+ - ALSA sink and source can now use ProcessLatency param to configure
+ the internal latency. The latencyOffsetNsec property is also
+ exposed so that the latency can be adjusted in pavucontrol as
+ well.
+
+## media-session
+ - Fix a critical issue where the default device was not remembered
+ anymore when it was removed.
+ - Fix the issue where some apps need to be restarted when nodes go
+ away and reappear.
+ - Improve routing of streams. Streams that have a specific target
+ set will now be moved to the target when it appears instead of
+ staying on the fallback.
+ - Small memory leak fixes.
+ - Try to switch back to the user selected profile after finishing a
+ Bluetooth recording.
+
+## Bluetooth
+ - Add support for HF indicator 2 battery status.
+ - Add support for XAPL battery status.
+ - Set the Communication intended role for HFP profile.
+ - Enable SBC-XQ by default if not disabled by quirks.
+ - Fix some potential crashes due to excessive polling.
+ - Add aptx-LL codec and enable duplex for aptx-LL devices.
+ - Add FastStream codec. This is a codec that can use a
+ duplex SBC channel.
+
+## PulseAudio server
+ - Suggests a samplerate for the graph.
+ - Support for handling S/PDIF (IEC958) formats was added. This will
+ start working when the session manager supports configuring streams
+ and nodes in passthrough mode.
+ - Be smarter when handling devices without a negotiated format
+ yet so that they are visible as well. This makes virtual
+ devices show up immediately.
+
+## ALSA plugin
+ - Now suggests a samplerate for the graph.
+
+## JACK
+ - The jack.pc file can only be generated with meson >= 0.59.0. When
+ the jack-devel option is enabled, it will generate an error with
+ older meson.
+ - Small stability improvements when connecting/disconnecting in
+ Ardour.
+
+
+# PipeWire 0.3.33 (2021-08-05)
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - Better support for virtual sinks/sources for Pro Audio
+ profile.
+ - Better DMA-BUF format modifier negotiation.
+ - Support multiple sample rates in the graph. Not enabled
+ by default yet.
+ - Bluetooth can now automatically switch between headset
+ and audio profile.
+ - Documentation updates.
+ - Many improvements and crasher fixes.
+
+## PipeWire
+ - Make AUX channels an official channel map, use this for the
+ PRO audio profile so that we can name the channels. This
+ make it possible to define virtual sources and sinks for
+ Pro Audio devices in a more reliable way.
+ - Fix scheduling of some virtual sinks/sources. (#1407)
+ - Fix potential corruption of ringbuffer because of multiple
+ concurrent writers. This might be the cause for many reported
+ crashes. (#1451)
+ - Don't place sockets in $HOME. (#1443)
+ - Improve DMA-BUF negotiation. Add a flag to avoid fixation
+ of a property so that producers can negotiate more
+ efficiently. This is used to negotiate DMA-BUF modifiers,
+ which should make more efficient use of the GPU. (#1084)
+ - Add support for multiple sample rates. The graph can switch
+ when IDLE to one of the supported rates. Add an option to
+ lock the rate as well. This is not enabled by default yet
+ because of driver bugs that need to be worked around first.
+ - Add node.lock-quantum property that can be used to lock the
+ quantum in place.
+ - Improve latency reporting in the loopback module.
+ - Make new client-node method to send the peer port id to the
+ mixer. This can be used to know where the buffers entering the
+ mixer are coming from. (#1471)
+
+## Tools
+ - pw-top should now also correctly show bluetooth devices. (#1540)
+
+## media-session
+ - Handle unset of the default node.
+ - Added a module that can switch the bluetooth profile to headset
+ profile when a stream wants to record from it.
+
+## JACK
+ - Only call the jack callbacks when the client is active. Some
+ JACK applications don't expect callbacks before the client is
+ active and crash (x42-dpl). (#1461)
+ - Emit client unregister event.
+ - Add per-client match rules in the config file to set app
+ specific configuration and tweaks. (#1456)
+ - Use peer_id to implement jack_port_get_buffer() from one of
+ our peer ports to get the data before it enters the mixer.
+ Makes the capture monitors work in Ardour6.8. (#1471)
+
+## Bluetooth
+ - Add some broken kernel versions to the mSBC blocklist
+ - Avoid looping and consuming CPU when we can't write to the
+ BT socket.
+ - Use libfreeaptx instead of libopenaptx.
+ - Fix rounding errors in HW volume conversion.
+
+## PulseAudio server
+ - implement module-switch-on-connect to emulate pulseaudio
+ behaviour of new devices. Some desktop environments expect
+ this behaviour and break otherwise.
+ - Fix stream cleanup, make sure the stream is stopped before
+ destroying it. Might be cause for some of the reported
+ crashes.
+ - Update message API to use the JSON format.
+
+## Other
+ - Many documentation updates.
+ - Many cleanups and small improvements.
+ - Support the latest libcamera version. (#1435)
+
+
+# PipeWire 0.3.32 (2021-07-20)
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - Real-time priority handling for threads was reworked. Freewheeling
+ will now drop RT priorities to avoid being killed.
+ - Problems with filter chains and echo-cancel being linked in a loop
+ was fixed.
+ - alsamixer should now be able to see the mixer controls again.
+ - JACK has seen some latency reporting improvements that make Ardour
+ report latencies correctly.
+ - Many bugfixes and improvements.
+
+
+## PipeWire
+ - Fix a bug in the neon audio resampler code.
+ - There is now a node.link-group property to relate linked streams.
+ this can be used to track the dataflow with coupled streams.
+ - Fix a crash when recalculating latency on a destroyed port. (#1371)
+ - Filter chains and other modules that create streams can now also
+ be added to the daemon config itself. (#1309)
+ - Fix some potential deadlocks in timerfd. (#1377)
+ - Feedback links are skipped when recalculating latency to avoid
+ loops.
+ - The dummy driver and null-sink now stop the timerfd when following
+ another driver instead of generating useless graph wakeups.
+ - rt.limit was increased to 2 seconds. Some applications got killed
+ because they run lengthy code in the Real-Time thread. (#1344)
+ - Fix s24_32 to float, it was not sign extending properly. (#1393)
+ - The performance of the feedback loop check algorithm was improved
+ a lot, making complex graphs start much much faster.
+ - The zeroconf publish module now doesn't republish nodes every time
+ the volume changes. (#1406)
+ - A potential memory corruption error has been fixed in the loop
+ that could cause random crashes.
+ - Mempools can now be created from multiple threads at the same
+ time.
+
+## media-session
+ - Loops in coupled streams are now avoided. (#1394)
+ - Port changes for inactive profiles are ignored now by the
+ default-route module. (#1403)
+
+## ALSA
+ - Make sure that alibpref is not part of the device node name because
+ it is random. (#1362)
+ - Fixed an off-by-one that could cause midi events to end up with a
+ wrong timestamp and thus being discarded by some apps. (#1395)
+ - Fix some memory leaks when destroying a card object.
+
+## JACK
+ - Fix some invalid cycle wakeups that could cause JACK application to
+ run with a 0 buffer size. (#1386)
+ - JACK can now use rtkit to manage realtime priorities on threads.
+ - The Real-time priority is dropped when entering freewheel mode to
+ make sure we don't get killed when using too much CPU.
+ - jack_recompute_total_latencies() is now implemented, fixing the
+ latency reporting in Ardour. (#1388)
+ - Fix some overflows in time calculations.
+ - Ensure frame_rate in position is never 0.
+ - Graph callbacks are now emitted as well.
+
+## Bluetooth
+ - RTP payload type is now set correctly for aptX, LDAC and SBC, which
+ should improve compatibility with devices that care about this.
+
+## PulseAudio server
+ - There is now a quirks database to deal with bad clients. The database
+ is builtin but can be made external later.
+ - Teams is now lied to and told all sink/sources use s16 samples to make
+ it show all sinks/sources.
+ - Firefox is forced to remove the DONT_MOVE flag on capture streams so
+ that you can move firefox streams with other tools.
+ - The UNDERFLOW warnings are now made into info log messages to not
+ spam the log too much. Many application just let things underrun
+ and PulseAudio did not warn about this either. (#910)
+
+## ALSA plugin
+ - The alsa plugin now uses the right metadata for finding the default
+ source and sink, which makes the volume controls reappear. (#1384)
+
+## Other
+ - Cleanups in pulse-server and pipewire.
+ - Documentation additions.
+
+# PipeWire 0.3.31 (2021-06-28)
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - Fixes for alsa-lib 1.2.5
+ - New pulseaudio modules: module-avahi-zeroconf,
+ module-pipe-source, module-roc-sink, module-roc-source.
+ - JACK has seen massive stability improvements. Locking
+ and correctness wrt to callbacks has been reworked. Also
+ thread priorities have improved.
+ - Handle various crashes and lockups when running out of file
+ descriptors.
+ - Bluetooth now uses a hardware database to disable
+ non-working features on listed devices.
+ - Scheduling quantum and rate can now be changed dynamically
+ with pw-metadata.
+ - Many bugfixes and improvements.
+
+
+## PipeWire
+ - Improve cleanup of context in error cases.
+ - There is now a pw-test framework for improved unit tests.
+ - Improve property serialization to valid JSON.
+ - Fix some macros to work with better with coverity.
+ - Metadata permissions are checked now. Clients need the
+ M permission on an object to be able to set metadata for
+ it.
+ - The core metadata object will now remove metadata for
+ removed objects, the implementor does not need to worry
+ about that anymore.
+ - Audioadapter will now follow the rate of the graph with
+ the resampler adjusting itself dynamically.
+ - Core now has a metadata implementation helper. A context
+ will expose a metadata with settings that can be changed
+ at runtime. This can be used to change the loglevel or
+ graph quantum and samplerate on the fly.
+ - An infinite loop was fixed in the audio converter.
+ - Handle out-of-fds more gracefully. Handle truncated
+ control data by dropping the client connection.
+ - Fix profiler crash with many streams.
+ - Improve latency handling in pw-filter. There is now a
+ default handler and a ProcessLatency parameter to simplify
+ latency reporting.
+ - Latency reporting was improved in devices and streams.
+ - And example sink/source was added.
+
+## ALSA
+ - hardware mute and volume are now properties on the
+ Route param to make things easier.
+ - More fixes for alsa-ucm 1.2.5.
+
+## Tools
+ - spa-json-dump now properly encodes string and keys.
+ - pw-dump now shows the correct subject of the metadata.
+
+
+## PulseAudio server
+ - Ensure the node.description is set, some applications
+ crash otherwise (TeamSpeak).
+ - Module loading and unloading was improved.
+ - module-avahi-zeroconf was implemented.
+ - module-pipe-source was implemented
+ - module-roc-sink and module-roc-source was implemented.
+ - The maximum amount of connections has been limited to 64,
+ like pulseaudio.
+ - Handle out-of-fds more gracefully.
+ - Fix overflow of read/write pointers.
+ - Source and sink state are now decoupled from the monitor
+ state and will report IDLE when not playing anything.
+
+## media-session
+ - Port switching should now happen to/from the port that
+ actually changed.
+
+## JACK
+ - The locking was reviewed. All callbacks are now emitted
+ from the PipeWire thread with the lock released and
+ the process function will be disabled for the duration
+ of the callback. This ensures that no two callbacks are
+ called at the same time.
+ - Improve internal consistency and try to never call callbacks
+ with invalid objects.
+ - Monitor port can now be accessed with system:monitor_%d
+ - client threads are now created with SCHED_FIFO and module-rt
+ is used to create the other RT threads. This should avoid
+ SIGKILL from RTKit in some cases.
+
+## Bluetooth
+ - Various bugfixes to improve connections to devices.
+ - Handle delayed UUID connection.
+ - There is now a hardware database that can disable features
+ in listed devices.
+ - Use libusb to detect availability of mSBC.
+
+## ALSA
+ - The virtual device name can now also contain a media role.
+
+
+# PipeWire 0.3.30
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+This is a quick emergency release to fix some severe
+problems with the previous release.
+
+## Highlights
+ - Recording from a monitor port should work again.
+ - JACK applications should now be more stable again.
+ - Freewheeling should not lock up anymore.
+ - Fix lockups in many pulseaudio apps.
+ - module-echo-cancel was implemented in pipewire-pulse
+ - Many other stability fixes.
+
+## PipeWire
+ - Improve module path logic.
+ - Improve logger formatting
+
+## PulseAudio server
+ - Make sure to pass 64 bits values for time on ARM 32 bits to
+ avoid protocol errors.
+ - Avoid a crash when unloading module-combine-sink.
+ - Avoid overflow in requested bytes, resulting in stalled
+ audio.
+ - Implement module-echo-cancel.
+
+## Bluetooth
+ - Handle latency parameters instead of failing.
+
+## JACK
+ - Fix locking in many places to avoid deadlocks and crashes.
+ - Fix port rename.
+ - Stop freewheeling correctly instead of deadlocking.
+
+# PipeWire 0.3.29
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - Latency reporting is now implemented.
+ - Many documentation updates and cleanups.
+ - module-combine-sink was added to PulseAudio server.
+ - Better handling of multichannel input profiles.
+ - Fix 100% volume issue when monitor suspends or profile
+ changes in some cases.
+ - Bugfixes and crashes
+
+
+## PipeWire
+ - A new module-rt was added to acquire real-time scheduling
+ privileges without using RTKit.
+ - Documentation fixes and updates. Docs are now using a
+ custom theme.
+ - There is now a MANDATORY flag on properties that influence
+ how properties are filtered.
+ - Filter-chain now parses the LADSPA_PATH correctly when it
+ contains a colon separated list.
+ - Move `#pipewire` IRC channel to oftc.net.
+ - Fix an error where param changes were not emitted in all
+ cases.
+ - Implement Latency reporting. Latency values are propagated
+ through the graph so that each node knows the latency to
+ the output/input device. Synchronization in pw-stream has
+ been updated to use this.
+ - Some more upmix cases are added so that LFE, SIDE and REAR
+ can be generated from a mono channel as well.
+ - pw-stream and pw-filter will now emit the process event from
+ the real-time thread in a safe way, potentially avoiding some
+ of the harder to debug crashes.
+ - Fix potential stack overflow with serialize_dict.
+ - Add PIPEWIRE_NO_CONFIG to run without custom config files.
+ - The WebRTC echo canceler was added. Next versions will
+ integrate this better.
+
+## PulseAudio server
+ - module-combine-sink was implemented.
+ - Fix some segfaults when DBus connections fail.
+ - Support for listening on IPv6 was added.
+ - Fix a bug where many flushes could result in requests for too
+ much data from the client, causing sync, latency and garbled
+ sound problems after many seeks.
+
+## ALSA
+ - Also probe input paths for multichannel mappings. This makes
+ multichannel input ports show up in more cases.
+ - Fix headphones/front volume issue on some cards.
+ - Fix max volume issue when profile changes.
+ - Fix issue with UCM local config that was not available when the
+ device was opened in the server but the UCM was opened by the
+ session manager. Fixes alsa 1.2.5 compatibility.
+
+## JACK
+ - Implement latency reporting with the new Latency params.
+
+# PipeWire 0.3.28
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+## Highlights
+ - Freewheeling was implemented. This makes it possible to
+ export projects in ardour.
+ - A new powerful filter-chain module was added that can
+ be used to created all kinds of filter-chains from ladspa
+ and builtin plugins.
+ - Many more pulseaudio modules are now implemented:
+ module-ladspa-sink, module-ladspa-source, module-pipe-sink,
+ module-tunnel-sink, module-tunnel-source,
+ module-zeroconf-discover
+ - Fix a bug where devices would not appear after logout/login.
+ - Fix a bug where the volume was reset to 0 and devices would
+ have no audio.
+ - Config files are now installed in the data dir, system
+ overrides in /etc/pipewire and $HOME are checked first.
+
+
+## PipeWire
+ - Implement freewheeling for JACK clients
+ - Add filter-chain module that can be used to construct
+ arbitrary graphs from ladspa and builtin plugins.
+ - Add new property to easily set algorithm params
+ - Add module-pulse-tunnel to tunnel audio to and from
+ a PulseAudio compatible server.
+ - Add a avahi zeroconf discover module, create pulse-tunnel
+ when PulseAudio devices are announced.
+ - Config files are now installed in the data dir, system
+ overrides in /etc/pipewire and $HOME are checked first.
+ - Applications now have their monitor ports named with the
+ "monitor" prefix to avoid confusion with the output ports.
+ - LICENSE clarifications.
+
+## GStreamer
+ - fixes to the pipewiresink plugin.
+
+## SPA plugins
+ - Fix a bug where the volume was reset to 0
+ - Add events to dbus plugin. This can be used to detect dbus
+ disconnects.
+
+## Media-session
+ - Handle dbus disconnect.
+ - Handle device reservation errors.
+
+## PulseAudio server
+ - Implement module-ladspa-sink and a new PipeWire-only
+ module-ladspa-source
+ - Implement module-pipe-sink
+ - Implement module-tunnel-sink and module-tunnel-source
+ - Fix a bug with module argument parsing
+ - Implement module-zeroconf-discover
+
+## ALSA plugin
+ - improve error handling
+
+PipeWire 0.3.27
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Fix bug that caused bluetooth devices to stop working.
+ - Fix session-manager crash when switching users caused by
+ the DBus plugin cleanup errors.
+ - Improve volume handling of monitor ports.
+ - Fix GStreamer v4l2 support.
+ - Implement module-remap-sink and module-remap-source in
+ pipewire-pulse.
+ - More fixes and improvements.
+
+
+- PipeWire
+ - Move the loopback code into a module. Use this in pw-loopback
+ and pipewire-pulse. Fix some cleanup crashes.
+ - A dummy echo-cancel module was added. Later versions will
+ include the webrtc echo-canceler.
+ - State files don't have the X permission anymore.
+ - Move i18n core into a private header file.
+ - Stream can now advertise properties and receive property
+ updates.
+ - Fix an issue where the wrong index was used to address a port.
+ It caused Bluetooth devices to stop working.
+
+- SPA plugins
+ - Only do LFE filtering on channels we created.
+ - Improve name and description of devices.
+ - Improve cleanup in DBus connections and sources to avoid crash
+ when destroying.
+ - Improved volume handling. Hardware, Software and Monitor
+ volumes are now properly separated and handled.
+ - Support for S8 and S8P formats was added.
+
+- Tools
+ - pw-cli can now also create Struct from JSON arrays.
+
+- Session-manager
+ - The session manager can now also create passive links. This
+ makes is possible to suspend effect chains together with the
+ sinks when not in use.
+ - Match rules now check the complete property value instead of
+ only the start.
+ - Handle multiple pending param enumerations, take only last
+ result. This fixes some volume update issues.
+
+- GStreamer plugins
+ - GStreamer plugins now advertise handling DMABUF explicitly. This
+ is currently the only way to avoid a memcpy for v4l2 devices.
+
+- Device support
+ - sync ACP with pulseaudio, merge upstream patch instead of our
+ hack to workaround missing duplex devices.
+ - V4l2 devices don't expose their fd anymore. Previously the fd
+ and mmap offsets were passed to the client to access the buffer
+ memory but that could create security issues.
+
+- Bluetooth
+ - Don't unregister the profiles on shutdown because this can cause
+ delay, just close the dbus connection.
+ - Bluetooth devices now try to use the global samplerate from the
+ graph.
+
+- PulseAudio server
+ - Implement remap-sink and remap-source modules using the
+ new loopback module.
+
+PipeWire 0.3.26
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - I18n support, with translations merged from PulseAudio.
+ - New pw-link tool.
+ - Many Bluetooth improvements, support for hardware volumes.
+ - Support for 64 channel devices.
+ - Stability fixes and improvements.
+
+
+- PipeWire improvements
+ - The link factory can now also make links between nodes and
+ ports by name so that it can be used in scripts.
+ - Add module-protocol-simple that can stream raw audio on a
+ socket.
+ - Added i18n support. Merge PulseAudio translations for the ACP
+ library so that we don't cause regressions.
+ - Support more than 19 channels in the channel mixer.
+ This makes all channels usable on 32 and 64 channel cards.
+ - Detect if we're running in a VM and allow for tweaking some
+ settings such as the max-quantum to make things work better in
+ VMs.
+ - Fix a potential crash when connecting a client and updating
+ permissions.
+ - Fix a potential crash when trying to link incompatible ports.
+ - Lingering links in error will now be destroyed automatically.
+
+- Tools
+ - Added new pw-link tool to list and monitor ports and to
+ list, monitor, create and destroy links between them.
+ - pw-cli can now also list params by name.
+ - pw-dump now outputs Spa:String:JSON types in metadata as properly
+ parsed and formatted JSON so that tools can parse the
+ metadata values using a JSON parser.
+
+- Session-manager
+ - Add logind support. The bluetooth monitor can only be started
+ for one user at the time, so use logind detect active seats.
+ - ALSA icon names were improved to match what PulseAudio does.
+ - Improve the bluetooth icon name. Also use the device alias
+ as the device description, like PulseAudio.
+
+- Device support
+ - When devices become inaccessible, they are now removed from
+ the PipeWire graph.
+ - Fix datatype selection for buffers in v4l2 and libcamera.
+
+- Bluetooth
+ - Various memory leaks and crashes are fixed.
+ - Added support for AVRCP hardware volume.
+ - Added support for HSP/HFP hardware volume.
+
+- PulseAudio server
+ - Fix module-loopback connections to monitor ports.
+ - Implement module-native-protocol-tcp.
+ - Handle nodes and streams with > 32 channels. The PulseAudio
+ API only supports up to 32 channels so only make those 32
+ first channels available with the PA API.
+ - Implement module-simple-protocol-tcp.
+ - Improve events emitted by the server.
+ - Improvements to channels and channel_map properties on
+ modules. one can imply the other and they should match when
+ both given.
+ - null-sink will now have their volume work correctly by
+ default.
+
+- JACK
+ - JACK development files can now optionally be installed.
+
+PipeWire 0.3.25
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Many stability improvements.
+ - Plug fd leak in flatpak detection
+ - add pw-loopback tool and support module-loopback
+ - volume restore for virtual sinks/sources or other sink/sources
+ without hardware volume.
+ - Fix cracks and pops in audio capture.
+ - Many bluetooth improvements and compatibility fixes.
+
+
+- PipeWire improvements
+ - Hex encode invalid SEC_LABEL properties to avoid generating
+ invalid json.
+ - Small fixes to how nodes are started to avoid crashes.
+ - Make sure ports are only scheduled after being fully
+ negotiated to avoid crashes.
+ - Implement coverity into CI, fix some bugs detected by
+ coverity.
+ - Plug leak in flatpak detection.
+ - Fix crash when removing globals in some cases.
+ - Fix crash because the mixer info was not removed from a
+ port in all cases.
+ - Add PIPEWIRE_AUTOCONNECT environment variable to disable
+ stream autoconnect. Also add a config option to disable
+ autoconnect.
+ - Improve wildcard in format helpers.
+ - Add env variable to disable journald logging.
+
+- Tools
+ - Add a new pw-loopback tool to loop a capture device to a
+ playback device.
+ - Display localized strings correctly in pw-top
+ - Add some more options to pw-dot
+
+- Session-manager
+ - When a new node is configured and some stream have this
+ as the default target, move them to it.
+ - Fix some crashes.
+ - Implement volume restore on nodes without routes. This makes
+ it possible to restore volume on purely software nodes like
+ null-sinks.
+ - Also try to suspend errored nodes so that they may leave the
+ error state and be reused again.
+ - Break endless link loops when something went wrong.
+
+- Device support
+ - Fix monitor volumes, they are now separate from the hardware
+ volume.
+ - Fix cracks and pops in alsa capture caused by mismatch between
+ resampler and capture source.
+ - Add start-delay config option to alsa sink.
+ - Ensure the PipeWire midi ports start from a higher number so
+ that the lower port numbers are available to apps as before.
+
+- Bluetooth
+ - source devices are now removed when idle
+ - Support using pipewire as Audio Gateway.
+ - LDAC encoding quality can be configured now
+ - Implement codec switching for HFP
+ - Implement codec switching with new device property.
+ - Improved stability and compatibility
+ - Autoconnect device profiles at startup
+ - Add AAC bitrate mode configuration
+ - Make it possible to use an A2DP source as an input device. You
+ can then use your phone as an A2DP microphone, for example.
+ - Remove battery reporting when RFCOMM connections is closed.
+
+- PulseAudio server
+ - Add some workarounds for Blueman
+ - Set correct errno values, fixes a hang in load-module of a
+ non-existing module
+ - Try to not send inconsistent information to clients.
+ - Fix some crashes.
+ - Add support for the new send-message API, use this to
+ switch bluetooth codecs.
+ - Fix draining by making sure we are started.
+ - Handle 0 sink and source as the default sink/source.
+ - Implement module-loopback
+
+- JACK
+ - Fix some memory leaks when closing a client
+ - Add self-connect config option to limit where clients
+ can connect themselves.
+ - Don't crash when apps call _port_get_buffer() on a
+ port that is not their own but simply return NULL.
+ This fixes a crash in Ardour6.
+ - Improve client added/removed callbacks. Sometimes it would
+ emit a client remove when there were still ports for the
+ client.
+ - make sure midi port names are stable across reboots.
+
+PipeWire 0.3.24
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Many JACK midi improvements and device support.
+ - Fixes in gnome-control-center default sink/source handling.
+ - Many small performance improvements in alsa device handling
+ and latency. There should also be less cracks/pops and xruns
+ now.
+ - Fixes for gnome-control-center default sink/source handling.
+ - More bluetooth compatibility improvements.
+
+
+- PipeWire improvements
+ - Implement simple upmixing.
+ - Disable the resampler when not used. This improves latency
+ and CPU usage.
+ - Handle max-quantum on devices and try to not make the quantum
+ larger than the device buffer size.
+ - Improvements to how nodes and links are activated. It should
+ now result in less xruns and cracks/pops.
+ - meson uses the feature options everywhere now.
+ - Handle volume remap in the channelmixer. This fixes the channels
+ on multichannel devices.
+ - Try to escape invalid JSON string characters.
+ - Keep better track of changed parameters in audioconvert.
+ - Improve config files, make arrays where needed.
+ - Respect NO_COLOR where possible
+ - Support in-place config file parsing to avoid allocations and
+ improve startup performance.
+ - There is now a config option to enable non-power-of-two quantums.
+ - Preliminary support for upmixing and generating LFE channels.
+
+- Session-manager
+ - Default nodes are not stored as JSON in the metadata. This
+ is more readable and introspectable.
+ - More default-nodes and default-routes improvements. Port
+ switching should work better now.
+ - Wait until all devices are scanned before linking clients.
+ - Fixes some crashes.
+ - Sinks (monitors) can now be set as default sources.
+
+- Device support
+ - Fix startup timers for alsa devices.
+ - Improve timers in alsa when quantum changes. It should cause
+ less xruns and cracks.
+ - Fix UCM setup of capture devices.
+ - Only disable IRQ in alsa when not batch. For batch devices the
+ hw pointers are updated each IRQ so we need to keep them enabled.
+ This massively improves latency on USB batch devices to the same
+ level as JACK (with small enough period size).
+
+- Bluetooth
+ - Improvements to profile switches.
+ - Improvements to volume handling.
+ - Fixes for A2DP sources
+ - Add support for battery status when available.
+ - Many other small improvements.
+
+- PulseAudio server
+ - Handle NULL in set_default_sink/source to clear the default.
+ - Implement a workaround for gnome-control-center when setting
+ the default sink/source. It also sets the target in
+ stream-restore to the new default. This fixes moving streams
+ in gnome-control-center.
+ - Fix some races by replying to some requests after the operation
+ completed.
+ - Prefer formats of the extended format API.
+ - Create a pid file on startup to improve compatibility with apps
+ that look for it.
+ - Capture streams can now be moved to monitors with pavucontrol.
+ - Fixes for crashes.
+
+- JACK
+ - jack clients can now connect to the 'default' server.
+ - Move midi ports back to the midi client.
+ - Only mark midi hardware ports as terminal/physical.
+ - Use the same midi names as a2jmidid.
+ - match system ports in get_ports.
+ - Improve compatibility with some apps that require a
+ fixed latency.
+ - Beginnings of the libjackserver implementation.
+
+
+PipeWire 0.3.23
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Fixes for some critical bugs in last release.
+ - Fix bug where audio was not drained properly at the end of
+ playback, causing repeating sound.
+ - Profile and route switching was improved and should mimic
+ more what pulseaudio did.
+ - Various fixes for xruns in capture and playback.
+ - Bluetooth now supports delay adjustment and various other
+ improvements.
+ - The pulseaudio server now correctly identifies AC3 and DTS
+ streams and returns a not supported error instead of playing
+ static.
+ - Multichannel support was improved in the alsa plugin and
+ the channel mixer. Channels should now play on the right
+ speakers in all cases.
+
+- PipeWire improvements
+ - Small fixes and improvements in JSON parsing and encoding.
+ - Improvements to param handling in audioconverter. It would
+ previously not always notify of changes.
+ - Avoid updating some properties that we use internally such
+ as the object id and the node.id.
+ - log.level in the config files is now actually used.
+ - the PIPEWIRE_LATENCY env variable should always override
+ any application settings in filter/stream/jack.
+ - The config file can now contain filer and stream properties
+ to, for example, control the resampler, mixer and latency.
+ - Add sandboxing to the systemd services
+ - Various FreeBSD fixes.
+ - Improve draining and a way to exit the drain state as well.
+ - Many multichannel fixes. Channel remapping should now be
+ correct.
+ - Fix bug with repeating audio at the end of playback because
+ the drain in the resampler was not draining all channels.
+ - RTKit default rt.prio has been increased to 88. This will
+ likely still be clamped to 20 until distros increase the
+ max priority.
+
+- Session-manager
+ - Don't try to switch to Pro Audio profile, this should be
+ a user choice only.
+ - Don't crash when metadata was disabled such as when not
+ using the audio features of pipewire.
+ - Rework the profile and route handling.
+ - Add systemd unit files for the media-session
+ - Device names should now also have sane names so that tab
+ pactl completion works on them.
+
+- Device support
+ - Fix ALSA format enumeration in more cases. Use the channels
+ and rate as a filter.
+ - Make sure the graph doesn't ever use buffers larger than
+ the alsa device buffer size or we get xruns.
+ - Tuning of the alsa device timeout handling and dynamic
+ resampler. There should now not be any xruns when streams
+ appear and disappear or when the quantum changes.
+ - Fix bug in alsa device when reassigning to a new driver,
+ in some cases the dynamic resampler was not activated and
+ things would drift out of sync and fail.
+ - Fixes in quantum changes for ALSA capture and how the
+ resampler is drained and fed with the new samples.
+
+- Bluetooth
+ - Delay adjustment has been implemented now. Bluetooth
+ devices should now be more synchronized with video due
+ to proper delay reporting. Because BT delays can be
+ large, it can cause hickups in some players.
+ - Fix volume in bluetooth devices.
+ - Codec switch improvements.
+
+- PulseAudio server
+ - Latency offset adjustment is now implemented and functional
+ for bluetooth devices. It is not working for alsa devices
+ yet.
+ - Handle unsupported formats. Previously we would accept encoded
+ formats and play noise. This fixes AC3 playback in vlc.
+ - Move some of the configurable parameters to the config file.
+ - Fix a fatal use after free when playing samples
+ - Improve module handling. loaded modules now show up in the
+ list of modules and can be unloaded. This also prepares the
+ core for more module implementations later.
+
+- ALSA plugin
+ - Fix drain with very large buffers, we need to manually start
+ the stream before draining.
+ - Fix the channel layout handling.
+ - Improve compatibility with apps that expect the poll to only
+ return when there is activity.
+ - Fix drain for capture
+
+- JACK
+ - Add a config option to shorten and filter client names
+ - Increase the length of the client name size and make sure
+ we don't exceed the allocated size.
+ - We now include our own jack header files so we can build
+ without depending on another jack-devel package. We don't
+ yet install the headers or provide pkgconfig files.
+
+
+PipeWire 0.3.22
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Per client config files replace the module-profiles. It's
+ now possible to tweak settings and load custom modules.
+ - Pro Audio card profile support. You can now select the
+ Pro Audio profile and have raw device access with the
+ maximum number of channels and no mixer controls. This is
+ the usual setup for managing high end Pro Audio cards.
+ - Many fixes and improvements in the JACK library to make
+ devices look and integrate better.
+ - Many bluetooth improvements. Playback should be more
+ reliable and better synchronized. Support for the HFP HF
+ profile.
+ - Small fixes and improvements all over the map.
+
+- PipeWire improvements
+ - Add support for restrictions requested by a client. This
+ makes it possible to implement Flatpak policy for emulated
+ PulseAudio clients as well.
+ - Fix removal of params in objects. Previously they would not
+ be removed from the cache.
+ - Remove mlock warnings by default. There is an option to enable
+ them again if you want to check if your system is optimized.
+ - Remove LimitMEMLOCK lines from the service files. They can
+ only lower the system settings and are thus not useful.
+ - Implement per-client config files. Each pipewire client will
+ now read a config file that you can use to configure the
+ context of the client.
+ - Implement state and config load/save in pipewire. This is used
+ by the session manager or other apps.
+ - Make an option to disable dbus support.
+ - Add tool to convert pipewire config to JSON.
+
+- Session-manager
+ - Give all permissions to Manager flatpak apps. In the future
+ we will use the Permission store to remember user settings.
+ - Improvements to default audio/sink handling.
+ - Add option to configure device suspend time.
+ - Small fixes in route handling.
+
+- Device support
+ - Complain when ACP profile files are not found and use
+ a fallback in order to get something working.
+ - Add volume support to monitor ports.
+ - Fix resume from suspend for ALSA in more cases.
+ - ALSA ACP cards now have a Pro Audio profile that exposes
+ the raw card devices.
+
+- Bluetooth
+ - Enable A2DP delay reporting. This improves audio/video sync
+ when playing audio over bluetooth.
+ - Fix stuttering in A2DP source
+ - Tweak buffer size and latency settings to avoid stuttering
+ - More work on HSP and HFP support
+ - Fix initial profile configuration
+ - Add HFP HF support
+
+- PulseAudio server
+ - Small tweaks in capture packet size to avoid crashes in some
+ apps.
+ - Detect Flatpak apps and requests the flatpak permissions from
+ the session manager. This means that Flatpak pulseaudio apps
+ will now run with reduced permissions.
+
+- ALSA plugin
+ - Reduce min buffer size in the plugin for lower possible
+ latency.
+
+- JACK
+ - implement some missing methods to make qjackctl work again.
+ - Use the context data thread instead of making our own. This
+ fixes the issue where the data thread was not given RT
+ priority correctly.
+ - Pass extra jack flags around in port properties. This makes
+ CV ports in carla work.
+ - Many tweaks to the port names and aliases. Unwanted characters
+ are filtered out, giving better names to jack apps. Default
+ device names are now equal to those seen in pulseaudio apps.
+ - Add an option to make a separate client for the monitor ports
+ of a device. This makes it more usable in apps.
+ - add support for system:playback_N and system:capture_N port
+ names for apps that hardcode these port names.
+
+
+PipeWire 0.3.21
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Many PulseAudio compatibility fixes. Handling of corked
+ streams, the prebuf setting, seek modes and stream flags
+ are now implemented correctly.
+ - Ports and Profiles are now managed by the session manager
+ and can save and restore previous settings.
+ - ALSA device handling has been tweaked for maximum
+ compatibility at the expense of latency. There are tuning
+ options in the config file.
+ - Improved Bluetooth support. HSP is disabled by default
+ because it is old and deprecated and in some cases causes
+ conflicts with the newer HFP profile. Codec switching is
+ now implemented as well.
+ - PipeWire accepts donations with liberapay now.
+
+- PipeWire improvements
+ - Improve draining in pw-stream.
+ - pw-stream now uses busy metadata by default. This makes sure
+ that no writer can write to buffers when readers are still
+ busy.
+ - Fix handling of empty array/choice instead of failing.
+ - Fix crashes when creating properties from empty strings.
+ - Make it possible to pass an array to module-access
+ access.allowed variables
+ - Fix small bug in argument parsing in pw-cat
+
+- Session-manager
+ - Restore route volumes in all cases, also when switching
+ routes.
+ - Use a default route volume for unknown routes instead of
+ letting the system decide on a default.
+ - Improve profile handling. Don't try to restore unavailable
+ profiles. Implement the profile switching in the session
+ manager now.
+ - Fix handling of Virtual sources as defaults.
+ - Handle port switching in the session manager. Implement
+ save and restore of default ports per profile.
+
+- GStreamer
+ - Fix a crash with zero SPA_PARAM_BUFFERS_size
+
+- Device support
+ - v4l2-source will now respect the requested memory types.
+ - ALSA buffering has been tweaked. USB devices should have
+ less XRuns by default. Parameters can be tweaked to
+ decrease the latency on capable devices. Also fix a case
+ where a quantum change would cause an xrun.
+ - Fix mute in bluetooth devices
+ - bluetooth devices are not paused in idle anymore for
+ improved compatibility.
+ - Codec switching for bluetooth is implemented along with
+ config options to select the codecs manually.
+ - HSP for bluetooth is now disabled by default. Most devices
+ support the newer HFP profile and some devices fail when
+ both are available.
+ - Reduce the amount of events the ALSA plugins emit by bundling
+ them.
+
+- PulseAudio server
+ - Implement the suspend command
+ - Fixes volume in sample info
+ - Fix playback of samples, sometimes samples would be clipped
+ short. Also implement the target sink for the sample.
+ - Use rate match to feed samples. This way the latency can
+ be kept to a minimum.
+ - Latency has been tuned some more, more closely emulating
+ pulseaudio behaviour.
+ - Improve default sink/source handling. Make sure all events
+ are sent correctly when defaults change.
+ - Handle underrun better without causing sync issues. Make sure
+ to pause in corked state.
+ - Implement rewind due to seeks, fixes GStreamer seeking.
+
+
+PipeWire 0.3.20
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Latency was reduced in ALSA and PulseAudio and time
+ reporting has improved a lot.
+ - Bluetooth now has a native HFP backed, SBC XQ and
+ mSBC support.
+ - Many bugfixes and improvements, improved device
+ support.
+
+- PipeWire improvements
+ - pw-dump can now dump all objects such as Endpoints
+ - pw-dump has a -m option to monitor changes
+ - pw-dump can now dump metadata
+ - pw-stream can now use the rate-match io to exactly
+ produce the required number of samples for the
+ current cycle. When using this feature, a stream can
+ achieve the same low-latency as pw-filter.
+ - spa-acp-tool can now load a custom profile-set and
+ correctly parses the volume updates
+ - There is now a nofail option when loading modules
+ - The connection has been made reentrant to fix some
+ strange random problems with metadata.
+ - Turn some errors into warnings or simply info.
+ - Executables are now built with PIE
+ - S24OE formats should work now (MAudio FastTrack Pro)
+ - Remove mlock warnings. Add support for mlockall with
+ a config option.
+
+- Session-manager
+ - There are now config files for bluez and v4l2 modules
+ - Improve ALSA device and node properties
+ - Bluetooth devices have better properties now.
+ - The default device routing has been improved.
+
+- Device support
+ - Port priorities are updated for UCM devices
+ - ACP devices notify change in routes in all cases
+ - There is now RW support in ALSA devices to increase
+ compatibility.
+ - Many improvements to Bluetooth. SBC XQ support can now
+ be enabled with a config option. mSBC can be enabled
+ with an option.
+ - Bluetooth devices not expose Routes so that they look
+ more like how PulseAudio handles them
+ - Gracefully handle missing profile-sets
+ - There is now a native HFP backend
+ - Improve card names in some cases.
+ - pause-on-idle is now disabled for ALSA devices. This can
+ reduce pops and clicks when the device is stopped.
+
+- ALSA plugin
+ - Use rate-match to reduce the latency
+ - Implement a _delay() function to get smoother timestamps.
+ - Fix property parsing. Fixes volume changes in alsamixer.
+
+- PulseAudio server
+ - Use rate-match to reduce the latency. This also reduces
+ the buffering in audioconvert and improves timestamp
+ reporting.
+ - Implement rate changes now that we have rate-match
+ support.
+ - pactl stats will now work
+ - Fix excessive memory usage when a capture client doesn't
+ read fast enough.
+
+PipeWire 0.3.19
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Startup after login should be fixed now with inotify
+ used to wait for permissions.
+ - Channels should be mapped correctly now.
+ - Many bluetooth improvements in LDAC, AptX-HD. AAC was
+ also added. Headsets should work better now.
+ - pipewire-libpulse was removed. It is now completely
+ replaced by pipewire-pulse.
+ - Fix a crasher bug in pipewire-pulse and some memory leaks.
+ - Fix a bug with feedback loop that would cause 100% CPU.
+ - A new pw-top tool to display real-time graph performance.
+ - The example session manager now has config files.
+ - The config file format was changed to use the SPA JSON
+ tokenizer. This makes it more flexible and extensible.
+
+- PipeWire improvements
+ - Fix debug of id in format channels
+ - Audioconvert should now remap channels correctly in all
+ cases.
+ - Feedback loops were not scheduled correctly and would
+ cause 100% CPU usage.
+ - Small improvements to the profiler to also log incomplete
+ graph status.
+ - a new tool pw-top was added that prints real-time performance
+ stats of the graph.
+ - the rtkit module now sets the nice level to -11
+
+- Session-manager
+ - The session manager would sometimes link dont-reconnect
+ nodes to another node, which would leak monitor streams in
+ pipewire-pulse.
+ - The session manager now has configuration files. Config files
+ can also be placed in the user home directory to make custom
+ configurations.
+ - The session managers now creates unique device and node
+ names for alsa and v4l2 devices.
+
+- Device support
+ - Many improvements in Bluetooth codecs, LDAC stuttering,
+ AptX-HD negotiation, LDAC ABR support
+ - Bluetooth supports AAC audio now.
+ - Many fixes to Bluetooth SCO transport used in headsets.
+ - inotify support in device monitors
+ - ACP was synced with the latest pulseaudio code
+ - Fix a bug in enumeration of device ports.
+
+- PulseAudio server
+ - seek flags and offset are now supported, making gstreamer
+ pulse elements work better.
+ - Fix a crasher bug in pipewire-pulse, we sometimes would
+ write too much to the ringbuffer
+ - Fix some memory leaks in error cases.
+ - Fix handling of NULL string to locate default sink/source
+
+- JACK layer
+ - Ports can also be found with the aliases now, making
+ qjackctl work in more cases.
+
+
+PipeWire 0.3.18
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - More work in the PulseAudio server. It should be compatible
+ with more applications.
+ - Bluetooth now support extra codecs such as AptX/HD and LDAC.
+ - Support for virtual sources and sink was improved a lot.
+ - Added a new pw-dump tool to dump the objects in JSON formats
+ and for filtering them with tools like jq.
+ - Many more stability fixes and improvements.
+
+- PipeWire improvements
+ - Silence some harmless warnings
+ - pw-cli can now be used to set parameters.
+ - Streams now perform the correct channel mapping when linked
+ to non-standard multichannel devices. Previously channels
+ would get swapped.
+ - port, node and device params are now cached in the server.
+ This avoids opening and closing devices whenever some client
+ enumerates formats, which improves performance a lot,
+ especially in cases where opening a device is slow.
+ - Add a command to keep a device open during negotiation. This
+ is used to enumerate and set a format while opening the
+ device just once, improving performance.
+ - The null-sink scheduling was fixed.
+ - A memory corruption bug was fixed in format conversion, this
+ could cause crashes, silent channels or other undefined
+ behaviour.
+ - There is now a simple JSON parser.
+
+- Session-manager
+ - Settings files are now stored in JSON. With the json parser
+ this is easier to parse and extend
+
+- Device support
+ - Bluetooth now supports additional codecs: LDAC, AptX and
+ AptX HD. LDAC is known to not work very well yet.
+ - ALSA devices will now default to the max supported channels
+ if nothing else is specified. This makes it possible to use
+ 8+ channel cards with the alsa-pcm module, which is not
+ supported with the default alsa-acp module.
+ - Enable mSBC support in oFono.
+ - Add an option to disable hardware mixers
+ - ALSA now improves support for batch devices.
+ - The udev rules had references to Pulseaudio removed in order
+ to not create conflicts.
+ - Fix a potential crash in bluetooth devices when
+ disconnecting.
+ - UCM cards now use HW volume when possible.
+
+- PulseAudio server
+ - The id can now be used as the name to locate cards and
+ devices
+ - Report streams with planar formats as well
+ - Better error reporting when stream create fails
+ - module-null-sink can now handle channels, rate and
+ channel_map properties
+ - Add support for 3 types of virtual devices: source,
+ sink and duplex.
+ - set-port was fixed
+ - Some buffer parameters were tweaked to improve
+ performance, compatibility and stuttering with lower
+ latency.
+ - NULL can be used as a name for the device sink/source
+ - Support lookup of monitor names
+ - Set properties more like pulseaudio so that some
+ clients (Teamspeak) don't crash anymore
+
+PipeWire 0.3.17
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Fix crasher bug for kwin when screensharing stopped.
+ - Massive improvements and compatibility fixes in the
+ PulseAudio server.
+ - The session manager now has a config directory in
+ /etc/pipewire/media-session.d/ It will look for files there
+ to activate session manager modules. Packagers can use
+ this to only activate the audio modules when the PulseAudio
+ server, libjack.so or the alsa modules are installed.
+
+- PipeWire improvements
+ - We now clear hooks before adding them. Some application
+ did not clear them and had random data for the destroy
+ callback.
+ - Return -ENOENT from unknown resources so apps can handle
+ this better. It's a common problem when an app tries to
+ introspect and object but it disappeared before the message
+ reached the server. Apps should ignore this.
+ - channelmap information is now passed with the volume
+ settings.
+ - DMABuf is not mmapp()ed anymore with the FLAG_MAP_BUFFERS in
+ the stream or filter. This is because DMABuf usually
+ requires more that just a simple mmap and is better left
+ for the application.
+ - increase the maximum number of ports for a client-node.
+ - adapter and node-factory now support the linger option to
+ keep the objects alive after the creating client disconnected.
+
+- Device support
+ - ALSA now handles error in close(), like when unplugging a
+ USB device.
+
+- Session-manager
+ - The session manager is now handling DONT_RECONNECT streams
+ without a target node. They get connected to a default node
+ once and then fail to reconnect.
+ - The session manager now exposes the stream setting as
+ metadata. This makes it possible for other components, such
+ as pulse-server to use this information. Information is stored
+ as a json object for easier consumption.
+ - The session manager now has a config directory in
+ /etc/pipewire/media-session.d/ packagers can use this
+
+- PulseAudio server
+ - Pulse server now acquire the dbus name.
+ - Improvements in timing and compatibility with many apps.
+ - The stream-restore extension is now implemented so that
+ the event volume can be configured.
+ - Many stability fixes and improvements.
+ - Fix some issues with module-load/unload
+
+
+PipeWire 0.3.16
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - Fix screensharing for old 0.2 clients
+ - Many pulse-server improvements. There is now a
+ pipewire-pulse binary that is the preferred solution for
+ PulseAudio compatibility. The replacement libpulse
+ libraries are now deprecated. This also makes audio in
+ Flatpak work.
+
+- PipeWire improvements
+ - Fix cleanup of listeners everywhere. Force remove of
+ listeners in _destroy to avoid crashes.
+ - Add support for a journald logger module.
+ - Various memory leak fixes
+ - Silence some warnings that spammed the logs.
+ - Fix flush in pw_stream. This fixes small glitches when
+ switching streams in music players.
+ - Various FreeBSD fixes and improvements.
+ - Fix some crashes when destroying objects.
+
+- Device support
+ - Reload the ALSA configuration when creating a node so that
+ hotplugged devices work in all cases.
+ - Fix memory leaks in ACP library. This also fixes issues
+ where the mixer device was not closed.
+ - Bluetooth now has support for the mSBC codec for SCO
+ source and sink.
+
+- pulse-server
+ - Many introspection and compatibility improvements. It should
+ now be as good or better than the replacement library.
+ - Implement sample cache to make notification events work.
+
+- JACK layer
+ - handle errors when linking, fixes jack_connect hang when
+ the ports were already linked.
+
+
+PipeWire 0.3.15
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - This is a quick update to fix critical issues with the
+ 0.3.14 update, which broke screen sharing and accidentally
+ enabled the experimental pulse-server.
+ - Fix some compatibility issues in pulse-server with
+ pavucontrol and fix an issue that would block the complete
+ server.
+
+- PipeWire improvements
+ - Permission checks for new clients are now done from a
+ global context, which makes it possible to assign initial
+ permissions to objects.
+ - Handle EINTR everywhere
+ - Fix an issue with the node state changes where a quick
+ pause/play would hang a client.
+
+- Session manager improvements
+ - Disable the bluez5 and pulse-bridge modules by default because
+ they interfere with pulseaudio. These options should only be
+ enabled if pulseaudio is removed or disabled in the system.
+ - Fix an issue where the session manager could end up in
+ infinite recursion while scanning for things to do.
+ - The session manager will now always configure nodes to remix
+ to the channel configuration of the device. This fixes the case
+ where mono streams would only end up on one channel of a stereo
+ device.
+
+- Device support
+ - Initial merge of A2DP extra codec support using the new bluez5
+ API.
+
+- pulse-server
+ - Create the runtime directory when it doesn't exist.
+ - Don't ever block the server, use non-blocking IO everywhere.
+ - Fill description of profiles with the name if not otherwise set,
+ this fixes a crash in pavucontrol.
+ - the connection debug category will now also debug pulse
+ messages.
+ - Respect the no_remix flag to make the control panel channel
+ check work.
+
+- ALSA plugin
+ - implement pause
+
+
+PipeWire 0.3.14
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Highlights
+ - This release focuses on bugfixes and stability
+ improvements.
+ - A new experimental pulse-server module was added. This
+ module implements the pulseaudio protocol on top of
+ PipeWire and can be used to make flatpaks work with
+ PipeWire. It looks like this might be a better way
+ forward compared to the libpulse.so replacement library.
+ - A2DP bluetooth was reworked. Playback should work a lot
+ better now. Support was also added to automatically link
+ an A2DP source to a playback device, which makes it possible
+ to use PipeWire as a bluetooth receiver as well.
+ - Improvements to the routing and volume restore features
+ of the session manager.
+
+
+- PipeWire improvements
+ - The channelmixer does not normalize volumes anymore. Volumes
+ are only normalized for monitoring streams now.
+ - Streams can actually start in the inactive state now.
+ - The channelmixer can now also convert volume updates from one
+ channel layout to another. This makes saved volumes work
+ even when streams have different channel layouts.
+ - Clients are only registered after the properties have been
+ updated.
+ - Links now have a new active state.
+ - Drivers can now also specify a minimum quantum. This makes it
+ possible for bluetooth devices to specify an optimum quantum
+ for the given codec settings and MTU.
+ - The amount of data sent over the socket was reduced by only
+ sending the data that changed.
+ - Client objects are now exposed after they uploaded their
+ properties, which makes the new object more useful.
+
+- Tools improvements
+ - pw-cat will now add metadata to the PipeWire streams.
+
+- Session manager improvements
+ - Fix crashes when reading bad data in stored settings.
+ - volume and routing is improved. Settings are now remembered
+ per application or media-role.
+ - The session manager remembers the last device used per stream
+ - Fix a bug when moving streams where it could sometimes end
+ up with linking a stream to multiple devices.
+ - Use RTKit to set realtime priority on the data thread in the
+ session manager. This improves performance of the pulse-server
+ and bluetooth devices.
+ - Add a new property to mark streams that want to capture from
+ the monitor of the default sink.
+ - NODE_TARGET can now also contain the node name. This avoids
+ some lookups in the pulseaudio layer when selecting target
+ nodes by name.
+ - the -e and -d options are more usable now and can be used to
+ add and remove modules from the default list of modules.
+
+- Device support
+ - v4l2: add some workarounds for buggy drivers. Add Limited
+ support for droidcam.
+ - ACP: improve selection of default port and profiles.
+ - ACP: add support for using the hardware mixer for more than
+ 8 channel streams.
+ - ACP: support the new port type and availability group found
+ in PulseAudio.
+ - A2DP bluetooth timings were reworked. Automatic linking of
+ A2DP sources was added to make it possible for PipeWire to
+ act as a bluetooth receiver. The code was reworked to allow
+ other codecs such as APTX and LDAC in the future.
+ - Try harder to recover from ALSA errors.
+
+- GStreamer improvements
+ - Fix some crashes in the monitor that cause
+ gnome-initial-setup to crash.
+
+- PulseAudio layer improvements
+ - Many compatibility improvements. Improved playback in
+ chrome. Fix a crash in firefox when the daemon is stopped.
+ - Fix a leak in the formats.
+ - Fix !ADJUST_LATENCY streams like paplay.
+ - Make the device option in paplay work.
+ - Fix volume/mute notifications, this makes plasma volume updates
+ work again.
+ - Do the conversion between PulseAudio cubic volumes and PipeWire
+ linear volumes. Volume levels should behave now like they did
+ with PulseAudio.
+
+- JACK layer improvements
+ - Return an error when we run out of midi events. Some application
+ rely on this behaviour.
+
+- ALSA plugin improvements
+ - The ALSA plugin now also supports the node name in the
+ playback_node and capture_node properties.
+
+
+PipeWire 0.3.13
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- PipeWire improvements
+ - Add pw-reserve tool to reserve or monitor a device on DBus.
+ - Install spa-resample, a tool to resample a file.
+ - Install spa-acp-tool, a tool to inspect the card profile.
+ - Various fixes and improvements
+ - Fix a bug in pw-stream where a capture stream could run out
+ of buffers and become silent.
+ - Rework the processing loops in the adapter and stream. There
+ is now less latency in PulseAudio and ALSA layers.
+
+- Session manager improvements
+ - Improve the device reservation code. We now try to acquire
+ the device using the dbus device reservation API before we
+ probe the device. This avoids conflicts with a running
+ PulseAudio where devices would disappear (because they were
+ locked by the other process).
+ - Don't fail on invalid input from the config files.
+ - Audio devices now have the same name as what PulseAudio
+ would assign.
+
+- Device support
+ - v4l2: try to use the format before enumerating the size and
+ framerate. Some drivers don't check the format and might now
+ work better.
+ - v4l2: Fall back to MMAP when EXPBUF fails. Fix MMAP access,
+ just export the fd and the mapoffset. This should make more
+ devices work.
+ - Fix crash in ALSA Card Profile (ACP) code.
+ - ACP: fix selection of default profile. Prefer any possibly
+ available profile over 'Off'. This makes some card at least
+ start with something.
+ - Fix soft volume. After setting the volume to 0, it would stay
+ at 0 until pushed over the max volume. This should fix
+ various volume related issues.
+
+- PulseAudio layer improvements
+ - Rework the buffering and latency measurements and tweak the
+ buffer attributes. This should make browsers and media
+ players work better. This should also improve speechd
+ performance.
+
+- JACK layer improvements
+ - Fix compilation against newer JACK.
+
+
+PipeWire 0.3.12
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- PipeWire improvements
+ * the channelmap converter now handles unknown and strange
+ channellayouts much better.
+ * the resampler is now cleared correctly, avoiding clicks and
+ pops at the start of sound.
+ * Fixes for various crasher bugs. (paplay drain, vlc shutdown,
+ pactl info, ...)
+ * Fix a race condition in the node state changes that caused
+ all kinds of sync and other issues (vlc, mpv, ...)
+ * Improve the binary name property of applications
+ * Fix the scheduling again of nodes that always need a driver
+ such as the jack clients.
+
+- Session manager improvements
+ * fix routing to default nodes. Sometimes nodes were not routed to
+ the default node (bluetooth)
+
+- Device support
+ * disable channelmap from ALSA by default. This is what PulseAudio
+ does and thus provides better compatibility.
+ * fix a bug in how the resampler was used in the ALSA source,
+ causing distortion and errors when using low latency capture
+ clients. (Discord, webrtc, ...)
+ * Small bluetooth improvements. More work is needed for reliable
+ bluetooth playback.
+
+- GStreamer plugins
+ * the device provider now stops the processing loop before shutting
+ down, which avoids crashes (gnome-initial-setup).
+
+- PulseAudio layer improvements
+ * the buffer attributes were reworked to ensure compatibility with
+ many more applications such as mpv and audacious.
+ * the pulseaudio layer will now try hard to not hand out invalid
+ channel maps to the application. (avoids crashes in
+ gnome-volume-control). The channel map will now also look more
+ like what PulseAudio does.
+ * the @DEFAULT_SINK/SOURCE/MONITOR@ wildcards now work. This
+ fixes the problem with volume keys when they are bound to
+ scripts using pactl and the default sink/source wildcards.
+ * the PIPEWIRE_LATENCY environment variable now works again
+ * Fix some leaks of ports and port info. Also fix the leak of the
+ context when the mainloop is stopped.
+ * The sink/source format_info array is now filled up completely,
+ this is actually not implemented yet in the real PulseAudio.
+
+- JACK layer improvements
+ * jack now returns version 3.0.0 and has PipeWire in the version
+ string so that apps can report this.
+
+
+PipeWire 0.3.11
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- PipeWire improvements
+ * Properly cleanup the mixer structures when a port is removed,
+ this should fix client crashes related to port config changes
+ and other random crashes.
+ * Optimize the preferred formats in the audio converter. Higher
+ quality formats with higher performance are chosen first.
+ * Make sure the time reported by pw_stream is always increasing,
+ even when the driver and clock changes.
+ * There is now also a system service and socket that can be used
+ to enable PipeWire systemwide. This is however not recommended
+ and disabled by default.
+ * Fix channelmixer 5.1 to stereo mix matrix. It was not reading
+ the conversion matrix correctly and cause channels to be
+ dropped. The channelmixer will now also normalize the volume,
+ like what pulseaudio does.
+ * The channelmixer will now just copy channels when no layout
+ has been given. It has also optimized paths for this. This
+ makes it possible for apps to request > 8 channels from the
+ alsa plugin (ardour).
+ * Port, Node and Link will now also emit an error on the
+ resources in addition to updating the error in the info. This
+ would make it easier to track negotiation errors in the session
+ manager later.
+ * many small fixes and cleanups.
+ * Fix compatibility:
+ + DOSBox: fix crash because of double free in pw_stream
+
+- Session manager improvements
+ * The session manager will now try to configure the client to
+ the channel configuration of the sink/source. It will only
+ do this for downmixing, never for upmixing and also never
+ when the client has the dont-remix property set. It will
+ also renegotiate the channel layout when moving a stream to
+ a new sink/source.
+ * Configuration state is now saved in XDG_CONFIG_HOME.
+ Previously it was saved in $HOME/.pipewire-media-session/
+ You can migrate the state by moving the files to
+ $XDG_CONFIG_HOME/pipewire-media-session (or
+ $HOME/.config/pipewire-media-session as a fallback when
+ XDG_CONFIG_HOME is not set).
+
+- Device support
+ * Bluetooth sources and sinks should work better now.
+ * There is now also a new bluetooth backend using hsphfpd.
+ * fix the ALSA UCM Off profile for alsa pcm devices
+ * improve ALSA port and profile switching. The ACP device will
+ now switch to the best port and profile when availability
+ changes.
+
+- PulseAudio layer improvements
+ * Implement some more callbacks. The pulse layer will now also
+ notify applications of stream moved, started and latency
+ changes.
+ * Fix error code when an object was not found. We now return
+ PA_ERR_NOENTITY instead of PA_ERR_INVALID.
+ * Add some support for loading new null sinks. Applications such
+ as pulseeffects use this. Note that pulseeffects does not yet
+ work reliably but can start now.
+ * Improve handling of profile and port updates, it should work
+ much more reliable now. Apps should now also again receive
+ volume updates from sinks/sources.
+ * Fix compatibility:
+ + openal-soft 1.20
+ + pavucontrol (checks PA_ERR_NOENTITY)
+
+- JACK layer improvements
+ * improve default source and sink handling. It was not updated
+ correctly in all cases.
+ * add samplerate and period to the pw-jack wrapper to easily
+ configure the desired samplerate and period for the app.
+
+- ALSA plugin improvements
+ * Add a mixer entry in the alsa config file.
+ * Implement support for planar types, rework the processing
+ function to make it more robust.
+ * refuse to load the alsa plugin when linked against 0.2. This
+ catches some old apps linked against 0.2 that want to use the
+ alsa plugin.
+ * Fix compatibility:
+ + linphone (ALSA SIGFPE when _status() is called
+ before _prepare()).
+
+PipeWire 0.3.10
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Many improvements to the pulse layer.
+ * GStreamer pulsesink element now works.
+ * Fixes some segfaults.
+ * Enable rtkit for client threads.
+ * fixes capture of monitor stream by name
+ * implement some more extensions, this makes paman
+ work and removes some warnings.
+
+- Many improvements to the GStreamer elements
+ * negotiation rework, avoid calling GStreamer methods from
+ the PipeWire callbacks because they might block and cause
+ deadlocks.
+ * Add support for non-string property values.
+ * improve stability after buffer and format
+ renegotiation.
+ * Rework the device provider.
+ * pipewiresink can now provide a stream that can
+ be consumed by apps like cheese.
+
+- Many improvements to the JACK layer:
+ * Rework the buffer_size callbacks. Make sure we call
+ the callback from a 'safe' thread and that we don't
+ call the process callback while the application is
+ handling the callback. This improves stability in
+ apps like Carla when PipeWire dynamically changes
+ the buffer size.
+ * Improve compatibility with apps that call
+ get_buffer_frames() with a 0 size (calfjackrack)
+ * JACK can now create nodes that can be set as a
+ sink/source in PulseAudio/ALSA apps (you can make an
+ effects rack and set that as default sink for
+ apps).
+
+- Added a group id property for nodes. This makes it
+ possible to schedule nodes with the same driver even
+ when they are otherwise not linked together. To make
+ this work well a new flag needed to be added to nodes
+ to signal when they are ready for processing.
+
+ Together with the GStreamer fixes, this makes things
+ like:
+
+ gst-launch-1.0 -v pipewiresrc path=51 stream-properties="props,node.group=1" !
+ audio/x-raw ! pipewiresink stream-properties="props,node.group=1"
+
+ work as expected with PipeWire managing the resampling
+ to keep the clocks of the devices in sync.
+
+ This can later also be used to force devices to be grouped
+ together to create a JACK-like scheduling group.
+
+- Streams and filter now use PIPEWIRE_NODE and
+ PIPEWIRE_LATENCY env variables as fallback.
+
+- ACP add per device port list. This makes UCM devices
+ expose the right ports.
+
+- Fix some segfaults in ACP and UCM.
+
+- make pw-cat use the metadata to find default devices.
+
+- The media session can now save and load audio device
+ Profiles and Routes (volumes), stream volumes and
+ the default sink and sources.
+
+PipeWire 0.3.9
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Fix bad audio in chrome
+
+- Remove some errors that are not real errors.
+
+- Fix 100% cpu when disconnecting devices.
+
+- Improve pulseaudio introspection of formats
+
+- Fix JACK metadata handling, carla can now monitor the
+ port it creates and insert midi.
+
+- Add a new permission bit (M) that is needed to be able
+ to configure metadata on an object. Improve security of
+ metadata some more, only allow metadata on objects that
+ are visible to the client setting the metadata.
+
+- Add support for videocrop in the GStreamer elements.
+
+- Improve handling of the runtime directory for the
+ server sockets. Add some reasonable fallback when
+ XDG_RUNTIME_DIR is not set, as suggested in the spec.
+
+- Improve ALSA device names from ACP.
+
+- Fix various crasher bugs. One in the pulse layer, one in
+ the session manager.
+
+- Make alsa plugin respect the PIPEWIRE_REMOTE env variable.
+
+- Various compile fixes.
+
+
+PipeWire 0.3.8
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Fix an embarrassing crasher in the JACK layer when
+ metadata keys were removed.
+
+- Make it possible to add properties to jack clients with
+ a PIPEWIRE_PROPS env variable. This can be used to make
+ JACK nodes look like a device (like an effects rack).
+
+- Improvements in the session manager in how it links
+ ports. Now it will try to link matching channels first
+ and be more intelligent otherwise. The session manager
+ will also configure the stream to the device port
+ configuration when needed.
+
+- Add ofono backend for Bluetooth HeadSet support.
+
+- Improve default source and sink handling. They are now
+ stored with their id, instead of name, in the metadata.
+ This makes it work better with JACK because of JACK's
+ limited name length.
+
+- Improve environment variables to make it possible to
+ create and connect to servers other than "pipewire-0".
+ Implement this in pulseaudio, JACK and alsa layers.
+
+- Add an alsa mixer plugin so that alsamixer works with
+ PipeWire. It will configure the default source/sink
+ volumes.
+
+- Fix capture devices. There was something wrong with how
+ the resampler was used that caused corruption in the
+ signal when the resampler was active.
+
+- We now ship alsa card paths, profile-sets configuration
+ files and udev rules so that we don't have to rely on
+ the pulseaudio ones.
+
+- Many build and stability fixes.
+
+
+PipeWire 0.3.7
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Improved PulseAudio compatibility. The alsa card profile
+ code was reused from PulseAudio. Devices now support
+ all profiles, ports, jack detection, UCM and hardware
+ mixers that PulseAudio implements. There should not
+ be (almost) any difference between PipeWire and PulseAudio
+ in how it presents and manages devices.
+ Other missing API pieces such as the default sink/source
+ and move_stream are implemented now. At this point
+ it should be possible to replace PulseAudio with the
+ compatibility layer for those who want to try.
+
+- Many fixes and improvements to the GStreamer elements.
+ pipewiresrc now has the ability to periodically resend
+ the last frame. This makes it possible for use-cases like
+ screensharing to only update the screen on changes while
+ still keeping the client side encoder busy. PipeWire
+ elements can now also share a connection between them.
+
+- Improvements to the bluetooth nodes. Dynamically adding
+ and removing devices should work much smoother now. Many
+ fixes and improvements to a2dp and sco nodes.
+
+- Reduced memory usage by using less pre-allocated memory
+ where possible. JACK clients are especially using less
+ memory.
+
+- Support for passive links is added again. These are links
+ that don't cause the associated driver to become active.
+ This makes it possible to have blocks of effects+sinks go
+ to suspend as a group when not in use.
+
+- Both consumers and producers can now ask to renegotiate
+ the format. This required some cleanups and improvements
+ to how links and node states were handled. More work is
+ needed to implement more use cases.
+
+- Important fixes to how memory is shared with clients. Memory
+ was not correctly freed in all cases, which would result
+ in reuse of the wrong memory.
+
+- Support for planar formats for audio and video was added.
+
+- Improved error handling in the session manager.
+
+- Metadata is now used to manage default audio source and
+ sink devices. The session manager will try to link streams
+ to the default device. Changing the default device will
+ move streams to the new device. PulseAudio and JACK layers
+ respect the default source/sinks.
+
+- Metadata is used to tag the desired output device for
+ a stream and the session manager will move streams when
+ the metadata changes. The PulseAudio layer uses this to
+ implement the move_stream feature.
+
+- Many fixes to the security modules. The session manager now
+ has a flatpak module that grants permissions to flatpak
+ apps. The PulseAudio layer now respects the permissions of
+ objects. Security related properties are made read-only
+ now. Different access modules can now coexist.
+
+- The portal module has been split up in 2 parts:
+ 1) a part living in the daemon that monitors the portal
+ dbus owner and tags all clients from this PID. This
+ part has to run in the daemon in order to securely
+ tag the clients.
+ 2) a part in the session manager that uses the permission
+ store to manage the permissions of portal managed
+ clients.
+
+
+PipeWire 0.3.6
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Extensive memory leak fixing and stress testing was done.
+ A big leak in screen sharing with DMA-BUF was fixed.
+- Compile fixes
+- Stability improvements in jack and pulseaudio layers.
+- Added the old portal module to make the Camera portal
+ work again. This will be moved to the session manager in
+ future versions.
+- Improvements to the GStreamer source and sink shutdown.
+- Fix compatibility with v2 clients again when negotiating
+ buffers.
+
+
+PipeWire 0.3.5
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Compiler fixes
+- Add pw-midiplay and pw-midirecord aliases
+- Add pw-mididump tool
+- Add pw-metadata tool to inspect, add and remove metadata
+ for objects.
+- Docs updates, man pages
+- install alsa config files
+- Fix linked sink/source in pulseaudio
+- ratelimit graph processing warnings
+- improve buffer handling in GStreamer elements
+- Fix power usage by removing the queue for the alsa
+ sequencer system announce messages.
+- Fix metadata clear() method dispatch.
+- Improve parameter enumeration, make it possible to detect
+ missing parameters vs no-compatible parameters so that we
+ can use defaults in the first case and error in the second
+ case.
+- Fix cleanup of proxy objects. Stability improvements on
+ plug/unplug in session manager.
+- Make it possible to set log level from config file
+- improve debug of param negotiation errors. Log the
+ parameters to stderr/journal.
+- Make it possible to configure global logger
+ implementation.
+- Fix NEON detection
+- JACK and PulseAudio compatibility improvements
+
+
+PipeWire 0.3.4
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- A quick update with some important stability fixes.
+
+
+PipeWire 0.3.3
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- NEON optimizations for audio conversion (32 and 64 bits)
+- rework of session manager implementation
+- Add option to disable modules in the session manager
+- Release midi hardware devices when suspended
+- various build fixes
+- Clean up options of various utils
+- Stability improvements
+- Mayor improvements in pulseaudio emulation. Improved
+ timings and compatibility.
+- Implementation of drain and flush in pulse and alsa
+ emulation.
+- Implement poll on file descriptors.
+- Improvement of metadata for jack emulation.
+- Fix memory and thread problems in jack emulation.
+- Simplification of state changes. Should make more use
+ cases work in the jack emulation.
+- Improvements in the gstreamer elements. Removal of
+ extra internal queue. pipewiresink can now be used to
+ play audio.
+- Add pw-jack and pw-pulse scripts to run pulseaudio and
+ jack applications with the right library path.
+
+
+PipeWire 0.3.2
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- build fixes
+- Added support for data type negotiation. This makes it
+ possible for a client to say that it can handle DMABuf
+ and MemFd and then let the server select a compatible
+ format.
+- Handle errors when enumerating parameters better.
+- Add support for rate, format, channels and period_bytes
+ to the alsa config file to restrict what alsa apps can
+ negotiate.
+- Fix JACK midi output.
+- Optimizations in common audio format conversions using
+ AVX2. Small optimizations to plugins.
+- Change the vulkan compute example to an MIT licensed
+ shader.
+- Remove some hardcoded defaults in the audio and video
+ processing and use the values from the processing
+ context. This also fixes the vulkan example.
+- Correct the documentation and defaults in the daemon
+ config file.
+- Fix alsa and v4l2 buffer recycle. A paused client could
+ cause the server to leak all buffers.
+- Remove some warnings that should be ignored.
+- Fix a crash in the bluez5 plugins.
+- Try to select higher quality formats first when
+ negotiating a format with an audio device.
+- Fix an infinite loop in udev detection in some cases.
+- Add non-interactive mode to pw-cli. You can now just
+ do "pw-cli ls Port" to get a listing of all ports.
+ pw-cli will now also connect to the default server by
+ default and has options to select a different server.
+- Allow the server to go up to the maximum quantum (8192
+ samples or ~=180ms) if a client explicitly wants this.
+
+
+PipeWire 0.3.1
+
+This is a bugfix release that is API and ABI compatible
+with previous 0.3.x releases.
+
+- Don't load the rtkit module by default. It can cause a
+ sigkill, which is not desirable for mutter, for example.
+ Only enable this for the jack library for now.
+- Don't use pthread cancel by default because it uses a
+ signal that might crash some apps. Only use it for
+ the jack library because jack clients really expect this.
+- Build fixes for -Werror=suggest-attribute=format
+- improve error messages, don't report harmless errors and
+ warnings. Try to send error messages to the proxy that
+ started the operation or is the owner of the object.
+- pw-cat: midi improvement, add midi recording and dump
+ in verbose mode
+- fix properties when loading spa-nodes from the config
+- Fix and update some examples
+- jack: check arguments and don't crash when invalid
+- Fix buffer memory upload.
+- jack: fix compatibility with zrythm. Fix timemaster
+ install, improve sample_rate callback. Fix reposition
+ handling.
+- fix crash in port after buffer negotiation error.
+- add support for control ports in pw_filter
+- fix cleanup of the metadata module
+- improve param enumeration.
+- Clear stream buffers when the format is cleared.
+- Add create-object command in the config file to create
+ object from a factory.
+- Fix crash after the driver was not removed from unassigned
+ nodes. Also properly pause inactive nodes.
+- Use "true" and "false" in properties when we are talking
+ about a boolean.
+- pulseaudio: improve compatibility
+
+
+PipeWire 0.3.0
+
+The 0.3 release is a major milestone in the development of
+PipeWire. It features a complete redesign of the scheduling
+mechanisms that make it possible to run a JACK compatibility
+layer with comparable performance to JACK2.
+
+The API has been reworked and is declared stable now. All
+development files and runtime paths are versioned so that
+future incompatible changes can be done without breaking
+existing applications.
+
+PipeWire 0.3 also includes a (now mandatory) session manager
+that populates and controls the PipeWire graph. This example
+session manager is very simple and not configurable. It is
+expected that future version will either switch to a more
+flexible session manager (like WirePlumber) or improve the
+configuration options of the example session manager.
+
+PipeWire 0.3 includes both PulseAudio, JACK and ALSA
+compatibility libraries that are known to support a wide range
+of applications. The ALSA library is pretty complete at this
+point. The JACK and mostly the PulseAudio compatibility
+libraries need more work. See the Wiki pages for the current
+compatibility problems. We do not yet encourage people to
+switch away from their existing audio solutions (PulseAudio
+or JACK) but we would love to hear from people who try it
+anyways. Future versions will mostly focus on improving
+compatibility further to make PipeWire a drop-in replacement.
+
+PipeWire comes with some GStreamer plugins to consume and
+produce data for PipeWire. The consumer (pipewiresrc) is
+working well in most cases. The sink (pipewiresink) is known
+to be somewhat problematic for now.
+
+PipeWire 0.2.97
+
+Eighth pre-release for upcoming 0.3:
+
+- Build fixes
+- pw-cat improvement: Fix remote name, add midi support
+- add device subscribe params for completeness
+- jack and pulseaudio compatibility fixes
+- Fix a bug in resampler, add quality option, tweaked quality
+ settings, tested now against https://src.infinitewave.ca/
+ testsignals and submitted results for publication.
+- Fix awkwardness in buffer negotiations, the default number of
+ buffers was 4 and jack could only handle 2, causing
+ corruption. Also implement negotiation of Step ranges.
+- Fix device reservation to work together with pulseaudio,
+ previously we would block pulseaudio.
+
+PipeWire 0.2.96
+
+Seventh pre-release for upcoming 0.3:
+
+- jack: improve compatibility
+- Fix unit test
+- Fix license of jack and alsa libs
+- Make start/stop more threadsafe
+- Fix rt-kit again, add params to configure things, increase default
+ soft/hard limits to avoid being killed.
+- version 0 compatibility improvements, tested with firefox, cheese,
+ GStreamer and chrome using compat layers.
+- Fix timing for gstreamer source
+- Require libspa in pkg-config file
+- Limit buffers to 16 to support old clients
+
+PipeWire 0.2.95
+
+Sixth pre-release for upcoming 0.3:
+
+- Fix tests for big endian some more
+- Improve v2 compatibility mode: improve type negotiation and
+ update_permissions
+- Workaround for firefox screen sharing
+
+
+PipeWire 0.2.94
+
+Fifth pre-release for upcoming 0.3:
+
+- Fix man page names
+- Fix jack set_sync_timeout
+- Improve JACK compatibility with apps that cache buffer pointers.
+- Improve mlock failure warning message, add property to configure
+ if mlock should be used.
+- Improve OBJECT_PATH in alsa objects
+- Install in versioned directory
+- Add pw-profiler tool
+- Improve pulseaudio compatibility wrt pa_operations
+- Thread safety fixes in remote nodes when activating/deactivating
+- Improve JACK names on duplicates
+- Add option to ignore failure when loading modules
+
+PipeWire 0.2.93
+
+Fourth pre-release for upcoming 0.3:
+
+- Fix unit tests on 32 bits
+- Append -pw version to pulse and jack libs. This way we can install
+ it next to the real libraries and use a symlink to enable it.
+- Improve jack support by killing threads with pthread_cancel. This
+ then also remove the eventfd from the data-loop, making it
+ maybe a little faster.
+- Fix jack_client_close() compatibility
+- Fix some segfaults in the session manager
+- Improve debug of protocol messages
+- Add examples options
+- Don't fail when alsa is not found
+- Fix some compiler warnings with a new spa_aprintf() helper.
+- Add pw-cat, the simple audio playback/record tool
+- Rename pipewire tools to pw- prefix
+- Add improve pw-cli object dump feature
+
+
+PipeWire 0.2.92
+
+Third pre-release for upcoming 0.3:
+
+- Improve old version check some more
+- Fix unit tests on little/big endian
+- Fix compilation when CPU has no optimisations
+- Install jack and pulse libraries
+- Handle -EACCESS in flatpack access module
+
+
+PipeWire 0.2.91
+
+It is mostly a bugfix release to make the new version install and
+run correctly in distros.
+
+- Install session manager, fix path to find the session manager
+- Fix alsa buffer reuse
+- Small fixes for crasher bugs
+- Implement pw_core_set_paused() to suspend/resume even
+ processing. This can be used when using multiple connections
+ to a daemon and one needs to pause one connection until the
+ other one completes an action. Used by session managers.
+- Improve old version check
+
+
+PipeWire 0.2.90
+
+This is the first pre-release of the 0.3 version. It consists of a
+major rewrite and is not API or ABI compatible with the 0.2
+branch.
+
+
+PipeWire 0.2.7
+
+This is mostly a bugfix release and is API/ABI compatible with
+previous 0.2 versions.
+
+Work is ongoing in the work branch that features a completely new
+scheduling method that will enable audio support. Some of these
+API changes are backported in this branch.
+
+- Add support for alsa-lib 1.1.9 which changed the include path
+- Improve error checking and reporting in the protocol
+- deviceprovider: fix probing without starting
+- add sentinel to some functions
+- compiler fixes for musl
+- Revert object tree permission checks that broke things, this is
+ probably not a good idea (and the tree of objects is going to
+ be removed later)
+
+
+PipeWire 0.2.6
+
+- Improve error checking for threads
+- Fix some memory and fd leaks
+- Fix compilation with C++ compilers and clang
+- DISABLE_RTKIT should now not try to use dbus at all
+- Camera Portal fixes:
+ - add Camera media.role
+ - Rename module-flatpak to module-portal
+ - Use the portal permissions store for camera checks
+- Actually use the passed fd in pipewiresrc
+- Make properties with "pipewire." prefix read-only
+- Add security label to client object
+- Enforce link permissions
+- Permissions of objects are now combined with parent permissions
+- Remove libv4l2 dependency, it is not used
+- Improve format negotiation in autolink #146
+- Try to avoid list corruption with event emission #143
+- Fix destroy of client-node memory corruption
+- Various small improvements
+
+PipeWire 0.2.5
+
+- build fixes for systemd
+- Add cursor and bitmap metadata. This can be used to send a cursor
+ sprite with the video stream.
+- permissions were set too strict for non-flatpak clients
+- Fix crash in loop caused by thread unsafe hook emission
+- Add more error checking for thread-loop
+- Small cleanups and bugfixes
+
+PipeWire 0.2.4
+
+- Install man pages in right directory
+- Add systemd socket activation
+- Various memory leak and corruption fixes in properties, dbus and
+ buffer mmapped memory.
+- Fix v4l2 crash on unplug
+- improve stream cleanup
+
+PipeWire 0.2.3
+
+- Fix deviceprovider caps introspection
+- Refcounting fixes in pipewiresrc
+- Remove clock interpolation from stream
+- Improve clock in gstreamer elements
+- Remove spalib
+- Fix crash with pw_map
+- Add version number to hook list
+- Improve driver mode in gstreamer elements
+- add daemon options
+- add man pages
+
+PipeWire 0.2.2
+
+- Increment API version and .so version
+
+PipeWire 0.2.1
+
+- Various fixes to memory handling
+- Fixes for shutdown
+- v4l2 fix enumeration of frame intervals
+- Make the daemon stop when the setup commands fail
+- Improve safety of hooks
+- Update stream API to more future proof version
+- Add more options to stream API such as scheduling in the
+ main thread and automatic mapping of buffers
+- Add version file and macros to check compile time and
+ runtime versions of pipewire
+- Future proof some structs
+
+
+PipeWire 0.1.9
+
+- Various build fixes
+- Do more permission checks
+- Add support for doing async connections. This can be used to
+ make connections through the portal later.
+- Fix device creation from the GStreamer device monitor
+- v4l2 experiment with controls
+- move rtkit to a module to avoid dbus dependency
+- use dmabuf allocator in gstreamer elements
+- Add DSP module for pro audio cases, remove jack module. The
+ idea is to make a replacement jack client library that talks
+ pipewire directly instead of trying to emulate a jack server.
+- Various memory handling improvements
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..612f500
--- /dev/null
+++ b/README.md
@@ -0,0 +1,218 @@
+# PipeWire
+
+[PipeWire](https://pipewire.org) is a server and user space API to
+deal with multimedia pipelines. This includes:
+
+ - Making available sources of video (such as from a capture devices or
+ application provided streams) and multiplexing this with
+ clients.
+ - Accessing sources of video for consumption.
+ - Generating graphs for audio and video processing.
+
+Nodes in the graph can be implemented as separate processes,
+communicating with sockets and exchanging multimedia content using fd
+passing.
+
+## Building and installation
+
+The preferred way to install PipeWire is to install it with your
+distribution package system. This ensures PipeWire is integrated
+into the rest of your system for the best experience.
+
+If you want to build and install PipeWire yourself, refer to
+[install](INSTALL.md) for instructions.
+
+## Usage
+
+The most important purpose of PipeWire is to run your favorite apps.
+
+Some applications use the native PipeWire API, such as most compositors
+(gnome-shell, wayland, ...) to implement screen sharing. These apps will
+just work automatically.
+
+Most audio applications can use either ALSA, JACK or PulseAudio as a
+backend. PipeWire provides support for all 3 backends. Depending on how
+your distribution has configured things this should just work automatically
+or with the provided scripts shown below.
+
+PipeWire can use environment variables to control the behaviour of
+applications:
+
+* `PIPEWIRE_DEBUG=<level>` to increase the debug level (or use one of
+ `XEWIDT` for none, error, warnings, info,
+ debug, or trace, respectively).
+* `PIPEWIRE_LOG=<filename>` to redirect log to filename
+* `PIPEWIRE_LOG_SYSTEMD=false` to disable logging to systemd journal
+* `PIPEWIRE_LATENCY=<num/denom>` to configure latency as a fraction. 10/1000
+ configures a 10ms latency. Usually this is
+ expressed as a fraction of the samplerate,
+ like 256/48000, which uses 256 samples at a
+ samplerate of 48KHz for a latency of 5.33ms.
+ This function does not attempt to configure
+ the samplerate.
+* `PIPEWIRE_RATE=<num/denom>` to configure a rate for the graph.
+* `PIPEWIRE_QUANTUM=<num/denom>` to configure latency as a fraction and a
+ samplerate. This function will attempt to change
+ the graph samplerate to `denom` and use the
+ specified `num` as the buffer size.
+* `PIPEWIRE_NODE=<id>` to request a link to the specified node. The
+ id can be a node.name or object.serial of the target node.
+
+### Using tools
+
+`pw-cat` can be used to play and record audio and midi. Use `pw-cat -h` to get
+some more help. There are some aliases like `pw-play` and `pw-record` to make
+things easier:
+
+```
+$ pw-play /home/wim/data/01.\ Firepower.wav
+```
+
+### Running JACK applications
+
+Depending on how the system was configured, you can either run PipeWire and
+JACK side-by-side or have PipeWire take over the functionality of JACK
+completely.
+
+In dual mode, JACK apps will by default use the JACK server. To direct a JACK
+app to PipeWire, you can use the `pw-jack` script like this:
+
+```
+$ pw-jack <appname>
+```
+
+If you replaced JACK with PipeWire completely, `pw-jack` does not have any
+effect and can be omitted.
+
+JACK applications will automatically use the buffer-size chosen by the
+server. You can force a maximum buffer size (latency) by setting the
+`PIPEWIRE_LATENCY` environment variable like so:
+
+```
+PIPEWIRE_LATENCY=128/48000 jack_simple_client
+```
+Requests the `jack_simple_client` to run with a buffer of 128 or
+less samples.
+
+
+### Running PulseAudio applications
+
+PipeWire can run a PulseAudio compatible replacement server. You can't
+use both servers at the same time. Usually your package manager will
+make the server conflict so that you can only install one or the
+other.
+
+PulseAudio applications still use the regular PulseAudio client
+libraries and you don't need to do anything else than change the
+server implementation.
+
+A successful swap of the server can be verified by checking the
+output of
+
+```
+pactl info
+```
+It should include the string:
+```
+...
+Server Name: PulseAudio (on PipeWire 0.3.x)
+...
+```
+
+You can use pavucontrol to change profiles and ports, change volumes
+or redirect streams, just like with PulseAudio.
+
+
+### Running ALSA applications
+
+If the PipeWire alsa module is installed, it can be seen with
+
+```
+$ aplay -L
+```
+
+ALSA applications can then use the `pipewire:` device to use PipeWire
+as the audio system.
+
+### Running GStreamer applications
+
+PipeWire includes 2 GStreamer elements called `pipewiresrc` and
+`pipewiresink`. They can be used in pipelines such as this:
+
+```
+$ gst-launch-1.0 pipewiresrc ! videoconvert ! autovideosink
+```
+
+Or to play a beeping sound:
+
+```
+$ gst-launch-1.0 audiotestsrc ! pipewiresink
+```
+
+PipeWire provides a device monitor as well so that
+
+```
+$ gst-device-monitor-1.0
+```
+
+shows the PipeWire devices and applications like cheese will
+automatically use the PipeWire video source when possible.
+
+### Inspecting the PipeWire state
+
+To inspect and manipulate the PipeWire graph via GUI, you can use [Helvum](https://gitlab.freedesktop.org/ryuukyu/helvum).
+
+Alternatively, you can use use one of the excellent JACK tools, such as `Carla`,
+`catia`, `qjackctl`, ...
+However, you will not be able to see all features like the video
+ports.
+
+`pw-mon` dumps and monitors the state of the PipeWire daemon.
+
+`pw-dot` can dump a graph of the pipeline, check out the help for
+how to do this.
+
+`pw-top` monitors the real-time status of the graph. This is handy to
+find out what clients are running and how much DSP resources they
+use.
+
+`pw-dump` dumps the state of the PipeWire daemon in JSON format. This
+can be used to find out the properties and parameters of the objects
+in the PipeWire daemon.
+
+There is a more complicated tool to inspect the state of the server
+with `pw-cli`. This tool can be used interactively or it can execute
+single commands like this to get the server information:
+
+```
+$ pw-cli info 0
+```
+
+## Documentation
+
+Find tutorials and design documentation [here](doc/index.dox).
+
+The (incomplete) autogenerated API docs are [here](https://docs.pipewire.org).
+
+The Wiki can be found [here](https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/home)
+
+## Contributing
+
+PipeWire is Free Software and is developed in the open. It is mostly
+licensed under the [MIT license](COPYING). Check [LICENSE](LICENSE) for
+more details about the exceptions.
+
+Contributors are encouraged to submit merge requests or file bugs on
+[gitlab](https://gitlab.freedesktop.org/pipewire).
+
+Join us on IRC at #pipewire on [OFTC](https://www.oftc.net/).
+
+We adhere to the Contributor Covenant for our [code of conduct](CODE_OF_CONDUCT.md).
+
+[Donate using Liberapay](https://liberapay.com/PipeWire/donate).
+
+## Getting help
+
+You can ask for help on the IRC channel (see above). You can also ask
+questions by [raising](https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/new)
+a gitlab issue.
diff --git a/autogen.sh b/autogen.sh
new file mode 100755
index 0000000..5e0c381
--- /dev/null
+++ b/autogen.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# Only there to make jhbuild happy
+
+if [ -z "$MESON" ]; then
+ MESON=$(which meson)
+fi
+if [ -z "$MESON" ]; then
+ echo "error: Meson not found."
+ echo "Install meson to configure and build PipeWire. If meson" \
+ "is already installed, set the environment variable MESON" \
+ "to the binary's path."
+ exit 1;
+fi
+
+mkdir -p builddir
+$MESON setup "$@" builddir # use 'autogen.sh --reconfigure' to update
+ln -sf builddir/Makefile Makefile
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
new file mode 100644
index 0000000..15f9ae1
--- /dev/null
+++ b/doc/Doxyfile.in
@@ -0,0 +1,68 @@
+PROJECT_NAME = PipeWire
+PROJECT_NUMBER = @PACKAGE_VERSION@
+OUTPUT_DIRECTORY = "@output_directory@"
+FULL_PATH_NAMES = NO
+JAVADOC_AUTOBRIEF = YES
+TAB_SIZE = 8
+OPTIMIZE_OUTPUT_FOR_C = YES
+EXTRACT_ALL = YES
+EXTRACT_STATIC = YES
+STRIP_FROM_PATH = @path_prefixes@
+STRIP_FROM_INC_PATH = @path_prefixes@
+SHOW_FILES = NO
+SHOW_INCLUDE_FILES = NO
+GENERATE_TODOLIST = NO
+GENERATE_TESTLIST = NO
+GENERATE_BUGLIST = NO
+QUIET = YES
+WARN_NO_PARAMDOC = YES
+HAVE_DOT = @HAVE_DOT@
+INPUT = @inputs@
+FILTER_PATTERNS = "*.c=@c_input_filter@" "*.h=@h_input_filter@"
+FILE_PATTERNS = "*.h" "*.c"
+RECURSIVE = YES
+EXAMPLE_PATH = "@top_srcdir@/src/examples" \
+ "@top_srcdir@/spa/examples" \
+ "@top_srcdir@/doc"
+EXAMPLE_PATTERNS = "*.c"
+
+REFERENCED_BY_RELATION = NO
+REFERENCES_RELATION = NO
+IGNORE_PREFIX = pw_ \
+ PW_ \
+ spa_ \
+ SPA_
+GENERATE_TREEVIEW = YES
+SEARCHENGINE = YES
+GENERATE_LATEX = NO
+
+MACRO_EXPANSION = YES
+EXPAND_ONLY_PREDEF = YES
+PREDEFINED = PA_C_DECL_BEGIN= \
+ PA_C_DECL_END= \
+ __USE_ISOC11 \
+ SPA_EXPORT \
+ SPA_PRINTF_FUNC \
+ SPA_DEPRECATED \
+ SPA_SENTINEL \
+ SPA_UNUSED \
+ SPA_NORETURN \
+ SPA_RESTRICT
+HTML_EXTRA_STYLESHEET = @cssfiles@
+
+MAX_INITIALIZER_LINES = 1
+SORT_MEMBER_DOCS = NO
+
+CALL_GRAPH = NO
+CALLER_GRAPH = NO
+CLASS_GRAPH = NO
+COLLABORATION_GRAPH = NO
+GROUP_GRAPHS = NO
+INCLUDED_BY_GRAPH = NO
+INCLUDE_GRAPH = NO
+GRAPHICAL_HIERARCHY = NO
+DIRECTORY_GRAPH = NO
+TEMPLATE_RELATIONS = NO
+
+# Fix up some apparent Doxygen mis-parsing
+EXCLUDE_SYMBOLS = "desc" "methods" "msgid_plural" "n" "name" "props" "utils" "start"
diff --git a/doc/api-tree.dox b/doc/api-tree.dox
new file mode 100644
index 0000000..0c5c8fe
--- /dev/null
+++ b/doc/api-tree.dox
@@ -0,0 +1,126 @@
+/**
+
+\defgroup api_pw_core Core API
+\brief PipeWire Core API
+\{
+\addtogroup pw_pipewire
+\addtogroup pw_main_loop
+\addtogroup pw_context
+\addtogroup pw_client
+\addtogroup pw_core
+\addtogroup pw_device
+\addtogroup pw_factory
+\addtogroup pw_link
+\addtogroup pw_loop
+\addtogroup pw_module
+\addtogroup pw_node
+\addtogroup pw_permission
+\addtogroup pw_port
+\addtogroup pw_proxy
+\addtogroup pw_registry
+\addtogroup pw_type
+\addtogroup pw_keys
+\}
+
+\defgroup api_pw_impl Implementation API
+\brief PipeWire Object Implementation API
+\{
+\addtogroup pw_impl_client
+\addtogroup pw_impl_core
+\addtogroup pw_impl_device
+\addtogroup pw_impl_factory
+\addtogroup pw_impl_link
+\addtogroup pw_impl_metadata
+\addtogroup pw_impl_module
+\addtogroup pw_impl_node
+\addtogroup pw_impl_port
+\addtogroup pw_buffers
+\addtogroup pw_control
+\addtogroup pw_data_loop
+\addtogroup pw_global
+\addtogroup pw_protocol
+\addtogroup pw_resource
+\addtogroup pw_thread_loop
+\addtogroup pw_work_queue
+\}
+
+\defgroup api_pw_util Utilities
+\brief PipeWire Utilities
+\{
+\addtogroup pw_array
+\addtogroup pw_conf
+\addtogroup pw_gettext
+\addtogroup pw_log
+\addtogroup pw_map
+\addtogroup pw_memblock
+\addtogroup pw_properties
+\addtogroup pw_thread
+\addtogroup pw_utils
+\}
+
+\defgroup api_pw_ext Extensions
+\brief PipeWire Extensions
+\{
+\addtogroup pw_client_node
+\addtogroup pw_metadata
+\addtogroup pw_profiler
+\addtogroup pw_protocol_native
+\addtogroup pw_session_manager
+\}
+
+\defgroup api_spa SPA
+\brief Simple Plugin API
+\{
+\addtogroup spa_buffer
+\addtogroup spa_control
+\addtogroup spa_debug
+\addtogroup spa_device
+\addtogroup spa_graph
+\addtogroup spa_node
+\addtogroup spa_param
+\addtogroup spa_pod
+\defgroup spa_utils Utilities
+Utility data structures, macros, etc.
+\{
+\addtogroup spa_ansi
+\addtogroup spa_utils_defs
+\addtogroup spa_dict
+\addtogroup spa_list
+\addtogroup spa_hooks
+\addtogroup spa_interfaces
+\addtogroup spa_json
+\addtogroup spa_keys
+\addtogroup spa_names
+\addtogroup spa_result
+\addtogroup spa_ringbuffer
+\addtogroup spa_string
+\addtogroup spa_types
+\}
+\defgroup spa_support Support
+Support interfaces provided by host
+\{
+\addtogroup spa_cpu
+\addtogroup spa_dbus
+\addtogroup spa_i18n
+\addtogroup spa_log
+\addtogroup spa_loop
+\addtogroup spa_handle
+\addtogroup spa_plugin_loader
+\addtogroup spa_system
+\addtogroup spa_thread
+\}
+\}
+
+\defgroup pw_stream Stream
+\{
+\}
+
+\defgroup pw_filter Filter
+\{
+\}
+
+\defgroup pwtest Test Suite
+\{
+\}
+
+*/
diff --git a/doc/api.dox b/doc/api.dox
new file mode 100644
index 0000000..880127e
--- /dev/null
+++ b/doc/api.dox
@@ -0,0 +1,89 @@
+/** \page page_api PipeWire API
+
+The PipeWire API consists of several parts:
+
+- The \ref pw_stream for a convenient way to send and receive data streams from/to PipeWire.
+
+- The \ref pw_filter for a convenient way to implement processing filters.
+
+- The \ref api_pw_core to access a PipeWire instance. This API is used
+by all clients that need to communicate with the \ref page_daemon and provides
+the necessary structs to interface with the daemon.
+
+- The \ref api_pw_impl is primarily used by the \ref page_daemon itself but also by the
+\ref page_session_manager and modules/extensions that need to build objects in
+the graph.
+
+- The \ref api_pw_util containing various utility functions and structures.
+
+- The \ref api_pw_ext for interfacing with certain extension modules.
+
+The APIs work through proxy objects, so that calling a method on an object
+invokes that same method on the remote side. Marshalling and de-marshalling is
+handled transparently by the \ref page_module_protocol_native.
+The below graph illustrates this approach:
+
+\dot
+digraph API {
+ compound=true;
+ node [shape="box"];
+ rankdir="RL";
+
+ subgraph cluster_daemon {
+ rankdir="TB";
+ label="PipeWire daemon";
+ style="dashed";
+
+ impl_core [label="Core Impl. Object"];
+ impl_device [label="Device Impl. Object"];
+ impl_node [label="Node Impl. Object"];
+ }
+
+ subgraph cluster_client {
+ rankdir="TB";
+ label="PipeWire client";
+ style="dashed";
+
+ obj_core [label="Core Object"];
+ obj_device [label="Device Object"];
+ obj_node [label="Node Object"];
+ }
+
+ obj_core -> impl_core;
+ obj_device -> impl_device;
+ obj_node -> impl_node;
+
+}
+\enddot
+
+It is common for clients to use both the \ref api_pw_core and the \ref api_pw_impl
+and both APIs are provided by the same library.
+
+- \subpage page_client_impl
+- \subpage page_proxy
+- \subpage page_streams
+- \subpage page_thread_loop
+
+
+\addtogroup api_pw_core Core API
+
+The Core API to access a PipeWire instance. This API is used by all
+clients to communicate with the \ref page_daemon.
+
+If you are familiar with Wayland implementation, the Core API is
+roughly equivalent to libwayland-client.
+
+See: \ref page_api
+
+
+\addtogroup api_pw_impl Implementation API
+
+The implementation API provides the tools to build new objects and
+modules.
+
+If you are familiar with Wayland implementation, the Implementation API is
+roughly equivalent to libwayland-server.
+
+See: \ref page_api
+
+*/
diff --git a/doc/custom.css b/doc/custom.css
new file mode 100644
index 0000000..43690cb
--- /dev/null
+++ b/doc/custom.css
@@ -0,0 +1,19 @@
+:root {
+ /* --page-background-color: #729fcf; */
+ --primary-color: #729fcf;
+ --primary-dark-color: #729fcf;
+ --header-background: #729fcf;
+ --header-foreground: rgba(255, 255, 255, 0.7);
+ --font-family: 'Source Sans Pro', 'Source Sans', sans-serif;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --code-background: #f5f5f5;
+ --code-foreground: #333333;
+ --fragment-background: #f5f5f5;
+ --fragment-foreground: #333333;
+ --fragment-keyword: #c7254e;
+ --fragment-link: #729fcf;
+ }
+}
diff --git a/doc/dma-buf.dox b/doc/dma-buf.dox
new file mode 100644
index 0000000..87d61eb
--- /dev/null
+++ b/doc/dma-buf.dox
@@ -0,0 +1,163 @@
+/** \page page_dma_buf DMA-BUF Sharing
+
+PipeWire supports sharing Direct Memory Access buffers (DMA-BUFs) between
+clients via the `SPA_DATA_DmaBuf` data type. However properly negotiating
+DMA-BUF support on both the producer and the consumer side require following
+a specific procedure. This page describes said procedure by using events and
+methods from the filter or stream API.
+
+Note: This article focuses mostly on DMA-BUF sharing from arbitrary devices,
+like discrete GPUs. For using DMA-BUFs created by v4l2 please refer to the
+corresponding paragraph.
+
+# Capability Negotiations
+
+The capability negotiation for DMA-BUFs is complicated by the fact that a
+usable and preferred optimal modifier for a given format can only be
+determined by the allocator. This allocator has to be invoked with the intersection
+of all supported modifiers for every client. As a result, the fixation of the
+modifier is delegated from PipeWire to the node responsible for
+allocating the buffers.
+
+## pw_stream_connect
+
+The stream parameters should contain two `SPA_PARAM_EnumFormat` objects for
+each format: one for DMA-BUFs, one for shared memory buffers as a fallback.
+
+Query the list of all supported modifiers from your graphics API of choice.
+Add a `SPA_FORMAT_VIDEO_modifier` property to the first stream parameter with
+the flags `SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE`. The
+value of the property should be set to a `SPA_CHOICE_Enum` containing one
+`long` choice per supported modifier, plus `DRM_FORMAT_MOD_INVALID` if the
+graphics API supports modifier-less buffers.
+
+Note: When a producer is only supporting modifier-less buffers it can omit
+the `SPA_POD_PROP_FLAG_DONT_FIXATE` (see param_changed hook, For producers).
+
+The second stream parameter should not contain any `SPA_FORMAT_VIDEO_modifier`
+property.
+
+To prioritise DMA-BUFs place those `SPA_PARAM_EnumFormat` containing modifiers
+first, when emitting them to PipeWire.
+
+## param_changed Hook
+
+When the `param_changed` hook is called for a `SPA_PARAM_Format` the client
+has to parse the `spa_pod` directly. Use
+`spa_pod_find_prop(param, NULL, SPA_FORMAT_VIDEO_modifier)` to check
+whether modifiers were negotiated. If they were negotiated, set the
+`SPA_PARAM_BUFFERS_dataType` property to `1 << SPA_DATA_DmaBuf`. If they were
+not negotiated, fall back to shared memory by setting the
+`SPA_PARAM_BUFFERS_dataType` property to `1 << SPA_DATA_MemFd`,
+`1 << SPA_DATA_MemPtr`, or both.
+
+While consumers only have to parse the resulting `SPA_PARAM_Format` for any
+format related information, it's up to the producer to fixate onto a single
+format modifier pair. The producer is also responsible to check if all clients
+announce sufficient capabilities or fallback to shared memory buffers when
+possible.
+
+### For Consumers
+
+Use `spa_format_video_raw_parse` to get the format and modifier.
+
+### For Producers
+
+Producers have to handle two cases when it comes to modifiers wrt. to the
+previous announced capabilities: Using only the modifier-less API, only the
+modifier-aware one, or supporting both.
+
+- modifier-less:
+ In this case only the modifier `DRM_FORMAT_MOD_INVALID` was announced with
+ the format.
+ It is sufficient to check if the `SPA_PARAM_Format` contains the modifier
+ property as described above. If that is the case, use DMA-BUFs for screen-sharing,
+ else fall back to SHM, if possible.
+- modifier-aware:
+ In this case a list with all supported modifiers will be returned in the format.
+ (using `DRM_FORMAT_MOD_INVALID` as the token for the modifier-less API).
+ On the `param_changed` event check if the modifier key is present and has the flag
+ `SPA_POD_PROP_FLAG_DONT_FIXATE` attached to it. In this case, extract all modifiers
+ from the list and do a test allocation with your allocator to choose the preferred
+ modifier. Fixate on that `EnumFormat` by announcing a `SPA_PARAM_EnumFormat` with
+ only one modifier in the `SPA_CHOICE_Enum` and without the
+ `SPA_POD_PROP_FLAG_DONT_FIXATE` flag, followed by the previous announced
+ `EnumFormat`. This will retrigger the `param_changed` event with an
+ `SPA_PARAM_Format` as described below.
+ If the `SPA_PARAM_Format` contains a modifier key, without the flag
+ `SPA_POD_PROP_FLAG_DONT_FIXATE`, it should only contain one value in the
+ `SPA_CHOICE_Enum`. In this case announce the `SPA_PARAM_Buffers` accordingly
+ to the selected format and modifier. It is important to query the plane count
+ of the used format modifier pair and set `SPA_PARAM_BUFFERS_blocks` accordingly.
+
+Note: When test allocating a buffer, collect all possible modifiers, while omitting
+`DRM_FORMAT_MOD_INVALID` from the `SPA_FORMAT_VIDEO_modifier` property and
+pass them all to the graphics API. If the allocation fails and the list of
+possible modifiers contains `DRM_FORMAT_MOD_INVALID`, fall back to allocating
+without an explicit modifier if the graphics API allows it.
+
+## add_buffer Hook
+
+This is relevant for producers.
+
+Allocate a DMA-BUF only using the negotiated format and modifier.
+
+## on_event Hook
+
+This is relevant for consumers.
+
+Check the type of the dequeued buffer. If its `SPA_DATA_MemFd` or
+`SPA_DATA_MemPtr` use the fallback SHM import mechanism.
+If it's `SPA_DATA_DmaBuf`
+get the DMA-BUF FDs (the plane count is encoded in the `n_datas` variable of the
+`spa_buffer` struct) and import them with the graphics API.
+
+Note: Some graphics APIs have separated functions for the modifier-less case
+(`DRM_FORMAT_MOD_INVALID`) or are omitting the modifier, since it might be used
+for error handling.
+
+## Example Programs
+
+- \ref video-src-fixate.c "": \snippet{doc} video-src-fixate.c title
+- \ref video-play-fixate.c "": \snippet{doc} video-play-fixate.c title
+
+# DMA-BUF Mapping Warning
+
+It's important to make sure all consumers of the PipeWire stream are prepared
+to deal with DMA-BUFs. Most DMA-BUFs cannot be treated like shared memory in general
+because of the following issues:
+
+- DMA-BUFs can use hardware-specific tiling and compression as described by
+ modifiers. Thus, a `mmap(3)` on the DMA-BUF FD will not give a linear view of
+ the buffer contents.
+- DMA-BUFs need to be properly synchronized with the asynchronous reads and
+ writes of the hardware. A `mmap(3)` call is not enough to guarantee proper
+ synchronization. (Maybe add link to linux syscall doc??)
+- Blindly accessing the DMA-BUFs via `mmap(3)` can be extremely slow if the
+ buffer has been allocated on discrete hardware. Consumers are better off
+ using a proper graphics API (such as EGL, Vulkan or VA-API) to process the
+ DMA-BUFs.
+
+# Size of DMA-BUFs
+
+When importing a DMA-BUF with a proper graphics API the size of a single buffer plane
+is no relevant property since it will be derived by the driver from the other properties.
+Therefore consumers should ignore the field `maxsize` of a `spa_data` and the field
+`size` of a `spa_chunk` struct. Producers are allowed to set both to 0.
+In cases where mapping a single plane is required the size should be obtained locally
+via the filedescriptor.
+
+# v4l2
+
+Another use case for streaming via DMA-BUFs are exporting a camera feed from v4l2
+as DMA-BUFs. Those are located in the main memory where it is possible to mmap them.
+This should be done as follows: Neither producer nor consumer should announce a
+modifier, but both should include `1 << SPA_DATA_DmaBuf` in the
+`SPA_PARAM_BUFFERS_dataType` property. It's the the responsibility of the producer
+while the `add_buffer` event to choose DMA-BUF as the used buffer type even though
+no modifier is present, if it can guarantee, that the used buffer is mmapable.
+
+Note: For now v4l2 uses planar buffers without modifiers. This is the reason for
+this special case.
+
+*/
diff --git a/doc/doxygen-awesome.css b/doc/doxygen-awesome.css
new file mode 100644
index 0000000..6000d2f
--- /dev/null
+++ b/doc/doxygen-awesome.css
@@ -0,0 +1,1364 @@
+/**
+
+Doxygen Awesome
+https://github.com/jothepro/doxygen-awesome-css
+
+MIT License
+
+Copyright (c) 2021 jothepro
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+*/
+
+:root {
+ /* primary theme color. This will affect the entire websites color scheme: links, arrows, labels, ... */
+ --primary-color: #1982d2;
+ --primary-dark-color: #00559f;
+ --primary-light-color: #7aabd6;
+ --primary-lighter-color: #cae1f1;
+ --primary-lightest-color: #e9f1f8;
+
+ /* page base colors */
+ --page-background-color: white;
+ --page-foreground-color: #2c3e50;
+ --page-secondary-foreground-color: #67727e;
+
+ /* color for all separators on the website: hr, borders, ... */
+ --separator-color: #dedede;
+
+ /* border radius for all rounded components. Will affect many components, like dropdowns, memitems, codeblocks, ... */
+ --border-radius-large: 8px;
+ --border-radius-small: 4px;
+ --border-radius-medium: 6px;
+
+ /* default spacings. Most compontest reference these values for spacing, to provide uniform spacing on the page. */
+ --spacing-small: 5px;
+ --spacing-medium: 10px;
+ --spacing-large: 16px;
+
+ /* default box shadow used for raising an element above the normal content. Used in dropdowns, Searchresult, ... */
+ --box-shadow: 0 2px 10px 0 rgba(0,0,0,.1);
+
+ --odd-color: rgba(0,0,0,.03);
+
+ /* font-families. will affect all text on the website
+ * font-family: the normal font for text, headlines, menus
+ * font-family-monospace: used for preformatted text in memtitle, code, fragments
+ */
+ --font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;
+ --font-family-monospace: source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace;
+
+ /* font sizes */
+ --page-font-size: 15.6px;
+ --navigation-font-size: 14.4px;
+ --code-font-size: 14.4px; /* affects code, fragment */
+ --title-font-size: 22px;
+
+ /* content text properties. These only affect the page content, not the navigation or any other ui elements */
+ --content-line-height: 27px;
+ /* The content is centered and constraint in it's width. To make the content fill the whole page, set the variable to auto.*/
+ --content-maxwidth: 900px;
+
+ /* colors for various content boxes: @warning, @note, @deprecated @bug */
+ --warning-color: #fca49b;
+ --warning-color-dark: #b61825;
+ --warning-color-darker: #75070f;
+ --note-color: rgba(255,229,100,.3);
+ --note-color-dark: #c39900;
+ --note-color-darker: #8d7400;
+ --deprecated-color: rgb(214, 216, 224);
+ --deprecated-color-dark: #5b6269;
+ --deprecated-color-darker: #43454a;
+ --bug-color: rgb(246, 208, 178);
+ --bug-color-dark: #a53a00;
+ --bug-color-darker: #5b1d00;
+ --invariant-color: #b7f8d0;
+ --invariant-color-dark: #00ba44;
+ --invariant-color-darker: #008622;
+
+ /* blockquote colors */
+ --blockquote-background: #f5f5f5;
+ --blockquote-foreground: #727272;
+
+ /* table colors */
+ --tablehead-background: #f1f1f1;
+ --tablehead-foreground: var(--page-foreground-color);
+
+ /* menu-display: block | none
+ * Visibility of the top navigation on screens >= 768px. On smaller screen the menu is always visible.
+ * `GENERATE_TREEVIEW` MUST be enabled!
+ */
+ --menu-display: block;
+
+ --menu-focus-foreground: var(--page-background-color);
+ --menu-focus-background: var(--primary-color);
+ --menu-selected-background: rgba(0,0,0,.05);
+
+
+ --header-background: var(--page-background-color);
+ --header-foreground: var(--page-foreground-color);
+
+ /* searchbar colors */
+ --searchbar-background: var(--side-nav-background);
+ --searchbar-foreground: var(--page-foreground-color);
+
+ /* searchbar size
+ * (`searchbar-width` is only applied on screens >= 768px.
+ * on smaller screens the searchbar will always fill the entire screen width) */
+ --searchbar-height: 33px;
+ --searchbar-width: 210px;
+
+ /* code block colors */
+ --code-background: #f5f5f5;
+ --code-foreground: var(--page-foreground-color);
+
+ /* fragment colors */
+ --fragment-background: #282c34;
+ --fragment-foreground: #ffffff;
+ --fragment-keyword: #cc99cd;
+ --fragment-keywordtype: #ab99cd;
+ --fragment-keywordflow: #e08000;
+ --fragment-token: #7ec699;
+ --fragment-comment: #999999;
+ --fragment-link: #98c0e3;
+ --fragment-preprocessor: #65cabe;
+ --fragment-linenumber-color: #cccccc;
+ --fragment-linenumber-background: #35393c;
+ --fragment-linenumber-border: #1f1f1f;
+ --fragment-lineheight: 20px;
+
+ /* sidebar navigation (treeview) colors */
+ --side-nav-background: #fbfbfb;
+ --side-nav-foreground: var(--page-foreground-color);
+ --side-nav-arrow-color: var(--page-background-color);
+
+ /* height of an item in any tree / collapsible table */
+ --tree-item-height: 30px;
+}
+
+@media screen and (max-width: 767px) {
+ :root {
+ --page-font-size: 16px;
+ --navigation-font-size: 16px;
+ --code-font-size: 15px; /* affects code, fragment */
+ --title-font-size: 22px;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --primary-color: #00559f;
+ --primary-dark-color: #1982d2;
+ --primary-light-color: #4779ac;
+ --primary-lighter-color: #191e21;
+ --primary-lightest-color: #191a1c;
+
+ --box-shadow: 0 2px 10px 0 rgba(0,0,0,.35);
+
+ --odd-color: rgba(0,0,0,.1);
+
+ --menu-selected-background: rgba(0,0,0,.4);
+
+ --page-background-color: #1C1D1F;
+ --page-foreground-color: #d2dbde;
+ --page-secondary-foreground-color: #859399;
+ --separator-color: #000000;
+ --side-nav-background: #252628;
+
+ --code-background: #2a2c2f;
+
+ --tablehead-background: #2a2c2f;
+
+ --blockquote-background: #1f2022;
+ --blockquote-foreground: #77848a;
+
+ --warning-color: #b61825;
+ --warning-color-dark: #510a02;
+ --warning-color-darker: #f5b1aa;
+ --note-color: rgb(255, 183, 0);
+ --note-color-dark: #9f7300;
+ --note-color-darker: #fff6df;
+ --deprecated-color: rgb(88, 90, 96);
+ --deprecated-color-dark: #262e37;
+ --deprecated-color-darker: #a0a5b0;
+ --bug-color: rgb(248, 113, 0);
+ --bug-color-dark: #812a00;
+ --bug-color-darker: #ffd3be;
+ }
+}
+
+body {
+ color: var(--page-foreground-color);
+ background-color: var(--page-background-color);
+ font-size: var(--page-font-size);
+}
+
+body, table, div, p, dl, #nav-tree .label, .title, .sm-dox a, .sm-dox a:hover, .sm-dox a:focus, #projectname, .SelectItem, #MSearchField, .navpath li.navelem a, .navpath li.navelem a:hover {
+ font-family: var(--font-family);
+}
+
+h1, h2, h3, h4, h5 {
+ margin-top: .9em;
+ font-weight: 600;
+ line-height: initial;
+}
+
+p, div, table, dl {
+ font-size: var(--page-font-size);
+}
+
+a, a.el:visited, a.el:hover, a.el:focus, a.el:active {
+ color: var(--primary-dark-color);
+}
+
+/*
+ Title and top navigation
+ */
+
+#top {
+ background: var(--header-background);
+ border-bottom: 1px solid var(--separator-color);
+}
+
+@media screen and (min-width: 768px) {
+ #top {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
+ }
+}
+
+#main-nav {
+ flex-grow: 5;
+ padding: var(--spacing-small) var(--spacing-medium);
+}
+
+#titlearea {
+ width: auto;
+ padding: var(--spacing-medium) var(--spacing-large);
+ background: none;
+ color: var(--header-foreground);
+ border-bottom: none;
+}
+
+@media screen and (max-width: 767px) {
+ #titlearea {
+ padding-bottom: var(--spacing-small);
+ }
+}
+
+#titlearea table tbody tr {
+ height: auto !important;
+}
+
+#projectname {
+ font-size: var(--title-font-size);
+ font-weight: 600;
+}
+
+#projectnumber {
+ font-family: inherit;
+ font-size: 60%;
+}
+
+#projectbrief {
+ font-family: inherit;
+ font-size: 80%;
+}
+
+#projectlogo {
+ vertical-align: middle;
+}
+
+#projectlogo img {
+ max-height: calc(var(--title-font-size) * 2);
+ margin-right: var(--spacing-small);
+}
+
+.sm-dox, .tabs, .tabs2, .tabs3 {
+ background: none;
+ padding: 0;
+}
+
+.tabs, .tabs2, .tabs3 {
+ border-bottom: 1px solid var(--separator-color);
+ margin-bottom: -1px;
+}
+
+@media screen and (max-width: 767px) {
+ .sm-dox a span.sub-arrow {
+ background: var(--code-background);
+ }
+}
+
+@media screen and (min-width: 768px) {
+ .sm-dox li, .tablist li {
+ display: var(--menu-display);
+ }
+
+ .sm-dox a span.sub-arrow {
+ border-color: var(--header-foreground) transparent transparent transparent;
+ }
+
+ .sm-dox a:hover span.sub-arrow {
+ border-color: var(--menu-focus-foreground) transparent transparent transparent;
+ }
+
+ .sm-dox ul a span.sub-arrow {
+ border-color: transparent transparent transparent var(--header-foreground);
+ }
+
+ .sm-dox ul a:hover span.sub-arrow {
+ border-color: transparent transparent transparent var(--menu-focus-foreground);
+ }
+}
+
+.sm-dox ul {
+ background: var(--page-background-color);
+ box-shadow: var(--box-shadow);
+ border: 1px solid var(--separator-color);
+ border-radius: var(--border-radius-medium) !important;
+ padding: var(--spacing-small);
+ animation: ease-out 150ms slideInMenu;
+}
+
+@keyframes slideInMenu {
+ from {
+ opacity: 0;
+ transform: translate(0px, -2px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translate(0px, 0px);
+ }
+}
+
+.sm-dox ul a {
+ color: var(--page-foreground-color);
+ background: var(--page-background-color);
+ font-size: var(--navigation-font-size);
+}
+
+.sm-dox>li>ul:after {
+ border-bottom-color: var(--page-background-color) !important;
+}
+
+.sm-dox>li>ul:before {
+ border-bottom-color: var(--separator-color) !important;
+}
+
+.sm-dox ul a:hover, .sm-dox ul a:active, .sm-dox ul a:focus {
+ font-size: var(--navigation-font-size);
+ color: var(--menu-focus-foreground);
+ text-shadow: none;
+ background-color: var(--menu-focus-background);
+ border-radius: var(--border-radius-small) !important;
+}
+
+.sm-dox a, .sm-dox a:focus, .tablist li, .tablist li a, .tablist li.current a {
+ text-shadow: none;
+ background: transparent;
+ background-image: none !important;
+ color: var(--header-foreground);
+ font-weight: normal;
+ font-size: var(--navigation-font-size);
+}
+
+.sm-dox a:focus {
+ outline: auto;
+}
+
+.sm-dox a:hover, .sm-dox a:active, .tablist li a:hover {
+ text-shadow: none;
+ font-weight: normal;
+ background: var(--menu-focus-background);
+ color: var(--menu-focus-foreground);
+ border-radius: var(--border-radius-small) !important;
+ font-size: var(--navigation-font-size);
+}
+
+.tablist li.current {
+ border-radius: var(--border-radius-small);
+ background: var(--menu-selected-background);
+}
+
+.tablist li {
+ margin: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
+}
+
+.tablist a {
+ padding: 0 var(--spacing-large);
+}
+
+
+/*
+ Search box
+ */
+
+#MSearchBox {
+ height: var(--searchbar-height);
+ background: var(--searchbar-background);
+ border-radius: var(--searchbar-height);
+ border: 1px solid var(--separator-color);
+ overflow: hidden;
+ width: var(--searchbar-width);
+ position: relative;
+ box-shadow: none;
+ display: block;
+ margin-top: 0;
+}
+
+.left #MSearchSelect {
+ left: 0;
+}
+
+.tabs .left #MSearchSelect {
+ padding-left: 0;
+}
+
+.tabs #MSearchBox {
+ position: absolute;
+ right: var(--spacing-medium);
+}
+
+@media screen and (max-width: 767px) {
+ .tabs #MSearchBox {
+ position: relative;
+ right: 0;
+ margin-left: var(--spacing-medium);
+ margin-top: 0;
+ }
+}
+
+#MSearchSelectWindow, #MSearchResultsWindow {
+ z-index: 9999;
+}
+
+#MSearchBox.MSearchBoxActive {
+ border-color: var(--primary-color);
+ box-shadow: inset 0 0 0 1px var(--primary-color);
+}
+
+#main-menu > li:last-child {
+ margin-right: 0;
+}
+
+@media screen and (max-width: 767px) {
+ #main-menu > li:last-child {
+ height: 50px;
+ }
+}
+
+#MSearchField {
+ font-size: var(--navigation-font-size);
+ height: calc(var(--searchbar-height) - 2px);
+ background: transparent;
+ width: calc(var(--searchbar-width) - 64px);
+}
+
+.MSearchBoxActive #MSearchField {
+ color: var(--searchbar-foreground);
+}
+
+#MSearchSelect {
+ top: calc(calc(var(--searchbar-height) / 2) - 11px);
+}
+
+.left #MSearchSelect {
+ padding-left: 8px;
+}
+
+#MSearchBox span.left, #MSearchBox span.right {
+ background: none;
+}
+
+#MSearchBox span.right {
+ padding-top: calc(calc(var(--searchbar-height) / 2) - 12px);
+}
+
+.tabs #MSearchBox span.right {
+ top: calc(calc(var(--searchbar-height) / 2) - 12px);
+}
+
+@keyframes slideInSearchResults {
+ from {
+ opacity: 0;
+ transform: translate(0, 15px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translate(0, 20px);
+ }
+}
+
+#MSearchResultsWindow {
+ left: auto !important;
+ right: var(--spacing-medium);
+ border-radius: var(--border-radius-large);
+ border: 1px solid var(--separator-color);
+ transform: translate(0, 20px);
+ box-shadow: var(--box-shadow);
+ animation: ease-out 280ms slideInSearchResults;
+ background: var(--page-background-color);
+}
+
+iframe#MSearchResults {
+ background: var(--page-background-color);
+ margin: 4px;
+}
+
+#MSearchSelectWindow {
+ border: 1px solid var(--separator-color);
+ border-radius: var(--border-radius-medium);
+ box-shadow: var(--box-shadow);
+ background: var(--page-background-color);
+}
+
+#MSearchSelectWindow a.SelectItem {
+ font-size: var(--navigation-font-size);
+ line-height: var(--content-line-height);
+ margin: 0 var(--spacing-small);
+ border-radius: var(--border-radius-small);
+ color: var(--page-foreground-color);
+}
+
+#MSearchSelectWindow a.SelectItem:hover {
+ background: var(--menu-focus-background);
+ color: var(--menu-focus-foreground);
+}
+
+@media screen and (max-width: 767px) {
+ #MSearchBox {
+ margin-top: var(--spacing-medium);
+ margin-bottom: var(--spacing-medium);
+ width: calc(100vw - 30px);
+ }
+
+ #main-menu > li:last-child {
+ float: none !important;
+ }
+
+ #MSearchField {
+ width: calc(100vw - 95px);
+ }
+
+ @keyframes slideInSearchResultsMobile {
+ from {
+ opacity: 0;
+ transform: translate(0, 15px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translate(0, 20px);
+ }
+ }
+
+ #MSearchResultsWindow {
+ left: var(--spacing-medium) !important;
+ right: var(--spacing-medium);
+ overflow: auto;
+ transform: translate(0, 20px);
+ animation: ease-out 280ms slideInSearchResultsMobile;
+ }
+}
+
+/*
+ Tree view
+ */
+
+#side-nav {
+ padding: 0 !important;
+ background: var(--side-nav-background);
+}
+
+@media screen and (max-width: 767px) {
+ #side-nav {
+ display: none;
+ }
+
+ #doc-content {
+ margin-left: 0 !important;
+ height: auto !important;
+ padding-bottom: calc(2 * var(--spacing-large));
+ }
+}
+
+#nav-tree {
+ background: transparent;
+}
+
+#nav-tree .label {
+ font-size: var(--navigation-font-size);
+}
+
+#nav-tree .item {
+ height: var(--tree-item-height);
+ line-height: var(--tree-item-height);
+}
+
+#nav-sync {
+ top: 12px !important;
+ right: 12px;
+}
+
+#nav-tree .selected {
+ text-shadow: none;
+ background-image: none;
+ background-color: transparent;
+ box-shadow: inset 4px 0 0 0 var(--primary-dark-color);
+}
+
+#nav-tree a {
+ color: var(--side-nav-foreground);
+}
+
+#nav-tree a:focus {
+ outline-style: auto;
+}
+
+.arrow {
+ color: var(--primary-light-color);
+ font-family: serif;
+ height: auto;
+ text-align: right;
+}
+
+#nav-tree .arrow {
+ opacity: 0;
+}
+
+#nav-tree div.item:hover .arrow, #nav-tree a:focus .arrow {
+ opacity: 1;
+}
+
+#nav-tree .selected a {
+ color: var(--primary-dark-color);
+ font-weight: bolder;
+}
+
+.ui-resizable-e {
+ background: var(--separator-color);
+ width: 1px;
+}
+
+/*
+ Contents
+ */
+
+div.header {
+ border-bottom: 1px solid var(--separator-color);
+ background-color: var(--page-background-color);
+ background-image: none;
+}
+
+div.contents, div.header .title, div.header .summary {
+ max-width: var(--content-maxwidth);
+}
+
+div.contents, div.header .title {
+ line-height: initial;
+ margin: calc(var(--spacing-medium) + .2em) auto var(--spacing-medium) auto;
+}
+
+div.header .summary {
+ margin: var(--spacing-medium) auto 0 auto;
+}
+
+div.headertitle {
+ padding: 0;
+}
+
+div.header .title {
+ font-weight: 600;
+ font-size: 210%;
+ padding: var(--spacing-medium) var(--spacing-large);
+ word-break: break-word;
+}
+
+div.header .summary {
+ width: auto;
+ display: block;
+ float: none;
+ padding: 0 var(--spacing-large);
+}
+
+td.memSeparator {
+ border-color: var(--separator-color);
+}
+
+.mdescLeft, .mdescRight, .memItemLeft, .memItemRight, .memTemplItemLeft, .memTemplItemRight, .memTemplParams {
+ background: var(--code-background);
+}
+
+.mdescRight {
+ color: var(--page-secondary-foreground-color);
+}
+
+span.mlabel {
+ background: var(--primary-color);
+ border: none;
+ padding: 4px 9px;
+ border-radius: 12px;
+ margin-right: var(--spacing-medium);
+}
+
+span.mlabel:last-of-type {
+ margin-right: 2px;
+}
+
+div.contents {
+ padding: 0 var(--spacing-large);
+}
+
+div.contents p, div.contents li {
+ line-height: var(--content-line-height);
+}
+
+div.contents div.dyncontent {
+ margin: var(--spacing-medium) 0;
+}
+
+@media (prefers-color-scheme: dark) {
+ div.contents div.dyncontent img {
+ filter: hue-rotate(180deg) invert();
+ }
+}
+
+h2.groupheader {
+ border-bottom: 1px solid var(--separator-color);
+ color: var(--page-foreground-color);
+}
+
+blockquote {
+ padding: var(--spacing-small) var(--spacing-medium);
+ background: var(--blockquote-background);
+ color: var(--blockquote-foreground);
+ border-left: 2px solid var(--blockquote-foreground);
+ margin: 0;
+}
+
+blockquote p {
+ margin: var(--spacing-small) 0 var(--spacing-medium) 0;
+}
+.paramname {
+ color: var(--primary-dark-color);
+}
+
+.glow {
+ text-shadow: 0 0 15px var(--primary-light-color) !important;
+}
+
+.alphachar a {
+ color: var(--page-foreground-color);
+}
+
+/*
+ Table of Contents
+ */
+
+div.toc {
+ background-color: var(--side-nav-background);
+ border: 1px solid var(--separator-color);
+ border-radius: var(--border-radius-medium);
+ box-shadow: var(--box-shadow);
+ padding: 0 var(--spacing-large);
+ margin: 0 0 var(--spacing-medium) var(--spacing-medium);
+}
+
+div.toc h3 {
+ color: var(--side-nav-foreground);
+ font-size: var(--navigation-font-size);
+ margin: var(--spacing-large) 0;
+}
+
+div.toc li {
+ font-size: var(--navigation-font-size);
+ padding: 0;
+ background: none;
+}
+
+div.toc li:before {
+ content: '↓';
+ font-weight: 800;
+ font-family: var(--font-family);
+ margin-right: var(--spacing-small);
+ color: var(--side-nav-foreground);
+ opacity: .4;
+}
+
+div.toc ul li.level1 {
+ margin: 0;
+}
+
+div.toc ul li.level2, div.toc ul li.level3 {
+ margin-top: 0;
+}
+
+
+@media screen and (max-width: 767px) {
+ div.toc {
+ float: none;
+ width: auto;
+ margin: 0 0 var(--spacing-medium) 0;
+ }
+}
+
+/*
+ Code & Fragments
+ */
+
+code, div.fragment, pre.fragment {
+ border-radius: var(--border-radius-small);
+ border: none;
+ overflow: hidden;
+}
+
+code {
+ display: inline;
+ background: var(--code-background);
+ color: var(--code-foreground);
+ padding: 2px 6px;
+ word-break: break-word;
+}
+
+div.fragment, pre.fragment {
+ margin: var(--spacing-medium) 0;
+ padding: 14px 16px;
+ background: var(--fragment-background);
+ color: var(--fragment-foreground);
+ overflow-x: auto;
+}
+
+@media screen and (max-width: 767px) {
+ div.fragment, pre.fragment {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .contents > div.fragment, .textblock > div.fragment, .textblock > pre.fragment {
+ margin: var(--spacing-medium) calc(0px - var(--spacing-large));
+ border-radius: 0;
+ }
+
+ .textblock li > .fragment {
+ margin: var(--spacing-medium) calc(0px - var(--spacing-large));
+ }
+
+ .memdoc li > .fragment {
+ margin: var(--spacing-medium) calc(0px - var(--spacing-medium));
+ }
+
+ .memdoc > div.fragment, .memdoc > pre.fragment, dl dd > div.fragment, dl dd pre.fragment {
+ margin: var(--spacing-medium) calc(0px - var(--spacing-medium));
+ border-radius: 0;
+ }
+}
+
+code, code a, pre.fragment, div.fragment, div.fragment .line, div.fragment span, div.fragment .line a, div.fragment .line span {
+ font-family: var(--font-family-monospace);
+ font-size: var(--code-font-size) !important;
+}
+
+div.line:after {
+ margin-right: var(--spacing-medium);
+}
+
+div.fragment .line, pre.fragment {
+ white-space: pre;
+ word-wrap: initial;
+ line-height: var(--fragment-lineheight);
+}
+
+div.fragment span.keyword {
+ color: var(--fragment-keyword);
+}
+
+div.fragment span.keywordtype {
+ color: var(--fragment-keywordtype);
+}
+
+div.fragment span.keywordflow {
+ color: var(--fragment-keywordflow);
+}
+
+div.fragment span.stringliteral {
+ color: var(--fragment-token)
+}
+
+div.fragment span.comment {
+ color: var(--fragment-comment);
+}
+
+div.fragment a.code {
+ color: var(--fragment-link);
+}
+
+div.fragment span.preprocessor {
+ color: var(--fragment-preprocessor);
+}
+
+div.fragment span.lineno {
+ display: inline-block;
+ width: 27px;
+ border-right: none;
+ background: var(--fragment-linenumber-background);
+ color: var(--fragment-linenumber-color);
+}
+
+div.fragment span.lineno a {
+ background: none;
+ color: var(--fragment-link);
+}
+
+div.fragment .line:first-child .lineno {
+ box-shadow: -999999px 0px 0 999999px var(--fragment-linenumber-background), -999998px 0px 0 999999px var(--fragment-linenumber-border);
+}
+
+/*
+ dl warning, attention, note, deprecated, bug, ...
+ */
+
+dl.warning, dl.attention, dl.note, dl.deprecated, dl.bug, dl.invariant, dl.pre {
+ padding: var(--spacing-medium);
+ margin: var(--spacing-medium) 0;
+ color: var(--page-background-color);
+ overflow: hidden;
+ margin-left: 0;
+ border-radius: var(--border-radius-small);
+}
+
+dl.section dd {
+ margin-bottom: 2px;
+}
+
+dl.warning, dl.attention {
+ background: var(--warning-color);
+ border-left: 8px solid var(--warning-color-dark);
+ color: var(--warning-color-darker);
+}
+
+dl.warning dt, dl.attention dt {
+ color: var(--warning-color-dark);
+}
+
+dl.note {
+ background: var(--note-color);
+ border-left: 8px solid var(--note-color-dark);
+ color: var(--note-color-darker);
+}
+
+dl.note dt {
+ color: var(--note-color-dark);
+}
+
+dl.bug {
+ background: var(--bug-color);
+ border-left: 8px solid var(--bug-color-dark);
+ color: var(--bug-color-darker);
+}
+
+dl.bug dt a {
+ color: var(--bug-color-dark) !important;
+}
+
+dl.deprecated {
+ background: var(--deprecated-color);
+ border-left: 8px solid var(--deprecated-color-dark);
+ color: var(--deprecated-color-darker);
+}
+
+dl.deprecated dt a {
+ color: var(--deprecated-color-dark) !important;
+}
+
+dl.section dd, dl.bug dd, dl.deprecated dd {
+ margin-inline-start: 0px;
+}
+
+dl.invariant, dl.pre {
+ background: var(--invariant-color);
+ border-left: 8px solid var(--invariant-color-dark);
+ color: var(--invariant-color-darker);
+}
+
+/*
+ memitem
+ */
+
+div.memdoc, div.memproto, h2.memtitle {
+ box-shadow: none;
+ background-image: none;
+ border: none;
+}
+
+div.memdoc {
+ padding: 0 var(--spacing-medium);
+ background: var(--page-background-color);
+}
+
+h2.memtitle, div.memitem {
+ border: 1px solid var(--separator-color);
+}
+
+div.memproto, h2.memtitle {
+ background: var(--code-background);
+ text-shadow: none;
+}
+
+h2.memtitle {
+ font-weight: 500;
+ font-family: monospace, fixed;
+ border-bottom: none;
+ border-top-left-radius: var(--border-radius-medium);
+ border-top-right-radius: var(--border-radius-medium);
+ word-break: break-all;
+}
+
+a:target + h2.memtitle, a:target + h2.memtitle + div.memitem {
+ border-color: var(--primary-light-color);
+}
+
+a:target + h2.memtitle {
+ box-shadow: -3px -3px 3px 0 var(--primary-lightest-color), 3px -3px 3px 0 var(--primary-lightest-color);
+}
+
+a:target + h2.memtitle + div.memitem {
+ box-shadow: 0 0 10px 0 var(--primary-lighter-color);
+}
+
+div.memitem {
+ border-top-right-radius: var(--border-radius-medium);
+ border-bottom-right-radius: var(--border-radius-medium);
+ border-bottom-left-radius: var(--border-radius-medium);
+ overflow: hidden;
+ display: block !important;
+}
+
+div.memdoc {
+ border-radius: 0;
+}
+
+div.memproto {
+ border-radius: 0 var(--border-radius-small) 0 0;
+ overflow: auto;
+ border-bottom: 1px solid var(--separator-color);
+ padding: var(--spacing-medium);
+ margin-bottom: -1px;
+}
+
+div.memtitle {
+ border-top-right-radius: var(--border-radius-medium);
+ border-top-left-radius: var(--border-radius-medium);
+}
+
+div.memproto table.memname {
+ font-family: monospace, fixed;
+ color: var(--page-foreground-color);
+}
+
+table.mlabels, table.mlabels > tbody {
+ display: block;
+}
+
+td.mlabels-left {
+ width: auto;
+}
+
+table.mlabels > tbody > tr:first-child {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+}
+
+.memname, .memitem span.mlabels {
+ margin: 0
+}
+
+/*
+ reflist
+ */
+
+dl.reflist {
+ border-radius: var(--border-radius-medium);
+ border: 1px solid var(--separator-color);
+ overflow: hidden;
+ padding: 0;
+}
+
+
+dl.reflist dt, dl.reflist dd {
+ box-shadow: none;
+ text-shadow: none;
+ background-image: none;
+ border: none;
+ padding: 12px;
+}
+
+
+dl.reflist dt {
+ border-radius: 0;
+ background: var(--code-background);
+ border-bottom: 1px solid var(--separator-color);
+ color: var(--page-foreground-color)
+}
+
+
+dl.reflist dd {
+ background: none;
+}
+
+/*
+ Table
+ */
+
+table.markdownTable, table.fieldtable {
+ width: 100%;
+ border: 1px solid var(--separator-color);
+ margin: var(--spacing-medium) 0;
+}
+
+table.fieldtable {
+ box-shadow: none;
+ border-radius: var(--border-radius-small);
+}
+
+th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone {
+ background: var(--tablehead-background);
+ color: var(--tablehead-foreground);
+ font-weight: 600;
+}
+
+table.markdownTable td, table.markdownTable th, table.fieldtable dt {
+ border: 1px solid var(--separator-color);
+ padding: var(--spacing-small) var(--spacing-medium);
+}
+
+table.fieldtable th {
+ font-size: var(--page-font-size);
+ font-weight: 600;
+ background-image: none;
+ background-color: var(--tablehead-background);
+ color: var(--tablehead-foreground);
+ border-bottom: 1px solid var(--separator-color);
+}
+
+.fieldtable td.fieldtype, .fieldtable td.fieldname {
+ border-bottom: 1px solid var(--separator-color);
+ border-right: 1px solid var(--separator-color);
+}
+
+.fieldtable td.fielddoc {
+ border-bottom: 1px solid var(--separator-color);
+}
+
+.memberdecls td.glow, .fieldtable tr.glow {
+ background-color: var(--primary-light-color);
+ box-shadow: 0 0 15px var(--primary-lighter-color);
+}
+
+table.memberdecls {
+ display: block;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+
+/*
+ Horizontal Rule
+ */
+
+hr {
+ margin-top: var(--spacing-large);
+ margin-bottom: var(--spacing-large);
+ border-top:1px solid var(--separator-color);
+}
+
+.contents hr {
+ box-shadow: var(--content-maxwidth) 0 0 0 var(--separator-color), calc(0px - var(--content-maxwidth)) 0 0 0 var(--separator-color);
+}
+
+.contents img {
+ max-width: 100%;
+}
+
+/*
+ Directories
+ */
+div.directory {
+ border-top: 1px solid var(--separator-color);
+ border-bottom: 1px solid var(--separator-color);
+ width: auto;
+}
+
+table.directory {
+ font-family: var(--font-family);
+ font-size: var(--page-font-size);
+ font-weight: normal;
+}
+
+.directory td.entry {
+ padding: var(--spacing-small);
+ display: flex;
+ align-items: center;
+}
+
+.directory tr.even {
+ background-color: var(--odd-color);
+}
+
+.icona {
+ width: auto;
+ height: auto;
+ margin: 0 var(--spacing-small);
+}
+
+.icon {
+ background: var(--primary-dark-color);
+ width: 18px;
+ height: 18px;
+ line-height: 18px;
+}
+
+.iconfopen, .icondoc, .iconfclosed {
+ background-position: center;
+ margin-bottom: 0;
+}
+
+.icondoc {
+ filter: saturate(0.2);
+}
+
+@media screen and (max-width: 767px) {
+ div.directory {
+ margin-left: calc(0px - var(--spacing-medium));
+ margin-right: calc(0px - var(--spacing-medium));
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .iconfopen, .iconfclosed {
+ filter: hue-rotate(180deg) invert();
+ }
+}
+
+/*
+ Class list
+ */
+
+.classindex dl.odd {
+ background: var(--odd-color);
+ border-radius: var(--border-radius-small);
+}
+
+@media screen and (max-width: 767px) {
+ .classindex {
+ margin: 0 calc(0px - var(--spacing-small));
+ }
+}
+
+/*
+ Footer and nav-path
+ */
+
+#nav-path {
+ margin-bottom: -1px;
+ width: 100%;
+}
+
+#nav-path ul {
+ background-image: none;
+ background: var(--page-background-color);
+ border: none;
+ border-top: 1px solid var(--separator-color);
+ border-bottom: 1px solid var(--separator-color);
+ font-size: var(--navigation-font-size);
+}
+
+img.footer {
+ width: 60px;
+}
+
+.navpath li.footer {
+ color: var(--page-secondary-foreground-color);
+}
+
+address.footer {
+ margin-bottom: var(--spacing-large);
+}
+
+#nav-path li.navelem {
+ background-image: none;
+ display: flex;
+ align-items: center;
+}
+
+.navpath li.navelem a {
+ text-shadow: none;
+ display: inline-block;
+ color: var(--primary-dark-color)
+}
+
+li.navelem {
+ padding: 0;
+ margin-left: -8px;
+}
+
+li.navelem:first-child {
+ margin-left: var(--spacing-large);
+}
+
+li.navelem:first-child:before {
+ display: none;
+}
+
+#nav-path li.navelem:after {
+ content: '';
+ border: 5px solid var(--page-background-color);
+ border-bottom-color: transparent;
+ border-right-color: transparent;
+ border-top-color: transparent;
+ transform: scaleY(4.2);
+ z-index: 10;
+ margin-left: 6px;
+}
+
+#nav-path li.navelem:before {
+ content: '';
+ border: 5px solid var(--separator-color);
+ border-bottom-color: transparent;
+ border-right-color: transparent;
+ border-top-color: transparent;
+ transform: scaleY(3.2);
+ margin-right: var(--spacing-small);
+}
+
+@media (prefers-color-scheme: dark) {
+ #nav-path li.navelem:after {
+ text-shadow: 3px 0 0 var(--separator-color), 8px 0 6px rgba(0,0,0,0.4);
+ }
+}
+
+.navpath li.navelem a:hover {
+ color: var(--primary-color);
+}
diff --git a/doc/examples.dox.in b/doc/examples.dox.in
new file mode 100644
index 0000000..fd11ac8
--- /dev/null
+++ b/doc/examples.dox.in
@@ -0,0 +1,9 @@
+/**
+
+\page page_examples List of example programs
+
+@example_ref@
+
+@example_doxygen@
+
+*/
diff --git a/doc/index.dox b/doc/index.dox
new file mode 100644
index 0000000..1602b7b
--- /dev/null
+++ b/doc/index.dox
@@ -0,0 +1,45 @@
+/** \mainpage PipeWire
+
+PipeWire is low-level multimedia framework that provides:
+
+- Graph based processing.
+- Support for out-of-process processing graphs with minimal overhead.
+- Flexible and extensible media format negotiation and buffer allocation.
+- Hard real-time capable plugins.
+- Very low-latency for both audio and video processing.
+
+See \ref page_overview for an overview of PipeWire and \ref page_design
+for the design principles guiding PipeWire.
+
+# Components
+
+PipeWire ships with the following components:
+
+- A \ref page_daemon that implements the IPC and graph processing.
+- An example \ref page_session_manager that manages objects in the \ref page_daemon.
+- A set of \ref page_tools to introspect and use the \ref page_daemon.
+- A \ref page_library to develop PipeWire applications and plugins (\ref
+ page_tutorial "tutorial").
+- The \ref page_spa used by both the \ref page_daemon and in the \ref
+ page_library.
+
+# API Documentation
+
+See \ref page_api.
+
+# More Documentation
+
+See our [Wiki](https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/home) for
+More information on how to configure and use PipeWire.
+
+# Resources
+
+- [PipeWire and AGL](https://wiki.automotivelinux.org/_media/pipewire_agl_20181206.pdf)
+- [LAC 2020 Paper](https://lac2020.sciencesconf.org/307881/document)
+- [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)
+- [Bluetooth, PipeWire and Whatsapp calls](https://gjhenrique.com/pipewire.html)
+- [Intoduction to PipeWire](https://bootlin.com/blog/an-introduction-to-pipewire/)
+
+*/
diff --git a/doc/input-filter-h.sh b/doc/input-filter-h.sh
new file mode 100755
index 0000000..dc4604a
--- /dev/null
+++ b/doc/input-filter-h.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+#
+# Doxygen input filter, which tries to fix documentation of callback
+# method macros.
+#
+# This is used for .h files.
+#
+
+FILENAME="$1"
+
+# Add \ingroup commands for the file, for each \addgroup in it
+BASEFILE=$(echo "$FILENAME" | sed -e 's@.*src/pipewire/@pipewire/@; s@.*spa/include/spa/@spa/@; s@.*src/test/@test/@;')
+
+echo "/** \file"
+echo "\`$BASEFILE\`"
+sed -n -e '/.*\\addtogroup [a-zA-Z0-9_].*/ { s/.*addtogroup /\\ingroup /; p; }' < "$FILENAME" | sort | uniq
+echo " */"
+
+# Add \sa and \copydoc for (struct *methods) callback macros.
+# #define pw_core_add_listener(...) pw_core_method(c,add_listener,...) -> add \sa and \copydoc
+# #define spa_system_read(...) spa_system_method_r(c,read,...) -> add \sa and \copydoc
+#
+# Also:
+# Ensure all macros are included (also those defined inside a struct),
+# by adding /** \ingroup XXX */ before each definition.
+# Also ensure all opaque structs get included.
+sed -e 's@^\(#define .*[[:space:]]\)\(.*_method\)\((.,[[:space:]]*\)\([a-z_]\+\)\(.*)[[:space:]]*\)$@\1\2\3\4\5 /**< \\copydoc \2s.\4\n\n\\sa \2s.\4 */@;' \
+ -e 's@^\(#define .*[[:space:]]\)\(.*_method\)\(_[rvs](.,[[:space:]]*\)\([a-z_]\+\)\(.*)[[:space:]]*\)$@\1\2\3\4\5 /**< \\copydoc \2s.\4\n\n\\sa \2s.\4 */@;' \
+ -e '/\\addtogroup/ { h; s@.*\\addtogroup \(.*\).*@/** \\ingroup \1 */@; x; }' \
+ -e '/#define \(PW\|SPA\)_[A-Z].*[^\\][ ]*$/ { x; p; x; }' \
+ -e 's@^\([ ]*struct \)\([a-zA-Z0-9_]*\)\(;.*\)$@/** \\struct \2 */\n\1\2\3@;' \
+< "$FILENAME"
diff --git a/doc/input-filter.sh b/doc/input-filter.sh
new file mode 100755
index 0000000..8c71bef
--- /dev/null
+++ b/doc/input-filter.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+#
+# Doxygen input filter that adds \privatesection to all files,
+# and removes macros.
+#
+# This is used for .c files, and causes Doxygen to not include
+# any symbols from them, unless they also appeared in a header file.
+#
+echo -n "/** \privatesection */ "
+sed -e 's/#define.*//' < "$1"
diff --git a/doc/manpage.dox.in b/doc/manpage.dox.in
new file mode 100644
index 0000000..9e6df78
--- /dev/null
+++ b/doc/manpage.dox.in
@@ -0,0 +1,5 @@
+/** \page @pagename@ @title@
+
+\verbinclude @filename@
+
+*/
diff --git a/doc/meson.build b/doc/meson.build
new file mode 100644
index 0000000..b1cb115
--- /dev/null
+++ b/doc/meson.build
@@ -0,0 +1,162 @@
+doxyfile_conf = configuration_data()
+doxyfile_conf.set('PACKAGE_NAME', meson.project_name())
+doxyfile_conf.set('PACKAGE_VERSION', meson.project_version())
+doxyfile_conf.set('top_srcdir', meson.project_source_root())
+doxyfile_conf.set('top_builddir', meson.project_build_root())
+doxyfile_conf.set('output_directory', meson.current_build_dir())
+
+dot_found = find_program('dot', required: false).found()
+summary({'dot (used with doxygen)': dot_found}, bool_yn: true, section: 'Optional programs')
+if dot_found
+ doxyfile_conf.set('HAVE_DOT', 'YES')
+else
+ doxyfile_conf.set('HAVE_DOT', 'NO')
+endif
+
+# Note: order here is how doxygen will expose the pages in the sidebar
+# api-tree.dox should be first to determine ordering of Modules.
+extra_docs = [
+ 'api-tree.dox',
+ 'index.dox',
+ 'overview.dox',
+ 'pipewire.dox',
+ 'pipewire-design.dox',
+ 'pipewire-access.dox',
+ 'pipewire-midi.dox',
+ 'pipewire-portal.dox',
+ 'pipewire-daemon.dox',
+ 'pipewire-library.dox',
+ 'pipewire-modules.dox',
+ 'pipewire-session-manager.dox',
+ 'pipewire-objects-design.dox',
+ 'pipewire-audio.dox',
+ 'tutorial.dox',
+ 'tutorial1.dox',
+ 'tutorial2.dox',
+ 'tutorial3.dox',
+ 'tutorial4.dox',
+ 'tutorial5.dox',
+ 'tutorial6.dox',
+ 'api.dox',
+ 'spa-index.dox',
+ 'spa-plugins.dox',
+ 'spa-design.dox',
+ 'spa-pod.dox',
+ 'spa-buffer.dox',
+ 'pulseaudio.dox',
+ 'dma-buf.dox',
+]
+
+inputs = []
+foreach extra : extra_docs
+ inputs += meson.project_source_root() / 'doc' / extra
+endforeach
+foreach h : pipewire_headers
+ inputs += meson.project_source_root() / 'src' / 'pipewire' / h
+endforeach
+foreach h : pipewire_ext_headers
+ inputs += meson.project_source_root() / 'src' / 'pipewire' / 'extensions' / h
+endforeach
+foreach h : pipewire_ext_sm_headers
+ inputs += meson.project_source_root() / 'src' / 'pipewire' / 'extensions' / h
+endforeach
+foreach h : pipewire_sources
+ inputs += meson.project_source_root() / 'src' / 'pipewire' / h
+endforeach
+foreach h : module_sources
+ inputs += meson.project_source_root() / 'src' / 'modules' / h
+endforeach
+inputs += meson.project_source_root() / 'test' / 'pwtest.h'
+input_dirs = [ meson.project_source_root() / 'spa' / 'include' / 'spa' ]
+
+path_prefixes = [
+ meson.project_source_root() / 'src',
+ meson.project_source_root() / 'spa' / 'include',
+ meson.project_source_root(),
+]
+
+cssfiles = [
+ meson.project_source_root() / 'doc' / 'doxygen-awesome.css',
+ meson.project_source_root() / 'doc' / 'custom.css'
+]
+
+# Example files (in order from simple to esoteric)
+example_files = [
+ 'tutorial1.c',
+ 'tutorial2.c',
+ 'tutorial3.c',
+ 'tutorial4.c',
+ 'tutorial5.c',
+ 'tutorial6.c',
+]
+foreach h : examples
+ example_files += [h + '.c']
+endforeach
+foreach h : spa_examples
+ example_files += ['spa/examples/' + h + '.c']
+endforeach
+
+example_doxygen = []
+example_ref = []
+foreach h : example_files
+ example_doxygen += ['\\example ' + h,
+ '\\snippet{doc} ' + h + ' title',
+ '<br>',
+ '\\snippet{doc} ' + h + ' doc']
+ example_ref += ['- \\ref ' + h + ' "": \snippet{doc} ' + h + ' title']
+endforeach
+
+examples_dox_conf = configuration_data()
+examples_dox_conf.set('example_doxygen', '\n'.join(example_doxygen))
+examples_dox_conf.set('example_ref', '\n'.join(example_ref))
+examples_dox = configure_file(input: 'examples.dox.in',
+ output: 'examples.dox',
+ configuration: examples_dox_conf)
+
+input_dirs += [ 'doc/examples.dox' ]
+
+man_doxygen = []
+man_subpages = []
+foreach m : manpages
+ manconf = configuration_data()
+ pagename = 'page_man_' + m.split('.rst.in').get(0).replace('.', '_').replace('-', '_')
+ filename = m.split('.rst.in').get(0) + '.dox'
+ manconf.set('pagename', pagename)
+ manconf.set('title', m.split('.rst.in').get(0).replace('.1','').replace('.5',''))
+ manconf.set('filename', meson.project_source_root() / 'man' / m)
+ manfile = configure_file(input: 'manpage.dox.in',
+ output: filename,
+ configuration: manconf)
+ man_doxygen += [manfile]
+ man_subpages += ['- \subpage ' + pagename]
+ input_dirs += [ 'doc/' + filename ]
+endforeach
+
+pw_tools_dox_conf = configuration_data()
+pw_tools_dox_conf.set('man_subpages', '\n'.join(man_subpages))
+pw_tools_dox = configure_file(input: 'pipewire-tools.dox.in',
+ output: 'pipewire-tools.dox',
+ configuration: pw_tools_dox_conf)
+input_dirs += [ 'doc/pipewire-tools.dox' ]
+
+doxyfile_conf.set('inputs', ' '.join(inputs + input_dirs))
+doxyfile_conf.set('cssfiles', ' '.join(cssfiles))
+doxyfile_conf.set('path_prefixes', ' '.join(path_prefixes))
+doxyfile_conf.set('c_input_filter', meson.project_source_root() / 'doc' / 'input-filter.sh')
+doxyfile_conf.set('h_input_filter', meson.project_source_root() / 'doc' / 'input-filter-h.sh')
+
+doxyfile = configure_file(input: 'Doxyfile.in',
+ output: 'Doxyfile',
+ configuration: doxyfile_conf)
+
+docdir = get_option('docdir')
+if docdir == ''
+ docdir = pipewire_datadir / 'doc' / meson.project_name()
+endif
+
+html_target = custom_target('pipewire-docs',
+ input: [ doxyfile, examples_dox, pw_tools_dox ] + inputs + cssfiles + man_doxygen,
+ output: [ 'html' ],
+ command: [ doxygen, doxyfile ],
+ install: true,
+ install_dir: docdir)
diff --git a/doc/overview.dox b/doc/overview.dox
new file mode 100644
index 0000000..b96caf2
--- /dev/null
+++ b/doc/overview.dox
@@ -0,0 +1,42 @@
+/** \page page_overview Overview
+
+PipeWire is a new low-level multimedia framework designed from scratch that
+aims to provide:
+
+- Graph based processing.
+- Support for out-of-process processing graphs with minimal overhead.
+- Flexible and extensible media format negotiation and buffer allocation.
+- Hard real-time capable plugins.
+- Achieve very low-latency for both audio and video processing.
+
+The framework is used to build a modular daemon that can be configured to:
+
+- Be a low-latency audio server with features like PulseAudio and/or JACK.
+- A video capture server that can manage hardware video capture devices and
+ provide access to them.
+- A central hub where video can be made available for other applications
+ such as the gnome-shell screencast API.
+
+
+# Motivation
+
+Linux has no unified framework for exchanging multimedia content between
+applications or even devices. In most cases, developers realized that
+a user-space daemon is needed to make this possible:
+
+- For video content, we typically rely on the compositor to render our
+ data.
+- For video capture, we usually go directly to the hardware devices, with
+ all security implications and inflexible routing that this brings.
+- For consumer audio, we use PulseAudio to manage and mix multiple streams
+ from clients.
+- For Pro audio, we use JACK to manage the graph of nodes.
+
+None of these solutions (except perhaps to some extent Wayland) however
+were designed to support the security features that are required when
+dealing with flatpaks or other containerized applications. PipeWire
+aims to solve this problem and provides a unified framework to run both
+consumer and pro audio as well as video capture and processing in a
+secure way.
+
+*/
diff --git a/doc/pipewire-access.dox b/doc/pipewire-access.dox
new file mode 100644
index 0000000..3632280
--- /dev/null
+++ b/doc/pipewire-access.dox
@@ -0,0 +1,126 @@
+/** \page page_access Access Control
+
+This document explains how access control is designed and implemented.
+
+PipeWire implements per client permissions on the objects in the graph.
+Permissions include `R` (read), `W` (write), `X` (execute) and `M` (metadata).
+
+- `R`: An object with permission `R` is visible to the client. The client will receive
+ registry events for the object and can interact with it.
+- `W`: An object with permission `W` can be modified. This is usually done
+ through a method that modifies the state of the object. The `W` permission
+ usually implies the `X` permission.
+- `X`: An object with permission `X` allows invoking methods on the object.
+ Some of those methods will only query state, others will modify the object.
+ As said above, modifying the object through one of these methods requires
+ the `W` permission.
+- `M`: An object with `M` permission can be used as the subject in metadata.
+
+Clients with all permissions set are referred to as "ALL" in the
+documentation.
+
+
+# Use Cases
+
+## New Clients Need Their Permissions Configured
+
+A new client is not allowed to communicate with the PipeWire daemon until
+it has been configured with permissions.
+
+## Flatpaks Can't Modify Other Stream/Device Volumes
+
+An application running as Flatpak should not be able to modify the state of
+certain objects. Permissions of the relevant PipeWire objects should not have
+the `W` permission to avoid this.
+
+## Flatpaks Can't Move Other Streams To Different Devices
+
+Streams are moved to another device by setting the `target.node` metadata
+on the node ID. By not setting the `M` bit on the other objects, this can be
+avoided.
+
+## Application Should Be Restricted In What They Can See
+
+In general, applications should only be able to see the objects that they are
+allowed to see. For example, a web browser that was given access to a camera
+should not be able to see (and thus receive input data from) audio devices.
+
+## "Manager" Applications Require Full Access
+
+Some applications require full access to the PipeWire graph, including
+moving streams between nodes (by setting metadata) and modifying properties
+(eg. volume). These applications must work even when running as Flatpak.
+
+
+# Design
+
+## The PipeWire Daemon
+
+Immediately after a new client connects to the PipeWire daemon and updates
+its properties, the client will be registered and made visible to other
+clients.
+
+The PipeWire core will emit a `check_access` event in the \ref pw_context_events
+context for the the new client. The implementer of this event is responsible
+for assigning permissions to the client.
+
+Clients with permission `R` on the core object can continue communicating
+with the daemon. Clients without permission `R` on the core are suspended
+and are not able to send more messages.
+
+A suspended client can only resume processing after some other client
+sets the core permissions to `R`. This other client is usually a session
+manager, see e.g. \ref page_session_manager.
+
+## The PipeWire Access Module
+
+The \ref page_module_access hooks into the `check_access` event that is
+emitted when a new client is registered. The module checks the permissions of
+the client and stores those in the \ref PW_KEY_ACCESS
+property on the client object. If this property is already set, the access
+module does nothing.
+
+If the property is not set it will go through a set of checks to determine
+the permissions for a client. See the \ref page_module_access documentation
+for details, particularly on the values documented below. Depending on the
+value of the \ref PW_KEY_ACCESS property one the following happens:
+
+- `"allowed"`, `"unrestricted"`: ALL permissions are set on the core
+ object and the client will be able to resume.
+- `"restricted"`, `"flatpak"`, `"$access.force"`: No permissions are set on
+ the core object and the client will be suspended.
+- `"rejected"`: An error is sent to the client and the client is
+ suspended.
+
+As detailed above, the client may be suspended. In that case the session
+manager or another client is required to configure permissions on the object
+for it to resume.
+
+## The Session Manager
+
+The session manager listens for new clients to appear. It will use the
+\ref PW_KEY_ACCESS property to determine what to do.
+
+For clients that are suspended with `"restricted"`, `"flatpak"` or
+`"$access.force"` access, the session manager needs to set permissions on the
+client for the various PipeWire objects in the graph that it is allowed to
+interact with. To resume a client, the session manager needs to set
+permission `R` on the core object for the client.
+
+Permissions of objects for a client can be changed at any time by the
+session manager. Removing the client core permission `R` will suspend the
+client.
+
+The session manager needs to do additional checks to determine if the
+manager permissions can be given to the particular client and then
+configure ALL permissions on the client. Possible checks include
+permission store checks or ask the user if the application is allowed
+full access.
+
+Manager applications (ie. applications that need to modify the graph) will
+set the \ref PW_KEY_MEDIA_CATEGORY property in the client object to "Manager".
+
+For details on the pipewire-media-session implementation of access control,
+see \ref page_media_session.
+
+*/
diff --git a/doc/pipewire-architecture.dox b/doc/pipewire-architecture.dox
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/doc/pipewire-architecture.dox
diff --git a/doc/pipewire-audio.dox b/doc/pipewire-audio.dox
new file mode 100644
index 0000000..b39e869
--- /dev/null
+++ b/doc/pipewire-audio.dox
@@ -0,0 +1,127 @@
+/** \page page_audio Audio
+
+This document explains how Audio is implemented.
+
+# Use Cases
+
+## Audio Devices Are Made Available As Processing Nodes/Ports
+
+Applications need to be able to see a port for each stream of an
+audio device.
+
+## Audio Devices Can Be Plugged and Unplugged
+
+When devices are plugged and unplugged the associated nodes/ports
+need to be created and removed.
+
+## Audio Port In Canonical Format
+
+It must be possible to make individual audio channels available
+as a single mono stream with a fixed format and samplerate.
+
+This makes it possible to link any of the audio ports together
+without doing conversions.
+
+## Applications Can Connect To Audio Devices
+
+Applications can create ports that can connect to the audio ports
+so that data can be provided to or consumed from them.
+
+It should be possible to automatically connect an application to
+a sink/source when it requests this.
+
+## Default Audio Sink and Sources
+
+It should be possible to mark a source or sink as the default source
+and sink so that applications are routed to them by default.
+
+It should be possible to change the default audio sink/source.
+
+## Application Should Be Able To Move Between Sinks/Sources
+
+It should be possible to move an application from one device to
+another dynamically.
+
+## Exclusive Access
+
+Application should be able to connect to a device in exclusive mode.
+This allows the application to negotiate a specific format with the
+device such as a compressed format.
+
+Exclusive access means that only one application can access the device
+because mixing is in general not possible when negotiating
+compressed formats.
+
+
+# Design
+
+## SPA
+
+Audio devices are implemented with an \ref spa_device "SPA Device" object.
+
+This object is then responsible for controlling the \ref spa_node "SPA Nodes" that
+provide the audio ports to interface with the device.
+
+The nodes operate on the native audio formats supported by the device.
+This includes the sample rate as well as the number of channels and
+the audio format.
+
+## Audio Adapter
+
+An SPA Node called the "adapter" is usually used with the SPA device node as
+the internal node.
+
+The function of the adapter is to convert the device native format to
+the required external format. This can include format or samplerate
+conversion but also channel remixing/remapping.
+
+The audio adapter is also responsible for exposing the audio channels
+as separate mono ports. This is called the DSP setup.
+
+The audio adapter can also be configured in passthrough mode when it
+will not do any conversions but simply pass through the port information
+of the internal node. This can be used to implement exclusive access.
+
+Setup of the different configurations of the adapter can be done with
+the PortConfig parameter.
+
+## The Session Manager
+
+The session manager is responsible for creating nodes and ports for
+the various audio devices. It will need to wrap them into an audio
+adapter so that the specific configuration of the node can be
+decided by the policy mode.
+
+The session manager should create name and description for the
+devices and nodes.
+
+The session manager is responsible for assigning priorities to the
+nodes. At least \ref PW_KEY_PRIORITY_SESSION and \ref PW_KEY_PRIORITY_DRIVER
+need to be set.
+
+The session manager might need to work with other services to gain
+exclusive access to the device (eg. DBus).
+
+
+# Implementation
+
+## PipeWire Media Session (alsa-monitor)
+
+PipeWire media session uses the \ref SPA_NAME_API_ALSA_ENUM_UDEV plugin
+for enumerating the ALSA devices. For each device it does:
+
+- Try to acquire the DBus device reservation object to gain exclusive
+ access to the device.
+- Create an SPA device instance for the device and monitor this device instance.
+- For each node created by the device, create an adapter with
+ an ALSA PCM node in the context of the PipeWire daemon.
+
+The session manager will also create suitable names and descriptions
+for the devices and nodes that it creates as well as assign session
+and driver priorities.
+
+The session manager has the option to add extra properties on the
+devices and nodes that it creates to control their behavior. This
+is implemented with match rules.
+
+*/
diff --git a/doc/pipewire-daemon.dox b/doc/pipewire-daemon.dox
new file mode 100644
index 0000000..c1a0649
--- /dev/null
+++ b/doc/pipewire-daemon.dox
@@ -0,0 +1,175 @@
+/** \page page_daemon PipeWire Daemon
+
+The PipeWire daemon is the central process that manages data exchange between
+devices and clients.
+
+Typically general, users run one PipeWire daemon that listens for incoming
+connections and manages devices. Clients (including the \ref
+page_session_manager) are separate processes that talk to the daemon using the
+PipeWire socket (default: `$XDG_RUNTIME_DIR/pipewire-0`). This approach
+provides address-space separation between the privileged daemon and
+non-privileged clients.
+
+\dot
+digraph pw {
+ compound=true;
+ node [shape="box"];
+
+ subgraph cluster_pw {
+ rankdir="TB";
+ label="PipeWire daemon";
+ style="dashed";
+
+ subgraph cluster_prot_native {
+ label="pipewire-module-protocol-native";
+ style="solid";
+ socket [label="$XDG_RUNTIME_DIR/pipewire-0"];
+ mod_impl [label="module implementation"];
+
+ socket -> mod_impl;
+ }
+ core [label="PipeWire Core"];
+ alsa [label="PipeWire ALSA support"];
+
+ mod_impl -> core;
+ core -> alsa;
+ }
+
+ kernel
+
+ client1 [ label="Media Player" ];
+ client2 [ label="Audio Software" ];
+ sm [ label="Session Manager", style="dotted" ];
+
+ client1 -> socket;
+ client2 -> socket;
+ sm -> socket;
+ alsa -> kernel;
+}
+\enddot
+
+As shown above, the protocol is handled by the \ref
+page_module_protocol_native. From PipeWire's point-of-view this module is just
+another module.
+
+# Configuration Files
+
+On startup, the daemon reads a configuration file to configure itself.
+It executes a series of commands listed in the config file. The lookup order
+for configuration files are:
+
+- `$XDG_CONFIG_HOME/pipewire/pipewire.conf` (usually `$HOME/.config/pipewire/pipewire.conf`)
+- `$sysconfdir/pipewire/pipewire.conf` (usually `/etc/pipewire/pipewire.conf`)
+- `$datadir/pipewire/pipewire.conf` (usually `/usr/share/pipewire/pipewire.conf`)
+
+The first configuration file found is loaded as the base configuration.
+
+Next, configuration sections are collected in the directories in this
+order:
+
+- `$datadir/pipewire/pipewire.conf.d/` (usually `/usr/share/pipewire/pipewire.conf.d/`)
+- `$sysconfdir/pipewire/pipewire.conf.d/` (usually `/etc/pipewire/pipewire.conf.d/`)
+- `$XDG_CONFIG_HOME/pipewire/pipewire.conf.d/` (usually `$HOME/.config/pipewire/pipewire.conf.d/`)
+
+They are applied to the global configuration file. Properties are overwritten
+and array elements are appended. This makes it possible to make small custom customizations
+or additions to the main configuration file.
+
+The environment variables `PIPEWIRE_CONFIG_DIR`, `PIPEWIRE_CONFIG_PREFIX`,
+and `PIPEWIRE_CONFIG_NAME`. Can be used to specify an alternative configuration
+directory, subdirectory, and filename respectively.
+
+## Configuration File Format
+
+PipeWire's configuration file format is JSON. In addition to true JSON
+PipeWire also understands a more compact JSON representation. Where
+`"` can be omitted around strings, no trailing commas are required and
+`:` or `=` can be used to separate object keys from their values.
+Also, `#` can be used to start a comment until the end of the line.
+
+The configuration file format is grouped into sections. A section is
+either a dictionary (`{}`) or an array (`[]`). Dictionary and array entries
+are separated by whitespace and may be simple value assignment, an array or
+a dictionary. For example:
+
+```
+# A dictionary section
+context.properties = {
+ # Keys often have a dot notation
+ core.daemon = true
+}
+
+# An array section containing three dictionary objects
+context.modules = [
+ # a dictionary object with one key assigned to a string
+ { name = libpipewire-module-protocol-native }
+ { name = libpipewire-module-profiler }
+
+ # a dictionary object with two keys, one assigned to a string
+ # the other one to an array of strings
+ { name = libpipewire-module-portal
+ flags = [ ifexists nofail ]
+ }
+]
+```
+
+Allowed configuration file sections are:
+
+- **context.properties** (dictionary): These properties configure the
+ pipewire instance.
+- **context.spa-libs** (dictionary): Maps plugin features with globs to a
+ spa library.
+- **context.modules** (array): Each entry in the array is a dictionary with
+ the name of the module to load, including optional args and flags. Most
+ modules support being loaded multiple times.
+- **context.objects** (array): Each entry in the array is a dictionary con‐
+ taining the factory to create an object from and optional extra argu‐
+ ments specific to that factory.
+- **context.exec** (array): Each entry in the array is dictionary containing
+ the path of a program to execute on startup and optional args. This ar‐
+ ray usually contains an entry to start the session manager.
+
+
+# Logging
+
+The `PIPEWIRE_DEBUG` environment variable can be used to enable
+more debugging. This variable supports the following format:
+
+- `PIPEWIRE_DEBUG=[<level>][,<glob1>:<level1>][,<glob2>:<level2>,...]` where the globs are
+ shell globs to match on log topics and the levels are the respective
+ log level to set for that topic. Globs are applied in order and a matching
+ glob overrides an earlier glob for that category. A level without a glob
+ prefix will set the global log level and is a more preformant version of
+ `*:<level>`. For example, `PIPEWIRE_DEBUG=E,mod.*:D,mod.foo:X` enables global error messages,
+ debugging on all modules but no messages on the foo module.
+- `<level>` specifies the log level:
+
+ + `X` or `0`: No logging is enabled.
+ + `E` or `1`: Error logging is enabled.
+ + `W` or `2`: Warnings are enabled.
+ + `I` or `3`: Informational messages are enabled.
+ + `D` or `4`: Debug messages are enabled.
+ + `T` or `5`: Trace messages are enabled. These messages can be logged
+ from the realtime threads.
+
+PipeWire uses a `category.topic` naming scheme, with the following categories:
+
+- `pw.*`: PipeWire internal topics.
+- `mod.*`: Module topics, for example `mod.foo` would usually refer to the
+ `foo` module.
+- `ms.*`: Media session topics.
+- `ms.mod.*`: Media session modules, for example `ms.foo` would usually refer
+ to the `media-session-foo` module.
+- `conn.*`: Connection specific topics such as printing raw messages sent over
+ a communication socket. These are in a separate namespace as they are
+ usually vastly more verbose than the normal debugging topics.
+ This namespace must be explicitly enabled with a `conn.<glob>` glob.
+
+The behavior of the logging can be further controlled with the following
+environment variables:
+
+- `PIPEWIRE_LOG_SYSTEMD=false`: Disable logging to the systemd journal.
+- `PIPEWIRE_LOG=<filename>`: Redirect the log to the given filename.
+- `PIPEWIRE_LOG_LINE=false`: Don't log filename, function, and source code line.
+
+*/
diff --git a/doc/pipewire-design.dox b/doc/pipewire-design.dox
new file mode 100644
index 0000000..59b29ed
--- /dev/null
+++ b/doc/pipewire-design.dox
@@ -0,0 +1,70 @@
+/** \page page_design Design
+
+A short overview of PipeWire's design.
+
+PipeWire is a media server that can run graphs of multimedia nodes.
+Nodes can run inside the server process or in separate processes,
+communicating with the server.
+
+PipeWire was designed to:
+
+- Be efficient for raw video using fd passing and audio with
+ shared ringbuffers.
+- Be able to provide/consume/process media from any process.
+- Provide policy to restrict access to devices and streams.
+- Be easily extensible.
+
+Although an initial goal, the design is not limited to raw video
+only and should be able to handle compressed video and other
+media as well.
+
+PipeWire uses the \ref page_spa "SPA plugin API" for the nodes in the graph.
+SPA is designed for low-latency and efficient processing of any multimedia
+format. SPA also provides a number of helper utilities that are not available
+in the standard C library.
+
+Some of the application we intend to build:
+
+- v4l2 device provider: Provide controlled access to v4l2 devices
+ and share one device between multiple processes.
+- gnome-shell video provider: GNOME Shell provides a node that
+ gives the contents of the frame buffer for screen sharing or
+ screen recording.
+- Audio server: Mix and playback multiple audio streams. The design
+ is more like CRAS (Chromium audio server) than PulseAudio and with
+ the added benefit that processing can be arranged in a graph.
+- Professional audio graph processing like JACK.
+- Media playback backend.
+
+
+# Protocol
+
+The native protocol and object model is similar to
+[Wayland](https://wayland.freedesktop.org) but with custom
+serialization/deserialization of messages. This is because the data structures
+in the messages are more complicated and not easily expressible in XML.
+See \ref page_module_protocol_native for details.
+
+
+# Extensibility
+
+The functionality of the server is implemented and extended with modules and
+extensions. Modules are server side bits of logic that hook into various
+places to provide extra features. This mostly means controlling the processing
+graph in some way. See \ref page_pipewire_modules for a list of current
+modules.
+
+Extensions are the client side version of the modules. Most extensions provide
+both a client side and server side init function. New interfaces or new object
+implementation can easily be added with modules/extensions.
+
+Some of the extensions that can be written:
+
+- Protocol extensions: A client/server side API (.h) together with protocol
+ extensions and server/client side logic to implement a new object or
+ interface.
+- A module to check security of method calls.
+- A module to automatically create, link or relink nodes.
+- A module to suspend idle nodes.
+
+*/
diff --git a/doc/pipewire-library.dox b/doc/pipewire-library.dox
new file mode 100644
index 0000000..cecb930
--- /dev/null
+++ b/doc/pipewire-library.dox
@@ -0,0 +1,240 @@
+/** \page page_library PipeWire Library
+
+There are two main components that make up the PipeWire library:
+
+1. An implementation of a graph based media processing engine.
+2. An asynchronous IPC mechanism to manipulate and introspect
+ a graph in another process.
+
+There is usually a daemon that implements the global graph and
+clients that operate on this graph.
+
+The IPC mechanism in PipeWire is inspired by Wayland in that it
+follows the same design principles of objects and methods/events
+along with how this API is presented to the user.
+
+PipeWire has a plugin architecture that allows new features to
+be added (or removed) by the user. Plugins can hook into many
+aspects of PipeWire and change the behaviour or number of
+features dynamically.
+
+
+# Principles
+
+The PipeWire API is an object oriented asynchronous protocol.
+All requests and replies are method invocations on some object.
+
+Objects are identified with a unique ID. Each object implements an
+interface and requests result in invocations of methods on the
+interface.
+
+The protocol is message based. A message sent by a client to the
+server is called a method. A message from the server to the client
+is called an event. Unlike Wayland, these messages are not (yet)
+described in an external protocol file but implemented directly in
+a protocol plugin. Protocol plugins can be added to add new
+objects or even protocols when required.
+
+Messages are encoded with \ref page_spa_pod, which make it
+possible to encode complex objects with right types.
+
+Events from the server can be a reply to a method or can be emitted
+when the server state changes.
+
+Upon connecting to a server, it will broadcast its state. Clients
+should listen for these state changes and cache them. There is no
+need (or mechanism) to query the state of the server.
+
+The server also has a registry object that, when listening to,
+will broadcast the presence of global objects and any changes in
+their state.
+
+State about objects can be obtained by binding to them and listening
+for state changes.
+
+
+# Versioning
+
+All interfaces have a version number. The maximum supported version
+number of an interface is advertized in the registry global event.
+
+A client asks for a specific version of an interface when it binds
+to them. It is the task of the server to adapt to the version of the
+client.
+
+Interfaces increase their version number when new methods or events
+are added. Methods or events should never be removed or changed for
+simplicity.
+
+
+# Proxies and Resources
+
+When a client connects to a PipeWire daemon, a new `struct pw_proxy`
+object is created with ID 0. The `struct pw_core` interface is
+assigned to the proxy.
+
+On the server side there is an equivalent `struct pw_resource` with
+ID 0. Whenever the client sends a message on the proxy (by calling
+a method on the interface of the proxy) it will transparently result
+in a callback on the resource with the same ID.
+
+Likewise if the server sends a message (an event) on a resource, it
+will result in an event on the client proxy with the same ID.
+
+PipeWire will notify a client when a resource ID (and thus also proxy
+ID) becomes unused. The client is responsible for destroying the
+proxy when it no longer wants to use it.
+
+
+# Interfaces
+
+## struct pw_loop
+
+An abstraction for a `poll(2)` loop. It is usually part of one of:
+
+- `struct pw_main_loop`: A helper that can run and stop a `pw_loop`.
+- `struct pw_thread_loop`: A helper that can run and stop a `pw_loop`
+ in a different thread. It also has some helper
+ functions for various thread related synchronization
+ issues.
+- `struct pw_data_loop`: A helper that can run and stop a `pw_loop`
+ in a real-time thread along with some useful helper
+ functions.
+
+## struct pw_context
+
+The main context for PipeWire resources. It keeps track of the mainloop,
+loaded modules, the processing graph and proxies to remote PipeWire
+instances.
+
+An application has to select an implementation of a `struct pw_loop`
+when creating a context.
+
+The context has methods to create the various objects you can use to
+build a server or client application.
+
+## struct pw_core
+
+A proxy to a remote PipeWire instance. This is used to send messages
+to a remote PipeWire daemon and to receive events from it.
+
+A core proxy can be used to receive errors from the remote daemon
+or to perform a roundtrip message to flush out pending requests.
+
+Other core methods and events are used internally for the object
+life cycle management.
+
+## struct pw_registry
+
+A proxy to a PipeWire registry object. It emits events about the
+available objects on the server and can be used to bind to those
+objects in order to call methods or receive events from them.
+
+## struct pw_module
+
+A proxy to a loadable module. Modules implement functionality such
+as provide new objects or policy.
+
+## struct pw_factory
+
+A proxy to an object that can create other objects.
+
+## struct pw_device
+
+A proxy to a device object. Device objects model a physical hardware
+or software device in the system and can create other objects
+such as nodes or other devices.
+
+## struct pw_node
+
+A Proxy to a processing node in the graph. Nodes can have input and
+output ports and the ports can be linked together to form a graph.
+
+## struct pw_port
+
+A Proxy to an input or output port of a node. They can be linked
+together to form a processing graph.
+
+## struct pw_link
+
+A proxy to a link between in output and input port. A link negotiates
+a format and buffers between ports. A port can be linked to many other
+ports and PipeWire will manage mixing and duplicating the buffers.
+
+
+# High Level Helper Objects
+
+Some high level objects are implemented to make it easier to interface
+with a PipeWire graph.
+
+## struct pw_filter
+
+A `struct pw_filter` allows you implement a processing filter that can
+be added to a PipeWire graph. It is comparable to a JACK client.
+
+## struct pw_stream
+
+A `struct pw_stream` makes it easy to implement a playback or capture
+client for the graph. It takes care of format conversion and buffer
+sizes. It is comparable to Core Audio AudioQueue or a PulseAudio
+stream.
+
+
+# Security
+
+With the default native protocol, clients connect to PipeWire using
+a named socket. This results in a client socket that is used to
+send messages.
+
+For sandboxed clients, it is possible to get the client socket via
+other ways, like using the portal. In that case, a portal will
+do the connection for the client and then hands the connection socket
+to the client.
+
+All objects in PipeWire have per client permission bits, currently
+READ, WRITE, EXECUTE and METADATA. A client can not see an object
+unless it has READ permissions. Similarly, a client can only execute
+methods on an object when the EXECUTE bit is set and to modify the
+state of an object, the client needs WRITE permissions.
+
+A client (the portal after it makes a connection) can drop permissions
+on an object. Once dropped, it can never reacquire the permission.
+
+Clients with WRITE/EXECUTE permissions on another client can
+add and remove permissions for the client at will.
+
+Clients with MODIFY permissions on another object can set or remove
+metadata on that object.
+
+Clients that need permissions assigned to them can be started in
+blocked mode and resume when permissions are assigned to them by
+a session manager or portal, for example.
+
+PipeWire uses memfd (`memfd_create(2)`) or DMA-BUF for sharing media
+and data between clients. Clients can thus not look at other clients
+data unless they can see the objects and connect to them.
+
+
+# Implementation
+
+PipeWire also exposes an API to implement the server side objects in
+a graph.
+
+
+# Error Reporting
+
+Functions return either NULL with errno set or a negative int error
+code when an error occurs. Error codes are used from the SPA plugin
+library on which PipeWire is built.
+
+Some functions might return asynchronously. The error code for such
+functions is positive and SPA_RESULT_IS_ASYNC() will return true.
+SPA_RESULT_ASYNC_SEQ() can be used to get the unique sequence number
+associated with the async operation.
+
+The object returning the async result code will have some way to
+signal the completion of the async operation (with, for example, a
+callback). The sequence number can be used to see which operation
+completed.
+
+*/
diff --git a/doc/pipewire-midi.dox b/doc/pipewire-midi.dox
new file mode 100644
index 0000000..77c2b27
--- /dev/null
+++ b/doc/pipewire-midi.dox
@@ -0,0 +1,103 @@
+/** \page page_midi MIDI Support
+
+This document explains how MIDI is implemented.
+
+
+# Use Cases
+
+## MIDI Devices Are Made Available As Processing Nodes/Ports
+
+Applications need to be able to see a port for each stream of a
+MIDI device.
+
+## MIDI Devices Can Be Plugged and Unplugged
+
+When devices are plugged and unplugged the associated nodes/ports
+need to be created and removed.
+
+## Applications Can Connect To MIDI Devices
+
+Applications can create ports that can connect to the MIDI ports
+so that data can be provided to or consumed from them.
+
+## Some MIDI Devices Are Sinks Or Sources For MIDI Data
+
+It should be possible to create a MIDI sink or source that routes the
+MIDI events to specific MIDI ports.
+
+One example of such a sink would be in front of a software MIDI
+renderer.
+
+An example of a MIDI source would be after a virtual keyboard or
+as a mix from many MIDI input devices.
+
+## Applications Should Auto-connect To MIDI Sinks Or Sources
+
+An application should be able to be connected to a MIDI sink when
+it wants to play MIDI data.
+
+An application should be able to connect to a MIDI source when it
+wants to capture MIDI data.
+
+
+# Design
+
+## SPA
+
+MIDI devices/streams are implemented with an \ref spa_node with generic
+control input and output Ports. These ports have a media type of
+`"application/control"` and the data transported over these ports
+are of type \ref spa_pod_sequence with the \ref spa_pod_control type set to
+\ref SPA_CONTROL_Midi.
+
+This means that every MIDI event is timestamped with the sample
+offset against the current graph clock cycle to get sample accurate
+midi events that can be aligned with the corresponding sample data.
+
+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.
+
+## The PipeWire Daemon
+
+Nothing special is implemented for MIDI. Negotiation of formats
+happens between `"application/control"` media types and buffers are
+negotiated in the same way as any generic format.
+
+## The Session Manager
+
+The session manager needs to create the MIDI nodes/ports for the available
+devices.
+
+This can either be done as a single node with ports per device/stream
+or as separate nodes created by a MIDI device monitor.
+
+The session manager needs to be aware of the various MIDI sinks and sources
+in order to route MIDI streams to them from applications that want this.
+
+
+# Implementation
+
+## PipeWire Media Session
+
+PipeWire media session 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.
+
+The media session will check the permissions on `/dev/snd/seq` before
+attempting to create this node. It will also use inotify to wait
+until the sequencer device node is accessible.
+
+## JACK
+
+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.
+
+There is a 1 to 1 mapping between the JACK events and control
+messages so there is no information loss or need for complicated
+conversions.
+
+*/
diff --git a/doc/pipewire-modules.dox b/doc/pipewire-modules.dox
new file mode 100644
index 0000000..322d258
--- /dev/null
+++ b/doc/pipewire-modules.dox
@@ -0,0 +1,84 @@
+/** \page page_pipewire_modules PipeWire Modules
+
+A PipeWire module is effectively a PipeWire client in an `.so` file that
+shares the \ref pw_context with the loading entity. Usually modules are
+loaded when they are listed in the configuration files. For example the
+default configuration file loads several modules:
+
+```
+context.modules = [
+ ...
+ # The native communication protocol.
+ { name = libpipewire-module-protocol-native }
+
+ # The profile module. Allows application to access profiler
+ # and performance data. It provides an interface that is used
+ # by pw-top and pw-profiler.
+ { name = libpipewire-module-profiler }
+
+ # Allows applications to create metadata objects. It creates
+ # a factory for Metadata objects.
+ { name = libpipewire-module-metadata }
+
+ # Creates a factory for making devices that run in the
+ # context of the PipeWire server.
+ { name = libpipewire-module-spa-device-factory }
+ ...
+]
+```
+The matching libraries are:
+```
+$ ls -1 /usr/lib64/pipewire-0.3/libpipewire-module*
+...
+/usr/lib64/pipewire-0.3/libpipewire-module-metadata.so
+/usr/lib64/pipewire-0.3/libpipewire-module-profiler.so
+/usr/lib64/pipewire-0.3/libpipewire-module-protocol-native.so
+/usr/lib64/pipewire-0.3/libpipewire-module-spa-device-factory.so
+...
+```
+
+A module's entry point is the `pipewire__module_init` function, see \ref
+PIPEWIRE_SYMBOL_MODULE_INIT.
+
+\code
+int pipewire__module_init(struct pw_impl_module *module, const char *args).`
+\endcode
+
+See the \ref page_module_example_sink and \ref page_module_example_source
+modules for a general oveview of how modules look like.
+
+List of known modules:
+
+- \subpage page_module_access
+- \subpage page_module_adapter
+- \subpage page_module_avb
+- \subpage page_module_client_device
+- \subpage page_module_client_node
+- \subpage page_module_combine_stream
+- \subpage page_module_echo_cancel
+- \subpage page_module_example_sink
+- \subpage page_module_example_source
+- \subpage page_module_fallback_sink
+- \subpage page_module_filter_chain
+- \subpage page_module_link_factory
+- \subpage page_module_loopback
+- \subpage page_module_metadata
+- \subpage page_module_pipe_tunnel
+- \subpage page_module_portal
+- \subpage page_module_profiler
+- \subpage page_module_protocol_native
+- \subpage page_module_protocol_pulse
+- \subpage page_module_protocol_simple
+- \subpage page_module_pulse_tunnel
+- \subpage page_module_raop_sink
+- \subpage page_module_raop_discover
+- \subpage page_module_roc_sink
+- \subpage page_module_roc_source
+- \subpage page_module_rtp_sink
+- \subpage page_module_rtp_source
+- \subpage page_module_rt
+- \subpage page_module_session_manager
+- \subpage page_module_x11_bell
+- \subpage page_module_zeroconf_discover
+
+*/
diff --git a/doc/pipewire-objects-design.dox b/doc/pipewire-objects-design.dox
new file mode 100644
index 0000000..f67b6b6
--- /dev/null
+++ b/doc/pipewire-objects-design.dox
@@ -0,0 +1,347 @@
+/** \page page_objects_design Objects Design
+
+This document is a design reference on the various objects that exist
+in the PipeWire media and session management graphs. Explaining what these
+objects are, how they are meant to be used, and how they relate to other
+kinds of objects and concepts that exist in subsystems or other libraries.
+
+
+# The Media Graph
+
+The media graph represents and enables the media flow inside the PipeWire
+daemon and between the daemon and its clients. It consists of nodes, ports
+and links.
+
+```
++------------+ +------------+
+| | | |
+| +--------+ Link +--------+ |
+| Node | Port |--------| Port | Node |
+| +--------+ +--------+ |
+| | | |
++------------+ +------------+
+```
+
+## Node
+
+A **node** is a media processing element. It consumes and/or produces buffers
+that contain data, such as audio or video.
+
+A node may operate entirely inside the PipeWire daemon or it may be operating
+in a client process. In the second case, media is transferred to/from that
+client using the PipeWire protocol.
+
+In an analogy to GStreamer, a _node_ is similar (but not equal) to a
+GStreamer _element_.
+
+## Port
+
+A **port** is attached on a **node** and provides an interface for input
+or output of media on the node. A node may have multiple ports.
+
+A port always has a direction, input or output:
+
+- Input: it allows media input into the node (in other terms, it is a _sink_)
+- Output: it outputs media out of the node (in other terms, it is a _source_)
+
+In an analogy to GStreamer, a _port_ is similar (but not equal) to a
+GStreamer _pad_.
+
+## Link
+
+A **link** connects two ports of opposite direction, making media flow from
+the output port to the input port.
+
+
+# The Session Management Graph
+
+The session management graph is a virtual, higher level representation of the
+media flow. It is created entirely by the session manager and it can affect
+the routing on the media graph only through the session manager's actions.
+
+The session management graph is useful to abstract the complexity of the
+actual media flow both for the target user and for the policy management
+codebase.
+
+```
++---------------------+ +----------------------+
+| | | |
+| +----------------+ Endpoint Link +----------------+ |
+| Endpoint |Endpoint Stream |-----------------|Endpoint Stream | Endpoint |
+| +----------------+ +----------------+ |
+| | | |
++---------------------+ +----------------------+
+```
+
+## Endpoint
+
+An **endpoint** is a session management object that provides a representation
+of user conceivable places where media can be routed to/from.
+
+Examples of endpoints associated with hardware on a desktop-like system:
+
+- Laptop speakers.
+- USB webcam.
+- Bluetooth headset microphone.
+- Line out stereo jack port.
+
+Examples of endpoints associated with hardware in a car:
+
+- Speakers amplifier.
+- Front right seat microphone array.
+- Rear left seat headphones.
+- Bluetooth phone voice gateway.
+- Hardware FM radio device.
+
+Examples of endpoints associated with software:
+
+- Desktop screen capture source.
+- Media player application.
+- Camera application.
+
+In most cases an endpoint maps to a node on the media graph, but this is not
+always the case. An endpoint may be backed by several nodes or no nodes at all.
+Different endpoints may also be sharing nodes in some cases.
+
+An endpoint that does not map to any node may be useful to represent hardware
+that the session manager needs to be able to control, but there is no way
+to route media to/from that hardware through the PipeWire media graph. For
+example, in a car we may have a CD player device that is directly wired to the
+speakers amplifier and therefore audio flows between them without passing
+through the controlling CPU. However, it is useful for the session manager to
+be able to represent the *CD player endpoint* and the _endpoint link_ between
+it and the amplifier, so that it can apply audio policy that takes into account
+whether the CD player is playing or not.
+
+### Target
+
+An **endpoint** may be grouping together targets that can be reached by
+following the same route and they are mutually exclusive with each other.
+
+For example, the speakers and the headphones jack on a laptop are usually
+mutually exclusive by hardware design (hardware mutes the speakers when the
+headphones are enabled) and they share the same ALSA PCM device, so audio still
+follows the same route to reach both.
+
+In this case, a session manager may choose to group these two targets into the
+same endpoint, using a parameter on the _endpoint_ object to allow the user
+to choose the target (if the hardware allows configuring this at all).
+
+## Endpoint Stream
+
+An **endpoint stream** is attached to an **endpoint** and represents a logical
+path that can be taken to reach this endpoint, often associated with
+a _use case_.
+
+For example, the "Speakers amplifier" endpoint in a car might have the
+following streams:
+
+- _Music_: A path to play music;
+ the implementation will output this to all speakers, using the volume
+ that has been configured for the "Music" use case.
+- _Voice_: A path to play a voice message; such as a navigation message or
+ feedback from a voice assistant, the implementation will output this
+ to the front speakers only. Lowering the volume of the music (if any)
+ on these speakers at the same time.
+- _Emergency_: A path to play an emergency situation sound (a beep,
+ or equivalent); the implementation will output this on all speakers.
+ Increasing the volume to a factory defined value if necessary (to ensure
+ that it is audible) while muting audio from all other streams at the
+ same time.
+
+In another example, a microphone that can be used for activating a voice
+assistant might have the following streams:
+
+- _Capture_: A path to capture directly from the microphone; this can be used
+ by an application that listens for the assistant's wake-word in order
+ to activate the full voice recognition engine.
+- _CaptureDelayed_: A path to capture with a constant delay (meaning that
+ starting capturing now will actually capture something that was spoken
+ a little earlier); this can be used by the full voice recognition engine,
+ allowing it to start after the wake-word has been spoken while capturing
+ audio that also includes the wake-word.
+
+Endpoint streams may be mutually exclusive or they may used simultaneously,
+depending on the implementation.
+
+Endpoint streams may be implemented in many ways:
+
+- By plugging additional nodes in the media graph that link to the device node
+ (ex. a simple buffering node linked to an alsa source node could implement
+ the _CaptureDelayed_ stream in the above microphone example).
+- By using a different device node (ex. different ALSA device on the same card)
+ that has a special meaning for the hardware.
+- By triggering switches on the hardware (ex. modify ALSA controls on the
+ same device).
+
+## Endpoint Link
+
+An **endpoint link** connects two streams from two different endpoints, creating
+a logical representation of media flow between the endpoints.
+
+An **endpoint link** may be implemented by creating one or more _links_ in the
+underlying media graph, or it may be implemented by configuring hardware
+resources to enable media flow, in case the flow does not pass through the
+media graph.
+
+### Constructing
+
+Constructing an **endpoint link** is done by asking the _endpoint stream_
+objects to prepare it. First, the source stream is asked to provide linking
+information. When the information is retrieved, the sink stream is asked to
+use this information to prepare and to provide its own linking information.
+When this is done, the session manager is asked to create the link using the
+provided information.
+
+This mechanism allows stream implementations:
+
+- To prepare for linking, adjusting hardware paths if necessary.
+- To check for stream linking compatibility; not all streams can be connected
+ to all others (ex. streams with media flow in the hardware cannot be linked
+ to streams that are backed by nodes in the media graph).
+- To provide implementation specific information for linking; in the standard
+ case this is going to be a list of _ports_ to be linked in the media graph,
+ but in a hardware-flow case it can be any kind of hardware-specific detail.
+
+
+# Other Related Objects
+
+## Device
+
+A **device** represents a handle to an underlying API that is used to create
+higher level objects, such as nodes, or other devices.
+
+Well-known devices include:
+
+| Device API | Description |
+| :--- | :--- |
+| alsa.pcm.device | A handle to an ALSA card (ex. `hw:0`, `hw:1`, etc). |
+| alsa.seq.device | A handle to an ALSA Midi device. |
+| v4l2.device | A handle to a V4L2 device (`/dev/video0`, `/dev/video1`, etc..). |
+| jack.device | A JACK client, allowing PipeWire to slave to JACK for audio input/output. |
+
+A device may have a _profile_, which allows the user to choose between
+multiple configurations that the device may be capable of having, or to simply
+turn the device _off_, which means that the handle is closed and not used
+by PipeWire.
+
+## Session
+
+The **session** represents the session manager and can be used to expose
+global properties or methods that affect the session management.
+
+### Default Endpoints
+
+The session is responsible for book-keeping the default device endpoints (one
+for each kind of device) that is to be used to link new clients when
+simulating a PulseAudio-like behavior, where the user can choose from the UI
+device preferences.
+
+For example, a system may have both "Speakers" and "HDMI" endpoints on the
+"Audio Output" category and the user may be offered to make a choice within
+the UI to select which endpoint they want to use by default for audio output.
+This preference is meant to be stored in the session object.
+
+### Multiple Sessions
+
+It is not currently defined whether it is allowed to have multiple sessions
+or not and how the system should behave if this happens.
+
+
+# Mappings To Underlying Subsystem Objects
+
+## ALSA UCM
+
+This is a ***proposal***
+
+| ALSA / UCM | PipeWire |
+| :--- | :--- |
+| ALSA card | device |
+| UCM verb | device profile |
+| UCM device | endpoint (+ target, grouping conflicting devices into the same endpoint) |
+| UCM modifier | endpoint stream |
+| PCM stream | node |
+
+In UCM mode, an ALSA card is represented as a PipeWire device, with the
+available UCM verbs listed as profiles of the device.
+
+Activating a profile (ie. a verb) will create the necessary nodes for the
+available PCM streams and at the same time it will also create one endpoint
+for each UCM device. Optionally conflicting UCM devices can be grouped in
+the same endpoint, listing the conflicting options as targets of the endpoint.
+
+The available UCM modifiers for each UCM device will be added as streams, plus
+one "default" stream for accessing the device with no modifiers.
+
+## ALSA Fallback
+
+| ALSA | PipeWire |
+| :--- | :--- |
+| card | device |
+| PCM stream | node + endpoint |
+
+In the case where UCM (or another similar mechanism) is not available,
+ALSA cards are represented as PipeWire devices with only two profiles on/off.
+
+When the on profile is activated, a node and an associated endpoint are created
+for every available PCM stream.
+
+Endpoints in this case have only one "default" stream, unless they are extended
+by the session manager to have software-backed streams.
+
+## V4L2
+
+***FIXME***
+
+| V4L2 | PipeWire |
+| :--- | :--- |
+| device | device + node |
+
+
+# Relationship To Other API's
+
+## PulseAudio
+
+### Mapping PipeWire Objects For Access By PulseAudio Clients
+
+| PipeWire | PulseAudio |
+| :--- | :--- |
+| device | card |
+| device profile | card profile |
+| endpoint (associated with a device) | sink / source |
+| endpoint (associated with a client) | sink-input / source-output |
+| endpoint target | port |
+| endpoint stream | N/A, PA clients will be limited to the default stream |
+
+### Mapping PulseAudio Clients To PipeWire
+
+| PulseAudio | PipeWire |
+| :--- | :--- |
+| stream | client + node + endpoint (no targets, 1 default stream) |
+
+## Jack
+
+Note: This section is about JACK clients connecting to PipeWire through the
+JACK compatibility library. The scenario where PipeWire connects to another
+JACK server as a client is out of scope here.
+
+### Mapping PipeWire Objects For Access By JACK Clients
+
+| PipeWire | JACK |
+| :--- | :--- |
+| node | client |
+| port | port |
+| device | N/A |
+| endpoint | N/A |
+
+### Mapping JACK Clients To PipeWire
+
+| JACK | PipeWire |
+| :--- | :--- |
+| client | client + node |
+| port | port |
+
+JACK clients do not create endpoints. A session manager should be JACK aware
+in order to anticipate direct node linking.
+
+*/
diff --git a/doc/pipewire-portal.dox b/doc/pipewire-portal.dox
new file mode 100644
index 0000000..721d981
--- /dev/null
+++ b/doc/pipewire-portal.dox
@@ -0,0 +1,215 @@
+/** \page page_portal Portal Access Control
+
+This document explains how clients from the portal are handled.
+
+The portal is a DBus service that exposes interfaces to
+request access to the PipeWire daemon to perform a certain set of
+functions. The PipeWire daemon runs outside the sandbox, the portal is a way
+for clients inside the sandbox to connect to and use PipeWire.
+
+The PipeWire socket is not exposed in the sandbox. Instead, The portal
+connects to PipeWire on behalf of the client, informing PipeWire that this
+client is a portal-managed client. PipeWire can detect and enforce
+extra permission checks on the portal managed clients.
+
+Once such portal is the [camera
+portal](https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.portal.Camera)
+that provides a PipeWire session to stream video from a camera.
+
+
+# Use Cases
+
+## New Portal Managed Clients Need Device Permissions Configured
+
+When a new client is detected, the available objects need to be
+scanned and permissions configured for each of them.
+
+Only the devices belonging to the media_roles given by the
+portal are considered.
+
+## New Devices Need To Be Made Visible To Portal Managed Clients
+
+Newly created objects are made visible to a client when the client
+is allowed to interact with it.
+
+Only the devices belonging to the media_roles given by the
+portal are considered.
+
+## Permissions For A Device Need To Be Revoked
+
+The session manager listens to changes in the permissions of devices
+and will remove the client permissions accordingly.
+
+Usually this is implemented by listening to the permission store
+DBus object. The desktop environment might provide a configuration panel
+where these permissions can be managed.
+
+
+# Design
+
+## The Portal
+
+A sandboxed client cannot connect to PipeWire directly. Instead, it connects
+to the sandbox side of the portal which then connects the PipeWire daemon to
+configure the session. The portal then hands the file descriptor of the
+PipeWire connection to the client and the client can use this file descriptor
+to interface with the PipeWire session directly.
+
+When the portal connects, it will set the following properties on the
+client object:
+
+- `"pipewire.access.portal.is_portal" = true` for the connection of the
+ portal itself (as opposed to a client managed by the portal).
+- `"pipewire.access.portal.app_id"` the [application ID](https://docs.flatpak.org/en/latest/conventions.html#application-ids) of the client.
+- `"pipewire.access.portal.media_roles"` media roles of the client.
+ Currently only `"Camera"` is defined.
+
+Before returning the connection to a client, the portal configures
+minimal permissions on the client. No objects are initially visible. It is
+the task of the \ref page_session_manager to make the objects in the graph
+visible, depending on the client's `media_roles` (see also \ref
+PW_KEY_MEDIA_ROLE).
+
+## The PipeWire Portal Module
+
+The PipeWire daemon uses the \ref page_module_portal to find the PID of the
+processes that owns the DBus name `org.freedesktop.portal.Desktop`
+(see the [XDG Desktop Portal](https://github.com/flatpak/xdg-desktop-portal)).
+
+Client connections from this PID are tagged as \ref PW_KEY_ACCESS
+`"portal"` (see \ref page_module_access). It will also set ALL permissions for
+this client so that it can resume.
+
+\dot
+digraph pw {
+ compound=true;
+ node [shape="box"];
+ rankdir="TB";
+
+ dbus [label="org.freedesktop.portal.Desktop"];
+
+ portal_access [label="PipeWire (mod: Portal Access)"];
+ portal [label="xdg-desktop-portal"];
+
+ dbus -> portal_access [arrowhead="dot"];
+ dbus -> portal [arrowhead="dot"];
+
+ portal_access -> portal [label="pipewire.access = portal"];
+
+ { rank="same"; portal_access; portal}
+}
+\enddot
+
+## The Client
+
+A client can ask the portal for a connection to the PipeWire daemon.
+
+\dot
+digraph pw {
+ compound=true;
+ node [shape="box"];
+ rankdir="LR";
+
+ pw [label="PipeWire"];
+ portal [label="xdg-desktop-portal"];
+ client [label="client"];
+
+ client -> portal;
+ portal -> pw [label="portal.is_portal=true", arrowhead="none"]
+
+ {rank="min"; pw};
+ {rank="max"; client};
+}
+\enddot
+
+The portal maintains an (unrestricted) connection to the PipeWire daemon with
+`"pipewire.access.portal.is_portal" = true` to identify the nodes the client
+needs access to. It then creates a new restricted connection for the client,
+tagged with additional information.
+
+\dot
+digraph pw {
+ compound=true;
+ node [shape="box"];
+ rankdir="LR";
+
+ pw [label="PipeWire"];
+ portal [label="xdg-desktop-portal"];
+ client [label="client"];
+
+ client -> portal [arrowhead="none"];
+ portal -> pw [label="portal.is_portal=true", arrowhead="none"]
+ portal -> pw [label="portal.app_id = $appid"]
+
+ {rank="min"; pw};
+ {rank="max"; client};
+}
+\enddot
+
+The file descriptor for this restricted connection is passed back to the
+client which can now make use of the resources it has been permitted to
+access.
+
+\dot
+digraph pw {
+ compound=true;
+ node [shape="box"];
+ rankdir="LR";
+
+ pw [label="PipeWire"];
+ portal [label="xdg-desktop-portal"];
+ client [label="client"];
+
+ portal -> pw [label="portal.is_portal=true", arrowhead="none"]
+
+ pw->client [label="restricted connection"];
+
+ {rank="min"; pw};
+ {rank="max"; client};
+}
+\enddot
+
+## The Session Manager
+
+The session manager listens for new clients to appear. It will use the
+\ref PW_KEY_ACCESS property to find portal connections. For client connections
+from the portal the session manager checks the requested `media_roles` and
+enables or disables access to the respective PipeWire objects.
+It might have to consult a database to decide what is allowed, for example the
+[org.freedesktop.impl.portal.PermissionStore](https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.impl.portal.PermissionStore).
+
+\dot
+strict digraph pw {
+ compound=true;
+ node [shape="box"];
+ rankdir="LR";
+
+ portal [label="xdg-desktop-portal"];
+ client [label="client"];
+
+
+ subgraph {
+ rankdir="TB";
+ permissions [label="PermissionStore"];
+
+ sm->permissions;
+
+ sm [label="Session Manager"];
+ pw [label="PipeWire"];
+ sm -> pw [headlabel="allow $media.roles"];
+ pw -> sm;
+ portal -> pw [label="portal.app_id = $appid"];
+ }
+
+ client -> portal [arrowhead="none"];
+
+ {rank="min"; sm, pw};
+ {rank="max"; client};
+}
+\enddot
+
+In the case of the [XDG Desktop
+Portal](https://github.com/flatpak/xdg-desktop-portal), the portal itself
+queries the PermissionStore directly.
+
+*/
diff --git a/doc/pipewire-session-manager.dox b/doc/pipewire-session-manager.dox
new file mode 100644
index 0000000..d21ba9a
--- /dev/null
+++ b/doc/pipewire-session-manager.dox
@@ -0,0 +1,46 @@
+/** \page page_session_manager PipeWire Session Manager
+
+The \ref page_daemon is primarily a framework that allows devices and
+applications to exchange data.
+
+It provides the mechanism to do so but the policy deciding which components
+can talk to each other and when is controlled by the session manager. As
+outlined in \ref page_objects_design, PipeWire provides a media graph
+consisting of devices, nodes and ports. The session manager is the one that
+decides on the links between those elements.
+
+Two prominent session managers currently exist:
+
+- [PipeWire Media Session](https://gitlab.freedesktop.org/pipewire/media-session), the
+example session manager.
+- [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber), a
+modular session manager based on GObject.
+[Documentation](https://pipewire.pages.freedesktop.org/wireplumber/)
+
+This page describes some of the requirements for session managers in general.
+
+
+# Client Management
+
+PipeWire provides a \ref page_access "permission system" to limit client's
+access to resources but only \ref page_module_access "basic permission
+handling". The session manager is expected to decide whether clients may
+access specific resources.
+
+
+# Device Management
+
+PipeWire's responsibility is to open devices, however the decision on which
+devices should be opened is the job of a session manager, including the
+configuration of those devices.
+
+
+# Endpoint Grouping
+
+An endpoint is, effectively, a group of nodes that are a logical unit that can
+consume or produce media data. For example, a Bluetooth speaker may present as
+several nodes but is only one logical unit to stream audio to.
+
+See \ref page_objects_design for details on Endpoints.
+
+*/
diff --git a/doc/pipewire-tools.dox.in b/doc/pipewire-tools.dox.in
new file mode 100644
index 0000000..e0bf116
--- /dev/null
+++ b/doc/pipewire-tools.dox.in
@@ -0,0 +1,7 @@
+/** \page page_tools PipeWire Tools
+
+Manual pages:
+
+@man_subpages@
+
+*/
diff --git a/doc/pipewire.dox b/doc/pipewire.dox
new file mode 100644
index 0000000..03b9c6f
--- /dev/null
+++ b/doc/pipewire.dox
@@ -0,0 +1,26 @@
+/** \page page_pipewire PipeWire Design
+
+# Internals
+
+- \subpage page_design
+- \subpage page_audio
+- \subpage page_access
+- \subpage page_portal
+- \subpage page_midi
+- \subpage page_objects_design
+- \subpage page_library
+- \subpage page_dma_buf
+
+
+# Components
+
+- \subpage page_daemon
+- \subpage page_tools
+- \subpage page_session_manager
+
+
+# Backends
+
+- \subpage page_pulseaudio
+
+*/
diff --git a/doc/pulseaudio.dox b/doc/pulseaudio.dox
new file mode 100644
index 0000000..2ac19a9
--- /dev/null
+++ b/doc/pulseaudio.dox
@@ -0,0 +1,69 @@
+/** \page page_pulseaudio PulseAudio Compatibility
+
+# Internals - Mapping Between ALSA and Streams
+
+This explains the mapping between alsa cards and streams and session manager
+objects.
+
+## ALSA Cards
+
+An ALSA card is exposed as a PipeWire device.
+
+## Streams
+
+Each ALSA PCM is opened and a node is created for each PCM stream.
+
+
+# Session Manager
+
+## ALSA UCM
+
+The mapping of the PipeWire object hierarchy to the ALSA object hierarchy is the following:
+
+One PipeWire device is created for every ALSA card.
+
+- For each UCM verb, a node is created for the associated PCM devices.
+- For each UCM verb, an endpoint is created.
+
+In a first step: For each available combination of UCM device and modifier,
+a stream is created. Streams are marked with compatible other streams.
+
+Streams with the same modifier and mutually exclusive devices are grouped
+into one stream and the UCM devices are exposed on the endpoint as destinations.
+
+## ALSA Fallback
+
+Each PCM stream (node) becomes an endpoint. The endpoint references the
+ALSA device ID.
+
+Each endpoint has one stream (for now) called HiFi Playback / HiFi Capture.
+
+More streams can be created depending on the format of the node.
+
+## ALSA Pulse UCM
+
+Using the ALSA backend of PulseAudio we can create the following streams.
+
+## ALSA Pulse Fallback
+
+The pulse ALSA backend will use the mixer controls and some probing to
+create the following nodes and endpoints.
+
+
+# PulseAudio
+
+PulseAudio uses the session manager API to construct cards with profiles
+and sink/source with ports.
+
+If an endpoint references a device, a card object is created for the device.
+
+Each endpoint becomes a sink/source.
+
+Each Stream in the endpoint becomes a profile on the PulseAudio card. Because
+only one profile is selected on the device, only one stream is visible on
+the endpoint. This clashes with the notion that multiple streams can be
+active at the same time but is a PulseAudio limitation.
+
+Each Endpoint destination becomes a port on the sink/source.
+
+*/
diff --git a/doc/spa-buffer.dox b/doc/spa-buffer.dox
new file mode 100644
index 0000000..ddd0935
--- /dev/null
+++ b/doc/spa-buffer.dox
@@ -0,0 +1,71 @@
+/** \page page_spa_buffer SPA Buffers
+
+> What is the array of `spa_data` in `spa_buffer`?
+
+A \ref spa_buffer "SPA Buffer" contains metadata and data. There can be many metadata items (headers, color info, cursor position, etc) in the buffer. The metadata items are stored in the metas array. In the same way, the buffer can contain multiple data blocks in the datas array. Each data block is, for example, a video plane or an audio channel. There are `n_datas` of those blocks.
+
+> What is the `void*` data pointer in `spa_data`?
+
+The data information either has a file descriptor or a data pointer. The type of the `spa_data` tells you what to expect. For a file descriptor, the data pointer can optionally be set when the FD is mapped into memory. Otherwise the user has to mmap the data themselves.
+
+Also associated with each `spa_data` is a chunk, which is read/write and contains the valid region in the `spa_data` (offset, size, stride and some flags).
+
+The reason why is this set up like this is that the metadata memory, the data and chunks can be directly transported in shared memory while the buffer structure can be negotiated separately (describing the shared memory). This way buffers can be shared but no process can destroy the structure of the buffers.
+
+
+ * The buffer skeleton is placed in memory like below and can
+ * be accessed as a regular structure.
+ *
+ * +==============================+
+ * | struct spa_buffer |
+ * | uint32_t n_metas | number of metas
+ * | uint32_t n_datas | number of datas
+ * +-| struct spa_meta *metas | pointer to array of metas
+ * +|-| struct spa_data *datas | pointer to array of datas
+ * || +------------------------------+
+ * |+>| struct spa_meta |
+ * | | uint32_t type | metadata
+ * | | uint32_t size | size of metadata
+ * +|--| void *data | pointer to metadata
+ * || | ... <n_metas> | more spa_meta follow
+ * || +------------------------------+
+ * |+->| struct spa_data |
+ * | | uint32_t type | memory type
+ * | | uint32_t flags |
+ * | | int fd | fd of shared memory block
+ * | | uint32_t mapoffset | offset in shared memory of data
+ * | | uint32_t maxsize | size of data block
+ * | +-| void *data | pointer to data
+ * |+|-| struct spa_chunk *chunk | pointer to chunk
+ * ||| | ... <n_datas> | more spa_data follow
+ * ||| +==============================+
+ * VVV
+ *
+ * metadata, chunk and memory can either be placed right
+ * after the skeleton (inlined) or in a separate piece of memory.
+ *
+ * vvv
+ * ||| +==============================+
+ * +-->| meta data memory | metadata memory, 8 byte aligned
+ * || | ... <n_metas> |
+ * || +------------------------------+
+ * +->| struct spa_chunk | memory for n_datas chunks
+ * | | uint32_t offset |
+ * | | uint32_t size |
+ * | | int32_t stride |
+ * | | int32_t dummy |
+ * | | ... <n_datas> chunks |
+ * | +------------------------------+
+ * +>| data | memory for n_datas data, aligned
+ * | ... <n_datas> blocks | according to alignments
+ * +==============================+
+
+Taken from [here](https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/11f95fe11e07192cec19fddb4fafc708e023e49c/spa/include/spa/buffer/alloc.h).
+
+
+
+\addtogroup spa_buffer
+
+See: \ref page_spa_buffer
+
+*/
diff --git a/doc/spa-design.dox b/doc/spa-design.dox
new file mode 100644
index 0000000..403e27d
--- /dev/null
+++ b/doc/spa-design.dox
@@ -0,0 +1,35 @@
+/** \page page_spa_design SPA Design
+
+# Conventions
+
+## Types
+
+Types are generally divided into two categories:
+
+- String types: They identify interfaces and highlevel object types.
+- Integer types: These are enumerations used in the parts where high
+ performance/ease of use/low space overhead is needed.
+
+The SPA type is system is statis and very simple but still allows you
+to make and introspect complex object type hierarchies.
+
+See the type system docs for more info.
+
+## Error Codes
+
+SPA uses negative integers as errno style error codes. Functions that return an
+int result code generated an error when < 0. `spa_strerror()` can be used to
+get a string representation of the error code.
+
+SPA also has a way to encode asynchronous results. This is done by setting a
+high bit (bit 30, the `ASYNC_BIT`) in the result code and a sequence number
+in the lower bits. This result is normally identified as a positive success
+result code and the sequence number can later be matched to the completion
+event.
+
+## Useful Macros
+
+SPA comes with some useful macros defined in `<spa/utils/defs.h>` and a
+number of utility functions, see \ref spa_utils.
+
+*/
diff --git a/doc/spa-index.dox b/doc/spa-index.dox
new file mode 100644
index 0000000..f7f5ffe
--- /dev/null
+++ b/doc/spa-index.dox
@@ -0,0 +1,88 @@
+/** \page page_spa SPA (Simple Plugin API)
+
+\ref api_spa (Simple Plugin API) is an extensible API to implement all kinds of
+plugins.
+
+It is inspired by many other plugin APIs, mostly LV2 and
+GStreamer. SPA provides two parts:
+
+- A header-only API with no external dependencies.
+- A set of support libraries ("plugins") for commonly used functionality.
+
+The usual approach is that PipeWire and PipeWire clients can use the
+header-only functions to interact with the plugins. Those plugins are
+usually loaded at runtime (through `dlopen(3)`).
+
+
+# Motivation
+
+SPA was designed with the following goals in mind:
+
+- No dependencies, SPA is shipped as a set of header files that have no dependencies except for the standard C library.
+- Very efficient both in space and in time.
+- Very configurable and usable in many different environments. All aspects
+ of the plugin environment can be configured and changed, like logging,
+ poll loops, system calls, etc.
+- Consistent API.
+- Extensible; new API can be added with minimal effort, existing API can be updated and versioned.
+
+The original user of SPA is PipeWire, which uses SPA to implement the
+low-level multimedia processing plugins, device detection, mainloops, CPU
+detection, logging, among other things. SPA however can be used outside
+of PipeWire with minimal problems.
+
+
+# The SPA Header-Only API
+
+A very simple example on how SPA headers work are the \ref spa_utils, a set
+of utilities commonly required by C projects. SPA functions use the `spa_`
+namespace and are easy to identify.
+
+\code
+/* cc $(pkg-config --cflags libspa-0.2) -o spa-test spa-test.c */
+
+#include <stdint.h>
+#include <spa/utils/string.h>
+
+int main(int argc, char **argv) {
+ uint32_t val;
+
+ if (spa_atoi32(argv[1], &val, 16))
+ printf("argv[1] is hex %#x\n", val);
+ else
+ printf("argv[1] is not a hex number\n");
+
+ return 0;
+}
+\endcode
+
+
+# SPA Plugins
+
+SPA plugins are shared libraries (`.so` files) that can be loaded at
+runtime. Each library provides one or more "factories", each of which may
+implement several "interfaces". Code that uses SPA plugins then uses those
+interfaces (through SPA header files) to interact with the plugin.
+
+For example, the PipeWire daemon can load the normal `printf`-based logger
+or a systemd journal-based logger. Both of those provide the \ref spa_log
+interface and once instantiated, PipeWire no longer has to differentiate
+between the two logging facilities.
+
+Please see \ref page_spa_plugins for the details on how to use SPA plugins.
+
+
+# Further details
+
+- \ref api_spa
+- \subpage page_spa_design
+- \subpage page_spa_plugins
+- \subpage page_spa_pod
+- \subpage page_spa_buffer
+
+
+\addtogroup api_spa
+
+See: \ref page_spa, \ref page_spa_design
+
+*/
diff --git a/doc/spa-plugins.dox b/doc/spa-plugins.dox
new file mode 100644
index 0000000..af14d5e
--- /dev/null
+++ b/doc/spa-plugins.dox
@@ -0,0 +1,360 @@
+/** \page page_spa_plugins SPA Plugins
+
+\ref spa_handle "SPA plugins" are dynamically loadable objects that contain objects and interfaces that
+can be introspected and used at runtime in any application. This document
+introduces the basic concepts of SPA plugins. It first covers using the API
+and then talks about implementing new plugins.
+
+
+# Outline
+
+To use a plugin, the following steps are required:
+
+- **Load** the shared library.
+- **Enumerate** the available factories.
+- **Enumerate** the interfaces in each factory.
+- **Instantiate** the desired interface.
+- **Use** the interface-specific functions.
+
+In pseudo-code, loading a logger interface looks like this:
+
+\code{.py}
+handle = dlopen("$SPA_PLUGIN_DIR/support/libspa-support.so")
+factory_enumeration_func = dlsym(handle, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)
+spa_log *logger = NULL
+
+while True:
+ factory = get_next_factory(factory_enumeration_func):
+ if factory != SPA_NAME_SUPPORT_LOG: # <spa/utils/name.h>
+ continue
+
+ interface_info = get_next_interface_info(factory)
+ if info->type != SPA_TYPE_INTERFACE_Log: # </spa/support/log.h>
+ continue
+
+ interface = spa_load_interface(handle, interface_info->type)
+ logger = (struct spa_log *)interface
+ break
+
+spa_log_error(log, "This is an error message\n")
+\endcode
+
+SPA does not specify where plugins need to live, although plugins are
+normally installed in `/usr/lib64/spa-0.2/` or equivalent. Plugins and API
+are versioned and many versions can live on the same system.
+
+\note The directory the SPA plugins reside in is available through
+ `pkg-config --variable plugindir libspa-0.2`
+
+The `spa-inspect` tool provides a CLI interface to inspect SPA plugins:
+
+\verbatim
+$ export SPA_PLUGIN_DIR=$(pkg-config --variable plugindir libspa-0.2)
+$ spa-inspect ${SPA_PLUGIN_DIR}/support/libspa-support.so
+...
+factory version: 1
+factory name: 'support.cpu'
+factory info:
+ none
+factory interfaces:
+ interface: 'Spa:Pointer:Interface:CPU'
+factory instance:
+ interface: 'Spa:Pointer:Interface:CPU'
+skipping unknown interface
+factory version: 1
+factory name: 'support.loop'
+factory info:
+ none
+factory interfaces:
+ interface: 'Spa:Pointer:Interface:Loop'
+ interface: 'Spa:Pointer:Interface:LoopControl'
+ interface: 'Spa:Pointer:Interface:LoopUtils'
+...
+\endverbatim
+
+
+# Open A Plugin
+
+A plugin is opened with a platform specific API. In this example we use
+`dlopen()` as the method used on Linux.
+
+A plugin always consists of two parts, the vendor path and then the .so file.
+
+As an example we will load the "support/libspa-support.so" plugin. You will
+usually use some mapping between functionality and plugin path as we'll see
+later, instead of hardcoding the plugin name.
+
+To `dlopen` a plugin we then need to prefix the plugin path like this:
+
+\code{.c}
+#define SPA_PLUGIN_DIR /usr/lib64/spa-0.2/"
+void *hnd = dlopen(SPA_PLUGIN_DIR"/support/libspa-support.so", RTLD_NOW);
+\endcode
+
+The environment variable `SPA_PLUGIN_DIR` and `pkg-config` variable
+`plugindir` are usually used to find the location of the plugins. You will
+have to do some more work to construct the shared object path.
+
+The plugin must have exactly one public symbol, called
+`spa_handle_factory_enum`, which is defined with the macro
+`SPA_HANDLE_FACTORY_ENUM_FUNC_NAME` to get some compile time checks and avoid
+typos in the symbol name. We can get the symbol like so:
+
+\code{.c}
+spa_handle_factory_enum_func_t enum_func;
+enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME));
+\endcode
+
+If this symbol is not available, the library is not a valid SPA plugin.
+
+
+# Enumerating Factories
+
+With the `enum_func` we can now enumerate all the factories in the plugin:
+
+\code{.c}
+uint32_t i;
+const struct spa_handle_factory *factory = NULL;
+for (i = 0;;) {
+ if (enum_func(&factory, &i) <= 0)
+ break;
+ // check name and version, introspect interfaces,
+ // do something with the factory.
+}
+\endcode
+
+A factory has a version, a name, some properties and a couple of functions
+that we can check and use. The main use of a factory is to create an
+actual new object from it.
+
+We can enumerate the interfaces that we will find on this new object with
+the `spa_handle_factory_enum_interface_info()` method. Interface types
+are simple strings that uniquely define the interface (see also the type
+system).
+
+The name of the factory is a well-known name that describes the functionality
+of the objects created from the factory. `<spa/utils/names.h>` contains
+definitions for common functionality, for example:
+
+\code{.c}
+#define SPA_NAME_SUPPORT_CPU "support.cpu" // A CPU interface
+#define SPA_NAME_SUPPORT_LOG "support.log" // A Log interface
+#define SPA_NAME_SUPPORT_DBUS "support.dbus" // A DBUS interface
+\endcode
+
+Usually the name will be mapped to a specific plugin. This way an
+alternative compatible implementation can be made in a different library.
+
+
+# Making A Handle
+
+Once we have a suitable factory, we need to allocate memory for the object
+it can create. SPA usually does not allocate memory itself but relies on
+the application and the stack for storage.
+
+First get the size of the required memory:
+
+\code{.c}
+struct spa_dict *extra_params = NULL;
+size_t size = spa_handle_factory_get_size(factory, extra_params);
+\endcode
+
+Sometimes the memory can depend on the extra parameters given in
+`_get_size()`. Next we need to allocate the memory and initialize the object
+in it:
+
+\code{.c}
+handle = calloc(1, size);
+spa_handle_factory_init(factory, handle,
+ NULL, // info
+ NULL, // support
+ 0 // n_support
+ );
+\endcode
+
+The info parameter should contain the same extra properties given in
+`spa_handle_factory_get_size()`.
+
+The support parameter is an array of `struct spa_support` items. They
+contain a string type and a pointer to extra support objects. This can
+be a logging API or a main loop API for example. Some plugins require
+certain support libraries to function.
+
+
+# Retrieving An Interface
+
+When a SPA handle is made, you can retrieve any of the interfaces that
+it provides:
+
+\code{.c}
+void *iface;
+spa_handle_get_interface(handle, SPA_NAME_SUPPORT_LOG, &iface);
+\endcode
+
+If this method succeeds, you can cast the `iface` variable to
+`struct spa_log *` and start using the log interface methods.
+
+\code{.c}
+struct spa_log *log = iface;
+spa_log_warn(log, "Hello World!\n");
+\endcode
+
+
+# Clearing An Object
+
+After you are done with a handle you can clear it with
+`spa_handle_clear()` and you can unload the library with `dlclose()`.
+
+
+# SPA Interfaces
+
+We briefly talked about retrieving an interface from a plugin in the
+previous section. Now we will explore what an interface actually is
+and how to use it.
+
+When you retrieve an interface from a handle, you get a reference to
+a small structure that contains the type (string) of the interface,
+a version and a structure with a set of methods (and data) that are
+the implementation of the interface. Calling a method on the interface
+will just call the appropriate method in the implementation.
+
+Interfaces are defined in a header file (for example see
+`<spa/support/log.h>` for the logger API). It is a self contained
+definition that you can just use in your application after you `dlopen()`
+the plugin.
+
+Some interfaces also provide extra fields in the interface, like the
+log interface above that has the log level as a read/write parameter.
+
+See \ref spa_interface for some implementation details on interfaces.
+
+
+# SPA Events
+
+Some interfaces will also allow you to register a callback (a hook or
+listener) to be notified of events. This is usually when something
+changed internally in the interface and it wants to notify the registered
+listeners about this.
+
+For example, the `struct spa_node` interface has a method to register such
+an event handler like this:
+
+\code{.c}
+static void node_info(void *data, const struct spa_node_info *info)
+{
+ printf("got node info!\n");
+}
+
+static struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .info = node_info,
+};
+
+struct spa_hook listener;
+spa_zero(listener);
+spa_node_add_listener(node, &listener, &node_event, my_data);
+\endcode
+
+You make a structure with pointers to the events you are interested in
+and then use `spa_node_add_listener()` to register a listener. The
+`struct spa_hook` is used by the interface to keep track of registered
+event listeners.
+
+Whenever the node information is changed, your `node_info` method will
+be called with `my_data` as the first data field. The events are usually
+also triggered when the listener is added, to enumerate the current
+state of the object.
+
+Events have a `version` field, set to `SPA_VERSION_NODE_EVENTS` in the
+above example. It should contain the version of the event structure
+you compiled with. When new events are added later, the version field
+will be checked and the new signal will be ignored for older versions.
+
+You can remove your listener with:
+
+\code{.c}
+spa_hook_remove(&listener);
+\endcode
+
+
+# API Results
+
+Some interfaces provide API that gives you a list or enumeration of
+objects/values. To avoid allocation overhead and ownership problems,
+SPA uses events to push results to the application. This makes it
+possible for the plugin to temporarily create complex objects on the
+stack and push this to the application without allocation or ownership
+problems. The application can look at the pushed result and keep/copy
+only what it wants to keep.
+
+## Synchronous Results
+
+Here is an example of enumerating parameters on a node interface.
+
+First install a listener for the result:
+
+\code{.c}
+static void node_result(void *data, int seq, int res,
+ uint32_t type, const void *result)
+{
+ const struct spa_result_node_params *r =
+ (const struct spa_result_node_params *) result;
+ printf("got param:\n");
+ spa_debug_pod(0, NULL, r->param);
+}
+
+struct spa_hook listener = { 0 };
+static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .result = node_result,
+};
+
+spa_node_add_listener(node, &listener, &node_events, node);
+\endcode
+
+Then perform the `enum_param` method:
+
+\code{.c}
+int res = spa_node_enum_params(node, 0, SPA_PARAM_EnumFormat, 0, MAXINT, NULL);
+\endcode
+
+This triggers the result event handler with a 0 sequence number for each
+supported format. After this completes, remove the listener again:
+
+\code{.c}
+spa_hook_remove(&listener);
+\endcode
+
+## Asynchronous Results
+
+Asynchronous results are pushed to the application in the same way as
+synchronous results, they are just pushed later. You can check that
+a result is asynchronous by the return value of the enum function:
+
+\code{.c}
+int res = spa_node_enum_params(node, 0, SPA_PARAM_EnumFormat, 0, MAXINT, NULL);
+
+if (SPA_RESULT_IS_ASYNC(res)) {
+ // result will be received later
+ ...
+}
+\endcode
+
+In the case of async results, the result callback will be called with the
+sequence number of the async result code, which can be obtained with:
+
+\code{.c}
+expected_seq = SPA_RESULT_ASYNC_SEQ(res);
+\endcode
+
+# Implementing A New Plugin
+
+***FIXME***
+
+
+
+\addtogroup spa_handle
+
+See: \ref page_spa_plugins
+
+*/
diff --git a/doc/spa-pod.dox b/doc/spa-pod.dox
new file mode 100644
index 0000000..0c88bb7
--- /dev/null
+++ b/doc/spa-pod.dox
@@ -0,0 +1,530 @@
+/** \page page_spa_pod SPA POD
+
+\ref spa_pod (plain old data) is a sort of data container. It is comparable to
+DBus Variant or LV2 Atom.
+
+A POD can express nested structures of objects (with properties), vectors,
+arrays, sequences and various primitives types. All information in the POD
+is laid out sequentially in memory and can be written directly to
+storage or exchanged between processes or threads without additional
+marshalling.
+
+Each POD is made of a 32 bits size followed by a 32 bits type field,
+followed by the POD contents. This makes it possible to skip over unknown
+POD types. The POD start is always aligned to 8 bytes.
+
+POD's can be efficiently constructed and parsed in real-time threads without
+requiring memory allocations.
+
+POD's use the SPA type system for the basic types and containers. See
+the SPA types for more info.
+
+
+# Types
+
+POD's can contain a number of basic SPA types:
+
+- `SPA_TYPE_None`: No value or a NULL pointer.
+- `SPA_TYPE_Bool`: A boolean value.
+- `SPA_TYPE_Id`: An enumerated value.
+- `SPA_TYPE_Int`, `SPA_TYPE_Long`, `SPA_TYPE_Float`, `SPA_TYPE_Double`:
+ various numeral types, 32 and 64 bits.
+- `SPA_TYPE_String`: A string.
+- `SPA_TYPE_Bytes`: A byte array.
+- `SPA_TYPE_Rectangle`: A rectangle with width and height.
+- `SPA_TYPE_Fraction`: A fraction with numerator and denominator.
+- `SPA_TYPE_Bitmap`: An array of bits.
+
+POD's can be grouped together in these container types:
+
+- `SPA_TYPE_Array`: An array of equal sized objects.
+- `SPA_TYPE_Struct`: A collection of types and objects.
+- `SPA_TYPE_Object`: An object with properties.
+- `SPA_TYPE_Sequence`: A timed sequence of POD's.
+
+POD's can also contain some extra types:
+
+- `SPA_TYPE_Pointer`: A typed pointer in memory.
+- `SPA_TYPE_Fd`: A file descriptor.
+- `SPA_TYPE_Choice`: A choice of values.
+- `SPA_TYPE_Pod`: A generic type for the POD itself.
+
+
+# Constructing A POD
+
+A POD is usually constructed with a `struct spa_pod_builder`. The builder
+needs to be initialized with a memory region to write into. It is
+also possible to dynamically grow the memory as needed.
+
+The most common way to construct a POD is on the stack. This does
+not require any memory allocations. The size of the POD can be
+estimated pretty easily and if the buffer is not large enough, an
+appropriate error will be generated.
+
+The code fragment below initializes a POD builder to write into
+the stack allocated buffer.
+
+\code{.c}
+uint8_t buffer[4096];
+struct spa_pod_builder b;
+spa_pod_builder_init(&b, buffer, sizeof(buffer));
+\endcode
+
+Next we need to write some object into the builder. Let's write
+a simple struct with an Int and Float in it. Structs are comparable
+to JSON arrays.
+
+\code{.c}
+struct spa_pod_frame f;
+spa_pod_builder_push_struct(&b, &f);
+\endcode
+
+First we open the struct container, the `struct spa_pod_frame` keeps
+track of the container context. Next we add some values to
+the container like this:
+
+\code{.c}
+spa_pod_builder_int(&b, 5);
+spa_pod_builder_float(&b, 3.1415f);
+\endcode
+
+Then we close the container by popping the frame again:
+
+\code{.c}
+struct spa_pod *pod;
+pod = spa_pod_builder_pop(&b, &f);
+\endcode
+
+`spa_pod_builder_pop()` returns a reference to the object we completed
+on the stack.
+
+## Using varargs Builder
+
+We can also use the following construct to make POD objects:
+
+\code{.c}
+spa_pod_builder_push_struct(&b, &f);
+spa_pod_builder_add(&b,
+ SPA_POD_Int(5),
+ SPA_POD_Float(3.1415f));
+pod = spa_pod_builder_pop(&b, &f);
+\endcode
+
+Or even shorter:
+
+\code{.c}
+pod = spa_pod_builder_add_struct(&b,
+ SPA_POD_Int(5),
+ SPA_POD_Float(3.1415f));
+\endcode
+
+It's not possible to use the varargs builder to make a sequence or
+array, use the normal builder methods for that.
+
+## Making Objects
+
+POD objects are containers for properties and are comparable to JSON
+objects.
+
+Start by pushing an object:
+
+\code{.c}
+spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+\endcode
+
+An object requires an object type (`SPA_TYPE_OBJECT_Props`) and a context
+ID (`SPA_PARAM_Props`). The object type defines the properties that can be
+added to the object and their meaning. The SPA type system allows you to
+make this connection (See the type system).
+
+Next we can push some properties in the object:
+
+\code{.c}
+spa_pod_builder_prop(&b, SPA_PROP_device, 0);
+spa_pod_builder_string(&b, "hw:0");
+spa_pod_builder_prop(&b, SPA_PROP_frequency, 0);
+spa_pod_builder_float(&b, 440.0);
+\endcode
+
+As can be seen, we always need to push a prop (with key and flags)
+and then the associated value. For performance reasons it is a good
+idea to always push (and parse) the object keys in ascending order.
+
+Don't forget to pop the result when the object is finished:
+
+\code{.c}
+pod = spa_pod_builder_pop(&b, &f);
+\endcode
+
+There is a shortcut for making objects:
+
+\code{.c}
+pod = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+ SPA_PROP_device, SPA_POD_String("hw:0"),
+ SPA_PROP_frequency, SPA_POD_Float(440.0f));
+\endcode
+
+## Choice Values
+
+It is possible to express ranges or enumerations of possible
+values for properties (and to some extend structs). This is achieved
+with choice values.
+
+Choice values are really just a choice type and an array of choice values
+(of the same type). Depending on the choice type, the array values are
+interpreted in different ways:
+
+- `SPA_CHOICE_None`: No choice, first value is current.
+- `SPA_CHOICE_Range`: Range: default, min, max.
+- `SPA_CHOICE_Step`: Range with step: default, min, max, step.
+- `SPA_CHOICE_Enum`: Enum: default, alternative,...
+- `SPA_CHOICE_Flags`: Bitmask of flags.
+
+Let's illustrate this with a props object that specifies a range of
+possible values for the frequency:
+
+\code{.c}
+struct spa_pod_frame f2;
+
+spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+spa_pod_builder_prop(&b, SPA_PROP_frequency, 0);
+spa_pod_builder_push_choice(&b, &f2, SPA_CHOICE_Range, 0);
+spa_pod_builder_float(&b, 440.0); // default
+spa_pod_builder_float(&b, 110.0); // min
+spa_pod_builder_float(&b, 880.0); // min
+pod = spa_pod_builder_pop(&b, &f2);
+pod = spa_pod_builder_pop(&b, &f);
+\endcode
+
+As you can see, first push the choice as a range, then the values. A range
+choice expects at least three values, the default value, minimum and maximum
+values. There is a shortcut for this as well using varargs:
+
+\code{.c}
+pod = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+ SPA_PROP_frequency, SPA_POD_CHOICE_RANGE_Float(440.0f, 110.0f, 880.0f));
+\endcode
+
+## Choice Examples
+
+This is a description of a possible `SPA_TYPE_OBJECT_Format` as used when
+enumerating allowed formats (`SPA_PARAM_EnumFormat`) in SPA objects:
+
+\code{.c}
+pod = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ // specify the media type and subtype
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ // audio/raw properties
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(
+ SPA_AUDIO_FORMAT_S16, // default
+ SPA_AUDIO_FORMAT_S16, // alternative1
+ SPA_AUDIO_FORMAT_S32, // alternative2
+ SPA_AUDIO_FORMAT_f32 // alternative3
+ ),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(
+ 44100, // default
+ 8000, // min
+ 192000 // max
+ ),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2));
+\endcode
+
+## Fixate
+
+We can remove all choice values from the object with the
+`spa_pod_object_fixate()` method. This modifies the pod in-place and sets all
+choice properties to `SPA_CHOICE_None`, forcing the default value as the
+only available value in the choice.
+
+Running fixate on our previous example would result in an object equivalent
+to:
+
+\code{.c}
+pod = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ // specify the media type and subtype
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ // audio/raw properties
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(44100),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2));
+\endcode
+
+
+# Parsing A POD
+
+Parsing a POD usually consists of:
+
+- Validating if raw bytes + size can contain a valid POD.
+- Inspecting the type of a POD.
+- Looping over the items in an object or struct.
+- Getting data out of POD's.
+
+## Validating Bytes
+
+Use `spa_pod_from_data()` to check if maxsize of bytes in data contain
+a POD at the size bytes starting at offset. This function checks that
+the POD size will fit and not overflow.
+
+\code{.c}
+struct spa_pod *pod;
+pod = spa_pod_from_data(data, maxsize, offset, size);
+\endcode
+
+## Checking The Type Of POD
+
+Use one of `spa_pod_is_bool()`, `spa_pod_is_int()`, etc to check
+for the type of the pod. For simple (non-container) types,
+`spa_pod_get_bool()`, `spa_pod_get_int()` etc can be used to
+extract the value of the pod.
+
+`spa_pod_is_object_type()` can be used to check if the POD contains
+an object of the expected type.
+
+## Struct Fields
+
+To iterate over the fields of a struct use:
+
+\code{.c}
+struct spa_pod *pod, *obj;
+SPA_POD_STRUCT_FOREACH(obj, pod) {
+ printf("field type:%d\n", pod->type);
+}
+\endcode
+
+For parsing structs it is usually much easier to use the parser
+below.
+
+## Object Properties
+
+To iterate over the properties in an object you can do:
+
+\code{.c}
+struct spa_pod_prop *prop;
+struct spa_pod_object *obj = (struct spa_pod_object*)pod;
+SPA_POD_OBJECT_FOREACH(pod, prop) {
+ printf("prop key:%d\n", prop->key);
+}
+\endcode
+
+There is a function to retrieve the property for a certain key
+in the object. If the properties of the object are in ascending
+order, you can start searching from the previous key.
+
+\code{.c}
+struct spa_pod_prop *prop;
+prop = spa_pod_find_prop(obj, NULL, SPA_FORMAT_AUDIO_format);
+ // .. use first prop
+prop = spa_pod_find_prop(obj, prop, SPA_FORMAT_AUDIO_rate);
+ // .. use next prop
+\endcode
+
+## Parser
+
+Similar to the builder, there is a parser object as well.
+
+If the fields in a struct are known, it is much easier to use the
+parser. Similarly, if the object type (and thus its keys) are known,
+the parser is easier.
+
+First initialize a `struct spa_pod_parser`:
+
+\code{.c}
+struct spa_pod_parser p;
+spa_pod_parser_pod(&p, obj);
+\endcode
+
+You can then enter containers such as objects or structs with a push
+operation:
+
+\code{.c}
+struct spa_pod_frame f;
+spa_pod_parser_push_struct(&p, &f);
+\endcode
+
+You need to store the context in a `struct spa_pod_frame` to be able
+to exit the container again later.
+
+You can then parse each field. The parser takes care of moving to the
+next field.
+
+\code{.c}
+uint32_t id, val;
+spa_pod_parser_get_id(&p, &id);
+spa_pod_parser_get_int(&p, &val);
+...
+\endcode
+
+And finally exit the container again:
+
+\code{.c}
+spa_pod_parser_pop(&p, &f);
+\endcode
+
+## Parser With Variable Arguments
+
+In most cases, parsing objects is easier with the variable argument
+functions. The parse function look like the mirror image of the builder
+functions.
+
+To parse a struct:
+
+\code{.c}
+spa_pod_parser_get_struct(&p,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&val));
+\endcode
+
+To parse properties in an object:
+
+\code{.c}
+uint32_t type, subtype, format, rate, channels;
+spa_pod_parser_get_object(&p,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&subtype),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(&format),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(&rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(&channels));
+\endcode
+
+When parsing objects it is possible to have optional fields. You can
+make a field optional be parsing it with the `SPA_POD_OPT_` prefix
+for the type.
+
+In the next example, the rate and channels fields are optional
+and when they are not present, the variables will not be changed.
+
+\code{.c}
+uint32_t type, subtype, format, rate = 0, channels = 0;
+spa_pod_parser_get_object(&p,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&subtype),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(&format),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&channels));
+\endcode
+
+It is not possible to parse a sequence or array with the parser.
+Use the iterator for this.
+
+## Choice Values
+
+The parser will handle choice values as long as they are of type
+`none`. It will then parse the single value from the choice. When
+dealing with other choice values, it's possible to parse the
+property values into a `struct spa_pod` and then inspect the choice
+manually, if needed.
+
+Here is an example of parsing the format values as a POD:
+
+\code{.c}
+uint32_t type, subtype;
+struct spa_pod *format;
+spa_pod_parser_get_object(&p,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&subtype),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Pod(&format));
+\endcode
+
+`spa_pod_get_values()` is a useful function. It returns a
+`struct spa_pod*` with and array of values. For normal POD's
+and choice none values, it simply returns the POD and one value.
+For other choice values it returns the choice type and an array
+of values:
+
+\code{.c}
+struct spa_pod *value;
+uint32_t n_vals, choice;
+
+value = spa_pod_get_values(pod, &n_vals, &choice);
+
+switch (choice) {
+case SPA_CHOICE_None:
+ // one single value
+ break;
+case SPA_CHOICE_Range:
+ // array of values of type of pod, cast to right type
+ // to iterate.
+ uint32_t *v = SPA_POD_BODY(values);
+ if (n_vals < 3)
+ break;
+ printf("default value: %u\n", v[0]);
+ printf("min value: %u\n", v[1]);
+ printf("max value: %u\n", v[2]);
+ break;
+
+ // ...
+default:
+ break;
+}
+\endcode
+
+
+# Filter
+
+Given two POD objects of the same type (object, struct, ..) one can
+run a filter and generate a new POD that only contains values that
+are compatible with both input POD's.
+
+This is, for example, used to find a compatible format between two ports.
+
+As an example we can run a filter on two simple POD's:
+
+\code{.c}
+pod = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(
+ SPA_AUDIO_FORMAT_S16, // default
+ SPA_AUDIO_FORMAT_S16, // alternative1
+ SPA_AUDIO_FORMAT_S32, // alternative2
+ SPA_AUDIO_FORMAT_f32 // alternative3
+ ));
+
+filter = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(
+ SPA_AUDIO_FORMAT_S16, // default
+ SPA_AUDIO_FORMAT_S16, // alternative1
+ SPA_AUDIO_FORMAT_f64 // alternative2
+ ));
+
+struct spa_pod *result;
+if (spa_pod_filter(&b, &result, pod, filter) < 0)
+ goto exit_error;
+\endcode
+
+Filter will contain a POD equivalent to:
+
+\code{.c}
+result = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_AUDIO_FORMAT_S16);
+\endcode
+
+# POD Layout
+
+Each POD has a 32 bits size field, followed by a 32 bits type field. The size
+field specifies the size following the type field.
+
+Each POD is aligned to an 8 byte boundary.
+
+
+\addtogroup spa_pod
+
+See: \ref page_spa_pod
+
+*/
diff --git a/doc/tutorial.dox b/doc/tutorial.dox
new file mode 100644
index 0000000..bc53725
--- /dev/null
+++ b/doc/tutorial.dox
@@ -0,0 +1,21 @@
+/** \page page_tutorial Tutorial
+
+Welcome to the PipeWire tutorial. The goal is to learn
+PipeWire API step-by-step with simple short examples.
+
+- \subpage page_tutorial1
+- \subpage page_tutorial2
+- \subpage page_tutorial3
+- \subpage page_tutorial4
+- \subpage page_tutorial5
+- \subpage page_tutorial6
+
+
+# More Example Programs
+
+- \ref audio-src.c "": \snippet{doc} audio-src.c title
+- \ref audio-dsp-filter.c "": \snippet{doc} audio-dsp-filter.c title
+- \ref video-play.c "": \snippet{doc} video-play.c title
+- \subpage page_examples
+
+*/
diff --git a/doc/tutorial1.c b/doc/tutorial1.c
new file mode 100644
index 0000000..050aa88
--- /dev/null
+++ b/doc/tutorial1.c
@@ -0,0 +1,19 @@
+/*
+ [title]
+ \ref page_tutorial1
+ [title]
+ */
+/* [code] */
+#include <pipewire/pipewire.h>
+
+int main(int argc, char *argv[])
+{
+ pw_init(&argc, &argv);
+
+ fprintf(stdout, "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+}
+/* [code] */
diff --git a/doc/tutorial1.dox b/doc/tutorial1.dox
new file mode 100644
index 0000000..6e88cce
--- /dev/null
+++ b/doc/tutorial1.dox
@@ -0,0 +1,47 @@
+/** \page page_tutorial1 Tutorial - Part 1: Getting Started
+
+
+\ref page_tutorial "Index" | \ref page_tutorial2
+
+In this tutorial we show the basics of a simple PipeWire application.
+Use this tutorial to get started and help you set up your development
+environment.
+
+
+# Initialization
+
+Let get started with the simplest application.
+
+\snippet tutorial1.c code
+
+Before you can use any PipeWire functions, you need to call `pw_init()`.
+
+
+# Compilation
+
+PipeWire provides a pkg-config file named `libpipewire-0.3` (note: the version
+suffix may change with future releases of PipeWire).
+To compile the simple test application, copy it into a test1.c file and
+use pkg-config to provide the required dependencies:
+
+ gcc -Wall test1.c -o test1 $(pkg-config --cflags --libs libpipewire-0.3)
+
+then run it with:
+
+ # ./test1
+ Compiled with libpipewire 0.3.5
+ Linked with libpipewire 0.3.5
+ #
+
+Use your build system's pkg-config support to integrate it into your project.
+For example, a minimal [meson.build](https://mesonbuild.com/) entry would look
+like this:
+
+ project('test1', ['c'])
+ pipewire_dep = dependency('libpipewire-0.3')
+ executable('test1', 'test1.c',
+ dependencies: [pipewire_dep])
+
+\ref page_tutorial "Index" | \ref page_tutorial2
+
+*/
diff --git a/doc/tutorial2.c b/doc/tutorial2.c
new file mode 100644
index 0000000..66647d7
--- /dev/null
+++ b/doc/tutorial2.c
@@ -0,0 +1,56 @@
+/*
+ [title]
+ \ref page_tutorial2
+ [title]
+ */
+/* [code] */
+#include <pipewire/pipewire.h>
+
+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[])
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ pw_init(&argc, &argv);
+
+ loop = pw_main_loop_new(NULL /* properties */);
+ context = pw_context_new(pw_main_loop_get_loop(loop),
+ NULL /* properties */,
+ 0 /* user_data size */);
+
+ core = pw_context_connect(context,
+ NULL /* properties */,
+ 0 /* user_data size */);
+
+ registry = pw_core_get_registry(core, PW_VERSION_REGISTRY,
+ 0 /* user_data size */);
+
+ spa_zero(registry_listener);
+ pw_registry_add_listener(registry, &registry_listener,
+ &registry_events, NULL);
+
+ pw_main_loop_run(loop);
+
+ pw_proxy_destroy((struct pw_proxy*)registry);
+ pw_core_disconnect(core);
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+
+ return 0;
+}
+/* [code] */
diff --git a/doc/tutorial2.dox b/doc/tutorial2.dox
new file mode 100644
index 0000000..1688aed
--- /dev/null
+++ b/doc/tutorial2.dox
@@ -0,0 +1,129 @@
+/** \page page_tutorial2 Tutorial - Part 2: Enumerating Objects
+
+\ref page_tutorial1 | \ref page_tutorial "Index" | \ref page_tutorial3
+
+In this tutorial we show how to connect to a PipeWire daemon and
+enumerate the objects that it has.
+
+Let take a look at the following application to start.
+
+\snippet tutorial2.c code
+
+To compile the simple test application, copy it into a tutorial2.c file and
+use:
+
+ gcc -Wall tutorial2.c -o tutorial2 $(pkg-config --cflags --libs libpipewire-0.3)
+
+Let's break this down:
+
+First we need to initialize the PipeWire library with `pw_init()` as we
+saw in the previous tutorial. This will load and configure the right
+modules and setup logging and other tasks.
+
+\code{.c}
+ ...
+ pw_init(&argc, &argv);
+ ...
+\endcode
+
+Next we need to create one of the `struct pw_loop` wrappers. PipeWire
+ships with 2 types of mainloop implementations. We will use the
+`struct pw_main_loop` implementation, we will see later how we can
+use the `struct pw_thread_loop` implementation as well.
+
+The mainloop is an abstraction of a big poll loop, waiting for events
+to occur and things to do. Most of the PipeWire work will actually
+be performed in the context of this loop and so we need to make one
+first.
+
+We then need to make a new context object with the loop. This context
+object will manage the resources for us and will make it possible for
+us to connect to a PipeWire daemon:
+
+\code{.c}
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ loop = pw_main_loop_new(NULL /* properties */);
+ context = pw_context_new(pw_main_loop_get_loop(loop),
+ NULL /* properties */,
+ 0 /* user_data size */);
+\endcode
+
+It is possible to give extra properties when making the mainloop or
+context to tweak its features and functionality. It is also possible
+to add extra data to the allocated objects for your user data. It will
+stay alive for as long as the object is alive. We will use this
+feature later.
+
+A real implementation would also need to check if the allocation
+succeeded and do some error handling, but we leave that out to make
+the code easier to read.
+
+With the context we can now connect to the PipeWire daemon:
+
+\code{.c}
+ struct pw_core *core;
+ core = pw_context_connect(context,
+ NULL /* properties */,
+ 0 /* user_data size */);
+\endcode
+
+This creates a socket between the client and the server and makes
+a proxy object (with ID 0) for the core. Don't forget to check the
+result here, a NULL value means that the connection failed.
+
+At this point we can send messages to the server and receive events.
+For now we're not going to handle events on this core proxy but
+we're going to handle them on the registry object.
+
+
+\code{.c}
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ 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);
+\endcode
+
+From the core we get the registry proxy object and when we use
+`pw_registry_add_listener()` to listen for events. We need a
+small `struct spa_hook` to keep track of the listener and a
+reference to the `struct pw_registry_events` that contains the
+events we want to listen to.
+
+This is how we define the event handler and the function to
+handle the events:
+
+\code{.c}
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+};
+
+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);
+}
+\endcode
+
+Now that everything is set up we can start the mainloop and let
+the communication between client and server continue:
+
+\code{.c}
+ pw_main_loop_run(loop);
+\endcode
+
+Since we don't call `pw_main_loop_quit()` anywhere, this loop will
+continue forever. In the next tutorial we'll see how we can nicely
+exit our application after we received all server objects.
+
+\ref page_tutorial1 | \ref page_tutorial "Index" | \ref page_tutorial3
+
+*/
diff --git a/doc/tutorial3.c b/doc/tutorial3.c
new file mode 100644
index 0000000..17c0ee4
--- /dev/null
+++ b/doc/tutorial3.c
@@ -0,0 +1,89 @@
+/*
+ [title]
+ \ref page_tutorial3
+ [title]
+ */
+/* [code] */
+#include <pipewire/pipewire.h>
+
+/* [roundtrip] */
+struct roundtrip_data {
+ int pending;
+ struct pw_main_loop *loop;
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct roundtrip_data *d = data;
+
+ if (id == PW_ID_CORE && seq == d->pending)
+ pw_main_loop_quit(d->loop);
+}
+
+static void roundtrip(struct pw_core *core, struct pw_main_loop *loop)
+{
+ static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ };
+
+ struct roundtrip_data d = { .loop = loop };
+ struct spa_hook core_listener;
+
+ pw_core_add_listener(core, &core_listener, &core_events, &d);
+
+ d.pending = pw_core_sync(core, PW_ID_CORE, 0);
+
+ pw_main_loop_run(loop);
+
+ spa_hook_remove(&core_listener);
+}
+/* [roundtrip] */
+
+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[])
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ pw_init(&argc, &argv);
+
+ loop = pw_main_loop_new(NULL /* properties */);
+ context = pw_context_new(pw_main_loop_get_loop(loop),
+ NULL /* properties */,
+ 0 /* user_data size */);
+
+ core = pw_context_connect(context,
+ NULL /* properties */,
+ 0 /* user_data size */);
+
+ registry = pw_core_get_registry(core, PW_VERSION_REGISTRY,
+ 0 /* user_data size */);
+
+ pw_registry_add_listener(registry, &registry_listener,
+ &registry_events, NULL);
+
+ roundtrip(core, loop);
+
+ pw_proxy_destroy((struct pw_proxy*)registry);
+ pw_core_disconnect(core);
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+
+ return 0;
+}
+/* [code] */
diff --git a/doc/tutorial3.dox b/doc/tutorial3.dox
new file mode 100644
index 0000000..776ea14
--- /dev/null
+++ b/doc/tutorial3.dox
@@ -0,0 +1,119 @@
+/** \page page_tutorial3 Tutorial - Part 3: Forcing A Roundtrip
+
+\ref page_tutorial2 | \ref page_tutorial "Index" | \ref page_tutorial4
+
+In this tutorial we show how to force a roundtrip to the server
+to make sure an action completed.
+
+We'll change our example from \ref page_tutorial2 "Tutorial 2" slightly
+and add the extra code to implement the roundtrip.
+
+Let's take the following small method first:
+
+\snippet tutorial3.c roundtrip
+
+Let's take a look at what this method does.
+
+\code{.c}
+ struct spa_hook core_listener;
+
+ pw_core_add_listener(core, &core_listener, &core_events, &d);
+\endcode
+
+First of all we add a listener for the events of the core
+object. We are only interested in the `done` event in this
+tutorial. This is the event handler:
+
+\code{.c}
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct roundtrip_data *d = data;
+
+ if (id == PW_ID_CORE && seq == d->pending)
+ pw_main_loop_quit(d->loop);
+}
+\endcode
+
+When the done event is received for an object with id `PW_ID_CORE` and
+a certain sequence number `seq`, this function will call `pw_main_loop_quit()`.
+
+Next we do:
+
+\code{.c}
+ d.pending = pw_core_sync(core, PW_ID_CORE, 0);
+\endcode
+
+This triggers the `sync` method on the core object with id
+`PW_ID_CORE` and sequence number 0.
+
+Because this is a method on a proxy object, it will be executed
+asynchronously and the return value will reflect this. PipeWire
+uses the return values of the underlying SPA (Simple Plugin API)
+helper objects (See also \ref page_spa_design ).
+
+Because all messages on the PipeWire server are handled sequentially,
+the sync method will be executed after all previous methods are
+completed. The PipeWire server will emit a `done` event with the
+same ID and the return value of the original `pw_core_sync()`
+method in the sequence number.
+
+We then run the mainloop to send the messages to the server and
+receive the events:
+
+\code{.c}
+ pw_main_loop_run(loop);
+\endcode
+
+When we get the done event, we can compare it to the sync method
+and then we know that we did a complete roundtrip and there are no
+more pending methods on the server. We can quit the mainloop and
+remove the listener:
+
+\code{.c}
+ spa_hook_remove(&core_listener);
+\endcode
+
+If we add this roundtrip method to our code and call it instead of the
+`pw_main_loop_run()` we will exit the program after all previous methods
+are finished. This means that the `pw_core_get_registry()` call
+completed and thus that we also received all events for the globals
+on the server.
+
+\snippet tutorial3.c code
+
+To compile the simple test application, copy it into a tutorial3.c file and
+use:
+
+ gcc -Wall tutorial3.c -o tutorial3 $(pkg-config --cflags --libs libpipewire-0.3)
+
+Now that our program completes, we can take a look at how we can destroy
+the objects we created. Let's destroy each of them in reverse order that we
+created them:
+
+\code{.c}
+ pw_proxy_destroy((struct pw_proxy*)registry);
+\endcode
+
+The registry is a proxy and can be destroyed with the generic proxy destroy
+method. After destroying the object, you should not use it anymore. It is
+an error to destroy an object more than once.
+
+We can disconnect from the server with:
+
+\code{.c}
+ pw_core_disconnect(core);
+\endcode
+
+This will also destroy the core proxy object and will remove the proxies
+that might have been created on this connection.
+
+We can finally destroy our context and mainloop to conclude this tutorial:
+
+\code{.c}
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+\endcode
+
+\ref page_tutorial2 | \ref page_tutorial "Index" | \ref page_tutorial4
+
+*/
diff --git a/doc/tutorial4.c b/doc/tutorial4.c
new file mode 100644
index 0000000..ff0cb27
--- /dev/null
+++ b/doc/tutorial4.c
@@ -0,0 +1,112 @@
+/*
+ [title]
+ \ref page_tutorial4
+ [title]
+ */
+/* [code] */
+#include <math.h>
+
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_VOLUME 0.7
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_stream *stream;
+ double accumulator;
+};
+
+/* [on_process] */
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ int i, c, n_frames, stride;
+ int16_t *dst, val;
+
+ if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((dst = buf->datas[0].data) == NULL)
+ return;
+
+ stride = sizeof(int16_t) * DEFAULT_CHANNELS;
+ n_frames = buf->datas[0].maxsize / stride;
+
+ for (i = 0; i < n_frames; i++) {
+ data->accumulator += M_PI_M2 * 440 / DEFAULT_RATE;
+ if (data->accumulator >= M_PI_M2)
+ data->accumulator -= M_PI_M2;
+
+ val = sin(data->accumulator) * DEFAULT_VOLUME * 16767.f;
+ for (c = 0; c < DEFAULT_CHANNELS; c++)
+ *dst++ = val;
+ }
+
+ 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);
+}
+/* [on_process] */
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .process = on_process,
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "audio-src",
+ pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ NULL),
+ &stream_events,
+ &data);
+
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_S16,
+ .channels = DEFAULT_CHANNELS,
+ .rate = DEFAULT_RATE ));
+
+ 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);
+
+ pw_main_loop_run(data.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+
+ return 0;
+}
+/* [code] */
diff --git a/doc/tutorial4.dox b/doc/tutorial4.dox
new file mode 100644
index 0000000..b8d1707
--- /dev/null
+++ b/doc/tutorial4.dox
@@ -0,0 +1,158 @@
+/** \page page_tutorial4 Tutorial - Part 4: Playing A Tone
+
+\ref page_tutorial3 | \ref page_tutorial "Index" | \ref page_tutorial5
+
+In this tutorial we show how to use a stream to play a tone.
+
+Let's take a look at the code before we break it down:
+
+\snippet tutorial4.c code
+
+Save as tutorial4.c and compile with:
+
+ gcc -Wall tutorial4.c -o tutorial4 -lm $(pkg-config --cflags --libs libpipewire-0.3)
+
+We start with the usual boilerplate, `pw_init()` and a `pw_main_loop_new()`.
+We're going to store our objects in a structure so that we can pass them
+around in callbacks later.
+
+\code{.c}
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_stream *stream;
+ double accumulator;
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+\endcode
+
+Next we create a stream object. It takes the mainloop as first argument and
+a stream name as the second. Next we provide some properties for the stream
+and a callback + data.
+
+\code{.c}
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "audio-src",
+ pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ NULL),
+ &stream_events,
+ &data);
+\endcode
+
+We are using `pw_stream_new_simple()` but there is also a `pw_stream_new()` that
+takes an existing `struct pw_core` as the first argument and that requires you
+to add the event handle manually, for more control. The `pw_stream_new_simple()`
+is, as the name implies, easier to use because it creates a `struct pw_context`
+and `struct pw_core` automatically.
+
+In the properties we need to give as much information about the stream as we
+can so that the session manager can make good decisions about how and where
+to route this stream. There are three important properties to configure:
+
+- `PW_KEY_MEDIA_TYPE`: The media type; like Audio, Video, MIDI.
+- `PW_KEY_MEDIA_CATEGORY`: The category; like Playback, Capture, Duplex, Monitor.
+- `PW_KEY_MEDIA_ROLE`: The media role; like Movie, Music, Camera, Screen,
+ Communication, Game, Notification, DSP, Production, Accessibility, Test.
+
+The properties are owned by the stream and freed when the stream is destroyed
+later.
+
+This is the event structure that we use to listen for events:
+
+\code{.c}
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .process = on_process,
+};
+\endcode
+
+We are for the moment only interested now in the `process` event. This event
+is called whenever we need to produce more data. We'll see how that function
+is implemented but first we need to setup the format of the stream:
+
+\code{.c}
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_S16,
+ .channels = DEFAULT_CHANNELS,
+ .rate = DEFAULT_RATE ));
+\endcode
+
+This is using a `struct spa_pod_builder` to make a `struct spa_pod *` object
+in the buffer array on the stack. The parameter is of type `SPA_PARAM_EnumFormat`
+which means that it enumerates the possible formats for this stream. We have
+only one, a Signed 16 bit stereo format at 44.1KHz.
+
+We use `spa_format_audio_raw_build()` which is a helper function to make the param
+with the builder. See \ref page_spa_pod for more information about how to
+make these POD objects.
+
+Now we're ready to connect the stream and run the main loop:
+
+\code{.c}
+ 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);
+
+ pw_main_loop_run(data.loop);
+\endcode
+
+To connect we specify that we have a `PW_DIRECTION_OUTPUT` stream. The third argument
+is always `PW_ID_ANY`. Next we set some flags:
+
+- `PW_STREAM_FLAG_AUTOCONNECT`: Automatically connect this stream. This instructs
+ the session manager to link us to some consumer.
+- `PW_STREAM_FLAG_MAP_BUFFERS`: mmap the buffers for us so we can access the
+ memory. If you don't set these flags you have either work with the fd or mmap
+ yourself.
+- `PW_STREAM_FLAG_RT_PROCESS`: Run the process function in the realtime thread.
+ Only use this if the process function only uses functions that are realtime
+ safe, this means no allocation or file access or any locking.
+
+And last we pass the extra parameters for our stream. Here we only have the
+allowed formats (`SPA_PARAM_EnumFormat`).
+
+Running the mainloop will then start processing and will result in our
+`process` callback to be called. Let's have a look at that function now.
+
+The main program flow of the process function is:
+
+- `pw_stream_dequeue_buffer()` to obtain a buffer to write into.
+- Get pointers in buffer memory to write to.
+- Write data into buffer.
+- Adjust buffer with number of written bytes, offset, stride.
+- `pw_stream_queue_buffer()` to queue the buffer for playback.
+
+\snippet tutorial4.c on_process
+
+Check out the docs for \ref page_spa_buffer for more information
+about how to work with buffers.
+
+Try to change the number of channels, samplerate or format; the stream
+will automatically convert to the format on the server.
+
+
+\ref page_tutorial3 | \ref page_tutorial "Index" | \ref page_tutorial5
+
+*/
diff --git a/doc/tutorial5.c b/doc/tutorial5.c
new file mode 100644
index 0000000..e49da91
--- /dev/null
+++ b/doc/tutorial5.c
@@ -0,0 +1,141 @@
+/*
+ [title]
+ \ref page_tutorial5
+ [title]
+ */
+/* [code] */
+#include <spa/param/video/format-utils.h>
+#include <spa/debug/types.h>
+#include <spa/param/video/type-info.h>
+
+#include <pipewire/pipewire.h>
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_stream *stream;
+
+ struct spa_video_info format;
+};
+
+/* [on_process] */
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+
+ if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if (buf->datas[0].data == NULL)
+ return;
+
+ /** copy frame data to screen */
+ printf("got a frame of size %d\n", buf->datas[0].chunk->size);
+
+ pw_stream_queue_buffer(data->stream, b);
+}
+/* [on_process] */
+
+static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = userdata;
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ if (spa_format_parse(param,
+ &data->format.media_type,
+ &data->format.media_subtype) < 0)
+ return;
+
+ if (data->format.media_type != SPA_MEDIA_TYPE_video ||
+ data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return;
+
+ if (spa_format_video_raw_parse(param, &data->format.info.raw) < 0)
+ return;
+
+ printf("got video format:\n");
+ printf(" format: %d (%s)\n", data->format.info.raw.format,
+ spa_debug_type_find_name(spa_type_video_format,
+ data->format.info.raw.format));
+ printf(" size: %dx%d\n", data->format.info.raw.size.width,
+ data->format.info.raw.size.height);
+ printf(" framerate: %d/%d\n", data->format.info.raw.framerate.num,
+ data->format.info.raw.framerate.denom);
+
+ /** prepare to render video of this size */
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .param_changed = on_param_changed,
+ .process = on_process,
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct pw_properties *props;
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+
+ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+ PW_KEY_MEDIA_CATEGORY, "Capture",
+ PW_KEY_MEDIA_ROLE, "Camera",
+ NULL);
+ if (argc > 1)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "video-capture",
+ props,
+ &stream_events,
+ &data);
+
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ 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,
+ SPA_VIDEO_FORMAT_RGB,
+ SPA_VIDEO_FORMAT_RGB,
+ SPA_VIDEO_FORMAT_RGBA,
+ SPA_VIDEO_FORMAT_RGBx,
+ SPA_VIDEO_FORMAT_BGRx,
+ SPA_VIDEO_FORMAT_YUY2,
+ SPA_VIDEO_FORMAT_I420),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(4096, 4096)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25, 1),
+ &SPA_FRACTION(0, 1),
+ &SPA_FRACTION(1000, 1)));
+
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, 1);
+
+ pw_main_loop_run(data.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+
+ return 0;
+}
+/* [code] */
diff --git a/doc/tutorial5.dox b/doc/tutorial5.dox
new file mode 100644
index 0000000..e73c1cf
--- /dev/null
+++ b/doc/tutorial5.dox
@@ -0,0 +1,223 @@
+/** \page page_tutorial5 Tutorial - Part 5: Capturing Video Frames
+
+\ref page_tutorial4 | \ref page_tutorial "Index" | \ref page_tutorial6
+
+In this tutorial we show how to use a stream to capture a
+stream of video frames.
+
+Even though we are now working with a different media type and
+we are capturing instead of playback, you will see that this
+example is very similar to \ref page_tutorial4.
+
+Let's take a look at the code before we break it down:
+
+\snippet tutorial5.c code
+
+Save as tutorial5.c and compile with:
+
+ gcc -Wall tutorial5.c -o tutorial5 -lm $(pkg-config --cflags --libs libpipewire-0.3)
+
+Most of the application is structured like \ref page_tutorial4.
+
+We create a stream object with different properties to make it a Camera
+Video Capture stream.
+
+\code{.c}
+ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+ PW_KEY_MEDIA_CATEGORY, "Capture",
+ PW_KEY_MEDIA_ROLE, "Camera",
+ NULL);
+ if (argc > 1)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "video-capture",
+ props,
+ &stream_events,
+ &data);
+\endcode
+
+We also optionally allow the user to pass the name of the target node where the session
+manager is supposed to connect the node. The user may also give the value of the
+unique target node serial (`PW_KEY_OBJECT_SERIAL`) as the value.
+
+In addition to the `process` event, we are also going to listen to a new event,
+`param_changed`:
+
+\code{.c}
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .param_changed = on_param_changed,
+ .process = on_process,
+};
+\endcode
+
+Because we capture a stream of a wide range of different
+video formats and resolutions, we have to describe our accepted formats in
+a different way:
+
+\code{.c}
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ 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,
+ SPA_VIDEO_FORMAT_RGB,
+ SPA_VIDEO_FORMAT_RGB,
+ SPA_VIDEO_FORMAT_RGBA,
+ SPA_VIDEO_FORMAT_RGBx,
+ SPA_VIDEO_FORMAT_BGRx,
+ SPA_VIDEO_FORMAT_YUY2,
+ SPA_VIDEO_FORMAT_I420),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(4096, 4096)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25, 1),
+ &SPA_FRACTION(0, 1),
+ &SPA_FRACTION(1000, 1)));
+\endcode
+
+This is using a `struct spa_pod_builder` to make a `struct spa_pod *` object
+in the buffer array on the stack. The parameter is of type `SPA_PARAM_EnumFormat`
+which means that it enumerates the possible formats for this stream.
+
+In this example we use the builder to create some `CHOICE` entries for
+the format properties.
+
+We have an enumeration of formats, we need to first give the amount of enumerations
+that follow, then the default (preferred) value, followed by alternatives in order
+of preference:
+
+\code{.c}
+ SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(7,
+ SPA_VIDEO_FORMAT_RGB, /* default */
+ SPA_VIDEO_FORMAT_RGB, /* alternative 1 */
+ SPA_VIDEO_FORMAT_RGBA, /* alternative 2 */
+ SPA_VIDEO_FORMAT_RGBx, /* .. etc.. */
+ SPA_VIDEO_FORMAT_BGRx,
+ SPA_VIDEO_FORMAT_YUY2,
+ SPA_VIDEO_FORMAT_I420),
+\endcode
+
+We also have a `RANGE` of values for the size. We need to give a default (preferred)
+size and then a min and max value:
+
+\code{.c}
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240), /* default */
+ &SPA_RECTANGLE(1, 1), /* min */
+ &SPA_RECTANGLE(4096, 4096)), /* max */
+\endcode
+
+We have something similar for the framerate.
+
+Note that there are other video parameters that we don't specify here. This
+means that we don't have any restrictions for their values.
+
+See \ref page_spa_pod for more information about how to make these
+POD objects.
+
+Now we're ready to connect the stream and run the main loop:
+
+\code{.c}
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, 1);
+
+ pw_main_loop_run(data.loop);
+\endcode
+
+To connect we specify that we have a `PW_DIRECTION_INPUT` stream. The third
+argument is always `PW_ID_ANY`.
+
+We're setting the `PW_STREAM_FLAG_AUTOCONNECT` flag to make an automatic
+connection to a suitable camera and `PW_STREAM_FLAG_MAP_BUFFERS` to let the
+stream mmap the data for us.
+
+And last we pass the extra parameters for our stream. Here we only have the
+allowed formats (`SPA_PARAM_EnumFormat`).
+
+Running the mainloop will start the connection and negotiation process.
+First our `param_changed` event will be called with the format that was
+negotiated between our stream and the camera. This is always something that
+is compatible with what we enumerated in the EnumFormat param when we
+connected.
+
+Let's take a look at how we can parse the format in the `param_changed`
+event:
+
+\code{.c}
+static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = userdata;
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+\endcode
+
+First check if there is a param. A NULL param means that it is cleared. The ID
+of the param tells you what param it is. We are only interested in Format
+param (`SPA_PARAM_Format`).
+
+We can parse the media type and subtype as below and ensure that it is
+of the right type. In our example this will always be true but when your
+EnumFormat contains different media types or subtypes, this is how you can
+parse them:
+
+\code{.c}
+ if (spa_format_parse(param,
+ &data->format.media_type,
+ &data->format.media_subtype) < 0)
+ return;
+
+ if (data->format.media_type != SPA_MEDIA_TYPE_video ||
+ data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return;
+\endcode
+
+For the `video/raw` media type/subtype there is a utility function to
+parse out the values into a `struct spa_video_info`. This makes it easier
+to deal with.
+
+\code{.c}
+ if (spa_format_video_raw_parse(param, &data->format.info.raw) < 0)
+ return;
+
+ printf("got video format:\n");
+ printf(" format: %d (%s)\n", data->format.info.raw.format,
+ spa_debug_type_find_name(spa_type_video_format,
+ data->format.info.raw.format));
+ printf(" size: %dx%d\n", data->format.info.raw.size.width,
+ data->format.info.raw.size.height);
+ printf(" framerate: %d/%d\n", data->format.info.raw.framerate.num,
+ data->format.info.raw.framerate.denom);
+
+ /** prepare to render video of this size */
+}
+\endcode
+
+In this example we dump the video size and parameters but in a real playback
+or capture application you might want to set up the screen or encoder to
+deal with the format.
+
+After negotiation, the process function is called for each new frame. Check out
+\ref page_tutorial4 for another example.
+
+\snippet tutorial5.c on_process
+
+In a real playback application, one would do something with the data, like
+copy it to the screen or encode it into a file.
+
+\ref page_tutorial4 | \ref page_tutorial "Index" | \ref page_tutorial6
+
+*/
diff --git a/doc/tutorial6.c b/doc/tutorial6.c
new file mode 100644
index 0000000..45d4485
--- /dev/null
+++ b/doc/tutorial6.c
@@ -0,0 +1,97 @@
+/*
+ [title]
+ \ref page_tutorial6
+ [title]
+ */
+/* [code] */
+#include <pipewire/pipewire.h>
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_client *client;
+ struct spa_hook client_listener;
+};
+
+/* [client_info] */
+static void client_info(void *object, const struct pw_client_info *info)
+{
+ struct data *data = object;
+ const struct spa_dict_item *item;
+
+ printf("client: id:%u\n", info->id);
+ printf("\tprops:\n");
+ spa_dict_for_each(item, info->props)
+ printf("\t\t%s: \"%s\"\n", item->key, item->value);
+
+ pw_main_loop_quit(data->loop);
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_info,
+};
+/* [client_info] */
+
+/* [registry_event_global] */
+static void registry_event_global(void *_data, uint32_t id,
+ uint32_t permissions, const char *type,
+ uint32_t version, const struct spa_dict *props)
+{
+ struct data *data = _data;
+ if (data->client != NULL)
+ return;
+
+ if (strcmp(type, PW_TYPE_INTERFACE_Client) == 0) {
+ data->client = pw_registry_bind(data->registry,
+ id, type, PW_VERSION_CLIENT, 0);
+ pw_client_add_listener(data->client,
+ &data->client_listener,
+ &client_events, data);
+ }
+}
+/* [registry_event_global] */
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data;
+
+ spa_zero(data);
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL /* properties */ );
+ data.context = pw_context_new(pw_main_loop_get_loop(data.loop),
+ NULL /* properties */ ,
+ 0 /* user_data size */ );
+
+ data.core = pw_context_connect(data.context, NULL /* properties */ ,
+ 0 /* user_data size */ );
+
+ data.registry = pw_core_get_registry(data.core, PW_VERSION_REGISTRY,
+ 0 /* user_data size */ );
+
+ pw_registry_add_listener(data.registry, &data.registry_listener,
+ &registry_events, &data);
+
+ pw_main_loop_run(data.loop);
+
+ pw_proxy_destroy((struct pw_proxy *)data.client);
+ pw_proxy_destroy((struct pw_proxy *)data.registry);
+ pw_core_disconnect(data.core);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ return 0;
+}
+/* [code] */
diff --git a/doc/tutorial6.dox b/doc/tutorial6.dox
new file mode 100644
index 0000000..0cee850
--- /dev/null
+++ b/doc/tutorial6.dox
@@ -0,0 +1,69 @@
+/** \page page_tutorial6 Tutorial - Part 6: Binding Objects
+
+\ref page_tutorial5 | \ref page_tutorial "Index"
+
+In this tutorial we show how to bind to an object so that we can
+receive events and call methods on the object.
+
+Let take a look at the following application to start.
+
+\snippet tutorial6.c code
+
+To compile the simple test application, copy it into a tutorial6.c file and
+use:
+
+ gcc -Wall tutorial6.c -o tutorial6 $(pkg-config --cflags --libs libpipewire-0.3)
+
+Most of this is the same as \ref page_tutorial2 where we simply
+enumerated all objects on the server. Instead of just printing the object
+id and some other properties, in this example we also bind to the object.
+
+We use the `pw_registry_bind()` method on our registry object like this:
+
+\snippet tutorial6.c registry_event_global
+
+We bind to the first client object that we see. This gives us a pointer
+to a `struct pw_proxy` that we can also cast to a `struct pw_client`.
+
+On the proxy we can call methods and listen for events. PipeWire will
+automatically serialize the method calls and events between client and
+server for us.
+
+We can now listen for events by adding a listener. We're going to
+listen to the info event on the client object that is emitted right
+after we bind to it or when it changes. This is not very different
+from the registry listener we added before:
+
+\snippet tutorial6.c client_info
+
+\code{.c}
+static void registry_event_global(void *_data, uint32_t id,
+ uint32_t permissions, const char *type,
+ uint32_t version, const struct spa_dict *props)
+{
+ /* ... */
+ pw_client_add_listener(data->client,
+ &data->client_listener,
+ &client_events, data);
+ /* ... */
+}
+\endcode
+
+We're also quitting the mainloop after we get the info to nicely stop
+our tutorial application.
+
+When we stop the application, don't forget to destroy all proxies that
+you created. Otherwise, they will be leaked:
+
+\code{.c}
+ /* ... */
+ pw_proxy_destroy((struct pw_proxy *)data.client);
+ /* ... */
+
+ return 0;
+}
+\endcode
+
+\ref page_tutorial5 | \ref page_tutorial "Index"
+
+*/
diff --git a/include/valgrind/memcheck.h b/include/valgrind/memcheck.h
new file mode 100644
index 0000000..2740578
--- /dev/null
+++ b/include/valgrind/memcheck.h
@@ -0,0 +1,302 @@
+
+/*
+ ----------------------------------------------------------------
+
+ Notice that the following BSD-style license applies to this one
+ file (memcheck.h) only. The rest of Valgrind is licensed under the
+ terms of the GNU General Public License, version 2, unless
+ otherwise indicated. See the COPYING file in the source
+ distribution for details.
+
+ ----------------------------------------------------------------
+
+ This file is part of MemCheck, a heavyweight Valgrind tool for
+ detecting memory errors.
+
+ Copyright (C) 2000-2013 Julian Seward. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. The origin of this software must not be misrepresented; you must
+ not claim that you wrote the original software. If you use this
+ software in a product, an acknowledgment in the product
+ documentation would be appreciated but is not required.
+
+ 3. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+ 4. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ ----------------------------------------------------------------
+
+ Notice that the above BSD-style license applies to this one file
+ (memcheck.h) only. The entire rest of Valgrind is licensed under
+ the terms of the GNU General Public License, version 2. See the
+ COPYING file in the source distribution for details.
+
+ ----------------------------------------------------------------
+*/
+
+
+#ifndef __MEMCHECK_H
+#define __MEMCHECK_H
+
+
+/* This file is for inclusion into client (your!) code.
+
+ You can use these macros to manipulate and query memory permissions
+ inside your own programs.
+
+ See comment near the top of valgrind.h on how to use them.
+*/
+
+#include "valgrind.h"
+
+/* !! ABIWARNING !! ABIWARNING !! ABIWARNING !! ABIWARNING !!
+ This enum comprises an ABI exported by Valgrind to programs
+ which use client requests. DO NOT CHANGE THE ORDER OF THESE
+ ENTRIES, NOR DELETE ANY -- add new ones at the end. */
+typedef
+ enum {
+ VG_USERREQ__MAKE_MEM_NOACCESS = VG_USERREQ_TOOL_BASE('M','C'),
+ VG_USERREQ__MAKE_MEM_UNDEFINED,
+ VG_USERREQ__MAKE_MEM_DEFINED,
+ VG_USERREQ__DISCARD,
+ VG_USERREQ__CHECK_MEM_IS_ADDRESSABLE,
+ VG_USERREQ__CHECK_MEM_IS_DEFINED,
+ VG_USERREQ__DO_LEAK_CHECK,
+ VG_USERREQ__COUNT_LEAKS,
+
+ VG_USERREQ__GET_VBITS,
+ VG_USERREQ__SET_VBITS,
+
+ VG_USERREQ__CREATE_BLOCK,
+
+ VG_USERREQ__MAKE_MEM_DEFINED_IF_ADDRESSABLE,
+
+ /* Not next to VG_USERREQ__COUNT_LEAKS because it was added later. */
+ VG_USERREQ__COUNT_LEAK_BLOCKS,
+
+ VG_USERREQ__ENABLE_ADDR_ERROR_REPORTING_IN_RANGE,
+ VG_USERREQ__DISABLE_ADDR_ERROR_REPORTING_IN_RANGE,
+
+ /* This is just for memcheck's internal use - don't use it */
+ _VG_USERREQ__MEMCHECK_RECORD_OVERLAP_ERROR
+ = VG_USERREQ_TOOL_BASE('M','C') + 256
+ } Vg_MemCheckClientRequest;
+
+
+
+/* Client-code macros to manipulate the state of memory. */
+
+/* Mark memory at _qzz_addr as unaddressable for _qzz_len bytes. */
+#define VALGRIND_MAKE_MEM_NOACCESS(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__MAKE_MEM_NOACCESS, \
+ (_qzz_addr), (_qzz_len), 0, 0, 0)
+
+/* Similarly, mark memory at _qzz_addr as addressable but undefined
+ for _qzz_len bytes. */
+#define VALGRIND_MAKE_MEM_UNDEFINED(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__MAKE_MEM_UNDEFINED, \
+ (_qzz_addr), (_qzz_len), 0, 0, 0)
+
+/* Similarly, mark memory at _qzz_addr as addressable and defined
+ for _qzz_len bytes. */
+#define VALGRIND_MAKE_MEM_DEFINED(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__MAKE_MEM_DEFINED, \
+ (_qzz_addr), (_qzz_len), 0, 0, 0)
+
+/* Similar to VALGRIND_MAKE_MEM_DEFINED except that addressability is
+ not altered: bytes which are addressable are marked as defined,
+ but those which are not addressable are left unchanged. */
+#define VALGRIND_MAKE_MEM_DEFINED_IF_ADDRESSABLE(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__MAKE_MEM_DEFINED_IF_ADDRESSABLE, \
+ (_qzz_addr), (_qzz_len), 0, 0, 0)
+
+/* Create a block-description handle. The description is an ascii
+ string which is included in any messages pertaining to addresses
+ within the specified memory range. Has no other effect on the
+ properties of the memory range. */
+#define VALGRIND_CREATE_BLOCK(_qzz_addr,_qzz_len, _qzz_desc) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__CREATE_BLOCK, \
+ (_qzz_addr), (_qzz_len), (_qzz_desc), \
+ 0, 0)
+
+/* Discard a block-description-handle. Returns 1 for an
+ invalid handle, 0 for a valid handle. */
+#define VALGRIND_DISCARD(_qzz_blkindex) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__DISCARD, \
+ 0, (_qzz_blkindex), 0, 0, 0)
+
+
+/* Client-code macros to check the state of memory. */
+
+/* Check that memory at _qzz_addr is addressable for _qzz_len bytes.
+ If suitable addressibility is not established, Valgrind prints an
+ error message and returns the address of the first offending byte.
+ Otherwise it returns zero. */
+#define VALGRIND_CHECK_MEM_IS_ADDRESSABLE(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0, \
+ VG_USERREQ__CHECK_MEM_IS_ADDRESSABLE, \
+ (_qzz_addr), (_qzz_len), 0, 0, 0)
+
+/* Check that memory at _qzz_addr is addressable and defined for
+ _qzz_len bytes. If suitable addressibility and definedness are not
+ established, Valgrind prints an error message and returns the
+ address of the first offending byte. Otherwise it returns zero. */
+#define VALGRIND_CHECK_MEM_IS_DEFINED(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0, \
+ VG_USERREQ__CHECK_MEM_IS_DEFINED, \
+ (_qzz_addr), (_qzz_len), 0, 0, 0)
+
+/* Use this macro to force the definedness and addressibility of an
+ lvalue to be checked. If suitable addressibility and definedness
+ are not established, Valgrind prints an error message and returns
+ the address of the first offending byte. Otherwise it returns
+ zero. */
+#define VALGRIND_CHECK_VALUE_IS_DEFINED(__lvalue) \
+ VALGRIND_CHECK_MEM_IS_DEFINED( \
+ (volatile unsigned char *)&(__lvalue), \
+ (unsigned long)(sizeof (__lvalue)))
+
+
+/* Do a full memory leak check (like --leak-check=full) mid-execution. */
+#define VALGRIND_DO_LEAK_CHECK \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__DO_LEAK_CHECK, \
+ 0, 0, 0, 0, 0)
+
+/* Same as VALGRIND_DO_LEAK_CHECK but only showing the entries for
+ which there was an increase in leaked bytes or leaked nr of blocks
+ since the previous leak search. */
+#define VALGRIND_DO_ADDED_LEAK_CHECK \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__DO_LEAK_CHECK, \
+ 0, 1, 0, 0, 0)
+
+/* Same as VALGRIND_DO_ADDED_LEAK_CHECK but showing entries with
+ increased or decreased leaked bytes/blocks since previous leak
+ search. */
+#define VALGRIND_DO_CHANGED_LEAK_CHECK \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__DO_LEAK_CHECK, \
+ 0, 2, 0, 0, 0)
+
+/* Do a summary memory leak check (like --leak-check=summary) mid-execution. */
+#define VALGRIND_DO_QUICK_LEAK_CHECK \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__DO_LEAK_CHECK, \
+ 1, 0, 0, 0, 0)
+
+/* Return number of leaked, dubious, reachable and suppressed bytes found by
+ all previous leak checks. They must be lvalues. */
+#define VALGRIND_COUNT_LEAKS(leaked, dubious, reachable, suppressed) \
+ /* For safety on 64-bit platforms we assign the results to private
+ unsigned long variables, then assign these to the lvalues the user
+ specified, which works no matter what type 'leaked', 'dubious', etc
+ are. We also initialise '_qzz_leaked', etc because
+ VG_USERREQ__COUNT_LEAKS doesn't mark the values returned as
+ defined. */ \
+ { \
+ unsigned long _qzz_leaked = 0, _qzz_dubious = 0; \
+ unsigned long _qzz_reachable = 0, _qzz_suppressed = 0; \
+ VALGRIND_DO_CLIENT_REQUEST_STMT( \
+ VG_USERREQ__COUNT_LEAKS, \
+ &_qzz_leaked, &_qzz_dubious, \
+ &_qzz_reachable, &_qzz_suppressed, 0); \
+ leaked = _qzz_leaked; \
+ dubious = _qzz_dubious; \
+ reachable = _qzz_reachable; \
+ suppressed = _qzz_suppressed; \
+ }
+
+/* Return number of leaked, dubious, reachable and suppressed bytes found by
+ all previous leak checks. They must be lvalues. */
+#define VALGRIND_COUNT_LEAK_BLOCKS(leaked, dubious, reachable, suppressed) \
+ /* For safety on 64-bit platforms we assign the results to private
+ unsigned long variables, then assign these to the lvalues the user
+ specified, which works no matter what type 'leaked', 'dubious', etc
+ are. We also initialise '_qzz_leaked', etc because
+ VG_USERREQ__COUNT_LEAKS doesn't mark the values returned as
+ defined. */ \
+ { \
+ unsigned long _qzz_leaked = 0, _qzz_dubious = 0; \
+ unsigned long _qzz_reachable = 0, _qzz_suppressed = 0; \
+ VALGRIND_DO_CLIENT_REQUEST_STMT( \
+ VG_USERREQ__COUNT_LEAK_BLOCKS, \
+ &_qzz_leaked, &_qzz_dubious, \
+ &_qzz_reachable, &_qzz_suppressed, 0); \
+ leaked = _qzz_leaked; \
+ dubious = _qzz_dubious; \
+ reachable = _qzz_reachable; \
+ suppressed = _qzz_suppressed; \
+ }
+
+
+/* Get the validity data for addresses [zza..zza+zznbytes-1] and copy it
+ into the provided zzvbits array. Return values:
+ 0 if not running on valgrind
+ 1 success
+ 2 [previously indicated unaligned arrays; these are now allowed]
+ 3 if any parts of zzsrc/zzvbits are not addressable.
+ The metadata is not copied in cases 0, 2 or 3 so it should be
+ impossible to segfault your system by using this call.
+*/
+#define VALGRIND_GET_VBITS(zza,zzvbits,zznbytes) \
+ (unsigned)VALGRIND_DO_CLIENT_REQUEST_EXPR(0, \
+ VG_USERREQ__GET_VBITS, \
+ (const char*)(zza), \
+ (char*)(zzvbits), \
+ (zznbytes), 0, 0)
+
+/* Set the validity data for addresses [zza..zza+zznbytes-1], copying it
+ from the provided zzvbits array. Return values:
+ 0 if not running on valgrind
+ 1 success
+ 2 [previously indicated unaligned arrays; these are now allowed]
+ 3 if any parts of zza/zzvbits are not addressable.
+ The metadata is not copied in cases 0, 2 or 3 so it should be
+ impossible to segfault your system by using this call.
+*/
+#define VALGRIND_SET_VBITS(zza,zzvbits,zznbytes) \
+ (unsigned)VALGRIND_DO_CLIENT_REQUEST_EXPR(0, \
+ VG_USERREQ__SET_VBITS, \
+ (const char*)(zza), \
+ (const char*)(zzvbits), \
+ (zznbytes), 0, 0 )
+
+/* Disable and re-enable reporting of addressing errors in the
+ specified address range. */
+#define VALGRIND_DISABLE_ADDR_ERROR_REPORTING_IN_RANGE(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__DISABLE_ADDR_ERROR_REPORTING_IN_RANGE, \
+ (_qzz_addr), (_qzz_len), 0, 0, 0)
+
+#define VALGRIND_ENABLE_ADDR_ERROR_REPORTING_IN_RANGE(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__ENABLE_ADDR_ERROR_REPORTING_IN_RANGE, \
+ (_qzz_addr), (_qzz_len), 0, 0, 0)
+
+#endif
+
diff --git a/include/valgrind/valgrind.h b/include/valgrind/valgrind.h
new file mode 100644
index 0000000..cfb40a2
--- /dev/null
+++ b/include/valgrind/valgrind.h
@@ -0,0 +1,6647 @@
+/* -*- c -*-
+ ----------------------------------------------------------------
+
+ Notice that the following BSD-style license applies to this one
+ file (valgrind.h) only. The rest of Valgrind is licensed under the
+ terms of the GNU General Public License, version 2, unless
+ otherwise indicated. See the COPYING file in the source
+ distribution for details.
+
+ ----------------------------------------------------------------
+
+ This file is part of Valgrind, a dynamic binary instrumentation
+ framework.
+
+ Copyright (C) 2000-2017 Julian Seward. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. The origin of this software must not be misrepresented; you must
+ not claim that you wrote the original software. If you use this
+ software in a product, an acknowledgment in the product
+ documentation would be appreciated but is not required.
+
+ 3. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+ 4. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ ----------------------------------------------------------------
+
+ Notice that the above BSD-style license applies to this one file
+ (valgrind.h) only. The entire rest of Valgrind is licensed under
+ the terms of the GNU General Public License, version 2. See the
+ COPYING file in the source distribution for details.
+
+ ----------------------------------------------------------------
+*/
+
+
+/* This file is for inclusion into client (your!) code.
+
+ You can use these macros to manipulate and query Valgrind's
+ execution inside your own programs.
+
+ The resulting executables will still run without Valgrind, just a
+ little bit more slowly than they otherwise would, but otherwise
+ unchanged. When not running on valgrind, each client request
+ consumes very few (eg. 7) instructions, so the resulting performance
+ loss is negligible unless you plan to execute client requests
+ millions of times per second. Nevertheless, if that is still a
+ problem, you can compile with the NVALGRIND symbol defined (gcc
+ -DNVALGRIND) so that client requests are not even compiled in. */
+
+#ifndef __VALGRIND_H
+#define __VALGRIND_H
+
+
+/* ------------------------------------------------------------------ */
+/* VERSION NUMBER OF VALGRIND */
+/* ------------------------------------------------------------------ */
+
+/* Specify Valgrind's version number, so that user code can
+ conditionally compile based on our version number. Note that these
+ were introduced at version 3.6 and so do not exist in version 3.5
+ or earlier. The recommended way to use them to check for "version
+ X.Y or later" is (eg)
+
+#if defined(__VALGRIND_MAJOR__) && defined(__VALGRIND_MINOR__) \
+ && (__VALGRIND_MAJOR__ > 3 \
+ || (__VALGRIND_MAJOR__ == 3 && __VALGRIND_MINOR__ >= 6))
+*/
+#define __VALGRIND_MAJOR__ 3
+#define __VALGRIND_MINOR__ 15
+
+
+#include <stdarg.h>
+
+/* Nb: this file might be included in a file compiled with -ansi. So
+ we can't use C++ style "//" comments nor the "asm" keyword (instead
+ use "__asm__"). */
+
+/* Derive some tags indicating what the target platform is. Note
+ that in this file we're using the compiler's CPP symbols for
+ identifying architectures, which are different to the ones we use
+ within the rest of Valgrind. Note, __powerpc__ is active for both
+ 32 and 64-bit PPC, whereas __powerpc64__ is only active for the
+ latter (on Linux, that is).
+
+ Misc note: how to find out what's predefined in gcc by default:
+ gcc -Wp,-dM somefile.c
+*/
+#undef PLAT_x86_darwin
+#undef PLAT_amd64_darwin
+#undef PLAT_x86_win32
+#undef PLAT_amd64_win64
+#undef PLAT_x86_linux
+#undef PLAT_amd64_linux
+#undef PLAT_ppc32_linux
+#undef PLAT_ppc64be_linux
+#undef PLAT_ppc64le_linux
+#undef PLAT_arm_linux
+#undef PLAT_arm64_linux
+#undef PLAT_s390x_linux
+#undef PLAT_mips32_linux
+#undef PLAT_mips64_linux
+#undef PLAT_x86_solaris
+#undef PLAT_amd64_solaris
+
+
+#if defined(__APPLE__) && defined(__i386__)
+# define PLAT_x86_darwin 1
+#elif defined(__APPLE__) && defined(__x86_64__)
+# define PLAT_amd64_darwin 1
+#elif (defined(__MINGW32__) && !defined(__MINGW64__)) \
+ || defined(__CYGWIN32__) \
+ || (defined(_WIN32) && defined(_M_IX86))
+# define PLAT_x86_win32 1
+#elif defined(__MINGW64__) \
+ || (defined(_WIN64) && defined(_M_X64))
+# define PLAT_amd64_win64 1
+#elif defined(__linux__) && defined(__i386__)
+# define PLAT_x86_linux 1
+#elif defined(__linux__) && defined(__x86_64__) && !defined(__ILP32__)
+# define PLAT_amd64_linux 1
+#elif defined(__linux__) && defined(__powerpc__) && !defined(__powerpc64__)
+# define PLAT_ppc32_linux 1
+#elif defined(__linux__) && defined(__powerpc__) && defined(__powerpc64__) && _CALL_ELF != 2
+/* Big Endian uses ELF version 1 */
+# define PLAT_ppc64be_linux 1
+#elif defined(__linux__) && defined(__powerpc__) && defined(__powerpc64__) && _CALL_ELF == 2
+/* Little Endian uses ELF version 2 */
+# define PLAT_ppc64le_linux 1
+#elif defined(__linux__) && defined(__arm__) && !defined(__aarch64__)
+# define PLAT_arm_linux 1
+#elif defined(__linux__) && defined(__aarch64__) && !defined(__arm__)
+# define PLAT_arm64_linux 1
+#elif defined(__linux__) && defined(__s390__) && defined(__s390x__)
+# define PLAT_s390x_linux 1
+#elif defined(__linux__) && defined(__mips__) && (__mips==64)
+# define PLAT_mips64_linux 1
+#elif defined(__linux__) && defined(__mips__) && (__mips!=64)
+# define PLAT_mips32_linux 1
+#elif defined(__sun) && defined(__i386__)
+# define PLAT_x86_solaris 1
+#elif defined(__sun) && defined(__x86_64__)
+# define PLAT_amd64_solaris 1
+#else
+/* If we're not compiling for our target platform, don't generate
+ any inline asms. */
+# if !defined(NVALGRIND)
+# define NVALGRIND 1
+# endif
+#endif
+
+
+/* ------------------------------------------------------------------ */
+/* ARCHITECTURE SPECIFICS for SPECIAL INSTRUCTIONS. There is nothing */
+/* in here of use to end-users -- skip to the next section. */
+/* ------------------------------------------------------------------ */
+
+/*
+ * VALGRIND_DO_CLIENT_REQUEST(): a statement that invokes a Valgrind client
+ * request. Accepts both pointers and integers as arguments.
+ *
+ * VALGRIND_DO_CLIENT_REQUEST_STMT(): a statement that invokes a Valgrind
+ * client request that does not return a value.
+
+ * VALGRIND_DO_CLIENT_REQUEST_EXPR(): a C expression that invokes a Valgrind
+ * client request and whose value equals the client request result. Accepts
+ * both pointers and integers as arguments. Note that such calls are not
+ * necessarily pure functions -- they may have side effects.
+ */
+
+#define VALGRIND_DO_CLIENT_REQUEST(_zzq_rlval, _zzq_default, \
+ _zzq_request, _zzq_arg1, _zzq_arg2, \
+ _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ do { (_zzq_rlval) = VALGRIND_DO_CLIENT_REQUEST_EXPR((_zzq_default), \
+ (_zzq_request), (_zzq_arg1), (_zzq_arg2), \
+ (_zzq_arg3), (_zzq_arg4), (_zzq_arg5)); } while (0)
+
+#define VALGRIND_DO_CLIENT_REQUEST_STMT(_zzq_request, _zzq_arg1, \
+ _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ do { (void) VALGRIND_DO_CLIENT_REQUEST_EXPR(0, \
+ (_zzq_request), (_zzq_arg1), (_zzq_arg2), \
+ (_zzq_arg3), (_zzq_arg4), (_zzq_arg5)); } while (0)
+
+#if defined(NVALGRIND)
+
+/* Define NVALGRIND to completely remove the Valgrind magic sequence
+ from the compiled code (analogous to NDEBUG's effects on
+ assert()) */
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ (_zzq_default)
+
+#else /* ! NVALGRIND */
+
+/* The following defines the magic code sequences which the JITter
+ spots and handles magically. Don't look too closely at them as
+ they will rot your brain.
+
+ The assembly code sequences for all architectures is in this one
+ file. This is because this file must be stand-alone, and we don't
+ want to have multiple files.
+
+ For VALGRIND_DO_CLIENT_REQUEST, we must ensure that the default
+ value gets put in the return slot, so that everything works when
+ this is executed not under Valgrind. Args are passed in a memory
+ block, and so there's no intrinsic limit to the number that could
+ be passed, but it's currently five.
+
+ The macro args are:
+ _zzq_rlval result lvalue
+ _zzq_default default value (result returned when running on real CPU)
+ _zzq_request request code
+ _zzq_arg1..5 request params
+
+ The other two macros are used to support function wrapping, and are
+ a lot simpler. VALGRIND_GET_NR_CONTEXT returns the value of the
+ guest's NRADDR pseudo-register and whatever other information is
+ needed to safely run the call original from the wrapper: on
+ ppc64-linux, the R2 value at the divert point is also needed. This
+ information is abstracted into a user-visible type, OrigFn.
+
+ VALGRIND_CALL_NOREDIR_* behaves the same as the following on the
+ guest, but guarantees that the branch instruction will not be
+ redirected: x86: call *%eax, amd64: call *%rax, ppc32/ppc64:
+ branch-and-link-to-r11. VALGRIND_CALL_NOREDIR is just text, not a
+ complete inline asm, since it needs to be combined with more magic
+ inline asm stuff to be useful.
+*/
+
+/* ----------------- x86-{linux,darwin,solaris} ---------------- */
+
+#if defined(PLAT_x86_linux) || defined(PLAT_x86_darwin) \
+ || (defined(PLAT_x86_win32) && defined(__GNUC__)) \
+ || defined(PLAT_x86_solaris)
+
+typedef
+ struct {
+ unsigned int nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "roll $3, %%edi ; roll $13, %%edi\n\t" \
+ "roll $29, %%edi ; roll $19, %%edi\n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ __extension__ \
+ ({volatile unsigned int _zzq_args[6]; \
+ volatile unsigned int _zzq_result; \
+ _zzq_args[0] = (unsigned int)(_zzq_request); \
+ _zzq_args[1] = (unsigned int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned int)(_zzq_arg5); \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %EDX = client_request ( %EAX ) */ \
+ "xchgl %%ebx,%%ebx" \
+ : "=d" (_zzq_result) \
+ : "a" (&_zzq_args[0]), "0" (_zzq_default) \
+ : "cc", "memory" \
+ ); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ volatile unsigned int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %EAX = guest_NRADDR */ \
+ "xchgl %%ecx,%%ecx" \
+ : "=a" (__addr) \
+ : \
+ : "cc", "memory" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_CALL_NOREDIR_EAX \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* call-noredir *%EAX */ \
+ "xchgl %%edx,%%edx\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "xchgl %%edi,%%edi\n\t" \
+ : : : "cc", "memory" \
+ ); \
+ } while (0)
+
+#endif /* PLAT_x86_linux || PLAT_x86_darwin || (PLAT_x86_win32 && __GNUC__)
+ || PLAT_x86_solaris */
+
+/* ------------------------- x86-Win32 ------------------------- */
+
+#if defined(PLAT_x86_win32) && !defined(__GNUC__)
+
+typedef
+ struct {
+ unsigned int nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+#if defined(_MSC_VER)
+
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ __asm rol edi, 3 __asm rol edi, 13 \
+ __asm rol edi, 29 __asm rol edi, 19
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ valgrind_do_client_request_expr((uintptr_t)(_zzq_default), \
+ (uintptr_t)(_zzq_request), (uintptr_t)(_zzq_arg1), \
+ (uintptr_t)(_zzq_arg2), (uintptr_t)(_zzq_arg3), \
+ (uintptr_t)(_zzq_arg4), (uintptr_t)(_zzq_arg5))
+
+static __inline uintptr_t
+valgrind_do_client_request_expr(uintptr_t _zzq_default, uintptr_t _zzq_request,
+ uintptr_t _zzq_arg1, uintptr_t _zzq_arg2,
+ uintptr_t _zzq_arg3, uintptr_t _zzq_arg4,
+ uintptr_t _zzq_arg5)
+{
+ volatile uintptr_t _zzq_args[6];
+ volatile unsigned int _zzq_result;
+ _zzq_args[0] = (uintptr_t)(_zzq_request);
+ _zzq_args[1] = (uintptr_t)(_zzq_arg1);
+ _zzq_args[2] = (uintptr_t)(_zzq_arg2);
+ _zzq_args[3] = (uintptr_t)(_zzq_arg3);
+ _zzq_args[4] = (uintptr_t)(_zzq_arg4);
+ _zzq_args[5] = (uintptr_t)(_zzq_arg5);
+ __asm { __asm lea eax, _zzq_args __asm mov edx, _zzq_default
+ __SPECIAL_INSTRUCTION_PREAMBLE
+ /* %EDX = client_request ( %EAX ) */
+ __asm xchg ebx,ebx
+ __asm mov _zzq_result, edx
+ }
+ return _zzq_result;
+}
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ volatile unsigned int __addr; \
+ __asm { __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %EAX = guest_NRADDR */ \
+ __asm xchg ecx,ecx \
+ __asm mov __addr, eax \
+ } \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_CALL_NOREDIR_EAX ERROR
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm { __SPECIAL_INSTRUCTION_PREAMBLE \
+ __asm xchg edi,edi \
+ } \
+ } while (0)
+
+#else
+#error Unsupported compiler.
+#endif
+
+#endif /* PLAT_x86_win32 */
+
+/* ----------------- amd64-{linux,darwin,solaris} --------------- */
+
+#if defined(PLAT_amd64_linux) || defined(PLAT_amd64_darwin) \
+ || defined(PLAT_amd64_solaris) \
+ || (defined(PLAT_amd64_win64) && defined(__GNUC__))
+
+typedef
+ struct {
+ unsigned long int nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "rolq $3, %%rdi ; rolq $13, %%rdi\n\t" \
+ "rolq $61, %%rdi ; rolq $51, %%rdi\n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ __extension__ \
+ ({ volatile unsigned long int _zzq_args[6]; \
+ volatile unsigned long int _zzq_result; \
+ _zzq_args[0] = (unsigned long int)(_zzq_request); \
+ _zzq_args[1] = (unsigned long int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned long int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned long int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned long int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned long int)(_zzq_arg5); \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %RDX = client_request ( %RAX ) */ \
+ "xchgq %%rbx,%%rbx" \
+ : "=d" (_zzq_result) \
+ : "a" (&_zzq_args[0]), "0" (_zzq_default) \
+ : "cc", "memory" \
+ ); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ volatile unsigned long int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %RAX = guest_NRADDR */ \
+ "xchgq %%rcx,%%rcx" \
+ : "=a" (__addr) \
+ : \
+ : "cc", "memory" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_CALL_NOREDIR_RAX \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* call-noredir *%RAX */ \
+ "xchgq %%rdx,%%rdx\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "xchgq %%rdi,%%rdi\n\t" \
+ : : : "cc", "memory" \
+ ); \
+ } while (0)
+
+#endif /* PLAT_amd64_linux || PLAT_amd64_darwin || PLAT_amd64_solaris */
+
+/* ------------------------- amd64-Win64 ------------------------- */
+
+#if defined(PLAT_amd64_win64) && !defined(__GNUC__)
+
+#error Unsupported compiler.
+
+#endif /* PLAT_amd64_win64 */
+
+/* ------------------------ ppc32-linux ------------------------ */
+
+#if defined(PLAT_ppc32_linux)
+
+typedef
+ struct {
+ unsigned int nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "rlwinm 0,0,3,0,31 ; rlwinm 0,0,13,0,31\n\t" \
+ "rlwinm 0,0,29,0,31 ; rlwinm 0,0,19,0,31\n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ \
+ __extension__ \
+ ({ unsigned int _zzq_args[6]; \
+ unsigned int _zzq_result; \
+ unsigned int* _zzq_ptr; \
+ _zzq_args[0] = (unsigned int)(_zzq_request); \
+ _zzq_args[1] = (unsigned int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned int)(_zzq_arg5); \
+ _zzq_ptr = _zzq_args; \
+ __asm__ volatile("mr 3,%1\n\t" /*default*/ \
+ "mr 4,%2\n\t" /*ptr*/ \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %R3 = client_request ( %R4 ) */ \
+ "or 1,1,1\n\t" \
+ "mr %0,3" /*result*/ \
+ : "=b" (_zzq_result) \
+ : "b" (_zzq_default), "b" (_zzq_ptr) \
+ : "cc", "memory", "r3", "r4"); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ unsigned int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %R3 = guest_NRADDR */ \
+ "or 2,2,2\n\t" \
+ "mr %0,3" \
+ : "=b" (__addr) \
+ : \
+ : "cc", "memory", "r3" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* branch-and-link-to-noredir *%R11 */ \
+ "or 3,3,3\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "or 5,5,5\n\t" \
+ ); \
+ } while (0)
+
+#endif /* PLAT_ppc32_linux */
+
+/* ------------------------ ppc64-linux ------------------------ */
+
+#if defined(PLAT_ppc64be_linux)
+
+typedef
+ struct {
+ unsigned long int nraddr; /* where's the code? */
+ unsigned long int r2; /* what tocptr do we need? */
+ }
+ OrigFn;
+
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "rotldi 0,0,3 ; rotldi 0,0,13\n\t" \
+ "rotldi 0,0,61 ; rotldi 0,0,51\n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ \
+ __extension__ \
+ ({ unsigned long int _zzq_args[6]; \
+ unsigned long int _zzq_result; \
+ unsigned long int* _zzq_ptr; \
+ _zzq_args[0] = (unsigned long int)(_zzq_request); \
+ _zzq_args[1] = (unsigned long int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned long int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned long int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned long int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned long int)(_zzq_arg5); \
+ _zzq_ptr = _zzq_args; \
+ __asm__ volatile("mr 3,%1\n\t" /*default*/ \
+ "mr 4,%2\n\t" /*ptr*/ \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %R3 = client_request ( %R4 ) */ \
+ "or 1,1,1\n\t" \
+ "mr %0,3" /*result*/ \
+ : "=b" (_zzq_result) \
+ : "b" (_zzq_default), "b" (_zzq_ptr) \
+ : "cc", "memory", "r3", "r4"); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ unsigned long int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %R3 = guest_NRADDR */ \
+ "or 2,2,2\n\t" \
+ "mr %0,3" \
+ : "=b" (__addr) \
+ : \
+ : "cc", "memory", "r3" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %R3 = guest_NRADDR_GPR2 */ \
+ "or 4,4,4\n\t" \
+ "mr %0,3" \
+ : "=b" (__addr) \
+ : \
+ : "cc", "memory", "r3" \
+ ); \
+ _zzq_orig->r2 = __addr; \
+ }
+
+#define VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* branch-and-link-to-noredir *%R11 */ \
+ "or 3,3,3\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "or 5,5,5\n\t" \
+ ); \
+ } while (0)
+
+#endif /* PLAT_ppc64be_linux */
+
+#if defined(PLAT_ppc64le_linux)
+
+typedef
+ struct {
+ unsigned long int nraddr; /* where's the code? */
+ unsigned long int r2; /* what tocptr do we need? */
+ }
+ OrigFn;
+
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "rotldi 0,0,3 ; rotldi 0,0,13\n\t" \
+ "rotldi 0,0,61 ; rotldi 0,0,51\n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ \
+ __extension__ \
+ ({ unsigned long int _zzq_args[6]; \
+ unsigned long int _zzq_result; \
+ unsigned long int* _zzq_ptr; \
+ _zzq_args[0] = (unsigned long int)(_zzq_request); \
+ _zzq_args[1] = (unsigned long int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned long int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned long int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned long int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned long int)(_zzq_arg5); \
+ _zzq_ptr = _zzq_args; \
+ __asm__ volatile("mr 3,%1\n\t" /*default*/ \
+ "mr 4,%2\n\t" /*ptr*/ \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %R3 = client_request ( %R4 ) */ \
+ "or 1,1,1\n\t" \
+ "mr %0,3" /*result*/ \
+ : "=b" (_zzq_result) \
+ : "b" (_zzq_default), "b" (_zzq_ptr) \
+ : "cc", "memory", "r3", "r4"); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ unsigned long int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %R3 = guest_NRADDR */ \
+ "or 2,2,2\n\t" \
+ "mr %0,3" \
+ : "=b" (__addr) \
+ : \
+ : "cc", "memory", "r3" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %R3 = guest_NRADDR_GPR2 */ \
+ "or 4,4,4\n\t" \
+ "mr %0,3" \
+ : "=b" (__addr) \
+ : \
+ : "cc", "memory", "r3" \
+ ); \
+ _zzq_orig->r2 = __addr; \
+ }
+
+#define VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* branch-and-link-to-noredir *%R12 */ \
+ "or 3,3,3\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "or 5,5,5\n\t" \
+ ); \
+ } while (0)
+
+#endif /* PLAT_ppc64le_linux */
+
+/* ------------------------- arm-linux ------------------------- */
+
+#if defined(PLAT_arm_linux)
+
+typedef
+ struct {
+ unsigned int nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "mov r12, r12, ror #3 ; mov r12, r12, ror #13 \n\t" \
+ "mov r12, r12, ror #29 ; mov r12, r12, ror #19 \n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ \
+ __extension__ \
+ ({volatile unsigned int _zzq_args[6]; \
+ volatile unsigned int _zzq_result; \
+ _zzq_args[0] = (unsigned int)(_zzq_request); \
+ _zzq_args[1] = (unsigned int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned int)(_zzq_arg5); \
+ __asm__ volatile("mov r3, %1\n\t" /*default*/ \
+ "mov r4, %2\n\t" /*ptr*/ \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* R3 = client_request ( R4 ) */ \
+ "orr r10, r10, r10\n\t" \
+ "mov %0, r3" /*result*/ \
+ : "=r" (_zzq_result) \
+ : "r" (_zzq_default), "r" (&_zzq_args[0]) \
+ : "cc","memory", "r3", "r4"); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ unsigned int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* R3 = guest_NRADDR */ \
+ "orr r11, r11, r11\n\t" \
+ "mov %0, r3" \
+ : "=r" (__addr) \
+ : \
+ : "cc", "memory", "r3" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* branch-and-link-to-noredir *%R4 */ \
+ "orr r12, r12, r12\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "orr r9, r9, r9\n\t" \
+ : : : "cc", "memory" \
+ ); \
+ } while (0)
+
+#endif /* PLAT_arm_linux */
+
+/* ------------------------ arm64-linux ------------------------- */
+
+#if defined(PLAT_arm64_linux)
+
+typedef
+ struct {
+ unsigned long int nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "ror x12, x12, #3 ; ror x12, x12, #13 \n\t" \
+ "ror x12, x12, #51 ; ror x12, x12, #61 \n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ \
+ __extension__ \
+ ({volatile unsigned long int _zzq_args[6]; \
+ volatile unsigned long int _zzq_result; \
+ _zzq_args[0] = (unsigned long int)(_zzq_request); \
+ _zzq_args[1] = (unsigned long int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned long int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned long int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned long int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned long int)(_zzq_arg5); \
+ __asm__ volatile("mov x3, %1\n\t" /*default*/ \
+ "mov x4, %2\n\t" /*ptr*/ \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* X3 = client_request ( X4 ) */ \
+ "orr x10, x10, x10\n\t" \
+ "mov %0, x3" /*result*/ \
+ : "=r" (_zzq_result) \
+ : "r" ((unsigned long int)(_zzq_default)), \
+ "r" (&_zzq_args[0]) \
+ : "cc","memory", "x3", "x4"); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ unsigned long int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* X3 = guest_NRADDR */ \
+ "orr x11, x11, x11\n\t" \
+ "mov %0, x3" \
+ : "=r" (__addr) \
+ : \
+ : "cc", "memory", "x3" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* branch-and-link-to-noredir X8 */ \
+ "orr x12, x12, x12\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "orr x9, x9, x9\n\t" \
+ : : : "cc", "memory" \
+ ); \
+ } while (0)
+
+#endif /* PLAT_arm64_linux */
+
+/* ------------------------ s390x-linux ------------------------ */
+
+#if defined(PLAT_s390x_linux)
+
+typedef
+ struct {
+ unsigned long int nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+/* __SPECIAL_INSTRUCTION_PREAMBLE will be used to identify Valgrind specific
+ * code. This detection is implemented in platform specific toIR.c
+ * (e.g. VEX/priv/guest_s390_decoder.c).
+ */
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "lr 15,15\n\t" \
+ "lr 1,1\n\t" \
+ "lr 2,2\n\t" \
+ "lr 3,3\n\t"
+
+#define __CLIENT_REQUEST_CODE "lr 2,2\n\t"
+#define __GET_NR_CONTEXT_CODE "lr 3,3\n\t"
+#define __CALL_NO_REDIR_CODE "lr 4,4\n\t"
+#define __VEX_INJECT_IR_CODE "lr 5,5\n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ __extension__ \
+ ({volatile unsigned long int _zzq_args[6]; \
+ volatile unsigned long int _zzq_result; \
+ _zzq_args[0] = (unsigned long int)(_zzq_request); \
+ _zzq_args[1] = (unsigned long int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned long int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned long int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned long int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned long int)(_zzq_arg5); \
+ __asm__ volatile(/* r2 = args */ \
+ "lgr 2,%1\n\t" \
+ /* r3 = default */ \
+ "lgr 3,%2\n\t" \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ __CLIENT_REQUEST_CODE \
+ /* results = r3 */ \
+ "lgr %0, 3\n\t" \
+ : "=d" (_zzq_result) \
+ : "a" (&_zzq_args[0]), "0" (_zzq_default) \
+ : "cc", "2", "3", "memory" \
+ ); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ volatile unsigned long int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ __GET_NR_CONTEXT_CODE \
+ "lgr %0, 3\n\t" \
+ : "=a" (__addr) \
+ : \
+ : "cc", "3", "memory" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_CALL_NOREDIR_R1 \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ __CALL_NO_REDIR_CODE
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ __VEX_INJECT_IR_CODE); \
+ } while (0)
+
+#endif /* PLAT_s390x_linux */
+
+/* ------------------------- mips32-linux ---------------- */
+
+#if defined(PLAT_mips32_linux)
+
+typedef
+ struct {
+ unsigned int nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+/* .word 0x342
+ * .word 0x742
+ * .word 0xC2
+ * .word 0x4C2*/
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "srl $0, $0, 13\n\t" \
+ "srl $0, $0, 29\n\t" \
+ "srl $0, $0, 3\n\t" \
+ "srl $0, $0, 19\n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ __extension__ \
+ ({ volatile unsigned int _zzq_args[6]; \
+ volatile unsigned int _zzq_result; \
+ _zzq_args[0] = (unsigned int)(_zzq_request); \
+ _zzq_args[1] = (unsigned int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned int)(_zzq_arg5); \
+ __asm__ volatile("move $11, %1\n\t" /*default*/ \
+ "move $12, %2\n\t" /*ptr*/ \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* T3 = client_request ( T4 ) */ \
+ "or $13, $13, $13\n\t" \
+ "move %0, $11\n\t" /*result*/ \
+ : "=r" (_zzq_result) \
+ : "r" (_zzq_default), "r" (&_zzq_args[0]) \
+ : "$11", "$12", "memory"); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ volatile unsigned int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* %t9 = guest_NRADDR */ \
+ "or $14, $14, $14\n\t" \
+ "move %0, $11" /*result*/ \
+ : "=r" (__addr) \
+ : \
+ : "$11" \
+ ); \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_CALL_NOREDIR_T9 \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* call-noredir *%t9 */ \
+ "or $15, $15, $15\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "or $11, $11, $11\n\t" \
+ ); \
+ } while (0)
+
+
+#endif /* PLAT_mips32_linux */
+
+/* ------------------------- mips64-linux ---------------- */
+
+#if defined(PLAT_mips64_linux)
+
+typedef
+ struct {
+ unsigned long nraddr; /* where's the code? */
+ }
+ OrigFn;
+
+/* dsll $0,$0, 3
+ * dsll $0,$0, 13
+ * dsll $0,$0, 29
+ * dsll $0,$0, 19*/
+#define __SPECIAL_INSTRUCTION_PREAMBLE \
+ "dsll $0,$0, 3 ; dsll $0,$0,13\n\t" \
+ "dsll $0,$0,29 ; dsll $0,$0,19\n\t"
+
+#define VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ _zzq_default, _zzq_request, \
+ _zzq_arg1, _zzq_arg2, _zzq_arg3, _zzq_arg4, _zzq_arg5) \
+ __extension__ \
+ ({ volatile unsigned long int _zzq_args[6]; \
+ volatile unsigned long int _zzq_result; \
+ _zzq_args[0] = (unsigned long int)(_zzq_request); \
+ _zzq_args[1] = (unsigned long int)(_zzq_arg1); \
+ _zzq_args[2] = (unsigned long int)(_zzq_arg2); \
+ _zzq_args[3] = (unsigned long int)(_zzq_arg3); \
+ _zzq_args[4] = (unsigned long int)(_zzq_arg4); \
+ _zzq_args[5] = (unsigned long int)(_zzq_arg5); \
+ __asm__ volatile("move $11, %1\n\t" /*default*/ \
+ "move $12, %2\n\t" /*ptr*/ \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* $11 = client_request ( $12 ) */ \
+ "or $13, $13, $13\n\t" \
+ "move %0, $11\n\t" /*result*/ \
+ : "=r" (_zzq_result) \
+ : "r" (_zzq_default), "r" (&_zzq_args[0]) \
+ : "$11", "$12", "memory"); \
+ _zzq_result; \
+ })
+
+#define VALGRIND_GET_NR_CONTEXT(_zzq_rlval) \
+ { volatile OrigFn* _zzq_orig = &(_zzq_rlval); \
+ volatile unsigned long int __addr; \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ /* $11 = guest_NRADDR */ \
+ "or $14, $14, $14\n\t" \
+ "move %0, $11" /*result*/ \
+ : "=r" (__addr) \
+ : \
+ : "$11"); \
+ _zzq_orig->nraddr = __addr; \
+ }
+
+#define VALGRIND_CALL_NOREDIR_T9 \
+ __SPECIAL_INSTRUCTION_PREAMBLE \
+ /* call-noredir $25 */ \
+ "or $15, $15, $15\n\t"
+
+#define VALGRIND_VEX_INJECT_IR() \
+ do { \
+ __asm__ volatile(__SPECIAL_INSTRUCTION_PREAMBLE \
+ "or $11, $11, $11\n\t" \
+ ); \
+ } while (0)
+
+#endif /* PLAT_mips64_linux */
+
+/* Insert assembly code for other platforms here... */
+
+#endif /* NVALGRIND */
+
+
+/* ------------------------------------------------------------------ */
+/* PLATFORM SPECIFICS for FUNCTION WRAPPING. This is all very */
+/* ugly. It's the least-worst tradeoff I can think of. */
+/* ------------------------------------------------------------------ */
+
+/* This section defines magic (a.k.a appalling-hack) macros for doing
+ guaranteed-no-redirection macros, so as to get from function
+ wrappers to the functions they are wrapping. The whole point is to
+ construct standard call sequences, but to do the call itself with a
+ special no-redirect call pseudo-instruction that the JIT
+ understands and handles specially. This section is long and
+ repetitious, and I can't see a way to make it shorter.
+
+ The naming scheme is as follows:
+
+ CALL_FN_{W,v}_{v,W,WW,WWW,WWWW,5W,6W,7W,etc}
+
+ 'W' stands for "word" and 'v' for "void". Hence there are
+ different macros for calling arity 0, 1, 2, 3, 4, etc, functions,
+ and for each, the possibility of returning a word-typed result, or
+ no result.
+*/
+
+/* Use these to write the name of your wrapper. NOTE: duplicates
+ VG_WRAP_FUNCTION_Z{U,Z} in pub_tool_redir.h. NOTE also: inserts
+ the default behaviour equivalence class tag "0000" into the name.
+ See pub_tool_redir.h for details -- normally you don't need to
+ think about this, though. */
+
+/* Use an extra level of macroisation so as to ensure the soname/fnname
+ args are fully macro-expanded before pasting them together. */
+#define VG_CONCAT4(_aa,_bb,_cc,_dd) _aa##_bb##_cc##_dd
+
+#define I_WRAP_SONAME_FNNAME_ZU(soname,fnname) \
+ VG_CONCAT4(_vgw00000ZU_,soname,_,fnname)
+
+#define I_WRAP_SONAME_FNNAME_ZZ(soname,fnname) \
+ VG_CONCAT4(_vgw00000ZZ_,soname,_,fnname)
+
+/* Use this macro from within a wrapper function to collect the
+ context (address and possibly other info) of the original function.
+ Once you have that you can then use it in one of the CALL_FN_
+ macros. The type of the argument _lval is OrigFn. */
+#define VALGRIND_GET_ORIG_FN(_lval) VALGRIND_GET_NR_CONTEXT(_lval)
+
+/* Also provide end-user facilities for function replacement, rather
+ than wrapping. A replacement function differs from a wrapper in
+ that it has no way to get hold of the original function being
+ called, and hence no way to call onwards to it. In a replacement
+ function, VALGRIND_GET_ORIG_FN always returns zero. */
+
+#define I_REPLACE_SONAME_FNNAME_ZU(soname,fnname) \
+ VG_CONCAT4(_vgr00000ZU_,soname,_,fnname)
+
+#define I_REPLACE_SONAME_FNNAME_ZZ(soname,fnname) \
+ VG_CONCAT4(_vgr00000ZZ_,soname,_,fnname)
+
+/* Derivatives of the main macros below, for calling functions
+ returning void. */
+
+#define CALL_FN_v_v(fnptr) \
+ do { volatile unsigned long _junk; \
+ CALL_FN_W_v(_junk,fnptr); } while (0)
+
+#define CALL_FN_v_W(fnptr, arg1) \
+ do { volatile unsigned long _junk; \
+ CALL_FN_W_W(_junk,fnptr,arg1); } while (0)
+
+#define CALL_FN_v_WW(fnptr, arg1,arg2) \
+ do { volatile unsigned long _junk; \
+ CALL_FN_W_WW(_junk,fnptr,arg1,arg2); } while (0)
+
+#define CALL_FN_v_WWW(fnptr, arg1,arg2,arg3) \
+ do { volatile unsigned long _junk; \
+ CALL_FN_W_WWW(_junk,fnptr,arg1,arg2,arg3); } while (0)
+
+#define CALL_FN_v_WWWW(fnptr, arg1,arg2,arg3,arg4) \
+ do { volatile unsigned long _junk; \
+ CALL_FN_W_WWWW(_junk,fnptr,arg1,arg2,arg3,arg4); } while (0)
+
+#define CALL_FN_v_5W(fnptr, arg1,arg2,arg3,arg4,arg5) \
+ do { volatile unsigned long _junk; \
+ CALL_FN_W_5W(_junk,fnptr,arg1,arg2,arg3,arg4,arg5); } while (0)
+
+#define CALL_FN_v_6W(fnptr, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { volatile unsigned long _junk; \
+ CALL_FN_W_6W(_junk,fnptr,arg1,arg2,arg3,arg4,arg5,arg6); } while (0)
+
+#define CALL_FN_v_7W(fnptr, arg1,arg2,arg3,arg4,arg5,arg6,arg7) \
+ do { volatile unsigned long _junk; \
+ CALL_FN_W_7W(_junk,fnptr,arg1,arg2,arg3,arg4,arg5,arg6,arg7); } while (0)
+
+/* ----------------- x86-{linux,darwin,solaris} ---------------- */
+
+#if defined(PLAT_x86_linux) || defined(PLAT_x86_darwin) \
+ || defined(PLAT_x86_solaris)
+
+/* These regs are trashed by the hidden call. No need to mention eax
+ as gcc can already see that, plus causes gcc to bomb. */
+#define __CALLER_SAVED_REGS /*"eax"*/ "ecx", "edx"
+
+/* Macros to save and align the stack before making a function
+ call and restore it afterwards as gcc may not keep the stack
+ pointer aligned if it doesn't realise calls are being made
+ to other functions. */
+
+#define VALGRIND_ALIGN_STACK \
+ "movl %%esp,%%edi\n\t" \
+ "andl $0xfffffff0,%%esp\n\t"
+#define VALGRIND_RESTORE_STACK \
+ "movl %%edi,%%esp\n\t"
+
+/* These CALL_FN_ macros assume that on x86-linux, sizeof(unsigned
+ long) == 4. */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[1]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[2]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $12, %%esp\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $8, %%esp\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[4]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $4, %%esp\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[5]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[6]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $12, %%esp\n\t" \
+ "pushl 20(%%eax)\n\t" \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[7]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $8, %%esp\n\t" \
+ "pushl 24(%%eax)\n\t" \
+ "pushl 20(%%eax)\n\t" \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[8]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $4, %%esp\n\t" \
+ "pushl 28(%%eax)\n\t" \
+ "pushl 24(%%eax)\n\t" \
+ "pushl 20(%%eax)\n\t" \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[9]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "pushl 32(%%eax)\n\t" \
+ "pushl 28(%%eax)\n\t" \
+ "pushl 24(%%eax)\n\t" \
+ "pushl 20(%%eax)\n\t" \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[10]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $12, %%esp\n\t" \
+ "pushl 36(%%eax)\n\t" \
+ "pushl 32(%%eax)\n\t" \
+ "pushl 28(%%eax)\n\t" \
+ "pushl 24(%%eax)\n\t" \
+ "pushl 20(%%eax)\n\t" \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[11]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $8, %%esp\n\t" \
+ "pushl 40(%%eax)\n\t" \
+ "pushl 36(%%eax)\n\t" \
+ "pushl 32(%%eax)\n\t" \
+ "pushl 28(%%eax)\n\t" \
+ "pushl 24(%%eax)\n\t" \
+ "pushl 20(%%eax)\n\t" \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5, \
+ arg6,arg7,arg8,arg9,arg10, \
+ arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[12]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "subl $4, %%esp\n\t" \
+ "pushl 44(%%eax)\n\t" \
+ "pushl 40(%%eax)\n\t" \
+ "pushl 36(%%eax)\n\t" \
+ "pushl 32(%%eax)\n\t" \
+ "pushl 28(%%eax)\n\t" \
+ "pushl 24(%%eax)\n\t" \
+ "pushl 20(%%eax)\n\t" \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5, \
+ arg6,arg7,arg8,arg9,arg10, \
+ arg11,arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[13]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ _argvec[12] = (unsigned long)(arg12); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "pushl 48(%%eax)\n\t" \
+ "pushl 44(%%eax)\n\t" \
+ "pushl 40(%%eax)\n\t" \
+ "pushl 36(%%eax)\n\t" \
+ "pushl 32(%%eax)\n\t" \
+ "pushl 28(%%eax)\n\t" \
+ "pushl 24(%%eax)\n\t" \
+ "pushl 20(%%eax)\n\t" \
+ "pushl 16(%%eax)\n\t" \
+ "pushl 12(%%eax)\n\t" \
+ "pushl 8(%%eax)\n\t" \
+ "pushl 4(%%eax)\n\t" \
+ "movl (%%eax), %%eax\n\t" /* target->%eax */ \
+ VALGRIND_CALL_NOREDIR_EAX \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "edi" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#endif /* PLAT_x86_linux || PLAT_x86_darwin || PLAT_x86_solaris */
+
+/* ---------------- amd64-{linux,darwin,solaris} --------------- */
+
+#if defined(PLAT_amd64_linux) || defined(PLAT_amd64_darwin) \
+ || defined(PLAT_amd64_solaris)
+
+/* ARGREGS: rdi rsi rdx rcx r8 r9 (the rest on stack in R-to-L order) */
+
+/* These regs are trashed by the hidden call. */
+#define __CALLER_SAVED_REGS /*"rax",*/ "rcx", "rdx", "rsi", \
+ "rdi", "r8", "r9", "r10", "r11"
+
+/* This is all pretty complex. It's so as to make stack unwinding
+ work reliably. See bug 243270. The basic problem is the sub and
+ add of 128 of %rsp in all of the following macros. If gcc believes
+ the CFA is in %rsp, then unwinding may fail, because what's at the
+ CFA is not what gcc "expected" when it constructs the CFIs for the
+ places where the macros are instantiated.
+
+ But we can't just add a CFI annotation to increase the CFA offset
+ by 128, to match the sub of 128 from %rsp, because we don't know
+ whether gcc has chosen %rsp as the CFA at that point, or whether it
+ has chosen some other register (eg, %rbp). In the latter case,
+ adding a CFI annotation to change the CFA offset is simply wrong.
+
+ So the solution is to get hold of the CFA using
+ __builtin_dwarf_cfa(), put it in a known register, and add a
+ CFI annotation to say what the register is. We choose %rbp for
+ this (perhaps perversely), because:
+
+ (1) %rbp is already subject to unwinding. If a new register was
+ chosen then the unwinder would have to unwind it in all stack
+ traces, which is expensive, and
+
+ (2) %rbp is already subject to precise exception updates in the
+ JIT. If a new register was chosen, we'd have to have precise
+ exceptions for it too, which reduces performance of the
+ generated code.
+
+ However .. one extra complication. We can't just whack the result
+ of __builtin_dwarf_cfa() into %rbp and then add %rbp to the
+ list of trashed registers at the end of the inline assembly
+ fragments; gcc won't allow %rbp to appear in that list. Hence
+ instead we need to stash %rbp in %r15 for the duration of the asm,
+ and say that %r15 is trashed instead. gcc seems happy to go with
+ that.
+
+ Oh .. and this all needs to be conditionalised so that it is
+ unchanged from before this commit, when compiled with older gccs
+ that don't support __builtin_dwarf_cfa. Furthermore, since
+ this header file is freestanding, it has to be independent of
+ config.h, and so the following conditionalisation cannot depend on
+ configure time checks.
+
+ Although it's not clear from
+ 'defined(__GNUC__) && defined(__GCC_HAVE_DWARF2_CFI_ASM)',
+ this expression excludes Darwin.
+ .cfi directives in Darwin assembly appear to be completely
+ different and I haven't investigated how they work.
+
+ For even more entertainment value, note we have to use the
+ completely undocumented __builtin_dwarf_cfa(), which appears to
+ really compute the CFA, whereas __builtin_frame_address(0) claims
+ to but actually doesn't. See
+ https://bugs.kde.org/show_bug.cgi?id=243270#c47
+*/
+#if defined(__GNUC__) && defined(__GCC_HAVE_DWARF2_CFI_ASM)
+# define __FRAME_POINTER \
+ ,"r"(__builtin_dwarf_cfa())
+# define VALGRIND_CFI_PROLOGUE \
+ "movq %%rbp, %%r15\n\t" \
+ "movq %2, %%rbp\n\t" \
+ ".cfi_remember_state\n\t" \
+ ".cfi_def_cfa rbp, 0\n\t"
+# define VALGRIND_CFI_EPILOGUE \
+ "movq %%r15, %%rbp\n\t" \
+ ".cfi_restore_state\n\t"
+#else
+# define __FRAME_POINTER
+# define VALGRIND_CFI_PROLOGUE
+# define VALGRIND_CFI_EPILOGUE
+#endif
+
+/* Macros to save and align the stack before making a function
+ call and restore it afterwards as gcc may not keep the stack
+ pointer aligned if it doesn't realise calls are being made
+ to other functions. */
+
+#define VALGRIND_ALIGN_STACK \
+ "movq %%rsp,%%r14\n\t" \
+ "andq $0xfffffffffffffff0,%%rsp\n\t"
+#define VALGRIND_RESTORE_STACK \
+ "movq %%r14,%%rsp\n\t"
+
+/* These CALL_FN_ macros assume that on amd64-linux, sizeof(unsigned
+ long) == 8. */
+
+/* NB 9 Sept 07. There is a nasty kludge here in all these CALL_FN_
+ macros. In order not to trash the stack redzone, we need to drop
+ %rsp by 128 before the hidden call, and restore afterwards. The
+ nastiness is that it is only by luck that the stack still appears
+ to be unwindable during the hidden call - since then the behaviour
+ of any routine using this macro does not match what the CFI data
+ says. Sigh.
+
+ Why is this important? Imagine that a wrapper has a stack
+ allocated local, and passes to the hidden call, a pointer to it.
+ Because gcc does not know about the hidden call, it may allocate
+ that local in the redzone. Unfortunately the hidden call may then
+ trash it before it comes to use it. So we must step clear of the
+ redzone, for the duration of the hidden call, to make it safe.
+
+ Probably the same problem afflicts the other redzone-style ABIs too
+ (ppc64-linux); but for those, the stack is
+ self describing (none of this CFI nonsense) so at least messing
+ with the stack pointer doesn't give a danger of non-unwindable
+ stack. */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[1]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[2]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[4]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[5]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[6]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "movq 40(%%rax), %%r8\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[7]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "movq 48(%%rax), %%r9\n\t" \
+ "movq 40(%%rax), %%r8\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[8]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $136,%%rsp\n\t" \
+ "pushq 56(%%rax)\n\t" \
+ "movq 48(%%rax), %%r9\n\t" \
+ "movq 40(%%rax), %%r8\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[9]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "pushq 64(%%rax)\n\t" \
+ "pushq 56(%%rax)\n\t" \
+ "movq 48(%%rax), %%r9\n\t" \
+ "movq 40(%%rax), %%r8\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[10]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $136,%%rsp\n\t" \
+ "pushq 72(%%rax)\n\t" \
+ "pushq 64(%%rax)\n\t" \
+ "pushq 56(%%rax)\n\t" \
+ "movq 48(%%rax), %%r9\n\t" \
+ "movq 40(%%rax), %%r8\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[11]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "pushq 80(%%rax)\n\t" \
+ "pushq 72(%%rax)\n\t" \
+ "pushq 64(%%rax)\n\t" \
+ "pushq 56(%%rax)\n\t" \
+ "movq 48(%%rax), %%r9\n\t" \
+ "movq 40(%%rax), %%r8\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[12]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $136,%%rsp\n\t" \
+ "pushq 88(%%rax)\n\t" \
+ "pushq 80(%%rax)\n\t" \
+ "pushq 72(%%rax)\n\t" \
+ "pushq 64(%%rax)\n\t" \
+ "pushq 56(%%rax)\n\t" \
+ "movq 48(%%rax), %%r9\n\t" \
+ "movq 40(%%rax), %%r8\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11,arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[13]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ _argvec[12] = (unsigned long)(arg12); \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ VALGRIND_ALIGN_STACK \
+ "subq $128,%%rsp\n\t" \
+ "pushq 96(%%rax)\n\t" \
+ "pushq 88(%%rax)\n\t" \
+ "pushq 80(%%rax)\n\t" \
+ "pushq 72(%%rax)\n\t" \
+ "pushq 64(%%rax)\n\t" \
+ "pushq 56(%%rax)\n\t" \
+ "movq 48(%%rax), %%r9\n\t" \
+ "movq 40(%%rax), %%r8\n\t" \
+ "movq 32(%%rax), %%rcx\n\t" \
+ "movq 24(%%rax), %%rdx\n\t" \
+ "movq 16(%%rax), %%rsi\n\t" \
+ "movq 8(%%rax), %%rdi\n\t" \
+ "movq (%%rax), %%rax\n\t" /* target->%rax */ \
+ VALGRIND_CALL_NOREDIR_RAX \
+ VALGRIND_RESTORE_STACK \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=a" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r14", "r15" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#endif /* PLAT_amd64_linux || PLAT_amd64_darwin || PLAT_amd64_solaris */
+
+/* ------------------------ ppc32-linux ------------------------ */
+
+#if defined(PLAT_ppc32_linux)
+
+/* This is useful for finding out about the on-stack stuff:
+
+ extern int f9 ( int,int,int,int,int,int,int,int,int );
+ extern int f10 ( int,int,int,int,int,int,int,int,int,int );
+ extern int f11 ( int,int,int,int,int,int,int,int,int,int,int );
+ extern int f12 ( int,int,int,int,int,int,int,int,int,int,int,int );
+
+ int g9 ( void ) {
+ return f9(11,22,33,44,55,66,77,88,99);
+ }
+ int g10 ( void ) {
+ return f10(11,22,33,44,55,66,77,88,99,110);
+ }
+ int g11 ( void ) {
+ return f11(11,22,33,44,55,66,77,88,99,110,121);
+ }
+ int g12 ( void ) {
+ return f12(11,22,33,44,55,66,77,88,99,110,121,132);
+ }
+*/
+
+/* ARGREGS: r3 r4 r5 r6 r7 r8 r9 r10 (the rest on stack somewhere) */
+
+/* These regs are trashed by the hidden call. */
+#define __CALLER_SAVED_REGS \
+ "lr", "ctr", "xer", \
+ "cr0", "cr1", "cr2", "cr3", "cr4", "cr5", "cr6", "cr7", \
+ "r0", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", \
+ "r11", "r12", "r13"
+
+/* Macros to save and align the stack before making a function
+ call and restore it afterwards as gcc may not keep the stack
+ pointer aligned if it doesn't realise calls are being made
+ to other functions. */
+
+#define VALGRIND_ALIGN_STACK \
+ "mr 28,1\n\t" \
+ "rlwinm 1,1,0,0,27\n\t"
+#define VALGRIND_RESTORE_STACK \
+ "mr 1,28\n\t"
+
+/* These CALL_FN_ macros assume that on ppc32-linux,
+ sizeof(unsigned long) == 4. */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[1]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[2]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[4]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[5]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[6]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 7,20(11)\n\t" \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[7]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 7,20(11)\n\t" \
+ "lwz 8,24(11)\n\t" \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[8]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 7,20(11)\n\t" \
+ "lwz 8,24(11)\n\t" \
+ "lwz 9,28(11)\n\t" \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[9]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 7,20(11)\n\t" \
+ "lwz 8,24(11)\n\t" \
+ "lwz 9,28(11)\n\t" \
+ "lwz 10,32(11)\n\t" /* arg8->r10 */ \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[10]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ _argvec[9] = (unsigned long)arg9; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "addi 1,1,-16\n\t" \
+ /* arg9 */ \
+ "lwz 3,36(11)\n\t" \
+ "stw 3,8(1)\n\t" \
+ /* args1-8 */ \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 7,20(11)\n\t" \
+ "lwz 8,24(11)\n\t" \
+ "lwz 9,28(11)\n\t" \
+ "lwz 10,32(11)\n\t" /* arg8->r10 */ \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[11]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ _argvec[9] = (unsigned long)arg9; \
+ _argvec[10] = (unsigned long)arg10; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "addi 1,1,-16\n\t" \
+ /* arg10 */ \
+ "lwz 3,40(11)\n\t" \
+ "stw 3,12(1)\n\t" \
+ /* arg9 */ \
+ "lwz 3,36(11)\n\t" \
+ "stw 3,8(1)\n\t" \
+ /* args1-8 */ \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 7,20(11)\n\t" \
+ "lwz 8,24(11)\n\t" \
+ "lwz 9,28(11)\n\t" \
+ "lwz 10,32(11)\n\t" /* arg8->r10 */ \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[12]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ _argvec[9] = (unsigned long)arg9; \
+ _argvec[10] = (unsigned long)arg10; \
+ _argvec[11] = (unsigned long)arg11; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "addi 1,1,-32\n\t" \
+ /* arg11 */ \
+ "lwz 3,44(11)\n\t" \
+ "stw 3,16(1)\n\t" \
+ /* arg10 */ \
+ "lwz 3,40(11)\n\t" \
+ "stw 3,12(1)\n\t" \
+ /* arg9 */ \
+ "lwz 3,36(11)\n\t" \
+ "stw 3,8(1)\n\t" \
+ /* args1-8 */ \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 7,20(11)\n\t" \
+ "lwz 8,24(11)\n\t" \
+ "lwz 9,28(11)\n\t" \
+ "lwz 10,32(11)\n\t" /* arg8->r10 */ \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11,arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[13]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ _argvec[9] = (unsigned long)arg9; \
+ _argvec[10] = (unsigned long)arg10; \
+ _argvec[11] = (unsigned long)arg11; \
+ _argvec[12] = (unsigned long)arg12; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "addi 1,1,-32\n\t" \
+ /* arg12 */ \
+ "lwz 3,48(11)\n\t" \
+ "stw 3,20(1)\n\t" \
+ /* arg11 */ \
+ "lwz 3,44(11)\n\t" \
+ "stw 3,16(1)\n\t" \
+ /* arg10 */ \
+ "lwz 3,40(11)\n\t" \
+ "stw 3,12(1)\n\t" \
+ /* arg9 */ \
+ "lwz 3,36(11)\n\t" \
+ "stw 3,8(1)\n\t" \
+ /* args1-8 */ \
+ "lwz 3,4(11)\n\t" /* arg1->r3 */ \
+ "lwz 4,8(11)\n\t" \
+ "lwz 5,12(11)\n\t" \
+ "lwz 6,16(11)\n\t" /* arg4->r6 */ \
+ "lwz 7,20(11)\n\t" \
+ "lwz 8,24(11)\n\t" \
+ "lwz 9,28(11)\n\t" \
+ "lwz 10,32(11)\n\t" /* arg8->r10 */ \
+ "lwz 11,0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ VALGRIND_RESTORE_STACK \
+ "mr %0,3" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#endif /* PLAT_ppc32_linux */
+
+/* ------------------------ ppc64-linux ------------------------ */
+
+#if defined(PLAT_ppc64be_linux)
+
+/* ARGREGS: r3 r4 r5 r6 r7 r8 r9 r10 (the rest on stack somewhere) */
+
+/* These regs are trashed by the hidden call. */
+#define __CALLER_SAVED_REGS \
+ "lr", "ctr", "xer", \
+ "cr0", "cr1", "cr2", "cr3", "cr4", "cr5", "cr6", "cr7", \
+ "r0", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", \
+ "r11", "r12", "r13"
+
+/* Macros to save and align the stack before making a function
+ call and restore it afterwards as gcc may not keep the stack
+ pointer aligned if it doesn't realise calls are being made
+ to other functions. */
+
+#define VALGRIND_ALIGN_STACK \
+ "mr 28,1\n\t" \
+ "rldicr 1,1,0,59\n\t"
+#define VALGRIND_RESTORE_STACK \
+ "mr 1,28\n\t"
+
+/* These CALL_FN_ macros assume that on ppc64-linux, sizeof(unsigned
+ long) == 8. */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+0]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+1]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+2]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+3]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+4]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+5]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(11)\n\t" /* arg5->r7 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+6]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(11)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(11)\n\t" /* arg6->r8 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+7]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(11)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(11)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(11)\n\t" /* arg7->r9 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+8]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(11)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(11)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(11)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(11)\n\t" /* arg8->r10 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+9]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ _argvec[2+9] = (unsigned long)arg9; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "addi 1,1,-128\n\t" /* expand stack frame */ \
+ /* arg9 */ \
+ "ld 3,72(11)\n\t" \
+ "std 3,112(1)\n\t" \
+ /* args1-8 */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(11)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(11)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(11)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(11)\n\t" /* arg8->r10 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+10]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ _argvec[2+9] = (unsigned long)arg9; \
+ _argvec[2+10] = (unsigned long)arg10; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "addi 1,1,-128\n\t" /* expand stack frame */ \
+ /* arg10 */ \
+ "ld 3,80(11)\n\t" \
+ "std 3,120(1)\n\t" \
+ /* arg9 */ \
+ "ld 3,72(11)\n\t" \
+ "std 3,112(1)\n\t" \
+ /* args1-8 */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(11)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(11)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(11)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(11)\n\t" /* arg8->r10 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+11]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ _argvec[2+9] = (unsigned long)arg9; \
+ _argvec[2+10] = (unsigned long)arg10; \
+ _argvec[2+11] = (unsigned long)arg11; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "addi 1,1,-144\n\t" /* expand stack frame */ \
+ /* arg11 */ \
+ "ld 3,88(11)\n\t" \
+ "std 3,128(1)\n\t" \
+ /* arg10 */ \
+ "ld 3,80(11)\n\t" \
+ "std 3,120(1)\n\t" \
+ /* arg9 */ \
+ "ld 3,72(11)\n\t" \
+ "std 3,112(1)\n\t" \
+ /* args1-8 */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(11)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(11)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(11)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(11)\n\t" /* arg8->r10 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11,arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+12]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ _argvec[2+9] = (unsigned long)arg9; \
+ _argvec[2+10] = (unsigned long)arg10; \
+ _argvec[2+11] = (unsigned long)arg11; \
+ _argvec[2+12] = (unsigned long)arg12; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 11,%1\n\t" \
+ "std 2,-16(11)\n\t" /* save tocptr */ \
+ "ld 2,-8(11)\n\t" /* use nraddr's tocptr */ \
+ "addi 1,1,-144\n\t" /* expand stack frame */ \
+ /* arg12 */ \
+ "ld 3,96(11)\n\t" \
+ "std 3,136(1)\n\t" \
+ /* arg11 */ \
+ "ld 3,88(11)\n\t" \
+ "std 3,128(1)\n\t" \
+ /* arg10 */ \
+ "ld 3,80(11)\n\t" \
+ "std 3,120(1)\n\t" \
+ /* arg9 */ \
+ "ld 3,72(11)\n\t" \
+ "std 3,112(1)\n\t" \
+ /* args1-8 */ \
+ "ld 3, 8(11)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(11)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(11)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(11)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(11)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(11)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(11)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(11)\n\t" /* arg8->r10 */ \
+ "ld 11, 0(11)\n\t" /* target->r11 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R11 \
+ "mr 11,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(11)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#endif /* PLAT_ppc64be_linux */
+
+/* ------------------------- ppc64le-linux ----------------------- */
+#if defined(PLAT_ppc64le_linux)
+
+/* ARGREGS: r3 r4 r5 r6 r7 r8 r9 r10 (the rest on stack somewhere) */
+
+/* These regs are trashed by the hidden call. */
+#define __CALLER_SAVED_REGS \
+ "lr", "ctr", "xer", \
+ "cr0", "cr1", "cr2", "cr3", "cr4", "cr5", "cr6", "cr7", \
+ "r0", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", \
+ "r11", "r12", "r13"
+
+/* Macros to save and align the stack before making a function
+ call and restore it afterwards as gcc may not keep the stack
+ pointer aligned if it doesn't realise calls are being made
+ to other functions. */
+
+#define VALGRIND_ALIGN_STACK \
+ "mr 28,1\n\t" \
+ "rldicr 1,1,0,59\n\t"
+#define VALGRIND_RESTORE_STACK \
+ "mr 1,28\n\t"
+
+/* These CALL_FN_ macros assume that on ppc64-linux, sizeof(unsigned
+ long) == 8. */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+0]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+1]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+2]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+3]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+4]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+5]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(12)\n\t" /* arg5->r7 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+6]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(12)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(12)\n\t" /* arg6->r8 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+7]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(12)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(12)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(12)\n\t" /* arg7->r9 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+8]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(12)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(12)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(12)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(12)\n\t" /* arg8->r10 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+9]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ _argvec[2+9] = (unsigned long)arg9; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "addi 1,1,-128\n\t" /* expand stack frame */ \
+ /* arg9 */ \
+ "ld 3,72(12)\n\t" \
+ "std 3,96(1)\n\t" \
+ /* args1-8 */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(12)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(12)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(12)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(12)\n\t" /* arg8->r10 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+10]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ _argvec[2+9] = (unsigned long)arg9; \
+ _argvec[2+10] = (unsigned long)arg10; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "addi 1,1,-128\n\t" /* expand stack frame */ \
+ /* arg10 */ \
+ "ld 3,80(12)\n\t" \
+ "std 3,104(1)\n\t" \
+ /* arg9 */ \
+ "ld 3,72(12)\n\t" \
+ "std 3,96(1)\n\t" \
+ /* args1-8 */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(12)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(12)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(12)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(12)\n\t" /* arg8->r10 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+11]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ _argvec[2+9] = (unsigned long)arg9; \
+ _argvec[2+10] = (unsigned long)arg10; \
+ _argvec[2+11] = (unsigned long)arg11; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "addi 1,1,-144\n\t" /* expand stack frame */ \
+ /* arg11 */ \
+ "ld 3,88(12)\n\t" \
+ "std 3,112(1)\n\t" \
+ /* arg10 */ \
+ "ld 3,80(12)\n\t" \
+ "std 3,104(1)\n\t" \
+ /* arg9 */ \
+ "ld 3,72(12)\n\t" \
+ "std 3,96(1)\n\t" \
+ /* args1-8 */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(12)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(12)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(12)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(12)\n\t" /* arg8->r10 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11,arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3+12]; \
+ volatile unsigned long _res; \
+ /* _argvec[0] holds current r2 across the call */ \
+ _argvec[1] = (unsigned long)_orig.r2; \
+ _argvec[2] = (unsigned long)_orig.nraddr; \
+ _argvec[2+1] = (unsigned long)arg1; \
+ _argvec[2+2] = (unsigned long)arg2; \
+ _argvec[2+3] = (unsigned long)arg3; \
+ _argvec[2+4] = (unsigned long)arg4; \
+ _argvec[2+5] = (unsigned long)arg5; \
+ _argvec[2+6] = (unsigned long)arg6; \
+ _argvec[2+7] = (unsigned long)arg7; \
+ _argvec[2+8] = (unsigned long)arg8; \
+ _argvec[2+9] = (unsigned long)arg9; \
+ _argvec[2+10] = (unsigned long)arg10; \
+ _argvec[2+11] = (unsigned long)arg11; \
+ _argvec[2+12] = (unsigned long)arg12; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "mr 12,%1\n\t" \
+ "std 2,-16(12)\n\t" /* save tocptr */ \
+ "ld 2,-8(12)\n\t" /* use nraddr's tocptr */ \
+ "addi 1,1,-144\n\t" /* expand stack frame */ \
+ /* arg12 */ \
+ "ld 3,96(12)\n\t" \
+ "std 3,120(1)\n\t" \
+ /* arg11 */ \
+ "ld 3,88(12)\n\t" \
+ "std 3,112(1)\n\t" \
+ /* arg10 */ \
+ "ld 3,80(12)\n\t" \
+ "std 3,104(1)\n\t" \
+ /* arg9 */ \
+ "ld 3,72(12)\n\t" \
+ "std 3,96(1)\n\t" \
+ /* args1-8 */ \
+ "ld 3, 8(12)\n\t" /* arg1->r3 */ \
+ "ld 4, 16(12)\n\t" /* arg2->r4 */ \
+ "ld 5, 24(12)\n\t" /* arg3->r5 */ \
+ "ld 6, 32(12)\n\t" /* arg4->r6 */ \
+ "ld 7, 40(12)\n\t" /* arg5->r7 */ \
+ "ld 8, 48(12)\n\t" /* arg6->r8 */ \
+ "ld 9, 56(12)\n\t" /* arg7->r9 */ \
+ "ld 10, 64(12)\n\t" /* arg8->r10 */ \
+ "ld 12, 0(12)\n\t" /* target->r12 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R12 \
+ "mr 12,%1\n\t" \
+ "mr %0,3\n\t" \
+ "ld 2,-16(12)\n\t" /* restore tocptr */ \
+ VALGRIND_RESTORE_STACK \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[2]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r28" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#endif /* PLAT_ppc64le_linux */
+
+/* ------------------------- arm-linux ------------------------- */
+
+#if defined(PLAT_arm_linux)
+
+/* These regs are trashed by the hidden call. */
+#define __CALLER_SAVED_REGS "r0", "r1", "r2", "r3","r4", "r12", "r14"
+
+/* Macros to save and align the stack before making a function
+ call and restore it afterwards as gcc may not keep the stack
+ pointer aligned if it doesn't realise calls are being made
+ to other functions. */
+
+/* This is a bit tricky. We store the original stack pointer in r10
+ as it is callee-saves. gcc doesn't allow the use of r11 for some
+ reason. Also, we can't directly "bic" the stack pointer in thumb
+ mode since r13 isn't an allowed register number in that context.
+ So use r4 as a temporary, since that is about to get trashed
+ anyway, just after each use of this macro. Side effect is we need
+ to be very careful about any future changes, since
+ VALGRIND_ALIGN_STACK simply assumes r4 is usable. */
+#define VALGRIND_ALIGN_STACK \
+ "mov r10, sp\n\t" \
+ "mov r4, sp\n\t" \
+ "bic r4, r4, #7\n\t" \
+ "mov sp, r4\n\t"
+#define VALGRIND_RESTORE_STACK \
+ "mov sp, r10\n\t"
+
+/* These CALL_FN_ macros assume that on arm-linux, sizeof(unsigned
+ long) == 4. */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[1]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[2]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[4]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[5]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[6]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "sub sp, sp, #4 \n\t" \
+ "ldr r0, [%1, #20] \n\t" \
+ "push {r0} \n\t" \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[7]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r0, [%1, #20] \n\t" \
+ "ldr r1, [%1, #24] \n\t" \
+ "push {r0, r1} \n\t" \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[8]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "sub sp, sp, #4 \n\t" \
+ "ldr r0, [%1, #20] \n\t" \
+ "ldr r1, [%1, #24] \n\t" \
+ "ldr r2, [%1, #28] \n\t" \
+ "push {r0, r1, r2} \n\t" \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[9]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r0, [%1, #20] \n\t" \
+ "ldr r1, [%1, #24] \n\t" \
+ "ldr r2, [%1, #28] \n\t" \
+ "ldr r3, [%1, #32] \n\t" \
+ "push {r0, r1, r2, r3} \n\t" \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[10]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "sub sp, sp, #4 \n\t" \
+ "ldr r0, [%1, #20] \n\t" \
+ "ldr r1, [%1, #24] \n\t" \
+ "ldr r2, [%1, #28] \n\t" \
+ "ldr r3, [%1, #32] \n\t" \
+ "ldr r4, [%1, #36] \n\t" \
+ "push {r0, r1, r2, r3, r4} \n\t" \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[11]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r0, [%1, #40] \n\t" \
+ "push {r0} \n\t" \
+ "ldr r0, [%1, #20] \n\t" \
+ "ldr r1, [%1, #24] \n\t" \
+ "ldr r2, [%1, #28] \n\t" \
+ "ldr r3, [%1, #32] \n\t" \
+ "ldr r4, [%1, #36] \n\t" \
+ "push {r0, r1, r2, r3, r4} \n\t" \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5, \
+ arg6,arg7,arg8,arg9,arg10, \
+ arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[12]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "sub sp, sp, #4 \n\t" \
+ "ldr r0, [%1, #40] \n\t" \
+ "ldr r1, [%1, #44] \n\t" \
+ "push {r0, r1} \n\t" \
+ "ldr r0, [%1, #20] \n\t" \
+ "ldr r1, [%1, #24] \n\t" \
+ "ldr r2, [%1, #28] \n\t" \
+ "ldr r3, [%1, #32] \n\t" \
+ "ldr r4, [%1, #36] \n\t" \
+ "push {r0, r1, r2, r3, r4} \n\t" \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5, \
+ arg6,arg7,arg8,arg9,arg10, \
+ arg11,arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[13]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ _argvec[12] = (unsigned long)(arg12); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr r0, [%1, #40] \n\t" \
+ "ldr r1, [%1, #44] \n\t" \
+ "ldr r2, [%1, #48] \n\t" \
+ "push {r0, r1, r2} \n\t" \
+ "ldr r0, [%1, #20] \n\t" \
+ "ldr r1, [%1, #24] \n\t" \
+ "ldr r2, [%1, #28] \n\t" \
+ "ldr r3, [%1, #32] \n\t" \
+ "ldr r4, [%1, #36] \n\t" \
+ "push {r0, r1, r2, r3, r4} \n\t" \
+ "ldr r0, [%1, #4] \n\t" \
+ "ldr r1, [%1, #8] \n\t" \
+ "ldr r2, [%1, #12] \n\t" \
+ "ldr r3, [%1, #16] \n\t" \
+ "ldr r4, [%1] \n\t" /* target->r4 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_R4 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, r0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "r10" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#endif /* PLAT_arm_linux */
+
+/* ------------------------ arm64-linux ------------------------ */
+
+#if defined(PLAT_arm64_linux)
+
+/* These regs are trashed by the hidden call. */
+#define __CALLER_SAVED_REGS \
+ "x0", "x1", "x2", "x3","x4", "x5", "x6", "x7", "x8", "x9", \
+ "x10", "x11", "x12", "x13", "x14", "x15", "x16", "x17", \
+ "x18", "x19", "x20", "x30", \
+ "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", \
+ "v10", "v11", "v12", "v13", "v14", "v15", "v16", "v17", \
+ "v18", "v19", "v20", "v21", "v22", "v23", "v24", "v25", \
+ "v26", "v27", "v28", "v29", "v30", "v31"
+
+/* x21 is callee-saved, so we can use it to save and restore SP around
+ the hidden call. */
+#define VALGRIND_ALIGN_STACK \
+ "mov x21, sp\n\t" \
+ "bic sp, x21, #15\n\t"
+#define VALGRIND_RESTORE_STACK \
+ "mov sp, x21\n\t"
+
+/* These CALL_FN_ macros assume that on arm64-linux,
+ sizeof(unsigned long) == 8. */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[1]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[2]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[4]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[5]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[6]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x4, [%1, #40] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[7]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x4, [%1, #40] \n\t" \
+ "ldr x5, [%1, #48] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[8]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x4, [%1, #40] \n\t" \
+ "ldr x5, [%1, #48] \n\t" \
+ "ldr x6, [%1, #56] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[9]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x4, [%1, #40] \n\t" \
+ "ldr x5, [%1, #48] \n\t" \
+ "ldr x6, [%1, #56] \n\t" \
+ "ldr x7, [%1, #64] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[10]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "sub sp, sp, #0x20 \n\t" \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x4, [%1, #40] \n\t" \
+ "ldr x5, [%1, #48] \n\t" \
+ "ldr x6, [%1, #56] \n\t" \
+ "ldr x7, [%1, #64] \n\t" \
+ "ldr x8, [%1, #72] \n\t" \
+ "str x8, [sp, #0] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[11]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "sub sp, sp, #0x20 \n\t" \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x4, [%1, #40] \n\t" \
+ "ldr x5, [%1, #48] \n\t" \
+ "ldr x6, [%1, #56] \n\t" \
+ "ldr x7, [%1, #64] \n\t" \
+ "ldr x8, [%1, #72] \n\t" \
+ "str x8, [sp, #0] \n\t" \
+ "ldr x8, [%1, #80] \n\t" \
+ "str x8, [sp, #8] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[12]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "sub sp, sp, #0x30 \n\t" \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x4, [%1, #40] \n\t" \
+ "ldr x5, [%1, #48] \n\t" \
+ "ldr x6, [%1, #56] \n\t" \
+ "ldr x7, [%1, #64] \n\t" \
+ "ldr x8, [%1, #72] \n\t" \
+ "str x8, [sp, #0] \n\t" \
+ "ldr x8, [%1, #80] \n\t" \
+ "str x8, [sp, #8] \n\t" \
+ "ldr x8, [%1, #88] \n\t" \
+ "str x8, [sp, #16] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10,arg11, \
+ arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[13]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ _argvec[12] = (unsigned long)(arg12); \
+ __asm__ volatile( \
+ VALGRIND_ALIGN_STACK \
+ "sub sp, sp, #0x30 \n\t" \
+ "ldr x0, [%1, #8] \n\t" \
+ "ldr x1, [%1, #16] \n\t" \
+ "ldr x2, [%1, #24] \n\t" \
+ "ldr x3, [%1, #32] \n\t" \
+ "ldr x4, [%1, #40] \n\t" \
+ "ldr x5, [%1, #48] \n\t" \
+ "ldr x6, [%1, #56] \n\t" \
+ "ldr x7, [%1, #64] \n\t" \
+ "ldr x8, [%1, #72] \n\t" \
+ "str x8, [sp, #0] \n\t" \
+ "ldr x8, [%1, #80] \n\t" \
+ "str x8, [sp, #8] \n\t" \
+ "ldr x8, [%1, #88] \n\t" \
+ "str x8, [sp, #16] \n\t" \
+ "ldr x8, [%1, #96] \n\t" \
+ "str x8, [sp, #24] \n\t" \
+ "ldr x8, [%1] \n\t" /* target->x8 */ \
+ VALGRIND_BRANCH_AND_LINK_TO_NOREDIR_X8 \
+ VALGRIND_RESTORE_STACK \
+ "mov %0, x0" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS, "x21" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#endif /* PLAT_arm64_linux */
+
+/* ------------------------- s390x-linux ------------------------- */
+
+#if defined(PLAT_s390x_linux)
+
+/* Similar workaround as amd64 (see above), but we use r11 as frame
+ pointer and save the old r11 in r7. r11 might be used for
+ argvec, therefore we copy argvec in r1 since r1 is clobbered
+ after the call anyway. */
+#if defined(__GNUC__) && defined(__GCC_HAVE_DWARF2_CFI_ASM)
+# define __FRAME_POINTER \
+ ,"d"(__builtin_dwarf_cfa())
+# define VALGRIND_CFI_PROLOGUE \
+ ".cfi_remember_state\n\t" \
+ "lgr 1,%1\n\t" /* copy the argvec pointer in r1 */ \
+ "lgr 7,11\n\t" \
+ "lgr 11,%2\n\t" \
+ ".cfi_def_cfa r11, 0\n\t"
+# define VALGRIND_CFI_EPILOGUE \
+ "lgr 11, 7\n\t" \
+ ".cfi_restore_state\n\t"
+#else
+# define __FRAME_POINTER
+# define VALGRIND_CFI_PROLOGUE \
+ "lgr 1,%1\n\t"
+# define VALGRIND_CFI_EPILOGUE
+#endif
+
+/* Nb: On s390 the stack pointer is properly aligned *at all times*
+ according to the s390 GCC maintainer. (The ABI specification is not
+ precise in this regard.) Therefore, VALGRIND_ALIGN_STACK and
+ VALGRIND_RESTORE_STACK are not defined here. */
+
+/* These regs are trashed by the hidden call. Note that we overwrite
+ r14 in s390_irgen_noredir (VEX/priv/guest_s390_irgen.c) to give the
+ function a proper return address. All others are ABI defined call
+ clobbers. */
+#define __CALLER_SAVED_REGS "0","1","2","3","4","5","14", \
+ "f0","f1","f2","f3","f4","f5","f6","f7"
+
+/* Nb: Although r11 is modified in the asm snippets below (inside
+ VALGRIND_CFI_PROLOGUE) it is not listed in the clobber section, for
+ two reasons:
+ (1) r11 is restored in VALGRIND_CFI_EPILOGUE, so effectively it is not
+ modified
+ (2) GCC will complain that r11 cannot appear inside a clobber section,
+ when compiled with -O -fno-omit-frame-pointer
+ */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[1]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-160\n\t" \
+ "lg 1, 0(1)\n\t" /* target->r1 */ \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,160\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "d" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+/* The call abi has the arguments in r2-r6 and stack */
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[2]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-160\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,160\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1, arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-160\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,160\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1, arg2, arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[4]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-160\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,160\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1, arg2, arg3, arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[5]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-160\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,160\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1, arg2, arg3, arg4, arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[6]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-160\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 6,40(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,160\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"6","7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1, arg2, arg3, arg4, arg5, \
+ arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[7]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-168\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 6,40(1)\n\t" \
+ "mvc 160(8,15), 48(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,168\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"6","7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1, arg2, arg3, arg4, arg5, \
+ arg6, arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[8]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-176\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 6,40(1)\n\t" \
+ "mvc 160(8,15), 48(1)\n\t" \
+ "mvc 168(8,15), 56(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,176\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"6","7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1, arg2, arg3, arg4, arg5, \
+ arg6, arg7 ,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[9]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-184\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 6,40(1)\n\t" \
+ "mvc 160(8,15), 48(1)\n\t" \
+ "mvc 168(8,15), 56(1)\n\t" \
+ "mvc 176(8,15), 64(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,184\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"6","7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1, arg2, arg3, arg4, arg5, \
+ arg6, arg7 ,arg8, arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[10]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ _argvec[9] = (unsigned long)arg9; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-192\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 6,40(1)\n\t" \
+ "mvc 160(8,15), 48(1)\n\t" \
+ "mvc 168(8,15), 56(1)\n\t" \
+ "mvc 176(8,15), 64(1)\n\t" \
+ "mvc 184(8,15), 72(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,192\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"6","7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1, arg2, arg3, arg4, arg5, \
+ arg6, arg7 ,arg8, arg9, arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[11]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ _argvec[9] = (unsigned long)arg9; \
+ _argvec[10] = (unsigned long)arg10; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-200\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 6,40(1)\n\t" \
+ "mvc 160(8,15), 48(1)\n\t" \
+ "mvc 168(8,15), 56(1)\n\t" \
+ "mvc 176(8,15), 64(1)\n\t" \
+ "mvc 184(8,15), 72(1)\n\t" \
+ "mvc 192(8,15), 80(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,200\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"6","7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1, arg2, arg3, arg4, arg5, \
+ arg6, arg7 ,arg8, arg9, arg10, arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[12]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ _argvec[9] = (unsigned long)arg9; \
+ _argvec[10] = (unsigned long)arg10; \
+ _argvec[11] = (unsigned long)arg11; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-208\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 6,40(1)\n\t" \
+ "mvc 160(8,15), 48(1)\n\t" \
+ "mvc 168(8,15), 56(1)\n\t" \
+ "mvc 176(8,15), 64(1)\n\t" \
+ "mvc 184(8,15), 72(1)\n\t" \
+ "mvc 192(8,15), 80(1)\n\t" \
+ "mvc 200(8,15), 88(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,208\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"6","7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1, arg2, arg3, arg4, arg5, \
+ arg6, arg7 ,arg8, arg9, arg10, arg11, arg12)\
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[13]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)arg1; \
+ _argvec[2] = (unsigned long)arg2; \
+ _argvec[3] = (unsigned long)arg3; \
+ _argvec[4] = (unsigned long)arg4; \
+ _argvec[5] = (unsigned long)arg5; \
+ _argvec[6] = (unsigned long)arg6; \
+ _argvec[7] = (unsigned long)arg7; \
+ _argvec[8] = (unsigned long)arg8; \
+ _argvec[9] = (unsigned long)arg9; \
+ _argvec[10] = (unsigned long)arg10; \
+ _argvec[11] = (unsigned long)arg11; \
+ _argvec[12] = (unsigned long)arg12; \
+ __asm__ volatile( \
+ VALGRIND_CFI_PROLOGUE \
+ "aghi 15,-216\n\t" \
+ "lg 2, 8(1)\n\t" \
+ "lg 3,16(1)\n\t" \
+ "lg 4,24(1)\n\t" \
+ "lg 5,32(1)\n\t" \
+ "lg 6,40(1)\n\t" \
+ "mvc 160(8,15), 48(1)\n\t" \
+ "mvc 168(8,15), 56(1)\n\t" \
+ "mvc 176(8,15), 64(1)\n\t" \
+ "mvc 184(8,15), 72(1)\n\t" \
+ "mvc 192(8,15), 80(1)\n\t" \
+ "mvc 200(8,15), 88(1)\n\t" \
+ "mvc 208(8,15), 96(1)\n\t" \
+ "lg 1, 0(1)\n\t" \
+ VALGRIND_CALL_NOREDIR_R1 \
+ "lgr %0, 2\n\t" \
+ "aghi 15,216\n\t" \
+ VALGRIND_CFI_EPILOGUE \
+ : /*out*/ "=d" (_res) \
+ : /*in*/ "a" (&_argvec[0]) __FRAME_POINTER \
+ : /*trash*/ "cc", "memory", __CALLER_SAVED_REGS,"6","7" \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+
+#endif /* PLAT_s390x_linux */
+
+/* ------------------------- mips32-linux ----------------------- */
+
+#if defined(PLAT_mips32_linux)
+
+/* These regs are trashed by the hidden call. */
+#define __CALLER_SAVED_REGS "$2", "$3", "$4", "$5", "$6", \
+"$7", "$8", "$9", "$10", "$11", "$12", "$13", "$14", "$15", "$24", \
+"$25", "$31"
+
+/* These CALL_FN_ macros assume that on mips-linux, sizeof(unsigned
+ long) == 4. */
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[1]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "subu $29, $29, 16 \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 16\n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[2]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "subu $29, $29, 16 \n\t" \
+ "lw $4, 4(%1) \n\t" /* arg1*/ \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 16 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[3]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "subu $29, $29, 16 \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 16 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[4]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "subu $29, $29, 16 \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 16 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[5]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "subu $29, $29, 16 \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 16 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[6]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "lw $4, 20(%1) \n\t" \
+ "subu $29, $29, 24\n\t" \
+ "sw $4, 16($29) \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 24 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[7]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "lw $4, 20(%1) \n\t" \
+ "subu $29, $29, 32\n\t" \
+ "sw $4, 16($29) \n\t" \
+ "lw $4, 24(%1) \n\t" \
+ "nop\n\t" \
+ "sw $4, 20($29) \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 32 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[8]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "lw $4, 20(%1) \n\t" \
+ "subu $29, $29, 32\n\t" \
+ "sw $4, 16($29) \n\t" \
+ "lw $4, 24(%1) \n\t" \
+ "sw $4, 20($29) \n\t" \
+ "lw $4, 28(%1) \n\t" \
+ "sw $4, 24($29) \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 32 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[9]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "lw $4, 20(%1) \n\t" \
+ "subu $29, $29, 40\n\t" \
+ "sw $4, 16($29) \n\t" \
+ "lw $4, 24(%1) \n\t" \
+ "sw $4, 20($29) \n\t" \
+ "lw $4, 28(%1) \n\t" \
+ "sw $4, 24($29) \n\t" \
+ "lw $4, 32(%1) \n\t" \
+ "sw $4, 28($29) \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 40 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[10]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "lw $4, 20(%1) \n\t" \
+ "subu $29, $29, 40\n\t" \
+ "sw $4, 16($29) \n\t" \
+ "lw $4, 24(%1) \n\t" \
+ "sw $4, 20($29) \n\t" \
+ "lw $4, 28(%1) \n\t" \
+ "sw $4, 24($29) \n\t" \
+ "lw $4, 32(%1) \n\t" \
+ "sw $4, 28($29) \n\t" \
+ "lw $4, 36(%1) \n\t" \
+ "sw $4, 32($29) \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 40 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[11]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "lw $4, 20(%1) \n\t" \
+ "subu $29, $29, 48\n\t" \
+ "sw $4, 16($29) \n\t" \
+ "lw $4, 24(%1) \n\t" \
+ "sw $4, 20($29) \n\t" \
+ "lw $4, 28(%1) \n\t" \
+ "sw $4, 24($29) \n\t" \
+ "lw $4, 32(%1) \n\t" \
+ "sw $4, 28($29) \n\t" \
+ "lw $4, 36(%1) \n\t" \
+ "sw $4, 32($29) \n\t" \
+ "lw $4, 40(%1) \n\t" \
+ "sw $4, 36($29) \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 48 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5, \
+ arg6,arg7,arg8,arg9,arg10, \
+ arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[12]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "lw $4, 20(%1) \n\t" \
+ "subu $29, $29, 48\n\t" \
+ "sw $4, 16($29) \n\t" \
+ "lw $4, 24(%1) \n\t" \
+ "sw $4, 20($29) \n\t" \
+ "lw $4, 28(%1) \n\t" \
+ "sw $4, 24($29) \n\t" \
+ "lw $4, 32(%1) \n\t" \
+ "sw $4, 28($29) \n\t" \
+ "lw $4, 36(%1) \n\t" \
+ "sw $4, 32($29) \n\t" \
+ "lw $4, 40(%1) \n\t" \
+ "sw $4, 36($29) \n\t" \
+ "lw $4, 44(%1) \n\t" \
+ "sw $4, 40($29) \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 48 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5, \
+ arg6,arg7,arg8,arg9,arg10, \
+ arg11,arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long _argvec[13]; \
+ volatile unsigned long _res; \
+ _argvec[0] = (unsigned long)_orig.nraddr; \
+ _argvec[1] = (unsigned long)(arg1); \
+ _argvec[2] = (unsigned long)(arg2); \
+ _argvec[3] = (unsigned long)(arg3); \
+ _argvec[4] = (unsigned long)(arg4); \
+ _argvec[5] = (unsigned long)(arg5); \
+ _argvec[6] = (unsigned long)(arg6); \
+ _argvec[7] = (unsigned long)(arg7); \
+ _argvec[8] = (unsigned long)(arg8); \
+ _argvec[9] = (unsigned long)(arg9); \
+ _argvec[10] = (unsigned long)(arg10); \
+ _argvec[11] = (unsigned long)(arg11); \
+ _argvec[12] = (unsigned long)(arg12); \
+ __asm__ volatile( \
+ "subu $29, $29, 8 \n\t" \
+ "sw $28, 0($29) \n\t" \
+ "sw $31, 4($29) \n\t" \
+ "lw $4, 20(%1) \n\t" \
+ "subu $29, $29, 56\n\t" \
+ "sw $4, 16($29) \n\t" \
+ "lw $4, 24(%1) \n\t" \
+ "sw $4, 20($29) \n\t" \
+ "lw $4, 28(%1) \n\t" \
+ "sw $4, 24($29) \n\t" \
+ "lw $4, 32(%1) \n\t" \
+ "sw $4, 28($29) \n\t" \
+ "lw $4, 36(%1) \n\t" \
+ "sw $4, 32($29) \n\t" \
+ "lw $4, 40(%1) \n\t" \
+ "sw $4, 36($29) \n\t" \
+ "lw $4, 44(%1) \n\t" \
+ "sw $4, 40($29) \n\t" \
+ "lw $4, 48(%1) \n\t" \
+ "sw $4, 44($29) \n\t" \
+ "lw $4, 4(%1) \n\t" \
+ "lw $5, 8(%1) \n\t" \
+ "lw $6, 12(%1) \n\t" \
+ "lw $7, 16(%1) \n\t" \
+ "lw $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "addu $29, $29, 56 \n\t" \
+ "lw $28, 0($29) \n\t" \
+ "lw $31, 4($29) \n\t" \
+ "addu $29, $29, 8 \n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) _res; \
+ } while (0)
+
+#endif /* PLAT_mips32_linux */
+
+/* ------------------------- mips64-linux ------------------------- */
+
+#if defined(PLAT_mips64_linux)
+
+/* These regs are trashed by the hidden call. */
+#define __CALLER_SAVED_REGS "$2", "$3", "$4", "$5", "$6", \
+"$7", "$8", "$9", "$10", "$11", "$12", "$13", "$14", "$15", "$24", \
+"$25", "$31"
+
+/* These CALL_FN_ macros assume that on mips64-linux,
+ sizeof(long long) == 8. */
+
+#define MIPS64_LONG2REG_CAST(x) ((long long)(long)x)
+
+#define CALL_FN_W_v(lval, orig) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[1]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ __asm__ volatile( \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "0" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_W(lval, orig, arg1) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[2]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ __asm__ volatile( \
+ "ld $4, 8(%1)\n\t" /* arg1*/ \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_WW(lval, orig, arg1,arg2) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[3]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = _orig.nraddr; \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ __asm__ volatile( \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+
+#define CALL_FN_W_WWW(lval, orig, arg1,arg2,arg3) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[4]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = _orig.nraddr; \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ __asm__ volatile( \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_WWWW(lval, orig, arg1,arg2,arg3,arg4) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[5]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ __asm__ volatile( \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_5W(lval, orig, arg1,arg2,arg3,arg4,arg5) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[6]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ _argvec[5] = MIPS64_LONG2REG_CAST(arg5); \
+ __asm__ volatile( \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $8, 40(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_6W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[7]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ _argvec[5] = MIPS64_LONG2REG_CAST(arg5); \
+ _argvec[6] = MIPS64_LONG2REG_CAST(arg6); \
+ __asm__ volatile( \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $8, 40(%1)\n\t" \
+ "ld $9, 48(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_7W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[8]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ _argvec[5] = MIPS64_LONG2REG_CAST(arg5); \
+ _argvec[6] = MIPS64_LONG2REG_CAST(arg6); \
+ _argvec[7] = MIPS64_LONG2REG_CAST(arg7); \
+ __asm__ volatile( \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $8, 40(%1)\n\t" \
+ "ld $9, 48(%1)\n\t" \
+ "ld $10, 56(%1)\n\t" \
+ "ld $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_8W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[9]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ _argvec[5] = MIPS64_LONG2REG_CAST(arg5); \
+ _argvec[6] = MIPS64_LONG2REG_CAST(arg6); \
+ _argvec[7] = MIPS64_LONG2REG_CAST(arg7); \
+ _argvec[8] = MIPS64_LONG2REG_CAST(arg8); \
+ __asm__ volatile( \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $8, 40(%1)\n\t" \
+ "ld $9, 48(%1)\n\t" \
+ "ld $10, 56(%1)\n\t" \
+ "ld $11, 64(%1)\n\t" \
+ "ld $25, 0(%1) \n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_9W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[10]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ _argvec[5] = MIPS64_LONG2REG_CAST(arg5); \
+ _argvec[6] = MIPS64_LONG2REG_CAST(arg6); \
+ _argvec[7] = MIPS64_LONG2REG_CAST(arg7); \
+ _argvec[8] = MIPS64_LONG2REG_CAST(arg8); \
+ _argvec[9] = MIPS64_LONG2REG_CAST(arg9); \
+ __asm__ volatile( \
+ "dsubu $29, $29, 8\n\t" \
+ "ld $4, 72(%1)\n\t" \
+ "sd $4, 0($29)\n\t" \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $8, 40(%1)\n\t" \
+ "ld $9, 48(%1)\n\t" \
+ "ld $10, 56(%1)\n\t" \
+ "ld $11, 64(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "daddu $29, $29, 8\n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_10W(lval, orig, arg1,arg2,arg3,arg4,arg5,arg6, \
+ arg7,arg8,arg9,arg10) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[11]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ _argvec[5] = MIPS64_LONG2REG_CAST(arg5); \
+ _argvec[6] = MIPS64_LONG2REG_CAST(arg6); \
+ _argvec[7] = MIPS64_LONG2REG_CAST(arg7); \
+ _argvec[8] = MIPS64_LONG2REG_CAST(arg8); \
+ _argvec[9] = MIPS64_LONG2REG_CAST(arg9); \
+ _argvec[10] = MIPS64_LONG2REG_CAST(arg10); \
+ __asm__ volatile( \
+ "dsubu $29, $29, 16\n\t" \
+ "ld $4, 72(%1)\n\t" \
+ "sd $4, 0($29)\n\t" \
+ "ld $4, 80(%1)\n\t" \
+ "sd $4, 8($29)\n\t" \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $8, 40(%1)\n\t" \
+ "ld $9, 48(%1)\n\t" \
+ "ld $10, 56(%1)\n\t" \
+ "ld $11, 64(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "daddu $29, $29, 16\n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_11W(lval, orig, arg1,arg2,arg3,arg4,arg5, \
+ arg6,arg7,arg8,arg9,arg10, \
+ arg11) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[12]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ _argvec[5] = MIPS64_LONG2REG_CAST(arg5); \
+ _argvec[6] = MIPS64_LONG2REG_CAST(arg6); \
+ _argvec[7] = MIPS64_LONG2REG_CAST(arg7); \
+ _argvec[8] = MIPS64_LONG2REG_CAST(arg8); \
+ _argvec[9] = MIPS64_LONG2REG_CAST(arg9); \
+ _argvec[10] = MIPS64_LONG2REG_CAST(arg10); \
+ _argvec[11] = MIPS64_LONG2REG_CAST(arg11); \
+ __asm__ volatile( \
+ "dsubu $29, $29, 24\n\t" \
+ "ld $4, 72(%1)\n\t" \
+ "sd $4, 0($29)\n\t" \
+ "ld $4, 80(%1)\n\t" \
+ "sd $4, 8($29)\n\t" \
+ "ld $4, 88(%1)\n\t" \
+ "sd $4, 16($29)\n\t" \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $8, 40(%1)\n\t" \
+ "ld $9, 48(%1)\n\t" \
+ "ld $10, 56(%1)\n\t" \
+ "ld $11, 64(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "daddu $29, $29, 24\n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#define CALL_FN_W_12W(lval, orig, arg1,arg2,arg3,arg4,arg5, \
+ arg6,arg7,arg8,arg9,arg10, \
+ arg11,arg12) \
+ do { \
+ volatile OrigFn _orig = (orig); \
+ volatile unsigned long long _argvec[13]; \
+ volatile unsigned long long _res; \
+ _argvec[0] = MIPS64_LONG2REG_CAST(_orig.nraddr); \
+ _argvec[1] = MIPS64_LONG2REG_CAST(arg1); \
+ _argvec[2] = MIPS64_LONG2REG_CAST(arg2); \
+ _argvec[3] = MIPS64_LONG2REG_CAST(arg3); \
+ _argvec[4] = MIPS64_LONG2REG_CAST(arg4); \
+ _argvec[5] = MIPS64_LONG2REG_CAST(arg5); \
+ _argvec[6] = MIPS64_LONG2REG_CAST(arg6); \
+ _argvec[7] = MIPS64_LONG2REG_CAST(arg7); \
+ _argvec[8] = MIPS64_LONG2REG_CAST(arg8); \
+ _argvec[9] = MIPS64_LONG2REG_CAST(arg9); \
+ _argvec[10] = MIPS64_LONG2REG_CAST(arg10); \
+ _argvec[11] = MIPS64_LONG2REG_CAST(arg11); \
+ _argvec[12] = MIPS64_LONG2REG_CAST(arg12); \
+ __asm__ volatile( \
+ "dsubu $29, $29, 32\n\t" \
+ "ld $4, 72(%1)\n\t" \
+ "sd $4, 0($29)\n\t" \
+ "ld $4, 80(%1)\n\t" \
+ "sd $4, 8($29)\n\t" \
+ "ld $4, 88(%1)\n\t" \
+ "sd $4, 16($29)\n\t" \
+ "ld $4, 96(%1)\n\t" \
+ "sd $4, 24($29)\n\t" \
+ "ld $4, 8(%1)\n\t" \
+ "ld $5, 16(%1)\n\t" \
+ "ld $6, 24(%1)\n\t" \
+ "ld $7, 32(%1)\n\t" \
+ "ld $8, 40(%1)\n\t" \
+ "ld $9, 48(%1)\n\t" \
+ "ld $10, 56(%1)\n\t" \
+ "ld $11, 64(%1)\n\t" \
+ "ld $25, 0(%1)\n\t" /* target->t9 */ \
+ VALGRIND_CALL_NOREDIR_T9 \
+ "daddu $29, $29, 32\n\t" \
+ "move %0, $2\n" \
+ : /*out*/ "=r" (_res) \
+ : /*in*/ "r" (&_argvec[0]) \
+ : /*trash*/ "memory", __CALLER_SAVED_REGS \
+ ); \
+ lval = (__typeof__(lval)) (long)_res; \
+ } while (0)
+
+#endif /* PLAT_mips64_linux */
+
+/* ------------------------------------------------------------------ */
+/* ARCHITECTURE INDEPENDENT MACROS for CLIENT REQUESTS. */
+/* */
+/* ------------------------------------------------------------------ */
+
+/* Some request codes. There are many more of these, but most are not
+ exposed to end-user view. These are the public ones, all of the
+ form 0x1000 + small_number.
+
+ Core ones are in the range 0x00000000--0x0000ffff. The non-public
+ ones start at 0x2000.
+*/
+
+/* These macros are used by tools -- they must be public, but don't
+ embed them into other programs. */
+#define VG_USERREQ_TOOL_BASE(a,b) \
+ ((unsigned int)(((a)&0xff) << 24 | ((b)&0xff) << 16))
+#define VG_IS_TOOL_USERREQ(a, b, v) \
+ (VG_USERREQ_TOOL_BASE(a,b) == ((v) & 0xffff0000))
+
+/* !! ABIWARNING !! ABIWARNING !! ABIWARNING !! ABIWARNING !!
+ This enum comprises an ABI exported by Valgrind to programs
+ which use client requests. DO NOT CHANGE THE NUMERIC VALUES OF THESE
+ ENTRIES, NOR DELETE ANY -- add new ones at the end of the most
+ relevant group. */
+typedef
+ enum { VG_USERREQ__RUNNING_ON_VALGRIND = 0x1001,
+ VG_USERREQ__DISCARD_TRANSLATIONS = 0x1002,
+
+ /* These allow any function to be called from the simulated
+ CPU but run on the real CPU. Nb: the first arg passed to
+ the function is always the ThreadId of the running
+ thread! So CLIENT_CALL0 actually requires a 1 arg
+ function, etc. */
+ VG_USERREQ__CLIENT_CALL0 = 0x1101,
+ VG_USERREQ__CLIENT_CALL1 = 0x1102,
+ VG_USERREQ__CLIENT_CALL2 = 0x1103,
+ VG_USERREQ__CLIENT_CALL3 = 0x1104,
+
+ /* Can be useful in regression testing suites -- eg. can
+ send Valgrind's output to /dev/null and still count
+ errors. */
+ VG_USERREQ__COUNT_ERRORS = 0x1201,
+
+ /* Allows the client program and/or gdbserver to execute a monitor
+ command. */
+ VG_USERREQ__GDB_MONITOR_COMMAND = 0x1202,
+
+ /* These are useful and can be interpreted by any tool that
+ tracks malloc() et al, by using vg_replace_malloc.c. */
+ VG_USERREQ__MALLOCLIKE_BLOCK = 0x1301,
+ VG_USERREQ__RESIZEINPLACE_BLOCK = 0x130b,
+ VG_USERREQ__FREELIKE_BLOCK = 0x1302,
+ /* Memory pool support. */
+ VG_USERREQ__CREATE_MEMPOOL = 0x1303,
+ VG_USERREQ__DESTROY_MEMPOOL = 0x1304,
+ VG_USERREQ__MEMPOOL_ALLOC = 0x1305,
+ VG_USERREQ__MEMPOOL_FREE = 0x1306,
+ VG_USERREQ__MEMPOOL_TRIM = 0x1307,
+ VG_USERREQ__MOVE_MEMPOOL = 0x1308,
+ VG_USERREQ__MEMPOOL_CHANGE = 0x1309,
+ VG_USERREQ__MEMPOOL_EXISTS = 0x130a,
+
+ /* Allow printfs to valgrind log. */
+ /* The first two pass the va_list argument by value, which
+ assumes it is the same size as or smaller than a UWord,
+ which generally isn't the case. Hence are deprecated.
+ The second two pass the vargs by reference and so are
+ immune to this problem. */
+ /* both :: char* fmt, va_list vargs (DEPRECATED) */
+ VG_USERREQ__PRINTF = 0x1401,
+ VG_USERREQ__PRINTF_BACKTRACE = 0x1402,
+ /* both :: char* fmt, va_list* vargs */
+ VG_USERREQ__PRINTF_VALIST_BY_REF = 0x1403,
+ VG_USERREQ__PRINTF_BACKTRACE_VALIST_BY_REF = 0x1404,
+
+ /* Stack support. */
+ VG_USERREQ__STACK_REGISTER = 0x1501,
+ VG_USERREQ__STACK_DEREGISTER = 0x1502,
+ VG_USERREQ__STACK_CHANGE = 0x1503,
+
+ /* Wine support */
+ VG_USERREQ__LOAD_PDB_DEBUGINFO = 0x1601,
+
+ /* Querying of debug info. */
+ VG_USERREQ__MAP_IP_TO_SRCLOC = 0x1701,
+
+ /* Disable/enable error reporting level. Takes a single
+ Word arg which is the delta to this thread's error
+ disablement indicator. Hence 1 disables or further
+ disables errors, and -1 moves back towards enablement.
+ Other values are not allowed. */
+ VG_USERREQ__CHANGE_ERR_DISABLEMENT = 0x1801,
+
+ /* Some requests used for Valgrind internal, such as
+ self-test or self-hosting. */
+ /* Initialise IR injection */
+ VG_USERREQ__VEX_INIT_FOR_IRI = 0x1901,
+ /* Used by Inner Valgrind to inform Outer Valgrind where to
+ find the list of inner guest threads */
+ VG_USERREQ__INNER_THREADS = 0x1902
+ } Vg_ClientRequest;
+
+#if !defined(__GNUC__)
+# define __extension__ /* */
+#endif
+
+
+/* Returns the number of Valgrinds this code is running under. That
+ is, 0 if running natively, 1 if running under Valgrind, 2 if
+ running under Valgrind which is running under another Valgrind,
+ etc. */
+#define RUNNING_ON_VALGRIND \
+ (unsigned)VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* if not */, \
+ VG_USERREQ__RUNNING_ON_VALGRIND, \
+ 0, 0, 0, 0, 0) \
+
+
+/* Discard translation of code in the range [_qzz_addr .. _qzz_addr +
+ _qzz_len - 1]. Useful if you are debugging a JITter or some such,
+ since it provides a way to make sure valgrind will retranslate the
+ invalidated area. Returns no value. */
+#define VALGRIND_DISCARD_TRANSLATIONS(_qzz_addr,_qzz_len) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__DISCARD_TRANSLATIONS, \
+ _qzz_addr, _qzz_len, 0, 0, 0)
+
+#define VALGRIND_INNER_THREADS(_qzz_addr) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__INNER_THREADS, \
+ _qzz_addr, 0, 0, 0, 0)
+
+
+/* These requests are for getting Valgrind itself to print something.
+ Possibly with a backtrace. This is a really ugly hack. The return value
+ is the number of characters printed, excluding the "**<pid>** " part at the
+ start and the backtrace (if present). */
+
+#if defined(__GNUC__) || defined(__INTEL_COMPILER) && !defined(_MSC_VER)
+/* Modern GCC will optimize the static routine out if unused,
+ and unused attribute will shut down warnings about it. */
+static int VALGRIND_PRINTF(const char *format, ...)
+ __attribute__((format(__printf__, 1, 2), __unused__));
+#endif
+static int
+#if defined(_MSC_VER)
+__inline
+#endif
+VALGRIND_PRINTF(const char *format, ...)
+{
+#if defined(NVALGRIND)
+ (void)format;
+ return 0;
+#else /* NVALGRIND */
+#if defined(_MSC_VER) || defined(__MINGW64__)
+ uintptr_t _qzz_res;
+#else
+ unsigned long _qzz_res;
+#endif
+ va_list vargs;
+ va_start(vargs, format);
+#if defined(_MSC_VER) || defined(__MINGW64__)
+ _qzz_res = VALGRIND_DO_CLIENT_REQUEST_EXPR(0,
+ VG_USERREQ__PRINTF_VALIST_BY_REF,
+ (uintptr_t)format,
+ (uintptr_t)&vargs,
+ 0, 0, 0);
+#else
+ _qzz_res = VALGRIND_DO_CLIENT_REQUEST_EXPR(0,
+ VG_USERREQ__PRINTF_VALIST_BY_REF,
+ (unsigned long)format,
+ (unsigned long)&vargs,
+ 0, 0, 0);
+#endif
+ va_end(vargs);
+ return (int)_qzz_res;
+#endif /* NVALGRIND */
+}
+
+#if defined(__GNUC__) || defined(__INTEL_COMPILER) && !defined(_MSC_VER)
+static int VALGRIND_PRINTF_BACKTRACE(const char *format, ...)
+ __attribute__((format(__printf__, 1, 2), __unused__));
+#endif
+static int
+#if defined(_MSC_VER)
+__inline
+#endif
+VALGRIND_PRINTF_BACKTRACE(const char *format, ...)
+{
+#if defined(NVALGRIND)
+ (void)format;
+ return 0;
+#else /* NVALGRIND */
+#if defined(_MSC_VER) || defined(__MINGW64__)
+ uintptr_t _qzz_res;
+#else
+ unsigned long _qzz_res;
+#endif
+ va_list vargs;
+ va_start(vargs, format);
+#if defined(_MSC_VER) || defined(__MINGW64__)
+ _qzz_res = VALGRIND_DO_CLIENT_REQUEST_EXPR(0,
+ VG_USERREQ__PRINTF_BACKTRACE_VALIST_BY_REF,
+ (uintptr_t)format,
+ (uintptr_t)&vargs,
+ 0, 0, 0);
+#else
+ _qzz_res = VALGRIND_DO_CLIENT_REQUEST_EXPR(0,
+ VG_USERREQ__PRINTF_BACKTRACE_VALIST_BY_REF,
+ (unsigned long)format,
+ (unsigned long)&vargs,
+ 0, 0, 0);
+#endif
+ va_end(vargs);
+ return (int)_qzz_res;
+#endif /* NVALGRIND */
+}
+
+
+/* These requests allow control to move from the simulated CPU to the
+ real CPU, calling an arbitrary function.
+
+ Note that the current ThreadId is inserted as the first argument.
+ So this call:
+
+ VALGRIND_NON_SIMD_CALL2(f, arg1, arg2)
+
+ requires f to have this signature:
+
+ Word f(Word tid, Word arg1, Word arg2)
+
+ where "Word" is a word-sized type.
+
+ Note that these client requests are not entirely reliable. For example,
+ if you call a function with them that subsequently calls printf(),
+ there's a high chance Valgrind will crash. Generally, your prospects of
+ these working are made higher if the called function does not refer to
+ any global variables, and does not refer to any libc or other functions
+ (printf et al). Any kind of entanglement with libc or dynamic linking is
+ likely to have a bad outcome, for tricky reasons which we've grappled
+ with a lot in the past.
+*/
+#define VALGRIND_NON_SIMD_CALL0(_qyy_fn) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__CLIENT_CALL0, \
+ _qyy_fn, \
+ 0, 0, 0, 0)
+
+#define VALGRIND_NON_SIMD_CALL1(_qyy_fn, _qyy_arg1) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__CLIENT_CALL1, \
+ _qyy_fn, \
+ _qyy_arg1, 0, 0, 0)
+
+#define VALGRIND_NON_SIMD_CALL2(_qyy_fn, _qyy_arg1, _qyy_arg2) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__CLIENT_CALL2, \
+ _qyy_fn, \
+ _qyy_arg1, _qyy_arg2, 0, 0)
+
+#define VALGRIND_NON_SIMD_CALL3(_qyy_fn, _qyy_arg1, _qyy_arg2, _qyy_arg3) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0 /* default return */, \
+ VG_USERREQ__CLIENT_CALL3, \
+ _qyy_fn, \
+ _qyy_arg1, _qyy_arg2, \
+ _qyy_arg3, 0)
+
+
+/* Counts the number of errors that have been recorded by a tool. Nb:
+ the tool must record the errors with VG_(maybe_record_error)() or
+ VG_(unique_error)() for them to be counted. */
+#define VALGRIND_COUNT_ERRORS \
+ (unsigned)VALGRIND_DO_CLIENT_REQUEST_EXPR( \
+ 0 /* default return */, \
+ VG_USERREQ__COUNT_ERRORS, \
+ 0, 0, 0, 0, 0)
+
+/* Several Valgrind tools (Memcheck, Massif, Helgrind, DRD) rely on knowing
+ when heap blocks are allocated in order to give accurate results. This
+ happens automatically for the standard allocator functions such as
+ malloc(), calloc(), realloc(), memalign(), new, new[], free(), delete,
+ delete[], etc.
+
+ But if your program uses a custom allocator, this doesn't automatically
+ happen, and Valgrind will not do as well. For example, if you allocate
+ superblocks with mmap() and then allocates chunks of the superblocks, all
+ Valgrind's observations will be at the mmap() level and it won't know that
+ the chunks should be considered separate entities. In Memcheck's case,
+ that means you probably won't get heap block overrun detection (because
+ there won't be redzones marked as unaddressable) and you definitely won't
+ get any leak detection.
+
+ The following client requests allow a custom allocator to be annotated so
+ that it can be handled accurately by Valgrind.
+
+ VALGRIND_MALLOCLIKE_BLOCK marks a region of memory as having been allocated
+ by a malloc()-like function. For Memcheck (an illustrative case), this
+ does two things:
+
+ - It records that the block has been allocated. This means any addresses
+ within the block mentioned in error messages will be
+ identified as belonging to the block. It also means that if the block
+ isn't freed it will be detected by the leak checker.
+
+ - It marks the block as being addressable and undefined (if 'is_zeroed' is
+ not set), or addressable and defined (if 'is_zeroed' is set). This
+ controls how accesses to the block by the program are handled.
+
+ 'addr' is the start of the usable block (ie. after any
+ redzone), 'sizeB' is its size. 'rzB' is the redzone size if the allocator
+ can apply redzones -- these are blocks of padding at the start and end of
+ each block. Adding redzones is recommended as it makes it much more likely
+ Valgrind will spot block overruns. `is_zeroed' indicates if the memory is
+ zeroed (or filled with another predictable value), as is the case for
+ calloc().
+
+ VALGRIND_MALLOCLIKE_BLOCK should be put immediately after the point where a
+ heap block -- that will be used by the client program -- is allocated.
+ It's best to put it at the outermost level of the allocator if possible;
+ for example, if you have a function my_alloc() which calls
+ internal_alloc(), and the client request is put inside internal_alloc(),
+ stack traces relating to the heap block will contain entries for both
+ my_alloc() and internal_alloc(), which is probably not what you want.
+
+ For Memcheck users: if you use VALGRIND_MALLOCLIKE_BLOCK to carve out
+ custom blocks from within a heap block, B, that has been allocated with
+ malloc/calloc/new/etc, then block B will be *ignored* during leak-checking
+ -- the custom blocks will take precedence.
+
+ VALGRIND_FREELIKE_BLOCK is the partner to VALGRIND_MALLOCLIKE_BLOCK. For
+ Memcheck, it does two things:
+
+ - It records that the block has been deallocated. This assumes that the
+ block was annotated as having been allocated via
+ VALGRIND_MALLOCLIKE_BLOCK. Otherwise, an error will be issued.
+
+ - It marks the block as being unaddressable.
+
+ VALGRIND_FREELIKE_BLOCK should be put immediately after the point where a
+ heap block is deallocated.
+
+ VALGRIND_RESIZEINPLACE_BLOCK informs a tool about reallocation. For
+ Memcheck, it does four things:
+
+ - It records that the size of a block has been changed. This assumes that
+ the block was annotated as having been allocated via
+ VALGRIND_MALLOCLIKE_BLOCK. Otherwise, an error will be issued.
+
+ - If the block shrunk, it marks the freed memory as being unaddressable.
+
+ - If the block grew, it marks the new area as undefined and defines a red
+ zone past the end of the new block.
+
+ - The V-bits of the overlap between the old and the new block are preserved.
+
+ VALGRIND_RESIZEINPLACE_BLOCK should be put after allocation of the new block
+ and before deallocation of the old block.
+
+ In many cases, these three client requests will not be enough to get your
+ allocator working well with Memcheck. More specifically, if your allocator
+ writes to freed blocks in any way then a VALGRIND_MAKE_MEM_UNDEFINED call
+ will be necessary to mark the memory as addressable just before the zeroing
+ occurs, otherwise you'll get a lot of invalid write errors. For example,
+ you'll need to do this if your allocator recycles freed blocks, but it
+ zeroes them before handing them back out (via VALGRIND_MALLOCLIKE_BLOCK).
+ Alternatively, if your allocator reuses freed blocks for allocator-internal
+ data structures, VALGRIND_MAKE_MEM_UNDEFINED calls will also be necessary.
+
+ Really, what's happening is a blurring of the lines between the client
+ program and the allocator... after VALGRIND_FREELIKE_BLOCK is called, the
+ memory should be considered unaddressable to the client program, but the
+ allocator knows more than the rest of the client program and so may be able
+ to safely access it. Extra client requests are necessary for Valgrind to
+ understand the distinction between the allocator and the rest of the
+ program.
+
+ Ignored if addr == 0.
+*/
+#define VALGRIND_MALLOCLIKE_BLOCK(addr, sizeB, rzB, is_zeroed) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__MALLOCLIKE_BLOCK, \
+ addr, sizeB, rzB, is_zeroed, 0)
+
+/* See the comment for VALGRIND_MALLOCLIKE_BLOCK for details.
+ Ignored if addr == 0.
+*/
+#define VALGRIND_RESIZEINPLACE_BLOCK(addr, oldSizeB, newSizeB, rzB) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__RESIZEINPLACE_BLOCK, \
+ addr, oldSizeB, newSizeB, rzB, 0)
+
+/* See the comment for VALGRIND_MALLOCLIKE_BLOCK for details.
+ Ignored if addr == 0.
+*/
+#define VALGRIND_FREELIKE_BLOCK(addr, rzB) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__FREELIKE_BLOCK, \
+ addr, rzB, 0, 0, 0)
+
+/* Create a memory pool. */
+#define VALGRIND_CREATE_MEMPOOL(pool, rzB, is_zeroed) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__CREATE_MEMPOOL, \
+ pool, rzB, is_zeroed, 0, 0)
+
+/* Create a memory pool with some flags specifying extended behaviour.
+ When flags is zero, the behaviour is identical to VALGRIND_CREATE_MEMPOOL.
+
+ The flag VALGRIND_MEMPOOL_METAPOOL specifies that the pieces of memory
+ associated with the pool using VALGRIND_MEMPOOL_ALLOC will be used
+ by the application as superblocks to dole out MALLOC_LIKE blocks using
+ VALGRIND_MALLOCLIKE_BLOCK. In other words, a meta pool is a "2 levels"
+ pool : first level is the blocks described by VALGRIND_MEMPOOL_ALLOC.
+ The second level blocks are described using VALGRIND_MALLOCLIKE_BLOCK.
+ Note that the association between the pool and the second level blocks
+ is implicit : second level blocks will be located inside first level
+ blocks. It is necessary to use the VALGRIND_MEMPOOL_METAPOOL flag
+ for such 2 levels pools, as otherwise valgrind will detect overlapping
+ memory blocks, and will abort execution (e.g. during leak search).
+
+ Such a meta pool can also be marked as an 'auto free' pool using the flag
+ VALGRIND_MEMPOOL_AUTO_FREE, which must be OR-ed together with the
+ VALGRIND_MEMPOOL_METAPOOL. For an 'auto free' pool, VALGRIND_MEMPOOL_FREE
+ will automatically free the second level blocks that are contained
+ inside the first level block freed with VALGRIND_MEMPOOL_FREE.
+ In other words, calling VALGRIND_MEMPOOL_FREE will cause implicit calls
+ to VALGRIND_FREELIKE_BLOCK for all the second level blocks included
+ in the first level block.
+ Note: it is an error to use the VALGRIND_MEMPOOL_AUTO_FREE flag
+ without the VALGRIND_MEMPOOL_METAPOOL flag.
+*/
+#define VALGRIND_MEMPOOL_AUTO_FREE 1
+#define VALGRIND_MEMPOOL_METAPOOL 2
+#define VALGRIND_CREATE_MEMPOOL_EXT(pool, rzB, is_zeroed, flags) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__CREATE_MEMPOOL, \
+ pool, rzB, is_zeroed, flags, 0)
+
+/* Destroy a memory pool. */
+#define VALGRIND_DESTROY_MEMPOOL(pool) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__DESTROY_MEMPOOL, \
+ pool, 0, 0, 0, 0)
+
+/* Associate a piece of memory with a memory pool. */
+#define VALGRIND_MEMPOOL_ALLOC(pool, addr, size) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__MEMPOOL_ALLOC, \
+ pool, addr, size, 0, 0)
+
+/* Disassociate a piece of memory from a memory pool. */
+#define VALGRIND_MEMPOOL_FREE(pool, addr) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__MEMPOOL_FREE, \
+ pool, addr, 0, 0, 0)
+
+/* Disassociate any pieces outside a particular range. */
+#define VALGRIND_MEMPOOL_TRIM(pool, addr, size) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__MEMPOOL_TRIM, \
+ pool, addr, size, 0, 0)
+
+/* Resize and/or move a piece associated with a memory pool. */
+#define VALGRIND_MOVE_MEMPOOL(poolA, poolB) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__MOVE_MEMPOOL, \
+ poolA, poolB, 0, 0, 0)
+
+/* Resize and/or move a piece associated with a memory pool. */
+#define VALGRIND_MEMPOOL_CHANGE(pool, addrA, addrB, size) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__MEMPOOL_CHANGE, \
+ pool, addrA, addrB, size, 0)
+
+/* Return 1 if a mempool exists, else 0. */
+#define VALGRIND_MEMPOOL_EXISTS(pool) \
+ (unsigned)VALGRIND_DO_CLIENT_REQUEST_EXPR(0, \
+ VG_USERREQ__MEMPOOL_EXISTS, \
+ pool, 0, 0, 0, 0)
+
+/* Mark a piece of memory as being a stack. Returns a stack id.
+ start is the lowest addressable stack byte, end is the highest
+ addressable stack byte. */
+#define VALGRIND_STACK_REGISTER(start, end) \
+ (unsigned)VALGRIND_DO_CLIENT_REQUEST_EXPR(0, \
+ VG_USERREQ__STACK_REGISTER, \
+ start, end, 0, 0, 0)
+
+/* Unmark the piece of memory associated with a stack id as being a
+ stack. */
+#define VALGRIND_STACK_DEREGISTER(id) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__STACK_DEREGISTER, \
+ id, 0, 0, 0, 0)
+
+/* Change the start and end address of the stack id.
+ start is the new lowest addressable stack byte, end is the new highest
+ addressable stack byte. */
+#define VALGRIND_STACK_CHANGE(id, start, end) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__STACK_CHANGE, \
+ id, start, end, 0, 0)
+
+/* Load PDB debug info for Wine PE image_map. */
+#define VALGRIND_LOAD_PDB_DEBUGINFO(fd, ptr, total_size, delta) \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__LOAD_PDB_DEBUGINFO, \
+ fd, ptr, total_size, delta, 0)
+
+/* Map a code address to a source file name and line number. buf64
+ must point to a 64-byte buffer in the caller's address space. The
+ result will be dumped in there and is guaranteed to be zero
+ terminated. If no info is found, the first byte is set to zero. */
+#define VALGRIND_MAP_IP_TO_SRCLOC(addr, buf64) \
+ (unsigned)VALGRIND_DO_CLIENT_REQUEST_EXPR(0, \
+ VG_USERREQ__MAP_IP_TO_SRCLOC, \
+ addr, buf64, 0, 0, 0)
+
+/* Disable error reporting for this thread. Behaves in a stack like
+ way, so you can safely call this multiple times provided that
+ VALGRIND_ENABLE_ERROR_REPORTING is called the same number of times
+ to re-enable reporting. The first call of this macro disables
+ reporting. Subsequent calls have no effect except to increase the
+ number of VALGRIND_ENABLE_ERROR_REPORTING calls needed to re-enable
+ reporting. Child threads do not inherit this setting from their
+ parents -- they are always created with reporting enabled. */
+#define VALGRIND_DISABLE_ERROR_REPORTING \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__CHANGE_ERR_DISABLEMENT, \
+ 1, 0, 0, 0, 0)
+
+/* Re-enable error reporting, as per comments on
+ VALGRIND_DISABLE_ERROR_REPORTING. */
+#define VALGRIND_ENABLE_ERROR_REPORTING \
+ VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__CHANGE_ERR_DISABLEMENT, \
+ -1, 0, 0, 0, 0)
+
+/* Execute a monitor command from the client program.
+ If a connection is opened with GDB, the output will be sent
+ according to the output mode set for vgdb.
+ If no connection is opened, output will go to the log output.
+ Returns 1 if command not recognised, 0 otherwise. */
+#define VALGRIND_MONITOR_COMMAND(command) \
+ VALGRIND_DO_CLIENT_REQUEST_EXPR(0, VG_USERREQ__GDB_MONITOR_COMMAND, \
+ command, 0, 0, 0, 0)
+
+
+#undef PLAT_x86_darwin
+#undef PLAT_amd64_darwin
+#undef PLAT_x86_win32
+#undef PLAT_amd64_win64
+#undef PLAT_x86_linux
+#undef PLAT_amd64_linux
+#undef PLAT_ppc32_linux
+#undef PLAT_ppc64be_linux
+#undef PLAT_ppc64le_linux
+#undef PLAT_arm_linux
+#undef PLAT_s390x_linux
+#undef PLAT_mips32_linux
+#undef PLAT_mips64_linux
+#undef PLAT_x86_solaris
+#undef PLAT_amd64_solaris
+
+#endif /* __VALGRIND_H */
diff --git a/man/meson.build b/man/meson.build
new file mode 100644
index 0000000..a262fb8
--- /dev/null
+++ b/man/meson.build
@@ -0,0 +1,44 @@
+manpage_conf = configuration_data()
+manpage_conf.set('PACKAGE_NAME', meson.project_name())
+manpage_conf.set('PACKAGE_VERSION', meson.project_version())
+manpage_conf.set('PACKAGE_URL', 'https://pipewire.org')
+manpage_conf.set('PACKAGE_BUGREPORT', 'https://gitlab.freedesktop.org/pipewire/pipewire/issues')
+manpage_conf.set('PIPEWIRE_CONFIG_DIR', pipewire_configdir)
+manpage_conf.set('PIPEWIRE_CONFDATADIR', pipewire_confdatadir)
+
+manpages = [
+ 'pipewire.1.rst.in',
+ 'pipewire-pulse.1.rst.in',
+ 'pipewire.conf.5.rst.in',
+ 'pw-cat.1.rst.in',
+ 'pw-cli.1.rst.in',
+ 'pw-dot.1.rst.in',
+ 'pw-link.1.rst.in',
+ 'pw-metadata.1.rst.in',
+ 'pw-mididump.1.rst.in',
+ 'pw-mon.1.rst.in',
+ 'pw-profiler.1.rst.in',
+ 'pw-top.1.rst.in',
+]
+
+if get_option('pipewire-jack').allowed()
+ manpages += 'pw-jack.1.rst.in'
+endif
+
+if not generate_manpages
+ subdir_done()
+endif
+
+foreach m : manpages
+ file = m.split('.rst.in').get(0)
+ rst = configure_file(input : m,
+ output : file + '.rst',
+ configuration : manpage_conf)
+ section = file.split('.').get(-1)
+ custom_target(file + '.target',
+ output : file,
+ input : rst,
+ command : [rst2man, '@INPUT@', '@OUTPUT@'],
+ install : true,
+ install_dir : get_option('mandir') / 'man' + section)
+endforeach
diff --git a/man/pipewire-pulse.1.rst.in b/man/pipewire-pulse.1.rst.in
new file mode 100644
index 0000000..eed994b
--- /dev/null
+++ b/man/pipewire-pulse.1.rst.in
@@ -0,0 +1,48 @@
+pipewire-pulse
+##############
+
+-----------------------------------
+The PipeWire PulseAudio replacement
+-----------------------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pipewire-pulse** [*options*]
+
+DESCRIPTION
+===========
+
+**pipewire-pulse** starts a PulseAudio-compatible daemon that integrates with
+the PipeWire media server. This daemon is a drop-in replacement for the
+PulseAudio daemon.
+
+OPTIONS
+=======
+
+-h | --help
+ Show help.
+
+-v | --verbose
+ Increase the verbosity by one level. This option may be specified multiple
+ times.
+
+--version
+ Show version information.
+
+-c | --config=FILE
+ Load the given config file (Default: pipewire-pulse.conf).
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>;
+PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``
diff --git a/man/pipewire.1.rst.in b/man/pipewire.1.rst.in
new file mode 100644
index 0000000..8b63839
--- /dev/null
+++ b/man/pipewire.1.rst.in
@@ -0,0 +1,54 @@
+pipewire
+########
+
+-------------------------
+The PipeWire media server
+-------------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pipewire** [*options*]
+
+DESCRIPTION
+===========
+
+PipeWire is a service that facilitates sharing of multimedia content
+between devices and applications.
+
+The **pipewire** daemon reads a config file that is further documented in
+``pipewire.conf(5)`` manual page.
+
+OPTIONS
+=======
+
+-h | --help
+ Show help.
+
+-v | --verbose
+ Increase the verbosity by one level. This option may be specified multiple
+ times.
+
+--version
+ Show version information.
+
+-c | --config=FILE
+ Load the given config file (Default: pipewire.conf).
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>;
+PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pw-top(1)``,
+``pw-dump(1)``,
+``pw-mon(1)``,
+``pw-cat(1)``,
+``pw-cli(1)``,
diff --git a/man/pipewire.conf.5.rst.in b/man/pipewire.conf.5.rst.in
new file mode 100644
index 0000000..3e3a954
--- /dev/null
+++ b/man/pipewire.conf.5.rst.in
@@ -0,0 +1,113 @@
+pipewire.conf
+#############
+
+--------------------------------------
+The PipeWire server configuration file
+--------------------------------------
+
+:Manual section: 5
+:Manual group: File Formats Manual
+
+.. _synopsis:
+
+SYNOPSIS
+========
+
+*$XDG_CONFIG_HOME/pipewire/pipewire.conf*
+
+*@PIPEWIRE_CONFIG_DIR@/pipewire.conf*
+
+*@PIPEWIRE_CONFDATADIR@/pipewire.conf*
+
+*@PIPEWIRE_CONFDATADIR@/pipewire.conf.d/*
+
+*@PIPEWIRE_CONFIG_DIR@/pipewire.conf.d/*
+
+*$XDG_CONFIG_HOME/pipewire/pipewire.conf.d/*
+
+DESCRIPTION
+===========
+
+PipeWire is a service that facilitates sharing of multimedia content
+between devices and applications.
+
+On startup, the daemon reads a main configuration file to configure
+itself. It executes a series of commands listed in the config
+file.
+
+The config files are loaded in the order listed in the SYNOPSIS_.
+The environment variables ``PIPEWIRE_CONFIG_DIR``, ``PIPEWIRE_CONFIG_PREFIX``
+and ``PIPEWIRE_CONFIG_NAME`` can be used to specify an alternative config
+directory, subdirectory and file respectively.
+
+Next to the configuration file can be a directory with the same name as
+the file with a ``.d/`` suffix. All directories in the SYNOPSIS_ directory
+search paths are traversed in the listed order and the contents of the
+``*.conf`` files inside them are appended to the main configuration file
+as overrides. Object sections are merged and array sections are appended.
+
+
+CONFIGURATION FILE FORMAT
+=========================
+
+The configuration file format is grouped into sections. A section
+is either a dictionary, {}, or an array, []. Dictionary and array
+entries are separated by whitespace and may be simple value
+assignment, an array or a dictionary. For example:
+
+name = value # simple assignment
+
+name = { key1 = value1 key2 = value2 } # a dictionary with two
+entries
+
+name = [ value1 value2 ] # an array with two entries
+
+name = [ { k = v1 } { k = v2 } ] # an array of dictionaries
+
+
+The configuration files can be expressed in full JSON syntax but for ease
+of use, a relaxed format may be used where:
+
+ * ``:`` to delimit keys and values can be substuted by ``=`` or a space.
+ * ``"`` around keys and string can be omited as long as no special characters
+ are used in the strings.
+ * ``,`` to separate objects can be replaced with a whitespace character.
+ * ``#`` can be used to start a comment until the line end
+
+
+CONFIGURATION FILE SECTIONS
+===========================
+
+context.properties
+ Dictionary. These properties configure the PipeWire instance.
+
+context.spa-libs
+ Dictionary. Maps plugin features with globs to a spa library.
+
+context.modules
+ Array of dictionaries. Each entry in the array is a dictionary with the *name* of the module to load,
+ including optional *args* and *flags*. Most modules support being loaded
+ multiple times.
+
+context.objects
+ Array of dictionaries. Each entry in the array is a dictionary containing the *factory* to create an
+ object from and optional extra arguments specific to that factory.
+
+context.exec
+ Array of dictionaries. Each entry in the array is dictionary containing the *path* of a program to
+ execute on startup and optional *args*.
+
+ This array used to contain an entry to start the session manager but this mode
+ of operation has since been demoted to development aid. Avoid starting a
+ session manager in this way in production environment.
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``pw-mon(1)``,
diff --git a/man/pw-cat.1.rst.in b/man/pw-cat.1.rst.in
new file mode 100644
index 0000000..f8df95d
--- /dev/null
+++ b/man/pw-cat.1.rst.in
@@ -0,0 +1,174 @@
+pw-cat
+######
+
+-----------------------------------
+Play and record media with PipeWire
+-----------------------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-cat** [*options*] [*FILE* \| -]
+| **pw-play** [*options*] [*FILE* \| -]
+| **pw-record** [*options*] [*FILE* \| -]
+| **pw-midiplay** [*options*] [*FILE* \| -]
+| **pw-midirecord** [*options*] [*FILE* \| -]
+| **pw-dsdplay** [*options*] [*FILE* \| -]
+
+DESCRIPTION
+===========
+
+**pw-cat** is a simple tool for playing back or
+capturing raw or encoded media files on a PipeWire
+server. It understands all audio file formats supported by
+``libsndfile`` for PCM capture and playback.
+
+It understands standard MIDI files for playback and recording. This tool
+will not render MIDI files, it will simply make the MIDI events available
+to the graph. You need a MIDI renderer such as qsynth, timidity or a hardware
+MIDI rendered to hear the MIDI.
+
+DSD playback is supported with the DSF file format. This tool will only work
+with native DSD capable hardware and will produce an error when no such
+hardware was found.
+
+When the *FILE* is - input and output will be from STDIN and
+STDOUT respectively.
+
+OPTIONS
+=======
+
+-h | --help
+ Show help.
+
+--version
+ Show version information.
+
+-v | --verbose
+ Verbose operation.
+
+-R | --remote=NAME
+ The name the *remote* instance to connect to. If left unspecified,
+ a connection is made to the default PipeWire instance.
+
+-p | --playback
+ Playback mode. Read data from the specified file, and play it back. If the tool
+ is called under the name **pw-play** or **pw-midiplay** this is the default.
+
+-r | --record
+ Recording mode. Capture data and write it to the specified file. If the tool is
+ called under the name **pw-record** or **pw-midirecord** this is the default.
+
+-m | --midi
+ MIDI mode. *FILE* is a MIDI file. If the tool is called under the name
+ **pw-midiplay** or **pw-midirecord** this is the default.
+ Note that this program will *not* render the MIDI events into audible samples,
+ it will simply provide the MIDI events in the graph. You need a separate
+ MIDI renderer such as qsynth, timidity or a hardware renderer to hear the MIDI.
+
+-d | --dsd
+ DSD mode. *FILE* is a DSF file. If the tool is called under the name
+ **pw-dsdplay** this is the default.
+ Note that this program will *not* render the DSD audio. You need a DSD capable
+ device to play DSD content or this program will exit with an error.
+
+--media-type=VALUE
+ Set the media type property (default Audio/Midi depending on mode).
+ The media type is used by the session manager to select a suitable target
+ to link to.
+
+--media-category=VALUE
+ Set the media category property (default Playback/Capture depending on mode).
+ The media type is used by the session manager to select a suitable target
+ to link to.
+
+--media-role=VALUE
+ Set the media role property (default Music).
+ The media type is used by the session manager to select a suitable target
+ to link to.
+
+--target=VALUE
+ Set a node target (default auto). The value can be:
+
+ auto
+ Automatically select (Default)
+
+ 0
+ Don't try to link this node
+
+ <id>
+ The object.serial or the node.name of a target node
+
+--latency=VALUE[*units*]
+ Set the node latency (default 100ms)
+
+ The latency determines the minimum amount of time it takes
+ for a sample to travel from application to device (playback) and
+ from device to application (capture).
+
+ The latency determines the size of the buffers that the
+ application will be able to fill. Lower latency means smaller
+ buffers but higher overhead. Higher latency means larger buffers
+ and lower overhead.
+
+ Units can be **s** for seconds, **ms** for milliseconds,
+ **us** for microseconds, **ns** for nanoseconds.
+ If no units are given, the latency value is samples with the samplerate
+ of the file.
+
+-P | --properties=VALUE
+ Set extra stream properties as a JSON object.
+
+-q | --quality=VALUE
+ Resampler quality. When the samplerate of the source or
+ destination file does not match the samplerate of the server, the
+ data will be resampled. Higher quality uses more CPU. Values between 0 and 15 are
+ allowed, the default quality is 4.
+
+--rate=VALUE
+ The sample rate, default 48000.
+
+--channels=VALUE
+ The number of channels, default 2.
+
+--channel-map=VALUE
+ The channelmap. Possible values include:
+ **mono**, **stereo**, **surround-21**,
+ **quad**, **surround-22**, **surround-40**,
+ **surround-31**, **surround-41**,
+ **surround-50**, **surround-51**,
+ **surround-51r**, **surround-70**,
+ **surround-71** or a comma separated list of channel names:
+ **FL**, **FR**, **FC**, **LFE**,
+ **SL**, **SR**, **FLC**, **FRC**,
+ **RC**, **RL**, **RR**, **TC**,
+ **TFL**, **TFC**, **TFR**, **TRL**,
+ **TRC**, **TRR**, **RLC**, **RRC**,
+ **FLW**, **FRW**, **LFE2**, **FLH**,
+ **FCH**, **FRH**, **TFLC**, **TFRC**,
+ **TSL**, **TSR**, **LLFR**, **RLFE**,
+ **BC**, **BLC**, **BRC**
+
+--format=VALUE
+ The sample format to use. One of:
+ **u8**, **s8**, **s16** (default), **s24**, **s32**,
+ **f32**, **f64**.
+
+--volume=VALUE
+ The stream volume, default 1.000.
+ Depending on the locale you have configured, "," or "." may be
+ used as a decimal separator. Check with **locale** command.
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``PipeWire(1)``,
+``pw-mon(1)``,
diff --git a/man/pw-cli.1.rst.in b/man/pw-cli.1.rst.in
new file mode 100644
index 0000000..ca0a2e8
--- /dev/null
+++ b/man/pw-cli.1.rst.in
@@ -0,0 +1,190 @@
+pw-cli
+######
+
+-----------------------------------
+The PipeWire Command Line Interface
+-----------------------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-cli** [*command*]
+
+DESCRIPTION
+===========
+
+Interact with a PipeWire instance.
+
+When a command is given, **pw-cli**
+will execute the command and exit
+
+When no command is given, **pw-cli**
+starts an interactive session with the default PipeWire instance
+*pipewire-0*.
+
+Connections to other, remote instances can be made. The current instance
+name is displayed at the prompt. Some commands operate on the current
+instance and some on the local instance.
+
+Use the 'help' command to list the available commands.
+
+GENERAL COMMANDS
+================
+
+help | h
+ Show a quick help on the commands available. It also lists the aliases
+ for many commands.
+
+quit | q
+ Exit from **pw-cli**
+
+MODULE MANAGEMENT
+=================
+
+| Modules are loaded and unloaded in the local instance, thus the pw-cli
+| binary itself and can add functionality or objects to the local
+| instance. It is not possible in PipeWire to load modules in another
+| instance.
+
+load-module *name* [*arguments...*]
+ Load a module specified by its name and arguments. For most
+ modules it is OK to be loaded more than once.
+
+ This command returns a module variable that can be used
+ to unload the module.
+
+unload-module *module-var*
+ Unload a module, specified either by its variable.
+
+OBJECT INTROSPECTION
+====================
+
+list-objects
+ List the objects of the current instance.
+
+ Objects are listed with their *id*, *type* and *version*.
+
+info *id* | *all*
+ Get information about a specific object or *all* objects.
+
+ Requesting info about an object will also notify you of changes.
+
+WORKING WITH REMOTES
+====================
+
+connect [*remote-name*]
+ Connect to a remote instance and make this the new current
+ instance.
+
+ If no remote name is specified, a connection is made to
+ the default remote instance, usually *pipewire-0*.
+
+ This command returns a remote var that can be used to disconnect or
+ switch remotes.
+
+disconnect [*remote-var*]
+ Disconnect from a *remote instance*.
+
+ If no remote name is specified, the current instance is disconnected.
+
+list-remotes
+ List all *remote instances*.
+
+switch-remote [*remote-var*]
+ Make the specified *remote* the current instance.
+
+ If no remote name is specified, the local instance is made current.
+
+NODE MANAGEMENT
+===============
+
+create-node *factory-name* [*properties...*]
+ Create a node from a factory in the current instance.
+
+ Properties are key=value pairs separated by whitespace.
+
+ This command returns a *node variable*.
+
+export-node *node-id* [*remote-var*]
+ Export a node from the local instance to the specified instance.
+ When no instance is specified, the node will be exported to the current
+ instance.
+
+DEVICE MANAGEMENT
+=================
+
+create-device *factory-name* [*properties...*]
+ Create a device from a factory in the current instance.
+
+ Properties are key=value pairs separated by whitespace.
+
+ This command returns a *device variable*.
+
+
+LINK MANAGEMENT
+===============
+
+create-link *node-id* *port-id* *node-id* *port-id* [*properties...*]
+ Create a link between 2 nodes and ports.
+
+ Port *ids* can be *-1* to automatically select an available port.
+
+ Properties are key=value pairs separated by whitespace.
+
+ This command returns a *link variable*.
+
+GLOBALS MANAGEMENT
+==================
+
+destroy *object-id*
+ Destroy a global object.
+
+
+PARAMETER MANAGEMENT
+====================
+
+enum-params *object-id* *param-id*
+ Enumerate params of an object.
+
+ *param-id* can also be given as the param short name.
+
+set-param *object-id* *param-id* *param-json*
+ Set param of an object.
+
+ *param-id* can also be given as the param short name.
+
+PERMISSION MANAGEMENT
+=====================
+
+permissions *client-id* *object-id* *permission*
+ Set permissions for a client.
+
+ *object-id* can be *-1* to set the default permissions.
+
+get-permissions *client-id*
+ Get permissions of a client.
+
+
+COMMAND MANAGEMENT
+==================
+
+send-command *object-id*
+ Send a command to an object.
+
+
+EXAMPLES
+========
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``pw-mon(1)``,
diff --git a/man/pw-dot.1.rst.in b/man/pw-dot.1.rst.in
new file mode 100644
index 0000000..4599539
--- /dev/null
+++ b/man/pw-dot.1.rst.in
@@ -0,0 +1,65 @@
+pw-dot
+######
+
+---------------------------
+The PipeWire dot graph dump
+---------------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-dot** [*options*]
+
+DESCRIPTION
+===========
+
+Create a .dot file of the PipeWire graph.
+
+The .dot file can then be visualized with a tool like **dotty**
+or rendered to a PNG file with ``dot -Tpng pw.dot -o pw.png``.
+
+OPTIONS
+=======
+
+-r | --remote=NAME
+ The name the remote instance to connect to. If left unspecified,
+ a connection is made to the default PipeWire instance.
+
+-h | --help
+ Show help.
+
+--version
+ Show version information.
+
+-a | --all
+ Show all object types.
+
+-s | --smart
+ Show linked objects only.
+
+-d | --detail
+ Show all object properties.
+
+-o FILE | --output=FILE
+ Output file name (Default pw.dot). Use - for stdout.
+
+-L | --lr
+ Lay the graph from left to right, instead of dot's default top to bottom.
+
+-9 | --90
+ Lay the graph using 90-degree angles in edges.
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``pw-cli(1)``,
+``pw-mon(1)``,
diff --git a/man/pw-jack.1.rst.in b/man/pw-jack.1.rst.in
new file mode 100644
index 0000000..0781b3d
--- /dev/null
+++ b/man/pw-jack.1.rst.in
@@ -0,0 +1,65 @@
+pw-jack
+#######
+
+----------------------------
+Use PipeWire instead of JACK
+----------------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-jack** [*options*] *COMMAND* [*FILE*]
+
+DESCRIPTION
+===========
+
+**pw-jack** modifies the ``LD_LIBRARY_PATH`` environment
+variable so that applications will load PipeWire's reimplementation
+of the JACK client libraries instead of JACK's own
+libraries. This results in JACK clients being redirected to
+PipeWire.
+
+If PipeWire's reimplementation of the JACK client libraries
+has been installed as a system-wide replacement for JACK's
+own libraries, then the whole system already behaves in that way,
+in which case **pw-jack** has no practical effect.
+
+OPTIONS
+=======
+
+-h
+ Show help.
+
+-r NAME
+ The name of the remote instance to connect to. If left
+ unspecified, a connection is made to the default PipeWire
+ instance.
+
+-v
+ Verbose operation.
+
+EXAMPLES
+========
+
+| **pw-jack** sndfile-jackplay /usr/share/sounds/freedesktop/stereo/bell.oga
+
+NOTES
+=====
+
+Using PipeWire for audio is currently considered to be
+experimental.
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>;
+PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``jackd(1)``,
diff --git a/man/pw-link.1.rst.in b/man/pw-link.1.rst.in
new file mode 100644
index 0000000..7d1001c
--- /dev/null
+++ b/man/pw-link.1.rst.in
@@ -0,0 +1,139 @@
+pw-link
+#######
+
+-------------------------
+The PipeWire Link Command
+-------------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-link** [*options*] -o|-i|-l [*out-pattern*] [*in-pattern*]
+
+| **pw-link** [*options*] *output* *input*
+
+| **pw-link** [*options*] -d *output* *input*
+
+| **pw-link** [*options*] -d *link-id*
+
+DESCRIPTION
+===========
+
+List, create and destroy links between PipeWire ports.
+
+COMMON OPTIONS
+==============
+
+-r | --remote=NAME
+ The name the *remote* instance to monitor. If left unspecified,
+ a connection is made to the default PipeWire instance.
+
+-h | --help
+ Show help.
+
+--version
+ Show version information.
+
+LISTING PORTS AND LINKS
+=======================
+
+Specify one of -o, -i or -l to list the matching optional input and
+output ports and their links.
+
+-o | --output
+ List output ports
+
+-i | --input
+ List output ports
+
+-l | --links
+ List links
+
+-m | --monitor
+ Monitor links and ports. **pw-link** will not exit but monitor and
+ print new and destroyed ports or links.
+
+-I | --id
+ List IDs. Also list the unique link and port ids.
+
+-v | --verbose
+ Verbose port properties. Also list the port-object-path and the port-alias.
+
+CONNECTING PORTS
+================
+
+Without any list option (-i, -o or -l), the given ports will be linked.
+Valid port specifications are:
+
+*port-id*
+ As obtained with the -I option when listing ports.
+
+*node-name:port-name*
+ As obtained when listing ports.
+
+*port-object-path*
+ As obtained from the first alternative name for the port when listing
+ them with the -v option.
+
+*port-alias*
+ As obtained from the second alternative name for the ports when listing
+ them with the -v option.
+
+Extra options when linking can be given:
+
+-L | --linger
+ Linger. Will create a link that exists after **pw-link** is destroyed.
+ This is the default behaviour, unless the -m option is given.
+
+-P | --passive
+ Passive link. A passive link will keep both nodes it links inactive
+ unless another non-passive link is activating the nodes. You can use this
+ to link a sink to a filter and have them both suspended when nothing else
+ is linked to either of them.
+
+-p | --props=PROPS
+ Properties as JSON object. Give extra properties when creaing the link.
+
+DISCONNECTING PORTS
+===================
+
+When the -d option is given, an existing link between port is destroyed.
+
+To disconnect port, a single *link-id*, as obtained when listing links with
+the -I option, or two port specifications can be given. See the connecting
+ports section for valid port specifications.
+
+-d | --disconnect
+ Disconnect ports
+
+EXAMPLES
+========
+
+**pw-link** -iol
+ List all port and their links.
+
+**pw-link** -lm
+ List all links and monitor changes until **pw-link** is stopped.
+
+**pw-link** paplay:output_FL alsa_output.pci-0000_00_1b.0.analog-stereo:playback_FL
+ Link the given output port to the input port.
+
+**pw-link** -lI
+ List links and their Id.
+
+**pw-link** -d 89
+ Destroy the link with id 89.
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``pw-cli(1)``,
diff --git a/man/pw-metadata.1.rst.in b/man/pw-metadata.1.rst.in
new file mode 100644
index 0000000..f83618b
--- /dev/null
+++ b/man/pw-metadata.1.rst.in
@@ -0,0 +1,67 @@
+pw-metadata
+###########
+
+---------------------
+The PipeWire metadata
+---------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-metadata** [*options*] [*id* [*key* [*value* [*type* ] ] ] ]
+
+DESCRIPTION
+===========
+
+Monitor, set and delete metadata on PipeWire objects.
+
+Metadata are key/type/value triplets attached to objects identified
+by *id*. The metadata is shared between all applications
+binding to the same metadata object. When an object is destroyed, all its
+metadata is automatically removed.
+
+When no *value* is given, **pw-metadata** will query and
+log the metadata matching the optional arguments *id*
+and *key*. Without any arguments, all metadata is displayed.
+
+When *value* is given, **pw-metadata** will set the
+metadata for *id* and *key* to *value* and
+an optional *type*.
+
+OPTIONS
+=======
+
+-r | --remote=NAME
+ The name the remote instance to use. If left unspecified,
+ a connection is made to the default PipeWire instance.
+
+-h | --help
+ Show help.
+
+--version
+ Show version information.
+
+-m | --monitor
+ Keeps running and log the changes to the metadata.
+
+-d | --delete
+
+ Delete all metadata for *id* or for the
+ specified *key* of object *id*
+
+ Without any option, all metadata is removed
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``pw-mon(1)``,
+``pw-cli(1)``,
diff --git a/man/pw-mididump.1.rst.in b/man/pw-mididump.1.rst.in
new file mode 100644
index 0000000..bb56ec6
--- /dev/null
+++ b/man/pw-mididump.1.rst.in
@@ -0,0 +1,49 @@
+pw-mididump
+###########
+
+----------------------
+The PipeWire MIDI dump
+----------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-mididump** [*options*] [*FILE*]
+
+DESCRIPTION
+===========
+
+Dump MIDI messages to stdout.
+
+When a MIDI file is given, the events inside the file are printed.
+
+When no file is given, **pw-mididump** creates a PipeWire
+MIDI input stream and will print all MIDI events received on the port to
+stdout.
+
+OPTIONS
+=======
+
+-r | --remote=NAME
+ The name the remote instance to monitor. If left unspecified,
+ a connection is made to the default PipeWire instance.
+
+-h | --help
+ Show help.
+
+--version
+ Show version information.
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``pw-cat(1)``,
diff --git a/man/pw-mon.1.rst.in b/man/pw-mon.1.rst.in
new file mode 100644
index 0000000..775de0a
--- /dev/null
+++ b/man/pw-mon.1.rst.in
@@ -0,0 +1,46 @@
+pw-mon
+######
+
+--------------------
+The PipeWire monitor
+--------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-mon** [*options*]
+
+DESCRIPTION
+===========
+
+Monitor objects on the PipeWire instance.
+
+OPTIONS
+=======
+
+-r | --remote=NAME
+ The name the *remote* instance to monitor. If left unspecified,
+ a connection is made to the default PipeWire instance.
+
+-h | --help
+ Show help.
+
+--version
+ Show version information.
+
+-N | --color=WHEN
+ Whether to use color, one of 'never', 'always', or 'auto'. The
+ default is 'auto'. **-N** is equivalent to **--color=never**.
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
diff --git a/man/pw-profiler.1.rst.in b/man/pw-profiler.1.rst.in
new file mode 100644
index 0000000..6fb57c8
--- /dev/null
+++ b/man/pw-profiler.1.rst.in
@@ -0,0 +1,57 @@
+pw-profiler
+###########
+
+---------------------
+The PipeWire profiler
+---------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-profiler** [*options*]
+
+DESCRIPTION
+===========
+
+Start profiling a PipeWire instance.
+
+If the server has the profiler module loaded, this program will
+connect to it and log the profiler data. Profiler data contains
+times and durations when processing nodes and devices started and
+completed.
+
+When this program is stopped, a set of **gnuplot** files and a script to generate
+SVG files from the .plot files is generated, along with a .html file to
+visualize the profiling results in a browser.
+
+This function uses the same data used by *pw-top*.
+
+OPTIONS
+=======
+
+-r | --remote=NAME
+ The name the remote instance to monitor. If left unspecified,
+ a connection is made to the default PipeWire instance.
+
+-h | --help
+ Show help.
+
+--version
+ Show version information.
+
+-o | --output=FILE
+ Profiler output name (default "profiler.log").
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``pw-top(1)``,
diff --git a/man/pw-top.1.rst.in b/man/pw-top.1.rst.in
new file mode 100644
index 0000000..ab8569b
--- /dev/null
+++ b/man/pw-top.1.rst.in
@@ -0,0 +1,178 @@
+pw-top
+######
+
+---------------------------
+The PipeWire process viewer
+---------------------------
+
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+| **pw-top** [*options*]
+
+DESCRIPTION
+===========
+
+The *pw-top* program provides a dynamic real-time view of the pipewire
+node and device statistics.
+
+A hierarchical view is shown of Driver nodes and follower nodes. The Driver
+nodes are actively using a timer to schedule dataflow in the followers. The
+followers of a driver node as shown below their driver with a + sign in
+a tree-like representation.
+
+The columns presented are as follows:
+
+S
+ Node status.
+ E = ERROR
+ C = CREATING
+ S = SUSPENDED
+ I = IDLE
+ R = RUNNING
+
+ID
+ The ID of the pipewire node/device, as found in *pw-dump* and *pw-cli*
+
+QUANT
+ The current quantum (for drivers) and the suggested quantum for follower
+ nodes.
+
+ The quantum by itself needs to be divided by the RATE column to calculate
+ the duration of a scheduling period in fractions of a second.
+
+ For a QUANT of 1024 and a RATE of 48000, the duration of one period in the
+ graph is 1024/48000 or 21.3 milliseconds.
+
+ Follower nodes can have a 0 QUANT field, which means that the node does not
+ have a suggestion for the quantum and thus uses what the driver selected.
+
+ The driver will use the lowest quantum of any of the followers. If none of
+ the followers select a quantum, the default quantum in the pipewire configuration
+ file will be used.
+
+ The QUANT on the drivers usually translates directly into the number of audio
+ samples processed per processing cycle of the graph.
+
+ See also https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/FAQ#pipewire-buffering-explained
+
+RATE
+ The current rate (for drivers) and the suggested rate for follower
+ nodes.
+
+ This is the rate at which the *graph* processes data and needs to be combined with
+ the QUANT value to derive the duration of a processing cycle in the graph.
+
+ Some nodes can have a 0 RATE, which means that they don't have any rate
+ suggestion for the graph. Nodes that suggest a rate can make the graph switch
+ rates if the graph is otherwise idle and the new rate is allowed as
+ a possible graph rate (see the pipewire configuration file).
+
+ The RATE on (audio) driver nodes usually also translates directly to the
+ samplerate used by the device. Although some devices might not be able to
+ operate at the given samplerate, in which case resampling will need to be
+ done. The negotiated samplerate with the device and stream can be found in
+ the FORMAT column.
+
+WAIT
+ The waiting time of a node is the elapsed time between when the node
+ is ready to start processing and when it actually started processing.
+
+ For Driver nodes, this is the time between when the node wakes up to
+ start processing the graph and when the driver (and thus also the graph)
+ completes a cycle. The WAIT time for driver is thus the elapsed time
+ processing the graph.
+
+ For follower nodes, it is the time spent between being woken up (when all
+ dependencies of the node are satisfied) and when processing starts. The
+ WAIT time for follower nodes is thus mostly caused by context switching.
+
+ A value of --- means that the node was not signaled. A value of +++
+ means that the node was signaled but not awake.
+
+BUSY
+ The processing time is started when the node starts processing until it
+ completes and wakes up the next nodes in the graph.
+
+ A value of --- means that the node was not started. A value of +++
+ means that the node was started but did not complete.
+
+W/Q
+ Ratio of WAIT / QUANT.
+
+ The W/Q time of the driver node is a good measure of the graph
+ load. The running averages of the driver W/Q ratios are used as the DSP
+ load in other (JACK) tools.
+
+ Values of --- and +++ are copied from the WAIT column.
+
+B/Q
+ Ratio of BUSY / QUANT
+
+ This is a good measure of the load of a particular driver or follower
+ node.
+
+ Values of --- and +++ are copied from the BUSY column.
+
+ERR
+ Total of Xruns and Errors
+
+ Xruns for drivers are when the graph did not complete a cycle. This can
+ be because a node in the graph also has an Xrun. It can also be caused when
+ scheduling delays cause a deadline to be missed, causing a hardware
+ Xrun.
+
+ Xruns for followers are incremented when the node started processing but
+ did not complete before the end of the graph cycle deadline.
+
+FORMAT
+ The format used by the driver node or the stream. This is the hardware format
+ negotiated with the device or stream.
+
+ If the stream of driver has a different rate than the graph, resampling will
+ be done.
+
+ For raw audio formats, the layout is <sampleformat> <channels> <samplerate>.
+
+ For IEC958 passthrough audio formats, the layout is IEC958 <codec> <samplerate>.
+
+ For DSD formats, the layout is <dsd-rate> <channels>.
+
+ For Video formats, the layout is <pixelformat> <width>x<height>.
+
+NAME
+ Name assigned to the device/node, as found in *pw-dump* node.name
+
+ Names are prefixed by *+* when they are linked to a driver (entry above with no +)
+
+
+OPTIONS
+=======
+
+-h | --help
+ Show help.
+
+-r | --remote=NAME
+ The name the *remote* instance to monitor. If left unspecified,
+ a connection is made to the default PipeWire instance.
+
+--version
+ Show version information.
+
+
+AUTHORS
+=======
+
+The PipeWire Developers <@PACKAGE_BUGREPORT@>; PipeWire is available from @PACKAGE_URL@
+
+SEE ALSO
+========
+
+``pipewire(1)``,
+``pw-dump(1)``,
+``pw-cli(1)``,
+``pw-profiler(1)``,
+
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..f99350e
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,495 @@
+project('pipewire', ['c' ],
+ version : '0.3.65',
+ license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ],
+ meson_version : '>= 0.59.0',
+ default_options : [ 'warning_level=3',
+ 'c_std=gnu11',
+ 'cpp_std=c++17',
+ 'b_pie=true',
+ #'b_sanitize=address,undefined',
+ 'buildtype=debugoptimized' ])
+
+pipewire_version = meson.project_version()
+version_arr = pipewire_version.split('.')
+pipewire_version_major = version_arr[0]
+pipewire_version_minor = version_arr[1]
+pipewire_version_micro = version_arr[2]
+if version_arr.length() == 4
+ pipewire_version_nano = version_arr[3]
+else
+ pipewire_version_nano = 0
+endif
+
+spaversion = '0.2'
+apiversion = '0.3'
+soversion = 0
+libversion = '@0@.@1@.0'.format(soversion, pipewire_version_minor.to_int() * 100 + pipewire_version_micro.to_int())
+
+pipewire_name = 'pipewire-@0@'.format(apiversion)
+spa_name = 'spa-@0@'.format(spaversion)
+
+prefix = get_option('prefix')
+pipewire_bindir = prefix / get_option('bindir')
+pipewire_datadir = prefix / get_option('datadir')
+pipewire_libdir = prefix / get_option('libdir')
+pipewire_libexecdir = prefix / get_option('libexecdir')
+pipewire_localedir = prefix / get_option('localedir')
+pipewire_sysconfdir = prefix / get_option('sysconfdir')
+
+pipewire_configdir = pipewire_sysconfdir / 'pipewire'
+pipewire_confdatadir = pipewire_datadir / 'pipewire'
+modules_install_dir = pipewire_libdir / pipewire_name
+
+if host_machine.system() == 'linux'
+ # glibc ld.so interprets ${LIB} in a library loading path with an
+ # appropriate value for the current architecture, typically something
+ # like lib, lib64 or lib/x86_64-linux-gnu.
+ # This allows the same pw-jack script to work for both 32- and 64-bit
+ # applications on biarch/multiarch distributions, by setting something
+ # like LD_LIBRARY_PATH='/usr/${LIB}/pipewire-0.3/jack'.
+ # Note that ${LIB} is a special token expanded by the runtime linker,
+ # not an environment variable, and must be passed through literally.
+ modules_install_dir_dlopen = prefix / '${LIB}' / pipewire_name
+else
+ modules_install_dir_dlopen = modules_install_dir
+endif
+
+spa_plugindir = pipewire_libdir / spa_name
+spa_datadir = pipewire_datadir / spa_name
+
+alsadatadir = pipewire_datadir / 'alsa-card-profile' / 'mixer'
+
+pipewire_headers_dir = pipewire_name / 'pipewire'
+
+pkgconfig = import('pkgconfig')
+
+cc = meson.get_compiler('c')
+
+common_flags = [
+ '-fvisibility=hidden',
+ '-fno-strict-aliasing',
+ '-Werror=suggest-attribute=format',
+ '-Wsign-compare',
+ '-Wpointer-arith',
+ '-Wpointer-sign',
+ '-Wformat',
+ '-Wformat-security',
+ '-Wimplicit-fallthrough',
+ '-Wmissing-braces',
+ '-Wtype-limits',
+ '-Wvariadic-macros',
+ '-Wmaybe-uninitialized',
+ '-Wno-missing-field-initializers',
+ '-Wno-unused-parameter',
+ '-Wno-pedantic',
+ '-Wold-style-declaration',
+ '-Wdeprecated-declarations',
+ '-Wunused-result',
+]
+
+cc_flags = common_flags + [
+ '-D_GNU_SOURCE',
+ '-DFASTPATH',
+# '-DSPA_DEBUG_MEMCPY',
+]
+add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c')
+
+have_cpp = add_languages('cpp', native: false, required : false)
+
+if have_cpp
+ cxx = meson.get_compiler('cpp')
+ cxx_flags = common_flags + [ '-Wno-c99-designator' ]
+ add_project_arguments(cxx.get_supported_arguments(cxx_flags), language: 'cpp')
+endif
+
+sse_args = '-msse'
+sse2_args = '-msse2'
+ssse3_args = '-mssse3'
+sse41_args = '-msse4.1'
+fma_args = '-mfma'
+avx_args = '-mavx'
+avx2_args = '-mavx2'
+
+have_sse = cc.has_argument(sse_args)
+have_sse2 = cc.has_argument(sse2_args)
+have_ssse3 = cc.has_argument(ssse3_args)
+have_sse41 = cc.has_argument(sse41_args)
+have_fma = cc.has_argument(fma_args)
+have_avx = cc.has_argument(avx_args)
+have_avx2 = cc.has_argument(avx2_args)
+
+have_neon = false
+if host_machine.cpu_family() == 'aarch64'
+ if cc.compiles('''
+ #include <arm_neon.h>
+ int main () {
+ float *s;
+ asm volatile(
+ " ld1 { v0.4s }, [%[s]], #16\n"
+ " fcvtzs v0.4s, v0.4s, #31\n"
+ : [s] "+r" (s) : :);
+ }
+ ''',
+ name : 'aarch64 Neon Support')
+ neon_args = []
+ have_neon = true
+
+ endif
+elif cc.has_argument('-mfpu=neon')
+ if cc.compiles('''
+ #include <arm_neon.h>
+ int main () {
+ float *s;
+ asm volatile(
+ " vld1.32 { q0 }, [%[s]]!\n"
+ " vcvt.s32.f32 q0, q0, #31\n"
+ : [s] "+r" (s) : :);
+ }
+ ''',
+ args: '-mfpu=neon',
+ name : 'arm Neon Support')
+ neon_args = ['-mfpu=neon']
+ have_neon = true
+ endif
+endif
+
+libatomic = cc.find_library('atomic', required : false)
+
+test_8_byte_atomic = '''
+#include <stdint.h>
+
+int main(void)
+{
+ int64_t eight;
+ __atomic_fetch_add(&eight, 123, __ATOMIC_SEQ_CST);
+ return 0;
+}
+'''
+
+# We currently assume that libatomic is unnecessary for 4-byte atomic
+# operations on any reasonable architecture.
+if cc.links(
+ test_8_byte_atomic,
+ name : '8-byte __atomic_fetch_add without libatomic')
+ atomic_dep = dependency('', required: false)
+elif cc.links(
+ test_8_byte_atomic,
+ dependencies : libatomic,
+ name : '8-byte __atomic_fetch_add with libatomic')
+ atomic_dep = libatomic
+else
+ error('8-byte atomic operations are required')
+endif
+
+versiondata = configuration_data()
+versiondata.set('PIPEWIRE_VERSION_MAJOR', pipewire_version_major)
+versiondata.set('PIPEWIRE_VERSION_MINOR', pipewire_version_minor)
+versiondata.set('PIPEWIRE_VERSION_MICRO', pipewire_version_micro)
+versiondata.set('PIPEWIRE_VERSION_NANO', pipewire_version_nano)
+versiondata.set_quoted('PIPEWIRE_API_VERSION', apiversion)
+
+cdata = configuration_data()
+cdata.set_quoted('PIPEWIRE_CONFDATADIR', pipewire_confdatadir)
+cdata.set_quoted('LOCALEDIR', pipewire_localedir)
+cdata.set_quoted('LIBDIR', pipewire_libdir)
+cdata.set_quoted('GETTEXT_PACKAGE', meson.project_name())
+cdata.set_quoted('PACKAGE', 'pipewire')
+cdata.set_quoted('PACKAGE_NAME', 'PipeWire')
+cdata.set_quoted('PACKAGE_STRING', 'PipeWire @0@'.format(pipewire_version))
+cdata.set_quoted('PACKAGE_TARNAME', 'pipewire')
+cdata.set_quoted('PACKAGE_URL', 'https://pipewire.org')
+cdata.set_quoted('PACKAGE_VERSION', pipewire_version)
+cdata.set_quoted('MODULEDIR', modules_install_dir)
+cdata.set_quoted('PIPEWIRE_CONFIG_DIR', pipewire_configdir)
+cdata.set_quoted('PLUGINDIR', spa_plugindir)
+cdata.set_quoted('SPADATADIR', spa_datadir)
+cdata.set_quoted('PA_ALSA_PATHS_DIR', alsadatadir / 'paths')
+cdata.set_quoted('PA_ALSA_PROFILE_SETS_DIR', alsadatadir / 'profile-sets')
+
+if host_machine.endian() == 'big'
+ cdata.set('WORDS_BIGENDIAN', 1)
+endif
+
+check_headers = [
+ ['sys/mount.h', 'HAVE_SYS_MOUNT_H'],
+ ['sys/param.h', 'HAVE_SYS_PARAM_H'],
+ ['sys/random.h', 'HAVE_SYS_RANDOM_H'],
+ ['sys/vfs.h', 'HAVE_SYS_VFS_H'],
+ ['pwd.h', 'HAVE_PWD_H'],
+]
+
+foreach h : check_headers
+ cdata.set(h.get(1), cc.has_header(h.get(0)))
+endforeach
+
+cdata.set('HAVE_PIDFD_OPEN',
+ cc.get_define('SYS_pidfd_open', prefix: '#include <sys/syscall.h>') != '')
+
+systemd = dependency('systemd', required: get_option('systemd'))
+systemd_dep = dependency('libsystemd',required: get_option('systemd'))
+summary({'systemd conf data': systemd.found()}, bool_yn: true)
+summary({'libsystemd': systemd_dep.found()}, bool_yn: true)
+cdata.set('HAVE_SYSTEMD', systemd.found() and systemd_dep.found())
+
+configinc = include_directories('.')
+includes_inc = include_directories('include')
+pipewire_inc = include_directories('src')
+
+makedata = configuration_data()
+makedata.set('BUILD_ROOT', meson.project_build_root())
+makedata.set('SOURCE_ROOT', meson.project_source_root())
+makedata.set('VERSION', pipewire_version)
+if version_arr.length() == 4
+ makedata.set('TAG', 'HEAD')
+else
+ makedata.set('TAG', pipewire_version)
+endif
+
+configure_file(input : 'Makefile.in',
+ output : 'Makefile',
+ configuration : makedata)
+
+# Find dependencies
+mathlib = cc.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')
+dbus_dep = dependency('dbus-1', required : get_option('dbus'))
+summary({'dbus (Bluetooth, rt, portal, pw-reserve)': dbus_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+cdata.set('HAVE_DBUS', dbus_dep.found())
+sdl_dep = dependency('sdl2', required : get_option('sdl2'))
+summary({'SDL2 (video examples)': sdl_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+drm_dep = dependency('libdrm', required : false)
+readline_dep = dependency('readline', required : get_option('readline'))
+
+if not readline_dep.found()
+ readline_dep = cc.find_library('readline', required : get_option('readline'))
+endif
+
+# Both the FFmpeg SPA plugin and the pw-cat FFmpeg integration use libavcodec.
+# But only the latter also needs libavformat.
+# Search for these libraries here, globally, so both of these subprojects can reuse the results.
+pw_cat_ffmpeg = get_option('pw-cat-ffmpeg')
+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())
+else
+ avcodec_dep = dependency('', required: false)
+endif
+cdata.set('HAVE_PW_CAT_FFMPEG_INTEGRATION', pw_cat_ffmpeg.allowed())
+
+summary({'readline (for pw-cli)': readline_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+cdata.set('HAVE_READLINE', readline_dep.found())
+ncurses_dep = dependency('ncursesw', required : false)
+sndfile_dep = dependency('sndfile', version : '>= 1.0.20', required : get_option('sndfile'))
+summary({'sndfile': sndfile_dep.found()}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump/filter-chain')
+cdata.set('HAVE_SNDFILE', sndfile_dep.found())
+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'))
+summary({'Avahi DNS-SD (Zeroconf)': avahi_dep.found()}, bool_yn: true,
+ section: 'Streaming between daemons')
+
+x11_dep = dependency('x11-xcb', required : get_option('x11'))
+summary({'X11 (x11-bell)': x11_dep.found()}, bool_yn: true,
+ section: 'Misc dependencies')
+
+xfixes_dep = dependency('xfixes', required : get_option('x11-xfixes'), version: '>= 6')
+cdata.set('HAVE_XFIXES_6', xfixes_dep.found())
+
+canberra_dep = dependency('libcanberra', required : get_option('libcanberra'))
+summary({'libcanberra (x11-bell)': canberra_dep.found()}, bool_yn: true,
+ section: 'Misc dependencies')
+
+libusb_dep = dependency('libusb-1.0', required : get_option('libusb'))
+summary({'libusb (Bluetooth quirks)': libusb_dep.found()}, bool_yn: true, section: 'Backend')
+cdata.set('HAVE_LIBUSB', libusb_dep.found())
+
+cap_lib = dependency('libcap', required : false)
+cdata.set('HAVE_LIBCAP', cap_lib.found())
+
+glib2_dep = dependency('glib-2.0', required : get_option('flatpak'))
+summary({'GLib-2.0 (Flatpak support)': glib2_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+flatpak_support = glib2_dep.found()
+cdata.set('HAVE_GLIB2', flatpak_support)
+
+gio_dep = dependency('gio-2.0', version : '>= 2.26.0', required : get_option('gsettings'))
+summary({'GIO (GSettings)': gio_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+
+gst_option = get_option('gstreamer')
+gst_deps_def = {
+ 'glib-2.0': {'version': '>=2.32.0'},
+ 'gobject-2.0': {},
+ 'gmodule-2.0': {},
+ 'gio-2.0': {},
+ 'gio-unix-2.0': {},
+ 'gstreamer-1.0': {'version': '>= 1.10.0'},
+ 'gstreamer-plugins-base-1.0': {},
+ 'gstreamer-video-1.0': {},
+ 'gstreamer-audio-1.0': {},
+ 'gstreamer-allocators-1.0': {},
+}
+
+gst_dep = []
+foreach depname, kwargs: gst_deps_def
+ dep = dependency(depname, required: gst_option, kwargs: kwargs)
+ summary({depname: dep.found()}, bool_yn: true, section: 'GStreamer modules')
+ if not dep.found()
+ # Beware, there's logic below depending on the array clear here!
+ gst_dep = []
+ if get_option('gstreamer-device-provider').enabled()
+ error('`gstreamer-device-provider` is enabled but `@0@` was not found.'.format(depname))
+ endif
+ break
+ endif
+ gst_dep += [dep]
+endforeach
+
+# This code relies on the array being empty if any dependency was not found
+gst_dp_found = gst_dep.length() > 0
+summary({'gstreamer-device-provider': gst_dp_found}, bool_yn: true, section: 'Backend')
+
+cdata.set('HAVE_GSTREAMER_DEVICE_PROVIDER', get_option('gstreamer-device-provider').allowed())
+
+webrtc_dep = dependency('webrtc-audio-processing',
+ version : ['>= 0.2', '< 1.0'],
+ required : get_option('echo-cancel-webrtc'))
+summary({'WebRTC Echo Canceling': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies')
+cdata.set('HAVE_WEBRTC', webrtc_dep.found())
+
+# On FreeBSD and MidnightBSD, epoll-shim library is required for eventfd() and timerfd()
+epoll_shim_dep = (host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd'
+ ? dependency('epoll-shim', required: true)
+ : dependency('', required: false))
+
+libinotify_dep = (host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd'
+ ? dependency('libinotify', required: true)
+ : dependency('', required: false))
+
+# On FreeBSD and MidnightBSD, libintl library is required for gettext
+libintl_dep = cc.find_library('intl', required: false)
+if not libintl_dep.found()
+ libintl_dep = dependency('intl', required: false)
+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)
+summary({'pipewire-alsa': alsa_dep.found()}, bool_yn: true)
+
+if host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd'
+# On FreeBSD and MidnightBSD the OpenSSL library may come from base or a package.
+# Check for a package first and fallback to the base library if we can't find it via pkgconfig
+ openssl_lib = dependency('openssl', required: false)
+ if not openssl_lib.found()
+ openssl_lib = declare_dependency(link_args : [ '-lssl', '-lcrypto'])
+ endif
+else
+ openssl_lib = dependency('openssl', required: get_option('raop'))
+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)
+cdata.set('HAVE_LILV', lilv_lib.found())
+
+check_functions = [
+ ['gettid', '#include <unistd.h>', ['-D_GNU_SOURCE'], []],
+ ['memfd_create', '#include <sys/mman.h>', ['-D_GNU_SOURCE'], []],
+ ['getrandom', '#include <stddef.h>\n#include <sys/random.h>', ['-D_GNU_SOURCE'], []],
+ ['reallocarray', '#include <stdlib.h>', ['-D_GNU_SOURCE'], []],
+ ['sigabbrev_np', '#include <string.h>', ['-D_GNU_SOURCE'], []],
+ ['XSetIOErrorExitHandler', '#include <X11/Xlib.h>', [], [x11_dep]],
+]
+
+foreach f : check_functions
+ cdata.set('HAVE_' + f.get(0).to_upper(),
+ cc.has_function(f.get(0),
+ prefix: f.get(1),
+ args: f.get(2),
+ dependencies: f.get(3)))
+endforeach
+
+installed_tests_metadir = pipewire_datadir / 'installed-tests' / pipewire_name
+installed_tests_execdir = pipewire_libexecdir / 'installed-tests' / pipewire_name
+installed_tests_enabled = get_option('installed_tests').allowed()
+installed_tests_template = files('template.test.in')
+
+if get_option('tests').allowed()
+ gstack = find_program('gstack', required : false)
+ cdata.set('HAVE_GSTACK', gstack.found())
+endif
+
+subdir('po')
+subdir('spa')
+subdir('src')
+
+if get_option('tests').allowed()
+ subdir('test')
+endif
+
+configure_file(output : 'config.h',
+ configuration : cdata)
+
+if get_option('pipewire-jack').allowed()
+ subdir('pipewire-jack')
+endif
+if get_option('pipewire-v4l2').allowed()
+ subdir('pipewire-v4l2')
+endif
+
+if alsa_dep.found()
+ subdir('pipewire-alsa/alsa-plugins')
+ subdir('pipewire-alsa/conf')
+ subdir('pipewire-alsa/tests')
+endif
+
+generate_manpages = false
+if get_option('man').allowed()
+ rst2man = find_program('rst2man', required: false)
+ if not rst2man.found()
+ rst2man = find_program('rst2man.py', required: get_option('man'))
+ endif
+ if rst2man.found()
+ generate_manpages = true
+ endif
+endif
+
+summary({'Manpage generation': generate_manpages}, bool_yn: true)
+subdir('man')
+
+doxygen = find_program('doxygen', required : get_option('docs'))
+if doxygen.found()
+ subdir('doc')
+endif
+
+setenv = find_program('pw-uninstalled.sh')
+run_target('pw-uninstalled',
+ command : [setenv,
+ '-b@0@'.format(meson.project_build_root()),
+ '-v@0@'.format(pipewire_version)]
+)
+
+devenv = environment()
+
+builddir = meson.project_build_root()
+srcdir = meson.project_source_root()
+
+devenv.set('PIPEWIRE_CONFIG_DIR', pipewire_dep.get_variable('confdatadir'))
+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.set('PW_UNINSTALLED', '1')
+
+meson.add_devenv(devenv)
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..b4555f2
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,283 @@
+option('docdir',
+ type : 'string',
+ description : 'Directory for installing documentation to (defaults to pipewire_datadir/doc/meson.project_name() )')
+option('docs',
+ description: 'Build documentation',
+ type: 'feature',
+ value: 'disabled')
+option('examples',
+ description: 'Build examples',
+ type: 'feature',
+ value: 'enabled')
+option('man',
+ description: 'Build manpages',
+ type: 'feature',
+ value: 'auto')
+option('tests',
+ description: 'Build tests',
+ type: 'feature',
+ value: 'enabled',
+ yield : true)
+option('installed_tests',
+ description: 'Install manual and automated test executables',
+ type: 'feature',
+ value: 'disabled')
+option('gstreamer',
+ description: 'Build GStreamer plugins',
+ type: 'feature',
+ value: 'auto')
+option('gstreamer-device-provider',
+ description: 'Build GStreamer device provider plugin',
+ type: 'feature',
+ value: 'auto')
+option('systemd',
+ description: 'Enable systemd integration',
+ type: 'feature',
+ value: 'auto')
+option('systemd-system-service',
+ description: 'Install systemd system service file',
+ type: 'feature',
+ value: 'disabled')
+option('systemd-user-service',
+ description: 'Install systemd user service file (ignored without systemd)',
+ type: 'feature',
+ value: 'enabled')
+option('pipewire-alsa',
+ description: 'Enable pipewire-alsa integration',
+ type: 'feature',
+ value: 'auto')
+option('pipewire-jack',
+ description: 'Enable pipewire-jack integration',
+ type: 'feature',
+ value: 'enabled')
+option('pipewire-v4l2',
+ description: 'Enable pipewire-v4l2 integration',
+ type: 'feature',
+ value: 'enabled')
+option('jack-devel',
+ description: 'Install jack development files',
+ type: 'boolean',
+ value: 'false')
+option('libjack-path',
+ description: 'Where to install the libjack.so library',
+ type: 'string')
+option('libv4l2-path',
+ description: 'Where to install the libpw-v4l2.so library',
+ type: 'string')
+option('spa-plugins',
+ description: 'Enable spa plugins integration',
+ type: 'feature',
+ value: 'enabled')
+option('alsa',
+ description: 'Enable alsa spa plugin integration',
+ type: 'feature',
+ value: 'auto')
+option('audiomixer',
+ description: 'Enable audiomixer spa plugin integration',
+ type: 'feature',
+ value: 'enabled')
+option('audioconvert',
+ description: 'Enable audioconvert spa plugin integration',
+ type: 'feature',
+ value: 'enabled')
+option('bluez5',
+ description: 'Enable bluez5 spa plugin integration',
+ type: 'feature',
+ value: 'auto')
+option('bluez5-backend-hsp-native',
+ description: 'Enable HSP in native backend in bluez5 spa plugin',
+ type: 'feature',
+ value: 'enabled')
+option('bluez5-backend-hfp-native',
+ description: 'Enable HFP in native backend in bluez5 spa plugin',
+ type: 'feature',
+ value: 'enabled')
+option('bluez5-backend-native-mm',
+ description: 'Enable ModemManager in native backend in bluez5 spa plugin',
+ type: 'feature',
+ value: 'disabled')
+option('bluez5-backend-ofono',
+ description: 'Enable oFono HFP backend in bluez5 spa plugin (no dependency on oFono)',
+ type: 'feature',
+ value: 'enabled')
+option('bluez5-backend-hsphfpd',
+ description: 'Enable hsphfpd backend in bluez5 spa plugin (no dependency on hsphfpd)',
+ type: 'feature',
+ value: 'enabled')
+option('bluez5-codec-aptx',
+ description: 'Enable AptX Qualcomm open source codec implementation',
+ type: 'feature',
+ value: 'auto')
+option('bluez5-codec-ldac',
+ description: 'Enable LDAC Sony open source codec implementation',
+ type: 'feature',
+ value: 'auto')
+option('bluez5-codec-aac',
+ description: 'Enable Fraunhofer FDK AAC open source codec implementation',
+ type: 'feature',
+ value: 'auto')
+option('bluez5-codec-lc3plus',
+ description: 'Enable LC3plus open source codec implementation',
+ type: 'feature',
+ value: 'auto')
+option('bluez5-codec-opus',
+ description: 'Enable Opus open source codec implementation',
+ type: 'feature',
+ value: 'auto')
+option('bluez5-codec-lc3',
+ description: 'Enable LC3 open source codec implementation',
+ type: 'feature',
+ value: 'disabled')
+option('control',
+ description: 'Enable control spa plugin integration',
+ type: 'feature',
+ value: 'enabled')
+option('audiotestsrc',
+ description: 'Enable audiotestsrc spa plugin integration',
+ type: 'feature',
+ value: 'enabled')
+option('ffmpeg',
+ description: 'Enable ffmpeg spa plugin integration',
+ type: 'feature',
+ value: 'disabled')
+option('jack',
+ description: 'Enable jack spa plugin integration',
+ type: 'feature',
+ value: 'auto')
+option('support',
+ description: 'Enable support spa plugin integration',
+ type: 'feature',
+ value: 'enabled')
+option('evl',
+ description: 'Enable EVL support spa plugin integration',
+ type: 'feature',
+ value: 'disabled')
+option('test',
+ description: 'Enable test spa plugin integration',
+ type: 'feature',
+ value: 'disabled')
+option('v4l2',
+ description: 'Enable v4l2 spa plugin integration',
+ type: 'feature',
+ value: 'auto')
+option('dbus',
+ description: 'Enable code that depends on dbus',
+ type: 'feature',
+ value: 'enabled')
+option('libcamera',
+ description: 'Enable libcamera spa plugin integration',
+ type: 'feature',
+ value: 'auto')
+option('videoconvert',
+ description: 'Enable videoconvert spa plugin integration',
+ type: 'feature',
+ value: 'enabled')
+option('videotestsrc',
+ description: 'Enable videotestsrc spa plugin integration',
+ type: 'feature',
+ value: 'enabled')
+option('volume',
+ description: 'Build the legacy volume spa plugin',
+ type: 'feature',
+ value: 'disabled')
+option('vulkan',
+ description: 'Enable vulkan spa plugin integration',
+ type: 'feature',
+ value: 'disabled')
+option('pw-cat',
+ description: 'Build pw-cat/pw-play/pw-record',
+ type: 'feature',
+ value: 'auto')
+option('pw-cat-ffmpeg',
+ description: 'Enable FFmpeg integration in pw-cat/pw-play/pw-record',
+ type: 'feature',
+ value: 'disabled')
+option('udev',
+ description: 'Enable Udev integration',
+ type: 'feature',
+ value: 'auto')
+option('udevrulesdir',
+ type : 'string',
+ description : 'Directory for udev rules (defaults to /lib/udev/rules.d)')
+option('systemd-system-unit-dir',
+ type : 'string',
+ description : 'Directory for system systemd units (defaults to /usr/lib/systemd/system)')
+option('systemd-user-unit-dir',
+ type : 'string',
+ description : 'Directory for user systemd units (defaults to /usr/lib/systemd/user)')
+option('sdl2',
+ description: 'Enable code that depends on SDL 2',
+ type: 'feature',
+ value: 'auto')
+option('sndfile',
+ description: 'Enable code that depends on libsndfile',
+ type: 'feature',
+ value: 'auto')
+option('libpulse',
+ description: 'Enable code that depends on libpulse',
+ type: 'feature',
+ value: 'auto')
+option('roc',
+ description: 'Enable code that depends on roc toolkit',
+ type: 'feature',
+ value: 'auto')
+option('avahi',
+ description: 'Enable code that depends on avahi',
+ type: 'feature',
+ value: 'auto')
+option('echo-cancel-webrtc',
+ description : 'Enable WebRTC-based echo canceller',
+ type : 'feature',
+ value : 'auto')
+option('libusb',
+ description: 'Enable code that depends on libusb',
+ type: 'feature',
+ value: 'auto')
+option('session-managers',
+ description : 'Session managers to build (can be [] for none or an absolute path)',
+ type : 'array',
+ value : ['wireplumber'])
+option('raop',
+ description: 'Enable module for Remote Audio Output Protocol',
+ type: 'feature',
+ value: 'auto')
+option('lv2',
+ description: 'Enable loading of LV2 plugins',
+ type: 'feature',
+ value: 'auto')
+option('x11',
+ description: 'Enable code that depends on X11',
+ type: 'feature',
+ value: 'auto')
+option('x11-xfixes',
+ description: 'Enable code that depends on XFixes',
+ type: 'feature',
+ value: 'auto')
+option('libcanberra',
+ description: 'Enable code that depends on libcanberra',
+ type: 'feature',
+ value: 'auto')
+option('legacy-rtkit',
+ description: 'Build legacy rtkit module',
+ type: 'boolean',
+ value: 'true')
+option('avb',
+ description: 'Enable AVB code',
+ type: 'feature',
+ value: 'auto')
+option('flatpak',
+ description: 'Enable Flatpak support',
+ type: 'feature',
+ value: 'enabled')
+option('readline',
+ description: 'Enable code that depends on libreadline',
+ type: 'feature',
+ value: 'auto')
+option('gsettings',
+ description: 'Enable code that depends on gsettings',
+ type: 'feature',
+ value: 'auto')
+option('compress-offload',
+ description: 'Enable ALSA Compress-Offload support',
+ type: 'feature',
+ value: 'disabled')
diff --git a/pipewire-alsa/alsa-plugins/ctl_pipewire.c b/pipewire-alsa/alsa-plugins/ctl_pipewire.c
new file mode 100644
index 0000000..61394e2
--- /dev/null
+++ b/pipewire-alsa/alsa-plugins/ctl_pipewire.c
@@ -0,0 +1,1496 @@
+/* CTL - PipeWire plugin
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <alsa/asoundlib.h>
+#include <alsa/control_external.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/param/props.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/metadata.h>
+
+PW_LOG_TOPIC_STATIC(alsa_log_topic, "alsa.ctl");
+#define PW_LOG_TOPIC_DEFAULT alsa_log_topic
+
+#define DEFAULT_VOLUME_METHOD "cubic"
+
+#define VOLUME_MIN ((uint32_t) 0U)
+#define VOLUME_MAX ((uint32_t) 0x10000U)
+
+struct volume {
+ uint32_t channels;
+ long values[SPA_AUDIO_MAX_CHANNELS];
+};
+
+typedef struct {
+ snd_ctl_ext_t ext;
+
+ struct pw_properties *props;
+
+ struct spa_system *system;
+ struct pw_thread_loop *mainloop;
+
+ struct pw_context *context;
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_metadata *metadata;
+ struct spa_hook metadata_listener;
+
+ int fd;
+ int last_seq;
+ int pending_seq;
+ int error;
+
+ char default_sink[1024];
+ int sink_muted;
+ struct volume sink_volume;
+
+ char default_source[1024];
+ int source_muted;
+ struct volume source_volume;
+
+ int subscribed;
+#define VOLUME_METHOD_LINEAR (0)
+#define VOLUME_METHOD_CUBIC (1)
+ int volume_method;
+
+#define UPDATE_SINK_VOL (1<<0)
+#define UPDATE_SINK_MUTE (1<<1)
+#define UPDATE_SOURCE_VOL (1<<2)
+#define UPDATE_SOURCE_MUTE (1<<3)
+ int updated;
+
+ struct spa_list globals;
+} snd_ctl_pipewire_t;
+
+static inline uint32_t volume_from_linear(float vol, int method)
+{
+ if (vol <= 0.0f)
+ vol = 0.0f;
+
+ switch (method) {
+ case VOLUME_METHOD_CUBIC:
+ vol = cbrtf(vol);
+ break;
+ }
+ return SPA_CLAMP((uint64_t)lroundf(vol * VOLUME_MAX),
+ VOLUME_MIN, VOLUME_MAX);
+}
+
+static inline float volume_to_linear(uint32_t vol, int method)
+{
+ float v = ((float)vol) / VOLUME_MAX;
+
+ switch (method) {
+ case VOLUME_METHOD_CUBIC:
+ v = v * v * v;
+ break;
+ }
+ return v;
+}
+
+struct global;
+
+struct global_info {
+ const char *type;
+ uint32_t version;
+ const void *events;
+ pw_destroy_t destroy;
+ int (*init) (struct global *g);
+};
+
+struct global {
+ struct spa_list link;
+
+ snd_ctl_pipewire_t *ctl;
+
+ const struct global_info *ginfo;
+
+ uint32_t id;
+ uint32_t permissions;
+ struct pw_properties *props;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+
+ union {
+ struct {
+#define NODE_FLAG_SINK (1<<0)
+#define NODE_FLAG_SOURCE (1<<1)
+#define NODE_FLAG_DEVICE_VOLUME (1<<2)
+#define NODE_FLAG_DEVICE_MUTE (1<<3)
+ uint32_t flags;
+ uint32_t device_id;
+ uint32_t profile_device_id;
+ int priority;
+ float volume;
+ bool mute;
+ struct volume channel_volume;
+ } node;
+ struct {
+ uint32_t active_route_output;
+ uint32_t active_route_input;
+ } device;
+ };
+};
+
+#define SOURCE_VOL_NAME "Capture Volume"
+#define SOURCE_MUTE_NAME "Capture Switch"
+#define SINK_VOL_NAME "Master Playback Volume"
+#define SINK_MUTE_NAME "Master Playback Switch"
+
+static void do_resync(snd_ctl_pipewire_t *ctl)
+{
+ ctl->pending_seq = pw_core_sync(ctl->core, PW_ID_CORE, ctl->pending_seq);
+}
+
+static int wait_resync(snd_ctl_pipewire_t *ctl)
+{
+ int res;
+ do_resync(ctl);
+
+ while (true) {
+ pw_thread_loop_wait(ctl->mainloop);
+
+ res = ctl->error;
+ if (res < 0) {
+ ctl->error = 0;
+ return res;
+ }
+
+ if (ctl->pending_seq == ctl->last_seq)
+ break;
+ }
+ return 0;
+}
+
+static struct global *find_global(snd_ctl_pipewire_t *ctl, uint32_t id,
+ const char *name, const char *type)
+{
+ struct global *g;
+ uint32_t name_id = name ? (uint32_t)atoi(name) : SPA_ID_INVALID;
+ const char *str;
+
+ spa_list_for_each(g, &ctl->globals, link) {
+ if ((g->id == id || g->id == name_id) &&
+ (type == NULL || spa_streq(g->ginfo->type, type)))
+ return g;
+ if (name != NULL && name[0] != '\0' &&
+ (str = pw_properties_get(g->props, PW_KEY_NODE_NAME)) != NULL &&
+ spa_streq(name, str))
+ return g;
+ }
+ return NULL;
+}
+
+static struct global *find_best_node(snd_ctl_pipewire_t *ctl, uint32_t flags)
+{
+ struct global *g, *best = NULL;
+ spa_list_for_each(g, &ctl->globals, link) {
+ if ((spa_streq(g->ginfo->type, PW_TYPE_INTERFACE_Node)) &&
+ (flags == 0 || (g->node.flags & flags) == flags) &&
+ (best == NULL || best->node.priority < g->node.priority))
+ best = g;
+ }
+ return best;
+}
+
+static inline int poll_activate(snd_ctl_pipewire_t *ctl)
+{
+ spa_system_eventfd_write(ctl->system, ctl->fd, 1);
+ return 1;
+}
+
+static inline int poll_deactivate(snd_ctl_pipewire_t *ctl)
+{
+ uint64_t val;
+ spa_system_eventfd_read(ctl->system, ctl->fd, &val);
+ return 1;
+}
+
+static bool volume_equal(struct volume *a, struct volume *b)
+{
+ if (a == b)
+ return true;
+ if (a->channels != b->channels)
+ return false;
+ return memcmp(a->values, b->values, sizeof(float) * a->channels) == 0;
+}
+
+static int pipewire_update_volume(snd_ctl_pipewire_t * ctl)
+{
+ bool changed = false;
+ struct global *g;
+
+ if (ctl->default_sink[0] == '\0')
+ g = find_best_node(ctl, NODE_FLAG_SINK);
+ else
+ g = find_global(ctl, SPA_ID_INVALID, ctl->default_sink,
+ PW_TYPE_INTERFACE_Node);
+
+ if (g) {
+ if (!!ctl->sink_muted != !!g->node.mute) {
+ ctl->sink_muted = g->node.mute;
+ ctl->updated |= UPDATE_SINK_MUTE;
+ changed = true;
+ }
+ if (!volume_equal(&ctl->sink_volume, &g->node.channel_volume)) {
+ ctl->sink_volume = g->node.channel_volume;
+ ctl->updated |= UPDATE_SINK_VOL;
+ changed = true;
+ }
+ }
+
+ if (ctl->default_source[0] == '\0')
+ g = find_best_node(ctl, NODE_FLAG_SOURCE);
+ else
+ g = find_global(ctl, SPA_ID_INVALID, ctl->default_source,
+ PW_TYPE_INTERFACE_Node);
+
+ if (g) {
+ if (!!ctl->source_muted != !!g->node.mute) {
+ ctl->source_muted = g->node.mute;
+ ctl->updated |= UPDATE_SOURCE_MUTE;
+ changed = true;
+ }
+ if (!volume_equal(&ctl->source_volume, &g->node.channel_volume)) {
+ ctl->source_volume = g->node.channel_volume;
+ ctl->updated |= UPDATE_SOURCE_VOL;
+ changed = true;
+ }
+ }
+
+ if (changed)
+ poll_activate(ctl);
+
+ return 0;
+}
+
+static int pipewire_elem_count(snd_ctl_ext_t * ext)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+ int count = 0, err;
+
+ assert(ctl);
+
+ if (!ctl->mainloop)
+ return -EBADFD;
+
+ pw_thread_loop_lock(ctl->mainloop);
+
+ err = ctl->error;
+ if (err < 0) {
+ ctl->error = 0;
+ count = err;
+ goto finish;
+ }
+ err = pipewire_update_volume(ctl);
+ if (err < 0) {
+ count = err;
+ goto finish;
+ }
+
+ if (ctl->default_source[0] != '\0')
+ count += 2;
+ if (ctl->default_sink[0] != '\0')
+ count += 2;
+
+finish:
+ pw_thread_loop_unlock(ctl->mainloop);
+
+ return count;
+}
+
+static int pipewire_elem_list(snd_ctl_ext_t * ext, unsigned int offset,
+ snd_ctl_elem_id_t * id)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+ int err;
+
+ assert(ctl);
+
+ if (!ctl->mainloop)
+ return -EBADFD;
+
+ snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
+
+ pw_thread_loop_lock(ctl->mainloop);
+
+ err = ctl->error;
+ if (err < 0) {
+ ctl->error = 0;
+ goto finish;
+ }
+
+ if (ctl->default_source[0] != '\0') {
+ if (offset == 0)
+ snd_ctl_elem_id_set_name(id, SOURCE_VOL_NAME);
+ else if (offset == 1)
+ snd_ctl_elem_id_set_name(id, SOURCE_MUTE_NAME);
+ } else
+ offset += 2;
+
+ err = 0;
+finish:
+ pw_thread_loop_unlock(ctl->mainloop);
+
+ if (err >= 0) {
+ if (offset == 2)
+ snd_ctl_elem_id_set_name(id, SINK_VOL_NAME);
+ else if (offset == 3)
+ snd_ctl_elem_id_set_name(id, SINK_MUTE_NAME);
+ }
+
+ return err;
+}
+
+static snd_ctl_ext_key_t pipewire_find_elem(snd_ctl_ext_t * ext,
+ const snd_ctl_elem_id_t * id)
+{
+ const char *name;
+ unsigned int numid;
+
+ numid = snd_ctl_elem_id_get_numid(id);
+ if (numid > 0 && numid <= 4)
+ return numid - 1;
+
+ name = snd_ctl_elem_id_get_name(id);
+
+ if (spa_streq(name, SOURCE_VOL_NAME))
+ return 0;
+ if (spa_streq(name, SOURCE_MUTE_NAME))
+ return 1;
+ if (spa_streq(name, SINK_VOL_NAME))
+ return 2;
+ if (spa_streq(name, SINK_MUTE_NAME))
+ return 3;
+
+ return SND_CTL_EXT_KEY_NOT_FOUND;
+}
+
+static int pipewire_get_attribute(snd_ctl_ext_t * ext, snd_ctl_ext_key_t key,
+ int *type, unsigned int *acc,
+ unsigned int *count)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+ int err = 0;
+
+ if (key > 3)
+ return -EINVAL;
+
+ assert(ctl);
+
+ if (!ctl->mainloop)
+ return -EBADFD;
+
+ pw_thread_loop_lock(ctl->mainloop);
+
+ err = ctl->error;
+ if (err < 0) {
+ ctl->error = 0;
+ goto finish;
+ }
+
+ err = pipewire_update_volume(ctl);
+ if (err < 0)
+ goto finish;
+
+ if (key & 1)
+ *type = SND_CTL_ELEM_TYPE_BOOLEAN;
+ else
+ *type = SND_CTL_ELEM_TYPE_INTEGER;
+
+ *acc = SND_CTL_EXT_ACCESS_READWRITE;
+
+ if (key == 0)
+ *count = ctl->source_volume.channels;
+ else if (key == 2)
+ *count = ctl->sink_volume.channels;
+ else
+ *count = 1;
+
+finish:
+ pw_thread_loop_unlock(ctl->mainloop);
+
+ return err;
+}
+
+static int pipewire_get_integer_info(snd_ctl_ext_t * ext,
+ snd_ctl_ext_key_t key, long *imin,
+ long *imax, long *istep)
+{
+ *istep = 1;
+ *imin = VOLUME_MIN;
+ *imax = VOLUME_MAX;
+
+ return 0;
+}
+
+static int pipewire_read_integer(snd_ctl_ext_t * ext, snd_ctl_ext_key_t key,
+ long *value)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+ int err = 0;
+ uint32_t i;
+ struct volume *vol = NULL;
+
+ assert(ctl);
+
+ if (!ctl->mainloop)
+ return -EBADFD;
+
+ pw_thread_loop_lock(ctl->mainloop);
+
+ err = ctl->error;
+ if (err < 0) {
+ ctl->error = 0;
+ goto finish;
+ }
+
+ err = pipewire_update_volume(ctl);
+ if (err < 0)
+ goto finish;
+
+ switch (key) {
+ case 0:
+ vol = &ctl->source_volume;
+ break;
+ case 1:
+ *value = !ctl->source_muted;
+ break;
+ case 2:
+ vol = &ctl->sink_volume;
+ break;
+ case 3:
+ *value = !ctl->sink_muted;
+ break;
+ default:
+ err = -EINVAL;
+ goto finish;
+ }
+
+ if (vol) {
+ for (i = 0; i < vol->channels; i++)
+ value[i] = vol->values[i];
+ }
+
+finish:
+ pw_thread_loop_unlock(ctl->mainloop);
+
+ return err;
+}
+
+static struct spa_pod *build_volume_mute(struct spa_pod_builder *b, struct volume *volume,
+ int *mute, int volume_method)
+{
+ struct spa_pod_frame f[1];
+
+ spa_pod_builder_push_object(b, &f[0],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ if (volume) {
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i, n_volumes = 0;
+
+ n_volumes = volume->channels;
+ for (i = 0; i < n_volumes; i++)
+ volumes[i] = volume_to_linear(volume->values[i], volume_method);
+
+ spa_pod_builder_prop(b, SPA_PROP_channelVolumes, 0);
+ spa_pod_builder_array(b, sizeof(float),
+ SPA_TYPE_Float, n_volumes, volumes);
+ }
+ if (mute) {
+ spa_pod_builder_prop(b, SPA_PROP_mute, 0);
+ spa_pod_builder_bool(b, *mute ? true : false);
+ }
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static int set_volume_mute(snd_ctl_pipewire_t *ctl, const char *name, struct volume *volume, int *mute)
+{
+ struct global *g, *dg = NULL;
+ uint32_t id = SPA_ID_INVALID, device_id = SPA_ID_INVALID;
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[2];
+ struct spa_pod *param;
+
+ g = find_global(ctl, SPA_ID_INVALID, name, PW_TYPE_INTERFACE_Node);
+ if (g == NULL)
+ return -EINVAL;
+
+ if (SPA_FLAG_IS_SET(g->node.flags, NODE_FLAG_DEVICE_VOLUME) &&
+ (dg = find_global(ctl, g->node.device_id, NULL, PW_TYPE_INTERFACE_Device)) != NULL) {
+ if (g->node.flags & NODE_FLAG_SINK)
+ id = dg->device.active_route_output;
+ else if (g->node.flags & NODE_FLAG_SOURCE)
+ id = dg->device.active_route_input;
+ device_id = g->node.profile_device_id;
+ }
+ pw_log_debug("id %d device_id %d flags:%08x", id, device_id, g->node.flags);
+ if (id != SPA_ID_INVALID && device_id != SPA_ID_INVALID && dg != NULL) {
+ if (!SPA_FLAG_IS_SET(dg->permissions, PW_PERM_W | PW_PERM_X))
+ return -EPERM;
+
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route);
+ spa_pod_builder_add(&b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(id),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(device_id),
+ SPA_PARAM_ROUTE_save, SPA_POD_Bool(true),
+ 0);
+
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_props, 0);
+ build_volume_mute(&b, volume, mute, ctl->volume_method);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_log_debug("set device %d mute/volume for node %d", dg->id, g->id);
+ pw_device_set_param((struct pw_node*)dg->proxy,
+ SPA_PARAM_Route, 0, param);
+ } else {
+ if (!SPA_FLAG_IS_SET(g->permissions, PW_PERM_W | PW_PERM_X))
+ return -EPERM;
+
+ param = build_volume_mute(&b, volume, mute, ctl->volume_method);
+
+ pw_log_debug("set node %d mute/volume", g->id);
+ pw_node_set_param((struct pw_node*)g->proxy,
+ SPA_PARAM_Props, 0, param);
+ }
+ return 0;
+}
+
+static int pipewire_write_integer(snd_ctl_ext_t * ext, snd_ctl_ext_key_t key,
+ long *value)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+ int err = 0;
+ uint32_t i;
+ struct volume *vol = NULL;
+
+ assert(ctl);
+
+ if (!ctl->mainloop)
+ return -EBADFD;
+
+ pw_thread_loop_lock(ctl->mainloop);
+
+ err = ctl->error;
+ if (err < 0) {
+ ctl->error = 0;
+ goto finish;
+ }
+
+ err = pipewire_update_volume(ctl);
+ if (err < 0)
+ goto finish;
+
+ switch (key) {
+ case 0:
+ vol = &ctl->source_volume;
+ break;
+ case 1:
+ if (!!ctl->source_muted == !*value)
+ goto finish;
+ ctl->source_muted = !*value;
+ break;
+ case 2:
+ vol = &ctl->sink_volume;
+ break;
+ case 3:
+ if (!!ctl->sink_muted == !*value)
+ goto finish;
+ ctl->sink_muted = !*value;
+ break;
+ default:
+ err = -EINVAL;
+ goto finish;
+ }
+
+ if (vol) {
+ for (i = 0; i < vol->channels; i++)
+ if (value[i] != vol->values[i])
+ break;
+
+ if (i == vol->channels)
+ goto finish;
+
+ for (i = 0; i < vol->channels; i++)
+ vol->values[i] = value[i];
+
+ if (key == 0)
+ err = set_volume_mute(ctl, ctl->default_source, vol, NULL);
+ else
+ err = set_volume_mute(ctl, ctl->default_sink, vol, NULL);
+ } else {
+ if (key == 1)
+ err = set_volume_mute(ctl, ctl->default_source, NULL, &ctl->source_muted);
+ else
+ err = set_volume_mute(ctl, ctl->default_sink, NULL, &ctl->sink_muted);
+ }
+ if (err < 0)
+ goto finish;
+
+ err = wait_resync(ctl);
+
+ if (err < 0)
+ goto finish;
+
+ err = 1;
+
+finish:
+ pw_thread_loop_unlock(ctl->mainloop);
+
+ return err;
+}
+
+static void pipewire_subscribe_events(snd_ctl_ext_t * ext, int subscribe)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+
+ assert(ctl);
+
+ if (!ctl->mainloop)
+ return;
+
+ pw_thread_loop_lock(ctl->mainloop);
+
+ ctl->subscribed = !!(subscribe & SND_CTL_EVENT_MASK_VALUE);
+
+ pw_thread_loop_unlock(ctl->mainloop);
+}
+
+static int pipewire_read_event(snd_ctl_ext_t * ext, snd_ctl_elem_id_t * id,
+ unsigned int *event_mask)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+ int offset;
+ int err;
+
+ assert(ctl);
+
+ if (!ctl->mainloop)
+ return -EBADFD;
+
+ pw_thread_loop_lock(ctl->mainloop);
+
+ err = ctl->error;
+ if (err < 0) {
+ ctl->error = 0;
+ goto finish;
+ }
+
+ if (!ctl->updated || !ctl->subscribed) {
+ err = -EAGAIN;
+ goto finish;
+ }
+
+ if (ctl->default_source[0] != '\0')
+ offset = 2;
+ else
+ offset = 0;
+
+ if (ctl->updated & UPDATE_SOURCE_VOL) {
+ pipewire_elem_list(ext, 0, id);
+ ctl->updated &= ~UPDATE_SOURCE_VOL;
+ } else if (ctl->updated & UPDATE_SOURCE_MUTE) {
+ pipewire_elem_list(ext, 1, id);
+ ctl->updated &= ~UPDATE_SOURCE_MUTE;
+ } else if (ctl->updated & UPDATE_SINK_VOL) {
+ pipewire_elem_list(ext, offset + 0, id);
+ ctl->updated &= ~UPDATE_SINK_VOL;
+ } else if (ctl->updated & UPDATE_SINK_MUTE) {
+ pipewire_elem_list(ext, offset + 1, id);
+ ctl->updated &= ~UPDATE_SINK_MUTE;
+ }
+
+ *event_mask = SND_CTL_EVENT_MASK_VALUE;
+
+ err = 1;
+
+finish:
+ if (!ctl->updated)
+ poll_deactivate(ctl);
+
+ pw_thread_loop_unlock(ctl->mainloop);
+
+ return err;
+}
+
+static int pipewire_ctl_poll_revents(snd_ctl_ext_t * ext, struct pollfd *pfd,
+ unsigned int nfds,
+ unsigned short *revents)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+ int err = 0;
+
+ assert(ctl);
+
+ if (!ctl->mainloop)
+ return -EBADFD;
+
+ pw_thread_loop_lock(ctl->mainloop);
+
+ err = ctl->error;
+ if (err < 0) {
+ ctl->error = 0;
+ goto finish;
+ }
+
+ if (ctl->updated)
+ *revents = POLLIN;
+ else
+ *revents = 0;
+
+ err = 0;
+
+finish:
+ pw_thread_loop_unlock(ctl->mainloop);
+
+ return err;
+}
+
+static void snd_ctl_pipewire_free(snd_ctl_pipewire_t *ctl)
+{
+ if (ctl == NULL)
+ return;
+
+ pw_log_debug("%p:", ctl);
+ if (ctl->mainloop)
+ pw_thread_loop_stop(ctl->mainloop);
+ if (ctl->registry)
+ pw_proxy_destroy((struct pw_proxy*)ctl->registry);
+ if (ctl->context)
+ pw_context_destroy(ctl->context);
+ if (ctl->fd >= 0)
+ spa_system_close(ctl->system, ctl->fd);
+ if (ctl->mainloop)
+ pw_thread_loop_destroy(ctl->mainloop);
+ pw_properties_free(ctl->props);
+ free(ctl);
+}
+
+static void pipewire_close(snd_ctl_ext_t * ext)
+{
+ snd_ctl_pipewire_t *ctl = ext->private_data;
+ snd_ctl_pipewire_free(ctl);
+}
+
+static const snd_ctl_ext_callback_t pipewire_ext_callback = {
+ .elem_count = pipewire_elem_count,
+ .elem_list = pipewire_elem_list,
+ .find_elem = pipewire_find_elem,
+ .get_attribute = pipewire_get_attribute,
+ .get_integer_info = pipewire_get_integer_info,
+ .read_integer = pipewire_read_integer,
+ .write_integer = pipewire_write_integer,
+ .subscribe_events = pipewire_subscribe_events,
+ .read_event = pipewire_read_event,
+ .poll_revents = pipewire_ctl_poll_revents,
+ .close = pipewire_close,
+};
+
+/** device */
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ struct global *g = data;
+ snd_ctl_pipewire_t *ctl = g->ctl;
+ uint32_t n;
+
+ pw_log_debug("info");
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
+ for (n = 0; n < info->n_params; n++) {
+ if (!(info->params[n].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ switch (info->params[n].id) {
+ case SPA_PARAM_Route:
+ pw_device_enum_params((struct pw_device*)g->proxy,
+ 0, info->params[n].id, 0, -1, NULL);
+ break;
+ default:
+ break;
+ }
+ }
+
+ }
+ do_resync(ctl);
+}
+
+static void parse_props(struct global *g, const struct spa_pod *param, bool device)
+{
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ snd_ctl_pipewire_t *ctl = g->ctl;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &g->node.volume) < 0)
+ continue;
+ pw_log_debug("update node %d volume", g->id);
+ SPA_FLAG_UPDATE(g->node.flags, NODE_FLAG_DEVICE_VOLUME, device);
+ break;
+ case SPA_PROP_mute:
+ if (spa_pod_get_bool(&prop->value, &g->node.mute) < 0)
+ continue;
+ SPA_FLAG_UPDATE(g->node.flags, NODE_FLAG_DEVICE_MUTE, device);
+ pw_log_debug("update node %d mute", g->id);
+ break;
+ case SPA_PROP_channelVolumes:
+ {
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t n_volumes, i;
+
+ n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ volumes, SPA_AUDIO_MAX_CHANNELS);
+
+ g->node.channel_volume.channels = n_volumes;
+ for (i = 0; i < n_volumes; i++)
+ g->node.channel_volume.values[i] =
+ volume_from_linear(volumes[i], ctl->volume_method);
+
+ SPA_FLAG_UPDATE(g->node.flags, NODE_FLAG_DEVICE_VOLUME, device);
+ pw_log_debug("update node %d channelVolumes", g->id);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+}
+
+static struct global *find_node_for_route(snd_ctl_pipewire_t *ctl, uint32_t card, uint32_t device)
+{
+ struct global *n;
+ spa_list_for_each(n, &ctl->globals, link) {
+ if (spa_streq(n->ginfo->type, PW_TYPE_INTERFACE_Node) &&
+ (n->node.device_id == card) &&
+ (n->node.profile_device_id == device))
+ return n;
+ }
+ return NULL;
+}
+
+static void device_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct global *g = data;
+ snd_ctl_pipewire_t *ctl = g->ctl;
+
+ pw_log_debug("param %d", id);
+
+ switch (id) {
+ case SPA_PARAM_Route:
+ {
+ uint32_t idx, device;
+ enum spa_direction direction;
+ struct spa_pod *props = NULL;
+ struct global *ng;
+
+ if (spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(&direction),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&device),
+ SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props)) < 0) {
+ pw_log_warn("device %d: can't parse route", g->id);
+ return;
+ }
+ if (direction == SPA_DIRECTION_OUTPUT)
+ g->device.active_route_output = idx;
+ else
+ g->device.active_route_input = idx;
+
+ pw_log_debug("device %d: active %s route %d", g->id,
+ direction == SPA_DIRECTION_OUTPUT ? "output" : "input",
+ idx);
+
+ ng = find_node_for_route(ctl, g->id, device);
+ if (props && ng)
+ parse_props(ng, props, true);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = device_event_param,
+};
+
+static const struct global_info device_info = {
+ .type = PW_TYPE_INTERFACE_Device,
+ .version = PW_VERSION_DEVICE,
+ .events = &device_events,
+};
+
+/** node */
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct global *g = data;
+ snd_ctl_pipewire_t *ctl = g->ctl;
+ const char *str;
+ uint32_t i;
+
+ pw_log_debug("update %d %"PRIu64, g->id, info->change_mask);
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS && info->props) {
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")))
+ g->node.profile_device_id = atoi(str);
+ else
+ g->node.profile_device_id = SPA_ID_INVALID;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)))
+ g->node.device_id = atoi(str);
+ else
+ g->node.device_id = SPA_ID_INVALID;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_PRIORITY_SESSION)))
+ g->node.priority = atoi(str);
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS))) {
+ if (spa_streq(str, "Audio/Sink"))
+ g->node.flags |= NODE_FLAG_SINK;
+ else if (spa_streq(str, "Audio/Source"))
+ g->node.flags |= NODE_FLAG_SOURCE;
+ }
+ }
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ switch (info->params[i].id) {
+ case SPA_PARAM_Props:
+ pw_node_enum_params((struct pw_node*)g->proxy,
+ 0, info->params[i].id, 0, -1, NULL);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ do_resync(ctl);
+}
+
+
+static void node_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct global *g = data;
+ pw_log_debug("update param %d %d", g->id, id);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ if (!SPA_FLAG_IS_SET(g->node.flags, NODE_FLAG_DEVICE_VOLUME | NODE_FLAG_DEVICE_MUTE))
+ parse_props(g, param, false);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = node_event_param,
+};
+
+static const struct global_info node_info = {
+ .type = PW_TYPE_INTERFACE_Node,
+ .version = PW_VERSION_NODE,
+ .events = &node_events,
+};
+
+/** 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,
+ const char *type,
+ const char *value)
+{
+ struct global *g = data;
+ snd_ctl_pipewire_t *ctl = g->ctl;
+
+ if (subject == PW_ID_CORE) {
+ if (key == NULL || spa_streq(key, "default.audio.sink")) {
+ if (value == NULL ||
+ json_object_find(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",
+ ctl->default_source, sizeof(ctl->default_source)) < 0)
+ ctl->default_source[0] = '\0';
+ pw_log_debug("found default source: %s", ctl->default_source);
+ }
+ }
+ return 0;
+}
+
+static int metadata_init(struct global *g)
+{
+ snd_ctl_pipewire_t *ctl = g->ctl;
+ ctl->metadata = (struct pw_metadata*)g->proxy;
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property,
+};
+
+static const struct global_info metadata_info = {
+ .type = PW_TYPE_INTERFACE_Metadata,
+ .version = PW_VERSION_METADATA,
+ .events = &metadata_events,
+ .init = metadata_init
+};
+
+/** proxy */
+static void proxy_removed(void *data)
+{
+ struct global *g = data;
+ pw_proxy_destroy(g->proxy);
+}
+
+static void proxy_destroy(void *data)
+{
+ struct global *g = data;
+ spa_list_remove(&g->link);
+ g->proxy = NULL;
+ pw_properties_free(g->props);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = proxy_removed,
+ .destroy = proxy_destroy
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ snd_ctl_pipewire_t *ctl = data;
+ const struct global_info *info = NULL;
+ struct pw_proxy *proxy;
+ const char *str;
+
+ pw_log_debug("got %d %s", id, type);
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Device)) {
+ if (props == NULL ||
+ ((str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)) == NULL) ||
+ (!spa_streq(str, "Audio/Device")))
+ return;
+
+ pw_log_debug("found device %d", id);
+ info = &device_info;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ if (props == NULL ||
+ ((str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)) == NULL) ||
+ ((!spa_streq(str, "Audio/Sink")) &&
+ (!spa_streq(str, "Audio/Source"))))
+ return;
+
+ pw_log_debug("found node %d type:%s", id, str);
+ info = &node_info;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata)) {
+ if (props == NULL ||
+ ((str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) == NULL) ||
+ (!spa_streq(str, "default")))
+ return;
+ if (ctl->metadata != NULL)
+ return;
+ info = &metadata_info;
+ }
+ if (info) {
+ struct global *g;
+
+ proxy = pw_registry_bind(ctl->registry,
+ id, info->type, info->version,
+ sizeof(struct global));
+
+ g = pw_proxy_get_user_data(proxy);
+ g->ctl = ctl;
+ g->ginfo = info;
+ g->id = id;
+ g->permissions = permissions;
+ g->props = props ? pw_properties_new_dict(props) : NULL;
+ g->proxy = proxy;
+ spa_list_append(&ctl->globals, &g->link);
+
+ pw_proxy_add_listener(proxy,
+ &g->proxy_listener,
+ &proxy_events, g);
+
+ if (info->events) {
+ pw_proxy_add_object_listener(proxy,
+ &g->object_listener,
+ info->events, g);
+ }
+ if (info->init)
+ info->init(g);
+
+ do_resync(ctl);
+ }
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ snd_ctl_pipewire_t *ctl = data;
+ struct global *g;
+ const char *name;
+
+ if ((g = find_global(ctl, id, NULL, NULL)) == NULL)
+ return;
+
+ if (spa_streq(g->ginfo->type, PW_TYPE_INTERFACE_Node)) {
+ if ((name = pw_properties_get(g->props, PW_KEY_NODE_NAME)) == NULL)
+ return;
+
+ if (spa_streq(name, ctl->default_sink))
+ ctl->default_sink[0] = '\0';
+ if (spa_streq(name, ctl->default_source))
+ ctl->default_source[0] = '\0';
+ }
+ pw_proxy_destroy(g->proxy);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ snd_ctl_pipewire_t *ctl = data;
+
+ pw_log_warn("%p: error id:%u seq:%d res:%d (%s): %s", ctl,
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE) {
+ switch (res) {
+ case -ENOENT:
+ break;
+ default:
+ ctl->error = res;
+ if (ctl->fd != -1)
+ poll_activate(ctl);
+ }
+ }
+ pw_thread_loop_signal(ctl->mainloop, false);
+}
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ snd_ctl_pipewire_t *ctl = data;
+
+ pw_log_debug("done %d %d %d", id, seq, ctl->pending_seq);
+
+ if (id != PW_ID_CORE)
+ return;
+
+ ctl->last_seq = seq;
+ if (seq == ctl->pending_seq) {
+ pipewire_update_volume(ctl);
+ pw_thread_loop_signal(ctl->mainloop, false);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+ .done = on_core_done,
+};
+
+static int execute_match(void *data, const char *location, const char *action,
+ const char *val, size_t len)
+{
+ snd_ctl_pipewire_t *ctl = data;
+ if (spa_streq(action, "update-props"))
+ pw_properties_update_string(ctl->props, val, len);
+ return 1;
+}
+
+SPA_EXPORT
+SND_CTL_PLUGIN_DEFINE_FUNC(pipewire)
+{
+ snd_config_iterator_t i, next;
+ const char *server = NULL;
+ const char *device = NULL;
+ const char *source = NULL;
+ const char *sink = NULL;
+ const char *fallback_name = NULL;
+ int err;
+ const char *str;
+ snd_ctl_pipewire_t *ctl;
+ struct pw_loop *loop;
+
+ pw_init(NULL, NULL);
+
+ PW_LOG_TOPIC_INIT(alsa_log_topic);
+
+ snd_config_for_each(i, next, conf) {
+ snd_config_t *n = snd_config_iterator_entry(i);
+ const char *id;
+ if (snd_config_get_id(n, &id) < 0)
+ continue;
+ if (spa_streq(id, "comment") || spa_streq(id, "type")
+ || spa_streq(id, "hint"))
+ continue;
+ if (spa_streq(id, "server")) {
+ if (snd_config_get_string(n, &server) < 0) {
+ SNDERR("Invalid type for %s", id);
+ return -EINVAL;
+ } else if (!*server) {
+ server = NULL;
+ }
+ continue;
+ }
+ if (spa_streq(id, "device")) {
+ if (snd_config_get_string(n, &device) < 0) {
+ SNDERR("Invalid type for %s", id);
+ return -EINVAL;
+ } else if (!*device) {
+ device = NULL;
+ }
+ continue;
+ }
+ if (spa_streq(id, "source")) {
+ if (snd_config_get_string(n, &source) < 0) {
+ SNDERR("Invalid type for %s", id);
+ return -EINVAL;
+ } else if (!*source) {
+ source = NULL;
+ }
+ continue;
+ }
+ if (spa_streq(id, "sink")) {
+ if (snd_config_get_string(n, &sink) < 0) {
+ SNDERR("Invalid type for %s", id);
+ return -EINVAL;
+ } else if (!*sink) {
+ sink = NULL;
+ }
+ continue;
+ }
+ if (spa_streq(id, "fallback")) {
+ if (snd_config_get_string(n, &fallback_name) < 0) {
+ SNDERR("Invalid value for %s", id);
+ return -EINVAL;
+ }
+ continue;
+ }
+ SNDERR("Unknown field %s", id);
+ return -EINVAL;
+ }
+
+ if (fallback_name && name && spa_streq(name, fallback_name))
+ fallback_name = NULL; /* no fallback for the same name */
+
+ ctl = calloc(1, sizeof(*ctl));
+ if (!ctl)
+ return -ENOMEM;
+
+ spa_list_init(&ctl->globals);
+
+ if (source == NULL)
+ source = device;
+ if (source != NULL)
+ snprintf(ctl->default_source, sizeof(ctl->default_source),
+ "%s", source);
+ if (sink == NULL)
+ sink = device;
+ if (sink != NULL)
+ snprintf(ctl->default_sink, sizeof(ctl->default_sink),
+ "%s", sink);
+
+ ctl->mainloop = pw_thread_loop_new("alsa-pipewire", NULL);
+ if (ctl->mainloop == NULL) {
+ err = -errno;
+ goto error;
+ }
+ loop = pw_thread_loop_get_loop(ctl->mainloop);
+
+ ctl->system = loop->system;
+ ctl->fd = spa_system_eventfd_create(ctl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ if (ctl->fd == -1) {
+ err = -errno;
+ goto error;
+ }
+
+ 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) {
+ err = -errno;
+ goto error;
+ }
+
+ ctl->props = pw_properties_new(NULL, NULL);
+ if (ctl->props == NULL) {
+ err = -errno;
+ goto error;
+ }
+
+ if (server)
+ pw_properties_set(ctl->props, PW_KEY_REMOTE_NAME, server);
+
+ pw_context_conf_update_props(ctl->context, "alsa.properties", ctl->props);
+
+ pw_context_conf_section_match_rules(ctl->context, "alsa.rules",
+ &pw_context_get_properties(ctl->context)->dict,
+ execute_match, ctl);
+
+ if (pw_properties_get(ctl->props, PW_KEY_APP_NAME) == NULL)
+ pw_properties_setf(ctl->props, PW_KEY_APP_NAME, "PipeWire ALSA [%s]",
+ pw_get_prgname());
+
+ str = getenv("PIPEWIRE_ALSA");
+ if (str != NULL)
+ pw_properties_update_string(ctl->props, str, strlen(str));
+
+ if ((str = pw_properties_get(ctl->props, "alsa.volume-method")) == NULL)
+ str = DEFAULT_VOLUME_METHOD;
+
+ if (spa_streq(str, "cubic"))
+ ctl->volume_method = VOLUME_METHOD_CUBIC;
+ else if (spa_streq(str, "linear"))
+ ctl->volume_method = VOLUME_METHOD_LINEAR;
+ else {
+ ctl->volume_method = VOLUME_METHOD_CUBIC;
+ SNDERR("unknown alsa.volume-method %s, using cubic", str);
+ }
+
+ if ((err = pw_thread_loop_start(ctl->mainloop)) < 0)
+ goto error;
+
+ pw_thread_loop_lock(ctl->mainloop);
+ ctl->core = pw_context_connect(ctl->context, pw_properties_copy(ctl->props), 0);
+ if (ctl->core == NULL) {
+ err = -errno;
+ goto error_unlock;
+ }
+ pw_core_add_listener(ctl->core,
+ &ctl->core_listener,
+ &core_events, ctl);
+
+ ctl->registry = pw_core_get_registry(ctl->core, PW_VERSION_REGISTRY, 0);
+ if (ctl->registry == NULL) {
+ err = -errno;
+ goto error_unlock;
+ }
+
+ pw_registry_add_listener(ctl->registry,
+ &ctl->registry_listener,
+ &registry_events, ctl);
+
+ err = wait_resync(ctl);
+ if (err < 0)
+ goto error_unlock;
+
+ pw_thread_loop_unlock(ctl->mainloop);
+
+ ctl->ext.version = SND_CTL_EXT_VERSION;
+ ctl->ext.card_idx = 0;
+ strncpy(ctl->ext.id, "pipewire", sizeof(ctl->ext.id) - 1);
+ strncpy(ctl->ext.driver, "PW plugin", sizeof(ctl->ext.driver) - 1);
+ strncpy(ctl->ext.name, "PipeWire", sizeof(ctl->ext.name) - 1);
+ strncpy(ctl->ext.longname, "PipeWire", sizeof(ctl->ext.longname) - 1);
+ strncpy(ctl->ext.mixername, "PipeWire", sizeof(ctl->ext.mixername) - 1);
+ ctl->ext.poll_fd = ctl->fd;
+
+ ctl->ext.callback = &pipewire_ext_callback;
+ ctl->ext.private_data = ctl;
+
+ err = snd_ctl_ext_create(&ctl->ext, name, mode);
+ if (err < 0)
+ goto error;
+
+ *handlep = ctl->ext.handle;
+
+ return 0;
+
+error_unlock:
+ pw_thread_loop_unlock(ctl->mainloop);
+error:
+ snd_ctl_pipewire_free(ctl);
+ pw_log_error("error %d (%s)", err, spa_strerror(err));
+
+ if (fallback_name)
+ return snd_ctl_open_fallback(handlep, root,
+ fallback_name, name, mode);
+
+ return err;
+}
+
+SPA_EXPORT
+SND_CTL_PLUGIN_SYMBOL(pipewire);
diff --git a/pipewire-alsa/alsa-plugins/meson.build b/pipewire-alsa/alsa-plugins/meson.build
new file mode 100644
index 0000000..cbc34ad
--- /dev/null
+++ b/pipewire-alsa/alsa-plugins/meson.build
@@ -0,0 +1,27 @@
+pipewire_alsa_plugin_pcm_sources = [
+ 'pcm_pipewire.c',
+]
+pipewire_alsa_plugin_ctl_sources = [
+ 'ctl_pipewire.c',
+]
+
+pipewire_alsa_plugin_c_args = [
+ '-DPIC',
+]
+
+pipewire_alsa_pcm_plugin = shared_library('asound_module_pcm_pipewire',
+ pipewire_alsa_plugin_pcm_sources,
+ c_args : pipewire_alsa_plugin_c_args,
+ include_directories : [configinc],
+ dependencies : [pipewire_dep, alsa_dep],
+ install : true,
+ install_dir : pipewire_libdir / 'alsa-lib',
+)
+pipewire_alsa_ctl_plugin = shared_library('asound_module_ctl_pipewire',
+ pipewire_alsa_plugin_ctl_sources,
+ c_args : pipewire_alsa_plugin_c_args,
+ include_directories : [configinc],
+ dependencies : [pipewire_dep, alsa_dep, mathlib],
+ install : true,
+ install_dir : pipewire_libdir / 'alsa-lib',
+)
diff --git a/pipewire-alsa/alsa-plugins/pcm_pipewire.c b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
new file mode 100644
index 0000000..dc0cd81
--- /dev/null
+++ b/pipewire-alsa/alsa-plugins/pcm_pipewire.c
@@ -0,0 +1,1333 @@
+/* PCM - PipeWire plugin
+ *
+ * Copyright © 2017 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#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>
+#include <sys/mman.h>
+
+#include <alsa/asoundlib.h>
+#include <alsa/pcm_external.h>
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/debug/types.h>
+#include <spa/param/props.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/pipewire.h>
+
+#define ATOMIC_INC(s) __atomic_add_fetch(&(s), 1, __ATOMIC_SEQ_CST)
+#define ATOMIC_LOAD(s) __atomic_load_n(&(s), __ATOMIC_SEQ_CST)
+
+#define SEQ_WRITE(s) ATOMIC_INC(s)
+#define SEQ_WRITE_SUCCESS(s1,s2) ((s1) + 1 == (s2) && ((s2) & 1) == 0)
+
+#define SEQ_READ(s) ATOMIC_LOAD(s)
+#define SEQ_READ_SUCCESS(s1,s2) ((s1) == (s2) && ((s2) & 1) == 0)
+
+PW_LOG_TOPIC_STATIC(alsa_log_topic, "alsa.pcm");
+#define PW_LOG_TOPIC_DEFAULT alsa_log_topic
+
+#define MIN_BUFFERS 2u
+#define MAX_BUFFERS 64u
+
+#define MAX_CHANNELS 64
+#define MAX_RATE (48000*8)
+
+#define MIN_PERIOD 64
+
+#define MIN_PERIOD_BYTES (128)
+#define MAX_PERIOD_BYTES (2*1024*1024)
+
+#define MIN_BUFFER_BYTES (2*MIN_PERIOD_BYTES)
+#define MAX_BUFFER_BYTES (2*MAX_PERIOD_BYTES)
+
+typedef struct {
+ snd_pcm_ioplug_t io;
+
+ snd_output_t *output;
+ FILE *log_file;
+
+ int fd;
+ int error;
+ unsigned int activated:1; /* PipeWire is activated? */
+ unsigned int drained:1;
+ unsigned int draining:1;
+ unsigned int xrun_detected:1;
+ unsigned int hw_params_changed:1;
+ unsigned int active:1;
+
+ snd_pcm_uframes_t hw_ptr;
+ snd_pcm_uframes_t boundary;
+ snd_pcm_uframes_t min_avail;
+ unsigned int sample_bits;
+ uint32_t blocks;
+ uint32_t stride;
+
+ struct spa_system *system;
+ struct pw_thread_loop *main_loop;
+
+ struct pw_properties *props;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ int64_t delay;
+ uint64_t transfered;
+ uint64_t buffered;
+ int64_t now;
+ uintptr_t seq;
+
+ struct spa_audio_info_raw format;
+} snd_pcm_pipewire_t;
+
+static int snd_pcm_pipewire_stop(snd_pcm_ioplug_t *io);
+
+static int check_active(snd_pcm_ioplug_t *io)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ snd_pcm_sframes_t avail;
+ bool active;
+
+ avail = snd_pcm_ioplug_avail(io, pw->hw_ptr, io->appl_ptr);
+
+ if (io->state == SND_PCM_STATE_DRAINING) {
+ active = pw->drained;
+ }
+ else if (avail >= 0 && avail < (snd_pcm_sframes_t)pw->min_avail) {
+ active = false;
+ }
+ else if (avail >= (snd_pcm_sframes_t)pw->min_avail) {
+ active = true;
+ } else {
+ active = false;
+ }
+ if (pw->active != active) {
+ pw_log_trace("%p: avail:%lu min-avail:%lu state:%s hw:%lu appl:%lu active:%d->%d state:%s",
+ pw, avail, pw->min_avail, snd_pcm_state_name(io->state),
+ pw->hw_ptr, io->appl_ptr, pw->active, active,
+ snd_pcm_state_name(io->state));
+ }
+ return active;
+}
+
+
+static int update_active(snd_pcm_ioplug_t *io)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ pw->active = check_active(io);
+ uint64_t val;
+
+ if (pw->active || pw->error < 0)
+ spa_system_eventfd_write(pw->system, io->poll_fd, 1);
+ else
+ spa_system_eventfd_read(pw->system, io->poll_fd, &val);
+
+ return pw->active;
+}
+
+static void snd_pcm_pipewire_free(snd_pcm_pipewire_t *pw)
+{
+ if (pw == NULL)
+ return;
+
+ pw_log_debug("%p: free", pw);
+ if (pw->main_loop)
+ pw_thread_loop_stop(pw->main_loop);
+ if (pw->stream)
+ pw_stream_destroy(pw->stream);
+ if (pw->context)
+ pw_context_destroy(pw->context);
+ if (pw->fd >= 0)
+ spa_system_close(pw->system, pw->fd);
+ if (pw->main_loop)
+ pw_thread_loop_destroy(pw->main_loop);
+ pw_properties_free(pw->props);
+ snd_output_close(pw->output);
+ fclose(pw->log_file);
+ free(pw);
+}
+
+static int snd_pcm_pipewire_close(snd_pcm_ioplug_t *io)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ pw_log_debug("%p: close", pw);
+ snd_pcm_pipewire_free(pw);
+ return 0;
+}
+
+static int snd_pcm_pipewire_poll_descriptors(snd_pcm_ioplug_t *io, struct pollfd *pfds, unsigned int space)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ update_active(io);
+ pfds->fd = pw->fd;
+ pfds->events = POLLIN | POLLERR | POLLNVAL;
+ return 1;
+}
+
+static int snd_pcm_pipewire_poll_revents(snd_pcm_ioplug_t *io,
+ struct pollfd *pfds, unsigned int nfds,
+ unsigned short *revents)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+
+ assert(pfds && nfds == 1 && revents);
+
+ if (pw->error < 0)
+ return pw->error;
+
+ *revents = pfds[0].revents & ~(POLLIN | POLLOUT);
+ if (pfds[0].revents & POLLIN && check_active(io)) {
+ *revents |= (io->stream == SND_PCM_STREAM_PLAYBACK) ? POLLOUT : POLLIN;
+ update_active(io);
+ }
+
+ return 0;
+}
+
+static snd_pcm_sframes_t snd_pcm_pipewire_pointer(snd_pcm_ioplug_t *io)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ if (pw->xrun_detected)
+ return -EPIPE;
+ if (pw->error < 0)
+ return pw->error;
+ if (io->buffer_size == 0)
+ return 0;
+#ifdef SND_PCM_IOPLUG_FLAG_BOUNDARY_WA
+ return pw->hw_ptr;
+#else
+ return pw->hw_ptr % io->buffer_size;
+#endif
+}
+
+static int snd_pcm_pipewire_delay(snd_pcm_ioplug_t *io, snd_pcm_sframes_t *delayp)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ uintptr_t seq1, seq2;
+ int64_t elapsed = 0, delay, now, avail;
+ struct timespec ts;
+ int64_t diff;
+
+ do {
+ seq1 = SEQ_READ(pw->seq);
+
+ delay = pw->delay + pw->transfered;
+ now = pw->now;
+ if (io->stream == SND_PCM_STREAM_PLAYBACK)
+ avail = snd_pcm_ioplug_hw_avail(io, pw->hw_ptr, io->appl_ptr);
+ else
+ avail = snd_pcm_ioplug_avail(io, pw->hw_ptr, io->appl_ptr);
+
+ seq2 = SEQ_READ(pw->seq);
+ } while (!SEQ_READ_SUCCESS(seq1, seq2));
+
+ if (now != 0 && (io->state == SND_PCM_STATE_RUNNING ||
+ io->state == SND_PCM_STATE_DRAINING)) {
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ diff = SPA_TIMESPEC_TO_NSEC(&ts) - now;
+ elapsed = (io->rate * diff) / SPA_NSEC_PER_SEC;
+
+ if (io->stream == SND_PCM_STREAM_PLAYBACK)
+ delay -= SPA_MIN(elapsed, delay);
+ else
+ delay += SPA_MIN(elapsed, (int64_t)io->buffer_size);
+ }
+
+ *delayp = delay + avail;
+
+ pw_log_trace("avail:%"PRIi64" filled %"PRIi64" elapsed:%"PRIi64" delay:%ld hw:%lu appl:%lu",
+ avail, delay, elapsed, *delayp, pw->hw_ptr, io->appl_ptr);
+
+ return 0;
+}
+
+static snd_pcm_uframes_t
+snd_pcm_pipewire_process(snd_pcm_pipewire_t *pw, struct pw_buffer *b,
+ snd_pcm_uframes_t *hw_avail,snd_pcm_uframes_t want)
+{
+ snd_pcm_ioplug_t *io = &pw->io;
+ snd_pcm_channel_area_t *pwareas;
+ snd_pcm_uframes_t xfer = 0;
+ snd_pcm_uframes_t nframes;
+ unsigned int channel;
+ struct spa_data *d;
+ void *ptr;
+ uint32_t bl, offset, size;
+
+ d = b->buffer->datas;
+ pwareas = alloca(io->channels * sizeof(snd_pcm_channel_area_t));
+
+ for (bl = 0; bl < pw->blocks; bl++) {
+ if (io->stream == SND_PCM_STREAM_PLAYBACK) {
+ size = SPA_MIN(d[bl].maxsize, pw->min_avail * pw->stride);
+ } else {
+ offset = SPA_MIN(d[bl].chunk->offset, d[bl].maxsize);
+ size = SPA_MIN(d[bl].chunk->size, d[bl].maxsize - offset);
+ }
+ want = SPA_MIN(want, size / pw->stride);
+ }
+ nframes = SPA_MIN(want, *hw_avail);
+
+ if (pw->blocks == 1) {
+ if (io->stream == SND_PCM_STREAM_PLAYBACK) {
+ d[0].chunk->size = want * pw->stride;
+ d[0].chunk->offset = offset = 0;
+ } else {
+ offset = SPA_MIN(d[0].chunk->offset, d[0].maxsize);
+ }
+ ptr = SPA_PTROFF(d[0].data, offset, void);
+ for (channel = 0; channel < io->channels; channel++) {
+ pwareas[channel].addr = ptr;
+ pwareas[channel].first = channel * pw->sample_bits;
+ pwareas[channel].step = io->channels * pw->sample_bits;
+ }
+ } else {
+ for (channel = 0; channel < io->channels; channel++) {
+ if (io->stream == SND_PCM_STREAM_PLAYBACK) {
+ d[channel].chunk->size = want * pw->stride;
+ d[channel].chunk->offset = offset = 0;
+ } else {
+ offset = SPA_MIN(d[channel].chunk->offset, d[channel].maxsize);
+ }
+ ptr = SPA_PTROFF(d[channel].data, offset, void);
+ pwareas[channel].addr = ptr;
+ pwareas[channel].first = 0;
+ pwareas[channel].step = pw->sample_bits;
+ }
+ }
+
+ if (io->state == SND_PCM_STATE_RUNNING ||
+ io->state == SND_PCM_STATE_DRAINING) {
+ snd_pcm_uframes_t hw_ptr = pw->hw_ptr;
+ xfer = nframes;
+ if (xfer > 0) {
+ const snd_pcm_channel_area_t *areas = snd_pcm_ioplug_mmap_areas(io);
+ const snd_pcm_uframes_t offset = hw_ptr % io->buffer_size;
+
+ if (io->stream == SND_PCM_STREAM_PLAYBACK)
+ snd_pcm_areas_copy_wrap(pwareas, 0, nframes,
+ areas, offset,
+ io->buffer_size,
+ io->channels, xfer,
+ io->format);
+ else
+ snd_pcm_areas_copy_wrap(areas, offset,
+ io->buffer_size,
+ pwareas, 0, nframes,
+ io->channels, xfer,
+ io->format);
+
+ hw_ptr += xfer;
+ if (hw_ptr >= pw->boundary)
+ hw_ptr -= pw->boundary;
+ pw->hw_ptr = hw_ptr;
+ *hw_avail -= xfer;
+ }
+ }
+ /* check if requested frames were copied */
+ if (xfer < want) {
+ /* always fill the not yet written PipeWire buffer with silence */
+ if (io->stream == SND_PCM_STREAM_PLAYBACK) {
+ const snd_pcm_uframes_t frames = want - xfer;
+
+ snd_pcm_areas_silence(pwareas, xfer, io->channels,
+ frames, io->format);
+ xfer += frames;
+ }
+ if (io->state == SND_PCM_STATE_RUNNING ||
+ io->state == SND_PCM_STATE_DRAINING) {
+ /* report Xrun to user application */
+ pw->xrun_detected = true;
+ }
+ }
+ return xfer;
+}
+
+static void on_stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ snd_pcm_pipewire_t *pw = data;
+ snd_pcm_ioplug_t *io = &pw->io;
+ const struct spa_pod *params[4];
+ uint32_t n_params = 0;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ uint32_t buffers, size;
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ io->period_size = pw->min_avail;
+
+ buffers = SPA_CLAMP(io->buffer_size / io->period_size, MIN_BUFFERS, MAX_BUFFERS);
+ size = io->period_size * pw->stride;
+
+ pw_log_info("%p: buffer_size:%lu period_size:%lu buffers:%u size:%u min_avail:%lu",
+ pw, io->buffer_size, io->period_size, buffers, size, pw->min_avail);
+
+ params[n_params++] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers, MIN_BUFFERS, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(pw->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(size, size, INT_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(pw->stride));
+
+ pw_stream_update_params(pw->stream, params, n_params);
+}
+
+static void on_stream_drained(void *data)
+{
+ snd_pcm_pipewire_t *pw = data;
+ pw->drained = true;
+ pw->draining = false;
+ pw_log_debug("%p: drained", pw);
+ pw_thread_loop_signal(pw->main_loop, false);
+}
+
+static void on_stream_process(void *data)
+{
+ snd_pcm_pipewire_t *pw = data;
+ snd_pcm_ioplug_t *io = &pw->io;
+ struct pw_buffer *b;
+ snd_pcm_uframes_t hw_avail, before, want, xfer;
+ struct pw_time pwt;
+ int64_t delay;
+
+ pw_stream_get_time_n(pw->stream, &pwt, sizeof(pwt));
+
+ delay = pwt.delay;
+ if (pwt.rate.num != 0)
+ delay = delay * io->rate * pwt.rate.num / pwt.rate.denom;
+
+ before = hw_avail = snd_pcm_ioplug_hw_avail(io, pw->hw_ptr, io->appl_ptr);
+
+ if (pw->drained)
+ goto done;
+
+ b = pw_stream_dequeue_buffer(pw->stream);
+ if (b == NULL)
+ return;
+
+ want = b->requested ? b->requested : hw_avail;
+
+ SEQ_WRITE(pw->seq);
+
+ if (pw->now != pwt.now) {
+ pw->transfered = pw->buffered;
+ pw->buffered = 0;
+ }
+
+ xfer = snd_pcm_pipewire_process(pw, b, &hw_avail, want);
+
+ pw->delay = delay;
+ /* the buffer is now queued in the stream and consumed */
+ if (io->stream == SND_PCM_STREAM_PLAYBACK)
+ pw->transfered += xfer;
+
+ /* more then requested data transfered, use them in next iteration */
+ pw->buffered = (want == 0 || pw->transfered < want) ? 0 : (pw->transfered % want);
+
+ pw->now = pwt.now;
+ SEQ_WRITE(pw->seq);
+
+ pw_log_trace("%p: avail-before:%lu avail:%lu want:%lu xfer:%lu hw:%lu appl:%lu",
+ pw, before, hw_avail, want, xfer, pw->hw_ptr, io->appl_ptr);
+
+ pw_stream_queue_buffer(pw->stream, b);
+
+ if (io->state == SND_PCM_STATE_DRAINING && !pw->draining && hw_avail == 0) {
+ if (io->stream == SND_PCM_STREAM_CAPTURE) {
+ on_stream_drained (pw); /* since pw_stream does not call drained() for capture */
+ } else {
+ pw_stream_flush(pw->stream, true);
+ pw->draining = true;
+ pw->drained = false;
+ }
+ }
+done:
+ update_active(io);
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .param_changed = on_stream_param_changed,
+ .process = on_stream_process,
+ .drained = on_stream_drained,
+};
+
+static int pipewire_start(snd_pcm_pipewire_t *pw)
+{
+ if (!pw->activated && pw->stream != NULL) {
+ pw_stream_set_active(pw->stream, true);
+ pw->activated = true;
+ }
+ return 0;
+}
+
+static int snd_pcm_pipewire_drain(snd_pcm_ioplug_t *io)
+{
+ int res;
+ snd_pcm_pipewire_t *pw = io->private_data;
+
+ pw_thread_loop_lock(pw->main_loop);
+ pw_log_debug("%p: drain", pw);
+ pw->drained = false;
+ pw->draining = false;
+ pipewire_start(pw);
+ while (!pw->drained && pw->error >= 0 && pw->activated) {
+ pw_thread_loop_wait(pw->main_loop);
+ }
+ res = pw->error;
+ pw_thread_loop_unlock(pw->main_loop);
+ return res;
+}
+
+static int snd_pcm_pipewire_prepare(snd_pcm_ioplug_t *io)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ snd_pcm_sw_params_t *swparams;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ uint32_t min_period;
+
+ pw_thread_loop_lock(pw->main_loop);
+
+ snd_pcm_sw_params_alloca(&swparams);
+ if (snd_pcm_sw_params_current(io->pcm, swparams) == 0) {
+ snd_pcm_sw_params_get_avail_min(swparams, &pw->min_avail);
+ snd_pcm_sw_params_get_boundary(swparams, &pw->boundary);
+ snd_pcm_sw_params_dump(swparams, pw->output);
+ fflush(pw->log_file);
+ } else {
+ pw->min_avail = io->period_size;
+ pw->boundary = io->buffer_size;
+ }
+
+ min_period = (MIN_PERIOD * io->rate / 48000);
+ pw->min_avail = SPA_MAX(pw->min_avail, min_period);
+
+ pw_log_debug("%p: prepare error:%d stream:%p buffer-size:%lu "
+ "period-size:%lu min-avail:%ld", pw, pw->error,
+ pw->stream, io->buffer_size, io->period_size, pw->min_avail);
+
+ if (pw->error >= 0 && pw->stream != NULL && !pw->hw_params_changed)
+ goto done;
+ pw->hw_params_changed = false;
+
+ 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);
+
+ if (pw->stream != NULL) {
+ pw_stream_update_properties(pw->stream, &pw->props->dict);
+ pw_stream_update_params(pw->stream, params, 1);
+ goto done;
+ }
+
+ pw->stream = pw_stream_new(pw->core, NULL, pw_properties_copy(pw->props));
+ if (pw->stream == NULL)
+ goto error;
+
+ pw_stream_add_listener(pw->stream, &pw->stream_listener, &stream_events, pw);
+
+ pw->error = 0;
+
+ pw_stream_connect(pw->stream,
+ io->stream == SND_PCM_STREAM_PLAYBACK ?
+ PW_DIRECTION_OUTPUT :
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, 1);
+
+done:
+ pw->hw_ptr = 0;
+ pw->now = 0;
+ pw->xrun_detected = false;
+ pw->drained = false;
+ pw->draining = false;
+
+ pw_thread_loop_unlock(pw->main_loop);
+
+ return 0;
+
+error:
+ pw_thread_loop_unlock(pw->main_loop);
+ return -ENOMEM;
+}
+
+static int snd_pcm_pipewire_start(snd_pcm_ioplug_t *io)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+
+ pw_thread_loop_lock(pw->main_loop);
+ pw_log_debug("%p: start", pw);
+ pipewire_start(pw);
+ pw_thread_loop_unlock(pw->main_loop);
+ return 0;
+}
+
+static int snd_pcm_pipewire_stop(snd_pcm_ioplug_t *io)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+
+ pw_log_debug("%p: stop", pw);
+ update_active(io);
+
+ pw_thread_loop_lock(pw->main_loop);
+ if (pw->activated && pw->stream != NULL) {
+ pw_stream_set_active(pw->stream, false);
+ pw->activated = false;
+ }
+ pw_thread_loop_unlock(pw->main_loop);
+ return 0;
+}
+
+static int snd_pcm_pipewire_pause(snd_pcm_ioplug_t * io, int enable)
+{
+ pw_log_debug("%p: pause", io);
+
+ if (enable)
+ snd_pcm_pipewire_stop(io);
+ else
+ snd_pcm_pipewire_start(io);
+
+ return 0;
+}
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define _FORMAT_LE(p, fmt) p ? SPA_AUDIO_FORMAT_UNKNOWN : SPA_AUDIO_FORMAT_ ## fmt ## _OE
+#define _FORMAT_BE(p, fmt) p ? SPA_AUDIO_FORMAT_ ## fmt ## P : SPA_AUDIO_FORMAT_ ## fmt
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+#define _FORMAT_LE(p, fmt) p ? SPA_AUDIO_FORMAT_ ## fmt ## P : SPA_AUDIO_FORMAT_ ## fmt
+#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)
+{
+ switch (info->channels) {
+ case 8:
+ info->position[6] = SPA_AUDIO_CHANNEL_SL;
+ info->position[7] = SPA_AUDIO_CHANNEL_SR;
+ SPA_FALLTHROUGH
+ case 6:
+ info->position[5] = SPA_AUDIO_CHANNEL_LFE;
+ SPA_FALLTHROUGH
+ case 5:
+ info->position[4] = SPA_AUDIO_CHANNEL_FC;
+ SPA_FALLTHROUGH
+ case 4:
+ info->position[2] = SPA_AUDIO_CHANNEL_RL;
+ info->position[3] = SPA_AUDIO_CHANNEL_RR;
+ SPA_FALLTHROUGH
+ case 2:
+ info->position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->position[1] = SPA_AUDIO_CHANNEL_FR;
+ return 1;
+ case 1:
+ info->position[0] = SPA_AUDIO_CHANNEL_MONO;
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static int snd_pcm_pipewire_hw_params(snd_pcm_ioplug_t * io,
+ snd_pcm_hw_params_t * params)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ bool planar;
+
+ snd_pcm_hw_params_dump(params, pw->output);
+ fflush(pw->log_file);
+
+ pw_log_debug("%p: hw_params buffer_size:%lu period_size:%lu", pw, io->buffer_size, io->period_size);
+
+ switch(io->access) {
+ case SND_PCM_ACCESS_MMAP_INTERLEAVED:
+ case SND_PCM_ACCESS_RW_INTERLEAVED:
+ planar = false;
+ break;
+ case SND_PCM_ACCESS_MMAP_NONINTERLEAVED:
+ case SND_PCM_ACCESS_RW_NONINTERLEAVED:
+ planar = true;
+ break;
+ default:
+ SNDERR("PipeWire: invalid access: %d\n", io->access);
+ return -EINVAL;
+ }
+
+ switch(io->format) {
+ case SND_PCM_FORMAT_U8:
+ pw->format.format = planar ? SPA_AUDIO_FORMAT_U8P : SPA_AUDIO_FORMAT_U8;
+ break;
+ case SND_PCM_FORMAT_S16_LE:
+ pw->format.format = _FORMAT_LE(planar, S16);
+ break;
+ case SND_PCM_FORMAT_S16_BE:
+ pw->format.format = _FORMAT_BE(planar, S16);
+ break;
+ case SND_PCM_FORMAT_S24_LE:
+ pw->format.format = _FORMAT_LE(planar, S24_32);
+ break;
+ case SND_PCM_FORMAT_S24_BE:
+ pw->format.format = _FORMAT_BE(planar, S24_32);
+ break;
+ case SND_PCM_FORMAT_S32_LE:
+ pw->format.format = _FORMAT_LE(planar, S32);
+ break;
+ case SND_PCM_FORMAT_S32_BE:
+ pw->format.format = _FORMAT_BE(planar, S32);
+ break;
+ case SND_PCM_FORMAT_S24_3LE:
+ pw->format.format = _FORMAT_LE(planar, S24);
+ break;
+ case SND_PCM_FORMAT_S24_3BE:
+ pw->format.format = _FORMAT_BE(planar, S24);
+ break;
+ case SND_PCM_FORMAT_FLOAT_LE:
+ pw->format.format = _FORMAT_LE(planar, F32);
+ break;
+ case SND_PCM_FORMAT_FLOAT_BE:
+ pw->format.format = _FORMAT_BE(planar, F32);
+ 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);
+
+ pw->sample_bits = snd_pcm_format_physical_width(io->format);
+ if (planar) {
+ pw->blocks = io->channels;
+ pw->stride = pw->sample_bits / 8;
+ } else {
+ pw->blocks = 1;
+ 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),
+ io->channels, io->rate, pw->stride, pw->blocks);
+
+ return 0;
+}
+
+static int snd_pcm_pipewire_sw_params(snd_pcm_ioplug_t * io,
+ snd_pcm_sw_params_t * sw_params)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+
+ pw_thread_loop_lock(pw->main_loop);
+ if (pw->stream) {
+ snd_pcm_uframes_t min_avail;
+ snd_pcm_sw_params_get_avail_min( sw_params, &min_avail);
+ snd_pcm_sw_params_get_boundary(sw_params, &pw->boundary);
+ if (min_avail != pw->min_avail) {
+ char latency[64];
+ struct spa_dict_item item[1];
+ uint32_t min_period = (MIN_PERIOD * io->rate / 48000);
+
+ pw->min_avail = SPA_MAX(min_avail, min_period);
+
+ spa_scnprintf(latency, sizeof(latency), "%lu/%u", pw->min_avail, io->rate);
+ item[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latency);
+
+ pw_log_debug("%p: sw_params update props %p %ld", pw, pw->stream, pw->min_avail);
+ pw_stream_update_properties(pw->stream, &SPA_DICT_INIT(item, 1));
+ }
+ } else {
+ pw_log_debug("%p: sw_params pre-prepare noop", pw);
+ }
+ pw_thread_loop_unlock(pw->main_loop);
+
+ return 0;
+}
+
+struct chmap_info {
+ enum snd_pcm_chmap_position pos;
+ enum spa_audio_channel channel;
+};
+
+static const struct chmap_info chmap_info[] = {
+ [SND_CHMAP_UNKNOWN] = { SND_CHMAP_UNKNOWN, SPA_AUDIO_CHANNEL_UNKNOWN },
+ [SND_CHMAP_NA] = { SND_CHMAP_NA, SPA_AUDIO_CHANNEL_NA },
+ [SND_CHMAP_MONO] = { SND_CHMAP_MONO, SPA_AUDIO_CHANNEL_MONO },
+ [SND_CHMAP_FL] = { SND_CHMAP_FL, SPA_AUDIO_CHANNEL_FL },
+ [SND_CHMAP_FR] = { SND_CHMAP_FR, SPA_AUDIO_CHANNEL_FR },
+ [SND_CHMAP_RL] = { SND_CHMAP_RL, SPA_AUDIO_CHANNEL_RL },
+ [SND_CHMAP_RR] = { SND_CHMAP_RR, SPA_AUDIO_CHANNEL_RR },
+ [SND_CHMAP_FC] = { SND_CHMAP_FC, SPA_AUDIO_CHANNEL_FC },
+ [SND_CHMAP_LFE] = { SND_CHMAP_LFE, SPA_AUDIO_CHANNEL_LFE },
+ [SND_CHMAP_SL] = { SND_CHMAP_SL, SPA_AUDIO_CHANNEL_SL },
+ [SND_CHMAP_SR] = { SND_CHMAP_SR, SPA_AUDIO_CHANNEL_SR },
+ [SND_CHMAP_RC] = { SND_CHMAP_RC, SPA_AUDIO_CHANNEL_RC },
+ [SND_CHMAP_FLC] = { SND_CHMAP_FLC, SPA_AUDIO_CHANNEL_FLC },
+ [SND_CHMAP_FRC] = { SND_CHMAP_FRC, SPA_AUDIO_CHANNEL_FRC },
+ [SND_CHMAP_RLC] = { SND_CHMAP_RLC, SPA_AUDIO_CHANNEL_RLC },
+ [SND_CHMAP_RRC] = { SND_CHMAP_RRC, SPA_AUDIO_CHANNEL_RRC },
+ [SND_CHMAP_FLW] = { SND_CHMAP_FLW, SPA_AUDIO_CHANNEL_FLW },
+ [SND_CHMAP_FRW] = { SND_CHMAP_FRW, SPA_AUDIO_CHANNEL_FRW },
+ [SND_CHMAP_FLH] = { SND_CHMAP_FLH, SPA_AUDIO_CHANNEL_FLH },
+ [SND_CHMAP_FCH] = { SND_CHMAP_FCH, SPA_AUDIO_CHANNEL_FCH },
+ [SND_CHMAP_FRH] = { SND_CHMAP_FRH, SPA_AUDIO_CHANNEL_FRH },
+ [SND_CHMAP_TC] = { SND_CHMAP_TC, SPA_AUDIO_CHANNEL_TC },
+ [SND_CHMAP_TFL] = { SND_CHMAP_TFL, SPA_AUDIO_CHANNEL_TFL },
+ [SND_CHMAP_TFR] = { SND_CHMAP_TFR, SPA_AUDIO_CHANNEL_TFR },
+ [SND_CHMAP_TFC] = { SND_CHMAP_TFC, SPA_AUDIO_CHANNEL_TFC },
+ [SND_CHMAP_TRL] = { SND_CHMAP_TRL, SPA_AUDIO_CHANNEL_TRL },
+ [SND_CHMAP_TRR] = { SND_CHMAP_TRR, SPA_AUDIO_CHANNEL_TRR },
+ [SND_CHMAP_TRC] = { SND_CHMAP_TRC, SPA_AUDIO_CHANNEL_TRC },
+ [SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, SPA_AUDIO_CHANNEL_TFLC },
+ [SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, SPA_AUDIO_CHANNEL_TFRC },
+ [SND_CHMAP_TSL] = { SND_CHMAP_TSL, SPA_AUDIO_CHANNEL_TSL },
+ [SND_CHMAP_TSR] = { SND_CHMAP_TSR, SPA_AUDIO_CHANNEL_TSR },
+ [SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, SPA_AUDIO_CHANNEL_LLFE },
+ [SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, SPA_AUDIO_CHANNEL_RLFE },
+ [SND_CHMAP_BC] = { SND_CHMAP_BC, SPA_AUDIO_CHANNEL_BC },
+ [SND_CHMAP_BLC] = { SND_CHMAP_BLC, SPA_AUDIO_CHANNEL_BLC },
+ [SND_CHMAP_BRC] = { SND_CHMAP_BRC, SPA_AUDIO_CHANNEL_BRC },
+};
+
+static enum snd_pcm_chmap_position channel_to_chmap(enum spa_audio_channel channel)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(chmap_info, info)
+ if (info->channel == channel)
+ return info->pos;
+ return SND_CHMAP_UNKNOWN;
+}
+
+static enum spa_audio_channel chmap_to_channel(enum snd_pcm_chmap_position pos)
+{
+ if (pos >= SPA_N_ELEMENTS(chmap_info))
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+ return chmap_info[pos].channel;
+}
+
+static int snd_pcm_pipewire_set_chmap(snd_pcm_ioplug_t * io,
+ const snd_pcm_chmap_t * map)
+{
+ snd_pcm_pipewire_t *pw = io->private_data;
+ unsigned int i;
+
+ pw->format.channels = map->channels;
+ for (i = 0; i < map->channels; i++) {
+ pw->format.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]));
+ }
+ return 1;
+}
+
+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;
+
+ 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]);
+
+ return map;
+}
+
+static void make_map(snd_pcm_chmap_query_t **maps, int index, int channels, ...)
+{
+ va_list args;
+ int i;
+
+ maps[index] = malloc(sizeof(snd_pcm_chmap_query_t) + (channels * sizeof(unsigned int)));
+ maps[index]->type = SND_CHMAP_TYPE_FIXED;
+ maps[index]->map.channels = channels;
+ va_start(args, channels);
+ for (i = 0; i < channels; i++)
+ maps[index]->map.pos[i] = va_arg(args, int);
+ va_end(args);
+}
+
+static snd_pcm_chmap_query_t **snd_pcm_pipewire_query_chmaps(snd_pcm_ioplug_t *io)
+{
+ snd_pcm_chmap_query_t **maps;
+
+ maps = calloc(7, sizeof(*maps));
+ make_map(maps, 0, 1, SND_CHMAP_MONO);
+ make_map(maps, 1, 2, SND_CHMAP_FL, SND_CHMAP_FR);
+ make_map(maps, 2, 4, SND_CHMAP_FL, SND_CHMAP_FR, SND_CHMAP_RL, SND_CHMAP_RR);
+ make_map(maps, 3, 5, SND_CHMAP_FL, SND_CHMAP_FR, SND_CHMAP_RL, SND_CHMAP_RR,
+ SND_CHMAP_FC);
+ make_map(maps, 4, 6, SND_CHMAP_FL, SND_CHMAP_FR, SND_CHMAP_RL, SND_CHMAP_RR,
+ SND_CHMAP_FC, SND_CHMAP_LFE);
+ make_map(maps, 5, 8, SND_CHMAP_FL, SND_CHMAP_FR, SND_CHMAP_RL, SND_CHMAP_RR,
+ SND_CHMAP_FC, SND_CHMAP_LFE, SND_CHMAP_SL, SND_CHMAP_SR);
+
+ return maps;
+}
+
+static snd_pcm_ioplug_callback_t pipewire_pcm_callback = {
+ .close = snd_pcm_pipewire_close,
+ .start = snd_pcm_pipewire_start,
+ .stop = snd_pcm_pipewire_stop,
+ .pause = snd_pcm_pipewire_pause,
+ .pointer = snd_pcm_pipewire_pointer,
+ .delay = snd_pcm_pipewire_delay,
+ .drain = snd_pcm_pipewire_drain,
+ .prepare = snd_pcm_pipewire_prepare,
+ .poll_descriptors = snd_pcm_pipewire_poll_descriptors,
+ .poll_revents = snd_pcm_pipewire_poll_revents,
+ .hw_params = snd_pcm_pipewire_hw_params,
+ .sw_params = snd_pcm_pipewire_sw_params,
+ .set_chmap = snd_pcm_pipewire_set_chmap,
+ .get_chmap = snd_pcm_pipewire_get_chmap,
+ .query_chmaps = snd_pcm_pipewire_query_chmaps,
+};
+
+static int pipewire_set_hw_constraint(snd_pcm_pipewire_t *pw)
+{
+ unsigned int access_list[] = {
+ SND_PCM_ACCESS_MMAP_INTERLEAVED,
+ SND_PCM_ACCESS_MMAP_NONINTERLEAVED,
+ SND_PCM_ACCESS_RW_INTERLEAVED,
+ SND_PCM_ACCESS_RW_NONINTERLEAVED
+ };
+ unsigned int format_list[] = {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ SND_PCM_FORMAT_FLOAT_LE,
+ SND_PCM_FORMAT_S32_LE,
+ SND_PCM_FORMAT_S24_LE,
+ SND_PCM_FORMAT_S24_3LE,
+ SND_PCM_FORMAT_S24_3BE,
+ SND_PCM_FORMAT_S16_LE,
+#elif __BYTE_ORDER == __BIG_ENDIAN
+ SND_PCM_FORMAT_FLOAT_BE,
+ SND_PCM_FORMAT_S32_BE,
+ SND_PCM_FORMAT_S24_BE,
+ SND_PCM_FORMAT_S24_3LE,
+ SND_PCM_FORMAT_S24_3BE,
+ SND_PCM_FORMAT_S16_BE,
+#endif
+ SND_PCM_FORMAT_U8,
+ };
+ int val;
+ int min_rate;
+ int max_rate;
+ int min_channels;
+ int max_channels;
+ int min_period_bytes;
+ int max_period_bytes;
+ int min_buffer_bytes;
+ int max_buffer_bytes;
+ const char *str;
+ snd_pcm_format_t format;
+ int err;
+
+ val = pw_properties_get_uint32(pw->props, "alsa.rate", 0);
+ if (val > 0) {
+ min_rate = max_rate = SPA_CLAMP(val, 1, MAX_RATE);
+ } else {
+ min_rate = 1;
+ max_rate = MAX_RATE;
+ }
+ val = pw_properties_get_uint32(pw->props, "alsa.channels", 0);
+ if (val > 0) {
+ min_channels = max_channels = SPA_CLAMP(val, 1, MAX_CHANNELS);
+ } else {
+ min_channels = 1;
+ max_channels = MAX_CHANNELS;
+ }
+ val = pw_properties_get_uint32(pw->props, "alsa.period-bytes", 0);
+ if (val > 0) {
+ min_period_bytes = max_period_bytes = SPA_CLAMP(val,
+ MIN_PERIOD_BYTES, MAX_PERIOD_BYTES);
+ } else {
+ min_period_bytes = MIN_PERIOD_BYTES;
+ max_period_bytes = MAX_PERIOD_BYTES;
+ }
+ val = pw_properties_get_uint32(pw->props, "alsa.buffer-bytes", 0);
+ if (val > 0) {
+ min_buffer_bytes = max_buffer_bytes = SPA_CLAMP(val,
+ MIN_BUFFER_BYTES, MAX_BUFFER_BYTES);
+ } else {
+ min_buffer_bytes = MIN_BUFFER_BYTES;
+ max_buffer_bytes = MAX_BUFFER_BYTES;
+ }
+ if (min_period_bytes * 2 > max_buffer_bytes)
+ min_period_bytes = max_period_bytes = max_buffer_bytes / 2;
+
+ if ((err = snd_pcm_ioplug_set_param_list(&pw->io, SND_PCM_IOPLUG_HW_ACCESS,
+ SPA_N_ELEMENTS(access_list), access_list)) < 0 ||
+ (err = snd_pcm_ioplug_set_param_minmax(&pw->io, SND_PCM_IOPLUG_HW_CHANNELS,
+ min_channels, max_channels)) < 0 ||
+ (err = snd_pcm_ioplug_set_param_minmax(&pw->io, SND_PCM_IOPLUG_HW_RATE,
+ min_rate, max_rate)) < 0 ||
+ (err = snd_pcm_ioplug_set_param_minmax(&pw->io, SND_PCM_IOPLUG_HW_BUFFER_BYTES,
+ min_buffer_bytes,
+ max_buffer_bytes)) < 0 ||
+ (err = snd_pcm_ioplug_set_param_minmax(&pw->io,
+ SND_PCM_IOPLUG_HW_PERIOD_BYTES,
+ min_period_bytes,
+ max_period_bytes)) < 0 ||
+ (err = snd_pcm_ioplug_set_param_minmax(&pw->io, SND_PCM_IOPLUG_HW_PERIODS,
+ MIN_BUFFERS, 1024)) < 0) {
+ pw_log_warn("Can't set param list: %s", snd_strerror(err));
+ return err;
+ }
+ format = SND_PCM_FORMAT_UNKNOWN;
+ if ((str = pw_properties_get(pw->props, "alsa.format")))
+ format = snd_pcm_format_value(str);
+
+ if (format != SND_PCM_FORMAT_UNKNOWN) {
+ err = snd_pcm_ioplug_set_param_list(&pw->io,
+ SND_PCM_IOPLUG_HW_FORMAT,
+ 1, (unsigned int *)&format);
+ if (err < 0) {
+ pw_log_warn("Can't set param list: %s", snd_strerror(err));
+ return err;
+ }
+ } else {
+ err = snd_pcm_ioplug_set_param_list(&pw->io,
+ SND_PCM_IOPLUG_HW_FORMAT,
+ SPA_N_ELEMENTS(format_list),
+ format_list);
+ if (err < 0) {
+ pw_log_warn("Can't set param list: %s", snd_strerror(err));
+ return err;
+ }
+ }
+ return 0;
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ snd_pcm_pipewire_t *pw = data;
+
+ pw_log_warn("%p: error id:%u seq:%d res:%d (%s): %s", pw,
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE) {
+ pw->error = res;
+ if (pw->fd != -1)
+ update_active(&pw->io);
+ }
+ pw_thread_loop_signal(pw->main_loop, false);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+
+static ssize_t log_write(void *cookie, const char *buf, size_t size)
+{
+ int len;
+
+ while (size > 0) {
+ len = strcspn(buf, "\n");
+ if (len > 0)
+ pw_log_debug("%.*s", (int)len, buf);
+ buf += len + 1;
+ size -= len + 1;
+ }
+ return size;
+}
+
+static cookie_io_functions_t io_funcs = {
+ .write = log_write,
+};
+
+static int execute_match(void *data, const char *location, const char *action,
+ const char *val, size_t len)
+{
+ snd_pcm_pipewire_t *pw = data;
+ if (spa_streq(action, "update-props"))
+ pw_properties_update_string(pw->props, val, len);
+ return 1;
+}
+
+static int snd_pcm_pipewire_open(snd_pcm_t **pcmp,
+ struct pw_properties *props, snd_pcm_stream_t stream, int mode)
+{
+ snd_pcm_pipewire_t *pw;
+ int err;
+ const char *str, *node_name = NULL;
+ struct pw_loop *loop;
+
+ assert(pcmp);
+ pw = calloc(1, sizeof(*pw));
+ if (!pw)
+ return -ENOMEM;
+
+ pw->props = props;
+ pw->fd = -1;
+ pw->io.poll_fd = -1;
+ pw->log_file = fopencookie(pw, "w", io_funcs);
+ if (pw->log_file == NULL) {
+ pw_log_error("can't create log file: %m");
+ err = -errno;
+ goto error;
+ }
+ if ((err = snd_output_stdio_attach(&pw->output, pw->log_file, 0)) < 0) {
+ pw_log_error("can't attach log file: %s", snd_strerror(err));
+ goto error;
+ }
+
+ pw->main_loop = pw_thread_loop_new("alsa-pipewire", NULL);
+ if (pw->main_loop == NULL) {
+ err = -errno;
+ goto error;
+ }
+ loop = pw_thread_loop_get_loop(pw->main_loop);
+ 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) {
+ err = -errno;
+ goto error;
+ }
+
+ pw_context_conf_update_props(pw->context, "alsa.properties", pw->props);
+
+ pw_context_conf_section_match_rules(pw->context, "alsa.rules",
+ &pw_context_get_properties(pw->context)->dict, execute_match, pw);
+
+ if (pw_properties_get(pw->props, PW_KEY_APP_NAME) == NULL)
+ pw_properties_setf(pw->props, PW_KEY_APP_NAME, "PipeWire ALSA [%s]",
+ pw_get_prgname());
+ if (pw_properties_get(pw->props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(pw->props, PW_KEY_NODE_NAME, "ALSA %s",
+ stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture");
+ if (pw_properties_get(pw->props, PW_KEY_MEDIA_TYPE) == NULL)
+ pw_properties_set(pw->props, PW_KEY_MEDIA_TYPE, "Audio");
+ if (pw_properties_get(pw->props, PW_KEY_MEDIA_CATEGORY) == NULL)
+ pw_properties_set(pw->props, PW_KEY_MEDIA_CATEGORY,
+ stream == SND_PCM_STREAM_PLAYBACK ?
+ "Playback" : "Capture");
+
+ str = getenv("PIPEWIRE_ALSA");
+ if (str != NULL)
+ pw_properties_update_string(pw->props, str, strlen(str));
+
+ str = getenv("PIPEWIRE_NODE");
+ if (str != NULL && str[0])
+ pw_properties_set(pw->props, PW_KEY_TARGET_OBJECT, str);
+
+ node_name = pw_properties_get(pw->props, PW_KEY_NODE_NAME);
+ if (pw_properties_get(pw->props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_set(pw->props, PW_KEY_MEDIA_NAME, node_name);
+
+ if ((err = pw_thread_loop_start(pw->main_loop)) < 0)
+ goto error;
+
+ pw_thread_loop_lock(pw->main_loop);
+ pw->core = pw_context_connect(pw->context, pw_properties_copy(pw->props), 0);
+ if (pw->core == NULL) {
+ err = -errno;
+ pw_thread_loop_unlock(pw->main_loop);
+ goto error;
+ }
+ pw_core_add_listener(pw->core, &pw->core_listener, &core_events, pw);
+ pw_thread_loop_unlock(pw->main_loop);
+
+ pw->fd = spa_system_eventfd_create(pw->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ pw->io.version = SND_PCM_IOPLUG_VERSION;
+ pw->io.name = "ALSA <-> PipeWire PCM I/O Plugin";
+ pw->io.callback = &pipewire_pcm_callback;
+ pw->io.private_data = pw;
+ pw->io.poll_fd = pw->fd;
+ pw->io.poll_events = POLLIN;
+ pw->io.mmap_rw = 1;
+#ifdef SND_PCM_IOPLUG_FLAG_BOUNDARY_WA
+ pw->io.flags = SND_PCM_IOPLUG_FLAG_BOUNDARY_WA;
+#else
+#warning hw_ptr updates of buffer_size will not be recognized by the ALSA library. Consider to update your ALSA library.
+#endif
+ pw->io.flags |= SND_PCM_IOPLUG_FLAG_MONOTONIC;
+
+ if ((err = snd_pcm_ioplug_create(&pw->io, node_name, stream, mode)) < 0)
+ goto error;
+
+ if ((err = pipewire_set_hw_constraint(pw)) < 0)
+ goto error;
+
+ pw_log_debug("%p: opened name:%s stream:%s mode:%d", pw, node_name,
+ snd_pcm_stream_name(pw->io.stream), mode);
+
+ *pcmp = pw->io.pcm;
+
+ return 0;
+
+error:
+ pw_log_debug("%p: failed to open %s :%s", pw, node_name, spa_strerror(err));
+ snd_pcm_pipewire_free(pw);
+ return err;
+}
+
+
+SPA_EXPORT
+SND_PCM_PLUGIN_DEFINE_FUNC(pipewire)
+{
+ snd_config_iterator_t i, next;
+ struct pw_properties *props;
+ const char *str;
+ long val;
+ int err;
+
+ pw_init(NULL, NULL);
+ if (strstr(pw_get_library_version(), "0.2") != NULL)
+ return -ENOTSUP;
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ return -errno;
+
+ PW_LOG_TOPIC_INIT(alsa_log_topic);
+
+ snd_config_for_each(i, next, conf) {
+ snd_config_t *n = snd_config_iterator_entry(i);
+ const char *id;
+ if (snd_config_get_id(n, &id) < 0)
+ continue;
+ if (spa_streq(id, "comment") || spa_streq(id, "type") || spa_streq(id, "hint"))
+ continue;
+ if (spa_streq(id, "name")) {
+ if (snd_config_get_string(n, &str) == 0)
+ pw_properties_set(props, PW_KEY_NODE_NAME, str);
+ continue;
+ }
+ if (spa_streq(id, "server")) {
+ if (snd_config_get_string(n, &str) == 0)
+ pw_properties_set(props, PW_KEY_REMOTE_NAME, str);
+ continue;
+ }
+ if (spa_streq(id, "playback_node")) {
+ if (stream == SND_PCM_STREAM_PLAYBACK &&
+ snd_config_get_string(n, &str) == 0)
+ if (str != NULL && !spa_streq(str, "-1"))
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, str);
+ continue;
+ }
+ if (spa_streq(id, "capture_node")) {
+ if (stream == SND_PCM_STREAM_CAPTURE &&
+ snd_config_get_string(n, &str) == 0)
+ if (str != NULL && !spa_streq(str, "-1"))
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, str);
+ continue;
+ }
+ if (spa_streq(id, "role")) {
+ if (snd_config_get_string(n, &str) == 0)
+ if (str != NULL && *str)
+ pw_properties_set(props, PW_KEY_MEDIA_ROLE, str);
+ continue;
+ }
+ if (spa_streq(id, "exclusive")) {
+ if (snd_config_get_bool(n))
+ pw_properties_set(props, PW_KEY_NODE_EXCLUSIVE, "true");
+ continue;
+ }
+ if (spa_streq(id, "rate")) {
+ if (snd_config_get_integer(n, &val) == 0) {
+ if (val != 0)
+ pw_properties_setf(props, "alsa.rate", "%ld", val);
+ } else {
+ SNDERR("%s: invalid type", id);
+ }
+ continue;
+ }
+ if (spa_streq(id, "format")) {
+ if (snd_config_get_string(n, &str) == 0) {
+ if (str != NULL && *str)
+ pw_properties_set(props, "alsa.format", str);
+ } else {
+ SNDERR("%s: invalid type", id);
+ }
+ continue;
+ }
+ if (spa_streq(id, "channels")) {
+ if (snd_config_get_integer(n, &val) == 0) {
+ if (val != 0)
+ pw_properties_setf(props, "alsa.channels", "%ld", val);
+ } else {
+ SNDERR("%s: invalid type", id);
+ }
+ continue;
+ }
+ if (spa_streq(id, "period_bytes")) {
+ if (snd_config_get_integer(n, &val) == 0) {
+ if (val != 0)
+ pw_properties_setf(props, "alsa.period-bytes", "%ld", val);
+ } else {
+ SNDERR("%s: invalid type", id);
+ }
+ continue;
+ }
+ if (spa_streq(id, "buffer_bytes")) {
+ long val;
+
+ if (snd_config_get_integer(n, &val) == 0) {
+ if (val != 0)
+ pw_properties_setf(props, "alsa.buffer-bytes", "%ld", val);
+ } else {
+ SNDERR("%s: invalid type", id);
+ }
+ continue;
+ }
+ SNDERR("Unknown field %s", id);
+ pw_properties_free(props);
+ return -EINVAL;
+ }
+
+ err = snd_pcm_pipewire_open(pcmp, props, stream, mode);
+
+ return err;
+}
+
+SPA_EXPORT
+SND_PCM_PLUGIN_SYMBOL(pipewire);
diff --git a/pipewire-alsa/conf/50-pipewire.conf b/pipewire-alsa/conf/50-pipewire.conf
new file mode 100644
index 0000000..a3a08a6
--- /dev/null
+++ b/pipewire-alsa/conf/50-pipewire.conf
@@ -0,0 +1,106 @@
+# Add a specific named PipeWire pcm
+
+defaults.pipewire.server "pipewire-0"
+defaults.pipewire.node "-1"
+defaults.pipewire.exclusive false
+defaults.pipewire.role ""
+defaults.pipewire.rate 0
+defaults.pipewire.format ""
+defaults.pipewire.channels 0
+defaults.pipewire.period_bytes 0
+defaults.pipewire.buffer_bytes 0
+
+pcm.pipewire {
+ @args [ SERVER NODE EXCLUSIVE ROLE RATE FORMAT CHANNELS PERIOD_BYTES BUFFER_BYTES ]
+ @args.SERVER {
+ type string
+ default {
+ @func refer
+ name defaults.pipewire.server
+ }
+ }
+ @args.NODE {
+ type string
+ default {
+ @func refer
+ name defaults.pipewire.node
+ }
+ }
+ @args.EXCLUSIVE {
+ type integer
+ default {
+ @func refer
+ name defaults.pipewire.exclusive
+ }
+ }
+ @args.ROLE {
+ type string
+ default {
+ @func refer
+ name defaults.pipewire.role
+ }
+ }
+ @args.RATE {
+ type integer
+ default {
+ @func refer
+ name defaults.pipewire.rate
+ }
+ }
+ @args.FORMAT {
+ type string
+ default {
+ @func refer
+ name defaults.pipewire.format
+ }
+ }
+ @args.CHANNELS {
+ type integer
+ default {
+ @func refer
+ name defaults.pipewire.channels
+ }
+ }
+ @args.PERIOD_BYTES {
+ type integer
+ default {
+ @func refer
+ name defaults.pipewire.period_bytes
+ }
+ }
+ @args.BUFFER_BYTES {
+ type integer
+ default {
+ @func refer
+ name defaults.pipewire.buffer_bytes
+ }
+ }
+
+ type pipewire
+ server $SERVER
+ playback_node $NODE
+ capture_node $NODE
+ exclusive $EXCLUSIVE
+ role $ROLE
+ rate $RATE
+ format $FORMAT
+ channels $CHANNELS
+ period_bytes $PERIOD_BYTES
+ buffer_bytes $BUFFER_BYTES
+ hint {
+ show on
+ description "PipeWire Sound Server"
+ }
+}
+
+ctl.pipewire {
+ @args.SERVER {
+ type string
+ default {
+ @func refer
+ name defaults.pipewire.server
+ }
+ }
+ type pipewire
+ server $SERVER
+}
diff --git a/pipewire-alsa/conf/99-pipewire-default.conf b/pipewire-alsa/conf/99-pipewire-default.conf
new file mode 100644
index 0000000..814506e
--- /dev/null
+++ b/pipewire-alsa/conf/99-pipewire-default.conf
@@ -0,0 +1,13 @@
+pcm.!default {
+ type pipewire
+ playback_node "-1"
+ capture_node "-1"
+ hint {
+ show on
+ description "Default ALSA Output (currently PipeWire Media Server)"
+ }
+}
+
+ctl.!default {
+ type pipewire
+}
diff --git a/pipewire-alsa/conf/meson.build b/pipewire-alsa/conf/meson.build
new file mode 100644
index 0000000..a7336b1
--- /dev/null
+++ b/pipewire-alsa/conf/meson.build
@@ -0,0 +1,5 @@
+alsaconfdir = pipewire_datadir / 'alsa' / 'alsa.conf.d'
+
+install_data(['50-pipewire.conf', '99-pipewire-default.conf'],
+ install_dir: alsaconfdir,
+)
diff --git a/pipewire-alsa/tests/meson.build b/pipewire-alsa/tests/meson.build
new file mode 100644
index 0000000..f48dfb5
--- /dev/null
+++ b/pipewire-alsa/tests/meson.build
@@ -0,0 +1,23 @@
+test_apps = [
+ [ 'test-pipewire-alsa-stress', [alsa_dep, pthread_lib] ],
+]
+
+foreach a : test_apps
+ executable('pw-' + a[0], a[0] + '.c',
+ dependencies : a[1],
+ include_directories: [includes_inc],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir
+ )
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'pw-' + a[0])
+ configure_file(
+ input: installed_tests_template,
+ output: 'pw-' + a[0] + '.test',
+ install_dir: installed_tests_metadir,
+ configuration: test_conf
+ )
+ endif
+endforeach
diff --git a/pipewire-alsa/tests/test-pipewire-alsa-stress.c b/pipewire-alsa/tests/test-pipewire-alsa-stress.c
new file mode 100644
index 0000000..b6de3e5
--- /dev/null
+++ b/pipewire-alsa/tests/test-pipewire-alsa-stress.c
@@ -0,0 +1,151 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Axis Communications AB
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Stress test using pipewire-alsa.
+ [title]
+ */
+
+#include <alsa/asoundlib.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+#define DEFAULT_PCM "pipewire"
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define N_THREADS 20
+
+static void *
+thread_func(void *data)
+{
+ snd_pcm_t *pcm = NULL;
+ snd_pcm_hw_params_t *params;
+ int res;
+ long n = (long)data;
+ unsigned int sample_rate = DEFAULT_RATE;
+
+ res = snd_pcm_open(&pcm, DEFAULT_PCM, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
+ if (res < 0) {
+ fprintf(stderr, "open failed: %s\n", snd_strerror(res));
+ pcm = NULL;
+ goto fail;
+ }
+ printf("opened %ld\n", n);
+
+ snd_pcm_hw_params_alloca(&params);
+ res = snd_pcm_hw_params_any(pcm, params);
+ if (res < 0) {
+ fprintf(stderr, "params_any failed: %s\n", snd_strerror(res));
+ goto fail;
+ }
+
+ res = snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED);
+ if (res < 0) {
+ fprintf(stderr, "set_access failed: %s\n", snd_strerror(res));
+ goto fail;
+ }
+
+ res = snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S32_LE);
+ if (res < 0) {
+ fprintf(stderr, "set_format failed: %s\n", snd_strerror(res));
+ goto fail;
+ }
+
+ res = snd_pcm_hw_params_set_rate_near(pcm, params, &sample_rate, 0);
+ if (res < 0) {
+ fprintf(stderr, "set_rate_near failed: %s\n", snd_strerror(res));
+ goto fail;
+ }
+
+ res = snd_pcm_hw_params_set_channels(pcm, params, DEFAULT_CHANNELS);
+ if (res < 0) {
+ fprintf(stderr, "set_channels failed: %s\n", snd_strerror(res));
+ goto fail;
+ }
+
+ res = snd_pcm_hw_params(pcm, params);
+ if (res < 0) {
+ fprintf(stderr, "params failed: %s\n", snd_strerror(res));
+ goto fail;
+ }
+
+ res = snd_pcm_prepare(pcm);
+ if (res < 0) {
+ fprintf(stderr, "prepare failed: %s (%d)\n", snd_strerror(res), res);
+ goto fail;
+ }
+ printf("prepared %ld\n", n);
+
+ res = snd_pcm_close(pcm);
+ if (res < 0) {
+ fprintf(stderr, "close failed: %s\n", snd_strerror(res));
+ pcm = NULL;
+ goto fail;
+ }
+ printf("closed %ld\n", n);
+
+ return NULL;
+
+fail:
+ if (pcm != NULL) {
+ res = snd_pcm_close(pcm);
+ if (res < 0) {
+ fprintf(stderr, "close failed: %s\n", snd_strerror(res));
+ }
+ }
+ exit(EXIT_FAILURE);
+}
+
+int
+main(int argc, char *argv[])
+{
+ pthread_t t[N_THREADS] = { 0 };
+ long n;
+ int s;
+
+ /* avoid rtkit in this test */
+ setenv("PIPEWIRE_CONFIG_NAME", "client.conf", false);
+
+ while (true) {
+ for (n=0; n < N_THREADS; n++) {
+ if ((s = pthread_create(&(t[n]), NULL, thread_func, (void *)n)) != 0) {
+ fprintf(stderr, "pthread_create: %s\n", strerror(s));
+ exit(EXIT_FAILURE);
+ }
+ printf("created %ld\n", n);
+ }
+ for (n=0; n < N_THREADS; n++) {
+ if (t[n] != 0 && (s = pthread_join(t[n], NULL)) != 0) {
+ fprintf(stderr, "pthread_join: %s\n", strerror(s));
+ exit(EXIT_FAILURE);
+ }
+ printf("joined %ld\n", n);
+ t[n] = 0;
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/pipewire-jack/examples/video-dsp-play.c b/pipewire-jack/examples/video-dsp-play.c
new file mode 100644
index 0000000..be4c94c
--- /dev/null
+++ b/pipewire-jack/examples/video-dsp-play.c
@@ -0,0 +1,203 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#include <SDL2/SDL.h>
+
+#include <jack/jack.h>
+#include <pipewire-jack-extensions.h>
+
+#define MAX_BUFFERS 64
+
+#define JACK_DEFAULT_VIDEO_TYPE "32 bit float RGBA video"
+
+#define CLAMP(v,low,high) \
+({ \
+ __typeof__(v) _v = (v); \
+ __typeof__(low) _low = (low); \
+ __typeof__(high) _high = (high); \
+ (_v < _low) ? _low : (_v > _high) ? _high : _v; \
+})
+
+struct pixel {
+ float r, g, b, a;
+};
+
+struct data {
+ const char *path;
+
+ SDL_Renderer *renderer;
+ SDL_Window *window;
+ SDL_Texture *texture;
+ SDL_Texture *cursor;
+
+ jack_client_t *client;
+ const char *client_name;
+ jack_port_t *in_port;
+
+ jack_image_size_t size;
+
+ int counter;
+ SDL_Rect rect;
+ SDL_Rect cursor_rect;
+};
+
+static void handle_events(struct data *data)
+{
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ exit(0);
+ break;
+ }
+ }
+}
+
+static int
+process (jack_nframes_t nframes, void *arg)
+{
+ struct data *data = (struct data*)arg;
+ void *sdata, *ddata;
+ int sstride, dstride;
+ uint32_t i, j;
+ uint8_t *src, *dst;
+
+ sdata = jack_port_get_buffer (data->in_port, nframes);
+
+ handle_events(data);
+
+ if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ goto done;
+ }
+
+ /* copy video image in texture */
+ sstride = data->size.stride;
+
+ src = sdata;
+ dst = ddata;
+
+ for (i = 0; i < data->size.height; i++) {
+ struct pixel *p = (struct pixel *) src;
+ for (j = 0; j < data->size.width; j++) {
+ dst[j * 4 + 0] = CLAMP(lrintf(p[j].r * 255.0f), 0, 255);
+ dst[j * 4 + 1] = CLAMP(lrintf(p[j].g * 255.0f), 0, 255);
+ dst[j * 4 + 2] = CLAMP(lrintf(p[j].b * 255.0f), 0, 255);
+ dst[j * 4 + 3] = CLAMP(lrintf(p[j].a * 255.0f), 0, 255);
+ }
+ src += sstride;
+ dst += dstride;
+ }
+ SDL_UnlockTexture(data->texture);
+
+ SDL_RenderClear(data->renderer);
+ SDL_RenderCopy(data->renderer, data->texture, &data->rect, NULL);
+ SDL_RenderPresent(data->renderer);
+
+ done:
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ jack_options_t options = JackNullOption;
+ jack_status_t status;
+ int res;
+
+ data.client = jack_client_open ("video-dsp-play", 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);
+ }
+
+ jack_set_process_callback (data.client, process, &data);
+
+ if ((res = jack_get_video_image_size(data.client, &data.size)) < 0) {
+ fprintf(stderr, "can't get video size: %d %s\n", res, strerror(-res));
+ return -1;
+ }
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ if (SDL_CreateWindowAndRenderer
+ (data.size.width, data.size.height, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) {
+ fprintf(stderr, "can't create window: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ data.texture = SDL_CreateTexture(data.renderer,
+ SDL_PIXELFORMAT_RGBA32,
+ SDL_TEXTUREACCESS_STREAMING,
+ data.size.width,
+ data.size.height);
+ data.rect.x = 0;
+ data.rect.y = 0;
+ data.rect.w = data.size.width;
+ data.rect.h = data.size.height;
+
+ data.in_port = jack_port_register (data.client, "input",
+ JACK_DEFAULT_VIDEO_TYPE,
+ JackPortIsInput, 0);
+
+ if (data.in_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);
+
+ SDL_DestroyTexture(data.texture);
+ SDL_DestroyRenderer(data.renderer);
+ SDL_DestroyWindow(data.window);
+
+ return 0;
+}
diff --git a/pipewire-jack/jack/control.h b/pipewire-jack/jack/control.h
new file mode 100644
index 0000000..e466abc
--- /dev/null
+++ b/pipewire-jack/jack/control.h
@@ -0,0 +1,658 @@
+/* -*- Mode: C ; c-basic-offset: 4 -*- */
+/*
+ JACK control API
+
+ Copyright (C) 2008 Nedko Arnaudov
+ Copyright (C) 2008 GRAME
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; version 2 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+/**
+ * @file jack/control.h
+ * @ingroup publicheader
+ * @brief JACK control API
+ *
+ */
+
+#ifndef JACKCTL_H__2EEDAD78_DF4C_4B26_83B7_4FF1A446A47E__INCLUDED
+#define JACKCTL_H__2EEDAD78_DF4C_4B26_83B7_4FF1A446A47E__INCLUDED
+
+#include <jack/types.h>
+#include <jack/jslist.h>
+#include <jack/systemdeps.h>
+#if !defined(sun) && !defined(__sun__)
+#include <stdbool.h>
+#endif
+
+/** Parameter types, intentionally similar to jack_driver_param_type_t */
+typedef enum
+{
+ JackParamInt = 1, /**< @brief value type is a signed integer */
+ JackParamUInt, /**< @brief value type is an unsigned integer */
+ JackParamChar, /**< @brief value type is a char */
+ JackParamString, /**< @brief value type is a string with max size of ::JACK_PARAM_STRING_MAX+1 chars */
+ JackParamBool, /**< @brief value type is a boolean */
+} jackctl_param_type_t;
+
+/** Driver types */
+typedef enum
+{
+ JackMaster = 1, /**< @brief master driver */
+ JackSlave /**< @brief slave driver */
+} jackctl_driver_type_t;
+
+/** @brief Max value that jackctl_param_type_t type can have */
+#define JACK_PARAM_MAX (JackParamBool + 1)
+
+/** @brief Max length of string parameter value, excluding terminating null char */
+#define JACK_PARAM_STRING_MAX 127
+
+/** @brief Type for parameter value */
+/* intentionally similar to jack_driver_param_value_t */
+union jackctl_parameter_value
+{
+ uint32_t ui; /**< @brief member used for ::JackParamUInt */
+ int32_t i; /**< @brief member used for ::JackParamInt */
+ char c; /**< @brief member used for ::JackParamChar */
+ char str[JACK_PARAM_STRING_MAX + 1]; /**< @brief member used for ::JackParamString */
+ bool b; /**< @brief member used for ::JackParamBool */
+};
+
+/** opaque type for server object */
+typedef struct jackctl_server jackctl_server_t;
+
+/** opaque type for driver object */
+typedef struct jackctl_driver jackctl_driver_t;
+
+/** opaque type for internal client object */
+typedef struct jackctl_internal jackctl_internal_t;
+
+/** opaque type for parameter object */
+typedef struct jackctl_parameter jackctl_parameter_t;
+
+/** opaque type for sigmask object */
+typedef struct jackctl_sigmask jackctl_sigmask_t;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+#if 0
+} /* Adjust editor indent */
+#endif
+
+/**
+ * @defgroup ControlAPI The API for starting and controlling a JACK server
+ * @{
+ */
+
+/**
+ * Call this function to setup process signal handling. As a general
+ * rule, it is required for proper operation for the server object.
+ *
+ * @param flags signals setup flags, use 0 for none. Currently no
+ * flags are defined
+ *
+ * @return the configurated signal set.
+ */
+jackctl_sigmask_t *
+jackctl_setup_signals(
+ unsigned int flags);
+
+/**
+ * Call this function to wait on a signal set.
+ *
+ * @param signals signals set to wait on
+ */
+void
+jackctl_wait_signals(
+ jackctl_sigmask_t * signals);
+
+/**
+ * \bold THIS FUNCTION IS DEPRECATED AND SHOULD NOT BE USED IN
+ * NEW JACK PROJECTS
+ *
+ * @deprecated Please use jackctl_server_create2().
+ */
+jackctl_server_t *
+jackctl_server_create(
+ bool (* on_device_acquire)(const char * device_name),
+ void (* on_device_release)(const char * device_name));
+
+/**
+ * Call this function to create server object.
+ *
+ * @param on_device_acquire - Optional callback to be called before device is acquired. If false is returned, device usage will fail
+ * @param on_device_release - Optional callback to be called after device is released.
+ * @param on_device_reservation_loop - Optional callback to be called when looping/idling the reservation.
+ *
+ * @return server object handle, NULL if creation of server object
+ * failed. Successfully created server object must be destroyed with
+ * paired call to ::jackctl_server_destroy
+ */
+jackctl_server_t *
+jackctl_server_create2(
+ bool (* on_device_acquire)(const char * device_name),
+ void (* on_device_release)(const char * device_name),
+ void (* on_device_reservation_loop)(void));
+
+/**
+ * Call this function to destroy server object.
+ *
+ * @param server server object handle to destroy
+ */
+void
+jackctl_server_destroy(
+ jackctl_server_t * server);
+
+/**
+ * Call this function to open JACK server
+ *
+ * @param server server object handle
+ * @param driver driver to use
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_open(
+ jackctl_server_t * server,
+ jackctl_driver_t * driver);
+
+/**
+ * Call this function to start JACK server
+ *
+ * @param server server object handle
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_start(
+ jackctl_server_t * server);
+
+/**
+ * Call this function to stop JACK server
+ *
+ * @param server server object handle
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_stop(
+ jackctl_server_t * server);
+
+/**
+ * Call this function to close JACK server
+ *
+ * @param server server object handle
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_close(
+ jackctl_server_t * server);
+
+/**
+ * Call this function to get list of available drivers. List node data
+ * pointers is a driver object handle (::jackctl_driver_t).
+ *
+ * @param server server object handle to get drivers for
+ *
+ * @return Single linked list of driver object handles. Must not be
+ * modified. Always same for same server object.
+ */
+const JSList *
+jackctl_server_get_drivers_list(
+ jackctl_server_t * server);
+
+/**
+ * Call this function to get list of server parameters. List node data
+ * pointers is a parameter object handle (::jackctl_parameter_t).
+ *
+ * @param server server object handle to get parameters for
+ *
+ * @return Single linked list of parameter object handles. Must not be
+ * modified. Always same for same server object.
+ */
+const JSList *
+jackctl_server_get_parameters(
+ jackctl_server_t * server);
+
+/**
+ * Call this function to get list of available internal clients. List node data
+ * pointers is a internal client object handle (::jackctl_internal_t).
+ *
+ * @param server server object handle to get internal clients for
+ *
+ * @return Single linked list of internal client object handles. Must not be
+ * modified. Always same for same server object.
+ */
+const JSList *
+jackctl_server_get_internals_list(
+ jackctl_server_t * server);
+
+/**
+ * Call this function to load one internal client.
+ * (can be used when the server is running)
+ *
+ * @param server server object handle
+ * @param internal internal to use
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_load_internal(
+ jackctl_server_t * server,
+ jackctl_internal_t * internal);
+
+/**
+ * Call this function to unload one internal client.
+ * (can be used when the server is running)
+ *
+ * @param server server object handle
+ * @param internal internal to unload
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_unload_internal(
+ jackctl_server_t * server,
+ jackctl_internal_t * internal);
+
+/**
+ * Call this function to load a session file.
+ * (can be used when the server is running)
+ *
+ * @param server server object handle
+ * @param file the session file to load, containing a list of
+ * internal clients and connections to be made.
+ *
+ * @return success status: true - success, false - fail
+ */
+bool jackctl_server_load_session_file(
+ jackctl_server_t * server_ptr,
+ const char * file);
+
+/**
+ * Call this function to add a slave in the driver slave list.
+ * (cannot be used when the server is running that is between
+ * jackctl_server_start and jackctl_server_stop)
+ *
+ * @param server server object handle
+ * @param driver driver to add in the driver slave list.
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_add_slave(jackctl_server_t * server,
+ jackctl_driver_t * driver);
+
+/**
+ * Call this function to remove a slave from the driver slave list.
+ * (cannot be used when the server is running that is between
+ * jackctl_server_start and jackctl_server_stop)
+ *
+ * @param server server object handle
+ * @param driver driver to remove from the driver slave list.
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_remove_slave(jackctl_server_t * server,
+ jackctl_driver_t * driver);
+
+/**
+ * Call this function to switch master driver.
+ *
+ * @param server server object handle
+ * @param driver driver to switch to
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_server_switch_master(jackctl_server_t * server,
+ jackctl_driver_t * driver);
+
+
+/**
+ * Call this function to get name of driver.
+ *
+ * @param driver driver object handle to get name of
+ *
+ * @return driver name. Must not be modified. Always same for same
+ * driver object.
+ */
+const char *
+jackctl_driver_get_name(
+ jackctl_driver_t * driver);
+
+/**
+ * Call this function to get type of driver.
+ *
+ * @param driver driver object handle to get name of
+ *
+ * @return driver type. Must not be modified. Always same for same
+ * driver object.
+ */
+jackctl_driver_type_t
+jackctl_driver_get_type(
+ jackctl_driver_t * driver);
+
+/**
+ * Call this function to get list of driver parameters. List node data
+ * pointers is a parameter object handle (::jackctl_parameter_t).
+ *
+ * @param driver driver object handle to get parameters for
+ *
+ * @return Single linked list of parameter object handles. Must not be
+ * modified. Always same for same driver object.
+ */
+const JSList *
+jackctl_driver_get_parameters(
+ jackctl_driver_t * driver);
+
+/**
+ * Call this function to parse parameters for a driver.
+ *
+ * @param driver driver object handle
+ * @param argc parameter list len
+ * @param argv parameter list, as an array of char*
+ *
+ * @return success status: true - success, false - fail
+ */
+int
+jackctl_driver_params_parse(
+ jackctl_driver_t * driver,
+ int argc,
+ char* argv[]);
+
+/**
+ * Call this function to get name of internal client.
+ *
+ * @param internal internal object handle to get name of
+ *
+ * @return internal name. Must not be modified. Always same for same
+ * internal object.
+ */
+const char *
+jackctl_internal_get_name(
+ jackctl_internal_t * internal);
+
+/**
+ * Call this function to get list of internal parameters. List node data
+ * pointers is a parameter object handle (::jackctl_parameter_t).
+ *
+ * @param internal internal object handle to get parameters for
+ *
+ * @return Single linked list of parameter object handles. Must not be
+ * modified. Always same for same internal object.
+ */
+const JSList *
+jackctl_internal_get_parameters(
+ jackctl_internal_t * internal);
+
+/**
+ * Call this function to get parameter name.
+ *
+ * @param parameter parameter object handle to get name of
+ *
+ * @return parameter name. Must not be modified. Always same for same
+ * parameter object.
+ */
+const char *
+jackctl_parameter_get_name(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to get parameter short description.
+ *
+ * @param parameter parameter object handle to get short description of
+ *
+ * @return parameter short description. Must not be modified. Always
+ * same for same parameter object.
+ */
+const char *
+jackctl_parameter_get_short_description(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to get parameter long description.
+ *
+ * @param parameter parameter object handle to get long description of
+ *
+ * @return parameter long description. Must not be modified. Always
+ * same for same parameter object.
+ */
+const char *
+jackctl_parameter_get_long_description(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to get parameter type.
+ *
+ * @param parameter parameter object handle to get type of
+ *
+ * @return parameter type. Always same for same parameter object.
+ */
+jackctl_param_type_t
+jackctl_parameter_get_type(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to get parameter character.
+ *
+ * @param parameter parameter object handle to get character of
+ *
+ * @return character.
+ */
+char
+jackctl_parameter_get_id(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to check whether parameter has been set, or its
+ * default value is being used.
+ *
+ * @param parameter parameter object handle to check
+ *
+ * @return true - parameter is set, false - parameter is using default
+ * value.
+ */
+bool
+jackctl_parameter_is_set(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to reset parameter to its default value.
+ *
+ * @param parameter parameter object handle to reset value of
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_parameter_reset(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to get parameter value.
+ *
+ * @param parameter parameter object handle to get value of
+ *
+ * @return parameter value.
+ */
+union jackctl_parameter_value
+jackctl_parameter_get_value(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to set parameter value.
+ *
+ * @param parameter parameter object handle to get value of
+ * @param value_ptr pointer to variable containing parameter value
+ *
+ * @return success status: true - success, false - fail
+ */
+bool
+jackctl_parameter_set_value(
+ jackctl_parameter_t * parameter,
+ const union jackctl_parameter_value * value_ptr);
+
+/**
+ * Call this function to get parameter default value.
+ *
+ * @param parameter parameter object handle to get default value of
+ *
+ * @return parameter default value.
+ */
+union jackctl_parameter_value
+jackctl_parameter_get_default_value(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function check whether parameter has range constraint.
+ *
+ * @param parameter object handle of parameter to check
+ *
+ * @return whether parameter has range constraint.
+ */
+bool
+jackctl_parameter_has_range_constraint(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function check whether parameter has enumeration constraint.
+ *
+ * @param parameter object handle of parameter to check
+ *
+ * @return whether parameter has enumeration constraint.
+ */
+bool
+jackctl_parameter_has_enum_constraint(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function get how many enumeration values parameter has.
+ *
+ * @param parameter object handle of parameter
+ *
+ * @return number of enumeration values
+ */
+uint32_t
+jackctl_parameter_get_enum_constraints_count(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to get parameter enumeration value.
+ *
+ * @param parameter object handle of parameter
+ * @param index index of parameter enumeration value
+ *
+ * @return enumeration value.
+ */
+union jackctl_parameter_value
+jackctl_parameter_get_enum_constraint_value(
+ jackctl_parameter_t * parameter,
+ uint32_t index);
+
+/**
+ * Call this function to get parameter enumeration value description.
+ *
+ * @param parameter object handle of parameter
+ * @param index index of parameter enumeration value
+ *
+ * @return enumeration value description.
+ */
+const char *
+jackctl_parameter_get_enum_constraint_description(
+ jackctl_parameter_t * parameter,
+ uint32_t index);
+
+/**
+ * Call this function to get parameter range.
+ *
+ * @param parameter object handle of parameter
+ * @param min_ptr pointer to variable receiving parameter minimum value
+ * @param max_ptr pointer to variable receiving parameter maximum value
+ */
+void
+jackctl_parameter_get_range_constraint(
+ jackctl_parameter_t * parameter,
+ union jackctl_parameter_value * min_ptr,
+ union jackctl_parameter_value * max_ptr);
+
+/**
+ * Call this function to check whether parameter constraint is strict,
+ * i.e. whether supplying non-matching value will not work for sure.
+ *
+ * @param parameter parameter object handle to check
+ *
+ * @return whether parameter constraint is strict.
+ */
+bool
+jackctl_parameter_constraint_is_strict(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to check whether parameter has fake values,
+ * i.e. values have no user meaningful meaning and only value
+ * description is meaningful to user.
+ *
+ * @param parameter parameter object handle to check
+ *
+ * @return whether parameter constraint is strict.
+ */
+bool
+jackctl_parameter_constraint_is_fake_value(
+ jackctl_parameter_t * parameter);
+
+/**
+ * Call this function to log an error message.
+ *
+ * @param format string
+ */
+void
+jack_error(
+ const char *format,
+ ...);
+
+/**
+ * Call this function to log an information message.
+ *
+ * @param format string
+ */
+void
+jack_info(
+ const char *format,
+ ...);
+
+/**
+ * Call this function to log an information message but only when
+ * verbose mode is enabled.
+ *
+ * @param format string
+ */
+void
+jack_log(
+ const char *format,
+ ...);
+
+/* @} */
+
+#if 0
+{ /* Adjust editor indent */
+#endif
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* #ifndef JACKCTL_H__2EEDAD78_DF4C_4B26_83B7_4FF1A446A47E__INCLUDED */
diff --git a/pipewire-jack/jack/intclient.h b/pipewire-jack/jack/intclient.h
new file mode 100644
index 0000000..d4503ae
--- /dev/null
+++ b/pipewire-jack/jack/intclient.h
@@ -0,0 +1,130 @@
+/*
+* Copyright (C) 2004 Jack O'Quin
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published by
+* the Free Software Foundation; either version 2.1 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this program; if not, write to the Free Software
+* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+*
+*/
+
+#ifndef __jack_intclient_h__
+#define __jack_intclient_h__
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <jack/types.h>
+
+/**
+ * Get an internal client's name. This is useful when @ref
+ * JackUseExactName was not specified on jack_internal_client_load()
+ * and @ref JackNameNotUnique status was returned. In that case, the
+ * actual name will differ from the @a client_name requested.
+ *
+ * @param client requesting JACK client's handle.
+ *
+ * @param intclient handle returned from jack_internal_client_load()
+ * or jack_internal_client_handle().
+ *
+ * @return NULL if unsuccessful, otherwise pointer to the internal
+ * client name obtained from the heap via malloc(). The caller should
+ * jack_free() this storage when no longer needed.
+ */
+char *jack_get_internal_client_name (jack_client_t *client,
+ jack_intclient_t intclient);
+
+/**
+ * Return the @ref jack_intclient_t handle for an internal client
+ * running in the JACK server.
+ *
+ * @param client requesting JACK client's handle.
+ *
+ * @param client_name for the internal client of no more than
+ * jack_client_name_size() characters. The name scope is local to the
+ * current server.
+ *
+ * @param status (if non-NULL) an address for JACK to return
+ * information from this operation. This status word is formed by
+ * OR-ing together the relevant @ref JackStatus bits.
+ *
+ * @return Opaque internal client handle if successful. If 0, the
+ * internal client was not found, and @a *status includes the @ref
+ * JackNoSuchClient and @ref JackFailure bits.
+ */
+jack_intclient_t jack_internal_client_handle (jack_client_t *client,
+ const char *client_name,
+ jack_status_t *status);
+
+/**
+ * Load an internal client into the JACK server.
+ *
+ * Internal clients run inside the JACK server process. They can use
+ * most of the same functions as external clients. Each internal
+ * client is built as a shared object module, which must declare
+ * jack_initialize() and jack_finish() entry points called at load and
+ * unload times. See @ref inprocess.c for an example.
+ *
+ * @param client loading JACK client's handle.
+ *
+ * @param client_name of at most jack_client_name_size() characters
+ * for the internal client to load. The name scope is local to the
+ * current server.
+ *
+ * @param options formed by OR-ing together @ref JackOptions bits.
+ * Only the @ref JackLoadOptions bits are valid.
+ *
+ * @param status (if non-NULL) an address for JACK to return
+ * information from the load operation. This status word is formed by
+ * OR-ing together the relevant @ref JackStatus bits.
+ *
+ * <b>Optional parameters:</b> depending on corresponding [@a options
+ * bits] additional parameters may follow @a status (in this order).
+ *
+ * @arg [@ref JackLoadName] <em>(char *) load_name</em> is the shared
+ * object file from which to load the new internal client (otherwise
+ * use the @a client_name).
+ *
+ * @arg [@ref JackLoadInit] <em>(char *) load_init</em> an arbitrary
+ * string passed to the internal client's jack_initialize() routine
+ * (otherwise NULL), of no more than @ref JACK_LOAD_INIT_LIMIT bytes.
+ *
+ * @return Opaque internal client handle if successful. If this is 0,
+ * the load operation failed, the internal client was not loaded, and
+ * @a *status includes the @ref JackFailure bit.
+ */
+jack_intclient_t jack_internal_client_load (jack_client_t *client,
+ const char *client_name,
+ jack_options_t options,
+ jack_status_t *status, ...);
+/**
+ * Unload an internal client from a JACK server. This calls the
+ * intclient's jack_finish() entry point then removes it. See @ref
+ * inprocess.c for an example.
+ *
+ * @param client unloading JACK client's handle.
+ *
+ * @param intclient handle returned from jack_internal_client_load() or
+ * jack_internal_client_handle().
+ *
+ * @return 0 if successful, otherwise @ref JackStatus bits.
+ */
+jack_status_t jack_internal_client_unload (jack_client_t *client,
+ jack_intclient_t intclient);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __jack_intclient_h__ */
diff --git a/pipewire-jack/jack/jack.h b/pipewire-jack/jack/jack.h
new file mode 100644
index 0000000..2b83cb1
--- /dev/null
+++ b/pipewire-jack/jack/jack.h
@@ -0,0 +1,1477 @@
+/*
+ Copyright (C) 2001 Paul Davis
+ Copyright (C) 2004 Jack O'Quin
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __jack_h__
+#define __jack_h__
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <jack/systemdeps.h>
+#include <jack/types.h>
+#include <jack/transport.h>
+
+/**
+ * Note: More documentation can be found in jack/types.h.
+ */
+
+ /*************************************************************
+ * NOTE: JACK_WEAK_EXPORT ***MUST*** be used on every function
+ * added to the JACK API after the 0.116.2 release.
+ *
+ * Functions that predate this release are marked with
+ * JACK_WEAK_OPTIONAL_EXPORT which can be defined at compile
+ * time in a variety of ways. The default definition is empty,
+ * so that these symbols get normal linkage. If you wish to
+ * use all JACK symbols with weak linkage, include
+ * <jack/weakjack.h> before jack.h.
+ *************************************************************/
+
+#include <jack/weakmacros.h>
+
+/**
+ * Call this function to get version of the JACK, in form of several numbers
+ *
+ * @param major_ptr pointer to variable receiving major version of JACK.
+ *
+ * @param minor_ptr pointer to variable receiving minor version of JACK.
+ *
+ * @param major_ptr pointer to variable receiving micro version of JACK.
+ *
+ * @param major_ptr pointer to variable receiving protocol version of JACK.
+ *
+ */
+void
+jack_get_version(
+ int *major_ptr,
+ int *minor_ptr,
+ int *micro_ptr,
+ int *proto_ptr) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Call this function to get version of the JACK, in form of a string
+ *
+ * @return Human readable string describing JACK version being used.
+ *
+ */
+const char *
+jack_get_version_string(void) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @defgroup ClientFunctions Creating & manipulating clients
+ * @{
+ */
+
+/**
+ * Open an external client session with a JACK server. This interface
+ * is more complex but more powerful than jack_client_new(). With it,
+ * clients may choose which of several servers to connect, and control
+ * whether and how to start the server automatically, if it was not
+ * already running. There is also an option for JACK to generate a
+ * unique client name, when necessary.
+ *
+ * @param client_name of at most jack_client_name_size() characters.
+ * The name scope is local to each server. Unless forbidden by the
+ * @ref JackUseExactName option, the server will modify this name to
+ * create a unique variant, if needed.
+ *
+ * @param options formed by OR-ing together @ref JackOptions bits.
+ * Only the @ref JackOpenOptions bits are allowed.
+ *
+ * @param status (if non-NULL) an address for JACK to return
+ * information from the open operation. This status word is formed by
+ * OR-ing together the relevant @ref JackStatus bits.
+ *
+ *
+ * <b>Optional parameters:</b> depending on corresponding [@a options
+ * bits] additional parameters may follow @a status (in this order).
+ *
+ * @arg [@ref JackServerName] <em>(char *) server_name</em> selects
+ * from among several possible concurrent server instances. Server
+ * names are unique to each user. If unspecified, use "default"
+ * unless \$JACK_DEFAULT_SERVER is defined in the process environment.
+ *
+ * @return Opaque client handle if successful. If this is NULL, the
+ * open operation failed, @a *status includes @ref JackFailure and the
+ * caller is not a JACK client.
+ */
+jack_client_t * jack_client_open (const char *client_name,
+ jack_options_t options,
+ jack_status_t *status, ...) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+* \bold THIS FUNCTION IS DEPRECATED AND SHOULD NOT BE USED IN
+* NEW JACK CLIENTS
+*
+* @deprecated Please use jack_client_open().
+*/
+jack_client_t * jack_client_new (const char *client_name) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * Disconnects an external client from a JACK server.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_client_close (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the maximum number of characters in a JACK client name
+ * including the final NULL character. This value is a constant.
+ */
+int jack_client_name_size (void) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return pointer to actual client name. This is useful when @ref
+ * JackUseExactName is not specified on open and @ref
+ * JackNameNotUnique status was returned. In that case, the actual
+ * name will differ from the @a client_name requested.
+ */
+char * jack_get_client_name (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Get the session ID for a client name.
+ *
+ * The session manager needs this to reassociate a client name to the session_id.
+ *
+ * The caller is responsible for calling jack_free(3) on any non-NULL
+ * returned value.
+ */
+char *jack_get_uuid_for_client_name (jack_client_t *client,
+ const char *client_name) JACK_WEAK_EXPORT;
+
+/**
+ * Get the client name for a session_id.
+ *
+ * In order to snapshot the graph connections, the session manager needs to map
+ * session_ids to client names.
+ *
+ * The caller is responsible for calling jack_free(3) on any non-NULL
+ * returned value.
+ */
+char *jack_get_client_name_by_uuid (jack_client_t *client,
+ const char *client_uuid ) JACK_WEAK_EXPORT;
+
+/**
+ * Load an internal client into the Jack server.
+ *
+ * Internal clients run inside the JACK server process. They can use
+ * most of the same functions as external clients. Each internal
+ * client must declare jack_initialize() and jack_finish() entry
+ * points, called at load and unload times. See inprocess.c for an
+ * example of how to write an internal client.
+ *
+ * @deprecated Please use jack_internal_client_load().
+ *
+ * @param client_name of at most jack_client_name_size() characters.
+ *
+ * @param load_name of a shared object file containing the code for
+ * the new client.
+ *
+ * @param load_init an arbitrary string passed to the jack_initialize()
+ * routine of the new client (may be NULL).
+ *
+ * @return 0 if successful.
+ */
+int jack_internal_client_new (const char *client_name,
+ const char *load_name,
+ const char *load_init) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * Remove an internal client from a JACK server.
+ *
+ * @deprecated Please use jack_internal_client_unload().
+ */
+void jack_internal_client_close (const char *client_name) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * Tell the Jack server that the program is ready to start processing
+ * audio.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_activate (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell the Jack server to remove this @a client from the process
+ * graph. Also, disconnect all ports belonging to it, since inactive
+ * clients have no port connections.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_deactivate (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return pid of client. If not available, 0 will be returned.
+ */
+int jack_get_client_pid (const char *name) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the pthread ID of the thread running the JACK client side
+ * real-time code.
+ */
+jack_native_thread_t jack_client_thread_id (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+/**
+ * @param client pointer to JACK client structure.
+ *
+ * Check if the JACK subsystem is running with -R (--realtime).
+ *
+ * @return 1 if JACK is running realtime, 0 otherwise
+ */
+int jack_is_realtime (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @defgroup NonCallbackAPI The non-callback API
+ * @{
+ */
+
+/**
+ * \bold THIS FUNCTION IS DEPRECATED AND SHOULD NOT BE USED IN
+ * NEW JACK CLIENTS.
+ *
+ * @deprecated Please use jack_cycle_wait() and jack_cycle_signal() functions.
+ */
+jack_nframes_t jack_thread_wait (jack_client_t *client, int status) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Wait until this JACK client should process data.
+ *
+ * @param client - pointer to a JACK client structure
+ *
+ * @return the number of frames of data to process
+ */
+jack_nframes_t jack_cycle_wait (jack_client_t* client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Signal next clients in the graph.
+ *
+ * @param client - pointer to a JACK client structure
+ * @param status - if non-zero, calling thread should exit
+ */
+void jack_cycle_signal (jack_client_t* client, int status) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell the Jack server to call @a thread_callback in the RT thread.
+ * Typical use are in conjunction with @a jack_cycle_wait and @a jack_cycle_signal functions.
+ * The code in the supplied function must be suitable for real-time
+ * execution. That means that it cannot call functions that might
+ * block for a long time. This includes malloc, free, printf,
+ * pthread_mutex_lock, sleep, wait, poll, select, pthread_join,
+ * pthread_cond_wait, etc, etc. See
+ * http://jackit.sourceforge.net/docs/design/design.html#SECTION00411000000000000000
+ * for more information.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+*/
+int jack_set_process_thread(jack_client_t* client, JackThreadCallback thread_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+/**
+ * @defgroup ClientCallbacks Setting Client Callbacks
+ * @{
+ */
+
+/**
+ * Tell JACK to call @a thread_init_callback once just after
+ * the creation of the thread in which all other callbacks
+ * will be handled.
+ *
+ * The code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code, causing JACK
+ * to remove that client from the process() graph.
+ */
+int jack_set_thread_init_callback (jack_client_t *client,
+ JackThreadInitCallback thread_init_callback,
+ void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @param client pointer to JACK client structure.
+ * @param function The jack_shutdown function pointer.
+ * @param arg The arguments for the jack_shutdown function.
+ *
+ * Register a function (and argument) to be called if and when the
+ * JACK server shuts down the client thread. The function must
+ * be written as if it were an asynchronous POSIX signal
+ * handler --- use only async-safe functions, and remember that it
+ * is executed from another thread. A typical function might
+ * set a flag or write to a pipe so that the rest of the
+ * application knows that the JACK client thread has shut
+ * down.
+ *
+ * NOTE: clients do not need to call this. It exists only
+ * to help more complex clients understand what is going
+ * on. It should be called before jack_client_activate().
+ *
+ * NOTE: if a client calls this AND jack_on_info_shutdown(), then
+ * in case of a client thread shutdown, the callback
+ * passed to this function will not be called, and the one passed to
+ * jack_on_info_shutdown() will.
+ *
+ * NOTE: application should typically signal another thread to correctly
+ * finish cleanup, that is by calling "jack_client_close"
+ * (since "jack_client_close" cannot be called directly in the context
+ * of the thread that calls the shutdown callback).
+ */
+void jack_on_shutdown (jack_client_t *client,
+ JackShutdownCallback shutdown_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @param client pointer to JACK client structure.
+ * @param function The jack_info_shutdown function pointer.
+ * @param arg The arguments for the jack_info_shutdown function.
+ *
+ * Register a function (and argument) to be called if and when the
+ * JACK server shuts down the client thread. The function must
+ * be written as if it were an asynchronous POSIX signal
+ * handler --- use only async-safe functions, and remember that it
+ * is executed from another thread. A typical function might
+ * set a flag or write to a pipe so that the rest of the
+ * application knows that the JACK client thread has shut
+ * down.
+ *
+ * NOTE: clients do not need to call this. It exists only
+ * to help more complex clients understand what is going
+ * on. It should be called before jack_client_activate().
+ *
+ * NOTE: if a client calls this AND jack_on_shutdown(), then
+ * in case of a client thread shutdown, the callback passed to
+ * jack_on_info_shutdown() will be called.
+ *
+ * NOTE: application should typically signal another thread to correctly
+ * finish cleanup, that is by calling "jack_client_close"
+ * (since "jack_client_close" cannot be called directly in the context
+ * of the thread that calls the shutdown callback).
+ */
+void jack_on_info_shutdown (jack_client_t *client,
+ JackInfoShutdownCallback shutdown_callback, void *arg) JACK_WEAK_EXPORT;
+
+/**
+ * Tell the Jack server to call @a process_callback whenever there is
+ * work be done, passing @a arg as the second argument.
+ *
+ * The code in the supplied function must be suitable for real-time
+ * execution. That means that it cannot call functions that might
+ * block for a long time. This includes malloc, free, printf,
+ * pthread_mutex_lock, sleep, wait, poll, select, pthread_join,
+ * pthread_cond_wait, etc, etc. See
+ * http://jackit.sourceforge.net/docs/design/design.html#SECTION00411000000000000000
+ * for more information.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_set_process_callback (jack_client_t *client,
+ JackProcessCallback process_callback,
+ void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell the Jack server to call @a freewheel_callback
+ * whenever we enter or leave "freewheel" mode, passing @a
+ * arg as the second argument. The first argument to the
+ * callback will be non-zero if JACK is entering freewheel
+ * mode, and zero otherwise.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_set_freewheel_callback (jack_client_t *client,
+ JackFreewheelCallback freewheel_callback,
+ void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell JACK to call @a bufsize_callback whenever the size of the
+ * buffer that will be passed to the @a process_callback is about to
+ * change. Clients that depend on knowing the buffer size must supply
+ * a @a bufsize_callback before activating themselves.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @param client pointer to JACK client structure.
+ * @param bufsize_callback function to call when the buffer size changes.
+ * @param arg argument for @a bufsize_callback.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_buffer_size_callback (jack_client_t *client,
+ JackBufferSizeCallback bufsize_callback,
+ void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell the Jack server to call @a srate_callback whenever the system
+ * sample rate changes.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_sample_rate_callback (jack_client_t *client,
+ JackSampleRateCallback srate_callback,
+ void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell the JACK server to call @a client_registration_callback whenever a
+ * client is registered or unregistered, passing @a arg as a parameter.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_client_registration_callback (jack_client_t *client,
+ JackClientRegistrationCallback
+ registration_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell the JACK server to call @a registration_callback whenever a
+ * port is registered or unregistered, passing @a arg as a parameter.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+ int jack_set_port_registration_callback (jack_client_t *client,
+ JackPortRegistrationCallback
+ registration_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+ /**
+ * Tell the JACK server to call @a connect_callback whenever a
+ * port is connected or disconnected, passing @a arg as a parameter.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_port_connect_callback (jack_client_t *client,
+ JackPortConnectCallback
+ connect_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+ /**
+ * Tell the JACK server to call @a rename_callback whenever a
+ * port is renamed, passing @a arg as a parameter.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_port_rename_callback (jack_client_t *client,
+ JackPortRenameCallback
+ rename_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell the JACK server to call @a graph_callback whenever the
+ * processing graph is reordered, passing @a arg as a parameter.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_graph_order_callback (jack_client_t *client,
+ JackGraphOrderCallback graph_callback,
+ void *) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Tell the JACK server to call @a xrun_callback whenever there is a
+ * xrun, passing @a arg as a parameter.
+ *
+ * All "notification events" are received in a separated non RT thread,
+ * the code in the supplied function does not need to be
+ * suitable for real-time execution.
+ *
+ * NOTE: this function cannot be called while the client is activated
+ * (after jack_activate has been called.)
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_xrun_callback (jack_client_t *client,
+ JackXRunCallback xrun_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+/**
+ * Tell the Jack server to call @a latency_callback whenever it
+ * is necessary to recompute the latencies for some or all
+ * Jack ports.
+ *
+ * @a latency_callback will be called twice each time it is
+ * needed, once being passed JackCaptureLatency and once
+ * JackPlaybackLatency. See @ref LatencyFunctions for
+ * the definition of each type of latency and related functions.
+ *
+ * <b>IMPORTANT: Most JACK clients do NOT need to register a latency
+ * callback.</b>
+ *
+ * Clients that meet any of the following conditions do NOT
+ * need to register a latency callback:
+ *
+ * - have only input ports
+ * - have only output ports
+ * - their output is totally unrelated to their input
+ * - their output is not delayed relative to their input
+ * (i.e. data that arrives in a given process()
+ * callback is processed and output again in the
+ * same callback)
+ *
+ * Clients NOT registering a latency callback MUST also
+ * satisfy this condition:
+ *
+ * - have no multiple distinct internal signal pathways
+ *
+ * This means that if your client has more than 1 input and
+ * output port, and considers them always "correlated"
+ * (e.g. as a stereo pair), then there is only 1 (e.g. stereo)
+ * signal pathway through the client. This would be true,
+ * for example, of a stereo FX rack client that has a
+ * left/right input pair and a left/right output pair.
+ *
+ * However, this is somewhat a matter of perspective. The
+ * same FX rack client could be connected so that its
+ * two input ports were connected to entirely separate
+ * sources. Under these conditions, the fact that the client
+ * does not register a latency callback MAY result
+ * in port latency values being incorrect.
+ *
+ * Clients that do not meet any of those conditions SHOULD
+ * register a latency callback.
+ *
+ * Another case is when a client wants to use
+ * @ref jack_port_get_latency_range(), which only returns meaningful
+ * values when ports get connected and latency values change.
+ *
+ * See the documentation for @ref jack_port_set_latency_range()
+ * on how the callback should operate. Remember that the @a mode
+ * argument given to the latency callback will need to be
+ * passed into @ref jack_port_set_latency_range()
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_latency_callback (jack_client_t *client,
+ JackLatencyCallback latency_callback,
+ void *) JACK_WEAK_EXPORT;
+/*@}*/
+
+/**
+ * @defgroup ServerClientControl Controlling & querying JACK server operation
+ * @{
+ */
+
+/**
+ * Start/Stop JACK's "freewheel" mode.
+ *
+ * When in "freewheel" mode, JACK no longer waits for
+ * any external event to begin the start of the next process
+ * cycle.
+ *
+ * As a result, freewheel mode causes "faster than realtime"
+ * execution of a JACK graph. If possessed, real-time
+ * scheduling is dropped when entering freewheel mode, and
+ * if appropriate it is reacquired when stopping.
+ *
+ * IMPORTANT: on systems using capabilities to provide real-time
+ * scheduling (i.e. Linux kernel 2.4), if onoff is zero, this function
+ * must be called from the thread that originally called jack_activate().
+ * This restriction does not apply to other systems (e.g. Linux kernel 2.6
+ * or OS X).
+ *
+ * @param client pointer to JACK client structure
+ * @param onoff if non-zero, freewheel mode starts. Otherwise
+ * freewheel mode ends.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_set_freewheel(jack_client_t* client, int onoff) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Change the buffer size passed to the @a process_callback.
+ *
+ * This operation stops the JACK engine process cycle, then calls all
+ * registered @a bufsize_callback functions before restarting the
+ * process cycle. This will cause a gap in the audio flow, so it
+ * should only be done at appropriate stopping points.
+ *
+ * @see jack_set_buffer_size_callback()
+ *
+ * @param client pointer to JACK client structure.
+ * @param nframes new buffer size. Must be a power of two.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_buffer_size (jack_client_t *client, jack_nframes_t nframes) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the sample rate of the jack system, as set by the user when
+ * jackd was started.
+ */
+jack_nframes_t jack_get_sample_rate (jack_client_t *) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the current maximum size that will ever be passed to the @a
+ * process_callback. It should only be used *before* the client has
+ * been activated. This size may change, clients that depend on it
+ * must register a @a bufsize_callback so they will be notified if it
+ * does.
+ *
+ * @see jack_set_buffer_size_callback()
+ */
+jack_nframes_t jack_get_buffer_size (jack_client_t *) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Old-style interface to become the timebase for the entire JACK
+ * subsystem.
+ *
+ * @deprecated This function still exists for compatibility with the
+ * earlier transport interface, but it does nothing. Instead, see
+ * transport.h and use jack_set_timebase_callback().
+ *
+ * @return ENOSYS, function not implemented.
+ */
+int jack_engine_takeover_timebase (jack_client_t *) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * @return the current CPU load estimated by JACK. This is a running
+ * average of the time it takes to execute a full process cycle for
+ * all clients as a percentage of the real time available per cycle
+ * determined by the buffer size and sample rate.
+ */
+float jack_cpu_load (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+/**
+ * @defgroup PortFunctions Creating & manipulating ports
+ * @{
+ */
+
+/**
+ * Create a new port for the client. This is an object used for moving
+ * data of any type in or out of the client. Ports may be connected
+ * in various ways.
+ *
+ * Each port has a short name. The port's full name contains the name
+ * of the client concatenated with a colon (:) followed by its short
+ * name. The jack_port_name_size() is the maximum length of this full
+ * name. Exceeding that will cause the port registration to fail and
+ * return NULL.
+ *
+ * The @a port_name must be unique among all ports owned by this client.
+ * If the name is not unique, the registration will fail.
+ *
+ * All ports have a type, which may be any non-NULL and non-zero
+ * length string, passed as an argument. Some port types are built
+ * into the JACK API, currently only JACK_DEFAULT_AUDIO_TYPE.
+ *
+ * @param client pointer to JACK client structure.
+ * @param port_name non-empty short name for the new port (not
+ * including the leading @a "client_name:"). Must be unique.
+ * @param port_type port type name. If longer than
+ * jack_port_type_size(), only that many characters are significant.
+ * @param flags @ref JackPortFlags bit mask.
+ * @param buffer_size must be non-zero if this is not a built-in @a
+ * port_type. Otherwise, it is ignored.
+ *
+ * @return jack_port_t pointer on success, otherwise NULL.
+ */
+jack_port_t * jack_port_register (jack_client_t *client,
+ const char *port_name,
+ const char *port_type,
+ unsigned long flags,
+ unsigned long buffer_size) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Remove the port from the client, disconnecting any existing
+ * connections.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_port_unregister (jack_client_t *client, jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * This returns a pointer to the memory area associated with the
+ * specified port. For an output port, it will be a memory area
+ * that can be written to; for an input port, it will be an area
+ * containing the data from the port's connection(s), or
+ * zero-filled. if there are multiple inbound connections, the data
+ * will be mixed appropriately.
+ *
+ * FOR OUTPUT PORTS ONLY : DEPRECATED in Jack 2.0 !!
+ * ---------------------------------------------------
+ * You may cache the value returned, but only between calls to
+ * your "blocksize" callback. For this reason alone, you should
+ * either never cache the return value or ensure you have
+ * a "blocksize" callback and be sure to invalidate the cached
+ * address from there.
+ *
+ * Caching output ports is DEPRECATED in Jack 2.0, due to some new optimization (like "pipelining").
+ * Port buffers have to be retrieved in each callback for proper functioning.
+ */
+void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the UUID of the jack_port_t
+ *
+ * @see jack_uuid_to_string() to convert into a string representation
+ */
+jack_uuid_t jack_port_uuid (const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the full name of the jack_port_t (including the @a
+ * "client_name:" prefix).
+ *
+ * @see jack_port_name_size().
+ */
+const char * jack_port_name (const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the short name of the jack_port_t (not including the @a
+ * "client_name:" prefix).
+ *
+ * @see jack_port_name_size().
+ */
+const char * jack_port_short_name (const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the @ref JackPortFlags of the jack_port_t.
+ */
+int jack_port_flags (const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the @a port type, at most jack_port_type_size() characters
+ * including a final NULL.
+ */
+const char * jack_port_type (const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+ /**
+ * @return the @a port type id.
+ */
+jack_port_type_id_t jack_port_type_id (const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return TRUE if the jack_port_t belongs to the jack_client_t.
+ */
+int jack_port_is_mine (const jack_client_t *client, const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return number of connections to or from @a port.
+ *
+ * @pre The calling client must own @a port.
+ */
+int jack_port_connected (const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return TRUE if the locally-owned @a port is @b directly connected
+ * to the @a port_name.
+ *
+ * @see jack_port_name_size()
+ */
+int jack_port_connected_to (const jack_port_t *port,
+ const char *port_name) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return a null-terminated array of full port names to which the @a
+ * port is connected. If none, returns NULL.
+ *
+ * The caller is responsible for calling jack_free() on any non-NULL
+ * returned value.
+ *
+ * @param port locally owned jack_port_t pointer.
+ *
+ * @see jack_port_name_size(), jack_port_get_all_connections()
+ */
+const char ** jack_port_get_connections (const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return a null-terminated array of full port names to which the @a
+ * port is connected. If none, returns NULL.
+ *
+ * The caller is responsible for calling jack_free() on any non-NULL
+ * returned value.
+ *
+ * This differs from jack_port_get_connections() in two important
+ * respects:
+ *
+ * 1) You may not call this function from code that is
+ * executed in response to a JACK event. For example,
+ * you cannot use it in a GraphReordered handler.
+ *
+ * 2) You need not be the owner of the port to get information
+ * about its connections.
+ *
+ * @see jack_port_name_size()
+ */
+const char ** jack_port_get_all_connections (const jack_client_t *client,
+ const jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ *
+ * @deprecated This function will be removed from a future version
+ * of JACK. Do not use it. There is no replacement. It has
+ * turned out to serve essentially no purpose in real-life
+ * JACK clients.
+ */
+int jack_port_tie (jack_port_t *src, jack_port_t *dst) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ *
+ * @deprecated This function will be removed from a future version
+ * of JACK. Do not use it. There is no replacement. It has
+ * turned out to serve essentially no purpose in real-life
+ * JACK clients.
+ */
+int jack_port_untie (jack_port_t *port) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * \bold THIS FUNCTION IS DEPRECATED AND SHOULD NOT BE USED IN
+ * NEW JACK CLIENTS
+ *
+ * Modify a port's short name. May be called at any time. If the
+ * resulting full name (including the @a "client_name:" prefix) is
+ * longer than jack_port_name_size(), it will be truncated.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_port_set_name (jack_port_t *port, const char *port_name) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * Modify a port's short name. May NOT be called from a callback handling a server event.
+ * If the resulting full name (including the @a "client_name:" prefix) is
+ * longer than jack_port_name_size(), it will be truncated.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ *
+ * This differs from jack_port_set_name() by triggering PortRename notifications to
+ * clients that have registered a port rename handler.
+ */
+int jack_port_rename (jack_client_t* client, jack_port_t *port, const char *port_name) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Set @a alias as an alias for @a port. May be called at any time.
+ * If the alias is longer than jack_port_name_size(), it will be truncated.
+ *
+ * After a successful call, and until JACK exits or
+ * @function jack_port_unset_alias() is called, @alias may be
+ * used as a alternate name for the port.
+ *
+ * Ports can have up to two aliases - if both are already
+ * set, this function will return an error.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_port_set_alias (jack_port_t *port, const char *alias) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Remove @a alias as an alias for @a port. May be called at any time.
+ *
+ * After a successful call, @a alias can no longer be
+ * used as a alternate name for the port.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_port_unset_alias (jack_port_t *port, const char *alias) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Get any aliases known for @port.
+ *
+ * @return the number of aliases discovered for the port
+ */
+int jack_port_get_aliases (const jack_port_t *port, char* const aliases[2]) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * If @ref JackPortCanMonitor is set for this @a port, turn input
+ * monitoring on or off. Otherwise, do nothing.
+ */
+int jack_port_request_monitor (jack_port_t *port, int onoff) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * If @ref JackPortCanMonitor is set for this @a port_name, turn input
+ * monitoring on or off. Otherwise, do nothing.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ *
+ * @see jack_port_name_size()
+ */
+int jack_port_request_monitor_by_name (jack_client_t *client,
+ const char *port_name, int onoff) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * If @ref JackPortCanMonitor is set for a port, this function turns
+ * on input monitoring if it was off, and turns it off if only one
+ * request has been made to turn it on. Otherwise it does nothing.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_port_ensure_monitor (jack_port_t *port, int onoff) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return TRUE if input monitoring has been requested for @a port.
+ */
+int jack_port_monitoring_input (jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Establish a connection between two ports.
+ *
+ * When a connection exists, data written to the source port will
+ * be available to be read at the destination port.
+ *
+ * @pre The port types must be identical.
+ *
+ * @pre The @ref JackPortFlags of the @a source_port must include @ref
+ * JackPortIsOutput.
+ *
+ * @pre The @ref JackPortFlags of the @a destination_port must include
+ * @ref JackPortIsInput.
+ *
+ * @return 0 on success, EEXIST if the connection is already made,
+ * otherwise a non-zero error code
+ */
+int jack_connect (jack_client_t *client,
+ const char *source_port,
+ const char *destination_port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Remove a connection between two ports.
+ *
+ * @pre The port types must be identical.
+ *
+ * @pre The @ref JackPortFlags of the @a source_port must include @ref
+ * JackPortIsOutput.
+ *
+ * @pre The @ref JackPortFlags of the @a destination_port must include
+ * @ref JackPortIsInput.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_disconnect (jack_client_t *client,
+ const char *source_port,
+ const char *destination_port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Perform the same function as jack_disconnect() using port handles
+ * rather than names. This avoids the name lookup inherent in the
+ * name-based version.
+ *
+ * Clients connecting their own ports are likely to use this function,
+ * while generic connection clients (e.g. patchbays) would use
+ * jack_disconnect().
+ */
+int jack_port_disconnect (jack_client_t *client, jack_port_t *port) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the maximum number of characters in a full JACK port name
+ * including the final NULL character. This value is a constant.
+ *
+ * A port's full name contains the owning client name concatenated
+ * with a colon (:) followed by its short name and a NULL
+ * character.
+ */
+int jack_port_name_size(void) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the maximum number of characters in a JACK port type name
+ * including the final NULL character. This value is a constant.
+ */
+int jack_port_type_size(void) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the buffersize of a port of type @arg port_type.
+ *
+ * this function may only be called in a buffer_size callback.
+ */
+size_t jack_port_type_get_buffer_size (jack_client_t *client, const char *port_type) JACK_WEAK_EXPORT;
+
+/*@}*/
+
+/**
+ * @defgroup LatencyFunctions Managing and determining latency
+ *
+ * The purpose of JACK's latency API is to allow clients to
+ * easily answer two questions:
+ *
+ * - How long has it been since the data read from a port arrived
+ * at the edge of the JACK graph (either via a physical port
+ * or being synthesized from scratch)?
+ *
+ * - How long will it be before the data written to a port arrives
+ * at the edge of a JACK graph?
+
+ * To help answering these two questions, all JACK ports have two
+ * latency values associated with them, both measured in frames:
+ *
+ * <b>capture latency</b>: how long since the data read from
+ * the buffer of a port arrived at
+ * a port marked with JackPortIsTerminal.
+ * The data will have come from the "outside
+ * world" if the terminal port is also
+ * marked with JackPortIsPhysical, or
+ * will have been synthesized by the client
+ * that owns the terminal port.
+ *
+ * <b>playback latency</b>: how long until the data
+ * written to the buffer of port will reach a port
+ * marked with JackPortIsTerminal.
+ *
+ * Both latencies might potentially have more than one value
+ * because there may be multiple pathways to/from a given port
+ * and a terminal port. Latency is therefore generally
+ * expressed a min/max pair.
+ *
+ * In most common setups, the minimum and maximum latency
+ * are the same, but this design accommodates more complex
+ * routing, and allows applications (and thus users) to
+ * detect cases where routing is creating an anomalous
+ * situation that may either need fixing or more
+ * sophisticated handling by clients that care about
+ * latency.
+ *
+ * See also @ref jack_set_latency_callback for details on how
+ * clients that add latency to the signal path should interact
+ * with JACK to ensure that the correct latency figures are
+ * used.
+ * @{
+ */
+
+/**
+ * The port latency is zero by default. Clients that control
+ * physical hardware with non-zero latency should call this
+ * to set the latency to its correct value. Note that the value
+ * should include any systemic latency present "outside" the
+ * physical hardware controlled by the client. For example,
+ * for a client controlling a digital audio interface connected
+ * to an external digital converter, the latency setting should
+ * include both buffering by the audio interface *and* the converter.
+ *
+ * @deprecated This method will be removed in the next major
+ * release of JACK. It should not be used in new code, and should
+ * be replaced by a latency callback that calls @ref
+ * jack_port_set_latency_range().
+ */
+void jack_port_set_latency (jack_port_t *port, jack_nframes_t) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * return the latency range defined by @a mode for
+ * @a port, in frames.
+ *
+ * See @ref LatencyFunctions for the definition of each latency value.
+ *
+ * This function is best used from callbacks, specifically the latency callback.
+ * Before a port is connected, this returns the default latency: zero.
+ * Therefore it only makes sense to call jack_port_get_latency_range() when
+ * the port is connected, and that gets signalled by the latency callback.
+ * See @ref jack_set_latency_callback() for details.
+ */
+void jack_port_get_latency_range (jack_port_t *port, jack_latency_callback_mode_t mode, jack_latency_range_t *range) JACK_WEAK_EXPORT;
+
+/**
+ * set the minimum and maximum latencies defined by
+ * @a mode for @a port, in frames.
+ *
+ * See @ref LatencyFunctions for the definition of each latency value.
+ *
+ * This function should ONLY be used inside a latency
+ * callback. The client should determine the current
+ * value of the latency using @ref jack_port_get_latency_range()
+ * (called using the same mode as @a mode)
+ * and then add some number of frames to that reflects
+ * latency added by the client.
+ *
+ * How much latency a client adds will vary
+ * dramatically. For most clients, the answer is zero
+ * and there is no reason for them to register a latency
+ * callback and thus they should never call this
+ * function.
+ *
+ * More complex clients that take an input signal,
+ * transform it in some way and output the result but
+ * not during the same process() callback will
+ * generally know a single constant value to add
+ * to the value returned by @ref jack_port_get_latency_range().
+ *
+ * Such clients would register a latency callback (see
+ * @ref jack_set_latency_callback) and must know what input
+ * ports feed which output ports as part of their
+ * internal state. Their latency callback will update
+ * the ports' latency values appropriately.
+ *
+ * A pseudo-code example will help. The @a mode argument to the latency
+ * callback will determine whether playback or capture
+ * latency is being set. The callback will use
+ * @ref jack_port_set_latency_range() as follows:
+ *
+ * \code
+ * jack_latency_range_t range;
+ * if (mode == JackPlaybackLatency) {
+ * foreach input_port in (all self-registered port) {
+ * jack_port_get_latency_range (port_feeding_input_port, JackPlaybackLatency, &range);
+ * range.min += min_delay_added_as_signal_flows_from port_feeding to input_port;
+ * range.max += max_delay_added_as_signal_flows_from port_feeding to input_port;
+ * jack_port_set_latency_range (input_port, JackPlaybackLatency, &range);
+ * }
+ * } else if (mode == JackCaptureLatency) {
+ * foreach output_port in (all self-registered port) {
+ * jack_port_get_latency_range (port_fed_by_output_port, JackCaptureLatency, &range);
+ * range.min += min_delay_added_as_signal_flows_from_output_port_to_fed_by_port;
+ * range.max += max_delay_added_as_signal_flows_from_output_port_to_fed_by_port;
+ * jack_port_set_latency_range (output_port, JackCaptureLatency, &range);
+ * }
+ * }
+ * \endcode
+ *
+ * In this relatively simple pseudo-code example, it is assumed that
+ * each input port or output is connected to only 1 output or input
+ * port respectively.
+ *
+ * If a port is connected to more than 1 other port, then the
+ * range.min and range.max values passed to @ref
+ * jack_port_set_latency_range() should reflect the minimum and
+ * maximum values across all connected ports.
+ *
+ * See the description of @ref jack_set_latency_callback for more
+ * information.
+ */
+void jack_port_set_latency_range (jack_port_t *port, jack_latency_callback_mode_t mode, jack_latency_range_t *range) JACK_WEAK_EXPORT;
+
+/**
+ * Request a complete recomputation of all port latencies. This
+ * can be called by a client that has just changed the internal
+ * latency of its port using jack_port_set_latency
+ * and wants to ensure that all signal pathways in the graph
+ * are updated with respect to the values that will be returned
+ * by jack_port_get_total_latency. It allows a client
+ * to change multiple port latencies without triggering a
+ * recompute for each change.
+ *
+ * @return zero for successful execution of the request. non-zero
+ * otherwise.
+ */
+int jack_recompute_total_latencies (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the time (in frames) between data being available or
+ * delivered at/to a port, and the time at which it arrived at or is
+ * delivered to the "other side" of the port. E.g. for a physical
+ * audio output port, this is the time between writing to the port and
+ * when the signal will leave the connector. For a physical audio
+ * input port, this is the time between the sound arriving at the
+ * connector and the corresponding frames being readable from the
+ * port.
+ *
+ * @deprecated This method will be removed in the next major
+ * release of JACK. It should not be used in new code, and should
+ * be replaced by jack_port_get_latency_range() in any existing
+ * use cases.
+ */
+jack_nframes_t jack_port_get_latency (jack_port_t *port) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * The maximum of the sum of the latencies in every
+ * connection path that can be drawn between the port and other
+ * ports with the @ref JackPortIsTerminal flag set.
+ *
+ * @deprecated This method will be removed in the next major
+ * release of JACK. It should not be used in new code, and should
+ * be replaced by jack_port_get_latency_range() in any existing
+ * use cases.
+ */
+jack_nframes_t jack_port_get_total_latency (jack_client_t *client,
+ jack_port_t *port) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * Request a complete recomputation of a port's total latency. This
+ * can be called by a client that has just changed the internal
+ * latency of its port using jack_port_set_latency
+ * and wants to ensure that all signal pathways in the graph
+ * are updated with respect to the values that will be returned
+ * by jack_port_get_total_latency.
+ *
+ * @return zero for successful execution of the request. non-zero
+ * otherwise.
+ *
+ * @deprecated This method will be removed in the next major
+ * release of JACK. It should not be used in new code, and should
+ * be replaced by jack_recompute_total_latencies() in any existing
+ * use cases.
+ */
+int jack_recompute_total_latency (jack_client_t*, jack_port_t* port) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/*@}*/
+
+/**
+ * @defgroup PortSearching Looking up ports
+ * @{
+ */
+
+/**
+ * @param port_name_pattern A regular expression used to select
+ * ports by name. If NULL or of zero length, no selection based
+ * on name will be carried out.
+ * @param type_name_pattern A regular expression used to select
+ * ports by type. If NULL or of zero length, no selection based
+ * on type will be carried out.
+ * @param flags A value used to select ports by their flags.
+ * If zero, no selection based on flags will be carried out.
+ *
+ * @return a NULL-terminated array of ports that match the specified
+ * arguments. The caller is responsible for calling jack_free() any
+ * non-NULL returned value.
+ *
+ * @see jack_port_name_size(), jack_port_type_size()
+ */
+const char ** jack_get_ports (jack_client_t *client,
+ const char *port_name_pattern,
+ const char *type_name_pattern,
+ unsigned long flags) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return address of the jack_port_t named @a port_name.
+ *
+ * @see jack_port_name_size()
+ */
+jack_port_t * jack_port_by_name (jack_client_t *client, const char *port_name) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return address of the jack_port_t of a @a port_id.
+ */
+jack_port_t * jack_port_by_id (jack_client_t *client,
+ jack_port_id_t port_id) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+/**
+ * @defgroup TimeFunctions Handling time
+ * @{
+ *
+ * JACK time is in units of 'frames', according to the current sample rate.
+ * The absolute value of frame times is meaningless, frame times have meaning
+ * only relative to each other.
+ */
+
+/**
+ * @return the estimated time in frames that has passed since the JACK
+ * server began the current process cycle.
+ */
+jack_nframes_t jack_frames_since_cycle_start (const jack_client_t *) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the estimated current time in frames.
+ * This function is intended for use in other threads (not the process
+ * callback). The return value can be compared with the value of
+ * jack_last_frame_time to relate time in other threads to JACK time.
+ */
+jack_nframes_t jack_frame_time (const jack_client_t *) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the precise time at the start of the current process cycle.
+ * This function may only be used from the process callback, and can
+ * be used to interpret timestamps generated by jack_frame_time() in
+ * other threads with respect to the current process cycle.
+ *
+ * This is the only jack time function that returns exact time:
+ * when used during the process callback it always returns the same
+ * value (until the next process callback, where it will return
+ * that value + nframes, etc). The return value is guaranteed to be
+ * monotonic and linear in this fashion unless an xrun occurs.
+ * If an xrun occurs, clients must check this value again, as time
+ * may have advanced in a non-linear way (e.g. cycles may have been skipped).
+ */
+jack_nframes_t jack_last_frame_time (const jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * This function may only be used from the process callback.
+ * It provides the internal cycle timing information as used by
+ * most of the other time related functions. This allows the
+ * caller to map between frame counts and microseconds with full
+ * precision (i.e. without rounding frame times to integers),
+ * and also provides e.g. the microseconds time of the start of
+ * the current cycle directly (it has to be computed otherwise).
+ *
+ * If the return value is zero, the following information is
+ * provided in the variables pointed to by the arguments:
+ *
+ * current_frames: the frame time counter at the start of the
+ * current cycle, same as jack_last_frame_time().
+ * current_usecs: the microseconds time at the start of the
+ * current cycle.
+ * next_usecs: the microseconds time of the start of the next
+ * next cycle as computed by the DLL.
+ * period_usecs: the current best estimate of the period time in
+ * microseconds.
+ *
+ * NOTES:
+ *
+ * Because of the types used, all the returned values except period_usecs
+ * are unsigned. In computations mapping between frames and microseconds
+ * *signed* differences are required. The easiest way is to compute those
+ * separately and assign them to the appropriate signed variables,
+ * int32_t for frames and int64_t for usecs. See the implementation of
+ * jack_frames_to_time() and Jack_time_to_frames() for an example.
+ *
+ * Unless there was an xrun, skipped cycles, or the current cycle is the
+ * first after freewheeling or starting Jack, the value of current_usecs
+ * will always be the value of next_usecs of the previous cycle.
+ *
+ * The value of period_usecs will in general NOT be exactly equal to
+ * the difference of next_usecs and current_usecs. This is because to
+ * ensure stability of the DLL and continuity of the mapping, a fraction
+ * of the loop error must be included in next_usecs. For an accurate
+ * mapping between frames and microseconds, the difference of next_usecs
+ * and current_usecs should be used, and not period_usecs.
+ *
+ * @return zero if OK, non-zero otherwise.
+ */
+int jack_get_cycle_times(const jack_client_t *client,
+ jack_nframes_t *current_frames,
+ jack_time_t *current_usecs,
+ jack_time_t *next_usecs,
+ float *period_usecs) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the estimated time in microseconds of the specified frame time
+ */
+jack_time_t jack_frames_to_time(const jack_client_t *client, jack_nframes_t) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return the estimated time in frames for the specified system time.
+ */
+jack_nframes_t jack_time_to_frames(const jack_client_t *client, jack_time_t) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @return return JACK's current system time in microseconds,
+ * using the JACK clock source.
+ *
+ * The value returned is guaranteed to be monotonic, but not linear.
+ */
+jack_time_t jack_get_time(void) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+/**
+ * @defgroup ErrorOutput Controlling error/information output
+ */
+/*@{*/
+
+/**
+ * Display JACK error message.
+ *
+ * Set via jack_set_error_function(), otherwise a JACK-provided
+ * default will print @a msg (plus a newline) to stderr.
+ *
+ * @param msg error message text (no newline at end).
+ */
+extern void (*jack_error_callback)(const char *msg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Set the @ref jack_error_callback for error message display.
+ * Set it to NULL to restore default_jack_error_callback function.
+ *
+ * The JACK library provides two built-in callbacks for this purpose:
+ * default_jack_error_callback() and silent_jack_error_callback().
+ */
+void jack_set_error_function (void (*func)(const char *)) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Display JACK info message.
+ *
+ * Set via jack_set_info_function(), otherwise a JACK-provided
+ * default will print @a msg (plus a newline) to stdout.
+ *
+ * @param msg info message text (no newline at end).
+ */
+extern void (*jack_info_callback)(const char *msg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Set the @ref jack_info_callback for info message display.
+ * Set it to NULL to restore default_jack_info_callback function.
+ *
+ * The JACK library provides two built-in callbacks for this purpose:
+ * default_jack_info_callback() and silent_jack_info_callback().
+ */
+void jack_set_info_function (void (*func)(const char *)) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+/**
+ * The free function to be used on memory returned by jack_port_get_connections,
+ * jack_port_get_all_connections, jack_get_ports and jack_get_internal_client_name functions.
+ * This is MANDATORY on Windows when otherwise all nasty runtime version related crashes can occur.
+ * Developers are strongly encouraged to use this function instead of the standard "free" function in new code.
+ *
+ * @param ptr the memory pointer to be deallocated.
+ */
+void jack_free(void* ptr) JACK_OPTIONAL_WEAK_EXPORT;
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __jack_h__ */
diff --git a/pipewire-jack/jack/jslist.h b/pipewire-jack/jack/jslist.h
new file mode 100644
index 0000000..3ec0ce9
--- /dev/null
+++ b/pipewire-jack/jack/jslist.h
@@ -0,0 +1,293 @@
+/*
+ Based on gslist.c from glib-1.2.9 (LGPL).
+
+ Adaption to JACK, Copyright (C) 2002 Kai Vehmanen.
+ - replaced use of gtypes with normal ANSI C types
+ - glib's memory allocation routines replaced with
+ malloc/free calls
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __jack_jslist_h__
+#define __jack_jslist_h__
+
+#include <stdlib.h>
+#include <jack/systemdeps.h>
+
+#ifdef sun
+#define __inline__
+#endif
+
+typedef struct _JSList JSList;
+
+typedef int (*JCompareFunc) (void* a, void* b);
+struct _JSList
+{
+ void *data;
+ JSList *next;
+};
+
+static __inline__
+JSList*
+jack_slist_alloc (void)
+{
+ JSList *new_list;
+
+ new_list = (JSList*)malloc(sizeof(JSList));
+ if (new_list) {
+ new_list->data = NULL;
+ new_list->next = NULL;
+ }
+
+ return new_list;
+}
+
+static __inline__
+JSList*
+jack_slist_prepend (JSList* list, void* data)
+{
+ JSList *new_list;
+
+ new_list = (JSList*)malloc(sizeof(JSList));
+ if (new_list) {
+ new_list->data = data;
+ new_list->next = list;
+ }
+
+ return new_list;
+}
+
+#define jack_slist_next(slist) ((slist) ? (((JSList *)(slist))->next) : NULL)
+static __inline__
+JSList*
+jack_slist_last (JSList *list)
+{
+ if (list) {
+ while (list->next)
+ list = list->next;
+ }
+
+ return list;
+}
+
+static __inline__
+JSList*
+jack_slist_remove_link (JSList *list,
+ JSList *link)
+{
+ JSList *tmp;
+ JSList *prev;
+
+ prev = NULL;
+ tmp = list;
+
+ while (tmp) {
+ if (tmp == link) {
+ if (prev)
+ prev->next = tmp->next;
+ if (list == tmp)
+ list = list->next;
+
+ tmp->next = NULL;
+ break;
+ }
+
+ prev = tmp;
+ tmp = tmp->next;
+ }
+
+ return list;
+}
+
+static __inline__
+void
+jack_slist_free (JSList *list)
+{
+ while (list) {
+ JSList *next = list->next;
+ free(list);
+ list = next;
+ }
+}
+
+static __inline__
+void
+jack_slist_free_1 (JSList *list)
+{
+ if (list) {
+ free(list);
+ }
+}
+
+static __inline__
+JSList*
+jack_slist_remove (JSList *list,
+ void *data)
+{
+ JSList *tmp;
+ JSList *prev;
+
+ prev = NULL;
+ tmp = list;
+
+ while (tmp) {
+ if (tmp->data == data) {
+ if (prev)
+ prev->next = tmp->next;
+ if (list == tmp)
+ list = list->next;
+
+ tmp->next = NULL;
+ jack_slist_free (tmp);
+
+ break;
+ }
+
+ prev = tmp;
+ tmp = tmp->next;
+ }
+
+ return list;
+}
+
+static __inline__
+unsigned int
+jack_slist_length (JSList *list)
+{
+ unsigned int length;
+
+ length = 0;
+ while (list) {
+ length++;
+ list = list->next;
+ }
+
+ return length;
+}
+
+static __inline__
+JSList*
+jack_slist_find (JSList *list,
+ void *data)
+{
+ while (list) {
+ if (list->data == data)
+ break;
+ list = list->next;
+ }
+
+ return list;
+}
+
+static __inline__
+JSList*
+jack_slist_copy (JSList *list)
+{
+ JSList *new_list = NULL;
+
+ if (list) {
+ JSList *last;
+
+ new_list = jack_slist_alloc ();
+ new_list->data = list->data;
+ last = new_list;
+ list = list->next;
+ while (list) {
+ last->next = jack_slist_alloc ();
+ last = last->next;
+ last->data = list->data;
+ list = list->next;
+ }
+ }
+
+ return new_list;
+}
+
+static __inline__
+JSList*
+jack_slist_append (JSList *list,
+ void *data)
+{
+ JSList *new_list;
+ JSList *last;
+
+ new_list = jack_slist_alloc ();
+ new_list->data = data;
+
+ if (list) {
+ last = jack_slist_last (list);
+ last->next = new_list;
+
+ return list;
+ } else
+ return new_list;
+}
+
+static __inline__
+JSList*
+jack_slist_sort_merge (JSList *l1,
+ JSList *l2,
+ JCompareFunc compare_func)
+{
+ JSList list, *l;
+
+ l = &list;
+
+ while (l1 && l2) {
+ if (compare_func(l1->data, l2->data) < 0) {
+ l = l->next = l1;
+ l1 = l1->next;
+ } else {
+ l = l->next = l2;
+ l2 = l2->next;
+ }
+ }
+ l->next = l1 ? l1 : l2;
+
+ return list.next;
+}
+
+static __inline__
+JSList*
+jack_slist_sort (JSList *list,
+ JCompareFunc compare_func)
+{
+ JSList *l1, *l2;
+
+ if (!list)
+ return NULL;
+ if (!list->next)
+ return list;
+
+ l1 = list;
+ l2 = list->next;
+
+ while ((l2 = l2->next) != NULL) {
+ if ((l2 = l2->next) == NULL)
+ break;
+ l1 = l1->next;
+ }
+ l2 = l1->next;
+ l1->next = NULL;
+
+ return jack_slist_sort_merge (jack_slist_sort (list, compare_func),
+ jack_slist_sort (l2, compare_func),
+ compare_func);
+}
+
+#endif /* __jack_jslist_h__ */
+
diff --git a/pipewire-jack/jack/metadata.h b/pipewire-jack/jack/metadata.h
new file mode 100644
index 0000000..75a4105
--- /dev/null
+++ b/pipewire-jack/jack/metadata.h
@@ -0,0 +1,322 @@
+/*
+ Copyright (C) 2011-2014 David Robillard
+ Copyright (C) 2013 Paul Davis
+
+ This program is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or (at
+ your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software Foundation,
+ Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+*/
+
+/**
+ * @file jack/metadata.h
+ * @ingroup publicheader
+ * @brief JACK Metadata API
+ *
+ */
+
+#ifndef __jack_metadata_h__
+#define __jack_metadata_h__
+
+#include <jack/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @defgroup Metadata Metadata API.
+ * @{
+ */
+
+/**
+ * A single property (key:value pair).
+ *
+ * Although there is no semantics imposed on metadata keys and values, it is
+ * much less useful to use it to associate highly structured data with a port
+ * (or client), since this then implies the need for some (presumably
+ * library-based) code to parse the structure and be able to use it.
+ *
+ * The real goal of the metadata API is to be able to tag ports (and clients)
+ * with small amounts of data that is outside of the core JACK API but
+ * nevertheless useful.
+ */
+typedef struct {
+ /** The key of this property (URI string). */
+ const char* key;
+
+ /** The property value (null-terminated string). */
+ const char* data;
+
+ /**
+ * Type of data, either a MIME type or URI.
+ *
+ * If type is NULL or empty, the data is assumed to be a UTF-8 encoded
+ * string (text/plain). The data is a null-terminated string regardless of
+ * type, so values can always be copied, but clients should not try to
+ * interpret values of an unknown type.
+ *
+ * Example values:
+ * - image/png;base64 (base64 encoded PNG image)
+ * - http://www.w3.org/2001/XMLSchema#int (integer)
+ *
+ * Official types are preferred, but clients may use any syntactically
+ * valid MIME type (which start with a type and slash, like "text/...").
+ * If a URI type is used, it must be a complete absolute URI
+ * (which start with a scheme and colon, like "http:").
+ */
+ const char* type;
+} jack_property_t;
+
+/**
+ * Set a property on @p subject.
+ *
+ * See the above documentation for rules about @p subject and @p key.
+ * @param subject The subject to set the property on.
+ * @param key The key of the property.
+ * @param value The value of the property.
+ * @param type The type of the property. See the discussion of
+ * types in the definition of jack_property_t above.
+ * @return 0 on success.
+ */
+int
+jack_set_property(jack_client_t*,
+ jack_uuid_t subject,
+ const char* key,
+ const char* value,
+ const char* type);
+
+/**
+ * Get a property on @p subject.
+ *
+ * @param subject The subject to get the property from.
+ * @param key The key of the property.
+ * @param value Set to the value of the property if found, or NULL otherwise.
+ * The caller must free this value with jack_free().
+ * @param type The type of the property if set, or NULL. See the discussion
+ * of types in the definition of jack_property_t above.
+ * If non-null, the caller must free this value with jack_free().
+ *
+ * @return 0 on success, -1 if the @p subject has no @p key property.
+ */
+int
+jack_get_property(jack_uuid_t subject,
+ const char* key,
+ char** value,
+ char** type);
+
+/**
+ * A description of a subject (a set of properties).
+ */
+typedef struct {
+ jack_uuid_t subject; /**< Subject being described. */
+ uint32_t property_cnt; /**< Number of elements in "properties". */
+ jack_property_t* properties; /**< Array of properties. */
+ uint32_t property_size; /**< Private, do not use. */
+} jack_description_t;
+
+/**
+ * Free a description.
+ *
+ * @param desc a jack_description_t whose associated memory will all be released
+ * @param free_description_itself if non-zero, then @param desc will also be passed to free()
+ */
+void
+jack_free_description (jack_description_t* desc, int free_description_itself);
+
+/**
+ * Get a description of @p subject.
+ * @param subject The subject to get all properties of.
+ * @param desc Set to the description of subject if found, or NULL otherwise.
+ * The caller must free this value with jack_free_description().
+ * @return the number of properties, -1 if no @p subject with any properties exists.
+ */
+int
+jack_get_properties (jack_uuid_t subject,
+ jack_description_t* desc);
+
+/**
+ * Get descriptions for all subjects with metadata.
+ * @param descs Set to an array of descriptions.
+ * The caller must free each of these with jack_free_description(),
+ * and the array itself with jack_free().
+ * @return the number of descriptions, or -1 on error.
+ */
+int
+jack_get_all_properties (jack_description_t** descs);
+
+/**
+ * Remove a single property on a subject.
+ *
+ * @param client The JACK client making the request to remove the property.
+ * @param subject The subject to remove the property from.
+ * @param key The key of the property to be removed.
+ *
+ * @return 0 on success, -1 otherwise
+ */
+int jack_remove_property (jack_client_t* client, jack_uuid_t subject, const char* key);
+
+/**
+ * Remove all properties on a subject.
+ *
+ * @param client The JACK client making the request to remove some properties.
+ * @param subject The subject to remove all properties from.
+ *
+ * @return a count of the number of properties removed, or -1 on error.
+ */
+int jack_remove_properties (jack_client_t* client, jack_uuid_t subject);
+
+/**
+ * Remove all properties.
+ *
+ * WARNING!! This deletes all metadata managed by a running JACK server.
+ * Data lost cannot be recovered (though it can be recreated by new calls
+ * to jack_set_property()).
+ *
+ * @param client The JACK client making the request to remove all properties
+ *
+ * @return 0 on success, -1 otherwise
+ */
+int jack_remove_all_properties (jack_client_t* client);
+
+typedef enum {
+ PropertyCreated,
+ PropertyChanged,
+ PropertyDeleted
+} jack_property_change_t;
+
+/**
+ * Prototype for the client supplied function that is called by the
+ * engine anytime a property or properties have been modified.
+ *
+ * Note that when the key is empty, it means all properties have been
+ * modified. This is often used to indicate that the removal of all keys.
+ *
+ * @param subject The subject the change relates to, this can be either a client or port
+ * @param key The key of the modified property (URI string)
+ * @param change Wherever the key has been created, changed or deleted
+ * @param arg pointer to a client supplied structure
+ */
+typedef void (*JackPropertyChangeCallback)(jack_uuid_t subject,
+ const char* key,
+ jack_property_change_t change,
+ void* arg);
+
+/**
+ * Arrange for @p client to call @p callback whenever a property is created,
+ * changed or deleted.
+ *
+ * @param client the JACK client making the request
+ * @param callback the function to be invoked when a property change occurs
+ * @param arg the argument to be passed to @param callback when it is invoked
+ *
+ * @return 0 success, -1 otherwise.
+ */
+int jack_set_property_change_callback (jack_client_t* client,
+ JackPropertyChangeCallback callback,
+ void* arg);
+
+/**
+ * A value that identifies what the hardware port is connected to (an external
+ * device of some kind). Possible values might be "E-Piano" or "Master 2 Track".
+ */
+extern const char* JACK_METADATA_CONNECTED;
+
+/**
+ * The supported event types of an event port.
+ *
+ * This is a kludge around Jack only supporting MIDI, particularly for OSC.
+ * This property is a comma-separated list of event types, currently "MIDI" or
+ * "OSC". If this contains "OSC", the port may carry OSC bundles (first byte
+ * '#') or OSC messages (first byte '/'). Note that the "status byte" of both
+ * OSC events is not a valid MIDI status byte, so MIDI clients that check the
+ * status byte will gracefully ignore OSC messages if the user makes an
+ * inappropriate connection.
+ */
+extern const char* JACK_METADATA_EVENT_TYPES;
+
+/**
+ * A value that should be shown when attempting to identify the
+ * specific hardware outputs of a client. Typical values might be
+ * "ADAT1", "S/PDIF L" or "MADI 43".
+ */
+extern const char* JACK_METADATA_HARDWARE;
+
+/**
+ * A value with a MIME type of "image/png;base64" that is an encoding of an
+ * NxN (with 32 < N <= 128) image to be used when displaying a visual
+ * representation of that client or port.
+ */
+extern const char* JACK_METADATA_ICON_LARGE;
+
+/**
+ * The name of the icon for the subject (typically client).
+ *
+ * This is used for looking up icons on the system, possibly with many sizes or
+ * themes. Icons should be searched for according to the freedesktop Icon
+ *
+ * Theme Specification:
+ * https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
+ */
+extern const char* JACK_METADATA_ICON_NAME;
+
+/**
+ * A value with a MIME type of "image/png;base64" that is an encoding of an
+ * NxN (with N <=32) image to be used when displaying a visual representation
+ * of that client or port.
+ */
+extern const char* JACK_METADATA_ICON_SMALL;
+
+/**
+ * Order for a port.
+ *
+ * This is used to specify the best order to show ports in user interfaces.
+ * The value MUST be an integer. There are no other requirements, so there may
+ * be gaps in the orders for several ports. Applications should compare the
+ * orders of ports to determine their relative order, but must not assign any
+ * other relevance to order values.
+ *
+ * It is encouraged to use http://www.w3.org/2001/XMLSchema#int as the type.
+ */
+extern const char* JACK_METADATA_ORDER;
+
+/**
+ * A value that should be shown to the user when displaying a port to the user,
+ * unless the user has explicitly overridden that a request to show the port
+ * name, or some other key value.
+ */
+extern const char* JACK_METADATA_PRETTY_NAME;
+
+/**
+ */
+extern const char* JACK_METADATA_PORT_GROUP;
+
+/**
+ * The type of an audio signal.
+ *
+ * This property allows audio ports to be tagged with a "meaning". The value
+ * is a simple string. Currently, the only type is "CV", for "control voltage"
+ * ports. Hosts SHOULD be take care to not treat CV ports as audible and send
+ * their output directly to speakers. In particular, CV ports are not
+ * necessarily periodic at all and may have very high DC.
+ */
+extern const char* JACK_METADATA_SIGNAL_TYPE;
+
+/**
+ * @}
+ */
+
+#ifdef __cplusplus
+} /* namespace */
+#endif
+
+#endif /* __jack_metadata_h__ */
diff --git a/pipewire-jack/jack/midiport.h b/pipewire-jack/jack/midiport.h
new file mode 100644
index 0000000..0ae5e79
--- /dev/null
+++ b/pipewire-jack/jack/midiport.h
@@ -0,0 +1,197 @@
+/*
+ Copyright (C) 2004 Ian Esten
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+
+#ifndef __JACK_MIDIPORT_H
+#define __JACK_MIDIPORT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <jack/weakmacros.h>
+#include <jack/types.h>
+#include <stdlib.h>
+
+
+/** Type for raw event data contained in @ref jack_midi_event_t. */
+typedef unsigned char jack_midi_data_t;
+
+
+/** A Jack MIDI event. */
+typedef struct _jack_midi_event
+{
+ jack_nframes_t time; /**< Sample index at which event is valid */
+ size_t size; /**< Number of bytes of data in \a buffer */
+ jack_midi_data_t *buffer; /**< Raw MIDI data */
+} jack_midi_event_t;
+
+
+/**
+ * @defgroup MIDIAPI Reading and writing MIDI data
+ * @{
+ */
+
+/** Get number of events in a port buffer.
+ *
+ * @param port_buffer Port buffer from which to retrieve event.
+ * @return number of events inside @a port_buffer
+ */
+uint32_t
+jack_midi_get_event_count(void* port_buffer) JACK_OPTIONAL_WEAK_EXPORT;
+
+
+/** Get a MIDI event from an event port buffer.
+ *
+ * Jack MIDI is normalised, the MIDI event returned by this function is
+ * guaranteed to be a complete MIDI event (the status byte will always be
+ * present, and no realtime events will interspersed with the event).
+ *
+ * This rule does not apply to System Exclusive MIDI messages
+ * since they can be of arbitrary length.
+ * To maintain smooth realtime operation such events CAN be delivered
+ * as multiple, non-normalised events.
+ * The maximum size of one event "chunk" depends on the MIDI backend in use.
+ * For example the midiseq driver will create chunks of 256 bytes.
+ * The first SysEx "chunked" event starts with 0xF0 and the last
+ * delivered chunk ends with 0xF7.
+ * To receive the full SysEx message, a caller of jack_midi_event_get()
+ * must concatenate chunks until a chunk ends with 0xF7.
+ *
+ * @param event Event structure to store retrieved event in.
+ * @param port_buffer Port buffer from which to retrieve event.
+ * @param event_index Index of event to retrieve.
+ * @return 0 on success, ENODATA if buffer is empty.
+ */
+int
+jack_midi_event_get(jack_midi_event_t *event,
+ void *port_buffer,
+ uint32_t event_index) JACK_OPTIONAL_WEAK_EXPORT;
+
+
+/** Clear an event buffer.
+ *
+ * This should be called at the beginning of each process cycle before calling
+ * @ref jack_midi_event_reserve or @ref jack_midi_event_write. This
+ * function may not be called on an input port's buffer.
+ *
+ * @param port_buffer Port buffer to clear (must be an output port buffer).
+ */
+void
+jack_midi_clear_buffer(void *port_buffer) JACK_OPTIONAL_WEAK_EXPORT;
+
+/** Reset an event buffer (from data allocated outside of JACK).
+ *
+ * This should be called at the beginning of each process cycle before calling
+ * @ref jack_midi_event_reserve or @ref jack_midi_event_write. This
+ * function may not be called on an input port's buffer.
+ *
+ * @deprecated Please use jack_midi_clear_buffer().
+ *
+ * @param port_buffer Port buffer to reset.
+ */
+void
+jack_midi_reset_buffer(void *port_buffer) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+
+/** Get the size of the largest event that can be stored by the port.
+ *
+ * This function returns the current space available, taking into account
+ * events already stored in the port.
+ *
+ * @param port_buffer Port buffer to check size of.
+ */
+size_t
+jack_midi_max_event_size(void* port_buffer) JACK_OPTIONAL_WEAK_EXPORT;
+
+
+/** Allocate space for an event to be written to an event port buffer.
+ *
+ * Clients are to write the actual event data to be written starting at the
+ * pointer returned by this function. Clients must not write more than
+ * @a data_size bytes into this buffer. Clients must write normalised
+ * MIDI data to the port - no running status and no (1-byte) realtime
+ * messages interspersed with other messages (realtime messages are fine
+ * when they occur on their own, like other messages).
+ *
+ * Events must be written in order, sorted by their sample offsets.
+ * JACK will not sort the events for you, and will refuse to store
+ * out-of-order events.
+ *
+ * @param port_buffer Buffer to write event to.
+ * @param time Sample offset of event.
+ * @param data_size Length of event's raw data in bytes.
+ * @return Pointer to the beginning of the reserved event's data buffer, or
+ * NULL on error (ie not enough space).
+ */
+jack_midi_data_t*
+jack_midi_event_reserve(void *port_buffer,
+ jack_nframes_t time,
+ size_t data_size) JACK_OPTIONAL_WEAK_EXPORT;
+
+
+/** Write an event into an event port buffer.
+ *
+ * This function is simply a wrapper for @ref jack_midi_event_reserve
+ * which writes the event data into the space reserved in the buffer.
+ *
+ * Clients must not write more than
+ * @a data_size bytes into this buffer. Clients must write normalised
+ * MIDI data to the port - no running status and no (1-byte) realtime
+ * messages interspersed with other messages (realtime messages are fine
+ * when they occur on their own, like other messages).
+ *
+ * Events must be written in order, sorted by their sample offsets.
+ * JACK will not sort the events for you, and will refuse to store
+ * out-of-order events.
+ *
+ * @param port_buffer Buffer to write event to.
+ * @param time Sample offset of event.
+ * @param data Message data to be written.
+ * @param data_size Length of @a data in bytes.
+ * @return 0 on success, ENOBUFS if there's not enough space in buffer for event.
+ */
+int
+jack_midi_event_write(void *port_buffer,
+ jack_nframes_t time,
+ const jack_midi_data_t *data,
+ size_t data_size) JACK_OPTIONAL_WEAK_EXPORT;
+
+
+/** Get the number of events that could not be written to @a port_buffer.
+ *
+ * This function returning a non-zero value implies @a port_buffer is full.
+ * Currently the only way this can happen is if events are lost on port mixdown.
+ *
+ * @param port_buffer Port to receive count for.
+ * @returns Number of events that could not be written to @a port_buffer.
+ */
+uint32_t
+jack_midi_get_lost_event_count(void *port_buffer) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#endif /* __JACK_MIDIPORT_H */
+
+
diff --git a/pipewire-jack/jack/net.h b/pipewire-jack/jack/net.h
new file mode 100644
index 0000000..3f214be
--- /dev/null
+++ b/pipewire-jack/jack/net.h
@@ -0,0 +1,429 @@
+/*
+ Copyright (C) 2009-2010 Grame
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __net_h__
+#define __net_h__
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <jack/systemdeps.h>
+#include <jack/types.h>
+#include <jack/weakmacros.h>
+
+#define DEFAULT_MULTICAST_IP "225.3.19.154"
+#define DEFAULT_PORT 19000
+#define DEFAULT_MTU 1500
+#define MASTER_NAME_SIZE 256
+
+// Possible error codes
+
+#define NO_ERROR 0
+#define SOCKET_ERROR -1
+#define SYNC_PACKET_ERROR -2
+#define DATA_PACKET_ERROR -3
+
+#define RESTART_CB_API 1
+
+enum JackNetEncoder {
+
+ JackFloatEncoder = 0, // samples are transmitted as float
+ JackIntEncoder = 1, // samples are transmitted as 16 bits integer
+ JackCeltEncoder = 2, // samples are transmitted using CELT codec (http://www.celt-codec.org/)
+ JackOpusEncoder = 3, // samples are transmitted using OPUS codec (http://www.opus-codec.org/)
+};
+
+typedef struct {
+
+ int audio_input; // from master or to slave (-1 to take master audio physical inputs)
+ int audio_output; // to master or from slave (-1 to take master audio physical outputs)
+ int midi_input; // from master or to slave (-1 to take master MIDI physical inputs)
+ int midi_output; // to master or from slave (-1 to take master MIDI physical outputs)
+ int mtu; // network Maximum Transmission Unit
+ int time_out; // in second, -1 means infinite
+ int encoder; // encoder type (one of JackNetEncoder)
+ int kbps; // KB per second for CELT or OPUS codec
+ int latency; // network latency in number of buffers
+
+} jack_slave_t;
+
+typedef struct {
+
+ int audio_input; // master audio physical outputs (-1 to take slave wanted audio inputs)
+ int audio_output; // master audio physical inputs (-1 to take slave wanted audio outputs)
+ int midi_input; // master MIDI physical outputs (-1 to take slave wanted MIDI inputs)
+ int midi_output; // master MIDI physical inputs (-1 to take slave wanted MIDI outputs)
+ jack_nframes_t buffer_size; // master buffer size
+ jack_nframes_t sample_rate; // master sample rate
+ char master_name[MASTER_NAME_SIZE]; // master machine name
+ int time_out; // in second, -1 means infinite
+ int partial_cycle; // if 'true', partial buffers will be used
+
+} jack_master_t;
+
+/**
+ * jack_net_slave_t is an opaque type. You may only access it using the
+ * API provided.
+ */
+typedef struct _jack_net_slave jack_net_slave_t;
+
+ /**
+ * Open a network connection with the master machine.
+ *
+ * @param ip the multicast address of the master
+ * @param port the connection port
+ * @param name the JACK client name
+ * @param request a connection request structure
+ * @param result a connection result structure
+ *
+ * @return Opaque net handle if successful or NULL in case of error.
+ */
+jack_net_slave_t* jack_net_slave_open(const char* ip, int port, const char* name, jack_slave_t* request, jack_master_t* result);
+
+/**
+ * Close the network connection with the master machine.
+ *
+ * @param net the network connection to be closed
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_net_slave_close(jack_net_slave_t* net);
+
+/**
+ * Prototype for Process callback.
+ *
+ * @param nframes buffer size
+ * @param audio_input number of audio inputs
+ * @param audio_input_buffer an array of audio input buffers (from master)
+ * @param midi_input number of MIDI inputs
+ * @param midi_input_buffer an array of MIDI input buffers (from master)
+ * @param audio_output number of audio outputs
+ * @param audio_output_buffer an array of audio output buffers (to master)
+ * @param midi_output number of MIDI outputs
+ * @param midi_output_buffer an array of MIDI output buffers (to master)
+ * @param arg pointer to a client supplied structure supplied by jack_set_net_process_callback()
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef int (* JackNetSlaveProcessCallback) (jack_nframes_t buffer_size,
+ int audio_input,
+ float** audio_input_buffer,
+ int midi_input,
+ void** midi_input_buffer,
+ int audio_output,
+ float** audio_output_buffer,
+ int midi_output,
+ void** midi_output_buffer,
+ void* data);
+
+/**
+ * Set network process callback.
+ *
+ * @param net the network connection
+ * @param net_callback the process callback
+ * @param arg pointer to a client supplied structure
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_net_slave_process_callback(jack_net_slave_t * net, JackNetSlaveProcessCallback net_callback, void *arg);
+
+/**
+ * Start processing thread, the net_callback will start to be called.
+ *
+ * @param net the network connection
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_net_slave_activate(jack_net_slave_t* net);
+
+/**
+ * Stop processing thread.
+ *
+ * @param net the network connection
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_net_slave_deactivate(jack_net_slave_t* net);
+
+/**
+ * Test if slave is still active.
+ *
+ * @param net the network connection
+ *
+ * @return a boolean
+ */
+int jack_net_slave_is_active(jack_net_slave_t* net);
+
+/**
+ * Prototype for BufferSize callback.
+ *
+ * @param nframes buffer size
+ * @param arg pointer to a client supplied structure supplied by jack_set_net_buffer_size_callback()
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef int (*JackNetSlaveBufferSizeCallback)(jack_nframes_t nframes, void *arg);
+
+/**
+ * Set network buffer size callback.
+ *
+ * @param net the network connection
+ * @param bufsize_callback the buffer size callback
+ * @param arg pointer to a client supplied structure
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_net_slave_buffer_size_callback(jack_net_slave_t *net, JackNetSlaveBufferSizeCallback bufsize_callback, void *arg);
+
+/**
+ * Prototype for SampleRate callback.
+ *
+ * @param nframes sample rate
+ * @param arg pointer to a client supplied structure supplied by jack_set_net_sample_rate_callback()
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef int (*JackNetSlaveSampleRateCallback)(jack_nframes_t nframes, void *arg);
+
+/**
+ * Set network sample rate callback.
+ *
+ * @param net the network connection
+ * @param samplerate_callback the sample rate callback
+ * @param arg pointer to a client supplied structure
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_net_slave_sample_rate_callback(jack_net_slave_t *net, JackNetSlaveSampleRateCallback samplerate_callback, void *arg);
+
+/**
+ * Prototype for server Shutdown callback (if not set, the client will just restart, waiting for an available master again).
+ *
+ * @param arg pointer to a client supplied structure supplied by jack_set_net_shutdown_callback()
+ */
+typedef void (*JackNetSlaveShutdownCallback)(void* arg);
+
+/**
+ * Set network shutdown callback.
+ *
+ * @param net the network connection
+ * @param shutdown_callback the shutdown callback
+ * @param arg pointer to a client supplied structure
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_net_slave_shutdown_callback(jack_net_slave_t *net, JackNetSlaveShutdownCallback shutdown_callback, void *arg) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * Prototype for server Restart callback : this is the new preferable way to be notified when the master has disappeared.
+ * The client may want to retry connecting a certain number of time (which will be done using the time_out value given in jack_net_slave_open)
+ * by returning 0. Otherwise returning a non-zero error code will definitively close the connection
+ * (and jack_net_slave_is_active will later on return false).
+ * If both Shutdown and Restart are supplied, Restart callback will be used.
+ *
+ * @param arg pointer to a client supplied structure supplied by jack_set_net_restart_callback()
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+typedef int (*JackNetSlaveRestartCallback)(void* arg);
+
+/**
+ * Set network restart callback.
+ *
+ * @param net the network connection
+ * @param restart_callback the shutdown callback
+ * @param arg pointer to a client supplied structure
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_net_slave_restart_callback(jack_net_slave_t *net, JackNetSlaveRestartCallback restart_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Prototype for server Error callback.
+ *
+ * @param error_code an error code (see "Possible error codes")
+ * @param arg pointer to a client supplied structure supplied by jack_set_net_error_callback()
+ */
+typedef void (*JackNetSlaveErrorCallback) (int error_code, void* arg);
+
+/**
+ * Set error restart callback.
+ *
+ * @param net the network connection
+ * @param error_callback the error callback
+ * @param arg pointer to a client supplied structure
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_net_slave_error_callback(jack_net_slave_t *net, JackNetSlaveErrorCallback error_callback, void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * jack_net_master_t is an opaque type, you may only access it using the API provided.
+ */
+typedef struct _jack_net_master jack_net_master_t;
+
+ /**
+ * Open a network connection with the slave machine.
+ *
+ * @param ip the multicast address of the master
+ * @param port the connection port
+ * @param request a connection request structure
+ * @param result a connection result structure
+ *
+ * @return Opaque net handle if successful or NULL in case of error.
+ */
+jack_net_master_t* jack_net_master_open(const char* ip, int port, jack_master_t* request, jack_slave_t* result);
+
+/**
+ * Close the network connection with the slave machine.
+ *
+ * @param net the network connection to be closed
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_net_master_close(jack_net_master_t* net);
+
+/**
+ * Receive sync and data from the network (complete buffer).
+ *
+ * @param net the network connection
+ * @param audio_input number of audio inputs
+ * @param audio_input_buffer an array of audio input buffers
+ * @param midi_input number of MIDI inputs
+ * @param midi_input_buffer an array of MIDI input buffers
+ *
+ * @return zero on success, non-zero on error
+ */
+int jack_net_master_recv(jack_net_master_t* net, int audio_input, float** audio_input_buffer, int midi_input, void** midi_input_buffer);
+
+/**
+ * Receive sync and data from the network (incomplete buffer).
+ *
+ * @param net the network connection
+ * @param audio_input number of audio inputs
+ * @param audio_input_buffer an array of audio input buffers
+ * @param midi_input number of MIDI inputs
+ * @param midi_input_buffer an array of MIDI input buffers
+ * @param frames the number of frames to receive
+ *
+ * @return zero on success, non-zero on error
+ */
+int jack_net_master_recv_slice(jack_net_master_t* net, int audio_input, float** audio_input_buffer, int midi_input, void** midi_input_buffer, int frames);
+
+/**
+ * Send sync and data to the network (complete buffer).
+ *
+ * @param net the network connection
+ * @param audio_output number of audio outputs
+ * @param audio_output_buffer an array of audio output buffers
+ * @param midi_output number of MIDI outputs
+ * @param midi_output_buffer an array of MIDI output buffers
+ *
+ * @return zero on success, non-zero on error
+ */
+int jack_net_master_send(jack_net_master_t* net, int audio_output, float** audio_output_buffer, int midi_output, void** midi_output_buffer);
+
+/**
+ * Send sync and data to the network (incomplete buffer).
+ *
+ * @param net the network connection
+ * @param audio_output number of audio outputs
+ * @param audio_output_buffer an array of audio output buffers
+ * @param midi_output number of MIDI outputs
+ * @param midi_output_buffer an array of MIDI output buffers
+ * @param frames the number of frames to send
+ *
+ * @return zero on success, non-zero on error
+ */
+int jack_net_master_send_slice(jack_net_master_t* net, int audio_output, float** audio_output_buffer, int midi_output, void** midi_output_buffer, int frames);
+
+// Experimental Adapter API
+
+/**
+ * jack_adapter_t is an opaque type, you may only access it using the API provided.
+ */
+typedef struct _jack_adapter jack_adapter_t;
+
+/**
+ * Create an adapter.
+ *
+ * @param input number of audio inputs
+ * @param output of audio outputs
+ * @param host_buffer_size the host buffer size in frames
+ * @param host_sample_rate the host buffer sample rate
+ * @param adapted_buffer_size the adapted buffer size in frames
+ * @param adapted_sample_rate the adapted buffer sample rate
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+jack_adapter_t* jack_create_adapter(int input, int output,
+ jack_nframes_t host_buffer_size,
+ jack_nframes_t host_sample_rate,
+ jack_nframes_t adapted_buffer_size,
+ jack_nframes_t adapted_sample_rate);
+
+/**
+ * Destroy an adapter.
+ *
+ * @param adapter the adapter to be destroyed
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_destroy_adapter(jack_adapter_t* adapter);
+
+/**
+ * Flush internal state of an adapter.
+ *
+ * @param adapter the adapter to be flushed
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+void jack_flush_adapter(jack_adapter_t* adapter);
+
+/**
+ * Push input to and pull output from adapter ringbuffer.
+ *
+ * @param adapter the adapter
+ * @param input an array of audio input buffers
+ * @param output an array of audio output buffers
+ * @param frames number of frames
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_adapter_push_and_pull(jack_adapter_t* adapter, float** input, float** output, unsigned int frames);
+
+/**
+ * Pull input from and push output to adapter ringbuffer.
+ *
+ * @param adapter the adapter
+ * @param input an array of audio input buffers
+ * @param output an array of audio output buffers
+ * @param frames number of frames
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_adapter_pull_and_push(jack_adapter_t* adapter, float** input, float** output, unsigned int frames);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __net_h__ */
diff --git a/pipewire-jack/jack/ringbuffer.h b/pipewire-jack/jack/ringbuffer.h
new file mode 100644
index 0000000..5204383
--- /dev/null
+++ b/pipewire-jack/jack/ringbuffer.h
@@ -0,0 +1,243 @@
+/*
+ Copyright (C) 2000 Paul Davis
+ Copyright (C) 2003 Rohan Drape
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef _RINGBUFFER_H
+#define _RINGBUFFER_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <sys/types.h>
+
+/** @file ringbuffer.h
+ *
+ * A set of library functions to make lock-free ringbuffers available
+ * to JACK clients. The `capture_client.c' (in the example_clients
+ * directory) is a fully functioning user of this API.
+ *
+ * The key attribute of a ringbuffer is that it can be safely accessed
+ * by two threads simultaneously -- one reading from the buffer and
+ * the other writing to it -- without using any synchronization or
+ * mutual exclusion primitives. For this to work correctly, there can
+ * only be a single reader and a single writer thread. Their
+ * identities cannot be interchanged.
+ */
+
+typedef struct {
+ char *buf;
+ size_t len;
+}
+jack_ringbuffer_data_t ;
+
+typedef struct {
+ char *buf;
+ volatile size_t write_ptr;
+ volatile size_t read_ptr;
+ size_t size;
+ size_t size_mask;
+ int mlocked;
+}
+jack_ringbuffer_t ;
+
+/**
+ * Allocates a ringbuffer data structure of a specified size. The
+ * caller must arrange for a call to jack_ringbuffer_free() to release
+ * the memory associated with the ringbuffer.
+ *
+ * @param sz the ringbuffer size in bytes.
+ *
+ * @return a pointer to a new jack_ringbuffer_t, if successful; NULL
+ * otherwise.
+ */
+jack_ringbuffer_t *jack_ringbuffer_create(size_t sz);
+
+/**
+ * Frees the ringbuffer data structure allocated by an earlier call to
+ * jack_ringbuffer_create().
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ */
+void jack_ringbuffer_free(jack_ringbuffer_t *rb);
+
+/**
+ * Fill a data structure with a description of the current readable
+ * data held in the ringbuffer. This description is returned in a two
+ * element array of jack_ringbuffer_data_t. Two elements are needed
+ * because the data to be read may be split across the end of the
+ * ringbuffer.
+ *
+ * The first element will always contain a valid @a len field, which
+ * may be zero or greater. If the @a len field is non-zero, then data
+ * can be read in a contiguous fashion using the address given in the
+ * corresponding @a buf field.
+ *
+ * If the second element has a non-zero @a len field, then a second
+ * contiguous stretch of data can be read from the address given in
+ * its corresponding @a buf field.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ * @param vec a pointer to a 2 element array of jack_ringbuffer_data_t.
+ *
+ */
+void jack_ringbuffer_get_read_vector(const jack_ringbuffer_t *rb,
+ jack_ringbuffer_data_t *vec);
+
+/**
+ * Fill a data structure with a description of the current writable
+ * space in the ringbuffer. The description is returned in a two
+ * element array of jack_ringbuffer_data_t. Two elements are needed
+ * because the space available for writing may be split across the end
+ * of the ringbuffer.
+ *
+ * The first element will always contain a valid @a len field, which
+ * may be zero or greater. If the @a len field is non-zero, then data
+ * can be written in a contiguous fashion using the address given in
+ * the corresponding @a buf field.
+ *
+ * If the second element has a non-zero @a len field, then a second
+ * contiguous stretch of data can be written to the address given in
+ * the corresponding @a buf field.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ * @param vec a pointer to a 2 element array of jack_ringbuffer_data_t.
+ */
+void jack_ringbuffer_get_write_vector(const jack_ringbuffer_t *rb,
+ jack_ringbuffer_data_t *vec);
+
+/**
+ * Read data from the ringbuffer.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ * @param dest a pointer to a buffer where data read from the
+ * ringbuffer will go.
+ * @param cnt the number of bytes to read.
+ *
+ * @return the number of bytes read, which may range from 0 to cnt.
+ */
+size_t jack_ringbuffer_read(jack_ringbuffer_t *rb, char *dest, size_t cnt);
+
+/**
+ * Read data from the ringbuffer. Opposed to jack_ringbuffer_read()
+ * this function does not move the read pointer. Thus it's
+ * a convenient way to inspect data in the ringbuffer in a
+ * continuous fashion. The price is that the data is copied
+ * into a user provided buffer. For "raw" non-copy inspection
+ * of the data in the ringbuffer use jack_ringbuffer_get_read_vector().
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ * @param dest a pointer to a buffer where data read from the
+ * ringbuffer will go.
+ * @param cnt the number of bytes to read.
+ *
+ * @return the number of bytes read, which may range from 0 to cnt.
+ */
+size_t jack_ringbuffer_peek(jack_ringbuffer_t *rb, char *dest, size_t cnt);
+
+/**
+ * Advance the read pointer.
+ *
+ * After data have been read from the ringbuffer using the pointers
+ * returned by jack_ringbuffer_get_read_vector(), use this function to
+ * advance the buffer pointers, making that space available for future
+ * write operations.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ * @param cnt the number of bytes read.
+ */
+void jack_ringbuffer_read_advance(jack_ringbuffer_t *rb, size_t cnt);
+
+/**
+ * Return the number of bytes available for reading.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ *
+ * @return the number of bytes available to read.
+ */
+size_t jack_ringbuffer_read_space(const jack_ringbuffer_t *rb);
+
+/**
+ * Lock a ringbuffer data block into memory.
+ *
+ * Uses the mlock() system call. This is not a realtime operation.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ */
+int jack_ringbuffer_mlock(jack_ringbuffer_t *rb);
+
+/**
+ * Reset the read and write pointers, making an empty buffer.
+ *
+ * This is not thread safe.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ */
+void jack_ringbuffer_reset(jack_ringbuffer_t *rb);
+
+/**
+ * Reset the internal "available" size, and read and write pointers, making an empty buffer.
+ *
+ * This is not thread safe.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ * @param sz the new size, that must be less than allocated size.
+ */
+void jack_ringbuffer_reset_size (jack_ringbuffer_t * rb, size_t sz);
+
+/**
+ * Write data into the ringbuffer.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ * @param src a pointer to the data to be written to the ringbuffer.
+ * @param cnt the number of bytes to write.
+ *
+ * @return the number of bytes write, which may range from 0 to cnt
+ */
+size_t jack_ringbuffer_write(jack_ringbuffer_t *rb, const char *src,
+ size_t cnt);
+
+/**
+ * Advance the write pointer.
+ *
+ * After data have been written the ringbuffer using the pointers
+ * returned by jack_ringbuffer_get_write_vector(), use this function
+ * to advance the buffer pointer, making the data available for future
+ * read operations.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ * @param cnt the number of bytes written.
+ */
+void jack_ringbuffer_write_advance(jack_ringbuffer_t *rb, size_t cnt);
+
+/**
+ * Return the number of bytes available for writing.
+ *
+ * @param rb a pointer to the ringbuffer structure.
+ *
+ * @return the amount of free space (in bytes) available for writing.
+ */
+size_t jack_ringbuffer_write_space(const jack_ringbuffer_t *rb);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/pipewire-jack/jack/session.h b/pipewire-jack/jack/session.h
new file mode 100644
index 0000000..1198a09
--- /dev/null
+++ b/pipewire-jack/jack/session.h
@@ -0,0 +1,302 @@
+/*
+ Copyright (C) 2001 Paul Davis
+ Copyright (C) 2004 Jack O'Quin
+ Copyright (C) 2010 Torben Hohn
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+*/
+
+#ifndef __jack_session_h__
+#define __jack_session_h__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <jack/types.h>
+#include <jack/weakmacros.h>
+
+/**
+ * @defgroup SessionClientFunctions Session API for clients.
+ *
+ * @deprecated Use of JACK-Session is currently deprecated and unsupported.
+ * JACK developers recommend the use of NSM instead.
+ * See https://github.com/linuxaudio/new-session-manager
+ * @{
+ */
+
+
+/**
+ * Session event type.
+ *
+ * If a client can't save templates, i might just do a normal save.
+ *
+ * There is no "quit without saving" event because a client might refuse to
+ * quit when it has unsaved data, but other clients may have already quit.
+ * This results in too much confusion, so it is unsupported.
+ */
+enum JackSessionEventType {
+ /**
+ * Save the session completely.
+ *
+ * The client may save references to data outside the provided directory,
+ * but it must do so by creating a link inside the provided directory and
+ * referring to that in any save files. The client must not refer to data
+ * files outside the provided directory directly in save files, because
+ * this makes it impossible for the session manager to create a session
+ * archive for distribution or archival.
+ */
+ JackSessionSave = 1,
+
+ /**
+ * Save the session completely, then quit.
+ *
+ * The rules for saving are exactly the same as for JackSessionSave.
+ */
+ JackSessionSaveAndQuit = 2,
+
+ /**
+ * Save a session template.
+ *
+ * A session template is a "skeleton" of the session, but without any data.
+ * Clients must save a session that, when restored, will create the same
+ * ports as a full save would have. However, the actual data contained in
+ * the session may not be saved (e.g. a DAW would create the necessary
+ * tracks, but not save the actual recorded data).
+ */
+ JackSessionSaveTemplate = 3
+};
+
+typedef enum JackSessionEventType jack_session_event_type_t;
+
+/**
+ * @ref jack_session_flags_t bits
+ */
+enum JackSessionFlags {
+ /**
+ * An error occurred while saving.
+ */
+ JackSessionSaveError = 0x01,
+
+ /**
+ * Client needs to be run in a terminal.
+ */
+ JackSessionNeedTerminal = 0x02
+};
+
+/**
+ * Session flags.
+ */
+typedef enum JackSessionFlags jack_session_flags_t;
+
+struct _jack_session_event {
+ /**
+ * The type of this session event.
+ */
+ jack_session_event_type_t type;
+
+ /**
+ * Session directory path, with trailing separator.
+ *
+ * This directory is exclusive to the client; when saving the client may
+ * create any files it likes in this directory.
+ */
+ const char *session_dir;
+
+ /**
+ * Client UUID which must be passed to jack_client_open on session load.
+ *
+ * The client can specify this in the returned command line, or save it
+ * in a state file within the session directory.
+ */
+ const char *client_uuid;
+
+ /**
+ * Reply (set by client): the command line needed to restore the client.
+ *
+ * This is a platform dependent command line. It must contain
+ * ${SESSION_DIR} instead of the actual session directory path. More
+ * generally, just as in session files, clients should not include any
+ * paths outside the session directory here as this makes
+ * archival/distribution impossible.
+ *
+ * This field is set to NULL by Jack when the event is delivered to the
+ * client. The client must set to allocated memory that is safe to
+ * free(). This memory will be freed by jack_session_event_free.
+ */
+ char *command_line;
+
+ /**
+ * Reply (set by client): Session flags.
+ */
+ jack_session_flags_t flags;
+
+ /**
+ * Future flags. Set to zero for now.
+ */
+ uint32_t future;
+};
+
+typedef struct _jack_session_event jack_session_event_t;
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever a session notification is sent via jack_session_notify().
+ *
+ * Ownership of the memory of @a event is passed to the application.
+ * It must be freed using jack_session_event_free when it's not used anymore.
+ *
+ * The client must promptly call jack_session_reply for this event.
+ *
+ * @deprecated Use of JACK-Session is currently deprecated and unsupported.
+ * JACK developers recommend the use of NSM instead.
+ * See https://github.com/linuxaudio/new-session-manager
+ *
+ * @param event The event structure.
+ * @param arg Pointer to a client supplied structure.
+ */
+typedef void (*JackSessionCallback)(jack_session_event_t *event,
+ void *arg);
+
+/**
+ * Tell the JACK server to call @a session_callback when a session event
+ * is to be delivered.
+ *
+ * setting more than one session_callback per process is probably a design
+ * error. if you have a multiclient application its more sensible to create
+ * a jack_client with only a session callback set.
+ *
+ * @deprecated Use of JACK-Session is currently deprecated and unsupported.
+ * JACK developers recommend the use of NSM instead.
+ * See https://github.com/linuxaudio/new-session-manager
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_set_session_callback (jack_client_t *client,
+ JackSessionCallback session_callback,
+ void *arg) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * Reply to a session event.
+ *
+ * This can either be called directly from the callback, or later from a
+ * different thread. For example, it is possible to push the event through a
+ * queue and execute the save code from the GUI thread.
+ *
+ * @deprecated Use of JACK-Session is currently deprecated and unsupported.
+ * JACK developers recommend the use of NSM instead.
+ * See https://github.com/linuxaudio/new-session-manager
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int jack_session_reply (jack_client_t *client,
+ jack_session_event_t *event) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+
+/**
+ * Free memory used by a jack_session_event_t.
+ *
+ * This also frees the memory used by the command_line pointer, if its non NULL.
+ *
+ * @deprecated Use of JACK-Session is currently deprecated and unsupported.
+ * JACK developers recommend the use of NSM instead.
+ * See https://github.com/linuxaudio/new-session-manager
+ */
+void jack_session_event_free (jack_session_event_t *event) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+
+/**
+ * Get the assigned uuid for client.
+ * Safe to call from callback and all other threads.
+ *
+ * The caller is responsible for calling jack_free(3) on any non-NULL
+ * returned value.
+ */
+char *jack_client_get_uuid (jack_client_t *client) JACK_WEAK_EXPORT;
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup JackSessionManagerAPI API for a session manager.
+ *
+ * @{
+ */
+
+typedef struct {
+ const char *uuid;
+ const char *client_name;
+ const char *command;
+ jack_session_flags_t flags;
+} jack_session_command_t;
+
+/**
+ * Send an event to all clients listening for session callbacks.
+ *
+ * The returned strings of the clients are accumulated and returned as an array
+ * of jack_session_command_t. its terminated by ret[i].uuid == NULL target ==
+ * NULL means send to all interested clients. otherwise a clientname
+ */
+jack_session_command_t *jack_session_notify (
+ jack_client_t* client,
+ const char *target,
+ jack_session_event_type_t type,
+ const char *path) JACK_WEAK_EXPORT;
+
+/**
+ * Free the memory allocated by a session command.
+ *
+ * @deprecated Use of JACK-Session is currently deprecated and unsupported.
+ * JACK developers recommend the use of NSM instead.
+ * See https://github.com/linuxaudio/new-session-manager
+ */
+void jack_session_commands_free (jack_session_command_t *cmds) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * Reserve a client name and associate it with a UUID.
+ *
+ * When a client later calls jack_client_open() and specifies the UUID, jackd
+ * will assign the reserved name. This allows a session manager to know in
+ * advance under which client name its managed clients will appear.
+ *
+ * @return 0 on success, otherwise a non-zero error code
+ */
+int
+jack_reserve_client_name (jack_client_t *client,
+ const char *name,
+ const char *uuid) JACK_WEAK_EXPORT;
+
+/**
+ * Find out whether a client has set up a session callback.
+ *
+ * @deprecated Use of JACK-Session is currently deprecated and unsupported.
+ * JACK developers recommend the use of NSM instead.
+ * See https://github.com/linuxaudio/new-session-manager
+ *
+ * @return 0 when the client has no session callback, 1 when it has one.
+ * -1 on error.
+ */
+int
+jack_client_has_session_callback (jack_client_t *client, const char *client_name) JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT;
+
+/**
+ * @}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/pipewire-jack/jack/statistics.h b/pipewire-jack/jack/statistics.h
new file mode 100644
index 0000000..28c270d
--- /dev/null
+++ b/pipewire-jack/jack/statistics.h
@@ -0,0 +1,57 @@
+/*
+* Copyright (C) 2004 Rui Nuno Capela, Lee Revell
+*
+* This program is free software; you can redistribute it and/or
+* modify it under the terms of the GNU Lesser General Public License
+* as published by the Free Software Foundation; either version 2.1
+* of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public
+* License along with this program; if not, write to the Free
+* Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+* 02111-1307, USA.
+*
+*/
+
+#ifndef __statistics_h__
+#define __statistics_h__
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <jack/types.h>
+
+/**
+ * @return the maximum delay reported by the backend since
+ * startup or reset. When compared to the period size in usecs, this
+ * can be used to estimate the ideal period size for a given setup.
+ */
+float jack_get_max_delayed_usecs (jack_client_t *client);
+
+/**
+ * @return the delay in microseconds due to the most recent XRUN
+ * occurrence. This probably only makes sense when called from a @ref
+ * JackXRunCallback defined using jack_set_xrun_callback().
+ */
+float jack_get_xrun_delayed_usecs (jack_client_t *client);
+
+/**
+ * Reset the maximum delay counter. This would be useful
+ * to estimate the effect that a change to the configuration of a running
+ * system (e.g. toggling kernel preemption) has on the delay
+ * experienced by JACK, without having to restart the JACK engine.
+ */
+void jack_reset_max_delayed_usecs (jack_client_t *client);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __statistics_h__ */
diff --git a/pipewire-jack/jack/systemdeps.h b/pipewire-jack/jack/systemdeps.h
new file mode 100644
index 0000000..84b4ce3
--- /dev/null
+++ b/pipewire-jack/jack/systemdeps.h
@@ -0,0 +1,141 @@
+/*
+Copyright (C) 2004-2012 Grame
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef __jack_systemdeps_h__
+#define __jack_systemdeps_h__
+
+#ifndef POST_PACKED_STRUCTURE
+
+ #ifdef __GNUC__
+ /* POST_PACKED_STRUCTURE needs to be a macro which
+ expands into a compiler directive. The directive must
+ tell the compiler to arrange the preceding structure
+ declaration so that it is packed on byte-boundaries rather
+ than use the natural alignment of the processor and/or
+ compiler.
+ */
+
+ #define PRE_PACKED_STRUCTURE
+ #define POST_PACKED_STRUCTURE __attribute__((__packed__))
+
+ #else
+
+ #ifdef _MSC_VER
+ #define PRE_PACKED_STRUCTURE1 __pragma(pack(push,1))
+ #define PRE_PACKED_STRUCTURE PRE_PACKED_STRUCTURE1
+ /* PRE_PACKED_STRUCTURE needs to be a macro which
+ expands into a compiler directive. The directive must
+ tell the compiler to arrange the following structure
+ declaration so that it is packed on byte-boundaries rather
+ than use the natural alignment of the processor and/or
+ compiler.
+ */
+ #define POST_PACKED_STRUCTURE ;__pragma(pack(pop))
+ /* and POST_PACKED_STRUCTURE needs to be a macro which
+ restores the packing to its previous setting */
+ #else
+ #define PRE_PACKED_STRUCTURE
+ #define POST_PACKED_STRUCTURE
+ #endif /* _MSC_VER */
+
+ #endif /* __GNUC__ */
+
+#endif
+
+#if defined(_WIN32) && !defined(__CYGWIN__) && !defined(GNU_WIN32)
+
+ #ifdef __MINGW32__
+ # include <winsock2.h> // mingw gives warning if we include windows.h before winsock2.h
+ #endif
+
+ #include <windows.h>
+
+ #ifdef _MSC_VER /* Microsoft compiler */
+ #define __inline__ inline
+ #if (!defined(int8_t) && !defined(_STDINT_H))
+ #define __int8_t_defined
+ typedef INT8 int8_t;
+ typedef UINT8 uint8_t;
+ typedef INT16 int16_t;
+ typedef UINT16 uint16_t;
+ typedef INT32 int32_t;
+ typedef UINT32 uint32_t;
+ typedef INT64 int64_t;
+ typedef UINT64 uint64_t;
+ #endif
+ #elif __MINGW32__ /* MINGW */
+ #include <stdint.h>
+ #include <sys/types.h>
+ #else /* other compilers ...*/
+ #include <inttypes.h>
+ #include <pthread.h>
+ #include <sys/types.h>
+ #endif
+
+ #if !defined(_PTHREAD_H) && !defined(PTHREAD_WIN32)
+ /**
+ * to make jack API independent of different thread implementations,
+ * we define jack_native_thread_t to HANDLE here.
+ */
+ typedef HANDLE jack_native_thread_t;
+ #else
+ #ifdef PTHREAD_WIN32 // Added by JE - 10-10-2011
+ #include <ptw32/pthread.h> // Makes sure we #include the ptw32 version !
+ #endif
+ /**
+ * to make jack API independent of different thread implementations,
+ * we define jack_native_thread_t to pthread_t here.
+ */
+ typedef pthread_t jack_native_thread_t;
+ #endif
+
+#endif /* _WIN32 && !__CYGWIN__ && !GNU_WIN32 */
+
+#if defined(__APPLE__) || defined(__linux__) || defined(__sun__) || defined(sun) || defined(__unix__) || defined(__CYGWIN__) || defined(GNU_WIN32)
+
+ #if defined(__CYGWIN__) || defined(GNU_WIN32)
+ #include <stdint.h>
+ #endif
+ #include <inttypes.h>
+ #include <pthread.h>
+ #include <sys/types.h>
+
+ /**
+ * to make jack API independent of different thread implementations,
+ * we define jack_native_thread_t to pthread_t here.
+ */
+ typedef pthread_t jack_native_thread_t;
+
+#endif /* __APPLE__ || __linux__ || __sun__ || sun */
+
+#if (defined(__arm__) || defined(__aarch64__) || defined(__mips__) || defined(__ppc__) || defined(__powerpc__)) && !defined(__APPLE__)
+ #undef POST_PACKED_STRUCTURE
+ #define POST_PACKED_STRUCTURE
+#endif /* __arm__ || __aarch64__ || __mips__ || __ppc__ || __powerpc__ */
+
+/** define JACK_LIB_EXPORT, useful for internal clients */
+#if defined(_WIN32)
+ #define JACK_LIB_EXPORT __declspec(dllexport)
+#elif defined(__GNUC__)
+ #define JACK_LIB_EXPORT __attribute__((visibility("default")))
+#else
+ #define JACK_LIB_EXPORT
+#endif
+
+#endif /* __jack_systemdeps_h__ */
diff --git a/pipewire-jack/jack/thread.h b/pipewire-jack/jack/thread.h
new file mode 100644
index 0000000..776c520
--- /dev/null
+++ b/pipewire-jack/jack/thread.h
@@ -0,0 +1,160 @@
+/*
+ Copyright (C) 2004 Paul Davis
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __jack_thread_h__
+#define __jack_thread_h__
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <jack/systemdeps.h>
+#include <jack/weakmacros.h>
+
+/* use 512KB stack per thread - the default is way too high to be feasible
+ * with mlockall() on many systems */
+#define THREAD_STACK 524288
+
+/** @file thread.h
+ *
+ * Library functions to standardize thread creation for JACK and its
+ * clients. These interfaces hide some system variations in the
+ * handling of realtime scheduling and associated privileges.
+ */
+
+/**
+ * @defgroup ClientThreads Creating and managing client threads
+ * @{
+ */
+
+ /**
+ * @returns if JACK is running with realtime scheduling, this returns
+ * the priority that any JACK-created client threads will run at.
+ * Otherwise returns -1.
+ */
+
+int jack_client_real_time_priority (jack_client_t*) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * @returns if JACK is running with realtime scheduling, this returns
+ * the maximum priority that a JACK client thread should use if the thread
+ * is subject to realtime scheduling. Otherwise returns -1.
+ */
+
+int jack_client_max_real_time_priority (jack_client_t*) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Attempt to enable realtime scheduling for a thread. On some
+ * systems that may require special privileges.
+ *
+ * @param thread POSIX thread ID.
+ * @param priority requested thread priority.
+ *
+ * @returns 0, if successful; EPERM, if the calling process lacks
+ * required realtime privileges; otherwise some other error number.
+ */
+int jack_acquire_real_time_scheduling (jack_native_thread_t thread, int priority) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Create a thread for JACK or one of its clients. The thread is
+ * created executing @a start_routine with @a arg as its sole
+ * argument.
+ *
+ * @param client the JACK client for whom the thread is being created. May be
+ * NULL if the client is being created within the JACK server.
+ * @param thread place to return POSIX thread ID.
+ * @param priority thread priority, if realtime.
+ * @param realtime true for the thread to use realtime scheduling. On
+ * some systems that may require special privileges.
+ * @param start_routine function the thread calls when it starts.
+ * @param arg parameter passed to the @a start_routine.
+ *
+ * @returns 0, if successful; otherwise some error number.
+ */
+int jack_client_create_thread (jack_client_t* client,
+ jack_native_thread_t *thread,
+ int priority,
+ int realtime, /* boolean */
+ void *(*start_routine)(void*),
+ void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Drop realtime scheduling for a thread.
+ *
+ * @param thread POSIX thread ID.
+ *
+ * @returns 0, if successful; otherwise an error number.
+ */
+int jack_drop_real_time_scheduling (jack_native_thread_t thread) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Stop the thread, waiting for the thread handler to terminate.
+ *
+ * @param thread POSIX thread ID.
+ *
+ * @returns 0, if successful; otherwise an error number.
+ */
+int jack_client_stop_thread(jack_client_t* client, jack_native_thread_t thread) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Kill the thread.
+ *
+ * @param thread POSIX thread ID.
+ *
+ * @returns 0, if successful; otherwise an error number.
+ */
+ int jack_client_kill_thread(jack_client_t* client, jack_native_thread_t thread) JACK_OPTIONAL_WEAK_EXPORT;
+
+#ifndef _WIN32
+
+ typedef int (*jack_thread_creator_t)(pthread_t*,
+ const pthread_attr_t*,
+ void* (*function)(void*),
+ void* arg);
+/**
+ * This function can be used in very very specialized cases
+ * where it is necessary that client threads created by JACK
+ * are created by something other than pthread_create(). After
+ * it is used, any threads that JACK needs for the client will
+ * will be created by calling the function passed to this
+ * function.
+ *
+ * No normal application/client should consider calling this.
+ * The specific case for which it was created involves running
+ * win32/x86 plugins under Wine on Linux, where it is necessary
+ * that all threads that might call win32 functions are known
+ * to Wine.
+ *
+ * Set it to NULL to restore thread creation function.
+ *
+ * @param creator a function that creates a new thread
+ *
+ */
+void jack_set_thread_creator (jack_thread_creator_t creator) JACK_OPTIONAL_WEAK_EXPORT;
+
+#endif
+
+/* @} */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __jack_thread_h__ */
diff --git a/pipewire-jack/jack/transport.h b/pipewire-jack/jack/transport.h
new file mode 100644
index 0000000..4cec6e0
--- /dev/null
+++ b/pipewire-jack/jack/transport.h
@@ -0,0 +1,247 @@
+/*
+ Copyright (C) 2002 Paul Davis
+ Copyright (C) 2003 Jack O'Quin
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __jack_transport_h__
+#define __jack_transport_h__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <jack/types.h>
+#include <jack/weakmacros.h>
+
+/**
+ * @defgroup TransportControl Transport and Timebase control
+ * @{
+ */
+
+/**
+ * Called by the timebase master to release itself from that
+ * responsibility.
+ *
+ * If the timebase master releases the timebase or leaves the JACK
+ * graph for any reason, the JACK engine takes over at the start of
+ * the next process cycle. The transport state does not change. If
+ * rolling, it continues to play, with frame numbers as the only
+ * available position information.
+ *
+ * @see jack_set_timebase_callback
+ *
+ * @param client the JACK client structure.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_release_timebase (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Register (or unregister) as a slow-sync client, one that cannot
+ * respond immediately to transport position changes.
+ *
+ * The @a sync_callback will be invoked at the first available
+ * opportunity after its registration is complete. If the client is
+ * currently active this will be the following process cycle,
+ * otherwise it will be the first cycle after calling jack_activate().
+ * After that, it runs according to the ::JackSyncCallback rules.
+ * Clients that don't set a @a sync_callback are assumed to be ready
+ * immediately any time the transport wants to start.
+ *
+ * @param client the JACK client structure.
+ * @param sync_callback is a realtime function that returns TRUE when
+ * the client is ready. Setting @a sync_callback to NULL declares that
+ * this client no longer requires slow-sync processing.
+ * @param arg an argument for the @a sync_callback function.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_set_sync_callback (jack_client_t *client,
+ JackSyncCallback sync_callback,
+ void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Set the timeout value for slow-sync clients.
+ *
+ * This timeout prevents unresponsive slow-sync clients from
+ * completely halting the transport mechanism. The default is two
+ * seconds. When the timeout expires, the transport starts rolling,
+ * even if some slow-sync clients are still unready. The @a
+ * sync_callbacks of these clients continue being invoked, giving them
+ * a chance to catch up.
+ *
+ * @see jack_set_sync_callback
+ *
+ * @param client the JACK client structure.
+ * @param timeout is delay (in microseconds) before the timeout expires.
+ *
+ * @return 0 on success, otherwise a non-zero error code.
+ */
+int jack_set_sync_timeout (jack_client_t *client,
+ jack_time_t timeout) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Register as timebase master for the JACK subsystem.
+ *
+ * The timebase master registers a callback that updates extended
+ * position information such as beats or timecode whenever necessary.
+ * Without this extended information, there is no need for this
+ * function.
+ *
+ * There is never more than one master at a time. When a new client
+ * takes over, the former @a timebase_callback is no longer called.
+ * Taking over the timebase may be done conditionally, so it fails if
+ * there was a master already.
+ *
+ * @param client the JACK client structure.
+ * @param conditional non-zero for a conditional request.
+ * @param timebase_callback is a realtime function that returns
+ * position information.
+ * @param arg an argument for the @a timebase_callback function.
+ *
+ * @return
+ * - 0 on success;
+ * - EBUSY if a conditional request fails because there was already a
+ * timebase master;
+ * - other non-zero error code.
+ */
+int jack_set_timebase_callback (jack_client_t *client,
+ int conditional,
+ JackTimebaseCallback timebase_callback,
+ void *arg) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Reposition the transport to a new frame number.
+ *
+ * May be called at any time by any client. The new position takes
+ * effect in two process cycles. If there are slow-sync clients and
+ * the transport is already rolling, it will enter the
+ * ::JackTransportStarting state and begin invoking their @a
+ * sync_callbacks until ready. This function is realtime-safe.
+ *
+ * @see jack_transport_reposition, jack_set_sync_callback
+ *
+ * @param client the JACK client structure.
+ * @param frame frame number of new transport position.
+ *
+ * @return 0 if valid request, non-zero otherwise.
+ */
+int jack_transport_locate (jack_client_t *client,
+ jack_nframes_t frame) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Query the current transport state and position.
+ *
+ * This function is realtime-safe, and can be called from any thread.
+ * If called from the process thread, @a pos corresponds to the first
+ * frame of the current cycle and the state returned is valid for the
+ * entire cycle.
+ *
+ * @param client the JACK client structure.
+ * @param pos pointer to structure for returning current transport
+ * position; @a pos->valid will show which fields contain valid data.
+ * If @a pos is NULL, do not return position information.
+ *
+ * @return Current transport state.
+ */
+jack_transport_state_t jack_transport_query (const jack_client_t *client,
+ jack_position_t *pos) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Return an estimate of the current transport frame,
+ * including any time elapsed since the last transport
+ * positional update.
+ *
+ * @param client the JACK client structure
+ */
+jack_nframes_t jack_get_current_transport_frame (const jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Request a new transport position.
+ *
+ * May be called at any time by any client. The new position takes
+ * effect in two process cycles. If there are slow-sync clients and
+ * the transport is already rolling, it will enter the
+ * ::JackTransportStarting state and begin invoking their @a
+ * sync_callbacks until ready. This function is realtime-safe.
+ *
+ * @see jack_transport_locate, jack_set_sync_callback
+ *
+ * @param client the JACK client structure.
+ * @param pos requested new transport position.
+ *
+ * @return 0 if valid request, EINVAL if position structure rejected.
+ */
+int jack_transport_reposition (jack_client_t *client,
+ const jack_position_t *pos) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Start the JACK transport rolling.
+ *
+ * Any client can make this request at any time. It takes effect no
+ * sooner than the next process cycle, perhaps later if there are
+ * slow-sync clients. This function is realtime-safe.
+ *
+ * @see jack_set_sync_callback
+ *
+ * @param client the JACK client structure.
+ */
+void jack_transport_start (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Stop the JACK transport.
+ *
+ * Any client can make this request at any time. It takes effect on
+ * the next process cycle. This function is realtime-safe.
+ *
+ * @param client the JACK client structure.
+ */
+void jack_transport_stop (jack_client_t *client) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Gets the current transport info structure (deprecated).
+ *
+ * @param client the JACK client structure.
+ * @param tinfo current transport info structure. The "valid" field
+ * describes which fields contain valid data.
+ *
+ * @deprecated This is for compatibility with the earlier transport
+ * interface. Use jack_transport_query(), instead.
+ *
+ * @pre Must be called from the process thread.
+ */
+void jack_get_transport_info (jack_client_t *client,
+ jack_transport_info_t *tinfo) JACK_OPTIONAL_WEAK_EXPORT;
+
+/**
+ * Set the transport info structure (deprecated).
+ *
+ * @deprecated This function still exists for compatibility with the
+ * earlier transport interface, but it does nothing. Instead, define
+ * a ::JackTimebaseCallback.
+ */
+void jack_set_transport_info (jack_client_t *client,
+ jack_transport_info_t *tinfo) JACK_OPTIONAL_WEAK_EXPORT;
+
+/*@}*/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __jack_transport_h__ */
diff --git a/pipewire-jack/jack/types.h b/pipewire-jack/jack/types.h
new file mode 100644
index 0000000..b62af96
--- /dev/null
+++ b/pipewire-jack/jack/types.h
@@ -0,0 +1,740 @@
+/*
+ Copyright (C) 2001 Paul Davis
+ Copyright (C) 2004 Jack O'Quin
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __jack_types_h__
+#define __jack_types_h__
+
+#include <jack/systemdeps.h>
+
+typedef uint64_t jack_uuid_t;
+
+typedef int32_t jack_shmsize_t;
+
+/**
+ * Type used to represent sample frame counts.
+ */
+typedef uint32_t jack_nframes_t;
+
+/**
+ * Maximum value that can be stored in jack_nframes_t
+ */
+#define JACK_MAX_FRAMES (4294967295U) /* This should be UINT32_MAX, but C++ has a problem with that. */
+
+/**
+ * Type used to represent the value of free running
+ * monotonic clock with units of microseconds.
+ */
+typedef uint64_t jack_time_t;
+
+/**
+ * Maximum size of @a load_init string passed to an internal client
+ * jack_initialize() function via jack_internal_client_load().
+ */
+#define JACK_LOAD_INIT_LIMIT 1024
+
+/**
+ * jack_intclient_t is an opaque type representing a loaded internal
+ * client. You may only access it using the API provided in @ref
+ * intclient.h "<jack/intclient.h>".
+ */
+typedef uint64_t jack_intclient_t;
+
+/**
+ * jack_port_t is an opaque type. You may only access it using the
+ * API provided.
+ */
+typedef struct _jack_port jack_port_t;
+
+/**
+ * jack_client_t is an opaque type. You may only access it using the
+ * API provided.
+ */
+typedef struct _jack_client jack_client_t;
+
+/**
+ * Ports have unique ids. A port registration callback is the only
+ * place you ever need to know their value.
+ */
+typedef uint32_t jack_port_id_t;
+
+typedef uint32_t jack_port_type_id_t;
+
+/**
+ * @ref jack_options_t bits
+ */
+enum JackOptions {
+
+ /**
+ * Null value to use when no option bits are needed.
+ */
+ JackNullOption = 0x00,
+
+ /**
+ * Do not automatically start the JACK server when it is not
+ * already running. This option is always selected if
+ * \$JACK_NO_START_SERVER is defined in the calling process
+ * environment.
+ */
+ JackNoStartServer = 0x01,
+
+ /**
+ * Use the exact client name requested. Otherwise, JACK
+ * automatically generates a unique one, if needed.
+ */
+ JackUseExactName = 0x02,
+
+ /**
+ * Open with optional <em>(char *) server_name</em> parameter.
+ */
+ JackServerName = 0x04,
+
+ /**
+ * Load internal client from optional <em>(char *)
+ * load_name</em>. Otherwise use the @a client_name.
+ */
+ JackLoadName = 0x08,
+
+ /**
+ * Pass optional <em>(char *) load_init</em> string to the
+ * jack_initialize() entry point of an internal client.
+ */
+ JackLoadInit = 0x10,
+
+ /**
+ * pass a SessionID Token this allows the sessionmanager to identify the client again.
+ */
+ JackSessionID = 0x20
+};
+
+/** Valid options for opening an external client. */
+#define JackOpenOptions (JackSessionID|JackServerName|JackNoStartServer|JackUseExactName)
+
+/** Valid options for loading an internal client. */
+#define JackLoadOptions (JackLoadInit|JackLoadName|JackUseExactName)
+
+/**
+ * Options for several JACK operations, formed by OR-ing together the
+ * relevant @ref JackOptions bits.
+ */
+typedef enum JackOptions jack_options_t;
+
+/**
+ * @ref jack_status_t bits
+ */
+enum JackStatus {
+
+ /**
+ * Overall operation failed.
+ */
+ JackFailure = 0x01,
+
+ /**
+ * The operation contained an invalid or unsupported option.
+ */
+ JackInvalidOption = 0x02,
+
+ /**
+ * The desired client name was not unique. With the @ref
+ * JackUseExactName option this situation is fatal. Otherwise,
+ * the name was modified by appending a dash and a two-digit
+ * number in the range "-01" to "-99". The
+ * jack_get_client_name() function will return the exact string
+ * that was used. If the specified @a client_name plus these
+ * extra characters would be too long, the open fails instead.
+ */
+ JackNameNotUnique = 0x04,
+
+ /**
+ * The JACK server was started as a result of this operation.
+ * Otherwise, it was running already. In either case the caller
+ * is now connected to jackd, so there is no race condition.
+ * When the server shuts down, the client will find out.
+ */
+ JackServerStarted = 0x08,
+
+ /**
+ * Unable to connect to the JACK server.
+ */
+ JackServerFailed = 0x10,
+
+ /**
+ * Communication error with the JACK server.
+ */
+ JackServerError = 0x20,
+
+ /**
+ * Requested client does not exist.
+ */
+ JackNoSuchClient = 0x40,
+
+ /**
+ * Unable to load internal client
+ */
+ JackLoadFailure = 0x80,
+
+ /**
+ * Unable to initialize client
+ */
+ JackInitFailure = 0x100,
+
+ /**
+ * Unable to access shared memory
+ */
+ JackShmFailure = 0x200,
+
+ /**
+ * Client's protocol version does not match
+ */
+ JackVersionError = 0x400,
+
+ /**
+ * Backend error
+ */
+ JackBackendError = 0x800,
+
+ /**
+ * Client zombified failure
+ */
+ JackClientZombie = 0x1000
+};
+
+/**
+ * Status word returned from several JACK operations, formed by
+ * OR-ing together the relevant @ref JackStatus bits.
+ */
+typedef enum JackStatus jack_status_t;
+
+/**
+ * @ref jack_latency_callback_mode_t
+ */
+enum JackLatencyCallbackMode {
+
+ /**
+ * Latency Callback for Capture Latency.
+ * Input Ports have their latency value setup.
+ * In the Callback the client needs to set the latency of the output ports
+ */
+ JackCaptureLatency,
+
+ /**
+ * Latency Callback for Playback Latency.
+ * Output Ports have their latency value setup.
+ * In the Callback the client needs to set the latency of the input ports
+ */
+ JackPlaybackLatency
+
+};
+
+/**
+ * Type of Latency Callback (Capture or Playback)
+ */
+typedef enum JackLatencyCallbackMode jack_latency_callback_mode_t;
+
+/**
+ * Prototype for the client supplied function that is called
+ * by the engine when port latencies need to be recalculated
+ *
+ * @param mode playback or capture latency
+ * @param arg pointer to a client supplied data
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef void (*JackLatencyCallback)(jack_latency_callback_mode_t mode, void *arg);
+
+/**
+ * the new latency API operates on Ranges.
+ */
+PRE_PACKED_STRUCTURE
+struct _jack_latency_range
+{
+ /**
+ * minimum latency
+ */
+ jack_nframes_t min;
+ /**
+ * maximum latency
+ */
+ jack_nframes_t max;
+} POST_PACKED_STRUCTURE;
+
+typedef struct _jack_latency_range jack_latency_range_t;
+
+/**
+ * Prototype for the client supplied function that is called
+ * by the engine anytime there is work to be done.
+ *
+ * @pre nframes == jack_get_buffer_size()
+ * @pre nframes == pow(2,x)
+ *
+ * @param nframes number of frames to process
+ * @param arg pointer to a client supplied structure
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef int (*JackProcessCallback)(jack_nframes_t nframes, void *arg);
+
+/**
+ * Prototype for the client thread routine called
+ * by the engine when the client is inserted in the graph.
+ *
+ * @param arg pointer to a client supplied structure
+ *
+ */
+typedef void *(*JackThreadCallback)(void* arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * once after the creation of the thread in which other
+ * callbacks will be made. Special thread characteristics
+ * can be set from this callback, for example. This is a
+ * highly specialized callback and most clients will not
+ * and should not use it.
+ *
+ * @param arg pointer to a client supplied structure
+ *
+ * @return void
+ */
+typedef void (*JackThreadInitCallback)(void *arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever the processing graph is reordered.
+ *
+ * @param arg pointer to a client supplied structure
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef int (*JackGraphOrderCallback)(void *arg);
+
+/**
+ * Prototype for the client-supplied function that is called whenever
+ * an xrun has occurred.
+ *
+ * @see jack_get_xrun_delayed_usecs()
+ *
+ * @param arg pointer to a client supplied structure
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef int (*JackXRunCallback)(void *arg);
+
+/**
+ * Prototype for the @a bufsize_callback that is invoked whenever the
+ * JACK engine buffer size changes. Although this function is called
+ * in the JACK process thread, the normal process cycle is suspended
+ * during its operation, causing a gap in the audio flow. So, the @a
+ * bufsize_callback can allocate storage, touch memory not previously
+ * referenced, and perform other operations that are not realtime
+ * safe.
+ *
+ * @param nframes buffer size
+ * @param arg pointer supplied by jack_set_buffer_size_callback().
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef int (*JackBufferSizeCallback)(jack_nframes_t nframes, void *arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * when the engine sample rate changes.
+ *
+ * @param nframes new engine sample rate
+ * @param arg pointer to a client supplied structure
+ *
+ * @return zero on success, non-zero on error
+ */
+typedef int (*JackSampleRateCallback)(jack_nframes_t nframes, void *arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever a port is registered or unregistered.
+ *
+ * @param port the ID of the port
+ * @param arg pointer to a client supplied data
+ * @param register non-zero if the port is being registered,
+ * zero if the port is being unregistered
+ */
+typedef void (*JackPortRegistrationCallback)(jack_port_id_t port, int /* register */, void *arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever a client is registered or unregistered.
+ *
+ * @param name a null-terminated string containing the client name
+ * @param register non-zero if the client is being registered,
+ * zero if the client is being unregistered
+ * @param arg pointer to a client supplied structure
+ */
+typedef void (*JackClientRegistrationCallback)(const char* name, int /* register */, void *arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever a port is connected or disconnected.
+ *
+ * @param a one of two ports connected or disconnected
+ * @param b one of two ports connected or disconnected
+ * @param connect non-zero if ports were connected
+ * zero if ports were disconnected
+ * @param arg pointer to a client supplied data
+ */
+typedef void (*JackPortConnectCallback)(jack_port_id_t a, jack_port_id_t b, int connect, void* arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever the port name has been changed.
+ *
+ * @param port the port that has been renamed
+ * @param new_name the new name
+ * @param arg pointer to a client supplied structure
+ */
+typedef void (*JackPortRenameCallback)(jack_port_id_t port, const char* old_name, const char* new_name, void *arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever jackd starts or stops freewheeling.
+ *
+ * @param starting non-zero if we start starting to freewheel, zero otherwise
+ * @param arg pointer to a client supplied structure
+ */
+typedef void (*JackFreewheelCallback)(int starting, void *arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever jackd is shutdown. Note that after server shutdown,
+ * the client pointer is *not* deallocated by libjack,
+ * the application is responsible to properly use jack_client_close()
+ * to release client resources. Warning: jack_client_close() cannot be
+ * safely used inside the shutdown callback and has to be called outside of
+ * the callback context.
+ *
+ * @param arg pointer to a client supplied structure
+ */
+typedef void (*JackShutdownCallback)(void *arg);
+
+/**
+ * Prototype for the client supplied function that is called
+ * whenever jackd is shutdown. Note that after server shutdown,
+ * the client pointer is *not* deallocated by libjack,
+ * the application is responsible to properly use jack_client_close()
+ * to release client resources. Warning: jack_client_close() cannot be
+ * safely used inside the shutdown callback and has to be called outside of
+ * the callback context.
+
+ * @param code a status word, formed by OR-ing together the relevant @ref JackStatus bits.
+ * @param reason a string describing the shutdown reason (backend failure, server crash... etc...).
+ * Note that this string will not be available anymore after the callback returns, so possibly copy it.
+ * @param arg pointer to a client supplied structure
+ */
+typedef void (*JackInfoShutdownCallback)(jack_status_t code, const char* reason, void *arg);
+
+/**
+ * Used for the type argument of jack_port_register() for default
+ * audio ports and midi ports.
+ */
+#define JACK_DEFAULT_AUDIO_TYPE "32 bit float mono audio"
+#define JACK_DEFAULT_MIDI_TYPE "8 bit raw midi"
+
+/**
+ * For convenience, use this typedef if you want to be able to change
+ * between float and double. You may want to typedef sample_t to
+ * jack_default_audio_sample_t in your application.
+ */
+typedef float jack_default_audio_sample_t;
+
+/**
+ * A port has a set of flags that are formed by AND-ing together the
+ * desired values from the list below. The flags "JackPortIsInput" and
+ * "JackPortIsOutput" are mutually exclusive and it is an error to use
+ * them both.
+ */
+enum JackPortFlags {
+
+ /**
+ * if JackPortIsInput is set, then the port can receive
+ * data.
+ */
+ JackPortIsInput = 0x1,
+
+ /**
+ * if JackPortIsOutput is set, then data can be read from
+ * the port.
+ */
+ JackPortIsOutput = 0x2,
+
+ /**
+ * if JackPortIsPhysical is set, then the port corresponds
+ * to some kind of physical I/O connector.
+ */
+ JackPortIsPhysical = 0x4,
+
+ /**
+ * if JackPortCanMonitor is set, then a call to
+ * jack_port_request_monitor() makes sense.
+ *
+ * Precisely what this means is dependent on the client. A typical
+ * result of it being called with TRUE as the second argument is
+ * that data that would be available from an output port (with
+ * JackPortIsPhysical set) is sent to a physical output connector
+ * as well, so that it can be heard/seen/whatever.
+ *
+ * Clients that do not control physical interfaces
+ * should never create ports with this bit set.
+ */
+ JackPortCanMonitor = 0x8,
+
+ /**
+ * JackPortIsTerminal means:
+ *
+ * for an input port: the data received by the port
+ * will not be passed on or made
+ * available at any other port
+ *
+ * for an output port: the data available at the port
+ * does not originate from any other port
+ *
+ * Audio synthesizers, I/O hardware interface clients, HDR
+ * systems are examples of clients that would set this flag for
+ * their ports.
+ */
+ JackPortIsTerminal = 0x10,
+
+};
+
+/**
+ * Transport states.
+ */
+typedef enum {
+
+ /* the order matters for binary compatibility */
+ JackTransportStopped = 0, /**< Transport halted */
+ JackTransportRolling = 1, /**< Transport playing */
+ JackTransportLooping = 2, /**< For OLD_TRANSPORT, now ignored */
+ JackTransportStarting = 3, /**< Waiting for sync ready */
+ JackTransportNetStarting = 4, /**< Waiting for sync ready on the network*/
+
+} jack_transport_state_t;
+
+typedef uint64_t jack_unique_t; /**< Unique ID (opaque) */
+
+/**
+ * Optional struct jack_position_t fields.
+ */
+typedef enum {
+
+ JackPositionBBT = 0x10, /**< Bar, Beat, Tick */
+ JackPositionTimecode = 0x20, /**< External timecode */
+ JackBBTFrameOffset = 0x40, /**< Frame offset of BBT information */
+ JackAudioVideoRatio = 0x80, /**< audio frames per video frame */
+ JackVideoFrameOffset = 0x100 /**< frame offset of first video frame */
+
+} jack_position_bits_t;
+
+/** all valid position bits */
+#define JACK_POSITION_MASK (JackPositionBBT|JackPositionTimecode)
+#define EXTENDED_TIME_INFO
+
+PRE_PACKED_STRUCTURE
+struct _jack_position {
+
+ /* these four cannot be set from clients: the server sets them */
+ jack_unique_t unique_1; /**< unique ID */
+ jack_time_t usecs; /**< monotonic, free-rolling */
+ jack_nframes_t frame_rate; /**< current frame rate (per second) */
+ jack_nframes_t frame; /**< frame number, always present */
+
+ jack_position_bits_t valid; /**< which other fields are valid */
+
+ /* JackPositionBBT fields: */
+ int32_t bar; /**< current bar */
+ int32_t beat; /**< current beat-within-bar */
+ int32_t tick; /**< current tick-within-beat */
+ double bar_start_tick;
+
+ float beats_per_bar; /**< time signature "numerator" */
+ float beat_type; /**< time signature "denominator" */
+ double ticks_per_beat;
+ double beats_per_minute;
+
+ /* JackPositionTimecode fields: (EXPERIMENTAL: could change) */
+ double frame_time; /**< current time in seconds */
+ double next_time; /**< next sequential frame_time
+ (unless repositioned) */
+
+ /* JackBBTFrameOffset fields: */
+ jack_nframes_t bbt_offset; /**< frame offset for the BBT fields
+ (the given bar, beat, and tick
+ values actually refer to a time
+ frame_offset frames before the
+ start of the cycle), should
+ be assumed to be 0 if
+ JackBBTFrameOffset is not
+ set. If JackBBTFrameOffset is
+ set and this value is zero, the BBT
+ time refers to the first frame of this
+ cycle. If the value is positive,
+ the BBT time refers to a frame that
+ many frames before the start of the
+ cycle. */
+
+ /* JACK video positional data (experimental) */
+
+ float audio_frames_per_video_frame; /**< number of audio frames
+ per video frame. Should be assumed
+ zero if JackAudioVideoRatio is not
+ set. If JackAudioVideoRatio is set
+ and the value is zero, no video
+ data exists within the JACK graph */
+
+ jack_nframes_t video_offset; /**< audio frame at which the first video
+ frame in this cycle occurs. Should
+ be assumed to be 0 if JackVideoFrameOffset
+ is not set. If JackVideoFrameOffset is
+ set, but the value is zero, there is
+ no video frame within this cycle. */
+
+ /* For binary compatibility, new fields should be allocated from
+ * this padding area with new valid bits controlling access, so
+ * the existing structure size and offsets are preserved. */
+ int32_t padding[7];
+
+ /* When (unique_1 == unique_2) the contents are consistent. */
+ jack_unique_t unique_2; /**< unique ID */
+
+} POST_PACKED_STRUCTURE;
+
+typedef struct _jack_position jack_position_t;
+
+/**
+ * Prototype for the @a sync_callback defined by slow-sync clients.
+ * When the client is active, this callback is invoked just before
+ * process() in the same thread. This occurs once after registration,
+ * then subsequently whenever some client requests a new position, or
+ * the transport enters the ::JackTransportStarting state. This
+ * realtime function must not wait.
+ *
+ * The transport @a state will be:
+ *
+ * - ::JackTransportStopped when a new position is requested;
+ * - ::JackTransportStarting when the transport is waiting to start;
+ * - ::JackTransportRolling when the timeout has expired, and the
+ * position is now a moving target.
+ *
+ * @param state current transport state.
+ * @param pos new transport position.
+ * @param arg the argument supplied by jack_set_sync_callback().
+ *
+ * @return TRUE (non-zero) when ready to roll.
+ */
+typedef int (*JackSyncCallback)(jack_transport_state_t state,
+ jack_position_t *pos,
+ void *arg);
+
+
+/**
+ * Prototype for the @a timebase_callback used to provide extended
+ * position information. Its output affects all of the following
+ * process cycle. This realtime function must not wait.
+ *
+ * This function is called immediately after process() in the same
+ * thread whenever the transport is rolling, or when any client has
+ * requested a new position in the previous cycle. The first cycle
+ * after jack_set_timebase_callback() is also treated as a new
+ * position, or the first cycle after jack_activate() if the client
+ * had been inactive.
+ *
+ * The timebase master may not use its @a pos argument to set @a
+ * pos->frame. To change position, use jack_transport_reposition() or
+ * jack_transport_locate(). These functions are realtime-safe, the @a
+ * timebase_callback can call them directly.
+ *
+ * @param state current transport state.
+ * @param nframes number of frames in current period.
+ * @param pos address of the position structure for the next cycle; @a
+ * pos->frame will be its frame number. If @a new_pos is FALSE, this
+ * structure contains extended position information from the current
+ * cycle. If TRUE, it contains whatever was set by the requester.
+ * The @a timebase_callback's task is to update the extended
+ * information here.
+ * @param new_pos TRUE (non-zero) for a newly requested @a pos, or for
+ * the first cycle after the @a timebase_callback is defined.
+ * @param arg the argument supplied by jack_set_timebase_callback().
+ */
+typedef void (*JackTimebaseCallback)(jack_transport_state_t state,
+ jack_nframes_t nframes,
+ jack_position_t *pos,
+ int new_pos,
+ void *arg);
+
+/*********************************************************************
+ * The following interfaces are DEPRECATED. They are only provided
+ * for compatibility with the earlier JACK transport implementation.
+ *********************************************************************/
+
+/**
+ * Optional struct jack_transport_info_t fields.
+ *
+ * @see jack_position_bits_t.
+ */
+typedef enum {
+
+ JackTransportState = 0x1, /**< Transport state */
+ JackTransportPosition = 0x2, /**< Frame number */
+ JackTransportLoop = 0x4, /**< Loop boundaries (ignored) */
+ JackTransportSMPTE = 0x8, /**< SMPTE (ignored) */
+ JackTransportBBT = 0x10 /**< Bar, Beat, Tick */
+
+} jack_transport_bits_t;
+
+/**
+ * Deprecated struct for transport position information.
+ *
+ * @deprecated This is for compatibility with the earlier transport
+ * interface. Use the jack_position_t struct, instead.
+ */
+typedef struct {
+
+ /* these two cannot be set from clients: the server sets them */
+
+ jack_nframes_t frame_rate; /**< current frame rate (per second) */
+ jack_time_t usecs; /**< monotonic, free-rolling */
+
+ jack_transport_bits_t valid; /**< which fields are legal to read */
+ jack_transport_state_t transport_state;
+ jack_nframes_t frame;
+ jack_nframes_t loop_start;
+ jack_nframes_t loop_end;
+
+ long smpte_offset; /**< SMPTE offset (from frame 0) */
+ float smpte_frame_rate; /**< 29.97, 30, 24 etc. */
+
+ int bar;
+ int beat;
+ int tick;
+ double bar_start_tick;
+
+ float beats_per_bar;
+ float beat_type;
+ double ticks_per_beat;
+ double beats_per_minute;
+
+} jack_transport_info_t;
+
+
+#endif /* __jack_types_h__ */
diff --git a/pipewire-jack/jack/uuid.h b/pipewire-jack/jack/uuid.h
new file mode 100644
index 0000000..406c119
--- /dev/null
+++ b/pipewire-jack/jack/uuid.h
@@ -0,0 +1,50 @@
+/*
+ Copyright (C) 2013 Paul Davis
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __jack_uuid_h__
+#define __jack_uuid_h__
+
+#include <jack/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define JACK_UUID_SIZE 36
+#define JACK_UUID_STRING_SIZE (JACK_UUID_SIZE+1) /* includes trailing null */
+#define JACK_UUID_EMPTY_INITIALIZER 0
+
+extern jack_uuid_t jack_client_uuid_generate (void);
+extern jack_uuid_t jack_port_uuid_generate (uint32_t port_id);
+
+extern uint32_t jack_uuid_to_index (jack_uuid_t);
+
+extern int jack_uuid_compare (jack_uuid_t, jack_uuid_t);
+extern void jack_uuid_copy (jack_uuid_t* dst, jack_uuid_t src);
+extern void jack_uuid_clear (jack_uuid_t*);
+extern int jack_uuid_parse (const char *buf, jack_uuid_t*);
+extern void jack_uuid_unparse (jack_uuid_t, char buf[JACK_UUID_STRING_SIZE]);
+extern int jack_uuid_empty (jack_uuid_t);
+
+#ifdef __cplusplus
+} /* namespace */
+#endif
+
+#endif /* __jack_uuid_h__ */
+
diff --git a/pipewire-jack/jack/weakjack.h b/pipewire-jack/jack/weakjack.h
new file mode 100644
index 0000000..c253c63
--- /dev/null
+++ b/pipewire-jack/jack/weakjack.h
@@ -0,0 +1,125 @@
+/*
+ Copyright (C) 2010 Paul Davis
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __weakjack_h__
+#define __weakjack_h__
+
+/**
+ * @defgroup WeakLinkage Managing support for newer/older versions of JACK
+ * @{ One challenge faced by developers is that of taking
+ * advantage of new features introduced in new versions
+ * of [ JACK ] while still supporting older versions of
+ * the system. Normally, if an application uses a new
+ * feature in a library/API, it is unable to run on
+ * earlier versions of the library/API that do not
+ * support that feature. Such applications would either
+ * fail to launch or crash when an attempt to use the
+ * feature was made. This problem cane be solved using
+ * weakly-linked symbols.
+ *
+ * When a symbol in a framework is defined as weakly
+ * linked, the symbol does not have to be present at
+ * runtime for a process to continue running. The static
+ * linker identifies a weakly linked symbol as such in
+ * any code module that references the symbol. The
+ * dynamic linker uses this same information at runtime
+ * to determine whether a process can continue
+ * running. If a weakly linked symbol is not present in
+ * the framework, the code module can continue to run as
+ * long as it does not reference the symbol. However, if
+ * the symbol is present, the code can use it normally.
+ *
+ * (adapted from: http://developer.apple.com/library/mac/#documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html)
+ *
+ * A concrete example will help. Suppose that someone uses a version
+ * of a JACK client we'll call "Jill". Jill was linked against a version
+ * of JACK that contains a newer part of the API (say, jack_set_latency_callback())
+ * and would like to use it if it is available.
+ *
+ * When Jill is run on a system that has a suitably "new" version of
+ * JACK, this function will be available entirely normally. But if Jill
+ * is run on a system with an old version of JACK, the function isn't
+ * available.
+ *
+ * With normal symbol linkage, this would create a startup error whenever
+ * someone tries to run Jill with the "old" version of JACK. However, functions
+ * added to JACK after version 0.116.2 are all declared to have "weak" linkage
+ * which means that their absence doesn't cause an error during program
+ * startup. Instead, Jill can test whether or not the symbol jack_set_latency_callback
+ * is null or not. If its null, it means that the JACK installed on this machine
+ * is too old to support this function. If it's not null, then Jill can use it
+ * just like any other function in the API. For example:
+ *
+ * \code
+ * if (jack_set_latency_callback) {
+ * jack_set_latency_callback (jill_client, jill_latency_callback, arg);
+ * }
+ * \endcode
+ *
+ * However, there are clients that may want to use this approach to parts of the
+ * the JACK API that predate 0.116.2. For example, they might want to see if even
+ * really old basic parts of the API like jack_client_open() exist at runtime.
+ *
+ * Such clients should include <jack/weakjack.h> before any other JACK header.
+ * This will make the \b entire JACK API be subject to weak linkage, so that any
+ * and all functions can be checked for existence at runtime. It is important
+ * to understand that very few clients need to do this - if you use this
+ * feature you should have a clear reason to do so.
+ *
+ *
+ */
+
+#ifdef __APPLE__
+#define WEAK_ATTRIBUTE weak_import
+#else
+#define WEAK_ATTRIBUTE __weak__
+#endif
+
+#ifndef JACK_OPTIONAL_WEAK_EXPORT
+/* JACK_OPTIONAL_WEAK_EXPORT needs to be a macro which
+ expands into a compiler directive. If non-null, the directive
+ must tell the compiler to arrange for weak linkage of
+ the symbol it used with. For this to work fully may
+ require linker arguments for the client as well.
+*/
+#ifdef __GNUC__
+#define JACK_OPTIONAL_WEAK_EXPORT __attribute__((WEAK_ATTRIBUTE))
+#else
+/* Add other things here for non-gcc platforms */
+#endif
+#endif
+
+#ifndef JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT
+/* JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT needs to be a macro
+ which expands into a compiler directive. If non-null, the directive
+ must tell the compiler to arrange for weak linkage of the
+ symbol it is used with AND optionally to mark the symbol
+ as deprecated. For this to work fully may require
+ linker arguments for the client as well.
+*/
+#ifdef __GNUC__
+#define JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT __attribute__((WEAK_ATTRIBUTE,__deprecated__))
+#else
+/* Add other things here for non-gcc platforms */
+#endif
+#endif
+
+/*@}*/
+
+#endif /* weakjack */
diff --git a/pipewire-jack/jack/weakmacros.h b/pipewire-jack/jack/weakmacros.h
new file mode 100644
index 0000000..944fddb
--- /dev/null
+++ b/pipewire-jack/jack/weakmacros.h
@@ -0,0 +1,97 @@
+/*
+ Copyright (C) 2010 Paul Davis
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+#ifndef __weakmacros_h__
+#define __weakmacros_h__
+
+/*************************************************************
+ * NOTE: JACK_WEAK_EXPORT ***MUST*** be used on every function
+ * added to the JACK API after the 0.116.2 release.
+ *
+ * Functions that predate this release are marked with
+ * JACK_WEAK_OPTIONAL_EXPORT which can be defined at compile
+ * time in a variety of ways. The default definition is empty,
+ * so that these symbols get normal linkage. If you wish to
+ * use all JACK symbols with weak linkage, include
+ * <jack/weakjack.h> before jack.h.
+ *************************************************************/
+
+#ifdef __APPLE__
+#define WEAK_ATTRIBUTE weak_import
+#else
+#define WEAK_ATTRIBUTE __weak__
+#endif
+
+#ifndef JACK_WEAK_EXPORT
+#ifdef __GNUC__
+/* JACK_WEAK_EXPORT needs to be a macro which
+ expands into a compiler directive. If non-null, the directive
+ must tell the compiler to arrange for weak linkage of
+ the symbol it used with. For this to work full may
+ require linker arguments in the client as well.
+*/
+
+#ifdef _WIN32
+ /*
+ Not working with __declspec(dllexport) so normal linking
+ Linking with JackWeakAPI.cpp will be the preferred way.
+ */
+ #define JACK_WEAK_EXPORT
+#else
+ #define JACK_WEAK_EXPORT __attribute__((WEAK_ATTRIBUTE))
+#endif
+
+#else
+/* Add other things here for non-gcc platforms */
+
+#ifdef _WIN32
+#define JACK_WEAK_EXPORT
+#endif
+
+#endif
+#endif
+
+#ifndef JACK_WEAK_EXPORT
+#define JACK_WEAK_EXPORT
+#endif
+
+#ifndef JACK_OPTIONAL_WEAK_EXPORT
+#define JACK_OPTIONAL_WEAK_EXPORT
+#endif
+
+#ifndef JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT
+#ifdef __GNUC__
+#define JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT __attribute__((__deprecated__))
+#else
+/* Add other things here for non-gcc platforms */
+
+#ifdef _WIN32
+#define JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT
+#endif
+
+#endif /* __GNUC__ */
+
+#ifndef JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT
+#define JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT
+#endif
+
+#endif
+
+#endif /* __weakmacros_h__ */
+
diff --git a/pipewire-jack/meson.build b/pipewire-jack/meson.build
new file mode 100644
index 0000000..fecc282
--- /dev/null
+++ b/pipewire-jack/meson.build
@@ -0,0 +1,5 @@
+jack_inc = include_directories('.')
+if get_option('jack-devel') == true
+ install_subdir('jack', install_dir: get_option('includedir'), strip_directory: false)
+endif
+subdir('src')
diff --git a/pipewire-jack/src/control.c b/pipewire-jack/src/control.c
new file mode 100644
index 0000000..da06e2c
--- /dev/null
+++ b/pipewire-jack/src/control.c
@@ -0,0 +1,472 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Florian Hülsmann <fh@cbix.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+
+#include <jack/control.h>
+#include <jack/jslist.h>
+
+#include <pipewire/pipewire.h>
+
+struct jackctl_sigmask
+{
+ sigset_t signals;
+};
+
+struct jackctl_sigmask sigmask;
+
+SPA_EXPORT
+jackctl_sigmask_t * jackctl_setup_signals(unsigned int flags)
+{
+ // stub
+ pw_log_warn("not implemented %d", flags);
+ sigemptyset(&sigmask.signals);
+ return &sigmask;
+}
+
+SPA_EXPORT
+void jackctl_wait_signals(jackctl_sigmask_t * signals)
+{
+ // stub
+ pw_log_warn("not implemented %p", signals);
+}
+
+SPA_EXPORT
+jackctl_server_t * jackctl_server_create(
+ bool (* on_device_acquire)(const char * device_name),
+ void (* on_device_release)(const char * device_name))
+{
+ pw_log_error("deprecated");
+ return jackctl_server_create2(on_device_acquire, on_device_release, NULL);
+}
+
+struct jackctl_server
+{
+ // stub
+ JSList * empty;
+ JSList * drivers;
+};
+
+struct jackctl_driver
+{
+ // stub
+};
+
+SPA_EXPORT
+jackctl_server_t * jackctl_server_create2(
+ bool (* on_device_acquire)(const char * device_name),
+ void (* on_device_release)(const char * device_name),
+ void (* on_device_reservation_loop)(void))
+{
+ // stub
+ pw_log_warn("not implemented %p %p %p", on_device_acquire, on_device_release, on_device_reservation_loop);
+
+ // setup server
+ jackctl_server_t * server;
+ server = (jackctl_server_t *)malloc(sizeof(jackctl_server_t));
+ if (server == NULL) {
+ return NULL;
+ }
+ server->empty = NULL;
+ server->drivers = NULL;
+
+ // setup dummy (default) driver
+ jackctl_driver_t * dummy;
+ dummy = (jackctl_driver_t *)malloc(sizeof(jackctl_driver_t));
+ if (dummy == NULL) {
+ free(server);
+ return NULL;
+ }
+ server->drivers = jack_slist_append (server->drivers, dummy);
+
+ return server;
+}
+
+SPA_EXPORT
+void jackctl_server_destroy(jackctl_server_t * server)
+{
+ // stub
+ pw_log_warn("%p: not implemented", server);
+
+ if (server) {
+ if (server->drivers) {
+ free(server->drivers->data);
+ }
+ jack_slist_free(server->empty);
+ jack_slist_free(server->drivers);
+ free(server);
+ }
+}
+
+SPA_EXPORT
+bool jackctl_server_open(jackctl_server_t * server, jackctl_driver_t * driver)
+{
+ // stub
+ pw_log_warn("%p: not implemented %p", server, driver);
+ return true;
+}
+
+SPA_EXPORT
+bool jackctl_server_start(jackctl_server_t * server)
+{
+ // stub
+ pw_log_warn("%p: not implemented", server);
+ return true;
+}
+
+SPA_EXPORT
+bool jackctl_server_stop(jackctl_server_t * server)
+{
+ // stub
+ pw_log_warn("%p: not implemented", server);
+ return false;
+}
+
+SPA_EXPORT
+bool jackctl_server_close(jackctl_server_t * server)
+{
+ // stub
+ pw_log_warn("%p: not implemented", server);
+ return false;
+}
+
+SPA_EXPORT
+const JSList * jackctl_server_get_drivers_list(jackctl_server_t * server)
+{
+ // stub
+ pw_log_warn("%p: not implemented", server);
+ if (server == NULL) {
+ pw_log_warn("server == NULL");
+ return NULL;
+ }
+ return server->drivers;
+}
+
+SPA_EXPORT
+const JSList * jackctl_server_get_parameters(jackctl_server_t * server)
+{
+ // stub
+ pw_log_warn("%p: not implemented", server);
+ if (server == NULL) {
+ return NULL;
+ }
+ return server->empty;
+}
+
+SPA_EXPORT
+const JSList * jackctl_server_get_internals_list(jackctl_server_t * server)
+{
+ // stub
+ pw_log_warn("%p: not implemented", server);
+ if (server == NULL) {
+ return NULL;
+ }
+ return server->empty;
+}
+
+SPA_EXPORT
+bool jackctl_server_load_internal(jackctl_server_t * server, jackctl_internal_t * internal)
+{
+ // stub
+ pw_log_warn("%p: not implemented %p", server, internal);
+ return true;
+}
+
+SPA_EXPORT
+bool jackctl_server_unload_internal(jackctl_server_t * server, jackctl_internal_t * internal)
+{
+ // stub
+ pw_log_warn("%p: not implemented %p", server, internal);
+ return true;
+}
+
+SPA_EXPORT
+bool jackctl_server_load_session_file(jackctl_server_t * server_ptr, const char * file)
+{
+ // stub
+ pw_log_warn("%p: not implemented %s", server_ptr, file);
+ return false;
+}
+
+SPA_EXPORT
+bool jackctl_server_add_slave(jackctl_server_t * server, jackctl_driver_t * driver)
+{
+ // stub
+ pw_log_warn("%p: not implemented %p", server, driver);
+ return false;
+}
+
+SPA_EXPORT
+bool jackctl_server_remove_slave(jackctl_server_t * server, jackctl_driver_t * driver)
+{
+ // stub
+ pw_log_warn("%p: not implemented %p", server, driver);
+ return false;
+}
+
+SPA_EXPORT
+bool jackctl_server_switch_master(jackctl_server_t * server, jackctl_driver_t * driver)
+{
+ // stub
+ pw_log_warn("%p: not implemented %p", server, driver);
+ return false;
+}
+
+
+SPA_EXPORT
+const char * jackctl_driver_get_name(jackctl_driver_t * driver)
+{
+ // stub
+ pw_log_warn("%p: not implemented", driver);
+ return "dummy";
+}
+
+SPA_EXPORT
+jackctl_driver_type_t jackctl_driver_get_type(jackctl_driver_t * driver)
+{
+ // stub
+ pw_log_warn("%p: not implemented", driver);
+ return (jackctl_driver_type_t)0;
+}
+
+SPA_EXPORT
+const JSList * jackctl_driver_get_parameters(jackctl_driver_t * driver)
+{
+ // stub
+ pw_log_warn("%p: not implemented", driver);
+ return NULL;
+}
+
+SPA_EXPORT
+int jackctl_driver_params_parse(jackctl_driver_t * driver, int argc, char* argv[])
+{
+ // stub
+ pw_log_warn("%p: not implemented %d %p", driver, argc, argv);
+ return 1;
+}
+
+SPA_EXPORT
+const char * jackctl_internal_get_name(jackctl_internal_t * internal)
+{
+ // stub
+ pw_log_warn("not implemented %p", internal);
+ return "pipewire-jack-stub";
+}
+
+SPA_EXPORT
+const JSList * jackctl_internal_get_parameters(jackctl_internal_t * internal)
+{
+ // stub
+ pw_log_warn("not implemented %p", internal);
+ return NULL;
+}
+
+SPA_EXPORT
+const char * jackctl_parameter_get_name(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return "pipewire-jack-stub";
+}
+
+SPA_EXPORT
+const char * jackctl_parameter_get_short_description(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return "pipewire-jack-stub";
+}
+
+SPA_EXPORT
+const char * jackctl_parameter_get_long_description(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return "pipewire-jack-stub";
+}
+
+SPA_EXPORT
+jackctl_param_type_t jackctl_parameter_get_type(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return (jackctl_param_type_t)0;
+}
+
+SPA_EXPORT
+char jackctl_parameter_get_id(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return 0;
+}
+
+SPA_EXPORT
+bool jackctl_parameter_is_set(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return false;
+}
+
+SPA_EXPORT
+bool jackctl_parameter_reset(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return false;
+}
+
+SPA_EXPORT
+union jackctl_parameter_value jackctl_parameter_get_value(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ union jackctl_parameter_value value;
+ memset(&value, 0, sizeof(value));
+ return value;
+}
+
+SPA_EXPORT
+bool jackctl_parameter_set_value(
+ jackctl_parameter_t * parameter,
+ const union jackctl_parameter_value * value_ptr)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return false;
+}
+
+SPA_EXPORT
+union jackctl_parameter_value jackctl_parameter_get_default_value(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ union jackctl_parameter_value value;
+ memset(&value, 0, sizeof(value));
+ return value;
+}
+
+SPA_EXPORT
+bool jackctl_parameter_has_range_constraint(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return false;
+}
+
+SPA_EXPORT
+bool jackctl_parameter_has_enum_constraint(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return false;
+}
+
+SPA_EXPORT
+uint32_t jackctl_parameter_get_enum_constraints_count(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("%p: not implemented", parameter);
+ return 0;
+}
+
+SPA_EXPORT
+union jackctl_parameter_value jackctl_parameter_get_enum_constraint_value(
+ jackctl_parameter_t * parameter,
+ uint32_t index)
+{
+ // stub
+ pw_log_warn("%p: not implemented %d", parameter, index);
+ union jackctl_parameter_value value;
+ memset(&value, 0, sizeof(value));
+ return value;
+}
+
+SPA_EXPORT
+const char * jackctl_parameter_get_enum_constraint_description(
+ jackctl_parameter_t * parameter,
+ uint32_t index)
+{
+ // stub
+ pw_log_warn("%p: not implemented %d", parameter, index);
+ return "pipewire-jack-stub";
+}
+
+SPA_EXPORT
+void jackctl_parameter_get_range_constraint(
+ jackctl_parameter_t * parameter,
+ union jackctl_parameter_value * min_ptr,
+ union jackctl_parameter_value * max_ptr)
+{
+ // stub
+ pw_log_warn("%p: not implemented %p %p", parameter, min_ptr, max_ptr);
+}
+
+SPA_EXPORT
+bool jackctl_parameter_constraint_is_strict(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("not implemented %p", parameter);
+ return false;
+}
+
+SPA_EXPORT
+bool jackctl_parameter_constraint_is_fake_value(jackctl_parameter_t * parameter)
+{
+ // stub
+ pw_log_warn("not implemented %p", parameter);
+ return false;
+}
+
+SPA_EXPORT SPA_PRINTF_FUNC(1, 2)
+void jack_error(const char *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ pw_log_logv(SPA_LOG_LEVEL_ERROR, "", 0, "", format, args);
+ va_end(args);
+}
+
+SPA_EXPORT SPA_PRINTF_FUNC(1, 2)
+void jack_info(const char *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ pw_log_logv(SPA_LOG_LEVEL_INFO, "", 0, "", format, args);
+ va_end(args);
+}
+
+SPA_EXPORT SPA_PRINTF_FUNC(1, 2)
+void jack_log(const char *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ pw_log_logv(SPA_LOG_LEVEL_DEBUG, "", 0, "", format, args);
+ va_end(args);
+}
diff --git a/pipewire-jack/src/dummy.c b/pipewire-jack/src/dummy.c
new file mode 100644
index 0000000..1d6bb57
--- /dev/null
+++ b/pipewire-jack/src/dummy.c
@@ -0,0 +1,39 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <regex.h>
+#include <math.h>
+
+#include <pipewire/pipewire.h>
+
+static void reg(void) __attribute__ ((constructor));
+static void reg(void)
+{
+ pw_init(NULL, NULL);
+}
diff --git a/pipewire-jack/src/export.c b/pipewire-jack/src/export.c
new file mode 100644
index 0000000..4c9daf0
--- /dev/null
+++ b/pipewire-jack/src/export.c
@@ -0,0 +1,36 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ */
+
+#include <spa/utils/defs.h>
+
+#define JACK_METADATA_PREFIX "http://jackaudio.org/metadata/"
+SPA_EXPORT const char *JACK_METADATA_CONNECTED = JACK_METADATA_PREFIX "connected";
+SPA_EXPORT const char *JACK_METADATA_EVENT_TYPES = JACK_METADATA_PREFIX "event-types";
+SPA_EXPORT const char *JACK_METADATA_HARDWARE = JACK_METADATA_PREFIX "hardware";
+SPA_EXPORT const char *JACK_METADATA_ICON_LARGE = JACK_METADATA_PREFIX "icon-large";
+SPA_EXPORT const char *JACK_METADATA_ICON_NAME = JACK_METADATA_PREFIX "icon-name";
+SPA_EXPORT const char *JACK_METADATA_ICON_SMALL = JACK_METADATA_PREFIX "icon-small";
+SPA_EXPORT const char *JACK_METADATA_ORDER = JACK_METADATA_PREFIX "order";
+SPA_EXPORT const char *JACK_METADATA_PORT_GROUP = JACK_METADATA_PREFIX "port-group";
+SPA_EXPORT const char *JACK_METADATA_PRETTY_NAME = JACK_METADATA_PREFIX "pretty-name";
+SPA_EXPORT const char *JACK_METADATA_SIGNAL_TYPE = JACK_METADATA_PREFIX "signal-type";
+#undef JACK_METADATA_PREFIX
diff --git a/pipewire-jack/src/meson.build b/pipewire-jack/src/meson.build
new file mode 100644
index 0000000..20d1ccf
--- /dev/null
+++ b/pipewire-jack/src/meson.build
@@ -0,0 +1,98 @@
+pipewire_jack_sources = [
+ 'export.c',
+ 'pipewire-jack.c',
+ 'ringbuffer.c',
+ 'uuid.c',
+]
+
+pipewire_jackserver_sources = pipewire_jack_sources
+pipewire_jackserver_sources += [
+ 'control.c',
+]
+
+pipewire_net_sources = [
+ 'net.c',
+]
+pipewire_jack_c_args = [
+ '-DPIC',
+]
+
+libjack_path = get_option('libjack-path')
+if libjack_path == ''
+ libjack_path = modules_install_dir / 'jack'
+ libjack_path_dlopen = modules_install_dir_dlopen / 'jack'
+else
+ libjack_path_dlopen = libjack_path
+endif
+
+tools_config = configuration_data()
+tools_config.set('LIBJACK_PATH', libjack_path_dlopen)
+
+configure_file(input : 'pw-jack.in',
+ output : 'pw-jack',
+ configuration : tools_config,
+ install_dir : pipewire_bindir)
+
+pipewire_jack = shared_library('jack',
+ pipewire_jack_sources,
+ soversion : soversion,
+ version : libversion,
+ c_args : pipewire_jack_c_args,
+ include_directories : [configinc, jack_inc],
+ dependencies : [pipewire_dep, mathlib],
+ install : true,
+ install_dir : libjack_path,
+)
+
+pipewire_jackserver = shared_library('jackserver',
+ pipewire_jackserver_sources,
+ soversion : soversion,
+ version : libversion,
+ c_args : pipewire_jack_c_args,
+ include_directories : [configinc, jack_inc],
+ dependencies : [pipewire_dep, mathlib],
+ install : true,
+ install_dir : libjack_path,
+)
+
+pipewire_jacknet = shared_library('jacknet',
+ pipewire_net_sources,
+ soversion : soversion,
+ version : libversion,
+ c_args : pipewire_jack_c_args,
+ include_directories : [configinc, jack_inc],
+ dependencies : [pipewire_dep, mathlib],
+ install : true,
+ install_dir : libjack_path,
+)
+
+
+if get_option('jack-devel') == true
+ if meson.version().version_compare('<0.59.0')
+ error(
+ '''
+ Before version 0.59.0 Meson creates a wrong jack pkg-config file.
+ For that reason this is now an error. Please update Meson,
+ if you want to have JACK development files.
+ ''')
+ endif
+
+ pkgconfig.generate(filebase : 'jack',
+ libraries : [pipewire_jack],
+ name : 'jack',
+ description : 'PipeWire JACK API',
+ version : '1.9.17',
+ extra_cflags : '-D_REENTRANT',
+ unescaped_variables: ['server_libs=-L${libdir} -ljackserver', 'jack_implementation=pipewire'])
+endif
+
+if sdl_dep.found()
+ executable('video-dsp-play',
+ '../examples/video-dsp-play.c',
+ include_directories : [jack_inc],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'examples' / 'jack',
+ dependencies : [sdl_dep, mathlib],
+ link_with: pipewire_jack,
+ )
+endif
diff --git a/pipewire-jack/src/metadata.c b/pipewire-jack/src/metadata.c
new file mode 100644
index 0000000..da3d75f
--- /dev/null
+++ b/pipewire-jack/src/metadata.c
@@ -0,0 +1,421 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#include <spa/utils/string.h>
+
+#include <jack/metadata.h>
+#include <jack/uuid.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/metadata.h>
+
+static jack_description_t *find_description(jack_uuid_t subject)
+{
+ jack_description_t *desc;
+ pw_array_for_each(desc, &globals.descriptions) {
+ if (jack_uuid_compare(desc->subject, subject) == 0)
+ return desc;
+ }
+ return NULL;
+}
+
+static void set_property(jack_property_t *prop, const char *key, const char *value, const char *type)
+{
+ prop->key = strdup(key);
+ prop->data = strdup(value);
+ prop->type = strdup(type);
+}
+
+static void clear_property(jack_property_t *prop)
+{
+ free((char*)prop->key);
+ free((char*)prop->data);
+ free((char*)prop->type);
+}
+
+static jack_property_t *copy_properties(jack_property_t *src, uint32_t cnt)
+{
+ jack_property_t *dst;
+ uint32_t i;
+ dst = malloc(sizeof(jack_property_t) * cnt);
+ if (dst != NULL) {
+ for (i = 0; i < cnt; i++)
+ set_property(&dst[i], src[i].key, src[i].data, src[i].type);
+ }
+ return dst;
+}
+
+static int copy_description(jack_description_t *dst, jack_description_t *src)
+{
+ dst->properties = copy_properties(src->properties, src->property_cnt);
+ if (dst->properties == NULL)
+ return -errno;
+ jack_uuid_copy(&dst->subject, src->subject);
+ dst->property_cnt = src->property_cnt;
+ dst->property_size = src->property_size;
+ return dst->property_cnt;
+}
+
+static jack_description_t *add_description(jack_uuid_t subject)
+{
+ jack_description_t *desc;
+ desc = pw_array_add(&globals.descriptions, sizeof(*desc));
+ if (desc != NULL) {
+ spa_zero(*desc);
+ jack_uuid_copy(&desc->subject, subject);
+ }
+ return desc;
+}
+
+static void remove_description(jack_description_t *desc)
+{
+ jack_free_description(desc, false);
+ pw_array_remove(&globals.descriptions, desc);
+}
+
+static jack_property_t *find_property(jack_description_t *desc, const char *key)
+{
+ uint32_t i;
+ for (i = 0; i < desc->property_cnt; i++) {
+ jack_property_t *prop = &desc->properties[i];
+ if (spa_streq(prop->key, key))
+ return prop;
+ }
+ return NULL;
+}
+
+static jack_property_t *add_property(jack_description_t *desc, const char *key,
+ const char *value, const char *type)
+{
+ jack_property_t *prop;
+ void *np;
+ size_t ns;
+
+ if (desc->property_cnt == desc->property_size) {
+ ns = desc->property_size > 0 ? desc->property_size * 2 : 8;
+ np = pw_reallocarray(desc->properties, ns, sizeof(*prop));
+ if (np == NULL)
+ return NULL;
+ desc->property_size = ns;
+ desc->properties = np;
+ }
+ prop = &desc->properties[desc->property_cnt++];
+ set_property(prop, key, value, type);
+ return prop;
+}
+
+static void remove_property(jack_description_t *desc, jack_property_t *prop)
+{
+ clear_property(prop);
+ desc->property_cnt--;
+ memmove(desc->properties, SPA_PTROFF(prop, sizeof(*prop), void),
+ SPA_PTRDIFF(SPA_PTROFF(desc->properties, sizeof(*prop) * desc->property_cnt, void),
+ prop));
+
+ if (desc->property_cnt == 0)
+ remove_description(desc);
+}
+
+static int change_property(jack_property_t *prop, const char *value, const char *type)
+{
+ int changed = 0;
+ if (!spa_streq(prop->data, value)) {
+ free((char*)prop->data);
+ prop->data = strdup(value);
+ changed++;
+ }
+ if (!spa_streq(prop->type, type)) {
+ free((char*)prop->type);
+ prop->type = strdup(type);
+ changed++;
+ }
+ return changed;
+}
+
+static int update_property(struct client *c,
+ jack_uuid_t subject,
+ const char* key,
+ const char* type,
+ const char* value)
+{
+ jack_property_change_t change;
+ jack_description_t *desc;
+ int changed = 0;
+
+ pthread_mutex_lock(&globals.lock);
+ desc = find_description(subject);
+
+ if (key == NULL) {
+ if (desc != NULL) {
+ remove_description(desc);
+ change = PropertyDeleted;
+ changed++;
+ }
+ } else {
+ jack_property_t *prop;
+
+ prop = desc ? find_property(desc, key) : NULL;
+
+ if (value == NULL || type == NULL) {
+ if (prop != NULL) {
+ remove_property(desc, prop);
+ change = PropertyDeleted;
+ changed++;
+ }
+ } else if (prop == NULL) {
+ if (desc == NULL)
+ desc = add_description(subject);
+ if (desc == NULL) {
+ changed = -errno;
+ pw_log_warn("add_description failed: %m");
+ } else if (add_property(desc, key, value, type) == NULL) {
+ changed = -errno;
+ pw_log_warn("add_property failed: %m");
+ } else {
+ change = PropertyCreated;
+ changed++;
+ }
+ } else {
+ changed = change_property(prop, value, type);
+ change = PropertyChanged;
+ }
+ }
+ pthread_mutex_unlock(&globals.lock);
+
+ if (c->property_callback && changed > 0) {
+ pw_log_info("emit %"PRIu64" %s", (uint64_t)subject, key);
+ c->property_callback(subject, key, change, c->property_arg);
+ }
+ return changed;
+}
+
+
+SPA_EXPORT
+int jack_set_property(jack_client_t*client,
+ jack_uuid_t subject,
+ const char* key,
+ const char* value,
+ const char* type)
+{
+ struct client *c = (struct client *) client;
+ struct object *o;
+ uint32_t serial;
+ int res = -1;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+ spa_return_val_if_fail(key != NULL, -EINVAL);
+ spa_return_val_if_fail(value != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+ if (c->metadata == NULL)
+ goto done;
+
+ if (subject & (1<<30))
+ goto done;
+
+ serial = jack_uuid_to_index(subject);
+ if ((o = find_by_serial(c, serial)) == NULL)
+ goto done;
+
+ if (type == NULL)
+ type = "";
+
+ pw_log_info("set id:%u (%"PRIu64") '%s' to '%s@%s'", o->id, subject, key, value, type);
+ if (update_property(c, subject, key, type, value))
+ pw_metadata_set_property(c->metadata->proxy, o->id, key, type, value);
+ res = 0;
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_get_property(jack_uuid_t subject,
+ const char* key,
+ char** value,
+ char** type)
+{
+ jack_description_t *desc;
+ jack_property_t *prop;
+ int res = -1;
+
+ pthread_mutex_lock(&globals.lock);
+ desc = find_description(subject);
+ if (desc == NULL)
+ goto done;
+
+ prop = find_property(desc, key);
+ if (prop == NULL)
+ goto done;
+
+ *value = strdup(prop->data);
+ *type = strdup(prop->type);
+ res = 0;
+
+ pw_log_debug("subject:%"PRIu64" key:'%s' value:'%s' type:'%s'",
+ subject, key, *value, *type);
+done:
+ pthread_mutex_unlock(&globals.lock);
+ return res;
+}
+
+SPA_EXPORT
+void jack_free_description (jack_description_t* desc, int free_description_itself)
+{
+ uint32_t n;
+
+ for (n = 0; n < desc->property_cnt; ++n)
+ clear_property(&desc->properties[n]);
+ free(desc->properties);
+ if (free_description_itself)
+ free(desc);
+}
+
+SPA_EXPORT
+int jack_get_properties (jack_uuid_t subject,
+ jack_description_t* desc)
+{
+ jack_description_t *d;
+ int res = -1;
+
+ spa_return_val_if_fail(desc != NULL, -EINVAL);
+
+ pthread_mutex_lock(&globals.lock);
+ d = find_description(subject);
+ if (d == NULL)
+ goto done;
+
+ res = copy_description(desc, d);
+done:
+ pthread_mutex_unlock(&globals.lock);
+ return res;
+}
+
+SPA_EXPORT
+int jack_get_all_properties (jack_description_t** result)
+{
+ uint32_t i;
+ jack_description_t *dst, *src;
+ struct pw_array *descriptions;
+ uint32_t len;
+
+ pthread_mutex_lock(&globals.lock);
+ descriptions = &globals.descriptions;
+ len = pw_array_get_len(descriptions, jack_description_t);
+ src = descriptions->data;
+ dst = malloc(descriptions->size);
+ for (i = 0; i < len; i++)
+ copy_description(&dst[i], &src[i]);
+ *result = dst;
+ pthread_mutex_unlock(&globals.lock);
+
+ return len;
+}
+
+SPA_EXPORT
+int jack_remove_property (jack_client_t* client, jack_uuid_t subject, const char* key)
+{
+ struct client *c = (struct client *) client;
+ uint32_t id;
+ int res = -1;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+ spa_return_val_if_fail(key != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ if (c->metadata == NULL)
+ goto done;
+
+ id = jack_uuid_to_index(subject);
+
+ pw_log_info("remove id:%u (%"PRIu64") '%s'", id, subject, key);
+ pw_metadata_set_property(c->metadata->proxy,
+ id, key, NULL, NULL);
+ res = 0;
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_remove_properties (jack_client_t* client, jack_uuid_t subject)
+{
+ struct client *c = (struct client *) client;
+ uint32_t id;
+ int res = -1;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+ if (c->metadata == NULL)
+ goto done;
+
+ id = jack_uuid_to_index(subject);
+
+ pw_log_info("remove id:%u (%"PRIu64")", id, subject);
+ pw_metadata_set_property(c->metadata->proxy,
+ id, NULL, NULL, NULL);
+ res = 0;
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_remove_all_properties (jack_client_t* client)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+ pw_metadata_clear(c->metadata->proxy);
+ pw_thread_loop_unlock(c->context.loop);
+
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_property_change_callback (jack_client_t* client,
+ JackPropertyChangeCallback callback,
+ void* arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ c->property_callback = callback;
+ c->property_arg = arg;
+ return 0;
+}
diff --git a/pipewire-jack/src/net.c b/pipewire-jack/src/net.c
new file mode 100644
index 0000000..e48b76b
--- /dev/null
+++ b/pipewire-jack/src/net.c
@@ -0,0 +1,169 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include <jack/net.h>
+
+#include <pipewire/pipewire.h>
+
+SPA_EXPORT
+jack_net_slave_t* jack_net_slave_open(const char* ip, int port, const char* name,
+ jack_slave_t* request, jack_master_t* result)
+{
+ return NULL;
+}
+
+SPA_EXPORT
+int jack_net_slave_close(jack_net_slave_t* net)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_set_net_slave_process_callback(jack_net_slave_t * net, JackNetSlaveProcessCallback net_callback, void *arg)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_net_slave_activate(jack_net_slave_t* net)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_net_slave_deactivate(jack_net_slave_t* net)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_net_slave_is_active(jack_net_slave_t* net)
+{
+ return false;
+}
+
+SPA_EXPORT
+int jack_set_net_slave_buffer_size_callback(jack_net_slave_t *net, JackNetSlaveBufferSizeCallback bufsize_callback, void *arg)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_set_net_slave_sample_rate_callback(jack_net_slave_t *net, JackNetSlaveSampleRateCallback samplerate_callback, void *arg)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_set_net_slave_shutdown_callback(jack_net_slave_t *net, JackNetSlaveShutdownCallback shutdown_callback, void *arg)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_set_net_slave_restart_callback(jack_net_slave_t *net, JackNetSlaveRestartCallback restart_callback, void *arg)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_set_net_slave_error_callback(jack_net_slave_t *net, JackNetSlaveErrorCallback error_callback, void *arg)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+jack_net_master_t* jack_net_master_open(const char* ip, int port, jack_master_t* request, jack_slave_t* result)
+{
+ return NULL;
+}
+
+SPA_EXPORT
+int jack_net_master_close(jack_net_master_t* net)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_net_master_recv(jack_net_master_t* net, int audio_input, float** audio_input_buffer, int midi_input, void** midi_input_buffer)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_net_master_recv_slice(jack_net_master_t* net, int audio_input, float** audio_input_buffer, int midi_input, void** midi_input_buffer, int frames)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_net_master_send(jack_net_master_t* net, int audio_output, float** audio_output_buffer, int midi_output, void** midi_output_buffer)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_net_master_send_slice(jack_net_master_t* net, int audio_output, float** audio_output_buffer, int midi_output, void** midi_output_buffer, int frames)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+jack_adapter_t* jack_create_adapter(int input, int output,
+ jack_nframes_t host_buffer_size,
+ jack_nframes_t host_sample_rate,
+ jack_nframes_t adapted_buffer_size,
+ jack_nframes_t adapted_sample_rate)
+{
+ return NULL;
+}
+
+SPA_EXPORT
+int jack_destroy_adapter(jack_adapter_t* adapter)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+void jack_flush_adapter(jack_adapter_t* adapter)
+{
+}
+
+SPA_EXPORT
+int jack_adapter_push_and_pull(jack_adapter_t* adapter, float** input, float** output, unsigned int frames)
+{
+ return ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_adapter_pull_and_push(jack_adapter_t* adapter, float** input, float** output, unsigned int frames)
+{
+ return ENOTSUP;
+}
diff --git a/pipewire-jack/src/pipewire-jack-extensions.h b/pipewire-jack/src/pipewire-jack-extensions.h
new file mode 100644
index 0000000..8e38e9e
--- /dev/null
+++ b/pipewire-jack/src/pipewire-jack-extensions.h
@@ -0,0 +1,50 @@
+/* PipeWire JACK extensions
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_JACK_EXTENSIONS_H
+#define PIPEWIRE_JACK_EXTENSIONS_H
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** 1.0 gamma, full range HDR 0.0 -> 1.0, pre-multiplied
+ * alpha, BT.2020 primaries, progressive */
+#define JACK_DEFAULT_VIDEO_TYPE "32 bit float RGBA video"
+
+typedef struct jack_image_size {
+ uint32_t width;
+ uint32_t height;
+ uint32_t stride;
+ uint32_t flags;
+} jack_image_size_t;
+
+int jack_get_video_image_size(jack_client_t *client, jack_image_size_t *size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_JACK_EXTENSIONS_H */
diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c
new file mode 100644
index 0000000..2e82677
--- /dev/null
+++ b/pipewire-jack/src/pipewire-jack.c
@@ -0,0 +1,6509 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <regex.h>
+#include <math.h>
+
+#include <jack/jack.h>
+#include <jack/session.h>
+#include <jack/thread.h>
+#include <jack/midiport.h>
+#include <jack/uuid.h>
+#include <jack/metadata.h>
+
+#include <spa/support/cpu.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/debug/types.h>
+#include <spa/debug/pod.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/private.h>
+#include <pipewire/thread.h>
+#include <pipewire/data-loop.h>
+
+#include "pipewire/extensions/client-node.h"
+#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
+
+#define DEFAULT_RT_MAX 88
+
+#define JACK_CLIENT_NAME_SIZE 256
+#define JACK_PORT_NAME_SIZE 256
+#define JACK_PORT_TYPE_SIZE 32
+#define MONITOR_EXT " Monitor"
+
+#define MAX_MIX 1024
+#define MAX_BUFFER_FRAMES 8192
+
+#define MAX_ALIGN 16
+#define MAX_BUFFERS 2
+#define MAX_BUFFER_DATAS 1u
+
+#define REAL_JACK_PORT_NAME_SIZE (JACK_CLIENT_NAME_SIZE + JACK_PORT_NAME_SIZE)
+
+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 SELF_CONNECT_ALLOW 0
+#define SELF_CONNECT_FAIL_EXT -1
+#define SELF_CONNECT_IGNORE_EXT 1
+#define SELF_CONNECT_FAIL_ALL -2
+#define SELF_CONNECT_IGNORE_ALL 2
+
+struct client;
+struct port;
+
+struct globals {
+ jack_thread_creator_t creator;
+ pthread_mutex_t lock;
+ struct pw_array descriptions;
+ struct spa_list free_objects;
+ struct spa_thread_utils *thread_utils;
+};
+
+static struct globals globals;
+static bool mlock_warned = false;
+
+#define OBJECT_CHUNK 8
+#define RECYCLE_THRESHOLD 128
+
+typedef void (*mix_func) (float *dst, float *src[], uint32_t n_src, bool aligned, uint32_t n_samples);
+
+static mix_func mix_function;
+
+struct object {
+ struct spa_list link;
+
+ struct client *client;
+
+#define INTERFACE_Port 0
+#define INTERFACE_Node 1
+#define INTERFACE_Link 2
+ uint32_t type;
+ uint32_t id;
+ uint32_t serial;
+
+ union {
+ struct {
+ char name[JACK_CLIENT_NAME_SIZE+1];
+ char node_name[512];
+ int32_t priority;
+ uint32_t client_id;
+ } node;
+ struct {
+ uint32_t src;
+ uint32_t dst;
+ uint32_t src_serial;
+ uint32_t dst_serial;
+ bool src_ours;
+ bool dst_ours;
+ bool is_complete;
+ struct port *our_input;
+ struct port *our_output;
+ } port_link;
+ struct {
+ unsigned long flags;
+ char name[REAL_JACK_PORT_NAME_SIZE+1];
+ char alias1[REAL_JACK_PORT_NAME_SIZE+1];
+ char alias2[REAL_JACK_PORT_NAME_SIZE+1];
+ char system[REAL_JACK_PORT_NAME_SIZE+1];
+ uint32_t system_id;
+ uint32_t type_id;
+ uint32_t node_id;
+ uint32_t monitor_requests;
+ int32_t priority;
+ struct port *port;
+ bool is_monitor;
+ struct object *node;
+ struct spa_latency_info latency[2];
+ } port;
+ };
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+ unsigned int removing:1;
+ unsigned int removed:1;
+};
+
+struct midi_buffer {
+#define MIDI_BUFFER_MAGIC 0x900df00d
+ uint32_t magic;
+ int32_t buffer_size;
+ uint32_t nframes;
+ int32_t write_pos;
+ uint32_t event_count;
+ uint32_t lost_events;
+};
+
+#define MIDI_INLINE_MAX 4
+
+struct midi_event {
+ uint16_t time;
+ uint16_t size;
+ union {
+ uint32_t byte_offset;
+ uint8_t inline_data[MIDI_INLINE_MAX];
+ };
+};
+
+struct buffer {
+ struct spa_list link;
+#define BUFFER_FLAG_OUT (1<<0)
+#define BUFFER_FLAG_MAPPED (1<<1)
+ uint32_t flags;
+ uint32_t id;
+
+ struct spa_data datas[MAX_BUFFER_DATAS];
+ uint32_t n_datas;
+
+ struct pw_memmap *mem[MAX_BUFFER_DATAS+1];
+ uint32_t n_mem;
+};
+
+struct mix {
+ struct spa_list link;
+ struct spa_list port_link;
+ uint32_t id;
+ uint32_t peer_id;
+ struct port *port;
+ struct port *peer_port;
+
+ struct spa_io_buffers *io;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+ struct spa_list queue;
+};
+
+struct port {
+ bool valid;
+ struct spa_list link;
+
+ struct client *client;
+
+ enum spa_direction direction;
+ uint32_t port_id;
+ struct object *object;
+ struct pw_properties *props;
+ struct spa_port_info info;
+#define IDX_EnumFormat 0
+#define IDX_Buffers 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Latency 4
+#define N_PORT_PARAMS 5
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct spa_io_buffers io;
+ struct spa_list mix;
+ struct mix *global_mix;
+
+ unsigned int empty_out:1;
+ unsigned int zeroed:1;
+
+ float *emptyptr;
+ float empty[MAX_BUFFER_FRAMES + MAX_ALIGN];
+
+ void *(*get_buffer) (struct port *p, jack_nframes_t frames);
+};
+
+struct link {
+ struct spa_list link;
+ struct spa_list target_link;
+ struct client *client;
+ uint32_t node_id;
+ struct pw_memmap *mem;
+ struct pw_node_activation *activation;
+ int signalfd;
+};
+
+struct context {
+ struct pw_loop *l;
+ struct pw_thread_loop *loop; /* thread_lock protects all below */
+ struct pw_context *context;
+
+ struct spa_thread_utils *old_thread_utils;
+ struct spa_thread_utils thread_utils;
+ pthread_mutex_t lock; /* protects map and lists below, in addition to thread_lock */
+ struct spa_list objects;
+ uint32_t free_count;
+};
+
+#define GET_DIRECTION(f) ((f) & JackPortIsInput ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT)
+
+#define GET_PORT(c,d,p) (pw_map_lookup(&c->ports[d], p))
+
+struct metadata {
+ struct pw_metadata *proxy;
+ struct spa_hook proxy_listener;
+ struct spa_hook listener;
+
+ char default_audio_sink[1024];
+ char default_audio_source[1024];
+};
+
+struct client {
+ char name[JACK_CLIENT_NAME_SIZE+1];
+
+ struct context context;
+
+ char *server_name;
+ char *load_name; /* load module name */
+ char *load_init; /* initialization string */
+ jack_uuid_t session_id; /* requested session_id */
+
+ struct pw_data_loop *loop;
+ struct pw_properties *props;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct pw_mempool *pool;
+ int pending_sync;
+ int last_sync;
+ int last_res;
+
+ struct spa_node_info info;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_client_node *node;
+ struct spa_hook node_listener;
+ struct spa_hook proxy_listener;
+
+ struct metadata *metadata;
+ struct metadata *settings;
+
+ uint32_t node_id;
+ uint32_t serial;
+ struct spa_source *socket_source;
+
+ JackThreadCallback thread_callback;
+ void *thread_arg;
+ JackThreadInitCallback thread_init_callback;
+ void *thread_init_arg;
+ JackShutdownCallback shutdown_callback;
+ void *shutdown_arg;
+ JackInfoShutdownCallback info_shutdown_callback;
+ void *info_shutdown_arg;
+ JackProcessCallback process_callback;
+ void *process_arg;
+ JackFreewheelCallback freewheel_callback;
+ void *freewheel_arg;
+ JackBufferSizeCallback bufsize_callback;
+ void *bufsize_arg;
+ JackSampleRateCallback srate_callback;
+ void *srate_arg;
+ JackClientRegistrationCallback registration_callback;
+ void *registration_arg;
+ JackPortRegistrationCallback portregistration_callback;
+ void *portregistration_arg;
+ JackPortConnectCallback connect_callback;
+ void *connect_arg;
+ JackPortRenameCallback rename_callback;
+ void *rename_arg;
+ JackGraphOrderCallback graph_callback;
+ void *graph_arg;
+ JackXRunCallback xrun_callback;
+ void *xrun_arg;
+ JackLatencyCallback latency_callback;
+ void *latency_arg;
+ JackSyncCallback sync_callback;
+ void *sync_arg;
+ JackTimebaseCallback timebase_callback;
+ void *timebase_arg;
+ JackPropertyChangeCallback property_callback;
+ void *property_arg;
+
+ struct spa_io_position *position;
+ uint32_t sample_rate;
+ uint32_t buffer_frames;
+ struct spa_fraction latency;
+
+ struct spa_list mix;
+ struct spa_list free_mix;
+
+ struct spa_list free_ports;
+ struct pw_map ports[2];
+
+ struct spa_list links;
+ uint32_t driver_id;
+ struct pw_node_activation *driver_activation;
+
+ struct pw_memmap *mem;
+ struct pw_node_activation *activation;
+ uint32_t xrun_count;
+
+ struct {
+ struct spa_io_position *position;
+ struct pw_node_activation *driver_activation;
+ struct spa_list target_links;
+ } rt;
+
+ pthread_mutex_t rt_lock;
+ unsigned int rt_locked:1;
+ unsigned int data_locked:1;
+
+ unsigned int started:1;
+ unsigned int active:1;
+ unsigned int destroyed:1;
+ unsigned int first:1;
+ unsigned int thread_entered:1;
+ unsigned int has_transport:1;
+ unsigned int allow_mlock:1;
+ unsigned int warn_mlock:1;
+ unsigned int timeowner_conditional:1;
+ unsigned int show_monitor:1;
+ unsigned int merge_monitor:1;
+ unsigned int short_name:1;
+ unsigned int filter_name:1;
+ unsigned int freewheeling:1;
+ unsigned int locked_process:1;
+ unsigned int default_as_system:1;
+ int self_connect_mode;
+ int rt_max;
+ unsigned int fix_midi_events:1;
+ unsigned int global_buffer_size:1;
+ char filter_char;
+
+ jack_position_t jack_position;
+ jack_transport_state_t jack_state;
+};
+
+static int do_sync(struct client *client);
+static struct object *find_by_serial(struct client *c, uint32_t serial);
+
+#include "metadata.c"
+
+int pw_jack_match_rules(const char *rules, size_t size, const struct spa_dict *props,
+ int (*matched) (void *data, const char *action, const char *val, int len),
+ void *data);
+
+static struct object * alloc_object(struct client *c, int type)
+{
+ struct object *o;
+ int i;
+
+ pthread_mutex_lock(&globals.lock);
+ if (spa_list_is_empty(&globals.free_objects)) {
+ o = calloc(OBJECT_CHUNK, sizeof(struct object));
+ if (o == NULL) {
+ pthread_mutex_unlock(&globals.lock);
+ return NULL;
+ }
+ for (i = 0; i < OBJECT_CHUNK; i++)
+ spa_list_append(&globals.free_objects, &o[i].link);
+ }
+ o = spa_list_first(&globals.free_objects, struct object, link);
+ spa_list_remove(&o->link);
+ pthread_mutex_unlock(&globals.lock);
+
+ o->client = c;
+ o->removed = false;
+ o->type = type;
+ pw_log_debug("%p: object:%p type:%d", c, o, type);
+
+ return o;
+}
+
+static void recycle_objects(struct client *c, uint32_t remain)
+{
+ struct object *o, *t;
+ pthread_mutex_lock(&globals.lock);
+ spa_list_for_each_safe(o, t, &c->context.objects, link) {
+ if (o->removed) {
+ pw_log_info("%p: recycle object:%p type:%d id:%u/%u",
+ c, o, o->type, o->id, o->serial);
+ spa_list_remove(&o->link);
+ memset(o, 0, sizeof(struct object));
+ spa_list_append(&globals.free_objects, &o->link);
+ if (--c->context.free_count == remain)
+ break;
+ }
+ }
+ pthread_mutex_unlock(&globals.lock);
+}
+
+/* JACK clients expect the objects to hang around after
+ * they are unregistered and freed. We mark the object removed and
+ * move it to the end of the queue. */
+static void free_object(struct client *c, struct object *o)
+{
+ pw_log_debug("%p: object:%p type:%d", c, o, o->type);
+ pthread_mutex_lock(&c->context.lock);
+ spa_list_remove(&o->link);
+ o->removed = true;
+ o->id = SPA_ID_INVALID;
+ spa_list_append(&c->context.objects, &o->link);
+ if (++c->context.free_count > RECYCLE_THRESHOLD)
+ recycle_objects(c, RECYCLE_THRESHOLD / 2);
+ pthread_mutex_unlock(&c->context.lock);
+
+}
+
+static void init_mix(struct mix *mix, uint32_t mix_id, struct port *port)
+{
+ mix->id = mix_id;
+ mix->port = port;
+ mix->io = NULL;
+ mix->n_buffers = 0;
+ spa_list_init(&mix->queue);
+ if (mix_id == SPA_ID_INVALID)
+ port->global_mix = mix;
+}
+static struct mix *find_mix_peer(struct client *c, uint32_t peer_id)
+{
+ struct mix *mix;
+ spa_list_for_each(mix, &c->mix, link) {
+ if (mix->peer_id == peer_id)
+ return mix;
+ }
+ return NULL;
+}
+
+static struct mix *find_mix(struct client *c, struct port *port, uint32_t mix_id)
+{
+ struct mix *mix;
+
+ spa_list_for_each(mix, &port->mix, port_link) {
+ if (mix->id == mix_id)
+ return mix;
+ }
+ return NULL;
+}
+
+static struct mix *ensure_mix(struct client *c, struct port *port, uint32_t mix_id)
+{
+ struct mix *mix;
+ uint32_t i;
+
+ if ((mix = find_mix(c, port, mix_id)) != NULL)
+ return mix;
+
+ if (spa_list_is_empty(&c->free_mix)) {
+ mix = calloc(OBJECT_CHUNK, sizeof(struct mix));
+ if (mix == NULL)
+ return NULL;
+ for (i = 0; i < OBJECT_CHUNK; i++)
+ spa_list_append(&c->free_mix, &mix[i].link);
+ }
+ mix = spa_list_first(&c->free_mix, struct mix, link);
+ spa_list_remove(&mix->link);
+ spa_list_append(&c->mix, &mix->link);
+
+ spa_list_append(&port->mix, &mix->port_link);
+
+ init_mix(mix, mix_id, port);
+
+ return mix;
+}
+
+static int clear_buffers(struct client *c, struct mix *mix)
+{
+ struct port *port = mix->port;
+ struct buffer *b;
+ uint32_t i, j;
+
+ pw_log_debug("%p: port %p clear buffers", c, port);
+
+ for (i = 0; i < mix->n_buffers; i++) {
+ b = &mix->buffers[i];
+
+ for (j = 0; j < b->n_mem; j++)
+ pw_memmap_free(b->mem[j]);
+
+ b->n_mem = 0;
+ }
+ mix->n_buffers = 0;
+ spa_list_init(&mix->queue);
+ return 0;
+}
+
+static void free_mix(struct client *c, struct mix *mix)
+{
+ clear_buffers(c, mix);
+ spa_list_remove(&mix->port_link);
+ if (mix->id == SPA_ID_INVALID)
+ mix->port->global_mix = NULL;
+ spa_list_remove(&mix->link);
+ spa_list_append(&c->free_mix, &mix->link);
+}
+
+static struct port * alloc_port(struct client *c, enum spa_direction direction)
+{
+ struct port *p;
+ struct object *o;
+ uint32_t i;
+
+ if (spa_list_is_empty(&c->free_ports)) {
+ p = calloc(OBJECT_CHUNK, sizeof(struct port));
+ if (p == NULL)
+ return NULL;
+ for (i = 0; i < OBJECT_CHUNK; i++)
+ spa_list_append(&c->free_ports, &p[i].link);
+ }
+ p = spa_list_first(&c->free_ports, struct port, link);
+ spa_list_remove(&p->link);
+
+ o = alloc_object(c, INTERFACE_Port);
+ o->id = SPA_ID_INVALID;
+ o->port.node_id = c->node_id;
+ o->port.port = p;
+ o->port.latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ o->port.latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ p->valid = true;
+ p->zeroed = false;
+ p->client = c;
+ p->object = o;
+ spa_list_init(&p->mix);
+ p->props = pw_properties_new(NULL, NULL);
+
+ p->direction = direction;
+ p->emptyptr = SPA_PTR_ALIGN(p->empty, MAX_ALIGN, float);
+ p->port_id = pw_map_insert_new(&c->ports[direction], p);
+
+ pthread_mutex_lock(&c->context.lock);
+ spa_list_append(&c->context.objects, &o->link);
+ pthread_mutex_unlock(&c->context.lock);
+
+ return p;
+}
+
+static void free_port(struct client *c, struct port *p)
+{
+ struct mix *m;
+
+ spa_list_consume(m, &p->mix, port_link)
+ free_mix(c, m);
+
+ pw_map_remove(&c->ports[p->direction], p->port_id);
+ free_object(c, p->object);
+ pw_properties_free(p->props);
+ spa_list_append(&c->free_ports, &p->link);
+}
+
+static struct object *find_node(struct client *c, const char *name)
+{
+ struct object *o;
+
+ spa_list_for_each(o, &c->context.objects, link) {
+ if (o->removing || o->removed || o->type != INTERFACE_Node)
+ continue;
+ if (spa_streq(o->node.name, name))
+ return o;
+ }
+ return NULL;
+}
+
+static bool is_port_default(struct client *c, struct object *o)
+{
+ struct object *ot;
+
+ if (c->metadata == NULL)
+ return false;
+
+ if ((ot = o->port.node) != NULL &&
+ (spa_streq(ot->node.node_name, c->metadata->default_audio_source) ||
+ spa_streq(ot->node.node_name, c->metadata->default_audio_sink)))
+ return true;
+
+ return false;
+}
+
+static struct object *find_port_by_name(struct client *c, const char *name)
+{
+ struct object *o;
+
+ spa_list_for_each(o, &c->context.objects, link) {
+ if (o->type != INTERFACE_Port || o->removed)
+ continue;
+ if (spa_streq(o->port.name, name) ||
+ spa_streq(o->port.alias1, name) ||
+ spa_streq(o->port.alias2, name))
+ return o;
+ if (is_port_default(c, o) && spa_streq(o->port.system, name))
+ return o;
+ }
+ return NULL;
+}
+
+static struct object *find_by_id(struct client *c, uint32_t id)
+{
+ struct object *o;
+ spa_list_for_each(o, &c->context.objects, link) {
+ if (o->id == id)
+ return o;
+ }
+ return NULL;
+}
+
+static struct object *find_by_serial(struct client *c, uint32_t serial)
+{
+ struct object *o;
+ spa_list_for_each(o, &c->context.objects, link) {
+ if (o->serial == serial)
+ return o;
+ }
+ return NULL;
+}
+
+static struct object *find_id(struct client *c, uint32_t id, bool valid)
+{
+ struct object *o = find_by_id(c, id);
+ if (o != NULL && (!valid || o->client == c))
+ return o;
+ return NULL;
+}
+
+static struct object *find_type(struct client *c, uint32_t id, uint32_t type, bool valid)
+{
+ struct object *o = find_id(c, id, valid);
+ if (o != NULL && o->type == type)
+ return o;
+ return NULL;
+}
+
+static struct object *find_link(struct client *c, uint32_t src, uint32_t dst)
+{
+ struct object *l;
+
+ spa_list_for_each(l, &c->context.objects, link) {
+ if (l->type != INTERFACE_Link || l->removed)
+ continue;
+ if (l->port_link.src == src &&
+ l->port_link.dst == dst) {
+ return l;
+ }
+ }
+ return NULL;
+}
+
+static struct buffer *dequeue_buffer(struct client *c, struct mix *mix)
+{
+ struct buffer *b;
+
+ if (SPA_UNLIKELY(spa_list_is_empty(&mix->queue)))
+ return NULL;
+
+ b = spa_list_first(&mix->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ pw_log_trace_fp("%p: port %p: dequeue buffer %d", c, mix->port, b->id);
+
+ return b;
+}
+
+#if defined (__SSE__)
+#include <xmmintrin.h>
+static void mix_sse(float *dst, float *src[], uint32_t n_src, bool aligned, uint32_t n_samples)
+{
+ uint32_t i, n, unrolled;
+ __m128 in[1];
+
+ if (SPA_IS_ALIGNED(dst, 16) && aligned)
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for (n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_load_ps(&src[0][n]);
+ for (i = 1; i < n_src; i++)
+ in[0] = _mm_add_ps(in[0], _mm_load_ps(&src[i][n]));
+ _mm_store_ps(&dst[n], in[0]);
+ }
+ for (; n < n_samples; n++) {
+ in[0] = _mm_load_ss(&src[0][n]);
+ for (i = 1; i < n_src; i++)
+ in[0] = _mm_add_ss(in[0], _mm_load_ss(&src[i][n]));
+ _mm_store_ss(&dst[n], in[0]);
+ }
+}
+#endif
+
+static void mix_c(float *dst, float *src[], uint32_t n_src, bool aligned, uint32_t n_samples)
+{
+ uint32_t n, i;
+ for (n = 0; n < n_samples; n++) {
+ float t = src[0][n];
+ for (i = 1; i < n_src; i++)
+ t += src[i][n];
+ dst[n] = t;
+ }
+}
+
+SPA_EXPORT
+void jack_get_version(int *major_ptr, int *minor_ptr, int *micro_ptr, int *proto_ptr)
+{
+ if (major_ptr)
+ *major_ptr = 3;
+ if (minor_ptr)
+ *minor_ptr = 0;
+ if (micro_ptr)
+ *micro_ptr = 0;
+ if (proto_ptr)
+ *proto_ptr = 0;
+}
+
+#define do_callback_expr(c,expr,callback,...) \
+({ \
+ if (c->callback && c->active) { \
+ pw_thread_loop_unlock(c->context.loop); \
+ if (c->locked_process) \
+ pthread_mutex_lock(&c->rt_lock); \
+ (expr); \
+ pw_log_debug("emit " #callback); \
+ c->callback(__VA_ARGS__); \
+ if (c->locked_process) \
+ pthread_mutex_unlock(&c->rt_lock); \
+ pw_thread_loop_lock(c->context.loop); \
+ } else { \
+ if (c->active) \
+ (expr); \
+ pw_log_debug("skip " #callback \
+ " cb:%p active:%d", c->callback, \
+ c->active); \
+ } \
+})
+
+#define do_callback(c,callback,...) do_callback_expr(c,(void)0,callback,__VA_ARGS__)
+
+#define do_rt_callback_res(c,callback,...) \
+({ \
+ int res = 0; \
+ if (c->callback) { \
+ if (pthread_mutex_trylock(&c->rt_lock) == 0) { \
+ c->rt_locked = true; \
+ res = c->callback(__VA_ARGS__); \
+ c->rt_locked = false; \
+ pthread_mutex_unlock(&c->rt_lock); \
+ } else { \
+ pw_log_debug("skip " #callback \
+ " cb:%p", c->callback); \
+ } \
+ } \
+ res; \
+})
+
+SPA_EXPORT
+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());
+ return name;
+}
+
+static void on_sync_reply(void *data, uint32_t id, int seq)
+{
+ struct client *client = data;
+ if (id != PW_ID_CORE)
+ return;
+ client->last_sync = seq;
+ if (client->pending_sync == seq)
+ pw_thread_loop_signal(client->context.loop, false);
+}
+
+
+static void on_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct client *client = data;
+
+ pw_log_warn("%p: error id:%u seq:%d res:%d (%s): %s", client,
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE) {
+ client->last_res = res;
+ if (!client->destroyed)
+ do_callback(client, shutdown_callback, client->shutdown_arg);
+ }
+ pw_thread_loop_signal(client->context.loop, false);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_sync_reply,
+ .error = on_error,
+};
+
+static int do_sync(struct client *client)
+{
+ bool in_data_thread = pw_data_loop_in_thread(client->loop);
+
+ if (pw_thread_loop_in_thread(client->context.loop)) {
+ pw_log_warn("sync requested from callback");
+ return 0;
+ }
+ if (client->last_res == -EPIPE)
+ return -EPIPE;
+
+ client->last_res = 0;
+ client->pending_sync = pw_proxy_sync((struct pw_proxy*)client->core, client->pending_sync);
+
+ while (true) {
+ if (in_data_thread) {
+ if (client->rt_locked)
+ pthread_mutex_unlock(&client->rt_lock);
+ client->data_locked = true;
+ }
+ pw_thread_loop_wait(client->context.loop);
+
+ if (in_data_thread) {
+ client->data_locked = false;
+ if (client->rt_locked)
+ pthread_mutex_lock(&client->rt_lock);
+ }
+
+ if (client->last_res < 0)
+ return client->last_res;
+
+ if (client->pending_sync == client->last_sync)
+ break;
+ }
+ return 0;
+}
+
+static void on_node_removed(void *data)
+{
+ struct client *client = data;
+ pw_proxy_destroy((struct pw_proxy*)client->node);
+}
+
+static void on_node_destroy(void *data)
+{
+ struct client *client = data;
+ client->node = NULL;
+ spa_hook_remove(&client->proxy_listener);
+ spa_hook_remove(&client->node_listener);
+}
+
+static void on_node_bound(void *data, uint32_t global_id)
+{
+ struct client *client = data;
+ client->node_id = global_id;
+}
+
+static const struct pw_proxy_events node_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = on_node_removed,
+ .destroy = on_node_destroy,
+ .bound = on_node_bound,
+};
+
+static struct link *find_activation(struct spa_list *links, uint32_t node_id)
+{
+ struct link *l;
+
+ spa_list_for_each(l, links, link) {
+ if (l->node_id == node_id)
+ return l;
+ }
+ return NULL;
+}
+
+static void client_remove_source(struct client *c)
+{
+ if (c->socket_source) {
+ pw_loop_destroy_source(c->loop->loop, c->socket_source);
+ c->socket_source = NULL;
+ }
+}
+
+static int
+do_remove_sources(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct client *c = user_data;
+ client_remove_source(c);
+ return 0;
+}
+
+static inline void reuse_buffer(struct client *c, struct mix *mix, uint32_t id)
+{
+ struct buffer *b;
+
+ b = &mix->buffers[id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ pw_log_trace_fp("%p: port %p: recycle buffer %d", c, mix->port, id);
+ spa_list_append(&mix->queue, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+}
+
+
+static size_t convert_from_midi(void *midi, void *buffer, size_t size)
+{
+ struct spa_pod_builder b = { 0, };
+ uint32_t i, count;
+ struct spa_pod_frame f;
+
+ count = jack_midi_get_event_count(midi);
+
+ spa_pod_builder_init(&b, buffer, size);
+ spa_pod_builder_push_sequence(&b, &f, 0);
+
+ 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, SPA_CONTROL_Midi);
+ spa_pod_builder_bytes(&b, ev.buffer, ev.size);
+ }
+ spa_pod_builder_pop(&b, &f);
+ return b.state.offset;
+}
+
+static inline void fix_midi_event(uint8_t *data, size_t size)
+{
+ /* fixup NoteOn with vel 0 */
+ if (size > 2 && (data[0] & 0xF0) == 0x90 && data[2] == 0x00) {
+ data[0] = 0x80 + (data[0] & 0x0F);
+ data[2] = 0x40;
+ }
+}
+
+static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b)
+{
+ if (a->offset < b->offset)
+ return -1;
+ if (a->offset > b->offset)
+ return 1;
+ if (a->type != b->type)
+ return 0;
+ 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)
+ return 0;
+
+ da = SPA_POD_BODY(&a->value);
+ db = SPA_POD_BODY(&b->value);
+ if ((da[0] & 0xf) != (db[0] & 0xf))
+ return 0;
+ return priotab[(db[0]>>4) & 7] - priotab[(da[0]>>4) & 7];
+ }
+ default:
+ return 0;
+ }
+}
+
+static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix)
+{
+ struct spa_pod_control *c[n_seq];
+ uint32_t i;
+ int res;
+
+ for (i = 0; i < n_seq; i++)
+ c[i] = spa_pod_control_first(&seq[i]->body);
+
+ while (true) {
+ struct spa_pod_control *next = NULL;
+ uint32_t next_index = 0;
+
+ for (i = 0; i < n_seq; i++) {
+ if (!spa_pod_control_is_inside(&seq[i]->body,
+ SPA_POD_BODY_SIZE(seq[i]), c[i]))
+ continue;
+
+ if (next == NULL || event_sort(c[i], next) <= 0) {
+ next = c[i];
+ next_index = i;
+ }
+ }
+ if (SPA_UNLIKELY(next == NULL))
+ break;
+
+ switch(next->type) {
+ case SPA_CONTROL_Midi:
+ {
+ uint8_t *data = SPA_POD_BODY(&next->value);
+ size_t size = SPA_POD_BODY_SIZE(&next->value);
+
+ if (fix)
+ fix_midi_event(data, size);
+
+ if ((res = jack_midi_event_write(midi, next->offset, data, size)) < 0)
+ pw_log_warn("midi %p: can't write event: %s", midi,
+ spa_strerror(res));
+ break;
+ }
+ }
+ c[next_index] = spa_pod_control_next(c[next_index]);
+ }
+}
+
+
+static inline void *get_buffer_output(struct port *p, uint32_t frames, uint32_t stride, struct buffer **buf)
+{
+ struct mix *mix;
+ struct client *c = p->client;
+ void *ptr = NULL;
+ struct buffer *b;
+ struct spa_data *d;
+
+ if (frames == 0 || !p->valid)
+ return NULL;
+
+ if (SPA_UNLIKELY((mix = p->global_mix) == NULL))
+ return NULL;
+
+ pw_log_trace_fp("%p: port %s %d get buffer %d n_buffers:%d",
+ c, p->object->port.name, p->port_id, frames, mix->n_buffers);
+
+ if (SPA_UNLIKELY(mix->n_buffers == 0))
+ return NULL;
+
+ if (p->io.status == SPA_STATUS_HAVE_DATA &&
+ p->io.buffer_id < mix->n_buffers) {
+ b = &mix->buffers[p->io.buffer_id];
+ d = &b->datas[0];
+ } else {
+ if (p->io.buffer_id < mix->n_buffers) {
+ reuse_buffer(c, mix, p->io.buffer_id);
+ p->io.buffer_id = SPA_ID_INVALID;
+ }
+ if (SPA_UNLIKELY((b = dequeue_buffer(c, mix)) == NULL)) {
+ pw_log_warn("port %p: out of buffers", p);
+ return NULL;
+ }
+ d = &b->datas[0];
+ d->chunk->offset = 0;
+ d->chunk->size = frames * sizeof(float);
+ d->chunk->stride = stride;
+
+ p->io.status = SPA_STATUS_HAVE_DATA;
+ p->io.buffer_id = b->id;
+ }
+ ptr = d->data;
+ if (buf)
+ *buf = b;
+ return ptr;
+}
+
+static inline void process_empty(struct port *p, uint32_t frames)
+{
+ void *ptr;
+
+ switch (p->object->port.type_id) {
+ case TYPE_ID_AUDIO:
+ ptr = get_buffer_output(p, frames, sizeof(float), NULL);
+ if (SPA_LIKELY(ptr != NULL))
+ memcpy(ptr, p->emptyptr, frames * sizeof(float));
+ break;
+ case TYPE_ID_MIDI:
+ {
+ struct buffer *b;
+ ptr = get_buffer_output(p, MAX_BUFFER_FRAMES, 1, &b);
+ if (SPA_LIKELY(ptr != NULL)) {
+ b->datas[0].chunk->size = convert_from_midi(p->emptyptr,
+ ptr, MAX_BUFFER_FRAMES * sizeof(float));
+ }
+ break;
+ }
+ default:
+ pw_log_warn("port %p: unhandled format %d", p, p->object->port.type_id);
+ break;
+ }
+}
+
+static void prepare_output(struct port *p, uint32_t frames)
+{
+ struct mix *mix;
+
+ if (SPA_UNLIKELY(p->empty_out))
+ process_empty(p, frames);
+
+ spa_list_for_each(mix, &p->mix, port_link) {
+ if (SPA_LIKELY(mix->io != NULL))
+ *mix->io = p->io;
+ }
+}
+
+static void complete_process(struct client *c, uint32_t frames)
+{
+ struct port *p;
+ struct mix *mix;
+ union pw_map_item *item;
+
+ pw_array_for_each(item, &c->ports[SPA_DIRECTION_INPUT].items) {
+ if (pw_map_item_is_free(item))
+ continue;
+ p = item->data;
+ if (!p->valid)
+ continue;
+ spa_list_for_each(mix, &p->mix, port_link) {
+ if (SPA_LIKELY(mix->io != NULL))
+ mix->io->status = SPA_STATUS_NEED_DATA;
+ }
+ }
+ pw_array_for_each(item, &c->ports[SPA_DIRECTION_OUTPUT].items) {
+ if (pw_map_item_is_free(item))
+ continue;
+ p = item->data;
+ if (!p->valid)
+ continue;
+ prepare_output(p, frames);
+ p->io.status = SPA_STATUS_NEED_DATA;
+ }
+}
+
+static inline void debug_position(struct client *c, jack_position_t *p)
+{
+ pw_log_trace("usecs: %"PRIu64, p->usecs);
+ pw_log_trace("frame_rate: %u", p->frame_rate);
+ pw_log_trace("frame: %u", p->frame);
+ pw_log_trace("valid: %08x", p->valid);
+
+ if (p->valid & JackPositionBBT) {
+ pw_log_trace("BBT");
+ pw_log_trace(" bar: %u", p->bar);
+ pw_log_trace(" beat: %u", p->beat);
+ pw_log_trace(" tick: %u", p->tick);
+ pw_log_trace(" bar_start_tick: %f", p->bar_start_tick);
+ pw_log_trace(" beats_per_bar: %f", p->beats_per_bar);
+ pw_log_trace(" beat_type: %f", p->beat_type);
+ pw_log_trace(" ticks_per_beat: %f", p->ticks_per_beat);
+ pw_log_trace(" beats_per_minute: %f", p->beats_per_minute);
+ }
+ if (p->valid & JackPositionTimecode) {
+ pw_log_trace("Timecode:");
+ pw_log_trace(" frame_time: %f", p->frame_time);
+ pw_log_trace(" next_time: %f", p->next_time);
+ }
+ if (p->valid & JackBBTFrameOffset) {
+ pw_log_trace("BBTFrameOffset:");
+ pw_log_trace(" bbt_offset: %u", p->bbt_offset);
+ }
+ if (p->valid & JackAudioVideoRatio) {
+ pw_log_trace("AudioVideoRatio:");
+ pw_log_trace(" audio_frames_per_video_frame: %f", p->audio_frames_per_video_frame);
+ }
+ if (p->valid & JackVideoFrameOffset) {
+ pw_log_trace("JackVideoFrameOffset:");
+ pw_log_trace(" video_offset: %u", p->video_offset);
+ }
+}
+
+static inline void jack_to_position(jack_position_t *s, struct pw_node_activation *a)
+{
+ struct spa_io_segment *d = &a->segment;
+
+ if (s->valid & JackPositionBBT) {
+ d->bar.flags = SPA_IO_SEGMENT_BAR_FLAG_VALID;
+ if (s->valid & JackBBTFrameOffset)
+ d->bar.offset = s->bbt_offset;
+ else
+ d->bar.offset = 0;
+ d->bar.signature_num = s->beats_per_bar;
+ d->bar.signature_denom = s->beat_type;
+ d->bar.bpm = s->beats_per_minute;
+ d->bar.beat = (s->bar - 1) * s->beats_per_bar + (s->beat - 1) +
+ (s->tick / s->ticks_per_beat);
+ }
+}
+
+static inline jack_transport_state_t position_to_jack(struct pw_node_activation *a, jack_position_t *d)
+{
+ struct spa_io_position *s = &a->position;
+ jack_transport_state_t state;
+ struct spa_io_segment *seg = &s->segments[0];
+ uint64_t running;
+
+ switch (s->state) {
+ default:
+ case SPA_IO_POSITION_STATE_STOPPED:
+ state = JackTransportStopped;
+ break;
+ case SPA_IO_POSITION_STATE_STARTING:
+ state = JackTransportStarting;
+ break;
+ case SPA_IO_POSITION_STATE_RUNNING:
+ if (seg->flags & SPA_IO_SEGMENT_FLAG_LOOPING)
+ state = JackTransportLooping;
+ else
+ state = JackTransportRolling;
+ break;
+ }
+ if (SPA_UNLIKELY(d == NULL))
+ return state;
+
+ d->unique_1++;
+ d->usecs = s->clock.nsec / SPA_NSEC_PER_USEC;
+ d->frame_rate = s->clock.rate.denom;
+
+ if ((int64_t)s->clock.position < s->offset) {
+ d->frame = seg->position;
+ } else {
+ running = s->clock.position - s->offset;
+ if (running >= seg->start &&
+ (seg->duration == 0 || running < seg->start + seg->duration))
+ d->frame = (running - seg->start) * seg->rate + seg->position;
+ else
+ d->frame = seg->position;
+ }
+ d->valid = 0;
+ if (a->segment_owner[0] && SPA_FLAG_IS_SET(seg->bar.flags, SPA_IO_SEGMENT_BAR_FLAG_VALID)) {
+ double abs_beat;
+ long beats;
+
+ d->valid |= JackPositionBBT;
+
+ d->bbt_offset = seg->bar.offset;
+ if (seg->bar.offset)
+ d->valid |= JackBBTFrameOffset;
+
+ d->beats_per_bar = seg->bar.signature_num;
+ d->beat_type = seg->bar.signature_denom;
+ d->ticks_per_beat = 1920.0f;
+ d->beats_per_minute = seg->bar.bpm;
+
+ abs_beat = seg->bar.beat;
+
+ d->bar = abs_beat / d->beats_per_bar;
+ beats = d->bar * d->beats_per_bar;
+ d->bar_start_tick = beats * d->ticks_per_beat;
+ d->beat = abs_beat - beats;
+ beats += d->beat;
+ d->tick = (abs_beat - beats) * d->ticks_per_beat;
+ d->bar++;
+ d->beat++;
+ }
+ d->unique_2 = d->unique_1;
+ return state;
+}
+
+static void recompute_latencies(struct client *c)
+{
+ do_callback(c, latency_callback, JackCaptureLatency, c->latency_arg);
+ do_callback(c, latency_callback, JackPlaybackLatency, c->latency_arg);
+}
+
+static int
+do_buffer_frames(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ uint32_t buffer_frames = *((uint32_t*)data);
+ struct client *c = user_data;
+ if (c->buffer_frames != buffer_frames)
+ do_callback_expr(c, c->buffer_frames = buffer_frames, bufsize_callback, buffer_frames, c->bufsize_arg);
+ recompute_latencies(c);
+ return 0;
+}
+
+static inline int check_buffer_frames(struct client *c, struct spa_io_position *pos)
+{
+ uint32_t buffer_frames = pos->clock.duration;
+ if (SPA_UNLIKELY(buffer_frames != c->buffer_frames)) {
+ pw_log_info("%p: bufferframes old:%d new:%d cb:%p", c,
+ c->buffer_frames, buffer_frames, c->bufsize_callback);
+ if (c->buffer_frames != (uint32_t)-1)
+ pw_loop_invoke(c->context.l, do_buffer_frames, 0,
+ &buffer_frames, sizeof(buffer_frames), false, c);
+ else
+ c->buffer_frames = buffer_frames;
+ }
+ return c->buffer_frames == buffer_frames;
+}
+
+static int
+do_sample_rate(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct client *c = user_data;
+ uint32_t sample_rate = *((uint32_t*)data);
+ do_callback_expr(c, c->sample_rate = sample_rate, srate_callback, sample_rate, c->srate_arg);
+ return 0;
+}
+
+static inline int check_sample_rate(struct client *c, struct spa_io_position *pos)
+{
+ uint32_t sample_rate = pos->clock.rate.denom;
+ if (SPA_UNLIKELY(sample_rate != c->sample_rate)) {
+ pw_log_info("%p: sample_rate old:%d new:%d cb:%p", c,
+ c->sample_rate, sample_rate, c->srate_callback);
+ if (c->srate_callback != NULL) {
+ pw_loop_invoke(c->context.l, do_sample_rate, 0,
+ &sample_rate, sizeof(sample_rate), false, c);
+ } else {
+ c->sample_rate = sample_rate;
+ }
+ }
+ return c->sample_rate == sample_rate;
+}
+
+static inline uint32_t cycle_run(struct client *c)
+{
+ uint64_t cmd;
+ struct timespec ts;
+ int fd = c->socket_source->fd;
+ struct spa_io_position *pos = c->rt.position;
+ struct pw_node_activation *activation = c->activation;
+ struct pw_node_activation *driver = c->rt.driver_activation;
+
+ while (true) {
+ if (SPA_UNLIKELY(read(fd, &cmd, sizeof(cmd)) != sizeof(cmd))) {
+ if (errno == EINTR)
+ continue;
+ if (errno == EWOULDBLOCK || errno == EAGAIN)
+ return 0;
+ pw_log_warn("%p: read failed %m", c);
+ }
+ break;
+ }
+ if (SPA_UNLIKELY(cmd > 1))
+ pw_log_info("%p: missed %"PRIu64" wakeups", c, cmd - 1);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ activation->status = PW_NODE_ACTIVATION_AWAKE;
+ activation->awake_time = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ if (SPA_UNLIKELY(c->first)) {
+ if (c->thread_init_callback)
+ c->thread_init_callback(c->thread_init_arg);
+ c->first = false;
+ }
+
+ if (SPA_UNLIKELY(pos == NULL)) {
+ pw_log_error("%p: missing position", c);
+ return 0;
+ }
+
+ if (check_buffer_frames(c, pos) == 0)
+ return 0;
+ if (check_sample_rate(c, pos) == 0)
+ return 0;
+
+ if (SPA_LIKELY(driver)) {
+ c->jack_state = position_to_jack(driver, &c->jack_position);
+
+ if (SPA_UNLIKELY(activation->pending_sync)) {
+ if (c->sync_callback == NULL ||
+ c->sync_callback(c->jack_state, &c->jack_position, c->sync_arg))
+ activation->pending_sync = false;
+ }
+ if (SPA_UNLIKELY(c->xrun_count != driver->xrun_count &&
+ c->xrun_count != 0 && c->xrun_callback))
+ c->xrun_callback(c->xrun_arg);
+ c->xrun_count = driver->xrun_count;
+ }
+ pw_log_trace_fp("%p: wait %"PRIu64" frames:%d rate:%d pos:%d delay:%"PRIi64" corr:%f", c,
+ activation->awake_time, c->buffer_frames, c->sample_rate,
+ c->jack_position.frame, pos->clock.delay, pos->clock.rate_diff);
+
+ return c->buffer_frames;
+}
+
+static inline uint32_t cycle_wait(struct client *c)
+{
+ int res;
+ uint32_t nframes;
+
+ do {
+ res = pw_data_loop_wait(c->loop, -1);
+ if (SPA_UNLIKELY(res <= 0)) {
+ pw_log_warn("%p: wait error %m", c);
+ return 0;
+ }
+ nframes = cycle_run(c);
+ } while (!nframes);
+
+ return nframes;
+}
+
+static inline void signal_sync(struct client *c)
+{
+ struct timespec ts;
+ uint64_t cmd, nsec;
+ struct link *l;
+ struct pw_node_activation *activation = c->activation;
+
+ complete_process(c, c->buffer_frames);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ nsec = SPA_TIMESPEC_TO_NSEC(&ts);
+ activation->status = PW_NODE_ACTIVATION_FINISHED;
+ activation->finish_time = nsec;
+
+ cmd = 1;
+ spa_list_for_each(l, &c->rt.target_links, target_link) {
+ struct pw_node_activation_state *state;
+
+ if (SPA_UNLIKELY(l->activation == NULL))
+ continue;
+
+ state = &l->activation->state[0];
+
+ pw_log_trace_fp("%p: link %p %p %d/%d", c, l, state,
+ state->pending, state->required);
+
+ if (pw_node_activation_state_dec(state, 1)) {
+ l->activation->status = PW_NODE_ACTIVATION_TRIGGERED;
+ l->activation->signal_time = nsec;
+
+ pw_log_trace_fp("%p: signal %p %p", c, l, state);
+
+ if (SPA_UNLIKELY(write(l->signalfd, &cmd, sizeof(cmd)) != sizeof(cmd)))
+ pw_log_warn("%p: write failed %m", c);
+ }
+ }
+}
+
+static inline void cycle_signal(struct client *c, int status)
+{
+ struct pw_node_activation *driver = c->rt.driver_activation;
+ struct pw_node_activation *activation = c->activation;
+
+ if (SPA_LIKELY(status == 0)) {
+ if (c->timebase_callback && driver && driver->segment_owner[0] == c->node_id) {
+ if (activation->pending_new_pos ||
+ c->jack_state == JackTransportRolling ||
+ c->jack_state == JackTransportLooping) {
+ c->timebase_callback(c->jack_state,
+ c->buffer_frames,
+ &c->jack_position,
+ activation->pending_new_pos,
+ c->timebase_arg);
+
+ activation->pending_new_pos = false;
+
+ debug_position(c, &c->jack_position);
+ jack_to_position(&c->jack_position, activation);
+ }
+ }
+ }
+ signal_sync(c);
+}
+
+static void
+on_rtsocket_condition(void *data, int fd, uint32_t mask)
+{
+ struct client *c = data;
+
+ if (SPA_UNLIKELY(mask & (SPA_IO_ERR | SPA_IO_HUP))) {
+ pw_log_warn("%p: got error", c);
+ client_remove_source(c);
+ return;
+ }
+ if (SPA_UNLIKELY(c->thread_callback)) {
+ if (!c->thread_entered) {
+ c->thread_entered = true;
+ c->thread_callback(c->thread_arg);
+ }
+ } else if (SPA_LIKELY(mask & SPA_IO_IN)) {
+ uint32_t buffer_frames;
+ int status = 0;
+
+ buffer_frames = cycle_run(c);
+
+ if (buffer_frames > 0)
+ status = do_rt_callback_res(c, process_callback, buffer_frames, c->process_arg);
+
+ cycle_signal(c, status);
+ }
+}
+
+static int
+do_clear_link(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct link *link = user_data;
+ spa_list_remove(&link->target_link);
+ return 0;
+}
+
+static void clear_link(struct client *c, struct link *link)
+{
+ pw_data_loop_invoke(c->loop,
+ do_clear_link, 1, NULL, 0, !c->data_locked, link);
+ pw_memmap_free(link->mem);
+ close(link->signalfd);
+ spa_list_remove(&link->link);
+ free(link);
+}
+
+static void clean_transport(struct client *c)
+{
+ struct link *l;
+
+ if (!c->has_transport)
+ return;
+
+ pw_data_loop_invoke(c->loop,
+ do_remove_sources, 1, NULL, 0, !c->data_locked, c);
+
+ spa_list_consume(l, &c->links, link)
+ clear_link(c, l);
+
+ c->has_transport = false;
+}
+
+static int client_node_transport(void *data,
+ int readfd, int writefd,
+ uint32_t mem_id, uint32_t offset, uint32_t size)
+{
+ struct client *c = (struct client *) data;
+
+ clean_transport(c);
+
+ c->mem = pw_mempool_map_id(c->pool, mem_id,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, NULL);
+ if (c->mem == NULL) {
+ pw_log_debug("%p: can't map activation: %m", c);
+ return -errno;
+ }
+ c->activation = c->mem->ptr;
+
+ pw_log_debug("%p: create client transport with fds %d %d for node %u",
+ c, readfd, writefd, c->node_id);
+
+ close(writefd);
+ c->socket_source = pw_loop_add_io(c->loop->loop,
+ readfd,
+ SPA_IO_ERR | SPA_IO_HUP,
+ true, on_rtsocket_condition, c);
+
+ c->has_transport = true;
+ c->position = &c->activation->position;
+ pw_thread_loop_signal(c->context.loop, false);
+
+ return 0;
+}
+
+static int client_node_set_param(void *data,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct client *c = (struct client *) data;
+ pw_proxy_error((struct pw_proxy*)c->node, -ENOTSUP, "not supported");
+ return -ENOTSUP;
+}
+
+static int install_timeowner(struct client *c)
+{
+ struct pw_node_activation *a;
+ uint32_t owner;
+
+ if (!c->timebase_callback)
+ return 0;
+
+ if ((a = c->driver_activation) == NULL)
+ return -EIO;
+
+ pw_log_debug("%p: activation %p", c, a);
+
+ /* was ok */
+ owner = ATOMIC_LOAD(a->segment_owner[0]);
+ if (owner == c->node_id)
+ return 0;
+
+ /* try to become owner */
+ if (c->timeowner_conditional) {
+ if (!ATOMIC_CAS(a->segment_owner[0], 0, c->node_id)) {
+ pw_log_debug("%p: owner:%u id:%u", c, owner, c->node_id);
+ return -EBUSY;
+ }
+ } else {
+ ATOMIC_STORE(a->segment_owner[0], c->node_id);
+ }
+
+ pw_log_debug("%p: timebase installed for id:%u", c, c->node_id);
+
+ return 0;
+}
+
+static int
+do_update_driver_activation(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct client *c = user_data;
+ c->rt.position = c->position;
+ c->rt.driver_activation = c->driver_activation;
+ if (c->position) {
+ pw_log_info("%p: driver:%d clock:%s", c,
+ c->driver_id, c->position->clock.name);
+ check_sample_rate(c, c->position);
+ check_buffer_frames(c, c->position);
+ }
+ return 0;
+}
+
+static int update_driver_activation(struct client *c)
+{
+ jack_client_t *client = (jack_client_t*)c;
+ struct link *link;
+ bool freewheeling;
+
+ pw_log_debug("%p: driver %d", c, c->driver_id);
+
+ freewheeling = SPA_FLAG_IS_SET(c->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL);
+ if (c->freewheeling != freewheeling) {
+ jack_native_thread_t thr = jack_client_thread_id(client);
+
+ c->freewheeling = freewheeling;
+ if (freewheeling && thr) {
+ jack_drop_real_time_scheduling(thr);
+ }
+
+ do_callback(c, freewheel_callback, freewheeling, c->freewheel_arg);
+
+ if (!freewheeling && thr) {
+ jack_acquire_real_time_scheduling(thr,
+ jack_client_real_time_priority(client));
+ }
+ }
+
+ link = find_activation(&c->links, c->driver_id);
+ c->driver_activation = link ? link->activation : NULL;
+ pw_data_loop_invoke(c->loop,
+ do_update_driver_activation, SPA_ID_INVALID, NULL, 0, false, c);
+ install_timeowner(c);
+
+ return 0;
+}
+
+static int client_node_set_io(void *data,
+ uint32_t id,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct client *c = (struct client *) data;
+ struct pw_memmap *old, *mm;
+ void *ptr;
+ uint32_t tag[5] = { c->node_id, id, };
+
+ old = pw_mempool_find_tag(c->pool, tag, sizeof(tag));
+
+ if (mem_id == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ } else {
+ mm = pw_mempool_map_id(c->pool, mem_id,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, tag);
+ if (mm == NULL) {
+ pw_log_warn("%p: can't map memory id %u", c, mem_id);
+ return -errno;
+ }
+ ptr = mm->ptr;
+ }
+ pw_log_debug("%p: set io %s %p", c,
+ spa_debug_type_find_name(spa_type_io, id), ptr);
+
+ switch (id) {
+ case SPA_IO_Position:
+ c->position = ptr;
+ c->driver_id = ptr ? c->position->clock.id : SPA_ID_INVALID;
+ update_driver_activation(c);
+ break;
+ default:
+ break;
+ }
+ pw_memmap_free(old);
+
+ return 0;
+}
+
+static int client_node_event(void *data, const struct spa_event *event)
+{
+ return -ENOTSUP;
+}
+
+static int client_node_command(void *data, const struct spa_command *command)
+{
+ struct client *c = (struct client *) data;
+
+ pw_log_debug("%p: got command %d", c, SPA_COMMAND_TYPE(command));
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if (c->started) {
+ pw_loop_update_io(c->loop->loop,
+ c->socket_source, SPA_IO_ERR | SPA_IO_HUP);
+
+ c->started = false;
+ }
+ break;
+
+ case SPA_NODE_COMMAND_Start:
+ if (!c->started) {
+ pw_loop_update_io(c->loop->loop,
+ c->socket_source,
+ SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP);
+ c->started = true;
+ c->first = true;
+ c->thread_entered = false;
+ }
+ break;
+ default:
+ pw_log_warn("%p: unhandled node command %d", c, SPA_COMMAND_TYPE(command));
+ pw_proxy_errorf((struct pw_proxy*)c->node, -ENOTSUP,
+ "unhandled command %d", SPA_COMMAND_TYPE(command));
+ }
+ return 0;
+}
+
+static int client_node_add_port(void *data,
+ enum spa_direction direction,
+ uint32_t port_id, const struct spa_dict *props)
+{
+ struct client *c = (struct client *) data;
+ pw_proxy_error((struct pw_proxy*)c->node, -ENOTSUP, "add port not supported");
+ return -ENOTSUP;
+}
+
+static int client_node_remove_port(void *data,
+ enum spa_direction direction,
+ uint32_t port_id)
+{
+ struct client *c = (struct client *) data;
+ pw_proxy_error((struct pw_proxy*)c->node, -ENOTSUP, "remove port not supported");
+ return -ENOTSUP;
+}
+
+static int param_enum_format(struct client *c, struct port *p,
+ struct spa_pod **param, struct spa_pod_builder *b)
+{
+ switch (p->object->port.type_id) {
+ case TYPE_ID_AUDIO:
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
+ break;
+ case TYPE_ID_MIDI:
+ *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));
+ break;
+ case TYPE_ID_VIDEO:
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32));
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 1;
+}
+
+static int param_format(struct client *c, struct port *p,
+ struct spa_pod **param, struct spa_pod_builder *b)
+{
+ switch (p->object->port.type_id) {
+ case TYPE_ID_AUDIO:
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ 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_MIDI:
+ *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));
+ break;
+ case TYPE_ID_VIDEO:
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32));
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 1;
+}
+
+static int param_buffers(struct client *c, struct port *p,
+ struct spa_pod **param, struct spa_pod_builder *b)
+{
+ switch (p->object->port.type_id) {
+ case TYPE_ID_AUDIO:
+ case TYPE_ID_MIDI:
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_STEP_Int(
+ MAX_BUFFER_FRAMES * sizeof(float),
+ sizeof(float),
+ INT32_MAX,
+ sizeof(float)),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(p->object->port.type_id == TYPE_ID_AUDIO ?
+ sizeof(float) : 1));
+ break;
+ case TYPE_ID_VIDEO:
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ 320 * 240 * 4 * 4,
+ 0,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(4, 4, INT32_MAX));
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 1;
+}
+
+static int param_io(struct client *c, struct port *p,
+ struct spa_pod **param, struct spa_pod_builder *b)
+{
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_ParamIO, SPA_PARAM_IO,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ return 1;
+}
+
+static int param_latency(struct client *c, struct port *p,
+ struct spa_pod **param, struct spa_pod_builder *b)
+{
+ *param = spa_latency_build(b, SPA_PARAM_Latency,
+ &p->object->port.latency[p->direction]);
+ return 1;
+}
+
+static int param_latency_other(struct client *c, struct port *p,
+ struct spa_pod **param, struct spa_pod_builder *b)
+{
+ *param = spa_latency_build(b, SPA_PARAM_Latency,
+ &p->object->port.latency[SPA_DIRECTION_REVERSE(p->direction)]);
+ return 1;
+}
+
+/* called from thread-loop */
+static int port_set_format(struct client *c, struct port *p,
+ uint32_t flags, const struct spa_pod *param)
+{
+ struct spa_pod *params[6];
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ if (param == NULL) {
+ struct mix *mix;
+
+ pw_log_debug("%p: port %p clear format", c, p);
+
+ spa_list_for_each(mix, &p->mix, port_link)
+ clear_buffers(c, mix);
+
+ p->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ }
+ else {
+ struct spa_audio_info info = { 0 };
+ if (spa_format_parse(param, &info.media_type, &info.media_subtype) < 0)
+ return -EINVAL;
+
+ switch (info.media_type) {
+ case SPA_MEDIA_TYPE_audio:
+ {
+ if (info.media_subtype != SPA_MEDIA_SUBTYPE_dsp)
+ return -EINVAL;
+
+ if (spa_format_audio_dsp_parse(param, &info.info.dsp) < 0)
+ return -EINVAL;
+ if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32)
+ return -EINVAL;
+ break;
+ }
+ case SPA_MEDIA_TYPE_application:
+ if (info.media_subtype != SPA_MEDIA_SUBTYPE_control)
+ return -EINVAL;
+ break;
+ case SPA_MEDIA_TYPE_video:
+ {
+ struct spa_video_info vinfo = { 0 };
+
+ if (info.media_subtype != SPA_MEDIA_SUBTYPE_dsp)
+ return -EINVAL;
+ if (spa_format_video_dsp_parse(param, &vinfo.info.dsp) < 0)
+ return -EINVAL;
+ if (vinfo.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32)
+ return -EINVAL;
+ break;
+ }
+ default:
+ return -EINVAL;
+ }
+ p->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ }
+
+ pw_log_info("port %s: update", p->object->port.name);
+
+ p->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+
+ param_enum_format(c, p, &params[0], &b);
+ param_format(c, p, &params[1], &b);
+ param_buffers(c, p, &params[2], &b);
+ param_io(c, p, &params[3], &b);
+ param_latency(c, p, &params[4], &b);
+ param_latency_other(c, p, &params[5], &b);
+
+ pw_client_node_port_update(c->node,
+ p->direction,
+ p->port_id,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO,
+ SPA_N_ELEMENTS(params),
+ (const struct spa_pod **) params,
+ &p->info);
+ p->info.change_mask = 0;
+ return 0;
+}
+
+/* called from thread-loop */
+static void port_update_latency(struct port *p)
+{
+ struct client *c = p->client;
+ struct spa_pod *params[6];
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ param_enum_format(c, p, &params[0], &b);
+ param_format(c, p, &params[1], &b);
+ param_buffers(c, p, &params[2], &b);
+ param_io(c, p, &params[3], &b);
+ param_latency(c, p, &params[4], &b);
+ param_latency_other(c, p, &params[5], &b);
+
+ pw_log_info("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;
+
+ pw_client_node_port_update(c->node,
+ p->direction,
+ p->port_id,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO,
+ SPA_N_ELEMENTS(params),
+ (const struct spa_pod **) params,
+ &p->info);
+ p->info.change_mask = 0;
+}
+
+/* called from thread-loop */
+static void default_latency(struct client *c, enum spa_direction direction,
+ struct spa_latency_info *latency)
+{
+ enum spa_direction other;
+ union pw_map_item *item;
+ struct port *p;
+
+ other = SPA_DIRECTION_REVERSE(direction);
+
+ spa_latency_info_combine_start(latency, direction);
+
+ pw_array_for_each(item, &c->ports[other].items) {
+ if (pw_map_item_is_free(item))
+ continue;
+ p = item->data;
+ spa_latency_info_combine(latency, &p->object->port.latency[direction]);
+ }
+
+ spa_latency_info_combine_finish(latency);
+}
+
+/* called from thread-loop */
+static void default_latency_callback(jack_latency_callback_mode_t mode, struct client *c)
+{
+ struct spa_latency_info latency, *current;
+ enum spa_direction direction;
+ union pw_map_item *item;
+ struct port *p;
+
+ if (mode == JackPlaybackLatency)
+ direction = SPA_DIRECTION_INPUT;
+ else
+ direction = SPA_DIRECTION_OUTPUT;
+
+ default_latency(c, direction, &latency);
+
+ pw_log_info("client %p: update %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, c,
+ latency.direction == SPA_DIRECTION_INPUT ? "playback" : "capture",
+ latency.min_quantum, latency.max_quantum,
+ latency.min_rate, latency.max_rate,
+ latency.min_ns, latency.max_ns);
+
+ pw_array_for_each(item, &c->ports[direction].items) {
+ if (pw_map_item_is_free(item))
+ continue;
+ p = item->data;
+ current = &p->object->port.latency[direction];
+ if (spa_latency_info_compare(current, &latency) == 0)
+ continue;
+ *current = latency;
+ port_update_latency(p);
+ }
+}
+
+/* called from thread-loop */
+static int port_set_latency(struct client *c, struct port *p,
+ uint32_t flags, const struct spa_pod *param)
+{
+ struct spa_latency_info info;
+ jack_latency_callback_mode_t mode;
+ struct spa_latency_info *current;
+ int res;
+
+ if (param == NULL)
+ return 0;
+
+ if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+
+ current = &p->object->port.latency[info.direction];
+ if (spa_latency_info_compare(current, &info) == 0)
+ return 0;
+
+ *current = info;
+
+ pw_log_info("port %s: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, p->object->port.name,
+ info.direction == SPA_DIRECTION_INPUT ? "playback" : "capture",
+ info.min_quantum, info.max_quantum,
+ info.min_rate, info.max_rate,
+ info.min_ns, info.max_ns);
+
+ if (info.direction == p->direction)
+ return 0;
+
+ if (info.direction == SPA_DIRECTION_INPUT)
+ mode = JackPlaybackLatency;
+ else
+ mode = JackCaptureLatency;
+
+ if (c->latency_callback)
+ do_callback(c, latency_callback, mode, c->latency_arg);
+ else
+ default_latency_callback(mode, c);
+
+ port_update_latency(p);
+
+ return 0;
+}
+
+/* called from thread-loop */
+static int client_node_port_set_param(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct client *c = (struct client *) data;
+ struct port *p = GET_PORT(c, direction, port_id);
+
+ if (p == NULL || !p->valid)
+ return -EINVAL;
+
+ pw_log_info("client %p: port %s %d.%d id:%d (%s) %p", c, p->object->port.name,
+ direction, port_id, id,
+ spa_debug_type_find_name(spa_type_param, id), param);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ return port_set_format(c, p, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ return port_set_latency(c, p, flags, param);
+ default:
+ break;
+ }
+ return 0;
+}
+
+static inline void *init_buffer(struct port *p)
+{
+ void *data = p->emptyptr;
+ if (p->zeroed)
+ return data;
+
+ if (p->object->port.type_id == TYPE_ID_MIDI) {
+ struct midi_buffer *mb = data;
+ mb->magic = MIDI_BUFFER_MAGIC;
+ mb->buffer_size = MAX_BUFFER_FRAMES * sizeof(float);
+ mb->nframes = MAX_BUFFER_FRAMES;
+ mb->write_pos = 0;
+ mb->event_count = 0;
+ mb->lost_events = 0;
+ pw_log_debug("port %p: init midi buffer size:%d", p, mb->buffer_size);
+ } else
+ memset(data, 0, MAX_BUFFER_FRAMES * sizeof(float));
+
+ p->zeroed = true;
+ return data;
+}
+
+static int client_node_port_use_buffers(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t flags,
+ uint32_t n_buffers,
+ struct pw_client_node_buffer *buffers)
+{
+ struct client *c = (struct client *) data;
+ struct port *p = GET_PORT(c, direction, port_id);
+ struct buffer *b;
+ uint32_t i, j, fl;
+ int res;
+ struct mix *mix;
+
+ if (p == NULL || !p->valid) {
+ res = -EINVAL;
+ goto done;
+ }
+ if ((mix = ensure_mix(c, p, mix_id)) == NULL) {
+ res = -ENOMEM;
+ goto done;
+ }
+
+ pw_log_debug("%p: port %p %d %d.%d use_buffers %d", c, p, direction,
+ port_id, mix_id, n_buffers);
+
+ if (n_buffers > MAX_BUFFERS) {
+ pw_log_error("%p: too many buffers %u > %u", c, n_buffers, MAX_BUFFERS);
+ return -ENOSPC;
+ }
+
+ if (p->object->port.type_id == TYPE_ID_VIDEO && direction == SPA_DIRECTION_INPUT) {
+ fl = PW_MEMMAP_FLAG_READ;
+ } else {
+ /* some apps write to the input buffer so we want everything readwrite */
+ fl = PW_MEMMAP_FLAG_READWRITE;
+ }
+
+ /* clear previous buffers */
+ clear_buffers(c, mix);
+
+ for (i = 0; i < n_buffers; i++) {
+ off_t offset;
+ struct spa_buffer *buf;
+ struct pw_memmap *mm;
+
+ mm = pw_mempool_map_id(c->pool, buffers[i].mem_id,
+ fl, buffers[i].offset, buffers[i].size, NULL);
+ if (mm == NULL) {
+ pw_log_warn("%p: can't map memory id %u: %m", c, buffers[i].mem_id);
+ continue;
+ }
+
+ buf = buffers[i].buffer;
+
+ b = &mix->buffers[i];
+ b->id = i;
+ b->flags = 0;
+ b->n_mem = 0;
+ b->mem[b->n_mem++] = mm;
+
+ pw_log_debug("%p: add buffer id:%u offset:%u size:%u map:%p ptr:%p",
+ c, buffers[i].mem_id, buffers[i].offset,
+ buffers[i].size, mm, mm->ptr);
+
+ offset = 0;
+ for (j = 0; j < buf->n_metas; j++) {
+ struct spa_meta *m = &buf->metas[j];
+ offset += SPA_ROUND_UP_N(m->size, 8);
+ }
+
+ b->n_datas = SPA_MIN(buf->n_datas, MAX_BUFFER_DATAS);
+
+ for (j = 0; j < b->n_datas; j++) {
+ struct spa_data *d = &b->datas[j];
+
+ memcpy(d, &buf->datas[j], sizeof(struct spa_data));
+ d->chunk =
+ SPA_PTROFF(mm->ptr, offset + sizeof(struct spa_chunk) * j,
+ struct spa_chunk);
+
+ if (d->type == SPA_DATA_MemId) {
+ uint32_t mem_id = SPA_PTR_TO_UINT32(d->data);
+ struct pw_memblock *bm;
+ struct pw_memmap *bmm;
+
+ bm = pw_mempool_find_id(c->pool, mem_id);
+ if (bm == NULL) {
+ pw_log_error("%p: unknown buffer mem %u", c, mem_id);
+ res = -ENODEV;
+ goto done;
+
+ }
+
+ d->fd = bm->fd;
+ d->type = bm->type;
+ d->data = NULL;
+
+ bmm = pw_memblock_map(bm, fl, d->mapoffset, d->maxsize, NULL);
+ if (bmm == NULL) {
+ res = -errno;
+ pw_log_error("%p: failed to map buffer mem %m", c);
+ d->data = NULL;
+ goto done;
+ }
+ b->mem[b->n_mem++] = bmm;
+ d->data = bmm->ptr;
+
+ pw_log_debug("%p: data %d %u -> fd %d %d",
+ c, j, bm->id, bm->fd, d->maxsize);
+ } else if (d->type == SPA_DATA_MemPtr) {
+ int offs = SPA_PTR_TO_INT(d->data);
+ d->data = SPA_PTROFF(mm->ptr, offs, void);
+ d->fd = -1;
+ pw_log_debug("%p: data %d %u -> mem %p %d",
+ c, j, b->id, d->data, d->maxsize);
+ } else {
+ pw_log_warn("unknown buffer data type %d", d->type);
+ }
+ if (c->allow_mlock && mlock(d->data, d->maxsize) < 0) {
+ if (errno != ENOMEM || !mlock_warned) {
+ pw_log(c->warn_mlock ? SPA_LOG_LEVEL_WARN : SPA_LOG_LEVEL_DEBUG,
+ "%p: Failed to mlock memory %p %u: %s", c,
+ d->data, d->maxsize,
+ errno == ENOMEM ?
+ "This is not a problem but for best performance, "
+ "consider increasing RLIMIT_MEMLOCK" : strerror(errno));
+ mlock_warned |= errno == ENOMEM;
+ }
+ }
+ }
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ if (direction == SPA_DIRECTION_OUTPUT)
+ reuse_buffer(c, mix, b->id);
+
+ }
+ pw_log_debug("%p: have %d buffers", c, n_buffers);
+ mix->n_buffers = n_buffers;
+ res = 0;
+
+ done:
+ if (res < 0)
+ pw_proxy_error((struct pw_proxy*)c->node, res, spa_strerror(res));
+ return res;
+}
+
+static int client_node_port_set_io(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t id,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct client *c = (struct client *) data;
+ struct port *p = GET_PORT(c, direction, port_id);
+ struct pw_memmap *mm, *old;
+ struct mix *mix;
+ uint32_t tag[5] = { c->node_id, direction, port_id, mix_id, id };
+ void *ptr;
+ int res = 0;
+
+ if (p == NULL || !p->valid) {
+ res = -EINVAL;
+ goto exit;
+ }
+
+ if ((mix = ensure_mix(c, p, mix_id)) == NULL) {
+ res = -ENOMEM;
+ goto exit;
+ }
+
+ old = pw_mempool_find_tag(c->pool, tag, sizeof(tag));
+
+ if (mem_id == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ }
+ else {
+ mm = pw_mempool_map_id(c->pool, mem_id,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, tag);
+ if (mm == NULL) {
+ pw_log_warn("%p: can't map memory id %u", c, mem_id);
+ res = -EINVAL;
+ goto exit_free;
+ }
+ ptr = mm->ptr;
+ }
+
+ pw_log_debug("%p: port %p mix:%d set io:%s id:%u ptr:%p", c, p, mix_id,
+ spa_debug_type_find_name(spa_type_io, id), id, ptr);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ mix->io = ptr;
+ break;
+ default:
+ break;
+ }
+exit_free:
+ pw_memmap_free(old);
+exit:
+ if (res < 0)
+ pw_proxy_error((struct pw_proxy*)c->node, res, spa_strerror(res));
+ return res;
+}
+
+static int
+do_activate_link(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct link *link = user_data;
+ struct client *c = link->client;
+ pw_log_trace("link %p activate", link);
+ spa_list_append(&c->rt.target_links, &link->target_link);
+ return 0;
+}
+
+static int client_node_set_activation(void *data,
+ uint32_t node_id,
+ int signalfd,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct client *c = (struct client *) data;
+ struct pw_memmap *mm;
+ struct link *link;
+ void *ptr;
+ int res = 0;
+
+ if (c->node_id == node_id) {
+ pw_log_debug("%p: our activation %u: %u %u %u", c, node_id,
+ mem_id, offset, size);
+ close(signalfd);
+ return 0;
+ }
+
+ if (mem_id == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ size = 0;
+ }
+ else {
+ mm = pw_mempool_map_id(c->pool, mem_id,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, NULL);
+ if (mm == NULL) {
+ pw_log_warn("%p: can't map memory id %u", c, mem_id);
+ res = -EINVAL;
+ goto exit;
+ }
+ ptr = mm->ptr;
+ }
+
+ pw_log_debug("%p: set activation %u: %u %u %u %p", c, node_id,
+ mem_id, offset, size, ptr);
+
+ if (ptr) {
+ link = calloc(1, sizeof(struct link));
+ if (link == NULL) {
+ res = -errno;
+ goto exit;
+ }
+ link->client = c;
+ link->node_id = node_id;
+ link->mem = mm;
+ link->activation = ptr;
+ link->signalfd = signalfd;
+ spa_list_append(&c->links, &link->link);
+
+ pw_data_loop_invoke(c->loop,
+ do_activate_link, SPA_ID_INVALID, NULL, 0, false, link);
+ }
+ else {
+ link = find_activation(&c->links, node_id);
+ if (link == NULL) {
+ res = -EINVAL;
+ goto exit;
+ }
+ clear_link(c, link);
+ }
+
+ if (c->driver_id == node_id)
+ update_driver_activation(c);
+
+ exit:
+ if (res < 0)
+ pw_proxy_error((struct pw_proxy*)c->node, res, spa_strerror(res));
+ return res;
+}
+
+static int client_node_port_set_mix_info(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t peer_id,
+ const struct spa_dict *props)
+{
+ struct client *c = (struct client *) data;
+ struct port *p = GET_PORT(c, direction, port_id);
+ struct mix *mix;
+ struct object *l;
+ uint32_t src, dst;
+ int res = 0;
+
+ if (p == NULL || !p->valid) {
+ res = -EINVAL;
+ goto exit;
+ }
+
+ if ((mix = ensure_mix(c, p, mix_id)) == NULL) {
+ res = -ENOMEM;
+ goto exit;
+ }
+ mix->peer_id = peer_id;
+
+ if (direction == SPA_DIRECTION_INPUT) {
+ src = peer_id;
+ dst = p->object->id;
+ } else {
+ src = p->object->id;
+ dst = peer_id;
+ }
+
+ if ((l = find_link(c, src, dst)) != NULL) {
+ if (direction == SPA_DIRECTION_INPUT)
+ mix->peer_port = l->port_link.our_output;
+ else
+ mix->peer_port = l->port_link.our_input;
+
+ pw_log_debug("peer port %p %p %p", mix->peer_port,
+ l->port_link.our_output, l->port_link.our_input);
+
+ if (!l->port_link.is_complete) {
+ l->port_link.is_complete = true;
+ pw_log_info("%p: our link %u/%u -> %u/%u completed", c,
+ l->port_link.src, l->port_link.src_serial,
+ l->port_link.dst, l->port_link.dst_serial);
+ do_callback(c, connect_callback,
+ l->port_link.src_serial, l->port_link.dst_serial, 1, c->connect_arg);
+ recompute_latencies(c);
+ do_callback(c, graph_callback, c->graph_arg);
+ }
+ }
+
+exit:
+ if (res < 0)
+ pw_proxy_error((struct pw_proxy*)c->node, res, spa_strerror(res));
+ return res;
+}
+
+static const struct pw_client_node_events client_node_events = {
+ PW_VERSION_CLIENT_NODE_EVENTS,
+ .transport = client_node_transport,
+ .set_param = client_node_set_param,
+ .set_io = client_node_set_io,
+ .event = client_node_event,
+ .command = client_node_command,
+ .add_port = client_node_add_port,
+ .remove_port = client_node_remove_port,
+ .port_set_param = client_node_port_set_param,
+ .port_use_buffers = client_node_port_use_buffers,
+ .port_set_io = client_node_port_set_io,
+ .set_activation = client_node_set_activation,
+ .port_set_mix_info = client_node_port_set_mix_info,
+};
+
+#define CHECK(expression,label) \
+do { \
+ if ((errno = expression) != 0) { \
+ res = -errno; \
+ pw_log_error(#expression ": %s", strerror(errno)); \
+ goto label; \
+ } \
+} while(false);
+
+static struct spa_thread *impl_create(void *object,
+ const struct spa_dict *props,
+ void *(*start)(void*), void *arg)
+{
+ struct client *c = (struct client *) object;
+ struct spa_thread *thr;
+ int res = 0;
+
+ pw_log_info("create thread");
+ if (globals.creator != NULL) {
+ pthread_t pt;
+ pthread_attr_t *attr = NULL, attributes;
+
+ attr = pw_thread_fill_attr(props, &attributes);
+
+ res = -globals.creator(&pt, attr, start, arg);
+ if (attr)
+ pthread_attr_destroy(attr);
+ if (res != 0)
+ goto error;
+ thr = (struct spa_thread*)pt;
+ } else {
+ thr = spa_thread_utils_create(c->context.old_thread_utils, props, start, arg);
+ }
+ return thr;
+error:
+ pw_log_warn("create RT thread failed: %s", strerror(res));
+ errno = -res;
+ return NULL;
+
+}
+
+static int impl_join(void *object,
+ struct spa_thread *thread, void **retval)
+{
+ struct client *c = (struct client *) object;
+ pw_log_info("join thread");
+ return spa_thread_utils_join(c->context.old_thread_utils, thread, retval);
+}
+
+static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority)
+{
+ struct client *c = (struct client *) object;
+ return spa_thread_utils_acquire_rt(c->context.old_thread_utils, thread, priority);
+}
+
+static int impl_drop_rt(void *object, struct spa_thread *thread)
+{
+ struct client *c = (struct client *) object;
+ return spa_thread_utils_drop_rt(c->context.old_thread_utils, thread);
+}
+
+static struct spa_thread_utils_methods thread_utils_impl = {
+ SPA_VERSION_THREAD_UTILS_METHODS,
+ .create = impl_create,
+ .join = impl_join,
+ .acquire_rt = impl_acquire_rt,
+ .drop_rt = impl_drop_rt,
+};
+
+static jack_port_type_id_t string_to_type(const char *port_type)
+{
+ 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("other", port_type))
+ return TYPE_ID_OTHER;
+ else
+ return SPA_ID_INVALID;
+}
+
+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_OTHER:
+ return "other";
+ default:
+ return NULL;
+ }
+}
+
+static jack_uuid_t client_make_uuid(uint32_t id, bool monitor)
+{
+ jack_uuid_t uuid = 0x2; /* JackUUIDClient */
+ uuid = (uuid << 32) | (id + 1);
+ if (monitor)
+ uuid |= (1 << 30);
+ pw_log_debug("uuid %d -> %"PRIu64, id, uuid);
+ 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)
+{
+ struct client *c = (struct client *) data;
+ struct object *o;
+ jack_uuid_t uuid;
+
+ pw_log_debug("set id:%u key:'%s' value:'%s' type:'%s'", id, key, value, type);
+
+ if (id == PW_ID_CORE) {
+ if (key == NULL || spa_streq(key, "default.audio.sink")) {
+ if (value != NULL) {
+ if (json_object_find(value, "name",
+ c->metadata->default_audio_sink,
+ sizeof(c->metadata->default_audio_sink)) < 0)
+ value = NULL;
+ }
+ if (value == NULL)
+ c->metadata->default_audio_sink[0] = '\0';
+ }
+ if (key == NULL || spa_streq(key, "default.audio.source")) {
+ if (value != NULL) {
+ if (json_object_find(value, "name",
+ c->metadata->default_audio_source,
+ sizeof(c->metadata->default_audio_source)) < 0)
+ value = NULL;
+ }
+ if (value == NULL)
+ c->metadata->default_audio_source[0] = '\0';
+ }
+ } else {
+ if ((o = find_id(c, id, true)) == NULL)
+ return -EINVAL;
+
+ switch (o->type) {
+ case INTERFACE_Node:
+ uuid = client_make_uuid(o->serial, false);
+ break;
+ case INTERFACE_Port:
+ uuid = jack_port_uuid_generate(o->serial);
+ break;
+ default:
+ return -EINVAL;
+ }
+ update_property(c, uuid, key, type, value);
+ }
+
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property
+};
+
+static void metadata_proxy_removed(void *data)
+{
+ struct client *c = data;
+ pw_proxy_destroy((struct pw_proxy*)c->metadata->proxy);
+}
+
+static void metadata_proxy_destroy(void *data)
+{
+ struct client *c = data;
+ spa_hook_remove(&c->metadata->proxy_listener);
+ spa_hook_remove(&c->metadata->listener);
+ c->metadata = NULL;
+}
+
+static const struct pw_proxy_events metadata_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = metadata_proxy_removed,
+ .destroy = metadata_proxy_destroy,
+};
+
+static void settings_proxy_removed(void *data)
+{
+ struct client *c = data;
+ pw_proxy_destroy((struct pw_proxy*)c->settings->proxy);
+}
+
+static void settings_proxy_destroy(void *data)
+{
+ struct client *c = data;
+ spa_hook_remove(&c->settings->proxy_listener);
+ c->settings = NULL;
+}
+
+static const struct pw_proxy_events settings_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = settings_proxy_removed,
+ .destroy = settings_proxy_destroy,
+};
+static void proxy_removed(void *data)
+{
+ struct object *o = data;
+ pw_proxy_destroy(o->proxy);
+}
+
+static void proxy_destroy(void *data)
+{
+ struct object *o = data;
+ spa_hook_remove(&o->proxy_listener);
+ spa_hook_remove(&o->object_listener);
+ o->proxy = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = proxy_removed,
+ .destroy = proxy_destroy,
+};
+
+static void port_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+
+ switch (id) {
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if (spa_latency_parse(param, &info) < 0)
+ return;
+ o->port.latency[info.direction] = info;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT,
+ .param = port_param,
+};
+
+#define FILTER_NAME " ()[].:*$"
+#define FILTER_PORT " ()[].*$"
+
+static void filter_name(char *str, const char *filter, char filter_char)
+{
+ char *p;
+ for (p = str; *p; p++) {
+ if (strchr(filter, *p) != NULL)
+ *p = filter_char;
+ }
+}
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct client *c = (struct client *) data;
+ struct object *o, *ot, *op;
+ const char *str;
+ bool is_first = false, graph_changed = false;
+ uint32_t serial;
+
+ if (props == NULL)
+ return;
+
+ str = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL);
+ if (!spa_atou32(str, &serial, 0))
+ serial = SPA_ID_INVALID;
+
+ 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;
+ char tmp[JACK_CLIENT_NAME_SIZE+1];
+
+ o = alloc_object(c, INTERFACE_Node);
+
+ if ((str = spa_dict_lookup(props, PW_KEY_CLIENT_ID)) != NULL)
+ o->node.client_id = atoi(str);
+
+ node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
+
+ if (id == c->node_id) {
+ pw_log_debug("%p: add our node %d", c, id);
+ if (node_name != NULL)
+ snprintf(c->name, sizeof(c->name), "%s", node_name);
+ c->serial = serial;
+ }
+ snprintf(o->node.node_name, sizeof(o->node.node_name),
+ "%s", node_name);
+
+ app = spa_dict_lookup(props, PW_KEY_APP_NAME);
+
+ if (c->short_name) {
+ str = spa_dict_lookup(props, PW_KEY_NODE_NICK);
+ if (str == NULL)
+ str = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
+ } else {
+ str = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
+ if (str == NULL)
+ str = spa_dict_lookup(props, PW_KEY_NODE_NICK);
+ }
+ if (str == NULL)
+ str = node_name;
+ if (str == NULL)
+ str = "node";
+
+ if (app && !spa_streq(app, str))
+ snprintf(tmp, sizeof(tmp), "%s/%s", app, str);
+ else
+ snprintf(tmp, sizeof(tmp), "%s", str);
+
+ if (c->filter_name)
+ filter_name(tmp, FILTER_NAME, c->filter_char);
+
+ ot = find_node(c, tmp);
+ if (ot != NULL && o->node.client_id != ot->node.client_id) {
+ snprintf(o->node.name, sizeof(o->node.name), "%.*s-%d",
+ (int)(sizeof(tmp)-11), tmp, id);
+ } else {
+ is_first = ot == NULL;
+ snprintf(o->node.name, sizeof(o->node.name), "%s", tmp);
+ }
+
+ if ((str = spa_dict_lookup(props, PW_KEY_PRIORITY_SESSION)) != NULL)
+ o->node.priority = pw_properties_parse_int(str);
+
+ pw_log_debug("%p: add node %d", c, id);
+
+ 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_Port)) {
+ const struct spa_dict_item *item;
+ unsigned long flags = 0;
+ jack_port_type_id_t type_id;
+ uint32_t node_id;
+ bool is_monitor = false;
+ char tmp[REAL_JACK_PORT_NAME_SIZE+1];
+
+ if ((str = spa_dict_lookup(props, PW_KEY_FORMAT_DSP)) == NULL)
+ str = "other";
+ if ((type_id = string_to_type(str)) == SPA_ID_INVALID)
+ goto exit;
+
+ if ((str = spa_dict_lookup(props, PW_KEY_NODE_ID)) == NULL)
+ goto exit;
+
+ node_id = atoi(str);
+
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_EXTRA)) != NULL &&
+ spa_strstartswith(str, "jack:flags:"))
+ flags = atoi(str+11);
+
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_NAME)) == NULL)
+ goto exit;
+
+ spa_dict_for_each(item, props) {
+ if (spa_streq(item->key, PW_KEY_PORT_DIRECTION)) {
+ if (spa_streq(item->value, "in"))
+ flags |= JackPortIsInput;
+ else if (spa_streq(item->value, "out"))
+ flags |= JackPortIsOutput;
+ }
+ else if (spa_streq(item->key, PW_KEY_PORT_PHYSICAL)) {
+ if (pw_properties_parse_bool(item->value))
+ flags |= JackPortIsPhysical;
+ }
+ else if (spa_streq(item->key, PW_KEY_PORT_TERMINAL)) {
+ if (pw_properties_parse_bool(item->value))
+ flags |= JackPortIsTerminal;
+ }
+ else if (spa_streq(item->key, PW_KEY_PORT_CONTROL)) {
+ if (pw_properties_parse_bool(item->value))
+ type_id = TYPE_ID_MIDI;
+ }
+ else if (spa_streq(item->key, PW_KEY_PORT_MONITOR)) {
+ is_monitor = pw_properties_parse_bool(item->value);
+ }
+ }
+ if (is_monitor && !c->show_monitor)
+ goto exit;
+
+ o = NULL;
+ if (node_id == c->node_id) {
+ snprintf(tmp, sizeof(tmp), "%s:%s", c->name, str);
+ o = find_port_by_name(c, tmp);
+ if (o != NULL)
+ pw_log_info("%p: %s found our port %p", c, tmp, o);
+ }
+ if (o == NULL) {
+ if ((ot = find_type(c, node_id, INTERFACE_Node, true)) == NULL)
+ goto exit;
+
+ o = alloc_object(c, INTERFACE_Port);
+ if (o == NULL)
+ goto exit;
+
+ o->port.system_id = 0;
+ o->port.priority = ot->node.priority;
+ o->port.node = ot;
+ o->port.latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ o->port.latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ o->proxy = pw_registry_bind(c->registry,
+ id, type, PW_VERSION_PORT, 0);
+ if (o->proxy) {
+ uint32_t ids[1] = { SPA_PARAM_Latency };
+
+ pw_proxy_add_listener(o->proxy,
+ &o->proxy_listener, &proxy_events, o);
+ pw_proxy_add_object_listener(o->proxy,
+ &o->object_listener, &port_events, o);
+
+ pw_port_subscribe_params((struct pw_port*)o->proxy,
+ ids, 1);
+ }
+ pthread_mutex_lock(&c->context.lock);
+ spa_list_append(&c->context.objects, &o->link);
+ pthread_mutex_unlock(&c->context.lock);
+
+ if (is_monitor && !c->merge_monitor)
+ snprintf(tmp, sizeof(tmp), "%.*s%s:%s",
+ (int)(JACK_CLIENT_NAME_SIZE-(sizeof(MONITOR_EXT)-1)),
+ ot->node.name, MONITOR_EXT, str);
+ else
+ snprintf(tmp, sizeof(tmp), "%s:%s", ot->node.name, str);
+
+ if (c->filter_name)
+ filter_name(tmp, FILTER_PORT, c->filter_char);
+
+ op = find_port_by_name(c, tmp);
+ if (op != NULL)
+ snprintf(o->port.name, sizeof(o->port.name), "%.*s-%u",
+ (int)(sizeof(tmp)-11), tmp, serial);
+ else
+ snprintf(o->port.name, sizeof(o->port.name), "%s", tmp);
+ }
+
+ if ((str = spa_dict_lookup(props, PW_KEY_OBJECT_PATH)) != NULL)
+ snprintf(o->port.alias1, sizeof(o->port.alias1), "%s", str);
+
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_ALIAS)) != NULL)
+ snprintf(o->port.alias2, sizeof(o->port.alias2), "%s", str);
+
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_ID)) != NULL) {
+ o->port.system_id = atoi(str);
+ snprintf(o->port.system, sizeof(o->port.system), "system:%s_%d",
+ flags & JackPortIsInput ? "playback" :
+ is_monitor ? "monitor" : "capture",
+ o->port.system_id+1);
+ }
+
+ o->port.flags = flags;
+ o->port.type_id = type_id;
+ o->port.node_id = node_id;
+ o->port.is_monitor = is_monitor;
+
+ pw_log_debug("%p: %p add port %d name:%s %d", c, o, id,
+ o->port.name, type_id);
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ struct object *p;
+
+ o = alloc_object(c, INTERFACE_Link);
+
+ pthread_mutex_lock(&c->context.lock);
+ spa_list_append(&c->context.objects, &o->link);
+ pthread_mutex_unlock(&c->context.lock);
+
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT)) == NULL)
+ goto exit_free;
+ o->port_link.src = pw_properties_parse_int(str);
+
+ if ((p = find_type(c, o->port_link.src, INTERFACE_Port, true)) == NULL)
+ goto exit_free;
+ o->port_link.src_serial = p->serial;
+
+ o->port_link.src_ours = p->port.port != NULL &&
+ p->port.port->client == c;
+ if (o->port_link.src_ours)
+ o->port_link.our_output = p->port.port;
+
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT)) == NULL)
+ goto exit_free;
+ o->port_link.dst = pw_properties_parse_int(str);
+
+ if ((p = find_type(c, o->port_link.dst, INTERFACE_Port, true)) == NULL)
+ goto exit_free;
+ o->port_link.dst_serial = p->serial;
+
+ o->port_link.dst_ours = p->port.port != NULL &&
+ p->port.port->client == c;
+ if (o->port_link.dst_ours)
+ o->port_link.our_input = p->port.port;
+
+ o->port_link.is_complete = !o->port_link.src_ours && !o->port_link.dst_ours;
+ pw_log_debug("%p: add link %d %u/%u->%u/%u", c, id,
+ o->port_link.src, o->port_link.src_serial,
+ o->port_link.dst, o->port_link.dst_serial);
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata)) {
+ struct pw_proxy *proxy;
+
+ if (c->metadata != NULL)
+ goto exit;
+ if ((str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) == NULL)
+ goto exit;
+
+ if (spa_streq(str, "default")) {
+ proxy = pw_registry_bind(c->registry,
+ id, type, PW_VERSION_METADATA, sizeof(struct metadata));
+
+ c->metadata = pw_proxy_get_user_data(proxy);
+ c->metadata->proxy = (struct pw_metadata*)proxy;
+ c->metadata->default_audio_sink[0] = '\0';
+ c->metadata->default_audio_source[0] = '\0';
+
+ pw_proxy_add_listener(proxy,
+ &c->metadata->proxy_listener,
+ &metadata_proxy_events, c);
+ pw_metadata_add_listener(proxy,
+ &c->metadata->listener,
+ &metadata_events, c);
+ } else if (spa_streq(str, "settings")) {
+ proxy = pw_registry_bind(c->registry,
+ id, type, PW_VERSION_METADATA, sizeof(struct metadata));
+
+ c->settings = pw_proxy_get_user_data(proxy);
+ c->settings->proxy = (struct pw_metadata*)proxy;
+ pw_proxy_add_listener(proxy,
+ &c->settings->proxy_listener,
+ &settings_proxy_events, c);
+ }
+ goto exit;
+ }
+ else {
+ goto exit;
+ }
+
+ o->id = id;
+ o->serial = serial;
+
+ switch (o->type) {
+ case INTERFACE_Node:
+ if (is_first) {
+ pw_log_info("%p: client added \"%s\"", c, o->node.name);
+ do_callback(c, registration_callback,
+ o->node.name, 1, c->registration_arg);
+ graph_changed = true;
+ }
+ break;
+
+ case INTERFACE_Port:
+ pw_log_info("%p: port added %u/%u \"%s\"", c, o->id, o->serial, o->port.name);
+ do_callback(c, portregistration_callback,
+ o->serial, 1, c->portregistration_arg);
+ graph_changed = true;
+ break;
+
+ case INTERFACE_Link:
+ pw_log_info("%p: link %u %u/%u -> %u/%u added complete:%d", c,
+ o->id, o->port_link.src, o->port_link.src_serial,
+ o->port_link.dst, o->port_link.dst_serial,
+ o->port_link.is_complete);
+ if (o->port_link.is_complete) {
+ do_callback(c, connect_callback,
+ o->port_link.src_serial,
+ o->port_link.dst_serial, 1, c->connect_arg);
+ graph_changed = true;
+ }
+ break;
+ }
+ if (graph_changed) {
+ recompute_latencies(c);
+ do_callback(c, graph_callback, c->graph_arg);
+ }
+
+ exit:
+ return;
+ exit_free:
+ free_object(c, o);
+ return;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct client *c = (struct client *) data;
+ struct object *o;
+ bool graph_changed = false;
+
+ pw_log_debug("%p: removed: %u", c, id);
+
+ if ((o = find_id(c, id, true)) == NULL)
+ return;
+
+ if (o->proxy) {
+ pw_proxy_destroy(o->proxy);
+ o->proxy = NULL;
+ }
+ o->removing = true;
+
+ switch (o->type) {
+ case INTERFACE_Node:
+ if (c->metadata) {
+ if (spa_streq(o->node.node_name, c->metadata->default_audio_sink))
+ c->metadata->default_audio_sink[0] = '\0';
+ if (spa_streq(o->node.node_name, c->metadata->default_audio_source))
+ c->metadata->default_audio_source[0] = '\0';
+ }
+ if (find_node(c, o->node.name) == NULL) {
+ pw_log_info("%p: client %u removed \"%s\"", c, o->id, o->node.name);
+ do_callback(c, registration_callback,
+ o->node.name, 0, c->registration_arg);
+ graph_changed = true;
+ }
+ break;
+ case INTERFACE_Port:
+ pw_log_info("%p: port %u/%u removed \"%s\"", c, o->id, o->serial, o->port.name);
+ do_callback(c, portregistration_callback,
+ o->serial, 0, c->portregistration_arg);
+ graph_changed = true;
+ break;
+ case INTERFACE_Link:
+ if (o->port_link.is_complete &&
+ find_type(c, o->port_link.src, INTERFACE_Port, true) != NULL &&
+ find_type(c, o->port_link.dst, INTERFACE_Port, true) != NULL) {
+ pw_log_info("%p: link %u %u/%u -> %u/%u removed", c, o->id,
+ o->port_link.src, o->port_link.src_serial,
+ o->port_link.dst, o->port_link.dst_serial);
+ o->port_link.is_complete = false;
+ do_callback(c, connect_callback,
+ o->port_link.src_serial, o->port_link.dst_serial, 0, c->connect_arg);
+ graph_changed = true;
+ } else
+ pw_log_warn("unlink between unknown ports %d and %d",
+ o->port_link.src, o->port_link.dst);
+ break;
+ }
+ if (graph_changed) {
+ recompute_latencies(c);
+ do_callback(c, graph_callback, c->graph_arg);
+ }
+
+ o->removing = false;
+ free_object(c, o);
+
+ return;
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void varargs_parse (struct client *c, jack_options_t options, va_list ap)
+{
+ if ((options & JackServerName))
+ c->server_name = va_arg(ap, char *);
+ if ((options & JackLoadName))
+ c->load_name = va_arg(ap, char *);
+ if ((options & JackLoadInit))
+ c->load_init = va_arg(ap, char *);
+ if ((options & JackSessionID)) {
+ char *sid = va_arg(ap, char *);
+ if (sid) {
+ const long long id = atoll(sid);
+ if (id > 0)
+ c->session_id = id;
+ }
+ }
+}
+
+
+static int execute_match(void *data, const char *location, const char *action,
+ const char *val, size_t len)
+{
+ struct client *client = data;
+ if (spa_streq(action, "update-props"))
+ pw_properties_update_string(client->props, val, len);
+ return 1;
+}
+
+SPA_EXPORT
+jack_client_t * jack_client_open (const char *client_name,
+ jack_options_t options,
+ jack_status_t *status, ...)
+{
+ struct client *client;
+ const struct spa_support *support;
+ uint32_t n_support;
+ const char *str;
+ struct spa_cpu *cpu_iface;
+ va_list ap;
+
+ if (getenv("PIPEWIRE_NOJACK") != NULL ||
+ getenv("PIPEWIRE_INTERNAL") != NULL ||
+ strstr(pw_get_library_version(), "0.2") != NULL)
+ goto disabled;
+
+ spa_return_val_if_fail(client_name != NULL, NULL);
+
+ client = calloc(1, sizeof(struct client));
+ if (client == NULL)
+ goto disabled;
+
+ pw_log_info("%p: open '%s' options:%d", client, client_name, options);
+
+ va_start(ap, status);
+ varargs_parse(client, options, ap);
+ va_end(ap);
+
+ snprintf(client->name, sizeof(client->name), "pw-%s", client_name);
+
+ pthread_mutex_init(&client->context.lock, NULL);
+ spa_list_init(&client->context.objects);
+
+ client->node_id = SPA_ID_INVALID;
+
+ client->buffer_frames = (uint32_t)-1;
+ client->sample_rate = (uint32_t)-1;
+ client->latency = SPA_FRACTION(-1, -1);
+
+ spa_list_init(&client->mix);
+ spa_list_init(&client->free_mix);
+
+ spa_list_init(&client->free_ports);
+ pw_map_init(&client->ports[SPA_DIRECTION_INPUT], 32, 32);
+ pw_map_init(&client->ports[SPA_DIRECTION_OUTPUT], 32, 32);
+
+ spa_list_init(&client->links);
+ client->driver_id = SPA_ID_INVALID;
+
+ spa_list_init(&client->rt.target_links);
+ pthread_mutex_init(&client->rt_lock, NULL);
+
+ if (client->server_name != NULL &&
+ spa_streq(client->server_name, "default"))
+ client->server_name = NULL;
+
+ client->props = pw_properties_new(
+ "loop.cancel", "true",
+ PW_KEY_REMOTE_NAME, client->server_name,
+ PW_KEY_CLIENT_NAME, client_name,
+ PW_KEY_CLIENT_API, "jack",
+ PW_KEY_CONFIG_NAME, "jack.conf",
+ NULL);
+ if (client->props == NULL)
+ goto no_props;
+
+ client->context.loop = pw_thread_loop_new(client->name, NULL);
+ client->context.l = pw_thread_loop_get_loop(client->context.loop);
+ client->context.context = pw_context_new(
+ client->context.l,
+ pw_properties_copy(client->props),
+ 0);
+ if (client->context.context == NULL)
+ goto no_props;
+
+ client->allow_mlock = client->context.context->settings.mem_allow_mlock;
+ client->warn_mlock = client->context.context->settings.mem_warn_mlock;
+
+ pw_context_conf_update_props(client->context.context,
+ "jack.properties", client->props);
+
+ pw_context_conf_section_match_rules(client->context.context, "jack.rules",
+ &client->context.context->properties->dict, execute_match, client);
+
+ support = pw_context_get_support(client->context.context, &n_support);
+
+ mix_function = mix_c;
+ cpu_iface = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+ if (cpu_iface) {
+#if defined (__SSE__)
+ uint32_t flags = spa_cpu_get_flags(cpu_iface);
+ if (flags & SPA_CPU_FLAG_SSE)
+ mix_function = mix_sse;
+#endif
+ }
+ client->context.old_thread_utils =
+ pw_context_get_object(client->context.context,
+ SPA_TYPE_INTERFACE_ThreadUtils);
+ if (client->context.old_thread_utils == NULL)
+ client->context.old_thread_utils = pw_thread_utils_get();
+
+ globals.thread_utils = client->context.old_thread_utils;
+
+ client->context.thread_utils.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_ThreadUtils,
+ SPA_VERSION_THREAD_UTILS,
+ &thread_utils_impl, client);
+
+ client->loop = client->context.context->data_loop_impl;
+ pw_data_loop_stop(client->loop);
+
+ pw_context_set_object(client->context.context,
+ SPA_TYPE_INTERFACE_ThreadUtils,
+ &client->context.thread_utils);
+
+ pw_thread_loop_start(client->context.loop);
+
+ pw_thread_loop_lock(client->context.loop);
+
+ client->core = pw_context_connect(client->context.context,
+ pw_properties_copy(client->props), 0);
+ if (client->core == NULL)
+ goto server_failed;
+
+ client->pool = pw_core_get_mempool(client->core);
+
+ pw_core_add_listener(client->core,
+ &client->core_listener,
+ &core_events, client);
+ client->registry = pw_core_get_registry(client->core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(client->registry,
+ &client->registry_listener,
+ &registry_events, client);
+
+ if ((str = getenv("PIPEWIRE_PROPS")) != NULL)
+ pw_properties_update_string(client->props, str, strlen(str));
+ if ((str = getenv("PIPEWIRE_QUANTUM")) != NULL) {
+ struct spa_fraction q;
+ if (sscanf(str, "%u/%u", &q.num, &q.denom) == 2 && q.denom != 0) {
+ pw_properties_setf(client->props, PW_KEY_NODE_RATE,
+ "1/%u", q.denom);
+ pw_properties_setf(client->props, PW_KEY_NODE_LATENCY,
+ "%u/%u", q.num, q.denom);
+ } else {
+ pw_log_warn("invalid PIPEWIRE_QUANTUM: %s", str);
+ }
+ }
+ if ((str = getenv("PIPEWIRE_LATENCY")) != NULL)
+ pw_properties_set(client->props, PW_KEY_NODE_LATENCY, str);
+ if ((str = getenv("PIPEWIRE_RATE")) != NULL)
+ pw_properties_set(client->props, PW_KEY_NODE_RATE, str);
+ if ((str = getenv("PIPEWIRE_LINK_PASSIVE")) != NULL)
+ pw_properties_set(client->props, PW_KEY_NODE_PASSIVE, str);
+
+ if ((str = pw_properties_get(client->props, PW_KEY_NODE_LATENCY)) != NULL) {
+ uint32_t num, denom;
+ if (sscanf(str, "%u/%u", &num, &denom) == 2 && denom != 0) {
+ client->latency = SPA_FRACTION(num, denom);
+ }
+ }
+ if (pw_properties_get(client->props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(client->props, PW_KEY_NODE_NAME, client_name);
+ if (pw_properties_get(client->props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(client->props, PW_KEY_NODE_GROUP, "jack-%d", getpid());
+ if (pw_properties_get(client->props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(client->props, PW_KEY_NODE_DESCRIPTION, client_name);
+ if (pw_properties_get(client->props, PW_KEY_MEDIA_TYPE) == NULL)
+ pw_properties_set(client->props, PW_KEY_MEDIA_TYPE, "Audio");
+ if (pw_properties_get(client->props, PW_KEY_MEDIA_CATEGORY) == NULL)
+ pw_properties_set(client->props, PW_KEY_MEDIA_CATEGORY, "Duplex");
+ if (pw_properties_get(client->props, PW_KEY_MEDIA_ROLE) == NULL)
+ pw_properties_set(client->props, PW_KEY_MEDIA_ROLE, "DSP");
+ if (pw_properties_get(client->props, PW_KEY_NODE_ALWAYS_PROCESS) == NULL)
+ pw_properties_set(client->props, PW_KEY_NODE_ALWAYS_PROCESS, "true");
+ if (pw_properties_get(client->props, PW_KEY_NODE_LOCK_QUANTUM) == NULL)
+ pw_properties_set(client->props, PW_KEY_NODE_LOCK_QUANTUM, "true");
+ pw_properties_set(client->props, PW_KEY_NODE_TRANSPORT_SYNC, "true");
+
+ client->node = pw_core_create_object(client->core,
+ "client-node",
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE,
+ &client->props->dict,
+ 0);
+ if (client->node == NULL)
+ goto init_failed;
+
+ pw_client_node_add_listener(client->node,
+ &client->node_listener, &client_node_events, client);
+ pw_proxy_add_listener((struct pw_proxy*)client->node,
+ &client->proxy_listener, &node_proxy_events, client);
+
+ client->info = SPA_NODE_INFO_INIT();
+ client->info.max_input_ports = UINT32_MAX;
+ client->info.max_output_ports = UINT32_MAX;
+ client->info.change_mask = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS;
+ client->info.flags = SPA_NODE_FLAG_RT;
+ client->info.props = &client->props->dict;
+
+ pw_client_node_update(client->node,
+ PW_CLIENT_NODE_UPDATE_INFO,
+ 0, NULL, &client->info);
+ client->info.change_mask = 0;
+
+ client->show_monitor = pw_properties_get_bool(client->props, "jack.show-monitor", true);
+ client->merge_monitor = pw_properties_get_bool(client->props, "jack.merge-monitor", true);
+ client->short_name = pw_properties_get_bool(client->props, "jack.short-name", false);
+ client->filter_name = pw_properties_get_bool(client->props, "jack.filter-name", false);
+ client->filter_char = ' ';
+ if ((str = pw_properties_get(client->props, "jack.filter-char")) != NULL && str[0] != '\0')
+ client->filter_char = str[0];
+ client->locked_process = pw_properties_get_bool(client->props, "jack.locked-process", true);
+ client->default_as_system = pw_properties_get_bool(client->props, "jack.default-as-system", false);
+ client->fix_midi_events = pw_properties_get_bool(client->props, "jack.fix-midi-events", true);
+ client->global_buffer_size = pw_properties_get_bool(client->props, "jack.global-buffer-size", false);
+
+ client->self_connect_mode = SELF_CONNECT_ALLOW;
+ if ((str = pw_properties_get(client->props, "jack.self-connect-mode")) != NULL) {
+ if (spa_streq(str, "fail-external"))
+ client->self_connect_mode = SELF_CONNECT_FAIL_EXT;
+ else if (spa_streq(str, "ignore-external"))
+ client->self_connect_mode = SELF_CONNECT_IGNORE_EXT;
+ else if (spa_streq(str, "fail-all"))
+ client->self_connect_mode = SELF_CONNECT_FAIL_ALL;
+ else if (spa_streq(str, "ignore-all"))
+ client->self_connect_mode = SELF_CONNECT_IGNORE_ALL;
+ }
+ client->rt_max = pw_properties_get_int32(client->props, "rt.prio", DEFAULT_RT_MAX);
+
+ if (status)
+ *status = 0;
+
+ while (true) {
+ pw_thread_loop_wait(client->context.loop);
+
+ if (client->last_res < 0)
+ goto init_failed;
+
+ if (client->has_transport)
+ break;
+ }
+
+ if (!spa_streq(client->name, client_name)) {
+ if (status)
+ *status |= JackNameNotUnique;
+ if (options & JackUseExactName)
+ goto exit_unlock;
+ }
+ pw_thread_loop_unlock(client->context.loop);
+
+ pw_log_info("%p: opened", client);
+ return (jack_client_t *)client;
+
+no_props:
+ if (status)
+ *status = JackFailure | JackInitFailure;
+ goto exit;
+init_failed:
+ if (status)
+ *status = JackFailure | JackInitFailure;
+ goto exit_unlock;
+server_failed:
+ if (status)
+ *status = JackFailure | JackServerFailed;
+ goto exit_unlock;
+exit_unlock:
+ pw_thread_loop_unlock(client->context.loop);
+exit:
+ jack_client_close((jack_client_t *) client);
+ return NULL;
+disabled:
+ if (status)
+ *status = JackFailure | JackInitFailure;
+ return NULL;
+}
+
+SPA_EXPORT
+jack_client_t * jack_client_new (const char *client_name)
+{
+ jack_options_t options = JackUseExactName;
+ jack_status_t status;
+
+ if (getenv("JACK_START_SERVER") == NULL)
+ options |= JackNoStartServer;
+
+ return jack_client_open(client_name, options, &status, NULL);
+}
+
+SPA_EXPORT
+int jack_client_close (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ struct object *o;
+ int res;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_log_info("%p: close", client);
+
+ c->destroyed = true;
+
+ res = jack_deactivate(client);
+
+ clean_transport(c);
+
+ if (c->context.loop)
+ pw_thread_loop_stop(c->context.loop);
+
+ if (c->registry) {
+ spa_hook_remove(&c->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)c->registry);
+ }
+ if (c->metadata && c->metadata->proxy) {
+ pw_proxy_destroy((struct pw_proxy*)c->metadata->proxy);
+ }
+ if (c->settings && c->settings->proxy) {
+ pw_proxy_destroy((struct pw_proxy*)c->settings->proxy);
+ }
+
+ if (c->core) {
+ spa_hook_remove(&c->core_listener);
+ pw_core_disconnect(c->core);
+ }
+
+ if (c->context.context)
+ pw_context_destroy(c->context.context);
+
+ if (c->context.loop)
+ pw_thread_loop_destroy(c->context.loop);
+
+ pw_log_debug("%p: free", client);
+
+ spa_list_consume(o, &c->context.objects, link)
+ free_object(c, o);
+ recycle_objects(c, 0);
+
+ pw_map_clear(&c->ports[SPA_DIRECTION_INPUT]);
+ pw_map_clear(&c->ports[SPA_DIRECTION_OUTPUT]);
+
+ pthread_mutex_destroy(&c->context.lock);
+ pthread_mutex_destroy(&c->rt_lock);
+ pw_properties_free(c->props);
+ free(c);
+
+ return res;
+}
+
+SPA_EXPORT
+jack_intclient_t jack_internal_client_handle (jack_client_t *client,
+ const char *client_name, jack_status_t *status)
+{
+ struct client *c = (struct client *) client;
+ spa_return_val_if_fail(c != NULL, 0);
+ if (status)
+ *status = JackNoSuchClient | JackFailure;
+ return 0;
+}
+
+SPA_EXPORT
+jack_intclient_t jack_internal_client_load (jack_client_t *client,
+ const char *client_name, jack_options_t options,
+ jack_status_t *status, ...)
+{
+ struct client *c = (struct client *) client;
+ spa_return_val_if_fail(c != NULL, 0);
+ if (status)
+ *status = JackNoSuchClient | JackFailure;
+ return 0;
+}
+
+SPA_EXPORT
+jack_status_t jack_internal_client_unload (jack_client_t *client,
+ jack_intclient_t intclient)
+{
+ struct client *c = (struct client *) client;
+ spa_return_val_if_fail(c != NULL, 0);
+ return JackFailure | JackNoSuchClient;
+}
+
+SPA_EXPORT
+char *jack_get_internal_client_name (jack_client_t *client,
+ jack_intclient_t intclient)
+{
+ struct client *c = (struct client *) client;
+ spa_return_val_if_fail(c != NULL, NULL);
+ return strdup(c->name);
+}
+
+SPA_EXPORT
+int jack_client_name_size (void)
+{
+ /* The JACK API specifies that this value includes the final NULL character. */
+ pw_log_trace("%d", JACK_CLIENT_NAME_SIZE+1);
+ return JACK_CLIENT_NAME_SIZE+1;
+}
+
+SPA_EXPORT
+char * jack_get_client_name (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ spa_return_val_if_fail(c != NULL, NULL);
+ return c->name;
+}
+
+SPA_EXPORT
+char *jack_get_uuid_for_client_name (jack_client_t *client,
+ const char *client_name)
+{
+ struct client *c = (struct client *) client;
+ struct object *o;
+ char *uuid = NULL;
+ bool monitor;
+
+ spa_return_val_if_fail(c != NULL, NULL);
+ spa_return_val_if_fail(client_name != NULL, NULL);
+
+ monitor = spa_strendswith(client_name, MONITOR_EXT);
+
+ pthread_mutex_lock(&c->context.lock);
+
+ spa_list_for_each(o, &c->context.objects, link) {
+ if (o->type != INTERFACE_Node)
+ continue;
+ if (spa_streq(o->node.name, client_name) ||
+ (monitor && spa_strneq(o->node.name, client_name,
+ strlen(client_name) - strlen(MONITOR_EXT)))) {
+ uuid = spa_aprintf( "%" PRIu64, client_make_uuid(o->serial, monitor));
+ break;
+ }
+ }
+ pw_log_debug("%p: name %s -> %s", client, client_name, uuid);
+ pthread_mutex_unlock(&c->context.lock);
+ return uuid;
+}
+
+SPA_EXPORT
+char *jack_get_client_name_by_uuid (jack_client_t *client,
+ const char *client_uuid )
+{
+ struct client *c = (struct client *) client;
+ struct object *o;
+ jack_uuid_t uuid;
+ char *name = NULL;
+ bool monitor;
+
+ spa_return_val_if_fail(c != NULL, NULL);
+ spa_return_val_if_fail(client_uuid != NULL, NULL);
+
+ if (jack_uuid_parse(client_uuid, &uuid) < 0)
+ return NULL;
+
+ monitor = uuid & (1 << 30);
+
+ pthread_mutex_lock(&c->context.lock);
+ spa_list_for_each(o, &c->context.objects, link) {
+ if (o->type != INTERFACE_Node)
+ continue;
+ if (client_make_uuid(o->serial, monitor) == uuid) {
+ pw_log_debug("%p: uuid %s (%"PRIu64")-> %s",
+ client, client_uuid, uuid, o->node.name);
+ name = spa_aprintf("%s%s", o->node.name, monitor ? MONITOR_EXT : "");
+ break;
+ }
+ }
+ pthread_mutex_unlock(&c->context.lock);
+ return name;
+}
+
+SPA_EXPORT
+int jack_internal_client_new (const char *client_name,
+ const char *load_name,
+ const char *load_init)
+{
+ pw_log_warn("not implemented %s %s %s", client_name, load_name, load_init);
+ return -ENOTSUP;
+}
+
+SPA_EXPORT
+void jack_internal_client_close (const char *client_name)
+{
+ pw_log_warn("not implemented %s", client_name);
+}
+
+static int do_activate(struct client *c)
+{
+ int res;
+ pw_client_node_set_active(c->node, true);
+ res = do_sync(c);
+ return res;
+}
+
+SPA_EXPORT
+int jack_activate (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ int res = 0;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_log_info("%p: active:%d", c, c->active);
+
+ if (c->active)
+ return 0;
+
+ pw_thread_loop_lock(c->context.loop);
+ pw_data_loop_start(c->loop);
+
+ if ((res = do_activate(c)) < 0)
+ goto done;
+
+ c->activation->pending_new_pos = true;
+ c->activation->pending_sync = true;
+
+ c->active = true;
+
+ do_callback(c, graph_callback, c->graph_arg);
+
+done:
+ if (res < 0)
+ pw_data_loop_stop(c->loop);
+
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_deactivate (jack_client_t *client)
+{
+ struct object *l;
+ struct client *c = (struct client *) client;
+ int res;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_log_info("%p: active:%d", c, c->active);
+
+ if (!c->active)
+ return 0;
+
+ pw_thread_loop_lock(c->context.loop);
+ pw_data_loop_stop(c->loop);
+
+ pw_client_node_set_active(c->node, false);
+
+ c->activation->pending_new_pos = false;
+ c->activation->pending_sync = false;
+
+ spa_list_for_each(l, &c->context.objects, link) {
+ if (l->type != INTERFACE_Link || l->removed)
+ continue;
+ if (l->port_link.src_ours || l->port_link.dst_ours)
+ pw_registry_destroy(c->registry, l->id);
+ }
+
+ res = do_sync(c);
+
+ pw_thread_loop_unlock(c->context.loop);
+
+ if (res < 0)
+ return res;
+
+ c->active = false;
+
+ return 0;
+}
+
+SPA_EXPORT
+int jack_get_client_pid (const char *name)
+{
+ pw_log_error("not implemented on library side");
+ return 0;
+}
+
+SPA_EXPORT
+jack_native_thread_t jack_client_thread_id (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, (pthread_t){0});
+
+ return (jack_native_thread_t)pw_data_loop_get_thread(c->loop);
+}
+
+SPA_EXPORT
+int jack_is_realtime (jack_client_t *client)
+{
+ return 1;
+}
+
+SPA_EXPORT
+jack_nframes_t jack_thread_wait (jack_client_t *client, int status)
+{
+ pw_log_error("%p: jack_thread_wait: deprecated, use jack_cycle_wait/jack_cycle_signal", client);
+ return 0;
+}
+
+SPA_EXPORT
+jack_nframes_t jack_cycle_wait (jack_client_t* client)
+{
+ struct client *c = (struct client *) client;
+ jack_nframes_t res;
+
+ spa_return_val_if_fail(c != NULL, 0);
+
+ res = cycle_wait(c);
+ pw_log_trace("%p: result:%d", c, res);
+ return res;
+}
+
+SPA_EXPORT
+void jack_cycle_signal (jack_client_t* client, int status)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_if_fail(c != NULL);
+
+ pw_log_trace("%p: status:%d", c, status);
+ cycle_signal(c, status);
+}
+
+SPA_EXPORT
+int jack_set_process_thread(jack_client_t* client, JackThreadCallback thread_callback, void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ } else if (c->process_callback) {
+ pw_log_error("%p: process callback was already set", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, thread_callback, arg);
+ c->thread_callback = thread_callback;
+ c->thread_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_thread_init_callback (jack_client_t *client,
+ JackThreadInitCallback thread_init_callback,
+ void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_log_debug("%p: %p %p", c, thread_init_callback, arg);
+ c->thread_init_callback = thread_init_callback;
+ c->thread_init_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+void jack_on_shutdown (jack_client_t *client,
+ JackShutdownCallback shutdown_callback, void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_if_fail(c != NULL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ } else {
+ pw_log_debug("%p: %p %p", c, shutdown_callback, arg);
+ c->shutdown_callback = shutdown_callback;
+ c->shutdown_arg = arg;
+ }
+}
+
+SPA_EXPORT
+void jack_on_info_shutdown (jack_client_t *client,
+ JackInfoShutdownCallback shutdown_callback, void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_if_fail(c != NULL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ } else {
+ pw_log_debug("%p: %p %p", c, shutdown_callback, arg);
+ c->info_shutdown_callback = shutdown_callback;
+ c->info_shutdown_arg = arg;
+ }
+}
+
+SPA_EXPORT
+int jack_set_process_callback (jack_client_t *client,
+ JackProcessCallback process_callback,
+ void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ } else if (c->thread_callback) {
+ pw_log_error("%p: thread callback was already set", c);
+ return -EIO;
+ }
+
+ pw_log_debug("%p: %p %p", c, process_callback, arg);
+ c->process_callback = process_callback;
+ c->process_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_freewheel_callback (jack_client_t *client,
+ JackFreewheelCallback freewheel_callback,
+ void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, freewheel_callback, arg);
+ c->freewheel_callback = freewheel_callback;
+ c->freewheel_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_buffer_size_callback (jack_client_t *client,
+ JackBufferSizeCallback bufsize_callback,
+ void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, bufsize_callback, arg);
+ c->bufsize_callback = bufsize_callback;
+ c->bufsize_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_sample_rate_callback (jack_client_t *client,
+ JackSampleRateCallback srate_callback,
+ void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, srate_callback, arg);
+ c->srate_callback = srate_callback;
+ c->srate_arg = arg;
+ if (c->srate_callback && c->sample_rate != (uint32_t)-1)
+ c->srate_callback(c->sample_rate, c->srate_arg);
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_client_registration_callback (jack_client_t *client,
+ JackClientRegistrationCallback
+ registration_callback, void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, registration_callback, arg);
+ c->registration_callback = registration_callback;
+ c->registration_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_port_registration_callback (jack_client_t *client,
+ JackPortRegistrationCallback
+ registration_callback, void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, registration_callback, arg);
+ c->portregistration_callback = registration_callback;
+ c->portregistration_arg = arg;
+ return 0;
+}
+
+
+SPA_EXPORT
+int jack_set_port_connect_callback (jack_client_t *client,
+ JackPortConnectCallback
+ connect_callback, void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, connect_callback, arg);
+ c->connect_callback = connect_callback;
+ c->connect_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_port_rename_callback (jack_client_t *client,
+ JackPortRenameCallback rename_callback,
+ void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, rename_callback, arg);
+ c->rename_callback = rename_callback;
+ c->rename_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_graph_order_callback (jack_client_t *client,
+ JackGraphOrderCallback graph_callback,
+ void *data)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, graph_callback, data);
+ c->graph_callback = graph_callback;
+ c->graph_arg = data;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_xrun_callback (jack_client_t *client,
+ JackXRunCallback xrun_callback, void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, xrun_callback, arg);
+ c->xrun_callback = xrun_callback;
+ c->xrun_arg = arg;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_latency_callback (jack_client_t *client,
+ JackLatencyCallback latency_callback,
+ void *data)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_debug("%p: %p %p", c, latency_callback, data);
+ c->latency_callback = latency_callback;
+ c->latency_arg = data;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_freewheel(jack_client_t* client, int onoff)
+{
+ struct client *c = (struct client *) client;
+
+ pw_log_info("%p: freewheel %d", client, onoff);
+
+ pw_thread_loop_lock(c->context.loop);
+ pw_properties_set(c->props, "node.group",
+ onoff ? "pipewire.freewheel" : "");
+
+ c->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ c->info.props = &c->props->dict;
+
+ pw_client_node_update(c->node,
+ PW_CLIENT_NODE_UPDATE_INFO,
+ 0, NULL, &c->info);
+ c->info.change_mask = 0;
+ pw_thread_loop_unlock(c->context.loop);
+
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_buffer_size (jack_client_t *client, jack_nframes_t nframes)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_log_info("%p: buffer-size %u", client, nframes);
+
+ pw_thread_loop_lock(c->context.loop);
+ if (c->global_buffer_size && c->settings && c->settings->proxy) {
+ char val[256];
+ snprintf(val, sizeof(val), "%u", nframes == 1 ? 0: nframes);
+ pw_metadata_set_property(c->settings->proxy, 0,
+ "clock.force-quantum", "", val);
+ } else {
+ pw_properties_setf(c->props, PW_KEY_NODE_FORCE_QUANTUM, "%u", nframes);
+
+ c->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ c->info.props = &c->props->dict;
+
+ pw_client_node_update(c->node,
+ PW_CLIENT_NODE_UPDATE_INFO,
+ 0, NULL, &c->info);
+ c->info.change_mask = 0;
+ }
+ pw_thread_loop_unlock(c->context.loop);
+
+ return 0;
+}
+
+SPA_EXPORT
+jack_nframes_t jack_get_sample_rate (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ jack_nframes_t res = -1;
+
+ spa_return_val_if_fail(c != NULL, 0);
+
+ if (!c->active)
+ res = c->latency.denom;
+ if (c->active || res == (uint32_t)-1) {
+ res = c->sample_rate;
+ if (res == (uint32_t)-1) {
+ if (c->rt.position)
+ res = c->rt.position->clock.rate.denom;
+ else if (c->position)
+ res = c->position->clock.rate.denom;
+ }
+ }
+ pw_log_debug("sample_rate: %u", res);
+ return res;
+}
+
+SPA_EXPORT
+jack_nframes_t jack_get_buffer_size (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ jack_nframes_t res = -1;
+
+ spa_return_val_if_fail(c != NULL, 0);
+
+ if (!c->active)
+ res = c->latency.num;
+ if (c->active || res == (uint32_t)-1) {
+ res = c->buffer_frames;
+ if (res == (uint32_t)-1) {
+ if (c->rt.position)
+ res = c->rt.position->clock.duration;
+ else if (c->position)
+ res = c->position->clock.duration;
+ }
+ }
+ c->buffer_frames = res;
+ pw_log_debug("buffer_frames: %u", res);
+ return res;
+}
+
+SPA_EXPORT
+int jack_engine_takeover_timebase (jack_client_t *client)
+{
+ pw_log_error("%p: deprecated", client);
+ return 0;
+}
+
+SPA_EXPORT
+float jack_cpu_load (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ float res = 0.0f;
+
+ spa_return_val_if_fail(c != NULL, 0.0);
+
+ if (c->driver_activation)
+ res = c->driver_activation->cpu_load[0] * 100.0f;
+
+ pw_log_trace("%p: cpu load %f", client, res);
+ return res;
+}
+
+#include "statistics.c"
+
+static void *get_buffer_input_float(struct port *p, jack_nframes_t frames);
+static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames);
+static void *get_buffer_input_empty(struct port *p, jack_nframes_t frames);
+static void *get_buffer_output_float(struct port *p, jack_nframes_t frames);
+static void *get_buffer_output_midi(struct port *p, jack_nframes_t frames);
+static void *get_buffer_output_empty(struct port *p, jack_nframes_t frames);
+
+SPA_EXPORT
+jack_port_t * jack_port_register (jack_client_t *client,
+ const char *port_name,
+ const char *port_type,
+ unsigned long flags,
+ unsigned long buffer_frames)
+{
+ struct client *c = (struct client *) client;
+ enum spa_direction direction;
+ struct object *o;
+ jack_port_type_id_t type_id;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct spa_pod *params[6];
+ uint32_t n_params = 0;
+ struct port *p;
+ int res;
+
+ spa_return_val_if_fail(c != NULL, NULL);
+ spa_return_val_if_fail(port_name != NULL, NULL);
+ spa_return_val_if_fail(port_type != NULL, NULL);
+
+ pw_log_info("%p: port register \"%s:%s\" \"%s\" %08lx %ld",
+ c, c->name, port_name, port_type, flags, buffer_frames);
+
+ if (flags & JackPortIsInput)
+ direction = PW_DIRECTION_INPUT;
+ else if (flags & JackPortIsOutput)
+ direction = PW_DIRECTION_OUTPUT;
+ else {
+ pw_log_warn("invalid port flags %lu for %s", flags, port_name);
+ return NULL;
+ }
+
+ if ((type_id = string_to_type(port_type)) == SPA_ID_INVALID) {
+ pw_log_warn("unknown port type %s", port_type);
+ return NULL;
+ }
+
+ if ((p = alloc_port(c, direction)) == NULL) {
+ pw_log_warn("can't allocate port %s: %m", port_name);
+ return NULL;
+ }
+
+ o = p->object;
+ o->port.flags = flags;
+ snprintf(o->port.name, sizeof(o->port.name), "%s:%s", c->name, port_name);
+ o->port.type_id = type_id;
+
+ init_buffer(p);
+
+ if (direction == SPA_DIRECTION_INPUT) {
+ switch (type_id) {
+ case TYPE_ID_AUDIO:
+ case TYPE_ID_VIDEO:
+ p->get_buffer = get_buffer_input_float;
+ break;
+ case TYPE_ID_MIDI:
+ p->get_buffer = get_buffer_input_midi;
+ break;
+ default:
+ p->get_buffer = get_buffer_input_empty;
+ break;
+ }
+ } else {
+ switch (type_id) {
+ case TYPE_ID_AUDIO:
+ case TYPE_ID_VIDEO:
+ p->get_buffer = get_buffer_output_float;
+ break;
+ case TYPE_ID_MIDI:
+ p->get_buffer = get_buffer_output_midi;
+ break;
+ default:
+ p->get_buffer = get_buffer_output_empty;
+ break;
+ }
+ }
+
+ pw_log_debug("%p: port %p", c, p);
+
+ spa_list_init(&p->mix);
+
+ pw_properties_set(p->props, PW_KEY_FORMAT_DSP, port_type);
+ pw_properties_set(p->props, PW_KEY_PORT_NAME, port_name);
+ if (flags > 0x1f) {
+ pw_properties_setf(p->props, PW_KEY_PORT_EXTRA,
+ "jack:flags:%lu", flags & ~0x1f);
+ }
+ if (flags & JackPortIsPhysical)
+ pw_properties_set(p->props, PW_KEY_PORT_PHYSICAL, "true");
+ if (flags & JackPortIsTerminal)
+ pw_properties_set(p->props, PW_KEY_PORT_TERMINAL, "true");
+
+ p->info = SPA_PORT_INFO_INIT();
+ p->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ p->info.flags = SPA_PORT_FLAG_NO_REF;
+ p->info.change_mask |= SPA_PORT_CHANGE_MASK_PROPS;
+ p->info.props = &p->props->dict;
+ p->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ p->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ p->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ p->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ p->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ p->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ p->info.params = p->params;
+ p->info.n_params = N_PORT_PARAMS;
+
+ param_enum_format(c, p, &params[n_params++], &b);
+ param_buffers(c, p, &params[n_params++], &b);
+ param_io(c, p, &params[n_params++], &b);
+ param_latency(c, p, &params[n_params++], &b);
+ param_latency_other(c, p, &params[n_params++], &b);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ pw_client_node_port_update(c->node,
+ direction,
+ p->port_id,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO,
+ n_params,
+ (const struct spa_pod **) params,
+ &p->info);
+
+ p->info.change_mask = 0;
+
+ res = do_sync(c);
+
+ pw_thread_loop_unlock(c->context.loop);
+
+ if (res < 0) {
+ pw_log_warn("can't create port %s: %s", port_name,
+ spa_strerror(res));
+ return NULL;
+ }
+
+ return (jack_port_t *) o;
+}
+
+static int
+do_invalidate_port(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct port *p = user_data;
+ p->valid = false;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_port_unregister (jack_client_t *client, jack_port_t *port)
+{
+ struct client *c = (struct client *) client;
+ struct object *o = (struct object *) port;
+ struct port *p;
+ int res;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ p = o->port.port;
+ if (o->type != INTERFACE_Port || p == NULL || !p->valid ||
+ o->client != c) {
+ pw_log_error("%p: invalid port %p", client, port);
+ res = -EINVAL;
+ goto done;
+ }
+ pw_data_loop_invoke(c->loop,
+ do_invalidate_port, 1, NULL, 0, !c->data_locked, p);
+
+ pw_log_info("%p: port %p unregister \"%s\"", client, port, o->port.name);
+
+ pw_client_node_port_update(c->node,
+ p->direction,
+ p->port_id,
+ 0, 0, NULL, NULL);
+
+ res = do_sync(c);
+ if (res < 0) {
+ pw_log_warn("can't unregister port %s: %s", o->port.name,
+ spa_strerror(res));
+ }
+ free_port(c, p);
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+static struct buffer *get_mix_buffer(struct mix *mix, jack_nframes_t frames)
+{
+ struct spa_io_buffers *io;
+
+ if (mix->peer_port != NULL)
+ prepare_output(mix->peer_port, frames);
+
+ io = mix->io;
+ if (io == NULL ||
+ io->status != SPA_STATUS_HAVE_DATA ||
+ io->buffer_id >= mix->n_buffers)
+ return NULL;
+
+ return &mix->buffers[io->buffer_id];
+}
+
+static void *get_buffer_input_float(struct port *p, jack_nframes_t frames)
+{
+ struct mix *mix;
+ struct buffer *b;
+ void *ptr = NULL;
+ float *mix_ptr[MAX_MIX], *np;
+ uint32_t n_ptr = 0;
+ bool ptr_aligned = true;
+
+ spa_list_for_each(mix, &p->mix, port_link) {
+ struct spa_data *d;
+ uint32_t offset, size;
+
+ pw_log_trace_fp("%p: port %s mix %d.%d get buffer %d",
+ p->client, p->object->port.name, p->port_id, mix->id, frames);
+
+ if ((b = get_mix_buffer(mix, frames)) == NULL)
+ continue;
+
+ d = &b->datas[0];
+ offset = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offset);
+ if (size / sizeof(float) < frames)
+ continue;
+
+ np = SPA_PTROFF(d->data, offset, float);
+ if (!SPA_IS_ALIGNED(np, 16))
+ ptr_aligned = false;
+
+ mix_ptr[n_ptr++] = np;
+ if (n_ptr == MAX_MIX)
+ break;
+ }
+ if (n_ptr == 1) {
+ ptr = mix_ptr[0];
+ } else if (n_ptr > 1) {
+ ptr = p->emptyptr;
+ mix_function(ptr, mix_ptr, n_ptr, ptr_aligned, frames);
+ p->zeroed = false;
+ }
+ if (ptr == NULL)
+ ptr = init_buffer(p);
+ return ptr;
+}
+
+static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames)
+{
+ struct mix *mix;
+ void *ptr = p->emptyptr;
+ struct spa_pod_sequence *seq[MAX_MIX];
+ uint32_t n_seq = 0;
+
+ jack_midi_clear_buffer(ptr);
+
+ spa_list_for_each(mix, &p->mix, port_link) {
+ struct spa_data *d;
+ struct buffer *b;
+ void *pod;
+
+ pw_log_trace_fp("%p: port %p mix %d.%d get buffer %d",
+ p->client, p, p->port_id, mix->id, frames);
+
+ if ((b = get_mix_buffer(mix, frames)) == NULL)
+ continue;
+
+ d = &b->datas[0];
+
+ if ((pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size)) == NULL)
+ continue;
+ if (!spa_pod_is_sequence(pod))
+ continue;
+
+ seq[n_seq++] = pod;
+ if (n_seq == MAX_MIX)
+ break;
+ }
+ convert_to_midi(seq, n_seq, ptr, p->client->fix_midi_events);
+
+ return ptr;
+}
+
+static void *get_buffer_output_float(struct port *p, jack_nframes_t frames)
+{
+ void *ptr;
+
+ ptr = get_buffer_output(p, frames, sizeof(float), NULL);
+ if (SPA_UNLIKELY(p->empty_out = (ptr == NULL)))
+ ptr = p->emptyptr;
+ return ptr;
+}
+
+static void *get_buffer_output_midi(struct port *p, jack_nframes_t frames)
+{
+ p->empty_out = true;
+ return p->emptyptr;
+}
+
+static void *get_buffer_output_empty(struct port *p, jack_nframes_t frames)
+{
+ p->empty_out = true;
+ return p->emptyptr;
+}
+
+static void *get_buffer_input_empty(struct port *p, jack_nframes_t frames)
+{
+ return init_buffer(p);
+}
+
+SPA_EXPORT
+void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames)
+{
+ struct object *o = (struct object *) port;
+ struct port *p;
+ void *ptr;
+
+ spa_return_val_if_fail(o != NULL, NULL);
+
+ if (o->type != INTERFACE_Port || o->client == NULL)
+ return NULL;
+
+ if ((p = o->port.port) == NULL) {
+ struct mix *mix;
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t offset, size;
+
+ if ((mix = find_mix_peer(o->client, o->id)) == NULL)
+ return NULL;
+
+ pw_log_trace("peer mix: %p %d", mix, mix->peer_id);
+
+ if ((b = get_mix_buffer(mix, frames)) == NULL)
+ return NULL;
+
+ d = &b->datas[0];
+ offset = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offset);
+ if (size / sizeof(float) < frames)
+ return NULL;
+
+ return SPA_PTROFF(d->data, offset, void);
+ }
+ if (!p->valid)
+ return NULL;
+
+ ptr = p->get_buffer(p, frames);
+ pw_log_trace_fp("%p: port %p buffer %p empty:%u", p->client, p, ptr, p->empty_out);
+ return ptr;
+}
+
+SPA_EXPORT
+jack_uuid_t jack_port_uuid (const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ spa_return_val_if_fail(o != NULL, 0);
+ return jack_port_uuid_generate(o->serial);
+}
+
+static const char *port_name(struct object *o)
+{
+ const char *name;
+ struct client *c = o->client;
+ if (c->default_as_system && is_port_default(c, o))
+ name = o->port.system;
+ else
+ name = o->port.name;
+ return name;
+}
+
+SPA_EXPORT
+const char * jack_port_name (const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ spa_return_val_if_fail(o != NULL, NULL);
+ return port_name(o);
+}
+
+SPA_EXPORT
+const char * jack_port_short_name (const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ spa_return_val_if_fail(o != NULL, NULL);
+ return strchr(port_name(o), ':') + 1;
+}
+
+SPA_EXPORT
+int jack_port_flags (const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ spa_return_val_if_fail(o != NULL, 0);
+ return o->port.flags;
+}
+
+SPA_EXPORT
+const char * jack_port_type (const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ spa_return_val_if_fail(o != NULL, NULL);
+ return type_to_string(o->port.type_id);
+}
+
+SPA_EXPORT
+jack_port_type_id_t jack_port_type_id (const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ spa_return_val_if_fail(o != NULL, 0);
+ return o->port.type_id;
+}
+
+SPA_EXPORT
+int jack_port_is_mine (const jack_client_t *client, const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ spa_return_val_if_fail(o != NULL, 0);
+ return o->type == INTERFACE_Port &&
+ o->port.port != NULL &&
+ o->port.port->client == (struct client*)client;
+}
+
+SPA_EXPORT
+int jack_port_connected (const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ struct client *c;
+ struct object *l;
+ int res = 0;
+
+ spa_return_val_if_fail(o != NULL, 0);
+ if (o->type != INTERFACE_Port || o->client == NULL)
+ return 0;
+
+ c = o->client;
+
+ pthread_mutex_lock(&c->context.lock);
+ spa_list_for_each(l, &c->context.objects, link) {
+ if (l->type != INTERFACE_Link || l->removed)
+ continue;
+ if (!l->port_link.is_complete)
+ continue;
+ if (l->port_link.src_serial == o->serial ||
+ l->port_link.dst_serial == o->serial)
+ res++;
+ }
+ pthread_mutex_unlock(&c->context.lock);
+
+ pw_log_debug("%p: id:%u/%u res:%d", port, o->id, o->serial, res);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_port_connected_to (const jack_port_t *port,
+ const char *port_name)
+{
+ struct object *o = (struct object *) port;
+ struct client *c;
+ struct object *p, *l;
+ int res = 0;
+
+ spa_return_val_if_fail(o != NULL, 0);
+ spa_return_val_if_fail(port_name != NULL, 0);
+ if (o->type != INTERFACE_Port || o->client == NULL)
+ return 0;
+
+ c = o->client;
+
+ pthread_mutex_lock(&c->context.lock);
+
+ p = find_port_by_name(c, port_name);
+ if (p == NULL)
+ goto exit;
+
+ if (GET_DIRECTION(p->port.flags) == GET_DIRECTION(o->port.flags))
+ goto exit;
+
+ if (p->port.flags & JackPortIsOutput) {
+ l = p;
+ p = o;
+ o = l;
+ }
+ if ((l = find_link(c, o->id, p->id)) != NULL &&
+ l->port_link.is_complete)
+ res = 1;
+
+ exit:
+ pthread_mutex_unlock(&c->context.lock);
+ pw_log_debug("%p: id:%u/%u name:%s res:%d", port, o->id,
+ o->serial, port_name, res);
+
+ return res;
+}
+
+SPA_EXPORT
+const char ** jack_port_get_connections (const jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+
+ spa_return_val_if_fail(o != NULL, NULL);
+ if (o->type != INTERFACE_Port || o->client == NULL)
+ return NULL;
+
+ return jack_port_get_all_connections((jack_client_t *)o->client, port);
+}
+
+SPA_EXPORT
+const char ** jack_port_get_all_connections (const jack_client_t *client,
+ const jack_port_t *port)
+{
+ struct client *c = (struct client *) client;
+ struct object *o = (struct object *) port;
+ struct object *p, *l;
+ const char **res;
+ int count = 0;
+ struct pw_array tmp;
+
+ spa_return_val_if_fail(c != NULL, NULL);
+ spa_return_val_if_fail(o != NULL, NULL);
+
+ pw_array_init(&tmp, sizeof(void*) * 32);
+
+ pthread_mutex_lock(&c->context.lock);
+ spa_list_for_each(l, &c->context.objects, link) {
+ if (l->type != INTERFACE_Link || l->removed)
+ continue;
+ if (l->port_link.src_serial == o->serial)
+ p = find_type(c, l->port_link.dst, INTERFACE_Port, true);
+ else if (l->port_link.dst_serial == o->serial)
+ p = find_type(c, l->port_link.src, INTERFACE_Port, true);
+ else
+ continue;
+
+ if (p == NULL)
+ continue;
+
+ pw_array_add_ptr(&tmp, (void*)port_name(p));
+ count++;
+ }
+ pthread_mutex_unlock(&c->context.lock);
+
+ if (count == 0) {
+ pw_array_clear(&tmp);
+ res = NULL;
+ } else {
+ pw_array_add_ptr(&tmp, NULL);
+ res = tmp.data;
+ }
+ return res;
+}
+
+SPA_EXPORT
+int jack_port_tie (jack_port_t *src, jack_port_t *dst)
+{
+ pw_log_warn("not implemented %p %p", src, dst);
+ return -ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_port_untie (jack_port_t *port)
+{
+ pw_log_warn("not implemented %p", port);
+ return -ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_port_set_name (jack_port_t *port, const char *port_name)
+{
+ pw_log_warn("deprecated");
+ return 0;
+}
+
+SPA_EXPORT
+int jack_port_rename (jack_client_t* client, jack_port_t *port, const char *port_name)
+{
+ struct client *c = (struct client *) client;
+ struct object *o = (struct object *) port;
+ struct port *p;
+ int res = 0;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+ spa_return_val_if_fail(port_name != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ pw_log_info("%p: port rename %p %s -> %s:%s",
+ client, port, o->port.name, c->name, port_name);
+
+ p = o->port.port;
+ if (p == NULL || !p->valid) {
+ res = -EINVAL;
+ goto done;
+ }
+
+ pw_properties_set(p->props, PW_KEY_PORT_NAME, port_name);
+ snprintf(o->port.name, sizeof(o->port.name), "%s:%s", c->name, port_name);
+
+ p->info.change_mask |= SPA_PORT_CHANGE_MASK_PROPS;
+ p->info.props = &p->props->dict;
+
+ pw_client_node_port_update(c->node,
+ p->direction,
+ p->port_id,
+ PW_CLIENT_NODE_PORT_UPDATE_INFO,
+ 0, NULL,
+ &p->info);
+ p->info.change_mask = 0;
+
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_port_set_alias (jack_port_t *port, const char *alias)
+{
+ struct object *o = (struct object *) port;
+ struct client *c;
+ struct port *p;
+ const char *key;
+ int res = 0;
+
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+ spa_return_val_if_fail(alias != NULL, -EINVAL);
+
+ c = o->client;
+ if (o->type != INTERFACE_Port || c == NULL)
+ return -EINVAL;
+
+ pw_thread_loop_lock(c->context.loop);
+
+ p = o->port.port;
+ if (p == NULL || !p->valid) {
+ res = -EINVAL;
+ goto done;
+ }
+
+ if (o->port.alias1[0] == '\0') {
+ key = PW_KEY_OBJECT_PATH;
+ snprintf(o->port.alias1, sizeof(o->port.alias1), "%s", alias);
+ }
+ else if (o->port.alias2[0] == '\0') {
+ key = PW_KEY_PORT_ALIAS;
+ snprintf(o->port.alias2, sizeof(o->port.alias2), "%s", alias);
+ }
+ else {
+ res = -1;
+ goto done;
+ }
+
+ pw_properties_set(p->props, key, alias);
+
+ p->info.change_mask |= SPA_PORT_CHANGE_MASK_PROPS;
+ p->info.props = &p->props->dict;
+
+ pw_client_node_port_update(c->node,
+ p->direction,
+ p->port_id,
+ PW_CLIENT_NODE_PORT_UPDATE_INFO,
+ 0, NULL,
+ &p->info);
+ p->info.change_mask = 0;
+
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_port_unset_alias (jack_port_t *port, const char *alias)
+{
+ struct object *o = (struct object *) port;
+ struct client *c;
+ struct port *p;
+ const char *key;
+ int res = 0;
+
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+ spa_return_val_if_fail(alias != NULL, -EINVAL);
+
+ c = o->client;
+ if (o->type != INTERFACE_Port || c == NULL)
+ return -EINVAL;
+
+ pw_thread_loop_lock(c->context.loop);
+ p = o->port.port;
+ if (p == NULL || !p->valid) {
+ res = -EINVAL;
+ goto done;
+ }
+
+ if (spa_streq(o->port.alias1, alias))
+ key = PW_KEY_OBJECT_PATH;
+ else if (spa_streq(o->port.alias2, alias))
+ key = PW_KEY_PORT_ALIAS;
+ else {
+ res = -1;
+ goto done;
+ }
+
+ pw_properties_set(p->props, key, NULL);
+
+ p->info.change_mask |= SPA_PORT_CHANGE_MASK_PROPS;
+ p->info.props = &p->props->dict;
+
+ pw_client_node_port_update(c->node,
+ p->direction,
+ p->port_id,
+ PW_CLIENT_NODE_PORT_UPDATE_INFO,
+ 0, NULL,
+ &p->info);
+ p->info.change_mask = 0;
+
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_port_get_aliases (const jack_port_t *port, char* const aliases[2])
+{
+ struct object *o = (struct object *) port;
+ int res = 0;
+
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+ spa_return_val_if_fail(aliases != NULL, -EINVAL);
+ spa_return_val_if_fail(aliases[0] != NULL, -EINVAL);
+ spa_return_val_if_fail(aliases[1] != NULL, -EINVAL);
+
+ if (o->port.alias1[0] != '\0') {
+ snprintf(aliases[0], REAL_JACK_PORT_NAME_SIZE+1, "%s", o->port.alias1);
+ res++;
+ }
+ if (o->port.alias2[0] != '\0') {
+ snprintf(aliases[1], REAL_JACK_PORT_NAME_SIZE+1, "%s", o->port.alias2);
+ res++;
+ }
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_port_request_monitor (jack_port_t *port, int onoff)
+{
+ struct object *o = (struct object *) port;
+
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+
+ if (onoff)
+ o->port.monitor_requests++;
+ else if (o->port.monitor_requests > 0)
+ o->port.monitor_requests--;
+ return 0;
+}
+
+SPA_EXPORT
+int jack_port_request_monitor_by_name (jack_client_t *client,
+ const char *port_name, int onoff)
+{
+ struct client *c = (struct client *) client;
+ struct object *p;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+ spa_return_val_if_fail(port_name != NULL, -EINVAL);
+
+ pthread_mutex_lock(&c->context.lock);
+ p = find_port_by_name(c, port_name);
+ pthread_mutex_unlock(&c->context.lock);
+
+ if (p == NULL) {
+ pw_log_error("%p: jack_port_request_monitor_by_name called"
+ " with an incorrect port %s", client, port_name);
+ return -1;
+ }
+
+ return jack_port_request_monitor((jack_port_t*)p, onoff);
+}
+
+SPA_EXPORT
+int jack_port_ensure_monitor (jack_port_t *port, int onoff)
+{
+ struct object *o = (struct object *) port;
+
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+
+ if (onoff) {
+ if (o->port.monitor_requests == 0)
+ o->port.monitor_requests++;
+ } else {
+ if (o->port.monitor_requests > 0)
+ o->port.monitor_requests = 0;
+ }
+ return 0;
+}
+
+SPA_EXPORT
+int jack_port_monitoring_input (jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+ return o->port.monitor_requests > 0;
+}
+
+static void link_proxy_error(void *data, int seq, int res, const char *message)
+{
+ int *link_res = data;
+ *link_res = res;
+}
+
+static const struct pw_proxy_events link_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .error = link_proxy_error,
+};
+
+static int check_connect(struct client *c, struct object *src, struct object *dst)
+{
+ int src_self, dst_self, sum;
+
+ if (c->self_connect_mode == SELF_CONNECT_ALLOW)
+ return 1;
+
+ src_self = src->port.node_id == c->node_id ? 1 : 0;
+ dst_self = dst->port.node_id == c->node_id ? 1 : 0;
+ sum = src_self + dst_self;
+ /* check for no self connection first */
+ if (sum == 0)
+ return 1;
+
+ /* internal connection */
+ if (sum == 2 &&
+ (c->self_connect_mode == SELF_CONNECT_FAIL_EXT ||
+ c->self_connect_mode == SELF_CONNECT_IGNORE_EXT))
+ return 1;
+
+ /* failure -> -1 */
+ if (c->self_connect_mode < 0)
+ return -1;
+
+ /* ignore -> 0 */
+ return 0;
+}
+
+SPA_EXPORT
+int jack_connect (jack_client_t *client,
+ const char *source_port,
+ const char *destination_port)
+{
+ struct client *c = (struct client *) client;
+ struct object *src, *dst;
+ struct spa_dict props;
+ struct spa_dict_item items[6];
+ struct pw_proxy *proxy;
+ struct spa_hook listener;
+ char val[4][16];
+ const char *str;
+ int res, link_res = 0;
+
+ spa_return_val_if_fail(c != NULL, EINVAL);
+ spa_return_val_if_fail(source_port != NULL, EINVAL);
+ spa_return_val_if_fail(destination_port != NULL, EINVAL);
+
+ pw_log_info("%p: connect %s %s", client, source_port, destination_port);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ src = find_port_by_name(c, source_port);
+ dst = find_port_by_name(c, destination_port);
+
+ if (src == NULL || dst == NULL ||
+ !(src->port.flags & JackPortIsOutput) ||
+ !(dst->port.flags & JackPortIsInput) ||
+ src->port.type_id != dst->port.type_id) {
+ res = -EINVAL;
+ goto exit;
+ }
+ if ((res = check_connect(c, src, dst)) != 1)
+ goto exit;
+
+ snprintf(val[0], sizeof(val[0]), "%d", src->port.node_id);
+ snprintf(val[1], sizeof(val[1]), "%d", src->id);
+ snprintf(val[2], sizeof(val[2]), "%d", dst->port.node_id);
+ snprintf(val[3], sizeof(val[3]), "%d", dst->id);
+
+ props = SPA_DICT_INIT(items, 0);
+ items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_OUTPUT_NODE, val[0]);
+ items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_OUTPUT_PORT, val[1]);
+ items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_NODE, val[2]);
+ items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_PORT, val[3]);
+ items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_LINGER, "true");
+ if ((str = pw_properties_get(c->props, PW_KEY_NODE_PASSIVE)) != NULL &&
+ pw_properties_parse_bool(str))
+ items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_PASSIVE, "true");
+
+ proxy = pw_core_create_object(c->core,
+ "link-factory",
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ &props,
+ 0);
+ if (proxy == NULL) {
+ res = -errno;
+ goto exit;
+ }
+
+ spa_zero(listener);
+ pw_proxy_add_listener(proxy, &listener, &link_proxy_events, &link_res);
+
+ res = do_sync(c);
+
+ spa_hook_remove(&listener);
+
+ if (link_res < 0)
+ res = link_res;
+
+ pw_proxy_destroy(proxy);
+
+ exit:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return -res;
+}
+
+SPA_EXPORT
+int jack_disconnect (jack_client_t *client,
+ const char *source_port,
+ const char *destination_port)
+{
+ struct client *c = (struct client *) client;
+ struct object *src, *dst, *l;
+ int res;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+ spa_return_val_if_fail(source_port != NULL, -EINVAL);
+ spa_return_val_if_fail(destination_port != NULL, -EINVAL);
+
+ pw_log_info("%p: disconnect %s %s", client, source_port, destination_port);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ src = find_port_by_name(c, source_port);
+ dst = find_port_by_name(c, destination_port);
+
+ pw_log_debug("%p: %d %d", client, src->id, dst->id);
+
+ if (src == NULL || dst == NULL ||
+ !(src->port.flags & JackPortIsOutput) ||
+ !(dst->port.flags & JackPortIsInput)) {
+ res = -EINVAL;
+ goto exit;
+ }
+
+ if ((res = check_connect(c, src, dst)) != 1)
+ goto exit;
+
+ if ((l = find_link(c, src->id, dst->id)) == NULL) {
+ res = -ENOENT;
+ goto exit;
+ }
+
+ pw_registry_destroy(c->registry, l->id);
+
+ res = do_sync(c);
+
+ exit:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return -res;
+}
+
+SPA_EXPORT
+int jack_port_disconnect (jack_client_t *client, jack_port_t *port)
+{
+ struct client *c = (struct client *) client;
+ struct object *o = (struct object *) port;
+ struct object *l;
+ int res;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+ spa_return_val_if_fail(o != NULL, -EINVAL);
+
+ pw_log_debug("%p: disconnect %p", client, port);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ spa_list_for_each(l, &c->context.objects, link) {
+ if (l->type != INTERFACE_Link || l->removed)
+ continue;
+ if (l->port_link.src_serial == o->serial ||
+ l->port_link.dst_serial == o->serial) {
+ pw_registry_destroy(c->registry, l->id);
+ }
+ }
+ res = do_sync(c);
+
+ pw_thread_loop_unlock(c->context.loop);
+
+ return -res;
+}
+
+SPA_EXPORT
+int jack_port_name_size(void)
+{
+ return REAL_JACK_PORT_NAME_SIZE+1;
+}
+
+SPA_EXPORT
+int jack_port_type_size(void)
+{
+ return JACK_PORT_TYPE_SIZE+1;
+}
+
+SPA_EXPORT
+size_t jack_port_type_get_buffer_size (jack_client_t *client, const char *port_type)
+{
+ spa_return_val_if_fail(client != NULL, 0);
+ spa_return_val_if_fail(port_type != NULL, 0);
+
+ if (spa_streq(JACK_DEFAULT_AUDIO_TYPE, port_type))
+ return jack_get_buffer_size(client) * sizeof(float);
+ else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type))
+ return MAX_BUFFER_FRAMES * sizeof(float);
+ else if (spa_streq(JACK_DEFAULT_VIDEO_TYPE, port_type))
+ return 320 * 240 * 4 * sizeof(float);
+ else
+ return 0;
+}
+
+SPA_EXPORT
+void jack_port_set_latency (jack_port_t *port, jack_nframes_t frames)
+{
+ struct object *o = (struct object *) port;
+ struct client *c;
+ jack_latency_range_t range = { frames, frames };
+
+ spa_return_if_fail(o != NULL);
+ c = o->client;
+
+ pw_log_debug("%p: %s set latency %d", c, o->port.name, frames);
+
+ if (o->port.flags & JackPortIsOutput) {
+ jack_port_set_latency_range(port, JackCaptureLatency, &range);
+ }
+ if (o->port.flags & JackPortIsInput) {
+ jack_port_set_latency_range(port, JackPlaybackLatency, &range);
+ }
+}
+
+SPA_EXPORT
+void jack_port_get_latency_range (jack_port_t *port, jack_latency_callback_mode_t mode, jack_latency_range_t *range)
+{
+ struct object *o = (struct object *) port;
+ struct client *c;
+ jack_nframes_t nframes, rate;
+ int direction;
+ struct spa_latency_info *info;
+
+ spa_return_if_fail(o != NULL);
+ if (o->type != INTERFACE_Port || o->client == NULL)
+ return;
+ c = o->client;
+
+ if (mode == JackCaptureLatency)
+ direction = SPA_DIRECTION_OUTPUT;
+ else
+ direction = SPA_DIRECTION_INPUT;
+
+ nframes = jack_get_buffer_size((jack_client_t*)c);
+ rate = jack_get_sample_rate((jack_client_t*)c);
+ info = &o->port.latency[direction];
+
+ range->min = (info->min_quantum * nframes) +
+ info->min_rate + (info->min_ns * rate) / SPA_NSEC_PER_SEC;
+ range->max = (info->max_quantum * nframes) +
+ info->max_rate + (info->max_ns * rate) / SPA_NSEC_PER_SEC;
+
+ pw_log_debug("%p: %s get %d latency range %d %d", c, o->port.name,
+ mode, range->min, range->max);
+}
+
+static int
+do_port_update_latency(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct port *p = user_data;
+ port_update_latency(p);
+ return 0;
+}
+
+SPA_EXPORT
+void jack_port_set_latency_range (jack_port_t *port, jack_latency_callback_mode_t mode, jack_latency_range_t *range)
+{
+ struct object *o = (struct object *) port;
+ struct client *c;
+ enum spa_direction direction;
+ struct spa_latency_info *current, latency;
+ jack_nframes_t nframes;
+ struct port *p;
+
+ spa_return_if_fail(o != NULL);
+ if (o->type != INTERFACE_Port || o->client == NULL)
+ return;
+ c = o->client;
+
+ if (mode == JackCaptureLatency)
+ direction = SPA_DIRECTION_OUTPUT;
+ else
+ direction = SPA_DIRECTION_INPUT;
+
+ pw_log_info("%p: %s set %d latency range %d %d", c, o->port.name, mode, range->min, range->max);
+
+ latency = SPA_LATENCY_INFO(direction);
+
+ nframes = jack_get_buffer_size((jack_client_t*)c);
+
+ latency.min_rate = range->min;
+ if (latency.min_rate >= nframes) {
+ latency.min_quantum = latency.min_rate / nframes;
+ latency.min_rate %= nframes;
+ }
+
+ latency.max_rate = range->max;
+ if (latency.max_rate >= nframes) {
+ latency.max_quantum = latency.max_rate / nframes;
+ latency.max_rate %= nframes;
+ }
+
+ current = &o->port.latency[direction];
+
+ if ((p = o->port.port) == NULL)
+ return;
+ if (spa_latency_info_compare(current, &latency) == 0)
+ return;
+
+ pw_log_info("%p: %s update %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, c,
+ o->port.name,
+ latency.direction == SPA_DIRECTION_INPUT ? "playback" : "capture",
+ latency.min_quantum, latency.max_quantum,
+ latency.min_rate, latency.max_rate,
+ latency.min_ns, latency.max_ns);
+
+ *current = latency;
+
+ pw_loop_invoke(c->context.l, do_port_update_latency, 0,
+ NULL, 0, false, p);
+}
+
+static int
+do_recompute_latencies(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct client *c = user_data;
+ pw_log_debug("start");
+ recompute_latencies(c);
+ pw_log_debug("stop");
+ return 0;
+}
+
+SPA_EXPORT
+int jack_recompute_total_latencies (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ pw_loop_invoke(c->context.l, do_recompute_latencies, 0,
+ NULL, 0, false, c);
+ return 0;
+}
+
+static jack_nframes_t port_get_latency (jack_port_t *port)
+{
+ struct object *o = (struct object *) port;
+ jack_latency_range_t range = { 0, 0 };
+
+ spa_return_val_if_fail(o != NULL, 0);
+
+ if (o->port.flags & JackPortIsOutput) {
+ jack_port_get_latency_range(port, JackCaptureLatency, &range);
+ }
+ if (o->port.flags & JackPortIsInput) {
+ jack_port_get_latency_range(port, JackPlaybackLatency, &range);
+ }
+ return (range.min + range.max) / 2;
+}
+
+SPA_EXPORT
+jack_nframes_t jack_port_get_latency (jack_port_t *port)
+{
+ return port_get_latency(port);
+}
+
+SPA_EXPORT
+jack_nframes_t jack_port_get_total_latency (jack_client_t *client,
+ jack_port_t *port)
+{
+ return port_get_latency(port);
+}
+
+SPA_EXPORT
+int jack_recompute_total_latency (jack_client_t *client, jack_port_t* port)
+{
+ pw_log_warn("%p: not implemented %p", client, port);
+ return 0;
+}
+
+static int port_compare_func(const void *v1, const void *v2)
+{
+ const struct object *const*o1 = v1, *const*o2 = v2;
+ struct client *c = (*o1)->client;
+ int res;
+ bool is_cap1, is_cap2, is_def1 = false, is_def2 = false;
+
+ is_cap1 = ((*o1)->port.flags & JackPortIsOutput) == JackPortIsOutput &&
+ !(*o1)->port.is_monitor;
+ is_cap2 = ((*o2)->port.flags & JackPortIsOutput) == JackPortIsOutput &&
+ !(*o2)->port.is_monitor;
+
+ if (c->metadata) {
+ struct object *ot1, *ot2;
+
+ ot1 = (*o1)->port.node;
+
+ if (is_cap1)
+ is_def1 = ot1 != NULL && spa_streq(ot1->node.node_name,
+ c->metadata->default_audio_source);
+ else if (!is_cap1)
+ is_def1 = ot1 != NULL && spa_streq(ot1->node.node_name,
+ c->metadata->default_audio_sink);
+ ot2 = (*o2)->port.node;
+
+ if (is_cap2)
+ is_def2 = ot2 != NULL && spa_streq(ot2->node.node_name,
+ c->metadata->default_audio_source);
+ else if (!is_cap2)
+ is_def2 = ot2 != NULL && spa_streq(ot2->node.node_name,
+ c->metadata->default_audio_sink);
+ }
+ if ((*o1)->port.type_id != (*o2)->port.type_id)
+ res = (*o1)->port.type_id - (*o2)->port.type_id;
+ else if ((is_cap1 || is_cap2) && is_cap1 != is_cap2)
+ res = is_cap2 - is_cap1;
+ else if ((is_def1 || is_def2) && is_def1 != is_def2)
+ res = is_def2 - is_def1;
+ else if ((*o1)->port.priority != (*o2)->port.priority)
+ res = (*o2)->port.priority - (*o1)->port.priority;
+ else if ((res = (*o1)->port.node_id - (*o2)->port.node_id) == 0) {
+ if ((*o1)->port.is_monitor != (*o2)->port.is_monitor)
+ res = (*o1)->port.is_monitor - (*o2)->port.is_monitor;
+ if (res == 0)
+ res = (*o1)->port.system_id - (*o2)->port.system_id;
+ if (res == 0)
+ res = (*o1)->serial - (*o2)->serial;
+ }
+ pw_log_debug("port %s<->%s type:%d<->%d def:%d<->%d prio:%d<->%d id:%d<->%d res:%d",
+ (*o1)->port.name, (*o2)->port.name,
+ (*o1)->port.type_id, (*o2)->port.type_id,
+ is_def1, is_def2,
+ (*o1)->port.priority, (*o2)->port.priority,
+ (*o1)->serial, (*o2)->serial, res);
+ return res;
+}
+
+SPA_EXPORT
+const char ** jack_get_ports (jack_client_t *client,
+ const char *port_name_pattern,
+ const char *type_name_pattern,
+ unsigned long flags)
+{
+ struct client *c = (struct client *) client;
+ const char **res;
+ struct object *o;
+ struct pw_array tmp;
+ const char *str;
+ uint32_t i, count;
+ int r;
+ regex_t port_regex, type_regex;
+
+ spa_return_val_if_fail(c != NULL, NULL);
+
+ str = getenv("PIPEWIRE_NODE");
+
+ if (port_name_pattern && port_name_pattern[0]) {
+ if ((r = regcomp(&port_regex, port_name_pattern, REG_EXTENDED | REG_NOSUB)) != 0) {
+ pw_log_error("cant compile regex %s: %d", port_name_pattern, r);
+ return NULL;
+ }
+ }
+ if (type_name_pattern && type_name_pattern[0]) {
+ if ((r = regcomp(&type_regex, type_name_pattern, REG_EXTENDED | REG_NOSUB)) != 0) {
+ pw_log_error("cant compile regex %s: %d", type_name_pattern, r);
+ return NULL;
+ }
+ }
+
+ pw_log_debug("%p: ports target:%s name:\"%s\" type:\"%s\" flags:%08lx", c, str,
+ port_name_pattern, type_name_pattern, flags);
+
+ pthread_mutex_lock(&c->context.lock);
+ pw_array_init(&tmp, sizeof(void*) * 32);
+ count = 0;
+
+ spa_list_for_each(o, &c->context.objects, link) {
+ if (o->type != INTERFACE_Port || o->removed)
+ 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)
+ continue;
+ if (!SPA_FLAG_IS_SET(o->port.flags, flags))
+ continue;
+ if (str != NULL && o->port.node != NULL) {
+ if (!spa_strstartswith(o->port.name, str) &&
+ o->port.node->serial != atoll(str))
+ continue;
+ }
+
+ if (port_name_pattern && port_name_pattern[0]) {
+ bool match;
+ match = regexec(&port_regex, o->port.name, 0, NULL, 0) == 0;
+ if (!match && is_port_default(c, o))
+ match = regexec(&port_regex, o->port.system, 0, NULL, 0) == 0;
+ if (!match)
+ continue;
+ }
+ if (type_name_pattern && type_name_pattern[0]) {
+ if (regexec(&type_regex, type_to_string(o->port.type_id),
+ 0, NULL, 0) == REG_NOMATCH)
+ continue;
+ }
+ pw_log_debug("%p: port \"%s\" prio:%d matches (%d)",
+ c, o->port.name, o->port.priority, count);
+
+ pw_array_add_ptr(&tmp, o);
+ count++;
+ }
+ pthread_mutex_unlock(&c->context.lock);
+
+ if (count > 0) {
+ qsort(tmp.data, count, sizeof(struct object *), port_compare_func);
+ pw_array_add_ptr(&tmp, NULL);
+ res = tmp.data;
+ for (i = 0; i < count; i++)
+ res[i] = port_name((struct object*)res[i]);
+ } else {
+ pw_array_clear(&tmp);
+ res = NULL;
+ }
+
+ if (port_name_pattern && port_name_pattern[0])
+ regfree(&port_regex);
+ if (type_name_pattern && type_name_pattern[0])
+ regfree(&type_regex);
+
+ return res;
+}
+
+SPA_EXPORT
+jack_port_t * jack_port_by_name (jack_client_t *client, const char *port_name)
+{
+ struct client *c = (struct client *) client;
+ struct object *res;
+
+ spa_return_val_if_fail(c != NULL, NULL);
+
+ pthread_mutex_lock(&c->context.lock);
+ res = find_port_by_name(c, port_name);
+ pthread_mutex_unlock(&c->context.lock);
+
+ if (res == NULL)
+ pw_log_info("%p: port \"%s\" not found", c, port_name);
+
+ return (jack_port_t *)res;
+}
+
+SPA_EXPORT
+jack_port_t * jack_port_by_id (jack_client_t *client,
+ jack_port_id_t port_id)
+{
+ struct client *c = (struct client *) client;
+ struct object *res = NULL;
+
+ spa_return_val_if_fail(c != NULL, NULL);
+
+ pthread_mutex_lock(&c->context.lock);
+ res = find_by_serial(c, port_id);
+ if (res && res->type != INTERFACE_Port)
+ res = NULL;
+ pw_log_debug("%p: port %d -> %p", c, port_id, res);
+ pthread_mutex_unlock(&c->context.lock);
+
+ if (res == NULL)
+ pw_log_info("%p: port %d not found", c, port_id);
+
+ return (jack_port_t *)res;
+}
+
+SPA_EXPORT
+jack_nframes_t jack_frames_since_cycle_start (const jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ struct spa_io_position *pos;
+ struct timespec ts;
+ uint64_t diff;
+
+ spa_return_val_if_fail(c != NULL, 0);
+
+ if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
+ return 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ diff = SPA_TIMESPEC_TO_NSEC(&ts) - pos->clock.nsec;
+ return (jack_nframes_t) floor(((double)c->sample_rate * diff) / SPA_NSEC_PER_SEC);
+}
+
+SPA_EXPORT
+jack_nframes_t jack_frame_time (const jack_client_t *client)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return jack_time_to_frames(client, SPA_TIMESPEC_TO_USEC(&ts));
+}
+
+SPA_EXPORT
+jack_nframes_t jack_last_frame_time (const jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ struct spa_io_position *pos;
+
+ spa_return_val_if_fail(c != NULL, 0);
+
+ if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
+ return 0;
+
+ return pos->clock.position;
+}
+
+SPA_EXPORT
+int jack_get_cycle_times(const jack_client_t *client,
+ jack_nframes_t *current_frames,
+ jack_time_t *current_usecs,
+ jack_time_t *next_usecs,
+ float *period_usecs)
+{
+ struct client *c = (struct client *) client;
+ struct spa_io_position *pos;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
+ return -EIO;
+
+ *current_frames = pos->clock.position;
+ *current_usecs = pos->clock.nsec / SPA_NSEC_PER_USEC;
+ *period_usecs = pos->clock.duration * (float)SPA_USEC_PER_SEC / (c->sample_rate * pos->clock.rate_diff);
+ *next_usecs = pos->clock.next_nsec / SPA_NSEC_PER_USEC;
+
+ pw_log_trace("%p: %d %"PRIu64" %"PRIu64" %f", c, *current_frames,
+ *current_usecs, *next_usecs, *period_usecs);
+ return 0;
+}
+
+SPA_EXPORT
+jack_time_t jack_frames_to_time(const jack_client_t *client, jack_nframes_t frames)
+{
+ struct client *c = (struct client *) client;
+ struct spa_io_position *pos;
+ double df;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
+ return 0;
+
+ df = (frames - pos->clock.position) * (double)SPA_NSEC_PER_SEC / c->sample_rate;
+ return (pos->clock.nsec + (int64_t)rint(df)) / SPA_NSEC_PER_USEC;
+}
+
+SPA_EXPORT
+jack_nframes_t jack_time_to_frames(const jack_client_t *client, jack_time_t usecs)
+{
+ struct client *c = (struct client *) client;
+ struct spa_io_position *pos;
+ double du;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (SPA_UNLIKELY((pos = c->rt.position) == NULL))
+ return 0;
+
+ du = (usecs - pos->clock.nsec/SPA_NSEC_PER_USEC) * (double)c->sample_rate / SPA_USEC_PER_SEC;
+ return pos->clock.position + (int32_t)rint(du);
+}
+
+SPA_EXPORT
+jack_time_t jack_get_time(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return SPA_TIMESPEC_TO_USEC(&ts);
+}
+
+SPA_EXPORT
+void default_jack_error_callback(const char *desc)
+{
+ pw_log_error("pw jack error: %s",desc);
+}
+
+SPA_EXPORT
+void silent_jack_error_callback(const char *desc)
+{
+}
+
+SPA_EXPORT
+void (*jack_error_callback)(const char *msg);
+
+SPA_EXPORT
+void jack_set_error_function (void (*func)(const char *))
+{
+ jack_error_callback = (func == NULL) ? &default_jack_error_callback : func;
+}
+
+SPA_EXPORT
+void default_jack_info_callback(const char *desc)
+{
+ pw_log_info("pw jack info: %s", desc);
+}
+
+SPA_EXPORT
+void silent_jack_info_callback(const char *desc)
+{
+}
+
+SPA_EXPORT
+void (*jack_info_callback)(const char *msg);
+
+
+SPA_EXPORT
+void jack_set_info_function (void (*func)(const char *))
+{
+ jack_info_callback = (func == NULL) ? &default_jack_info_callback : func;
+}
+
+SPA_EXPORT
+void jack_free(void* ptr)
+{
+ free(ptr);
+}
+
+SPA_EXPORT
+int jack_release_timebase (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ struct pw_node_activation *a;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if ((a = c->driver_activation) == NULL)
+ return -EIO;
+
+ if (!ATOMIC_CAS(a->segment_owner[0], c->node_id, 0))
+ return -EINVAL;
+
+ c->timebase_callback = NULL;
+ c->timebase_arg = NULL;
+ c->activation->pending_new_pos = false;
+
+ return 0;
+}
+
+SPA_EXPORT
+int jack_set_sync_callback (jack_client_t *client,
+ JackSyncCallback sync_callback,
+ void *arg)
+{
+ int res = 0;
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ c->sync_callback = sync_callback;
+ c->sync_arg = arg;
+
+ if ((res = do_activate(c)) < 0)
+ goto done;
+
+ c->activation->pending_sync = true;
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_set_sync_timeout (jack_client_t *client,
+ jack_time_t timeout)
+{
+ int res = 0;
+ struct client *c = (struct client *) client;
+ struct pw_node_activation *a;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ if ((a = c->activation) == NULL)
+ res = -EIO;
+ else
+ a->sync_timeout = timeout;
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_set_timebase_callback (jack_client_t *client,
+ int conditional,
+ JackTimebaseCallback timebase_callback,
+ void *arg)
+{
+ int res = 0;
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+ spa_return_val_if_fail(timebase_callback != NULL, -EINVAL);
+
+ pw_thread_loop_lock(c->context.loop);
+
+ c->timebase_callback = timebase_callback;
+ c->timebase_arg = arg;
+ c->timeowner_conditional = conditional;
+ install_timeowner(c);
+
+ pw_log_debug("%p: timebase set id:%u", c, c->node_id);
+
+ if ((res = do_activate(c)) < 0)
+ goto done;
+
+ c->activation->pending_new_pos = true;
+done:
+ pw_thread_loop_unlock(c->context.loop);
+
+ return res;
+}
+
+SPA_EXPORT
+int jack_transport_locate (jack_client_t *client,
+ jack_nframes_t frame)
+{
+ jack_position_t pos;
+ pos.frame = frame;
+ pos.valid = (jack_position_bits_t)0;
+ return jack_transport_reposition(client, &pos);
+}
+
+SPA_EXPORT
+jack_transport_state_t jack_transport_query (const jack_client_t *client,
+ jack_position_t *pos)
+{
+ struct client *c = (struct client *) client;
+ struct pw_node_activation *a;
+ jack_transport_state_t jack_state = JackTransportStopped;
+
+ spa_return_val_if_fail(c != NULL, JackTransportStopped);
+
+ if (SPA_LIKELY((a = c->rt.driver_activation) != NULL)) {
+ jack_state = position_to_jack(a, pos);
+ } else if ((a = c->driver_activation) != NULL) {
+ jack_state = position_to_jack(a, pos);
+ } else if (pos != NULL) {
+ memset(pos, 0, sizeof(jack_position_t));
+ pos->frame_rate = jack_get_sample_rate((jack_client_t*)client);
+ }
+ return jack_state;
+}
+
+SPA_EXPORT
+jack_nframes_t jack_get_current_transport_frame (const jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ struct pw_node_activation *a;
+ struct spa_io_position *pos;
+ struct spa_io_segment *seg;
+ uint64_t running;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (SPA_UNLIKELY((a = c->rt.driver_activation) == NULL))
+ return -EIO;
+
+ pos = &a->position;
+ running = pos->clock.position - pos->offset;
+
+ if (pos->state == SPA_IO_POSITION_STATE_RUNNING) {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ uint64_t nsecs = SPA_TIMESPEC_TO_NSEC(&ts) - pos->clock.nsec;
+ running += (uint64_t)floor((((double) c->sample_rate) / SPA_NSEC_PER_SEC) * nsecs);
+ }
+ seg = &pos->segments[0];
+
+ return (running - seg->start) * seg->rate + seg->position;
+}
+
+SPA_EXPORT
+int jack_transport_reposition (jack_client_t *client,
+ const jack_position_t *pos)
+{
+ struct client *c = (struct client *) client;
+ struct pw_node_activation *a, *na;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ a = c->rt.driver_activation;
+ na = c->activation;
+ if (!a || !na)
+ return -EIO;
+
+ if (pos->valid & ~(JackPositionBBT|JackPositionTimecode))
+ return -EINVAL;
+
+ pw_log_debug("frame:%u", pos->frame);
+ spa_zero(na->reposition);
+ na->reposition.flags = 0;
+ na->reposition.start = 0;
+ na->reposition.duration = 0;
+ na->reposition.position = pos->frame;
+ na->reposition.rate = 1.0;
+ ATOMIC_STORE(a->reposition_owner, c->node_id);
+
+ return 0;
+}
+
+static void update_command(struct client *c, uint32_t command)
+{
+ struct pw_node_activation *a = c->rt.driver_activation;
+ if (!a)
+ return;
+ ATOMIC_STORE(a->command, command);
+}
+
+SPA_EXPORT
+void jack_transport_start (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ spa_return_if_fail(c != NULL);
+ update_command(c, PW_NODE_ACTIVATION_COMMAND_START);
+}
+
+SPA_EXPORT
+void jack_transport_stop (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ spa_return_if_fail(c != NULL);
+ update_command(c, PW_NODE_ACTIVATION_COMMAND_STOP);
+}
+
+SPA_EXPORT
+void jack_get_transport_info (jack_client_t *client,
+ jack_transport_info_t *tinfo)
+{
+ pw_log_error("%p: deprecated", client);
+ if (tinfo)
+ memset(tinfo, 0, sizeof(jack_transport_info_t));
+}
+
+SPA_EXPORT
+void jack_set_transport_info (jack_client_t *client,
+ jack_transport_info_t *tinfo)
+{
+ pw_log_error("%p: deprecated", client);
+ if (tinfo)
+ memset(tinfo, 0, sizeof(jack_transport_info_t));
+}
+
+SPA_EXPORT
+int jack_set_session_callback (jack_client_t *client,
+ JackSessionCallback session_callback,
+ void *arg)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, -EINVAL);
+
+ if (c->active) {
+ pw_log_error("%p: can't set callback on active client", c);
+ return -EIO;
+ }
+ pw_log_warn("%p: not implemented", client);
+ return -ENOTSUP;
+}
+
+SPA_EXPORT
+int jack_session_reply (jack_client_t *client,
+ jack_session_event_t *event)
+{
+ pw_log_warn("%p: not implemented", client);
+ return -ENOTSUP;
+}
+
+
+SPA_EXPORT
+void jack_session_event_free (jack_session_event_t *event)
+{
+ if (event) {
+ free((void *)event->session_dir);
+ free((void *)event->client_uuid);
+ free(event->command_line);
+ free(event);
+ }
+}
+
+SPA_EXPORT
+char *jack_client_get_uuid (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_val_if_fail(c != NULL, NULL);
+
+ return spa_aprintf("%"PRIu64, client_make_uuid(c->serial, false));
+}
+
+SPA_EXPORT
+jack_session_command_t *jack_session_notify (
+ jack_client_t* client,
+ const char *target,
+ jack_session_event_type_t type,
+ const char *path)
+{
+ struct client *c = (struct client *) client;
+ jack_session_command_t *cmds;
+ spa_return_val_if_fail(c != NULL, NULL);
+ pw_log_warn("not implemented");
+ cmds = calloc(1, sizeof(jack_session_command_t));
+ return cmds;
+}
+
+SPA_EXPORT
+void jack_session_commands_free (jack_session_command_t *cmds)
+{
+ int i;
+ if (cmds == NULL)
+ return;
+
+ for (i = 0; cmds[i].uuid != NULL; i++) {
+ free((char*)cmds[i].client_name);
+ free((char*)cmds[i].command);
+ free((char*)cmds[i].uuid);
+ }
+ free(cmds);
+}
+
+SPA_EXPORT
+int jack_reserve_client_name (jack_client_t *client,
+ const char *name,
+ const char *uuid)
+{
+ struct client *c = (struct client *) client;
+ spa_return_val_if_fail(c != NULL, -1);
+ pw_log_warn("not implemented");
+ return 0;
+}
+
+SPA_EXPORT
+int jack_client_has_session_callback (jack_client_t *client, const char *client_name)
+{
+ struct client *c = (struct client *) client;
+ spa_return_val_if_fail(c != NULL, -1);
+ return 0;
+}
+
+
+SPA_EXPORT
+int jack_client_real_time_priority (jack_client_t * client)
+{
+ return jack_client_max_real_time_priority(client) - 5;
+}
+
+SPA_EXPORT
+int jack_client_max_real_time_priority (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ int min, max;
+
+ spa_return_val_if_fail(c != NULL, -1);
+
+ spa_thread_utils_get_rt_range(&c->context.thread_utils, NULL, &min, &max);
+ return SPA_MIN(max, c->rt_max) - 1;
+}
+
+SPA_EXPORT
+int jack_acquire_real_time_scheduling (jack_native_thread_t thread, int priority)
+{
+ struct spa_thread *t = (struct spa_thread*)thread;
+ pw_log_info("acquire %p", t);
+ spa_return_val_if_fail(globals.thread_utils != NULL, -1);
+ spa_return_val_if_fail(t != NULL, -1);
+ return spa_thread_utils_acquire_rt(globals.thread_utils, t, priority);
+}
+
+SPA_EXPORT
+int jack_drop_real_time_scheduling (jack_native_thread_t thread)
+{
+ struct spa_thread *t = (struct spa_thread*)thread;
+ pw_log_info("drop %p", t);
+ spa_return_val_if_fail(globals.thread_utils != NULL, -1);
+ spa_return_val_if_fail(t != NULL, -1);
+ return spa_thread_utils_drop_rt(globals.thread_utils, t);
+}
+
+/**
+ * Create a thread for JACK or one of its clients. The thread is
+ * created executing @a start_routine with @a arg as its sole
+ * argument.
+ *
+ * @param client the JACK client for whom the thread is being created. May be
+ * NULL if the client is being created within the JACK server.
+ * @param thread place to return POSIX thread ID.
+ * @param priority thread priority, if realtime.
+ * @param realtime true for the thread to use realtime scheduling. On
+ * some systems that may require special privileges.
+ * @param start_routine function the thread calls when it starts.
+ * @param arg parameter passed to the @a start_routine.
+ *
+ * @returns 0, if successful; otherwise some error number.
+ */
+SPA_EXPORT
+int jack_client_create_thread (jack_client_t* client,
+ jack_native_thread_t *thread,
+ int priority,
+ int realtime, /* boolean */
+ void *(*start_routine)(void*),
+ void *arg)
+{
+ struct client *c = (struct client *) client;
+ int res = 0;
+ struct spa_thread *thr;
+
+ spa_return_val_if_fail(client != NULL, -EINVAL);
+ spa_return_val_if_fail(thread != NULL, -EINVAL);
+ spa_return_val_if_fail(start_routine != NULL, -EINVAL);
+
+ pw_log_info("client %p: create thread rt:%d prio:%d", client, realtime, priority);
+
+ thr = spa_thread_utils_create(&c->context.thread_utils, NULL, start_routine, arg);
+ if (thr == NULL)
+ res = -errno;
+ *thread = (pthread_t)thr;
+
+ if (res != 0) {
+ pw_log_warn("client %p: create RT thread failed: %s",
+ client, strerror(res));
+ } else if (realtime) {
+ /* Try to acquire RT scheduling, we don't fail here but the
+ * function will emit a warning. Real JACK fails here. */
+ jack_acquire_real_time_scheduling(*thread, priority);
+ }
+ return res;
+}
+
+SPA_EXPORT
+int jack_client_stop_thread(jack_client_t* client, jack_native_thread_t thread)
+{
+ struct client *c = (struct client *) client;
+ void* status;
+
+ if (thread == (jack_native_thread_t)NULL)
+ return -EINVAL;
+
+ spa_return_val_if_fail(client != NULL, -EINVAL);
+
+ pw_log_debug("join thread %lu", thread);
+ spa_thread_utils_join(&c->context.thread_utils, (struct spa_thread*)thread, &status);
+ pw_log_debug("stopped thread %lu", thread);
+ return 0;
+}
+
+SPA_EXPORT
+int jack_client_kill_thread(jack_client_t* client, jack_native_thread_t thread)
+{
+ struct client *c = (struct client *) client;
+ void* status;
+
+ if (thread == (jack_native_thread_t)NULL)
+ return -EINVAL;
+
+ spa_return_val_if_fail(client != NULL, -EINVAL);
+
+ pw_log_debug("cancel thread %lu", thread);
+ pthread_cancel(thread);
+ pw_log_debug("join thread %lu", thread);
+ spa_thread_utils_join(&c->context.thread_utils, (struct spa_thread*)thread, &status);
+ pw_log_debug("stopped thread %lu", thread);
+ return 0;
+}
+
+SPA_EXPORT
+void jack_set_thread_creator (jack_thread_creator_t creator)
+{
+ globals.creator = creator;
+}
+
+static inline uint8_t * midi_event_data (void* port_buffer,
+ const struct midi_event* event)
+{
+ if (SPA_LIKELY(event->size <= MIDI_INLINE_MAX))
+ return (uint8_t *)event->inline_data;
+ else
+ return SPA_PTROFF(port_buffer, event->byte_offset, uint8_t);
+}
+
+SPA_EXPORT
+uint32_t jack_midi_get_event_count(void* port_buffer)
+{
+ struct midi_buffer *mb = port_buffer;
+ if (mb == NULL || mb->magic != MIDI_BUFFER_MAGIC)
+ return 0;
+ return mb->event_count;
+}
+
+SPA_EXPORT
+int jack_midi_event_get(jack_midi_event_t *event,
+ void *port_buffer,
+ uint32_t event_index)
+{
+ struct midi_buffer *mb = port_buffer;
+ struct midi_event *ev = SPA_PTROFF(mb, sizeof(*mb), struct midi_event);
+ spa_return_val_if_fail(mb != NULL, -EINVAL);
+ spa_return_val_if_fail(ev != NULL, -EINVAL);
+ if (event_index >= mb->event_count)
+ return -ENOBUFS;
+ ev += event_index;
+ event->time = ev->time;
+ event->size = ev->size;
+ event->buffer = midi_event_data (port_buffer, ev);
+ return 0;
+}
+
+SPA_EXPORT
+void jack_midi_clear_buffer(void *port_buffer)
+{
+ struct midi_buffer *mb = port_buffer;
+ spa_return_if_fail(mb != NULL);
+ mb->event_count = 0;
+ mb->write_pos = 0;
+ mb->lost_events = 0;
+}
+
+SPA_EXPORT
+void jack_midi_reset_buffer(void *port_buffer)
+{
+ jack_midi_clear_buffer(port_buffer);
+}
+
+SPA_EXPORT
+size_t jack_midi_max_event_size(void* port_buffer)
+{
+ struct midi_buffer *mb = port_buffer;
+ size_t buffer_size;
+
+ spa_return_val_if_fail(mb != NULL, 0);
+
+ buffer_size = mb->buffer_size;
+
+ /* (event_count + 1) below accounts for jack_midi_port_internal_event_t
+ * which would be needed to store the next event */
+ size_t used_size = sizeof(struct midi_buffer)
+ + mb->write_pos
+ + ((mb->event_count + 1)
+ * sizeof(struct midi_event));
+
+ if (SPA_UNLIKELY(used_size > buffer_size)) {
+ return 0;
+ } else if (SPA_LIKELY((buffer_size - used_size) < MIDI_INLINE_MAX)) {
+ return MIDI_INLINE_MAX;
+ } else {
+ return buffer_size - used_size;
+ }
+}
+
+SPA_EXPORT
+jack_midi_data_t* jack_midi_event_reserve(void *port_buffer,
+ jack_nframes_t time,
+ size_t data_size)
+{
+ struct midi_buffer *mb = port_buffer;
+ struct midi_event *events = SPA_PTROFF(mb, sizeof(*mb), struct midi_event);
+ size_t buffer_size;
+
+ spa_return_val_if_fail(mb != NULL, NULL);
+
+ buffer_size = mb->buffer_size;
+
+ if (SPA_UNLIKELY(time >= mb->nframes)) {
+ pw_log_warn("midi %p: time:%d frames:%d", port_buffer, time, mb->nframes);
+ goto failed;
+ }
+
+ if (SPA_UNLIKELY(mb->event_count > 0 && time < events[mb->event_count - 1].time)) {
+ pw_log_warn("midi %p: time:%d ev:%d", port_buffer, time, mb->event_count);
+ goto failed;
+ }
+
+ /* Check if data_size is >0 and there is enough space in the buffer for the event. */
+ if (SPA_UNLIKELY(data_size <= 0)) {
+ pw_log_warn("midi %p: data_size:%zd", port_buffer, data_size);
+ goto failed; // return NULL?
+ } else if (SPA_UNLIKELY(jack_midi_max_event_size (port_buffer) < data_size)) {
+ pw_log_warn("midi %p: event too large: data_size:%zd", port_buffer, data_size);
+ goto failed;
+ } else {
+ struct midi_event *ev = &events[mb->event_count];
+ uint8_t *res;
+
+ ev->time = time;
+ ev->size = data_size;
+ if (SPA_LIKELY(data_size <= MIDI_INLINE_MAX)) {
+ res = ev->inline_data;
+ } else {
+ mb->write_pos += data_size;
+ ev->byte_offset = buffer_size - 1 - mb->write_pos;
+ res = SPA_PTROFF(mb, ev->byte_offset, uint8_t);
+ }
+ mb->event_count += 1;
+ return res;
+ }
+failed:
+ mb->lost_events++;
+ return NULL;
+}
+
+SPA_EXPORT
+int jack_midi_event_write(void *port_buffer,
+ jack_nframes_t time,
+ const jack_midi_data_t *data,
+ size_t data_size)
+{
+ jack_midi_data_t *retbuf = jack_midi_event_reserve (port_buffer, time, data_size);
+ if (SPA_UNLIKELY(retbuf == NULL))
+ return -ENOBUFS;
+ memcpy (retbuf, data, data_size);
+ return 0;
+}
+
+SPA_EXPORT
+uint32_t jack_midi_get_lost_event_count(void *port_buffer)
+{
+ struct midi_buffer *mb = port_buffer;
+ spa_return_val_if_fail(mb != NULL, 0);
+ return mb->lost_events;
+}
+
+/** extensions */
+
+SPA_EXPORT
+int jack_get_video_image_size(jack_client_t *client, jack_image_size_t *size)
+{
+ struct client *c = (struct client *) client;
+ struct pw_node_activation *a;
+
+ spa_return_val_if_fail(c != NULL, 0);
+
+ a = c->rt.driver_activation;
+ if (SPA_UNLIKELY(a == NULL))
+ a = c->activation;
+ if (SPA_UNLIKELY(a == NULL))
+ return -EIO;
+
+ if (SPA_UNLIKELY(!(a->position.video.flags & SPA_IO_VIDEO_SIZE_VALID)))
+ return -EIO;
+
+ size->width = a->position.video.size.width;
+ size->height = a->position.video.size.height;
+ size->stride = a->position.video.stride;
+ size->flags = 0;
+ return size->stride * size->height;
+}
+
+
+static void reg(void) __attribute__ ((constructor));
+static void reg(void)
+{
+ pw_init(NULL, NULL);
+ PW_LOG_TOPIC_INIT(jack_log_topic);
+ pthread_mutex_init(&globals.lock, NULL);
+ pw_array_init(&globals.descriptions, 16);
+ spa_list_init(&globals.free_objects);
+}
diff --git a/pipewire-jack/src/pw-jack.in b/pipewire-jack/src/pw-jack.in
new file mode 100755
index 0000000..f3dba1d
--- /dev/null
+++ b/pipewire-jack/src/pw-jack.in
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+# This file is part of PipeWire.
+#
+# Copyright © 2020 Wim Taymans
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+
+DEFAULT_SAMPLERATE=48000
+
+while getopts 'hr:vs:p:' param ; do
+ case $param in
+ r)
+ PIPEWIRE_REMOTE="$OPTARG"
+ export PIPEWIRE_REMOTE
+ ;;
+ v)
+ if [ -z "$PIPEWIRE_DEBUG" ]; then
+ PIPEWIRE_DEBUG=3
+ else
+ PIPEWIRE_DEBUG=$(( PIPEWIRE_DEBUG + 1 ))
+ fi
+ export PIPEWIRE_DEBUG
+ ;;
+ s)
+ SAMPLERATE="$OPTARG"
+ ;;
+ p)
+ PERIOD="$OPTARG"
+ ;;
+ *)
+ echo "$0 - run JACK applications on PipeWire"
+ echo " "
+ echo "$0 [options] application [arguments]"
+ echo " "
+ echo "options:"
+ echo " -h show brief help"
+ echo " -r <remote> remote daemon name"
+ echo " -v verbose debug info"
+ echo " -s samplerate (default \"$DEFAULT_SAMPLERATE\")"
+ echo " -p period in samples"
+ exit 0
+ ;;
+ esac
+done
+
+shift $(( OPTIND - 1 ))
+
+if [ -n "$PERIOD" ]; then
+ if [ -n "$SAMPLERATE" ]; then
+ PIPEWIRE_QUANTUM="$PERIOD/$SAMPLERATE"
+ else
+ PIPEWIRE_QUANTUM="$PERIOD/$DEFAULT_SAMPLERATE"
+ fi
+ export PIPEWIRE_QUANTUM
+fi
+LD_LIBRARY_PATH='@LIBJACK_PATH@'"${LD_LIBRARY_PATH+":$LD_LIBRARY_PATH"}"
+export LD_LIBRARY_PATH
+
+exec "$@"
diff --git a/pipewire-jack/src/ringbuffer.c b/pipewire-jack/src/ringbuffer.c
new file mode 100644
index 0000000..887c7dc
--- /dev/null
+++ b/pipewire-jack/src/ringbuffer.c
@@ -0,0 +1,302 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <spa/utils/defs.h>
+
+#include <jack/ringbuffer.h>
+
+SPA_EXPORT
+jack_ringbuffer_t *jack_ringbuffer_create(size_t sz)
+{
+ size_t power_of_two;
+ jack_ringbuffer_t *rb;
+
+ rb = calloc(1, sizeof(jack_ringbuffer_t));
+ if (rb == NULL)
+ return NULL;
+
+ for (power_of_two = 1; 1u << power_of_two < sz; power_of_two++);
+
+ rb->size = 1 << power_of_two;
+ rb->size_mask = rb->size - 1;
+ if ((rb->buf = calloc(1, rb->size)) == NULL) {
+ free (rb);
+ return NULL;
+ }
+ rb->mlocked = 0;
+
+ return rb;
+}
+
+SPA_EXPORT
+void jack_ringbuffer_free(jack_ringbuffer_t *rb)
+{
+#ifdef USE_MLOCK
+ if (rb->mlocked)
+ munlock (rb->buf, rb->size);
+#endif /* USE_MLOCK */
+ free (rb->buf);
+ free (rb);
+}
+
+SPA_EXPORT
+void jack_ringbuffer_get_read_vector(const jack_ringbuffer_t *rb,
+ jack_ringbuffer_data_t *vec)
+{
+ size_t free_cnt;
+ size_t cnt2;
+ size_t w, r;
+
+ w = rb->write_ptr;
+ r = rb->read_ptr;
+
+ if (w > r)
+ free_cnt = w - r;
+ else
+ free_cnt = (w - r + rb->size) & rb->size_mask;
+
+ cnt2 = r + free_cnt;
+
+ if (cnt2 > rb->size) {
+ vec[0].buf = &(rb->buf[r]);
+ vec[0].len = rb->size - r;
+ vec[1].buf = rb->buf;
+ vec[1].len = cnt2 & rb->size_mask;
+ } else {
+ vec[0].buf = &(rb->buf[r]);
+ vec[0].len = free_cnt;
+ vec[1].len = 0;
+ }
+}
+
+SPA_EXPORT
+void jack_ringbuffer_get_write_vector(const jack_ringbuffer_t *rb,
+ jack_ringbuffer_data_t *vec)
+{
+ size_t free_cnt;
+ size_t cnt2;
+ size_t w, r;
+
+ w = rb->write_ptr;
+ r = rb->read_ptr;
+
+ if (w > r)
+ free_cnt = ((r - w + rb->size) & rb->size_mask) - 1;
+ else if (w < r)
+ free_cnt = (r - w) - 1;
+ else
+ free_cnt = rb->size - 1;
+
+ cnt2 = w + free_cnt;
+
+ if (cnt2 > rb->size) {
+ vec[0].buf = &(rb->buf[w]);
+ vec[0].len = rb->size - w;
+ vec[1].buf = rb->buf;
+ vec[1].len = cnt2 & rb->size_mask;
+ } else {
+ vec[0].buf = &(rb->buf[w]);
+ vec[0].len = free_cnt;
+ vec[1].len = 0;
+ }
+}
+
+SPA_EXPORT
+size_t jack_ringbuffer_read(jack_ringbuffer_t *rb, char *dest, size_t cnt)
+{
+ size_t free_cnt;
+ size_t cnt2;
+ size_t to_read;
+ size_t n1, n2;
+
+ if ((free_cnt = jack_ringbuffer_read_space (rb)) == 0)
+ return 0;
+
+ to_read = cnt > free_cnt ? free_cnt : cnt;
+
+ cnt2 = rb->read_ptr + to_read;
+
+ if (cnt2 > rb->size) {
+ n1 = rb->size - rb->read_ptr;
+ n2 = cnt2 & rb->size_mask;
+ } else {
+ n1 = to_read;
+ n2 = 0;
+ }
+
+ memcpy (dest, &(rb->buf[rb->read_ptr]), n1);
+ rb->read_ptr = (rb->read_ptr + n1) & rb->size_mask;
+ if (n2) {
+ memcpy (dest + n1, &(rb->buf[rb->read_ptr]), n2);
+ rb->read_ptr = (rb->read_ptr + n2) & rb->size_mask;
+ }
+ return to_read;
+}
+
+SPA_EXPORT
+size_t jack_ringbuffer_peek(jack_ringbuffer_t *rb, char *dest, size_t cnt)
+{
+ size_t free_cnt;
+ size_t cnt2;
+ size_t to_read;
+ size_t n1, n2;
+ size_t tmp_read_ptr;
+
+ tmp_read_ptr = rb->read_ptr;
+
+ if ((free_cnt = jack_ringbuffer_read_space (rb)) == 0)
+ return 0;
+
+ to_read = cnt > free_cnt ? free_cnt : cnt;
+
+ cnt2 = tmp_read_ptr + to_read;
+
+ if (cnt2 > rb->size) {
+ n1 = rb->size - tmp_read_ptr;
+ n2 = cnt2 & rb->size_mask;
+ } else {
+ n1 = to_read;
+ n2 = 0;
+ }
+
+ memcpy (dest, &(rb->buf[tmp_read_ptr]), n1);
+ tmp_read_ptr = (tmp_read_ptr + n1) & rb->size_mask;
+
+ if (n2)
+ memcpy (dest + n1, &(rb->buf[tmp_read_ptr]), n2);
+
+ return to_read;
+}
+
+SPA_EXPORT
+void jack_ringbuffer_read_advance(jack_ringbuffer_t *rb, size_t cnt)
+{
+ size_t tmp = (rb->read_ptr + cnt) & rb->size_mask;
+ rb->read_ptr = tmp;
+}
+
+SPA_EXPORT
+size_t jack_ringbuffer_read_space(const jack_ringbuffer_t *rb)
+{
+ size_t w, r;
+
+ w = rb->write_ptr;
+ r = rb->read_ptr;
+
+ if (w > r)
+ return w - r;
+ else
+ return (w - r + rb->size) & rb->size_mask;
+}
+
+SPA_EXPORT
+int jack_ringbuffer_mlock(jack_ringbuffer_t *rb)
+{
+#ifdef USE_MLOCK
+ if (mlock (rb->buf, rb->size))
+ return -1;
+#endif /* USE_MLOCK */
+ rb->mlocked = 1;
+ return 0;
+}
+
+SPA_EXPORT
+void jack_ringbuffer_reset(jack_ringbuffer_t *rb)
+{
+ rb->read_ptr = 0;
+ rb->write_ptr = 0;
+ memset(rb->buf, 0, rb->size);
+}
+
+SPA_EXPORT
+void jack_ringbuffer_reset_size (jack_ringbuffer_t * rb, size_t sz)
+{
+ rb->size = sz;
+ rb->size_mask = rb->size - 1;
+ rb->read_ptr = 0;
+ rb->write_ptr = 0;
+}
+
+SPA_EXPORT
+size_t jack_ringbuffer_write(jack_ringbuffer_t *rb, const char *src,
+ size_t cnt)
+{
+ size_t free_cnt;
+ size_t cnt2;
+ size_t to_write;
+ size_t n1, n2;
+
+ if ((free_cnt = jack_ringbuffer_write_space (rb)) == 0)
+ return 0;
+
+ to_write = cnt > free_cnt ? free_cnt : cnt;
+
+ cnt2 = rb->write_ptr + to_write;
+
+ if (cnt2 > rb->size) {
+ n1 = rb->size - rb->write_ptr;
+ n2 = cnt2 & rb->size_mask;
+ } else {
+ n1 = to_write;
+ n2 = 0;
+ }
+
+ memcpy (&(rb->buf[rb->write_ptr]), src, n1);
+ rb->write_ptr = (rb->write_ptr + n1) & rb->size_mask;
+ if (n2) {
+ memcpy (&(rb->buf[rb->write_ptr]), src + n1, n2);
+ rb->write_ptr = (rb->write_ptr + n2) & rb->size_mask;
+ }
+ return to_write;
+}
+
+SPA_EXPORT
+void jack_ringbuffer_write_advance(jack_ringbuffer_t *rb, size_t cnt)
+{
+ size_t tmp = (rb->write_ptr + cnt) & rb->size_mask;
+ rb->write_ptr = tmp;
+}
+
+SPA_EXPORT
+size_t jack_ringbuffer_write_space(const jack_ringbuffer_t *rb)
+{
+ size_t w, r;
+
+ w = rb->write_ptr;
+ r = rb->read_ptr;
+
+ if (w > r)
+ return ((r - w + rb->size) & rb->size_mask) - 1;
+ else if (w < r)
+ return (r - w) - 1;
+ else
+ return rb->size - 1;
+}
diff --git a/pipewire-jack/src/statistics.c b/pipewire-jack/src/statistics.c
new file mode 100644
index 0000000..25ffb3e
--- /dev/null
+++ b/pipewire-jack/src/statistics.c
@@ -0,0 +1,66 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <jack/statistics.h>
+
+SPA_EXPORT
+float jack_get_max_delayed_usecs (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ float res = 0.0f;
+
+ spa_return_val_if_fail(c != NULL, 0.0);
+
+ if (c->driver_activation)
+ res = (float)c->driver_activation->max_delay / SPA_USEC_PER_SEC;
+
+ pw_log_trace("%p: max delay %f", client, res);
+ return res;
+}
+
+SPA_EXPORT
+float jack_get_xrun_delayed_usecs (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+ float res = 0.0f;
+
+ spa_return_val_if_fail(c != NULL, 0.0);
+
+ if (c->driver_activation)
+ res = (float)c->driver_activation->xrun_delay / SPA_USEC_PER_SEC;
+
+ pw_log_trace("%p: xrun delay %f", client, res);
+ return res;
+}
+
+SPA_EXPORT
+void jack_reset_max_delayed_usecs (jack_client_t *client)
+{
+ struct client *c = (struct client *) client;
+
+ spa_return_if_fail(c != NULL);
+
+ if (c->driver_activation)
+ c->driver_activation->max_delay = 0;
+}
diff --git a/pipewire-jack/src/uuid.c b/pipewire-jack/src/uuid.c
new file mode 100644
index 0000000..8584e11
--- /dev/null
+++ b/pipewire-jack/src/uuid.c
@@ -0,0 +1,111 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#include <jack/uuid.h>
+
+#include <pipewire/pipewire.h>
+
+SPA_EXPORT
+jack_uuid_t jack_client_uuid_generate (void)
+{
+ static uint32_t uuid_cnt = 0;
+ jack_uuid_t uuid = 0x2; /* JackUUIDClient */;
+ uuid = (uuid << 32) | ++uuid_cnt;
+ pw_log_debug("uuid %"PRIu64, uuid);
+ return uuid;
+}
+
+SPA_EXPORT
+jack_uuid_t jack_port_uuid_generate (uint32_t port_id)
+{
+ jack_uuid_t uuid = 0x1; /* JackUUIDPort */
+ uuid = (uuid << 32) | (port_id + 1);
+ pw_log_debug("uuid %d -> %"PRIu64, port_id, uuid);
+ return uuid;
+}
+
+SPA_EXPORT
+uint32_t jack_uuid_to_index (jack_uuid_t id)
+{
+ return (id & 0xffffff) - 1;
+}
+
+SPA_EXPORT
+int jack_uuid_compare (jack_uuid_t id1, jack_uuid_t id2)
+{
+ if (id1 == id2)
+ return 0;
+ if (id1 < id2)
+ return -1;
+ return 1;
+}
+
+SPA_EXPORT
+void jack_uuid_copy (jack_uuid_t* dst, jack_uuid_t src)
+{
+ spa_return_if_fail(dst != NULL);
+ *dst = src;
+}
+
+SPA_EXPORT
+void jack_uuid_clear (jack_uuid_t *id)
+{
+ spa_return_if_fail(id != NULL);
+ *id = 0;
+}
+
+SPA_EXPORT
+int jack_uuid_parse (const char *buf, jack_uuid_t *id)
+{
+ spa_return_val_if_fail(buf != NULL, -EINVAL);
+ spa_return_val_if_fail(id != NULL, -EINVAL);
+
+ if (sscanf (buf, "%" PRIu64, id) == 1) {
+ if (*id < (0x1LL << 32)) {
+ /* has not type bits set - not legal */
+ return -1;
+ }
+ return 0;
+ }
+ return -1;
+}
+
+SPA_EXPORT
+void jack_uuid_unparse (jack_uuid_t id, char buf[JACK_UUID_STRING_SIZE])
+{
+ spa_return_if_fail(buf != NULL);
+ snprintf (buf, JACK_UUID_STRING_SIZE, "%" PRIu64, id);
+}
+
+SPA_EXPORT
+int jack_uuid_empty (jack_uuid_t id)
+{
+ return id == 0;
+}
diff --git a/pipewire-v4l2/meson.build b/pipewire-v4l2/meson.build
new file mode 100644
index 0000000..9537275
--- /dev/null
+++ b/pipewire-v4l2/meson.build
@@ -0,0 +1 @@
+subdir('src')
diff --git a/pipewire-v4l2/src/meson.build b/pipewire-v4l2/src/meson.build
new file mode 100644
index 0000000..a3c97c1
--- /dev/null
+++ b/pipewire-v4l2/src/meson.build
@@ -0,0 +1,41 @@
+pipewire_v4l2_sources = [
+ 'pipewire-v4l2.c',
+ 'v4l2-func.c',
+]
+
+pipewire_v4l2_c_args = [
+ # Meson enables large file support unconditionally, which redirect file
+ # operations to 64-bit versions. This results in some symbols being
+ # renamed, for instance open() being renamed to open64(). As the V4L2
+ # adaptation wrapper needs to provide both 32-bit and 64-bit versions of
+ # file operations, disable transparent large file support.
+ '-U_FILE_OFFSET_BITS',
+ '-D_FILE_OFFSET_BITS=32',
+ '-D_LARGEFILE64_SOURCE',
+ '-fvisibility=hidden',
+]
+
+libv4l2_path = get_option('libv4l2-path')
+if libv4l2_path == ''
+ libv4l2_path = modules_install_dir / 'v4l2'
+ libv4l2_path_dlopen = modules_install_dir_dlopen / 'v4l2'
+else
+ libv4l2_path_dlopen = libv4l2_path
+endif
+
+tools_config = configuration_data()
+tools_config.set('LIBV4L2_PATH', libv4l2_path_dlopen)
+
+configure_file(input : 'pw-v4l2.in',
+ output : 'pw-v4l2',
+ configuration : tools_config,
+ install_dir : pipewire_bindir)
+
+pipewire_v4l2 = shared_library('pw-v4l2',
+ pipewire_v4l2_sources,
+ c_args : pipewire_v4l2_c_args,
+ include_directories : [configinc],
+ dependencies : [pipewire_dep, mathlib, dl_lib],
+ install : true,
+ install_dir : libv4l2_path,
+)
diff --git a/pipewire-v4l2/src/pipewire-v4l2.c b/pipewire-v4l2/src/pipewire-v4l2.c
new file mode 100644
index 0000000..88702f9
--- /dev/null
+++ b/pipewire-v4l2/src/pipewire-v4l2.c
@@ -0,0 +1,2592 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <dlfcn.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <pthread.h>
+#include <limits.h>
+#include <linux/videodev2.h>
+
+#include "pipewire-v4l2.h"
+
+#include <spa/utils/result.h>
+#include <spa/pod/iter.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/filter.h>
+#include <spa/param/video/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+PW_LOG_TOPIC_STATIC(v4l2_log_topic, "v4l2");
+#define PW_LOG_TOPIC_DEFAULT v4l2_log_topic
+
+#define MIN_BUFFERS 2u
+#define MAX_BUFFERS 32u
+#define DEFAULT_TIMEOUT 30
+
+#define DEFAULT_DRIVER "PipeWire"
+#define DEFAULT_CARD "PipeWire Camera"
+#define DEFAULT_BUS_INFO "PipeWire"
+
+#define MAX_DEV 32
+struct file_map {
+ void *addr;
+ struct file *file;
+};
+
+struct fd_map {
+ int fd;
+#define FD_MAP_DUP (1<<0)
+ uint32_t flags;
+ struct file *file;
+};
+
+struct globals {
+ struct fops old_fops;
+
+ pthread_mutex_t lock;
+ struct pw_array fd_maps;
+ struct pw_array file_maps;
+ uint32_t dev_map[MAX_DEV];
+};
+
+static struct globals globals;
+
+struct global;
+
+struct buffer_map {
+ void *addr;
+ uint32_t id;
+};
+
+struct buffer {
+ struct v4l2_buffer v4l2;
+ struct pw_buffer *buf;
+ uint32_t id;
+};
+
+struct file {
+ int ref;
+
+ uint32_t dev_id;
+ uint32_t serial;
+
+ struct pw_properties *props;
+ struct pw_thread_loop *loop;
+ struct pw_loop *l;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ int last_seq;
+ int pending_seq;
+ int error;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list globals;
+ struct global *node;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ enum v4l2_priority priority;
+
+ struct v4l2_format v4l2_format;
+ uint32_t reqbufs;
+
+ int reqbufs_fd;
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+ uint32_t size;
+
+ uint32_t sequence;
+
+ struct pw_array buffer_maps;
+
+ uint32_t last_fourcc;
+
+ unsigned int running:1;
+ unsigned int closed:1;
+ int fd;
+};
+
+struct global_info {
+ const char *type;
+ uint32_t version;
+ const void *events;
+ pw_destroy_t destroy;
+ int (*init) (struct global *g);
+};
+
+struct global {
+ struct spa_list link;
+
+ struct file *file;
+
+ const struct global_info *ginfo;
+
+ uint32_t id;
+ uint32_t permissions;
+ struct pw_properties *props;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+
+ int changed;
+ void *info;
+ struct spa_list pending_list;
+ struct spa_list param_list;
+
+ union {
+ struct {
+#define NODE_FLAG_SOURCE (1<<0)
+#define NODE_FLAG_SINK (1<<0)
+ uint32_t flags;
+ uint32_t device_id;
+ int priority;
+ } node;
+ };
+};
+
+struct param {
+ struct spa_list link;
+ uint32_t id;
+ int32_t seq;
+ struct spa_pod *param;
+};
+
+static uint32_t clear_params(struct spa_list *param_list, uint32_t id)
+{
+ struct param *p, *t;
+ uint32_t count = 0;
+
+ spa_list_for_each_safe(p, t, param_list, link) {
+ if (id == SPA_ID_INVALID || p->id == id) {
+ spa_list_remove(&p->link);
+ free(p);
+ count++;
+ }
+ }
+ return count;
+}
+
+static struct param *add_param(struct spa_list *params,
+ int seq, uint32_t id, const struct spa_pod *param)
+{
+ struct param *p;
+
+ if (id == SPA_ID_INVALID) {
+ if (param == NULL || !spa_pod_is_object(param)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ id = SPA_POD_OBJECT_ID(param);
+ }
+
+ p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0));
+ if (p == NULL)
+ return NULL;
+
+ p->id = id;
+ p->seq = seq;
+ if (param != NULL) {
+ p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ } else {
+ clear_params(params, id);
+ p->param = NULL;
+ }
+ spa_list_append(params, &p->link);
+
+ return p;
+}
+
+static void update_params(struct file *file)
+{
+ struct param *p, *t;
+ struct global *node;
+ struct pw_node_info *info;
+ uint32_t i;
+
+ if ((node = file->node) == NULL)
+ return;
+ if ((info = node->info) == NULL)
+ return;
+
+ for (i = 0; i < info->n_params; i++) {
+ spa_list_for_each_safe(p, t, &node->pending_list, link) {
+ if (p->id == info->params[i].id &&
+ p->seq != info->params[i].seq &&
+ p->param != NULL) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+ }
+
+ spa_list_consume(p, &node->pending_list, link) {
+ spa_list_remove(&p->link);
+ if (p->param == NULL) {
+ clear_params(&node->param_list, p->id);
+ free(p);
+ } else {
+ spa_list_append(&node->param_list, &p->link);
+ }
+ }
+}
+#define ATOMIC_DEC(s) __atomic_sub_fetch(&(s), 1, __ATOMIC_SEQ_CST)
+#define ATOMIC_INC(s) __atomic_add_fetch(&(s), 1, __ATOMIC_SEQ_CST)
+
+static struct file *make_file(void)
+{
+ struct file *file;
+
+ file = calloc(1, sizeof(*file));
+ if (file == NULL)
+ return NULL;
+
+ file->ref = 1;
+ file->fd = -1;
+ file->reqbufs_fd = -1;
+ file->priority = V4L2_PRIORITY_DEFAULT;
+ spa_list_init(&file->globals);
+ pw_array_init(&file->buffer_maps, sizeof(struct buffer_map) * MAX_BUFFERS);
+ return file;
+}
+
+static void free_file(struct file *file)
+{
+ pw_log_info("file:%d", file->fd);
+
+ if (file->loop)
+ pw_thread_loop_stop(file->loop);
+
+ if (file->registry) {
+ spa_hook_remove(&file->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)file->registry);
+ }
+ if (file->stream) {
+ spa_hook_remove(&file->stream_listener);
+ pw_stream_destroy(file->stream);
+ }
+ if (file->core) {
+ spa_hook_remove(&file->core_listener);
+ pw_core_disconnect(file->core);
+ }
+ if (file->context)
+ pw_context_destroy(file->context);
+ if (file->fd != -1)
+ spa_system_close(file->l->system, file->fd);
+ if (file->loop)
+ pw_thread_loop_destroy(file->loop);
+
+ pw_array_clear(&file->buffer_maps);
+ free(file);
+}
+
+static void unref_file(struct file *file)
+{
+ pw_log_debug("file:%d ref:%d", file->fd, file->ref);
+ if (ATOMIC_DEC(file->ref) <= 0)
+ free_file(file);
+}
+
+static int add_fd_map(int fd, struct file *file, uint32_t flags)
+{
+ struct fd_map *map;
+ pthread_mutex_lock(&globals.lock);
+ map = pw_array_add(&globals.fd_maps, sizeof(*map));
+ if (map != NULL) {
+ map->fd = fd;
+ map->flags = flags;
+ map->file = file;
+ ATOMIC_INC(file->ref);
+ pw_log_debug("fd:%d -> file:%d ref:%d", fd, file->fd, file->ref);
+ }
+ pthread_mutex_unlock(&globals.lock);
+ return 0;
+}
+
+static uint32_t find_dev_for_serial(uint32_t serial)
+{
+ uint32_t i, res = SPA_ID_INVALID;
+ pthread_mutex_lock(&globals.lock);
+ for (i = 0; i < SPA_N_ELEMENTS(globals.dev_map); i++) {
+ if (globals.dev_map[i] == serial) {
+ res = i;
+ break;
+ }
+ }
+ pthread_mutex_unlock(&globals.lock);
+ return res;
+}
+
+static bool add_dev_for_serial(uint32_t dev, uint32_t serial)
+{
+ pthread_mutex_lock(&globals.lock);
+ globals.dev_map[dev] = serial;
+ pthread_mutex_unlock(&globals.lock);
+ return true;
+}
+
+/* must be called with `globals.lock` held */
+static struct fd_map *find_fd_map_unlocked(int fd)
+{
+ struct fd_map *map;
+ pw_array_for_each(map, &globals.fd_maps) {
+ if (map->fd == fd) {
+ ATOMIC_INC(map->file->ref);
+ pw_log_debug("fd:%d find:%d ref:%d", map->fd, fd, map->file->ref);
+ return map;
+ }
+ }
+ return NULL;
+}
+
+static struct file *find_file(int fd, uint32_t *flags)
+{
+ pthread_mutex_lock(&globals.lock);
+
+ struct fd_map *map = find_fd_map_unlocked(fd);
+ struct file *file = NULL;
+
+ if (map != NULL) {
+ file = map->file;
+ *flags = map->flags;
+ }
+
+ pthread_mutex_unlock(&globals.lock);
+
+ return file;
+}
+
+static struct file *find_file_by_dev(uint32_t dev)
+{
+ struct fd_map *map = NULL, *tmp;
+ struct file *file = NULL;
+
+ pthread_mutex_lock(&globals.lock);
+ pw_array_for_each(tmp, &globals.fd_maps) {
+ if (tmp->file->dev_id == dev) {
+ if (tmp->file->closed)
+ tmp->file->fd = tmp->fd;
+ ATOMIC_INC(tmp->file->ref);
+ map = tmp;
+ pw_log_debug("dev:%d find:%d ref:%d",
+ tmp->file->dev_id, dev, tmp->file->ref);
+ break;
+ }
+ }
+ if (map != NULL)
+ file = map->file;
+ pthread_mutex_unlock(&globals.lock);
+
+ return file;
+}
+
+static struct file *remove_fd_map(int fd)
+{
+ pthread_mutex_lock(&globals.lock);
+
+ struct fd_map *map = find_fd_map_unlocked(fd);
+ struct file *file = NULL;
+
+ if (map != NULL) {
+ file = map->file;
+ pw_log_debug("fd:%d find:%d", map->fd, fd);
+ pw_array_remove(&globals.fd_maps, map);
+ }
+
+ pthread_mutex_unlock(&globals.lock);
+
+ if (file != NULL)
+ unref_file(file);
+
+ return file;
+}
+
+static int add_file_map(struct file *file, void *addr)
+{
+ struct file_map *map;
+ pthread_mutex_lock(&globals.lock);
+ map = pw_array_add(&globals.file_maps, sizeof(*map));
+ if (map != NULL) {
+ map->addr = addr;
+ map->file = file;
+ }
+ pthread_mutex_unlock(&globals.lock);
+ return 0;
+}
+
+/* must be called with `globals.lock` held */
+static struct file_map *find_file_map_unlocked(void *addr)
+{
+ struct file_map *map;
+
+ pw_array_for_each(map, &globals.file_maps) {
+ if (map->addr == addr)
+ return map;
+ }
+
+ return NULL;
+}
+static struct file *remove_file_map(void *addr)
+{
+ pthread_mutex_lock(&globals.lock);
+
+ struct file_map *map = find_file_map_unlocked(addr);
+ struct file *file = NULL;
+
+ if (map != NULL) {
+ file = map->file;
+ pw_array_remove(&globals.file_maps, map);
+ }
+
+ pthread_mutex_unlock(&globals.lock);
+
+ return file;
+}
+
+static int add_buffer_map(struct file *file, void *addr, uint32_t id)
+{
+ struct buffer_map *map;
+ map = pw_array_add(&file->buffer_maps, sizeof(*map));
+ if (map != NULL) {
+ map->addr = addr;
+ map->id = id;
+ }
+ return 0;
+}
+static struct buffer_map *find_buffer_map(struct file *file, void *addr)
+{
+ struct buffer_map *map;
+ pw_array_for_each(map, &file->buffer_maps) {
+ if (map->addr == addr)
+ return map;
+ }
+ return NULL;
+}
+static void remove_buffer_map(struct file *file, struct buffer_map *map)
+{
+ pw_array_remove(&file->buffer_maps, map);
+}
+
+static void do_resync(struct file *file)
+{
+ file->pending_seq = pw_core_sync(file->core, PW_ID_CORE, file->pending_seq);
+}
+
+static int wait_resync(struct file *file)
+{
+ int res;
+ do_resync(file);
+
+ while (true) {
+ pw_thread_loop_wait(file->loop);
+
+ res = file->error;
+ if (res < 0) {
+ file->error = 0;
+ return res;
+ }
+ if (file->pending_seq == file->last_seq)
+ break;
+ }
+ return 0;
+}
+
+static void on_sync_reply(void *data, uint32_t id, int seq)
+{
+ struct file *file = data;
+
+ if (id != PW_ID_CORE)
+ return;
+
+ file->last_seq = seq;
+ if (file->pending_seq == seq) {
+ update_params(file);
+ pw_thread_loop_signal(file->loop, false);
+ }
+}
+
+static void on_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct file *file = data;
+
+ pw_log_warn("file:%d: error id:%u seq:%d res:%d (%s): %s", file->fd,
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE) {
+ switch (res) {
+ case -ENOENT:
+ break;
+ default:
+ file->error = res;
+ }
+ }
+ pw_thread_loop_signal(file->loop, false);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_sync_reply,
+ .error = on_error,
+};
+
+/** node */
+static void node_event_info(void *object, const struct pw_node_info *info)
+{
+ struct global *g = object;
+ struct file *file = g->file;
+ const char *str;
+ uint32_t i;
+
+ info = g->info = pw_node_info_merge(g->info, info, g->changed == 0);
+ if (info == NULL)
+ return;
+
+ pw_log_debug("update %d %"PRIu64, g->id, info->change_mask);
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS && info->props) {
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)))
+ g->node.device_id = atoi(str);
+ else
+ g->node.device_id = SPA_ID_INVALID;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_PRIORITY_SESSION)))
+ g->node.priority = atoi(str);
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS))) {
+ if (spa_streq(str, "Video/Sink"))
+ g->node.flags |= NODE_FLAG_SINK;
+ else if (spa_streq(str, "Video/Source"))
+ g->node.flags |= NODE_FLAG_SOURCE;
+ }
+ }
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+ int res;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ add_param(&g->pending_list, info->params[i].seq, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_node_enum_params((struct pw_node*)g->proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ do_resync(file);
+}
+
+static void node_event_param(void *object, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct global *g = object;
+
+ pw_log_debug("update param %d %d %d", g->id, id, seq);
+ add_param(&g->pending_list, seq, id, param);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = node_event_param,
+};
+
+static const struct global_info node_info = {
+ .type = PW_TYPE_INTERFACE_Node,
+ .version = PW_VERSION_NODE,
+ .events = &node_events,
+};
+
+/** proxy */
+static void proxy_removed(void *data)
+{
+ struct global *g = data;
+ pw_proxy_destroy(g->proxy);
+}
+
+static void proxy_destroy(void *data)
+{
+ struct global *g = data;
+ spa_list_remove(&g->link);
+ g->proxy = NULL;
+ if (g->file)
+ g->file->node = NULL;
+ clear_params(&g->param_list, SPA_ID_INVALID);
+ clear_params(&g->pending_list, SPA_ID_INVALID);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = proxy_removed,
+ .destroy = proxy_destroy
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct file *file = data;
+ const struct global_info *info = NULL;
+ struct pw_proxy *proxy;
+ const char *str;
+ uint32_t serial = SPA_ID_INVALID, dev, req_serial;
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+
+ if (file->node != NULL)
+ return;
+
+ pw_log_info("got %d %s", id, type);
+
+ if (props == NULL)
+ return;
+ if (((str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)) == NULL) ||
+ ((!spa_streq(str, "Video/Sink")) &&
+ (!spa_streq(str, "Video/Source"))))
+ return;
+
+ if (((str = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL)) == NULL) ||
+ !spa_atou32(str, &serial, 10))
+ return;
+
+ if ((str = getenv("PIPEWIRE_V4L2_TARGET")) != NULL
+ && spa_atou32(str, &req_serial, 10)
+ && req_serial != serial)
+ return;
+
+ dev = find_dev_for_serial(serial);
+ if (dev != SPA_ID_INVALID && dev != file->dev_id)
+ return;
+
+ pw_log_info("found node:%d serial:%d type:%s", id, serial, str);
+ info = &node_info;
+ }
+ if (info) {
+ struct global *g;
+
+ proxy = pw_registry_bind(file->registry,
+ id, info->type, info->version,
+ sizeof(struct global));
+
+ g = pw_proxy_get_user_data(proxy);
+ g->file = file;
+ g->ginfo = info;
+ g->id = id;
+ g->permissions = permissions;
+ g->props = props ? pw_properties_new_dict(props) : NULL;
+ g->proxy = proxy;
+ spa_list_init(&g->pending_list);
+ spa_list_init(&g->param_list);
+ spa_list_append(&file->globals, &g->link);
+
+ pw_proxy_add_listener(proxy,
+ &g->proxy_listener,
+ &proxy_events, g);
+
+ if (info->events) {
+ pw_proxy_add_object_listener(proxy,
+ &g->object_listener,
+ info->events, g);
+ }
+ if (info->init)
+ info->init(g);
+
+ file->serial = serial;
+ file->node = g;
+
+ do_resync(file);
+ }
+}
+
+static struct global *find_global(struct file *file, uint32_t id)
+{
+ struct global *g;
+ spa_list_for_each(g, &file->globals, link) {
+ if (g->id == id)
+ return g;
+ }
+ return NULL;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct file *file = data;
+ struct global *g;
+
+ if ((g = find_global(file, id)) == NULL)
+ return;
+
+ pw_proxy_destroy(g->proxy);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static int do_dup(int oldfd, uint32_t flags)
+{
+ int res;
+ struct file *file;
+ uint32_t fl;
+
+ res = globals.old_fops.dup(oldfd);
+ if (res < 0)
+ return res;
+
+ if ((file = find_file(oldfd, &fl)) != NULL) {
+ add_fd_map(res, file, flags | fl);
+ unref_file(file);
+ pw_log_info("fd:%d %08x -> %d (%s)", oldfd, flags,
+ res, strerror(res < 0 ? errno : 0));
+ }
+ return res;
+}
+
+static int v4l2_dup(int oldfd)
+{
+ return do_dup(oldfd, FD_MAP_DUP);
+}
+
+static int v4l2_openat(int dirfd, const char *path, int oflag, mode_t mode)
+{
+ int res, flags;
+ struct file *file;
+ bool passthrough = true;
+ uint32_t dev_id = SPA_ID_INVALID;
+ char *real_path;
+
+ real_path = realpath(path, NULL);
+ if (!real_path)
+ real_path = (char *)path;
+
+ if (spa_strstartswith(real_path, "/dev/video")) {
+ if (spa_atou32(real_path+10, &dev_id, 10) && dev_id < MAX_DEV)
+ passthrough = false;
+ }
+
+ if (real_path && real_path != path)
+ free(real_path);
+
+ if (passthrough)
+ return globals.old_fops.openat(dirfd, path, oflag, mode);
+
+ pw_log_info("path:%s oflag:%d mode:%d", path, oflag, mode);
+
+ if ((file = find_file_by_dev(dev_id)) != NULL) {
+ res = do_dup(file->fd, 0);
+ unref_file(file);
+ if (res >= 0)
+ fcntl(res, F_SETFL, oflag);
+ return res;
+ }
+
+ if ((file = make_file()) == NULL)
+ goto error;
+
+ file->dev_id = dev_id;
+ file->props = pw_properties_new(
+ PW_KEY_CLIENT_API, "v4l2",
+ NULL);
+ file->loop = pw_thread_loop_new("v4l2", NULL);
+ if (file->loop == NULL)
+ goto error;
+
+ file->l = pw_thread_loop_get_loop(file->loop);
+ file->context = pw_context_new(file->l,
+ pw_properties_copy(file->props), 0);
+ if (file->context == NULL)
+ goto error;
+
+ pw_thread_loop_start(file->loop);
+
+ pw_thread_loop_lock(file->loop);
+
+ file->core = pw_context_connect(file->context,
+ pw_properties_copy(file->props), 0);
+ if (file->core == NULL)
+ goto error_unlock;
+
+ pw_core_add_listener(file->core,
+ &file->core_listener,
+ &core_events, file);
+ file->registry = pw_core_get_registry(file->core,
+ PW_VERSION_REGISTRY, 0);
+ if (file->registry == NULL)
+ goto error_unlock;
+
+ pw_registry_add_listener(file->registry,
+ &file->registry_listener,
+ &registry_events, file);
+
+ res = wait_resync(file);
+ if (res < 0) {
+ errno = -res;
+ goto error_unlock;
+ }
+ if (file->node == NULL) {
+ errno = ENOENT;
+ goto error_unlock;
+ }
+ pw_thread_loop_unlock(file->loop);
+
+ flags = SPA_FD_CLOEXEC;
+ if (oflag & O_NONBLOCK)
+ flags |= SPA_FD_NONBLOCK;
+
+ res = spa_system_eventfd_create(file->l->system, flags);
+ if (res < 0)
+ goto error;
+
+ file->fd = res;
+
+ pw_log_info("path:%s oflag:%d mode:%d -> %d (%s)", path, oflag, mode,
+ res, strerror(res < 0 ? errno : 0));
+
+ add_fd_map(res, file, 0);
+ add_dev_for_serial(file->dev_id, file->serial);
+ unref_file(file);
+
+ return res;
+
+error_unlock:
+ pw_thread_loop_unlock(file->loop);
+error:
+ res = -errno;
+ if (file)
+ free_file(file);
+
+ pw_log_info("path:%s oflag:%d mode:%d -> %d (%s)", path, oflag, mode,
+ -1, spa_strerror(res));
+
+ errno = -res;
+
+ return -1;
+}
+
+static int v4l2_close(int fd)
+{
+ struct file *file;
+
+ if ((file = remove_fd_map(fd)) == NULL)
+ return globals.old_fops.close(fd);
+
+ pw_log_info("fd:%d file:%d", fd, file->fd);
+
+ if (fd != file->fd)
+ spa_system_close(file->l->system, fd);
+
+ file->closed = true;
+ unref_file(file);
+
+ return 0;
+}
+
+#define KERNEL_VERSION(a, b, c) (((a) << 16) + ((b) << 8) + (c))
+
+static int vidioc_querycap(struct file *file, struct v4l2_capability *arg)
+{
+ int res = 0;
+ const char *str = NULL;
+ struct pw_node_info *info;
+
+ if (file->node == NULL)
+ return -EIO;
+
+ info = file->node->info;
+
+ if (info != NULL && info->props != NULL) {
+ str = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION);
+ }
+ if (str == NULL)
+ str = DEFAULT_CARD;
+
+ spa_scnprintf((char*)arg->driver, sizeof(arg->driver), "%s", DEFAULT_DRIVER);
+ spa_scnprintf((char*)arg->card, sizeof(arg->card), "%s", str);
+ spa_scnprintf((char*)arg->bus_info, sizeof(arg->bus_info), "platform:%s-%d",
+ DEFAULT_BUS_INFO, file->node->id);
+
+ arg->version = KERNEL_VERSION(5, 2, 0);
+ arg->device_caps = V4L2_CAP_VIDEO_CAPTURE
+ | V4L2_CAP_STREAMING
+ | V4L2_CAP_EXT_PIX_FORMAT;
+ arg->capabilities = arg->device_caps | V4L2_CAP_DEVICE_CAPS;
+ memset(arg->reserved, 0, sizeof(arg->reserved));
+
+ pw_log_info("file:%d -> %d (%s)", file->fd, res, spa_strerror(res));
+ return res;
+}
+
+struct format_info {
+ uint32_t fourcc;
+ uint32_t media_type;
+ uint32_t media_subtype;
+ uint32_t format;
+ uint32_t bpp;
+ const char *desc;
+};
+
+#define MAKE_FORMAT(fcc,mt,mst,bpp,fmt,...) \
+ { V4L2_PIX_FMT_ ## fcc, SPA_MEDIA_TYPE_ ## mt, SPA_MEDIA_SUBTYPE_ ## mst, SPA_VIDEO_FORMAT_ ## fmt, bpp, __VA_ARGS__ }
+
+static const struct format_info format_info[] = {
+ /* RGB formats */
+ MAKE_FORMAT(RGB332, video, raw, 4, UNKNOWN, "8-bit RGB 3-3-2"),
+ MAKE_FORMAT(ARGB555, video, raw, 4, UNKNOWN),
+ MAKE_FORMAT(XRGB555, video, raw, 4, RGB15, "16-bit XRGB 1-5-5-5"),
+ MAKE_FORMAT(ARGB555X, video, raw, 4, UNKNOWN),
+ MAKE_FORMAT(XRGB555X, video, raw, 4, BGR15, "16-bit XRGB 1-5-5-5 BE"),
+ MAKE_FORMAT(RGB565, video, raw, 4, RGB16, "16-bit RGB 5-6-5"),
+ MAKE_FORMAT(RGB565X, video, raw, 4, UNKNOWN),
+ MAKE_FORMAT(BGR666, video, raw, 4, UNKNOWN),
+ MAKE_FORMAT(BGR24, video, raw, 4, BGR),
+ MAKE_FORMAT(RGB24, video, raw, 4, RGB),
+ MAKE_FORMAT(ABGR32, video, raw, 4, BGRA),
+ MAKE_FORMAT(XBGR32, video, raw, 4, BGRx),
+ MAKE_FORMAT(ARGB32, video, raw, 4, ARGB),
+ MAKE_FORMAT(XRGB32, video, raw, 4, xRGB),
+
+ /* Deprecated Packed RGB Image Formats (alpha ambiguity) */
+ MAKE_FORMAT(RGB444, video, raw, 2, UNKNOWN),
+ MAKE_FORMAT(RGB555, video, raw, 2, RGB15),
+ MAKE_FORMAT(RGB555X, video, raw, 2, BGR15),
+ MAKE_FORMAT(BGR32, video, raw, 4, BGRx),
+ MAKE_FORMAT(RGB32, video, raw, 4, xRGB),
+
+ /* Grey formats */
+ MAKE_FORMAT(GREY, video, raw, 1, GRAY8),
+ MAKE_FORMAT(Y4, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(Y6, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(Y10, video, raw, 2, UNKNOWN),
+ MAKE_FORMAT(Y12, video, raw, 2, UNKNOWN),
+ MAKE_FORMAT(Y16, video, raw, 2, GRAY16_LE),
+ MAKE_FORMAT(Y16_BE, video, raw, 2, GRAY16_BE),
+ MAKE_FORMAT(Y10BPACK, video, raw, 2, UNKNOWN),
+
+ /* Palette formats */
+ MAKE_FORMAT(PAL8, video, raw, 1, UNKNOWN),
+
+ /* Chrominance formats */
+ MAKE_FORMAT(UV8, video, raw, 2, UNKNOWN),
+
+ /* Luminance+Chrominance formats */
+ MAKE_FORMAT(YVU410, video, raw, 1, YVU9, "Planar YVU 4:1:0"),
+ MAKE_FORMAT(YVU420, video, raw, 1, YV12, "Planar YVU 4:2:0"),
+ MAKE_FORMAT(YVU420M, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(YUYV, video, raw, 2, YUY2, "YUYV 4:2:2"),
+ MAKE_FORMAT(YYUV, video, raw, 2, UNKNOWN),
+ MAKE_FORMAT(YVYU, video, raw, 2, YVYU, "YVYU 4:2:2"),
+ MAKE_FORMAT(UYVY, video, raw, 2, UYVY, "UYVY 4:2:2"),
+ MAKE_FORMAT(VYUY, video, raw, 2, UNKNOWN),
+ MAKE_FORMAT(YUV422P, video, raw, 1, Y42B),
+ MAKE_FORMAT(YUV411P, video, raw, 1, Y41B),
+ MAKE_FORMAT(Y41P, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(YUV444, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(YUV555, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(YUV565, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(YUV32, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(YUV410, video, raw, 1, YUV9),
+ MAKE_FORMAT(YUV420, video, raw, 1, I420, "Planar YUV 4:2:0"),
+ MAKE_FORMAT(YUV420M, video, raw, 1, I420, "Planar YUV 4:2:0 (N-C)"),
+ MAKE_FORMAT(HI240, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(HM12, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(M420, video, raw, 1, UNKNOWN),
+
+ /* two planes -- one Y, one Cr + Cb interleaved */
+ MAKE_FORMAT(NV12, video, raw, 1, NV12, "Y/CbCr 4:2:0"),
+ MAKE_FORMAT(NV12M, video, raw, 1, NV12, "Y/CbCr 4:2:0 (N-C)"),
+ MAKE_FORMAT(NV12MT, video, raw, 1, NV12_64Z32, "Y/CbCr 4:2:0 (64x32 MB, N-C)"),
+ MAKE_FORMAT(NV12MT_16X16, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(NV21, video, raw, 1, NV21, "Y/CrCb 4:2:0"),
+ MAKE_FORMAT(NV21M, video, raw, 1, NV21, "Y/CrCb 4:2:0 (N-C)"),
+ MAKE_FORMAT(NV16, video, raw, 1, NV16, "Y/CbCr 4:2:2"),
+ MAKE_FORMAT(NV16M, video, raw, 1, NV16, "Y/CbCr 4:2:2 (N-C)"),
+ MAKE_FORMAT(NV61, video, raw, 1, NV61, "Y/CrCb 4:2:2"),
+ MAKE_FORMAT(NV61M, video, raw, 1, NV61, "Y/CrCb 4:2:2 (N-C)"),
+ MAKE_FORMAT(NV24, video, raw, 1, NV24, "Y/CbCr 4:4:4"),
+ MAKE_FORMAT(NV42, video, raw, 1, UNKNOWN),
+
+ /* Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm */
+ MAKE_FORMAT(SBGGR8, video, bayer, 1, UNKNOWN),
+ MAKE_FORMAT(SGBRG8, video, bayer, 1, UNKNOWN),
+ MAKE_FORMAT(SGRBG8, video, bayer, 1, UNKNOWN),
+ MAKE_FORMAT(SRGGB8, video, bayer, 1, UNKNOWN),
+
+ /* compressed formats */
+ MAKE_FORMAT(MJPEG, video, mjpg, 1, ENCODED, "Motion-JPEG"),
+ MAKE_FORMAT(JPEG, video, mjpg, 1, ENCODED, "JFIF JPEG"),
+ MAKE_FORMAT(PJPG, video, mjpg, 1, ENCODED, "GSPCA PJPG"),
+ MAKE_FORMAT(DV, video, dv, 1, ENCODED),
+ MAKE_FORMAT(MPEG, video, mpegts, 1, ENCODED),
+ MAKE_FORMAT(H264, video, h264, 1, ENCODED, "H.264"),
+ MAKE_FORMAT(H264_NO_SC, video, h264, 1, ENCODED, "H.264 (No Start Codes)"),
+ MAKE_FORMAT(H264_MVC, video, h264, 1, ENCODED, "H.264 MVC"),
+ MAKE_FORMAT(H263, video, h263, 1, ENCODED),
+ MAKE_FORMAT(MPEG1, video, mpeg1, 1, ENCODED),
+ MAKE_FORMAT(MPEG2, video, mpeg2, 1, ENCODED),
+ MAKE_FORMAT(MPEG4, video, mpeg4, 1, ENCODED),
+ MAKE_FORMAT(XVID, video, xvid, 1, ENCODED),
+ MAKE_FORMAT(VC1_ANNEX_G, video, vc1, 1, ENCODED),
+ MAKE_FORMAT(VC1_ANNEX_L, video, vc1, 1, ENCODED),
+ MAKE_FORMAT(VP8, video, vp8, 1, ENCODED),
+
+ /* Vendor-specific formats */
+ MAKE_FORMAT(WNVA, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(SN9C10X, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(PWC1, video, raw, 1, UNKNOWN),
+ MAKE_FORMAT(PWC2, video, raw, 1, UNKNOWN),
+};
+
+static const struct format_info *format_info_from_media_type(uint32_t type,
+ uint32_t subtype, uint32_t format)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+ if ((i->media_type == type) &&
+ (i->media_subtype == subtype) &&
+ (format == 0 || i->format == format))
+ return i;
+ return NULL;
+}
+
+static const struct format_info *format_info_from_fourcc(uint32_t fourcc)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+ if (i->fourcc == fourcc)
+ return i;
+ return NULL;
+}
+
+static int format_to_info(const struct v4l2_format *arg, struct spa_video_info *info)
+{
+ const struct format_info *fi;
+
+ pw_log_info("type: %u", arg->type);
+ pw_log_info("width: %u", arg->fmt.pix.width);
+ pw_log_info("height: %u", arg->fmt.pix.height);
+ pw_log_info("fmt: %.4s", (char*)&arg->fmt.pix.pixelformat);
+
+ if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ fi = format_info_from_fourcc(arg->fmt.pix.pixelformat);
+ if (fi == NULL)
+ return -EINVAL;
+
+ spa_zero(*info);
+ info->media_type = fi->media_type;
+ info->media_subtype = fi->media_subtype;
+
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ info->info.raw.format = fi->format;
+ info->info.raw.size.width = arg->fmt.pix.width;
+ info->info.raw.size.height = arg->fmt.pix.height;
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ info->info.h264.size.width = arg->fmt.pix.width;
+ info->info.h264.size.height = arg->fmt.pix.height;
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ case SPA_MEDIA_SUBTYPE_jpeg:
+ info->info.mjpg.size.width = arg->fmt.pix.width;
+ info->info.mjpg.size.height = arg->fmt.pix.height;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static struct spa_pod *info_to_param(struct spa_pod_builder *builder, uint32_t id,
+ struct spa_video_info *info)
+{
+ struct spa_pod *pod;
+
+ if (info->media_type != SPA_MEDIA_TYPE_video)
+ return NULL;
+
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ pod = spa_format_video_raw_build(builder, id, &info->info.raw);
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ case SPA_MEDIA_SUBTYPE_jpeg:
+ pod = spa_format_video_mjpg_build(builder, id, &info->info.mjpg);
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ pod = spa_format_video_h264_build(builder, id, &info->info.h264);
+ break;
+ default:
+ return NULL;
+ }
+ return pod;
+}
+
+static struct spa_pod *fmt_to_param(struct spa_pod_builder *builder, uint32_t id,
+ const struct v4l2_format *fmt)
+{
+ struct spa_video_info info;
+ if (format_to_info(fmt, &info) < 0)
+ return NULL;
+ return info_to_param(builder, id, &info);
+}
+
+static int param_to_info(const struct spa_pod *param, struct spa_video_info *info)
+{
+ int res;
+
+ spa_zero(*info);
+ if (spa_format_parse(param, &info->media_type, &info->media_subtype) < 0)
+ return -EINVAL;
+
+ if (info->media_type != SPA_MEDIA_TYPE_video)
+ return -EINVAL;
+
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ res = spa_format_video_raw_parse(param, &info->info.raw);
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ res = spa_format_video_h264_parse(param, &info->info.h264);
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ case SPA_MEDIA_SUBTYPE_jpeg:
+ res = spa_format_video_mjpg_parse(param, &info->info.mjpg);
+ break;
+ default:
+ return -EINVAL;
+ }
+ return res;
+}
+
+static int info_to_fmt(const struct spa_video_info *info, struct v4l2_format *fmt)
+{
+ const struct format_info *fi;
+ uint32_t format;
+
+ if (info->media_type != SPA_MEDIA_TYPE_video)
+ return -EINVAL;
+
+ if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+ format = info->info.raw.format;
+ } else {
+ format = SPA_VIDEO_FORMAT_ENCODED;
+ }
+
+ fi = format_info_from_media_type(info->media_type, info->media_subtype,
+ format);
+ if (fi == NULL)
+ return -EINVAL;
+
+ spa_zero(*fmt);
+ fmt->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ fmt->fmt.pix.pixelformat = fi->fourcc;
+ fmt->fmt.pix.field = V4L2_FIELD_NONE;
+
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ fmt->fmt.pix.width = info->info.raw.size.width;
+ fmt->fmt.pix.height = info->info.raw.size.height;
+ fmt->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ case SPA_MEDIA_SUBTYPE_jpeg:
+ fmt->fmt.pix.width = info->info.mjpg.size.width;
+ fmt->fmt.pix.height = info->info.mjpg.size.height;
+ fmt->fmt.pix.colorspace = V4L2_COLORSPACE_JPEG;
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ fmt->fmt.pix.width = info->info.h264.size.width;
+ fmt->fmt.pix.height = info->info.h264.size.height;
+ fmt->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
+ break;
+ default:
+ return -EINVAL;
+ }
+ if (fmt->fmt.pix.width == 0 ||
+ fmt->fmt.pix.height == 0)
+ return -EINVAL;
+
+ fmt->fmt.pix.bytesperline = SPA_ROUND_UP_N(fmt->fmt.pix.width, 4) * fi->bpp;
+ fmt->fmt.pix.sizeimage = fmt->fmt.pix.bytesperline *
+ SPA_ROUND_UP_N(fmt->fmt.pix.height, 2);
+
+ return 0;
+}
+
+static int param_to_fmt(const struct spa_pod *param, struct v4l2_format *fmt)
+{
+ struct spa_video_info info;
+ struct spa_pod *copy;
+ int res;
+
+ copy = spa_pod_copy(param);
+ spa_pod_fixate(copy);
+ res = param_to_info(copy, &info);
+ free(copy);
+
+ if (res < 0)
+ return -EINVAL;
+ if (info_to_fmt(&info, fmt) < 0)
+ return -EINVAL;
+ return 0;
+}
+
+static void on_stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct file *file = data;
+ const struct spa_pod *params[4];
+ uint32_t n_params = 0;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ uint32_t buffers, size, stride;
+ struct v4l2_format fmt;
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ if (param_to_fmt(param, &fmt) < 0)
+ return;
+
+ file->v4l2_format = fmt;
+
+ buffers = SPA_CLAMP(file->reqbufs, 1u, MAX_BUFFERS);
+ size = fmt.fmt.pix.sizeimage;
+ stride = fmt.fmt.pix.bytesperline;
+
+ params[n_params++] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers,
+ 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(size, 0, INT_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(stride, 0, INT_MAX),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<<SPA_DATA_MemFd)));
+
+
+ pw_stream_update_params(file->stream, params, n_params);
+
+}
+
+static void on_stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct file *file = data;
+
+ pw_log_info("file:%d: state %s", file->fd, pw_stream_state_as_string(state));
+
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ break;
+ case PW_STREAM_STATE_CONNECTING:
+ case PW_STREAM_STATE_PAUSED:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ }
+ pw_thread_loop_signal(file->loop, false);
+}
+
+static void on_stream_add_buffer(void *data, struct pw_buffer *b)
+{
+ struct file *file = data;
+ uint32_t id = file->n_buffers;
+ struct buffer *buf = &file->buffers[id];
+ struct v4l2_buffer vb;
+ struct spa_data *d = &b->buffer->datas[0];
+
+ file->size = d->maxsize;
+
+ pw_log_info("file:%d: id:%d fd:%"PRIi64" size:%u offset:%u", file->fd,
+ id, d->fd, file->size, id * file->size);
+
+ spa_zero(vb);
+ vb.index = id;
+ vb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ vb.flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
+ vb.memory = V4L2_MEMORY_MMAP;
+ vb.m.offset = id * file->size;
+ vb.length = file->size;
+
+ buf->v4l2 = vb;
+ buf->id = id;
+ buf->buf = b;
+ b->user_data = buf;
+
+ file->n_buffers++;
+}
+
+static void on_stream_remove_buffer(void *data, struct pw_buffer *b)
+{
+ struct file *file = data;
+ file->n_buffers--;
+}
+
+static void on_stream_process(void *data)
+{
+ struct file *file = data;
+ pw_log_debug("file:%d", file->fd);
+ spa_system_eventfd_write(file->l->system, file->fd, 1);
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .param_changed = on_stream_param_changed,
+ .state_changed = on_stream_state_changed,
+ .add_buffer = on_stream_add_buffer,
+ .remove_buffer = on_stream_remove_buffer,
+ .process = on_stream_process,
+};
+
+static int vidioc_enum_framesizes(struct file *file, struct v4l2_frmsizeenum *arg)
+{
+ uint32_t count = 0;
+ struct global *g = file->node;
+ struct param *p;
+ bool found = false;
+
+ pw_log_info("index: %u", arg->index);
+ pw_log_info("format: %.4s", (char*)&arg->pixel_format);
+
+ pw_thread_loop_lock(file->loop);
+ spa_list_for_each(p, &g->param_list, link) {
+ const struct format_info *fi;
+ uint32_t media_type, media_subtype, format;
+ struct spa_rectangle size;
+
+ if (p->id != SPA_PARAM_EnumFormat || p->param == NULL)
+ continue;
+
+ if (spa_format_parse(p->param, &media_type, &media_subtype) < 0)
+ continue;
+ if (media_type != SPA_MEDIA_TYPE_video)
+ continue;
+ if (media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(&format)) < 0)
+ continue;
+ } else {
+ format = SPA_VIDEO_FORMAT_ENCODED;
+ }
+
+ fi = format_info_from_media_type(media_type, media_subtype, format);
+ if (fi == NULL)
+ continue;
+
+ if (fi->fourcc != arg->pixel_format)
+ continue;
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&size)) < 0)
+ continue;
+
+ arg->type = V4L2_FRMSIZE_TYPE_DISCRETE;
+ arg->discrete.width = size.width;
+ arg->discrete.height = size.height;
+
+ pw_log_debug("count:%d %.4s %dx%d", count, (char*)&fi->fourcc,
+ size.width, size.height);
+ if (count == arg->index) {
+ found = true;
+ break;
+ }
+ count++;
+ }
+ pw_thread_loop_unlock(file->loop);
+
+ if (!found)
+ return -EINVAL;
+
+ switch (arg->type) {
+ case V4L2_FRMSIZE_TYPE_DISCRETE:
+ pw_log_info("type: discrete");
+ pw_log_info("width: %u", arg->discrete.width);
+ pw_log_info("height: %u", arg->discrete.height);
+ break;
+ case V4L2_FRMSIZE_TYPE_CONTINUOUS:
+ case V4L2_FRMSIZE_TYPE_STEPWISE:
+ pw_log_info("type: stepwise");
+ pw_log_info("min-width: %u", arg->stepwise.min_width);
+ pw_log_info("max-width: %u", arg->stepwise.max_width);
+ pw_log_info("step-width: %u", arg->stepwise.step_width);
+ pw_log_info("min-height: %u", arg->stepwise.min_height);
+ pw_log_info("max-height: %u", arg->stepwise.max_height);
+ pw_log_info("step-height: %u", arg->stepwise.step_height);
+ break;
+ }
+
+ memset(arg->reserved, 0, sizeof(arg->reserved));
+
+ return 0;
+}
+
+static int vidioc_enum_fmt(struct file *file, struct v4l2_fmtdesc *arg)
+{
+ uint32_t count = 0, last_fourcc = 0;
+ struct global *g = file->node;
+ struct param *p;
+ bool found = false;
+
+ pw_log_info("index: %u", arg->index);
+ pw_log_info("type: %u", arg->type);
+
+ if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ pw_thread_loop_lock(file->loop);
+ spa_list_for_each(p, &g->param_list, link) {
+ const struct format_info *fi;
+ uint32_t media_type, media_subtype, format;
+
+ if (p->id != SPA_PARAM_EnumFormat || p->param == NULL)
+ continue;
+
+ if (spa_format_parse(p->param, &media_type, &media_subtype) < 0)
+ continue;
+ if (media_type != SPA_MEDIA_TYPE_video)
+ continue;
+ if (media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(&format)) < 0)
+ continue;
+ } else {
+ format = SPA_VIDEO_FORMAT_ENCODED;
+ }
+
+ fi = format_info_from_media_type(media_type, media_subtype, format);
+ if (fi == NULL)
+ continue;
+
+ if (fi->fourcc == last_fourcc)
+ continue;
+ pw_log_info("count:%d fourcc:%.4s last:%.4s", count,
+ (char*)&fi->fourcc, (char*)&last_fourcc);
+
+ arg->flags = fi->format == SPA_VIDEO_FORMAT_ENCODED ? V4L2_FMT_FLAG_COMPRESSED : 0;
+ arg->pixelformat = fi->fourcc;
+ snprintf((char*)arg->description, sizeof(arg->description), "%s",
+ fi->desc ? fi->desc : "Unknown");
+ last_fourcc = fi->fourcc;
+ if (count == arg->index) {
+ found = true;
+ break;
+ }
+ count++;
+ }
+ pw_thread_loop_unlock(file->loop);
+
+ if (!found)
+ return -EINVAL;
+
+ pw_log_info("format: %.4s", (char*)&arg->pixelformat);
+ pw_log_info("flags: %u", arg->type);
+ memset(arg->reserved, 0, sizeof(arg->reserved));
+
+ return 0;
+}
+
+static int vidioc_g_fmt(struct file *file, struct v4l2_format *arg)
+{
+ struct param *p;
+ struct global *g = file->node;
+ int res;
+
+ if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ pw_thread_loop_lock(file->loop);
+ if (file->v4l2_format.fmt.pix.pixelformat != 0) {
+ *arg = file->v4l2_format;
+ } else {
+ struct v4l2_format tmp;
+ bool found = false;
+
+ spa_list_for_each(p, &g->param_list, link) {
+ if (p->id != SPA_PARAM_EnumFormat || p->param == NULL)
+ continue;
+
+ if (param_to_fmt(p->param, &tmp) < 0)
+ continue;
+
+ found = true;
+ break;
+ }
+ if (!found) {
+ res = -EINVAL;
+ goto exit_unlock;
+ }
+ *arg = file->v4l2_format = tmp;
+ }
+ res = 0;
+exit_unlock:
+ pw_thread_loop_unlock(file->loop);
+ return res;
+}
+
+static int score_diff(struct v4l2_format *fmt, struct v4l2_format *tmp)
+{
+ int score = 0, w, h;
+ if (fmt->fmt.pix.pixelformat != tmp->fmt.pix.pixelformat)
+ score += 20000;
+ w = SPA_ABS((int)fmt->fmt.pix.width - (int)tmp->fmt.pix.width);
+ h = SPA_ABS((int)fmt->fmt.pix.height - (int)tmp->fmt.pix.height);
+ return score + (w*w) + (h*h);
+}
+
+static int try_format(struct file *file, struct v4l2_format *fmt)
+{
+ struct param *p;
+ struct global *g = file->node;
+ struct v4l2_format best_fmt = *fmt;
+ int best = -1;
+
+ pw_log_info("in: type: %u", fmt->type);
+ if (fmt->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ pw_log_info("in: format: %.4s", (char*)&fmt->fmt.pix.pixelformat);
+ pw_log_info("in: width: %u", fmt->fmt.pix.width);
+ pw_log_info("in: height: %u", fmt->fmt.pix.height);
+ pw_log_info("in: field: %u", fmt->fmt.pix.field);
+ spa_list_for_each(p, &g->param_list, link) {
+ struct v4l2_format tmp;
+ int score;
+
+ if (p->param == NULL)
+ continue;
+
+ if (p->id != SPA_PARAM_EnumFormat && p->id != SPA_PARAM_Format)
+ continue;
+
+ if (param_to_fmt(p->param, &tmp) < 0)
+ continue;
+
+ score = score_diff(fmt, &tmp);
+ pw_log_debug("check: type: %u", tmp.type);
+ pw_log_debug("check: format: %.4s", (char*)&tmp.fmt.pix.pixelformat);
+ pw_log_debug("check: width: %u", tmp.fmt.pix.width);
+ pw_log_debug("check: height: %u", tmp.fmt.pix.height);
+ pw_log_debug("check: score: %d best:%d", score, best);
+
+ if (p->id == SPA_PARAM_Format) {
+ best_fmt = tmp;
+ break;
+ }
+ if (best == -1 || score < best) {
+ best = score;
+ best_fmt = tmp;
+ }
+ }
+ *fmt = best_fmt;
+ pw_log_info("out: format: %.4s", (char*)&fmt->fmt.pix.pixelformat);
+ pw_log_info("out: width: %u", fmt->fmt.pix.width);
+ pw_log_info("out: height: %u", fmt->fmt.pix.height);
+ pw_log_info("out: field: %u", fmt->fmt.pix.field);
+ pw_log_info("out: size: %u", fmt->fmt.pix.sizeimage);
+ return 0;
+}
+
+static int disconnect_stream(struct file *file)
+{
+ if (file->stream != NULL) {
+ pw_log_info("file:%d disconnect", file->fd);
+ pw_stream_destroy(file->stream);
+ file->stream = NULL;
+ file->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int connect_stream(struct file *file)
+{
+ int res;
+ struct global *g = file->node;
+ struct timespec abstime;
+ const char *error = NULL;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_pod *params[1];
+ struct pw_properties *props;
+
+ params[0] = fmt_to_param(&b, SPA_PARAM_EnumFormat, &file->v4l2_format);
+ if (params[0] == NULL) {
+ res = -EINVAL;
+ goto exit;
+ }
+
+ disconnect_stream(file);
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ res = -errno;
+ goto exit;
+ }
+
+ pw_properties_set(props, PW_KEY_CLIENT_API, "v4l2");
+ pw_properties_setf(props, PW_KEY_APP_NAME, "%s", pw_get_prgname());
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_TYPE) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_TYPE, "Video");
+ if (pw_properties_get(props, PW_KEY_MEDIA_CATEGORY) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, "Capture");
+
+ file->stream = pw_stream_new(file->core, "v4l2 capture", props);
+ if (file->stream == NULL) {
+ res = -errno;
+ goto exit;
+ }
+
+ pw_stream_add_listener(file->stream,
+ &file->stream_listener,
+ &stream_events, file);
+
+
+ file->error = 0;
+
+ pw_stream_connect(file->stream,
+ PW_DIRECTION_INPUT,
+ g->id,
+ PW_STREAM_FLAG_DONT_RECONNECT |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, 1);
+
+ pw_thread_loop_get_time (file->loop, &abstime,
+ DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC);
+
+ while (true) {
+ enum pw_stream_state state = pw_stream_get_state(file->stream, &error);
+
+ if (state == PW_STREAM_STATE_STREAMING)
+ break;
+
+ if (state == PW_STREAM_STATE_ERROR) {
+ res = -EIO;
+ goto exit;
+ }
+ if (file->error < 0) {
+ res = file->error;
+ goto exit;
+ }
+ if (pw_thread_loop_timed_wait_full(file->loop, &abstime) < 0) {
+ res = -ETIMEDOUT;
+ goto exit;
+ }
+ }
+ /* pause stream */
+ res = pw_stream_set_active(file->stream, false);
+exit:
+ return res;
+}
+
+static int vidioc_s_fmt(struct file *file, struct v4l2_format *arg)
+{
+ int res;
+
+ pw_thread_loop_lock(file->loop);
+ if ((res = try_format(file, arg)) < 0)
+ goto exit_unlock;
+
+ file->v4l2_format = *arg;
+
+exit_unlock:
+ pw_thread_loop_unlock(file->loop);
+ return res;
+}
+static int vidioc_try_fmt(struct file *file, struct v4l2_format *arg)
+{
+ int res;
+
+ pw_thread_loop_lock(file->loop);
+ res = try_format(file, arg);
+ pw_thread_loop_unlock(file->loop);
+ return res;
+}
+
+static int vidioc_g_parm(struct file *file, struct v4l2_streamparm *arg)
+{
+ if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ struct param *p;
+
+ pw_thread_loop_lock(file->loop);
+ bool found = false;
+ struct spa_video_info info;
+ int num = 0, denom = 0;
+
+ spa_list_for_each(p, &file->node->param_list, link) {
+ if (p->id != SPA_PARAM_EnumFormat || p->param == NULL)
+ continue;
+
+ if (param_to_info(p->param, &info) < 0)
+ continue;
+
+ switch (info.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ num = info.info.raw.framerate.num;
+ denom = info.info.raw.framerate.denom;
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ num = info.info.mjpg.framerate.num;
+ denom = info.info.mjpg.framerate.denom;
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ num = info.info.h264.framerate.num;
+ denom = info.info.h264.framerate.denom;
+ break;
+ }
+
+ if (num == 0 || denom == 0)
+ continue;
+
+ found = true;
+ break;
+ }
+
+ if (!found) {
+ pw_thread_loop_unlock(file->loop);
+ return -EINVAL;
+ }
+
+ pw_thread_loop_unlock(file->loop);
+
+ spa_zero(*arg);
+ arg->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ arg->parm.capture.capability = V4L2_CAP_TIMEPERFRAME;
+ arg->parm.capture.capturemode = 0;
+ arg->parm.capture.extendedmode = 0;
+ arg->parm.capture.readbuffers = 0;
+ arg->parm.capture.timeperframe.numerator = denom;
+ arg->parm.capture.timeperframe.denominator = num;
+
+ pw_log_info("VIDIOC_G_PARM frametime: %d/%d", num, denom);
+
+ return 0;
+}
+
+// TODO: implement setting parameters
+static int vidioc_s_parm(struct file *file, struct v4l2_streamparm *arg)
+{
+ pw_log_warn("VIDIOC_S_PARM is unimplemented, returning current value");
+ vidioc_g_parm(file, arg);
+ return 0;
+}
+
+static int vidioc_enuminput(struct file *file, struct v4l2_input *arg)
+{
+ uint32_t index = arg->index;
+ spa_zero(*arg);
+ arg->index = index;
+ switch (arg->index) {
+ case 0:
+ spa_scnprintf((char*)arg->name, sizeof(arg->name), "%s", DEFAULT_CARD);
+ arg->type = V4L2_INPUT_TYPE_CAMERA;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+static int vidioc_g_input(struct file *file, int *arg)
+{
+ *arg = 0;
+ return 0;
+}
+static int vidioc_s_input(struct file *file, int *arg)
+{
+ if (*arg != 0)
+ return -EINVAL;
+ return 0;
+}
+
+static int vidioc_g_priority(struct file *file, enum v4l2_priority *arg)
+{
+ *arg = file->priority;
+ pw_log_info("file:%d prio:%d", file->fd, *arg);
+ return 0;
+}
+static int vidioc_s_priority(struct file *file, int fd, enum v4l2_priority *arg)
+{
+ if (*arg > V4L2_PRIORITY_RECORD)
+ return -EINVAL;
+
+ if (file->fd != fd && file->priority > *arg)
+ return -EINVAL;
+
+ pw_log_info("file:%d (%d) prio:%d", file->fd, fd, *arg);
+ file->priority = *arg;
+ return 0;
+}
+
+static int vidioc_reqbufs(struct file *file, int fd, struct v4l2_requestbuffers *arg)
+{
+ int res;
+
+ pw_log_info("count: %u", arg->count);
+ pw_log_info("type: %u", arg->type);
+ pw_log_info("memory: %u", arg->memory);
+#ifdef V4L2_MEMORY_FLAG_NON_COHERENT
+ pw_log_info("flags: %08x", arg->flags);
+#endif
+
+ if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+ if (arg->memory != V4L2_MEMORY_MMAP)
+ return -EINVAL;
+
+ pw_thread_loop_lock(file->loop);
+
+ if (file->n_buffers > 0 && file->reqbufs_fd != fd) {
+ pw_log_info("%u fd:%d != %d", file->n_buffers, file->reqbufs_fd, fd);
+ res = -EBUSY;
+ goto exit_unlock;
+ }
+ if (arg->count == 0) {
+ if (pw_array_get_len(&file->buffer_maps, struct buffer_map) != 0) {
+ pw_log_info("fd:%d have maps", fd);
+ res = -EBUSY;
+ goto exit_unlock;
+ }
+ if (file->running) {
+ pw_log_info("fd:%d running", fd);
+ res = -EBUSY;
+ goto exit_unlock;
+ }
+ res = disconnect_stream(file);
+ file->reqbufs = 0;
+ file->reqbufs_fd = -1;
+ } else {
+ file->reqbufs = arg->count;
+
+ if ((res = connect_stream(file)) < 0)
+ goto exit_unlock;
+
+ arg->count = file->n_buffers;
+ file->reqbufs_fd = fd;
+ }
+#ifdef V4L2_MEMORY_FLAG_NON_COHERENT
+ arg->flags = 0;
+#endif
+#ifdef V4L2_BUF_CAP_SUPPORTS_MMAP
+ arg->capabilities = V4L2_BUF_CAP_SUPPORTS_MMAP;
+#endif
+ memset(arg->reserved, 0, sizeof(arg->reserved));
+
+ pw_log_info("result count: %u", arg->count);
+
+exit_unlock:
+ if (res < 0)
+ pw_log_info("error : %s", spa_strerror(res));
+ pw_thread_loop_unlock(file->loop);
+ return res;
+}
+
+static int vidioc_querybuf(struct file *file, struct v4l2_buffer *arg)
+{
+ int res;
+
+ if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ pw_thread_loop_lock(file->loop);
+ if (arg->index >= file->n_buffers) {
+ res = -EINVAL;
+ goto exit_unlock;
+ }
+ *arg = file->buffers[arg->index].v4l2;
+
+ res = 0;
+
+exit_unlock:
+ pw_thread_loop_unlock(file->loop);
+
+ return res;
+}
+
+static int vidioc_qbuf(struct file *file, struct v4l2_buffer *arg)
+{
+ int res = 0;
+ struct buffer *buf;
+
+ if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+ if (arg->memory != V4L2_MEMORY_MMAP)
+ return -EINVAL;
+
+ pw_thread_loop_lock(file->loop);
+ if (arg->index >= file->n_buffers) {
+ res = -EINVAL;
+ goto exit;
+ }
+ buf = &file->buffers[arg->index];
+
+ if (SPA_FLAG_IS_SET(buf->v4l2.flags, V4L2_BUF_FLAG_QUEUED)) {
+ res = -EINVAL;
+ goto exit;
+ }
+
+ SPA_FLAG_SET(buf->v4l2.flags, V4L2_BUF_FLAG_QUEUED);
+ arg->flags = buf->v4l2.flags;
+
+ pw_stream_queue_buffer(file->stream, buf->buf);
+exit:
+ pw_log_debug("file:%d %d -> %d (%s)", file->fd, arg->index, res, spa_strerror(res));
+ pw_thread_loop_unlock(file->loop);
+
+ return res;
+}
+static int vidioc_dqbuf(struct file *file, int fd, struct v4l2_buffer *arg)
+{
+ int res = 0;
+ struct pw_buffer *b;
+ struct buffer *buf;
+ uint64_t val;
+ struct spa_data *d;
+ struct timespec ts;
+
+ if (arg->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+ if (arg->memory != V4L2_MEMORY_MMAP)
+ return -EINVAL;
+
+ pw_log_debug("file:%d (%d) %d", file->fd, fd,
+ arg->index);
+
+ pw_thread_loop_lock(file->loop);
+ if (arg->index >= file->n_buffers) {
+ res = -EINVAL;
+ goto exit_unlock;
+ }
+
+ while (true) {
+ if (!file->running) {
+ res = -EINVAL;
+ goto exit_unlock;
+ }
+
+ b = pw_stream_dequeue_buffer(file->stream);
+ if (b != NULL)
+ break;
+
+ pw_thread_loop_unlock(file->loop);
+ res = spa_system_eventfd_read(file->l->system, fd, &val);
+ pw_thread_loop_lock(file->loop);
+ if (res < 0)
+ goto exit_unlock;
+ }
+
+
+ buf = b->user_data;
+ d = &buf->buf->buffer->datas[0];
+ SPA_FLAG_CLEAR(buf->v4l2.flags, V4L2_BUF_FLAG_QUEUED);
+
+ SPA_FLAG_UPDATE(buf->v4l2.flags, V4L2_BUF_FLAG_ERROR,
+ SPA_FLAG_IS_SET(d->chunk->flags, SPA_CHUNK_FLAG_CORRUPTED));
+
+ SPA_FLAG_SET(buf->v4l2.flags, V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC);
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ buf->v4l2.timestamp.tv_sec = ts.tv_sec;
+ buf->v4l2.timestamp.tv_usec = ts.tv_nsec / 1000;
+
+ buf->v4l2.field = V4L2_FIELD_NONE;
+ buf->v4l2.bytesused = d->chunk->size;
+ buf->v4l2.sequence = file->sequence++;
+ *arg = buf->v4l2;
+
+exit_unlock:
+ pw_log_debug("file:%d (%d) %d -> %d (%s)", file->fd, fd,
+ arg->index, res, spa_strerror(res));
+ pw_thread_loop_unlock(file->loop);
+ return res;
+}
+
+static int vidioc_streamon(struct file *file, int *arg)
+{
+ int res;
+
+ if (*arg != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ pw_thread_loop_lock(file->loop);
+ if (file->n_buffers == 0) {
+ res = -EINVAL;
+ goto exit_unlock;
+ }
+ if (file->running) {
+ res = 0;
+ goto exit_unlock;
+ }
+ res = pw_stream_set_active(file->stream, true);
+ if (res >= 0)
+ file->running = true;
+exit_unlock:
+ pw_thread_loop_unlock(file->loop);
+
+ pw_log_info("file:%d -> %d (%s)", file->fd, res, spa_strerror(res));
+ return res;
+}
+static int vidioc_streamoff(struct file *file, int *arg)
+{
+ int res;
+ uint32_t i;
+
+ if (*arg != V4L2_BUF_TYPE_VIDEO_CAPTURE)
+ return -EINVAL;
+
+ pw_thread_loop_lock(file->loop);
+ for (i = 0; i < file->n_buffers; i++) {
+ struct buffer *buf = &file->buffers[i];
+ SPA_FLAG_CLEAR(buf->v4l2.flags, V4L2_BUF_FLAG_QUEUED);
+ }
+ if (!file->running) {
+ res = 0;
+ goto exit_unlock;
+ }
+ res = pw_stream_set_active(file->stream, false);
+ file->running = false;
+ file->sequence = 0;
+
+exit_unlock:
+ pw_thread_loop_unlock(file->loop);
+
+ pw_log_info("file:%d -> %d (%s)", file->fd, res, spa_strerror(res));
+ return res;
+}
+
+// Copied from spa/plugins/v4l2/v4l2-utils.c
+// TODO: unify with source
+static struct {
+ uint32_t v4l2_id;
+ uint32_t spa_id;
+} control_map[] = {
+ { V4L2_CID_BRIGHTNESS, SPA_PROP_brightness },
+ { V4L2_CID_CONTRAST, SPA_PROP_contrast },
+ { V4L2_CID_SATURATION, SPA_PROP_saturation },
+ { V4L2_CID_HUE, SPA_PROP_hue },
+ { V4L2_CID_GAMMA, SPA_PROP_gamma },
+ { V4L2_CID_EXPOSURE, SPA_PROP_exposure },
+ { V4L2_CID_GAIN, SPA_PROP_gain },
+ { V4L2_CID_SHARPNESS, SPA_PROP_sharpness },
+};
+
+static uint32_t prop_id_to_control(uint32_t prop_id)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(control_map, c) {
+ if (c->spa_id == prop_id)
+ return c->v4l2_id;
+ }
+ if (prop_id >= SPA_PROP_START_CUSTOM)
+ return prop_id - SPA_PROP_START_CUSTOM;
+ return SPA_ID_INVALID;
+}
+
+static int vidioc_queryctrl(struct file *file, struct v4l2_queryctrl *arg)
+{
+ struct param *p;
+ bool found = false, next = false;
+
+ memset(arg->reserved, 0, sizeof(arg->reserved));
+
+ // TODO: V4L2_CTRL_FLAG_NEXT_COMPOUND
+ if (arg->id & V4L2_CTRL_FLAG_NEXT_CTRL) {
+ pw_log_debug("VIDIOC_QUERYCTRL: 0x%08" PRIx32 " | V4L2_CTRL_FLAG_NEXT_CTRL", arg->id);
+ arg->id = arg->id & ~V4L2_CTRL_FLAG_NEXT_CTRL & ~V4L2_CTRL_FLAG_NEXT_COMPOUND;
+ next = true;
+ }
+ pw_log_debug("VIDIOC_QUERYCTRL: 0x%08" PRIx32, arg->id);
+
+
+ if (file->node == NULL)
+ return -EIO;
+
+ pw_thread_loop_lock(file->loop);
+
+ // FIXME: place found item into a variable. Will fix unordered ctrls
+ spa_list_for_each(p, &file->node->param_list, link) {
+ uint32_t prop_id, ctrl_id, n_vals, choice = SPA_ID_INVALID;
+ const char *prop_description;
+ const struct spa_pod *type, *pod;
+
+ if (p->id != SPA_PARAM_PropInfo || p->param == NULL)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_id, SPA_POD_Id(&prop_id),
+ SPA_PROP_INFO_description, SPA_POD_String(&prop_description)) < 0)
+ continue;
+
+ if ((ctrl_id = prop_id_to_control(prop_id)) == SPA_ID_INVALID)
+ continue;
+
+ if ((next && ctrl_id > arg->id) || (!next && ctrl_id == arg->id)) {
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_type, SPA_POD_PodChoice(&type)) < 0)
+ continue;
+
+ // TODO: support setting controls
+ arg->flags = V4L2_CTRL_FLAG_READ_ONLY;
+ spa_scnprintf((char*)arg->name, sizeof(arg->name), "%s", prop_description);
+
+ // check type and populate range
+ pod = spa_pod_get_values(type, &n_vals, &choice);
+ if (spa_pod_is_int(pod)) {
+ if (n_vals < 4)
+ break;
+ arg->type = V4L2_CTRL_TYPE_INTEGER;
+ int *v = SPA_POD_BODY(pod);
+ arg->default_value = v[0];
+ arg->minimum = v[1];
+ arg->maximum = v[2];
+ arg->step = v[3];
+ }
+ else if (spa_pod_is_bool(pod) && n_vals > 0) {
+ arg->type = V4L2_CTRL_TYPE_BOOLEAN;
+ arg->default_value = SPA_POD_VALUE(struct spa_pod_bool, pod);
+ arg->minimum = 0;
+ arg->maximum = 1;
+ arg->step = 1;
+ }
+ else {
+ break;
+ }
+
+ arg->id = ctrl_id;
+ found = true;
+ pw_log_debug("ctrl 0x%08" PRIx32 " ok", arg->id);
+ break;
+ }
+ }
+
+ pw_thread_loop_unlock(file->loop);
+
+ if (!found) {
+ pw_log_info("not found ctrl 0x%08" PRIx32, arg->id);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int vidioc_g_ctrl(struct file *file, struct v4l2_control *arg)
+{
+ struct param *p;
+ bool found = false;
+
+ pw_log_debug("VIDIOC_G_CTRL: 0x%08" PRIx32, arg->id);
+
+ if (file->node == NULL)
+ return -EIO;
+
+ pw_thread_loop_lock(file->loop);
+
+ spa_list_for_each(p, &file->node->param_list, link) {
+ uint32_t prop_id, ctrl_id, n_vals, choice = SPA_ID_INVALID;
+ const char *prop_description;
+ const struct spa_pod *type, *pod;
+
+ if (p->id != SPA_PARAM_PropInfo || p->param == NULL)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_id, SPA_POD_Id(&prop_id),
+ SPA_PROP_INFO_description, SPA_POD_String(&prop_description)) < 0)
+ continue;
+
+ if ((ctrl_id = prop_id_to_control(prop_id)) == SPA_ID_INVALID)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_type, SPA_POD_PodChoice(&type)) < 0)
+ continue;
+
+ if (ctrl_id == arg->id) {
+ // TODO: support getting true ctrl values instead of defaults
+ pod = spa_pod_get_values(type, &n_vals, &choice);
+ if (spa_pod_is_int(pod)) {
+ if (n_vals < 4)
+ break;
+ int *v = SPA_POD_BODY(pod);
+ arg->value = v[0];
+ }
+ else if (spa_pod_is_bool(pod) && n_vals > 0) {
+ arg->value = SPA_POD_VALUE(struct spa_pod_bool, pod);
+ }
+ else {
+ break;
+ }
+
+ found = true;
+ pw_log_debug("ctrl 0x%08" PRIx32 " ok", arg->id);
+ break;
+ }
+ }
+
+ pw_thread_loop_unlock(file->loop);
+
+ if (!found) {
+ pw_log_info("not found ctrl 0x%08" PRIx32, arg->id);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int vidioc_s_ctrl(struct file *file, struct v4l2_control *arg)
+{
+ struct param *p;
+ bool found = false;
+
+ pw_log_info("VIDIOC_S_CTRL: 0x%08" PRIx32 " 0x%08" PRIx32, arg->id, arg->value);
+
+ if (file->node == NULL)
+ return -EIO;
+
+ pw_thread_loop_lock(file->loop);
+
+ spa_list_for_each(p, &file->node->param_list, link) {
+ uint32_t prop_id, ctrl_id, n_vals, choice = SPA_ID_INVALID;
+ const char *prop_description;
+ const struct spa_pod *type, *pod;
+
+ if (p->id != SPA_PARAM_PropInfo || p->param == NULL)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_id, SPA_POD_Id(&prop_id),
+ SPA_PROP_INFO_description, SPA_POD_String(&prop_description)) < 0)
+ continue;
+
+ if ((ctrl_id = prop_id_to_control(prop_id)) == SPA_ID_INVALID)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_type, SPA_POD_PodChoice(&type)) < 0)
+ continue;
+
+ if (ctrl_id == arg->id) {
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[1];
+ struct spa_pod *param;
+ pod = spa_pod_get_values(type, &n_vals, &choice);
+
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+
+ if (spa_pod_is_int(pod)) {
+ spa_pod_builder_add(&b, prop_id, SPA_POD_Int(arg->value), 0);
+ } else if (spa_pod_is_bool(pod)) {
+ spa_pod_builder_add(&b, prop_id, SPA_POD_Bool(arg->value), 0);
+ } else {
+ // TODO: float and other formats
+ pw_log_info("unknown type");
+ break;
+ }
+
+ param = spa_pod_builder_pop(&b, &f[0]);
+ pw_node_set_param(file->node->proxy, SPA_PARAM_Props, 0, param);
+
+ found = true;
+ pw_log_info("ctrl 0x%08" PRIx32 " set ok", arg->id);
+ break;
+ }
+ }
+
+ pw_thread_loop_unlock(file->loop);
+
+ if (!found) {
+ pw_log_info("not found ctrl 0x%08" PRIx32, arg->id);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int v4l2_ioctl(int fd, unsigned long int request, void *arg)
+{
+ int res;
+ struct file *file;
+ uint32_t flags;
+
+ if ((file = find_file(fd, &flags)) == NULL)
+ return globals.old_fops.ioctl(fd, request, arg);
+
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+ if (arg == NULL && (request & IOC_DIRMASK != IOC_VOID)) {
+#else
+ if (arg == NULL && (_IOC_DIR(request) & (_IOC_WRITE | _IOC_READ))) {
+#endif
+ res = -EFAULT;
+ goto done;
+ }
+
+ if (flags & FD_MAP_DUP)
+ fd = file->fd;
+
+ switch (request & 0xffffffff) {
+ case VIDIOC_QUERYCAP:
+ res = vidioc_querycap(file, (struct v4l2_capability *)arg);
+ break;
+ case VIDIOC_ENUM_FRAMESIZES:
+ res = vidioc_enum_framesizes(file, (struct v4l2_frmsizeenum *)arg);
+ break;
+ case VIDIOC_ENUM_FMT:
+ res = vidioc_enum_fmt(file, (struct v4l2_fmtdesc *)arg);
+ break;
+ case VIDIOC_G_FMT:
+ res = vidioc_g_fmt(file, (struct v4l2_format *)arg);
+ break;
+ case VIDIOC_S_FMT:
+ res = vidioc_s_fmt(file, (struct v4l2_format *)arg);
+ break;
+ case VIDIOC_TRY_FMT:
+ res = vidioc_try_fmt(file, (struct v4l2_format *)arg);
+ break;
+ case VIDIOC_G_PARM:
+ res = vidioc_g_parm(file, (struct v4l2_streamparm *)arg);
+ break;
+ case VIDIOC_S_PARM:
+ res = vidioc_s_parm(file, (struct v4l2_streamparm *)arg);
+ break;
+ case VIDIOC_ENUMINPUT:
+ res = vidioc_enuminput(file, (struct v4l2_input *)arg);
+ break;
+ case VIDIOC_G_INPUT:
+ res = vidioc_g_input(file, (int *)arg);
+ break;
+ case VIDIOC_S_INPUT:
+ res = vidioc_s_input(file, (int *)arg);
+ break;
+ case VIDIOC_G_PRIORITY:
+ res = vidioc_g_priority(file, (enum v4l2_priority *)arg);
+ break;
+ case VIDIOC_S_PRIORITY:
+ res = vidioc_s_priority(file, fd, (enum v4l2_priority *)arg);
+ break;
+ case VIDIOC_REQBUFS:
+ res = vidioc_reqbufs(file, fd, (struct v4l2_requestbuffers *)arg);
+ break;
+ case VIDIOC_QUERYBUF:
+ res = vidioc_querybuf(file, (struct v4l2_buffer *)arg);
+ break;
+ case VIDIOC_QBUF:
+ res = vidioc_qbuf(file, (struct v4l2_buffer *)arg);
+ break;
+ case VIDIOC_DQBUF:
+ res = vidioc_dqbuf(file, fd, (struct v4l2_buffer *)arg);
+ break;
+ case VIDIOC_STREAMON:
+ res = vidioc_streamon(file, (int *)arg);
+ break;
+ case VIDIOC_STREAMOFF:
+ res = vidioc_streamoff(file, (int *)arg);
+ break;
+ // TODO: VIDIOC_QUERY_EXT_CTRL
+ case VIDIOC_QUERYCTRL:
+ res = vidioc_queryctrl(file, (struct v4l2_queryctrl *)arg);
+ break;
+ case VIDIOC_G_CTRL:
+ res = vidioc_g_ctrl(file, (struct v4l2_control *)arg);
+ break;
+ case VIDIOC_S_CTRL:
+ res = vidioc_s_ctrl(file, (struct v4l2_control *)arg);
+ break;
+ default:
+ res = -ENOTTY;
+ break;
+ }
+done:
+ if (res < 0) {
+ errno = -res;
+ res = -1;
+ }
+ pw_log_debug("file:%d fd:%d request:%lx nr:%d arg:%p -> %d (%s)",
+ file->fd, fd, request, (int)_IOC_NR(request), arg,
+ res, strerror(res < 0 ? errno : 0));
+
+ unref_file(file);
+
+ return res;
+}
+
+static void *v4l2_mmap(void *addr, size_t length, int prot,
+ int flags, int fd, off64_t offset)
+{
+ void *res;
+ struct file *file;
+ off64_t id;
+ struct pw_map_range range;
+ struct buffer *buf;
+ struct spa_data *data;
+ uint32_t fl;
+
+ if ((file = find_file(fd, &fl)) == NULL)
+ return globals.old_fops.mmap(addr, length, prot, flags, fd, offset);
+
+ pw_thread_loop_lock(file->loop);
+ if (file->size == 0) {
+ errno = EIO;
+ res = MAP_FAILED;
+ goto error_unlock;
+ }
+ id = offset / file->size;
+ if ((id * file->size) != offset || file->size != length) {
+ errno = EINVAL;
+ res = MAP_FAILED;
+ goto error_unlock;
+ }
+ buf = &file->buffers[id];
+ data = &buf->buf->buffer->datas[0];
+
+ pw_map_range_init(&range, data->mapoffset, data->maxsize, 1024);
+
+ if (!SPA_FLAG_IS_SET(data->flags, SPA_DATA_FLAG_READABLE))
+ prot &= ~PROT_READ;
+ if (!SPA_FLAG_IS_SET(data->flags, SPA_DATA_FLAG_WRITABLE))
+ prot &= ~PROT_WRITE;
+
+ if (data->data == NULL)
+ res = globals.old_fops.mmap(addr, range.size, prot, flags, data->fd, range.offset);
+ else
+ res = data->data;
+
+ add_file_map(file, res);
+ add_buffer_map(file, res, id);
+ SPA_FLAG_SET(buf->v4l2.flags, V4L2_BUF_FLAG_MAPPED);
+
+ pw_log_info("file:%d addr:%p length:%zu prot:%d flags:%d fd:%"PRIi64
+ " offset:%"PRIi64" (%u - %u) -> %p (%s)" ,
+ file->fd, addr, length, prot, flags, data->fd, offset,
+ range.offset, range.size,
+ res, strerror(res == MAP_FAILED ? errno : 0));
+
+error_unlock:
+ pw_thread_loop_unlock(file->loop);
+ unref_file(file);
+ return res;
+}
+
+static int v4l2_munmap(void *addr, size_t length)
+{
+ int res;
+ struct buffer_map *bmap;
+ struct buffer *buf;
+ struct file *file;
+ struct spa_data *data;
+
+ if ((file = remove_file_map(addr)) == NULL)
+ return globals.old_fops.munmap(addr, length);
+
+ pw_thread_loop_lock(file->loop);
+
+ bmap = find_buffer_map(file, addr);
+ if (bmap == NULL) {
+ res = -EINVAL;
+ goto exit_unlock;
+ }
+ buf = &file->buffers[bmap->id];
+ data = &buf->buf->buffer->datas[0];
+
+ if (data->data == NULL)
+ res = globals.old_fops.munmap(addr, length);
+ else
+ res = 0;
+
+ pw_log_info("addr:%p length:%zu -> %d (%s)", addr, length,
+ res, strerror(res < 0 ? errno : 0));
+
+ buf->v4l2.flags &= ~V4L2_BUF_FLAG_MAPPED;
+ remove_buffer_map(file, bmap);
+
+exit_unlock:
+ pw_thread_loop_unlock(file->loop);
+
+ return res;
+}
+
+const struct fops fops = {
+ .openat = v4l2_openat,
+ .dup = v4l2_dup,
+ .close = v4l2_close,
+ .ioctl = v4l2_ioctl,
+ .mmap = v4l2_mmap,
+ .munmap = v4l2_munmap,
+};
+
+static void initialize(void)
+{
+ globals.old_fops.openat = dlsym(RTLD_NEXT, "openat64");
+ globals.old_fops.dup = dlsym(RTLD_NEXT, "dup");
+ globals.old_fops.close = dlsym(RTLD_NEXT, "close");
+ globals.old_fops.ioctl = dlsym(RTLD_NEXT, "ioctl");
+ globals.old_fops.mmap = dlsym(RTLD_NEXT, "mmap64");
+ globals.old_fops.munmap = dlsym(RTLD_NEXT, "munmap");
+
+ pw_init(NULL, NULL);
+ PW_LOG_TOPIC_INIT(v4l2_log_topic);
+
+ pthread_mutex_init(&globals.lock, NULL);
+ pw_array_init(&globals.file_maps, 1024);
+ pw_array_init(&globals.fd_maps, 256);
+}
+
+const struct fops *get_fops(void)
+{
+ static pthread_once_t initialized = PTHREAD_ONCE_INIT;
+ pthread_once(&initialized, initialize);
+ return &fops;
+}
diff --git a/pipewire-v4l2/src/pipewire-v4l2.h b/pipewire-v4l2/src/pipewire-v4l2.h
new file mode 100644
index 0000000..6ac2ae9
--- /dev/null
+++ b/pipewire-v4l2/src/pipewire-v4l2.h
@@ -0,0 +1,38 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+
+struct fops {
+ int (*openat)(int dirfd, const char *path, int oflag, mode_t mode);
+ int (*dup)(int oldfd);
+ int (*close)(int fd);
+ int (*ioctl)(int fd, unsigned long request, void *arg);
+ void *(*mmap)(void *addr, size_t length, int prot,
+ int flags, int fd, off64_t offset);
+ int (*munmap)(void *addr, size_t length);
+};
+
+const struct fops *get_fops(void);
diff --git a/pipewire-v4l2/src/pw-v4l2.in b/pipewire-v4l2/src/pw-v4l2.in
new file mode 100755
index 0000000..248d9ee
--- /dev/null
+++ b/pipewire-v4l2/src/pw-v4l2.in
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+# This file is part of PipeWire.
+#
+# Copyright © 2021 Wim Taymans
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+
+while getopts 'hr:vs:p:' param ; do
+ case $param in
+ r)
+ PIPEWIRE_REMOTE="$OPTARG"
+ export PIPEWIRE_REMOTE
+ ;;
+ v)
+ if [ -z "$PIPEWIRE_DEBUG" ]; then
+ PIPEWIRE_DEBUG=3
+ else
+ PIPEWIRE_DEBUG=$(( PIPEWIRE_DEBUG + 1 ))
+ fi
+ export PIPEWIRE_DEBUG
+ ;;
+ *)
+ echo "$0 - run v4l2 applications on PipeWire"
+ echo " "
+ echo "$0 [options] application [arguments]"
+ echo " "
+ echo "options:"
+ echo " -h show brief help"
+ echo " -r <remote> remote daemon name"
+ echo " -v verbose debug info"
+ exit 0
+ ;;
+ esac
+done
+
+shift $(( OPTIND - 1 ))
+
+if [ "$PW_UNINSTALLED" = 1 ] ; then
+ PW_V4L2_LD_PRELOAD="$PW_BUILDDIR"'/pipewire-v4l2/src/libpw-v4l2.so'
+else
+ PW_V4L2_LD_PRELOAD='@LIBV4L2_PATH@/libpw-v4l2.so'
+fi
+
+if [ "$LD_PRELOAD" = "" ] ; then
+ LD_PRELOAD="$PW_V4L2_LD_PRELOAD"
+else
+ LD_PRELOAD="$LD_PRELOAD $PW_V4L2_LD_PRELOAD"
+fi
+
+export LD_PRELOAD
+
+exec "$@"
diff --git a/pipewire-v4l2/src/v4l2-func.c b/pipewire-v4l2/src/v4l2-func.c
new file mode 100644
index 0000000..c28e834
--- /dev/null
+++ b/pipewire-v4l2/src/v4l2-func.c
@@ -0,0 +1,150 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+/*
+ * We need to export open* etc., but _FORTIFY_SOURCE defines conflicting
+ * always_inline versions. Disable _FORTIFY_SOURCE for this file, so we
+ * can define our overrides.
+ */
+#ifdef _FORTIFY_SOURCE
+#undef _FORTIFY_SOURCE
+#endif
+
+#include <stdio.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "pipewire-v4l2.h"
+
+#define SPA_EXPORT __attribute__((visibility("default")))
+
+#define extract_va_arg(type, arg, last) \
+{ \
+ va_list ap; \
+ va_start(ap, last); \
+ arg = va_arg(ap, type); \
+ va_end(ap); \
+}
+
+SPA_EXPORT int open(const char *path, int oflag, ...)
+{
+ mode_t mode = 0;
+ if (oflag & O_CREAT || oflag & O_TMPFILE)
+ extract_va_arg(mode_t, mode, oflag);
+
+ return get_fops()->openat(AT_FDCWD, path, oflag, mode);
+}
+
+/* _FORTIFY_SOURCE redirects open to __open_2 */
+SPA_EXPORT int __open_2(const char *path, int oflag)
+{
+ return open(path, oflag);
+}
+
+#ifndef open64
+SPA_EXPORT int open64(const char *path, int oflag, ...)
+{
+ mode_t mode = 0;
+ if (oflag & O_CREAT || oflag & O_TMPFILE)
+ extract_va_arg(mode_t, mode, oflag);
+
+ return get_fops()->openat(AT_FDCWD, path, oflag | O_LARGEFILE, mode);
+}
+
+SPA_EXPORT int __open64_2(const char *path, int oflag)
+{
+ return open(path, oflag);
+}
+#endif
+
+SPA_EXPORT int openat(int dirfd, const char *path, int oflag, ...)
+{
+ mode_t mode = 0;
+ if (oflag & O_CREAT || oflag & O_TMPFILE)
+ extract_va_arg(mode_t, mode, oflag);
+
+ return get_fops()->openat(dirfd, path, oflag, mode);
+}
+
+SPA_EXPORT int __openat_2(int dirfd, const char *path, int oflag)
+{
+ return openat(dirfd, path, oflag);
+}
+
+#ifndef openat64
+SPA_EXPORT int openat64(int dirfd, const char *path, int oflag, ...)
+{
+ mode_t mode = 0;
+ if (oflag & O_CREAT || oflag & O_TMPFILE)
+ extract_va_arg(mode_t, mode, oflag);
+
+ return get_fops()->openat(dirfd, path, oflag | O_LARGEFILE, mode);
+}
+
+SPA_EXPORT int __openat64_2(int dirfd, const char *path, int oflag)
+{
+ return openat(dirfd, path, oflag);
+}
+#endif
+
+SPA_EXPORT int dup(int oldfd)
+{
+ return get_fops()->dup(oldfd);
+}
+
+SPA_EXPORT int close(int fd)
+{
+ return get_fops()->close(fd);
+}
+
+SPA_EXPORT void *mmap(void *addr, size_t length, int prot, int flags,
+ int fd, off_t offset)
+{
+ return get_fops()->mmap(addr, length, prot, flags, fd, offset);
+}
+
+#ifndef mmap64
+SPA_EXPORT void *mmap64(void *addr, size_t length, int prot, int flags,
+ int fd, off64_t offset)
+{
+ return get_fops()->mmap(addr, length, prot, flags, fd, offset);
+}
+#endif
+
+SPA_EXPORT int munmap(void *addr, size_t length)
+{
+ return get_fops()->munmap(addr, length);
+}
+
+SPA_EXPORT int ioctl(int fd, unsigned long int request, ...)
+{
+ void *arg;
+ extract_va_arg(void *, arg, request);
+ return get_fops()->ioctl(fd, request, arg);
+}
diff --git a/po/LINGUAS b/po/LINGUAS
new file mode 100644
index 0000000..2532833
--- /dev/null
+++ b/po/LINGUAS
@@ -0,0 +1,53 @@
+af
+as
+be
+bg
+bn_IN
+ca
+cs
+da
+de_CH
+de
+el
+es
+fi
+fr
+gl
+gu
+he
+hi
+hr
+hu
+id
+it
+ja
+ka
+kk
+kn
+ko
+lt
+ml
+mr
+my
+nl
+nn
+oc
+or
+pa
+pl
+pt_BR
+pt
+ro
+ru
+sk
+sr@latin
+sr
+sv
+ta
+te
+tr
+uk
+zh_CN
+zh_TW
+eo
+si
diff --git a/po/POTFILES.in b/po/POTFILES.in
new file mode 100644
index 0000000..96c8261
--- /dev/null
+++ b/po/POTFILES.in
@@ -0,0 +1,25 @@
+src/daemon/pipewire.c
+src/daemon/pipewire.desktop.in
+src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
+src/modules/module-protocol-pulse/modules/module-tunnel-source.c
+src/modules/module-fallback-sink.c
+src/modules/module-pulse-tunnel.c
+src/modules/module-zeroconf-discover.c
+src/tools/pw-cat.c
+src/tools/pw-cat.c
+src/tools/pw-cli.c
+src/tools/pw-dot.c
+src/tools/pw-dump.c
+src/tools/pw-link.c
+src/tools/pw-loopback.c
+src/tools/pw-metadata.c
+src/tools/pw-mididump.c
+src/tools/pw-mon.c
+src/tools/pw-profiler.c
+src/tools/pw-top.c
+spa/plugins/alsa/acp/acp.c
+spa/plugins/alsa/acp/alsa-mixer.c
+spa/plugins/alsa/acp/alsa-util.c
+spa/plugins/alsa/acp/channelmap.h
+spa/plugins/alsa/acp/compat.c
+spa/plugins/bluez5/bluez5-device.c
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
new file mode 100644
index 0000000..d6b7692
--- /dev/null
+++ b/po/POTFILES.skip
@@ -0,0 +1 @@
+src/daemon/systemd/system/pipewire.service.in
diff --git a/po/af.po b/po/af.po
new file mode 100644
index 0000000..4e50323
--- /dev/null
+++ b/po/af.po
@@ -0,0 +1,577 @@
+# Afrikaans translation of PipeWire.
+# This file is distributed under the same license as the PipeWire package.
+# F Wolff <friedel@translate.org.za>, 2019.
+msgid ""
+msgstr ""
+"Project-Id-Version: master\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2019-01-09 12:17+0200\n"
+"Last-Translator: F Wolff <friedel@translate.org.za>\n"
+"Language-Team: translate-discuss-af@lists.sourceforge.net\n"
+"Language: af\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Virtaal 0.7.1\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Ingeboude oudio"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Af"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ongeldig)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Toevoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Dokstasietoevoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Dokstasiemikrofoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Dokstasie se lyn in"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Lyn in"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Mikrofoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Mikrofoon voor"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Mikrofoon agter"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Eksterne mikrofoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Interne mikrofoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Outomatiese versterkingkontrole"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Geen outomatiese versterkingkontrole"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Versterker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Geen versterker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Basversterker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Geen basversterker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Luidspreker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Oorfone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analoë toevoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Dokmikrofoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Kopstukmikrofoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analoë afvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Oorfone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Oorfone, mono-afvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Lyn uit"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analoë mono-afvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Luidsprekers"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Digitale afvoer (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitale toevoer (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Multikanaaltoevoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Multikanaalafvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Speletjieafvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Geselsafvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Geselsafvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Analoë omringklank 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analoë mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Analoë mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Analoë mono"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analoë stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Kopstuk"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Luidspreker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multikanaal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analoë omringklank 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analoë omringklank 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analoë omringklank 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analoë omringklank 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analoë omringklank 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analoë omringklank 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analoë omringklank 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analoë omringklank 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analoë omringklank 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analoë omringklank 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analoë omringklank 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitale stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitale omringklank 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitale omringklank 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitale omringklank 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitale stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitale omringklank 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analoë monodupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analoë stereodupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digitale stereodupleks (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Multikanaaldupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Stereodupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s-afvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s-toevoer"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Oorfone"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Draagbaar"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Kar"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "Hoëtroustel"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Foon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Bluetooth-toevoer"
diff --git a/po/as.po b/po/as.po
new file mode 100644
index 0000000..ba8f64a
--- /dev/null
+++ b/po/as.po
@@ -0,0 +1,619 @@
+# translation of pipewire.master-tx.as.po to Assamese
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Amitakhya Phukan <aphukan@fedoraproject.org>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.as\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:52+0000\n"
+"Last-Translator: Amitakhya Phukan <aphukan@fedoraproject.org>\n"
+"Language-Team: Assamese <>\n"
+"Language: as\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Lokalize 0.2\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "আভ্যন্তৰীণ অ'ডিঅ'"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "মোডেম"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "বন্ধ"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(অবৈধ)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "নিবেশ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "ডকিং স্টেছনৰ পৰা নিবেশ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "ডকিং স্টেছনৰ মাইক্ৰোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "ডকিং স্টেছনৰ পৰা নিবেশ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "মাইক্ৰোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "ডকিং স্টেছনৰ মাইক্ৰোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "মাইক্ৰোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "বহিস্থিত মাইক্ৰোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "অভ্যন্তৰীণ মাইক্ৰোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "ৰেডিঅ'"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "ভিডিঅ'"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "স্বয়ংক্ৰিয় গেইন নিয়ন্ত্ৰণ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "স্বয়ংক্ৰিয় গেইন নিয়ন্ত্ৰণ প্ৰয়োগ কৰা ন'হ'ব"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "বুস্ট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "বুস্ট প্ৰয়োগ কৰা ন'হ'ব"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "বিবৰ্ধক"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "বিবৰ্ধন প্ৰয়োগ কৰা ন'হ'ব"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "বুস্ট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "বুস্ট প্ৰয়োগ কৰা ন'হ'ব"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+#, fuzzy
+msgid "Headphones"
+msgstr "এনালগ হেড ফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "এনালগ নিবেশ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "ডকিং স্টেছনৰ মাইক্ৰোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "মাইক্ৰোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "এনালগ নিৰ্গম"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "এনালগ হেড ফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "এনালগ মোনো নিৰ্গম"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "লাইন ইন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "এনালগ মোনো নিৰ্গম"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "এনালগ স্টিৰিঅ'"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "ডিজিটেল স্টিৰিঅ' (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "ডিজিটেল স্টিৰিঅ' (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Null ফলাফল"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Null ফলাফল"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Null ফলাফল"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "নিবেশ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "এনালগ ছাৰাউন্ড ৭.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "এনালগ মোনো"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "এনালগ মোনো"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "এনালগ মোনো"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "এনালগ স্টিৰিঅ'"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "মোনো"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "এনালগ স্টিৰিঅ'"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "এনালগ ছাৰাউন্ড ২.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "এনালগ ছাৰাউন্ড ৩.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "এনালগ ছাৰাউন্ড ৩.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "এনালগ ছাৰাউন্ড ৪.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "এনালগ ছাৰাউন্ড ৪.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "এনালগ ছাৰাউন্ড ৫.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "এনালগ ছাৰাউন্ড ৫.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "এনালগ ছাৰাউন্ড ৬.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "এনালগ ছাৰাউন্ড ৬.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "এনালগ ছাৰাউন্ড ৭.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "এনালগ ছাৰাউন্ড ৭.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "ডিজিটেল স্টিৰিঅ' (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ডিজিটেল ছাৰাউন্ড ৪.০ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ডিজিটেল ছাৰাউন্ড ৫.১ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ডিজিটেল ছাৰাউন্ড ৫.১ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "ডিজিটেল স্টিৰিঅ' (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ডিজিটেল ছাৰাউন্ড ৫.১ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "এনালগ মোনো ডুপ্লেক্স"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "এনালগ স্টিৰিঅ' ডুপ্লেক্স"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ডিজিটেল স্টিৰিঅ' ডুপ্লেক্স (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "এনালগ স্টিৰিঅ' ডুপ্লেক্স"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "Null ফলাফল"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "নিবেশ"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() এ এটা বৰ ডাঙৰ মান ঘূৰালে: %lu bytes (%lu ms) ।\n"
+"অতি সম্ভৱ এইটো ALSA চালক '%s' ৰ এটা বাগ । অনুগ্ৰহ কৰি এই সমস্যা ALSA বিকাশকক "
+"জনাওক ।"
+msgstr[1] ""
+"snd_pcm_avail() এ এটা বৰ ডাঙৰ মান ঘূৰালে: %lu bytes (%lu ms) ।\n"
+"অতি সম্ভৱ এইটো ALSA চালক '%s' ৰ এটা বাগ । অনুগ্ৰহ কৰি এই সমস্যা ALSA বিকাশকক "
+"জনাওক ।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() এ এটা বৰ ডাঙৰ মান ঘূৰালে: %li bytes (%s%lu ms) ।\n"
+"অতি সম্ভৱ এইটো ALSA চালক '%s' ৰ এটা বাগ । অনুগ্ৰহ কৰি এই সমস্যা ALSA বিকাশকক "
+"জনাওক ।"
+msgstr[1] ""
+"snd_pcm_delay() এ এটা বৰ ডাঙৰ মান ঘূৰালে: %li bytes (%s%lu ms) ।\n"
+"অতি সম্ভৱ এইটো ALSA চালক '%s' ৰ এটা বাগ । অনুগ্ৰহ কৰি এই সমস্যা ALSA বিকাশকক "
+"জনাওক ।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() এ এটা বৰ ডাঙৰ মান ঘূৰালে: %lu bytes (%lu ms) ।\n"
+"অতি সম্ভৱ এইটো ALSA চালক '%s' ৰ এটা বাগ । অনুগ্ৰহ কৰি এই সমস্যা ALSA বিকাশকক "
+"জনাওক ।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() এ এটা বৰ ডাঙৰ মান ঘূৰালে: %lu bytes (%lu ms) ।\n"
+"অতি সম্ভৱ এইটো ALSA চালক '%s' ৰ এটা বাগ । অনুগ্ৰহ কৰি এই সমস্যা ALSA বিকাশকক "
+"জনাওক ।"
+msgstr[1] ""
+"snd_pcm_mmap_begin() এ এটা বৰ ডাঙৰ মান ঘূৰালে: %lu bytes (%lu ms) ।\n"
+"অতি সম্ভৱ এইটো ALSA চালক '%s' ৰ এটা বাগ । অনুগ্ৰহ কৰি এই সমস্যা ALSA বিকাশকক "
+"জনাওক ।"
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "এনালগ হেড ফোন"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/be.po b/po/be.po
new file mode 100644
index 0000000..e94995f
--- /dev/null
+++ b/po/be.po
@@ -0,0 +1,628 @@
+# Belarusian tanslation of PipeWire
+# Copyright (C) 2016 PipeWire's COPYRIGHT HOLDER
+# This file is distributed under the same license as the PipeWire package.
+#
+# Viktar Vaŭčkievič <victorenator@gmail.com>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2016-07-19 11:06+0300\n"
+"Last-Translator: Viktar Vaŭčkievič <victorenator@gmail.com>\n"
+"Language-Team: Belarusian <>\n"
+"Language: be\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"X-Generator: Lokalize 2.0\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Убудаванае аўдыя"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Мадэм"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Адключаны"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(некарэктнае)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Уваход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Уваход док-станцыі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Мікрафон док-станцыі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Лінейны ўваход док-станцыі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "Мікрафон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Пярэдні мікрафон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Задні мікрафон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Знешні мікрафон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Убудаваны мікрафон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Радыё"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Відэа"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Аўтаматычнае рэгуляванне ўзмацнення"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Без аўтаматычнага рэгулявання ўзмацнення"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Узмацненне"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Без ўзмацнення"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Узмацняльнік"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Без ўзмацняльніка"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Узмацненне басоў"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Без ўзмацнення басоў"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Дынамік"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Навушнікі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Аналагавы ўваход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Мікрафон док-станцыі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Мікрафон гарнітуры"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Аналагавы выхад"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Навушнікі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Аналагавы монавыхад"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Лінейны выхад"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Аналагавы монавыхад"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Дынамікі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Лічбавы выхад (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Лічбавы ўваход (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Шматканальны ўваход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Шматканальны выхад"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "%s выхад"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "%s выхад"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "%s уваход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Віртуальны абʼёмны прыёмнік"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Аналагавы мона"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Аналагавы мона"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Аналагавы мона"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Аналагавы стэрэа"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Мона"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Гарнітура"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Дынамік"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Шматканальны"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Аналагавы абʼёмны 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Аналагавы абʼёмны 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Аналагавы абʼёмны 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Аналагавы абʼёмны 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Аналагавы абʼёмны 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Аналагавы абʼёмны 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Аналагавы абʼёмны 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Аналагавы абʼёмны 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Аналагавы абʼёмны 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Аналагавы абʼёмны 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Аналагавы абʼёмны 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Лічбавы стэрэа (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Лічбавы абʼёмны 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Лічбавы абʼёмны 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Лічбавы абʼёмны 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Лічбавы стэрэа (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Лічбавы абʼёмны 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Аналагавы мона дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Аналагавы стэрэа дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Лічбавы стэрэа дуплекс (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Шматканальны дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Аналагавы стэрэа дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s выхад"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s уваход"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Выклік snd_pcm_avail() вярнуў выключна вялікае значэнне: %lu байтаў (%lu "
+"мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+msgstr[1] ""
+"Выклік snd_pcm_avail() вярнуў выключна вялікае значэнне: %lu байтаў (%lu "
+"мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+msgstr[2] ""
+"Выклік snd_pcm_avail() вярнуў выключна вялікае значэнне: %lu байтаў (%lu "
+"мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Выклік snd_pcm_delay() вярнуў выключна вялікае значэнне: %li байтаў (%s%lu "
+"мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+msgstr[1] ""
+"Выклік snd_pcm_delay() вярнуў выключна вялікае значэнне: %li байтаў (%s%lu "
+"мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+msgstr[2] ""
+"Выклік snd_pcm_delay() вярнуў выключна вялікае значэнне: %li байтаў (%s%lu "
+"мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"Выклік snd_pcm_avail_delay() вярнуў дзіўнае значэнне: затрымка %lu меншая за "
+"даступную %lu.\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Выклік snd_pcm_mmap_begin() вярнуў выключна вялікае значэнне: %lu байтаў "
+"(%lu мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+msgstr[1] ""
+"Выклік snd_pcm_mmap_begin() вярнуў выключна вялікае значэнне: %lu байтаў "
+"(%lu мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+msgstr[2] ""
+"Выклік snd_pcm_mmap_begin() вярнуў выключна вялікае значэнне: %lu байтаў "
+"(%lu мс).\n"
+"Хутчэй за ўсё, гэта памылка ў ALSA-драйверы «%s». Калі ласка, паведаміце аб "
+"гэтым распрацоўнікам ALSA."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Хэндс-фры"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Навушнік"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Партатыўная сістэма"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Аўтамабільная сістэма"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "Hi-Fi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Тэлефон"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Bluetooth-уваход"
diff --git a/po/bg.po b/po/bg.po
new file mode 100644
index 0000000..bde7d24
--- /dev/null
+++ b/po/bg.po
@@ -0,0 +1,569 @@
+# Valentin Laskov <laskov@festa.bg>, 2016. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2020-10-15 21:30+0000\n"
+"Last-Translator: Emanuil Novachev <em.novachev@gmail.com>\n"
+"Language-Team: Bulgarian <https://translate.fedoraproject.org/projects/"
+"pipewire/pipewire/bg/>\n"
+"Language: bg\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: Weblate 4.2.2\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr ""
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr ""
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/bn_IN.po b/po/bn_IN.po
new file mode 100644
index 0000000..54dc025
--- /dev/null
+++ b/po/bn_IN.po
@@ -0,0 +1,618 @@
+# translation of pipewire.master-tx.bn_IN.po to Bengali INDIA
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Runa Bhattacharjee <runab@redhat.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.bn_IN\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:52+0000\n"
+"Last-Translator: Runa Bhattacharjee <runab@redhat.com>\n"
+"Language-Team: Bengali INDIA <anubad@lists.ankur.org.in>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "অভ্যন্তরীণ অডিও"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "মোডেম"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "বন্ধ"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(অবৈধ)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "ইনপুট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "ডকিং স্টেশন থেকে ইনপুট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "ডকিং স্টেশনের মাইক্রোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "ডকিং স্টেশন থেকে ইনপুট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "মাইক্রোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "ডকিং স্টেশনের মাইক্রোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "মাইক্রোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "বহিস্থিত মাইক্রোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "অভ্যন্তরীণ মাইক্রোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "রেডিও"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "ভিডিও"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "স্বয়ংক্রিয় গেইন নিয়ন্ত্রণ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "স্বয়ংক্রিয় গেইন নিয়ন্ত্রণ প্রয়োগ করা হবে না"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "বুস্ট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "বুস্ট প্রয়োগ করা হবে না"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "বিবর্ধক"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "বিবর্ধন প্রয়োগ করা হবে না"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "বুস্ট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "বুস্ট প্রয়োগ করা হবে না"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "অ্যানালগ হেড-ফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "অ্যানালগ ইনপুট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "ডকিং স্টেশনের মাইক্রোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "মাইক্রোফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "অ্যানালগ আউটপুট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "অ্যানালগ হেড-ফোন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "অ্যানালগ মোনো আউটপুট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "লাইন-ইন"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "অ্যানালগ মোনো আউটপুট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "অ্যানালগ স্টিরিও"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "ডিজিট্যাল স্টিরিও (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "ডিজিট্যাল স্টিরিও (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Null ফলাফল"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Null ফলাফল"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Null ফলাফল"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "ইনপুট"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "অ্যানালগ সারাউন্ড ৭.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "অ্যানালগ মোনো"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "অ্যানালগ মোনো"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "অ্যানালগ মোনো"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "অ্যানালগ স্টিরিও"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "মোনো"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "অ্যানালগ স্টিরিও"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "অ্যানালগ সারাউন্ড ২.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "অ্যানালগ সারাউন্ড ৩.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "অ্যানালগ সারাউন্ড ৩.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "অ্যানালগ সারাউন্ড ৪.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "অ্যানালগ সারাউন্ড ৪.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "অ্যানালগ সারাউন্ড ৫.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "অ্যানালগ সারাউন্ড ৫.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "অ্যানালগ সারাউন্ড ৬.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "অ্যানালগ সারাউন্ড ৬.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "অ্যানালগ সারাউন্ড ৭.০"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "অ্যানালগ সারাউন্ড ৭.১"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "ডিজিট্যাল স্টিরিও (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ডিজিট্যাল সারাউন্ড ৪.০ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ডিজিট্যাল সারাউন্ড ৫.১ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ডিজিট্যাল সারাউন্ড ৫.১ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "ডিজিট্যাল স্টিরিও (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ডিজিট্যাল সারাউন্ড ৫.১ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "অ্যানালগ মোনো ডুপ্লে"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "অ্যানালগ স্টিরিও ডুপ্লে"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ডিজিট্যাল স্টিরিও ডুপ্লে (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "অ্যানালগ স্টিরিও ডুপ্লে"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "Null ফলাফল"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "ইনপুট"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() থেকে প্রাপ্ত মান অত্যাধিক বড়: %lu বাইট (%lu ms)।\n"
+"সম্ভবত এটি ALSA ড্রাইভার '%s'-র একটি বাগ। অনুগ্রহ করে এই সমস্যা সম্বন্ধে ALSA "
+"ডিভেলপরদের সূচিত করুন।"
+msgstr[1] ""
+"snd_pcm_avail() থেকে প্রাপ্ত মান অত্যাধিক বড়: %lu বাইট (%lu ms)।\n"
+"সম্ভবত এটি ALSA ড্রাইভার '%s'-র একটি বাগ। অনুগ্রহ করে এই সমস্যা সম্বন্ধে ALSA "
+"ডিভেলপরদের সূচিত করুন।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() থেকে প্রাপ্ত মান অত্যাধিক বড়: %li বাইট (%s%lu ms)।\n"
+"সম্ভবত এটি ALSA ড্রাইভার '%s'-র একটি বাগ। অনুগ্রহ করে এই সমস্যা সম্বন্ধে ALSA "
+"ডিভেলপরদের সূচিত করুন।"
+msgstr[1] ""
+"snd_pcm_delay() থেকে প্রাপ্ত মান অত্যাধিক বড়: %li বাইট (%s%lu ms)।\n"
+"সম্ভবত এটি ALSA ড্রাইভার '%s'-র একটি বাগ। অনুগ্রহ করে এই সমস্যা সম্বন্ধে ALSA "
+"ডিভেলপরদের সূচিত করুন।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() থেকে প্রাপ্ত মান অত্যাধিক বড়: %lu বাইট (%lu ms)।\n"
+"সম্ভবত এটি ALSA ড্রাইভার '%s'-র একটি বাগ। অনুগ্রহ করে এই সমস্যা সম্বন্ধে ALSA "
+"ডিভেলপরদের সূচিত করুন।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() থেকে প্রাপ্ত মান অত্যাধিক বড়: %lu বাইট (%lu ms)।\n"
+"সম্ভবত এটি ALSA ড্রাইভার '%s'-র একটি বাগ। অনুগ্রহ করে এই সমস্যা সম্বন্ধে ALSA "
+"ডিভেলপরদের সূচিত করুন।"
+msgstr[1] ""
+"snd_pcm_mmap_begin() থেকে প্রাপ্ত মান অত্যাধিক বড়: %lu বাইট (%lu ms)।\n"
+"সম্ভবত এটি ALSA ড্রাইভার '%s'-র একটি বাগ। অনুগ্রহ করে এই সমস্যা সম্বন্ধে ALSA "
+"ডিভেলপরদের সূচিত করুন।"
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "অ্যানালগ হেড-ফোন"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/ca.po b/po/ca.po
new file mode 100644
index 0000000..6a9dc9f
--- /dev/null
+++ b/po/ca.po
@@ -0,0 +1,619 @@
+# Catalan translation of pipewire by Softcatalà
+# Copyright (C) 2008 Free Software Foundation
+# This file is distributed under the same license as the pipewire package.
+#
+# Xavier Conde Rueda <xavi.conde@gmail.com>, 2008.
+# Agustí Grau <fletxa@gmail.com>, 2009.
+# Judith Pintó Subirada <judithp@gmail.com>
+# Jordi Mas i Herǹandez, <jmas@softcatala.org>, 2022
+#
+# This file is translated according to the glossary and style guide of
+# Softcatalà. If you plan to modify this file, please read first the page
+# of the Catalan translation team for the Fedora project at:
+# http://www.softcatala.org/projectes/fedora/
+# and contact the previous translator.
+#
+# Aquest fitxer s'ha de traduir d'acord amb el recull de termes i la guia
+# d'estil de Softcatalà. Si voleu modificar aquest fitxer, llegiu si
+# us plau la pàgina de catalanització del projecte Fedora a:
+# http://www.softcatala.org/projectes/fedora/
+# i contacteu l'anterior traductor/a.
+# Josep Torné Llavall <josep.torne@gmail.com>, 2009, 2012.
+# Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2016. #zanata
+# Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata
+# Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2017. #zanata
+# Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2019. #zanata
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2022-09-01 19:24+0000\n"
+"Last-Translator: Jordi Mas i Herǹandez, <jmas@softcatala.org>,\n"
+"Language-Team: Catalan <fedora@softcatala.net>\n"
+"Language: ca\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [opcions]\n"
+" -h, --help Mostra aquesta ajuda\n"
+" --version Mostra la versió\n"
+" -c, --config Carrega la configuració (predeterminada %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Sistema multimèdia PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Inicia el sistema multimèdia PipeWire"
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Àudio intern"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Mòdem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr "Dispositiu desconegut"
+
+#: src/tools/pw-cat.c:991
+#, 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 [opcions] <fitxer>\n"
+" -h, --help Mostra aquesta ajuda\n"
+" --version Mostra la versió\n"
+" -v, --verbose Habilita les operacions detallades\n"
+
+#: src/tools/pw-cat.c:998
+#, c-format, fuzzy
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+"-R, --remote Nom del dimoni remot\n"
+" --media-type Estableix el tipus de mitjà (per defecte %s)\n"
+" --media-category Estableix la categoria dels mitjans (per defecte %s)\n"
+" --media-role Estableix el rol del mitjà (per defecte %s)\n"
+" --target Estableix l'objectiu del node (per defecte %s)\n"
+" 0 vol dir que no enllaça\n"
+" --latency Estableix latència del node (per defecte %s)\n"
+" Xunit (unitat = s, ms, us, ns)\n"
+" o mostres directes (256)\n"
+" la taxa és la del fitxer d'origen\n"
+" --list-targets Llista d'objectius disponibles per a --target"
+
+#: src/tools/pw-cat.c:1016
+#, c-format, fuzzy
+msgid ""
+" --rate Sample rate (req. for rec) (default %u)\n"
+" --channels Number of channels (req. for rec) (default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", \"surround-51\",... or\n"
+" comma separated list of channel names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) (default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default %d)\n"
+"\n"
+msgstr ""
+"--rate Freqüència de mostreig (req. per rec) (predeterminat %u)\n"
+" --channels Nombre de canals (req. per rec) (predeterminat %u)\n"
+" --channel-map Mapa de canals\n"
+" un dels següents: \"estèreo\", \"surround-51\",... o\n"
+" Llista separada per comes dels noms dels canals: per exemple. \"FL,FR\"\n"
+" --format Format de mostra %s (req. per a rec) (predeterminat %s)\n"
+" --volume Volum de flux 0-1.0 (predeterminat %.3f)\n"
+" -q --qualitat Remostrador de qualitat (0 - 15) (per defecte %d)"
+
+#: src/tools/pw-cat.c:1033
+#, fuzzy
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+"-p, --playback Mode de reproducció\n"
+" -r, --record mode d'enregistrament\n"
+" -m, --midi Mode MIDI"
+
+#: src/tools/pw-cli.c:2932
+#, c-format, fuzzy
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s ]opcions] ]ordre]\n"
+" -h, --help Mostra aquesta ajuda\n"
+" --version Mostra la versió\n"
+" -d, --daemon Inicia com a dimoni (fals predeterminat)\n"
+" -r, --remote Nom del dimoni remot"
+
+#: spa/plugins/alsa/acp/acp.c:290
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:411 spa/plugins/alsa/acp/alsa-mixer.c:4704
+#: spa/plugins/bluez5/bluez5-device.c:1000
+msgid "Off"
+msgstr "Inactiu"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(incorrecte)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Entrada de l'estació d'acoblament"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Micròfon de l'estació d'acoblament"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Entrada de línia de l'estació d'acoblament"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Entrada de línia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Micròfon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Micròfon frontal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Micròfon posterior"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Micròfon extern"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Micròfon intern"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Ràdio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Vídeo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Control de guany automàtic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Sense control de guany automàtic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Accentuació"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Sense accentuació"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Sense amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Sense accentuació dels baixos"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Sense accentuació dels baixos"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Altaveu"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Auriculars"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Entrada analògica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Micròfon de l'acoblador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Micròfon d'auriculars"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Sortida analògica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Auriculars 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Sortida mono dels auriculars"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Sortida de línia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Sortida mono analògica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Altaveus"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Sortida digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Entrada digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Entrada multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Sortida multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Sortida del joc"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Sortida del xat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "Entrada del xat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Envoltant virtual 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Mono analògic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Mono analògic (esquerra)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Mono analògic (dreta)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * 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
+msgid "Analog Stereo"
+msgstr "Estèreo analògic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+msgid "Stereo"
+msgstr "Estèreo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4540
+#: spa/plugins/alsa/acp/alsa-mixer.c:4698
+#: spa/plugins/bluez5/bluez5-device.c:1135
+msgid "Headset"
+msgstr "Auriculars"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "Altaveu del telèfon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Envoltant analògic 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Envoltant analògic 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Envoltant analògic 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Envoltant analògic 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Envoltant analògic 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Envoltant analògic 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Envoltant analògic 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Envoltant analògic 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Envoltant analògic 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Envoltant analògic 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Envoltant analògic 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Estèreo digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Envoltant digital 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Envoltant digital 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Envoltant digital 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Estèreo digital (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Envoltant digital 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "Xat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Joc"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Dúplex mono analògic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Dúplex estèreo analògic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Dúplex estèreo digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Dúplex Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Dúplex estèreo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Xat mono + 7.1 envoltant"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "Sortida %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "Entrada %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
+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() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
+msgstr[1] ""
+"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
+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() ha retornat un valor excepcionalment gran: %li bytes (%s%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
+msgstr[1] ""
+"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li bytes (%s%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail %lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
+msgstr ""
+"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador d'ALSA «%s». Informeu d'aquest problema als desenvolupadors d'ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."
+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() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n"
+"Probablement es tracta d'un error del controlador de l'ALSA '%s'. Informeu d'aquest incident als desenvolupadors de l'ALSA."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Passarel·la d'àudio (A2DP Source & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Reproducció d'alta fidelitat (Sink A2DP, còdec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Dúplex d'alta fidelitat (A2DP Source/Sink, còdec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Reproducció d'alta fidelitat (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Dúplex d'alta fidelitat (A2DP Source/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Unitat d'ariculars pel cap (HSP/HFP, còdec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Unitat d'ariculars pel cap (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Mans lliures"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Auricular"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Portable"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Cotxe"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telèfon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/cs.po b/po/cs.po
new file mode 100644
index 0000000..b148320
--- /dev/null
+++ b/po/cs.po
@@ -0,0 +1,731 @@
+# Czech translation of pipewire.
+# Copyright (C) 2008, 2009 the author(s) of pipewire.
+# This file is distributed under the same license as the pipewire package.
+# Petr Kovar <pknbe@volny.cz>, 2008, 2009, 2012.
+# Daniel Rusek <mail@asciiwolf.com>, 2018.
+# Marek Černocký <marek@manet.cz>, 2018.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-09-15 15:26+0000\n"
+"PO-Revision-Date: 2022-10-21 16:44+0200\n"
+"Last-Translator: Daniel Rusek <mail@asciiwolf.com>\n"
+"Language-Team: čeština <gnome-cs-list@gnome.org>\n"
+"Language: cs\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+"X-Generator: Poedit 3.1.1\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [volby]\n"
+" -h, --help Zobrazit tuto nápovědu\n"
+" --version Zobrazit verzi\n"
+" -c, --config Načíst konfiguraci (výchozí je %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Multimediální systém PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Spustit multimediální systém PipeWire"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Tunel do %s/%s"
+
+#: src/modules/module-fallback-sink.c:51
+#| msgid "Game Output"
+msgid "Dummy Output"
+msgstr "Předstíraný výstup"
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Tunel pro %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Neznámé zařízení"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s na %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s na %s"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [volby] [<soubor>|-]\n"
+" -h, --help Zobrazit tuto nápovědu\n"
+" --version Zobrazit verzi\n"
+" -v, --verbose Povolit podrobné operace\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote Název vzdáleného démonu\n"
+" --media-type Nastavit typ médií (výchozí je %s)\n"
+" --media-category Nastavit kategorii médií (výchozí je "
+"%s)\n"
+" --media-role Nastavit roli médií (výchozí je %s)\n"
+" --target Nastavit cíl uzlu (výchozí je %s)\n"
+" 0 znamená nepropojovat\n"
+" --latency Nastavit latenci uzlu (výchozí je "
+"%s)\n"
+" Xjednotka (jednotka = s, ms, us, "
+"ns)\n"
+" nebo přímé vzorky (256)\n"
+" frekvence je stejná jako u "
+"zdrojového souboru\n"
+" -P --properties Nastavit vlastnosti uzlu\n"
+"\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Vzorkovací frekvence (vyžad. pro "
+"rec) (výchozí je %u)\n"
+" --channels Počet kanálů (vyžad. pro rec) "
+"(výchozí je %u)\n"
+" --channel-map Mapa kanálů\n"
+" jedno z: \"stereo\", "
+"\"surround-51\",... nebo\n"
+" čárkami oddělený seznam názvů "
+"kanálů: např. \"FL,FR\"\n"
+" --format Formát vzorku %s (vyžad. pro rec) "
+"(výchozí je %s)\n"
+" --volume Hlasitost streamu 0-1.0 (výchozí je "
+"%.3f)\n"
+" -q --quality Kvalita resampleru (0 - 15) (výchozí "
+"je %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Playback mód\n"
+" -r, --record Recording mód\n"
+" -m, --midi Midi mód\n"
+" -d, --dsd DSD mód\n"
+"\n"
+
+#: src/tools/pw-cli.c:2255
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [volby] [příkaz]\n"
+" -h, --help Zobrazit tuto nápovědu\n"
+" --version Zobrazit verzi\n"
+" -d, --daemon Spustit jako démon (výchozí je "
+"false)\n"
+" -r, --remote Název vzdáleného démonu\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
+msgid "Off"
+msgstr "Vypnuto"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Vstup dokovací stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Mikrofon dokovací stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Linkový vstup dokovací stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Linkový vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1454
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Přední mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Zadní mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Externí mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Interní mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "Rádio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "Obraz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "Automatické řízení zesílení"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Bez automatického řízení zesílení"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Zdůraznění"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Bez zdůraznění"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Zesilovač"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Bez zesilovače"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Zdůraznění basů"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Bez zdůraznění basů"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1460
+msgid "Speaker"
+msgstr "Reproduktor"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Sluchátka"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Analogový vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Dokovací mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Mikrofon náhlavní soupravy"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Analogový výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Sluchátka 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Sluchátkový výstup mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Linkový výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Analogový výstup mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Reproduktory"
+
+#: 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 "Digitální výstup (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitální vstup (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Vícekanálový vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Vícekanálový výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Herní výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Komunikační výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Komunikační vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Virtuální surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Analogové mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Analogové mono (levé)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Analogové mono (pravé)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Analogové stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+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:1442
+msgid "Headset"
+msgstr "Náhlavní souprava"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Hlasitý odposlech"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Více kanálů"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Analogový Surround 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Analogový Surround 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Analogový Surround 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Analogový Surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Analogový Surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Analogový Surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Analogový Surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Analogový Surround 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Analogový Surround 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Analogový Surround 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Analogový Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitální stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitální Surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitální Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitální Surround 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitální stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitální Surround 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Chat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Hra"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Analogové duplexní mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Analogové duplexní stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digitální duplexní stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Vícekanálový duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Duplexní stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono Chat + 7.1 Surround"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "Výstup %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "Vstup %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"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] ""
+"Volání snd_pcm_avail() vrátilo hodnotu, která je nezvykle vysoká: %lu bajtů "
+"(%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+msgstr[1] ""
+"Volání snd_pcm_avail() vrátilo hodnotu, která je nezvykle vysoká: %lu bajtů "
+"(%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+msgstr[2] ""
+"Volání snd_pcm_avail() vrátilo hodnotu, která je nezvykle vysoká: %lu bajtů "
+"(%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1253
+#, 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] ""
+"Volání snd_pcm_delay() vrátilo hodnotu, která je nezvykle vysoká: %li bajtů "
+"(%s%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+msgstr[1] ""
+"Volání snd_pcm_delay() vrátilo hodnotu, která je nezvykle vysoká: %li bajtů "
+"(%s%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+msgstr[2] ""
+"Volání snd_pcm_delay() vrátilo hodnotu, která je nezvykle vysoká: %li bajtů "
+"(%s%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1300
+#, 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 ""
+"Volání snd_pcm_delay() vrátilo hodnotu, která je podivná: zpoždění %lu je "
+"menší než možné %lu.\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1343
+#, 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] ""
+"Volání snd_pcm_mmap_begin() vrátilo hodnotu, která je nezvykle vysoká: %lu "
+"bajtů (%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+msgstr[1] ""
+"Volání snd_pcm_mmap_begin() vrátilo hodnotu, která je nezvykle vysoká: %lu "
+"bajtů (%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+msgstr[2] ""
+"Volání snd_pcm_mmap_begin() vrátilo hodnotu, která je nezvykle vysoká: %lu "
+"bajtů (%lu ms).\n"
+"S největší pravděpodobností se jedná o chybu v ovladači ALSA „%s“. Nahlaste "
+"prosím tento problém vývojářům ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(neplatné)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Vnitřní zvukový systém"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Zvuková brána (A2DP Source & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1272
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "High Fidelity Playback (A2DP Sink, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1275
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "High Fidelity Duplex (A2DP Source/Sink, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "High Fidelity Playback (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1285
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "High Fidelity Duplex (A2DP Source/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+#| msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "High Fidelity Playback (BAP Sink, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+#| msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "High Fidelity Input (BAP Source, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+#| msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "High Fidelity Duplex (BAP Source/Sink, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Jednotka náhlavní soupravy (HSP/HFP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1364
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Jednotka náhlavní soupravy (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Handsfree"
+msgstr "Handsfree"
+
+#: spa/plugins/bluez5/bluez5-device.c:1449
+#| msgid "Handsfree"
+msgid "Handsfree (HFP)"
+msgstr "Handsfree (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
+msgid "Headphone"
+msgstr "Sluchátko"
+
+#: spa/plugins/bluez5/bluez5-device.c:1472
+msgid "Portable"
+msgstr "Přenosné zařízení"
+
+#: spa/plugins/bluez5/bluez5-device.c:1478
+msgid "Car"
+msgstr "Auto"
+
+#: spa/plugins/bluez5/bluez5-device.c:1484
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1490
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1497
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+#| msgid "Bluetooth"
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/da.po b/po/da.po
new file mode 100644
index 0000000..4f297ec
--- /dev/null
+++ b/po/da.po
@@ -0,0 +1,647 @@
+# Danish translation for PipeWire.
+# Copyright (C) 2019 PipeWire's COPYRIGHT HOLDER
+# This file is distributed under the same license as the PipeWire package.
+# scootergrisen, 2019, 2021.
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire master\n"
+"Report-Msgid-Bugs-To: "
+"https://gitlab.freedesktop.org/pipewire/pipewire/issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2021-04-19 20:26+0200\n"
+"Last-Translator: scootergrisen\n"
+"Language-Team: Danish\n"
+"Language: da\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"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [tilvalg]\n"
+" -h, --help Vis denne hjælp\n"
+" --version Vis version\n"
+" -c, --config Indlæs konfiguration (standard %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "PipeWire-mediesystem"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Start PipeWire-mediesystemet"
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Indbygget lyd"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr "Ukendt enhed"
+
+#: src/tools/pw-cat.c:991
+#, 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 [tilvalg] <fil>\n"
+" -h, --help Vis denne hjælp\n"
+" --version Vis version\n"
+" -v, --verbose Aktivér uddybende handlinger\n"
+"\n"
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+" -R, --remote Navn for fjerndæmon\n"
+" --media-type Indstil medietype (standard %s)\n"
+" --media-category Indstil mediekategori (standard %s)\n"
+" --media-role Indstil medierolle (default %s)\n"
+" --target Indstil mål for knudepunkt (standard "
+"%s)\n"
+" 0 betyder ingen henvisning\n"
+" --latency Indstil latens for knudepunkt "
+"(standard %s)\n"
+" Xenhed (enhed = s, ms, us, ns)\n"
+" eller direkte datapunkter (256)\n"
+" hastigheden stammer fra kildefilen\n"
+" --list-targets Vis tilgængelige mål for --target\n"
+"\n"
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Målefrekvens (kræves ved optagelse) "
+"(standard %u)\n"
+" --channels Antal kanaler (kræves ved optagelse) "
+"(standard %u)\n"
+" --channel-map Kanalkort\n"
+" en af: \"stereo\", \"surround-51\", "
+"... eller\n"
+" kommasepareret liste over "
+"kanalnavne: f.eks. \"FL,FR\"\n"
+" --format Måleformat %s (kræves ved optagelse) "
+"(standard %s)\n"
+" --volume Lydstyrke for stream 0-1.0 (standard "
+"%.3f)\n"
+" -q --quality Kvalitet for resampler (0-15) "
+"(standard %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+" -p, --playback Afspilningstilstand\n"
+" -r, --record Optagelsestilstand\n"
+" -m, --midi Midi-tilstand\n"
+"\n"
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [tilvalg] [kommando]\n"
+" -h, --help Vis denne hjælp\n"
+" --version Vis version\n"
+" -d, --daemon Start som dæmon (standard false)\n"
+" -r, --remote Navn for fjerndæmon\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Fra"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ugyldig)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Input"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Input for dokingstation"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Mikrofon for dokingstation"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Line in for dokingstation"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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:1145
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Mikrofon foran"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Mikrofon bagved"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Ekstern mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Intern mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Automatisk styring af gain"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Ingen automatisk styring af gain"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Ingen boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Forstærker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Ingen forstærker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Bas boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Ingen bas boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Højttaler"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Hovedtelefoner"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analog input"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Dokmikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Mikrofon for headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analog output"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Hovedtelefoner 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Monooutput for hovedtelefoner"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Line out"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analog monooutput"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Højttalere"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI/DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Digital output (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Digital input (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Multikanal input"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Multikanal output"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Spilouput"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Chatoutput"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "Chatinput"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Virtuel surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analog mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Analog mono (venstre)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Analog mono (højre)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analog stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "Højttalertelefon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multikanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analog surround 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analog surround 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analog surround 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analog surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analog surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analog surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analog surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analog surround 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analog surround 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analog surround 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analog surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digital stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digital surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digital surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digital surround 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digital stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digital surround 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "Chat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Spil"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analog mono dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analog stereo dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digital stereo dupleks (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Multikanal dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Stereo dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Monochat + 7.1 surround"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s-output"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s-input"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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() returnerede en værdi som er usædvanlig stor: %lu byte (%lu "
+"ms).\n"
+"Det er højst sandsynligt en fejl i ALSA-driveren '%s'. Rapportér det "
+"venligst til ALSA-udviklerne."
+msgstr[1] ""
+"snd_pcm_avail() returnerede en værdi som er usædvanlig stor: %lu bytes (%lu "
+"ms).\n"
+"Det er højst sandsynligt en fejl i ALSA-driveren '%s'. Rapportér det "
+"venligst til ALSA-udviklerne."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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() returnerede en værdi som er usædvanlig stor: %li byte (%s%lu "
+"ms).\n"
+"Det er højst sandsynligt en fejl i ALSA-driveren '%s'. Rapportér det "
+"venligst til ALSA-udviklerne."
+msgstr[1] ""
+"snd_pcm_delay() returnerede en værdi som er usædvanlig stor: %li bytes (%s"
+"%lu ms).\n"
+"Det er højst sandsynligt en fejl i ALSA-driveren '%s'. Rapportér det "
+"venligst til ALSA-udviklerne."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() returnerede underlige værdier: forsinkelsen %lu er "
+"mindre end tilgængelige %lu.\n"
+"Det er højst sandsynligt en fejl i ALSA-driveren '%s'. Rapportér det "
+"venligst til ALSA-udviklerne."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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() returnerede en værdi som er usædvanlig stor: %lu byte "
+"(%lu ms).\n"
+"Det er højst sandsynligt en fejl i ALSA-driveren '%s'. Rapportér det "
+"venligst til ALSA-udviklerne."
+msgstr[1] ""
+"snd_pcm_mmap_begin() returnerede en værdi som er usædvanlig stor: %lu bytes "
+"(%lu ms).\n"
+"Det er højst sandsynligt en fejl i ALSA-driveren '%s'. Rapportér det "
+"venligst til ALSA-udviklerne."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Lydgateway (A2DP-kilde og HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "High fidelity afspilning (A2DP-sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "High fidelity dupleks (A2DP-kilde/-sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "High fidelity afspilning (A2DP-sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "High fidelity dupleks (A2DP-kilde/-sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Hovedenhed for headset (HSP/HFP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Hovedenhed for headset (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Håndfri"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Hovedtelefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Bærbar"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Bil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/de.po b/po/de.po
new file mode 100644
index 0000000..befadae
--- /dev/null
+++ b/po/de.po
@@ -0,0 +1,618 @@
+# translation of pipewire.master-tx.de.po to
+# German translation of pipewire
+# Copyright (C) 2008 pipewire
+# This file is distributed under the same license as the pipewire package.
+#
+#
+# Fabian Affolter <fab@fedoraproject.org>, 2008-2009.
+# Micha Pietsch <barney@fedoraproject.org>, 2008, 2009.
+# Hedda Peters <hpeters@redhat.com>, 2009, 2012.
+# Mario Blättermann <mario.blaettermann@gmail.com>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.de\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2021-01-09 11:36+0000\n"
+"Last-Translator: Tobias Weise <tobias.weise@web.de>\n"
+"Language-Team: German <https://translate.fedoraproject.org/projects/pipewire/"
+"pipewire/de/>\n"
+"Language: de\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: Weblate 4.4\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Internes Audio"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Aus"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ungültig)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Eingabe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Eingabe über Docking-Station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Mikrofon der Docking-Station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Line-Eingang der Docking-Station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Line-Eingang"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Vorderes Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Rückwärtiges Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Externes Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Internes Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Automatische Verstärkungsregelung"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Keine automatische Verstärkungsregelung"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Kein Boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Verstärker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Kein Verstärker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Bassverstärkung"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Keine Bassverstärkung"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Lautsprecher"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Kopfhörer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analoger Eingang"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Mikrofon der Docking-Station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Mikrofon am Kopfhörer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analoge Ausgabe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Kopfhörer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Analoge Mono-Ausgabe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Line-Ausgang"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analoge Mono-Ausgabe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Lautsprecher"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Digitalausgang (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitaleingang (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Mehrkanaleingang"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Mehrkanalausgang"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "%s-Ausgabe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "%s-Ausgabe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "%s-Eingabe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Virtuelles Surround-Ziel"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Analog Mono"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analog Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Lautsprecher"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Mehrkanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analog Surround 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analog Surround 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analog Surround 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analog Surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analog Surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analog Surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analog Surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analog Surround 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analog Surround 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analog Surround 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analog Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digital Stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digital Surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digital Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digital Surround 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digital Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digital Surround 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analog Mono Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analog Stereo Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digital Stereo Duplex (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Mehrkanal-Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Analog Stereo Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s-Ausgabe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s-Eingabe"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gibt einen Wert zurück, der außerordentlich groß ist: %lu "
+"bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
+"dieses Problem den ALSA-Entwicklern."
+msgstr[1] ""
+"snd_pcm_avail() gibt einen Wert zurück, der außerordentlich groß ist: %lu "
+"bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
+"dieses Problem den ALSA-Entwicklern."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gibt einen Wert zurück, der außerordentlich groß ist: %li "
+"Bytes (%s%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
+"dieses Problem den ALSA-Entwicklern."
+msgstr[1] ""
+"snd_pcm_delay() gibt einen Wert zurück, der außerordentlich groß ist: %li "
+"Bytes (%s%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
+"dieses Problem den ALSA-Entwicklern."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() gibt einen ungewöhnlichen Wert zurück: Verzögerung %lu "
+"ist kleiner als das verfügbare %lu.\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
+"dieses Problem den ALSA-Entwicklern."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gibt einen Wert zurück, der außerordentlich groß ist: "
+"%lu Bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
+"dieses Problem den ALSA-Entwicklern."
+msgstr[1] ""
+"snd_pcm_mmap_begin() gibt einen Wert zurück, der außerordentlich groß ist: "
+"%lu Bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie "
+"dieses Problem den ALSA-Entwicklern."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Freisprecheinrichtung"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Kopfhörer"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "tragbar"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Auto"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Bluetooth-Eingabe"
diff --git a/po/de_CH.po b/po/de_CH.po
new file mode 100644
index 0000000..59c0960
--- /dev/null
+++ b/po/de_CH.po
@@ -0,0 +1,638 @@
+# German translation of pipewire
+# Copyright (C) 2008 pipewire
+# This file is distributed under the same license as the pipewire package.
+#
+# Micha Pietsch <barney@fedoraproject.org>, 2008, 2009.
+# Fabian Affolter <fab@fedoraproject.org>, 2008-2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:53+0000\n"
+"Last-Translator: Fabian Affolter <fab@fedoraproject.org>\n"
+"Language-Team: German <fedora-trans-de@redhat.com>\n"
+"Language: de\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-Poedit-Language: German\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Internes Audio"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Aus"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ungültig)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+#, fuzzy
+msgid "Input"
+msgstr "Eingang %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "Internes Audio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "Internes Audio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "Internes Audio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+#, fuzzy
+msgid "Internal Microphone"
+msgstr "Internes Audio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+#, fuzzy
+msgid "Headphones"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+#, fuzzy
+msgid "Analog Input"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "Internes Audio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+#, fuzzy
+msgid "Analog Output"
+msgstr "Ausgang %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+#, fuzzy
+msgid "Analog Mono Output"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "Analog Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "Digital Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "Digital Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Ausgang %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Ausgang %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Ausgang %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Eingang %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Analog Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Analog Mono"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analog Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Analog Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+#, fuzzy
+msgid "Analog Surround 2.1"
+msgstr "Analog Surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+#, fuzzy
+msgid "Analog Surround 3.0"
+msgstr "Analog Surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+#, fuzzy
+msgid "Analog Surround 3.1"
+msgstr "Analog Surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analog Surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analog Surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analog Surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analog Surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+#, fuzzy
+msgid "Analog Surround 6.0"
+msgstr "Analog Surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+#, fuzzy
+msgid "Analog Surround 6.1"
+msgstr "Analog Surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+#, fuzzy
+msgid "Analog Surround 7.0"
+msgstr "Analog Surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analog Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digital Stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digital Surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digital Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digital Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digital Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digital Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+#, fuzzy
+msgid "Analog Mono Duplex"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+#, fuzzy
+msgid "Analog Stereo Duplex"
+msgstr "Analog Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+#, fuzzy
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digital Stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Analog Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "Ausgang %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "Eingang %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gibt einen Wert zurück, welche außerordentlich groß ist: %lu "
+"bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber '%s'. Bitte melden Sie "
+"diesen Punkt den ALSA-Entwicklern."
+msgstr[1] ""
+"snd_pcm_avail() gibt einen Wert zurück, welche außerordentlich groß ist: %lu "
+"bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber '%s'. Bitte melden Sie "
+"diesen Punkt den ALSA-Entwicklern."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gibt einen Wert zurück, welche außerordentlich groß ist: %li "
+"bytes (%s%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber '%s'. Bitte melden Sie "
+"diesen Punkt den ALSA-Entwicklern."
+msgstr[1] ""
+"snd_pcm_delay() gibt einen Wert zurück, welche außerordentlich groß ist: %li "
+"bytes (%s%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber '%s'. Bitte melden Sie "
+"diesen Punkt den ALSA-Entwicklern."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() gibt einen Wert zurück, welche außerordentlich groß ist: %lu "
+"bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber '%s'. Bitte melden Sie "
+"diesen Punkt den ALSA-Entwicklern."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gibt einen Wert zurück, welche außerordentlich groß "
+"ist: %lu bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber '%s'. Bitte melden Sie "
+"diesen Punkt den ALSA-Entwicklern."
+msgstr[1] ""
+"snd_pcm_mmap_begin() gibt einen Wert zurück, welche außerordentlich groß "
+"ist: %lu bytes (%lu ms).\n"
+"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber '%s'. Bitte melden Sie "
+"diesen Punkt den ALSA-Entwicklern."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "Analog Mono"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/el.po b/po/el.po
new file mode 100644
index 0000000..8e8dc9f
--- /dev/null
+++ b/po/el.po
@@ -0,0 +1,613 @@
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Dimitris Glezos <dimitris@glezos.com>, 2008.
+# Thalia Papoutsaki <saliyath@gmail.com>, 2009, 2012.
+# Dimitris Spingos (Δημήτρης Σπίγγος) <dmtrs32@gmail.com>, 2013, 2014.
+msgid ""
+msgstr ""
+"Project-Id-Version: el\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2014-05-08 09:53+0300\n"
+"Last-Translator: Dimitris Spingos (Δημήτρης Σπίγγος) <dmtrs32@gmail.com>\n"
+"Language-Team: team@lists.gnome.gr\n"
+"Language: el\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Virtaal 0.7.0\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Εσωτερικός ήχος"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Μόντεμ"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Ανενεργό"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(άκυρο)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Εισαγωγή"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Είσοδος σταθμού αγκύρωσης"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Μικρόφωνο σταθμού αγκύρωσης"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Γραμμή εισόδου σταθμού αγκύρωσης"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "Μικρόφωνο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Μπροστινό μικρόφωνο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Πίσω μικρόφωνο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Εξωτερικό μικρόφωνο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Εσωτερικό μικρόφωνο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Ραδιόφωνο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Βίντεο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Αυτόματος έλεγχος απολαβής"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Χωρίς αυτόματο έλεγχο απολαβής"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Ενίσχυση"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Χωρίς ενίσχυση"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Ενισχυτή"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Χωρίς ενισχυτή"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Ενίσχυση μπάσων"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Χωρίς ενίσχυση μπάσων"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Ηχείο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Ακουστικά"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Αναλογική είσοδος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Μικρόφωνο σταθμού αγκύρωσης"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Μικρόφωνο ακουστικού"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Αναλογική έξοδος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Ακουστικά"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Αναλογική μονοφωνική έξοδος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Γραμμή εξόδου"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Αναλογική μονοφωνική έξοδος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Ηχεία"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / Θύρα εμφάνισης"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Ψηφιακή έξοδος (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Ψηφιακή είσοδος (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#, fuzzy
+msgid "Multichannel Input"
+msgstr "Αναλογική 4κάναλη είσοδος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Μηδενική έξοδος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Έξοδος %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Έξοδος %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Είσοδος %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Εικονικός περιφερειακός ήχος δέκτη"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Αναλογικό μονοφωνικό"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Αναλογικό μονοφωνικό"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Αναλογικό μονοφωνικό"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Αναλογικό στερεοφωνικό"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Μονοφωνικό"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Ακουστικά"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Ηχείο"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Αναλογικός περιφερειακός ήχος 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Αναλογικός περιφερειακός ήχος 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Αναλογικός περιφερειακός ήχος 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Αναλογικός περιφερειακός ήχος 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Αναλογικός περιφερειακός ήχος 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Αναλογικός περιφερειακός ήχος 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Αναλογικός περιφερειακός ήχος 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Αναλογικός περιφερειακός ήχος 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Αναλογικός περιφερειακός ήχος 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Αναλογικός περιφερειακός ήχος 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Αναλογικός περιφερειακός ήχος 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Ψηφιακό στερεοφωνικό (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Ψηφιακός περιφερειακός ήχος 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Ψηφιακός περιφερειακός ήχος 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Ψηφιακός περιφερειακός ήχος 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Ψηφιακός στερεοφωνικός (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Ψηφιακός περιφερειακός ήχος 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Αναλογικός μονοφωνικός αμφίδρομος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Αναλογικός στερεοφωνικός αμφίδρομος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Ψηφιακός στερεοφωνικός αμφίδρομος (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Αναλογικός στερεοφωνικός αμφίδρομος"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "Έξοδος %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "Είσοδος %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Το snd_pcm_avail() επέστρεψε μια τιμή που είναι πολύ μεγάλη: %lu ψηφιολέξεις "
+"(%lu ms).\n"
+"Το πιθανότερο αυτό είναι ένα σφάλμα στον οδηγό ALSA '%s'. Παρακαλούμε, "
+"αναφέρτε αυτό το θέμα στους προγραμματιστές ALSA."
+msgstr[1] ""
+"Το snd_pcm_avail() επέστρεψε μια τιμή που είναι πολύ μεγάλη: %lu ψηφιολέξεις "
+"(%lu ms).\n"
+"Το πιθανότερο αυτό είναι ένα σφάλμα στον οδηγό ALSA '%s'. Παρακαλούμε, "
+"αναφέρτε αυτό το θέμα στους προγραμματιστές ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Το snd_pcm_delay() επέστρεψε μια τιμή που είναι πολύ μεγάλη: %li ψηφιολέξεις "
+"(%s%lu ms).\n"
+"Το πιθανότερο αυτό είναι ένα σφάλμα στον οδηγό ALSA '%s'. Παρακαλούμε, "
+"αναφέρτε αυτό το θέμα στους προγραμματιστές ALSA."
+msgstr[1] ""
+"Το snd_pcm_delay() επέστρεψε μια τιμή που είναι πολύ μεγάλη: %li ψηφιολέξεις "
+"(%s%lu ms).\n"
+"Το πιθανότερο αυτό είναι ένα σφάλμα στον οδηγό ALSA '%s'. Παρακαλούμε, "
+"αναφέρτε αυτό το θέμα στους προγραμματιστές ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"Το snd_pcm_avail_delay() επέστρεψε περίεργες τιμές: η καθυστέρηση %lu είναι "
+"μικρότερη από το avail %lu.\n"
+"Το πιθανότερο αυτό είναι ένα σφάλμα στον οδηγό ALSA '%s'. Παρακαλούμε, "
+"αναφέρτε αυτό το θέμα στους προγραμματιστές ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Το snd_pcm_mmap_begin() επέστρεψε μια τιμή που είναι πολύ μεγάλη: %lu "
+"ψηφιολέξεις (%lu ms).\n"
+"Το πιθανότερο αυτό είναι ένα σφάλμα στον οδηγό ALSA '%s'. Παρακαλούμε, "
+"αναφέρτε αυτό το θέμα στους προγραμματιστές ALSA."
+msgstr[1] ""
+"Το snd_pcm_mmap_begin() επέστρεψε μια τιμή που είναι πολύ μεγάλη: %lu "
+"ψηφιολέξεις (%lu ms).\n"
+"Το πιθανότερο αυτό είναι ένα σφάλμα στον οδηγό ALSA '%s'. Παρακαλούμε, "
+"αναφέρτε αυτό το θέμα στους προγραμματιστές ALSA."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Ανοιχτής ακρόασης"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Ακουστικό"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Φορητό"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Αυτοκίνητο"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "Υψηλή πιστότητα"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Τηλέφωνο"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Είσοδος μπλουτούθ"
diff --git a/po/eo.po b/po/eo.po
new file mode 100644
index 0000000..5595be2
--- /dev/null
+++ b/po/eo.po
@@ -0,0 +1,573 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the pipewire package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2021-02-05 01:40+0000\n"
+"Last-Translator: Carmen Bianca Bakker <carmen@carmenbianca.eu>\n"
+"Language-Team: Esperanto <https://translate.fedoraproject.org/projects/"
+"pipewire/pipewire/eo/>\n"
+"Language: eo\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: Weblate 4.4.2\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Integrita sono"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Malŝaltita"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(nevalida)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Enigo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Doka enigo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Doka mikrofono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "Mikrofono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Antaŭa mikrofono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Malantaŭa mikrofono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Ekstera mikrofono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Interna mikrofono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Amplifilo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Neniu amplifilo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Laŭtparolilo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Kapaŭskultilo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analoga enigo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Doka mikrofono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Kaptelefona mikrofono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analoga eligo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Kapaŭskultilo 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Kapaŭskultila mono-eligo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analoga mono-eligo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Laŭtparoliloj"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Cifera eligo (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Cifera enigo (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Plurkanala enigo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Plurkanala eligo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Lud-eligo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Babil-eligo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "Babil-enigo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Virtuala ĉirkaŭa sono 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analoga mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Analoga mono (maldekstra)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Analoga mono (dekstra)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analoga stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Kaptelefono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Plurkanala"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analoga ĉirkaŭa sono 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analoga ĉirkaŭa sono 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analoga ĉirkaŭa sono 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analoga ĉirkaŭa sono 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analoga ĉirkaŭa sono 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analoga ĉirkaŭa sono 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analoga ĉirkaŭa sono 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analoga ĉirkaŭa sono 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analoga ĉirkaŭa sono 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analoga ĉirkaŭa sono 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analoga ĉirkaŭa sono 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Cifera stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Cifera ĉirkaŭa sono 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Cifera ĉirkaŭa sono 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Cifera ĉirkaŭa sono 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Cifera stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Cifera ĉirkaŭa sono 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "Babilo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Ludo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analoga mono-dupleksa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analoga stereo-dupleksa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Cifera stereo-dupleksa (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Plurkanala dupleksa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Stereo-dupleksa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono-babilo + 7.1 ĉirkaŭa sono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s-eligo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s-enigo"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Libermana"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Kapaŭskultilo"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Aŭto"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefono"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "Bludento"
diff --git a/po/es.po b/po/es.po
new file mode 100644
index 0000000..aca5467
--- /dev/null
+++ b/po/es.po
@@ -0,0 +1,621 @@
+# Fedora Spanish translation of PipeWire.
+# This file is distributed under the same license as the PipeWire Package.
+#
+# Domingo Becker <domingobecker@gmail.com>, 2009.
+# Héctor Daniel Cabrera <h.daniel.cabrera@gmail.com>, 2009.
+# Fernando Gonzalez Blanco <fgonz@fedoraproject.org>, 2009, 2012.
+# Alberto Castillo <acg@albertocg.com>, 2016. #zanata
+# Gladys Guerrero Lozano <gguerrer@redhat.com>, 2016. #zanata
+# Máximo Castañeda Riloba <mcrcctm@gmail.com>, 2016. #zanata
+# Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2020-10-01 15:30+0000\n"
+"Last-Translator: Emilio Herrera <ehespinosa57@gmail.com>\n"
+"Language-Team: Spanish <https://translate.fedoraproject.org/projects/"
+"pipewire/pipewire/es/>\n"
+"Language: es\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: Weblate 4.2.2\n"
+"X-Poedit-Language: Spanish\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Audio Interno"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Módem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Apagado"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(inválido)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Entrada de estación de acoplamiento"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Micrófono de estación de acoplamiento"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Línea de entrada de estación de acoplamiento"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Línea de entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Micrófono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Micrófono frontal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Micrófono trasero"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Micrófono externo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Micrófono interno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Vídeo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Control automático de ganancia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Sin control automático de ganancia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Incremento de ganancia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Sin incremento de ganancia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Sin amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Potenciador de graves"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Sin potenciador de graves"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Altavoz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Auriculares"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Entrada analógica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Micrófono de estación de acoplamiento"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Micrófono acoplado a auriculares"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Salida analógica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Auriculares"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Salida mono analógica "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Línea de salida"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Salida mono analógica "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Altavoces"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Salida digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Entrada digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#, fuzzy
+msgid "Multichannel Input"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Salida %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Salida %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Entrada %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Destino envolvente virtual"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Mono analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Mono analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Mono analógico"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Estéreo analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+msgid "Stereo"
+msgstr "Estéreo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4540
+#: spa/plugins/alsa/acp/alsa-mixer.c:4698
+#: spa/plugins/bluez5/bluez5-device.c:1135
+msgid "Headset"
+msgstr "Auriculares"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Altavoz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Envolvente analógico 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Envolvente analógico 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Envolvente analógico 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Envolvente analógico 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Envolvente análogico 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Envolvente analógico 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Envolvente analógico 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Envolvente analógico 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Envolvente analógico 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Envolvente analógico 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Envolvente analógico 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Estéreo digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Envolvente digital 4.0 (IEC9588/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Envolvente digital 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Envolvente digital 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Estéreo digital (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Envolvente digital 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Mono analógico dúplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Estéreo analógico dúplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Estéreo digital dúplex (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#, fuzzy
+msgid "Multichannel Duplex"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Estéreo analógico dúplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "Salida %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "Entrada %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() devolvió un valor que es excepcionalmente grande: %lu bytes "
+"(%lu ms).\n"
+"Lo más probable es que sea un error del controlador ALSA '%s'. Por favor, "
+"informe de esto a los desarrolladores de ALSA."
+msgstr[1] ""
+"snd_pcm_avail() devolvió un valor que es excepcionalmente grande: %lu bytes "
+"(%lu ms).\n"
+"Lo más probable es que sea un error del controlador ALSA '%s'. Por favor, "
+"informe de esto a los desarrolladores de ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() devolvió un valor que es excepcionalmente grande: %li bytes "
+"(%s%lu ms).\n"
+"Lo más probable es que sea un error del controlador ALSA '%s'. Por favor, "
+"informe de esto a los desarrolladores de ALSA."
+msgstr[1] ""
+"snd_pcm_delay() devolvió un valor que es excepcionalmente grande: %li bytes "
+"(%s%lu ms).\n"
+"Lo más probable es que sea un error del controlador ALSA '%s'. Por favor, "
+"informe de esto a los desarrolladores de ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() devolvió valores raros: %lu es menor que los "
+"disponibles %lu.\n"
+"Lo más probable es que sea un error del controlador ALSA '%s'. Por favor, "
+"informe de esto a los desarrolladores de ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() devolvió un valor que es excepcionalmente grande: %lu "
+"bytes (%lu ms).\n"
+"Lo más probable es que sea un error del controlador ALSA '%s'. Por favor, "
+"informe de esto a los desarrolladores de ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() devolvió un valor que es excepcionalmente grande: %lu "
+"bytes (%lu ms).\n"
+"Lo más probable es que sea un error del controlador ALSA '%s'. Por favor, "
+"informe de esto a los desarrolladores de ALSA."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Manos libres"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Auriculares"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Portátil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Coche"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "Alta fidelidad"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Teléfono"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Entrada bluetooth"
diff --git a/po/fi.po b/po/fi.po
new file mode 100644
index 0000000..0a0bb14
--- /dev/null
+++ b/po/fi.po
@@ -0,0 +1,650 @@
+# pipewire translation to Finnish (fi).
+# Copyright (C) 2008 Timo Jyrinki
+# This file is distributed under the same license as the pipewire package.
+# Timo Jyrinki <timo.jyrinki@iki.fi>, 2008.
+# Ville-Pekka Vainio <vpivaini@cs.helsinki.fi>, 2009, 2012.
+#
+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"
+"Language: fi\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: Weblate 4.5.1\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [valinnat]\n"
+" -h, --help Näytä tämä ohje\n"
+" --version Näytä versio\n"
+" -c, --config Lataa asetukset (oletus %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "PipeWire-mediajärjestelmä"
+
+#: src/daemon/pipewire.desktop.in:5
+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/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modeemi"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr "Tuntematon laite"
+
+#: src/tools/pw-cat.c:991
+#, 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 [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
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\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"
+" 0 tarkoittaa: ei linkkiä\n"
+" --latency Aseta solmun viive (oletus %s)\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"
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate 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"
+"\n"
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+" -p, --playback Toisto\n"
+" -r, --record Nauhoitus\n"
+" -m, --midi MIDI-tila\n"
+"\n"
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [valinnat] [komento]\n"
+" -h, --help Näytä tämä ohje\n"
+" --version Näytä versio\n"
+" -d, --daemon Käynnistä taustaprosessina (oletus: "
+"ei)\n"
+" -r, --remote Taustaprosessin nimi\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Poissa"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(virheellinen)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Sisääntulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Telakan sisääntulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Telakan mikrofoni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+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
+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
+msgid "Microphone"
+msgstr "Mikrofoni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Etumikrofoni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Takamikrofoni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Ulkoinen mikrofoni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Sisäinen mikrofoni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Automaattinen äänenvoimakkuuden säätö"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Ei automaattista äänenvoimakkuuden säätöä"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Vahvistus"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Ei vahvistusta"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Vahvistin"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Ei vahvistinta"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Bassonvahvistus"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Ei basson vahvistusta"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Kaiutin"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Kuulokkeet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analoginen sisääntulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Telakan mikrofoni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Kuulokkeiden mikrofoni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analoginen ulostulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Kuulokkeet 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Kuulokkeiden monoulostulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Linjaulostulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analoginen monoulostulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Kaiuttimet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Digitaalinen ulostulo (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitaalinen sisääntulo (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Monikanavainen sisääntulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Monikanavainen ulostulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Peli-ulostulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Puhe-ulostulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "Puhe-sisääntulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Virtuaalinen tilaääni 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analoginen mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Analoginen mono (vasen)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Analoginen mono (oikea)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analoginen stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Kuulokkeet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "Kaiutinpuhelin"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Monikanavainen"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analoginen tilaääni 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analoginen tilaääni 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analoginen tilaääni 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analoginen tilaääni 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analoginen tilaääni 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analoginen tilaääni 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analoginen tilaääni 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analoginen tilaääni 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analoginen tilaääni 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analoginen tilaääni 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analoginen tilaääni 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitaalinen stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitaalinen tilaääni 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitaalinen tilaääni 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitaalinen tilaääni 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitaalinen stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitaalinen tilaääni 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "Puhe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Peli"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analoginen mono, molempisuuntainen"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analoginen stereo, molempisuuntainen"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digitaalinen stereo, molempisuuntainen (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Monikanavainen, molempisuuntainen"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Stereo, molempisuuntainen"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono-puhe + 7.1 tilaääni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s, ulostulo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, 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
+#, 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() palautti poikkeuksellisen suuren arvon: %lu tavu (%lu ms).\n"
+"Tämä on todennäköisesti ohjelmavirhe ALSA-ajurissa ”%s”. Ilmoita tästä "
+"ongelmasta ALSA-kehittäjille."
+msgstr[1] ""
+"snd_pcm_avail() palautti poikkeuksellisen suuren arvon: %lu tavua (%lu ms).\n"
+"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
+#, 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() palautti poikkeuksellisen suuren arvon: %li tavu (%s%lu "
+"ms).\n"
+"Tämä on todennäköisesti ohjelmavirhe ALSA-ajurissa ”%s”. Ilmoita tästä "
+"ongelmasta ALSA-kehittäjille."
+msgstr[1] ""
+"snd_pcm_delay() palautti poikkeuksellisen suuren arvon: %li tavua (%s%lu "
+"ms).\n"
+"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
+#, 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() palautti poikkeuksellisia arvoja: %lu on vähemmän kuin "
+"saatavissa oleva %lu.\n"
+"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
+#, 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() palautti poikkeuksellisen suuren arvon: %lu tavu (%lu "
+"ms).\n"
+"Tämä on todennäköisesti ohjelmavirhe ALSA-ajurissa ”%s”. Ilmoita tästä "
+"ongelmasta ALSA-kehittäjille."
+msgstr[1] ""
+"snd_pcm_mmap_begin() palautti poikkeuksellisen suuren arvon: %lu tavua (%lu "
+"ms).\n"
+"Tämä on todennäköisesti ohjelmavirhe ALSA-ajurissa ”%s”. Ilmoita tästä "
+"ongelmasta ALSA-kehittäjille."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+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
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Korkealaatuinen toisto (A2DP-kohde, %s-koodekki)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, 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
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Korkealaatuinen toisto (A2DP-kohde)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Korkealaatuinen molempisuuntainen (A2DP-lähde/kohde)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, 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
+msgid "Handsfree"
+msgstr "Kuulokemikrofoni"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Kuulokkeet"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Kannettava"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Auto"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "Hi-Fi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Puhelin"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/fr.po b/po/fr.po
new file mode 100644
index 0000000..df5babb
--- /dev/null
+++ b/po/fr.po
@@ -0,0 +1,623 @@
+# French translation of pipewire.
+# Copyright (C) 2006-2008 Lennart Poettering
+# This file is distributed under the same license as the pipewire package.
+#
+#
+# Robert-André Mauchin <zebob.m@pengzone.org>, 2008.
+# Michaël Ughetto <telimektar esraonline com>, 2008.
+# Pablo Martin-Gomez <pablo.martin-gomez@laposte.net>, 2008.
+# Corentin Perard <corentin.perard@gmail.com>, 2009.
+# Thomas Canniot <mrtom@fedoraproject.org>, 2009, 2012.
+# Sam Friedmann <sfriedma@redhat.com>, 2016. #zanata
+# Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata
+# Edouard Duliege <edouard.duliege@gmail.com>, 2017. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2020-12-13 17:35+0000\n"
+"Last-Translator: Julien Humbert <julroy67@gmail.com>\n"
+"Language-Team: French <https://translate.fedoraproject.org/projects/pipewire/"
+"pipewire/fr/>\n"
+"Language: fr\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: Weblate 4.3.2\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Audio interne"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Éteint"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(invalide)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Entrée"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Entrée de la station d’accueil"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Microphone de la station d’accueil"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Entrée ligne de la station d’accueil"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Entrée ligne"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Microphone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Microphone avant"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Microphone arrière"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Microphone externe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Microphone interne"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Vidéo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Contrôle automatique du gain"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Pas de contrôle automatique du gain"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Pas de boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Amplificateur"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Pas d’amplificateur"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Booster de basses"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Pas de booster de basses"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Haut-parleur"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Casque audio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Entrée analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Microphone de la station d’accueil"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Microphone casque"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Sortie analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Casque audio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Sortie mono analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Sortie ligne"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Sortie mono analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Haut-parleurs"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Sortie numérique (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Entrée numérique (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#, fuzzy
+msgid "Multichannel Input"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Sortie %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Sortie %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Entrée %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Destination surround virtuelle"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Mono analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Mono analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Mono analogique"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Stéréo analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+msgid "Stereo"
+msgstr "Stéréo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4540
+#: spa/plugins/alsa/acp/alsa-mixer.c:4698
+#: spa/plugins/bluez5/bluez5-device.c:1135
+msgid "Headset"
+msgstr "Casque"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Haut-parleur"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Surround analogique 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Surround analogique 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Surround analogique 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Surround analogique 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Surround analogique 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Surround analogique 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Surround analogique 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Surround analogique 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Surround analogique 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Surround analogique 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Surround analogique 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Stéréo numérique (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Surround numérique 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Surround numérique 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Surround numérique 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Stéréo numérique (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Surround numérique 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Duplex Mono analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Duplex stéréo analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Duplex stéréo numérique (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#, fuzzy
+msgid "Multichannel Duplex"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Duplex stéréo analogique"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "Sortie %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "Entrée %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() a retourné une valeur qui est exceptionnellement large : %lu "
+"octets (%lu ms).\n"
+"Il s’agit très probablement d’un bogue dans le pilote ALSA « %s ». Veuillez "
+"rapporter ce problème aux développeurs d’ALSA."
+msgstr[1] ""
+"snd_pcm_avail() a retourné une valeur qui est exceptionnellement large : %lu "
+"octets (%lu ms).\n"
+"Il s’agit très probablement d’un bogue dans le pilote ALSA « %s ». Veuillez "
+"rapporter ce problème aux développeurs d’ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() a retourné une valeur qui est exceptionnellement large : %li "
+"octets (%s%lu ms).\n"
+"Il s’agit très probablement d’un bogue dans le pilote ALSA « %s ». Veuillez "
+"rapporter ce problème aux développeurs d’ALSA."
+msgstr[1] ""
+"snd_pcm_delay() a retourné une valeur qui est exceptionnellement large : %li "
+"octets (%s%lu ms).\n"
+"Il s’agit très probablement d’un bogue dans le pilote ALSA « %s ». Veuillez "
+"rapporter ce problème aux développeurs d’ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() a retourné des valeurs inhabituelles : le délai %lu "
+"est inférieur au %lu disponible.\n"
+"Il s’agit très probablement d’un bogue dans le pilote ALSA « %s ». Veuillez "
+"rapporter ce problème aux développeurs d’ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() a retourné une valeur qui est exceptionnellement "
+"large : %lu octets (%lu·ms).\n"
+"Il s’agit très probablement d’un bogue dans le pilote ALSA « %s ». Veuillez "
+"rapporter ce problème aux développeurs d’ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() a retourné une valeur qui est exceptionnellement "
+"large : %lu octets (%lu·ms).\n"
+"Il s’agit très probablement d’un bogue dans le pilote ALSA « %s ». Veuillez "
+"rapporter ce problème aux développeurs d’ALSA."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Mains-libres"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Écouteurs"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Portable"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Voiture"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Téléphone"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Entrée Bluetooth"
diff --git a/po/gl.po b/po/gl.po
new file mode 100644
index 0000000..8be384f
--- /dev/null
+++ b/po/gl.po
@@ -0,0 +1,707 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Translators:
+# bassball93 <bassball93@gmail.com>, 2011.
+# mbouzada <mbouzada@gmail.com>, 2011.
+# Marcos Lans <marcoslansgarza@gmail.com>, 2018.
+# Fran Dieguez <frandieguez@gnome.org>, 2012-2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-07-10 03:27+0000\n"
+"PO-Revision-Date: 2022-08-23 09:47+0200\n"
+"Last-Translator: Fran Dieguez <frandieguez@gnome.org>\n"
+"Language-Team: Galician <Proxecto Trasno <proxecto@trasno.gal>>\n"
+"Language: gl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"X-Generator: Gtranslator 40.0\n"
+"X-DL-Team: gl\n"
+"X-DL-Module: PipeWire\n"
+"X-DL-Branch: master\n"
+"X-DL-Domain: po\n"
+"X-DL-State: Translating\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [opcións]\n"
+" -h, --help Mostra esta axuda\n"
+" --version Mostrar versión\n"
+" -c, --config Cargar configuración (Predeterminado "
+"%s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Sistema multimedia PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Iniciar o Sistema multimedia PipeWire"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Túnel a %s/%s"
+
+#: src/modules/module-fallback-sink.c:51
+#| msgid "Game Output"
+msgid "Dummy Output"
+msgstr "Saída de proba"
+
+#: src/modules/module-pulse-tunnel.c:648
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Túnel para %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Dispositivo descoñecido"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s en %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s en %s"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [opcións] [<ficheiro>|-]\n"
+" -h, --help Mostrar esta axuda\n"
+" --version Mostrar versión\n"
+" -v, --verbose Activar operacións verbosas\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote Nome do daemon remoto\n"
+" --media-type Estabelecer o tipo de medio (por "
+"omisión %s)\n"
+" --media-category Estabelecer a categoría multimedia "
+"(por omisión %s)\n"
+" --media-role Estabelecer o rol multimedia (por "
+"omisión %s)\n"
+" --target Estabelecer o nodo obxectivo (por "
+"omisión %s)\n"
+" 0 significa non ligar\n"
+" --latency Estabelecer a latencia do nodo (por "
+"omisión %s)\n"
+" Xunit (unidade = s, ms, us, ns)\n"
+" ou mostras directas samples (256)\n"
+" a taxa é un dos ficheiros de "
+"orixe\n"
+" -P --properties Estabelecer as propiedades do nodo\n"
+"\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Taxa de mostreo (solicitudes por "
+"segundo) (por omisión %u)\n"
+" --channels Número de canles (solicitudes por "
+"segundo) (por omisión %u)\n"
+" --channel-map Mapa de canles\n"
+" un de: \"stereo\", "
+"\"surround-51\",... or\n"
+" lista separada por comas dos "
+"nomes das canles: p.ex. \"FL,FR\"\n"
+" --format Formato de mostras %s (solicitudes "
+"por segundo) (por omisión %s)\n"
+" --volume Volume do fluxo 0-1.0 (por omisión "
+"%.3f)\n"
+" -q --quality Calidade do remostreador (0 - 15) "
+"(por omisión %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Modo de reprodución\n"
+" -r, --record Modo de grabación\n"
+" -m, --midi Modo MIDI\n"
+" -d, --dsd Modo DSD\n"
+"\n"
+
+#: src/tools/pw-cli.c:3165
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [opcións] [orde]\n"
+" -h, --help Mostrar esta axuda\n"
+" --version Mostrar versión\n"
+" -d, --daemon Iniciar como demonio (Por omisión "
+"falso)\n"
+" -r, --remote Modo de demonio remoto\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:446 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1185
+msgid "Off"
+msgstr "Apagado"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Entrada de estación acoplada (Docking Station)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Micrófono da estación acoplada (Docking Station)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Entrada de estación acoplada (Docking Station)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Liña de entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1357
+msgid "Microphone"
+msgstr "Micrófono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Micrófono frontal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Micrófono traseiro"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Micrófono externo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Micrófono interno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c: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 "Vídeo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "Control automático de ganancia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Sen control automático de ganancia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Enfatizador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Sen enfatizador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Sen amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Enfatizador baixo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Sen enfatizador baixo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1363
+msgid "Speaker"
+msgstr "Altofalante"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Auriculares"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Entrada analóxica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Micrófono do acople"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Micrófono con auricular"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Saída analóxica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Auriculares 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Saída monoaural para auriculares"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Liña de saída"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Saída monoaural analóxica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Altofalantes"
+
+#: 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 "Saída dixital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Entrada dixital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Entrada multicanle"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Saída multicanle"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Saída do xogo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Saída do chat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Entrada de chat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Envolvente virtual 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Monoaural analóxico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Monoaural analóxico (Esquerda)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Monoaural analóxico (Dereita)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Estéreo analóxico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr "Estéreo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1345
+msgid "Headset"
+msgstr "Auriculares con micro"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Altofalante"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Multicanle"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Envolvente analóxico 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Envolvente analóxico 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Envolvente analóxico 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Envolvente analóxico 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Envolvente analóxico 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Envolvente analóxico 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Envolvente analóxico 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Envolvente analóxico 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Envolvente analóxico 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Envolvente analóxico 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Envolvente analóxico 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Estéreo dixital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Envolvente dixital 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Envolvente dixital 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Envolvente dixital 5.1 (IEC958/ACDTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Estéreo dixital (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Envolvente dixital 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Chat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Xogo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Monoaural analóxico dúplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Estéreo analóxico dúplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Estéreo dixital dúplex (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Dúplex multicanle"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Dúplex estéreo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Chat mono + envolvente 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "Saída %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "Entrada %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267
+#, 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() devolveu un valor que é excepcionalmente grande: %lu bytes "
+"(%lu ms).\n"
+"O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
+"aos desenvolvedores de ALSA."
+msgstr[1] ""
+"snd_pcm_avail() devolveu un valor que é excepcionalmente grande: %lu bytes "
+"(%lu ms).\n"
+"O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
+"aos desenvolvedores de ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1239
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte "
+"(%s%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes "
+"(%s%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() devolveu un valor que é excepcionalmente grande: %li bytes "
+"(%s%lu ms).\n"
+"O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
+"aos desenvolvedores de ALSA."
+msgstr[1] ""
+"snd_pcm_delay() devolveu un valor que é excepcionalmente grande: %li bytes "
+"(%s%lu ms).\n"
+"O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
+"aos desenvolvedores de ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1286
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() devolveu valores estraños: o atraso de %lu é menor que "
+"o dispoñíbel %lu.\n"
+"O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
+"aos desenvolvedores de ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1329
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() devolveu un valor que é excepcionalmente grande: %lu "
+"bytes (%lu ms).\n"
+"O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
+"aos desenvolvedores de ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() devolveu un valor que é excepcionalmente grande: %lu "
+"bytes (%lu ms).\n"
+"O máis probábel é que sexa un erro do controlador ALSA «%s». Informe disto "
+"aos desenvolvedores de ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(incorrecto)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Audio interno"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Módem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1196
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Porta de enlace de son (Orixe A2DP e HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1221
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Reprodución de alta fidelidade (Sumideiro A2DP, códec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1224
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Dúplex de alta fidelidade (Orixe/sumideiro A2DP, códec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1232
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Reprodución de alta fidelidade (Sumideiro A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1234
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Dúplex de alta fidelidade (Orixe/sumideiro A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1262
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Unidade de auriculares de cabeza (HSP/HFP, códec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1267
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Unidade de auriculares de cabeza (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1346
+#: spa/plugins/bluez5/bluez5-device.c:1351
+#: spa/plugins/bluez5/bluez5-device.c:1358
+#: spa/plugins/bluez5/bluez5-device.c:1364
+#: spa/plugins/bluez5/bluez5-device.c:1370
+#: spa/plugins/bluez5/bluez5-device.c:1376
+#: spa/plugins/bluez5/bluez5-device.c:1382
+#: spa/plugins/bluez5/bluez5-device.c:1388
+#: spa/plugins/bluez5/bluez5-device.c:1394
+msgid "Handsfree"
+msgstr "Sen mans"
+
+#: spa/plugins/bluez5/bluez5-device.c:1352
+#| msgid "Handsfree"
+msgid "Handsfree (HFP)"
+msgstr "Sen mans (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1369
+msgid "Headphone"
+msgstr "Auriculares"
+
+#: spa/plugins/bluez5/bluez5-device.c:1375
+msgid "Portable"
+msgstr "Portátil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1381
+msgid "Car"
+msgstr "Automóbil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1387
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1393
+msgid "Phone"
+msgstr "Teléfono"
+
+#: spa/plugins/bluez5/bluez5-device.c:1400
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1401
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/gu.po b/po/gu.po
new file mode 100644
index 0000000..a13dc0c
--- /dev/null
+++ b/po/gu.po
@@ -0,0 +1,622 @@
+# translation of PipeWire.po to Gujarati
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Sweta Kothari <swkothar@redhat.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:53+0000\n"
+"Last-Translator: Sweta Kothari <swkothar@redhat.com>\n"
+"Language-Team: Gujarati\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "આંતરિક ઓડિયો"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "મોડેમ"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "બંધ"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(અયોગ્ય)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "ઇનપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "ડોકિંગ સ્ટેશન ઇનપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "ડોકિંગ સ્ટેશન માઇક્રોફોન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "ડોકિંગ સ્ટેશન ઇનપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "માઇક્રોફોન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "ડોકિંગ સ્ટેશન માઇક્રોફોન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "માઇક્રોફોન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "બહારનાં માઇક્રોફોન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "આંતરિક માઇક્રોફોન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "રેડિયો"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "વિડિયો"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Automatic Gain Control"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Automatic Gain Control નથી"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "બુસ્ટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "બુસ્ટ નથી"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "પરિવર્ધક"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "પરિવર્ધક નથી"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "બુસ્ટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "બુસ્ટ નથી"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "ઍનલૉગ હૅડફોનો"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "ઍનલૉગ ઇનપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "ડોકિંગ સ્ટેશન માઇક્રોફોન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "માઇક્રોફોન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "ઍનલૉગ આઉટપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "ઍનલૉગ હૅડફોનો"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "ઍનલૉગ મોનો આઉટપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "લાઇન-ઇન"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "ઍનલૉગ મોનો આઉટપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "ઍનલૉગ સ્ટેરિઓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "ડિજિટલ સ્ટેરિઓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "ડિજિટલ સ્ટેરિઓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "શૂન્ય આઉટપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "શૂન્ય આઉટપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "શૂન્ય આઉટપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "ઇનપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "ઍનલૉગ સરાઉન્ડ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "ઍનલૉગ મોનો"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "ઍનલૉગ મોનો"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "ઍનલૉગ મોનો"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "ઍનલૉગ સ્ટેરિઓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "મોનો"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "ઍનલૉગ સ્ટેરિઓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "ઍનલૉગ સરાઉન્ડ 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "ઍનલૉગ સરાઉન્ડ 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "ઍનલૉગ સરાઉન્ડ 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "ઍનલૉગ સરાઉન્ડ 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "ઍનલૉગ સરાઉન્ડ 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "ઍનલૉગ સરાઉન્ડ 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "ઍનલૉગ સરાઉન્ડ 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "ઍનલૉગ સરાઉન્ડ 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "ઍનલૉગ સરાઉન્ડ 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "ઍનલૉગ સરાઉન્ડ 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "ઍનલૉગ સરાઉન્ડ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "ડિજિટલ સ્ટેરિઓ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ડિજિટલ સરાઉન્ડ 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ડિજિટલ સરાઉન્ડ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ડિજિટલ સરાઉન્ડ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "ડિજિટલ સ્ટેરિઓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ડિજિટલ સરાઉન્ડ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "ઍનલૉગ મોનો ડુપ્લેક્ષ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "ઍનલૉગ સ્ટેરિઓ ડુપ્લેક્ષ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ડિજિટલ સ્ટેરિઓ ડુપ્લેક્ષ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "ઍનલૉગ સ્ટેરિઓ ડુપ્લેક્ષ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "શૂન્ય આઉટપુટ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "ઇનપુટ"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() કિંમતને પાછુ મળેલ છે કે જે અપવાદ રીતે વિશાળ છે: %lu bytes (%lu ms).\n"
+"ALSA ડ્રાઇવર '%s' માં મોટેભાગે આ ભૂલ જેવુ છે. ALSA ડેવલ્પરોમાં આ સમસ્યાને મહેરબાની કરીને "
+"અહેવાલ કરો."
+msgstr[1] ""
+"snd_pcm_avail() કિંમતને પાછુ મળેલ છે કે જે અપવાદ રીતે વિશાળ છે: %lu bytes (%lu ms).\n"
+"ALSA ડ્રાઇવર '%s' માં મોટેભાગે આ ભૂલ જેવુ છે. ALSA ડેવલ્પરોમાં આ સમસ્યાને મહેરબાની કરીને "
+"અહેવાલ કરો."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() કિંમતને પાછુ મળેલ છે કે જે અપવાદ રીતે વિશાળ છે: %li bytes (%s%lu "
+"ms).\n"
+"ALSA ડ્રાઇવર '%s' માં મોટેભાગે આ ભૂલ જેવુ છે. ALSA ડેવલ્પરોમાં આ સમસ્યાને મહેરબાની કરીને "
+"અહેવાલ કરો."
+msgstr[1] ""
+"snd_pcm_delay() કિંમતને પાછુ મળેલ છે કે જે અપવાદ રીતે વિશાળ છે: %li bytes (%s%lu "
+"ms).\n"
+"ALSA ડ્રાઇવર '%s' માં મોટેભાગે આ ભૂલ જેવુ છે. ALSA ડેવલ્પરોમાં આ સમસ્યાને મહેરબાની કરીને "
+"અહેવાલ કરો."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() કિંમતને પાછુ મળેલ છે કે જે અપવાદ રીતે વિશાળ છે: %lu bytes (%lu ms).\n"
+"ALSA ડ્રાઇવર '%s' માં મોટેભાગે આ ભૂલ જેવુ છે. ALSA ડેવલ્પરોમાં આ સમસ્યાને મહેરબાની કરીને "
+"અહેવાલ કરો."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() કિંમતને પાછુ મળેલ છે કે જે અપવાદ રીતે વિશાળ છે: %lu બાઇટો (%lu "
+"ms).\n"
+"ALSA ડ્રાઇવર '%s' માં મોટેભાગે આ ભૂલ જેવુ છે. ALSA ડેવલ્પરોમાં આ સમસ્યાને મહેરબાની કરીને "
+"અહેવાલ કરો."
+msgstr[1] ""
+"snd_pcm_mmap_begin() કિંમતને પાછુ મળેલ છે કે જે અપવાદ રીતે વિશાળ છે: %lu બાઇટો (%lu "
+"ms).\n"
+"ALSA ડ્રાઇવર '%s' માં મોટેભાગે આ ભૂલ જેવુ છે. ALSA ડેવલ્પરોમાં આ સમસ્યાને મહેરબાની કરીને "
+"અહેવાલ કરો."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "ઍનલૉગ હૅડફોનો"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/he.po b/po/he.po
new file mode 100644
index 0000000..5dc1361
--- /dev/null
+++ b/po/he.po
@@ -0,0 +1,573 @@
+#
+# Elad <el.il@doom.co.il>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2021-03-02 14:40+0000\n"
+"Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n"
+"Language-Team: Hebrew <https://translate.fedoraproject.org/projects/pipewire/"
+"pipewire/he/>\n"
+"Language: he\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: Weblate 4.4.2\n"
+"X-Poedit-Language: Hebrew\n"
+"X-Poedit-Country: Israel\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "צליל פנימי"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "מודם"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "מכובה"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(לא תקף)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "קלט"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "קלט מתחנת עגינה"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "מיקרופון מתחנת עגינה"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "קו נכנס מתחנת עגינה"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "מיקרופון"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "מיקרופון קדמי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "מיקרופון אחורי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "מיקרופון חיצוני"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "מיקרופון פנימי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "רדיו"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "וידאו"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "מגבר"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "אין מגבר"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "הגברת באסים"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "ללא הגברת באסים"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "רמקול"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "אוזניות אנלוגיות"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "קלט אנלוגי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "מיקרופון של תחנת עגינה"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "מיקרופון באוזניות"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "פלט אנלוגי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "אוזניות 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "פלט מונו לאוזניות"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "קו יוצא"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "פלט מונו אנלוגי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "רמקולים"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "פלט דיגיטלי (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "קלט דיגיטלי (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "קלט רב־ערוצי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "פלט רב־ערוצי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "פלט משחק"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "פלט צ׳אט"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "קלט צ׳אט"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "סראונד וירטואלי 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "מונו אנלוגי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "מונו אנלוגי (שמאל)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "מונו אנלוגי (ימין)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "סטריאו אנלוגי"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "מונו"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "דיבורית לחדר ישיבות"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "ערוצים מרובים"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "סראונד אנלוגי 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "סראונד אנלוגי 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "סראונד אנלוגי 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "סראונד אנלוגי 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "סראונד אנלוגי 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "סראונד אנלוגי 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "סראונד אנלוגי 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "סראונד אנלוגי 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "סראונד אנלוגי 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "סראונד אנלוגי 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "סראונד אנלוגי 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "סטריאו דיגיטלי (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "סראונד דיגיטלי 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "סראונד דיגיטלי 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "סראונד דיגיטלי 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "סטריאו דיגיטלי (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "סראונד דיגיטלי 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "משחק"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "מונו אנלוגי משולב"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "סטריאו אנלוגי משולב"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "סטריאו דיגיטלי משולב (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "סטריאו משולב"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "פלט %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "קלט %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "דיבורית"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "אוזניה"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "נייד"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "מכונית"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "טלפון"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/hi.po b/po/hi.po
new file mode 100644
index 0000000..db483e2
--- /dev/null
+++ b/po/hi.po
@@ -0,0 +1,627 @@
+# translation of pipewire.master-tx.po to Hindi
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Rajesh Ranjan <rajesh672@gmail.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:54+0000\n"
+"Last-Translator: Rajesh Ranjan <rajesh672@gmail.com>\n"
+"Language-Team: Hindi <hindi.sf.net>\n"
+"Language: hi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "आंतरिक ऑडियो"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "मॉडेम"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "बंद"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(अवैध)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "इनपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "डॉकिंग स्टेशन इनपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "डॉकिंग स्टेशन माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "डॉकिंग स्टेशन इनपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "डॉकिंग स्टेशन माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "बाहरी माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "आंतरिक माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "रेडियो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "वीडियो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "स्वचालित प्राप्ति नियंत्रण"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "कोई स्वचालित प्राप्ति नियंत्रण नहीं"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "बूस्ट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "कोई बढ़ावा नहीं"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "एंप्लीफायर"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "कोई एंप्लीफायर नहीं"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "बूस्ट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "कोई बढ़ावा नहीं"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "एनालॉग हेडफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "एनालॉग इनपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "डॉकिंग स्टेशन माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "एनालॉग आउटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "एनालॉग हेडफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "एनालॉग एकल आउटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "लाइन इन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "एनालॉग एकल आउटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "एनालॉग स्टीरियो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "डिजिटल सेटअप (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "डिजिटल सेटअप (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "रिक्त आउटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "रिक्त आउटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "रिक्त आउटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "इनपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "एनालॉग सर्राउंड 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "एनालॉग मोनो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "एनालॉग मोनो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "एनालॉग मोनो"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "एनालॉग स्टीरियो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "मोनो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "एनालॉग स्टीरियो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "एनालॉग सर्राउंड 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "एनालॉग सर्राउंड 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "एनालॉग सर्राउंड 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "एनालॉग सर्राउंड 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "एनालॉग सर्राउंड 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "एनालॉग सर्राउंड 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "एनालॉग सर्राउंड 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "एनालॉग सर्राउंड 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "एनालॉग सर्राउंड 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "एनालॉग सर्राउंड 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "एनालॉग सर्राउंड 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "डिजिटल स्टीरियो (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "डिजिटल सर्राउंड 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "डिजिटल सर्राउंड 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "डिजिटल सर्राउंड 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "डिजिटल सेटअप (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "डिजिटल सर्राउंड 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "एनालॉग एकल डुप्लेक्स"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "एनालॉग स्टीरियो डुप्लेक्स"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "डिजिटल स्टीरियो डुप्लेक्स (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "एनालॉग स्टीरियो डुप्लेक्स"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "रिक्त आउटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "इनपुट"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() ने एक मान दिया जो अप्रत्याशित रूप से बड़ा है: %lu बाइट (%lu ms).\n"
+"अधिक संभव है कि यह ALSA ड्राइवर '%s' में एक बग है. इस मुद्दे को ALSA डेवलेपर को रिपोर्ट "
+"करें."
+msgstr[1] ""
+"snd_pcm_avail() ने एक मान दिया जो अप्रत्याशित रूप से बड़ा है: %lu बाइट (%lu ms).\n"
+"अधिक संभव है कि यह ALSA ड्राइवर '%s' में एक बग है. इस मुद्दे को ALSA डेवलेपर को रिपोर्ट "
+"करें."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() ने एक मान दिया जो अप्रत्याशित रूप से बड़ा है: %li बाइट (%s%lu ms).\n"
+"अधिक संभव है कि यह ALSA ड्राइवर '%s' में एक बग है. इस मुद्दे को ALSA डेवलेपर को रिपोर्ट "
+"करें."
+msgstr[1] ""
+"snd_pcm_delay() ने एक मान दिया जो अप्रत्याशित रूप से बड़ा है: %li बाइट (%s%lu ms).\n"
+"अधिक संभव है कि यह ALSA ड्राइवर '%s' में एक बग है. इस मुद्दे को ALSA डेवलेपर को रिपोर्ट "
+"करें."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() ने एक मान दिया जो अप्रत्याशित रूप से बड़ा है: %lu बाइट (%lu ms).\n"
+"अधिक संभव है कि यह ALSA ड्राइवर '%s' में एक बग है. इस मुद्दे को ALSA डेवलेपर को रिपोर्ट "
+"करें."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() ने एक मान दिया जो अप्रत्याशित रूप से बड़ा है: %lu बाइट (%lu "
+"ms).\n"
+"अधिक संभव है कि यह ALSA ड्राइवर '%s' में एक बग है. इस मुद्दे को ALSA डेवलेपर को रिपोर्ट "
+"करें."
+msgstr[1] ""
+"snd_pcm_mmap_begin() ने एक मान दिया जो अप्रत्याशित रूप से बड़ा है: %lu बाइट (%lu "
+"ms).\n"
+"अधिक संभव है कि यह ALSA ड्राइवर '%s' में एक बग है. इस मुद्दे को ALSA डेवलेपर को रिपोर्ट "
+"करें."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "एनालॉग हेडफोन"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/hr.po b/po/hr.po
new file mode 100644
index 0000000..dbde609
--- /dev/null
+++ b/po/hr.po
@@ -0,0 +1,727 @@
+# Croatian translation for pipewire
+# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010
+# This file is distributed under the same license as the pipewire package.
+# gogo <trebelnik2@gmail.com>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2022-10-01 14:01+0200\n"
+"PO-Revision-Date: 2022-10-01 14:12+0200\n"
+"Last-Translator: gogo <trebelnik2@gmail.com>\n"
+"Language-Team: Croatian <https://translate.fedoraproject.org/projects/"
+"pipewire/pipewire/hr/>\n"
+"Language: hr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Poedit 3.0.1\n"
+"X-Launchpad-Export-Date: 2017-04-20 21:04+0000\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [mogućnosti]\n"
+" -h, --help Prikaži ovu pomoć\n"
+" --version Prikaži inačicu\n"
+" -c, --config Učitaj podešavanje (Zadano %s)\n"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Tunel do %s/%s"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Lažni izlaz"
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Tunel za %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Nepoznat uređaj"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s na %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s na %s"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [mogućnosti] [<datoteka>|-]\n"
+" -h, --help Prikaži ovu pomoć\n"
+" --version Prikaži inačicu\n"
+" -v, --verbose Omogući opširnije radnje\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote Naziv udaljenog pozadinskog "
+"programa\n"
+" --media-type Postavi vrstu medija (zadano je %s)\n"
+" --media-category Postavi kategoriju medija (zadano je "
+"%s)\n"
+" --media-role Postavi namjenu medija (zadano je "
+"%s)\n"
+" --target Postavi odredište čvora (zadano je "
+"%s)\n"
+" 0 znači bez povezivanja\n"
+" --latency Postavi latenciju čvora (zadano je "
+"%s)\n"
+" Xunit (jedinica = s, ms, us, ns)\n"
+" ili izravne uzorke (256)\n"
+" frekvencija je jednaka izvornoj "
+"datoteci\n"
+" -P --properties Postavi svojstva čvora\n"
+"\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Frekvencija (potrebno za snimanje) "
+"(zadano je %u)\n"
+" --channels Broj kanala (potrebno za snimanje) "
+"(zadano je %u)\n"
+" --channel-map Broj kanala\n"
+" jedan od: \"stereo\", "
+"\"surround-51\",... ili\n"
+" zarezom odvojen popis naziva "
+"kanala: npr. \"FL,FR\"\n"
+" --format Format %s (potrebno za snimanje) "
+"(zadano je %s)\n"
+" --volume Glasnoća zvuka strujanja 0-1.0 "
+"(zadano je %.3f)\n"
+" -q --quality Kvaliteta normalizacije zvuka (0 - "
+"15) (zadano je %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Način reprodukcije\n"
+" -r, --record Način snimanja\n"
+" -m, --midi Midi način\n"
+" -d, --dsd DSD način\n"
+"\n"
+
+#: src/tools/pw-cli.c:2250
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [mogućnosti] [naredba]\n"
+" -h, --help Prikaži ovu pomoć\n"
+" --version Prikaži inačicu\n"
+" -d, --daemon Pokreni kao pozadinski program "
+"(Zadano je laž)\n"
+" -r, --remote Naziv udaljenog pozadinskog "
+"programa\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
+msgid "Off"
+msgstr "Isključeno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Ulaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Ulaz priključne stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Mikrofon priključne stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Ulaz priključne stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Ulaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1454
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Prednji mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Stražnji mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Vanjski mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Unutarnji 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 "Automatska kontrola pojačanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Bez automatske kontrole pojačanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Pojačanje"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Bez pojačanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Pojačalo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Bez pojačala"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Pojačanje basa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Bez pojačanja basa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1460
+msgid "Speaker"
+msgstr "Zvučnik"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Slušalice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Analogni ulaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Ugrađeni mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Mikrofon sa slušalicama"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Analogni izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Slušalice 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Mono izlaz za slušalice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Analogni mono izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Zvučnici"
+
+#: 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 izlaz (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitalni ulaz (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Višekanalni ulaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Višekanalni izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Izlaz za igre"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Izlaz razgovora"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Ulaz razgovora"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Virtalni surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Analogni mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Analogni mono (lijevi)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Analogni mono (desni)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Analogni stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+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:1442
+msgid "Headset"
+msgstr "Slušalice s mikrofonom"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Zvučnik"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Višekanalni"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Analogni surround 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Analogni surround 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Analogni surround 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Analogni surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Analogni surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Analogni surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Analogni surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Analogni surround 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Analogni surround 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Analogni surround 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Analogni surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitalni stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitalni surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitalni surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitalni surround 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitalni stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitalni surround 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Razgovor"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Igra"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Analogni mono obostrani"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Analogni stereo obostrani"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digitalni stereo obostrani (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Višekanalni obostrani"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Stereo obostrani"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono razgovor + 7.1 Surround"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "%s izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "%s ulaz"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1187
+#: spa/plugins/alsa/acp/alsa-util.c:1281
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"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 vratio vrijednost koja je iznimno velika: %lu bajt (%lu "
+"ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+msgstr[1] ""
+"snd_pcm_avail() je vratio vrijednost koja je iznimno velika: %lu bajta (%lu "
+"ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+msgstr[2] ""
+"snd_pcm_avail() je vratio vrijednost koja je iznimno velika: %lu bajta (%lu "
+"ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1253
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte "
+"(%s%lu ms).\n"
+"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 vratio vrijednost koja je iznimno velika: %li bajt (%s%lu "
+"ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+msgstr[1] ""
+"snd_pcm_delay() je vratio vrijednost koja je iznimno velika: %li bajta "
+"(%s%lu ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+msgstr[2] ""
+"snd_pcm_delay() je vratio vrijednost koja je iznimno velika: %li bajta "
+"(%s%lu ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1300
+#, 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 vratio nepoznate vrijednosti: kašnjenje %lu je "
+"manje od %lu.\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1343
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"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 vratio vrijednost koja je iznimno velika: %lu bajt "
+"(%lu ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+msgstr[1] ""
+"snd_pcm_mmap_begin() je vratio vrijednost koja je iznimno velika: %lu bajta "
+"(%lu ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+msgstr[2] ""
+"snd_pcm_mmap_begin() je vratio vrijednost koja je iznimno velika: %lu bajta "
+"(%lu ms).\n"
+"Najvjerojatnije je ovo greška ALSA upravljačkog programa '%s'. Prijavite "
+"problem ALSA razvijateljima."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(neispravno)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Ugrađeni zvuk"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Zvučni pristupnik (A2DP izvor i HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1272
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Reprodukcija visoke autentičnosti (A2DP slivnik, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1275
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Telefonija visoke autentičnosti (A2DP slivnik, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Reprodukcija visoke autentičnosti (A2DP slivnik)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1285
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Telefonija visoke autentičnosti (A2DP izvor/slivnik)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Reprodukcija visoke autentičnosti (BAP slivnik, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Ulaz visoke autentičnosti (BAP izvor, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Telefonija visoke autentičnosti (BAP izvor/slivnik, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Jedinica slušalice s mikrofonom (HSP/HFP, kôdek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1364
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Jedinica slušalice s mikrofonom (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Handsfree"
+msgstr "Bez-ruku"
+
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Bez-ruku (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
+msgid "Headphone"
+msgstr "Slušalica"
+
+#: spa/plugins/bluez5/bluez5-device.c:1472
+msgid "Portable"
+msgstr "Prijenosnik"
+
+#: spa/plugins/bluez5/bluez5-device.c:1478
+msgid "Car"
+msgstr "Automobil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1484
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1490
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1497
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
+
+#~ msgid "PipeWire Media System"
+#~ msgstr "PipeWire medijski sustav"
+
+#~ msgid "Start the PipeWire Media System"
+#~ msgstr "Pokreni PipeWire medijski sustav"
diff --git a/po/hu.po b/po/hu.po
new file mode 100644
index 0000000..7fb3b3d
--- /dev/null
+++ b/po/hu.po
@@ -0,0 +1,715 @@
+# Hungarian translation for PipeWire.
+# Copyright (C) 2012, 2016, 2022. Free Software Foundation, Inc.
+# This file is distributed under the same license as the PipeWire package.
+#
+# KAMI <kami911 at gmail dot com>, 2012.
+# Gabor Kelemen <kelemeng at ubuntu dot com>, 2016.
+# Balázs Úr <ur.balazs at fsf dot hu>, 2016, 2022.
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire master\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-09-15 15:26+0000\n"
+"PO-Revision-Date: 2022-09-21 22:35+0200\n"
+"Last-Translator: Balázs Úr <ur.balazs at fsf dot hu>\n"
+"Language-Team: Hungarian <https://translate.fedoraproject.org/projects/"
+"pipewire/pipewire/hu/>\n"
+"Language: hu\n"
+"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: Lokalize 19.12.3\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [kapcsolók]\n"
+" -h, --help Ezen súgó megjelenítése\n"
+" --version Verzió megjelenítése\n"
+" -c, --config Beállítás betöltése (alapérték: %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "PipeWire médiarendszer"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "A PipeWire médiarendszer indítása"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Alagút ide: %s/%s"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Üres kimenet"
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Alagút ehhez: %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Ismeretlen eszköz"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s ezen: %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s ezen: %s"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [kapcsolók] [<fájl>|-]\n"
+" -h, --help Ezen súgó megjelenítése\n"
+" --version Verzió megjelenítése\n"
+" -v, --verbose Részletes műveletek engedélyezése\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote Távoli démon neve\n"
+" --media-type Médiatípus beállítása (alapérték: "
+"%s)\n"
+" --media-category Médiakategória beállítása\n"
+" (alapérték: %s)\n"
+" --media-role Médiaszerep beállítása (alapérték: "
+"%s)\n"
+" --target Csomópont céljának beállítása\n"
+" (alapérték: %s), a 0 azt jelenti,\n"
+" hogy ne linkeljen\n"
+" --latency Csomópont késleltetésének "
+"beállítása\n"
+" (alapérték: %s)\n"
+" Xegység (egység = s, ms, us, ns)\n"
+" vagy közvetlen minták (256)\n"
+" a gyakoriság a forrásfájl egyike\n"
+" -P --properties Csomópont tulajdonságainak "
+"beállítása\n"
+"\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Mintavételi gyakoriság (kötelező a\n"
+" rögzítéshez) (alapérték: %u)\n"
+" --channels Csatornák száma (kötelező a\n"
+" rögzítéshez) (alapérték: %u)\n"
+" --channel-map Csatornaleképezés\n"
+" ezek egyike: „stereo”, "
+"„surround-51”\n"
+" stb. vagy csatornanevek vesszővel\n"
+" tagolt listája, például: „FL,FR”\n"
+" --format Mintavételi formátum: %s (kötelező "
+"a\n"
+" rögzítéshez) (alapérték: %s)\n"
+" --volume Adatfolyam hangereje 0-1.0\n"
+" (alapérték: %.3f)\n"
+" -q --quality Újramintavételezési minőség (0-15)\n"
+" (alapérték: %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Lejátszási mód\n"
+" -r, --record Rögzítési mód\n"
+" -m, --midi Midi mód\n"
+" -d, --dsd DSD mód\n"
+"\n"
+
+#: src/tools/pw-cli.c:2255
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [kapcsolók] [parancs]\n"
+" -h, --help Ezen súgó megjelenítése\n"
+" --version Verzió megjelenítése\n"
+" -d, --daemon Indítás démonként (alapérték: "
+"hamis)\n"
+" -r, --remote Távoli démon neve\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
+msgid "Off"
+msgstr "Ki"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Bemenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Dokkolóállomás bemenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Dokkolóállomás mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Dokkolóállomás vonalbemenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Vonalbemenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1454
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Elülső mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Hátsó mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Külső mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Belső mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "Rádió"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "Videó"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "Automatikus erősítésszabályzás"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Nincs automatikus erősítésszabályzás"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Erősítés"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Nincs erősítés"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Erősítő"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Nincs erősítő"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Basszuskiemelés"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Nincs basszuskiemelés"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1460
+msgid "Speaker"
+msgstr "Hangszóró"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Fejhallgató"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Analóg bemenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Dokkolóállomás mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Fejhallgató mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Analóg kimenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "2. fejhallgató"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Fejhallató monó kimenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Vonalkimenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Analóg monó kimenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Hangszórók"
+
+#: 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 "Digitális kimenet (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitális bemenet (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Többcsatornás bemenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Többcsatornás kimenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Játék kimenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Csevegés kimenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Csevegés bemenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Virtuális térhatás 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Analóg monó"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Analóg monó (bal)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Analóg monó (jobb)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Analóg sztereó"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Monó"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr "Sztereó"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1442
+msgid "Headset"
+msgstr "Fejhallgató"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Mikrofonos fejhallgató"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Többcsatornás"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Analóg térhatású 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Analóg térhatású 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Analóg térhatású 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Analóg térhatású 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Analóg térhatású 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Analóg térhatású 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Analóg térhatású 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Analóg térhatású 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Analóg térhatású 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Analóg térhatású 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Analóg térhatású 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitális sztereó (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitális térhatású 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitális térhatású 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitális térhatású 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitális sztereó (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitális térhatású 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Csevegés"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Játék"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Analóg monó kétirányú"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Analóg sztereó kétirányú"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digitális sztereó kétirányú (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Többcsatornás kétirányú"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Sztereó kétirányú"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Monó csevegés + 7.1 térhatású"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "%s kimenet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "%s bemenet"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"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] ""
+"Az „snd_pcm_avail()” függvény különlegesen nagy értéket adott vissza: %lu "
+"bájt (%lu ms).\n"
+"Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
+"ezt a problémát az ALSA fejlesztői felé."
+msgstr[1] ""
+"Az „snd_pcm_avail()” függvény különlegesen nagy értéket adott vissza: %lu "
+"bájt (%lu ms).\n"
+"Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
+"ezt a problémát az ALSA fejlesztői felé."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1253
+#, 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] ""
+"Az „snd_pcm_delay()” függvény különlegesen nagy értéket adott vissza: %li "
+"bájt (%s%lu ms).\n"
+"Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
+"ezt a problémát az ALSA fejlesztői felé."
+msgstr[1] ""
+"Az „snd_pcm_delay()” függvény különlegesen nagy értéket adott vissza: %li "
+"bájt (%s%lu ms).\n"
+"Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
+"ezt a problémát az ALSA fejlesztői felé."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1300
+#, 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 ""
+"Az „snd_pcm_avail_delay()” függvény furcsa értékeket adott vissza: a "
+"késleltetés (%lu) kisebb, mint az elérhető %lu.\n"
+"Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
+"ezt a problémát az ALSA fejlesztői felé."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1343
+#, 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] ""
+"Az „snd_pcm_mmap_begin()” függvény különlegesen nagy értéket adott vissza: "
+"%lu bájt (%lu ms).\n"
+"Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
+"ezt a problémát az ALSA fejlesztői felé."
+msgstr[1] ""
+"Az „snd_pcm_mmap_begin()” függvény különlegesen nagy értéket adott vissza: "
+"%lu bájt (%lu ms).\n"
+"Ez valószínűleg egy hiba eredménye az ALSA „%s” illesztőprogramban. Jelentse "
+"ezt a problémát az ALSA fejlesztői felé."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(érvénytelen)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Beépített hangforrás"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Hang átjáró (A2DP forrás és HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1272
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Magas hűségű lejátszás (A2DP fogadó, %s kodek)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1275
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Magas hűségű kétirányú (A2DP forrás/fogadó, %s kodek)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Magas hűségű lejátszás (A2DP fogadó)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1285
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Magas hűségű kétirányú (A2DP forrás/fogadó)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Magas hűségű lejátszás (BAP fogadó, %s kodek)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Magas hűségű bemenet (BAP forrás, %s kodek)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Magas hűségű kétirányú (BAP forrás/fogadó, %s kodek)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Fejhallgató fejegység (HSP/HFP, %s kodek)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1364
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Fejhallgató fejegység (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Handsfree"
+msgstr "Kihangosító"
+
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Kihangosító (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
+msgid "Headphone"
+msgstr "Fejhallgató"
+
+#: spa/plugins/bluez5/bluez5-device.c:1472
+msgid "Portable"
+msgstr "Hordozható"
+
+#: spa/plugins/bluez5/bluez5-device.c:1478
+msgid "Car"
+msgstr "Autó"
+
+#: spa/plugins/bluez5/bluez5-device.c:1484
+msgid "HiFi"
+msgstr "Hi-Fi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1490
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1497
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/id.po b/po/id.po
new file mode 100644
index 0000000..f6700a8
--- /dev/null
+++ b/po/id.po
@@ -0,0 +1,671 @@
+# Indonesian translation of pipewire
+# Copyright (C) 2011 THE pipewire'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the pipewire package.
+#
+# Translators:
+# Andika Triwidada <andika@gmail.com>, 2011, 2012, 2018.
+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"
+"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"
+
+#: src/daemon/pipewire.c:45
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [opsi]\n"
+" -h, --help Tampilkan bantuan ini\n"
+" --version Tampilkan versi\n"
+" -c, --config Muat konfig (Baku %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Sistem Media PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+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
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Tunnel ke %s/%s"
+
+#: src/modules/module-pulse-tunnel.c:511
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Tunnel untuk %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:302
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s pada %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:306
+#, c-format
+msgid "%s on %s"
+msgstr "%s pada %s"
+
+#: src/tools/pw-cat.c:991
+#, 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 [opsi] <berkas>\n"
+" -h, --help Menampilkan bantuan ini\n"
+" --version Menampilkan versi\n"
+" -v, --verbose Memfungsikan pesan rinci\n"
+"\n"
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+" -R, --remote Nama daemon remote\n"
+" --media-type Atur tipe media (baku %s)\n"
+" --media-category Atur kategori media (baku %s)\n"
+" --media-role Atur peran media (baku %s)\n"
+" --target Atur target simpul (baku %s)\n"
+" 0 berarti jangan tautkan\n"
+" --latency Atur latensi simpul (baku %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or cuplikan langsung (256)\n"
+" laju adalah satu dari berkas "
+"sumber\n"
+" --list-targets Daftar target yang tersedia untuk --"
+"target\n"
+"\n"
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Laju cuplik (req. for rec) (baku "
+"%u)\n"
+" --channels Cacah kanal (req. for rec) (baku "
+"%u)\n"
+" --channel-map Peta kanal\n"
+" satu dari: \"stereo\", "
+"\"surround-51\",... or\n"
+" daftar dipisah koma dari nama "
+"kanal: mis. \"FL,FR\"\n"
+" --format Format cuplikan %s (req. for rec) "
+"(baku %s)\n"
+" --volume Volume stream 0-1.0 (baku %.3f)\n"
+" -q --quality Kualitas resampler (0 - 15) (baku "
+"%d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+" -p, --playback Mode main ulang\n"
+" -r, --record Mode perekaman\n"
+" -m, --midi Mode midi\n"
+"\n"
+
+#: src/tools/pw-cli.c:2959
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [opsi] [perintah]\n"
+" -h, --help Tampilkan bantuan ini\n"
+" --version Tampilkan versi\n"
+" -d, --daemon Mulai sebagai daemon (Baku = false)\n"
+" -r, --remote Nama daemon remote\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:291
+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
+msgid "Off"
+msgstr "Mati"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Masukan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Masukan Docking Station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Mikrofon Docking Station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+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
+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
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Mikrofon Depan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Mikrofon Belakang"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Mikrofon Eksternal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Mikrofon Internal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Kendali Penguatan Otomatis (AGC)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Tanpa Kendali Penguatan Otomatis (AGC)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Tanpa Boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Penguat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Tanpa Penguat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Boost Bass"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Tanpa Boost Bass"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1180
+msgid "Speaker"
+msgstr "Speaker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Headphone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Masukan Analog"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Mikrofon Dok"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Mikrofon Headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Keluaran Analog"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Headphone 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Keluaran Mono Headphone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Line Out"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Keluaran Mono Analog"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Speaker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Keluaran Digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Masukan Digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Masukan Multikanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Keluaran Multikanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Keluaran Permainan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Keluaran Obrolan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "Masukan Obrolan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Virtual Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analog Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Analog Mono (Kiri)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Analog Mono (Kanan)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analog Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "Speakerphone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multikanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analog Surround 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analog Surround 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analog Surround 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analog Surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analog Surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analog Surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analog Surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analog Surround 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analog Surround 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analog Surround 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analog Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digital Stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digital Surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digital Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Surround 5.1 Digital (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digital Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Surround 5.1 Digital (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "Obrolan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Permainan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analog Mono Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analog Stereo Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digital Stereo Duplex (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Dupleks Multikanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Dupleks Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono Chat + 7.1 Surround"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "Keluaran %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "Masukan %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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() 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."
+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
+#, 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() 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."
+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
+#, 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() mengembalikan nilai yang aneh: tundaan %lu kurang dari "
+"yang tersedia %lu.\n"
+"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
+#, 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() 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."
+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
+msgid "(invalid)"
+msgstr "(tak valid)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1030
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Audio Gateway (A2DP Source & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1053
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "High Fidelity Playback (A2DP Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1055
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1061
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "High Fidelity Playback (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1063
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "High Fidelity Duplex (A2DP Source/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1090
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Headset Head Unit (HSP/HFP, codec %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:1170
+msgid "Handsfree"
+msgstr "Handsfree"
+
+#: spa/plugins/bluez5/bluez5-device.c:1185
+msgid "Headphone"
+msgstr "Headphone"
+
+#: spa/plugins/bluez5/bluez5-device.c:1190
+msgid "Portable"
+msgstr "Portabel"
+
+#: spa/plugins/bluez5/bluez5-device.c:1195
+msgid "Car"
+msgstr "Mobil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1200
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1205
+msgid "Phone"
+msgstr "Telepon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1211
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/it.po b/po/it.po
new file mode 100644
index 0000000..bdd47ba
--- /dev/null
+++ b/po/it.po
@@ -0,0 +1,600 @@
+# Italian translation for PipeWire.
+# Copyright (C) 2008, 2009, 2012, 2015, 2019 The Free Software Foundation, Inc
+# This file is distributed under the same license as the pipewire package.
+#
+# Luca Ferretti <elle.uca@libero.it>, 2008, 2009.
+# mario_santagiuliana <mario at marionline.it>, 2009.
+# Milo Casagrande <milo@ubuntu.com>, 2009, 2012, 2015, 2019.
+# Albano Battistella <albano_battistella@hotmail.com>,2021
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2021-05-09 13:29+0100\n"
+"Last-Translator: Albano Battistella <albano_battistella@hotmail.com>\n"
+"Language-Team: Italian <>"
+"pipewire/pipewire/it/>\n"
+"Language: it\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: Weblate 4.2.2\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Sistema multimediale PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Avvia il sistema multimediale PipeWire"
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Audio interno"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr "Dispositivo sconosciuto"
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+msgid "Pro Audio"
+msgstr "Audio Professionale"
+
+#: spa/plugins/alsa/acp/acp.c:411 spa/plugins/alsa/acp/alsa-mixer.c:4704
+#: spa/plugins/bluez5/bluez5-device.c:1000
+msgid "Off"
+msgstr "Spento"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(non valido)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Ingresso"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Ingresso docking station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Microfono della docking station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Linea di ingresso nella docking station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Linea di ingresso"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Microfono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Microfono anteriore"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Microfono posteriore"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Microfono esterno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Microfono interno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Controllo automatico del guadagno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Nessun controllo automatico del guadagno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Nessun boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Amplificatore"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Nessun amplificatore"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Incremento bassi"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Nessun incremento bassi"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Altoparlante"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Cuffie analogiche"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Ingresso analogico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Microfono docking station"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Microfono auricolare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Uscita analogica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Cuffie 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Uscita mono cuffie"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Linea di uscita"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Uscita mono analogica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Altoparlanti"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Uscita digitale (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Ingresso digitale (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Ingresso multi canale"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Uscita multi canale"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Uscita gioco"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Uscita conversazione"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "Ingresso Chat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Virtual Sorround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Mono analogico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Mono analogico (Sinistra)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Mono analogico (Destra)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Stereo analogico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Cuffie con microfono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "Vivavoce"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multicanale"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Surround analogico 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Surround analogico 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Surround analogico 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Surround analogico 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Surround analogico 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Surround analogico 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Surround analogico 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Surround analogico 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Surround analogico 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Surround analogico 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Surround analogico 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Stereo digitale (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Surround digitale 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Surround digitale 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Surround digitale 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Stereo digitale (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Surround digitale 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "Chat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Gioco"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Duplex mono analogico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Duplex stereo analogico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Duplex stereo digitale (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Duplex multicanale"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Duplex stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "Uscita «%s»"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "Ingresso «%s»"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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() ha restituito un valore molto grande: %lu byte (%lu ms).\n"
+"Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
+"questo problema ai suoi sviluppatori."
+msgstr[1] ""
+"snd_pcm_avail() ha restituito un valore molto grande: %lu byte (%lu ms).\n"
+"Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
+"questo problema ai suoi sviluppatori."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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() ha restituito un valore molto grande: %li byte (%s%lu ms).\n"
+"Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
+"questo problema ai suoi sviluppatori."
+msgstr[1] ""
+"snd_pcm_delay() ha restituito un valore molto grande: %li byte (%s%lu ms).\n"
+"Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
+"questo problema ai suoi sviluppatori."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail() ha restituito dei valori strani: delay %lu è minore di avail "
+"%lu.\n"
+"Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
+"questo problema ai suoi sviluppatori."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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() ha restituito un valore molto grande: %lu byte (%lu "
+"ms).\n"
+"Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
+"questo problema ai suoi sviluppatori."
+msgstr[1] ""
+"snd_pcm_mmap_begin() ha restituito un valore molto grande: %lu byte (%lu "
+"ms).\n"
+"Molto probabilmente si tratta di un bug nel driver ALSA «%s». Segnalare "
+"questo problema ai suoi sviluppatori."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Gateway Audio (Sorgente A2DP & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Riproduzione ad alta fedeltà (A2DP Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Duplex ad alta fedeltà (Sorgente/Sink, A2DP codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Riproduzione ad alta fedeltà (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Duplex ad alta fedeltà (A2DP Source/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Vivavoce"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Cuffie"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Portabile"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Automobile"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefono"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/ja.po b/po/ja.po
new file mode 100644
index 0000000..bb16670
--- /dev/null
+++ b/po/ja.po
@@ -0,0 +1,602 @@
+# translation of ja.po to Japanese
+# PipeWire
+# Copyright (C) 2009.
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Hyu_gabaru Ryu_ichi <hyu_gabaru@yahoo.co.jp>, 2009.
+# Kiyoto Hashida <khashida@redhat.com>, 2009, 2012.
+# Kenzo Moriguchi <kmoriguc@redhat.com>, 2016. #zanata
+# Ooyama Yosiyuki <qqke6wd9k@apricot.ocn.ne.jp>, 2016. #zanata
+# Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2016-03-15 08:25+0000\n"
+"Last-Translator: Kenzo Moriguchi <kmoriguc@redhat.com>\n"
+"Language-Team: Japanese <jp@li.org>\n"
+"Language: ja\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Zanata 4.6.2\n"
+"Plural-Forms: Plural-Forms: nplurals=1; plural=0;\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "内部オーディオ"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "モデム"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "オフ"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "無効)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "入力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "ドッキングステーション入力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "ドッキングステーションマイクロフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "ドッキングステーションライン入力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "マイクロフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "フロントマイクロフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "リアマイクロフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "外部マイクロフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "内部マイクロフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "ラジオ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "ビデオ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "自動ゲイン制御"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "自動ゲイン制御なし"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "ブースト"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "ブーストなし"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "アンプ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "アンプなし"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "低音ブースト"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "低音ブーストなし"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "スピーカー"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "アナログヘッドフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "アナログ入力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "ドッキングステーションマイクロフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "ヘッドセットマイクロフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "アナログ出力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "アナログヘッドフォン"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "アナログモノ出力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "ライン出力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "アナログモノ出力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "スピーカー"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "デジタル出力 (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "デジタル入力 (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#, fuzzy
+msgid "Multichannel Input"
+msgstr "マルチチャネル"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "マルチチャネル"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "%s 出力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "%s 出力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "%s 入力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "仮想サラウンドシンク"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "アナログモノ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "アナログモノ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "アナログモノ"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "アナログステレオ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "モノ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "ヘッドセット"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "スピーカー"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "マルチチャネル"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "アナログサラウンド 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "アナログサラウンド 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "アナログサラウンド 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "アナログサラウンド 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "アナログサラウンド 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "アナログサラウンド 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "アナログサラウンド 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "アナログサラウンド 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "アナログサラウンド 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "アナログサラウンド 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "アナログサラウンド 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "デジタルステレオ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "デジタルサラウンド 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "デジタルサラウンド 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "デジタルサラウンド 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "デジタルステレオ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "デジタルサラウンド 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "アナログモノデュプレックス"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "アナログステレオデュプレックス"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "デジタルステレオデュプレックス (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+#, fuzzy
+msgid "Multichannel Duplex"
+msgstr "マルチチャネル"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "アナログステレオデュプレックス"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s 出力"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s 入力"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() は 例外的に大きな値を返しました: %lu バイト(%lu ms)。\n"
+"これは多分、ALSA ドライバー '%s' 内のバグです。この問題は ALSA 開発者宛に報告"
+"を提出して下さい。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() は 例外的に大きな値を返しました: %li バイト(%s%lu ms)。\n"
+"これは多分、ALSA ドライバー '%s' 内のバグです。この問題は ALSA 開発者宛に報告"
+"を提出して下さい。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() がおかしな値を返しました: 遅延 %lu は有効な値 %lu 未満"
+"です。\n"
+"これは多分、ALSA ドライバー '%s' 内のバグです。この問題は ALSA 開発者宛に報告"
+"を提出して下さい。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() は 例外的に大きな値を返しました: %lu バイト(%lu "
+"ms)。\n"
+"これは多分、ALSA ドライバー '%s' 内のバグです。この問題は ALSA 開発者宛に報告"
+"を提出して下さい。"
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "ハンズフリー"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "ヘッドフォン"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "ポータブル"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "車"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "電話"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Bluetooth 入力"
diff --git a/po/ka.po b/po/ka.po
new file mode 100644
index 0000000..47f8067
--- /dev/null
+++ b/po/ka.po
@@ -0,0 +1,692 @@
+# Georgian translation for pipewire.
+# Copyright (C) 2022 pipewire'S authors
+# This file is distributed under the same license as the pipewire package.
+# Temuri Doghonadze <temuri.doghonadze@gmail.com>, 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2022-06-30 12:50+0200\n"
+"PO-Revision-Date: 2022-11-20 11:50+0100\n"
+"Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n"
+"Language-Team: \n"
+"Language: ka\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 3.2\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [პარამეტრები]\n"
+" -h, --help ამ დახმარების ჩვენება\n"
+" --version ვერსიის ჩვენება\n"
+" -c, --config ჩატვირთვის კონფიგურაცია (ნაგულისხმები %s)\n"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "გვირაბი %s/%s -მდე"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "ნულოვანი გამოყვანა"
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "გვირაბი %s@%s-სთვის"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "უცნობი მოწყობილობა"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s %s@%s -ზე"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s %s-ზე"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [პარამეტრები] [<ფაილი>|-]\n"
+" -h, --help ამ დახმარების ჩვენება\n"
+" --version ვერსიის ჩვენება\n"
+" -v, --verbose დამატებითი შეტყობინებების გამოტანა\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote დაშორებული დემონის სახელი\n"
+" --media-type მედიის ტიპის დაყენება (ნაგულისხმები %s)\n"
+" --media-category მედია კატეგორიის დაყენება (ნაგულისხმები %s)\n"
+" --media-role მედიის როლის დაყენება (ნაგულისხმები %s)\n"
+" --target კვანძის სამიზნის დაყენება (ნაგულისხმები %s)\n"
+" 0 ნიშნავს არ მიბმა\n"
+" --latency კვანძის შეყოვნების დაყენება (ნაგულისხმები %s)\n"
+" Xunit (ერთეული = s, ms, us, ns)\n"
+" ან პირდაპირი ნიმუშები (256)\n"
+" მაჩვენებელი არის ერთ-ერთი წყაროს "
+"ფაილი\n"
+" -P --properties კვანძის თვისებების დაყენება\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate სემპლის_სიჩქარე (მოთხოვნილება rec.) (ნაგულისხმები %u)\n"
+" --channels არხების რაოდენობა (მოთხოვნილი ჩანაწერისთვის) (ნაგულისხმები "
+"%u)\n"
+" --channel-map არხის რუკა\n"
+" ერთ-ერთი: \"stereo\", "
+"\"surround-51\",... ან\n"
+" მძიმით გამოყოფილი არხის "
+"სახელების სია: მაგ. \"FL, FR\"\n"
+" --format ნიმუშის ფორმატი %s (მოთხოვნილება rec.) "
+"(ნაგულისხმები %s)\n"
+" --volume ნაკადის მოცულობა 0-1.0 (ნაგულისხმები %.3f)\n"
+" -q --quality Resampler ხარისხი (0 - 15) "
+"(ნაგულისხმები %d)\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback დაკვრის რეჟიმი\n"
+" -r, -- record ჩაწერის რეჟიმი\n"
+" -m, --midi Midi რეჟიმი\n"
+" -d, --dsd DSD რეჟიმი\n"
+"\n"
+
+#: src/tools/pw-cli.c:2250
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [პარამეტრები] [ბრძანება]\n"
+" -h, --help ამ დახმარების ჩვენება\n"
+" --version ვერსიის ჩვენება\n"
+" -d, --daemon დაწყება როგორც დემონი (ნაგულისხმები "
+"false)\n"
+" -r, --remote დაშორებული დემონის სახელი\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
+msgid "Off"
+msgstr "გამორთული"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Docking Station-ის შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Docking Station-ის მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Docking Station Line In"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Line In"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1454
+msgid "Microphone"
+msgstr "მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "წინა მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "უკანა მიკფოფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "გარე მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "შიდა მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "რადიო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "ვიდეო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "ხმის მომატების ავტომატური კონტროლი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "ხმის მომატების ავტომატური კონტროლის გამორთვა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "გაძლიერება"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "გაძლიერების გარეშე"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "გამაძლიერებელი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "გამაძლიერებლის გარეშე"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Bass-ის გაძლიერება"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Bass-ის გაძლიერების გარეშე"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1460
+msgid "Speaker"
+msgstr "დინამიკი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "ყურსაცვამები"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "ანალოგური შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "მისამაგრებელი მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "ყურსაცვამის მიროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "ანალოგური გამოტანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "ყურსაცვამები 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "ყურსაცვამები მონო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "ხაზოვანი გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "ანალოგური მონო გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "დინამიკები"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2757
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2758
+msgid "Digital Output (S/PDIF)"
+msgstr "ციფრული გამოყვანა (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "ციფრული შეტანა (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "მრავალარხიანი შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "მრავალარხიანი გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "თამაშის გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "ჩატის გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "ჩატის შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "ვირტუალური სივრცითი ხმა 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "ანალოგური მონო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "ანალოგური მონო (მარცხენა)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "ანალოგური მონო (მარჯვენა)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "ანალოგური სტერეო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "მონო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr "სტერეო"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1442
+msgid "Headset"
+msgstr "ყურსაცვამები & მიკროფონი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "სამაგიდო დინამიკი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "მრავალარხიანი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "ანალოგური სივრცითი 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "ანალოგური სივრცითი 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "ანალოგური სივრცითი 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "ანალოგური სივრცითი 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "ანალოგური სივრცითი 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "ანალოგური სივრცითი 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "ანალოგური სივრცითი 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "ანალოგური სივრცითი 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "ანალოგური სივრცითი 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "ანალოგური სივრცითი 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "ანალოგური სივრცითი 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "ციფრული სტერეო (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ციფრული სივრცითი 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ციფრული სივრცითი 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ციფრული სივრცითი 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "ციფრული სტერეო (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ციფრული სივრცითი 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "ჩატი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "თამაში"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "ანალოგური მონო დუპლექსი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "ანალოგური სტერეო დუპლექსი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ციფრული სტერეო დუპლექსი (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "მრავალარხიანი დუპლექსი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "სტერეო დუპლექსი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "მონო ჩატი + 7.1 სივრცითი"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "%s გამოყვანა"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "%s შეყვანა"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: %lu "
+"ბაიტი (%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+msgstr[1] ""
+"snd_pcm_avail()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: %lu "
+"ბაიტი (%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1253
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: %li "
+"ბაიტი (%s%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+msgstr[1] ""
+"snd_pcm_delay()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: %li "
+"ბაიტი (%s%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1300
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay()-ის მიერ დაბრუნებული მნიშვნელობები უცნაურია: დაყოვნება "
+"%lu უფრო მცირეა, ვიდრე ხელმისაწვდომი დრო %lu.\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1343
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: "
+"%lu ბაიტი (%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+msgstr[1] ""
+"snd_pcm_mmap_begin()-ის მიერ დაბრუნებული მნიშვნელობა არაჩვეულებრივად დიდია: "
+"%lu ბაიტი (%lu მწმ).\n"
+"ყველაზე ხშირად ეს ALSA-ს დრაივერის (%s) შეცდომის გამო ხდება. დაუკავშირდით "
+"ALSA-ის პროგრამისტებს."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(არასწორი)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "ჩაშენებული აუდიო"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "მოდემი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Audio Gateway (A2DP წყარო & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1272
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "მაღალი ხარისხის ხმა (A2DP Sink, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1275
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "მაღალი ხარისხის დუპლექსი (A2DP წყარო/Sink, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "მაღალი ხარისხის ხმა (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1285
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "მაღალი ხარისხის დუპლექსი(A2DP წყარო/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "მაღალი ხარისხის დაკვრა (BAP Sink, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "მაღალი ხარისხის შეყვანა (BAP წყარო, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "მაღალი ხარისხის დუპლექსი (BAP წყარო/Sink, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Headset Head Unit (HSP/HFP, კოდეკი %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1364
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Headset Head Unit (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Handsfree"
+msgstr "ხელის გარეშე სამართავი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "ხელის გარეშე სამართავი (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
+msgid "Headphone"
+msgstr "ყურსაცვამი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1472
+msgid "Portable"
+msgstr "გადატანადი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1478
+msgid "Car"
+msgstr "მანქანა"
+
+#: spa/plugins/bluez5/bluez5-device.c:1484
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1490
+msgid "Phone"
+msgstr "ტელეფონი"
+
+#: spa/plugins/bluez5/bluez5-device.c:1497
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/kk.po b/po/kk.po
new file mode 100644
index 0000000..6a00211
--- /dev/null
+++ b/po/kk.po
@@ -0,0 +1,578 @@
+# Kazakh translation of pipewire.
+# Copyright (C) 2020 The pipewire authors.
+# This file is distributed under the same license as the pipewire package.
+# Baurzhan Muftakhidinov <baurthefirst@gmail.com>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2020-06-30 08:04+0500\n"
+"Last-Translator: Baurzhan Muftakhidinov <baurthefirst@gmail.com>\n"
+"Language-Team: \n"
+"Language: kk\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.3.1\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Құрамындағы аудио"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Модем"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Сөнд."
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(жарамсыз)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Кіріс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Док-станция кірісі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Док-станция микрофоны"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Док-станцияның сызықтық кірісі"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "Микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Алдыңғы микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Артқы микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Сыртқы микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Ішкі микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Радио"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Видео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Күшейтуді автореттеу"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Күшейтуді автореттеу жоқ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Күшейту"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Күшейту жоқ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Күшейткіш"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Күшейткіш жоқ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Бас күшейту"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Бас күшейту жоқ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Динамик"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Құлаққаптар"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Аналогтық кіріс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Док-станция микрофоны"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Гарнитура микрофоны"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Аналогтық шығыс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Құлаққаптар"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Құлаққаптардың моно шығысы"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Сызықтық шығыс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Аналогтық моно шығысы"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Динамиктер"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Цифрлық шығыс (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Цифрлық кіріс (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Көпарналы кіріс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Көпарналы шығыс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Ойын шығысы"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Чат шығысы"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Чат шығысы"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Виртуалды көлемді аудиоқабылдағыш"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Аналогтық моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Аналогтық моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Аналогтық моно"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Аналогтық стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Гарнитура"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Динамик"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Көпарналы"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Аналогтық көлемді 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Аналогтық көлемді 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Аналогтық көлемді 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Аналогтық көлемді 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Аналогтық көлемді 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Аналогтық көлемді 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Аналогтық көлемді 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Аналогтық көлемді 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Аналогтық көлемді 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Аналогтық көлемді 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Аналогтық көлемді 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Цифрлық стерео (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Цифрлық көлемді 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Цифрлық көлемді 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Цифрлық көлемді 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Цифрлық стерео (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Цифрлық көлемді 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Аналогтық моно дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Аналогтық стерео дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Цифрлық стерео дуплекс (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Көпарналы дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Стерео дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s шығысы"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s кірісі"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Хендс-фри"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Құлаққап"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Портативті динамик"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Автомобильдік динамик"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Телефон"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/kn.po b/po/kn.po
new file mode 100644
index 0000000..6f94355
--- /dev/null
+++ b/po/kn.po
@@ -0,0 +1,618 @@
+# translation of pipewire.master-tx.kn.po to Kannada
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Shankar Prasad <svenkate@redhat.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.kn\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:54+0000\n"
+"Last-Translator: Shankar Prasad <svenkate@redhat.com>\n"
+"Language-Team: Kannada <kde-l10n-kn@kde.org>\n"
+"Language: kn\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Lokalize 1.0\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "ಆಂತರಿಕ ಆಡಿಯೊ"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "ಮಾಡೆಮ್"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "ಜಡ"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ಅಮಾನ್ಯ)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "ಇನ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "ಡಾಕಿಂಗ್ ಸ್ಟೇಶನ್ ಇನ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "ಡಾಕಿಂಗ್ ಸ್ಟೇಶನ್ ಮೈಕ್ರೊಫೋನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "ಡಾಕಿಂಗ್ ಸ್ಟೇಶನ್ ಇನ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "ಮೈಕ್ರೊಫೋನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "ಡಾಕಿಂಗ್ ಸ್ಟೇಶನ್ ಮೈಕ್ರೊಫೋನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "ಮೈಕ್ರೊಫೋನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "ಬಾಹ್ಯ ಮೈಕ್ರೊಫೋನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "ಆಂತರಿಕ ಮೈಕ್ರೊಫೋನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "ರೇಡಿಯೊ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "ವೀಡಿಯೊ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "ಆಟೊಮ್ಯಾಟಿಕ್ ಗೇನ್ ಕಂಟ್ರೋಲ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "ಯಾವುದೆ ಆಟೊಮ್ಯಾಟಿಕ್ ಗೇನ್ ಕಂಟ್ರೋಲ್ ಇಲ್ಲ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "ಬೂಸ್ಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "ಯಾವುದೆ ಬೂಸ್ಟ್ ಇಲ್ಲ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "ಆಂಪ್ಲಿಫಯರ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "ಯಾವುದೆ ಆಂಪ್ಲಿಫಯರ್ ಇಲ್ಲ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "ಬೂಸ್ಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "ಯಾವುದೆ ಬೂಸ್ಟ್ ಇಲ್ಲ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "ಅನಲಾಗ್ ಹೆಡ್‌ಫೋನ್‌ಗಳು"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "ಅನಲಾಗ್ ಇನ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "ಡಾಕಿಂಗ್ ಸ್ಟೇಶನ್ ಮೈಕ್ರೊಫೋನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "ಮೈಕ್ರೊಫೋನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "ಅನಲಾಗ್ ಔಟ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "ಅನಲಾಗ್ ಹೆಡ್‌ಫೋನ್‌ಗಳು"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "ಅನಲಾಗ್ ಮೊನೊ ಔಟ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "ಲೈನ್-ಇನ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "ಅನಲಾಗ್ ಮೊನೊ ಔಟ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "ಅನಲಾಗ್ ಸ್ಟೀರಿಯೋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "ಡಿಜಿಟಲ್ ಸ್ಟೀರಿಯೊ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "ಡಿಜಿಟಲ್ ಸ್ಟೀರಿಯೊ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "ಶೂನ್ಯ ಔಟ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "ಶೂನ್ಯ ಔಟ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "ಶೂನ್ಯ ಔಟ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "ಇನ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "ಅನಲಾಗ್ ಮೊನೊ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "ಅನಲಾಗ್ ಮೊನೊ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "ಅನಲಾಗ್ ಮೊನೊ"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "ಅನಲಾಗ್ ಸ್ಟೀರಿಯೋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "ಮೊನೊ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "ಅನಲಾಗ್ ಸ್ಟೀರಿಯೋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "ಅನಲಾಗ್ ಸರೌಂಡ್‌ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "ಡಿಜಿಟಲ್ ಸ್ಟೀರಿಯೊ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ಡಿಜಿಟಲ್ ಸರೌಂಡ್ 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ಡಿಜಿಟಲ್ ಸರೌಂಡ್ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ಡಿಜಿಟಲ್ ಸರೌಂಡ್ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "ಡಿಜಿಟಲ್ ಸ್ಟೀರಿಯೊ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ಡಿಜಿಟಲ್ ಸರೌಂಡ್ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "ಅನಲಾಗ್ ಮೊನೊ ಡ್ಯೂಪ್ಲೆಕ್ಸ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "ಅನಲಾಗ್ ಸ್ಟೀರಿಯೊ ಡ್ಯೂಪ್ಲೆಕ್ಸ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ಅನಲಾಗ್ ಸ್ಟೀರಿಯೊ ಡ್ಯೂಪ್ಲೆಕ್ಸ್ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "ಅನಲಾಗ್ ಸ್ಟೀರಿಯೊ ಡ್ಯೂಪ್ಲೆಕ್ಸ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "ಶೂನ್ಯ ಔಟ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "ಇನ್‌ಪುಟ್"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() ದಿಂದ ಅತ್ಯಂತ ದೊಡ್ಡದಾದ ಮೌಲ್ಯವು ಮರಳಿದೆ: %lu ಬೈಟ್‌ಗಳು (%lu ms).\n"
+"ಇದಕ್ಕೆ ALSA ಚಾಲಕ '%s' ದಲ್ಲಿನ ಒಂದು ದೋಷದ ಕಾರಣವಿರಬಹುದು. ದಯವಿಟ್ಟುಈ ತೊಂದರೆಯನ್ನು ALSA "
+"ವಿಕಸನಗಾರರ ಗಮನಕ್ಕೆ ತನ್ನಿ."
+msgstr[1] ""
+"snd_pcm_avail() ದಿಂದ ಅತ್ಯಂತ ದೊಡ್ಡದಾದ ಮೌಲ್ಯವು ಮರಳಿದೆ: %lu ಬೈಟ್‌ಗಳು (%lu ms).\n"
+"ಇದಕ್ಕೆ ALSA ಚಾಲಕ '%s' ದಲ್ಲಿನ ಒಂದು ದೋಷದ ಕಾರಣವಿರಬಹುದು. ದಯವಿಟ್ಟುಈ ತೊಂದರೆಯನ್ನು ALSA "
+"ವಿಕಸನಗಾರರ ಗಮನಕ್ಕೆ ತನ್ನಿ."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() ದಿಂದ ಅತ್ಯಂತ ದೊಡ್ಡದಾದ ಮೌಲ್ಯವು ಮರಳಿದೆ: %li ಬೈಟ್‌ಗಳು (%s%lu ms).\n"
+"ಇದಕ್ಕೆ ALSA ಚಾಲಕ '%s' ದಲ್ಲಿನ ಒಂದು ದೋಷದ ಕಾರಣವಿರಬಹುದು. ದಯವಿಟ್ಟುಈ ತೊಂದರೆಯನ್ನು ALSA "
+"ವಿಕಸನಗಾರರ ಗಮನಕ್ಕೆ ತನ್ನಿ."
+msgstr[1] ""
+"snd_pcm_delay() ದಿಂದ ಅತ್ಯಂತ ದೊಡ್ಡದಾದ ಮೌಲ್ಯವು ಮರಳಿದೆ: %li ಬೈಟ್‌ಗಳು (%s%lu ms).\n"
+"ಇದಕ್ಕೆ ALSA ಚಾಲಕ '%s' ದಲ್ಲಿನ ಒಂದು ದೋಷದ ಕಾರಣವಿರಬಹುದು. ದಯವಿಟ್ಟುಈ ತೊಂದರೆಯನ್ನು ALSA "
+"ವಿಕಸನಗಾರರ ಗಮನಕ್ಕೆ ತನ್ನಿ."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() ದಿಂದ ಅತ್ಯಂತ ದೊಡ್ಡದಾದ ಮೌಲ್ಯವು ಮರಳಿದೆ: %lu ಬೈಟ್‌ಗಳು (%lu ms).\n"
+"ಇದಕ್ಕೆ ALSA ಚಾಲಕ '%s' ದಲ್ಲಿನ ಒಂದು ದೋಷದ ಕಾರಣವಿರಬಹುದು. ದಯವಿಟ್ಟುಈ ತೊಂದರೆಯನ್ನು ALSA "
+"ವಿಕಸನಗಾರರ ಗಮನಕ್ಕೆ ತನ್ನಿ."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() ದಿಂದ ಅತ್ಯಂತ ದೊಡ್ಡದಾದ ಮೌಲ್ಯವು ಮರಳಿದೆ: %lu ಬೈಟ್‌ಗಳು (%lu ms).\n"
+"ಇದಕ್ಕೆ ALSA ಚಾಲಕ '%s' ದಲ್ಲಿನ ಒಂದು ದೋಷದ ಕಾರಣವಿರಬಹುದು. ದಯವಿಟ್ಟುಈ ತೊಂದರೆಯನ್ನು ALSA "
+"ವಿಕಸನಗಾರರ ಗಮನಕ್ಕೆ ತನ್ನಿ."
+msgstr[1] ""
+"snd_pcm_mmap_begin() ದಿಂದ ಅತ್ಯಂತ ದೊಡ್ಡದಾದ ಮೌಲ್ಯವು ಮರಳಿದೆ: %lu ಬೈಟ್‌ಗಳು (%lu ms).\n"
+"ಇದಕ್ಕೆ ALSA ಚಾಲಕ '%s' ದಲ್ಲಿನ ಒಂದು ದೋಷದ ಕಾರಣವಿರಬಹುದು. ದಯವಿಟ್ಟುಈ ತೊಂದರೆಯನ್ನು ALSA "
+"ವಿಕಸನಗಾರರ ಗಮನಕ್ಕೆ ತನ್ನಿ."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "ಅನಲಾಗ್ ಹೆಡ್‌ಫೋನ್‌ಗಳು"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/ko.po b/po/ko.po
new file mode 100644
index 0000000..a2d23f1
--- /dev/null
+++ b/po/ko.po
@@ -0,0 +1,596 @@
+# eukim <eukim@redhat.com>, 2013. #zanata
+# KimJeongYeon <jeongyeon.kim@samsung.com>, 2017.
+# Sangchul Lee <sc11.lee@samsung.com>, 2018.
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2018-06-21 15:10+0900\n"
+"Last-Translator: Sangchul Lee <sc11.lee@samsung.com>\n"
+"Language-Team: Korean\n"
+"Language: ko\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.8.7.1\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "내장 오디오 "
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "모뎀 "
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "끄기 "
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(잘못됨)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "입력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "도킹 스테이션 입력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "도킹 스테이션 마이크"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "도킹 스테이션 라인 입력 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "마이크"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "전면 마이크 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "후면 마이크 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "외부 마이크 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "내부 마이크 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "라디오 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "비디오 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "자동 게인 컨트롤"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "자동 게인 컨트롤 없음"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "부스트"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "부스트 없음"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "증폭"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "증폭 없음"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "베이스 부스트"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "베이스 부스트 없음"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "스피커"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "헤드폰"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "아날로그 입력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "도킹 스테이션 마이크 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "후면 마이크 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "아날로그 출력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "헤드폰"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "아날로그 모노 출력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "라인 출력 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "아날로그 모노 출력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "스피커"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "디지털 출력 (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "디지털 입력 (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+#, fuzzy
+msgid "Multichannel Input"
+msgstr "아날로그 4-채널 입력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "빈 출력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "%s 출력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "%s 출력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "%s 입력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "가상 서라운드 싱크 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "아날로그 모노 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "아날로그 모노 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "아날로그 모노 "
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "아날로그 스테레오 "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "모노"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "스피커"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "아날로그 서라운드 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "아날로그 서라운드 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "아날로그 서라운드 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "아날로그 서라운드 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "아날로그 서라운드 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "아날로그 서라운드 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "아날로그 서라운드 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "아날로그 서라운드 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "아날로그 서라운드 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "아날로그 서라운드 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "아날로그 서라운드 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "디지털 스테레오 (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "디지털 서라운드 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "디지털 서라운드 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "디지털 서라운드 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "디지털 스테레오 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "디지털 서라운드 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "아날로그 양방향 모노"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "아날로그 양방향 스테레오"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "아날로그 양방향 스테레오 (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "아날로그 양방향 스테레오"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s 출력"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s 입력"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail()이 %lu 바이트 (%lu ms)의 매우 큰 값을 반환했습니다.\n"
+"ALSA 드라이버 '%s'의 오류일 수 있습니다. ALSA 개발자에게 이 문제를 보고해주시"
+"기 바랍니다."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay()가 %li 바이트 (%s%lu ms)의 매우 큰 값을 반환했습니다.\n"
+"ALSA 드라이버 '%s'의 오류일 수 있습니다. ALSA 개발자에게 이 문제를 보고해주시"
+"기 바랍니다."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay()가 이상한 값을 반환했습니다: 지연 시간 %lu은 사용 가능"
+"한 시간 %lu 보다 작습니다.\n"
+"ALSA 드라이버 '%s'의 오류일 수 있습니다. ALSA 개발자에게 이 문제를 보고해 주"
+"시기 바랍니다."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin()이 %lu 바이트 (%lu ms)의 매우 큰 값을 반환했습니다.\n"
+"ALSA 드라이버 '%s'의 오류일 수 있습니다. ALSA 개발자에게 이 문제를 보고해 주"
+"시기 바랍니다."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+#, fuzzy
+msgid "Handsfree"
+msgstr "핸즈프리 게이트웨이"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "헤드폰"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "블루투스 출력 "
diff --git a/po/lt.po b/po/lt.po
new file mode 100644
index 0000000..875f583
--- /dev/null
+++ b/po/lt.po
@@ -0,0 +1,623 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Moo, 2017-2019
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2019-09-01 16:15+0300\n"
+"Last-Translator: Moo\n"
+"Language-Team: \n"
+"Language: lt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.2.1\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n"
+"%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Įtaisytas garsas"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modemas"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Išjungta"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(neteisinga)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Įvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Sujungimo stoties įvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Sujungimo stoties mikrofonas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Sujungimo stoties įvadinė linija"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Įvadinė linija"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Mikrofonas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Priekinis mikrofonas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Galinis mikrofonas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Išorinis mikrofonas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Vidinis mikrofonas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radijas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Vaizdas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Automatinis stiprinimo reguliavimas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Be automatinio stiprinimo reguliavimo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Pastiprinimas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Be pastiprinimo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Stiprintuvas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Be stiprintuvo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Žemų tonų pastiprinimas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Be žemų tonų pastiprinimo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Garsiakalbis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Ausinės"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analoginė įvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Doko mikrofonas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Ausinių mikrofonas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analoginė išvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Ausinės"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Ausinių mono išvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Išvadinė linija"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analoginė mono išvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Garsiakalbiai"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Skaitmeninė išvestis (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Skaitmeninė įvestis (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Daugiakanalė įvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Daugiakanalė išvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Žaidimo išvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Pokalbio išvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Pokalbio išvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Virtualus erdvinis rinktuvas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analoginė mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Analoginė mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Analoginė mono"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analoginė stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Ausinės su mikrofonu"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Garsiakalbis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Daugiakanalė"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analoginė erdvinė 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analoginė erdvinė 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analoginė erdvinė 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analoginė erdvinė 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analoginė erdvinė 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analoginė erdvinė 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analoginė erdvinė 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analoginė erdvinė 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analoginė erdvinė 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analoginė erdvinė 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analoginė erdvinė 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Skaitmeninė stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Skaitmeninė erdvinė 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Skaitmeninė erdvinė 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Skaitmeninė erdvinė 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Skaitmeninė stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Skaitmeninė erdvinė 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analoginė dvipusė mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analoginė dvipusė stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Skaitmeninė dvipusė stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Daugiakanalė dvipusė"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Dvipusė stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s išvestis"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s įvestis"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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() grąžino reikšmę, kuri yra išskirtinai didelė: %lu baitas "
+"(%lu ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+msgstr[1] ""
+"snd_pcm_avail() grąžino reikšmę, kuri yra išskirtinai didelė: %lu baitai "
+"(%lu ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+msgstr[2] ""
+"snd_pcm_avail() grąžino reikšmę, kuri yra išskirtinai didelė: %lu baitų (%lu "
+"ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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() grąžino reikšmę, kuri yra išskirtinai didelė: %li baitas (%s"
+"%lu ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+msgstr[1] ""
+"snd_pcm_delay() grąžino reikšmę, kuri yra išskirtinai didelė: %li baitai (%s"
+"%lu ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+msgstr[2] ""
+"snd_pcm_delay() grąžino reikšmę, kuri yra išskirtinai didelė: %li baitų (%s"
+"%lu ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() grąžino keistas reikšmes: delsa %lu yra mažesnė, nei "
+"prieinama %lu.\n"
+"Greičiausiai, tai yra klaida ALSA \"'%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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() grąžino reikšmę, kuri yra išskirtinai didelė: %lu "
+"baitas (%lu ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+msgstr[1] ""
+"snd_pcm_mmap_begin() grąžino reikšmę, kuri yra išskirtinai didelė: %lu "
+"baitai (%lu ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+msgstr[2] ""
+"snd_pcm_mmap_begin() grąžino reikšmę, kuri yra išskirtinai didelė: %lu baitų "
+"(%lu ms).\n"
+"Greičiausiai, tai yra klaida ALSA \"%s\" tvarkyklėje. Prašome apie šią "
+"klaidą pranešti ALSA kūrėjams."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Laisvų rankų įranga"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Ausinė"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Portatyvi sistema"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Automobilis"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefonas"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Bluetooth įvestis"
diff --git a/po/meson.build b/po/meson.build
new file mode 100644
index 0000000..eb4a7ff
--- /dev/null
+++ b/po/meson.build
@@ -0,0 +1,12 @@
+i18n = import('i18n')
+
+i18n.gettext(
+ meson.project_name(),
+ preset: 'glib',
+ # Page width is set to 90 characters in order to avoid bad wrapping of the
+ # bug reporting address.
+ args: ['--msgid-bugs-address=https://gitlab.freedesktop.org/pipewire/pipewire/issues/new',
+ '--width=90'],
+)
+
+po_dir = meson.current_source_dir()
diff --git a/po/ml.po b/po/ml.po
new file mode 100644
index 0000000..47cb946
--- /dev/null
+++ b/po/ml.po
@@ -0,0 +1,608 @@
+#
+# <>, YEAR, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.ml\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:41+0000\n"
+"Last-Translator: \n"
+"Language-Team: <en@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "ഇന്റേര്‍ണല്‍ ഓഡിയോ"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "മോഡം"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "ഓഫ്"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(അസാധു)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "ഇന്‍പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "ഡോക്കിങ് സ്റ്റേഷന്‍ ഇന്‍പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "ഡോക്കിങ് സ്റ്റേഷന്‍ മൈക്രോഫോണ്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "ഡോക്കിങ് സ്റ്റേഷന്‍ ഇന്‍പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "മൈക്രോഫോണ്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "ഡോക്കിങ് സ്റ്റേഷന്‍ മൈക്രോഫോണ്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "മൈക്രോഫോണ്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "എക്സ്റ്റേണല്‍ മൈക്രോഫോണ്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "ഇന്റേണല്‍ മൈക്രോഫോണ്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "റേഡിയോ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "വീഡിയോ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "ഓട്ടോമാറ്റിക് ഗെയിന്‍ കണ്ട്രോള്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "ഓട്ടോമാറ്റിക് ഗെയിന്‍ കണ്ട്രോള്‍ ലഭ്യമല്ല"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "ബൂസ്റ്റ്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "ബൂസ്റ്റ് ലഭ്യമല്ല"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "ആംപ്ലിഫയര്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "ആംപ്ലിഫയര്‍ ലഭ്യമല്ല"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "ബൂസ്റ്റ്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "ബൂസ്റ്റ് ലഭ്യമല്ല"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "അനലോഗ് ഹെഡ്ഫോണുകള്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "അനലോഗ് ഇന്‍പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "ഡോക്കിങ് സ്റ്റേഷന്‍ മൈക്രോഫോണ്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "മൈക്രോഫോണ്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "അനലോഗ് ഔട്ട്പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "അനലോഗ് ഹെഡ്ഫോണുകള്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "അനലോഗ് മോണോ ഔട്ട്പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "അനലോഗ് ലൈന്‍-ഇന്‍"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "അനലോഗ് മോണോ ഔട്ട്പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "അനലോഗ് സ്റ്റീരിയോ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "ഡിജിറ്റല്‍ സ്റ്റീരിയോ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "ഡിജിറ്റല്‍ സ്റ്റീരിയോ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "നള്‍ ഔട്ട്പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "നള്‍ ഔട്ട്പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "നള്‍ ഔട്ട്പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "ഇന്‍പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "അനലോഗ് സറൌണ്ട് 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "അനലോഗ് മോണോ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "അനലോഗ് മോണോ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "അനലോഗ് മോണോ"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "അനലോഗ് സ്റ്റീരിയോ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "മോണോ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "അനലോഗ് സ്റ്റീരിയോ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "അനലോഗ് സറൌണ്ട് 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "അനലോഗ് സറൌണ്ട് 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "അനലോഗ് സറൌണ്ട് 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "അനലോഗ് സറൌണ്ട് 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "അനലോഗ് സറൌണ്ട് 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "അനലോഗ് സറൌണ്ട് 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "അനലോഗ് സറൌണ്ട് 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "അനലോഗ് സറൌണ്ട് 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "അനലോഗ് സറൌണ്ട് 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "അനലോഗ് സറൌണ്ട് 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "അനലോഗ് സറൌണ്ട് 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "ഡിജിറ്റല്‍ സ്റ്റീരിയോ (IEC958) "
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ഡിജിറ്റല്‍ സറൌണ്ട് 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ഡിജിറ്റല്‍ സറൌണ്ട് 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ഡിജിറ്റല്‍ സറൌണ്ട് 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "ഡിജിറ്റല്‍ സ്റ്റീരിയോ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ഡിജിറ്റല്‍ സറൌണ്ട് 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "അനലോഗ് മോണോ ഡ്യൂപ്ലെക്സ്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "അനലോഗ് സ്റ്റീരിയോ ഡ്യൂപ്ലെക്സ്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ഡിജിറ്റല്‍ സ്റ്റീരിയോ ഡ്യൂപ്ലെക്സ് (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "അനലോഗ് സ്റ്റീരിയോ ഡ്യൂപ്ലെക്സ്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "നള്‍ ഔട്ട്പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "ഇന്‍പുട്ട്"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() നല്‍കിയ മൂല്ല്യം വളരെ വലുതാണു്: %lu ബൈറ്റുകള്‍ (%lu ms).\n"
+"ഇതു് ALSA ഡ്രൈവര്‍ '%s'-ലുള്ള ഒരു ബഗാവാം. ദയവായി ഈ പ്രശ്നം ALSA ഡവലപ്പര്‍സിനെ അറിയിക്കുക."
+msgstr[1] ""
+"snd_pcm_avail() നല്‍കിയ മൂല്ല്യം വളരെ വലുതാണു്: %lu ബൈറ്റുകള്‍ (%lu ms).\n"
+"ഇതു് ALSA ഡ്രൈവര്‍ '%s'-ലുള്ള ഒരു ബഗാവാം. ദയവായി ഈ പ്രശ്നം ALSA ഡവലപ്പര്‍സിനെ അറിയിക്കുക."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() നല്‍കിയ മൂല്ല്യം വളരെ വലുതാണു്: %li ബൈറ്റുകള്‍ (%s%lu ms).\n"
+"ഇതു് ALSA ഡ്രൈവര്‍ '%s'-ലുള്ള ഒരു ബഗാവാം. ദയവായി ഈ പ്രശ്നം ALSA ഡവലപ്പര്‍സിനെ അറിയിക്കുക."
+msgstr[1] ""
+"snd_pcm_delay() നല്‍കിയ മൂല്ല്യം വളരെ വലുതാണു്: %li ബൈറ്റുകള്‍ (%s%lu ms).\n"
+"ഇതു് ALSA ഡ്രൈവര്‍ '%s'-ലുള്ള ഒരു ബഗാവാം. ദയവായി ഈ പ്രശ്നം ALSA ഡവലപ്പര്‍സിനെ അറിയിക്കുക."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() നല്‍കിയ മൂല്ല്യം വളരെ വലുതാണു്: %lu ബൈറ്റുകള്‍ (%lu ms).\n"
+"ഇതു് ALSA ഡ്രൈവര്‍ '%s'-ലുള്ള ഒരു ബഗാവാം. ദയവായി ഈ പ്രശ്നം ALSA ഡവലപ്പര്‍സിനെ അറിയിക്കുക."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() നല്‍കിയ മൂല്ല്യം വളരെ വലുതാണു്: %lu ബൈറ്റുകള്‍(%lu ms).\n"
+"ഇതു് ALSA ഡ്രൈവര്‍ '%s'-ലുള്ള ഒരു ബഗാവാം. ദയവായി ഈ പ്രശ്നം ALSA ഡവലപ്പര്‍സിനെ അറിയിക്കുക."
+msgstr[1] ""
+"snd_pcm_mmap_begin() നല്‍കിയ മൂല്ല്യം വളരെ വലുതാണു്: %lu ബൈറ്റുകള്‍(%lu ms).\n"
+"ഇതു് ALSA ഡ്രൈവര്‍ '%s'-ലുള്ള ഒരു ബഗാവാം. ദയവായി ഈ പ്രശ്നം ALSA ഡവലപ്പര്‍സിനെ അറിയിക്കുക."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "അനലോഗ് ഹെഡ്ഫോണുകള്‍"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/mr.po b/po/mr.po
new file mode 100644
index 0000000..4bdddc8
--- /dev/null
+++ b/po/mr.po
@@ -0,0 +1,618 @@
+# translation of pipewire.master-tx.po to Marathi
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Sandeep Shedmake <sshedmak@redhat.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:54+0000\n"
+"Last-Translator: Sandeep Shedmake <sshedmak@redhat.com>\n"
+"Language-Team: Marathi <fedora-trans-mr@redhat.com>\n"
+"Language: mr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "आंतरीक ऑडिओ"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "मोडेम"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "बंद करा"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(अवैध)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "इंपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "डॉकिंग स्टेशन इंपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "डॉकिंग स्टेशन माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "डॉकिंग स्टेशन इंपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "डॉकिंग स्टेशन माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "बाहेरील माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "आंतरीक माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "रेडिओ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "विडिओ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "स्वयं गैन कंट्रोल"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "स्वयं गैन कंट्रोल अशक्य"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "बूस्ट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "बूस्ट अशक्य"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "ऍमप्लिफायर"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "ऍमप्लिफायर अशक्य"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "बूस्ट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "बूस्ट अशक्य"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "ऍनलॉग हेडफोन्स्"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "ऍनलॉग इंपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "डॉकिंग स्टेशन माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "माइक्रोफोन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "ऍनलॉग आऊटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "ऍनलॉग हेडफोन्स्"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "ऍनलॉग मोनो आऊटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "लाइन-इन"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "ऍनलॉग मोनो आऊटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "ऍनलॉग स्टिरीओ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "डिजीटल स्टिरीओ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "डिजीटल स्टिरीओ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Null आऊटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Null आऊटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Null आऊटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "इंपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "ऍनलॉग सर्राउंड 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "ऍनलॉग मोनो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "ऍनलॉग मोनो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "ऍनलॉग मोनो"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "ऍनलॉग स्टिरीओ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "मोनो"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "ऍनलॉग स्टिरीओ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "ऍनलॉग सर्राउंड 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "ऍनलॉग सर्राउंड 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "ऍनलॉग सर्राउंड 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "ऍनलॉग सर्राउंड 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "ऍनलॉग सर्राउंड 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "ऍनलॉग सर्राउंड 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "ऍनलॉग सर्राउंड 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "ऍनलॉग सर्राउंड 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "ऍनलॉग सर्राउंड 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "ऍनलॉग सर्राउंड 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "ऍनलॉग सर्राउंड 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "डिजीटल स्टिरीओ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "डिजीटल सर्राउंड 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "डिजीटल सर्राउंड 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "डिजीटल सर्राउंड 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "डिजीटल स्टिरीओ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "डिजीटल सर्राउंड 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "ऍनलॉग मोनो ड्युप्लेक्स्"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "ऍनलॉग स्टिरीओ ड्युप्लेक्स्"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "डिजीटल स्टिरीओ ड्युप्लेक्स् (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "ऍनलॉग स्टिरीओ ड्युप्लेक्स्"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "Null आऊटपुट"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "इंपुट"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() ने अपेक्षा पेक्षा मोठे मूल्य पूरवले: %lu बाईटस् (%lu ms).\n"
+"हे सहसा ALSA ड्राइवर '%s' अंतर्गत बग अशू शकते. कृपया या अडचणीस ALSA डेव्हलपर करीता "
+"कळवा."
+msgstr[1] ""
+"snd_pcm_avail() ने अपेक्षा पेक्षा मोठे मूल्य पूरवले: %lu बाईटस् (%lu ms).\n"
+"हे सहसा ALSA ड्राइवर '%s' अंतर्गत बग अशू शकते. कृपया या अडचणीस ALSA डेव्हलपर करीता "
+"कळवा."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() ने अपेक्षा पेक्षा मोठे मूल्य पूरवले: %li बाईटस् (%s% lu ms).\n"
+"हे सहसा ALSA ड्राइवर '%s' अंतर्गत बग अशू शकते. कृपया या अडचणीस ALSA डेव्हलपर करीता "
+"कळवा."
+msgstr[1] ""
+"snd_pcm_delay() ने अपेक्षा पेक्षा मोठे मूल्य पूरवले: %li बाईटस् (%s% lu ms).\n"
+"हे सहसा ALSA ड्राइवर '%s' अंतर्गत बग अशू शकते. कृपया या अडचणीस ALSA डेव्हलपर करीता "
+"कळवा."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() ने अपेक्षा पेक्षा मोठे मूल्य पूरवले: %lu बाईटस् (%lu ms).\n"
+"हे सहसा ALSA ड्राइवर '%s' अंतर्गत बग अशू शकते. कृपया या अडचणीस ALSA डेव्हलपर करीता "
+"कळवा."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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_nmap_begin() ने अपेक्षा पेक्षा मोठे मूल्य पूरवले: %lu बाईटस् (%lu ms).\n"
+"हे सहसा ALSA ड्राइवर '%s' अंतर्गत बग अशू शकते. कृपया या अडचणीस ALSA डेव्हलपर करीता "
+"कळवा."
+msgstr[1] ""
+"snd_pcm_nmap_begin() ने अपेक्षा पेक्षा मोठे मूल्य पूरवले: %lu बाईटस् (%lu ms).\n"
+"हे सहसा ALSA ड्राइवर '%s' अंतर्गत बग अशू शकते. कृपया या अडचणीस ALSA डेव्हलपर करीता "
+"कळवा."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "ऍनलॉग हेडफोन्स्"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/my.po b/po/my.po
new file mode 100644
index 0000000..68a9be7
--- /dev/null
+++ b/po/my.po
@@ -0,0 +1,595 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire-master\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2021-08-26 03:31+0000\n"
+"PO-Revision-Date: 2021-08-26 21:52+0630\n"
+"Language-Team: lw1nzayar@yandex.com\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Poedit 2.4.2\n"
+"Last-Translator: zayar lwin <lw1nzayar@yandex.com>\n"
+"Language: my\n"
+
+#: src/daemon/pipewire.c:45
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "ပိုက်ဝိုင်ယာ မီဒီယာစစ်စတမ်"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "ပိုက်ဝိုင်ယာ မီဒီယာစစ်စတမ် စတင်ရန်"
+
+#: src/examples/media-session/alsa-monitor.c:588
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "နဂိုတည်းကထည့်သွင်းထားသည်ံ့ အသံကရိယာ"
+
+#: src/examples/media-session/alsa-monitor.c:592
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "ဆ.သ.ရ-စက်"
+
+#: src/examples/media-session/alsa-monitor.c:601
+#: src/modules/module-zeroconf-discover.c:296
+msgid "Unknown device"
+msgstr "မသိသောစက်"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:173
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:173
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "%s/%sသို့ လုံခြုံစွာဒေတာပို့ဆောင်"
+
+#: src/modules/module-pulse-tunnel.c:534
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "%s@%sအတွက် လုံခြုံစွာဒေတာပို့ဆောင်"
+
+#: src/modules/module-zeroconf-discover.c:308
+#, c-format
+msgid "%s on %s@%s"
+msgstr ""
+
+#: src/modules/module-zeroconf-discover.c:312
+#, c-format
+msgid "%s on %s"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, 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 ""
+
+#: src/tools/pw-cat.c:1023
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1041
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1058
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2954
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:306
+msgid "Pro Audio"
+msgstr "ပရို အော်ဒီယို"
+
+#: spa/plugins/alsa/acp/acp.c:429 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1128
+msgid "Off"
+msgstr "ပိတ်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "အသံသွင်းမှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "ချိတ်ဆက်စခန်း အသံသွင်းမှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "ချိတ်ဆက်စခန်း မိုက်ခရိုဖုန်း"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "ချိတ်ဆက်စခန်း အသံလက်ခံကြိုး"
+
+#: 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:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "Microphone"
+msgstr "မိုက်ခရိုဖုန်း"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "မျက်နှာချင်းဆိုင်မိုက်ခရိုဖုန်း"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "နောက်ဘက်မိုက်ခရိုဖုန်း"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "အပြင်မိုက်ခရိုဖုန်း"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "အတွင်းမိုက်ခရိုဖုန်း"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "ရေဒီယို"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "ဗီဒီယို"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "အလိုအလျောက်gainထိန်းချုပ်မည်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "အလိုအလျောက်gainမထိန်းချုပ်ပါ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "အားတိုးသည်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "အားမတိုးပါ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "ချဲ့စက်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "ချဲ့စက်မရှိပါ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Bassတင်ထားသည်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Bassမတင်ထားပါ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1288
+msgid "Speaker"
+msgstr "စပီကာ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "နားကပ်စပီကာများ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "အန်နလော့ အသံသွင်းမှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "မိုက်ခရိုဖုန်းချိတ်ရန်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "မိုက်ပါနားကြပ်၏မိုက်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "အန်နလော့ အသံထွက်မှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "နားကပ်စပီကာများ ၂"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "နားကပ်စပီကာများ မိုနိုအသံထွက်မှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "အသံပို့ဆောင်ကြိုး"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "အန်နလော့ မိုနို အသံထွက်မှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "စပီကာများ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2757
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2758
+msgid "Digital Output (S/PDIF)"
+msgstr "ဒီဂျစ်တယ် အသံထွက်မှု (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "ဒီဂျစ်တယ် အသံသွင်းမှု (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "လိုင်းစုံ အသံသွင်းမှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "လိုင်းစုံ အသံထုတ်မှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "ဂိမ်းအသံထွက်မှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "စကားပြောဆို-အသံထွက်မှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "စကားပြောဆို-အသံထွက်မှု"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "ပတ်ပတ်လည်အယောင်သံ ၇.၁"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "အန်နလော့ မိုနို"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "အန်နလော့ မိုနို (ဘယ်)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "အန်နလော့ မိုနို (ညာ)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "အန်နလော့ စတယ်ရီယို"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "မိုနို"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr "စတယ်ရီယို"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1273
+msgid "Headset"
+msgstr "မိုက်ပါနားကြပ်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "စပီကာဖုန်း"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "လိုင်းစုံ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၂.၁"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၃.၀"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၃.၁"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၄.၀"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၄.၁"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၅.၀"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၅.၁"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၆.၀"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၆.၁"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၇.၀"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "အန်နလော့ ပတ်ပတ်လည် ၇.၁"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "ဒီဂျစ်တယ်စတယ်ရီယို (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ဒီဂျစ်တယ် ပတ်ပတ်လည် ၄.၀ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ဒီဂျစ်တယ် ပတ်ပတ်လည် ၅.၁ (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ဒီဂျစ်တယ် ပတ်ပတ်လည် ၅.၁ (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "ဒီဂျစ်တယ်စတယ်ရီယို (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ဒီဂျစ်တယ် ပတ်ပတ်လည် ၅.၁ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "စကားပြောဆို"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "ဂိမ်း"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "အန်နလော့ မိုနို ဒူပလက်စ်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "အန်နလော့ စတယ်ရီယို ဒူပလက်စ်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ဒီဂျစ်တယ်စတယ်ရီယို ဒူပလက်စ်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "လိုင်းစုံ ဒူပလက်စ်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "စတယ်ရီယို ဒူပလက်စ်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "မိုနို စကားပြောဆိုသံ + ၇.၁ ပတ်ပတ်လည်"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4750
+#, c-format
+msgid "%s Output"
+msgstr "%s ထုတ်ပို့ကရိယာ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4757
+#, c-format
+msgid "%s Input"
+msgstr "%s လက်ခံကရိယာ"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1239
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1286
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1329
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(မမှန်ကန်)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1138
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "အသံ ဂိတ်ဝေး (A2DP Source & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1161
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "High Fidelity Playback (A2DP Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1163
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1169
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "High Fidelity Playback (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1171
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "High Fidelity Duplex (A2DP Source/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1198
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "မိုက်ပါနားကြပ်ခေါင်းယူနစ် (HSP/HFP၊ codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1202
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "မိုက်ပါနားကြပ်ခေါင်းယူနစ် (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1278
+msgid "Handsfree"
+msgstr "လက်မလို"
+
+#: spa/plugins/bluez5/bluez5-device.c:1293
+msgid "Headphone"
+msgstr "နားကပ်စပီကာ"
+
+#: spa/plugins/bluez5/bluez5-device.c:1298
+msgid "Portable"
+msgstr "သယ်ရလွယ်ကူ"
+
+#: spa/plugins/bluez5/bluez5-device.c:1303
+msgid "Car"
+msgstr "ကား"
+
+#: spa/plugins/bluez5/bluez5-device.c:1308
+msgid "HiFi"
+msgstr "ဟိုင်ဖိုင်"
+
+#: spa/plugins/bluez5/bluez5-device.c:1313
+msgid "Phone"
+msgstr "ဖုန်း"
+
+#: spa/plugins/bluez5/bluez5-device.c:1319
+msgid "Bluetooth"
+msgstr "ဘလူးတု"
diff --git a/po/nl.po b/po/nl.po
new file mode 100644
index 0000000..733b7ee
--- /dev/null
+++ b/po/nl.po
@@ -0,0 +1,605 @@
+# Dutch translation of pipewire.master-tx.
+# Copyright (C) 2009 THE pipewire.master-tx'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the pipewire.master-tx package.
+# Geert Warrink <geert.warrink@onsnet.nu>, 2009.
+# Reinout van Schouwen <reinout@gmail.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2020-07-11 20:27+0000\n"
+"Last-Translator: Geert Warrink <geert.warrink@onsnet.nu>\n"
+"Language-Team: Dutch <https://translate.fedoraproject.org/projects/pipewire/"
+"pipewire/nl/>\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.1.1\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Intern geluid"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr "Onbekend apparaat"
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Uit"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ongeldig)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Invoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Docking station-invoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Docking station-microfoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Docking station-Lijn-in"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Lijn-in"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Microfoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Microfoon vooraan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Microfoon achteraan"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Externe microfoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Interne microfoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Automatische gain-controle"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Geen automatische gain-controle"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Boostversterking"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Geen boostversterking"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Versterker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Geen versterker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Boostversterking"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Geen boostversterking"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Luidspreker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Analoge koptelefoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analoge invoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Docking station-microfoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "Microfoon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analoge output"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Analoge koptelefoon 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Analoge mono-uitvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Lijn-in"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analoge mono-uitvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Luidsprekers"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Digitaal stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitale invoer (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Multikanaal invoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Multikanaal uitvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Spel uitvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Null-uitvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Invoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Virtueel surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analoog mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Analoog mono (Links)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Analoog mono (Rechts)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analoog stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "Luidspreker"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multikanaal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analoog surround 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analoog surround 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analoog surround 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analoog surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analoog surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analoog surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analoog surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analoog surround 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analoog surround 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analoog surround 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analoog surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitaal stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitaal surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitaal surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitaal surround 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitaal stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitaal surround 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Spel"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analoog mono duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analoog stereo duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digitaal stereo duplex (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Multikanaal-duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Stereo duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s uitvoer"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s invoer"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gaf een waarde terug die uitzonderlijk groot is: %lu bytes "
+"(%lu ms).\n"
+"Waarschijnlijk is dit een fout in het ALSA-stuurprogramma ‘%s’. Meld dit "
+"probleem alstublieft aan de ALSA-ontwikkelaars."
+msgstr[1] ""
+"snd_pcm_avail() gaf een waarde terug die uitzonderlijk groot is: %lu bytes "
+"(%lu ms).\n"
+"Waarschijnlijk is dit een fout in het ALSA-stuurprogramma ‘%s’. Meld dit "
+"probleem alstublieft aan de ALSA-ontwikkelaars."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gaf een waarde terug die uitzonderlijk groot is: %li bytes "
+"(%s%lu ms).\n"
+"Waarschijnlijk is dit een fout in het ALSA-stuurprogramma ‘%s’. Meld dit "
+"probleem alstublieft aan de ALSA-ontwikkelaars."
+msgstr[1] ""
+"snd_pcm_delay() gaf een waarde terug die uitzonderlijk groot is: %li bytes "
+"(%s%lu ms).\n"
+"Waarschijnlijk is dit een fout in het ALSA-stuurprogramma ‘%s’. Meld dit "
+"probleem alstublieft aan de ALSA-ontwikkelaars."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() gaf vreemde waardes terug: vertraging %lu is minder "
+"dan %lu.\n"
+"Waarschijnlijk is dit een fout in het ALSA-stuurprogramma '%s'. Meld dit "
+"probleem aan de ALSA-ontwikkelaars."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() gaf een waarde terug die uitzonderlijk groot is: %lu "
+"bytes (%lu ms).\n"
+"Waarschijnlijk is dit een fout in het ALSA-stuurprogramma ‘%s’. Meld dit "
+"probleem alstublieft aan de ALSA-ontwikkelaars."
+msgstr[1] ""
+"snd_pcm_mmap_begin() gaf een waarde terug die uitzonderlijk groot is: %lu "
+"bytes (%lu ms).\n"
+"Waarschijnlijk is dit een fout in het ALSA-stuurprogramma ‘%s’. Meld dit "
+"probleem alstublieft aan de ALSA-ontwikkelaars."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Handenvrij"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Koptelefoon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Draagbaar"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Auto"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefoon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/nn.po b/po/nn.po
new file mode 100644
index 0000000..f34c069
--- /dev/null
+++ b/po/nn.po
@@ -0,0 +1,638 @@
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Karl Ove Hufthammer <karl@huftis.org>, 2017.
+# Nicolai Syvertsen <saivert@saivert.com> 2021.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-20 16:34+0200\n"
+"PO-Revision-Date: 2021-02-07 15:40+0000\n"
+"Last-Translator: Karl Ove Hufthammer <karl@huftis.org>\n"
+"Language-Team: Norwegian Nynorsk <https://translate.fedoraproject.org/"
+"projects/pipewire/pipewire/nn/>\n"
+"Language: nn\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: Weblate 4.4.2\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "PipeWire mediasystem"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Start opp PipeWire mediasystem"
+
+#: src/examples/media-session/alsa-monitor.c:585
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Innebygd lyd"
+
+#: 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
+msgid "Unknown device"
+msgstr "Ukjend einhet"
+
+#: src/tools/pw-cat.c:991
+#, 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 [val] <fil>\n"
+" -h, --help Vis denne hjelpen\n"
+" --version Vis versjon\n"
+" -v, --verbose Slå på utdypene handling\n"
+"\n"
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+" -R, --remote Navn på fjerntjenar\n"
+" --media-type Set medietype (standard %s)\n"
+" --media-category Set mediekategori (standard %s)\n"
+" --media-role Set medierolle (standard %s)\n"
+" --target Set nodemål (standard %s)\n"
+" 0 betyr ikke tilknytt\n"
+" --latency Set nodelatens (standard %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" eller direkte i datapunkter (256)\n"
+" hastigheiten kjem frå kildefilen "
+"file\n"
+" --list-targets Vis tilgjengeleg mål for --target\n"
+"\n"
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Målehastigheit (krevjast for opptak) (default "
+"%u)\n"
+" --channels Antall kanalar (krevjast for opptak) "
+"(default %u)\n"
+" --channel-map Kanaloppsett\n"
+" ein av: \"stereo\", "
+"\"surround-51\",... or\n"
+" kommaskild liste av kanalnavn "
+": t.d. \"FL,FR\"\n"
+" --format Format for målingar %s (krevjast for opptak) "
+"(default %s)\n"
+" --volume Lydnivå for straum 0-1.0 (standard %.3f)\n"
+" -q --quality Kvaltet for gjenutvalg (0 - 15) (standard "
+"%d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+" -p, --playback Avspillingsmodus\n"
+" -r, --record Opptaksmodus\n"
+" -m, --midi Midi-modus\n"
+"\n"
+
+#: src/tools/pw-cli.c:2941
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [val] [kommando]\n"
+" -h, --help Vis denne hjelp\n"
+" --version Vis versjon\n"
+" -d, --daemon Start som tjenar (standard er false)\n"
+" -r, --remote Navn på fjerntjenar\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:290
+msgid "Pro Audio"
+msgstr "Profflyd"
+
+#: spa/plugins/alsa/acp/acp.c:411 spa/plugins/alsa/acp/alsa-mixer.c:4704
+#: spa/plugins/bluez5/bluez5-device.c:1000
+msgid "Off"
+msgstr "Av"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ugyldig)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Lyd inn"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Lyd inn frå dokkingstasjon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Mikrofon på dokkingstasjon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Linje inn på dokkingstasjon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Linje inn"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Frontmikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Bakmikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Ekstern mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Intern mikrofonen"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Automatisk lydnivåstyring (AGC)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Inga automatisk lydnivåstyring (AGC)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Lydforsterking"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Inga lydforsterking"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Forsterkar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Ingen forsterkar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Bassforsterking"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Inga bassforsterking"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Høgtalar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Hovudtelefonar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analog innlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Mikrofon på dokkingstasjon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Mikrofon på hovudsett"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analog utlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Hovudtelefonar 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Hovudtelefonar monolyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Linje ut"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analog mono-utlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Høgtalarar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI/DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Digital utlyd (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Digital innlyd (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Multikanals innlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Multikanals utlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Spellyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Nettprat utlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "Nettprat innlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Virtuell kringlyd 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analog mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Analog mono (venstre)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Analog mono (høgre)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analog stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Hovudsett"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "Høgtalartelefon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Multikanals"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analog kringlyd 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analog kringlyd 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analog kringlyd 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analog kringlyd 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analog kringlyd 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analog kringlyd 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analog kringlyd 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analog kringlyd 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analog kringlyd 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analog kringlyd 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analog kringlyd 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digital stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digital kringlyd 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digital kringlyd 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digital kringlyd 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digital stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digital kringlyd 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "Nettprat"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Spel"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analog mono dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analog stereo dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digital stereo duplex (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Multikanals dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Stereo dupleks"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono-nettprat + 7.1-kringlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s utlyd"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s innlyd"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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() gav ein verdi som er uvanleg stor: %lu byte (%lu ms).\n"
+"Dette kjem truleg av ein feil i ALSA-drivaren «%s». Meld frå om problemet "
+"til ALSA-utviklarane."
+msgstr[1] ""
+"snd_pcm_avail() gav ein verdi som er uvanleg stor: %lu byte (%lu ms).\n"
+"Dette kjem truleg av ein feil i ALSA-drivaren «%s». Meld frå om problemet "
+"til ALSA-utviklarane."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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() gav ein verdi som er uvanleg stor: %li byte (%s%lu ms).\n"
+"Dette kjem truleg av ein feil i ALSA-drivaren «%s». Meld frå om problemet "
+"til ALSA-utviklarane."
+msgstr[1] ""
+"snd_pcm_delay() gav ein verdi som er uvanleg stor: %li byte (%s%lu ms).\n"
+"Dette kjem truleg av ein feil i ALSA-drivaren «%s». Meld frå om problemet "
+"til ALSA-utviklarane."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() gav ein merkeleg verdi: delay %lu er mindre enn "
+"avail %lu.\n"
+"Dette kjem truleg av ein feil i ALSA-drivaren «%s». Meld frå om problemet "
+"til ALSA-utviklarane."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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() gav ein verdi som er uvanleg stor: %lu byte (%lu ms).\n"
+"Dette kjem truleg av ein feil i ALSA-drivaren «%s». Meld frå om problemet "
+"til ALSA-utviklarane."
+msgstr[1] ""
+"snd_pcm_mmap_begin() gav ein verdi som er uvanleg stor: %lu byte (%lu ms).\n"
+"Dette kjem truleg av ein feil i ALSA-drivaren «%s». Meld frå om problemet "
+"til ALSA-utviklarane."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Inngangsport for lyd (A2DP-kilde & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Avspilling med naturtru lydattgjeving (A2DP Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Toveis lyd med naturtru attgjeving (A2DP Source/Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Avspilling med naturtru lydattgjeving (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Toveis lyd med naturtru attgjeving (A2DP Source/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Hodesett (HSP/HFP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Hodesett (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Handfri"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Hovudtelefonar"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Portabel"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Bil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "Hi-fi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/oc.po b/po/oc.po
new file mode 100644
index 0000000..ed7f950
--- /dev/null
+++ b/po/oc.po
@@ -0,0 +1,617 @@
+# French translation of pipewire.
+# Copyright (C) 2006-2008 Lennart Poettering
+# This file is distributed under the same license as the pipewire package.
+# Robert-André Mauchin <zebob.m@pengzone.org>, 2008.
+# Michaël Ughetto <telimektar esraonline com>, 2008.
+# Pablo Martin-Gomez <pablo.martin-gomez@laposte.net>, 2008.
+# Corentin Perard <corentin.perard@gmail.com>, 2009.
+# Thomas Canniot <mrtom@fedoraproject.org>, 2009, 2012.
+# Cédric Valmary (Tot en Òc) <cvalmary@yahoo.fr>, 2015.
+# Cédric Valmary (totenoc.eu) <cvalmary@yahoo.fr>, 2016.
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire trunk\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2016-10-12 22:20+0200\n"
+"Last-Translator: Cédric Valmary (totenoc.eu) <cvalmary@yahoo.fr>\n"
+"Language-Team: Tot En Òc\n"
+"Language: oc\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Generator: Virtaal 0.7.1\n"
+"X-Launchpad-Export-Date: 2016-10-12 20:12+0000\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Àudio integrat"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modèm"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Atudat"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(invalid)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Entrada de l'estacion d'acuèlh"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Microfòn de l'estacion d'acuèlh"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Entrada linha de l'estacion d'acuèlh"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Entrada linha"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Micrò"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Microfòn avant"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Microfòn arrièr"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Microfòn extèrne"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Microfòn intèrne"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Ràdio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Vidèo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Contraròtle automatic del ganh"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Pas de contraròtle automatic del ganh"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Sens boost"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Pas d'amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Amplificacion bassas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Pas d'amplificacion de las bassas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Nautparlaire"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Escotadors"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Entrada analogica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Microfòn de l'estacion d'acuèlh"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Micro-casc"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Sortida analogica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Escotadors"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Sortida Analogica Monò"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Sortida linha"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Sortida Analogica Monò"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Nauts parlaires"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Sortida numerica (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Entrada numerica (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "%s Sortida"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "%s Sortida"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "%s Entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Collector ambiofonic virtual"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Monò analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Monò analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Monò analogic"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Esterèo analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+msgid "Stereo"
+msgstr "Esterèo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4540
+#: spa/plugins/alsa/acp/alsa-mixer.c:4698
+#: spa/plugins/bluez5/bluez5-device.c:1135
+msgid "Headset"
+msgstr "Casc àudio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Nautparlaire"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Surround analogic 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Surround analogic 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Surround analogic 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Surround analogic 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Surround analogic 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Surround analogic 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Surround analogic 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Surround analogic 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Surround analogic 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Surround analogic 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Surround analogic 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Esterèo numeric (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Surround numeric 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Surround numeric 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digital Surround 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Esterèo numeric (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digital Surround 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Duplèx Mono analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Duplèx esterèo analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Duplèx estèreo numeric (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Duplèx esterèo analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s Sortida"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s Entrada"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() a tornat una valor qu'es excepcionalament larga : %lu octets "
+"(%lu ms).\n"
+"S'agís fòrt probablament d'un bug dins lo pilòt ALSA « %s ». Raportatz "
+"aqueste problèma als desvolopaires d'ALSA."
+msgstr[1] ""
+"snd_pcm_avail() a tornat una valor qu'es excepcionalament larga : %lu octets "
+"(%lu ms).\n"
+"S'agís fòrt probablament d'un bug dins lo pilòt ALSA « %s ». Raportatz "
+"aqueste problèma als desvolopaires d'ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() a tornat una valor qu'es excepcionalament larga : %li octets "
+"%s%lu ms).\n"
+"S'agís fòrt probablament d'un bug dins lo pilòt ALSA « %s ». Raportatz "
+"aqueste problèma als desvolopaires d'ALSA."
+msgstr[1] ""
+"snd_pcm_delay() a tornat una valor qu'es excepcionalament larga : %li octets "
+"%s%lu ms).\n"
+"S'agís fòrt probablament d'un bug dins lo pilòt ALSA « %s ». Raportatz "
+"aqueste problèma als desvolopaires d'ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() a tornat de resultats anormals : lo relambi %lu es mai "
+"pichon que %lu.\n"
+"Es fòrt probablament un bug dins lo pilòt ALSA '%s'. Senhalatz-lo als "
+"desvolopaires d'ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() a tornat una valor qu'es excepcionalament larga : %lu "
+"octets (%lu·ms)..\n"
+"S'agís fòrt probablament d'un bug dins lo pilòt ALSA « %s ». Raportatz "
+"aqueste problèma als desvolopaires d'ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() a tornat una valor qu'es excepcionalament larga : %lu "
+"octets (%lu·ms)..\n"
+"S'agís fòrt probablament d'un bug dins lo pilòt ALSA « %s ». Raportatz "
+"aqueste problèma als desvolopaires d'ALSA."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Mans liuras"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Escotadors"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Portable"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Telefòn de veitura"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefòn"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Entrada Bluetooth"
diff --git a/po/or.po b/po/or.po
new file mode 100644
index 0000000..104f2e7
--- /dev/null
+++ b/po/or.po
@@ -0,0 +1,641 @@
+# translation of pipewire.master-tx.or.po to Oriya
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Manoj Kumar Giri <mgiri@redhat.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.or\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:55+0000\n"
+"Last-Translator: Manoj Kumar Giri <mgiri@redhat.com>\n"
+"Language-Team: Oriya <oriya-it@googlegroups.com>\n"
+"Language: or\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "ଆଭ୍ୟନ୍ତରୀଣ ଧ୍ୱନି"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "ମଡେମ"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "ଅଫ"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ଅବୈଧ)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "ନିବେଶ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "ଡକିଙ୍ଗ ଷ୍ଟେସନ ନିବେଶ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "ଡକିଙ୍ଗ ଷ୍ଟେସନ ମାଇକ୍ରୋଫୋନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "ଡକିଙ୍ଗ ଷ୍ଟେସନ ନିବେଶ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "ମାଇକ୍ରୋଫୋନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "ଡକିଙ୍ଗ ଷ୍ଟେସନ ମାଇକ୍ରୋଫୋନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "ମାଇକ୍ରୋଫୋନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "ବାହ୍ୟ ମାଇକ୍ରୋଫୋନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "ଆଭ୍ୟନ୍ତରୀଣ ମାଇକ୍ରୋଫୋନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "ରେଡିଓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "ଭିଡ଼ିଓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "ସ୍ୱୟଂଚାଳିତ ଲାଭ ନିୟନ୍ତ୍ରଣ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "କୌଣସି ସ୍ୱୟଂଚାଳିତ ଲାଭ ନିୟନ୍ତ୍ରଣ ନାହିଁ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "ବୃଦ୍ଧି"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "ବୃଦ୍ଧି ନାହିଁ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "ଏମ୍ପ୍ଲିଫାୟର"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "ଏମ୍ପ୍ଲିଫାୟର ନାହିଁ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "ବୃଦ୍ଧି"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "ବୃଦ୍ଧି ନାହିଁ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "ଏନାଲୋଗ ହେଡ଼ଫୋନଗୁଡ଼ିକ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "ଏନାଲୋଗ ନିବେଶ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "ଡକିଙ୍ଗ ଷ୍ଟେସନ ମାଇକ୍ରୋଫୋନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "ମାଇକ୍ରୋଫୋନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "ଏନାଲୋଗ ଫଳାଫଳ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "ଏନାଲୋଗ ହେଡ଼ଫୋନଗୁଡ଼ିକ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "ଏନାଲୋଗ ମୋନୋ ଫଳାଫଳ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "ଲାଇନ-ଇନ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "ଏନାଲୋଗ ମୋନୋ ଫଳାଫଳ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "ଏନାଲୋଗ ଷ୍ଟେରିଓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "ଡିଜିଟାଲ ଷ୍ଟେରିଓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "ଡିଜିଟାଲ ଷ୍ଟେରିଓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "ଶୂନ୍ୟ ଫଳାଫଳ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "ଶୂନ୍ୟ ଫଳାଫଳ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "ଶୂନ୍ୟ ଫଳାଫଳ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "ନିବେଶ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "ଏନାଲୋଗ ମୋନୋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "ଏନାଲୋଗ ମୋନୋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "ଏନାଲୋଗ ମୋନୋ"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "ଏନାଲୋଗ ଷ୍ଟେରିଓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "ମୋନୋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "ଏନାଲୋଗ ଷ୍ଟେରିଓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "ଏନାଲୋଗ ଚତୁଃ ପାର୍ଶ୍ୱ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "ଡିଜିଟାଲ ଷ୍ଟେରିଓ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ଡିଜିଟାଲ ଚତୁଃ ପାର୍ଶ୍ୱ 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ଡିଜିଟାଲ ଚତୁଃ ପାର୍ଶ୍ୱ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ଡିଜିଟାଲ ଚତୁଃ ପାର୍ଶ୍ୱ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "ଡିଜିଟାଲ ଷ୍ଟେରିଓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ଡିଜିଟାଲ ଚତୁଃ ପାର୍ଶ୍ୱ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "ଏନାଲୋଗ ମୋନୋ ଡ଼ୁପ୍ଲେକ୍ସ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "ଏନାଲୋଗ ଷ୍ଟେରିଓ ଡ଼ୁପ୍ଲେକ୍ସ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ଡିଜିଟାଲ ଷ୍ଟେରିଓ ଡ଼ୁପ୍ଲେକ୍ସ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "ଏନାଲୋଗ ଷ୍ଟେରିଓ ଡ଼ୁପ୍ଲେକ୍ସ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "ଶୂନ୍ୟ ଫଳାଫଳ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "ନିବେଶ"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() 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[1] ""
+"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."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() 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[1] ""
+"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."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() 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."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() 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[1] ""
+"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."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "ଏନାଲୋଗ ହେଡ଼ଫୋନଗୁଡ଼ିକ"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/pa.po b/po/pa.po
new file mode 100644
index 0000000..176377c
--- /dev/null
+++ b/po/pa.po
@@ -0,0 +1,614 @@
+# translation of pipewire.master-tx.pa.po to Punjabi
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Amanpreet Singh Alam <aalam@users.sf.net>, 2008.
+# A S Alam <aalam@users.sf.net>, 2009.
+# Jaswinder Singh <jsingh@redhat.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.pa\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:55+0000\n"
+"Last-Translator: Jaswinder Singh <jsingh@redhat.com>\n"
+"Language-Team: Punjabi/Panjabi <kde-i18n-doc@kde.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Lokalize 1.0\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "ਅੰਦਰੂਨੀ ਆਡੀਓ"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "ਮਾਡਮ"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "ਬੰਦ"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(ਅਢੁੱਕਵਾਂ)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "ਇੰਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "ਡੌਕਿੰਗ ਸਟੇਸ਼ਨ ਇੰਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "ਡੌਕਿੰਗ ਸਟੇਸ਼ਨ ਮਾਈਕਰੋਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "ਡੌਕਿੰਗ ਸਟੇਸ਼ਨ ਇੰਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "ਮਾਈਕਰੋਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "ਡੌਕਿੰਗ ਸਟੇਸ਼ਨ ਮਾਈਕਰੋਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "ਮਾਈਕਰੋਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "ਬਾਹਰੀ ਮਾਈਕਰੋਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "ਅੰਦਰੂਨੀ ਮਾਈਕਰੋਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "ਰੇਡੀਓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "ਵੀਡੀਓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "ਆਟੋਮੈਟਿਕ ਗੇਨ ਕੰਟਰੋਲ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "ਕੋਈ ਆਟੋਮੈਟਿਕ ਗੇਨ ਕੰਟਰੋਲ ਨਹੀਂ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "ਬੂਸਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "ਕੋਈ ਬੂਸਟ ਨਹੀਂ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "ਐਂਪਲੀਫਾਇਰ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "ਕੋਈ ਐਂਪਲੀਫਾਇਰ ਨਹੀਂ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "ਬੂਸਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "ਕੋਈ ਬੂਸਟ ਨਹੀਂ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "ਐਨਾਲਾਗ ਹੈੱਡਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "ਐਨਾਲਾਗ ਇੰਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "ਡੌਕਿੰਗ ਸਟੇਸ਼ਨ ਮਾਈਕਰੋਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "ਮਾਈਕਰੋਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "ਐਨਾਲਾਗ ਆਉਟਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "ਐਨਾਲਾਗ ਹੈੱਡਫੋਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "ਐਨਾਲਾਗ ਮੋਨੋ ਆਊਟਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "ਲਾਈਨ-ਇਨ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "ਐਨਾਲਾਗ ਮੋਨੋ ਆਊਟਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "ਐਨਾਲਾਗ ਸਟੀਰੀਓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਟੀਰੀਓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਟੀਰੀਓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "ਜ਼ੀਰੋ (Null) ਆਉਟਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "ਜ਼ੀਰੋ (Null) ਆਉਟਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "ਜ਼ੀਰੋ (Null) ਆਉਟਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "ਇੰਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "ਐਨਾਲਾਗ ਮੋਨੋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "ਐਨਾਲਾਗ ਮੋਨੋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "ਐਨਾਲਾਗ ਮੋਨੋ"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "ਐਨਾਲਾਗ ਸਟੀਰੀਓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "ਮੋਨੋ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "ਐਨਾਲਾਗ ਸਟੀਰੀਓ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "ਐਨਾਲਾਗ ਸਰਾਊਂਡ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਟੀਰੀਓ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਰਾਊਂਡ 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਰਾਊਂਡ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਰਾਊਂਡ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਟੀਰੀਓ (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਰਾਊਂਡ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "ਐਨਾਲਾਗ ਮੋਨੋ ਡੁਪਲੈਕਸ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "ਐਨਾਲਾਗ ਸਟੀਰੀਓ ਡੁਪਲੈਕਸ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "ਡਿਜ਼ੀਟਲ ਸਟੀਰੀਓ ਡੁਪਲੈਕਸ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "ਐਨਾਲਾਗ ਸਟੀਰੀਓ ਡੁਪਲੈਕਸ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "ਜ਼ੀਰੋ (Null) ਆਉਟਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "ਇੰਪੁੱਟ"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() ਤੋਂ ਇੱਕ ਮੁੱਲ ਮਿਲਿਆ ਹੈ, ਜੋ ਬਹੁਤ ਵੱਡਾ ਹੈ: %lu ਬਾਈਟ (%lu ms)।\n"
+"ਇਹ ALSA ਡਰਾਈਵਰ '%s' ਵਿਚਲਾ ਬੱਗ ਲੱਗਦਾ ਹੈ। ਇਸ ਮੁੱਦੇ ਦੀ ALSA ਡਿਵੈਲਪਰਾਂ ਨੂੰ ਰਿਪੋਰਟ ਦਿਓ ਜੀ।"
+msgstr[1] ""
+"snd_pcm_avail() ਤੋਂ ਇੱਕ ਮੁੱਲ ਮਿਲਿਆ ਹੈ, ਜੋ ਬਹੁਤ ਵੱਡਾ ਹੈ: %lu ਬਾਈਟ (%lu ms)।\n"
+"ਇਹ ALSA ਡਰਾਈਵਰ '%s' ਵਿਚਲਾ ਬੱਗ ਲੱਗਦਾ ਹੈ। ਇਸ ਮੁੱਦੇ ਦੀ ALSA ਡਿਵੈਲਪਰਾਂ ਨੂੰ ਰਿਪੋਰਟ ਦਿਓ ਜੀ।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() ਤੋਂ ਇੱਕ ਮੁੱਲ ਮਿਲਿਆ ਹੈ, ਜੋ ਬਹੁਤ ਵੱਡਾ ਹੈ: %li ਬਾਈਟ (%s%lu ms)।\n"
+"ਇਹ ALSA ਡਰਾਈਵਰ '%s' ਵਿਚਲਾ ਬੱਗ ਲੱਗਦਾ ਹੈ। ਇਸ ਮੁੱਦੇ ਦੀ ALSA ਡਿਵੈਲਪਰਾਂ ਨੂੰ ਰਿਪੋਰਟ ਦਿਓ ਜੀ।"
+msgstr[1] ""
+"snd_pcm_delay() ਤੋਂ ਇੱਕ ਮੁੱਲ ਮਿਲਿਆ ਹੈ, ਜੋ ਬਹੁਤ ਵੱਡਾ ਹੈ: %li ਬਾਈਟ (%s%lu ms)।\n"
+"ਇਹ ALSA ਡਰਾਈਵਰ '%s' ਵਿਚਲਾ ਬੱਗ ਲੱਗਦਾ ਹੈ। ਇਸ ਮੁੱਦੇ ਦੀ ALSA ਡਿਵੈਲਪਰਾਂ ਨੂੰ ਰਿਪੋਰਟ ਦਿਓ ਜੀ।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() ਤੋਂ ਇੱਕ ਮੁੱਲ ਮਿਲਿਆ ਹੈ, ਜੋ ਬਹੁਤ ਵੱਡਾ ਹੈ: %lu ਬਾਈਟ (%lu ms)।\n"
+"ਇਹ ALSA ਡਰਾਈਵਰ '%s' ਵਿਚਲਾ ਬੱਗ ਲੱਗਦਾ ਹੈ। ਇਸ ਮੁੱਦੇ ਦੀ ALSA ਡਿਵੈਲਪਰਾਂ ਨੂੰ ਰਿਪੋਰਟ ਦਿਓ ਜੀ।"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() ਤੋਂ ਇੱਕ ਮੁੱਲ ਮਿਲਿਆ ਹੈ, ਜੋ ਬਹੁਤ ਵੱਡਾ ਹੈ: %lu ਬਾਈਟ (%lu ms)।\n"
+"ਇਹ ALSA ਡਰਾਈਵਰ '%s' ਵਿਚਲਾ ਬੱਗ ਲੱਗਦਾ ਹੈ। ਇਸ ਮੁੱਦੇ ਦੀ ALSA ਡਿਵੈਲਪਰਾਂ ਨੂੰ ਰਿਪੋਰਟ ਦਿਓ ਜੀ।"
+msgstr[1] ""
+"snd_pcm_mmap_begin() ਤੋਂ ਇੱਕ ਮੁੱਲ ਮਿਲਿਆ ਹੈ, ਜੋ ਬਹੁਤ ਵੱਡਾ ਹੈ: %lu ਬਾਈਟ (%lu ms)।\n"
+"ਇਹ ALSA ਡਰਾਈਵਰ '%s' ਵਿਚਲਾ ਬੱਗ ਲੱਗਦਾ ਹੈ। ਇਸ ਮੁੱਦੇ ਦੀ ALSA ਡਿਵੈਲਪਰਾਂ ਨੂੰ ਰਿਪੋਰਟ ਦਿਓ ਜੀ।"
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "ਐਨਾਲਾਗ ਹੈੱਡਫੋਨ"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/pipewire.pot b/po/pipewire.pot
new file mode 100644
index 0000000..af26ba8
--- /dev/null
+++ b/po/pipewire.pot
@@ -0,0 +1,619 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the pipewire package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/issues/new\n"
+"POT-Creation-Date: 2022-06-30 12:50+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr ""
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr ""
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr ""
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr ""
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr ""
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr ""
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2250
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
+msgid "Off"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr ""
+
+#: 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:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1454
+msgid "Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1460
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2757
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2758
+msgid "Digital Output (S/PDIF)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr ""
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1442
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1187
+#: spa/plugins/alsa/acp/alsa-util.c:1281
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1253
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte "
+"(%s%lu ms).\n"
+"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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1300
+#, 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 ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1343
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr ""
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1272
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1275
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1285
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1364
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
+msgid "Headphone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1472
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1478
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1484
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1490
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1497
+msgid "Bluetooth"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr ""
diff --git a/po/pl.po b/po/pl.po
new file mode 100644
index 0000000..032979a
--- /dev/null
+++ b/po/pl.po
@@ -0,0 +1,718 @@
+# Polish translation for pipewire.
+# Copyright © 2008-2022 the pipewire authors.
+# This file is distributed under the same license as the pipewire package.
+# Piotr Drąg <piotrdrag@gmail.com>, 2008, 2012-2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-09-15 15:26+0000\n"
+"PO-Revision-Date: 2022-09-25 15:20+0200\n"
+"Last-Translator: Piotr Drąg <piotrdrag@gmail.com>\n"
+"Language-Team: Polish <community-poland@mozilla.org>\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"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:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [opcje]\n"
+" -h, --help Wyświetla tę pomoc\n"
+" --version Wyświetla wersję\n"
+" -c, --config Wczytuje konfigurację (domyślnie "
+"%s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "System multimediów PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Uruchomienie systemu multimediów PipeWire"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Tunel do %s/%s"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Głuche wyjście"
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Tunel dla %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Nieznane urządzenie"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s na %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s na %s"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [opcje] [<plik>|-]\n"
+" -h, --help Wyświetla tę pomoc\n"
+" --version Wyświetla wersję\n"
+" -v, --verbose Wyświetla więcej komunikatów\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote Nazwa zdalnej usługi\n"
+" --media-type Ustawia typ multimediów (domyślnie "
+"%s)\n"
+" --media-category Ustawia kategorię multimediów "
+"(domyślnie %s)\n"
+" --media-role Ustawia rolę multimediów (domyślnie "
+"%s)\n"
+" --target Ustawia węzeł docelowy (domyślnie "
+"%s)\n"
+" 0 oznacza brak wiązania\n"
+" --latency Ustawia opóźnienie węzła (domyślnie "
+"%s)\n"
+" Xjednostka (jednostka = s, ms, us, "
+"ns)\n"
+" lub bezpośrednie próbki (256)\n"
+" częstotliwość jest z pliku "
+"źródłowego\n"
+" -P --properties Ustawia właściwości węzła\n"
+"\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Częstotliwość próbki (wymagane do "
+"nagrywania) (domyślnie %u)\n"
+" --channels Liczba kanałów (wymagane do "
+"nagrywania) (domyślnie %u)\n"
+" --channel-map Mapa kanałów\n"
+" jedna z: „stereo”, "
+"„surround-51”… lub\n"
+" lista nazw kanałów rozdzielonych "
+"przecinkami: np. „FL,FR”\n"
+" --format Format próbki %s (wymagane do "
+"nagrywania) (domyślnie %s)\n"
+" --volume Głośność potoku w zakresie 0-1,0 "
+"(domyślnie %.3f)\n"
+" -q --quality Jakość resamplera od 0 do 15 "
+"(domyślnie %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Tryb odtwarzania\n"
+" -r, --record Tryb nagrywania\n"
+" -m, --midi Tryb MIDI\n"
+" -d, --dsd Tryb DSD\n"
+"\n"
+
+#: src/tools/pw-cli.c:2255
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [opcje] [polecenie]\n"
+" -h, --help Wyświetla tę pomoc\n"
+" --version Wyświetla wersję\n"
+" -d, --daemon Uruchamia jako usługę (domyślnie "
+"tego nie robi)\n"
+" -r, --remote Nazwa zdalnej usługi\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Dźwięk w zastosowaniach profesjonalnych"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
+msgid "Off"
+msgstr "Wyłączone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Wejście"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Wejście stacji dokującej"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Mikrofon stacji dokującej"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Wejście liniowe stacji dokującej"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+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:1454
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Przedni mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Tylny mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Zewnętrzny mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Wewnętrzny 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 "Wideo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "Automatyczne sterowanie natężeniem"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Brak automatycznego sterowania natężeniem"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Podbicie"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Brak podbicia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Amplituner"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Brak amplitunera"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Podbicie basów"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Brak podbicia basów"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1460
+msgid "Speaker"
+msgstr "Głośnik"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Słuchawki"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Wejście analogowe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Mikrofon stacji dokującej"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Mikrofon na słuchawkach"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Wyjście analogowe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Słuchawki 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Wyjście mono słuchawek"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Wyjście liniowe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Analogowe wyjście mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Głośniki"
+
+#: 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 "Wyjście cyfrowe (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Wejście cyfrowe (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Wejście wielokanałowe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Wyjście wielokanałowe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Wyjście gry"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Wyjście rozmowy"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Wejście rozmowy"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Wirtualne przestrzenne 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Analogowe mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Analogowe mono (lewy)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Analogowe mono (prawy)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Analogowe stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+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:1442
+msgid "Headset"
+msgstr "Słuchawki z mikrofonem"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+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
+msgid "Multichannel"
+msgstr "Wielokanałowe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Analogowe przestrzenne 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Analogowe przestrzenne 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Analogowe przestrzenne 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Analogowe przestrzenne 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Analogowe przestrzenne 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Analogowe przestrzenne 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Analogowe przestrzenne 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Analogowe przestrzenne 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Analogowe przestrzenne 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Analogowe przestrzenne 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Analogowe przestrzenne 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Cyfrowe stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Cyfrowe przestrzenne 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Cyfrowe przestrzenne 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Cyfrowe przestrzenne 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Cyfrowe stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Cyfrowe przestrzenne 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Rozmowa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Gra"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Analogowy dupleks mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Analogowy dupleks stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Cyfrowy dupleks stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Dupleks wielokanałowy"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Dupleks stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Rozmowa mono + przestrzenne 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "Wyjście %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, 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
+#, 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() zwróciło wyjątkowo dużą wartość: %lu bajt (%lu ms).\n"
+"Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
+"programistom usługi ALSA."
+msgstr[1] ""
+"snd_pcm_avail() zwróciło wyjątkowo dużą wartość: %lu bajty (%lu ms).\n"
+"Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
+"programistom usługi ALSA."
+msgstr[2] ""
+"snd_pcm_avail() zwróciło wyjątkowo dużą wartość: %lu bajtów (%lu ms).\n"
+"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
+#, 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() zwróciło wyjątkowo dużą wartość: %li bajt (%s%lu ms).\n"
+"Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
+"programistom usługi ALSA."
+msgstr[1] ""
+"snd_pcm_delay() zwróciło wyjątkowo dużą wartość: %li bajty (%s%lu ms).\n"
+"Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
+"programistom usługi ALSA."
+msgstr[2] ""
+"snd_pcm_delay() zwróciło wyjątkowo dużą wartość: %li bajtów (%s%lu ms).\n"
+"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
+#, 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() zwróciło dziwne wartości: opóźnienie %lu jest mniejsze "
+"niż korzyść %lu.\n"
+"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
+#, 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() zwróciło wyjątkowo dużą wartość: %lu bajt (%lu ms).\n"
+"Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
+"programistom usługi ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() zwróciło wyjątkowo dużą wartość: %lu bajty (%lu ms).\n"
+"Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
+"programistom usługi ALSA."
+msgstr[2] ""
+"snd_pcm_mmap_begin() zwróciło wyjątkowo dużą wartość: %lu bajtów (%lu ms).\n"
+"Prawdopodobnie jest to błąd sterownika ALSA „%s”. Proszę zgłosić ten problem "
+"programistom usługi ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(nieprawidłowe)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Wbudowany dźwięk"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Bramka dźwięku (źródło A2DP i AG HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1272
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1275
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Dupleks o wysokiej dokładności (źródło/odpływ A2DP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1285
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Dupleks o wysokiej dokładności (źródło/odpływ A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Odtwarzanie o wysokiej dokładności (odpływ BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Wejście o wysokiej dokładności (źródło BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Dupleks o wysokiej dokładności (źródło/odpływ BAP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Jednostka główna słuchawek z mikrofonem (HSP/HFP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1364
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Jednostka główna słuchawek z mikrofonem (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Handsfree"
+msgstr "Zestaw głośnomówiący"
+
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Zestaw głośnomówiący (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
+msgid "Headphone"
+msgstr "Słuchawki"
+
+#: spa/plugins/bluez5/bluez5-device.c:1472
+msgid "Portable"
+msgstr "Przenośne"
+
+#: spa/plugins/bluez5/bluez5-device.c:1478
+msgid "Car"
+msgstr "Samochód"
+
+#: spa/plugins/bluez5/bluez5-device.c:1484
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1490
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1497
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/pt.po b/po/pt.po
new file mode 100644
index 0000000..4f5e282
--- /dev/null
+++ b/po/pt.po
@@ -0,0 +1,677 @@
+# Portuguese translation for pipewire.
+# Copyright © 2008-2022 the pipewire authors.
+# This file is distributed under the same license as the pipewire package.
+# Rui Gouveia <rui.gouveia@globaltek.pt>, 2012.
+# Rui Gouveia <rui.gouveia@globaltek.pt>, 2012, 2015.
+# Pedro Albuquerque <palbuquerque73@openmailbox.com>, 2015.
+# Juliano de Souza Camargo <julianosc@pm.me>, 2020.
+# Hugo Carvalho <hugokarvalho@hotmail.com>, 2021 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-11-17 15:06+0100\n"
+"PO-Revision-Date: 2022-03-30 19:37+0100\n"
+"Last-Translator: Hugo Carvalho <hugokarvalho@hotmail.com>\n"
+"Language-Team: Portuguese <https://l10n.gnome.org/teams/pt/>\n"
+"Language: pt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 3.0.1\n"
+
+#: src/daemon/pipewire.c:45
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [options]\n"
+" -h, --help Mostra esta ajuda\n"
+" --version Mostra a versão\n"
+" -c, --config Carrega uma configuração (Padrão "
+"%s)\n"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:185
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:185
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Túnel para %s/%s"
+
+#: src/modules/module-pulse-tunnel.c:536
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Túnel para %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Dispositivo desconhecido"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s em %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s em %s"
+
+#: src/tools/pw-cat.c:1058
+#, 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 [options] <file>\n"
+" -h, --help Mostra esta ajuda\n"
+" --version Mostra a versão\n"
+" -v, --verbose Ativa operações descritivas\n"
+"\n"
+
+#: src/tools/pw-cat.c:1065
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+" -R, --remote Nome do daemon remoto\n"
+" --media-type Define o tipo de média (padrão: %s)\n"
+" --media-category Define a categoria de média (padrão: "
+"%s)\n"
+" --media-role Define o papel de média (padrão: "
+"%s)\n"
+" --target Define o alvo do nó (padrão: %s)\n"
+" 0 significa não vincular\n"
+" --latency Define a latência do nó (padrão: "
+"%s)\n"
+" Xunit (unidade = s, ms, us, ns)\n"
+" ou amostras diretas (256)\n"
+" a taxa é um dos ficheiros fontes\n"
+" --list-targets Lista alvos disponíveis para --"
+"target\n"
+"\n"
+
+#: src/tools/pw-cat.c:1083
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Taxa de amostra (req. por rec) "
+"(padrão: %u)\n"
+" --channels Número de canais (req. por rec) "
+"(padrão: %u)\n"
+" --channel-map Mapa de canal\n"
+" um de : “stereo”, "
+"“surround-51”,... ou\n"
+" lista separada por vírgulas de "
+"nomes de\n"
+" canal: e.x.: “FL,FR”\n"
+" --format Formato da amostra %s (req. por rec) "
+"(padrão: %s)\n"
+" --volume Volume do fluxo 0-1.0 (padrão: "
+"%.3f)\n"
+" -q --quality Qualidade da reamostra (0 - 15) "
+"(padrão: %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:1100
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Modo de reprodução\n"
+" -r, --record Modo de gravação\n"
+" -m, --midi Modo midi\n"
+" -d, --dsd Modo DSD\n"
+"\n"
+
+#: src/tools/pw-cli.c:3018
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [options] [command]\n"
+" -h, --help Mostra esta ajuda\n"
+" --version Mostra a versão\n"
+" -d, --daemon Inicia como daemon (falso por "
+"padrão)\n"
+" -r, --remote Nome do daemon remoto\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pró áudio"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Off"
+msgstr "Desligado"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Base de entrada da estação"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Base de microfone da estação"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Base de linha de entrada da estação"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Linha de entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1302
+msgid "Microphone"
+msgstr "Microfone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Microfone frontal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Microfone traseiro"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Microfone externo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Microfone interno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "Rádio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "Vídeo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "Controlo automático de aumento"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Sem controlo automático de aumento"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Aumentar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Não aumentar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Sem amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Aumentar graves"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Não aumentar graves"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1307
+msgid "Speaker"
+msgstr "Coluna"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Auscultadores"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Entrada analógica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Microfone de base"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Microfone de auscultadores"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Saída analógica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Auscultadores 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Saída analógica auscultadores"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Linha de saída"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Saída mono analógica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Colunas"
+
+#: 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 "Saída digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Entrada digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Entrada multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Saída multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Saída de jogo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Saída de conversa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Entrada de conversa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Surround Virtual 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Mono Analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Mono Analógico (Esquerda)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Mono Analógico (Direita)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Estéreo Analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr "Estéreo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1292
+msgid "Headset"
+msgstr "Auscultadores"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Coluna"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Surround 2.1 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Surround 3.0 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Surround 3.1 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Surround 4.0 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Surround 4.1 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Surround 5.0 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Surround 5.1 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Surround 6.0 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Surround 6.1 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Surround 7.0 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Surround 7.1 analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Estéreo Digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Surround Digital 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Surround Digital 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Surround Digital 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Estéreo Digital (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Surround 5.1 (IEC958/AC3) digital"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Conversa"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Jogo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Mono duplex analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Estéreo duplex analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Estéreo duplex digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Duplex multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Duplex estéreo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono Chat + 7.1 Surround"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4750
+#, c-format
+msgid "%s Output"
+msgstr "Saída %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4757
+#, c-format
+msgid "%s Input"
+msgstr "Entrada %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() retornou um valor excecionalmente elevado: %lu byte (%lu "
+"ms).\n"
+"Provavelmente isto é um erro no controlador ALSA '%s'. Por favor, reporte "
+"este problema aos programadores do ALSA."
+msgstr[1] ""
+"snd_pcm_avail() retornou um valor excecionalmente elevado: %lu bytes (%lu "
+"ms).\n"
+"Provavelmente isto é um erro no controlador ALSA '%s'. Por favor, reporte "
+"este problema aos programadores do ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1239
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() retornou um valor excecionalmente elevado: %li byte (%s%lu "
+"ms).\n"
+"Provavelmente isto é um erro no driver ALSA '%s'. Por favor, reporte este "
+"problema aos programadores do ALSA."
+msgstr[1] ""
+"snd_pcm_delay() retornou um valor excecionalmente elevado: %li bytes (%s%lu "
+"ms).\n"
+"Provavelmente isto é um erro no driver ALSA '%s'. Por favor, reporte este "
+"problema aos programadores do ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1286
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() retornou um valor excecionalmente elevado: %lu bytes "
+"(%lu ms).\n"
+"Provavelmente isto é um erro no controlador ALSA \"%s\". Por favor, reporte "
+"este problema aos programadores do ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1329
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() retornou um valor excecionalmente elevado: %lu byte "
+"(%lu ms).\n"
+"Provavelmente isto é um erro no driver ALSA '%s'. Por favor, reporte este "
+"problema aos programadores do ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() retornou um valor excecionalmente elevado: %lu bytes "
+"(%lu ms).\n"
+"Provavelmente isto é um erro no driver ALSA '%s'. Por favor, reporte este "
+"problema aos programadores do ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:464
+msgid "(invalid)"
+msgstr "(inválido)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Áudio interno"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Porta de áudio (A2DP Fonte & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1178
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Reprodução de Alta Fidelidade (A2DP Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Duplex de Alta Fidelidade (A2DP Fonte/Sink, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1188
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Reprodução de Alta Fidelidade (A2DP Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1190
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Duplex de Alta Fidelidade (A2DP Fonte/Sink)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1217
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Unidade de Auscultadores (HSP/HFP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1221
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Unidade de Auscultadores (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1297
+msgid "Handsfree"
+msgstr "Mãos livres"
+
+#: spa/plugins/bluez5/bluez5-device.c:1312
+msgid "Headphone"
+msgstr "Auscultadores"
+
+#: spa/plugins/bluez5/bluez5-device.c:1317
+msgid "Portable"
+msgstr "Portátil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+msgid "Car"
+msgstr "Carro"
+
+#: spa/plugins/bluez5/bluez5-device.c:1327
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1332
+msgid "Phone"
+msgstr "Telefone"
+
+#: spa/plugins/bluez5/bluez5-device.c:1338
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#~ msgid "PipeWire Media System"
+#~ msgstr "Sistema de Multimédia PipeWire"
+
+#~ msgid "Start the PipeWire Media System"
+#~ msgstr "Iniciar o Sistema de Multimédia PipeWire"
diff --git a/po/pt_BR.po b/po/pt_BR.po
new file mode 100644
index 0000000..9b70d6f
--- /dev/null
+++ b/po/pt_BR.po
@@ -0,0 +1,720 @@
+# Brazilian Portuguese translation for pipewire
+# Copyright (C) 2022 Rafael Fontenelle <rafaelff@gnome.org>
+# This file is distributed under the same license as the pipewire package.
+# Fabian Affolter <fab@fedoraproject.org>, 2008.
+# Igor Pires Soares <igor@projetofedora.org>, 2009, 2012.
+# Rafael Fontenelle <rafaelff@gnome.org>, 2013-2021.
+# Matheus Barbosa <mdpb.matheus@gmail.com>, 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-09-30 03:27+0000\n"
+"PO-Revision-Date: 2022-01-25 19:49-0300\n"
+"Last-Translator: Matheus Barbosa <mdpb.matheus@gmail.com>\n"
+"Language-Team: Brazilian Portuguese <gnome-pt_br-list@gnome.org>\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"X-Generator: Gtranslator 40.0\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [opções]\n"
+" -h, --help Mostra esta ajuda\n"
+" --version Mostra a versão\n"
+" -c, --config Carrega uma configuração (Padrão: "
+"%s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Sistema de Mídia PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Inicia o Sistema de Mídia PipeWire"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Túnel para %s/%s"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Saída de falsa"
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Túnel para %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Dispositivo desconhecido"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s em %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s em %s"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [opções] [<arquivo>|-]\n"
+" -h, --help Mostra esta ajuda\n"
+" --version Mostra a versão\n"
+" -v, --verbose Habilita operações verbosas\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote Nome do daemon remoto\n"
+" --media-type Define o tipo de mídia (padrão: %s)\n"
+" --media-category Define a categoria de mídia (padrão: "
+"%s)\n"
+" --media-role Define o papel de mídia (padrão: "
+"%s)\n"
+" --target Define o alvo do nó (padrão: %s)\n"
+" 0 significa não vincular\n"
+" --latency Define a latência do nó (padrão: "
+"%s)\n"
+" Xunit (unidade = s, ms, us, ns)\n"
+" ou amostras diretas (256)\n"
+" a taxa é um dos arquivos fontes\n"
+" --properties Define as propriedades do nó\n"
+"\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Taxa de amostra (req. por rec) "
+"(padrão: %u)\n"
+" --channels Número de canais (req. por rec) "
+"(padrão: %u)\n"
+" --channel-map Mapa de canal\n"
+" um de : “stereo”, "
+"“surround-51”,... ou\n"
+" lista separada por vírgulas de "
+"nomes de\n"
+" canal: e.x.: “FL,FR”\n"
+" --format Formata da amostra %s (req. por rec) "
+"(padrão: %s)\n"
+" --volume Volume do fluxo 0-1.0 (padrão: "
+"%.3f)\n"
+" -q --quality Qualidade da reamostra (0 - 15) "
+"(padrão: %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Modo de reprodução\n"
+" -r, --record Modo de gravação\n"
+" -m, --midi Modo Midi\n"
+" -d, --dsd Modo DSD\n"
+"\n"
+
+#: src/tools/pw-cli.c:2255
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [opções] [comando]\n"
+" -h, --help Mostra esta ajuda\n"
+" --version Mostra a versão\n"
+" -d, --daemon Inicia como daemon (Padrão: false)\n"
+" -r, --remote Nome do daemon remoto\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
+msgid "Off"
+msgstr "Desligado"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Entrada"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Entrada da base de encaixe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Microfone de estação de base de encaixe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Entrada de linha de estação de base de encaixe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Entrada de linha"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1454
+msgid "Microphone"
+msgstr "Microfone"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Microfone frontal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Microfone traseiro"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Microfone externo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Microfone interno"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "Rádio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "Vídeo"
+
+# https://pt.wikipedia.org/wiki/Controle_autom%C3%A1tico_de_ganho
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "Controle automático de ganho"
+
+# https://pt.wikipedia.org/wiki/Controle_autom%C3%A1tico_de_ganho
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Sem controle automático de ganho"
+
+# Este contexto de Boost é "reforço" no áudio, e não "impulso".
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Reforço"
+
+# Este contexto de Boost é "reforço" no áudio, e não "impulso".
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Sem reforço"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Amplificador"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Sem amplificador"
+
+# Este contexto de Boost é "reforço" no áudio, e não "impulso".
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Reforço de graves"
+
+# Este contexto de Boost é "reforço" no áudio, e não "impulso".
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Sem reforço de graves"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1460
+msgid "Speaker"
+msgstr "Auto-falante"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Fones de ouvido"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Entrada analógica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Microfone de base de encaixe"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Microfone de headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Saída analógica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Fones de ouvido 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Saída analógica fones de ouvido"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Saída de linha"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Saída analógica monofônica"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Alto-falantes"
+
+#: 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 "Saída digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Entrada digital (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Entrada multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Saída multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Saída de jogo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Saída de bate-papo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Entrada de bate-papo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Surround virtual 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Monofônico analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Monofônico analógico (Esquerdo)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Monofônico analógico (Direito)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Estéreo analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr "Estéreo"
+
+# Fone de ouvido não se encaixa como tradução aqui, pois há ou pode haver microfone junto.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1442
+msgid "Headset"
+msgstr "Headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Viva voz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Surround analógico 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Surround analógico 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Surround analógico 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Surround analógico 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Surround analógico 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Surround analógico 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Surround analógico 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Surround analógico 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Surround analógico 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Surround analógico 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Surround analógico 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Estéreo digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Surround digital 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Surround digital 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Surround digital 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Estéreo digital (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Surround digital 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Bate-papo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Jogo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Duplex monofônico analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Duplex estéreo analógico"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Duplex estéreo digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Duplex multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Duplex estéreo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Bate-papo monofônico + surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "Saída %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "Entrada %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"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() retornou um valor que é excepcionalmente grande: %lu byte "
+"(%lu ms).\n"
+"É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
+"relate esse problema aos desenvolvedores do ALSA."
+msgstr[1] ""
+"snd_pcm_avail() retornou um valor que é excepcionalmente grande: %lu bytes "
+"(%lu ms).\n"
+"É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
+"relate esse problema aos desenvolvedores do ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1253
+#, 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() retornou um valor que é excepcionalmente grande: %li byte (%s"
+"%lu ms).\n"
+"É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
+"relate esse problema aos desenvolvedores do ALSA."
+msgstr[1] ""
+"snd_pcm_delay() retornou um valor que é excepcionalmente grande: %li bytes "
+"(%s%lu ms).\n"
+"É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
+"relate esse problema aos desenvolvedores do ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1300
+#, 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() retornou um valor estranho: o atraso de %lu é menor do que "
+"(%lu ms).\n"
+"É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
+"relate esse problema aos desenvolvedores do ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1343
+#, 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() retornou um valor que é excepcionalmente grande: %lu "
+"byte (%lu ms).\n"
+"É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
+"relate esse problema aos desenvolvedores do ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() retornou um valor que é excepcionalmente grande: %lu "
+"bytes (%lu ms).\n"
+"É mais provável que isso seja um erro no driver “%s” do ALSA. Por favor, "
+"relate esse problema aos desenvolvedores do ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(inválido)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Áudio interno"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Gateway de áudio (fonte A2DP & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1272
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Reprodução de alta-fidelidade (destino A2DP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1275
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Duplex de alta-fidelidade (fonte/destino A2DP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Reprodução de alta-fidelidade (destino A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1285
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Duplex de alta-fidelidade (fonte/destino A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Reprodução de alta-fidelidade (destino BAP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Entrada de alta-fidelidade (fonte BAP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Duplex de alta-fidelidade (fonte/destino BAP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Unidade de headset (HSP/HFP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1364
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Unidade de headset (HSP/HFP)"
+
+# Supostamente relacionado a HFP, hands-free profile, mas não encontrei tradução comum
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Handsfree"
+msgstr "Handsfree"
+
+# Supostamente relacionado a HFP, hands-free profile, mas não encontrei tradução comum
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Handsfree (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
+msgid "Headphone"
+msgstr "Fones de ouvido"
+
+#: spa/plugins/bluez5/bluez5-device.c:1472
+msgid "Portable"
+msgstr "Portátil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1478
+msgid "Car"
+msgstr "Carro"
+
+#: spa/plugins/bluez5/bluez5-device.c:1484
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1490
+msgid "Phone"
+msgstr "Telefone"
+
+#: spa/plugins/bluez5/bluez5-device.c:1497
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/ro.po b/po/ro.po
new file mode 100644
index 0000000..8009bcd
--- /dev/null
+++ b/po/ro.po
@@ -0,0 +1,679 @@
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the pipewire package.
+#
+# Sergiu Bivol <sergiu@cip.md>, 2022.
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/issues/"
+"new\n"
+"POT-Creation-Date: 2021-11-17 15:06+0100\n"
+"PO-Revision-Date: 2022-02-05 12:06+0000\n"
+"Last-Translator: Sergiu Bivol <sergiu@cip.md>\n"
+"Language-Team: Romanian\n"
+"Language: ro\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 <"
+" 20)) ? 1 : 2;\n"
+"X-Generator: Lokalize 21.12.2\n"
+
+#: src/daemon/pipewire.c:45
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [opțiuni]\n"
+" -h, --help Arată acest ajutor\n"
+" --version Arată versiunea\n"
+" -c, --config Încarcă configurare (implicit %s)\n"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:185
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:185
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Tunelează spre %s/%s"
+
+#: src/modules/module-pulse-tunnel.c:536
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Tunelează pentru %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Dispozitiv necunoscut"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s pe %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s pe %s"
+
+#: src/tools/pw-cat.c:1058
+#, 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 [opțiuni] <fișier>\n"
+" -h, --help Arată acest ajutor\n"
+" --version Arată versiunea\n"
+" -v, --verbose Activează operații verboase\n"
+"\n"
+
+#: src/tools/pw-cat.c:1065
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+" -R, --remote Denumirea demonului distant\n"
+" --media-type Stabilește tipul mediului (implicit "
+"%s)\n"
+" --media-category Stabilește categoria mediului"
+" (implicit %s)\n"
+" --media-role Stabilește rolul mediului (implicit "
+"%s)\n"
+" --target Stabilește ținta nodului (implicit "
+"%s)\n"
+" 0 înseamnă „nu lega”\n"
+" --latency Stabilește latența nodului (implicit "
+"%s)\n"
+" Xunit (unitate = s, ms, us, ns)\n"
+" sau mostre directe (256)\n"
+" rata este cea a fișierului sursă\n"
+" --list-targets Enumeră țintele disponibile pentru"
+" --target\n"
+"\n"
+
+#: src/tools/pw-cat.c:1083
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Rată de eșantionare (nec. pt."
+" înregistrare) (implicit %u)\n"
+" --channels Număr de canale (nec. pt."
+" înregistrare) (implicit %u)\n"
+" --channel-map Hartă canale\n"
+" una dintre: \"stereo\","
+" \"surround-51\",... sau listă\n"
+" separată prin virgulă cu denumiri"
+" de canale: de ex. \"FL,FR\"\n"
+" --format Format de eșantionare %s (nec. pt."
+" înregistrare) (implicit %s)\n"
+" --volume Volum flux 0-1.0 (implicit %.3f)\n"
+" -q --quality Calitate de re-eșantionare (0 - 15)"
+" (implicit %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:1100
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Regim de redare\n"
+" -r, --record Regim de înregistrare\n"
+" -m, --midi Regim MIDI\n"
+" -d, --dsd regim DSD\n"
+"\n"
+
+#: src/tools/pw-cli.c:3018
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [opțiuni] [comandă]\n"
+" -h, --help Arată acest ajutor\n"
+" --version Arată versiunea\n"
+" -d, --daemon Pornește ca demon (implicit fals)\n"
+" -r, --remote Denumire demon distant\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Pro Audio"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Off"
+msgstr "Oprit"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Intrare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Intrare stație de andocare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Microfon stație de andocare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Linie de intrare stație de andocare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Linie de intrare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1302
+msgid "Microphone"
+msgstr "Microfon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Microfon frontal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Microfon spate"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Microfon extern"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Microfon intern"
+
+#: 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 "Control automat al nivelului de intrare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Fără control automat al nivelului de intrare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Stimulare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Fără stimulare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Amplificator"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Fără amplificator"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Stimulare bas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Fără stimulare bas"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1307
+msgid "Speaker"
+msgstr "Difuzor"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Căști"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Intrare analogică"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Microfon andocare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Microfon căști"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Ieșire analogică"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Căști 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Ieșire mono căști"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Linie de ieșire"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Ieșire mono analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Difuzoare"
+
+#: 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 "Ieșire digitală (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Intrare digitală (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Intrare multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Ieșire multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Ieșire joc"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Ieșire discuție"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Intrare discuție"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "surround virtual 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "mono analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "mono analogic (stânga)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "mono analogic (dreapta)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "stereo analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+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:1292
+msgid "Headset"
+msgstr "Set cu cască"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Difuzor"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "surround analogic 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "surround analogic 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "surround analogic 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "surround analogic 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "surround analogic 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "surround analogic 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "surround analogic 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "surround analogic 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "surround analogic 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "surround analogic 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "surround analogic 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "stereo digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "surround digital 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "surround digital 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "surround digital 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "stereo digital (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "surround digital 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Discuție"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "joc"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Duplex mono analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Duplex stereo analogic"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Duplex stereo digital (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Duplex multicanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Duplex stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Discuție mono + Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4750
+#, c-format
+msgid "%s Output"
+msgstr "Ieșire %s"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4757
+#, c-format
+msgid "%s Input"
+msgstr "Intrare %s"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1173
+#: spa/plugins/alsa/acp/alsa-util.c:1267
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() a întors o valoare excepțional de mare: %lu octet (%lu ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+msgstr[1] ""
+"snd_pcm_avail() a întors o valoare excepțional de mare: %lu octeți (%lu ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+msgstr[2] ""
+"snd_pcm_avail() a întors o valoare excepțional de mare: %lu de octeți (%lu"
+" ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1239
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() a întors o valoare excepțional de mare: %li octet (%s%lu"
+" ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+msgstr[1] ""
+"snd_pcm_delay() a întors o valoare excepțional de mare: %li octeți (%s%lu"
+" ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+msgstr[2] ""
+"snd_pcm_delay() a întors o valoare excepțional de mare: %li de octeți (%s%lu"
+" ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1286
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() a întors valori stranii: întârzierea %lu e mai mică"
+" decât disponibilul %lu.\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1329
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() a întors o valoare excepțional de mare: %lu octet (%lu"
+" ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() a întors o valoare excepțional de mare: %lu octeți (%lu"
+" ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+msgstr[2] ""
+"snd_pcm_mmap_begin() a întors o valoare excepțional de mare: %lu de octeți ("
+"%lu ms).\n"
+"Cel mai probabil, acesta e un defect în driver-ul ALSA „%s”. Raportați"
+" această problemă dezvoltatorilor ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:464
+msgid "(invalid)"
+msgstr "(nevalid)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Audio încorporat"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Poartă de acces audio (sursă A2DP și HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1178
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Redare de fidelitate înaltă (chiuvetă A2DP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Duplex de fidelitate înaltă (sursă/chiuvetă A2DP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1188
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Redare de fidelitate înaltă (chiuvetă A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1190
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Duplex de fidelitate înaltă (sursă/chiuvetă A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1217
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Bază pentru set cu cască (HSP/HFP, codec %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1221
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Bază pentru set cu cască (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1297
+msgid "Handsfree"
+msgstr "Mâini libere"
+
+#: spa/plugins/bluez5/bluez5-device.c:1312
+msgid "Headphone"
+msgstr "Cască"
+
+#: spa/plugins/bluez5/bluez5-device.c:1317
+msgid "Portable"
+msgstr "Portabil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+msgid "Car"
+msgstr "Mașină"
+
+#: spa/plugins/bluez5/bluez5-device.c:1327
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1332
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1338
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/ru.po b/po/ru.po
new file mode 100644
index 0000000..15ce856
--- /dev/null
+++ b/po/ru.po
@@ -0,0 +1,695 @@
+# Russian translation of pipewire.
+# Copyright (C) 2010 pipewire
+# This file is distributed under the same license as the pipewire package.
+#
+# Leonid Kurbatov <llenchikk@rambler.ru>, 2010, 2012.
+# Alexander Potashev <aspotashev@gmail.com>, 2014, 2019.
+# Victor Ryzhykh <victorr2007@yandex.ru>, 2021.
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\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-06-30 13:26+0300\n"
+"Last-Translator: Alexey Rubtsov <rushills@gmail.com>\n"
+"Language-Team: ru\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"X-Generator: Poedit 3.0\n"
+
+#: src/daemon/pipewire.c:45
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [опции]\n"
+" -h, --help Показать справку\n"
+" --version Информация о версии\n"
+" -c, --config Указать файл конфигурации (По "
+"умолчанию %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Мультимедийная система PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Запустить PipeWire"
+
+#: src/examples/media-session/alsa-monitor.c:585
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Встроенное аудио"
+
+#: src/examples/media-session/alsa-monitor.c:589
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Модем"
+
+#: src/examples/media-session/alsa-monitor.c:598
+#: src/modules/module-zeroconf-discover.c:290
+msgid "Unknown device"
+msgstr "Неизвестное устройство"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:182
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:182
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Туннель на %s/%s"
+
+#: src/modules/module-pulse-tunnel.c:511
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Туннель для %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:302
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s на %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:306
+#, c-format
+msgid "%s on %s"
+msgstr "%s по %s"
+
+#: src/tools/pw-cat.c:991
+#, c-format
+msgid ""
+"%s [options] <file>\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [опции] <файл>\n"
+" -h, --help Показать справку\n"
+" --version Информация о версии\n"
+" -v, --verbose Включить показ подробной информации\n"
+"\n"
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+" -R, --remote Имя удаленного фоновой службы\n"
+" --media-type Задать тип мультимедиа (по умолчанию "
+"%s)\n"
+" --media-category Задать категорию мультимедиа (по "
+"умолчанию %s)\n"
+" --media-role Задать роль мультимедиа (по "
+"умолчанию %s)\n"
+" --target Задать цель узла (по умолчанию %s)\n"
+" 0 значит не связывать\n"
+" --latency Задать задежку узла (по умолчанию "
+"%s)\n"
+" Xединица (единица = s, ms, us, "
+"ns)\n"
+" or direct samples (256)\n"
+" частота такая же как у источника "
+"file\n"
+" --list-targets Перечислить доступные цели для --"
+"target\n"
+"\n"
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Частота дискретизации (необходимо "
+"для записи) (По умолчанию %u)\n"
+" --channels Количество каналов (необходимо для "
+"записи) (default %u)\n"
+" --channel-map Карта каналов\n"
+" одно из: \"stereo\", "
+"\"surround-51\",... or\n"
+" список каналов через запятую "
+"names: eg. \"FL,FR\"\n"
+" --format Формат %s (необходимо для записи) "
+"(default %s)\n"
+" --volume Громкость 0-1.0 (по умолчанию %.3f)\n"
+" -q --quality Качество ресэмплера (0 - 15) (по "
+"умолчанию %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+" -p, --playback Режим проигрывания\n"
+" -r, --record Режим записи\n"
+" -m, --midi Режим MIDI\n"
+"\n"
+
+#: src/tools/pw-cli.c:2959
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [опции] [команда]\n"
+" -h, --help Показать справку\n"
+" --version Информация о версии\n"
+" -d, --daemon Запустить в режиме фоновой службы "
+"(По умолчанию false)\n"
+" -r, --remote Имя удаленного фоновой службы\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:291
+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
+msgid "Off"
+msgstr "Выключено"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Вход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Вход док-станции"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Микрофон док-станции"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Линейный вход док-станции"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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:1175
+msgid "Microphone"
+msgstr "Микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Фронтальный микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Тыловой микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Внешний микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Встроенный микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Радио"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Видео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Автоматическая регулировка усиления"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Нет автоматической регулировки усиления"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Усиление"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Нет усиления"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Усилитель"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Нет усилителя"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Усиление басов"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Нет усиления басов"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1180
+msgid "Speaker"
+msgstr "Динамик"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Наушники"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Аналоговый вход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Микрофон док-станции"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Микрофон гарнитуры"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Аналоговый выход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "Наушники 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "Моно-выход наушников"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Линейный выход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Аналоговый моно-выход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Динамики"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Цифровой выход (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Цифровой вход (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Многоканальный вход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Многоканальный выход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "Игровой выход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "Разговорный выход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "Разговорный выход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "Виртуальный объёмный звук 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Аналоговый моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "Аналоговый моно (левый)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "Аналоговый моно (правый)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Аналоговый стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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:1165
+msgid "Headset"
+msgstr "Гарнитура"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "Спикерфон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Многоканальный"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Аналоговый объёмный 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Аналоговый объёмный 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Аналоговый объёмный 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Аналоговый объёмный 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Аналоговый объёмный 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Аналоговый объёмный 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Аналоговый объёмный 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Аналоговый объёмный 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Аналоговый объёмный 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Аналоговый объёмный 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Аналоговый объёмный 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Цифровой стерео (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Цифровой объёмный 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Цифровой объёмный 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Цифровой объёмный 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Цифровой стерео (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Цифровой объёмный 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "Чат"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "Игра"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Аналоговый моно дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Аналоговый стерео дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Цифровой стерео дуплекс (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Многоканальный дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "Стерео дуплекс"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Моно чат + 7.1 Surround"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s выход"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s вход"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() возвращает значение, которое является исключительно большим: "
+"%lu байт (%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+msgstr[1] ""
+"snd_pcm_avail() возвращает значение, которое является исключительно большим: "
+"%lu байта (%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+msgstr[2] ""
+"snd_pcm_avail() возвращает значение, которое является исключительно большим: "
+"%lu байт (%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() возвращает значение, которое является исключительно большим: "
+"%li байт (%s%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+msgstr[1] ""
+"snd_pcm_delay() возвращает значение, которое является исключительно большим: "
+"%li байта (%s%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+msgstr[2] ""
+"snd_pcm_delay() возвращает значение, которое является исключительно большим: "
+"%li байт (%s%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() возвращает странное значение: задержка %lu меньше "
+"доступных %lu.\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() возвращает значение, которое является исключительно "
+"большим: %lu байт (%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+msgstr[1] ""
+"snd_pcm_mmap_begin() возвращает значение, которое является исключительно "
+"большим: %lu байта (%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+msgstr[2] ""
+"snd_pcm_mmap_begin() возвращает значение, которое является исключительно "
+"большим: %lu байт (%lu мс).\n"
+"Вероятно, это ошибка в драйвере ALSA «%s». Пожалуйста, сообщите об этой "
+"проблеме разработчикам ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(недействительно)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1030
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Адаптер аудиогарнитуры (приёмник A2DP и HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1053
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Воспроизведение высокого качества (приёмник A2DP, кодек %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1055
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Дуплекс высокого качества (источник / приёмник A2DP, кодек %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1061
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Воспроизведение высокого качества (приёмник A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1063
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Дуплекс высокого качества (источник / приёмник A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1090
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Гарнитура (HSP/HFP, кодек %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1094
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Гарнитура (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "Handsfree"
+msgstr "Hands-Free устройство"
+
+#: spa/plugins/bluez5/bluez5-device.c:1185
+msgid "Headphone"
+msgstr "Наушники"
+
+#: spa/plugins/bluez5/bluez5-device.c:1190
+msgid "Portable"
+msgstr "Портативное устройство"
+
+#: spa/plugins/bluez5/bluez5-device.c:1195
+msgid "Car"
+msgstr "Автомобильное устройство"
+
+#: spa/plugins/bluez5/bluez5-device.c:1200
+msgid "HiFi"
+msgstr "Hi-Fi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1205
+msgid "Phone"
+msgstr "Телефон"
+
+#: spa/plugins/bluez5/bluez5-device.c:1211
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/si.po b/po/si.po
new file mode 100644
index 0000000..431aaff
--- /dev/null
+++ b/po/si.po
@@ -0,0 +1,571 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the pipewire package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: si\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"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr ""
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr ""
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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] ""
+msgstr[1] ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/sk.po b/po/sk.po
new file mode 100644
index 0000000..b9a70c7
--- /dev/null
+++ b/po/sk.po
@@ -0,0 +1,587 @@
+# Slovak translation for PipeWire.
+# Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER
+# This file is distributed under the same license as the PipeWire package.
+# Dušan Kazik <prescott66@gmail.com>, 2014, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire master\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2020-11-25 08:35+0000\n"
+"Last-Translator: Dusan Kazik <prescott66@gmail.com>\n"
+"Language-Team: Slovak <https://translate.fedoraproject.org/projects/pipewire/"
+"pipewire/sk/>\n"
+"Language: sk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 1 : (n>=2 && n<=4) ? 2 : 0;\n"
+"X-Generator: Weblate 4.3.2\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Vstavaný zvuk"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Vypnuté"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(neplatné)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Vstup dokovacej stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Mikrofón dokovacej stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Vstupná linka dokovacej stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Vstupná linka"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Mikrofón"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "Predný mikrofón"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "Zadný mikrofón"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Externý mikrofón"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Vstavaný mikrofón"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Rádio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Automatické ovládanie zosilnenia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Bez automatického ovládania zosilnenia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Zosilnenie"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Bez zosilnenia"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Zosilňovač"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Bez zosilňovača"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "Zosilnenie basov"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "Bez zosilnenia basov"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "Reproduktor"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Slúchadlá"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analógový vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Mikrofón doku"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "Mikrofón headsetu"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analógový výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Slúchadlá"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Analógový mono výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "Výstupná linka"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analógový mono výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "Reproduktory"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "Digitálny výstup (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitálny vstup (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "Viackanálový vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "Viackanálový výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "%s výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "%s výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "%s vstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Virtuálny priestorový cieľ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analógový mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Analógový mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Analógový mono"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analógový stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "Headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Reproduktor"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "Viackanálový"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analógový priestorový 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analógový priestorový 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analógový priestorový 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analógový priestorový 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analógový priestorový 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analógový priestorový 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analógový priestorový 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analógový priestorový 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analógový priestorový 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analógový priestorový 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analógový priestorový 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitálne stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitálny priestorový 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitálny priestorový 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitálny priestorový 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitálny stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitálny priestorový 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Obojsmerný analógový mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Obojsmerný analógový stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Obojsmerný digitálny stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "Obojsmerný viackanálový"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Obojsmerný analógový stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s výstup"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s vstup"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, 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] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, 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] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, 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] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "Handsfree"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "Slúchadlo"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "Prenosné"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "Automobil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "Telefón"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "Vstup cez Bluetooth"
diff --git a/po/sr.po b/po/sr.po
new file mode 100644
index 0000000..247b587
--- /dev/null
+++ b/po/sr.po
@@ -0,0 +1,641 @@
+# Serbian translations for pipewire
+# Copyright (C) 2006 Lennart Poettering
+# This file is distributed under the same license as the pipewire package.
+# Igor Miletic (Игор Милетић) <grejigl-gnomeprevod@yahoo.ca>, 2009.
+# Miloš Komarčević <kmilos@gmail.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:55+0000\n"
+"Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n"
+"Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Унутрашњи звук"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Модем"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Искључено"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(неисправно)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Улаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Улаз прикључне станице"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "Микрофон прикључне станице"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "Улаз прикључне станице"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "Микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "Микрофон прикључне станице"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "Микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Спољни микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Унутрашњи микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Радио"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Видео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Самостална контрола појачања"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Без самосталне контроле појачања"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Подизање"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Без подизања"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Појачало"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Без појачала"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "Подизање"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "Без подизања"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Аналогне слушалице"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Аналогни улаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Микрофон прикључне станице"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "Микрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Аналогни излаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Аналогне слушалице"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Аналогни моно излаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "Линија у"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Аналогни моно излаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "Аналогни стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "Дигитални стерео (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "Дигитални стерео (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Празан излаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Празан излаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Празан излаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Улаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Аналогни окружујући 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Аналогни моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Аналогни моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Аналогни моно"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Аналогни стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Аналогни стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Аналогни окружујући 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Аналогни окружујући 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Аналогни окружујући 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Аналогни окружујући 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Аналогни окружујући 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Аналогни окружујући 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Аналогни окружујући 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Аналогни окружујући 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Аналогни окружујући 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Аналогни окружујући 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Аналогни окружујући 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Дигитални стерео (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Дигитални окружујући 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Дигитални окружујући 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Дигитални окружујући 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Дигитални стерео (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Дигитални окружујући 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Двосмерни аналогни моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Двосмерни аналогни стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Двосмерни дигитални стерео (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Двосмерни аналогни стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "Празан излаз"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "Улаз"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() је вратио вредност која је необично велика: %lu бајтова (%lu "
+"ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+msgstr[1] ""
+"snd_pcm_avail() је вратио вредност која је необично велика: %lu бајтова (%lu "
+"ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+msgstr[2] ""
+"snd_pcm_avail() је вратио вредност која је необично велика: %lu бајтова (%lu "
+"ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() је вратио вредност која је необично велика: %li бајтова (%s"
+"%lu ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+msgstr[1] ""
+"snd_pcm_delay() је вратио вредност која је необично велика: %li бајтова (%s"
+"%lu ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+msgstr[2] ""
+"snd_pcm_delay() је вратио вредност која је необично велика: %li бајтова (%s"
+"%lu ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() је вратио вредност која је необично велика: %lu бајтова (%lu "
+"ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() је вратио вредност која је необично велика: %lu "
+"бајтова (%lu ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+msgstr[1] ""
+"snd_pcm_mmap_begin() је вратио вредност која је необично велика: %lu "
+"бајтова (%lu ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+msgstr[2] ""
+"snd_pcm_mmap_begin() је вратио вредност која је необично велика: %lu "
+"бајтова (%lu ms).\n"
+"Ово је највероватније грешка у „%s“ ALSA управљачком програму. Пријавите "
+"овај проблем ALSA програмерима."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "Аналогне слушалице"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/sr@latin.po b/po/sr@latin.po
new file mode 100644
index 0000000..6f56d86
--- /dev/null
+++ b/po/sr@latin.po
@@ -0,0 +1,641 @@
+# Serbian(Latin) translations for pipewire
+# Copyright (C) 2006 Lennart Poettering
+# This file is distributed under the same license as the pipewire package.
+# Igor Miletic (Igor Miletić) <grejigl-gnomeprevod@yahoo.ca>, 2009.
+# Miloš Komarčević <kmilos@gmail.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:55+0000\n"
+"Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n"
+"Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "Unutrašnji zvuk"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "Modem"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "Isključeno"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(neispravno)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "Ulaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Ulaz priključne stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "Mikrofon priključne stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "Ulaz priključne stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+msgid "Line In"
+msgstr "Linija u"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2714
+#: spa/plugins/alsa/acp/alsa-mixer.c:2798
+#: spa/plugins/bluez5/bluez5-device.c:1145
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "Mikrofon priključne stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "Spoljni mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "Unutrašnji mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "Radio"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "Video"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "Samostalna kontrola pojačanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "Bez samostalne kontrole pojačanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "Podizanje"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "Bez podizanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "Pojačalo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "Bez pojačala"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "Podizanje"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "Bez podizanja"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "Analogne slušalice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "Analogni ulaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "Mikrofon priključne stanice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "Analogni izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "Analogne slušalice"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "Analogni mono izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "Linija u"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "Analogni mono izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "Analogni stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "Digitalni stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "Digitalni stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Prazan izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Prazan izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Prazan izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "Ulaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Analogni okružujući 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "Analogni mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "Analogni mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "Analogni mono"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "Analogni stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "Analogni stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analogni okružujući 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analogni okružujući 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analogni okružujući 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analogni okružujući 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analogni okružujući 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analogni okružujući 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analogni okružujući 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analogni okružujući 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analogni okružujući 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analogni okružujući 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analogni okružujući 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digitalni stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digitalni okružujući 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digitalni okružujući 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digitalni okružujući 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digitalni stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digitalni okružujući 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Dvosmerni analogni mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Dvosmerni analogni stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Dvosmerni digitalni stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Dvosmerni analogni stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "Prazan izlaz"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "Ulaz"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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 vratio vrednost koja je neobično velika: %lu bajtova (%lu "
+"ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+msgstr[1] ""
+"snd_pcm_avail() je vratio vrednost koja je neobično velika: %lu bajtova (%lu "
+"ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+msgstr[2] ""
+"snd_pcm_avail() je vratio vrednost koja je neobično velika: %lu bajtova (%lu "
+"ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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 vratio vrednost koja je neobično velika: %li bajtova (%s"
+"%lu ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+msgstr[1] ""
+"snd_pcm_delay() je vratio vrednost koja je neobično velika: %li bajtova (%s"
+"%lu ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+msgstr[2] ""
+"snd_pcm_delay() je vratio vrednost koja je neobično velika: %li bajtova (%s"
+"%lu ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() je vratio vrednost koja je neobično velika: %lu bajtova (%lu "
+"ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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 vratio vrednost koja je neobično velika: %lu "
+"bajtova (%lu ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+msgstr[1] ""
+"snd_pcm_mmap_begin() je vratio vrednost koja je neobično velika: %lu "
+"bajtova (%lu ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+msgstr[2] ""
+"snd_pcm_mmap_begin() je vratio vrednost koja je neobično velika: %lu "
+"bajtova (%lu ms).\n"
+"Ovo je najverovatnije greška u „%s“ ALSA upravljačkom programu. Prijavite "
+"ovaj problem ALSA programerima."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "Analogne slušalice"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/sv.po b/po/sv.po
new file mode 100644
index 0000000..86bf701
--- /dev/null
+++ b/po/sv.po
@@ -0,0 +1,713 @@
+# Swedish translation for pipewire.
+# Copyright © 2008-2022 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.
+#
+# Termer:
+# input/output: ingång/utgång (det handlar om ljud)
+# latency: latens
+# delay: fördröjning
+# boost: öka
+# gain: förstärkning
+# channel map: kanalmappning
+# passthrough: genomströmning
+# och en hel del termer som inte översätts inom ljuddomänen, ex. surround.
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
+"issues\n"
+"POT-Creation-Date: 2022-10-20 15:27+0000\n"
+"PO-Revision-Date: 2022-09-16 12:58+0200\n"
+"Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n"
+"Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n"
+"Language: sv\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Poedit 3.1.1\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [flaggor]\n"
+" -h, --help Visa denna hjälp\n"
+" --version Visa version\n"
+" -c, --config Läs in konfig (Standard %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "PipeWire mediasystem"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Starta mediasystemet PipeWire"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Tunnel till %s/%s"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Attrapputgång"
+
+#: src/modules/module-pulse-tunnel.c:681
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Tunnel för %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:335
+msgid "Unknown device"
+msgstr "Okänd enhet"
+
+#: src/modules/module-zeroconf-discover.c:347
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s på %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:351
+#, c-format
+msgid "%s on %s"
+msgstr "%s på %s"
+
+#: src/tools/pw-cat.c:782
+#, 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 [flaggor] [<fil>|-]\n"
+" -h, --help Visa denna hjälp\n"
+" --version Visa version\n"
+" -v, --verbose Aktivera utförliga operationer\n"
+"\n"
+
+#: src/tools/pw-cat.c:789
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote Fjärrdemonnamn\n"
+" --media-type Sätt mediatyp (standard %s)\n"
+" --media-category Sätt mediakategori (standard %s)\n"
+" --media-role Sätt mediaroll (standard %s)\n"
+" --target Sätt nodmål (standard %s)\n"
+" 0 betyder länka inte\n"
+" --latency Sätt nodlatens (standard %s)\n"
+" Xenhet (enhet = s, ms, us, ns)\n"
+" eller direkta samplar (256)\n"
+" hastigheten är källfilens\n"
+" -P --properties Sätt nodegenskaper\n"
+"\n"
+
+#: src/tools/pw-cat.c:807
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Samplingsfrekvens (krävs för insp.) "
+"(standard %u)\n"
+" --channels Antal kanaler (krävs för insp.) "
+"(standard %u)\n"
+" --channel-map Kanalmappning\n"
+" en av: \"stereo\", "
+"\"surround-51\",... eller\n"
+" kommaseparerad lista av "
+"kanalnamn: t.ex. \"FL,FR\"\n"
+" --format Samplingsformat %s (krävs för insp.) "
+"(standard %s)\n"
+" --volume Strömvolym 0-1.0 (standard %.3f)\n"
+" -q --quality Omsamplarkvalitet (0 - 15) (standard "
+"%d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:824
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Uppspelningsläge\n"
+" -r, --record Inspelningsläge\n"
+" -m, --midi Midiläge\n"
+" -d, --dsd DSD-läge\n"
+"\n"
+
+#: src/tools/pw-cli.c:2250
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [flaggor] [kommando]\n"
+" -h, --help Visa denna hjälp\n"
+" --version Show version\n"
+" -d, --daemon Starta som demon (Standard false)\n"
+" -r, --remote Fjärrdemonnamn\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Professionellt ljud"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1237
+msgid "Off"
+msgstr "Av"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Ingång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Ingång för dockningsstation"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Mikrofon för dockningsstation"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Linje in för dockningsstation"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+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:1455
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Frontmikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Bakre mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Extern mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Intern mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c: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 "Automatisk förstärkningskontroll"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Ingen automatisk förstärkningskontroll"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Ökning"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Ingen ökning"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Förstärkare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Ingen förstärkare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Basökning"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Ingen basökning"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1461
+msgid "Speaker"
+msgstr "Högtalare"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Hörlurar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Analog ingång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Dockmikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Headset-mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Analog utgång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Hörlurar 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Monoutgång för hörlurar"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Linje ut"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Analog monoutgång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Högtalare"
+
+#: 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 "Digital utgång (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Digital ingång (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Multikanalingång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Multikanalutgång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Spelutgång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Chatt-utgång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Chatt-ingång"
+
+#: 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:4471
+msgid "Analog Mono"
+msgstr "Analog mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Analog mono (vänster)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Analog mono (höger)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Analog stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Mono"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+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:1443
+msgid "Headset"
+msgstr "Headset"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Högtalartelefon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Multikanal"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Analog surround 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Analog surround 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Analog surround 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Analog surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Analog surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Analog surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Analog surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Analog surround 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Analog surround 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Analog surround 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Analog surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Digital stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digital surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digital surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digital surround 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Digital stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digital surround 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Chatt"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Spel"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Analog mono duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Analog stereo duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digital stereo duplex (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Multikanalduplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Stereo duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Mono Chatt + 7.1 Surround"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "%s-utgång"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, 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
+#, 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() returnerade ett värde som är exceptionellt stort: %lu byte "
+"(%lu ms).\n"
+"Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera "
+"problemet till ALSA-utvecklarna."
+msgstr[1] ""
+"snd_pcm_avail() returnerade ett värde som är exceptionellt stort: %lu byte "
+"(%lu ms).\n"
+"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
+#, 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() returnerade ett värde som är exceptionellt stort: %li byte "
+"(%s%lu ms).\n"
+"Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera "
+"problemet till ALSA-utvecklarna."
+msgstr[1] ""
+"snd_pcm_delay() returnerade ett värde som är exceptionellt stort: %li byte "
+"(%s%lu ms).\n"
+"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
+#, 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() returnerade konstiga värden: fördröjningen %lu är "
+"mindre än tillgängliga %lu.\n"
+"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
+#, 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() returnerade ett värde som är exceptionellt stort: %lu "
+"byte (%lu ms).\n"
+"Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera "
+"problemet till ALSA-utvecklarna."
+msgstr[1] ""
+"snd_pcm_mmap_begin() returnerade ett värde som är exceptionellt stort: %lu "
+"byte (%lu ms).\n"
+"Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera "
+"problemet till ALSA-utvecklarna."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(ogiltig)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Inbyggt ljud"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1248
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Audio gateway (A2DP-källa & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1273
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "High fidelity-uppspelning (A2DP-utgång, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1276
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "High fidelity duplex (A2DP-källa/utgång, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1284
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "High fidelity-uppspelning (A2DP-utgång)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1286
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "High fidelity duplex (A2DP-källa/utgång)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1323
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "High fidelity-uppspelning (BAP-utgång, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1327
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "High fidelity-ingång (BAP-källa, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1331
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "High fidelity duplex (BAP-källa/utgång, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1360
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Headset-huvudenhet (HSP/HFP, kodek %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1365
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Headset-huvudenhet (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1444
+#: spa/plugins/bluez5/bluez5-device.c:1449
+#: spa/plugins/bluez5/bluez5-device.c:1456
+#: spa/plugins/bluez5/bluez5-device.c:1462
+#: spa/plugins/bluez5/bluez5-device.c:1468
+#: spa/plugins/bluez5/bluez5-device.c:1474
+#: spa/plugins/bluez5/bluez5-device.c:1480
+#: spa/plugins/bluez5/bluez5-device.c:1486
+#: spa/plugins/bluez5/bluez5-device.c:1492
+msgid "Handsfree"
+msgstr "Handsfree"
+
+#: spa/plugins/bluez5/bluez5-device.c:1450
+msgid "Handsfree (HFP)"
+msgstr "Handsfree (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1467
+msgid "Headphone"
+msgstr "Hörlurar"
+
+#: spa/plugins/bluez5/bluez5-device.c:1473
+msgid "Portable"
+msgstr "Bärbar"
+
+#: spa/plugins/bluez5/bluez5-device.c:1479
+msgid "Car"
+msgstr "Bil"
+
+#: spa/plugins/bluez5/bluez5-device.c:1485
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1499
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
diff --git a/po/ta.po b/po/ta.po
new file mode 100644
index 0000000..d40b047
--- /dev/null
+++ b/po/ta.po
@@ -0,0 +1,647 @@
+# translation of pipewire.master-tx.ta.po to Tamil
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# I. Felix <ifelix@redhat.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.ta\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:56+0000\n"
+"Last-Translator: I. Felix <ifelix@redhat.com>\n"
+"Language-Team: Tamil <fedora-trans-ta@redhat.com>\n"
+"Language: ta\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\\n\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "உட்புற ஆடியோ"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "மாதிரி"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "ஆஃப்"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(தவறான)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "உள்ளீடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "டாக்கிங் ஸ்டேஷன் உள்ளீடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "டாக்கிங் ஸ்டேஷன் மைக்ரோஃபோன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "டாக்கிங் ஸ்டேஷன் உள்ளீடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "மைக்ரோஃபோன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "டாக்கிங் ஸ்டேஷன் மைக்ரோஃபோன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "மைக்ரோஃபோன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "வெளியார்ந்த மைக்ரோஃபோன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "உட்புற மைக்ரோஃபோன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "ரேடியோ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "வீடியோ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "தானியக்க லாப கட்டுப்பாடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "தானியக்க லாப கட்டுப்பாடு எதுவுமில்லை"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "பூஸ்ட்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "பூஸ்ட் இல்லை"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "ஆம்பிளிஃபையர்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "ஆம்ப்ளிஃபையர் இல்லை"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "பூஸ்ட்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "பூஸ்ட் இல்லை"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "அனலாக் ஹெட்ஃபோன்கள்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "அனலாக் உள்ளிடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "டாக்கிங் ஸ்டேஷன் மைக்ரோஃபோன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "மைக்ரோஃபோன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "அனலாக் வெளிப்பாடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "அனலாக் ஹெட்ஃபோன்கள்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "அனலாக் மோனோ வெளிப்பாடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "லைன்இன்"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "அனலாக் மோனோ வெளிப்பாடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "அனலாக் ஸ்டிரியோ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "Digital Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "Digital Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "பூஜ்ஜிய வெளிப்பாடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "பூஜ்ஜிய வெளிப்பாடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "பூஜ்ஜிய வெளிப்பாடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "உள்ளீடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "Analog Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "அனலாக் மோனோ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "அனலாக் மோனோ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "அனலாக் மோனோ"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "அனலாக் ஸ்டிரியோ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "மோனோ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "அனலாக் ஸ்டிரியோ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "Analog Surround 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "Analog Surround 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "Analog Surround 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "Analog Surround 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "Analog Surround 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "Analog Surround 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "Analog Surround 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "Analog Surround 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "Analog Surround 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "Analog Surround 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "Analog Surround 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "Digital Stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Digital Surround 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Digital Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Digital Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "Digital Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Digital Surround 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "Analog Mono Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "Analog Stereo Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Digital Stereo Duplex (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "Analog Stereo Duplex"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "பூஜ்ஜிய வெளிப்பாடு"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "உள்ளீடு"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() 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[1] ""
+"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."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() 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[1] ""
+"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."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() 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."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+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() 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[1] ""
+"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."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "அனலாக் ஹெட்ஃபோன்கள்"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/te.po b/po/te.po
new file mode 100644
index 0000000..1aea590
--- /dev/null
+++ b/po/te.po
@@ -0,0 +1,624 @@
+# translation of pipewire.master-tx.te.po to Telugu
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Krishna Babu K <kkrothap@redhat.com>, 2009, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire.master-tx.te\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2012-01-30 09:56+0000\n"
+"Last-Translator: Krishna Babu K <kkrothap@redhat.com>\n"
+"Language-Team: Telugu <en@li.org>\n"
+"Language: te\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "అంతర్గత ఆడియో"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "మోడెమ్"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "ఆఫ్"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(చెల్లని)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "ఇన్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "డాకింగ్ స్టేషన్ ఇన్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+#, fuzzy
+msgid "Docking Station Microphone"
+msgstr "డాకింగ్ స్టేషన్ మైక్రోఫోన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+#, fuzzy
+msgid "Docking Station Line In"
+msgstr "డాకింగ్ స్టేషన్ ఇన్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "మైక్రోఫోన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+#, fuzzy
+msgid "Front Microphone"
+msgstr "డాకింగ్ స్టేషన్ మైక్రోఫోన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+#, fuzzy
+msgid "Rear Microphone"
+msgstr "మైక్రోఫోన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "బహిర్గత మైక్రోఫోన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "అంతర్గత మైక్రోఫోన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "రేడియో"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "వీడియో"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "స్వయంచాలకంగా పొందు నియంత్రణ"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "స్వయంచాలకంగా పొందు ఏ నియంత్రణ లేదు"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "బూస్ట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "బూస్ట్ లేదు"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "ఎంప్లిఫైర్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "ఎంప్లిఫైర్ లేదు"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+#, fuzzy
+msgid "Bass Boost"
+msgstr "బూస్ట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+#, fuzzy
+msgid "No Bass Boost"
+msgstr "బూస్ట్ లేదు"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "ఎనలాగ్ హెడ్‌ఫోన్స్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "ఎనలాగ్ యిన్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "డాకింగ్ స్టేషన్ మైక్రోఫోన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+#, fuzzy
+msgid "Headset Microphone"
+msgstr "మైక్రోఫోన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "ఎనలాగ్ అవుట్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "ఎనలాగ్ హెడ్‌ఫోన్స్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+#, fuzzy
+msgid "Headphones Mono Output"
+msgstr "ఎనలాగ్ మోనో అవుట్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+#, fuzzy
+msgid "Line Out"
+msgstr "లైన్-యిన్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "ఎనలాగ్ మోనో అవుట్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+#, fuzzy
+msgid "Speakers"
+msgstr "ఎనలాగ్ స్టీరియో"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+#, fuzzy
+msgid "Digital Output (S/PDIF)"
+msgstr "డిజిటల్ స్టీరియో (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+#, fuzzy
+msgid "Digital Input (S/PDIF)"
+msgstr "డిజిటల్ స్టీరియో (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+#, fuzzy
+msgid "Multichannel Output"
+msgstr "Null అవుట్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+#, fuzzy
+msgid "Game Output"
+msgstr "Null అవుట్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+#, fuzzy
+msgid "Chat Output"
+msgstr "Null అవుట్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "ఇన్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "ఎనలాగ్ సరౌండ్ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "ఎనలాగ్ మోనో"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "ఎనలాగ్ మోనో"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "ఎనలాగ్ మోనో"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "ఎనలాగ్ స్టీరియో"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "మోనో"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "ఎనలాగ్ స్టీరియో"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "ఎనలాగ్ సరౌండ్ 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "ఎనలాగ్ సరౌండ్ 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "ఎనలాగ్ సరౌండ్ 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "ఎనలాగ్ సరౌండ్ 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "ఎనలాగ్ సరౌండ్ 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "ఎనలాగ్ సరౌండ్ 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "ఎనలాగ్ సరౌండ్ 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "ఎనలాగ్ సరౌండ్ 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "ఎనలాగ్ సరౌండ్ 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "ఎనలాగ్ సరౌండ్ 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "ఎనలాగ్ సరౌండ్ 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "డిజిటల్ స్టీరియో (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "డిజిటల్ సరౌండ్ 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "డిజిటల్ సరౌండ్ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+#, fuzzy
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "డిజిటల్ సరౌండ్ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "డిజిటల్ స్టీరియో (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+#, fuzzy
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "డిజిటల్ సరౌండ్ 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "ఎనలాగ్ మోనో డుప్లెక్స్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "ఎనలాగ్ స్టీరియో డుప్లెక్స్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "డిజిటల్ స్టీరియో డుప్లెక్స్ (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+#, fuzzy
+msgid "Stereo Duplex"
+msgstr "ఎనలాగ్ స్టీరియో డుప్లెక్స్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, fuzzy, c-format
+msgid "%s Output"
+msgstr "Null అవుట్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, fuzzy, c-format
+msgid "%s Input"
+msgstr "ఇన్పుట్"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() అనునది పెద్ద విలువను యిచ్చినది: %lu bytes (%lu ms).\n"
+"సాదారణంగా యిది ALSA డ్రైవర్ '%s' నందలి బగ్ కావచ్చును. దయచేసి దీనిని ALSA అభివృద్ది కారులకు "
+"నివేదించుము."
+msgstr[1] ""
+"snd_pcm_avail() అనునది పెద్ద విలువను యిచ్చినది: %lu bytes (%lu ms).\n"
+"సాదారణంగా యిది ALSA డ్రైవర్ '%s' నందలి బగ్ కావచ్చును. దయచేసి దీనిని ALSA అభివృద్ది కారులకు "
+"నివేదించుము."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() అనునది పెద్ద విలువను యిచ్చినది: %li bytes (%s%lu ms).\n"
+"సాదారణంగా యిది ALSA డ్రైవర్ '%s' నందు బగ్ కావచ్చును . దయచేసి దీనిని ALSA అభివృద్దికారులక "
+"నివేదించుము."
+msgstr[1] ""
+"snd_pcm_delay() అనునది పెద్ద విలువను యిచ్చినది: %li bytes (%s%lu ms).\n"
+"సాదారణంగా యిది ALSA డ్రైవర్ '%s' నందు బగ్ కావచ్చును . దయచేసి దీనిని ALSA అభివృద్దికారులక "
+"నివేదించుము."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, fuzzy, 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() అనునది పెద్ద విలువను యిచ్చినది: %lu bytes (%lu ms).\n"
+"సాదారణంగా యిది ALSA డ్రైవర్ '%s' నందలి బగ్ కావచ్చును. దయచేసి దీనిని ALSA అభివృద్ది కారులకు "
+"నివేదించుము."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, fuzzy, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() అనునది పెద్ద విలువను యిచ్చినది: %lu bytes (%lu ms).\n"
+"సాదారణంగా యిది ALSA డ్రైవర్ '%s'నందలి బగ్ కావచ్చును. దయచేసి దీనిని ALSA అభివృద్దికారులను నివేదించండి."
+msgstr[1] ""
+"snd_pcm_mmap_begin() అనునది పెద్ద విలువను యిచ్చినది: %lu bytes (%lu ms).\n"
+"సాదారణంగా యిది ALSA డ్రైవర్ '%s'నందలి బగ్ కావచ్చును. దయచేసి దీనిని ALSA అభివృద్దికారులను నివేదించండి."
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+#, fuzzy
+msgid "Headphone"
+msgstr "ఎనలాగ్ హెడ్‌ఫోన్స్"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr ""
diff --git a/po/tr.po b/po/tr.po
new file mode 100644
index 0000000..766f48f
--- /dev/null
+++ b/po/tr.po
@@ -0,0 +1,691 @@
+# Turkish translation for PipeWire.
+# Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER
+# This file is distributed under the same license as the PipeWire package.
+# Necdet Yücel <necdetyucel@gmail.com>, 2014.
+# Kaan Özdinçer <kaanozdincer@gmail.com>, 2014.
+# Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017.
+# Oğuz Ersen <oguz@ersen.moe>, 2021-2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire master\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2022-06-30 12:50+0200\n"
+"PO-Revision-Date: 2022-10-23 10:40+0300\n"
+"Last-Translator: Oğuz Ersen <oguz@ersen.moe>\n"
+"Language-Team: Turkish <tr>\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Weblate 4.4.2\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [seçenekler]\n"
+" -h, --help Bu yardımı göster\n"
+" --version Sürümü göster\n"
+" -c, --config Yapılandırmayı yükle (Öntanımlı %s)\n"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:180
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:180
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "%s/%s tüneli"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Temsili Çıkış"
+
+#: src/modules/module-pulse-tunnel.c:662
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "%s@%s için tünel"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Bilinmeyen aygıt"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s, %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s, %s"
+
+#: src/tools/pw-cat.c:784
+#, c-format
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [seçenekler] [<dosya>|-]\n"
+" -h, --help Bu yardımı göster\n"
+" --version Sürümü göster\n"
+" -v, --verbose Ayrıntılı işlemleri etkinleştir\n"
+"\n"
+
+#: src/tools/pw-cat.c:791
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote Uzak arka plan programı adı\n"
+" --media-type Ortam türünü ayarla (öntanımlı %s)\n"
+" --media-category Ortam kategorisini ayarla (öntanımlı "
+"%s)\n"
+" --media-role Ortam rolünü ayarla (öntanımlı %s)\n"
+" --target Düğüm hedefini ayarla (öntanımlı "
+"%s)\n"
+" 0, bağlanmayacağı anlamına gelir\n"
+" --latency Düğüm gecikmesini ayarla (öntanımlı "
+"%s)\n"
+" Xbirim (birim = s, ms, us, ns)\n"
+" veya doğrudan örneklemeler (256)\n"
+" oran kaynak dosyadan biridir\n"
+" -P --properties Düğüm özelliklerini ayarla\n"
+"\n"
+
+#: src/tools/pw-cat.c:809
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate Örnekleme oranı (kayıt için gerekli) "
+"(öntanımlı %u)\n"
+" --channels Kanal sayısı (kayıt için gerekli) "
+"(öntanımlı %u)\n"
+" --channel-map Kanal haritası\n"
+" şunlardan biri: \"stereo\", "
+"\"surround-51\",... veya\n"
+" kanal adlarının virgülle "
+"ayrılmış listesi: örn. \"FL,FR\"\n"
+" --format Örnekleme biçimi %s (kayıt için "
+"gerekli) (öntanımlı %s)\n"
+" --volume Akış ses seviyesi 0-1.0 (öntanımlı "
+"%.3f)\n"
+" -q --quality Yeniden örnekleyici kalitesi (0 - "
+"15) (öntanımlı %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:826
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback Çalma modu\n"
+" -r, --record Kayıt modu\n"
+" -m, --midi Midi modu\n"
+" -d, --dsd DSD modu\n"
+"\n"
+
+#: src/tools/pw-cli.c:2250
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [seçenekler] [komut]\n"
+" -h, --help Bu yardımı göster\n"
+" --version Sürümü göster\n"
+" -d, --daemon Arka plan programı olarak başlat "
+"(Öntanımlı olarak yanlış)\n"
+" -r, --remote Uzak arka plan programı adı\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Profesyonel Ses"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1236
+msgid "Off"
+msgstr "Kapalı"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Giriş"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Yerleştirme İstasyonu Girişi"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Yerleştirme İstasyonu Mikrofonu"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Yerleştirme İstasyonu Hat Girişi"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2656
+#: spa/plugins/alsa/acp/alsa-mixer.c:2747
+msgid "Line In"
+msgstr "Hat Girişi"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1454
+msgid "Microphone"
+msgstr "Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Ön Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Arka Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Harici Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Dahili Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "Radyo"
+
+#: 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 "Otomatik Kazanç Denetimi"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Otomatik Kazanç Denetimi Yok"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Artır"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Artırma Yok"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Yükseltici"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Yükseltici Yok"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Bas Artır"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Bas Artırma Yok"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1460
+msgid "Speaker"
+msgstr "Hoparlör"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Kulaklık"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Analog Giriş"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Yapışık Mikrofon"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Mikrofonlu Kulaklık"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Analog Çıkış"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Kulaklık 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Kulaklık Tek Kanallı Çıkış"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Hat Çıkışı"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Analog Tek Kanallı Çıkış"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Hoparlörler"
+
+#: 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 "Sayısal Çıkış (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Sayısal Giriş (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Çok Kanallı Giriş"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Çok Kanallı Çıkış"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Oyun Çıkışı"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Sohbet Çıkışı"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Sohbet Girişi"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Sanal Çevresel Ses 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Analog Tek Kanallı"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Analog Tek Kanallı (Sol)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Analog Tek Kanallı (Sağ)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Analog Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Tek Kanallı"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+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:1442
+msgid "Headset"
+msgstr "Kulaklık"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Hoparlör"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Çok kanallı"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Analog Çevresel Ses 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Analog Çevresel Ses 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Analog Çevresel Ses 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Analog Çevresel Ses 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Analog Çevresel Ses 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Analog Çevresel Ses 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Analog Çevresel Ses 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Analog Çevresel Ses 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Analog Çevresel Ses 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Analog Çevresel Ses 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Analog Çevresel Ses 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Sayısal Stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Sayısal Çevresel Ses 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Sayısal Çevresel Ses 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Sayısal Çevresel Ses 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Sayısal Stereo (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Sayısal Çevresel Ses 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Sohbet"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Oyun"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Analog Tek Kanallı İkili"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Analog İkili Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Sayısal İkili Stereo (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Çok Kanallı İkili"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "İkili Stereo"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Tek Kanallı Sohbet + 7.1 Çevresel Ses"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "%s Çıkışı"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "%s Girişi"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"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() son derece büyük bir değer döndürdü: %lu bayt (%lu ms).\n"
+"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
+"geliştiricilerine bildirin."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1253
+#, 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() son derece büyük bir değer döndürdü: %li bayt (%s%lu ms).\n"
+"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
+"geliştiricilerine bildirin."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1300
+#, 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() garip değerler döndü: gecikme %lu kazançtan %lu daha "
+"azdır.\n"
+"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
+"geliştiricilerine bildirin."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1343
+#, 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() son derece büyük bir değer döndürdü: %lu bayt (%lu "
+"ms).\n"
+"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
+"geliştiricilerine bildirin."
+
+#: spa/plugins/alsa/acp/channelmap.h:457
+msgid "(invalid)"
+msgstr "(geçersiz)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Dahili Ses"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Modem"
+
+#: spa/plugins/bluez5/bluez5-device.c:1247
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Ses Geçidi (A2DP Kaynak & HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1272
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1275
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1283
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1285
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1322
+#, c-format
+msgid "High Fidelity Playback (BAP Sink, codec %s)"
+msgstr "Yüksek Kaliteli Çalma (BAP Alıcı, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1326
+#, c-format
+msgid "High Fidelity Input (BAP Source, codec %s)"
+msgstr "Yüksek Kaliteli Giriş (BAP Kaynak, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1330
+#, c-format
+msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
+msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1359
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Kulaklık Ana Birimi (HSP/HFP, çözücü %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1364
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Kulaklık Ana Birimi (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1443
+#: spa/plugins/bluez5/bluez5-device.c:1448
+#: spa/plugins/bluez5/bluez5-device.c:1455
+#: spa/plugins/bluez5/bluez5-device.c:1461
+#: spa/plugins/bluez5/bluez5-device.c:1467
+#: spa/plugins/bluez5/bluez5-device.c:1473
+#: spa/plugins/bluez5/bluez5-device.c:1479
+#: spa/plugins/bluez5/bluez5-device.c:1485
+#: spa/plugins/bluez5/bluez5-device.c:1491
+msgid "Handsfree"
+msgstr "Ahizesiz"
+
+#: spa/plugins/bluez5/bluez5-device.c:1449
+msgid "Handsfree (HFP)"
+msgstr "Ahizesiz (HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1466
+msgid "Headphone"
+msgstr "Kulaklık"
+
+#: spa/plugins/bluez5/bluez5-device.c:1472
+msgid "Portable"
+msgstr "Taşınabilir"
+
+#: spa/plugins/bluez5/bluez5-device.c:1478
+msgid "Car"
+msgstr "Araba"
+
+#: spa/plugins/bluez5/bluez5-device.c:1484
+msgid "HiFi"
+msgstr "Yüksek Kalite"
+
+#: spa/plugins/bluez5/bluez5-device.c:1490
+msgid "Phone"
+msgstr "Telefon"
+
+#: spa/plugins/bluez5/bluez5-device.c:1497
+msgid "Bluetooth"
+msgstr "Bluetooth"
+
+#: spa/plugins/bluez5/bluez5-device.c:1498
+msgid "Bluetooth (HFP)"
+msgstr "Bluetooth (HFP)"
+
+#~ msgid "PipeWire Media System"
+#~ msgstr "PipeWire Ortam Sistemi"
+
+#~ msgid "Start the PipeWire Media System"
+#~ msgstr "PipeWire Ortam Sistemini Başlat"
diff --git a/po/uk.po b/po/uk.po
new file mode 100644
index 0000000..a8d04cb
--- /dev/null
+++ b/po/uk.po
@@ -0,0 +1,714 @@
+# Copyright (C) 2009 Free Software Foundation, Inc.
+# This file is distributed under the same license as the pipewire package.
+#
+# Yuri Chornoivan <yurchor@ukr.net>, 2009-2021, 2022.
+msgid ""
+msgstr ""
+"Project-Id-Version: pipewire\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/issue"
+"s\n"
+"POT-Creation-Date: 2022-05-20 15:26+0000\n"
+"PO-Revision-Date: 2022-06-18 13:07+0300\n"
+"Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n"
+"Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<"
+"=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Lokalize 20.12.0\n"
+
+#: src/daemon/pipewire.c:46
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [параметри]\n"
+" -h, --help вивести довідку\n"
+" --version вивести дані щодо версії\n"
+" -c, --config завантажити налаштування (типово, "
+"%s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "Мультимедійна система PipeWire"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "Запустити мультимедійну систему PipeWire"
+
+#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:183
+#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:183
+#, c-format
+msgid "Tunnel to %s/%s"
+msgstr "Тунель до %s/%s"
+
+#: src/modules/module-fallback-sink.c:51
+msgid "Dummy Output"
+msgstr "Фіктивний вихід"
+
+#: src/modules/module-pulse-tunnel.c:639
+#, c-format
+msgid "Tunnel for %s@%s"
+msgstr "Тунель для %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:332
+msgid "Unknown device"
+msgstr "Невідомий пристрій"
+
+#: src/modules/module-zeroconf-discover.c:344
+#, c-format
+msgid "%s on %s@%s"
+msgstr "%s на %s@%s"
+
+#: src/modules/module-zeroconf-discover.c:348
+#, c-format
+msgid "%s on %s"
+msgstr "%s на %s"
+
+#: src/tools/pw-cat.c:872
+#, c-format
+#| msgid ""
+#| "%s [options] <file>\n"
+#| " -h, --help Show this help\n"
+#| " --version Show version\n"
+#| " -v, --verbose Enable verbose operations\n"
+#| "\n"
+msgid ""
+"%s [options] [<file>|-]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [параметри] [<файл>|-]\n"
+" -h, --help вивести довідку\n"
+" --version вивести дані щодо версії\n"
+" -v, --verbose ввімкнути відображення докладної "
+"інформації\n"
+"\n"
+
+#: src/tools/pw-cat.c:879
+#, c-format
+#| msgid ""
+#| " -R, --remote Remote daemon name\n"
+#| " --media-type Set media type (default %s)\n"
+#| " --media-category Set media category (default %s)\n"
+#| " --media-role Set media role (default %s)\n"
+#| " --target Set node target (default %s)\n"
+#| " 0 means don't link\n"
+#| " --latency Set node latency (default %s)\n"
+#| " Xunit (unit = s, ms, us, ns)\n"
+#| " or direct samples (256)\n"
+#| " the rate is the one of the "
+#| "source file\n"
+#| "\n"
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" -P --properties Set node properties\n"
+"\n"
+msgstr ""
+" -R, --remote назва віддаленої фонової служби\n"
+" --media-type встановити тип мультимедіа (типово, "
+"%s)\n"
+" --media-category встановити категорію мультимедіа "
+"(типово, %s)\n"
+" --media-role встановити роль мультимедіа (типово, "
+"%s)\n"
+" --target встановити ціль вузла (типово, %s)\n"
+" 0 — не пов'язувати\n"
+" --latency встановити затримку вузла (типово, "
+"%s)\n"
+" Xодиниця (одиниця = s, ms, us, "
+"ns)\n"
+" або безпосередні семпли (256)\n"
+" частота — частота з файла джерела\n"
+" -P --properties встановити властивості вузла\n"
+"\n"
+
+#: src/tools/pw-cat.c:897
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate частота дискретизації (потрібна для "
+"запису) (типово, %u)\n"
+" --channels кількість каналів (потрібна для "
+"запису) (типово, %u)\n"
+" --channel-map карта каналів\n"
+" одне з таких значень: \"stereo"
+"\", \"surround-51\",... або\n"
+" список каналів, відокремлених "
+"комами; приклад: \"FL,FR\"\n"
+" --format формат семплу %s (потрібен для "
+"запису) (типово, %s)\n"
+" --volume гучність потоку 0-1.0 (типово, "
+"%.3f)\n"
+" -q --quality якість зміни дискретизації (0 - 15) "
+"(типово, %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:914
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+" -d, --dsd DSD mode\n"
+"\n"
+msgstr ""
+" -p, --playback режим відтворення\n"
+" -r, --record режим запису\n"
+" -m, --midi режим MIDI\n"
+" -d, --dsd режим DSD\n"
+"\n"
+
+#: src/tools/pw-cli.c:3139
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [параметри] [команда]\n"
+" -h, --help вивести довідку\n"
+" --version вивести дані щодо версії\n"
+" -d, --daemon запустити як фонову службу (типово, "
+"false)\n"
+" -r, --remote назва віддаленої фонової служби\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:321
+msgid "Pro Audio"
+msgstr "Професійний звук"
+
+#: spa/plugins/alsa/acp/acp.c:444 spa/plugins/alsa/acp/alsa-mixer.c:4648
+#: spa/plugins/bluez5/bluez5-device.c:1161
+msgid "Off"
+msgstr "Вимкнено"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2652
+msgid "Input"
+msgstr "Вхід"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2653
+msgid "Docking Station Input"
+msgstr "Вхідний канал док-станції"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2654
+msgid "Docking Station Microphone"
+msgstr "Мікрофон док-станції"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2655
+msgid "Docking Station Line In"
+msgstr "Лінійний вхід док-станції"
+
+#: 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:2657
+#: spa/plugins/alsa/acp/alsa-mixer.c:2741
+#: spa/plugins/bluez5/bluez5-device.c:1330
+msgid "Microphone"
+msgstr "Мікрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2658
+#: spa/plugins/alsa/acp/alsa-mixer.c:2742
+msgid "Front Microphone"
+msgstr "Передній мікрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2659
+#: spa/plugins/alsa/acp/alsa-mixer.c:2743
+msgid "Rear Microphone"
+msgstr "Задній мікрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2660
+msgid "External Microphone"
+msgstr "Зовнішній мікрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2661
+#: spa/plugins/alsa/acp/alsa-mixer.c:2745
+msgid "Internal Microphone"
+msgstr "Вбудований мікрофон"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2662
+#: spa/plugins/alsa/acp/alsa-mixer.c:2748
+msgid "Radio"
+msgstr "Радіо"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2663
+#: spa/plugins/alsa/acp/alsa-mixer.c:2749
+msgid "Video"
+msgstr "Відео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2664
+msgid "Automatic Gain Control"
+msgstr "Автоматичне керування підсиленням"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2665
+msgid "No Automatic Gain Control"
+msgstr "Без автоматичного керування підсиленням"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2666
+msgid "Boost"
+msgstr "Підсилення"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2667
+msgid "No Boost"
+msgstr "Без пісилення"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2668
+msgid "Amplifier"
+msgstr "Підсилювач"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2669
+msgid "No Amplifier"
+msgstr "Без підсилювача"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2670
+msgid "Bass Boost"
+msgstr "Підсилення басів"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2671
+msgid "No Bass Boost"
+msgstr "Без підсилення"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2672
+#: spa/plugins/bluez5/bluez5-device.c:1335
+msgid "Speaker"
+msgstr "Динамік"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2673
+#: spa/plugins/alsa/acp/alsa-mixer.c:2751
+msgid "Headphones"
+msgstr "Аналогові навушники"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2740
+msgid "Analog Input"
+msgstr "Аналогових вхід"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2744
+msgid "Dock Microphone"
+msgstr "Мікрофон док-станції"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2746
+msgid "Headset Microphone"
+msgstr "Мікрофон гарнітури"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2750
+msgid "Analog Output"
+msgstr "Аналогове відтворення"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2752
+msgid "Headphones 2"
+msgstr "Навушники 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2753
+msgid "Headphones Mono Output"
+msgstr "Моно-вихід навушників"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2754
+msgid "Line Out"
+msgstr "Лінійний вихід"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2755
+msgid "Analog Mono Output"
+msgstr "Аналоговий моно-вихід"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2756
+msgid "Speakers"
+msgstr "Акустичні колонки"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2757
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2758
+msgid "Digital Output (S/PDIF)"
+msgstr "Цифровий вихід (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2759
+msgid "Digital Input (S/PDIF)"
+msgstr "Цифровий вхід (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2760
+msgid "Multichannel Input"
+msgstr "Багатоканальний вхід"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2761
+msgid "Multichannel Output"
+msgstr "Багатоканальний вихід"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2762
+msgid "Game Output"
+msgstr "Ігровий вихід"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2763
+#: spa/plugins/alsa/acp/alsa-mixer.c:2764
+msgid "Chat Output"
+msgstr "Вихід спілкування"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2765
+msgid "Chat Input"
+msgstr "Вхід спілкування"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2766
+msgid "Virtual Surround 7.1"
+msgstr "Віртуальний об'ємний звук 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4471
+msgid "Analog Mono"
+msgstr "Аналогове моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4472
+msgid "Analog Mono (Left)"
+msgstr "Аналогове моно (лівий)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4473
+msgid "Analog Mono (Right)"
+msgstr "Аналогове моно (правий)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4474
+#: spa/plugins/alsa/acp/alsa-mixer.c:4482
+#: spa/plugins/alsa/acp/alsa-mixer.c:4483
+msgid "Analog Stereo"
+msgstr "Аналогове стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4475
+msgid "Mono"
+msgstr "Моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4476
+msgid "Stereo"
+msgstr "Стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4484
+#: spa/plugins/alsa/acp/alsa-mixer.c:4642
+#: spa/plugins/bluez5/bluez5-device.c:1320
+msgid "Headset"
+msgstr "Гарнітура"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4485
+#: spa/plugins/alsa/acp/alsa-mixer.c:4643
+msgid "Speakerphone"
+msgstr "Гучномовець"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4486
+#: spa/plugins/alsa/acp/alsa-mixer.c:4487
+msgid "Multichannel"
+msgstr "Багатоканальний"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4488
+msgid "Analog Surround 2.1"
+msgstr "Аналоговий об'ємний 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4489
+msgid "Analog Surround 3.0"
+msgstr "Аналоговий об'ємний 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4490
+msgid "Analog Surround 3.1"
+msgstr "Аналоговий об'ємний 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4491
+msgid "Analog Surround 4.0"
+msgstr "Аналоговий об'ємний 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4492
+msgid "Analog Surround 4.1"
+msgstr "Аналоговий об'ємний 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4493
+msgid "Analog Surround 5.0"
+msgstr "Аналоговий об'ємний 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4494
+msgid "Analog Surround 5.1"
+msgstr "Аналоговий об'ємний 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4495
+msgid "Analog Surround 6.0"
+msgstr "Аналоговий об'ємний 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4496
+msgid "Analog Surround 6.1"
+msgstr "Аналоговий об'ємний 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4497
+msgid "Analog Surround 7.0"
+msgstr "Аналоговий об'ємний 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4498
+msgid "Analog Surround 7.1"
+msgstr "Аналоговий об'ємний 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4499
+msgid "Digital Stereo (IEC958)"
+msgstr "Цифрове стерео (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4500
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "Цифровий об’ємний 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4501
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "Цифровий об’ємний 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4502
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "Цифровий об’ємний 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4503
+msgid "Digital Stereo (HDMI)"
+msgstr "Цифровий стерео (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4504
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "Цифровий об’ємний 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4505
+msgid "Chat"
+msgstr "Чат"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4506
+msgid "Game"
+msgstr "Гра"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4640
+msgid "Analog Mono Duplex"
+msgstr "Аналогове двобічне моно"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4641
+msgid "Analog Stereo Duplex"
+msgstr "Аналогове двобічне стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4644
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "Цифрове двобічне стерео (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4645
+msgid "Multichannel Duplex"
+msgstr "Багатоканальний двобічний"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4646
+msgid "Stereo Duplex"
+msgstr "Двобічне стерео"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4647
+msgid "Mono Chat + 7.1 Surround"
+msgstr "Моно, спілкування + об'ємний 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4754
+#, c-format
+msgid "%s Output"
+msgstr "%s-вихід"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4761
+#, c-format
+msgid "%s Input"
+msgstr "%s-вхід"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Функція snd_pcm_avail() повернула винятково велике значення: %lu байт (%lu "
+"мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+msgstr[1] ""
+"Функція snd_pcm_avail() повернула винятково велике значення: %lu байти (%lu "
+"мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+msgstr[2] ""
+"Функція snd_pcm_avail() повернула винятково велике значення: %lu байтів (%lu "
+"мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1239
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Функція snd_pcm_delay() повернула винятково велике значення: %li байт (%s%lu "
+"мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+msgstr[1] ""
+"Функція snd_pcm_delay() повернула винятково велике значення: %li байти (%s"
+"%lu мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+msgstr[2] ""
+"Функція snd_pcm_delay() повернула винятково велике значення: %li байтів (%s"
+"%lu мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1286
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() повернуто дивні значення: затримка %lu є меншою за "
+"доступну, %lu.\n"
+"Ймовірно, це пов’язано з помилкою у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+
+#: spa/plugins/alsa/acp/alsa-util.c:1329
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"Функція snd_pcm_mmap_begin() повернула винятково велике значення: %lu байт "
+"(%lu мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+msgstr[1] ""
+"Функція snd_pcm_mmap_begin() повернула винятково велике значення: %lu байти "
+"(%lu мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+msgstr[2] ""
+"Функція snd_pcm_mmap_begin() повернула винятково велике значення: %lu байтів "
+"(%lu мс).\n"
+"Ймовірно, ви натрапили на помилку у драйвері ALSA «%s». Будь ласка, "
+"повідомте про цю помилку розробникам ALSA."
+
+#: spa/plugins/alsa/acp/channelmap.h:464
+msgid "(invalid)"
+msgstr "(некоректний)"
+
+#: spa/plugins/alsa/acp/compat.c:189
+msgid "Built-in Audio"
+msgstr "Вбудоване аудіо"
+
+#: spa/plugins/alsa/acp/compat.c:194
+msgid "Modem"
+msgstr "Модем"
+
+#: spa/plugins/bluez5/bluez5-device.c:1172
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "Звуковий шлюз (джерело A2DP і HSP/HFP AG)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1197
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "Високоточне відтворення (приймач A2DP, кодек %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1200
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "Двобічний високоточний обмін (джерело/приймач A2DP, кодек %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1208
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "Високоточне відтворення (приймач A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1210
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "Двобічний високоточний обмін (джерело/приймач A2DP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1238
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr "Головний модуль гарнітури (HSP/HFP, кодек %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1243
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr "Головний модуль гарнітури (HSP/HFP)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1325
+msgid "Handsfree"
+msgstr "Hands-Free пристрій"
+
+#: spa/plugins/bluez5/bluez5-device.c:1340
+msgid "Headphone"
+msgstr "Навушники"
+
+#: spa/plugins/bluez5/bluez5-device.c:1345
+msgid "Portable"
+msgstr "Портативна акустика"
+
+#: spa/plugins/bluez5/bluez5-device.c:1350
+msgid "Car"
+msgstr "Автомобільна акустика"
+
+#: spa/plugins/bluez5/bluez5-device.c:1355
+msgid "HiFi"
+msgstr "Hi-Fi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1360
+msgid "Phone"
+msgstr "Телефон"
+
+#: spa/plugins/bluez5/bluez5-device.c:1366
+msgid "Bluetooth"
+msgstr "Bluetooth"
diff --git a/po/zh_CN.po b/po/zh_CN.po
new file mode 100644
index 0000000..53ce6e8
--- /dev/null
+++ b/po/zh_CN.po
@@ -0,0 +1,626 @@
+# Simplified Chinese translation for PipeWire.
+# Copyright (C) 2008 PULSEAUDIO COPYRIGHT HOLDER
+# This file is distributed under the same license as the pipewire package.
+# 闫丰刚 <sainry@gmail.com>, 2008, 2009.
+# Leah Liu <lliu@redhat.com>, 2009, 2012.
+# Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012.
+# Frank Hill <hxf.prc@gmail.com>, 2015.
+# Mingye Wang (Arthur2e5) <arthur200126@gmail.com>, 2015.
+#
+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"
+"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"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+"%s [选项]\n"
+" -h, --help 显示此帮助信息\n"
+" --version 显示版本\n"
+" -c, --config 加载配置文件 (默认 %s)\n"
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr "PipeWire多媒体系统"
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr "启动PipeWire多媒体系统"
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "内置音频"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "调制解调器"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr "未知设备"
+
+#: src/tools/pw-cat.c:991
+#, c-format
+msgid ""
+"%s [options] <file>\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -v, --verbose Enable verbose operations\n"
+"\n"
+msgstr ""
+"%s [选项] <文件>\n"
+" -h, --help 显示此帮助信息\n"
+" --version 显示版本\n"
+" -v, --verbose 输出详细操作\n"
+"\n"
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+" -R, --remote 远程守护程序名\n"
+" --media-type 设置媒体类型 (默认 %s)\n"
+" --media-category 设置媒体类别 (默认 %s)\n"
+" --media-role 设置媒体角色 (默认 %s)\n"
+" --target 设置目标节点 (默认 %s)\n"
+" 设为0则不链接节点\n"
+" --latency 设置节点延迟 (默认 %s)\n"
+" 时间 (单位可为 s, ms, us, ns)\n"
+" 或样本数 (如256)\n"
+" 对应的采样率则是媒体源文件采样率的"
+"其一\n"
+" --list-targets 列出可用的 --target 目标\n"
+"\n"
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+" --rate 采样率 (录制模式需要) (默认 %u)\n"
+" --channels 通道数 (录制模式需要) (默认 %u)\n"
+" --channel-map 通道映射\n"
+" \"stereo\", \"surround-51\",... "
+"中的其一或\n"
+" 以\",\"分隔的通道名列表: 如 \"FL,"
+"FR\"\n"
+" --format 采样格式 %s (录制模式需要) (默认 "
+"%s)\n"
+" --volume 媒体流音量 0-1.0 (默认 %.3f)\n"
+" -q --quality 重采样质量 (0 - 15) (默认 %d)\n"
+"\n"
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+" -p, --playback 回放模式\n"
+" -r, --record 录制模式\n"
+" -m, --midi Midi模式\n"
+"\n"
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+"%s [选项] [命令]\n"
+" -h, --help 显示此帮助信息\n"
+" --version 显示版本\n"
+" -d, --daemon 以守护程序方式启动 (默认关闭)\n"
+" -r, --remote 远程守护程序名\n"
+"\n"
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "关"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(无效)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "输入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "扩展坞输入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "扩展坞话筒"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "扩展坞线输入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "话筒"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "前麦克风"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "后麦克风"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "外部话筒"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "内部话筒"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "无线电"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "视频"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "自动增益控制"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "无自动增益控制"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "增强"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "无增强"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "功放"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "无功放"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "重低音增强"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "无重低音增强"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "扬声器"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "模拟耳机"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "模拟输入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "扩展坞麦克风"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "头挂麦克风"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "模拟输出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+msgid "Headphones 2"
+msgstr "模拟耳机 2"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "模拟单声道输出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "线缆输出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "模拟单声道输出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "扬声器"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "数字输出 (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "数字输入 (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "多声道输入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "多声道输出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "游戏输出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "语音输出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+msgid "Chat Input"
+msgstr "语音输入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+msgid "Virtual Surround 7.1"
+msgstr "虚拟环绕 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "模拟单声道"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+msgid "Analog Mono (Left)"
+msgstr "模拟单声道 (左声道)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+msgid "Analog Mono (Right)"
+msgstr "模拟单声道 (右声道)"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "模拟立体声"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "单声道"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "耳机"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+msgid "Speakerphone"
+msgstr "扬声麦克风"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "多声道"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "模拟环绕 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "模拟环绕 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "模拟环绕 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "模拟环绕 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "模拟环绕 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "模拟环绕 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "模拟环绕 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "模拟环绕 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "模拟环绕 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "模拟环绕 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "模拟环绕 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "数字立体声(IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "数字环绕 4.0(IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "数字环绕 5.1(IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "数字环绕 5.1(IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "数字立体声(HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "数字环绕 5.1(HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr "语音"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr "游戏"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "模拟单声道双工"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "模拟立体声双工"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "数字立体声双工(IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "多声道双工"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "模拟立体声双工"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr "单声道语音 + 7.1 环绕声"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s 输出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s 输入"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() 返回的值非常大:%lu 字节(%lu 毫秒)。\n"
+"这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() 返回的值非常大:%li 字节(%s%lu 毫秒)。\n"
+"这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() 返回的值非常很奇怪:延迟 %lu 小于可用 (avail) %lu。\n"
+"这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() 返回的值非常大:%lu 字节(%lu ms)。\n"
+"这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。"
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr "音频网关 (A2DP 信源 或 HSP/HFP 网关)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr "高保真回放 (A2DP 信宿, 编码 %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr "高保真双工 (A2DP 信源/信宿, 编码 %s)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr "高保真回放 (A2DP 信宿)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr "高保真双工 (A2DP 信源/信宿)"
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, 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
+msgid "Handsfree"
+msgstr "免手操作"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "头戴耳机"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "便携式"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "车内"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "高保真"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "电话"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+msgid "Bluetooth"
+msgstr "蓝牙"
diff --git a/po/zh_TW.po b/po/zh_TW.po
new file mode 100644
index 0000000..1a768a9
--- /dev/null
+++ b/po/zh_TW.po
@@ -0,0 +1,585 @@
+# Chinese (Taiwan) translation for pipewire.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012.
+# pan93412 <pan93412@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PipeWire Volume Control\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/"
+"issues/new\n"
+"POT-Creation-Date: 2021-04-18 16:54+0800\n"
+"PO-Revision-Date: 2020-01-11 13:49+0800\n"
+"Last-Translator: pan93412 <pan93412@gmail.com>\n"
+"Language-Team: Chinese <zh-l10n@lists.linux.org.tw>\n"
+"Language: zh_TW\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Lokalize 19.12.0\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: src/daemon/pipewire.c:43
+#, c-format
+msgid ""
+"%s [options]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -c, --config Load config (Default %s)\n"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:4
+msgid "PipeWire Media System"
+msgstr ""
+
+#: src/daemon/pipewire.desktop.in:5
+msgid "Start the PipeWire Media System"
+msgstr ""
+
+#: src/examples/media-session/alsa-monitor.c:526
+#: spa/plugins/alsa/acp/compat.c:187
+msgid "Built-in Audio"
+msgstr "內部音效"
+
+#: src/examples/media-session/alsa-monitor.c:530
+#: spa/plugins/alsa/acp/compat.c:192
+msgid "Modem"
+msgstr "數據機"
+
+#: src/examples/media-session/alsa-monitor.c:539
+msgid "Unknown device"
+msgstr ""
+
+#: src/tools/pw-cat.c:991
+#, 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 ""
+
+#: src/tools/pw-cat.c:998
+#, c-format
+msgid ""
+" -R, --remote Remote daemon name\n"
+" --media-type Set media type (default %s)\n"
+" --media-category Set media category (default %s)\n"
+" --media-role Set media role (default %s)\n"
+" --target Set node target (default %s)\n"
+" 0 means don't link\n"
+" --latency Set node latency (default %s)\n"
+" Xunit (unit = s, ms, us, ns)\n"
+" or direct samples (256)\n"
+" the rate is the one of the source "
+"file\n"
+" --list-targets List available targets for --target\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1016
+#, c-format
+msgid ""
+" --rate Sample rate (req. for rec) (default "
+"%u)\n"
+" --channels Number of channels (req. for rec) "
+"(default %u)\n"
+" --channel-map Channel map\n"
+" one of: \"stereo\", "
+"\"surround-51\",... or\n"
+" comma separated list of channel "
+"names: eg. \"FL,FR\"\n"
+" --format Sample format %s (req. for rec) "
+"(default %s)\n"
+" --volume Stream volume 0-1.0 (default %.3f)\n"
+" -q --quality Resampler quality (0 - 15) (default "
+"%d)\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cat.c:1033
+msgid ""
+" -p, --playback Playback mode\n"
+" -r, --record Recording mode\n"
+" -m, --midi Midi mode\n"
+"\n"
+msgstr ""
+
+#: src/tools/pw-cli.c:2932
+#, c-format
+msgid ""
+"%s [options] [command]\n"
+" -h, --help Show this help\n"
+" --version Show version\n"
+" -d, --daemon Start as daemon (Default false)\n"
+" -r, --remote Remote daemon name\n"
+"\n"
+msgstr ""
+
+#: spa/plugins/alsa/acp/acp.c:290
+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
+msgid "Off"
+msgstr "關閉"
+
+#: spa/plugins/alsa/acp/channelmap.h:466
+msgid "(invalid)"
+msgstr "(無效)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2709
+msgid "Input"
+msgstr "輸入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2710
+msgid "Docking Station Input"
+msgstr "Docking Station 輸入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2711
+msgid "Docking Station Microphone"
+msgstr "Docking Station 麥克風"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2712
+msgid "Docking Station Line In"
+msgstr "Docking Station 線路輸入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2713
+#: spa/plugins/alsa/acp/alsa-mixer.c:2804
+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
+msgid "Microphone"
+msgstr "麥克風"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2715
+#: spa/plugins/alsa/acp/alsa-mixer.c:2799
+msgid "Front Microphone"
+msgstr "前方麥克風"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2716
+#: spa/plugins/alsa/acp/alsa-mixer.c:2800
+msgid "Rear Microphone"
+msgstr "後方麥克風"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2717
+msgid "External Microphone"
+msgstr "外接麥克風"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2718
+#: spa/plugins/alsa/acp/alsa-mixer.c:2802
+msgid "Internal Microphone"
+msgstr "內建麥克風"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2719
+#: spa/plugins/alsa/acp/alsa-mixer.c:2805
+msgid "Radio"
+msgstr "無線電"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2720
+#: spa/plugins/alsa/acp/alsa-mixer.c:2806
+msgid "Video"
+msgstr "視訊"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2721
+msgid "Automatic Gain Control"
+msgstr "自動增益控制"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2722
+msgid "No Automatic Gain Control"
+msgstr "無自動增益控制"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2723
+msgid "Boost"
+msgstr "增強"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2724
+msgid "No Boost"
+msgstr "無增強"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2725
+msgid "Amplifier"
+msgstr "擴大器"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2726
+msgid "No Amplifier"
+msgstr "無擴大器"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2727
+msgid "Bass Boost"
+msgstr "低音增強"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2728
+msgid "No Bass Boost"
+msgstr "無低音增強"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2729
+#: spa/plugins/bluez5/bluez5-device.c:1150
+msgid "Speaker"
+msgstr "喇叭"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2730
+#: spa/plugins/alsa/acp/alsa-mixer.c:2808
+msgid "Headphones"
+msgstr "頭戴式耳機"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2797
+msgid "Analog Input"
+msgstr "類比輸入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2801
+msgid "Dock Microphone"
+msgstr "臺座麥克風"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2803
+msgid "Headset Microphone"
+msgstr "耳麥麥克風"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2807
+msgid "Analog Output"
+msgstr "類比輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2809
+#, fuzzy
+msgid "Headphones 2"
+msgstr "頭戴式耳機"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2810
+msgid "Headphones Mono Output"
+msgstr "頭戴式耳機單聲道輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2811
+msgid "Line Out"
+msgstr "線路輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2812
+msgid "Analog Mono Output"
+msgstr "類比單聲道輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2813
+msgid "Speakers"
+msgstr "喇叭"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2814
+msgid "HDMI / DisplayPort"
+msgstr "HDMI / DisplayPort"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2815
+msgid "Digital Output (S/PDIF)"
+msgstr "數位輸出 (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2816
+msgid "Digital Input (S/PDIF)"
+msgstr "數位輸入 (S/PDIF)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2817
+msgid "Multichannel Input"
+msgstr "多聲道輸入"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2818
+msgid "Multichannel Output"
+msgstr "多聲道輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2819
+msgid "Game Output"
+msgstr "遊戲輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2820
+#: spa/plugins/alsa/acp/alsa-mixer.c:2821
+msgid "Chat Output"
+msgstr "聊天輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2822
+#, fuzzy
+msgid "Chat Input"
+msgstr "聊天輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:2823
+#, fuzzy
+msgid "Virtual Surround 7.1"
+msgstr "虛擬環繞聲 sink"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4527
+msgid "Analog Mono"
+msgstr "類比單聲道"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4528
+#, fuzzy
+msgid "Analog Mono (Left)"
+msgstr "類比單聲道"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4529
+#, fuzzy
+msgid "Analog Mono (Right)"
+msgstr "類比單聲道"
+
+#. Note: Not translated to "Analog Stereo Input", because the source
+#. * name gets "Input" appended to it automatically, so adding "Input"
+#. * here would lead to the source name to become "Analog Stereo Input
+#. * Input". The same logic applies to analog-stereo-output,
+#. * multichannel-input and multichannel-output.
+#: spa/plugins/alsa/acp/alsa-mixer.c:4530
+#: spa/plugins/alsa/acp/alsa-mixer.c:4538
+#: spa/plugins/alsa/acp/alsa-mixer.c:4539
+msgid "Analog Stereo"
+msgstr "類比立體聲"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4531
+msgid "Mono"
+msgstr "單聲道"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4532
+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
+msgid "Headset"
+msgstr "耳麥"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4541
+#: spa/plugins/alsa/acp/alsa-mixer.c:4699
+#, fuzzy
+msgid "Speakerphone"
+msgstr "喇叭"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4542
+#: spa/plugins/alsa/acp/alsa-mixer.c:4543
+msgid "Multichannel"
+msgstr "多聲道"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4544
+msgid "Analog Surround 2.1"
+msgstr "類比環繞聲 2.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4545
+msgid "Analog Surround 3.0"
+msgstr "類比環繞聲 3.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4546
+msgid "Analog Surround 3.1"
+msgstr "類比環繞聲 3.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4547
+msgid "Analog Surround 4.0"
+msgstr "類比環繞聲 4.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4548
+msgid "Analog Surround 4.1"
+msgstr "類比環繞聲 4.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4549
+msgid "Analog Surround 5.0"
+msgstr "類比環繞聲 5.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4550
+msgid "Analog Surround 5.1"
+msgstr "類比環繞聲 5.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4551
+msgid "Analog Surround 6.0"
+msgstr "類比環繞聲 6.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4552
+msgid "Analog Surround 6.1"
+msgstr "類比環繞聲 6.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4553
+msgid "Analog Surround 7.0"
+msgstr "類比環繞聲 7.0"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4554
+msgid "Analog Surround 7.1"
+msgstr "類比環繞聲 7.1"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4555
+msgid "Digital Stereo (IEC958)"
+msgstr "數位立體聲 (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4556
+msgid "Digital Surround 4.0 (IEC958/AC3)"
+msgstr "數位環繞聲 4.0 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4557
+msgid "Digital Surround 5.1 (IEC958/AC3)"
+msgstr "數位環繞聲 5.1 (IEC958/AC3)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4558
+msgid "Digital Surround 5.1 (IEC958/DTS)"
+msgstr "數位環繞聲 5.1 (IEC958/DTS)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4559
+msgid "Digital Stereo (HDMI)"
+msgstr "數位立體聲 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4560
+msgid "Digital Surround 5.1 (HDMI)"
+msgstr "數位環繞聲 5.1 (HDMI)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4561
+msgid "Chat"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4562
+msgid "Game"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4696
+msgid "Analog Mono Duplex"
+msgstr "類比單聲道雙工"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4697
+msgid "Analog Stereo Duplex"
+msgstr "類比立體聲雙工"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4700
+msgid "Digital Stereo Duplex (IEC958)"
+msgstr "數位立體聲雙工 (IEC958)"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4701
+msgid "Multichannel Duplex"
+msgstr "多聲道雙工"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4702
+msgid "Stereo Duplex"
+msgstr "立體聲雙工"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4703
+msgid "Mono Chat + 7.1 Surround"
+msgstr ""
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4806
+#, c-format
+msgid "%s Output"
+msgstr "%s 輸出"
+
+#: spa/plugins/alsa/acp/alsa-mixer.c:4813
+#, c-format
+msgid "%s Input"
+msgstr "%s 輸入"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269
+#, c-format
+msgid ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu "
+"ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_avail() 傳回超出預期的大值:%lu bytes (%lu ms)。\n"
+"這很能是 ALSA 驅動程式「%s」的臭蟲。請回報這個問題給 ALSA 開發者。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1241
+#, c-format
+msgid ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
+"%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_delay() 傳回超出預期的大值:%li bytes (%s%lu ms)。\n"
+"這很能是 ALSA 驅動程式「%s」的臭蟲。請回報這個問題給 ALSA 開發者。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1288
+#, c-format
+msgid ""
+"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
+"%lu.\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr ""
+"snd_pcm_avail_delay() 傳回超出預期的大值:延遲 %lu 少於可用的 %lu。\n"
+"這很能是 ALSA 驅動程式「%s」的臭蟲。請回報這個問題給 ALSA 開發者。"
+
+#: spa/plugins/alsa/acp/alsa-util.c:1331
+#, c-format
+msgid ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgid_plural ""
+"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes "
+"(%lu ms).\n"
+"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
+"to the ALSA developers."
+msgstr[0] ""
+"snd_pcm_mmap_begin() 傳回超出預期的大值:%lu bytes (%lu ms)。\n"
+"這很能是 ALSA 驅動程式「%s」的臭蟲。請回報這個問題給 ALSA 開發者。"
+
+#: spa/plugins/bluez5/bluez5-device.c:1010
+msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1033
+#, c-format
+msgid "High Fidelity Playback (A2DP Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1035
+#, c-format
+msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1041
+msgid "High Fidelity Playback (A2DP Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1043
+msgid "High Fidelity Duplex (A2DP Source/Sink)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1070
+#, c-format
+msgid "Headset Head Unit (HSP/HFP, codec %s)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1074
+msgid "Headset Head Unit (HSP/HFP)"
+msgstr ""
+
+#: spa/plugins/bluez5/bluez5-device.c:1140
+msgid "Handsfree"
+msgstr "免持裝置"
+
+#: spa/plugins/bluez5/bluez5-device.c:1155
+msgid "Headphone"
+msgstr "頭戴式耳機"
+
+#: spa/plugins/bluez5/bluez5-device.c:1160
+msgid "Portable"
+msgstr "可攜裝置"
+
+#: spa/plugins/bluez5/bluez5-device.c:1165
+msgid "Car"
+msgstr "汽車"
+
+#: spa/plugins/bluez5/bluez5-device.c:1170
+msgid "HiFi"
+msgstr "HiFi"
+
+#: spa/plugins/bluez5/bluez5-device.c:1175
+msgid "Phone"
+msgstr "手機"
+
+#: spa/plugins/bluez5/bluez5-device.c:1181
+#, fuzzy
+msgid "Bluetooth"
+msgstr "藍牙輸入"
diff --git a/pw-uninstalled.sh b/pw-uninstalled.sh
new file mode 100755
index 0000000..1bb6c55
--- /dev/null
+++ b/pw-uninstalled.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+
+set -e
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+while getopts ":b:v:" opt; do
+ case ${opt} in
+ b)
+ BUILDDIR=${OPTARG}
+ ;;
+ v)
+ VERSION=${OPTARG}
+ echo "Version: ${VERSION}"
+ ;;
+ \?)
+ echo "Invalid option: -${OPTARG}"
+ exit 1
+ ;;
+ :)
+ echo "Option -${OPTARG} requires an argument"
+ exit 1
+ ;;
+ esac
+done
+
+if [ -z "${BUILDDIR}" ]; then
+ BUILDDIR=${SCRIPT_DIR}/builddir
+ echo "Using default build directory: ${BUILDDIR}"
+fi
+
+if [ ! -d "${BUILDDIR}" ]; then
+ echo "Invalid build directory: ${BUILDDIR}"
+ exit 1
+fi
+
+# the config file read by the daemon
+export PIPEWIRE_CONFIG_DIR="${BUILDDIR}/src/daemon"
+# the directory with SPA plugins
+export SPA_PLUGIN_DIR="${BUILDDIR}/spa/plugins"
+export SPA_DATA_DIR="${SCRIPT_DIR}/spa/plugins"
+# the directory with pipewire modules
+export PIPEWIRE_MODULE_DIR="${BUILDDIR}/src/modules"
+export PATH="${BUILDDIR}/src/daemon:${BUILDDIR}/src/tools:${BUILDDIR}/src/media-session:${BUILDDIR}/src/examples:${BUILDDIR}/pipewire-v4l2/src:${PATH}"
+export LD_LIBRARY_PATH="${BUILDDIR}/src/pipewire/:${BUILDDIR}/pipewire-jack/src/${LD_LIBRARY_PATH+":$LD_LIBRARY_PATH"}"
+export GST_PLUGIN_PATH="${BUILDDIR}/src/gst/${GST_PLUGIN_PATH+":${GST_PLUGIN_PATH}"}"
+# the directory with card profiles and paths
+export ACP_PATHS_DIR="${SCRIPT_DIR}/spa/plugins/alsa/mixer/paths"
+export ACP_PROFILES_DIR="${SCRIPT_DIR}/spa/plugins/alsa/mixer/profile-sets"
+# ALSA plugin directory
+export ALSA_PLUGIN_DIR="${BUILDDIR}/pipewire-alsa/alsa-plugins"
+
+export PW_BUILDDIR=$BUILDDIR
+export PW_UNINSTALLED=1
+export PKG_CONFIG_PATH="${BUILDDIR}/meson-uninstalled/:${PKG_CONFIG_PATH}"
+
+if [ -d "${BUILDDIR}/subprojects/wireplumber" ]; then
+ # FIXME: find a nice, shell-neutral way to specify a prompt
+ "${SCRIPT_DIR}"/subprojects/wireplumber/wp-uninstalled.sh -b"${BUILDDIR}"/subprojects/wireplumber "${SHELL}"
+elif [ -d "${BUILDDIR}/subprojects/media-session" ]; then
+ # FIXME: find a nice, shell-neutral way to specify a prompt
+ "${SCRIPT_DIR}"/subprojects/media-session/media-session-uninstalled.sh -b"${BUILDDIR}"/subprojects/media-session "${SHELL}"
+else
+ # FIXME: find a nice, shell-neutral way to specify a prompt
+ ${SHELL}
+fi
diff --git a/spa/examples/adapter-control.c b/spa/examples/adapter-control.c
new file mode 100644
index 0000000..7caf32f
--- /dev/null
+++ b/spa/examples/adapter-control.c
@@ -0,0 +1,771 @@
+/* Spa
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Running audioadapter nodes.
+ [title]
+ [doc]
+ Runs an output audioadapter using audiotestsrc as follower
+ with an input audioadapter using alsa-pcm-sink as follower
+ for easy testing.
+ [doc]
+ */
+
+#include "config.h"
+
+#include <math.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <pthread.h>
+#include <poll.h>
+
+#include <spa/control/control.h>
+#include <spa/graph/graph.h>
+#include <spa/support/plugin.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/audio/format-utils.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+static SPA_LOG_IMPL(default_log);
+
+#define MIN_LATENCY 1024
+#define CONTROL_BUFFER_SIZE 32768
+
+
+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];
+};
+
+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_support support[5];
+ uint32_t n_support;
+
+ struct spa_graph graph;
+ struct spa_graph_state graph_state;
+ struct spa_graph_node graph_source_node;
+ struct spa_graph_node graph_sink_node;
+ struct spa_graph_state graph_source_state;
+ struct spa_graph_state graph_sink_state;
+ struct spa_graph_port graph_source_port_0;
+ struct spa_graph_port graph_sink_port_0;
+
+ struct spa_node *source_follower_node; // audiotestsrc
+ struct spa_node *source_node; // adapter for audiotestsrc
+ struct spa_node *sink_follower_node; // alsa-pcm-sink
+ struct spa_node *sink_node; // adapter for alsa-pcm-sink
+
+ struct spa_io_position position;
+ struct spa_io_buffers source_sink_io[1];
+ struct spa_buffer *source_buffers[1];
+ struct buffer source_buffer[1];
+
+ struct spa_io_buffers control_io;
+ struct spa_buffer *control_buffers[1];
+ struct buffer control_buffer[1];
+
+ int buffer_count;
+ bool start_fade_in;
+ double volume_accum;
+ uint32_t volume_offs;
+
+ bool running;
+ pthread_t thread;
+};
+
+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;
+
+ if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL)
+ return -ENOMEM;
+
+ hnd = dlopen(path, RTLD_NOW);
+ free(path);
+
+ if (hnd == NULL) {
+ printf("can't load %s: %s\n", lib, dlerror());
+ return -ENOENT;
+ }
+ if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ printf("can't find enum function\n");
+ res = -ENOENT;
+ goto exit_cleanup;
+ }
+
+ 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 (factory->version < 1)
+ continue;
+ 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);
+ goto exit_cleanup;
+ }
+ return 0;
+ }
+ return -EBADF;
+
+exit_cleanup:
+ dlclose(hnd);
+ return res;
+}
+
+int init_data(struct data *data)
+{
+ int res;
+ const char *str;
+ struct spa_handle *handle = NULL;
+ void *iface;
+
+ if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
+ str = PLUGINDIR;
+ data->plugin_dir = str;
+
+ /* start not doing fade-in */
+ data->start_fade_in = true;
+ data->volume_accum = 0.0;
+ data->volume_offs = 0;
+
+ /* init the graph */
+ spa_graph_init(&data->graph, &data->graph_state);
+
+ /* set the default log */
+ data->log = &default_log.log;
+ data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data->log);
+
+ /* load and set support system */
+ 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);
+
+ /* load and set support loop and loop control */
+ 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;
+ 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);
+ 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 ((str = getenv("SPA_DEBUG")))
+ data->log->level = atoi(str);
+
+ return 0;
+}
+
+static int make_node(struct data *data, struct spa_node **node, const char *lib,
+ const char *name, const struct spa_dict *props)
+{
+ struct spa_handle *handle;
+ int res = 0;
+ void *hnd = NULL;
+ spa_handle_factory_enum_func_t enum_func;
+ uint32_t i;
+ char *path;
+
+ if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL)
+ return -ENOMEM;
+
+ hnd = dlopen(path, RTLD_NOW);
+ free(path);
+
+ if (hnd == NULL) {
+ printf("can't load %s: %s\n", lib, dlerror());
+ return -ENOENT;
+ }
+ if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ printf("can't find enum function\n");
+ res = -ENOENT;
+ goto exit_cleanup;
+ }
+
+ for (i = 0;;) {
+ const struct spa_handle_factory *factory;
+ void *iface;
+
+ if ((res = enum_func(&factory, &i)) <= 0) {
+ if (res != 0)
+ printf("can't enumerate factories: %s\n", spa_strerror(res));
+ break;
+ }
+ if (factory->version < 1)
+ continue;
+ 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, props, data->support,
+ data->n_support)) < 0) {
+ printf("can't make factory instance: %d\n", res);
+ goto exit_cleanup;
+ }
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) {
+ printf("can't get interface %d\n", res);
+ goto exit_cleanup;
+ }
+ *node = iface;
+ return 0;
+ }
+ return -EBADF;
+
+exit_cleanup:
+ dlclose(hnd);
+ return res;
+}
+
+static int fade_in(struct data *data)
+{
+ struct spa_pod_builder b;
+ struct spa_pod_frame f[1];
+ void *buffer = data->control_buffer->datas[0].data;
+ uint32_t buffer_size = data->control_buffer->datas[0].maxsize;
+ data->control_buffer->datas[0].chunk[0].size = buffer_size;
+
+ printf ("fading in\n");
+
+ spa_pod_builder_init(&b, buffer, buffer_size);
+ spa_pod_builder_push_sequence(&b, &f[0], 0);
+ data->volume_offs = 0;
+ do {
+ spa_pod_builder_control(&b, data->volume_offs, SPA_CONTROL_Properties);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ SPA_PROP_volume, SPA_POD_Float(data->volume_accum));
+ data->volume_accum += 0.003;
+ data->volume_offs += 200;
+ } while (data->volume_accum < 1.0);
+ spa_pod_builder_pop(&b, &f[0]);
+
+ return 0;
+}
+
+static int fade_out(struct data *data)
+{
+ struct spa_pod_builder b;
+ struct spa_pod_frame f[1];
+ void *buffer = data->control_buffer->datas[0].data;
+ uint32_t buffer_size = data->control_buffer->datas[0].maxsize;
+ data->control_buffer->datas[0].chunk[0].size = buffer_size;
+
+ printf ("fading out\n");
+
+ spa_pod_builder_init(&b, buffer, buffer_size);
+ spa_pod_builder_push_sequence(&b, &f[0], 0);
+ data->volume_offs = 200;
+ do {
+ spa_pod_builder_control(&b, data->volume_offs, SPA_CONTROL_Properties);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ SPA_PROP_volume, SPA_POD_Float(data->volume_accum));
+ data->volume_accum -= 0.003;
+ data->volume_offs += 200;
+ } while (data->volume_accum > 0.0);
+ spa_pod_builder_pop(&b, &f[0]);
+
+ return 0;
+}
+
+static void do_fade(struct data *data)
+{
+ switch (data->control_io.status) {
+ case SPA_STATUS_OK:
+ case SPA_STATUS_NEED_DATA:
+ break;
+ case SPA_STATUS_HAVE_DATA:
+ case SPA_STATUS_STOPPED:
+ default:
+ return;
+ }
+
+ /* fade */
+ if (data->start_fade_in)
+ fade_in(data);
+ else
+ fade_out(data);
+
+ data->control_io.status = SPA_STATUS_HAVE_DATA;
+ data->control_io.buffer_id = 0;
+
+ /* alternate */
+ data->start_fade_in = !data->start_fade_in;
+}
+
+static int on_sink_node_ready(void *_data, int status)
+{
+ struct data *data = _data;
+
+ /* only do fade in/out when buffer count is 0 */
+ if (data->buffer_count == 0)
+ do_fade(data);
+
+ /* update buffer count */
+ data->buffer_count++;
+ if (data->buffer_count > 64)
+ data->buffer_count = 0;
+
+ spa_graph_node_process(&data->graph_source_node);
+ spa_graph_node_process(&data->graph_sink_node);
+ return 0;
+}
+
+static const struct spa_node_callbacks sink_node_callbacks = {
+ SPA_VERSION_NODE_CALLBACKS,
+ .ready = on_sink_node_ready,
+};
+
+static int make_nodes(struct data *data, const char *device)
+{
+ int res = 0;
+ struct spa_pod *props;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ char value[32];
+ struct spa_dict_item items[2];
+ struct spa_audio_info_raw info;
+ struct spa_pod *param;
+
+ items[0] = SPA_DICT_ITEM_INIT("clock.quantum-limit", "8192");
+
+ /* make the source node (audiotestsrc) */
+ if ((res = make_node(data, &data->source_follower_node,
+ "audiotestsrc/libspa-audiotestsrc.so",
+ "audiotestsrc",
+ &SPA_DICT_INIT(items, 1))) < 0) {
+ printf("can't create source follower node (audiotestsrc): %d\n", res);
+ return res;
+ }
+
+ /* set the format on the source */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, 0,
+ &SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_S16,
+ .rate = 48000,
+ .channels = 2 ));
+ if ((res = spa_node_port_set_param(data->source_follower_node,
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_PARAM_Format, 0, param)) < 0) {
+ printf("can't set format on follower node (audiotestsrc): %d\n", res);
+ return res;
+ }
+
+ /* make the sink adapter node */
+ snprintf(value, sizeof(value), "pointer:%p", data->source_follower_node);
+ items[1] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value);
+ if ((res = make_node(data, &data->source_node,
+ "audioconvert/libspa-audioconvert.so",
+ SPA_NAME_AUDIO_ADAPT,
+ &SPA_DICT_INIT(items, 2))) < 0) {
+ printf("can't create source adapter node: %d\n", res);
+ return res;
+ }
+
+ /* setup the source node props */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ props = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ SPA_PROP_frequency, SPA_POD_Float(600.0),
+ SPA_PROP_volume, SPA_POD_Float(0.5),
+ SPA_PROP_live, SPA_POD_Bool(false));
+ if ((res = spa_node_set_param(data->source_node, SPA_PARAM_Props, 0, props)) < 0) {
+ printf("can't setup source follower node %d\n", res);
+ return res;
+ }
+
+ /* setup the source node port config */
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_F32P;
+ info.channels = 1;
+ info.rate = 48000;
+ info.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+ if ((res = spa_node_set_param(data->source_node, SPA_PARAM_PortConfig, 0, param) < 0)) {
+ printf("can't setup source node %d\n", res);
+ return res;
+ }
+
+ /* make the sink follower node (alsa-pcm-sink) */
+ if ((res = make_node(data, &data->sink_follower_node,
+ "alsa/libspa-alsa.so",
+ SPA_NAME_API_ALSA_PCM_SINK,
+ &SPA_DICT_INIT(items, 1))) < 0) {
+ printf("can't create sink follower node (alsa-pcm-sink): %d\n", res);
+ return res;
+ }
+
+ /* make the sink adapter node */
+ snprintf(value, sizeof(value), "pointer:%p", data->sink_follower_node);
+ items[1] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value);
+ if ((res = make_node(data, &data->sink_node,
+ "audioconvert/libspa-audioconvert.so",
+ SPA_NAME_AUDIO_ADAPT,
+ &SPA_DICT_INIT(items, 2))) < 0) {
+ printf("can't create sink adapter node: %d\n", res);
+ return res;
+ }
+
+ /* add sink follower node callbacks */
+ spa_node_set_callbacks(data->sink_node, &sink_node_callbacks, data);
+
+ /* setup the sink node props */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ props = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ SPA_PROP_device, SPA_POD_String(device ? device : "hw:0"),
+ SPA_PROP_minLatency, SPA_POD_Int(MIN_LATENCY));
+ if ((res = spa_node_set_param(data->sink_follower_node, SPA_PARAM_Props, 0, props)) < 0) {
+ printf("can't setup sink follower node %d\n", res);
+ return res;
+ }
+
+ /* setup the sink node port config */
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_F32P;
+ info.channels = 1;
+ info.rate = 48000;
+ info.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
+ SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(true),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+ if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_PortConfig, 0, param) < 0)) {
+ printf("can't setup sink node %d\n", res);
+ return res;
+ }
+
+ /* set io buffers on source and sink nodes */
+ data->source_sink_io[0] = SPA_IO_BUFFERS_INIT;
+ if ((res = spa_node_port_set_io(data->source_node,
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_IO_Buffers,
+ &data->source_sink_io[0], sizeof(data->source_sink_io[0]))) < 0) {
+ printf("can't set io buffers on port 0 of source node: %d\n", res);
+ return res;
+ }
+ if ((res = spa_node_port_set_io(data->sink_node,
+ SPA_DIRECTION_INPUT, 0,
+ SPA_IO_Buffers,
+ &data->source_sink_io[0], sizeof(data->source_sink_io[0]))) < 0) {
+ printf("can't set io buffers on port 0 of sink node: %d\n", res);
+ return res;
+ }
+ /* set io position and clock on source and sink nodes */
+ data->position.clock.rate = SPA_FRACTION(1, 48000);
+ data->position.clock.duration = 1024;
+ if ((res = spa_node_set_io(data->source_node,
+ SPA_IO_Position,
+ &data->position, sizeof(data->position))) < 0) {
+ printf("can't set io position on source node: %d\n", res);
+ return res;
+ }
+ if ((res = spa_node_set_io(data->sink_node,
+ SPA_IO_Position,
+ &data->position, sizeof(data->position))) < 0) {
+ printf("can't set io position on sink node: %d\n", res);
+ return res;
+ }
+ if ((res = spa_node_set_io(data->source_node,
+ SPA_IO_Clock,
+ &data->position.clock, sizeof(data->position.clock))) < 0) {
+ printf("can't set io clock on source node: %d\n", res);
+ return res;
+ }
+ if ((res = spa_node_set_io(data->sink_node,
+ SPA_IO_Clock,
+ &data->position.clock, sizeof(data->position.clock))) < 0) {
+ printf("can't set io clock on sink node: %d\n", res);
+ return res;
+ }
+
+ /* set io buffers on control port of sink node */
+ if ((res = spa_node_port_set_io(data->sink_node,
+ SPA_DIRECTION_INPUT, 1,
+ SPA_IO_Buffers,
+ &data->control_io, sizeof(data->control_io))) < 0) {
+ printf("can't set io buffers on control port 1 of sink node\n");
+ return res;
+ }
+
+ /* add source node to the graph */
+ spa_graph_node_init(&data->graph_source_node, &data->graph_source_state);
+ spa_graph_node_set_callbacks(&data->graph_source_node, &spa_graph_node_impl_default, data->source_node);
+ spa_graph_node_add(&data->graph, &data->graph_source_node);
+ spa_graph_port_init(&data->graph_source_port_0, SPA_DIRECTION_OUTPUT, 0, 0);
+ spa_graph_port_add(&data->graph_source_node, &data->graph_source_port_0);
+
+ /* add sink node to the graph */
+ spa_graph_node_init(&data->graph_sink_node, &data->graph_sink_state);
+ spa_graph_node_set_callbacks(&data->graph_sink_node, &spa_graph_node_impl_default, data->sink_node);
+ spa_graph_node_add(&data->graph, &data->graph_sink_node);
+ spa_graph_port_init(&data->graph_sink_port_0, SPA_DIRECTION_INPUT, 0, 0);
+ spa_graph_port_add(&data->graph_sink_node, &data->graph_sink_port_0);
+
+ /* link source and sink nodes */
+ spa_graph_port_link(&data->graph_source_port_0, &data->graph_sink_port_0);
+
+ return res;
+}
+
+static void
+init_buffer(struct data *data, struct spa_buffer **bufs, struct buffer *ba, int n_buffers,
+ size_t size)
+{
+ int i;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &ba[i];
+ bufs[i] = &b->buffer;
+
+ 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_MemPtr;
+ b->datas[0].flags = 0;
+ b->datas[0].fd = -1;
+ b->datas[0].mapoffset = 0;
+ b->datas[0].maxsize = size;
+ b->datas[0].data = malloc(size);
+ 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;
+ }
+}
+
+static int negotiate_formats(struct data *data)
+{
+ int res;
+ struct spa_pod *filter = NULL, *param = NULL;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ uint32_t state = 0;
+ size_t buffer_size = 1024;
+
+ /* set the sink and source formats */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_dsp_build(&b, 0,
+ &SPA_AUDIO_INFO_DSP_INIT(
+ .format = SPA_AUDIO_FORMAT_F32P));
+ if ((res = spa_node_port_set_param(data->source_node,
+ SPA_DIRECTION_OUTPUT, 0, SPA_PARAM_Format, 0, param)) < 0)
+ return res;
+ if ((res = spa_node_port_set_param(data->sink_node,
+ SPA_DIRECTION_INPUT, 0, SPA_PARAM_Format, 0, param)) < 0)
+ return res;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+ if ((res = spa_node_port_set_param(data->sink_node,
+ SPA_DIRECTION_INPUT, 1, SPA_PARAM_Format, 0, param)) < 0)
+ return res;
+
+ /* get the source node buffer size */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if ((res = spa_node_port_enum_params_sync(data->source_node,
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_PARAM_Buffers, &state, filter, &param, &b)) != 1)
+ return res ? res : -ENOTSUP;
+ spa_pod_fixate(param);
+ if ((res = spa_pod_parse_object(param, SPA_TYPE_OBJECT_ParamBuffers, NULL,
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(&buffer_size))) < 0)
+ return res;
+
+ /* use buffers on the source and sink */
+ init_buffer(data, data->source_buffers, data->source_buffer, 1, buffer_size);
+ if ((res = spa_node_port_use_buffers(data->source_node,
+ SPA_DIRECTION_OUTPUT, 0, 0, data->source_buffers, 1)) < 0)
+ return res;
+ if ((res = spa_node_port_use_buffers(data->sink_node,
+ SPA_DIRECTION_INPUT, 0, 0, data->source_buffers, 1)) < 0)
+ return res;
+
+ /* Set the control buffers */
+ init_buffer(data, data->control_buffers, data->control_buffer, 1, CONTROL_BUFFER_SIZE);
+ if ((res = spa_node_port_use_buffers(data->sink_node,
+ SPA_DIRECTION_INPUT, 1, 0, data->control_buffers, 1)) < 0)
+ return res;
+
+ return 0;
+}
+
+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;
+
+ return NULL;
+}
+
+static void run_async_sink(struct data *data)
+{
+ int res, err;
+ struct spa_command cmd;
+
+ cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start);
+ if ((res = spa_node_send_command(data->source_node, &cmd)) < 0)
+ printf("got error %d\n", res);
+ if ((res = spa_node_send_command(data->sink_node, &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;
+ }
+
+ printf("sleeping for 1000 seconds\n");
+ sleep(1000);
+
+ if (data->running) {
+ data->running = false;
+ pthread_join(data->thread, NULL);
+ }
+
+ spa_loop_control_enter(data->control);
+
+ cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause);
+ if ((res = spa_node_send_command(data->source_node, &cmd)) < 0)
+ printf("got error %d\n", res);
+ if ((res = spa_node_send_command(data->sink_node, &cmd)) < 0)
+ printf("got error %d\n", res);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ int res = 0;
+
+ /* init data */
+ if ((res = init_data(&data)) < 0) {
+ printf("can't init data: %d (%s)\n", res, spa_strerror(res));
+ return -1;
+ }
+
+ /* make the nodes (audiotestsrc and adapter with alsa-pcm-sink as follower) */
+ if ((res = make_nodes(&data, argc > 1 ? argv[1] : NULL)) < 0) {
+ printf("can't make nodes: %d (%s)\n", res, spa_strerror(res));
+ return -1;
+ }
+
+ /* Negotiate format */
+ if ((res = negotiate_formats(&data)) < 0) {
+ printf("can't negotiate nodes: %d (%s)\n", res, spa_strerror(res));
+ return -1;
+ }
+
+ spa_loop_control_enter(data.control);
+ run_async_sink(&data);
+ spa_loop_control_leave(data.control);
+}
diff --git a/spa/examples/example-control.c b/spa/examples/example-control.c
new file mode 100644
index 0000000..f26960c
--- /dev/null
+++ b/spa/examples/example-control.c
@@ -0,0 +1,560 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ [title]
+ */
+
+#include "config.h"
+
+#include <math.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <pthread.h>
+#include <poll.h>
+
+#include <spa/support/plugin.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/audio/format-utils.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+static SPA_LOG_IMPL(default_log);
+
+#define spa_debug(f,...) spa_log_trace(&default_log.log, f, __VA_ARGS__)
+
+#include <spa/graph/graph.h>
+
+#include <spa/debug/pod.h>
+
+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];
+};
+
+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_support support[5];
+ uint32_t n_support;
+
+ struct spa_graph graph;
+ struct spa_graph_state graph_state;
+ struct spa_graph_node source_node;
+ struct spa_graph_state source_state;
+ struct spa_graph_port source_out;
+ struct spa_graph_port sink_in;
+ struct spa_graph_node sink_node;
+ struct spa_graph_state sink_state;
+
+ struct spa_node *sink;
+
+ struct spa_node *source;
+ struct spa_io_buffers source_sink_io[1];
+ struct spa_buffer *source_buffers[1];
+ struct buffer source_buffer[1];
+
+ uint8_t ctrl[1024];
+ double freq_accum;
+ double volume_accum;
+
+ bool running;
+ pthread_t thread;
+};
+
+#define MIN_LATENCY 1024
+
+#define BUFFER_SIZE 4096
+
+static void
+init_buffer(struct data *data, struct spa_buffer **bufs, struct buffer *ba, int n_buffers,
+ size_t size)
+{
+ int i;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &ba[i];
+ bufs[i] = &b->buffer;
+
+ 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_MemPtr;
+ b->datas[0].flags = 0;
+ b->datas[0].fd = -1;
+ b->datas[0].mapoffset = 0;
+ b->datas[0].maxsize = size;
+ b->datas[0].data = malloc(size);
+ 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;
+ }
+}
+
+static int make_node(struct data *data, struct spa_node **node, const char *lib, const char *name)
+{
+ struct spa_handle *handle;
+ int res;
+ void *hnd;
+ spa_handle_factory_enum_func_t enum_func;
+ uint32_t i;
+ char *path;
+
+ 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", lib, dlerror());
+ free(path);
+ return -ENOENT;
+ }
+ free(path);
+ if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ printf("can't find enum function\n");
+ return -ENOENT;
+ }
+
+ for (i = 0;;) {
+ const struct spa_handle_factory *factory;
+ void *iface;
+
+ if ((res = enum_func(&factory, &i)) <= 0) {
+ if (res != 0)
+ printf("can't enumerate factories: %s\n", spa_strerror(res));
+ break;
+ }
+ if (factory->version < 1)
+ continue;
+ 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;
+ }
+ 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;
+ }
+ return -EBADF;
+}
+
+static void update_props(struct data *data)
+{
+ struct spa_pod_builder b;
+ struct spa_pod *pod;
+ struct spa_pod_frame f[2];
+
+ spa_pod_builder_init(&b, data->ctrl, sizeof(data->ctrl));
+
+#if 0
+ spa_pod_builder_push_sequence(&b, &f[0], 0);
+ spa_pod_builder_control(&b, 0, SPA_CONTROL_Properties);
+ spa_pod_builder_push_object(&b, &f[1], SPA_TYPE_OBJECT_Props, 0);
+ spa_pod_builder_prop(&b, SPA_PROP_frequency, 0);
+ spa_pod_builder_float(&b, ((sin(data->freq_accum) + 1.0) * 200.0) + 440.0);
+ spa_pod_builder_prop(&b, SPA_PROP_volume, 0);
+ spa_pod_builder_float(&b, (sin(data->volume_accum) / 2.0) + 0.5);
+ spa_pod_builder_pop(&b, &f[1]);
+ pod = spa_pod_builder_pop(&b, &f[0]);
+#else
+ spa_pod_builder_push_sequence(&b, &f[0], 0);
+ spa_pod_builder_control(&b, 0, SPA_CONTROL_Properties);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ SPA_PROP_frequency, SPA_POD_Float(((sin(data->freq_accum) + 1.0) * 200.0) + 440.0),
+ SPA_PROP_volume, SPA_POD_Float((sin(data->volume_accum) / 2.0) + 0.5));
+ pod = spa_pod_builder_pop(&b, &f[0]);
+#endif
+
+ spa_debug_pod(0, NULL, pod);
+
+ data->freq_accum += M_PI_M2 / 880.0;
+ if (data->freq_accum >= M_PI_M2)
+ data->freq_accum -= M_PI_M2;
+
+ data->volume_accum += M_PI_M2 / 2000.0;
+ if (data->volume_accum >= M_PI_M2)
+ data->volume_accum -= M_PI_M2;
+}
+
+static int on_sink_ready(void *_data, int status)
+{
+ struct data *data = _data;
+
+ update_props(data);
+
+ spa_graph_node_process(&data->source_node);
+ spa_graph_node_process(&data->sink_node);
+ return 0;
+}
+
+static int
+on_sink_reuse_buffer(void *_data, uint32_t port_id, uint32_t buffer_id)
+{
+ struct data *data = _data;
+
+ data->source_sink_io[0].buffer_id = buffer_id;
+ return 0;
+}
+
+static const struct spa_node_callbacks sink_callbacks = {
+ SPA_VERSION_NODE_CALLBACKS,
+ .ready = on_sink_ready,
+ .reuse_buffer = on_sink_reuse_buffer
+};
+
+static int make_nodes(struct data *data, const char *device)
+{
+ int res;
+ struct spa_pod *props;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[512];
+ //uint32_t idx;
+
+ if ((res = make_node(data, &data->sink,
+ "alsa/libspa-alsa.so",
+ SPA_NAME_API_ALSA_PCM_SINK)) < 0) {
+ printf("can't create alsa-sink: %d\n", res);
+ return res;
+ }
+ spa_node_set_callbacks(data->sink, &sink_callbacks, data);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ props = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ SPA_PROP_device, SPA_POD_String(device ? device : "hw:0"),
+ SPA_PROP_minLatency, SPA_POD_Int(MIN_LATENCY));
+
+ spa_debug_pod(0, NULL, props);
+
+ if ((res = spa_node_set_param(data->sink, SPA_PARAM_Props, 0, props)) < 0)
+ printf("got set_props error %d\n", res);
+
+ if ((res = make_node(data, &data->source,
+ "audiotestsrc/libspa-audiotestsrc.so",
+ "audiotestsrc")) < 0) {
+ printf("can't create audiotestsrc: %d\n", res);
+ return res;
+ }
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ props = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ SPA_PROP_frequency, SPA_POD_Float(600.0),
+ SPA_PROP_volume, SPA_POD_Float(0.5),
+ SPA_PROP_live, SPA_POD_Bool(false));
+
+ if ((res = spa_node_set_param(data->source, SPA_PARAM_Props, 0, props)) < 0)
+ printf("got set_props error %d\n", res);
+
+ if ((res = spa_node_port_set_io(data->source,
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_IO_Control,
+ &data->ctrl, sizeof(data->ctrl))) < 0) {
+ printf("can't set_io freq: %d\n", res);
+ return res;
+ }
+
+ data->source_sink_io[0] = SPA_IO_BUFFERS_INIT;
+
+ spa_node_port_set_io(data->source,
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_IO_Buffers,
+ &data->source_sink_io[0], sizeof(data->source_sink_io[0]));
+ spa_node_port_set_io(data->sink,
+ SPA_DIRECTION_INPUT, 0,
+ SPA_IO_Buffers,
+ &data->source_sink_io[0], sizeof(data->source_sink_io[0]));
+
+ spa_graph_node_init(&data->source_node, &data->source_state);
+ spa_graph_node_set_callbacks(&data->source_node, &spa_graph_node_impl_default, data->source);
+ spa_graph_node_add(&data->graph, &data->source_node);
+ spa_graph_port_init(&data->source_out, SPA_DIRECTION_OUTPUT, 0, 0);
+ spa_graph_port_add(&data->source_node, &data->source_out);
+
+ spa_graph_node_init(&data->sink_node, &data->sink_state);
+ spa_graph_node_set_callbacks(&data->sink_node, &spa_graph_node_impl_default, data->sink);
+ spa_graph_node_add(&data->graph, &data->sink_node);
+ spa_graph_port_init(&data->sink_in, SPA_DIRECTION_INPUT, 0, 0);
+ spa_graph_port_add(&data->sink_node, &data->sink_in);
+
+ spa_graph_port_link(&data->source_out, &data->sink_in);
+
+ return res;
+}
+
+static int negotiate_formats(struct data *data)
+{
+ int res;
+ struct spa_pod *format;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ format = spa_format_audio_raw_build(&b, 0,
+ &SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_S16,
+ .rate = 48000,
+ .channels = 2 ));
+
+ if ((res = spa_node_port_set_param(data->sink,
+ SPA_DIRECTION_INPUT, 0,
+ SPA_PARAM_Format, 0, format)) < 0)
+ return res;
+
+ if ((res = spa_node_port_set_param(data->source,
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_PARAM_Format, 0, format)) < 0)
+ return res;
+
+ init_buffer(data, data->source_buffers, data->source_buffer, 1, BUFFER_SIZE);
+ if ((res =
+ spa_node_port_use_buffers(data->sink,
+ SPA_DIRECTION_INPUT, 0, 0,
+ data->source_buffers, 1)) < 0)
+ return res;
+ if ((res =
+ spa_node_port_use_buffers(data->source,
+ SPA_DIRECTION_OUTPUT, 0, 0,
+ data->source_buffers, 1)) < 0)
+ return res;
+
+ 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_sink(struct data *data)
+{
+ int res, err;
+ struct spa_command cmd;
+
+ cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start);
+ if ((res = spa_node_send_command(data->sink, &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;
+ }
+
+ printf("sleeping for 1000 seconds\n");
+ sleep(1000);
+
+ if (data->running) {
+ data->running = false;
+ pthread_join(data->thread, NULL);
+ }
+
+ spa_loop_control_enter(data->control);
+
+ cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause);
+ if ((res = spa_node_send_command(data->sink, &cmd)) < 0)
+ printf("got error %d\n", res);
+}
+
+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;
+
+ 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", lib, dlerror());
+ free(path);
+ return -ENOENT;
+ }
+ free(path);
+ if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ printf("can't find enum function\n");
+ return -ENOENT;
+ }
+
+ 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 (factory->version < 1)
+ continue;
+ 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;
+}
+
+int init_data(struct data *data)
+{
+ int res;
+ const char *str;
+ struct spa_handle *handle = NULL;
+ void *iface;
+
+ if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
+ str = PLUGINDIR;
+ data->plugin_dir = str;
+
+ /* init the graph */
+ spa_graph_init(&data->graph, &data->graph_state);
+
+ /* set the default log */
+ data->log = &default_log.log;
+ data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data->log);
+
+ /* load and set support system */
+ 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);
+
+ /* load and set support loop and loop control */
+ 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;
+ 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);
+ 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 ((str = getenv("SPA_DEBUG")))
+ data->log->level = atoi(str);
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { NULL };
+ int res;
+
+ if ((res = init_data(&data)) < 0) {
+ printf("can't init data: %d (%s)\n", res, spa_strerror(res));
+ return -1;
+ }
+
+ if ((res = make_nodes(&data, argc > 1 ? argv[1] : NULL)) < 0) {
+ printf("can't make nodes: %d (%s)\n", res, spa_strerror(res));
+ return -1;
+ }
+ if ((res = negotiate_formats(&data)) < 0) {
+ printf("can't negotiate nodes: %d (%s)\n", res, spa_strerror(res));
+ return -1;
+ }
+
+ spa_loop_control_enter(data.control);
+ run_async_sink(&data);
+ spa_loop_control_leave(data.control);
+
+ return 0;
+}
diff --git a/spa/examples/local-libcamera.c b/spa/examples/local-libcamera.c
new file mode 100644
index 0000000..88ad02b
--- /dev/null
+++ b/spa/examples/local-libcamera.c
@@ -0,0 +1,562 @@
+/* Spa
+ *
+ * Copyright (C) 2020, Collabora Ltd.
+ * Author: Raghavendra Rao Sidlagatta <raghavendra.rao@collabora.com>
+ *
+ * local-libcamera.c
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Example using libspa-libcamera, 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 USE_BUFFER false
+
+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_support support[5];
+ 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);
+
+ SDL_RenderClear(data->renderer);
+ SDL_RenderCopy(data->renderer, texture, NULL, NULL);
+ SDL_RenderPresent(data->renderer);
+
+ if (SDL_LockTexture(texture, NULL, &sdata, &sstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ return -EIO;
+ }
+ } 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 + datas[0].mapoffset, PROT_READ,
+ MAP_PRIVATE, datas[0].fd, 0);
+ if (map == MAP_FAILED)
+ return -errno;
+ sdata = SPA_PTROFF(map, datas[0].mapoffset, uint8_t);
+ } 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 + datas[0].mapoffset);
+ }
+
+ 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, const char *device)
+{
+ 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,
+ "libcamera/libspa-libcamera.so",
+ SPA_NAME_API_LIBCAMERA_SOURCE)) < 0) {
+ printf("can't create libcamera-source: %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_device, SPA_POD_String(device ? device : "/dev/media0"));
+
+ 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_YUY2,
+ 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_YUY2,
+ .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_YUY2,
+ 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(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)
+{
+ int res, err;
+ 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;
+ }
+ }
+
+ if (data->running) {
+ data->running = false;
+ pthread_join(data->thread, NULL);
+ }
+
+ 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);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ int res;
+ const char *str;
+ struct spa_handle *handle = NULL;
+ void *iface;
+
+ 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);
+
+ 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;
+
+ 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);
+
+ 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 ((res = make_nodes(&data, argv[1])) < 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);
+ run_async_source(&data);
+ spa_loop_control_leave(data.control);
+
+ SDL_DestroyRenderer(data.renderer);
+
+ return 0;
+}
diff --git a/spa/examples/local-v4l2.c b/spa/examples/local-v4l2.c
new file mode 100644
index 0000000..bd6de7b
--- /dev/null
+++ b/spa/examples/local-v4l2.c
@@ -0,0 +1,553 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Example using libspa-v4l2, 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>
+
+static SPA_LOG_IMPL(default_log);
+
+#define MAX_BUFFERS 8
+
+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_support support[5];
+ 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 -ENOENT;
+ }
+ free(path);
+ if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ printf("can't find enum function\n");
+ return -ENOENT;
+ }
+
+ 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 (factory->version < 1)
+ continue;
+ 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);
+
+ SDL_RenderClear(data->renderer);
+ SDL_RenderCopy(data->renderer, texture, NULL, NULL);
+ SDL_RenderPresent(data->renderer);
+
+ if (SDL_LockTexture(texture, NULL, &sdata, &sstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ return -EIO;
+ }
+ } 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 + datas[0].mapoffset, PROT_READ,
+ MAP_PRIVATE, datas[0].fd, 0);
+ if (map == MAP_FAILED)
+ return -errno;
+ sdata = SPA_PTROFF(map, datas[0].mapoffset, uint8_t);
+ } 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 < 240; 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 + datas[0].mapoffset);
+ }
+
+ 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, const char *device)
+{
+ 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,
+ "v4l2/libspa-v4l2.so",
+ SPA_NAME_API_V4L2_SOURCE)) < 0) {
+ printf("can't create v4l2-source: %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_device, SPA_POD_String(device ? device : "/dev/video0"));
+
+ 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 = 0;
+ 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_YUY2,
+ SDL_TEXTUREACCESS_STREAMING, 320, 240);
+ 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_MemPtr;
+ b->datas[0].maxsize = stride * 240;
+ b->datas[0].data = ptr;
+ b->datas[0].chunk->offset = 0;
+ b->datas[0].chunk->size = stride * 240;
+ 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_YUY2,
+ .size = SPA_RECTANGLE(320, 240),
+ .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_YUY2,
+ SDL_TEXTUREACCESS_STREAMING, 320, 240);
+ 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(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)
+{
+ int res, err;
+ struct spa_command cmd;
+ SDL_Event event;
+ bool running = true;
+
+ 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;
+ }
+ }
+
+ if (data->running) {
+ data->running = false;
+ pthread_join(data->thread, NULL);
+ }
+
+ spa_loop_control_enter(data->control);
+
+ 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;
+
+ 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);
+
+ 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;
+
+ data.use_buffer = true;
+
+ 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);
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ printf("can't initialize SDL: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ if (SDL_CreateWindowAndRenderer
+ (320, 240, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) {
+ printf("can't create window: %s\n", SDL_GetError());
+ return -1;
+ }
+
+
+ if ((res = make_nodes(&data, argv[1])) < 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);
+ run_async_source(&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
new file mode 100644
index 0000000..7064a06
--- /dev/null
+++ b/spa/examples/meson.build
@@ -0,0 +1,32 @@
+# Examples, in order from simple to complicated
+spa_examples = [
+ 'adapter-control',
+ 'example-control',
+ 'local-libcamera',
+ 'local-v4l2',
+]
+
+spa_examples_extra_deps = {
+ 'local-v4l2': [sdl_dep],
+ 'local-libcamera': [sdl_dep, libcamera_dep],
+}
+
+foreach c : spa_examples
+ deps = spa_examples_extra_deps.get(c, [])
+
+ found = true
+ foreach dep : deps
+ found = found and dep.found()
+ endforeach
+
+ if found
+ executable(
+ c,
+ c + '.c',
+ include_directories : [configinc],
+ dependencies : [spa_dep, dl_lib, pthread_lib, mathlib] + deps,
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'examples' / 'spa'
+ )
+ endif
+endforeach
diff --git a/spa/include/meson.build b/spa/include/meson.build
new file mode 100644
index 0000000..443db7d
--- /dev/null
+++ b/spa/include/meson.build
@@ -0,0 +1,18 @@
+spa_sections = [
+ 'buffer',
+ 'control',
+ 'debug',
+ 'graph',
+ 'interfaces',
+ 'monitor',
+ 'node',
+ 'param',
+ 'pod',
+ 'support',
+ 'utils',
+]
+
+spa_headers = 'spa' # used by doxygen
+install_subdir('spa',
+ install_dir : get_option('includedir') / spa_name
+)
diff --git a/spa/include/spa/buffer/alloc.h b/spa/include/spa/buffer/alloc.h
new file mode 100644
index 0000000..ec85541
--- /dev/null
+++ b/spa/include/spa/buffer/alloc.h
@@ -0,0 +1,347 @@
+/* Simple Plugin API
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef SPA_BUFFER_ALLOC_H
+#define SPA_BUFFER_ALLOC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/buffer/buffer.h>
+
+/**
+ * \addtogroup spa_buffer
+ * \{
+ */
+
+/** information about the buffer layout */
+struct spa_buffer_alloc_info {
+#define SPA_BUFFER_ALLOC_FLAG_INLINE_META (1<<0) /**< add metadata data in the skeleton */
+#define SPA_BUFFER_ALLOC_FLAG_INLINE_CHUNK (1<<1) /**< add chunk data in the skeleton */
+#define SPA_BUFFER_ALLOC_FLAG_INLINE_DATA (1<<2) /**< add buffer data to the skeleton */
+#define SPA_BUFFER_ALLOC_FLAG_INLINE_ALL 0b111
+#define SPA_BUFFER_ALLOC_FLAG_NO_DATA (1<<3) /**< don't set data pointers */
+ uint32_t flags;
+ uint32_t max_align; /**< max of all alignments */
+ uint32_t n_metas;
+ uint32_t n_datas;
+ struct spa_meta *metas;
+ struct spa_data *datas;
+ uint32_t *data_aligns;
+ size_t skel_size; /**< size of the struct spa_buffer and inlined meta/chunk/data */
+ size_t meta_size; /**< size of the meta if not inlined */
+ size_t chunk_size; /**< size of the chunk if not inlined */
+ size_t data_size; /**< size of the data if not inlined */
+ size_t mem_size; /**< size of the total memory if not inlined */
+};
+
+/**
+ * Fill buffer allocation information
+ *
+ * Fill \a info with allocation information needed to allocate buffers
+ * with the given number of metadata and data members.
+ *
+ * The required size of the skeleton (the struct spa_buffer) information
+ * and the memory (for the metadata, chunk and buffer memory) will be
+ * calculated.
+ *
+ * The flags member in \a info should be configured before calling this
+ * functions.
+ *
+ * \param info the information to fill
+ * \param n_metas the number of metadatas for the buffer
+ * \param metas an array of metadata items
+ * \param n_datas the number of datas for the buffer
+ * \param datas an array of \a n_datas items
+ * \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,
+ uint32_t n_metas, struct spa_meta metas[],
+ uint32_t n_datas, struct spa_data datas[],
+ uint32_t data_aligns[])
+{
+ size_t size, *target;
+ uint32_t i;
+
+ info->n_metas = n_metas;
+ info->metas = metas;
+ info->n_datas = n_datas;
+ info->datas = datas;
+ info->data_aligns = data_aligns;
+ info->max_align = 16;
+ info->mem_size = 0;
+ /*
+ * The buffer skeleton is placed in memory like below and can
+ * be accessed as a regular structure.
+ *
+ * +==============================+
+ * | struct spa_buffer |
+ * | uint32_t n_metas | number of metas
+ * | uint32_t n_datas | number of datas
+ * +-| struct spa_meta *metas | pointer to array of metas
+ * +|-| struct spa_data *datas | pointer to array of datas
+ * || +------------------------------+
+ * |+>| struct spa_meta |
+ * | | uint32_t type | metadata
+ * | | uint32_t size | size of metadata
+ * +|--| void *data | pointer to metadata
+ * || | ... <n_metas> | more spa_meta follow
+ * || +------------------------------+
+ * |+->| struct spa_data |
+ * | | uint32_t type | memory type
+ * | | uint32_t flags |
+ * | | int fd | fd of shared memory block
+ * | | uint32_t mapoffset | offset in shared memory of data
+ * | | uint32_t maxsize | size of data block
+ * | +-| void *data | pointer to data
+ * |+|-| struct spa_chunk *chunk | pointer to chunk
+ * ||| | ... <n_datas> | more spa_data follow
+ * ||| +==============================+
+ * VVV
+ *
+ * metadata, chunk and memory can either be placed right
+ * after the skeleton (inlined) or in a separate piece of memory.
+ *
+ * vvv
+ * ||| +==============================+
+ * +-->| meta data memory | metadata memory, 8 byte aligned
+ * || | ... <n_metas> |
+ * || +------------------------------+
+ * +->| struct spa_chunk | memory for n_datas chunks
+ * | | uint32_t offset |
+ * | | uint32_t size |
+ * | | int32_t stride |
+ * | | int32_t dummy |
+ * | | ... <n_datas> chunks |
+ * | +------------------------------+
+ * +>| data | memory for n_datas data, aligned
+ * | ... <n_datas> blocks | according to alignments
+ * +==============================+
+ */
+ info->skel_size = sizeof(struct spa_buffer);
+ info->skel_size += n_metas * sizeof(struct spa_meta);
+ info->skel_size += n_datas * sizeof(struct spa_data);
+
+ for (i = 0, size = 0; i < n_metas; i++)
+ size += SPA_ROUND_UP_N(metas[i].size, 8);
+ info->meta_size = size;
+
+ if (SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_META))
+ target = &info->skel_size;
+ else
+ target = &info->mem_size;
+ *target += info->meta_size;
+
+ info->chunk_size = n_datas * sizeof(struct spa_chunk);
+ if (SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_CHUNK))
+ target = &info->skel_size;
+ else
+ target = &info->mem_size;
+ *target += info->chunk_size;
+
+ for (i = 0, size = 0; i < n_datas; i++) {
+ int64_t align = data_aligns[i];
+ info->max_align = SPA_MAX(info->max_align, data_aligns[i]);
+ size = SPA_ROUND_UP_N(size, align);
+ size += datas[i].maxsize;
+ }
+ info->data_size = size;
+
+ if (!SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_NO_DATA) &&
+ SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_DATA))
+ target = &info->skel_size;
+ else
+ target = &info->mem_size;
+
+ *target = SPA_ROUND_UP_N(*target, n_datas ? data_aligns[0] : 1);
+ *target += info->data_size;
+ *target = SPA_ROUND_UP_N(*target, info->max_align);
+
+ return 0;
+}
+
+/**
+ * Fill skeleton and data according to the allocation info
+ *
+ * Use the allocation info to create a struct \ref spa_buffer into
+ * \a skel_mem and \a data_mem.
+ *
+ * Depending on the flags given when calling \ref
+ * spa_buffer_alloc_fill_info(), the buffer meta, chunk and memory
+ * will be referenced in either skel_mem or data_mem.
+ *
+ * \param info an allocation info
+ * \param skel_mem memory to hold the struct \ref spa_buffer and the
+ * pointers to meta, chunk and memory.
+ * \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_buffer_alloc_layout(struct spa_buffer_alloc_info *info,
+ void *skel_mem, void *data_mem)
+{
+ struct spa_buffer *b = (struct spa_buffer*)skel_mem;
+ size_t size;
+ uint32_t i;
+ void **dp, *skel, *data;
+ struct spa_chunk *cp;
+
+ b->n_metas = info->n_metas;
+ b->metas = SPA_PTROFF(b, sizeof(struct spa_buffer), struct spa_meta);
+ b->n_datas = info->n_datas;
+ b->datas = SPA_PTROFF(b->metas, info->n_metas * sizeof(struct spa_meta), struct spa_data);
+
+ skel = SPA_PTROFF(b->datas, info->n_datas * sizeof(struct spa_data), void);
+ data = data_mem;
+
+ if (SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_META))
+ dp = &skel;
+ else
+ dp = &data;
+
+ for (i = 0; i < info->n_metas; i++) {
+ struct spa_meta *m = &b->metas[i];
+ *m = info->metas[i];
+ m->data = *dp;
+ *dp = SPA_PTROFF(*dp, SPA_ROUND_UP_N(m->size, 8), void);
+ }
+
+ size = info->n_datas * sizeof(struct spa_chunk);
+ if (SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_CHUNK)) {
+ cp = (struct spa_chunk*)skel;
+ skel = SPA_PTROFF(skel, size, void);
+ }
+ else {
+ cp = (struct spa_chunk*)data;
+ data = SPA_PTROFF(data, size, void);
+ }
+
+ if (SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_DATA))
+ dp = &skel;
+ else
+ dp = &data;
+
+ for (i = 0; i < info->n_datas; i++) {
+ struct spa_data *d = &b->datas[i];
+
+ *d = info->datas[i];
+ d->chunk = &cp[i];
+ if (!SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_NO_DATA)) {
+ *dp = SPA_PTR_ALIGN(*dp, info->data_aligns[i], void);
+ d->data = *dp;
+ *dp = SPA_PTROFF(*dp, d->maxsize, void);
+ }
+ }
+ return b;
+}
+
+/**
+ * Layout an array of buffers
+ *
+ * Use the allocation info to layout the memory of an array of buffers.
+ *
+ * \a skel_mem should point to at least info->skel_size * \a n_buffers bytes
+ * of memory.
+ * \a data_mem should point to at least info->mem_size * \a n_buffers bytes
+ * of memory.
+ *
+ * \param info the allocation info for one buffer
+ * \param n_buffers the number of buffers to create
+ * \param buffers a array with space to hold \a n_buffers pointers to buffers
+ * \param skel_mem memory for the struct \ref spa_buffer
+ * \param data_mem memory for the meta, chunk, memory of the buffer if not
+ * inlined in the skeleton.
+ * \return 0 on success.
+ *
+ */
+static inline 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)
+{
+ uint32_t i;
+ for (i = 0; i < n_buffers; i++) {
+ buffers[i] = spa_buffer_alloc_layout(info, skel_mem, data_mem);
+ skel_mem = SPA_PTROFF(skel_mem, info->skel_size, void);
+ data_mem = SPA_PTROFF(data_mem, info->mem_size, void);
+ }
+ return 0;
+}
+
+/**
+ * Allocate an array of buffers
+ *
+ * Allocate \a n_buffers with the given metadata, memory and alignment
+ * information.
+ *
+ * The buffer array, structures, data and metadata will all be allocated
+ * in one block of memory with the proper requested alignment.
+ *
+ * \param n_buffers the number of buffers to create
+ * \param flags extra flags
+ * \param n_metas number of metadatas
+ * \param metas \a n_metas metadata specification
+ * \param n_datas number of datas
+ * \param datas \a n_datas memory specification
+ * \param data_aligns \a n_datas alignment specifications
+ * \returns an array of \a n_buffers pointers to struct \ref spa_buffer
+ * with the given metadata, data and alignment or NULL when
+ * allocation failed.
+ *
+ */
+static inline 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[],
+ uint32_t data_aligns[])
+{
+
+ struct spa_buffer **buffers;
+ struct spa_buffer_alloc_info info = { flags | SPA_BUFFER_ALLOC_FLAG_INLINE_ALL, };
+ void *skel;
+
+ spa_buffer_alloc_fill_info(&info, n_metas, metas, n_datas, datas, data_aligns);
+
+ buffers = (struct spa_buffer **)calloc(1, info.max_align +
+ n_buffers * (sizeof(struct spa_buffer *) + info.skel_size));
+ if (buffers == NULL)
+ return NULL;
+
+ skel = SPA_PTROFF(buffers, sizeof(struct spa_buffer *) * n_buffers, void);
+ skel = SPA_PTR_ALIGN(skel, info.max_align, void);
+
+ spa_buffer_alloc_layout_array(&info, n_buffers, buffers, skel, NULL);
+
+ return buffers;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_BUFFER_ALLOC_H */
diff --git a/spa/include/spa/buffer/buffer.h b/spa/include/spa/buffer/buffer.h
new file mode 100644
index 0000000..47204ac
--- /dev/null
+++ b/spa/include/spa/buffer/buffer.h
@@ -0,0 +1,131 @@
+/* Simple Plugin API
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BUFFER_H
+#define SPA_BUFFER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/buffer/meta.h>
+
+/** \defgroup spa_buffer Buffers
+ *
+ * Buffers describe the data and metadata that is exchanged between
+ * ports of a node.
+ */
+
+/**
+ * \addtogroup spa_buffer
+ * \{
+ */
+
+enum spa_data_type {
+ SPA_DATA_Invalid,
+ SPA_DATA_MemPtr, /**< pointer to memory, the data field in
+ * struct spa_data is set. */
+ SPA_DATA_MemFd, /**< generic fd, mmap to get to memory */
+ SPA_DATA_DmaBuf, /**< fd to dmabuf memory */
+ SPA_DATA_MemId, /**< memory is identified with an id */
+
+ _SPA_DATA_LAST, /**< not part of ABI */
+};
+
+/** Chunk of memory, can change for each buffer */
+struct spa_chunk {
+ uint32_t offset; /**< offset of valid data. Should be taken
+ * modulo the data maxsize to get the offset
+ * in the data memory. */
+ uint32_t size; /**< size of valid data. Should be clamped to
+ * maxsize. */
+ int32_t stride; /**< stride of valid data */
+#define SPA_CHUNK_FLAG_NONE 0
+#define SPA_CHUNK_FLAG_CORRUPTED (1u<<0) /**< chunk data is corrupted in some way */
+#define SPA_CHUNK_FLAG_EMPTY (1u<<1) /**< chunk data is empty with media specific
+ * neutral data such as silence or black. This
+ * could be used to optimize processing. */
+ int32_t flags; /**< chunk flags */
+};
+
+/** Data for a buffer this stays constant for a buffer */
+struct spa_data {
+ uint32_t type; /**< memory type, one of enum spa_data_type, when
+ * allocating memory, the type contains a bitmask
+ * of allowed types. SPA_ID_INVALID is a special
+ * value for the allocator to indicate that the
+ * other side did not explicitly specify any
+ * supported data types. It should probably use
+ * a memory type that does not require special
+ * handling in addition to simple mmap/munmap. */
+#define SPA_DATA_FLAG_NONE 0
+#define SPA_DATA_FLAG_READABLE (1u<<0) /**< data is readable */
+#define SPA_DATA_FLAG_WRITABLE (1u<<1) /**< data is writable */
+#define SPA_DATA_FLAG_DYNAMIC (1u<<2) /**< data pointer can be changed */
+#define SPA_DATA_FLAG_READWRITE (SPA_DATA_FLAG_READABLE|SPA_DATA_FLAG_WRITABLE)
+ uint32_t flags; /**< data flags */
+ int64_t fd; /**< optional fd for data */
+ uint32_t mapoffset; /**< offset to map fd at */
+ uint32_t maxsize; /**< max size of data */
+ void *data; /**< optional data pointer */
+ struct spa_chunk *chunk; /**< valid chunk of memory */
+};
+
+/** A Buffer */
+struct spa_buffer {
+ uint32_t n_metas; /**< number of metadata */
+ uint32_t n_datas; /**< number of data members */
+ struct spa_meta *metas; /**< array of metadata */
+ struct spa_data *datas; /**< array of data members */
+};
+
+/** Find metadata in a buffer */
+static inline struct spa_meta *spa_buffer_find_meta(const struct spa_buffer *b, uint32_t type)
+{
+ uint32_t i;
+
+ for (i = 0; i < b->n_metas; i++)
+ if (b->metas[i].type == type)
+ return &b->metas[i];
+
+ return NULL;
+}
+
+static inline 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)
+ return m->data;
+ return NULL;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_BUFFER_H */
diff --git a/spa/include/spa/buffer/meta.h b/spa/include/spa/buffer/meta.h
new file mode 100644
index 0000000..dbd1446
--- /dev/null
+++ b/spa/include/spa/buffer/meta.h
@@ -0,0 +1,192 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_META_H
+#define SPA_META_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/pod/pod.h>
+
+/**
+ * \addtogroup spa_buffer
+ * \{
+ */
+
+enum spa_meta_type {
+ SPA_META_Invalid,
+ SPA_META_Header, /**< struct spa_meta_header */
+ SPA_META_VideoCrop, /**< struct spa_meta_region with cropping data */
+ SPA_META_VideoDamage, /**< array of struct spa_meta_region with damage, where an invalid entry or end-of-array marks the end. */
+ SPA_META_Bitmap, /**< struct spa_meta_bitmap */
+ SPA_META_Cursor, /**< struct spa_meta_cursor */
+ SPA_META_Control, /**< metadata contains a spa_meta_control
+ * associated with the data */
+ SPA_META_Busy, /**< don't write to buffer when count > 0 */
+ SPA_META_VideoTransform, /**< struct spa_meta_transform */
+
+ _SPA_META_LAST, /**< not part of ABI/API */
+};
+
+/**
+ * A metadata element.
+ *
+ * This structure is available on the buffer structure and contains
+ * the type of the metadata and a pointer/size to the actual metadata
+ * itself.
+ */
+struct spa_meta {
+ uint32_t type; /**< metadata type, one of enum spa_meta_type */
+ uint32_t size; /**< size of metadata */
+ void *data; /**< pointer to metadata */
+};
+
+static inline void *spa_meta_first(const struct spa_meta *m) {
+ return m->data;
+}
+#define spa_meta_first spa_meta_first
+static inline void *spa_meta_end(const struct spa_meta *m) {
+ return SPA_PTROFF(m->data,m->size,void);
+}
+#define spa_meta_end spa_meta_end
+#define spa_meta_check(p,m) (SPA_PTROFF(p,sizeof(*(p)),void) <= spa_meta_end(m))
+
+/**
+ * Describes essential buffer header metadata such as flags and
+ * timestamps.
+ */
+struct spa_meta_header {
+#define SPA_META_HEADER_FLAG_DISCONT (1 << 0) /**< data is not continuous with previous buffer */
+#define SPA_META_HEADER_FLAG_CORRUPTED (1 << 1) /**< data might be corrupted */
+#define SPA_META_HEADER_FLAG_MARKER (1 << 2) /**< media specific marker */
+#define SPA_META_HEADER_FLAG_HEADER (1 << 3) /**< data contains a codec specific header */
+#define SPA_META_HEADER_FLAG_GAP (1 << 4) /**< data contains media neutral data */
+#define SPA_META_HEADER_FLAG_DELTA_UNIT (1 << 5) /**< cannot be decoded independently */
+ uint32_t flags; /**< flags */
+ uint32_t offset; /**< offset in current cycle */
+ int64_t pts; /**< presentation timestamp in nanoseconds */
+ int64_t dts_offset; /**< decoding timestamp as a difference with pts */
+ uint64_t seq; /**< sequence number, increments with a
+ * media specific frequency */
+};
+
+/** metadata structure for Region or an array of these for RegionArray */
+struct spa_meta_region {
+ struct spa_region region;
+};
+
+static inline bool spa_meta_region_is_valid(const struct spa_meta_region *m) {
+ return m->region.size.width != 0 && m->region.size.height != 0;
+}
+#define spa_meta_region_is_valid spa_meta_region_is_valid
+
+/** iterate all the items in a metadata */
+#define spa_meta_for_each(pos,meta) \
+ for ((pos) = (__typeof(pos))spa_meta_first(meta); \
+ spa_meta_check(pos, meta); \
+ (pos)++)
+
+#define spa_meta_bitmap_is_valid(m) ((m)->format != 0)
+
+/**
+ * Bitmap information
+ *
+ * This metadata contains a bitmap image in the given format and size.
+ * It is typically used for cursor images or other small images that are
+ * better transferred inline.
+ */
+struct spa_meta_bitmap {
+ uint32_t format; /**< bitmap video format, one of enum spa_video_format. 0 is
+ * and invalid format and should be handled as if there is
+ * no new bitmap information. */
+ struct spa_rectangle size; /**< width and height of bitmap */
+ int32_t stride; /**< stride of bitmap data */
+ uint32_t offset; /**< offset of bitmap data in this structure. An offset of
+ * 0 means no image data (invisible), an offset >=
+ * sizeof(struct spa_meta_bitmap) contains valid bitmap
+ * info. */
+};
+
+#define spa_meta_cursor_is_valid(m) ((m)->id != 0)
+
+/**
+ * Cursor information
+ *
+ * Metadata to describe the position and appearance of a pointing device.
+ */
+struct spa_meta_cursor {
+ uint32_t id; /**< cursor id. an id of 0 is an invalid id and means that
+ * there is no new cursor data */
+ uint32_t flags; /**< extra flags */
+ struct spa_point position; /**< position on screen */
+ struct spa_point hotspot; /**< offsets for hotspot in bitmap, this field has no meaning
+ * when there is no valid bitmap (see below) */
+ uint32_t bitmap_offset; /**< offset of bitmap meta in this structure. When the offset
+ * is 0, there is no new bitmap information. When the offset is
+ * >= sizeof(struct spa_meta_cursor) there is a
+ * struct spa_meta_bitmap at the offset. */
+};
+
+/** a timed set of events associated with the buffer */
+struct spa_meta_control {
+ struct spa_pod_sequence sequence;
+};
+
+/** a busy counter for the buffer */
+struct spa_meta_busy {
+ uint32_t flags;
+ uint32_t count; /**< number of users busy with the buffer */
+};
+
+enum spa_meta_videotransform_value {
+ SPA_META_TRANSFORMATION_None = 0, /**< no transform */
+ SPA_META_TRANSFORMATION_90, /**< 90 degree counter-clockwise */
+ SPA_META_TRANSFORMATION_180, /**< 180 degree counter-clockwise */
+ SPA_META_TRANSFORMATION_270, /**< 270 degree counter-clockwise */
+ SPA_META_TRANSFORMATION_Flipped, /**< 180 degree flipped around the vertical axis. Equivalent
+ * to a reflexion through the vertical line splitting the
+ * bufffer in two equal sized parts */
+ SPA_META_TRANSFORMATION_Flipped90, /**< flip then rotate around 90 degree counter-clockwise */
+ SPA_META_TRANSFORMATION_Flipped180, /**< flip then rotate around 180 degree counter-clockwise */
+ SPA_META_TRANSFORMATION_Flipped270, /**< flip then rotate around 270 degree counter-clockwise */
+};
+
+/** a transformation of the buffer */
+struct spa_meta_videotransform {
+ uint32_t transform; /**< orientation transformation that was applied to the buffer,
+ * one of enum spa_meta_videotransform_value */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_META_H */
diff --git a/spa/include/spa/buffer/type-info.h b/spa/include/spa/buffer/type-info.h
new file mode 100644
index 0000000..8b38c49
--- /dev/null
+++ b/spa/include/spa/buffer/type-info.h
@@ -0,0 +1,94 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BUFFER_TYPES_H
+#define SPA_BUFFER_TYPES_H
+
+/**
+ * \addtogroup spa_buffer
+ * \{
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/buffer/buffer.h>
+#include <spa/buffer/meta.h>
+#include <spa/utils/type.h>
+
+#define SPA_TYPE_INFO_Buffer SPA_TYPE_INFO_POINTER_BASE "Buffer"
+#define SPA_TYPE_INFO_BUFFER_BASE SPA_TYPE_INFO_Buffer ":"
+
+/** Buffers contain data of a certain type */
+#define SPA_TYPE_INFO_Data SPA_TYPE_INFO_ENUM_BASE "Data"
+#define SPA_TYPE_INFO_DATA_BASE SPA_TYPE_INFO_Data ":"
+
+/** base type for fd based memory */
+#define SPA_TYPE_INFO_DATA_Fd SPA_TYPE_INFO_DATA_BASE "Fd"
+#define SPA_TYPE_INFO_DATA_FD_BASE SPA_TYPE_INFO_DATA_Fd ":"
+
+static const struct spa_type_info spa_type_data_type[] = {
+ { SPA_DATA_Invalid, SPA_TYPE_Int, SPA_TYPE_INFO_DATA_BASE "Invalid", NULL },
+ { SPA_DATA_MemPtr, SPA_TYPE_Int, SPA_TYPE_INFO_DATA_BASE "MemPtr", NULL },
+ { SPA_DATA_MemFd, SPA_TYPE_Int, SPA_TYPE_INFO_DATA_FD_BASE "MemFd", NULL },
+ { SPA_DATA_DmaBuf, SPA_TYPE_Int, SPA_TYPE_INFO_DATA_FD_BASE "DmaBuf", NULL },
+ { SPA_DATA_MemId, SPA_TYPE_Int, SPA_TYPE_INFO_DATA_BASE "MemId", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_Meta SPA_TYPE_INFO_POINTER_BASE "Meta"
+#define SPA_TYPE_INFO_META_BASE SPA_TYPE_INFO_Meta ":"
+
+#define SPA_TYPE_INFO_META_Array SPA_TYPE_INFO_META_BASE "Array"
+#define SPA_TYPE_INFO_META_ARRAY_BASE SPA_TYPE_INFO_META_Array ":"
+
+#define SPA_TYPE_INFO_META_Region SPA_TYPE_INFO_META_BASE "Region"
+#define SPA_TYPE_INFO_META_REGION_BASE SPA_TYPE_INFO_META_Region ":"
+
+#define SPA_TYPE_INFO_META_ARRAY_Region SPA_TYPE_INFO_META_ARRAY_BASE "Region"
+#define SPA_TYPE_INFO_META_ARRAY_REGION_BASE SPA_TYPE_INFO_META_ARRAY_Region ":"
+
+static const struct spa_type_info spa_type_meta_type[] = {
+ { SPA_META_Invalid, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Invalid", NULL },
+ { SPA_META_Header, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Header", NULL },
+ { SPA_META_VideoCrop, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_REGION_BASE "VideoCrop", NULL },
+ { SPA_META_VideoDamage, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_ARRAY_REGION_BASE "VideoDamage", NULL },
+ { SPA_META_Bitmap, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Bitmap", NULL },
+ { SPA_META_Cursor, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Cursor", NULL },
+ { SPA_META_Control, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Control", NULL },
+ { SPA_META_Busy, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Busy", NULL },
+ { SPA_META_VideoTransform, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "VideoTransform", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_BUFFER_TYPES_H */
diff --git a/spa/include/spa/control/control.h b/spa/include/spa/control/control.h
new file mode 100644
index 0000000..931ae01
--- /dev/null
+++ b/spa/include/spa/control/control.h
@@ -0,0 +1,62 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_CONTROL_H
+#define SPA_CONTROL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/pod/pod.h>
+
+/** \defgroup spa_control Control
+ * Control type declarations
+ */
+
+/**
+ * \addtogroup spa_control
+ * \{
+ */
+
+/** 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_LAST, /**< not part of ABI */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_CONTROL_H */
diff --git a/spa/include/spa/control/type-info.h b/spa/include/spa/control/type-info.h
new file mode 100644
index 0000000..d703783
--- /dev/null
+++ b/spa/include/spa/control/type-info.h
@@ -0,0 +1,61 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_CONTROL_TYPES_H
+#define SPA_CONTROL_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_control
+ * \{
+ */
+
+#include <spa/utils/defs.h>
+#include <spa/utils/type-info.h>
+#include <spa/control/control.h>
+
+/* base for parameter object enumerations */
+#define SPA_TYPE_INFO_Control SPA_TYPE_INFO_ENUM_BASE "Control"
+#define SPA_TYPE_INFO_CONTROL_BASE SPA_TYPE_INFO_Control ":"
+
+static const struct spa_type_info spa_type_control[] = {
+ { SPA_CONTROL_Invalid, SPA_TYPE_Int, SPA_TYPE_INFO_CONTROL_BASE "Invalid", NULL },
+ { 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 },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_CONTROL_TYPES_H */
diff --git a/spa/include/spa/debug/buffer.h b/spa/include/spa/debug/buffer.h
new file mode 100644
index 0000000..a1cd141
--- /dev/null
+++ b/spa/include/spa/debug/buffer.h
@@ -0,0 +1,133 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_BUFFER_H
+#define SPA_DEBUG_BUFFER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup spa_debug Debug
+ * Debugging utilities
+ */
+
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+#include <spa/debug/context.h>
+#include <spa/debug/mem.h>
+#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)
+{
+ uint32_t i;
+
+ spa_debugc(ctx, "%*s" "struct spa_buffer %p:", indent, "", buffer);
+ spa_debugc(ctx, "%*s" " n_metas: %u (at %p)", indent, "", buffer->n_metas, buffer->metas);
+ for (i = 0; i < buffer->n_metas; i++) {
+ struct spa_meta *m = &buffer->metas[i];
+ const char *type_name;
+
+ type_name = spa_debug_type_find_name(spa_type_meta_type, m->type);
+ spa_debugc(ctx, "%*s" " meta %d: type %d (%s), data %p, size %d:", indent, "", i, m->type,
+ type_name, m->data, m->size);
+
+ switch (m->type) {
+ case SPA_META_Header:
+ {
+ struct spa_meta_header *h = (struct spa_meta_header*)m->data;
+ spa_debugc(ctx, "%*s" " struct spa_meta_header:", indent, "");
+ spa_debugc(ctx, "%*s" " flags: %08x", indent, "", h->flags);
+ spa_debugc(ctx, "%*s" " offset: %u", indent, "", h->offset);
+ spa_debugc(ctx, "%*s" " seq: %" PRIu64, indent, "", h->seq);
+ spa_debugc(ctx, "%*s" " pts: %" PRIi64, indent, "", h->pts);
+ spa_debugc(ctx, "%*s" " dts_offset: %" PRIi64, indent, "", h->dts_offset);
+ break;
+ }
+ case SPA_META_VideoCrop:
+ {
+ struct spa_meta_region *h = (struct spa_meta_region*)m->data;
+ spa_debugc(ctx, "%*s" " struct spa_meta_region:", indent, "");
+ spa_debugc(ctx, "%*s" " x: %d", indent, "", h->region.position.x);
+ spa_debugc(ctx, "%*s" " y: %d", indent, "", h->region.position.y);
+ spa_debugc(ctx, "%*s" " width: %d", indent, "", h->region.size.width);
+ spa_debugc(ctx, "%*s" " height: %d", indent, "", h->region.size.height);
+ break;
+ }
+ case SPA_META_VideoDamage:
+ {
+ struct spa_meta_region *h;
+ spa_meta_for_each(h, m) {
+ spa_debugc(ctx, "%*s" " struct spa_meta_region:", indent, "");
+ spa_debugc(ctx, "%*s" " x: %d", indent, "", h->region.position.x);
+ spa_debugc(ctx, "%*s" " y: %d", indent, "", h->region.position.y);
+ spa_debugc(ctx, "%*s" " width: %d", indent, "", h->region.size.width);
+ spa_debugc(ctx, "%*s" " height: %d", indent, "", h->region.size.height);
+ }
+ break;
+ }
+ case SPA_META_Bitmap:
+ break;
+ case SPA_META_Cursor:
+ break;
+ default:
+ spa_debugc(ctx, "%*s" " Unknown:", indent, "");
+ spa_debugc_mem(ctx, 5, m->data, m->size);
+ }
+ }
+ spa_debugc(ctx, "%*s" " n_datas: \t%u (at %p)", indent, "", buffer->n_datas, buffer->datas);
+ for (i = 0; i < buffer->n_datas; i++) {
+ struct spa_data *d = &buffer->datas[i];
+ spa_debugc(ctx, "%*s" " type: %d (%s)", indent, "", d->type,
+ spa_debug_type_find_name(spa_type_data_type, d->type));
+ spa_debugc(ctx, "%*s" " flags: %d", indent, "", d->flags);
+ spa_debugc(ctx, "%*s" " data: %p", indent, "", d->data);
+ spa_debugc(ctx, "%*s" " fd: %" PRIi64, indent, "", d->fd);
+ spa_debugc(ctx, "%*s" " offset: %d", indent, "", d->mapoffset);
+ spa_debugc(ctx, "%*s" " maxsize: %u", indent, "", d->maxsize);
+ spa_debugc(ctx, "%*s" " chunk: %p", indent, "", d->chunk);
+ spa_debugc(ctx, "%*s" " offset: %d", indent, "", d->chunk->offset);
+ spa_debugc(ctx, "%*s" " size: %u", indent, "", d->chunk->size);
+ spa_debugc(ctx, "%*s" " stride: %d", indent, "", d->chunk->stride);
+ }
+ return 0;
+}
+
+static inline int spa_debug_buffer(int indent, const struct spa_buffer *buffer)
+{
+ return spa_debugc_buffer(NULL, indent, buffer);
+}
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_BUFFER_H */
diff --git a/spa/include/spa/debug/context.h b/spa/include/spa/debug/context.h
new file mode 100644
index 0000000..945e502
--- /dev/null
+++ b/spa/include/spa/debug/context.h
@@ -0,0 +1,62 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_CONTEXT_H
+#define SPA_DEBUG_CONTEXT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+
+#include <spa/utils/defs.h>
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+#ifndef spa_debugn
+#define spa_debugn(_fmt,...) printf((_fmt), ## __VA_ARGS__)
+#endif
+#ifndef spa_debug
+#define spa_debug(_fmt,...) spa_debugn(_fmt"\n", ## __VA_ARGS__)
+#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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_CONTEXT_H */
diff --git a/spa/include/spa/debug/dict.h b/spa/include/spa/debug/dict.h
new file mode 100644
index 0000000..ad47ad6
--- /dev/null
+++ b/spa/include/spa/debug/dict.h
@@ -0,0 +1,62 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_DICT_H
+#define SPA_DEBUG_DICT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+#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)
+{
+ const struct spa_dict_item *item;
+ spa_debugc(ctx, "%*sflags:%08x n_items:%d", indent, "", dict->flags, dict->n_items);
+ spa_dict_for_each(item, dict) {
+ spa_debugc(ctx, "%*s %s = \"%s\"", indent, "", item->key, item->value);
+ }
+ return 0;
+}
+
+static inline int spa_debug_dict(int indent, const struct spa_dict *dict)
+{
+ return spa_debugc_dict(NULL, indent, dict);
+}
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_DICT_H */
diff --git a/spa/include/spa/debug/format.h b/spa/include/spa/debug/format.h
new file mode 100644
index 0000000..e0bd955
--- /dev/null
+++ b/spa/include/spa/debug/format.h
@@ -0,0 +1,234 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_FORMAT_H
+#define SPA_DEBUG_FORMAT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/utils/string.h>
+#include <spa/debug/context.h>
+#include <spa/debug/types.h>
+#include <spa/param/type-info.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_debug_strbuf_format_value(struct spa_strbuf *buffer, const struct spa_type_info *info,
+ uint32_t type, void *body, uint32_t size)
+{
+
+ switch (type) {
+ case SPA_TYPE_Bool:
+ spa_strbuf_append(buffer, "%s", *(int32_t *) body ? "true" : "false");
+ break;
+ case SPA_TYPE_Id:
+ {
+ const char *str = spa_debug_type_find_short_name(info, *(int32_t *) body);
+ char tmp[64];
+ if (str == NULL) {
+ snprintf(tmp, sizeof(tmp), "%d", *(int32_t*)body);
+ str = tmp;
+ }
+ spa_strbuf_append(buffer, "%s", str);
+ break;
+ }
+ case SPA_TYPE_Int:
+ spa_strbuf_append(buffer, "%d", *(int32_t *) body);
+ break;
+ case SPA_TYPE_Long:
+ spa_strbuf_append(buffer, "%" PRIi64, *(int64_t *) body);
+ break;
+ case SPA_TYPE_Float:
+ spa_strbuf_append(buffer, "%f", *(float *) body);
+ break;
+ case SPA_TYPE_Double:
+ spa_strbuf_append(buffer, "%f", *(double *) body);
+ break;
+ case SPA_TYPE_String:
+ spa_strbuf_append(buffer, "%s", (char *) body);
+ break;
+ case SPA_TYPE_Rectangle:
+ {
+ struct spa_rectangle *r = (struct spa_rectangle *)body;
+ spa_strbuf_append(buffer, "%" PRIu32 "x%" PRIu32, r->width, r->height);
+ break;
+ }
+ case SPA_TYPE_Fraction:
+ {
+ struct spa_fraction *f = (struct spa_fraction *)body;
+ spa_strbuf_append(buffer, "%" PRIu32 "/%" PRIu32, f->num, f->denom);
+ break;
+ }
+ case SPA_TYPE_Bitmap:
+ spa_strbuf_append(buffer, "Bitmap");
+ break;
+ case SPA_TYPE_Bytes:
+ spa_strbuf_append(buffer, "Bytes");
+ break;
+ case SPA_TYPE_Array:
+ {
+ void *p;
+ struct spa_pod_array_body *b = (struct spa_pod_array_body *)body;
+ int i = 0;
+ info = info && info->values ? info->values : info;
+ spa_strbuf_append(buffer, "< ");
+ SPA_POD_ARRAY_BODY_FOREACH(b, size, p) {
+ if (i++ > 0)
+ spa_strbuf_append(buffer, ", ");
+ spa_debug_strbuf_format_value(buffer, info, b->child.type, p, b->child.size);
+ }
+ spa_strbuf_append(buffer, " >");
+ break;
+ }
+ default:
+ spa_strbuf_append(buffer, "INVALID type %d", type);
+ break;
+ }
+ return 0;
+}
+
+static inline int
+spa_debug_format_value(const struct spa_type_info *info,
+ uint32_t type, void *body, uint32_t size)
+{
+ char buffer[1024];
+ struct spa_strbuf buf;
+ spa_strbuf_init(&buf, buffer, sizeof(buffer));
+ spa_debug_strbuf_format_value(&buf, info, type, body, size);
+ spa_debugn("%s", buffer);
+ return 0;
+}
+
+static inline 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;
+ const char *media_subtype;
+ struct spa_pod_prop *prop;
+ uint32_t mtype, mstype;
+
+ if (info == NULL)
+ info = spa_type_format;
+
+ if (format == NULL || SPA_POD_TYPE(format) != SPA_TYPE_Object)
+ return -EINVAL;
+
+ if (spa_format_parse(format, &mtype, &mstype) < 0)
+ return -EINVAL;
+
+ media_type = spa_debug_type_find_name(spa_type_media_type, mtype);
+ media_subtype = spa_debug_type_find_name(spa_type_media_subtype, mstype);
+
+ spa_debugc(ctx, "%*s %s/%s", indent, "",
+ media_type ? spa_debug_type_short_name(media_type) : "unknown",
+ media_subtype ? spa_debug_type_short_name(media_subtype) : "unknown");
+
+ SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)format, prop) {
+ const char *key;
+ const struct spa_type_info *ti;
+ uint32_t i, type, size, n_vals, choice;
+ const struct spa_pod *val;
+ void *vals;
+ char buffer[1024];
+ struct spa_strbuf buf;
+
+ if (prop->key == SPA_FORMAT_mediaType ||
+ prop->key == SPA_FORMAT_mediaSubtype)
+ continue;
+
+ val = spa_pod_get_values(&prop->value, &n_vals, &choice);
+
+ type = val->type;
+ size = val->size;
+ vals = SPA_POD_BODY(val);
+
+ if (type < SPA_TYPE_None || type >= _SPA_TYPE_LAST)
+ continue;
+
+ ti = spa_debug_type_find(info, prop->key);
+ key = ti ? ti->name : NULL;
+
+ spa_strbuf_init(&buf, buffer, sizeof(buffer));
+ spa_strbuf_append(&buf, "%*s %16s : (%s) ", indent, "",
+ key ? spa_debug_type_short_name(key) : "unknown",
+ spa_debug_type_short_name(spa_types[type].name));
+
+ if (choice == SPA_CHOICE_None) {
+ spa_debug_strbuf_format_value(&buf, ti ? ti->values : NULL, type, vals, size);
+ } else {
+ const char *ssep, *esep, *sep;
+
+ switch (choice) {
+ case SPA_CHOICE_Range:
+ case SPA_CHOICE_Step:
+ ssep = "[ ";
+ sep = ", ";
+ esep = " ]";
+ break;
+ default:
+ case SPA_CHOICE_Enum:
+ case SPA_CHOICE_Flags:
+ ssep = "{ ";
+ sep = ", ";
+ esep = " }";
+ break;
+ }
+
+ spa_strbuf_append(&buf, "%s", ssep);
+
+ for (i = 1; i < n_vals; i++) {
+ vals = SPA_PTROFF(vals, size, void);
+ if (i > 1)
+ spa_strbuf_append(&buf, "%s", sep);
+ spa_debug_strbuf_format_value(&buf, ti ? ti->values : NULL, type, vals, size);
+ }
+ spa_strbuf_append(&buf, "%s", esep);
+ }
+ spa_debugc(ctx, "%s", buffer);
+ }
+ return 0;
+}
+
+static inline 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);
+}
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_FORMAT_H */
diff --git a/spa/include/spa/debug/log.h b/spa/include/spa/debug/log.h
new file mode 100644
index 0000000..9e2dc73
--- /dev/null
+++ b/spa/include/spa/debug/log.h
@@ -0,0 +1,103 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_LOG_H
+#define SPA_DEBUG_LOG_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+
+#include <spa/utils/defs.h>
+#include <spa/support/log.h>
+#include <spa/debug/context.h>
+
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+struct spa_debug_log_ctx {
+ struct spa_debug_context ctx;
+ struct spa_log *log;
+ enum spa_log_level level;
+ const struct spa_log_topic *topic;
+ const char *file;
+ int line;
+ const char *func;
+};
+
+SPA_PRINTF_FUNC(2,3)
+static inline void spa_debug_log_log(struct spa_debug_context *ctx, const char *fmt, ...)
+{
+ struct spa_debug_log_ctx *c = (struct spa_debug_log_ctx*)ctx;
+ va_list args;
+ va_start(args, fmt);
+ spa_log_logtv(c->log, c->level, c->topic, c->file, c->line, c->func, fmt, args);
+ va_end(args);
+}
+
+#define SPA_LOGF_DEBUG_INIT(_l,_lev,_t,_file,_line,_func) \
+ (struct spa_debug_log_ctx){ { spa_debug_log_log }, _l, _lev, _t, \
+ _file, _line, _func }
+
+#define SPA_LOGT_DEBUG_INIT(_l,_lev,_t) \
+ SPA_LOGF_DEBUG_INIT(_l,_lev,_t,__FILE__,__LINE__,__func__)
+
+#define SPA_LOG_DEBUG_INIT(l,lev) \
+ SPA_LOGT_DEBUG_INIT(l,lev,SPA_LOG_TOPIC_DEFAULT)
+
+#define spa_debug_log_pod(l,lev,indent,info,pod) \
+({ \
+ struct spa_debug_log_ctx c = SPA_LOG_DEBUG_INIT(l,lev); \
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(c.log, c.topic, c.level))) \
+ spa_debugc_pod(&c.ctx, indent, info, pod); \
+})
+
+#define spa_debug_log_format(l,lev,indent,info,format) \
+({ \
+ struct spa_debug_log_ctx c = SPA_LOG_DEBUG_INIT(l,lev); \
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(c.log, c.topic, c.level))) \
+ spa_debugc_format(&c.ctx, indent, info, format); \
+})
+
+#define spa_debug_log_mem(l,lev,indent,data,len) \
+({ \
+ struct spa_debug_log_ctx c = SPA_LOG_DEBUG_INIT(l,lev); \
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(c.log, c.topic, c.level))) \
+ spa_debugc_mem(&c.ctx, indent, data, len); \
+})
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_LOG_H */
diff --git a/spa/include/spa/debug/mem.h b/spa/include/spa/debug/mem.h
new file mode 100644
index 0000000..7924aba
--- /dev/null
+++ b/spa/include/spa/debug/mem.h
@@ -0,0 +1,71 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_MEM_H
+#define SPA_DEBUG_MEM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <inttypes.h>
+
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+#include <spa/debug/context.h>
+
+static inline 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];
+ size_t i;
+ int pos = 0;
+
+ for (i = 0; i < size; i++) {
+ if (i % 16 == 0)
+ pos = sprintf(buffer, "%p: ", &t[i]);
+ pos += sprintf(buffer + pos, "%02x ", t[i]);
+ if (i % 16 == 15 || i == size - 1) {
+ spa_debugc(ctx, "%*s" "%s", indent, "", buffer);
+ }
+ }
+ return 0;
+}
+
+static inline int spa_debug_mem(int indent, const void *data, size_t size)
+{
+ return spa_debugc_mem(NULL, indent, data, size);
+}
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_MEM_H */
diff --git a/spa/include/spa/debug/node.h b/spa/include/spa/debug/node.h
new file mode 100644
index 0000000..63b16d3
--- /dev/null
+++ b/spa/include/spa/debug/node.h
@@ -0,0 +1,67 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_NODE_H
+#define SPA_DEBUG_NODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+#include <spa/node/node.h>
+#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)
+{
+ spa_debugc(ctx, "%*s" "struct spa_port_info %p:", indent, "", info);
+ spa_debugc(ctx, "%*s" " flags: \t%08" PRIx64, indent, "", info->flags);
+ spa_debugc(ctx, "%*s" " rate: \t%d/%d", indent, "", info->rate.num, info->rate.denom);
+ spa_debugc(ctx, "%*s" " props:", indent, "");
+ if (info->props)
+ spa_debugc_dict(ctx, indent + 2, info->props);
+ else
+ spa_debugc(ctx, "%*s" " none", indent, "");
+ return 0;
+}
+
+static inline int spa_debug_port_info(int indent, const struct spa_port_info *info)
+{
+ return spa_debugc_port_info(NULL, indent, info);
+}
+/**
+ * \}
+ */
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_NODE_H */
diff --git a/spa/include/spa/debug/pod.h b/spa/include/spa/debug/pod.h
new file mode 100644
index 0000000..31b707e
--- /dev/null
+++ b/spa/include/spa/debug/pod.h
@@ -0,0 +1,227 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_POD_H
+#define SPA_DEBUG_POD_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+#include <spa/debug/context.h>
+#include <spa/debug/mem.h>
+#include <spa/debug/types.h>
+#include <spa/pod/pod.h>
+#include <spa/pod/iter.h>
+
+static inline 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)
+{
+ switch (type) {
+ case SPA_TYPE_Bool:
+ spa_debugc(ctx, "%*s" "Bool %s", indent, "", (*(int32_t *) body) ? "true" : "false");
+ break;
+ case SPA_TYPE_Id:
+ spa_debugc(ctx, "%*s" "Id %-8d (%s)", indent, "", *(int32_t *) body,
+ spa_debug_type_find_name(info, *(int32_t *) body));
+ break;
+ case SPA_TYPE_Int:
+ spa_debugc(ctx, "%*s" "Int %d", indent, "", *(int32_t *) body);
+ break;
+ case SPA_TYPE_Long:
+ spa_debugc(ctx, "%*s" "Long %" PRIi64 "", indent, "", *(int64_t *) body);
+ break;
+ case SPA_TYPE_Float:
+ spa_debugc(ctx, "%*s" "Float %f", indent, "", *(float *) body);
+ break;
+ case SPA_TYPE_Double:
+ spa_debugc(ctx, "%*s" "Double %f", indent, "", *(double *) body);
+ break;
+ case SPA_TYPE_String:
+ spa_debugc(ctx, "%*s" "String \"%s\"", indent, "", (char *) body);
+ break;
+ case SPA_TYPE_Fd:
+ spa_debugc(ctx, "%*s" "Fd %d", indent, "", *(int *) body);
+ break;
+ case SPA_TYPE_Pointer:
+ {
+ struct spa_pod_pointer_body *b = (struct spa_pod_pointer_body *)body;
+ spa_debugc(ctx, "%*s" "Pointer %s %p", indent, "",
+ spa_debug_type_find_name(SPA_TYPE_ROOT, b->type), b->value);
+ break;
+ }
+ case SPA_TYPE_Rectangle:
+ {
+ struct spa_rectangle *r = (struct spa_rectangle *)body;
+ spa_debugc(ctx, "%*s" "Rectangle %dx%d", 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);
+ break;
+ }
+ case SPA_TYPE_Bitmap:
+ spa_debugc(ctx, "%*s" "Bitmap", indent, "");
+ break;
+ case SPA_TYPE_Array:
+ {
+ struct spa_pod_array_body *b = (struct spa_pod_array_body *)body;
+ void *p;
+ const struct spa_type_info *ti = spa_debug_type_find(SPA_TYPE_ROOT, b->child.type);
+
+ spa_debugc(ctx, "%*s" "Array: child.size %d, child.type %s", indent, "",
+ b->child.size, ti ? ti->name : "unknown");
+
+ info = info && info->values ? info->values : info;
+ SPA_POD_ARRAY_BODY_FOREACH(b, size, p)
+ spa_debugc_pod_value(ctx, indent + 2, info, b->child.type, p, b->child.size);
+ break;
+ }
+ case SPA_TYPE_Choice:
+ {
+ struct spa_pod_choice_body *b = (struct spa_pod_choice_body *)body;
+ void *p;
+ const struct spa_type_info *ti = spa_debug_type_find(spa_type_choice, b->type);
+
+ spa_debugc(ctx, "%*s" "Choice: type %s, flags %08x %d %d", indent, "",
+ ti ? ti->name : "unknown", b->flags, size, b->child.size);
+
+ SPA_POD_CHOICE_BODY_FOREACH(b, size, p)
+ spa_debugc_pod_value(ctx, indent + 2, info, b->child.type, p, b->child.size);
+ break;
+ }
+ case SPA_TYPE_Struct:
+ {
+ struct spa_pod *b = (struct spa_pod *)body, *p;
+ spa_debugc(ctx, "%*s" "Struct: size %d", indent, "", size);
+ SPA_POD_FOREACH(b, size, p)
+ spa_debugc_pod_value(ctx, indent + 2, info, p->type, SPA_POD_BODY(p), p->size);
+ break;
+ }
+ case SPA_TYPE_Object:
+ {
+ struct spa_pod_object_body *b = (struct spa_pod_object_body *)body;
+ struct spa_pod_prop *p;
+ const struct spa_type_info *ti, *ii;
+
+ ti = spa_debug_type_find(info, b->type);
+ ii = ti ? spa_debug_type_find(ti->values, 0) : NULL;
+ ii = ii ? spa_debug_type_find(ii->values, b->id) : NULL;
+
+ spa_debugc(ctx, "%*s" "Object: size %d, type %s (%d), id %s (%d)", indent, "", size,
+ ti ? ti->name : "unknown", b->type, ii ? ii->name : "unknown", b->id);
+
+ info = ti ? ti->values : info;
+
+ SPA_POD_OBJECT_BODY_FOREACH(b, size, p) {
+ ii = spa_debug_type_find(info, p->key);
+
+ spa_debugc(ctx, "%*s" "Prop: key %s (%d), flags %08x", indent+2, "",
+ ii ? ii->name : "unknown", p->key, p->flags);
+
+ spa_debugc_pod_value(ctx, indent + 4, ii ? ii->values : NULL,
+ p->value.type,
+ SPA_POD_CONTENTS(struct spa_pod_prop, p),
+ p->value.size);
+ }
+ break;
+ }
+ case SPA_TYPE_Sequence:
+ {
+ struct spa_pod_sequence_body *b = (struct spa_pod_sequence_body *)body;
+ const struct spa_type_info *ti, *ii;
+ struct spa_pod_control *c;
+
+ ti = spa_debug_type_find(info, b->unit);
+
+ spa_debugc(ctx, "%*s" "Sequence: size %d, unit %s", indent, "", size,
+ ti ? ti->name : "unknown");
+
+ SPA_POD_SEQUENCE_BODY_FOREACH(b, size, c) {
+ ii = spa_debug_type_find(spa_type_control, c->type);
+
+ spa_debugc(ctx, "%*s" "Control: offset %d, type %s", indent+2, "",
+ c->offset, ii ? ii->name : "unknown");
+
+ spa_debugc_pod_value(ctx, indent + 4, ii ? ii->values : NULL,
+ c->value.type,
+ SPA_POD_CONTENTS(struct spa_pod_control, c),
+ c->value.size);
+ }
+ break;
+ }
+ case SPA_TYPE_Bytes:
+ spa_debugc(ctx, "%*s" "Bytes", indent, "");
+ spa_debugc_mem(ctx, indent + 2, body, size);
+ break;
+ case SPA_TYPE_None:
+ spa_debugc(ctx, "%*s" "None", indent, "");
+ spa_debugc_mem(ctx, indent + 2, body, size);
+ break;
+ default:
+ spa_debugc(ctx, "%*s" "unhandled POD type %d", indent, "", type);
+ break;
+ }
+ return 0;
+}
+
+static inline 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,
+ SPA_POD_TYPE(pod),
+ SPA_POD_BODY(pod),
+ SPA_POD_BODY_SIZE(pod));
+}
+
+static inline 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,
+ const struct spa_type_info *info, const struct spa_pod *pod)
+{
+ return spa_debugc_pod(NULL, indent, info, pod);
+}
+/**
+ * \}
+ */
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_POD_H */
diff --git a/spa/include/spa/debug/types.h b/spa/include/spa/debug/types.h
new file mode 100644
index 0000000..55fb379
--- /dev/null
+++ b/spa/include/spa/debug/types.h
@@ -0,0 +1,127 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEBUG_TYPES_H
+#define SPA_DEBUG_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_debug
+ * \{
+ */
+
+#include <spa/utils/type-info.h>
+
+#include <string.h>
+
+static inline const struct spa_type_info *spa_debug_type_find(const struct spa_type_info *info, uint32_t type)
+{
+ const struct spa_type_info *res;
+
+ if (info == NULL)
+ info = SPA_TYPE_ROOT;
+
+ while (info && info->name) {
+ if (info->type == SPA_ID_INVALID) {
+ if (info->values && (res = spa_debug_type_find(info->values, type)))
+ return res;
+ }
+ else if (info->type == type)
+ return info;
+ info++;
+ }
+ return NULL;
+}
+
+static inline const char *spa_debug_type_short_name(const char *name)
+{
+ const char *h;
+ if ((h = strrchr(name, ':')) != NULL)
+ name = h + 1;
+ return name;
+}
+
+static inline 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)
+{
+ const char *str;
+ if ((str = spa_debug_type_find_name(info, type)) == NULL)
+ return NULL;
+ 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)
+{
+ if (info == NULL)
+ info = SPA_TYPE_ROOT;
+
+ while (info && info->name) {
+ uint32_t res;
+ if (strcmp(info->name, name) == 0)
+ return info->type;
+ if (info->values && (res = spa_debug_type_find_type(info->values, name)) != SPA_ID_INVALID)
+ return res;
+ 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)
+{
+ while (info && info->name) {
+ if (strcmp(spa_debug_type_short_name(info->name), name) == 0)
+ return info;
+ if (strcmp(info->name, name) == 0)
+ return info;
+ if (info->type != 0 && info->type == (uint32_t)atoi(name))
+ return info;
+ info++;
+ }
+ return NULL;
+}
+
+static inline 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;
+ return info->type;
+}
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEBUG_NODE_H */
diff --git a/spa/include/spa/graph/graph.h b/spa/include/spa/graph/graph.h
new file mode 100644
index 0000000..0e887cd
--- /dev/null
+++ b/spa/include/spa/graph/graph.h
@@ -0,0 +1,365 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_GRAPH_H
+#define SPA_GRAPH_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup spa_graph Graph
+ * Node graph
+ */
+
+/**
+ * \addtogroup spa_graph
+ * \{
+ */
+
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+
+#ifndef spa_debug
+#define spa_debug(...)
+#endif
+
+struct spa_graph;
+struct spa_graph_node;
+struct spa_graph_link;
+struct spa_graph_port;
+
+struct spa_graph_state {
+ int status; /**< current status */
+ int32_t required; /**< required number of signals */
+ int32_t pending; /**< number of pending signals */
+};
+
+static inline void spa_graph_state_reset(struct spa_graph_state *state)
+{
+ state->pending = state->required;
+}
+
+struct spa_graph_link {
+ struct spa_list link;
+ struct spa_graph_state *state;
+ int (*signal) (void *data);
+ void *signal_data;
+};
+
+#define spa_graph_link_signal(l) ((l)->signal((l)->signal_data))
+
+#define spa_graph_state_dec(s,c) (__atomic_sub_fetch(&(s)->pending, c, __ATOMIC_SEQ_CST) == 0)
+
+static inline int spa_graph_link_trigger(struct spa_graph_link *link)
+{
+ struct spa_graph_state *state = link->state;
+
+ spa_debug("link %p: state %p: pending %d/%d", link, state,
+ state->pending, state->required);
+
+ if (spa_graph_state_dec(state, 1))
+ spa_graph_link_signal(link);
+
+ return state->status;
+}
+struct spa_graph {
+ uint32_t flags; /* flags */
+ struct spa_graph_node *parent; /* parent node or NULL when driver */
+ struct spa_graph_state *state; /* state of graph */
+ struct spa_list nodes; /* list of nodes of this graph */
+};
+
+struct spa_graph_node_callbacks {
+#define SPA_VERSION_GRAPH_NODE_CALLBACKS 0
+ uint32_t version;
+
+ int (*process) (void *data, struct spa_graph_node *node);
+ int (*reuse_buffer) (void *data, struct spa_graph_node *node,
+ uint32_t port_id, uint32_t buffer_id);
+};
+
+struct spa_graph_node {
+ struct spa_list link; /**< link in graph nodes list */
+ struct spa_graph *graph; /**< owner graph */
+ struct spa_list ports[2]; /**< list of input and output ports */
+ struct spa_list links; /**< list of links to next nodes */
+ uint32_t flags; /**< node flags */
+ struct spa_graph_state *state; /**< state of the node */
+ struct spa_graph_link graph_link; /**< link in graph */
+ struct spa_graph *subgraph; /**< subgraph or NULL */
+ struct spa_callbacks callbacks;
+ struct spa_list sched_link; /**< link for scheduler */
+};
+
+#define spa_graph_node_call(n,method,version,...) \
+({ \
+ int __res = 0; \
+ spa_callbacks_call_res(&(n)->callbacks, \
+ struct spa_graph_node_callbacks, __res, \
+ method, (version), ##__VA_ARGS__); \
+ __res; \
+})
+
+#define spa_graph_node_process(n) spa_graph_node_call((n), process, 0, (n))
+#define spa_graph_node_reuse_buffer(n,p,i) spa_graph_node_call((n), reuse_buffer, 0, (n), (p), (i))
+
+struct spa_graph_port {
+ struct spa_list link; /**< link in node port list */
+ struct spa_graph_node *node; /**< owner node */
+ enum spa_direction direction; /**< port direction */
+ uint32_t port_id; /**< port id */
+ uint32_t flags; /**< port flags */
+ struct spa_graph_port *peer; /**< peer */
+};
+
+static inline int spa_graph_node_trigger(struct spa_graph_node *node)
+{
+ struct spa_graph_link *l;
+ spa_debug("node %p trigger", node);
+ spa_list_for_each(l, &node->links, link)
+ spa_graph_link_trigger(l);
+ return 0;
+}
+
+static inline int spa_graph_run(struct spa_graph *graph)
+{
+ struct spa_graph_node *n, *t;
+ struct spa_list pending;
+
+ spa_graph_state_reset(graph->state);
+ spa_debug("graph %p run with state %p pending %d/%d", graph, graph->state,
+ graph->state->pending, graph->state->required);
+
+ spa_list_init(&pending);
+
+ spa_list_for_each(n, &graph->nodes, link) {
+ struct spa_graph_state *s = n->state;
+ spa_graph_state_reset(s);
+ spa_debug("graph %p node %p: state %p pending %d/%d status %d", graph, n,
+ s, s->pending, s->required, s->status);
+ if (--s->pending == 0)
+ spa_list_append(&pending, &n->sched_link);
+ }
+ spa_list_for_each_safe(n, t, &pending, sched_link)
+ spa_graph_node_process(n);
+
+ return 0;
+}
+
+static inline 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)
+{
+ 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)
+{
+ 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_list_init(&graph->nodes);
+ graph->flags = 0;
+ graph->state = state;
+ spa_debug("graph %p init state %p", graph, state);
+}
+
+static inline void
+spa_graph_link_add(struct spa_graph_node *out,
+ struct spa_graph_state *state,
+ struct spa_graph_link *link)
+{
+ link->state = state;
+ state->required++;
+ spa_debug("node %p add link %p to state %p %d", out, link, state, state->required);
+ spa_list_append(&out->links, &link->link);
+}
+
+static inline 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_graph_node_init(struct spa_graph_node *node, struct spa_graph_state *state)
+{
+ spa_list_init(&node->ports[SPA_DIRECTION_INPUT]);
+ spa_list_init(&node->ports[SPA_DIRECTION_OUTPUT]);
+ spa_list_init(&node->links);
+ node->flags = 0;
+ node->subgraph = NULL;
+ node->state = state;
+ node->state->required = node->state->pending = 0;
+ node->state->status = SPA_STATUS_OK;
+ node->graph_link.signal = spa_graph_link_signal_graph;
+ node->graph_link.signal_data = node;
+ spa_debug("node %p init state %p", node, state);
+}
+
+
+static inline int spa_graph_node_impl_sub_process(void *data, struct spa_graph_node *node)
+{
+ struct spa_graph *graph = node->subgraph;
+ spa_debug("node %p: sub process %p", node, graph);
+ return spa_graph_run(graph);
+}
+
+static const struct spa_graph_node_callbacks spa_graph_node_sub_impl_default = {
+ SPA_VERSION_GRAPH_NODE_CALLBACKS,
+ .process = spa_graph_node_impl_sub_process,
+};
+
+static inline void spa_graph_node_set_subgraph(struct spa_graph_node *node,
+ struct spa_graph *subgraph)
+{
+ node->subgraph = subgraph;
+ subgraph->parent = node;
+ spa_debug("node %p set subgraph %p", node, subgraph);
+}
+
+static inline void
+spa_graph_node_set_callbacks(struct spa_graph_node *node,
+ const struct spa_graph_node_callbacks *callbacks,
+ void *data)
+{
+ node->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+}
+
+static inline void
+spa_graph_node_add(struct spa_graph *graph,
+ struct spa_graph_node *node)
+{
+ node->graph = graph;
+ spa_list_append(&graph->nodes, &node->link);
+ node->state->required++;
+ spa_debug("node %p add to graph %p, state %p required %d",
+ node, graph, node->state, node->state->required);
+ spa_graph_link_add(node, graph->state, &node->graph_link);
+}
+
+static inline 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);
+ spa_graph_link_remove(&node->graph_link);
+ node->state->required--;
+ spa_list_remove(&node->link);
+}
+
+
+static inline void
+spa_graph_port_init(struct spa_graph_port *port,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags)
+{
+ spa_debug("port %p init type %d id %d", port, direction, port_id);
+ port->direction = direction;
+ port->port_id = port_id;
+ port->flags = flags;
+}
+
+static inline void
+spa_graph_port_add(struct spa_graph_node *node,
+ struct spa_graph_port *port)
+{
+ spa_debug("port %p add to node %p", port, node);
+ port->node = node;
+ spa_list_append(&node->ports[port->direction], &port->link);
+}
+
+static inline 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_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);
+ out->peer = in;
+ in->peer = out;
+}
+
+static inline void
+spa_graph_port_unlink(struct spa_graph_port *port)
+{
+ spa_debug("port %p unlink from %p", port, port->peer);
+ if (port->peer) {
+ port->peer->peer = NULL;
+ port->peer = NULL;
+ }
+}
+
+static inline 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;
+
+ spa_debug("node %p: process state %p: %d, node %p", node, state, state->status, n);
+ if ((state->status = spa_node_process(n)) != SPA_STATUS_OK)
+ spa_graph_node_trigger(node);
+
+ return state->status;
+}
+
+static inline int spa_graph_node_impl_reuse_buffer(void *data, struct spa_graph_node *node,
+ uint32_t port_id, uint32_t buffer_id)
+{
+ struct spa_node *n = (struct spa_node *)data;
+ return spa_node_port_reuse_buffer(n, port_id, buffer_id);
+}
+
+static const struct spa_graph_node_callbacks spa_graph_node_impl_default = {
+ SPA_VERSION_GRAPH_NODE_CALLBACKS,
+ .process = spa_graph_node_impl_process,
+ .reuse_buffer = spa_graph_node_impl_reuse_buffer,
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_GRAPH_H */
diff --git a/spa/include/spa/interfaces/audio/aec.h b/spa/include/spa/interfaces/audio/aec.h
new file mode 100644
index 0000000..c5dcb68
--- /dev/null
+++ b/spa/include/spa/interfaces/audio/aec.h
@@ -0,0 +1,110 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#include <spa/pod/builder.h>
+#include <spa/utils/dict.h>
+#include <spa/utils/hook.h>
+#include <spa/param/audio/raw.h>
+
+#ifndef SPA_AUDIO_AEC_H
+#define SPA_AUDIO_AEC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define SPA_TYPE_INTERFACE_AUDIO_AEC SPA_TYPE_INFO_INTERFACE_BASE "Audio:AEC"
+
+#define SPA_VERSION_AUDIO_AEC 1
+struct spa_audio_aec {
+ struct spa_interface iface;
+ const char *name;
+ const struct spa_dict *info;
+ const char *latency;
+};
+
+struct spa_audio_aec_info {
+#define SPA_AUDIO_AEC_CHANGE_MASK_PROPS (1u<<0)
+ uint64_t change_mask;
+
+ const struct spa_dict *props;
+};
+
+struct spa_audio_aec_events {
+#define SPA_VERSION_AUDIO_AEC_EVENTS 0
+ uint32_t version; /**< version of this structure */
+
+ /** Emitted when info changes */
+ void (*info) (void *data, const struct spa_audio_aec_info *info);
+};
+
+struct spa_audio_aec_methods {
+#define SPA_VERSION_AUDIO_AEC_METHODS 2
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct spa_audio_aec_events *events,
+ void *data);
+
+ int (*init) (void *object, const struct spa_dict *args, const struct spa_audio_info_raw *info);
+ int (*run) (void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples);
+ int (*set_props) (void *object, const struct spa_dict *args);
+ /* since 0.3.58, version 1:1 */
+ int (*activate) (void *object);
+ /* since 0.3.58, version 1:1 */
+ int (*deactivate) (void *object);
+
+ /* version 1:2 */
+ int (*enum_props) (void* object, int index, struct spa_pod_builder* builder);
+ int (*get_params) (void* object, struct spa_pod_builder* builder);
+ int (*set_params) (void *object, const struct spa_pod *args);
+};
+
+#define spa_audio_aec_method(o,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ struct spa_audio_aec *_o = (o); \
+ 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__)
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_AEC_H */
diff --git a/spa/include/spa/monitor/device.h b/spa/include/spa/monitor/device.h
new file mode 100644
index 0000000..59fea51
--- /dev/null
+++ b/spa/include/spa/monitor/device.h
@@ -0,0 +1,307 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEVICE_H
+#define SPA_DEVICE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/dict.h>
+#include <spa/pod/event.h>
+
+/**
+ * \defgroup spa_device Device
+ *
+ * The device interface can be used to monitor all kinds of devices
+ * and create objects as a result. Objects a typically other
+ * Devices or Nodes.
+ *
+ */
+
+/**
+ * \addtogroup spa_device
+ * \{
+ */
+#define SPA_TYPE_INTERFACE_Device SPA_TYPE_INFO_INTERFACE_BASE "Device"
+
+#define SPA_VERSION_DEVICE 0
+struct spa_device { struct spa_interface iface; };
+
+/**
+ * Information about the device and parameters it supports
+ *
+ * This information is part of the info event on a device.
+ */
+struct spa_device_info {
+#define SPA_VERSION_DEVICE_INFO 0
+ uint32_t version;
+
+#define SPA_DEVICE_CHANGE_MASK_FLAGS (1u<<0)
+#define SPA_DEVICE_CHANGE_MASK_PROPS (1u<<1)
+#define SPA_DEVICE_CHANGE_MASK_PARAMS (1u<<2)
+ uint64_t change_mask;
+ uint64_t flags;
+ const struct spa_dict *props; /**< device properties */
+ struct spa_param_info *params; /**< supported parameters */
+ uint32_t n_params; /**< number of elements in params */
+};
+
+#define SPA_DEVICE_INFO_INIT() ((struct spa_device_info){ SPA_VERSION_DEVICE_INFO, })
+
+/**
+ * Information about a device object
+ *
+ * This information is part of the object_info event on the device.
+ */
+struct spa_device_object_info {
+#define SPA_VERSION_DEVICE_OBJECT_INFO 0
+ uint32_t version;
+
+ const char *type; /**< the object type managed by this device */
+ const char *factory_name; /**< a factory name that implements the object */
+
+#define SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS (1u<<0)
+#define SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS (1u<<1)
+ uint64_t change_mask;
+ uint64_t flags;
+ const struct spa_dict *props; /**< extra object properties */
+};
+
+#define SPA_DEVICE_OBJECT_INFO_INIT() ((struct spa_device_object_info){ SPA_VERSION_DEVICE_OBJECT_INFO, })
+
+/** the result of spa_device_enum_params() */
+#define SPA_RESULT_TYPE_DEVICE_PARAMS 1
+struct spa_result_device_params {
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+ struct spa_pod *param;
+};
+
+#define SPA_DEVICE_EVENT_INFO 0
+#define SPA_DEVICE_EVENT_RESULT 1
+#define SPA_DEVICE_EVENT_EVENT 2
+#define SPA_DEVICE_EVENT_OBJECT_INFO 3
+#define SPA_DEVICE_EVENT_NUM 4
+
+/**
+ * spa_device_events:
+ *
+ * Events are always emitted from the main thread
+ */
+struct spa_device_events {
+ /** version of the structure */
+#define SPA_VERSION_DEVICE_EVENTS 0
+ uint32_t version;
+
+ /** notify extra information about the device */
+ void (*info) (void *data, const struct spa_device_info *info);
+
+ /** notify a result */
+ void (*result) (void *data, int seq, int res, uint32_t type, const void *result);
+
+ /** a device event */
+ void (*event) (void *data, const struct spa_event *event);
+
+ /** info changed for an object managed by the device, info is NULL when
+ * the object is removed */
+ void (*object_info) (void *data, uint32_t id,
+ const struct spa_device_object_info *info);
+};
+
+#define SPA_DEVICE_METHOD_ADD_LISTENER 0
+#define SPA_DEVICE_METHOD_SYNC 1
+#define SPA_DEVICE_METHOD_ENUM_PARAMS 2
+#define SPA_DEVICE_METHOD_SET_PARAM 3
+#define SPA_DEVICE_METHOD_NUM 4
+
+/**
+ * spa_device_methods:
+ */
+struct spa_device_methods {
+ /* the version of the methods. This can be used to expand this
+ * structure in the future */
+#define SPA_VERSION_DEVICE_METHODS 0
+ uint32_t version;
+
+ /**
+ * Set events to receive asynchronous notifications from
+ * the device.
+ *
+ * Setting the events will trigger the info event and an
+ * object_info event for each managed object on the new
+ * listener.
+ *
+ * \param object a \ref spa_device
+ * \param listener a listener
+ * \param events a struct \ref spa_device_events
+ * \param data data passed as first argument in functions of \a events
+ * \return 0 on success
+ * < 0 errno on error
+ */
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data);
+ /**
+ * Perform a sync operation.
+ *
+ * This method will emit the result event with the given sequence
+ * number synchronously or with the returned async return value
+ * asynchronously.
+ *
+ * Because all methods are serialized in the device, this can be used
+ * to wait for completion of all previous method calls.
+ *
+ * \param seq a sequence number
+ * \return 0 on success
+ * -EINVAL when node is NULL
+ * an async result
+ */
+ int (*sync) (void *object, int seq);
+
+ /**
+ * Enumerate the parameters of a device.
+ *
+ * Parameters are identified with an \a id. Some parameters can have
+ * multiple values, see the documentation of the parameter id.
+ *
+ * Parameters can be filtered by passing a non-NULL \a filter.
+ *
+ * The result callback will be called at most \a max times with a
+ * struct spa_result_device_params as the result.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param device a \ref spa_device
+ * \param seq a sequence number to pass to the result function
+ * \param id the param id to enumerate
+ * \param index the index of enumeration, pass 0 for the first item.
+ * \param max the maximum number of items to iterate
+ * \param filter and optional filter to use
+ * \return 0 when there are no more parameters to enumerate
+ * -EINVAL when invalid arguments are given
+ * -ENOENT the parameter \a id is unknown
+ * -ENOTSUP when there are no parameters
+ * implemented on \a device
+ */
+ int (*enum_params) (void *object, int seq,
+ uint32_t id, uint32_t index, uint32_t max,
+ const struct spa_pod *filter);
+
+ /**
+ * Set the configurable parameter in \a device.
+ *
+ * Usually, \a param will be obtained from enum_params and then
+ * modified but it is also possible to set another spa_pod
+ * as long as its keys and types match a supported object.
+ *
+ * Objects with property keys that are not known are ignored.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param object \ref spa_device
+ * \param id the parameter id to configure
+ * \param flags additional flags
+ * \param param the parameter to configure
+ *
+ * \return 0 on success
+ * -EINVAL when invalid arguments are given
+ * -ENOTSUP when there are no parameters implemented on \a device
+ * -ENOENT the parameter is unknown
+ */
+ int (*set_param) (void *object,
+ uint32_t id, uint32_t flags,
+ 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__)
+
+#define SPA_KEY_DEVICE_ENUM_API "device.enum.api" /**< the api used to discover this
+ * device */
+#define SPA_KEY_DEVICE_API "device.api" /**< the api used by the device
+ * Ex. "udev", "alsa", "v4l2". */
+#define SPA_KEY_DEVICE_NAME "device.name" /**< the name of the device */
+#define SPA_KEY_DEVICE_ALIAS "device.alias" /**< alternative name of the device */
+#define SPA_KEY_DEVICE_NICK "device.nick" /**< the device short name */
+#define SPA_KEY_DEVICE_DESCRIPTION "device.description" /**< a device description */
+#define SPA_KEY_DEVICE_ICON "device.icon" /**< icon for the device. A base64 blob
+ * containing PNG image data */
+#define SPA_KEY_DEVICE_ICON_NAME "device.icon-name" /**< an XDG icon name for the device.
+ * Ex. "sound-card-speakers-usb" */
+#define SPA_KEY_DEVICE_PLUGGED_USEC "device.plugged.usec" /**< when the device was plugged */
+
+#define SPA_KEY_DEVICE_BUS_ID "device.bus-id" /**< the device bus-id */
+#define SPA_KEY_DEVICE_BUS_PATH "device.bus-path" /**< bus path to the device in the OS'
+ * format.
+ * Ex. "pci-0000:00:14.0-usb-0:3.2:1.0" */
+#define SPA_KEY_DEVICE_BUS "device.bus" /**< bus of the device if applicable. One of
+ * "isa", "pci", "usb", "firewire",
+ * "bluetooth" */
+#define SPA_KEY_DEVICE_SUBSYSTEM "device.subsystem" /**< device subsystem */
+#define SPA_KEY_DEVICE_SYSFS_PATH "device.sysfs.path" /**< device sysfs path */
+
+#define SPA_KEY_DEVICE_VENDOR_ID "device.vendor.id" /**< vendor ID if applicable */
+#define SPA_KEY_DEVICE_VENDOR_NAME "device.vendor.name" /**< vendor name if applicable */
+#define SPA_KEY_DEVICE_PRODUCT_ID "device.product.id" /**< product ID if applicable */
+#define SPA_KEY_DEVICE_PRODUCT_NAME "device.product.name" /**< product name if applicable */
+#define SPA_KEY_DEVICE_SERIAL "device.serial" /**< Serial number if applicable */
+#define SPA_KEY_DEVICE_CLASS "device.class" /**< device class */
+#define SPA_KEY_DEVICE_CAPABILITIES "device.capabilities" /**< api specific device capabilities */
+#define SPA_KEY_DEVICE_FORM_FACTOR "device.form-factor" /**< form factor if applicable. One of
+ * "internal", "speaker", "handset", "tv",
+ * "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_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" */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEVICE_H */
diff --git a/spa/include/spa/monitor/event.h b/spa/include/spa/monitor/event.h
new file mode 100644
index 0000000..7b8a256
--- /dev/null
+++ b/spa/include/spa/monitor/event.h
@@ -0,0 +1,63 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_EVENT_DEVICE_H
+#define SPA_EVENT_DEVICE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/pod/event.h>
+
+/**
+ * \addtogroup spa_device
+ * \{
+ */
+
+/* object id of SPA_TYPE_EVENT_Device */
+enum spa_device_event {
+ SPA_DEVICE_EVENT_ObjectConfig,
+};
+
+#define SPA_DEVICE_EVENT_ID(ev) SPA_EVENT_ID(ev, SPA_TYPE_EVENT_Device)
+#define SPA_DEVICE_EVENT_INIT(id) SPA_EVENT_INIT(SPA_TYPE_EVENT_Device, id)
+
+/* properties for SPA_TYPE_EVENT_Device */
+enum spa_event_device {
+ SPA_EVENT_DEVICE_START,
+
+ SPA_EVENT_DEVICE_Object, /* an object id (Int) */
+ SPA_EVENT_DEVICE_Props, /* properties for an object (SPA_TYPE_OBJECT_Props) */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_EVENT_DEVICE */
diff --git a/spa/include/spa/monitor/type-info.h b/spa/include/spa/monitor/type-info.h
new file mode 100644
index 0000000..6bf781a
--- /dev/null
+++ b/spa/include/spa/monitor/type-info.h
@@ -0,0 +1,67 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEVICE_TYPE_INFO_H
+#define SPA_DEVICE_TYPE_INFO_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/type-info.h>
+
+#include <spa/monitor/event.h>
+
+/**
+ * \addtogroup spa_device
+ * \{
+ */
+
+#define SPA_TYPE_INFO_DeviceEvent SPA_TYPE_INFO_EVENT_BASE "Device"
+#define SPA_TYPE_INFO_DEVICE_EVENT_BASE SPA_TYPE_INFO_DeviceEvent ":"
+
+#define SPA_TYPE_INFO_DeviceEventId SPA_TYPE_INFO_ENUM_BASE "DeviceEventId"
+#define SPA_TYPE_INFO_DEVICE_EVENT_ID_BASE SPA_TYPE_INFO_DeviceEventId ":"
+
+static const struct spa_type_info spa_type_device_event_id[] = {
+ { SPA_DEVICE_EVENT_ObjectConfig, SPA_TYPE_EVENT_Device, SPA_TYPE_INFO_DEVICE_EVENT_ID_BASE "ObjectConfig", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+static const struct spa_type_info spa_type_device_event[] = {
+ { SPA_EVENT_DEVICE_START, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_EVENT_BASE, spa_type_device_event_id },
+ { SPA_EVENT_DEVICE_Object, SPA_TYPE_Int, SPA_TYPE_INFO_DEVICE_EVENT_BASE "Object", NULL },
+ { SPA_EVENT_DEVICE_Props, SPA_TYPE_OBJECT_Props, SPA_TYPE_INFO_DEVICE_EVENT_BASE "Props", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEVICE_TYPE_INFO_H */
diff --git a/spa/include/spa/monitor/utils.h b/spa/include/spa/monitor/utils.h
new file mode 100644
index 0000000..169fe47
--- /dev/null
+++ b/spa/include/spa/monitor/utils.h
@@ -0,0 +1,106 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DEVICE_UTILS_H
+#define SPA_DEVICE_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/pod/builder.h>
+#include <spa/monitor/device.h>
+
+/**
+ * \addtogroup spa_device
+ * \{
+ */
+
+struct spa_result_device_params_data {
+ struct spa_pod_builder *builder;
+ struct spa_result_device_params data;
+};
+
+static inline void spa_result_func_device_params(void *data, int seq, int res,
+ uint32_t type, const void *result)
+{
+ struct spa_result_device_params_data *d =
+ (struct spa_result_device_params_data *)data;
+ const struct spa_result_device_params *r =
+ (const struct spa_result_device_params *)result;
+ uint32_t offset = d->builder->state.offset;
+ if (spa_pod_builder_raw_padded(d->builder, r->param, SPA_POD_SIZE(r->param)) < 0)
+ return;
+ d->data.next = r->next;
+ d->data.param = spa_pod_builder_deref(d->builder, offset);
+}
+
+static inline 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,
+ struct spa_pod_builder *builder)
+{
+ struct spa_result_device_params_data data = { builder, };
+ struct spa_hook listener = {{0}};
+ static const struct spa_device_events device_events = {
+ .version = SPA_VERSION_DEVICE_EVENTS,
+ .info = NULL,
+ .result = spa_result_func_device_params,
+ };
+ int res;
+
+ spa_device_add_listener(device, &listener, &device_events, &data);
+ res = spa_device_enum_params(device, 0, id, *index, 1, filter);
+ spa_hook_remove(&listener);
+
+ if (data.data.param == NULL) {
+ if (res > 0)
+ res = 0;
+ } else {
+ *index = data.data.next;
+ *param = data.data.param;
+ res = 1;
+ }
+ return res;
+}
+
+#define spa_device_emit(hooks,method,version,...) \
+ spa_hook_list_call_simple(hooks, struct spa_device_events, \
+ method, version, ##__VA_ARGS__)
+
+#define spa_device_emit_info(hooks,i) spa_device_emit(hooks,info, 0, i)
+#define spa_device_emit_result(hooks,s,r,t,res) spa_device_emit(hooks,result, 0, s, r, t, res)
+#define spa_device_emit_event(hooks,e) spa_device_emit(hooks,event, 0, e)
+#define spa_device_emit_object_info(hooks,id,i) spa_device_emit(hooks,object_info, 0, id, i)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DEVICE_UTILS_H */
diff --git a/spa/include/spa/node/command.h b/spa/include/spa/node/command.h
new file mode 100644
index 0000000..9bf50fb
--- /dev/null
+++ b/spa/include/spa/node/command.h
@@ -0,0 +1,73 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_COMMAND_NODE_H
+#define SPA_COMMAND_NODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_node
+ * \{
+ */
+
+#include <spa/pod/command.h>
+
+/* object id of SPA_TYPE_COMMAND_Node */
+enum spa_node_command {
+ SPA_NODE_COMMAND_Suspend, /**< suspend a node, this removes all configured
+ * formats and closes any devices */
+ SPA_NODE_COMMAND_Pause, /**< pause a node. this makes it stop emitting
+ * scheduling events */
+ SPA_NODE_COMMAND_Start, /**< start a node, this makes it start emitting
+ * scheduling events */
+ SPA_NODE_COMMAND_Enable,
+ SPA_NODE_COMMAND_Disable,
+ SPA_NODE_COMMAND_Flush,
+ SPA_NODE_COMMAND_Drain,
+ SPA_NODE_COMMAND_Marker,
+ SPA_NODE_COMMAND_ParamBegin, /**< begin a set of parameter enumerations or
+ * configuration that require the device to
+ * remain opened, like query formats and then
+ * set a format */
+ SPA_NODE_COMMAND_ParamEnd, /**< end a transaction */
+ SPA_NODE_COMMAND_RequestProcess,/**< Sent to a driver when some other node emitted
+ * the RequestProcess event. */
+};
+
+#define SPA_NODE_COMMAND_ID(cmd) SPA_COMMAND_ID(cmd, SPA_TYPE_COMMAND_Node)
+#define SPA_NODE_COMMAND_INIT(id) SPA_COMMAND_INIT(SPA_TYPE_COMMAND_Node, id)
+
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_COMMAND_NODE_H */
diff --git a/spa/include/spa/node/event.h b/spa/include/spa/node/event.h
new file mode 100644
index 0000000..ceb6d60
--- /dev/null
+++ b/spa/include/spa/node/event.h
@@ -0,0 +1,64 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_EVENT_NODE_H
+#define SPA_EVENT_NODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_node
+ * \{
+ */
+
+#include <spa/pod/event.h>
+
+/* object id of SPA_TYPE_EVENT_Node */
+enum spa_node_event {
+ SPA_NODE_EVENT_Error,
+ SPA_NODE_EVENT_Buffering,
+ SPA_NODE_EVENT_RequestRefresh,
+ SPA_NODE_EVENT_RequestProcess, /*< Ask the driver to start processing
+ * the graph */
+};
+
+#define SPA_NODE_EVENT_ID(ev) SPA_EVENT_ID(ev, SPA_TYPE_EVENT_Node)
+#define SPA_NODE_EVENT_INIT(id) SPA_EVENT_INIT(SPA_TYPE_EVENT_Node, id)
+
+/* properties for SPA_TYPE_EVENT_Node */
+enum spa_event_node {
+ SPA_EVENT_NODE_START,
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_EVENT_NODE_H */
diff --git a/spa/include/spa/node/io.h b/spa/include/spa/node/io.h
new file mode 100644
index 0000000..9211f65
--- /dev/null
+++ b/spa/include/spa/node/io.h
@@ -0,0 +1,304 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_IO_H
+#define SPA_IO_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_node
+ * \{
+ */
+
+#include <spa/utils/defs.h>
+#include <spa/pod/pod.h>
+
+/** IO areas
+ *
+ * IO information for a port on a node. This is allocated
+ * by the host and configured on a node or all ports for which
+ * IO is requested.
+ *
+ * The plugin will communicate with the host through the IO
+ * areas.
+ */
+
+/** Different IO area types */
+enum spa_io_type {
+ SPA_IO_Invalid,
+ SPA_IO_Buffers, /**< area to exchange buffers, struct spa_io_buffers */
+ SPA_IO_Range, /**< expected byte range, struct spa_io_range */
+ SPA_IO_Clock, /**< area to update clock information, struct spa_io_clock */
+ SPA_IO_Latency, /**< latency reporting, struct spa_io_latency */
+ SPA_IO_Control, /**< area for control messages, struct spa_io_sequence */
+ SPA_IO_Notify, /**< area for notify messages, struct spa_io_sequence */
+ SPA_IO_Position, /**< position information in the graph, struct spa_io_position */
+ SPA_IO_RateMatch, /**< rate matching between nodes, struct spa_io_rate_match */
+ SPA_IO_Memory, /**< memory pointer, struct spa_io_memory */
+};
+
+/**
+ * IO area to exchange buffers.
+ *
+ * A set of buffers should first be configured on the node/port.
+ * Further references to those buffers will be made by using the
+ * id of the buffer.
+ *
+ * If status is SPA_STATUS_OK, the host should ignore
+ * the io area.
+ *
+ * If status is SPA_STATUS_NEED_DATA, the host should:
+ * 1) recycle the buffer in buffer_id, if possible
+ * 2) prepare a new buffer and place the id in buffer_id.
+ *
+ * If status is SPA_STATUS_HAVE_DATA, the host should consume
+ * the buffer in buffer_id and set the state to
+ * SPA_STATUS_NEED_DATA when new data is requested.
+ *
+ * If status is SPA_STATUS_STOPPED, some error occurred on the
+ * port.
+ *
+ * If status is SPA_STATUS_DRAINED, data from the io area was
+ * used to drain.
+ *
+ * Status can also be a negative errno value to indicate errors.
+ * such as:
+ * -EINVAL: buffer_id is invalid
+ * -EPIPE: no more buffers available
+ */
+struct spa_io_buffers {
+#define SPA_STATUS_OK 0
+#define SPA_STATUS_NEED_DATA (1<<0)
+#define SPA_STATUS_HAVE_DATA (1<<1)
+#define SPA_STATUS_STOPPED (1<<2)
+#define SPA_STATUS_DRAINED (1<<3)
+ int32_t status; /**< the status code */
+ uint32_t buffer_id; /**< a buffer id */
+};
+
+#define SPA_IO_BUFFERS_INIT ((struct spa_io_buffers) { SPA_STATUS_OK, SPA_ID_INVALID, })
+
+/**
+ * IO area to exchange a memory region
+ */
+struct spa_io_memory {
+ int32_t status; /**< the status code */
+ uint32_t size; /**< the size of \a data */
+ void *data; /**< a memory pointer */
+};
+#define SPA_IO_MEMORY_INIT ((struct spa_io_memory) { SPA_STATUS_OK, 0, NULL, })
+
+/** A range, suitable for input ports that can suggest a range to output ports */
+struct spa_io_range {
+ uint64_t offset; /**< offset in range */
+ uint32_t min_size; /**< minimum size of data */
+ uint32_t max_size; /**< maximum size of data */
+};
+
+/**
+ * Absolute time reporting.
+ *
+ * Nodes that can report clocking information will receive this io block.
+ * The application sets the id. This is usually set as part of the
+ * position information but can also be set separately.
+ *
+ * The clock counts the elapsed time according to the clock provider
+ * since the provider was last started.
+ */
+struct spa_io_clock {
+#define SPA_IO_CLOCK_FLAG_FREEWHEEL (1u<<0)
+ uint32_t flags; /**< clock flags */
+ uint32_t id; /**< unique clock id, set by application */
+ char name[64]; /**< clock name prefixed with API, set by node. The clock name
+ * is unique per clock and can be used to check if nodes
+ * share the same clock. */
+ uint64_t nsec; /**< time in nanoseconds against monotonic clock */
+ struct spa_fraction rate; /**< rate for position/duration/delay */
+ uint64_t position; /**< current position */
+ uint64_t duration; /**< duration of current cycle */
+ int64_t delay; /**< delay between position and hardware,
+ * positive for capture, negative for playback */
+ double rate_diff; /**< rate difference between clock and monotonic time */
+ uint64_t next_nsec; /**< estimated next wakeup time in nanoseconds */
+ uint32_t padding[8];
+};
+
+/* the size of the video in this cycle */
+struct spa_io_video_size {
+#define SPA_IO_VIDEO_SIZE_VALID (1<<0)
+ uint32_t flags; /**< optional flags */
+ uint32_t stride; /**< video stride in bytes */
+ struct spa_rectangle size; /**< the video size */
+ struct spa_fraction framerate; /**< the minimum framerate, the cycle duration is
+ * always smaller to ensure there is only one
+ * video frame per cycle. */
+ uint32_t padding[4];
+};
+
+/** latency reporting */
+struct spa_io_latency {
+ struct spa_fraction rate; /**< rate for min/max */
+ uint64_t min; /**< min latency */
+ uint64_t max; /**< max latency */
+};
+
+/** control stream, io area for SPA_IO_Control and SPA_IO_Notify */
+struct spa_io_sequence {
+ struct spa_pod_sequence sequence; /**< sequence of timed events */
+};
+
+/** bar and beat segment */
+struct spa_io_segment_bar {
+#define SPA_IO_SEGMENT_BAR_FLAG_VALID (1<<0)
+ uint32_t flags; /**< extra flags */
+ uint32_t offset; /**< offset in segment of this beat */
+ float signature_num; /**< time signature numerator */
+ float signature_denom; /**< time signature denominator */
+ double bpm; /**< beats per minute */
+ double beat; /**< current beat in segment */
+ uint32_t padding[8];
+};
+
+/** video frame segment */
+struct spa_io_segment_video {
+#define SPA_IO_SEGMENT_VIDEO_FLAG_VALID (1<<0)
+#define SPA_IO_SEGMENT_VIDEO_FLAG_DROP_FRAME (1<<1)
+#define SPA_IO_SEGMENT_VIDEO_FLAG_PULL_DOWN (1<<2)
+#define SPA_IO_SEGMENT_VIDEO_FLAG_INTERLACED (1<<3)
+ uint32_t flags; /**< flags */
+ uint32_t offset; /**< offset in segment */
+ struct spa_fraction framerate;
+ uint32_t hours;
+ uint32_t minutes;
+ uint32_t seconds;
+ uint32_t frames;
+ uint32_t field_count; /**< 0 for progressive, 1 and 2 for interlaced */
+ uint32_t padding[11];
+};
+
+/**
+ * A segment converts a running time to a segment (stream) position.
+ *
+ * The segment position is valid when the current running time is between
+ * start and start + duration. The position is then
+ * calculated as:
+ *
+ * (running time - start) * rate + position;
+ *
+ * Support for looping is done by specifying the LOOPING flags with a
+ * non-zero duration. When the running time reaches start + duration,
+ * duration is added to start and the loop repeats.
+ *
+ * Care has to be taken when the running time + clock.duration extends
+ * past the start + duration from the segment; the user should correctly
+ * wrap around and partially repeat the loop in the current cycle.
+ *
+ * Extra information can be placed in the segment by setting the valid flags
+ * and filling up the corresponding structures.
+ */
+struct spa_io_segment {
+ uint32_t version;
+#define SPA_IO_SEGMENT_FLAG_LOOPING (1<<0) /**< after the duration, the segment repeats */
+#define SPA_IO_SEGMENT_FLAG_NO_POSITION (1<<1) /**< position is invalid. The position can be invalid
+ * after a seek, for example, when the exact mapping
+ * of the extra segment info (bar, video, ...) to
+ * position has not been determined yet */
+ uint32_t flags; /**< extra flags */
+ uint64_t start; /**< value of running time when this
+ * info is active. Can be in the future for
+ * pending changes. It does not have to be in
+ * exact multiples of the clock duration. */
+ uint64_t duration; /**< duration when this info becomes invalid expressed
+ * in running time. If the duration is 0, this
+ * segment extends to the next segment. If the
+ * segment becomes invalid and the looping flag is
+ * set, the segment repeats. */
+ double rate; /**< overall rate of the segment, can be negative for
+ * backwards time reporting. */
+ uint64_t position; /**< The position when the running time == start.
+ * can be invalid when the owner of the extra segment
+ * information has not yet made the mapping. */
+
+ struct spa_io_segment_bar bar;
+ struct spa_io_segment_video video;
+};
+
+enum spa_io_position_state {
+ SPA_IO_POSITION_STATE_STOPPED,
+ SPA_IO_POSITION_STATE_STARTING,
+ SPA_IO_POSITION_STATE_RUNNING,
+};
+
+/** the maximum number of segments visible in the future */
+#define SPA_IO_POSITION_MAX_SEGMENTS 8
+
+/**
+ * The position information adds extra meaning to the raw clock times.
+ *
+ * It is set on all nodes and the clock id will contain the clock of the
+ * driving node in the graph.
+ *
+ * The position information contains 1 or more segments that convert the
+ * raw clock times to a stream time. They are sorted based on their
+ * start times, and thus the order in which they will activate in
+ * the future. This makes it possible to look ahead in the scheduled
+ * segments and anticipate the changes in the timeline.
+ */
+struct spa_io_position {
+ struct spa_io_clock clock; /**< clock position of driver, always valid and
+ * read only */
+ struct spa_io_video_size video; /**< size of the video in the current cycle */
+ int64_t offset; /**< an offset to subtract from the clock position
+ * to get a running time. This is the time that
+ * the state has been in the RUNNING state and the
+ * time that should be used to compare the segment
+ * start values against. */
+ uint32_t state; /**< one of enum spa_io_position_state */
+
+ uint32_t n_segments; /**< number of segments */
+ struct spa_io_segment segments[SPA_IO_POSITION_MAX_SEGMENTS]; /**< segments */
+};
+
+/** rate matching */
+struct spa_io_rate_match {
+ uint32_t delay; /**< extra delay in samples for resampler */
+ uint32_t size; /**< requested input size for resampler */
+ double rate; /**< rate for resampler */
+#define SPA_IO_RATE_MATCH_FLAG_ACTIVE (1 << 0)
+ uint32_t flags; /**< extra flags */
+ uint32_t padding[7];
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_IO_H */
diff --git a/spa/include/spa/node/keys.h b/spa/include/spa/node/keys.h
new file mode 100644
index 0000000..94a0285
--- /dev/null
+++ b/spa/include/spa/node/keys.h
@@ -0,0 +1,64 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_NODE_KEYS_H
+#define SPA_NODE_KEYS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_node
+ * \{
+ */
+
+/** node keys */
+#define SPA_KEY_NODE_NAME "node.name" /**< a node name */
+#define SPA_KEY_NODE_LATENCY "node.latency" /**< the requested node latency */
+#define SPA_KEY_NODE_MAX_LATENCY "node.max-latency" /**< maximum supported latency */
+
+#define SPA_KEY_NODE_DRIVER "node.driver" /**< the node can be a driver */
+#define SPA_KEY_NODE_ALWAYS_PROCESS "node.always-process" /**< call the process function even if
+ * not linked. */
+#define SPA_KEY_NODE_PAUSE_ON_IDLE "node.pause-on-idle" /**< if the node should be paused
+ * immediately when idle. */
+#define SPA_KEY_NODE_MONITOR "node.monitor" /**< the node has monitor ports */
+
+
+/** port keys */
+#define SPA_KEY_PORT_NAME "port.name" /**< a port name */
+#define SPA_KEY_PORT_ALIAS "port.alias" /**< a port alias */
+#define SPA_KEY_PORT_MONITOR "port.monitor" /**< this port is a monitor port */
+
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_NODE_KEYS_H */
diff --git a/spa/include/spa/node/node.h b/spa/include/spa/node/node.h
new file mode 100644
index 0000000..6efa6d0
--- /dev/null
+++ b/spa/include/spa/node/node.h
@@ -0,0 +1,680 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_NODE_H
+#define SPA_NODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup spa_node Node
+ *
+ * A spa_node is a component that can consume and produce buffers.
+ */
+
+/**
+ * \addtogroup spa_node
+ * \{
+ */
+
+#include <errno.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/type.h>
+#include <spa/utils/hook.h>
+#include <spa/buffer/buffer.h>
+#include <spa/node/event.h>
+#include <spa/node/command.h>
+
+
+#define SPA_TYPE_INTERFACE_Node SPA_TYPE_INFO_INTERFACE_BASE "Node"
+
+#define SPA_VERSION_NODE 0
+struct spa_node { struct spa_interface iface; };
+
+/**
+ * Node information structure
+ *
+ * Contains the basic node information.
+ */
+struct spa_node_info {
+ uint32_t max_input_ports;
+ uint32_t max_output_ports;
+#define SPA_NODE_CHANGE_MASK_FLAGS (1u<<0)
+#define SPA_NODE_CHANGE_MASK_PROPS (1u<<1)
+#define SPA_NODE_CHANGE_MASK_PARAMS (1u<<2)
+ uint64_t change_mask;
+
+#define SPA_NODE_FLAG_RT (1u<<0) /**< node can do real-time processing */
+#define SPA_NODE_FLAG_IN_DYNAMIC_PORTS (1u<<1) /**< input ports can be added/removed */
+#define SPA_NODE_FLAG_OUT_DYNAMIC_PORTS (1u<<2) /**< output ports can be added/removed */
+#define SPA_NODE_FLAG_IN_PORT_CONFIG (1u<<3) /**< input ports can be reconfigured with
+ * PortConfig parameter */
+#define SPA_NODE_FLAG_OUT_PORT_CONFIG (1u<<4) /**< output ports can be reconfigured with
+ * PortConfig parameter */
+#define SPA_NODE_FLAG_NEED_CONFIGURE (1u<<5) /**< node needs configuration before it can
+ * be started. */
+#define SPA_NODE_FLAG_ASYNC (1u<<6) /**< the process function might not
+ * immediately produce or consume data
+ * but might offload the work to a worker
+ * thread. */
+ uint64_t flags;
+ struct spa_dict *props; /**< extra node properties */
+ struct spa_param_info *params; /**< parameter information */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+#define SPA_NODE_INFO_INIT() ((struct spa_node_info) { 0, })
+
+/**
+ * Port information structure
+ *
+ * Contains the basic port information.
+ */
+struct spa_port_info {
+#define SPA_PORT_CHANGE_MASK_FLAGS (1u<<0)
+#define SPA_PORT_CHANGE_MASK_RATE (1u<<1)
+#define SPA_PORT_CHANGE_MASK_PROPS (1u<<2)
+#define SPA_PORT_CHANGE_MASK_PARAMS (1u<<3)
+ uint64_t change_mask;
+
+#define SPA_PORT_FLAG_REMOVABLE (1u<<0) /**< port can be removed */
+#define SPA_PORT_FLAG_OPTIONAL (1u<<1) /**< processing on port is optional */
+#define SPA_PORT_FLAG_CAN_ALLOC_BUFFERS (1u<<2) /**< the port can allocate buffer data */
+#define SPA_PORT_FLAG_IN_PLACE (1u<<3) /**< the port can process data in-place and
+ * will need a writable input buffer */
+#define SPA_PORT_FLAG_NO_REF (1u<<4) /**< the port does not keep a ref on the buffer.
+ * This means the node will always completely
+ * consume the input buffer and it will be
+ * recycled after process. */
+#define SPA_PORT_FLAG_LIVE (1u<<5) /**< output buffers from this port are
+ * timestamped against a live clock. */
+#define SPA_PORT_FLAG_PHYSICAL (1u<<6) /**< connects to some device */
+#define SPA_PORT_FLAG_TERMINAL (1u<<7) /**< data was not created from this port
+ * or will not be made available on another
+ * port */
+#define SPA_PORT_FLAG_DYNAMIC_DATA (1u<<8) /**< data pointer on buffers can be changed.
+ * Only the buffer data marked as DYNAMIC
+ * can be changed. */
+ uint64_t flags; /**< port flags */
+ struct spa_fraction rate; /**< rate of sequence numbers on port */
+ const struct spa_dict *props; /**< extra port properties */
+ struct spa_param_info *params; /**< parameter information */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+#define SPA_PORT_INFO_INIT() ((struct spa_port_info) { 0, })
+
+#define SPA_RESULT_TYPE_NODE_ERROR 1
+#define SPA_RESULT_TYPE_NODE_PARAMS 2
+
+/** an error result */
+struct spa_result_node_error {
+ const char *message;
+};
+
+/** the result of enum_params or port_enum_params. */
+struct spa_result_node_params {
+ uint32_t id; /**< id of parameter */
+ uint32_t index; /**< index of parameter */
+ uint32_t next; /**< next index of iteration */
+ struct spa_pod *param; /**< the result param */
+};
+
+#define SPA_NODE_EVENT_INFO 0
+#define SPA_NODE_EVENT_PORT_INFO 1
+#define SPA_NODE_EVENT_RESULT 2
+#define SPA_NODE_EVENT_EVENT 3
+#define SPA_NODE_EVENT_NUM 4
+
+/** events from the spa_node.
+ *
+ * All event are called from the main thread and multiple
+ * listeners can be registered for the events with
+ * spa_node_add_listener().
+ */
+struct spa_node_events {
+#define SPA_VERSION_NODE_EVENTS 0
+ uint32_t version; /**< version of this structure */
+
+ /** Emitted when info changes */
+ void (*info) (void *data, const struct spa_node_info *info);
+
+ /** Emitted when port info changes, NULL when port is removed */
+ void (*port_info) (void *data,
+ enum spa_direction direction, uint32_t port,
+ const struct spa_port_info *info);
+
+ /** notify a result.
+ *
+ * Some methods will trigger a result event with an optional
+ * result of the given type. Look at the documentation of the
+ * method to know when to expect a result event.
+ *
+ * The result event can be called synchronously, as an event
+ * called from inside the method itself, in which case the seq
+ * number passed to the method will be passed unchanged.
+ *
+ * The result event will be called asynchronously when the
+ * method returned an async return value. In this case, the seq
+ * number in the result will match the async return value of
+ * the method call. Users should match the seq number from
+ * request to the reply.
+ */
+ void (*result) (void *data, int seq, int res,
+ uint32_t type, const void *result);
+
+ /**
+ * \param node a spa_node
+ * \param event the event that was emitted
+ *
+ * This will be called when an out-of-bound event is notified
+ * on \a node.
+ */
+ void (*event) (void *data, const struct spa_event *event);
+};
+
+#define SPA_NODE_CALLBACK_READY 0
+#define SPA_NODE_CALLBACK_REUSE_BUFFER 1
+#define SPA_NODE_CALLBACK_XRUN 2
+#define SPA_NODE_CALLBACK_NUM 3
+
+/** Node callbacks
+ *
+ * Callbacks are called from the real-time data thread. Only
+ * one callback structure can be set on an spa_node.
+ */
+struct spa_node_callbacks {
+#define SPA_VERSION_NODE_CALLBACKS 0
+ uint32_t version;
+ /**
+ * \param node a spa_node
+ *
+ * The node is ready for processing.
+ *
+ * When this function is NULL, synchronous operation is requested
+ * on the ports.
+ */
+ int (*ready) (void *data, int state);
+
+ /**
+ * \param node a spa_node
+ * \param port_id an input port_id
+ * \param buffer_id the buffer id to be reused
+ *
+ * The node has a buffer that can be reused.
+ *
+ * When this function is NULL, the buffers to reuse will be set in
+ * the io area of the input ports.
+ */
+ int (*reuse_buffer) (void *data,
+ uint32_t port_id,
+ uint32_t buffer_id);
+
+ /**
+ * \param data user data
+ * \param trigger the timestamp in microseconds when the xrun happened
+ * \param delay the amount of microseconds of xrun.
+ * \param info an object with extra info (NULL for now)
+ *
+ * The node has encountered an over or underrun
+ *
+ * The info contains an object with more information
+ */
+ int (*xrun) (void *data, uint64_t trigger, uint64_t delay,
+ struct spa_pod *info);
+};
+
+
+/** flags that can be passed to set_param and port_set_param functions */
+#define SPA_NODE_PARAM_FLAG_TEST_ONLY (1 << 0) /**< Just check if the param is accepted */
+#define SPA_NODE_PARAM_FLAG_FIXATE (1 << 1) /**< Fixate the non-optional unset fields */
+#define SPA_NODE_PARAM_FLAG_NEAREST (1 << 2) /**< Allow set fields to be rounded to the
+ * nearest allowed field value. */
+
+/** flags to pass to the use_buffers functions */
+#define SPA_NODE_BUFFERS_FLAG_ALLOC (1 << 0) /**< Allocate memory for the buffers. This flag
+ * is ignored when the port does not have the
+ * SPA_PORT_FLAG_CAN_ALLOC_BUFFERS set. */
+
+
+#define SPA_NODE_METHOD_ADD_LISTENER 0
+#define SPA_NODE_METHOD_SET_CALLBACKS 1
+#define SPA_NODE_METHOD_SYNC 2
+#define SPA_NODE_METHOD_ENUM_PARAMS 3
+#define SPA_NODE_METHOD_SET_PARAM 4
+#define SPA_NODE_METHOD_SET_IO 5
+#define SPA_NODE_METHOD_SEND_COMMAND 6
+#define SPA_NODE_METHOD_ADD_PORT 7
+#define SPA_NODE_METHOD_REMOVE_PORT 8
+#define SPA_NODE_METHOD_PORT_ENUM_PARAMS 9
+#define SPA_NODE_METHOD_PORT_SET_PARAM 10
+#define SPA_NODE_METHOD_PORT_USE_BUFFERS 11
+#define SPA_NODE_METHOD_PORT_SET_IO 12
+#define SPA_NODE_METHOD_PORT_REUSE_BUFFER 13
+#define SPA_NODE_METHOD_PROCESS 14
+#define SPA_NODE_METHOD_NUM 15
+
+/**
+ * Node methods
+ */
+struct spa_node_methods {
+ /* the version of the node methods. This can be used to expand this
+ * structure in the future */
+#define SPA_VERSION_NODE_METHODS 0
+ uint32_t version;
+
+ /**
+ * Adds an event listener on \a node.
+ *
+ * Setting the events will trigger the info event and a
+ * port_info event for each managed port on the new
+ * listener.
+ *
+ * \param node a #spa_node
+ * \param listener a listener
+ * \param events a struct \ref spa_node_events
+ * \param data data passed as first argument in functions of \a events
+ * \return 0 on success
+ * < 0 errno on error
+ */
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data);
+ /**
+ * Set callbacks to on \a node.
+ * if \a callbacks is NULL, the current callbacks are removed.
+ *
+ * This function must be called from the main thread.
+ *
+ * All callbacks are called from the data thread.
+ *
+ * \param node a spa_node
+ * \param callbacks callbacks to set
+ * \return 0 on success
+ * -EINVAL when node is NULL
+ */
+ int (*set_callbacks) (void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data);
+ /**
+ * Perform a sync operation.
+ *
+ * This method will emit the result event with the given sequence
+ * number synchronously or with the returned async return value
+ * asynchronously.
+ *
+ * Because all methods are serialized in the node, this can be used
+ * to wait for completion of all previous method calls.
+ *
+ * \param seq a sequence number
+ * \return 0 on success
+ * -EINVAL when node is NULL
+ * an async result
+ */
+ int (*sync) (void *object, int seq);
+
+ /**
+ * Enumerate the parameters of a node.
+ *
+ * Parameters are identified with an \a id. Some parameters can have
+ * multiple values, see the documentation of the parameter id.
+ *
+ * Parameters can be filtered by passing a non-NULL \a filter.
+ *
+ * The function will emit the result event up to \a max times with
+ * the result value. The seq in the result will either be the \a seq
+ * number when executed synchronously or the async return value of
+ * this function when executed asynchronously.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param node a \ref spa_node
+ * \param seq a sequence number to pass to the result event when
+ * this method is executed synchronously.
+ * \param id the param id to enumerate
+ * \param start the index of enumeration, pass 0 for the first item
+ * \param max the maximum number of parameters to enumerate
+ * \param filter and optional filter to use
+ *
+ * \return 0 when no more items can be iterated.
+ * -EINVAL when invalid arguments are given
+ * -ENOENT the parameter \a id is unknown
+ * -ENOTSUP when there are no parameters
+ * implemented on \a node
+ * an async return value when the result event will be
+ * emitted later.
+ */
+ int (*enum_params) (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t max,
+ const struct spa_pod *filter);
+
+ /**
+ * Set the configurable parameter in \a node.
+ *
+ * Usually, \a param will be obtained from enum_params and then
+ * modified but it is also possible to set another spa_pod
+ * as long as its keys and types match a supported object.
+ *
+ * Objects with property keys that are not known are ignored.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param node a \ref spa_node
+ * \param id the parameter id to configure
+ * \param flags additional flags
+ * \param param the parameter to configure
+ *
+ * \return 0 on success
+ * -EINVAL when node is NULL
+ * -ENOTSUP when there are no parameters implemented on \a node
+ * -ENOENT the parameter is unknown
+ */
+ int (*set_param) (void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ /**
+ * Configure the given memory area with \a id on \a node. This
+ * structure is allocated by the host and is used to exchange
+ * data and parameters with the node.
+ *
+ * Setting an \a io of NULL will disable the node io.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param id the id of the io area, the available ids can be
+ * enumerated with the node parameters.
+ * \param data a io area memory
+ * \param size the size of \a data
+ * \return 0 on success
+ * -EINVAL when invalid input is given
+ * -ENOENT when \a id is unknown
+ * -ENOSPC when \a size is too small
+ */
+ int (*set_io) (void *object,
+ uint32_t id, void *data, size_t size);
+
+ /**
+ * Send a command to a node.
+ *
+ * Upon completion, a command might change the state of a node.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param node a spa_node
+ * \param command a spa_command
+ * \return 0 on success
+ * -EINVAL when node or command is NULL
+ * -ENOTSUP when this node can't process commands
+ * -EINVAL \a command is an invalid command
+ */
+ int (*send_command) (void *object, const struct spa_command *command);
+
+ /**
+ * Make a new port with \a port_id. The caller should use the lowest unused
+ * port id for the given \a direction.
+ *
+ * Port ids should be between 0 and max_ports as obtained from the info
+ * event.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param node a spa_node
+ * \param direction a enum \ref spa_direction
+ * \param port_id an unused port id
+ * \param props extra properties
+ * \return 0 on success
+ * -EINVAL when node is NULL
+ */
+ int (*add_port) (void *object,
+ enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props);
+
+ /**
+ * Remove a port with \a port_id.
+ *
+ * \param node a spa_node
+ * \param direction a enum \ref spa_direction
+ * \param port_id a port id
+ * \return 0 on success
+ * -EINVAL when node is NULL or when port_id is unknown or
+ * when the port can't be removed.
+ */
+ int (*remove_port) (void *object,
+ enum spa_direction direction, uint32_t port_id);
+
+ /**
+ * Enumerate all possible parameters of \a id on \a port_id of \a node
+ * that are compatible with \a filter.
+ *
+ * The result parameters can be queried and modified and ultimately be used
+ * to call port_set_param.
+ *
+ * The function will emit the result event up to \a max times with
+ * the result value. The seq in the result event will either be the
+ * \a seq number when executed synchronously or the async return
+ * value of this function when executed asynchronously.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param node a spa_node
+ * \param seq a sequence number to pass to the result event when
+ * this method is executed synchronously.
+ * \param direction an spa_direction
+ * \param port_id the port to query
+ * \param id the parameter id to query
+ * \param start the first index to query, 0 to get the first item
+ * \param max the maximum number of params to query
+ * \param filter a parameter filter or NULL for no filter
+ *
+ * \return 0 when no more items can be iterated.
+ * -EINVAL when invalid parameters are given
+ * -ENOENT when \a id is unknown
+ * an async return value when the result event will be
+ * emitted later.
+ */
+ int (*port_enum_params) (void *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);
+ /**
+ * Set a parameter on \a port_id of \a node.
+ *
+ * When \a param is NULL, the parameter will be unset.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param node a struct \ref spa_node
+ * \param direction a enum \ref spa_direction
+ * \param port_id the port to configure
+ * \param id the parameter id to set
+ * \param flags optional flags
+ * \param param a struct \ref spa_pod with the parameter to set
+ * \return 0 on success
+ * 1 on success, the value of \a param might have been
+ * changed depending on \a flags and the final value can be found by
+ * doing port_enum_params.
+ * -EINVAL when node is NULL or invalid arguments are given
+ * -ESRCH when one of the mandatory param
+ * properties is not specified and SPA_NODE_PARAM_FLAG_FIXATE was
+ * not set in \a flags.
+ * -ESRCH when the type or size of a property is not correct.
+ * -ENOENT when the param id is not found
+ */
+ int (*port_set_param) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ /**
+ * Tell the port to use the given buffers
+ *
+ * When \a flags contains SPA_NODE_BUFFERS_FLAG_ALLOC, the data
+ * in the buffers should point to an array of at least 1 data entry
+ * with the desired supported type that will be filled by this function.
+ *
+ * The port should also have a spa_io_buffers io area configured to exchange
+ * the buffers with the port.
+ *
+ * For an input port, all the buffers will remain dequeued.
+ * Once a buffer has been queued on a port in the spa_io_buffers,
+ * it should not be reused until the reuse_buffer callback is notified
+ * or when the buffer has been returned in the spa_io_buffers of
+ * the port.
+ *
+ * For output ports, all buffers will be queued in the port. When process
+ * returns SPA_STATUS_HAVE_DATA, buffers are available in one or more
+ * of the spa_io_buffers areas.
+ *
+ * When a buffer can be reused, port_reuse_buffer() should be called or the
+ * buffer_id should be placed in the spa_io_buffers area before calling
+ * process.
+ *
+ * Passing NULL as \a buffers will remove the reference that the port has
+ * on the buffers.
+ *
+ * When this function returns async, use the spa_node_sync operation to
+ * wait for completion.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param object an object implementing the interface
+ * \param direction a port direction
+ * \param port_id a port id
+ * \param flags extra flags
+ * \param buffers an array of buffer pointers
+ * \param n_buffers number of elements in \a buffers
+ * \return 0 on success
+ */
+ int (*port_use_buffers) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers);
+
+ /**
+ * Configure the given memory area with \a id on \a port_id. This
+ * structure is allocated by the host and is used to exchange
+ * data and parameters with the port.
+ *
+ * Setting an \a io of NULL will disable the port io.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param direction a spa_direction
+ * \param port_id a port id
+ * \param id the id of the io area, the available ids can be
+ * enumerated with the port parameters.
+ * \param data a io area memory
+ * \param size the size of \a data
+ * \return 0 on success
+ * -EINVAL when invalid input is given
+ * -ENOENT when \a id is unknown
+ * -ENOSPC when \a size is too small
+ */
+ int (*port_set_io) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size);
+
+ /**
+ * Tell an output port to reuse a buffer.
+ *
+ * This function must be called from the data thread.
+ *
+ * \param node a spa_node
+ * \param port_id a port id
+ * \param buffer_id a buffer id to reuse
+ * \return 0 on success
+ * -EINVAL when node is NULL
+ */
+ int (*port_reuse_buffer) (void *object, uint32_t port_id, uint32_t buffer_id);
+
+ /**
+ * Process the node
+ *
+ * This function must be called from the data thread.
+ *
+ * Output io areas with SPA_STATUS_NEED_DATA will recycle the
+ * buffers if any.
+ *
+ * Input areas with SPA_STATUS_HAVE_DATA are consumed if possible
+ * and the status is set to SPA_STATUS_NEED_DATA or SPA_STATUS_OK.
+ *
+ * When the node has new output buffers, the SPA_STATUS_HAVE_DATA
+ * bit will be set.
+ *
+ * When the node can accept new input in the next cycle, the
+ * SPA_STATUS_NEED_DATA bit will be set.
+ *
+ * Note that the node might return SPA_STATUS_NEED_DATA even when
+ * no input ports have this status. This means that the amount of
+ * data still available on the input ports is likely not going to
+ * be enough for the next cycle and the host might need to prefetch
+ * data for the next cycle.
+ */
+ int (*process) (void *object);
+};
+
+#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_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_process(n) spa_node_method(n, process, 0)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_NODE_H */
diff --git a/spa/include/spa/node/type-info.h b/spa/include/spa/node/type-info.h
new file mode 100644
index 0000000..1ebbfe5
--- /dev/null
+++ b/spa/include/spa/node/type-info.h
@@ -0,0 +1,107 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_NODE_TYPES_H
+#define SPA_NODE_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_node
+ * \{
+ */
+
+#include <spa/utils/type.h>
+
+#include <spa/node/command.h>
+#include <spa/node/event.h>
+#include <spa/node/io.h>
+
+#define SPA_TYPE_INFO_IO SPA_TYPE_INFO_ENUM_BASE "IO"
+#define SPA_TYPE_INFO_IO_BASE SPA_TYPE_INFO_IO ":"
+
+static const struct spa_type_info spa_type_io[] = {
+ { SPA_IO_Invalid, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Invalid", NULL },
+ { SPA_IO_Buffers, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Buffers", NULL },
+ { SPA_IO_Range, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Range", NULL },
+ { SPA_IO_Clock, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Clock", NULL },
+ { SPA_IO_Latency, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Latency", NULL },
+ { SPA_IO_Control, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Control", NULL },
+ { SPA_IO_Notify, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Notify", NULL },
+ { SPA_IO_Position, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Position", NULL },
+ { SPA_IO_RateMatch, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "RateMatch", NULL },
+ { SPA_IO_Memory, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "Memory", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_NodeEvent SPA_TYPE_INFO_EVENT_BASE "Node"
+#define SPA_TYPE_INFO_NODE_EVENT_BASE SPA_TYPE_INFO_NodeEvent ":"
+
+static const struct spa_type_info spa_type_node_event_id[] = {
+ { SPA_NODE_EVENT_Error, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "Error", NULL },
+ { SPA_NODE_EVENT_Buffering, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "Buffering", NULL },
+ { SPA_NODE_EVENT_RequestRefresh, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "RequestRefresh", NULL },
+ { SPA_NODE_EVENT_RequestProcess, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "RequestProcess", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+static const struct spa_type_info spa_type_node_event[] = {
+ { SPA_EVENT_NODE_START, SPA_TYPE_Id, SPA_TYPE_INFO_NODE_EVENT_BASE, spa_type_node_event_id },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_NodeCommand SPA_TYPE_INFO_COMMAND_BASE "Node"
+#define SPA_TYPE_INFO_NODE_COMMAND_BASE SPA_TYPE_INFO_NodeCommand ":"
+
+static const struct spa_type_info spa_type_node_command_id[] = {
+ { SPA_NODE_COMMAND_Suspend, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Suspend", NULL },
+ { SPA_NODE_COMMAND_Pause, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Pause", NULL },
+ { SPA_NODE_COMMAND_Start, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Start", NULL },
+ { SPA_NODE_COMMAND_Enable, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Enable", NULL },
+ { SPA_NODE_COMMAND_Disable, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Disable", NULL },
+ { SPA_NODE_COMMAND_Flush, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Flush", NULL },
+ { SPA_NODE_COMMAND_Drain, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Drain", NULL },
+ { SPA_NODE_COMMAND_Marker, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Marker", NULL },
+ { SPA_NODE_COMMAND_ParamBegin, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "ParamBegin", NULL },
+ { SPA_NODE_COMMAND_ParamEnd, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "ParamEnd", NULL },
+ { SPA_NODE_COMMAND_RequestProcess, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "RequestProcess", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+static const struct spa_type_info spa_type_node_command[] = {
+ { 0, SPA_TYPE_Id, SPA_TYPE_INFO_NODE_COMMAND_BASE, spa_type_node_command_id },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_NODE_TYPES_H */
diff --git a/spa/include/spa/node/utils.h b/spa/include/spa/node/utils.h
new file mode 100644
index 0000000..9503d8b
--- /dev/null
+++ b/spa/include/spa/node/utils.h
@@ -0,0 +1,158 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_NODE_UTILS_H
+#define SPA_NODE_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_node
+ * \{
+ */
+
+#include <spa/pod/builder.h>
+
+#include <spa/node/node.h>
+
+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,
+ int seq, int res, uint32_t type, const void *result)
+{
+ struct spa_result_node_params_data *d =
+ (struct spa_result_node_params_data *) data;
+ const struct spa_result_node_params *r =
+ (const struct spa_result_node_params *) result;
+ uint32_t offset = d->builder->state.offset;
+ if (spa_pod_builder_raw_padded(d->builder, r->param, SPA_POD_SIZE(r->param)) < 0)
+ return;
+ d->data.next = r->next;
+ d->data.param = spa_pod_builder_deref(d->builder, offset);
+}
+
+static inline 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,
+ struct spa_pod_builder *builder)
+{
+ struct spa_result_node_params_data data = { builder, };
+ struct spa_hook listener = {{0}};
+ static const struct spa_node_events node_events = {
+ .version = SPA_VERSION_NODE_EVENTS,
+ .info = NULL,
+ .port_info = NULL,
+ .result = spa_result_func_node_params,
+ };
+ int res;
+
+ res = spa_node_add_listener(node, &listener, &node_events, &data);
+ if (res >= 0) {
+ res = spa_node_enum_params(node, 0, id, *index, 1, filter);
+ spa_hook_remove(&listener);
+ }
+
+ if (data.data.param == NULL) {
+ if (res > 0)
+ res = 0;
+ } else {
+ *index = data.data.next;
+ *param = data.data.param;
+ res = 1;
+ }
+ return res;
+}
+
+static inline 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,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct spa_result_node_params_data data = { builder, };
+ struct spa_hook listener = {{0}};
+ static const struct spa_node_events node_events = {
+ .version = SPA_VERSION_NODE_EVENTS,
+ .info = NULL,
+ .port_info = NULL,
+ .result = spa_result_func_node_params,
+ };
+ int res;
+
+ res = spa_node_add_listener(node, &listener, &node_events, &data);
+ if (res >= 0) {
+ res = spa_node_port_enum_params(node, 0, direction, port_id,
+ id, *index, 1, filter);
+ spa_hook_remove(&listener);
+ }
+
+ if (data.data.param == NULL) {
+ if (res > 0)
+ res = 0;
+ } else {
+ *index = data.data.next;
+ *param = data.data.param;
+ res = 1;
+ }
+ return res;
+}
+
+#define spa_node_emit(hooks,method,version,...) \
+ spa_hook_list_call_simple(hooks, struct spa_node_events, \
+ method, version, ##__VA_ARGS__)
+
+#define spa_node_emit_info(hooks,...) spa_node_emit(hooks,info, 0, __VA_ARGS__)
+#define spa_node_emit_port_info(hooks,...) spa_node_emit(hooks,port_info, 0, __VA_ARGS__)
+#define spa_node_emit_result(hooks,...) spa_node_emit(hooks,result, 0, __VA_ARGS__)
+#define spa_node_emit_event(hooks,...) spa_node_emit(hooks,event, 0, __VA_ARGS__)
+
+
+#define spa_node_call(callbacks,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_callbacks_call_res(callbacks, struct spa_node_callbacks, \
+ _res, method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+#define spa_node_call_ready(hook,...) spa_node_call(hook, ready, 0, __VA_ARGS__)
+#define spa_node_call_reuse_buffer(hook,...) spa_node_call(hook, reuse_buffer, 0, __VA_ARGS__)
+#define spa_node_call_xrun(hook,...) spa_node_call(hook, xrun, 0, __VA_ARGS__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_NODE_UTILS_H */
diff --git a/spa/include/spa/param/audio/aac-types.h b/spa/include/spa/param/audio/aac-types.h
new file mode 100644
index 0000000..096729d
--- /dev/null
+++ b/spa/include/spa/param/audio/aac-types.h
@@ -0,0 +1,58 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_AAC_TYPES_H
+#define SPA_AUDIO_AAC_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/type.h>
+#include <spa/param/audio/aac.h>
+
+#define SPA_TYPE_INFO_AudioAACStreamFormat SPA_TYPE_INFO_ENUM_BASE "AudioAACStreamFormat"
+#define SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE SPA_TYPE_INFO_AudioAACStreamFormat ":"
+
+static const struct spa_type_info spa_type_audio_aac_stream_format[] = {
+ { SPA_AUDIO_AAC_STREAM_FORMAT_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "UNKNOWN", NULL },
+ { SPA_AUDIO_AAC_STREAM_FORMAT_RAW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "RAW", NULL },
+ { SPA_AUDIO_AAC_STREAM_FORMAT_MP2ADTS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP2ADTS", NULL },
+ { SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP4ADTS", NULL },
+ { SPA_AUDIO_AAC_STREAM_FORMAT_MP4LOAS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP4LOAS", NULL },
+ { SPA_AUDIO_AAC_STREAM_FORMAT_MP4LATM, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP4LATM", NULL },
+ { SPA_AUDIO_AAC_STREAM_FORMAT_ADIF, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "ADIF", NULL },
+ { SPA_AUDIO_AAC_STREAM_FORMAT_MP4FF, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP4FF", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_AAC_TYPES_H */
diff --git a/spa/include/spa/param/audio/aac-utils.h b/spa/include/spa/param/audio/aac-utils.h
new file mode 100644
index 0000000..4d53987
--- /dev/null
+++ b/spa/include/spa/param/audio/aac-utils.h
@@ -0,0 +1,89 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_AAC_UTILS_H
+#define SPA_AUDIO_AAC_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_aac_parse(const struct spa_pod *format, struct spa_audio_info_aac *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels),
+ SPA_FORMAT_AUDIO_bitrate, SPA_POD_OPT_Int(&info->bitrate),
+ SPA_FORMAT_AUDIO_AAC_streamFormat, SPA_POD_OPT_Id(&info->stream_format));
+
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_aac_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_aac *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_aac),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ if (info->bitrate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_bitrate, SPA_POD_Int(info->bitrate), 0);
+ if (info->stream_format != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_AAC_streamFormat, SPA_POD_Id(info->stream_format), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_AAC_UTILS_H */
diff --git a/spa/include/spa/param/audio/aac.h b/spa/include/spa/param/audio/aac.h
new file mode 100644
index 0000000..7faf449
--- /dev/null
+++ b/spa/include/spa/param/audio/aac.h
@@ -0,0 +1,71 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_AAC_H
+#define SPA_AUDIO_AAC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+enum spa_audio_aac_stream_format {
+ SPA_AUDIO_AAC_STREAM_FORMAT_UNKNOWN,
+ /* Raw AAC frames */
+ SPA_AUDIO_AAC_STREAM_FORMAT_RAW,
+ /* ISO/IEC 13818-7 MPEG-2 Audio Data Transport Stream (ADTS) */
+ SPA_AUDIO_AAC_STREAM_FORMAT_MP2ADTS,
+ /* ISO/IEC 14496-3 MPEG-4 Audio Data Transport Stream (ADTS) */
+ SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS,
+ /* ISO/IEC 14496-3 Low Overhead Audio Stream (LOAS) */
+ SPA_AUDIO_AAC_STREAM_FORMAT_MP4LOAS,
+ /* ISO/IEC 14496-3 Low Overhead Audio Transport Multiplex (LATM) */
+ SPA_AUDIO_AAC_STREAM_FORMAT_MP4LATM,
+ /* ISO/IEC 14496-3 Audio Data Interchange Format (ADIF) */
+ SPA_AUDIO_AAC_STREAM_FORMAT_ADIF,
+ /* ISO/IEC 14496-12 MPEG-4 file format */
+ SPA_AUDIO_AAC_STREAM_FORMAT_MP4FF,
+
+ SPA_AUDIO_AAC_STREAM_FORMAT_CUSTOM = 0x10000,
+};
+
+struct spa_audio_info_aac {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+ uint32_t bitrate; /*< stream bitrate */
+ enum spa_audio_aac_stream_format stream_format; /*< AAC audio stream format */
+};
+
+#define SPA_AUDIO_INFO_AAC_INIT(...) ((struct spa_audio_info_aac) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_AAC_H */
diff --git a/spa/include/spa/param/audio/alac-utils.h b/spa/include/spa/param/audio/alac-utils.h
new file mode 100644
index 0000000..39dbd78
--- /dev/null
+++ b/spa/include/spa/param/audio/alac-utils.h
@@ -0,0 +1,80 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_ALAC_UTILS_H
+#define SPA_AUDIO_ALAC_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_alac_parse(const struct spa_pod *format, struct spa_audio_info_alac *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_alac_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_alac *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_alac),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_ALAC_UTILS_H */
diff --git a/spa/include/spa/param/audio/alac.h b/spa/include/spa/param/audio/alac.h
new file mode 100644
index 0000000..37ae170
--- /dev/null
+++ b/spa/include/spa/param/audio/alac.h
@@ -0,0 +1,49 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_ALAC_H
+#define SPA_AUDIO_ALAC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+struct spa_audio_info_alac {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+};
+
+#define SPA_AUDIO_INFO_ALAC_INIT(...) ((struct spa_audio_info_alac) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_ALAC_H */
diff --git a/spa/include/spa/param/audio/amr-types.h b/spa/include/spa/param/audio/amr-types.h
new file mode 100644
index 0000000..7d87c01
--- /dev/null
+++ b/spa/include/spa/param/audio/amr-types.h
@@ -0,0 +1,52 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_AMR_TYPES_H
+#define SPA_AUDIO_AMR_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/type.h>
+#include <spa/param/audio/amr.h>
+
+#define SPA_TYPE_INFO_AudioAMRBandMode SPA_TYPE_INFO_ENUM_BASE "AudioAMRBandMode"
+#define SPA_TYPE_INFO_AUDIO_AMR_BAND_MODE_BASE SPA_TYPE_INFO_AudioAMRBandMode ":"
+
+static const struct spa_type_info spa_type_audio_amr_band_mode[] = {
+ { SPA_AUDIO_AMR_BAND_MODE_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AMR_BAND_MODE_BASE "UNKNOWN", NULL },
+ { SPA_AUDIO_AMR_BAND_MODE_NB, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AMR_BAND_MODE_BASE "NB", NULL },
+ { SPA_AUDIO_AMR_BAND_MODE_WB, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AMR_BAND_MODE_BASE "WB", NULL },
+ { 0, 0, NULL, NULL },
+};
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_AMR_TYPES_H */
diff --git a/spa/include/spa/param/audio/amr-utils.h b/spa/include/spa/param/audio/amr-utils.h
new file mode 100644
index 0000000..dafe1f8
--- /dev/null
+++ b/spa/include/spa/param/audio/amr-utils.h
@@ -0,0 +1,84 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_AMR_UTILS_H
+#define SPA_AUDIO_AMR_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_amr_parse(const struct spa_pod *format, struct spa_audio_info_amr *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels),
+ SPA_FORMAT_AUDIO_AMR_bandMode, SPA_POD_OPT_Id(&info->band_mode));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_amr_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_amr *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_amr),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ if (info->band_mode != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_AMR_bandMode, SPA_POD_Id(info->band_mode), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_AMR_UTILS_H */
diff --git a/spa/include/spa/param/audio/amr.h b/spa/include/spa/param/audio/amr.h
new file mode 100644
index 0000000..360b8eb
--- /dev/null
+++ b/spa/include/spa/param/audio/amr.h
@@ -0,0 +1,56 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_AMR_H
+#define SPA_AUDIO_AMR_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+enum spa_audio_amr_band_mode {
+ SPA_AUDIO_AMR_BAND_MODE_UNKNOWN,
+ SPA_AUDIO_AMR_BAND_MODE_NB,
+ SPA_AUDIO_AMR_BAND_MODE_WB,
+};
+
+struct spa_audio_info_amr {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+ enum spa_audio_amr_band_mode band_mode;
+};
+
+#define SPA_AUDIO_INFO_AMR_INIT(...) ((struct spa_audio_info_amr) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_AMR_H */
diff --git a/spa/include/spa/param/audio/ape-utils.h b/spa/include/spa/param/audio/ape-utils.h
new file mode 100644
index 0000000..a625900
--- /dev/null
+++ b/spa/include/spa/param/audio/ape-utils.h
@@ -0,0 +1,80 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_APE_UTILS_H
+#define SPA_AUDIO_APE_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_ape_parse(const struct spa_pod *format, struct spa_audio_info_ape *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_ape_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_ape *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_ape),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_APE_UTILS_H */
diff --git a/spa/include/spa/param/audio/ape.h b/spa/include/spa/param/audio/ape.h
new file mode 100644
index 0000000..3e48b15
--- /dev/null
+++ b/spa/include/spa/param/audio/ape.h
@@ -0,0 +1,49 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_APE_H
+#define SPA_AUDIO_APE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+struct spa_audio_info_ape {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+};
+
+#define SPA_AUDIO_INFO_APE_INIT(...) ((struct spa_audio_info_ape) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_APE_H */
diff --git a/spa/include/spa/param/audio/compressed.h b/spa/include/spa/param/audio/compressed.h
new file mode 100644
index 0000000..ec8a38a
--- /dev/null
+++ b/spa/include/spa/param/audio/compressed.h
@@ -0,0 +1,39 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Wim Taymans
+ * © 2022 Asymptotic Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_COMPRESSED_H
+#define SPA_AUDIO_COMPRESSED_H
+
+#include <spa/param/audio/aac.h>
+#include <spa/param/audio/alac.h>
+#include <spa/param/audio/amr.h>
+#include <spa/param/audio/ape.h>
+#include <spa/param/audio/flac.h>
+#include <spa/param/audio/mp3.h>
+#include <spa/param/audio/ra.h>
+#include <spa/param/audio/vorbis.h>
+#include <spa/param/audio/wma.h>
+
+#endif /* SPA_AUDIO_COMPRESSED_H */
diff --git a/spa/include/spa/param/audio/dsd-utils.h b/spa/include/spa/param/audio/dsd-utils.h
new file mode 100644
index 0000000..795a0b1
--- /dev/null
+++ b/spa/include/spa/param/audio/dsd-utils.h
@@ -0,0 +1,100 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_DSD_UTILS_H
+#define SPA_AUDIO_DSD_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/format-utils.h>
+#include <spa/param/audio/format.h>
+
+static inline int
+spa_format_audio_dsd_parse(const struct spa_pod *format, struct spa_audio_info_dsd *info)
+{
+ struct spa_pod *position = NULL;
+ int res;
+ info->flags = 0;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_bitorder, SPA_POD_OPT_Id(&info->bitorder),
+ SPA_FORMAT_AUDIO_interleave, SPA_POD_OPT_Int(&info->interleave),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels),
+ SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position));
+ if (position == NULL ||
+ !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS))
+ SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
+
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_dsd_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_dsd *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsd),
+ 0);
+ if (info->bitorder != SPA_PARAM_BITORDER_unknown)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_bitorder, SPA_POD_Id(info->bitorder), 0);
+ if (info->interleave != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_interleave, SPA_POD_Int(info->interleave), 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0) {
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ if (!SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ spa_pod_builder_add(builder, SPA_FORMAT_AUDIO_position,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id,
+ info->channels, info->position), 0);
+ }
+ }
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_DSD_UTILS_H */
diff --git a/spa/include/spa/param/audio/dsd.h b/spa/include/spa/param/audio/dsd.h
new file mode 100644
index 0000000..3b317f2
--- /dev/null
+++ b/spa/include/spa/param/audio/dsd.h
@@ -0,0 +1,81 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_DSD_H
+#define SPA_AUDIO_DSD_H
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/param.h>
+#include <spa/param/audio/raw.h>
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+/** Extra DSD audio flags */
+#define SPA_AUDIO_DSD_FLAG_NONE (0) /*< no valid flag */
+
+/* DSD bits are transferred in a buffer grouped in bytes with the bitorder
+ * defined by \a bitorder.
+ *
+ * Channels are placed in separate planes (interleave = 0) or interleaved
+ * using the interleave value. A negative interleave value means that the
+ * bytes need to be reversed in the group.
+ *
+ * Planar (interleave = 0):
+ * plane1: l1 l2 l3 l4 l5 ...
+ * plane2: r1 r2 r3 r4 r5 ...
+ *
+ * Interleaved 4:
+ * plane1: l1 l2 l3 l4 r1 r2 r3 r4 l5 l6 l7 l8 r5 r6 r7 r8 l9 ...
+ *
+ * Interleaved 2:
+ * plane1: l1 l2 r1 r2 l3 l4 r3 r4 ...
+ */
+struct spa_audio_info_dsd {
+ enum spa_param_bitorder bitorder; /*< the order of the bits */
+ uint32_t flags; /*< extra flags */
+ int32_t interleave; /*< interleave bytes */
+ uint32_t rate; /*< sample rate (in bytes per second) */
+ uint32_t channels; /*< channels */
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS]; /*< channel position from enum spa_audio_channel */
+};
+
+#define SPA_AUDIO_INFO_DSD_INIT(...) ((struct spa_audio_info_dsd) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_DSD_H */
diff --git a/spa/include/spa/param/audio/dsp-utils.h b/spa/include/spa/param/audio/dsp-utils.h
new file mode 100644
index 0000000..4bbfe8e
--- /dev/null
+++ b/spa/include/spa/param/audio/dsp-utils.h
@@ -0,0 +1,75 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_DSP_UTILS_H
+#define SPA_AUDIO_DSP_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_dsp_parse(const struct spa_pod *format, struct spa_audio_info_dsp *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_format, SPA_POD_OPT_Id(&info->format));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_dsp_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_dsp *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ 0);
+ if (info->format != SPA_AUDIO_FORMAT_UNKNOWN)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(info->format), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_DSP_UTILS_H */
diff --git a/spa/include/spa/param/audio/dsp.h b/spa/include/spa/param/audio/dsp.h
new file mode 100644
index 0000000..fbc0a3f
--- /dev/null
+++ b/spa/include/spa/param/audio/dsp.h
@@ -0,0 +1,48 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_DSP_H
+#define SPA_AUDIO_DSP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+struct spa_audio_info_dsp {
+ enum spa_audio_format format; /*< format, one of the DSP formats in enum spa_audio_format */
+};
+
+#define SPA_AUDIO_INFO_DSP_INIT(...) ((struct spa_audio_info_dsp) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_DSP_H */
diff --git a/spa/include/spa/param/audio/flac-utils.h b/spa/include/spa/param/audio/flac-utils.h
new file mode 100644
index 0000000..7c612d6
--- /dev/null
+++ b/spa/include/spa/param/audio/flac-utils.h
@@ -0,0 +1,80 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_FLAC_UTILS_H
+#define SPA_AUDIO_FLAC_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_flac_parse(const struct spa_pod *format, struct spa_audio_info_flac *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_flac_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_flac *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_flac),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_FLAC_UTILS_H */
diff --git a/spa/include/spa/param/audio/flac.h b/spa/include/spa/param/audio/flac.h
new file mode 100644
index 0000000..df4841e
--- /dev/null
+++ b/spa/include/spa/param/audio/flac.h
@@ -0,0 +1,49 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_FLAC_H
+#define SPA_AUDIO_FLAC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+struct spa_audio_info_flac {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+};
+
+#define SPA_AUDIO_INFO_FLAC_INIT(...) ((struct spa_audio_info_flac) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_FLAC_H */
diff --git a/spa/include/spa/param/audio/format-utils.h b/spa/include/spa/param/audio/format-utils.h
new file mode 100644
index 0000000..02aafe4
--- /dev/null
+++ b/spa/include/spa/param/audio/format-utils.h
@@ -0,0 +1,141 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_AUDIO_FORMAT_UTILS_H
+#define SPA_PARAM_AUDIO_FORMAT_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+#include <spa/param/audio/raw-utils.h>
+#include <spa/param/audio/dsp-utils.h>
+#include <spa/param/audio/iec958-utils.h>
+#include <spa/param/audio/dsd-utils.h>
+#include <spa/param/audio/mp3-utils.h>
+#include <spa/param/audio/aac-utils.h>
+#include <spa/param/audio/vorbis-utils.h>
+#include <spa/param/audio/wma-utils.h>
+#include <spa/param/audio/ra-utils.h>
+#include <spa/param/audio/amr-utils.h>
+#include <spa/param/audio/alac-utils.h>
+#include <spa/param/audio/flac-utils.h>
+#include <spa/param/audio/ape-utils.h>
+
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+static inline int
+spa_format_audio_parse(const struct spa_pod *format, struct spa_audio_info *info)
+{
+ int res;
+
+ if ((res = spa_format_parse(format, &info->media_type, &info->media_subtype)) < 0)
+ return res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio)
+ return -EINVAL;
+
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ return spa_format_audio_raw_parse(format, &info->info.raw);
+ case SPA_MEDIA_SUBTYPE_dsp:
+ return spa_format_audio_dsp_parse(format, &info->info.dsp);
+ case SPA_MEDIA_SUBTYPE_iec958:
+ return spa_format_audio_iec958_parse(format, &info->info.iec958);
+ case SPA_MEDIA_SUBTYPE_dsd:
+ return spa_format_audio_dsd_parse(format, &info->info.dsd);
+ case SPA_MEDIA_SUBTYPE_mp3:
+ return spa_format_audio_mp3_parse(format, &info->info.mp3);
+ case SPA_MEDIA_SUBTYPE_aac:
+ return spa_format_audio_aac_parse(format, &info->info.aac);
+ case SPA_MEDIA_SUBTYPE_vorbis:
+ return spa_format_audio_vorbis_parse(format, &info->info.vorbis);
+ case SPA_MEDIA_SUBTYPE_wma:
+ return spa_format_audio_wma_parse(format, &info->info.wma);
+ case SPA_MEDIA_SUBTYPE_ra:
+ return spa_format_audio_ra_parse(format, &info->info.ra);
+ case SPA_MEDIA_SUBTYPE_amr:
+ return spa_format_audio_amr_parse(format, &info->info.amr);
+ case SPA_MEDIA_SUBTYPE_alac:
+ return spa_format_audio_alac_parse(format, &info->info.alac);
+ case SPA_MEDIA_SUBTYPE_flac:
+ return spa_format_audio_flac_parse(format, &info->info.flac);
+ case SPA_MEDIA_SUBTYPE_ape:
+ return spa_format_audio_ape_parse(format, &info->info.ape);
+ }
+ return -ENOTSUP;
+}
+
+static inline struct spa_pod *
+spa_format_audio_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info *info)
+{
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ return spa_format_audio_raw_build(builder, id, &info->info.raw);
+ case SPA_MEDIA_SUBTYPE_dsp:
+ return spa_format_audio_dsp_build(builder, id, &info->info.dsp);
+ case SPA_MEDIA_SUBTYPE_iec958:
+ return spa_format_audio_iec958_build(builder, id, &info->info.iec958);
+ case SPA_MEDIA_SUBTYPE_dsd:
+ return spa_format_audio_dsd_build(builder, id, &info->info.dsd);
+ case SPA_MEDIA_SUBTYPE_mp3:
+ return spa_format_audio_mp3_build(builder, id, &info->info.mp3);
+ case SPA_MEDIA_SUBTYPE_aac:
+ return spa_format_audio_aac_build(builder, id, &info->info.aac);
+ case SPA_MEDIA_SUBTYPE_vorbis:
+ return spa_format_audio_vorbis_build(builder, id, &info->info.vorbis);
+ case SPA_MEDIA_SUBTYPE_wma:
+ return spa_format_audio_wma_build(builder, id, &info->info.wma);
+ case SPA_MEDIA_SUBTYPE_ra:
+ return spa_format_audio_ra_build(builder, id, &info->info.ra);
+ case SPA_MEDIA_SUBTYPE_amr:
+ return spa_format_audio_amr_build(builder, id, &info->info.amr);
+ case SPA_MEDIA_SUBTYPE_alac:
+ return spa_format_audio_alac_build(builder, id, &info->info.alac);
+ case SPA_MEDIA_SUBTYPE_flac:
+ return spa_format_audio_flac_build(builder, id, &info->info.flac);
+ case SPA_MEDIA_SUBTYPE_ape:
+ return spa_format_audio_ape_build(builder, id, &info->info.ape);
+ }
+ errno = ENOTSUP;
+ return NULL;
+}
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_AUDIO_FORMAT_UTILS_H */
diff --git a/spa/include/spa/param/audio/format.h b/spa/include/spa/param/audio/format.h
new file mode 100644
index 0000000..0e9e6ca
--- /dev/null
+++ b/spa/include/spa/param/audio/format.h
@@ -0,0 +1,80 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_AUDIO_FORMAT_H
+#define SPA_PARAM_AUDIO_FORMAT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/format.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/audio/dsp.h>
+#include <spa/param/audio/iec958.h>
+#include <spa/param/audio/dsd.h>
+#include <spa/param/audio/mp3.h>
+#include <spa/param/audio/aac.h>
+#include <spa/param/audio/vorbis.h>
+#include <spa/param/audio/wma.h>
+#include <spa/param/audio/ra.h>
+#include <spa/param/audio/amr.h>
+#include <spa/param/audio/alac.h>
+#include <spa/param/audio/flac.h>
+#include <spa/param/audio/ape.h>
+
+struct spa_audio_info {
+ uint32_t media_type;
+ uint32_t media_subtype;
+ union {
+ struct spa_audio_info_raw raw;
+ struct spa_audio_info_dsp dsp;
+ struct spa_audio_info_iec958 iec958;
+ struct spa_audio_info_dsd dsd;
+ struct spa_audio_info_mp3 mp3;
+ struct spa_audio_info_aac aac;
+ struct spa_audio_info_vorbis vorbis;
+ struct spa_audio_info_wma wma;
+ struct spa_audio_info_ra ra;
+ struct spa_audio_info_amr amr;
+ struct spa_audio_info_alac alac;
+ struct spa_audio_info_flac flac;
+ struct spa_audio_info_ape ape;
+ } info;
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_AUDIO_FORMAT_H */
diff --git a/spa/include/spa/param/audio/iec958-types.h b/spa/include/spa/param/audio/iec958-types.h
new file mode 100644
index 0000000..e0b8940
--- /dev/null
+++ b/spa/include/spa/param/audio/iec958-types.h
@@ -0,0 +1,59 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_IEC958_TYPES_H
+#define SPA_AUDIO_IEC958_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/type.h>
+#include <spa/param/audio/iec958.h>
+
+#define SPA_TYPE_INFO_AudioIEC958Codec SPA_TYPE_INFO_ENUM_BASE "AudioIEC958Codec"
+#define SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE SPA_TYPE_INFO_AudioIEC958Codec ":"
+
+static const struct spa_type_info spa_type_audio_iec958_codec[] = {
+ { SPA_AUDIO_IEC958_CODEC_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "UNKNOWN", NULL },
+ { SPA_AUDIO_IEC958_CODEC_PCM, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "PCM", NULL },
+ { SPA_AUDIO_IEC958_CODEC_DTS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "DTS", NULL },
+ { SPA_AUDIO_IEC958_CODEC_AC3, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "AC3", NULL },
+ { SPA_AUDIO_IEC958_CODEC_MPEG, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "MPEG", NULL },
+ { SPA_AUDIO_IEC958_CODEC_MPEG2_AAC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "MPEG2-AAC", NULL },
+ { SPA_AUDIO_IEC958_CODEC_EAC3, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "EAC3", NULL },
+ { SPA_AUDIO_IEC958_CODEC_TRUEHD, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "TrueHD", NULL },
+ { SPA_AUDIO_IEC958_CODEC_DTSHD, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "DTS-HD", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_RAW_IEC958_TYPES_H */
diff --git a/spa/include/spa/param/audio/iec958-utils.h b/spa/include/spa/param/audio/iec958-utils.h
new file mode 100644
index 0000000..da953d7
--- /dev/null
+++ b/spa/include/spa/param/audio/iec958-utils.h
@@ -0,0 +1,79 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_IEC958_UTILS_H
+#define SPA_AUDIO_IEC958_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_iec958_parse(const struct spa_pod *format, struct spa_audio_info_iec958 *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_iec958Codec, SPA_POD_OPT_Id(&info->codec),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_iec958_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_iec958 *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_iec958),
+ 0);
+ if (info->codec != SPA_AUDIO_IEC958_CODEC_UNKNOWN)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_iec958Codec, SPA_POD_Id(info->codec), 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_IEC958_UTILS_H */
diff --git a/spa/include/spa/param/audio/iec958.h b/spa/include/spa/param/audio/iec958.h
new file mode 100644
index 0000000..9445138
--- /dev/null
+++ b/spa/include/spa/param/audio/iec958.h
@@ -0,0 +1,69 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_IEC958_H
+#define SPA_AUDIO_IEC958_H
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+enum spa_audio_iec958_codec {
+ SPA_AUDIO_IEC958_CODEC_UNKNOWN,
+
+ SPA_AUDIO_IEC958_CODEC_PCM,
+ SPA_AUDIO_IEC958_CODEC_DTS,
+ SPA_AUDIO_IEC958_CODEC_AC3,
+ SPA_AUDIO_IEC958_CODEC_MPEG, /**< MPEG-1 or MPEG-2 (Part 3, not AAC) */
+ SPA_AUDIO_IEC958_CODEC_MPEG2_AAC, /**< MPEG-2 AAC */
+
+ SPA_AUDIO_IEC958_CODEC_EAC3,
+
+ SPA_AUDIO_IEC958_CODEC_TRUEHD, /**< Dolby TrueHD */
+ SPA_AUDIO_IEC958_CODEC_DTSHD, /**< DTS-HD Master Audio */
+};
+
+struct spa_audio_info_iec958 {
+ enum spa_audio_iec958_codec codec; /*< format, one of the DSP formats in enum spa_audio_format_dsp */
+ uint32_t flags; /*< extra flags */
+ uint32_t rate; /*< sample rate */
+};
+
+#define SPA_AUDIO_INFO_IEC958_INIT(...) ((struct spa_audio_info_iec958) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_IEC958_H */
diff --git a/spa/include/spa/param/audio/layout.h b/spa/include/spa/param/audio/layout.h
new file mode 100644
index 0000000..66154bf
--- /dev/null
+++ b/spa/include/spa/param/audio/layout.h
@@ -0,0 +1,192 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_LAYOUT_H
+#define SPA_AUDIO_LAYOUT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
+#include <endian.h>
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+#include <spa/param/audio/raw.h>
+
+struct spa_audio_layout_info {
+ uint32_t n_channels;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+};
+
+#define SPA_AUDIO_LAYOUT_Mono 1, { SPA_AUDIO_CHANNEL_MONO, }
+#define SPA_AUDIO_LAYOUT_Stereo 2, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, }
+#define SPA_AUDIO_LAYOUT_Quad 4, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, }
+#define SPA_AUDIO_LAYOUT_Pentagonal 5, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \
+ SPA_AUDIO_CHANNEL_FC, }
+#define SPA_AUDIO_LAYOUT_Hexagonal 6, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RC, }
+#define SPA_AUDIO_LAYOUT_Octagonal 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RC, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_Cube 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR }, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \
+ SPA_AUDIO_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFR, \
+ SPA_AUDIO_CHANNEL_TRL, SPA_AUDIO_CHANNEL_TRR, }
+
+
+#define SPA_AUDIO_LAYOUT_MPEG_1_0 SPA_AUDIO_LAYOUT_Mono
+#define SPA_AUDIO_LAYOUT_MPEG_2_0 SPA_AUDIO_LAYOUT_Stereo
+#define SPA_AUDIO_LAYOUT_MPEG_3_0A 3, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, }
+#define SPA_AUDIO_LAYOUT_MPEG_3_0B 3, { SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_FL, \
+ SPA_AUDIO_CHANNEL_FR, }
+#define SPA_AUDIO_LAYOUT_MPEG_4_0A 4, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RC, }
+#define SPA_AUDIO_LAYOUT_MPEG_4_0B 4, { SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_FL, \
+ SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_RC, }
+#define SPA_AUDIO_LAYOUT_MPEG_5_0A 5, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_MPEG_5_0B 5, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, \
+ SPA_AUDIO_CHANNEL_FC, }
+#define SPA_AUDIO_LAYOUT_MPEG_5_0C 5, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FC, \
+ SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_MPEG_5_0D 5, { SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_FL, \
+ SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_MPEG_5_1A 6, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_MPEG_5_1B 6, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, }
+#define SPA_AUDIO_LAYOUT_MPEG_5_1C 6, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FC, \
+ SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, SPA_AUDIO_CHANNEL_LFE, }
+#define SPA_AUDIO_LAYOUT_MPEG_5_1D 6, { SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_FL, \
+ SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, SPA_AUDIO_CHANNEL_LFE, }
+#define SPA_AUDIO_LAYOUT_MPEG_6_1A 7, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, \
+ SPA_AUDIO_CHANNEL_RC, }
+#define SPA_AUDIO_LAYOUT_MPEG_7_1A 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_MPEG_7_1B 8, { SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, SPA_AUDIO_CHANNEL_FL, \
+ SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_RL, \
+ SPA_AUDIO_CHANNEL_RR, SPA_AUDIO_CHANNEL_LFE, }
+#define SPA_AUDIO_LAYOUT_MPEG_7_1C 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, }
+
+
+#define SPA_AUDIO_LAYOUT_2_1 3, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_LFE, }
+
+#define SPA_AUDIO_LAYOUT_2RC 3, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_RC, }
+#define SPA_AUDIO_LAYOUT_2FC 3, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, }
+
+#define SPA_AUDIO_LAYOUT_3_1 4, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, }
+#define SPA_AUDIO_LAYOUT_4_0 4, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RC, }
+#define SPA_AUDIO_LAYOUT_2_2 4, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+
+#define SPA_AUDIO_LAYOUT_4_1 5, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_RC, }
+#define SPA_AUDIO_LAYOUT_5_0 5, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_5_0R 5, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RL, \
+ SPA_AUDIO_CHANNEL_RR, }
+#define SPA_AUDIO_LAYOUT_5_1 6, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_5_1R 6, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, }
+#define SPA_AUDIO_LAYOUT_6_0 6, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RC, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_6_0F 6, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FLC, SPA_AUDIO_CHANNEL_FRC, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_6_1 7, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_6_1F 7, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \
+ SPA_AUDIO_CHANNEL_RC, }
+#define SPA_AUDIO_LAYOUT_7_0 7, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RL, \
+ SPA_AUDIO_CHANNEL_RR, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_7_0F 7, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_FLC, \
+ SPA_AUDIO_CHANNEL_FRC, SPA_AUDIO_CHANNEL_SL, \
+ SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_7_1 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_7_1W 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_FLC, SPA_AUDIO_CHANNEL_FRC, \
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, }
+#define SPA_AUDIO_LAYOUT_7_1WR 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, \
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \
+ SPA_AUDIO_CHANNEL_FLC, SPA_AUDIO_CHANNEL_FRC, }
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_LAYOUT_H */
diff --git a/spa/include/spa/param/audio/mp3-types.h b/spa/include/spa/param/audio/mp3-types.h
new file mode 100644
index 0000000..b697275
--- /dev/null
+++ b/spa/include/spa/param/audio/mp3-types.h
@@ -0,0 +1,54 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_MP3_TYPES_H
+#define SPA_AUDIO_MP3_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/type.h>
+#include <spa/param/audio/mp3.h>
+
+#define SPA_TYPE_INFO_AudioMP3ChannelMode SPA_TYPE_INFO_ENUM_BASE "AudioMP3ChannelMode"
+#define SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE SPA_TYPE_INFO_AudioMP3ChannelMode ":"
+
+static const struct spa_type_info spa_type_audio_mp3_channel_mode[] = {
+ { SPA_AUDIO_MP3_CHANNEL_MODE_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "UNKNOWN", NULL },
+ { SPA_AUDIO_MP3_CHANNEL_MODE_MONO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "Mono", NULL },
+ { SPA_AUDIO_MP3_CHANNEL_MODE_STEREO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "Stereo", NULL },
+ { SPA_AUDIO_MP3_CHANNEL_MODE_JOINTSTEREO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "Joint-stereo", NULL },
+ { SPA_AUDIO_MP3_CHANNEL_MODE_DUAL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "Dual", NULL },
+ { 0, 0, NULL, NULL },
+};
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_MP3_TYPES_H */
diff --git a/spa/include/spa/param/audio/mp3-utils.h b/spa/include/spa/param/audio/mp3-utils.h
new file mode 100644
index 0000000..b764578
--- /dev/null
+++ b/spa/include/spa/param/audio/mp3-utils.h
@@ -0,0 +1,80 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_MP3_UTILS_H
+#define SPA_AUDIO_MP3_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_mp3_parse(const struct spa_pod *format, struct spa_audio_info_mp3 *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_mp3_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_mp3 *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_mp3),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_MP3_UTILS_H */
diff --git a/spa/include/spa/param/audio/mp3.h b/spa/include/spa/param/audio/mp3.h
new file mode 100644
index 0000000..ff724c7
--- /dev/null
+++ b/spa/include/spa/param/audio/mp3.h
@@ -0,0 +1,57 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_MP3_H
+#define SPA_AUDIO_MP3_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+enum spa_audio_mp3_channel_mode {
+ SPA_AUDIO_MP3_CHANNEL_MODE_UNKNOWN,
+ SPA_AUDIO_MP3_CHANNEL_MODE_MONO,
+ SPA_AUDIO_MP3_CHANNEL_MODE_STEREO,
+ SPA_AUDIO_MP3_CHANNEL_MODE_JOINTSTEREO,
+ SPA_AUDIO_MP3_CHANNEL_MODE_DUAL,
+};
+
+struct spa_audio_info_mp3 {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+};
+
+#define SPA_AUDIO_INFO_MP3_INIT(...) ((struct spa_audio_info_mp3) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_MP3_H */
diff --git a/spa/include/spa/param/audio/ra-utils.h b/spa/include/spa/param/audio/ra-utils.h
new file mode 100644
index 0000000..88356ba
--- /dev/null
+++ b/spa/include/spa/param/audio/ra-utils.h
@@ -0,0 +1,80 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_RA_UTILS_H
+#define SPA_AUDIO_RA_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_ra_parse(const struct spa_pod *format, struct spa_audio_info_ra *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_ra_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_ra *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_ra),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_RA_UTILS_H */
diff --git a/spa/include/spa/param/audio/ra.h b/spa/include/spa/param/audio/ra.h
new file mode 100644
index 0000000..041c3a3
--- /dev/null
+++ b/spa/include/spa/param/audio/ra.h
@@ -0,0 +1,49 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_RA_H
+#define SPA_AUDIO_RA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+struct spa_audio_info_ra {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+};
+
+#define SPA_AUDIO_INFO_RA_INIT(...) ((struct spa_audio_info_ra) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_RA_H */
diff --git a/spa/include/spa/param/audio/raw-types.h b/spa/include/spa/param/audio/raw-types.h
new file mode 100644
index 0000000..94e6364
--- /dev/null
+++ b/spa/include/spa/param/audio/raw-types.h
@@ -0,0 +1,278 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_RAW_TYPES_H
+#define SPA_AUDIO_RAW_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/utils/type.h>
+#include <spa/param/audio/raw.h>
+
+#define SPA_TYPE_INFO_AudioFormat SPA_TYPE_INFO_ENUM_BASE "AudioFormat"
+#define SPA_TYPE_INFO_AUDIO_FORMAT_BASE SPA_TYPE_INFO_AudioFormat ":"
+
+static const struct spa_type_info spa_type_audio_format[] = {
+ { SPA_AUDIO_FORMAT_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "UNKNOWN", NULL },
+ { SPA_AUDIO_FORMAT_ENCODED, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ENCODED", NULL },
+ { SPA_AUDIO_FORMAT_S8, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S8", NULL },
+ { SPA_AUDIO_FORMAT_U8, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U8", NULL },
+ { SPA_AUDIO_FORMAT_S16_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16LE", NULL },
+ { SPA_AUDIO_FORMAT_S16_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16BE", NULL },
+ { SPA_AUDIO_FORMAT_U16_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16LE", NULL },
+ { SPA_AUDIO_FORMAT_U16_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16BE", NULL },
+ { SPA_AUDIO_FORMAT_S24_32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32LE", NULL },
+ { SPA_AUDIO_FORMAT_S24_32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32BE", NULL },
+ { SPA_AUDIO_FORMAT_U24_32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32LE", NULL },
+ { SPA_AUDIO_FORMAT_U24_32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32BE", NULL },
+ { SPA_AUDIO_FORMAT_S32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32LE", NULL },
+ { SPA_AUDIO_FORMAT_S32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32BE", NULL },
+ { SPA_AUDIO_FORMAT_U32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32LE", NULL },
+ { SPA_AUDIO_FORMAT_U32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32BE", NULL },
+ { SPA_AUDIO_FORMAT_S24_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24LE", NULL },
+ { SPA_AUDIO_FORMAT_S24_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24BE", NULL },
+ { SPA_AUDIO_FORMAT_U24_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24LE", NULL },
+ { SPA_AUDIO_FORMAT_U24_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24BE", NULL },
+ { SPA_AUDIO_FORMAT_S20_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20LE", NULL },
+ { SPA_AUDIO_FORMAT_S20_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20BE", NULL },
+ { SPA_AUDIO_FORMAT_U20_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20LE", NULL },
+ { SPA_AUDIO_FORMAT_U20_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20BE", NULL },
+ { SPA_AUDIO_FORMAT_S18_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18LE", NULL },
+ { SPA_AUDIO_FORMAT_S18_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18BE", NULL },
+ { SPA_AUDIO_FORMAT_U18_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18LE", NULL },
+ { SPA_AUDIO_FORMAT_U18_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18BE", NULL },
+ { SPA_AUDIO_FORMAT_F32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32LE", NULL },
+ { SPA_AUDIO_FORMAT_F32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32BE", NULL },
+ { SPA_AUDIO_FORMAT_F64_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64LE", NULL },
+ { SPA_AUDIO_FORMAT_F64_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64BE", NULL },
+
+ { SPA_AUDIO_FORMAT_ULAW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ULAW", NULL },
+ { SPA_AUDIO_FORMAT_ALAW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ALAW", NULL },
+
+ { SPA_AUDIO_FORMAT_U8P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U8P", NULL },
+ { SPA_AUDIO_FORMAT_S16P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16P", NULL },
+ { SPA_AUDIO_FORMAT_S24_32P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32P", NULL },
+ { SPA_AUDIO_FORMAT_S32P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32P", NULL },
+ { SPA_AUDIO_FORMAT_S24P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24P", NULL },
+ { SPA_AUDIO_FORMAT_F32P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32P", NULL },
+ { SPA_AUDIO_FORMAT_F64P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64P", NULL },
+ { SPA_AUDIO_FORMAT_S8P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S8P", NULL },
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+ { SPA_AUDIO_FORMAT_S16_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16OE", NULL },
+ { SPA_AUDIO_FORMAT_S16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16", NULL },
+ { SPA_AUDIO_FORMAT_U16_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16OE", NULL },
+ { SPA_AUDIO_FORMAT_U16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16", NULL },
+ { SPA_AUDIO_FORMAT_S24_32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32OE", NULL },
+ { SPA_AUDIO_FORMAT_S24_32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32", NULL },
+ { SPA_AUDIO_FORMAT_U24_32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32OE", NULL },
+ { SPA_AUDIO_FORMAT_U24_32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32", NULL },
+ { SPA_AUDIO_FORMAT_S32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32OE", NULL },
+ { SPA_AUDIO_FORMAT_S32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32", NULL },
+ { SPA_AUDIO_FORMAT_U32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32OE", NULL },
+ { SPA_AUDIO_FORMAT_U32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32", NULL },
+ { SPA_AUDIO_FORMAT_S24_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24OE", NULL },
+ { SPA_AUDIO_FORMAT_S24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24", NULL },
+ { SPA_AUDIO_FORMAT_U24_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24OE", NULL },
+ { SPA_AUDIO_FORMAT_U24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24", NULL },
+ { SPA_AUDIO_FORMAT_S20_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20OE", NULL },
+ { SPA_AUDIO_FORMAT_S20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20", NULL },
+ { SPA_AUDIO_FORMAT_U20_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20OE", NULL },
+ { SPA_AUDIO_FORMAT_U20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20", NULL },
+ { SPA_AUDIO_FORMAT_S18_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18OE", NULL },
+ { SPA_AUDIO_FORMAT_S18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18", NULL },
+ { SPA_AUDIO_FORMAT_U18_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18OE", NULL },
+ { SPA_AUDIO_FORMAT_U18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18", NULL },
+ { SPA_AUDIO_FORMAT_F32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32OE", NULL },
+ { SPA_AUDIO_FORMAT_F32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32", NULL },
+ { SPA_AUDIO_FORMAT_F64_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64OE", NULL },
+ { SPA_AUDIO_FORMAT_F64, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64", NULL },
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ { SPA_AUDIO_FORMAT_S16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16", NULL },
+ { SPA_AUDIO_FORMAT_S16_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16OE", NULL },
+ { SPA_AUDIO_FORMAT_U16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16", NULL },
+ { SPA_AUDIO_FORMAT_U16_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16OE", NULL },
+ { SPA_AUDIO_FORMAT_S24_32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32", NULL },
+ { SPA_AUDIO_FORMAT_S24_32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32OE", NULL },
+ { SPA_AUDIO_FORMAT_U24_32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32", NULL },
+ { SPA_AUDIO_FORMAT_U24_32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32OE", NULL },
+ { SPA_AUDIO_FORMAT_S32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32", NULL },
+ { SPA_AUDIO_FORMAT_S32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32OE", NULL },
+ { SPA_AUDIO_FORMAT_U32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32", NULL },
+ { SPA_AUDIO_FORMAT_U32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32OE", NULL },
+ { SPA_AUDIO_FORMAT_S24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24", NULL },
+ { SPA_AUDIO_FORMAT_S24_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24OE", NULL },
+ { SPA_AUDIO_FORMAT_U24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24", NULL },
+ { SPA_AUDIO_FORMAT_U24_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24OE", NULL },
+ { SPA_AUDIO_FORMAT_S20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20", NULL },
+ { SPA_AUDIO_FORMAT_S20_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20OE", NULL },
+ { SPA_AUDIO_FORMAT_U20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20", NULL },
+ { SPA_AUDIO_FORMAT_U20_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20OE", NULL },
+ { SPA_AUDIO_FORMAT_S18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18", NULL },
+ { SPA_AUDIO_FORMAT_S18_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18OE", NULL },
+ { SPA_AUDIO_FORMAT_U18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18", NULL },
+ { SPA_AUDIO_FORMAT_U18_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18OE", NULL },
+ { SPA_AUDIO_FORMAT_F32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32", NULL },
+ { SPA_AUDIO_FORMAT_F32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32OE", NULL },
+ { SPA_AUDIO_FORMAT_F64, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64", NULL },
+ { SPA_AUDIO_FORMAT_F64_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64OE", NULL },
+#endif
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_AudioFlags SPA_TYPE_INFO_FLAGS_BASE "AudioFlags"
+#define SPA_TYPE_INFO_AUDIO_FLAGS_BASE SPA_TYPE_INFO_AudioFlags ":"
+
+static const struct spa_type_info spa_type_audio_flags[] = {
+ { SPA_AUDIO_FLAG_NONE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FLAGS_BASE "none", NULL },
+ { SPA_AUDIO_FLAG_UNPOSITIONED, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FLAGS_BASE "unpositioned", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_AudioChannel SPA_TYPE_INFO_ENUM_BASE "AudioChannel"
+#define SPA_TYPE_INFO_AUDIO_CHANNEL_BASE SPA_TYPE_INFO_AudioChannel ":"
+
+static const struct spa_type_info spa_type_audio_channel[] = {
+ { SPA_AUDIO_CHANNEL_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "UNK", NULL },
+ { SPA_AUDIO_CHANNEL_NA, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "NA", NULL },
+ { SPA_AUDIO_CHANNEL_MONO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "MONO", NULL },
+ { SPA_AUDIO_CHANNEL_FL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FL", NULL },
+ { SPA_AUDIO_CHANNEL_FR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FR", NULL },
+ { SPA_AUDIO_CHANNEL_FC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FC", NULL },
+ { SPA_AUDIO_CHANNEL_LFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "LFE", NULL },
+ { SPA_AUDIO_CHANNEL_SL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "SL", NULL },
+ { SPA_AUDIO_CHANNEL_SR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "SR", NULL },
+ { SPA_AUDIO_CHANNEL_FLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FLC", NULL },
+ { SPA_AUDIO_CHANNEL_FRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FRC", NULL },
+ { SPA_AUDIO_CHANNEL_RC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RC", NULL },
+ { SPA_AUDIO_CHANNEL_RL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RL", NULL },
+ { SPA_AUDIO_CHANNEL_RR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RR", NULL },
+ { SPA_AUDIO_CHANNEL_TC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TC", NULL },
+ { SPA_AUDIO_CHANNEL_TFL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFL", NULL },
+ { SPA_AUDIO_CHANNEL_TFC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFC", NULL },
+ { SPA_AUDIO_CHANNEL_TFR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFR", NULL },
+ { SPA_AUDIO_CHANNEL_TRL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TRL", NULL },
+ { SPA_AUDIO_CHANNEL_TRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TRC", NULL },
+ { SPA_AUDIO_CHANNEL_TRR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TRR", NULL },
+ { SPA_AUDIO_CHANNEL_RLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RLC", NULL },
+ { SPA_AUDIO_CHANNEL_RRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RRC", NULL },
+ { SPA_AUDIO_CHANNEL_FLW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FLW", NULL },
+ { SPA_AUDIO_CHANNEL_FRW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FRW", NULL },
+ { SPA_AUDIO_CHANNEL_LFE2, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "LFE2", NULL },
+ { SPA_AUDIO_CHANNEL_FLH, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FLH", NULL },
+ { SPA_AUDIO_CHANNEL_FCH, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FCH", NULL },
+ { SPA_AUDIO_CHANNEL_FRH, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FRH", NULL },
+ { SPA_AUDIO_CHANNEL_TFLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFLC", NULL },
+ { SPA_AUDIO_CHANNEL_TFRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFRC", NULL },
+ { SPA_AUDIO_CHANNEL_TSL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TSL", NULL },
+ { SPA_AUDIO_CHANNEL_TSR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TSR", NULL },
+ { SPA_AUDIO_CHANNEL_LLFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "LLFR", NULL },
+ { SPA_AUDIO_CHANNEL_RLFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RLFE", NULL },
+ { SPA_AUDIO_CHANNEL_BC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "BC", NULL },
+ { SPA_AUDIO_CHANNEL_BLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "BLC", NULL },
+ { SPA_AUDIO_CHANNEL_BRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "BRC", NULL },
+
+ { SPA_AUDIO_CHANNEL_AUX0, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX0", NULL },
+ { SPA_AUDIO_CHANNEL_AUX1, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX1", NULL },
+ { SPA_AUDIO_CHANNEL_AUX2, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX2", NULL },
+ { SPA_AUDIO_CHANNEL_AUX3, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX3", NULL },
+ { SPA_AUDIO_CHANNEL_AUX4, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX4", NULL },
+ { SPA_AUDIO_CHANNEL_AUX5, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX5", NULL },
+ { SPA_AUDIO_CHANNEL_AUX6, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX6", NULL },
+ { SPA_AUDIO_CHANNEL_AUX7, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX7", NULL },
+ { SPA_AUDIO_CHANNEL_AUX8, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX8", NULL },
+ { SPA_AUDIO_CHANNEL_AUX9, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX9", NULL },
+ { SPA_AUDIO_CHANNEL_AUX10, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX10", NULL },
+ { SPA_AUDIO_CHANNEL_AUX11, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX11", NULL },
+ { SPA_AUDIO_CHANNEL_AUX12, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX12", NULL },
+ { SPA_AUDIO_CHANNEL_AUX13, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX13", NULL },
+ { SPA_AUDIO_CHANNEL_AUX14, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX14", NULL },
+ { SPA_AUDIO_CHANNEL_AUX15, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX15", NULL },
+ { SPA_AUDIO_CHANNEL_AUX16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX16", NULL },
+ { SPA_AUDIO_CHANNEL_AUX17, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX17", NULL },
+ { SPA_AUDIO_CHANNEL_AUX18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX18", NULL },
+ { SPA_AUDIO_CHANNEL_AUX19, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX19", NULL },
+ { SPA_AUDIO_CHANNEL_AUX20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX20", NULL },
+ { SPA_AUDIO_CHANNEL_AUX21, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX21", NULL },
+ { SPA_AUDIO_CHANNEL_AUX22, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX22", NULL },
+ { SPA_AUDIO_CHANNEL_AUX23, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX23", NULL },
+ { SPA_AUDIO_CHANNEL_AUX24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX24", NULL },
+ { SPA_AUDIO_CHANNEL_AUX25, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX25", NULL },
+ { SPA_AUDIO_CHANNEL_AUX26, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX26", NULL },
+ { SPA_AUDIO_CHANNEL_AUX27, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX27", NULL },
+ { SPA_AUDIO_CHANNEL_AUX28, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX28", NULL },
+ { SPA_AUDIO_CHANNEL_AUX29, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX29", NULL },
+ { SPA_AUDIO_CHANNEL_AUX30, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX30", NULL },
+ { SPA_AUDIO_CHANNEL_AUX31, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX31", NULL },
+ { SPA_AUDIO_CHANNEL_AUX32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX32", NULL },
+ { SPA_AUDIO_CHANNEL_AUX33, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX33", NULL },
+ { SPA_AUDIO_CHANNEL_AUX34, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX34", NULL },
+ { SPA_AUDIO_CHANNEL_AUX35, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX35", NULL },
+ { SPA_AUDIO_CHANNEL_AUX36, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX36", NULL },
+ { SPA_AUDIO_CHANNEL_AUX37, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX37", NULL },
+ { SPA_AUDIO_CHANNEL_AUX38, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX38", NULL },
+ { SPA_AUDIO_CHANNEL_AUX39, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX39", NULL },
+ { SPA_AUDIO_CHANNEL_AUX40, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX40", NULL },
+ { SPA_AUDIO_CHANNEL_AUX41, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX41", NULL },
+ { SPA_AUDIO_CHANNEL_AUX42, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX42", NULL },
+ { SPA_AUDIO_CHANNEL_AUX43, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX43", NULL },
+ { SPA_AUDIO_CHANNEL_AUX44, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX44", NULL },
+ { SPA_AUDIO_CHANNEL_AUX45, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX45", NULL },
+ { SPA_AUDIO_CHANNEL_AUX46, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX46", NULL },
+ { SPA_AUDIO_CHANNEL_AUX47, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX47", NULL },
+ { SPA_AUDIO_CHANNEL_AUX48, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX48", NULL },
+ { SPA_AUDIO_CHANNEL_AUX49, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX49", NULL },
+ { SPA_AUDIO_CHANNEL_AUX50, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX50", NULL },
+ { SPA_AUDIO_CHANNEL_AUX51, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX51", NULL },
+ { SPA_AUDIO_CHANNEL_AUX52, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX52", NULL },
+ { SPA_AUDIO_CHANNEL_AUX53, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX53", NULL },
+ { SPA_AUDIO_CHANNEL_AUX54, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX54", NULL },
+ { SPA_AUDIO_CHANNEL_AUX55, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX55", NULL },
+ { SPA_AUDIO_CHANNEL_AUX56, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX56", NULL },
+ { SPA_AUDIO_CHANNEL_AUX57, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX57", NULL },
+ { SPA_AUDIO_CHANNEL_AUX58, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX58", NULL },
+ { SPA_AUDIO_CHANNEL_AUX59, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX59", NULL },
+ { SPA_AUDIO_CHANNEL_AUX60, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX60", NULL },
+ { SPA_AUDIO_CHANNEL_AUX61, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX61", NULL },
+ { SPA_AUDIO_CHANNEL_AUX62, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX62", NULL },
+ { SPA_AUDIO_CHANNEL_AUX63, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX63", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_RAW_RAW_TYPES_H */
diff --git a/spa/include/spa/param/audio/raw-utils.h b/spa/include/spa/param/audio/raw-utils.h
new file mode 100644
index 0000000..53378e2
--- /dev/null
+++ b/spa/include/spa/param/audio/raw-utils.h
@@ -0,0 +1,96 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_RAW_UTILS_H
+#define SPA_AUDIO_RAW_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_raw *info)
+{
+ struct spa_pod *position = NULL;
+ int res;
+ info->flags = 0;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_format, SPA_POD_OPT_Id(&info->format),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels),
+ SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position));
+ if (position == NULL ||
+ !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS))
+ SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED);
+
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_raw *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ 0);
+ if (info->format != SPA_AUDIO_FORMAT_UNKNOWN)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(info->format), 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0) {
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ if (!SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ spa_pod_builder_add(builder, SPA_FORMAT_AUDIO_position,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id,
+ info->channels, info->position), 0);
+ }
+ }
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_RAW_UTILS_H */
diff --git a/spa/include/spa/param/audio/raw.h b/spa/include/spa/param/audio/raw.h
new file mode 100644
index 0000000..ce9d511
--- /dev/null
+++ b/spa/include/spa/param/audio/raw.h
@@ -0,0 +1,317 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_RAW_H
+#define SPA_AUDIO_RAW_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
+#include <endian.h>
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#define SPA_AUDIO_MAX_CHANNELS 64u
+
+enum spa_audio_format {
+ SPA_AUDIO_FORMAT_UNKNOWN,
+ SPA_AUDIO_FORMAT_ENCODED,
+
+ /* interleaved formats */
+ SPA_AUDIO_FORMAT_START_Interleaved = 0x100,
+ SPA_AUDIO_FORMAT_S8,
+ SPA_AUDIO_FORMAT_U8,
+ SPA_AUDIO_FORMAT_S16_LE,
+ SPA_AUDIO_FORMAT_S16_BE,
+ SPA_AUDIO_FORMAT_U16_LE,
+ SPA_AUDIO_FORMAT_U16_BE,
+ SPA_AUDIO_FORMAT_S24_32_LE,
+ SPA_AUDIO_FORMAT_S24_32_BE,
+ SPA_AUDIO_FORMAT_U24_32_LE,
+ SPA_AUDIO_FORMAT_U24_32_BE,
+ SPA_AUDIO_FORMAT_S32_LE,
+ SPA_AUDIO_FORMAT_S32_BE,
+ SPA_AUDIO_FORMAT_U32_LE,
+ SPA_AUDIO_FORMAT_U32_BE,
+ SPA_AUDIO_FORMAT_S24_LE,
+ SPA_AUDIO_FORMAT_S24_BE,
+ SPA_AUDIO_FORMAT_U24_LE,
+ SPA_AUDIO_FORMAT_U24_BE,
+ SPA_AUDIO_FORMAT_S20_LE,
+ SPA_AUDIO_FORMAT_S20_BE,
+ SPA_AUDIO_FORMAT_U20_LE,
+ SPA_AUDIO_FORMAT_U20_BE,
+ SPA_AUDIO_FORMAT_S18_LE,
+ SPA_AUDIO_FORMAT_S18_BE,
+ SPA_AUDIO_FORMAT_U18_LE,
+ SPA_AUDIO_FORMAT_U18_BE,
+ SPA_AUDIO_FORMAT_F32_LE,
+ SPA_AUDIO_FORMAT_F32_BE,
+ SPA_AUDIO_FORMAT_F64_LE,
+ SPA_AUDIO_FORMAT_F64_BE,
+
+ SPA_AUDIO_FORMAT_ULAW,
+ SPA_AUDIO_FORMAT_ALAW,
+
+ /* planar formats */
+ SPA_AUDIO_FORMAT_START_Planar = 0x200,
+ SPA_AUDIO_FORMAT_U8P,
+ SPA_AUDIO_FORMAT_S16P,
+ SPA_AUDIO_FORMAT_S24_32P,
+ SPA_AUDIO_FORMAT_S32P,
+ SPA_AUDIO_FORMAT_S24P,
+ SPA_AUDIO_FORMAT_F32P,
+ SPA_AUDIO_FORMAT_F64P,
+ SPA_AUDIO_FORMAT_S8P,
+
+ /* other formats start here */
+ SPA_AUDIO_FORMAT_START_Other = 0x400,
+
+ /* Aliases */
+
+ /* DSP formats */
+ SPA_AUDIO_FORMAT_DSP_S32 = SPA_AUDIO_FORMAT_S24_32P,
+ SPA_AUDIO_FORMAT_DSP_F32 = SPA_AUDIO_FORMAT_F32P,
+ SPA_AUDIO_FORMAT_DSP_F64 = SPA_AUDIO_FORMAT_F64P,
+
+ /* native endian */
+#if __BYTE_ORDER == __BIG_ENDIAN
+ SPA_AUDIO_FORMAT_S16 = SPA_AUDIO_FORMAT_S16_BE,
+ SPA_AUDIO_FORMAT_U16 = SPA_AUDIO_FORMAT_U16_BE,
+ SPA_AUDIO_FORMAT_S24_32 = SPA_AUDIO_FORMAT_S24_32_BE,
+ SPA_AUDIO_FORMAT_U24_32 = SPA_AUDIO_FORMAT_U24_32_BE,
+ SPA_AUDIO_FORMAT_S32 = SPA_AUDIO_FORMAT_S32_BE,
+ SPA_AUDIO_FORMAT_U32 = SPA_AUDIO_FORMAT_U32_BE,
+ SPA_AUDIO_FORMAT_S24 = SPA_AUDIO_FORMAT_S24_BE,
+ SPA_AUDIO_FORMAT_U24 = SPA_AUDIO_FORMAT_U24_BE,
+ SPA_AUDIO_FORMAT_S20 = SPA_AUDIO_FORMAT_S20_BE,
+ SPA_AUDIO_FORMAT_U20 = SPA_AUDIO_FORMAT_U20_BE,
+ SPA_AUDIO_FORMAT_S18 = SPA_AUDIO_FORMAT_S18_BE,
+ SPA_AUDIO_FORMAT_U18 = SPA_AUDIO_FORMAT_U18_BE,
+ SPA_AUDIO_FORMAT_F32 = SPA_AUDIO_FORMAT_F32_BE,
+ SPA_AUDIO_FORMAT_F64 = SPA_AUDIO_FORMAT_F64_BE,
+ SPA_AUDIO_FORMAT_S16_OE = SPA_AUDIO_FORMAT_S16_LE,
+ SPA_AUDIO_FORMAT_U16_OE = SPA_AUDIO_FORMAT_U16_LE,
+ SPA_AUDIO_FORMAT_S24_32_OE = SPA_AUDIO_FORMAT_S24_32_LE,
+ SPA_AUDIO_FORMAT_U24_32_OE = SPA_AUDIO_FORMAT_U24_32_LE,
+ SPA_AUDIO_FORMAT_S32_OE = SPA_AUDIO_FORMAT_S32_LE,
+ SPA_AUDIO_FORMAT_U32_OE = SPA_AUDIO_FORMAT_U32_LE,
+ SPA_AUDIO_FORMAT_S24_OE = SPA_AUDIO_FORMAT_S24_LE,
+ SPA_AUDIO_FORMAT_U24_OE = SPA_AUDIO_FORMAT_U24_LE,
+ SPA_AUDIO_FORMAT_S20_OE = SPA_AUDIO_FORMAT_S20_LE,
+ SPA_AUDIO_FORMAT_U20_OE = SPA_AUDIO_FORMAT_U20_LE,
+ SPA_AUDIO_FORMAT_S18_OE = SPA_AUDIO_FORMAT_S18_LE,
+ SPA_AUDIO_FORMAT_U18_OE = SPA_AUDIO_FORMAT_U18_LE,
+ SPA_AUDIO_FORMAT_F32_OE = SPA_AUDIO_FORMAT_F32_LE,
+ SPA_AUDIO_FORMAT_F64_OE = SPA_AUDIO_FORMAT_F64_LE,
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ SPA_AUDIO_FORMAT_S16 = SPA_AUDIO_FORMAT_S16_LE,
+ SPA_AUDIO_FORMAT_U16 = SPA_AUDIO_FORMAT_U16_LE,
+ SPA_AUDIO_FORMAT_S24_32 = SPA_AUDIO_FORMAT_S24_32_LE,
+ SPA_AUDIO_FORMAT_U24_32 = SPA_AUDIO_FORMAT_U24_32_LE,
+ SPA_AUDIO_FORMAT_S32 = SPA_AUDIO_FORMAT_S32_LE,
+ SPA_AUDIO_FORMAT_U32 = SPA_AUDIO_FORMAT_U32_LE,
+ SPA_AUDIO_FORMAT_S24 = SPA_AUDIO_FORMAT_S24_LE,
+ SPA_AUDIO_FORMAT_U24 = SPA_AUDIO_FORMAT_U24_LE,
+ SPA_AUDIO_FORMAT_S20 = SPA_AUDIO_FORMAT_S20_LE,
+ SPA_AUDIO_FORMAT_U20 = SPA_AUDIO_FORMAT_U20_LE,
+ SPA_AUDIO_FORMAT_S18 = SPA_AUDIO_FORMAT_S18_LE,
+ SPA_AUDIO_FORMAT_U18 = SPA_AUDIO_FORMAT_U18_LE,
+ SPA_AUDIO_FORMAT_F32 = SPA_AUDIO_FORMAT_F32_LE,
+ SPA_AUDIO_FORMAT_F64 = SPA_AUDIO_FORMAT_F64_LE,
+ SPA_AUDIO_FORMAT_S16_OE = SPA_AUDIO_FORMAT_S16_BE,
+ SPA_AUDIO_FORMAT_U16_OE = SPA_AUDIO_FORMAT_U16_BE,
+ SPA_AUDIO_FORMAT_S24_32_OE = SPA_AUDIO_FORMAT_S24_32_BE,
+ SPA_AUDIO_FORMAT_U24_32_OE = SPA_AUDIO_FORMAT_U24_32_BE,
+ SPA_AUDIO_FORMAT_S32_OE = SPA_AUDIO_FORMAT_S32_BE,
+ SPA_AUDIO_FORMAT_U32_OE = SPA_AUDIO_FORMAT_U32_BE,
+ SPA_AUDIO_FORMAT_S24_OE = SPA_AUDIO_FORMAT_S24_BE,
+ SPA_AUDIO_FORMAT_U24_OE = SPA_AUDIO_FORMAT_U24_BE,
+ SPA_AUDIO_FORMAT_S20_OE = SPA_AUDIO_FORMAT_S20_BE,
+ SPA_AUDIO_FORMAT_U20_OE = SPA_AUDIO_FORMAT_U20_BE,
+ SPA_AUDIO_FORMAT_S18_OE = SPA_AUDIO_FORMAT_S18_BE,
+ SPA_AUDIO_FORMAT_U18_OE = SPA_AUDIO_FORMAT_U18_BE,
+ SPA_AUDIO_FORMAT_F32_OE = SPA_AUDIO_FORMAT_F32_BE,
+ SPA_AUDIO_FORMAT_F64_OE = SPA_AUDIO_FORMAT_F64_BE,
+#endif
+};
+
+#define SPA_AUDIO_FORMAT_IS_INTERLEAVED(fmt) ((fmt) > SPA_AUDIO_FORMAT_START_Interleaved && (fmt) < SPA_AUDIO_FORMAT_START_Planar)
+#define SPA_AUDIO_FORMAT_IS_PLANAR(fmt) ((fmt) > SPA_AUDIO_FORMAT_START_Planar && (fmt) < SPA_AUDIO_FORMAT_START_Other)
+
+enum spa_audio_channel {
+ SPA_AUDIO_CHANNEL_UNKNOWN, /**< unspecified */
+ SPA_AUDIO_CHANNEL_NA, /**< N/A, silent */
+
+ SPA_AUDIO_CHANNEL_MONO, /**< mono stream */
+
+ SPA_AUDIO_CHANNEL_FL, /**< front left */
+ SPA_AUDIO_CHANNEL_FR, /**< front right */
+ SPA_AUDIO_CHANNEL_FC, /**< front center */
+ SPA_AUDIO_CHANNEL_LFE, /**< LFE */
+ SPA_AUDIO_CHANNEL_SL, /**< side left */
+ SPA_AUDIO_CHANNEL_SR, /**< side right */
+ SPA_AUDIO_CHANNEL_FLC, /**< front left center */
+ SPA_AUDIO_CHANNEL_FRC, /**< front right center */
+ SPA_AUDIO_CHANNEL_RC, /**< rear center */
+ SPA_AUDIO_CHANNEL_RL, /**< rear left */
+ SPA_AUDIO_CHANNEL_RR, /**< rear right */
+ SPA_AUDIO_CHANNEL_TC, /**< top center */
+ SPA_AUDIO_CHANNEL_TFL, /**< top front left */
+ SPA_AUDIO_CHANNEL_TFC, /**< top front center */
+ SPA_AUDIO_CHANNEL_TFR, /**< top front right */
+ SPA_AUDIO_CHANNEL_TRL, /**< top rear left */
+ SPA_AUDIO_CHANNEL_TRC, /**< top rear center */
+ SPA_AUDIO_CHANNEL_TRR, /**< top rear right */
+ SPA_AUDIO_CHANNEL_RLC, /**< rear left center */
+ SPA_AUDIO_CHANNEL_RRC, /**< rear right center */
+ SPA_AUDIO_CHANNEL_FLW, /**< front left wide */
+ SPA_AUDIO_CHANNEL_FRW, /**< front right wide */
+ SPA_AUDIO_CHANNEL_LFE2, /**< LFE 2 */
+ SPA_AUDIO_CHANNEL_FLH, /**< front left high */
+ SPA_AUDIO_CHANNEL_FCH, /**< front center high */
+ SPA_AUDIO_CHANNEL_FRH, /**< front right high */
+ SPA_AUDIO_CHANNEL_TFLC, /**< top front left center */
+ SPA_AUDIO_CHANNEL_TFRC, /**< top front right center */
+ SPA_AUDIO_CHANNEL_TSL, /**< top side left */
+ SPA_AUDIO_CHANNEL_TSR, /**< top side right */
+ SPA_AUDIO_CHANNEL_LLFE, /**< left LFE */
+ SPA_AUDIO_CHANNEL_RLFE, /**< right LFE */
+ SPA_AUDIO_CHANNEL_BC, /**< bottom center */
+ SPA_AUDIO_CHANNEL_BLC, /**< bottom left center */
+ SPA_AUDIO_CHANNEL_BRC, /**< bottom right center */
+
+ SPA_AUDIO_CHANNEL_START_Aux = 0x1000, /**< aux channels */
+ SPA_AUDIO_CHANNEL_AUX0 = SPA_AUDIO_CHANNEL_START_Aux,
+ SPA_AUDIO_CHANNEL_AUX1,
+ SPA_AUDIO_CHANNEL_AUX2,
+ SPA_AUDIO_CHANNEL_AUX3,
+ SPA_AUDIO_CHANNEL_AUX4,
+ SPA_AUDIO_CHANNEL_AUX5,
+ SPA_AUDIO_CHANNEL_AUX6,
+ SPA_AUDIO_CHANNEL_AUX7,
+ SPA_AUDIO_CHANNEL_AUX8,
+ SPA_AUDIO_CHANNEL_AUX9,
+ SPA_AUDIO_CHANNEL_AUX10,
+ SPA_AUDIO_CHANNEL_AUX11,
+ SPA_AUDIO_CHANNEL_AUX12,
+ SPA_AUDIO_CHANNEL_AUX13,
+ SPA_AUDIO_CHANNEL_AUX14,
+ SPA_AUDIO_CHANNEL_AUX15,
+ SPA_AUDIO_CHANNEL_AUX16,
+ SPA_AUDIO_CHANNEL_AUX17,
+ SPA_AUDIO_CHANNEL_AUX18,
+ SPA_AUDIO_CHANNEL_AUX19,
+ SPA_AUDIO_CHANNEL_AUX20,
+ SPA_AUDIO_CHANNEL_AUX21,
+ SPA_AUDIO_CHANNEL_AUX22,
+ SPA_AUDIO_CHANNEL_AUX23,
+ SPA_AUDIO_CHANNEL_AUX24,
+ SPA_AUDIO_CHANNEL_AUX25,
+ SPA_AUDIO_CHANNEL_AUX26,
+ SPA_AUDIO_CHANNEL_AUX27,
+ SPA_AUDIO_CHANNEL_AUX28,
+ SPA_AUDIO_CHANNEL_AUX29,
+ SPA_AUDIO_CHANNEL_AUX30,
+ SPA_AUDIO_CHANNEL_AUX31,
+ SPA_AUDIO_CHANNEL_AUX32,
+ SPA_AUDIO_CHANNEL_AUX33,
+ SPA_AUDIO_CHANNEL_AUX34,
+ SPA_AUDIO_CHANNEL_AUX35,
+ SPA_AUDIO_CHANNEL_AUX36,
+ SPA_AUDIO_CHANNEL_AUX37,
+ SPA_AUDIO_CHANNEL_AUX38,
+ SPA_AUDIO_CHANNEL_AUX39,
+ SPA_AUDIO_CHANNEL_AUX40,
+ SPA_AUDIO_CHANNEL_AUX41,
+ SPA_AUDIO_CHANNEL_AUX42,
+ SPA_AUDIO_CHANNEL_AUX43,
+ SPA_AUDIO_CHANNEL_AUX44,
+ SPA_AUDIO_CHANNEL_AUX45,
+ SPA_AUDIO_CHANNEL_AUX46,
+ SPA_AUDIO_CHANNEL_AUX47,
+ SPA_AUDIO_CHANNEL_AUX48,
+ SPA_AUDIO_CHANNEL_AUX49,
+ SPA_AUDIO_CHANNEL_AUX50,
+ SPA_AUDIO_CHANNEL_AUX51,
+ SPA_AUDIO_CHANNEL_AUX52,
+ SPA_AUDIO_CHANNEL_AUX53,
+ SPA_AUDIO_CHANNEL_AUX54,
+ SPA_AUDIO_CHANNEL_AUX55,
+ SPA_AUDIO_CHANNEL_AUX56,
+ SPA_AUDIO_CHANNEL_AUX57,
+ SPA_AUDIO_CHANNEL_AUX58,
+ SPA_AUDIO_CHANNEL_AUX59,
+ SPA_AUDIO_CHANNEL_AUX60,
+ SPA_AUDIO_CHANNEL_AUX61,
+ SPA_AUDIO_CHANNEL_AUX62,
+ SPA_AUDIO_CHANNEL_AUX63,
+
+ SPA_AUDIO_CHANNEL_LAST_Aux = 0x1fff, /**< aux channels */
+
+ SPA_AUDIO_CHANNEL_START_Custom = 0x10000,
+};
+
+/** Extra audio flags */
+#define SPA_AUDIO_FLAG_NONE (0) /*< no valid flag */
+#define SPA_AUDIO_FLAG_UNPOSITIONED (1 << 0) /*< the position array explicitly
+ * contains unpositioned channels. */
+/** Audio information description */
+struct spa_audio_info_raw {
+ enum spa_audio_format format; /*< format, one of enum spa_audio_format */
+ uint32_t flags; /*< extra flags */
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS]; /*< channel position from enum spa_audio_channel */
+};
+
+#define SPA_AUDIO_INFO_RAW_INIT(...) ((struct spa_audio_info_raw) { __VA_ARGS__ })
+
+#define SPA_KEY_AUDIO_FORMAT "audio.format" /**< an audio format as string,
+ * Ex. "S16LE" */
+#define SPA_KEY_AUDIO_CHANNEL "audio.channel" /**< an audio channel as string,
+ * Ex. "FL" */
+#define SPA_KEY_AUDIO_CHANNELS "audio.channels" /**< an audio channel count as int */
+#define SPA_KEY_AUDIO_RATE "audio.rate" /**< an audio sample rate as int */
+#define SPA_KEY_AUDIO_POSITION "audio.position" /**< channel positions as comma separated list
+ * of channels ex. "FL,FR" */
+#define SPA_KEY_AUDIO_ALLOWED_RATES "audio.allowed-rates" /**< a list of allowed samplerates
+ * ex. "[ 44100 48000 ]" */
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_RAW_H */
diff --git a/spa/include/spa/param/audio/type-info.h b/spa/include/spa/param/audio/type-info.h
new file mode 100644
index 0000000..d0532d7
--- /dev/null
+++ b/spa/include/spa/param/audio/type-info.h
@@ -0,0 +1,35 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_TYPES_H
+#define SPA_AUDIO_TYPES_H
+
+#include <spa/param/audio/raw-types.h>
+#include <spa/param/audio/iec958-types.h>
+#include <spa/param/audio/mp3-types.h>
+#include <spa/param/audio/aac-types.h>
+#include <spa/param/audio/wma-types.h>
+#include <spa/param/audio/amr-types.h>
+
+#endif /* SPA_AUDIO_TYPES_H */
diff --git a/spa/include/spa/param/audio/vorbis-utils.h b/spa/include/spa/param/audio/vorbis-utils.h
new file mode 100644
index 0000000..475906e
--- /dev/null
+++ b/spa/include/spa/param/audio/vorbis-utils.h
@@ -0,0 +1,80 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_VORBIS_UTILS_H
+#define SPA_AUDIO_VORBIS_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_vorbis_parse(const struct spa_pod *format, struct spa_audio_info_vorbis *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_vorbis_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_vorbis *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_vorbis),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_VORBIS_UTILS_H */
diff --git a/spa/include/spa/param/audio/vorbis.h b/spa/include/spa/param/audio/vorbis.h
new file mode 100644
index 0000000..5fcfb66
--- /dev/null
+++ b/spa/include/spa/param/audio/vorbis.h
@@ -0,0 +1,49 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_VORBIS_H
+#define SPA_AUDIO_VORBIS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+struct spa_audio_info_vorbis {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+};
+
+#define SPA_AUDIO_INFO_VORBIS_INIT(...) ((struct spa_audio_info_vorbis) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_VORBIS_H */
diff --git a/spa/include/spa/param/audio/wma-types.h b/spa/include/spa/param/audio/wma-types.h
new file mode 100644
index 0000000..ae213c2
--- /dev/null
+++ b/spa/include/spa/param/audio/wma-types.h
@@ -0,0 +1,57 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_WMA_TYPES_H
+#define SPA_AUDIO_WMA_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/type.h>
+#include <spa/param/audio/wma.h>
+
+#define SPA_TYPE_INFO_AudioWMAProfile SPA_TYPE_INFO_ENUM_BASE "AudioWMAProfile"
+#define SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE SPA_TYPE_INFO_AudioWMAProfile ":"
+
+static const struct spa_type_info spa_type_audio_wma_profile[] = {
+ { SPA_AUDIO_WMA_PROFILE_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "UNKNOWN", NULL },
+ { SPA_AUDIO_WMA_PROFILE_WMA7, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA7", NULL },
+ { SPA_AUDIO_WMA_PROFILE_WMA8, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA8", NULL },
+ { SPA_AUDIO_WMA_PROFILE_WMA9, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA9", NULL },
+ { SPA_AUDIO_WMA_PROFILE_WMA10, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA10", NULL },
+ { SPA_AUDIO_WMA_PROFILE_WMA9_PRO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA9-Pro", NULL },
+ { SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA9-Lossless", NULL },
+ { SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA10-Lossless", NULL },
+ { 0, 0, NULL, NULL },
+};
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_WMA_TYPES_H */
diff --git a/spa/include/spa/param/audio/wma-utils.h b/spa/include/spa/param/audio/wma-utils.h
new file mode 100644
index 0000000..f3fe094
--- /dev/null
+++ b/spa/include/spa/param/audio/wma-utils.h
@@ -0,0 +1,93 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_WMA_UTILS_H
+#define SPA_AUDIO_WMA_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/format-utils.h>
+
+static inline int
+spa_format_audio_wma_parse(const struct spa_pod *format, struct spa_audio_info_wma *info)
+{
+ int res;
+ res = spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels),
+ SPA_FORMAT_AUDIO_bitrate, SPA_POD_OPT_Int(&info->bitrate),
+ SPA_FORMAT_AUDIO_blockAlign, SPA_POD_OPT_Int(&info->block_align),
+ SPA_FORMAT_AUDIO_WMA_profile, SPA_POD_OPT_Id(&info->profile));
+ return res;
+}
+
+static inline struct spa_pod *
+spa_format_audio_wma_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_wma *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_wma),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED),
+ 0);
+ if (info->rate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0);
+ if (info->channels != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0);
+ if (info->bitrate != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_bitrate, SPA_POD_Int(info->bitrate), 0);
+ if (info->block_align != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_blockAlign, SPA_POD_Int(info->block_align), 0);
+ if (info->profile != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_WMA_profile, SPA_POD_Id(info->profile), 0);
+
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_WMA_UTILS_H */
diff --git a/spa/include/spa/param/audio/wma.h b/spa/include/spa/param/audio/wma.h
new file mode 100644
index 0000000..6e098ee
--- /dev/null
+++ b/spa/include/spa/param/audio/wma.h
@@ -0,0 +1,67 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AUDIO_WMA_H
+#define SPA_AUDIO_WMA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/audio/raw.h>
+
+enum spa_audio_wma_profile {
+ SPA_AUDIO_WMA_PROFILE_UNKNOWN,
+
+ SPA_AUDIO_WMA_PROFILE_WMA7,
+ SPA_AUDIO_WMA_PROFILE_WMA8,
+ SPA_AUDIO_WMA_PROFILE_WMA9,
+ SPA_AUDIO_WMA_PROFILE_WMA10,
+ SPA_AUDIO_WMA_PROFILE_WMA9_PRO,
+ SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS,
+ SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS,
+
+ SPA_AUDIO_WMA_PROFILE_CUSTOM = 0x10000,
+};
+
+struct spa_audio_info_wma {
+ uint32_t rate; /*< sample rate */
+ uint32_t channels; /*< number of channels */
+ uint32_t bitrate; /*< stream bitrate */
+ uint32_t block_align; /*< block alignment */
+ enum spa_audio_wma_profile profile; /*< WMA profile */
+
+};
+
+#define SPA_AUDIO_INFO_WMA_INIT(...) ((struct spa_audio_info_wma) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AUDIO_WMA_H */
diff --git a/spa/include/spa/param/bluetooth/audio.h b/spa/include/spa/param/bluetooth/audio.h
new file mode 100644
index 0000000..27f3197
--- /dev/null
+++ b/spa/include/spa/param/bluetooth/audio.h
@@ -0,0 +1,74 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUETOOTH_AUDIO_H
+#define SPA_BLUETOOTH_AUDIO_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+enum spa_bluetooth_audio_codec {
+ SPA_BLUETOOTH_AUDIO_CODEC_START,
+
+ /* A2DP */
+ SPA_BLUETOOTH_AUDIO_CODEC_SBC,
+ SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ,
+ SPA_BLUETOOTH_AUDIO_CODEC_MPEG,
+ SPA_BLUETOOTH_AUDIO_CODEC_AAC,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
+ SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
+ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
+
+ /* HFP */
+ SPA_BLUETOOTH_AUDIO_CODEC_CVSD = 0x100,
+ SPA_BLUETOOTH_AUDIO_CODEC_MSBC,
+
+ /* BAP */
+ SPA_BLUETOOTH_AUDIO_CODEC_LC3 = 0x200,
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_BLUETOOTH_AUDIO_H */
diff --git a/spa/include/spa/param/bluetooth/type-info.h b/spa/include/spa/param/bluetooth/type-info.h
new file mode 100644
index 0000000..729bd58
--- /dev/null
+++ b/spa/include/spa/param/bluetooth/type-info.h
@@ -0,0 +1,78 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUETOOTH_TYPES_H
+#define SPA_BLUETOOTH_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/bluetooth/audio.h>
+
+#define SPA_TYPE_INFO_BluetoothAudioCodec SPA_TYPE_INFO_ENUM_BASE "BluetoothAudioCodec"
+#define SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE SPA_TYPE_INFO_BluetoothAudioCodec ":"
+
+static const struct spa_type_info spa_type_bluetooth_audio_codec[] = {
+ /* A2DP */
+ { SPA_BLUETOOTH_AUDIO_CODEC_SBC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "sbc", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "sbc_xq", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_MPEG, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "mpeg", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_AAC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aac", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_APTX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_hd", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_LDAC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "ldac", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll_duplex", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "faststream", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "faststream_duplex", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3plus_hr", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_51", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_71", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_duplex", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_pro", NULL },
+
+ { SPA_BLUETOOTH_AUDIO_CODEC_CVSD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "cvsd", NULL },
+ { SPA_BLUETOOTH_AUDIO_CODEC_MSBC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "msbc", NULL },
+
+ { SPA_BLUETOOTH_AUDIO_CODEC_LC3, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3", NULL },
+
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_BLUETOOTH_TYPES_H */
diff --git a/spa/include/spa/param/buffers-types.h b/spa/include/spa/param/buffers-types.h
new file mode 100644
index 0000000..50a0932
--- /dev/null
+++ b/spa/include/spa/param/buffers-types.h
@@ -0,0 +1,90 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_BUFFERS_TYPES_H
+#define SPA_PARAM_BUFFERS_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param-types.h>
+#include <spa/node/type-info.h>
+
+#include <spa/param/buffers.h>
+
+#define SPA_TYPE_INFO_PARAM_Meta SPA_TYPE_INFO_PARAM_BASE "Meta"
+#define SPA_TYPE_INFO_PARAM_META_BASE SPA_TYPE_INFO_PARAM_Meta ":"
+
+static const struct spa_type_info spa_type_param_meta[] = {
+ { SPA_PARAM_META_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_META_BASE, spa_type_param },
+ { SPA_PARAM_META_type, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_META_BASE "type", spa_type_meta_type },
+ { SPA_PARAM_META_size, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_META_BASE "size", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/** Base for parameters that describe IO areas to exchange data,
+ * control and properties with a node.
+ */
+#define SPA_TYPE_INFO_PARAM_IO SPA_TYPE_INFO_PARAM_BASE "IO"
+#define SPA_TYPE_INFO_PARAM_IO_BASE SPA_TYPE_INFO_PARAM_IO ":"
+
+static const struct spa_type_info spa_type_param_io[] = {
+ { SPA_PARAM_IO_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_IO_BASE, spa_type_param, },
+ { SPA_PARAM_IO_id, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_IO_BASE "id", spa_type_io },
+ { SPA_PARAM_IO_size, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_IO_BASE "size", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_PARAM_Buffers SPA_TYPE_INFO_PARAM_BASE "Buffers"
+#define SPA_TYPE_INFO_PARAM_BUFFERS_BASE SPA_TYPE_INFO_PARAM_Buffers ":"
+
+#define SPA_TYPE_INFO_PARAM_BlockInfo SPA_TYPE_INFO_PARAM_BUFFERS_BASE "BlockInfo"
+#define SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE SPA_TYPE_INFO_PARAM_BlockInfo ":"
+
+static const struct spa_type_info spa_type_param_buffers[] = {
+ { SPA_PARAM_BUFFERS_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_BUFFERS_BASE, spa_type_param, },
+ { SPA_PARAM_BUFFERS_buffers, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BUFFERS_BASE "buffers", NULL },
+ { SPA_PARAM_BUFFERS_blocks, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BUFFERS_BASE "blocks", NULL },
+ { SPA_PARAM_BUFFERS_size, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "size", NULL },
+ { SPA_PARAM_BUFFERS_stride, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "stride", NULL },
+ { SPA_PARAM_BUFFERS_align, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "align", NULL },
+ { SPA_PARAM_BUFFERS_dataType, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "dataType", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_BUFFERS_TYPES_H */
diff --git a/spa/include/spa/param/buffers.h b/spa/include/spa/param/buffers.h
new file mode 100644
index 0000000..d3733a6
--- /dev/null
+++ b/spa/include/spa/param/buffers.h
@@ -0,0 +1,72 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_BUFFERS_H
+#define SPA_PARAM_BUFFERS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param.h>
+
+/** properties for SPA_TYPE_OBJECT_ParamBuffers */
+enum spa_param_buffers {
+ SPA_PARAM_BUFFERS_START,
+ SPA_PARAM_BUFFERS_buffers, /**< number of buffers (Int) */
+ SPA_PARAM_BUFFERS_blocks, /**< number of data blocks per buffer (Int) */
+ SPA_PARAM_BUFFERS_size, /**< size of a data block memory (Int)*/
+ SPA_PARAM_BUFFERS_stride, /**< stride of data block memory (Int) */
+ SPA_PARAM_BUFFERS_align, /**< alignment of data block memory (Int) */
+ SPA_PARAM_BUFFERS_dataType, /**< possible memory types (Int, mask of enum spa_data_type) */
+};
+
+/** properties for SPA_TYPE_OBJECT_ParamMeta */
+enum spa_param_meta {
+ SPA_PARAM_META_START,
+ SPA_PARAM_META_type, /**< the metadata, one of enum spa_meta_type (Id enum spa_meta_type) */
+ SPA_PARAM_META_size, /**< the expected maximum size the meta (Int) */
+};
+
+/** properties for SPA_TYPE_OBJECT_ParamIO */
+enum spa_param_io {
+ SPA_PARAM_IO_START,
+ SPA_PARAM_IO_id, /**< type ID, uniquely identifies the io area (Id enum spa_io_type) */
+ SPA_PARAM_IO_size, /**< size of the io area (Int) */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_BUFFERS_H */
diff --git a/spa/include/spa/param/format-types.h b/spa/include/spa/param/format-types.h
new file mode 100644
index 0000000..7708276
--- /dev/null
+++ b/spa/include/spa/param/format-types.h
@@ -0,0 +1,191 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_FORMAT_TYPES_H
+#define SPA_PARAM_FORMAT_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/format.h>
+#include <spa/param/param-types.h>
+
+#include <spa/param/audio/type-info.h>
+#include <spa/param/video/type-info.h>
+
+#define SPA_TYPE_INFO_Format SPA_TYPE_INFO_PARAM_BASE "Format"
+#define SPA_TYPE_INFO_FORMAT_BASE SPA_TYPE_INFO_Format ":"
+
+#define SPA_TYPE_INFO_MediaType SPA_TYPE_INFO_ENUM_BASE "MediaType"
+#define SPA_TYPE_INFO_MEDIA_TYPE_BASE SPA_TYPE_INFO_MediaType ":"
+
+static const struct spa_type_info spa_type_media_type[] = {
+ { SPA_MEDIA_TYPE_unknown, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_TYPE_BASE "unknown", NULL },
+ { SPA_MEDIA_TYPE_audio, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_TYPE_BASE "audio", NULL },
+ { SPA_MEDIA_TYPE_video, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_TYPE_BASE "video", NULL },
+ { SPA_MEDIA_TYPE_image, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_TYPE_BASE "image", NULL },
+ { SPA_MEDIA_TYPE_binary, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_TYPE_BASE "binary", NULL },
+ { SPA_MEDIA_TYPE_stream, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_TYPE_BASE "stream", NULL },
+ { SPA_MEDIA_TYPE_application, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_TYPE_BASE "application", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_MediaSubtype SPA_TYPE_INFO_ENUM_BASE "MediaSubtype"
+#define SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE SPA_TYPE_INFO_MediaSubtype ":"
+
+static const struct spa_type_info spa_type_media_subtype[] = {
+ { SPA_MEDIA_SUBTYPE_unknown, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "unknown", NULL },
+ /* generic subtypes */
+ { SPA_MEDIA_SUBTYPE_raw, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "raw", NULL },
+ { SPA_MEDIA_SUBTYPE_dsp, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dsp", NULL },
+ { SPA_MEDIA_SUBTYPE_iec958, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "iec958", NULL },
+ { SPA_MEDIA_SUBTYPE_dsd, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dsd", NULL },
+ /* audio subtypes */
+ { SPA_MEDIA_SUBTYPE_mp3, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mp3", NULL },
+ { SPA_MEDIA_SUBTYPE_aac, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "aac", NULL },
+ { SPA_MEDIA_SUBTYPE_vorbis, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vorbis", NULL },
+ { SPA_MEDIA_SUBTYPE_wma, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "wma", NULL },
+ { SPA_MEDIA_SUBTYPE_ra, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "ra", NULL },
+ { SPA_MEDIA_SUBTYPE_sbc, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "sbc", NULL },
+ { SPA_MEDIA_SUBTYPE_adpcm, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "adpcm", NULL },
+ { SPA_MEDIA_SUBTYPE_g723, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g723", NULL },
+ { SPA_MEDIA_SUBTYPE_g726, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g726", NULL },
+ { SPA_MEDIA_SUBTYPE_g729, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g729", NULL },
+ { SPA_MEDIA_SUBTYPE_amr, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "amr", NULL },
+ { SPA_MEDIA_SUBTYPE_gsm, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "gsm", NULL },
+ { SPA_MEDIA_SUBTYPE_alac, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "alac", NULL },
+ { SPA_MEDIA_SUBTYPE_flac, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "flac", NULL },
+ { SPA_MEDIA_SUBTYPE_ape, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "ape", NULL },
+ /* video subtypes */
+ { SPA_MEDIA_SUBTYPE_h264, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h264", NULL },
+ { SPA_MEDIA_SUBTYPE_mjpg, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mjpg", NULL },
+ { SPA_MEDIA_SUBTYPE_dv, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dv", NULL },
+ { SPA_MEDIA_SUBTYPE_mpegts, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpegts", NULL },
+ { SPA_MEDIA_SUBTYPE_h263, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h263", NULL },
+ { SPA_MEDIA_SUBTYPE_mpeg1, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg1", NULL },
+ { SPA_MEDIA_SUBTYPE_mpeg2, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg2", NULL },
+ { SPA_MEDIA_SUBTYPE_mpeg4, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg4", NULL },
+ { SPA_MEDIA_SUBTYPE_xvid, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "xvid", NULL },
+ { SPA_MEDIA_SUBTYPE_vc1, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vc1", NULL },
+ { SPA_MEDIA_SUBTYPE_vp8, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp8", NULL },
+ { SPA_MEDIA_SUBTYPE_vp9, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp9", NULL },
+ { SPA_MEDIA_SUBTYPE_bayer, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "bayer", NULL },
+ /* image subtypes */
+ { SPA_MEDIA_SUBTYPE_jpeg, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "jpeg", NULL },
+ /* stream subtypes */
+ { SPA_MEDIA_SUBTYPE_midi, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "midi", NULL },
+ /* application subtypes */
+ { SPA_MEDIA_SUBTYPE_control, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "control", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_FormatAudio SPA_TYPE_INFO_FORMAT_BASE "Audio"
+#define SPA_TYPE_INFO_FORMAT_AUDIO_BASE SPA_TYPE_INFO_FormatAudio ":"
+
+#define SPA_TYPE_INFO_FORMAT_AUDIO_AAC SPA_TYPE_INFO_FORMAT_AUDIO_BASE "AAC"
+#define SPA_TYPE_INFO_FORMAT_AUDIO_AAC_BASE SPA_TYPE_INFO_FORMAT_AUDIO_AAC ":"
+#define SPA_TYPE_INFO_FORMAT_AUDIO_WMA SPA_TYPE_INFO_FORMAT_AUDIO_BASE "WMA"
+#define SPA_TYPE_INFO_FORMAT_AUDIO_WMA_BASE SPA_TYPE_INFO_FORMAT_AUDIO_WMA ":"
+#define SPA_TYPE_INFO_FORMAT_AUDIO_AMR SPA_TYPE_INFO_FORMAT_AUDIO_BASE "AMR"
+#define SPA_TYPE_INFO_FORMAT_AUDIO_AMR_BASE SPA_TYPE_INFO_FORMAT_AUDIO_AMR ":"
+
+#define SPA_TYPE_INFO_FormatVideo SPA_TYPE_INFO_FORMAT_BASE "Video"
+#define SPA_TYPE_INFO_FORMAT_VIDEO_BASE SPA_TYPE_INFO_FormatVideo ":"
+
+#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 ":"
+
+static const struct spa_type_info spa_type_format[] = {
+ { SPA_FORMAT_START, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_BASE, spa_type_param, },
+
+ { SPA_FORMAT_mediaType, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_BASE "mediaType",
+ spa_type_media_type, },
+ { SPA_FORMAT_mediaSubtype, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_BASE "mediaSubtype",
+ spa_type_media_subtype, },
+
+ { SPA_FORMAT_AUDIO_format, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "format",
+ spa_type_audio_format },
+ { SPA_FORMAT_AUDIO_flags, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "flags",
+ spa_type_audio_flags },
+ { SPA_FORMAT_AUDIO_rate, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "rate", NULL },
+ { SPA_FORMAT_AUDIO_channels, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "channels", NULL },
+ { SPA_FORMAT_AUDIO_position, SPA_TYPE_Array, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "position",
+ spa_type_prop_channel_map },
+
+ { SPA_FORMAT_AUDIO_iec958Codec, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "iec958Codec",
+ spa_type_audio_iec958_codec },
+
+ { SPA_FORMAT_AUDIO_bitorder, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "bitorder",
+ spa_type_param_bitorder },
+ { SPA_FORMAT_AUDIO_interleave, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "interleave", NULL },
+ { SPA_FORMAT_AUDIO_bitrate, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "bitrate", NULL },
+ { SPA_FORMAT_AUDIO_blockAlign, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "blockAlign", NULL },
+
+ { SPA_FORMAT_AUDIO_AAC_streamFormat, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_AAC_BASE "streamFormat",
+ spa_type_audio_aac_stream_format },
+ { SPA_FORMAT_AUDIO_WMA_profile, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_WMA_BASE "profile",
+ spa_type_audio_wma_profile },
+ { SPA_FORMAT_AUDIO_AMR_bandMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_AMR_BASE "bandMode",
+ spa_type_audio_amr_band_mode },
+
+ { SPA_FORMAT_VIDEO_format, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "format",
+ spa_type_video_format, },
+ { SPA_FORMAT_VIDEO_modifier, SPA_TYPE_Long, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "modifier", NULL },
+ { SPA_FORMAT_VIDEO_size, SPA_TYPE_Rectangle, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "size", NULL },
+ { SPA_FORMAT_VIDEO_framerate, SPA_TYPE_Fraction, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "framerate", NULL },
+ { SPA_FORMAT_VIDEO_maxFramerate, SPA_TYPE_Fraction, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "maxFramerate", NULL },
+ { SPA_FORMAT_VIDEO_views, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "views", NULL },
+ { SPA_FORMAT_VIDEO_interlaceMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "interlaceMode",
+ spa_type_video_interlace_mode, },
+ { SPA_FORMAT_VIDEO_pixelAspectRatio, SPA_TYPE_Fraction, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "pixelAspectRatio", NULL },
+ { SPA_FORMAT_VIDEO_multiviewMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "multiviewMode", NULL },
+ { SPA_FORMAT_VIDEO_multiviewFlags, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "multiviewFlags", NULL },
+ { SPA_FORMAT_VIDEO_chromaSite, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "chromaSite", NULL },
+ { SPA_FORMAT_VIDEO_colorRange, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorRange", NULL },
+ { SPA_FORMAT_VIDEO_colorMatrix, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorMatrix", NULL },
+ { SPA_FORMAT_VIDEO_transferFunction, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "transferFunction", NULL },
+ { SPA_FORMAT_VIDEO_colorPrimaries, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorPrimaries", NULL },
+ { SPA_FORMAT_VIDEO_profile, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "profile", NULL },
+ { SPA_FORMAT_VIDEO_level, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "level", NULL },
+
+ { 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 },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_FORMAT_TYPES_H */
diff --git a/spa/include/spa/param/format-utils.h b/spa/include/spa/param/format-utils.h
new file mode 100644
index 0000000..e2c83e2
--- /dev/null
+++ b/spa/include/spa/param/format-utils.h
@@ -0,0 +1,58 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_FORMAT_UTILS_H
+#define SPA_PARAM_FORMAT_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/param/format.h>
+
+static inline int
+spa_format_parse(const struct spa_pod *format, uint32_t *media_type, uint32_t *media_subtype)
+{
+ return spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_mediaType, SPA_POD_Id(media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(media_subtype));
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_FORMAT_UTILS_H */
diff --git a/spa/include/spa/param/format.h b/spa/include/spa/param/format.h
new file mode 100644
index 0000000..d184790
--- /dev/null
+++ b/spa/include/spa/param/format.h
@@ -0,0 +1,176 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_FORMAT_H
+#define SPA_PARAM_FORMAT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param.h>
+
+/** media type for SPA_TYPE_OBJECT_Format */
+enum spa_media_type {
+ SPA_MEDIA_TYPE_unknown,
+ SPA_MEDIA_TYPE_audio,
+ SPA_MEDIA_TYPE_video,
+ SPA_MEDIA_TYPE_image,
+ SPA_MEDIA_TYPE_binary,
+ SPA_MEDIA_TYPE_stream,
+ SPA_MEDIA_TYPE_application,
+};
+
+/** media subtype for SPA_TYPE_OBJECT_Format */
+enum spa_media_subtype {
+ SPA_MEDIA_SUBTYPE_unknown,
+ SPA_MEDIA_SUBTYPE_raw,
+ SPA_MEDIA_SUBTYPE_dsp,
+ SPA_MEDIA_SUBTYPE_iec958, /** S/PDIF */
+ SPA_MEDIA_SUBTYPE_dsd,
+
+ SPA_MEDIA_SUBTYPE_START_Audio = 0x10000,
+ SPA_MEDIA_SUBTYPE_mp3,
+ SPA_MEDIA_SUBTYPE_aac,
+ SPA_MEDIA_SUBTYPE_vorbis,
+ SPA_MEDIA_SUBTYPE_wma,
+ SPA_MEDIA_SUBTYPE_ra,
+ SPA_MEDIA_SUBTYPE_sbc,
+ SPA_MEDIA_SUBTYPE_adpcm,
+ SPA_MEDIA_SUBTYPE_g723,
+ SPA_MEDIA_SUBTYPE_g726,
+ SPA_MEDIA_SUBTYPE_g729,
+ SPA_MEDIA_SUBTYPE_amr,
+ SPA_MEDIA_SUBTYPE_gsm,
+ SPA_MEDIA_SUBTYPE_alac, /** since 0.3.65 */
+ SPA_MEDIA_SUBTYPE_flac, /** since 0.3.65 */
+ SPA_MEDIA_SUBTYPE_ape, /** since 0.3.65 */
+
+ SPA_MEDIA_SUBTYPE_START_Video = 0x20000,
+ SPA_MEDIA_SUBTYPE_h264,
+ SPA_MEDIA_SUBTYPE_mjpg,
+ SPA_MEDIA_SUBTYPE_dv,
+ SPA_MEDIA_SUBTYPE_mpegts,
+ SPA_MEDIA_SUBTYPE_h263,
+ SPA_MEDIA_SUBTYPE_mpeg1,
+ SPA_MEDIA_SUBTYPE_mpeg2,
+ SPA_MEDIA_SUBTYPE_mpeg4,
+ SPA_MEDIA_SUBTYPE_xvid,
+ SPA_MEDIA_SUBTYPE_vc1,
+ SPA_MEDIA_SUBTYPE_vp8,
+ SPA_MEDIA_SUBTYPE_vp9,
+ SPA_MEDIA_SUBTYPE_bayer,
+
+ SPA_MEDIA_SUBTYPE_START_Image = 0x30000,
+ SPA_MEDIA_SUBTYPE_jpeg,
+
+ SPA_MEDIA_SUBTYPE_START_Binary = 0x40000,
+
+ SPA_MEDIA_SUBTYPE_START_Stream = 0x50000,
+ SPA_MEDIA_SUBTYPE_midi,
+
+ SPA_MEDIA_SUBTYPE_START_Application = 0x60000,
+ SPA_MEDIA_SUBTYPE_control, /**< control stream, data contains
+ * spa_pod_sequence with control info. */
+};
+
+/** properties for audio SPA_TYPE_OBJECT_Format */
+enum spa_format {
+ SPA_FORMAT_START,
+
+ SPA_FORMAT_mediaType, /**< media type (Id enum spa_media_type) */
+ SPA_FORMAT_mediaSubtype, /**< media subtype (Id enum spa_media_subtype) */
+
+ /* Audio format keys */
+ SPA_FORMAT_START_Audio = 0x10000,
+ SPA_FORMAT_AUDIO_format, /**< audio format, (Id enum spa_audio_format) */
+ SPA_FORMAT_AUDIO_flags, /**< optional flags (Int) */
+ SPA_FORMAT_AUDIO_rate, /**< sample rate (Int) */
+ SPA_FORMAT_AUDIO_channels, /**< number of audio channels (Int) */
+ SPA_FORMAT_AUDIO_position, /**< channel positions (Id enum spa_audio_position) */
+
+ SPA_FORMAT_AUDIO_iec958Codec, /**< codec used (IEC958) (Id enum spa_audio_iec958_codec) */
+
+ SPA_FORMAT_AUDIO_bitorder, /**< bit order (Id enum spa_param_bitorder) */
+ SPA_FORMAT_AUDIO_interleave, /**< Interleave bytes (Int) */
+ SPA_FORMAT_AUDIO_bitrate, /**< bit rate (Int) */
+ SPA_FORMAT_AUDIO_blockAlign, /**< audio data block alignment (Int) */
+
+ SPA_FORMAT_AUDIO_AAC_streamFormat, /**< AAC stream format, (Id enum spa_audio_aac_stream_format) */
+
+ SPA_FORMAT_AUDIO_WMA_profile, /**< WMA profile (Id enum spa_audio_wma_profile) */
+
+ SPA_FORMAT_AUDIO_AMR_bandMode, /**< AMR band mode (Id enum spa_audio_amr_band_mode) */
+
+
+ /* Video Format keys */
+ SPA_FORMAT_START_Video = 0x20000,
+ SPA_FORMAT_VIDEO_format, /**< video format (Id enum spa_video_format) */
+ SPA_FORMAT_VIDEO_modifier, /**< format modifier (Long)
+ * use only with DMA-BUF and omit for other buffer types */
+ SPA_FORMAT_VIDEO_size, /**< size (Rectangle) */
+ SPA_FORMAT_VIDEO_framerate, /**< frame rate (Fraction) */
+ SPA_FORMAT_VIDEO_maxFramerate, /**< maximum frame rate (Fraction) */
+ SPA_FORMAT_VIDEO_views, /**< number of views (Int) */
+ SPA_FORMAT_VIDEO_interlaceMode, /**< (Id enum spa_video_interlace_mode) */
+ SPA_FORMAT_VIDEO_pixelAspectRatio, /**< (Rectangle) */
+ SPA_FORMAT_VIDEO_multiviewMode, /**< (Id enum spa_video_multiview_mode) */
+ SPA_FORMAT_VIDEO_multiviewFlags, /**< (Id enum spa_video_multiview_flags) */
+ SPA_FORMAT_VIDEO_chromaSite, /**< /Id enum spa_video_chroma_site) */
+ SPA_FORMAT_VIDEO_colorRange, /**< /Id enum spa_video_color_range) */
+ SPA_FORMAT_VIDEO_colorMatrix, /**< /Id enum spa_video_color_matrix) */
+ SPA_FORMAT_VIDEO_transferFunction, /**< /Id enum spa_video_transfer_function) */
+ SPA_FORMAT_VIDEO_colorPrimaries, /**< /Id enum spa_video_color_primaries) */
+ SPA_FORMAT_VIDEO_profile, /**< (Int) */
+ SPA_FORMAT_VIDEO_level, /**< (Int) */
+ SPA_FORMAT_VIDEO_H264_streamFormat, /**< (Id enum spa_h264_stream_format) */
+ SPA_FORMAT_VIDEO_H264_alignment, /**< (Id enum spa_h264_alignment) */
+
+ /* Image Format keys */
+ SPA_FORMAT_START_Image = 0x30000,
+ /* Binary Format keys */
+ SPA_FORMAT_START_Binary = 0x40000,
+ /* Stream Format keys */
+ SPA_FORMAT_START_Stream = 0x50000,
+ /* Application Format keys */
+ SPA_FORMAT_START_Application = 0x60000,
+};
+
+#define SPA_KEY_FORMAT_DSP "format.dsp" /**< a predefined DSP format,
+ * Ex. "32 bit float mono audio" */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_FORMAT_H */
diff --git a/spa/include/spa/param/latency-types.h b/spa/include/spa/param/latency-types.h
new file mode 100644
index 0000000..aa58b9f
--- /dev/null
+++ b/spa/include/spa/param/latency-types.h
@@ -0,0 +1,75 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_LATENCY_TYPES_H
+#define SPA_PARAM_LATENCY_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/utils/enum-types.h>
+#include <spa/param/param-types.h>
+#include <spa/param/latency.h>
+
+#define SPA_TYPE_INFO_PARAM_Latency SPA_TYPE_INFO_PARAM_BASE "Latency"
+#define SPA_TYPE_INFO_PARAM_LATENCY_BASE SPA_TYPE_INFO_PARAM_Latency ":"
+
+static const struct spa_type_info spa_type_param_latency[] = {
+ { SPA_PARAM_LATENCY_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_LATENCY_BASE, spa_type_param, },
+ { SPA_PARAM_LATENCY_direction, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_LATENCY_BASE "direction", spa_type_direction, },
+ { SPA_PARAM_LATENCY_minQuantum, SPA_TYPE_Float, SPA_TYPE_INFO_PARAM_LATENCY_BASE "minQuantum", NULL, },
+ { SPA_PARAM_LATENCY_maxQuantum, SPA_TYPE_Float, SPA_TYPE_INFO_PARAM_LATENCY_BASE "maxQuantum", NULL, },
+ { SPA_PARAM_LATENCY_minRate, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_LATENCY_BASE "minRate", NULL, },
+ { SPA_PARAM_LATENCY_maxRate, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_LATENCY_BASE "maxRate", NULL, },
+ { SPA_PARAM_LATENCY_minNs, SPA_TYPE_Long, SPA_TYPE_INFO_PARAM_LATENCY_BASE "minNs", NULL, },
+ { SPA_PARAM_LATENCY_maxNs, SPA_TYPE_Long, SPA_TYPE_INFO_PARAM_LATENCY_BASE "maxNs", NULL, },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_PARAM_ProcessLatency SPA_TYPE_INFO_PARAM_BASE "ProcessLatency"
+#define SPA_TYPE_INFO_PARAM_PROCESS_LATENCY_BASE SPA_TYPE_INFO_PARAM_ProcessLatency ":"
+
+static const struct spa_type_info spa_type_param_process_latency[] = {
+ { SPA_PARAM_PROCESS_LATENCY_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_LATENCY_BASE, spa_type_param, },
+ { SPA_PARAM_PROCESS_LATENCY_quantum, SPA_TYPE_Float, SPA_TYPE_INFO_PARAM_PROCESS_LATENCY_BASE "quantum", NULL, },
+ { SPA_PARAM_PROCESS_LATENCY_rate, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_PROCESS_LATENCY_BASE "rate", NULL, },
+ { SPA_PARAM_PROCESS_LATENCY_ns, SPA_TYPE_Long, SPA_TYPE_INFO_PARAM_PROCESS_LATENCY_BASE "ns", NULL, },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_LATENCY_TYPES_H */
diff --git a/spa/include/spa/param/latency-utils.h b/spa/include/spa/param/latency-utils.h
new file mode 100644
index 0000000..b467058
--- /dev/null
+++ b/spa/include/spa/param/latency-utils.h
@@ -0,0 +1,177 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_LATENCY_UTILS_H
+#define SPA_PARAM_LATENCY_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <float.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/param/latency.h>
+
+static inline int
+spa_latency_info_compare(const struct spa_latency_info *a, struct spa_latency_info *b)
+{
+ if (a->min_quantum == b->min_quantum &&
+ a->max_quantum == b->max_quantum &&
+ a->min_rate == b->min_rate &&
+ a->max_rate == b->max_rate &&
+ a->min_ns == b->min_ns &&
+ a->max_ns == b->max_ns)
+ return 0;
+ return 1;
+}
+
+static inline 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);
+}
+static inline 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)
+ info->min_rate = 0;
+ if (info->min_ns == UINT64_MAX)
+ info->min_ns = 0;
+}
+
+static inline int
+spa_latency_info_combine(struct spa_latency_info *info, const struct spa_latency_info *other)
+{
+ if (info->direction != other->direction)
+ return -EINVAL;
+ if (other->min_quantum < info->min_quantum)
+ info->min_quantum = other->min_quantum;
+ if (other->max_quantum > info->max_quantum)
+ info->max_quantum = other->max_quantum;
+ if (other->min_rate < info->min_rate)
+ info->min_rate = other->min_rate;
+ if (other->max_rate > info->max_rate)
+ info->max_rate = other->max_rate;
+ if (other->min_ns < info->min_ns)
+ info->min_ns = other->min_ns;
+ if (other->max_ns > info->max_ns)
+ info->max_ns = other->max_ns;
+ return 0;
+}
+
+static inline int
+spa_latency_parse(const struct spa_pod *latency, struct spa_latency_info *info)
+{
+ int res;
+ spa_zero(*info);
+ if ((res = spa_pod_parse_object(latency,
+ SPA_TYPE_OBJECT_ParamLatency, NULL,
+ SPA_PARAM_LATENCY_direction, SPA_POD_Id(&info->direction),
+ SPA_PARAM_LATENCY_minQuantum, SPA_POD_OPT_Float(&info->min_quantum),
+ SPA_PARAM_LATENCY_maxQuantum, SPA_POD_OPT_Float(&info->max_quantum),
+ SPA_PARAM_LATENCY_minRate, SPA_POD_OPT_Int(&info->min_rate),
+ SPA_PARAM_LATENCY_maxRate, SPA_POD_OPT_Int(&info->max_rate),
+ SPA_PARAM_LATENCY_minNs, SPA_POD_OPT_Long(&info->min_ns),
+ SPA_PARAM_LATENCY_maxNs, SPA_POD_OPT_Long(&info->max_ns))) < 0)
+ return res;
+ info->direction = (enum spa_direction)(info->direction & 1);
+ return 0;
+}
+
+static inline 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,
+ SPA_TYPE_OBJECT_ParamLatency, id,
+ SPA_PARAM_LATENCY_direction, SPA_POD_Id(info->direction),
+ SPA_PARAM_LATENCY_minQuantum, SPA_POD_Float(info->min_quantum),
+ SPA_PARAM_LATENCY_maxQuantum, SPA_POD_Float(info->max_quantum),
+ SPA_PARAM_LATENCY_minRate, SPA_POD_Int(info->min_rate),
+ SPA_PARAM_LATENCY_maxRate, SPA_POD_Int(info->max_rate),
+ SPA_PARAM_LATENCY_minNs, SPA_POD_Long(info->min_ns),
+ SPA_PARAM_LATENCY_maxNs, SPA_POD_Long(info->max_ns));
+}
+
+static inline int
+spa_process_latency_parse(const struct spa_pod *latency, struct spa_process_latency_info *info)
+{
+ int res;
+ spa_zero(*info);
+ if ((res = spa_pod_parse_object(latency,
+ SPA_TYPE_OBJECT_ParamProcessLatency, NULL,
+ SPA_PARAM_PROCESS_LATENCY_quantum, SPA_POD_OPT_Float(&info->quantum),
+ SPA_PARAM_PROCESS_LATENCY_rate, SPA_POD_OPT_Int(&info->rate),
+ SPA_PARAM_PROCESS_LATENCY_ns, SPA_POD_OPT_Long(&info->ns))) < 0)
+ return res;
+ return 0;
+}
+
+static inline struct spa_pod *
+spa_process_latency_build(struct spa_pod_builder *builder, uint32_t id,
+ const struct spa_process_latency_info *info)
+{
+ return (struct spa_pod *)spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_ParamProcessLatency, id,
+ SPA_PARAM_PROCESS_LATENCY_quantum, SPA_POD_Float(info->quantum),
+ SPA_PARAM_PROCESS_LATENCY_rate, SPA_POD_Int(info->rate),
+ SPA_PARAM_PROCESS_LATENCY_ns, SPA_POD_Long(info->ns));
+}
+
+static inline int
+spa_process_latency_info_add(const struct spa_process_latency_info *process,
+ struct spa_latency_info *info)
+{
+ info->min_quantum += process->quantum;
+ info->max_quantum += process->quantum;
+ info->min_rate += process->rate;
+ info->max_rate += process->rate;
+ info->min_ns += process->ns;
+ info->max_ns += process->ns;
+ return 0;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_LATENCY_UTILS_H */
diff --git a/spa/include/spa/param/latency.h b/spa/include/spa/param/latency.h
new file mode 100644
index 0000000..da3f66d
--- /dev/null
+++ b/spa/include/spa/param/latency.h
@@ -0,0 +1,89 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_LATENY_H
+#define SPA_PARAM_LATENY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param.h>
+
+/** properties for SPA_TYPE_OBJECT_ParamLatency */
+enum spa_param_latency {
+ SPA_PARAM_LATENCY_START,
+ SPA_PARAM_LATENCY_direction, /**< direction, input/output (Id enum spa_direction) */
+ SPA_PARAM_LATENCY_minQuantum, /**< min latency relative to quantum (Float) */
+ SPA_PARAM_LATENCY_maxQuantum, /**< max latency relative to quantum (Float) */
+ SPA_PARAM_LATENCY_minRate, /**< min latency (Int) relative to rate */
+ SPA_PARAM_LATENCY_maxRate, /**< max latency (Int) relative to rate */
+ SPA_PARAM_LATENCY_minNs, /**< min latency (Long) in nanoseconds */
+ SPA_PARAM_LATENCY_maxNs, /**< max latency (Long) in nanoseconds */
+};
+
+/** helper structure for managing latency objects */
+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;
+};
+
+#define SPA_LATENCY_INFO(dir,...) ((struct spa_latency_info) { .direction = (dir), ## __VA_ARGS__ })
+
+/** properties for SPA_TYPE_OBJECT_ParamProcessLatency */
+enum spa_param_process_latency {
+ SPA_PARAM_PROCESS_LATENCY_START,
+ SPA_PARAM_PROCESS_LATENCY_quantum, /**< latency relative to quantum (Float) */
+ SPA_PARAM_PROCESS_LATENCY_rate, /**< latency (Int) relative to rate */
+ SPA_PARAM_PROCESS_LATENCY_ns, /**< latency (Long) in nanoseconds */
+};
+
+/** Helper structure for managing process latency objects */
+struct spa_process_latency_info {
+ float quantum;
+ uint32_t rate;
+ uint64_t ns;
+};
+
+#define SPA_PROCESS_LATENCY_INFO_INIT(...) ((struct spa_process_latency_info) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_LATENY_H */
diff --git a/spa/include/spa/param/param-types.h b/spa/include/spa/param/param-types.h
new file mode 100644
index 0000000..56d1b18
--- /dev/null
+++ b/spa/include/spa/param/param-types.h
@@ -0,0 +1,115 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_TYPES_H
+#define SPA_PARAM_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/props.h>
+#include <spa/param/format.h>
+#include <spa/buffer/type-info.h>
+
+/* base for parameter object enumerations */
+#define SPA_TYPE_INFO_ParamId SPA_TYPE_INFO_ENUM_BASE "ParamId"
+#define SPA_TYPE_INFO_PARAM_ID_BASE SPA_TYPE_INFO_ParamId ":"
+
+static const struct spa_type_info spa_type_param[] = {
+ { SPA_PARAM_Invalid, SPA_TYPE_None, SPA_TYPE_INFO_PARAM_ID_BASE "Invalid", NULL },
+ { SPA_PARAM_PropInfo, SPA_TYPE_OBJECT_PropInfo, SPA_TYPE_INFO_PARAM_ID_BASE "PropInfo", NULL },
+ { SPA_PARAM_Props, SPA_TYPE_OBJECT_Props, SPA_TYPE_INFO_PARAM_ID_BASE "Props", NULL },
+ { SPA_PARAM_EnumFormat, SPA_TYPE_OBJECT_Format, SPA_TYPE_INFO_PARAM_ID_BASE "EnumFormat", NULL },
+ { SPA_PARAM_Format, SPA_TYPE_OBJECT_Format, SPA_TYPE_INFO_PARAM_ID_BASE "Format", NULL },
+ { SPA_PARAM_Buffers, SPA_TYPE_OBJECT_ParamBuffers, SPA_TYPE_INFO_PARAM_ID_BASE "Buffers", NULL },
+ { SPA_PARAM_Meta, SPA_TYPE_OBJECT_ParamMeta, SPA_TYPE_INFO_PARAM_ID_BASE "Meta", NULL },
+ { SPA_PARAM_IO, SPA_TYPE_OBJECT_ParamIO, SPA_TYPE_INFO_PARAM_ID_BASE "IO", NULL },
+ { SPA_PARAM_EnumProfile, SPA_TYPE_OBJECT_ParamProfile, SPA_TYPE_INFO_PARAM_ID_BASE "EnumProfile", NULL },
+ { SPA_PARAM_Profile, SPA_TYPE_OBJECT_ParamProfile, SPA_TYPE_INFO_PARAM_ID_BASE "Profile", NULL },
+ { SPA_PARAM_EnumPortConfig, SPA_TYPE_OBJECT_ParamPortConfig, SPA_TYPE_INFO_PARAM_ID_BASE "EnumPortConfig", NULL },
+ { SPA_PARAM_PortConfig, SPA_TYPE_OBJECT_ParamPortConfig, SPA_TYPE_INFO_PARAM_ID_BASE "PortConfig", NULL },
+ { SPA_PARAM_EnumRoute, SPA_TYPE_OBJECT_ParamRoute, SPA_TYPE_INFO_PARAM_ID_BASE "EnumRoute", NULL },
+ { SPA_PARAM_Route, SPA_TYPE_OBJECT_ParamRoute, SPA_TYPE_INFO_PARAM_ID_BASE "Route", NULL },
+ { SPA_PARAM_Control, SPA_TYPE_Sequence, SPA_TYPE_INFO_PARAM_ID_BASE "Control", NULL },
+ { SPA_PARAM_Latency, SPA_TYPE_OBJECT_ParamLatency, SPA_TYPE_INFO_PARAM_ID_BASE "Latency", NULL },
+ { SPA_PARAM_ProcessLatency, SPA_TYPE_OBJECT_ParamProcessLatency, SPA_TYPE_INFO_PARAM_ID_BASE "ProcessLatency", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/* base for parameter objects */
+#define SPA_TYPE_INFO_Param SPA_TYPE_INFO_OBJECT_BASE "Param"
+#define SPA_TYPE_INFO_PARAM_BASE SPA_TYPE_INFO_Param ":"
+
+#include <spa/param/audio/type-info.h>
+
+static const struct spa_type_info spa_type_prop_float_array[] = {
+ { SPA_PROP_START, SPA_TYPE_Float, SPA_TYPE_INFO_BASE "floatArray", 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 },
+};
+
+static const struct spa_type_info spa_type_prop_iec958_codec[] = {
+ { SPA_PROP_START, SPA_TYPE_Id, SPA_TYPE_INFO_BASE "iec958Codec", spa_type_audio_iec958_codec, },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_ParamBitorder SPA_TYPE_INFO_ENUM_BASE "ParamBitorder"
+#define SPA_TYPE_INFO_PARAM_BITORDER_BASE SPA_TYPE_INFO_ParamBitorder ":"
+
+static const struct spa_type_info spa_type_param_bitorder[] = {
+ { SPA_PARAM_BITORDER_unknown, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BITORDER_BASE "unknown", NULL },
+ { SPA_PARAM_BITORDER_msb, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BITORDER_BASE "msb", NULL },
+ { SPA_PARAM_BITORDER_lsb, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BITORDER_BASE "lsb", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_ParamAvailability SPA_TYPE_INFO_ENUM_BASE "ParamAvailability"
+#define SPA_TYPE_INFO_PARAM_AVAILABILITY_BASE SPA_TYPE_INFO_ParamAvailability ":"
+
+static const struct spa_type_info spa_type_param_availability[] = {
+ { SPA_PARAM_AVAILABILITY_unknown, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_AVAILABILITY_BASE "unknown", NULL },
+ { SPA_PARAM_AVAILABILITY_no, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_AVAILABILITY_BASE "no", NULL },
+ { SPA_PARAM_AVAILABILITY_yes, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_AVAILABILITY_BASE "yes", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_TYPES_H */
diff --git a/spa/include/spa/param/param.h b/spa/include/spa/param/param.h
new file mode 100644
index 0000000..1c26ef8
--- /dev/null
+++ b/spa/include/spa/param/param.h
@@ -0,0 +1,107 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_H
+#define SPA_PARAM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup spa_param Parameters
+ * Parameter value enumerations and type information
+ */
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/utils/defs.h>
+
+/** different parameter types that can be queried */
+enum spa_param_type {
+ SPA_PARAM_Invalid, /**< invalid */
+ SPA_PARAM_PropInfo, /**< property information as SPA_TYPE_OBJECT_PropInfo */
+ SPA_PARAM_Props, /**< properties as SPA_TYPE_OBJECT_Props */
+ SPA_PARAM_EnumFormat, /**< available formats as SPA_TYPE_OBJECT_Format */
+ SPA_PARAM_Format, /**< configured format as SPA_TYPE_OBJECT_Format */
+ SPA_PARAM_Buffers, /**< buffer configurations as SPA_TYPE_OBJECT_ParamBuffers*/
+ SPA_PARAM_Meta, /**< allowed metadata for buffers as SPA_TYPE_OBJECT_ParamMeta*/
+ SPA_PARAM_IO, /**< configurable IO areas as SPA_TYPE_OBJECT_ParamIO */
+ SPA_PARAM_EnumProfile, /**< profile enumeration as SPA_TYPE_OBJECT_ParamProfile */
+ SPA_PARAM_Profile, /**< profile configuration as SPA_TYPE_OBJECT_ParamProfile */
+ SPA_PARAM_EnumPortConfig, /**< port configuration enumeration as SPA_TYPE_OBJECT_ParamPortConfig */
+ SPA_PARAM_PortConfig, /**< port configuration as SPA_TYPE_OBJECT_ParamPortConfig */
+ SPA_PARAM_EnumRoute, /**< routing enumeration as SPA_TYPE_OBJECT_ParamRoute */
+ SPA_PARAM_Route, /**< routing configuration as SPA_TYPE_OBJECT_ParamRoute */
+ SPA_PARAM_Control, /**< Control parameter, a SPA_TYPE_Sequence */
+ SPA_PARAM_Latency, /**< latency reporting, a SPA_TYPE_OBJECT_ParamLatency */
+ SPA_PARAM_ProcessLatency, /**< processing latency, a SPA_TYPE_OBJECT_ParamProcessLatency */
+};
+
+/** information about a parameter */
+struct spa_param_info {
+ uint32_t id; /**< enum spa_param_type */
+#define SPA_PARAM_INFO_SERIAL (1<<0) /**< bit to signal update even when the
+ * read/write flags don't change */
+#define SPA_PARAM_INFO_READ (1<<1)
+#define SPA_PARAM_INFO_WRITE (1<<2)
+#define SPA_PARAM_INFO_READWRITE (SPA_PARAM_INFO_WRITE|SPA_PARAM_INFO_READ)
+ uint32_t flags;
+ uint32_t user; /**< private user field. You can use this to keep
+ * state. */
+ int32_t seq; /**< private seq field. You can use this to keep
+ * state of a pending update. */
+ uint32_t padding[4];
+};
+
+#define SPA_PARAM_INFO(id,flags) ((struct spa_param_info){ (id), (flags) })
+
+enum spa_param_bitorder {
+ SPA_PARAM_BITORDER_unknown, /**< unknown bitorder */
+ SPA_PARAM_BITORDER_msb, /**< most significant bit */
+ SPA_PARAM_BITORDER_lsb, /**< least significant bit */
+};
+
+enum spa_param_availability {
+ SPA_PARAM_AVAILABILITY_unknown, /**< unknown availability */
+ SPA_PARAM_AVAILABILITY_no, /**< not available */
+ SPA_PARAM_AVAILABILITY_yes, /**< available */
+};
+
+#include <spa/param/buffers.h>
+#include <spa/param/profile.h>
+#include <spa/param/port-config.h>
+#include <spa/param/route.h>
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_H */
diff --git a/spa/include/spa/param/port-config-types.h b/spa/include/spa/param/port-config-types.h
new file mode 100644
index 0000000..f05cdeb
--- /dev/null
+++ b/spa/include/spa/param/port-config-types.h
@@ -0,0 +1,73 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_PORT_CONFIG_TYPES_H
+#define SPA_PARAM_PORT_CONFIG_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/utils/enum-types.h>
+#include <spa/param/param-types.h>
+#include <spa/param/port-config.h>
+
+#define SPA_TYPE_INFO_ParamPortConfigMode SPA_TYPE_INFO_ENUM_BASE "ParamPortConfigMode"
+#define SPA_TYPE_INFO_PARAM_PORT_CONFIG_MODE_BASE SPA_TYPE_INFO_ParamPortConfigMode ":"
+
+static const struct spa_type_info spa_type_param_port_config_mode[] = {
+ { SPA_PARAM_PORT_CONFIG_MODE_none, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_PORT_CONFIG_MODE_BASE "none", NULL },
+ { SPA_PARAM_PORT_CONFIG_MODE_passthrough, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_PORT_CONFIG_MODE_BASE "passthrough", NULL },
+ { SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_PORT_CONFIG_MODE_BASE "convert", NULL },
+ { SPA_PARAM_PORT_CONFIG_MODE_dsp, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_PORT_CONFIG_MODE_BASE "dsp", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_PARAM_PortConfig SPA_TYPE_INFO_PARAM_BASE "PortConfig"
+#define SPA_TYPE_INFO_PARAM_PORT_CONFIG_BASE SPA_TYPE_INFO_PARAM_PortConfig ":"
+
+static const struct spa_type_info spa_type_param_port_config[] = {
+ { SPA_PARAM_PORT_CONFIG_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_PORT_CONFIG_BASE, spa_type_param, },
+ { SPA_PARAM_PORT_CONFIG_direction, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_PORT_CONFIG_BASE "direction", spa_type_direction, },
+ { SPA_PARAM_PORT_CONFIG_mode, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_PORT_CONFIG_BASE "mode", spa_type_param_port_config_mode },
+ { SPA_PARAM_PORT_CONFIG_monitor, SPA_TYPE_Bool, SPA_TYPE_INFO_PARAM_PORT_CONFIG_BASE "monitor", NULL },
+ { SPA_PARAM_PORT_CONFIG_control, SPA_TYPE_Bool, SPA_TYPE_INFO_PARAM_PORT_CONFIG_BASE "control", NULL },
+ { SPA_PARAM_PORT_CONFIG_format, SPA_TYPE_OBJECT_Format, SPA_TYPE_INFO_PARAM_PORT_CONFIG_BASE "format", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_PORT_CONFIG_TYPES_H */
diff --git a/spa/include/spa/param/port-config.h b/spa/include/spa/param/port-config.h
new file mode 100644
index 0000000..d88a3ff
--- /dev/null
+++ b/spa/include/spa/param/port-config.h
@@ -0,0 +1,66 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_PORT_CONFIG_H
+#define SPA_PARAM_PORT_CONFIG_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param.h>
+
+enum spa_param_port_config_mode {
+ SPA_PARAM_PORT_CONFIG_MODE_none, /**< no configuration */
+ SPA_PARAM_PORT_CONFIG_MODE_passthrough, /**< passthrough configuration */
+ SPA_PARAM_PORT_CONFIG_MODE_convert, /**< convert configuration */
+ SPA_PARAM_PORT_CONFIG_MODE_dsp, /**< dsp configuration, depending on the external
+ * format. For audio, ports will be configured for
+ * the given number of channels with F32 format. */
+};
+
+/** properties for SPA_TYPE_OBJECT_ParamPortConfig */
+enum spa_param_port_config {
+ SPA_PARAM_PORT_CONFIG_START,
+ SPA_PARAM_PORT_CONFIG_direction, /**< direction, input/output (Id enum spa_direction) */
+ SPA_PARAM_PORT_CONFIG_mode, /**< (Id enum spa_param_port_config_mode) mode */
+ SPA_PARAM_PORT_CONFIG_monitor, /**< (Bool) enable monitor output ports on input ports */
+ SPA_PARAM_PORT_CONFIG_control, /**< (Bool) enable control ports */
+ SPA_PARAM_PORT_CONFIG_format, /**< (Object) format filter */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_PORT_CONFIG_H */
diff --git a/spa/include/spa/param/profile-types.h b/spa/include/spa/param/profile-types.h
new file mode 100644
index 0000000..e8a0831
--- /dev/null
+++ b/spa/include/spa/param/profile-types.h
@@ -0,0 +1,65 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_PROFILE_TYPES_H
+#define SPA_PARAM_PROFILE_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param-types.h>
+
+#include <spa/param/profile.h>
+
+#define SPA_TYPE_INFO_PARAM_Profile SPA_TYPE_INFO_PARAM_BASE "Profile"
+#define SPA_TYPE_INFO_PARAM_PROFILE_BASE SPA_TYPE_INFO_PARAM_Profile ":"
+
+static const struct spa_type_info spa_type_param_profile[] = {
+ { SPA_PARAM_PROFILE_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_PROFILE_BASE, spa_type_param, },
+ { SPA_PARAM_PROFILE_index, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_PROFILE_BASE "index", NULL },
+ { SPA_PARAM_PROFILE_name, SPA_TYPE_String, SPA_TYPE_INFO_PARAM_PROFILE_BASE "name", NULL },
+ { SPA_PARAM_PROFILE_description, SPA_TYPE_String, SPA_TYPE_INFO_PARAM_PROFILE_BASE "description", NULL },
+ { SPA_PARAM_PROFILE_priority, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_PROFILE_BASE "priority", NULL },
+ { SPA_PARAM_PROFILE_available, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_PROFILE_BASE "available", spa_type_param_availability, },
+ { SPA_PARAM_PROFILE_info, SPA_TYPE_Struct, SPA_TYPE_INFO_PARAM_PROFILE_BASE "info", NULL, },
+ { SPA_PARAM_PROFILE_classes, SPA_TYPE_Struct, SPA_TYPE_INFO_PARAM_PROFILE_BASE "classes", NULL, },
+ { SPA_PARAM_PROFILE_save, SPA_TYPE_Bool, SPA_TYPE_INFO_PARAM_PROFILE_BASE "save", NULL, },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_PROFILE_TYPES_H */
diff --git a/spa/include/spa/param/profile.h b/spa/include/spa/param/profile.h
new file mode 100644
index 0000000..3242433
--- /dev/null
+++ b/spa/include/spa/param/profile.h
@@ -0,0 +1,72 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_PROFILE_H
+#define SPA_PARAM_PROFILE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param.h>
+
+/** properties for SPA_TYPE_OBJECT_ParamProfile */
+enum spa_param_profile {
+ SPA_PARAM_PROFILE_START,
+ SPA_PARAM_PROFILE_index, /**< profile index (Int) */
+ SPA_PARAM_PROFILE_name, /**< profile name (String) */
+ SPA_PARAM_PROFILE_description, /**< profile description (String) */
+ SPA_PARAM_PROFILE_priority, /**< profile priority (Int) */
+ SPA_PARAM_PROFILE_available, /**< availability of the profile
+ * (Id enum spa_param_availability) */
+ SPA_PARAM_PROFILE_info, /**< info (Struct(
+ * Int : n_items,
+ * (String : key,
+ * String : value)*)) */
+ SPA_PARAM_PROFILE_classes, /**< node classes provided by this profile
+ * (Struct(
+ * Int : number of items following
+ * Struct(
+ * String : class name (eg. "Audio/Source"),
+ * Int : number of nodes
+ * String : property (eg. "card.profile.devices"),
+ * Array of Int: device indexes
+ * )*)) */
+ SPA_PARAM_PROFILE_save, /**< If profile should be saved (Bool) */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_PROFILE_H */
diff --git a/spa/include/spa/param/profiler-types.h b/spa/include/spa/param/profiler-types.h
new file mode 100644
index 0000000..3f7297f
--- /dev/null
+++ b/spa/include/spa/param/profiler-types.h
@@ -0,0 +1,60 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_PROFILER_TYPES_H
+#define SPA_PARAM_PROFILER_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param-types.h>
+#include <spa/param/profiler.h>
+
+#define SPA_TYPE_INFO_Profiler SPA_TYPE_INFO_OBJECT_BASE "Profiler"
+#define SPA_TYPE_INFO_PROFILER_BASE SPA_TYPE_INFO_Profiler ":"
+
+static const struct spa_type_info spa_type_profiler[] = {
+ { SPA_PROFILER_START, SPA_TYPE_Id, SPA_TYPE_INFO_PROFILER_BASE, spa_type_param, },
+ { SPA_PROFILER_info, SPA_TYPE_Struct, SPA_TYPE_INFO_PROFILER_BASE "info", NULL, },
+ { 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, },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_PROFILER_TYPES_H */
diff --git a/spa/include/spa/param/profiler.h b/spa/include/spa/param/profiler.h
new file mode 100644
index 0000000..44b5688
--- /dev/null
+++ b/spa/include/spa/param/profiler.h
@@ -0,0 +1,97 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_PROFILER_H
+#define SPA_PARAM_PROFILER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param.h>
+
+/** properties for SPA_TYPE_OBJECT_Profiler */
+enum spa_profiler {
+ SPA_PROFILER_START,
+
+ SPA_PROFILER_START_Driver = 0x10000, /**< driver related profiler properties */
+ SPA_PROFILER_info, /**< Generic info, counter and CPU load,
+ * (Struct(
+ * Long : counter,
+ * Float : cpu_load fast,
+ * Float : cpu_load medium,
+ * Float : cpu_load slow),
+ * Int : xrun-count)) */
+ SPA_PROFILER_clock, /**< clock information
+ * (Struct(
+ * Int : clock flags,
+ * 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)) */
+ SPA_PROFILER_driverBlock, /**< generic driver info block
+ * (Struct(
+ * Int : driver_id,
+ * String : name,
+ * Long : driver prev_signal,
+ * Long : driver signal,
+ * Long : driver awake,
+ * Long : driver finish,
+ * Int : driver status),
+ * Fraction : latency)) */
+
+ SPA_PROFILER_START_Follower = 0x20000, /**< follower related profiler properties */
+ SPA_PROFILER_followerBlock, /**< generic follower info block
+ * (Struct(
+ * Int : id,
+ * String : name,
+ * Long : prev_signal,
+ * Long : signal,
+ * Long : awake,
+ * Long : finish,
+ * Int : status,
+ * Fraction : latency)) */
+
+ SPA_PROFILER_START_CUSTOM = 0x1000000,
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_PROFILER_H */
diff --git a/spa/include/spa/param/props-types.h b/spa/include/spa/param/props-types.h
new file mode 100644
index 0000000..aa3ea24
--- /dev/null
+++ b/spa/include/spa/param/props-types.h
@@ -0,0 +1,119 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_PROPS_TYPES_H
+#define SPA_PARAM_PROPS_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param-types.h>
+
+#include <spa/param/bluetooth/type-info.h>
+
+/** Props Param */
+#define SPA_TYPE_INFO_Props SPA_TYPE_INFO_PARAM_BASE "Props"
+#define SPA_TYPE_INFO_PROPS_BASE SPA_TYPE_INFO_Props ":"
+
+static const struct spa_type_info spa_type_props[] = {
+ { SPA_PROP_START, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE, spa_type_param, },
+ { SPA_PROP_unknown, SPA_TYPE_None, SPA_TYPE_INFO_PROPS_BASE "unknown", NULL },
+ { SPA_PROP_device, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "device", NULL },
+ { SPA_PROP_deviceName, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "deviceName", NULL },
+ { SPA_PROP_deviceFd, SPA_TYPE_Fd, SPA_TYPE_INFO_PROPS_BASE "deviceFd", NULL },
+ { SPA_PROP_card, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "card", NULL },
+ { SPA_PROP_cardName, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "cardName", NULL },
+ { SPA_PROP_minLatency, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "minLatency", NULL },
+ { SPA_PROP_maxLatency, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "maxLatency", NULL },
+ { SPA_PROP_periods, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "periods", NULL },
+ { SPA_PROP_periodSize, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "periodSize", NULL },
+ { SPA_PROP_periodEvent, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "periodEvent", NULL },
+ { SPA_PROP_live, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "live", NULL },
+ { SPA_PROP_rate, SPA_TYPE_Double, SPA_TYPE_INFO_PROPS_BASE "rate", NULL },
+ { SPA_PROP_quality, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "quality", NULL },
+ { SPA_PROP_bluetoothAudioCodec, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "bluetoothAudioCodec", spa_type_bluetooth_audio_codec },
+ { SPA_PROP_bluetoothOffloadActive, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "bluetoothOffloadActive", NULL },
+
+ { SPA_PROP_waveType, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "waveType", NULL },
+ { SPA_PROP_frequency, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "frequency", NULL },
+ { SPA_PROP_volume, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "volume", NULL },
+ { SPA_PROP_mute, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "mute", NULL },
+ { SPA_PROP_patternType, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "patternType", NULL },
+ { SPA_PROP_ditherType, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "ditherType", NULL },
+ { SPA_PROP_truncate, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "truncate", NULL },
+ { SPA_PROP_channelVolumes, SPA_TYPE_Array, SPA_TYPE_INFO_PROPS_BASE "channelVolumes", spa_type_prop_float_array },
+ { SPA_PROP_volumeBase, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "volumeBase", NULL },
+ { SPA_PROP_volumeStep, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "volumeStep", NULL },
+ { SPA_PROP_channelMap, SPA_TYPE_Array, SPA_TYPE_INFO_PROPS_BASE "channelMap", spa_type_prop_channel_map },
+ { SPA_PROP_monitorMute, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "monitorMute", NULL },
+ { SPA_PROP_monitorVolumes, SPA_TYPE_Array, SPA_TYPE_INFO_PROPS_BASE "monitorVolumes", spa_type_prop_float_array },
+ { SPA_PROP_latencyOffsetNsec, SPA_TYPE_Long, SPA_TYPE_INFO_PROPS_BASE "latencyOffsetNsec", NULL },
+ { SPA_PROP_softMute, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "softMute", NULL },
+ { SPA_PROP_softVolumes, SPA_TYPE_Array, SPA_TYPE_INFO_PROPS_BASE "softVolumes", spa_type_prop_float_array },
+ { SPA_PROP_iec958Codecs, SPA_TYPE_Array, SPA_TYPE_INFO_PROPS_BASE "iec958Codecs", spa_type_prop_iec958_codec },
+
+ { 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_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_params, SPA_TYPE_Struct, SPA_TYPE_INFO_PROPS_BASE "params", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/** Enum Property info */
+#define SPA_TYPE_INFO_PropInfo SPA_TYPE_INFO_PARAM_BASE "PropInfo"
+#define SPA_TYPE_INFO_PROP_INFO_BASE SPA_TYPE_INFO_PropInfo ":"
+
+static const struct spa_type_info spa_type_prop_info[] = {
+ { SPA_PROP_INFO_START, SPA_TYPE_Id, SPA_TYPE_INFO_PROP_INFO_BASE, spa_type_param, },
+ { SPA_PROP_INFO_id, SPA_TYPE_Id, SPA_TYPE_INFO_PROP_INFO_BASE "id", spa_type_props },
+ { SPA_PROP_INFO_name, SPA_TYPE_String, SPA_TYPE_INFO_PROP_INFO_BASE "name", NULL },
+ { SPA_PROP_INFO_type, SPA_TYPE_Pod, SPA_TYPE_INFO_PROP_INFO_BASE "type", NULL },
+ { SPA_PROP_INFO_labels, SPA_TYPE_Struct, SPA_TYPE_INFO_PROP_INFO_BASE "labels", NULL },
+ { SPA_PROP_INFO_container, SPA_TYPE_Id, SPA_TYPE_INFO_PROP_INFO_BASE "container", NULL },
+ { SPA_PROP_INFO_params, SPA_TYPE_Bool, SPA_TYPE_INFO_PROP_INFO_BASE "params", NULL },
+ { SPA_PROP_INFO_description, SPA_TYPE_String, SPA_TYPE_INFO_PROP_INFO_BASE "description", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_PROPS_TYPES_H */
diff --git a/spa/include/spa/param/props.h b/spa/include/spa/param/props.h
new file mode 100644
index 0000000..900dffa
--- /dev/null
+++ b/spa/include/spa/param/props.h
@@ -0,0 +1,132 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_PROPS_H
+#define SPA_PARAM_PROPS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param.h>
+
+/** properties of SPA_TYPE_OBJECT_PropInfo */
+enum spa_prop_info {
+ SPA_PROP_INFO_START,
+ SPA_PROP_INFO_id, /**< associated id of the property */
+ SPA_PROP_INFO_name, /**< name of the property */
+ SPA_PROP_INFO_type, /**< type and range/enums of property */
+ SPA_PROP_INFO_labels, /**< labels of property if any, this is a
+ * struct with pairs of values, the first one
+ * is of the type of the property, the second
+ * one is a string with a user readable label
+ * for the value. */
+ SPA_PROP_INFO_container, /**< type of container if any (Id) */
+ SPA_PROP_INFO_params, /**< is part of params property (Bool) */
+ SPA_PROP_INFO_description, /**< User readable description */
+};
+
+/** predefined properties for SPA_TYPE_OBJECT_Props */
+enum spa_prop {
+ SPA_PROP_START,
+
+ SPA_PROP_unknown, /**< an unknown property */
+
+ SPA_PROP_START_Device = 0x100, /**< device related properties */
+ SPA_PROP_device,
+ SPA_PROP_deviceName,
+ SPA_PROP_deviceFd,
+ SPA_PROP_card,
+ SPA_PROP_cardName,
+
+ SPA_PROP_minLatency,
+ SPA_PROP_maxLatency,
+ SPA_PROP_periods,
+ SPA_PROP_periodSize,
+ SPA_PROP_periodEvent,
+ SPA_PROP_live,
+ SPA_PROP_rate,
+ SPA_PROP_quality,
+ SPA_PROP_bluetoothAudioCodec,
+ SPA_PROP_bluetoothOffloadActive,
+
+ SPA_PROP_START_Audio = 0x10000, /**< audio related properties */
+ SPA_PROP_waveType,
+ SPA_PROP_frequency,
+ SPA_PROP_volume, /**< a volume (Float), 0.0 silence, 1.0 normal */
+ SPA_PROP_mute, /**< mute (Bool) */
+ SPA_PROP_patternType,
+ SPA_PROP_ditherType,
+ SPA_PROP_truncate,
+ SPA_PROP_channelVolumes, /**< a volume array, one volume per
+ * channel (Array of Float) */
+ SPA_PROP_volumeBase, /**< a volume base (Float) */
+ SPA_PROP_volumeStep, /**< a volume step (Float) */
+ SPA_PROP_channelMap, /**< a channelmap array
+ * (Array (Id enum spa_audio_channel)) */
+ SPA_PROP_monitorMute, /**< mute (Bool) */
+ SPA_PROP_monitorVolumes, /**< a volume array, one volume per
+ * channel (Array of Float) */
+ SPA_PROP_latencyOffsetNsec, /**< delay adjustment */
+ SPA_PROP_softMute, /**< mute (Bool) */
+ SPA_PROP_softVolumes, /**< a volume array, one volume per
+ * channel (Array of Float) */
+
+ SPA_PROP_iec958Codecs, /**< enabled IEC958 (S/PDIF) codecs,
+ * (Array (Id enum spa_audio_iec958_codec) */
+
+ SPA_PROP_START_Video = 0x20000, /**< video related properties */
+ SPA_PROP_brightness,
+ SPA_PROP_contrast,
+ SPA_PROP_saturation,
+ SPA_PROP_hue,
+ SPA_PROP_gamma,
+ SPA_PROP_exposure,
+ SPA_PROP_gain,
+ SPA_PROP_sharpness,
+
+ SPA_PROP_START_Other = 0x80000, /**< other properties */
+ SPA_PROP_params, /**< simple control params
+ * (Struct(
+ * (String : key,
+ * Pod : value)*)) */
+
+
+ SPA_PROP_START_CUSTOM = 0x1000000,
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_PROPS_H */
diff --git a/spa/include/spa/param/route-types.h b/spa/include/spa/param/route-types.h
new file mode 100644
index 0000000..b370c6c
--- /dev/null
+++ b/spa/include/spa/param/route-types.h
@@ -0,0 +1,71 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_ROUTE_TYPES_H
+#define SPA_PARAM_ROUTE_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/utils/enum-types.h>
+#include <spa/param/param-types.h>
+
+#include <spa/param/route.h>
+
+#define SPA_TYPE_INFO_PARAM_Route SPA_TYPE_INFO_PARAM_BASE "Route"
+#define SPA_TYPE_INFO_PARAM_ROUTE_BASE SPA_TYPE_INFO_PARAM_Route ":"
+
+static const struct spa_type_info spa_type_param_route[] = {
+ { SPA_PARAM_ROUTE_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_ROUTE_BASE, spa_type_param, },
+ { SPA_PARAM_ROUTE_index, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "index", NULL, },
+ { SPA_PARAM_ROUTE_direction, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_ROUTE_BASE "direction", spa_type_direction, },
+ { SPA_PARAM_ROUTE_device, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "device", NULL, },
+ { SPA_PARAM_ROUTE_name, SPA_TYPE_String, SPA_TYPE_INFO_PARAM_ROUTE_BASE "name", NULL, },
+ { SPA_PARAM_ROUTE_description, SPA_TYPE_String, SPA_TYPE_INFO_PARAM_ROUTE_BASE "description", NULL, },
+ { 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_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_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 },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_ROUTE_TYPES_H */
diff --git a/spa/include/spa/param/route.h b/spa/include/spa/param/route.h
new file mode 100644
index 0000000..2e79b94
--- /dev/null
+++ b/spa/include/spa/param/route.h
@@ -0,0 +1,69 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_ROUTE_H
+#define SPA_PARAM_ROUTE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/param.h>
+
+/** properties for SPA_TYPE_OBJECT_ParamRoute */
+enum spa_param_route {
+ SPA_PARAM_ROUTE_START,
+ SPA_PARAM_ROUTE_index, /**< index of the routing destination (Int) */
+ SPA_PARAM_ROUTE_direction, /**< direction, input/output (Id enum spa_direction) */
+ SPA_PARAM_ROUTE_device, /**< device id (Int) */
+ SPA_PARAM_ROUTE_name, /**< name of the routing destination (String) */
+ SPA_PARAM_ROUTE_description, /**< description of the destination (String) */
+ SPA_PARAM_ROUTE_priority, /**< priority of the destination (Int) */
+ SPA_PARAM_ROUTE_available, /**< availability of the destination
+ * (Id enum spa_param_availability) */
+ SPA_PARAM_ROUTE_info, /**< info (Struct(
+ * Int : n_items,
+ * (String : key,
+ * String : value)*)) */
+ SPA_PARAM_ROUTE_profiles, /**< associated profile indexes (Array of Int) */
+ SPA_PARAM_ROUTE_props, /**< properties SPA_TYPE_OBJECT_Props */
+ SPA_PARAM_ROUTE_devices, /**< associated device indexes (Array of Int) */
+ SPA_PARAM_ROUTE_profile, /**< profile id (Int) */
+ SPA_PARAM_ROUTE_save, /**< If route should be saved (Bool) */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_ROUTE_H */
diff --git a/spa/include/spa/param/type-info.h b/spa/include/spa/param/type-info.h
new file mode 100644
index 0000000..c6df3e0
--- /dev/null
+++ b/spa/include/spa/param/type-info.h
@@ -0,0 +1,38 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_TYPE_INFO_H
+#define SPA_PARAM_TYPE_INFO_H
+
+#include <spa/param/param-types.h>
+#include <spa/param/buffers-types.h>
+#include <spa/param/props-types.h>
+#include <spa/param/format-types.h>
+#include <spa/param/latency-types.h>
+#include <spa/param/port-config-types.h>
+#include <spa/param/profiler-types.h>
+#include <spa/param/profile-types.h>
+#include <spa/param/route-types.h>
+
+#endif /* SPA_PARAM_TYPE_INFO_H */
diff --git a/spa/include/spa/param/video/chroma.h b/spa/include/spa/param/video/chroma.h
new file mode 100644
index 0000000..0ad2072
--- /dev/null
+++ b/spa/include/spa/param/video/chroma.h
@@ -0,0 +1,64 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_CHROMA_H
+#define SPA_VIDEO_CHROMA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+/** Various Chroma settings.
+ */
+enum spa_video_chroma_site {
+ SPA_VIDEO_CHROMA_SITE_UNKNOWN = 0, /**< unknown cositing */
+ SPA_VIDEO_CHROMA_SITE_NONE = (1 << 0), /**< no cositing */
+ SPA_VIDEO_CHROMA_SITE_H_COSITED = (1 << 1), /**< chroma is horizontally cosited */
+ SPA_VIDEO_CHROMA_SITE_V_COSITED = (1 << 2), /**< chroma is vertically cosited */
+ SPA_VIDEO_CHROMA_SITE_ALT_LINE = (1 << 3), /**< chroma samples are sited on alternate lines */
+ /* some common chroma cositing */
+ /** chroma samples cosited with luma samples */
+ SPA_VIDEO_CHROMA_SITE_COSITED = (SPA_VIDEO_CHROMA_SITE_H_COSITED | SPA_VIDEO_CHROMA_SITE_V_COSITED),
+ /** jpeg style cositing, also for mpeg1 and mjpeg */
+ SPA_VIDEO_CHROMA_SITE_JPEG = (SPA_VIDEO_CHROMA_SITE_NONE),
+ /** mpeg2 style cositing */
+ SPA_VIDEO_CHROMA_SITE_MPEG2 = (SPA_VIDEO_CHROMA_SITE_H_COSITED),
+ /**< DV style cositing */
+ SPA_VIDEO_CHROMA_SITE_DV = (SPA_VIDEO_CHROMA_SITE_COSITED | SPA_VIDEO_CHROMA_SITE_ALT_LINE),
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_CHROMA_H */
diff --git a/spa/include/spa/param/video/color.h b/spa/include/spa/param/video/color.h
new file mode 100644
index 0000000..028239c
--- /dev/null
+++ b/spa/include/spa/param/video/color.h
@@ -0,0 +1,125 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_COLOR_H
+#define SPA_VIDEO_COLOR_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+/**
+ * Possible color range values. These constants are defined for 8 bit color
+ * values and can be scaled for other bit depths.
+ */
+enum spa_video_color_range {
+ SPA_VIDEO_COLOR_RANGE_UNKNOWN = 0, /**< unknown range */
+ SPA_VIDEO_COLOR_RANGE_0_255, /**< [0..255] for 8 bit components */
+ SPA_VIDEO_COLOR_RANGE_16_235 /**< [16..235] for 8 bit components. Chroma has
+ [16..240] range. */
+};
+
+/**
+ * The color matrix is used to convert between Y'PbPr and
+ * non-linear RGB (R'G'B')
+ */
+enum spa_video_color_matrix {
+ SPA_VIDEO_COLOR_MATRIX_UNKNOWN = 0, /**< unknown matrix */
+ SPA_VIDEO_COLOR_MATRIX_RGB, /**< identity matrix */
+ SPA_VIDEO_COLOR_MATRIX_FCC, /**< FCC color matrix */
+ SPA_VIDEO_COLOR_MATRIX_BT709, /**< ITU BT.709 color matrix */
+ SPA_VIDEO_COLOR_MATRIX_BT601, /**< ITU BT.601 color matrix */
+ SPA_VIDEO_COLOR_MATRIX_SMPTE240M, /**< SMTPE 240M color matrix */
+ SPA_VIDEO_COLOR_MATRIX_BT2020, /**< ITU-R BT.2020 color matrix. since 1.6. */
+};
+
+/**
+ * The video transfer function defines the formula for converting between
+ * non-linear RGB (R'G'B') and linear RGB
+ */
+enum spa_video_transfer_function {
+ SPA_VIDEO_TRANSFER_UNKNOWN = 0, /**< unknown transfer function */
+ SPA_VIDEO_TRANSFER_GAMMA10, /**< linear RGB, gamma 1.0 curve */
+ SPA_VIDEO_TRANSFER_GAMMA18, /**< Gamma 1.8 curve */
+ SPA_VIDEO_TRANSFER_GAMMA20, /**< Gamma 2.0 curve */
+ SPA_VIDEO_TRANSFER_GAMMA22, /**< Gamma 2.2 curve */
+ SPA_VIDEO_TRANSFER_BT709, /**< Gamma 2.2 curve with a linear segment in the lower range */
+ SPA_VIDEO_TRANSFER_SMPTE240M, /**< Gamma 2.2 curve with a linear segment in the lower range */
+ SPA_VIDEO_TRANSFER_SRGB, /**< Gamma 2.4 curve with a linear segment in the lower range */
+ SPA_VIDEO_TRANSFER_GAMMA28, /**< Gamma 2.8 curve */
+ SPA_VIDEO_TRANSFER_LOG100, /**< Logarithmic transfer characteristic 100:1 range */
+ SPA_VIDEO_TRANSFER_LOG316, /**< Logarithmic transfer characteristic 316.22777:1 range */
+ SPA_VIDEO_TRANSFER_BT2020_12, /**< Gamma 2.2 curve with a linear segment in the lower
+ * range. Used for BT.2020 with 12 bits per
+ * component. \since 1.6. */
+ SPA_VIDEO_TRANSFER_ADOBERGB, /**< Gamma 2.19921875. \since 1.8 */
+};
+
+/**
+ * The color primaries define the how to transform linear RGB values to and from
+ * the CIE XYZ colorspace.
+ */
+enum spa_video_color_primaries {
+ SPA_VIDEO_COLOR_PRIMARIES_UNKNOWN = 0, /**< unknown color primaries */
+ SPA_VIDEO_COLOR_PRIMARIES_BT709, /**< BT709 primaries */
+ SPA_VIDEO_COLOR_PRIMARIES_BT470M, /**< BT470M primaries */
+ SPA_VIDEO_COLOR_PRIMARIES_BT470BG, /**< BT470BG primaries */
+ SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M, /**< SMPTE170M primaries */
+ SPA_VIDEO_COLOR_PRIMARIES_SMPTE240M, /**< SMPTE240M primaries */
+ SPA_VIDEO_COLOR_PRIMARIES_FILM, /**< Generic film */
+ SPA_VIDEO_COLOR_PRIMARIES_BT2020, /**< BT2020 primaries. \since 1.6. */
+ SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB, /**< Adobe RGB primaries. \since 1.8 */
+};
+
+/**
+ * spa_video_colorimetry:
+ *
+ * Structure describing the color info.
+ */
+struct spa_video_colorimetry {
+ enum spa_video_color_range range; /**< The color range. This is the valid range for the
+ * samples. It is used to convert the samples to Y'PbPr
+ * values. */
+ enum spa_video_color_matrix matrix; /**< the color matrix. Used to convert between Y'PbPr and
+ * non-linear RGB (R'G'B') */
+ enum spa_video_transfer_function transfer; /**< The transfer function. Used to convert between
+ * R'G'B' and RGB */
+ enum spa_video_color_primaries primaries; /**< Color primaries. Used to convert between R'G'B'
+ * and CIE XYZ */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_COLOR_H */
diff --git a/spa/include/spa/param/video/dsp-utils.h b/spa/include/spa/param/video/dsp-utils.h
new file mode 100644
index 0000000..524655f
--- /dev/null
+++ b/spa/include/spa/param/video/dsp-utils.h
@@ -0,0 +1,83 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_DSP_UTILS_H
+#define SPA_VIDEO_DSP_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/video/dsp.h>
+
+static inline int
+spa_format_video_dsp_parse(const struct spa_pod *format,
+ struct spa_video_info_dsp *info)
+{
+ info->flags = SPA_VIDEO_FLAG_NONE;
+ if (spa_pod_find_prop (format, NULL, SPA_FORMAT_VIDEO_modifier)) {
+ info->flags |= SPA_VIDEO_FLAG_MODIFIER;
+ }
+
+ return spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_VIDEO_format, SPA_POD_OPT_Id(&info->format),
+ SPA_FORMAT_VIDEO_modifier, SPA_POD_OPT_Long(&info->modifier));
+}
+
+static inline struct spa_pod *
+spa_format_video_dsp_build(struct spa_pod_builder *builder, uint32_t id,
+ struct spa_video_info_dsp *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ 0);
+ if (info->format != SPA_VIDEO_FORMAT_UNKNOWN)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(info->format), 0);
+ if (info->modifier != 0 || info->flags & SPA_VIDEO_FLAG_MODIFIER)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_modifier, SPA_POD_Long(info->modifier), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_DSP_UTILS_H */
diff --git a/spa/include/spa/param/video/dsp.h b/spa/include/spa/param/video/dsp.h
new file mode 100644
index 0000000..f97046f
--- /dev/null
+++ b/spa/include/spa/param/video/dsp.h
@@ -0,0 +1,55 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_DSP_H
+#define SPA_VIDEO_DSP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/video/raw.h>
+
+struct spa_video_info_dsp {
+ enum spa_video_format format;
+ uint32_t flags;
+ uint64_t modifier;
+};
+
+#define SPA_VIDEO_INFO_DSP_INIT(...) ((struct spa_video_info_dsp) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_DSP_H */
diff --git a/spa/include/spa/param/video/encoded.h b/spa/include/spa/param/video/encoded.h
new file mode 100644
index 0000000..b34e0d8
--- /dev/null
+++ b/spa/include/spa/param/video/encoded.h
@@ -0,0 +1,31 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_ENCODED_H
+#define SPA_VIDEO_ENCODED_H
+
+#include <spa/param/video/h264.h>
+#include <spa/param/video/mjpg.h>
+
+#endif /* SPA_VIDEO_ENCODED_H */
diff --git a/spa/include/spa/param/video/format-utils.h b/spa/include/spa/param/video/format-utils.h
new file mode 100644
index 0000000..560f7f7
--- /dev/null
+++ b/spa/include/spa/param/video/format-utils.h
@@ -0,0 +1,84 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_VIDEO_FORMAT_UTILS_H
+#define SPA_PARAM_VIDEO_FORMAT_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/param/format-utils.h>
+#include <spa/param/video/format.h>
+#include <spa/param/video/raw-utils.h>
+#include <spa/param/video/dsp-utils.h>
+#include <spa/param/video/h264-utils.h>
+#include <spa/param/video/mjpg-utils.h>
+
+static inline int
+spa_format_video_parse(const struct spa_pod *format, struct spa_video_info *info)
+{
+ int res;
+
+ if ((res = spa_format_parse(format, &info->media_type, &info->media_subtype)) < 0)
+ return res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_video)
+ return -EINVAL;
+
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ return spa_format_video_raw_parse(format, &info->info.raw);
+ case SPA_MEDIA_SUBTYPE_dsp:
+ return spa_format_video_dsp_parse(format, &info->info.dsp);
+ case SPA_MEDIA_SUBTYPE_h264:
+ return spa_format_video_h264_parse(format, &info->info.h264);
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ return spa_format_video_mjpg_parse(format, &info->info.mjpg);
+ }
+ return -ENOTSUP;
+}
+
+static inline struct spa_pod *
+spa_format_video_build(struct spa_pod_builder *builder, uint32_t id, struct spa_video_info *info)
+{
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ return spa_format_video_raw_build(builder, id, &info->info.raw);
+ case SPA_MEDIA_SUBTYPE_dsp:
+ return spa_format_video_dsp_build(builder, id, &info->info.dsp);
+ case SPA_MEDIA_SUBTYPE_h264:
+ return spa_format_video_h264_build(builder, id, &info->info.h264);
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ return spa_format_video_mjpg_build(builder, id, &info->info.mjpg);
+ }
+ errno = ENOTSUP;
+ return NULL;
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_VIDEO_FORMAT_UTILS_H */
diff --git a/spa/include/spa/param/video/format.h b/spa/include/spa/param/video/format.h
new file mode 100644
index 0000000..0098bec
--- /dev/null
+++ b/spa/include/spa/param/video/format.h
@@ -0,0 +1,61 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PARAM_VIDEO_FORMAT_H
+#define SPA_PARAM_VIDEO_FORMAT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/format.h>
+#include <spa/param/video/raw.h>
+#include <spa/param/video/dsp.h>
+#include <spa/param/video/encoded.h>
+
+struct spa_video_info {
+ uint32_t media_type;
+ uint32_t media_subtype;
+ union {
+ struct spa_video_info_raw raw;
+ struct spa_video_info_dsp dsp;
+ struct spa_video_info_h264 h264;
+ struct spa_video_info_mjpg mjpg;
+ } info;
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PARAM_VIDEO_FORMAT_H */
diff --git a/spa/include/spa/param/video/h264-utils.h b/spa/include/spa/param/video/h264-utils.h
new file mode 100644
index 0000000..c105965
--- /dev/null
+++ b/spa/include/spa/param/video/h264-utils.h
@@ -0,0 +1,90 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_H264_UTILS_H
+#define SPA_VIDEO_H264_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/video/h264.h>
+
+static inline int
+spa_format_video_h264_parse(const struct spa_pod *format,
+ struct spa_video_info_h264 *info)
+{
+ return spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Rectangle(&info->size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_OPT_Fraction(&info->framerate),
+ SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_OPT_Fraction(&info->max_framerate),
+ SPA_FORMAT_VIDEO_H264_streamFormat, SPA_POD_OPT_Id(&info->stream_format),
+ SPA_FORMAT_VIDEO_H264_alignment, SPA_POD_OPT_Id(&info->alignment));
+}
+
+static inline struct spa_pod *
+spa_format_video_h264_build(struct spa_pod_builder *builder, uint32_t id,
+ struct spa_video_info_h264 *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_h264),
+ 0);
+ if (info->size.width != 0 && info->size.height != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&info->size), 0);
+ if (info->framerate.denom != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&info->framerate), 0);
+ if (info->max_framerate.denom != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(info->max_framerate), 0);
+ if (info->stream_format != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_H264_streamFormat, SPA_POD_Id(info->stream_format), 0);
+ if (info->alignment != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_H264_alignment, SPA_POD_Id(info->alignment), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_H264_UTILS_H */
diff --git a/spa/include/spa/param/video/h264.h b/spa/include/spa/param/video/h264.h
new file mode 100644
index 0000000..a33dda7
--- /dev/null
+++ b/spa/include/spa/param/video/h264.h
@@ -0,0 +1,68 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_H264_H
+#define SPA_VIDEO_H264_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/format.h>
+
+enum spa_h264_stream_format {
+ SPA_H264_STREAM_FORMAT_UNKNOWN = 0,
+ SPA_H264_STREAM_FORMAT_AVC,
+ SPA_H264_STREAM_FORMAT_AVC3,
+ SPA_H264_STREAM_FORMAT_BYTESTREAM
+};
+
+enum spa_h264_alignment {
+ SPA_H264_ALIGNMENT_UNKNOWN = 0,
+ SPA_H264_ALIGNMENT_AU,
+ SPA_H264_ALIGNMENT_NAL
+};
+
+struct spa_video_info_h264 {
+ struct spa_rectangle size;
+ struct spa_fraction framerate;
+ struct spa_fraction max_framerate;
+ enum spa_h264_stream_format stream_format;
+ enum spa_h264_alignment alignment;
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_H264_H */
diff --git a/spa/include/spa/param/video/mjpg-utils.h b/spa/include/spa/param/video/mjpg-utils.h
new file mode 100644
index 0000000..59b3965
--- /dev/null
+++ b/spa/include/spa/param/video/mjpg-utils.h
@@ -0,0 +1,82 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_MJPG_UTILS_H
+#define SPA_VIDEO_MJPG_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/video/mjpg.h>
+
+static inline int
+spa_format_video_mjpg_parse(const struct spa_pod *format,
+ struct spa_video_info_mjpg *info)
+{
+ return spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Rectangle(&info->size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_OPT_Fraction(&info->framerate),
+ SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_OPT_Fraction(&info->max_framerate));
+}
+
+static inline struct spa_pod *
+spa_format_video_mjpg_build(struct spa_pod_builder *builder, uint32_t id,
+ struct spa_video_info_mjpg *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ 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 (info->size.width != 0 && info->size.height != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&info->size), 0);
+ if (info->framerate.denom != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&info->framerate), 0);
+ if (info->max_framerate.denom != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(info->max_framerate), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_MJPG_UTILS_H */
diff --git a/spa/include/spa/param/video/mjpg.h b/spa/include/spa/param/video/mjpg.h
new file mode 100644
index 0000000..d5d91d4
--- /dev/null
+++ b/spa/include/spa/param/video/mjpg.h
@@ -0,0 +1,53 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_MJPG_H
+#define SPA_VIDEO_MJPG_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/format.h>
+
+struct spa_video_info_mjpg {
+ struct spa_rectangle size;
+ struct spa_fraction framerate;
+ struct spa_fraction max_framerate;
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_MJPG_H */
diff --git a/spa/include/spa/param/video/multiview.h b/spa/include/spa/param/video/multiview.h
new file mode 100644
index 0000000..ea16da8
--- /dev/null
+++ b/spa/include/spa/param/video/multiview.h
@@ -0,0 +1,134 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_MULTIVIEW_H
+#define SPA_VIDEO_MULTIVIEW_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+/**
+ * All possible stereoscopic 3D and multiview representations.
+ * In conjunction with \ref spa_video_multiview_flags, describes how
+ * multiview content is being transported in the stream.
+ */
+enum spa_video_multiview_mode {
+ /** A special value indicating no multiview information. Used in spa_video_info and other
+ * places to indicate that no specific multiview handling has been requested or provided.
+ * This value is never carried on caps. */
+ SPA_VIDEO_MULTIVIEW_MODE_NONE = -1,
+ SPA_VIDEO_MULTIVIEW_MODE_MONO = 0, /**< All frames are monoscopic */
+ /* Single view modes */
+ SPA_VIDEO_MULTIVIEW_MODE_LEFT, /**< All frames represent a left-eye view */
+ SPA_VIDEO_MULTIVIEW_MODE_RIGHT, /**< All frames represent a right-eye view */
+ /* Stereo view modes */
+ SPA_VIDEO_MULTIVIEW_MODE_SIDE_BY_SIDE, /**< Left and right eye views are provided
+ * in the left and right half of the frame
+ * respectively. */
+ SPA_VIDEO_MULTIVIEW_MODE_SIDE_BY_SIDE_QUINCUNX, /**< Left and right eye views are provided
+ * in the left and right half of the
+ * frame, but have been sampled using
+ * quincunx method, with half-pixel offset
+ * between the 2 views. */
+ SPA_VIDEO_MULTIVIEW_MODE_COLUMN_INTERLEAVED, /**< Alternating vertical columns of pixels
+ * represent the left and right eye view
+ * respectively. */
+ SPA_VIDEO_MULTIVIEW_MODE_ROW_INTERLEAVED, /**< Alternating horizontal rows of pixels
+ * represent the left and right eye view
+ * respectively. */
+ SPA_VIDEO_MULTIVIEW_MODE_TOP_BOTTOM, /**< The top half of the frame contains the
+ * left eye, and the bottom half the right
+ * eye. */
+ SPA_VIDEO_MULTIVIEW_MODE_CHECKERBOARD, /**< Pixels are arranged with alternating
+ * pixels representing left and right eye
+ * views in a checkerboard fashion. */
+ /* Padding for new frame packing modes */
+
+ SPA_VIDEO_MULTIVIEW_MODE_FRAME_BY_FRAME = 32, /**< Left and right eye views are provided
+ * in separate frames alternately. */
+ /* Multiview mode(s) */
+ SPA_VIDEO_MULTIVIEW_MODE_MULTIVIEW_FRAME_BY_FRAME, /**< Multipleindependent views are
+ * provided in separate frames in
+ * sequence. This method only applies to
+ * raw video buffers at the moment.
+ * Specific view identification is via
+ * \ref spa_video_multiview_meta on raw
+ * video buffers. */
+ SPA_VIDEO_MULTIVIEW_MODE_SEPARATED, /**< Multiple views are provided as separate
+ * \ref spa_data framebuffers attached
+ * to each \ref spa_buffer, described
+ * by the \ref spa_video_multiview_meta */
+ /* future expansion for annotated modes */
+};
+
+/**
+ * spa_video_multiview_flags are used to indicate extra properties of a
+ * stereo/multiview stream beyond the frame layout and buffer mapping
+ * that is conveyed in the \ref spa_video_multiview_mode.
+ */
+enum spa_video_multiview_flags {
+ SPA_VIDEO_MULTIVIEW_FLAGS_NONE = 0, /**< No flags */
+ SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_VIEW_FIRST = (1 << 0), /**< For stereo streams, the normal arrangement
+ * of left and right views is reversed */
+ SPA_VIDEO_MULTIVIEW_FLAGS_LEFT_FLIPPED = (1 << 1), /**< The left view is vertically mirrored */
+ SPA_VIDEO_MULTIVIEW_FLAGS_LEFT_FLOPPED = (1 << 2), /**< The left view is horizontally mirrored */
+ SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_FLIPPED = (1 << 3), /**< The right view is vertically mirrored */
+ SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_FLOPPED = (1 << 4), /**< The right view is horizontally mirrored */
+ SPA_VIDEO_MULTIVIEW_FLAGS_HALF_ASPECT = (1 << 14), /**< For frame-packed multiview
+ * modes, indicates that the individual
+ * views have been encoded with half the true
+ * width or height and should be scaled back
+ * up for display. This flag is used for
+ * overriding input layout interpretation
+ * by adjusting pixel-aspect-ratio.
+ * For side-by-side, column interleaved or
+ * checkerboard packings, the
+ * pixel width will be doubled.
+ * For row interleaved and
+ * top-bottom encodings, pixel height will
+ * be doubled */
+ SPA_VIDEO_MULTIVIEW_FLAGS_MIXED_MONO = (1 << 15), /**< The video stream contains both
+ * mono and multiview portions,
+ * signalled on each buffer by the
+ * absence or presence of the
+ * \ref SPA_VIDEO_BUFFER_FLAG_MULTIPLE_VIEW
+ * buffer flag. */
+};
+
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_MULTIVIEW_H */
diff --git a/spa/include/spa/param/video/raw-types.h b/spa/include/spa/param/video/raw-types.h
new file mode 100644
index 0000000..45fc230
--- /dev/null
+++ b/spa/include/spa/param/video/raw-types.h
@@ -0,0 +1,164 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_RAW_TYPES_H
+#define SPA_VIDEO_RAW_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+#include <spa/utils/type.h>
+#include <spa/param/video/raw.h>
+
+#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_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 },
+ { SPA_VIDEO_FORMAT_UYVY, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVY", NULL },
+ { SPA_VIDEO_FORMAT_AYUV, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV", NULL },
+ { SPA_VIDEO_FORMAT_RGBx, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx", NULL },
+ { SPA_VIDEO_FORMAT_BGRx, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx", NULL },
+ { SPA_VIDEO_FORMAT_xRGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB", NULL },
+ { SPA_VIDEO_FORMAT_xBGR, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR", NULL },
+ { SPA_VIDEO_FORMAT_RGBA, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA", NULL },
+ { SPA_VIDEO_FORMAT_BGRA, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA", NULL },
+ { SPA_VIDEO_FORMAT_ARGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB", NULL },
+ { SPA_VIDEO_FORMAT_ABGR, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR", NULL },
+ { SPA_VIDEO_FORMAT_RGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB", NULL },
+ { SPA_VIDEO_FORMAT_BGR, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR", NULL },
+ { SPA_VIDEO_FORMAT_Y41B, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y41B", NULL },
+ { SPA_VIDEO_FORMAT_Y42B, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y42B", NULL },
+ { SPA_VIDEO_FORMAT_YVYU, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVYU", NULL },
+ { SPA_VIDEO_FORMAT_Y444, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444", NULL },
+ { SPA_VIDEO_FORMAT_v210, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v210", NULL },
+ { SPA_VIDEO_FORMAT_v216, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v216", NULL },
+ { SPA_VIDEO_FORMAT_NV12, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12", NULL },
+ { SPA_VIDEO_FORMAT_NV21, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV21", NULL },
+ { SPA_VIDEO_FORMAT_GRAY8, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY8", NULL },
+ { SPA_VIDEO_FORMAT_GRAY16_BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_BE", NULL },
+ { SPA_VIDEO_FORMAT_GRAY16_LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_LE", NULL },
+ { SPA_VIDEO_FORMAT_v308, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v308", NULL },
+ { SPA_VIDEO_FORMAT_RGB16, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB16", NULL },
+ { SPA_VIDEO_FORMAT_BGR16, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR16", NULL },
+ { SPA_VIDEO_FORMAT_RGB15, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB15", NULL },
+ { SPA_VIDEO_FORMAT_BGR15, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR15", NULL },
+ { SPA_VIDEO_FORMAT_UYVP, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVP", NULL },
+ { SPA_VIDEO_FORMAT_A420, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420", NULL },
+ { SPA_VIDEO_FORMAT_RGB8P, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB8P", NULL },
+ { SPA_VIDEO_FORMAT_YUV9, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUV9", NULL },
+ { SPA_VIDEO_FORMAT_YVU9, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVU9", NULL },
+ { SPA_VIDEO_FORMAT_IYU1, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU1", NULL },
+ { SPA_VIDEO_FORMAT_ARGB64, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB64", NULL },
+ { SPA_VIDEO_FORMAT_AYUV64, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV64", NULL },
+ { SPA_VIDEO_FORMAT_r210, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "r210", NULL },
+ { SPA_VIDEO_FORMAT_I420_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10BE", NULL },
+ { SPA_VIDEO_FORMAT_I420_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10LE", NULL },
+ { SPA_VIDEO_FORMAT_I422_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10BE", NULL },
+ { SPA_VIDEO_FORMAT_I422_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10LE", NULL },
+ { SPA_VIDEO_FORMAT_Y444_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10BE", NULL },
+ { SPA_VIDEO_FORMAT_Y444_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10LE", NULL },
+ { SPA_VIDEO_FORMAT_GBR, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR", NULL },
+ { SPA_VIDEO_FORMAT_GBR_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10BE", NULL },
+ { SPA_VIDEO_FORMAT_GBR_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10LE", NULL },
+ { SPA_VIDEO_FORMAT_NV16, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV16", NULL },
+ { SPA_VIDEO_FORMAT_NV24, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV24", NULL },
+ { SPA_VIDEO_FORMAT_NV12_64Z32, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12_64Z32", NULL },
+ { SPA_VIDEO_FORMAT_A420_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10BE", NULL },
+ { SPA_VIDEO_FORMAT_A420_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10LE", NULL },
+ { SPA_VIDEO_FORMAT_A422_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10BE", NULL },
+ { SPA_VIDEO_FORMAT_A422_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10LE", NULL },
+ { SPA_VIDEO_FORMAT_A444_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10BE", NULL },
+ { SPA_VIDEO_FORMAT_A444_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10LE", NULL },
+ { SPA_VIDEO_FORMAT_NV61, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV61", NULL },
+ { SPA_VIDEO_FORMAT_P010_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10BE", NULL },
+ { SPA_VIDEO_FORMAT_P010_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10LE", NULL },
+ { SPA_VIDEO_FORMAT_IYU2, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU2", NULL },
+ { SPA_VIDEO_FORMAT_VYUY, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "VYUY", NULL },
+ { SPA_VIDEO_FORMAT_GBRA, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA", NULL },
+ { SPA_VIDEO_FORMAT_GBRA_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10BE", NULL },
+ { SPA_VIDEO_FORMAT_GBRA_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10LE", NULL },
+ { SPA_VIDEO_FORMAT_GBR_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12BE", NULL },
+ { SPA_VIDEO_FORMAT_GBR_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12LE", NULL },
+ { SPA_VIDEO_FORMAT_GBRA_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12BE", NULL },
+ { SPA_VIDEO_FORMAT_GBRA_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12LE", NULL },
+ { SPA_VIDEO_FORMAT_I420_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12BE", NULL },
+ { SPA_VIDEO_FORMAT_I420_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12LE", NULL },
+ { SPA_VIDEO_FORMAT_I422_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12BE", NULL },
+ { SPA_VIDEO_FORMAT_I422_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12LE", NULL },
+ { SPA_VIDEO_FORMAT_Y444_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12BE", NULL },
+ { SPA_VIDEO_FORMAT_Y444_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12LE", NULL },
+ { SPA_VIDEO_FORMAT_RGBA_F16, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_F16", NULL },
+ { SPA_VIDEO_FORMAT_RGBA_F32, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_F32", NULL },
+ { SPA_VIDEO_FORMAT_xRGB_210LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB_210LE", NULL },
+ { SPA_VIDEO_FORMAT_xBGR_210LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR_210LE", NULL },
+ { SPA_VIDEO_FORMAT_RGBx_102LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx_102LE", NULL },
+ { SPA_VIDEO_FORMAT_BGRx_102LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx_102LE", NULL },
+ { SPA_VIDEO_FORMAT_ARGB_210LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB_210LE", NULL },
+ { SPA_VIDEO_FORMAT_ABGR_210LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR_210LE", NULL },
+ { SPA_VIDEO_FORMAT_RGBA_102LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_102LE", NULL },
+ { SPA_VIDEO_FORMAT_BGRA_102LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA_102LE", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_VideoFlags SPA_TYPE_INFO_FLAGS_BASE "VideoFlags"
+#define SPA_TYPE_INFO_VIDEO_FLAGS_BASE SPA_TYPE_INFO_VideoFlags ":"
+
+static const struct spa_type_info spa_type_video_flags[] = {
+
+ { SPA_VIDEO_FLAG_NONE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FLAGS_BASE "none", NULL },
+ { SPA_VIDEO_FLAG_VARIABLE_FPS, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FLAGS_BASE "variable-fps", NULL },
+ { SPA_VIDEO_FLAG_PREMULTIPLIED_ALPHA, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FLAGS_BASE "premultiplied-alpha", NULL },
+ { SPA_VIDEO_FLAG_MODIFIER, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FLAGS_BASE "modifier", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+#define SPA_TYPE_INFO_VideoInterlaceMode SPA_TYPE_INFO_ENUM_BASE "VideoInterlaceMode"
+#define SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE SPA_TYPE_INFO_VideoInterlaceMode ":"
+
+static const struct spa_type_info spa_type_video_interlace_mode[] = {
+ { SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE "progressive", NULL },
+ { SPA_VIDEO_INTERLACE_MODE_INTERLEAVED, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE "interleaved", NULL },
+ { SPA_VIDEO_INTERLACE_MODE_MIXED, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE "mixed", NULL },
+ { SPA_VIDEO_INTERLACE_MODE_FIELDS, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE "fields", NULL },
+ { 0, 0, NULL, NULL },
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_RAW_TYPES_H */
diff --git a/spa/include/spa/param/video/raw-utils.h b/spa/include/spa/param/video/raw-utils.h
new file mode 100644
index 0000000..f48fcb9
--- /dev/null
+++ b/spa/include/spa/param/video/raw-utils.h
@@ -0,0 +1,135 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_RAW_UTILS_H
+#define SPA_VIDEO_RAW_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/param/video/raw.h>
+
+static inline int
+spa_format_video_raw_parse(const struct spa_pod *format,
+ struct spa_video_info_raw *info)
+{
+ info->flags = SPA_VIDEO_FLAG_NONE;
+ if (spa_pod_find_prop (format, NULL, SPA_FORMAT_VIDEO_modifier)) {
+ info->flags |= SPA_VIDEO_FLAG_MODIFIER;
+ }
+
+ return spa_pod_parse_object(format,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_VIDEO_format, SPA_POD_OPT_Id(&info->format),
+ SPA_FORMAT_VIDEO_modifier, SPA_POD_OPT_Long(&info->modifier),
+ SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Rectangle(&info->size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_OPT_Fraction(&info->framerate),
+ SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_OPT_Fraction(&info->max_framerate),
+ SPA_FORMAT_VIDEO_views, SPA_POD_OPT_Int(&info->views),
+ SPA_FORMAT_VIDEO_interlaceMode, SPA_POD_OPT_Id(&info->interlace_mode),
+ SPA_FORMAT_VIDEO_pixelAspectRatio, SPA_POD_OPT_Fraction(&info->pixel_aspect_ratio),
+ SPA_FORMAT_VIDEO_multiviewMode, SPA_POD_OPT_Id(&info->multiview_mode),
+ SPA_FORMAT_VIDEO_multiviewFlags, SPA_POD_OPT_Id(&info->multiview_flags),
+ SPA_FORMAT_VIDEO_chromaSite, SPA_POD_OPT_Id(&info->chroma_site),
+ SPA_FORMAT_VIDEO_colorRange, SPA_POD_OPT_Id(&info->color_range),
+ SPA_FORMAT_VIDEO_colorMatrix, SPA_POD_OPT_Id(&info->color_matrix),
+ SPA_FORMAT_VIDEO_transferFunction, SPA_POD_OPT_Id(&info->transfer_function),
+ SPA_FORMAT_VIDEO_colorPrimaries, SPA_POD_OPT_Id(&info->color_primaries));
+}
+
+static inline struct spa_pod *
+spa_format_video_raw_build(struct spa_pod_builder *builder, uint32_t id,
+ struct spa_video_info_raw *info)
+{
+ struct spa_pod_frame f;
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id);
+ 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),
+ 0);
+ if (info->format != SPA_VIDEO_FORMAT_UNKNOWN)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(info->format), 0);
+ if (info->size.width != 0 && info->size.height != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&info->size), 0);
+ if (info->framerate.denom != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&info->framerate), 0);
+ if (info->modifier != 0 || info->flags & SPA_VIDEO_FLAG_MODIFIER)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_modifier, SPA_POD_Long(info->modifier), 0);
+ if (info->max_framerate.denom != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(info->max_framerate), 0);
+ if (info->views != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_views, SPA_POD_Int(info->views), 0);
+ if (info->interlace_mode != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_interlaceMode, SPA_POD_Id(info->interlace_mode), 0);
+ if (info->pixel_aspect_ratio.denom != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_pixelAspectRatio,SPA_POD_Fraction(info->pixel_aspect_ratio), 0);
+ if (info->multiview_mode != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_multiviewMode, SPA_POD_Id(info->multiview_mode), 0);
+ if (info->multiview_flags != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_multiviewFlags,SPA_POD_Id(info->multiview_flags), 0);
+ if (info->chroma_site != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_chromaSite, SPA_POD_Id(info->chroma_site), 0);
+ if (info->color_range != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_colorRange, SPA_POD_Id(info->color_range), 0);
+ if (info->color_matrix != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_colorMatrix, SPA_POD_Id(info->color_matrix), 0);
+ if (info->transfer_function != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_transferFunction,SPA_POD_Id(info->transfer_function), 0);
+ if (info->color_primaries != 0)
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_colorPrimaries,SPA_POD_Id(info->color_primaries), 0);
+ return (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_RAW_UTILS_H */
diff --git a/spa/include/spa/param/video/raw.h b/spa/include/spa/param/video/raw.h
new file mode 100644
index 0000000..51c8827
--- /dev/null
+++ b/spa/include/spa/param/video/raw.h
@@ -0,0 +1,221 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_RAW_H
+#define SPA_VIDEO_RAW_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup spa_param
+ * \{
+ */
+
+#include <spa/param/format.h>
+#include <spa/param/video/chroma.h>
+#include <spa/param/video/color.h>
+#include <spa/param/video/multiview.h>
+
+#define SPA_VIDEO_MAX_PLANES 4
+#define SPA_VIDEO_MAX_COMPONENTS 4
+
+/**
+ * Video formats
+ *
+ * The components are in general described in big-endian order. There are some
+ * exceptions (e.g. RGB15 and RGB16) which use the host endianness.
+ *
+ * Most of the formats are identical to their GStreamer equivalent. See the
+ * GStreamer video formats documentation for more details:
+ *
+ * https://gstreamer.freedesktop.org/documentation/additional/design/mediatype-video-raw.html#formats
+ */
+enum spa_video_format {
+ SPA_VIDEO_FORMAT_UNKNOWN,
+ SPA_VIDEO_FORMAT_ENCODED,
+
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_YV12,
+ SPA_VIDEO_FORMAT_YUY2,
+ SPA_VIDEO_FORMAT_UYVY,
+ SPA_VIDEO_FORMAT_AYUV,
+ SPA_VIDEO_FORMAT_RGBx,
+ SPA_VIDEO_FORMAT_BGRx,
+ SPA_VIDEO_FORMAT_xRGB,
+ SPA_VIDEO_FORMAT_xBGR,
+ SPA_VIDEO_FORMAT_RGBA,
+ SPA_VIDEO_FORMAT_BGRA,
+ SPA_VIDEO_FORMAT_ARGB,
+ SPA_VIDEO_FORMAT_ABGR,
+ SPA_VIDEO_FORMAT_RGB,
+ SPA_VIDEO_FORMAT_BGR,
+ SPA_VIDEO_FORMAT_Y41B,
+ SPA_VIDEO_FORMAT_Y42B,
+ SPA_VIDEO_FORMAT_YVYU,
+ SPA_VIDEO_FORMAT_Y444,
+ SPA_VIDEO_FORMAT_v210,
+ SPA_VIDEO_FORMAT_v216,
+ SPA_VIDEO_FORMAT_NV12,
+ SPA_VIDEO_FORMAT_NV21,
+ SPA_VIDEO_FORMAT_GRAY8,
+ SPA_VIDEO_FORMAT_GRAY16_BE,
+ SPA_VIDEO_FORMAT_GRAY16_LE,
+ SPA_VIDEO_FORMAT_v308,
+ SPA_VIDEO_FORMAT_RGB16,
+ SPA_VIDEO_FORMAT_BGR16,
+ SPA_VIDEO_FORMAT_RGB15,
+ SPA_VIDEO_FORMAT_BGR15,
+ SPA_VIDEO_FORMAT_UYVP,
+ SPA_VIDEO_FORMAT_A420,
+ SPA_VIDEO_FORMAT_RGB8P,
+ SPA_VIDEO_FORMAT_YUV9,
+ SPA_VIDEO_FORMAT_YVU9,
+ SPA_VIDEO_FORMAT_IYU1,
+ SPA_VIDEO_FORMAT_ARGB64,
+ SPA_VIDEO_FORMAT_AYUV64,
+ SPA_VIDEO_FORMAT_r210,
+ SPA_VIDEO_FORMAT_I420_10BE,
+ SPA_VIDEO_FORMAT_I420_10LE,
+ SPA_VIDEO_FORMAT_I422_10BE,
+ SPA_VIDEO_FORMAT_I422_10LE,
+ SPA_VIDEO_FORMAT_Y444_10BE,
+ SPA_VIDEO_FORMAT_Y444_10LE,
+ SPA_VIDEO_FORMAT_GBR,
+ SPA_VIDEO_FORMAT_GBR_10BE,
+ SPA_VIDEO_FORMAT_GBR_10LE,
+ SPA_VIDEO_FORMAT_NV16,
+ SPA_VIDEO_FORMAT_NV24,
+ SPA_VIDEO_FORMAT_NV12_64Z32,
+ SPA_VIDEO_FORMAT_A420_10BE,
+ SPA_VIDEO_FORMAT_A420_10LE,
+ SPA_VIDEO_FORMAT_A422_10BE,
+ SPA_VIDEO_FORMAT_A422_10LE,
+ SPA_VIDEO_FORMAT_A444_10BE,
+ SPA_VIDEO_FORMAT_A444_10LE,
+ SPA_VIDEO_FORMAT_NV61,
+ SPA_VIDEO_FORMAT_P010_10BE,
+ SPA_VIDEO_FORMAT_P010_10LE,
+ SPA_VIDEO_FORMAT_IYU2,
+ SPA_VIDEO_FORMAT_VYUY,
+ SPA_VIDEO_FORMAT_GBRA,
+ SPA_VIDEO_FORMAT_GBRA_10BE,
+ SPA_VIDEO_FORMAT_GBRA_10LE,
+ SPA_VIDEO_FORMAT_GBR_12BE,
+ SPA_VIDEO_FORMAT_GBR_12LE,
+ SPA_VIDEO_FORMAT_GBRA_12BE,
+ SPA_VIDEO_FORMAT_GBRA_12LE,
+ SPA_VIDEO_FORMAT_I420_12BE,
+ SPA_VIDEO_FORMAT_I420_12LE,
+ SPA_VIDEO_FORMAT_I422_12BE,
+ SPA_VIDEO_FORMAT_I422_12LE,
+ SPA_VIDEO_FORMAT_Y444_12BE,
+ SPA_VIDEO_FORMAT_Y444_12LE,
+
+ SPA_VIDEO_FORMAT_RGBA_F16,
+ SPA_VIDEO_FORMAT_RGBA_F32,
+
+ SPA_VIDEO_FORMAT_xRGB_210LE, /**< 32-bit x:R:G:B 2:10:10:10 little endian */
+ SPA_VIDEO_FORMAT_xBGR_210LE, /**< 32-bit x:B:G:R 2:10:10:10 little endian */
+ SPA_VIDEO_FORMAT_RGBx_102LE, /**< 32-bit R:G:B:x 10:10:10:2 little endian */
+ SPA_VIDEO_FORMAT_BGRx_102LE, /**< 32-bit B:G:R:x 10:10:10:2 little endian */
+ SPA_VIDEO_FORMAT_ARGB_210LE, /**< 32-bit A:R:G:B 2:10:10:10 little endian */
+ SPA_VIDEO_FORMAT_ABGR_210LE, /**< 32-bit A:B:G:R 2:10:10:10 little endian */
+ SPA_VIDEO_FORMAT_RGBA_102LE, /**< 32-bit R:G:B:A 10:10:10:2 little endian */
+ SPA_VIDEO_FORMAT_BGRA_102LE, /**< 32-bit B:G:R:A 10:10:10:2 little endian */
+
+ /* Aliases */
+ SPA_VIDEO_FORMAT_DSP_F32 = SPA_VIDEO_FORMAT_RGBA_F32,
+};
+
+/**
+ * Extra video flags
+ */
+enum spa_video_flags {
+ SPA_VIDEO_FLAG_NONE = 0, /**< no flags */
+ SPA_VIDEO_FLAG_VARIABLE_FPS = (1 << 0), /**< a variable fps is selected, fps_n and fps_d
+ * denote the maximum fps of the video */
+ SPA_VIDEO_FLAG_PREMULTIPLIED_ALPHA = (1 << 1), /**< Each color has been scaled by the alpha value. */
+ SPA_VIDEO_FLAG_MODIFIER = (1 << 2), /**< use the format modifier */
+};
+
+/**
+ * The possible values of the #spa_video_interlace_mode describing the interlace
+ * mode of the stream.
+ */
+enum spa_video_interlace_mode {
+ SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE = 0, /**< all frames are progressive */
+ SPA_VIDEO_INTERLACE_MODE_INTERLEAVED, /**< 2 fields are interleaved in one video frame.
+ * Extra buffer flags describe the field order. */
+ SPA_VIDEO_INTERLACE_MODE_MIXED, /**< frames contains both interlaced and progressive
+ * video, the buffer flags describe the frame and
+ * fields. */
+ SPA_VIDEO_INTERLACE_MODE_FIELDS, /**< 2 fields are stored in one buffer, use the
+ * frame ID to get access to the required
+ * field. For multiview (the 'views'
+ * property > 1) the fields of view N can
+ * be found at frame ID (N * 2) and (N *
+ * 2) + 1. Each field has only half the
+ * amount of lines as noted in the height
+ * property. This mode requires multiple
+ * spa_data to describe the fields. */
+};
+
+/**
+ */
+struct spa_video_info_raw {
+ enum spa_video_format format; /**< the format */
+ uint32_t flags; /**< extra video flags */
+ uint64_t modifier; /**< format modifier
+ * only used with DMA-BUF */
+ struct spa_rectangle size; /**< the frame size of the video */
+ struct spa_fraction framerate; /**< the framerate of the video, 0/1 means variable rate */
+ struct spa_fraction max_framerate; /**< the maximum framerate of the video. This is only valid when
+ \ref framerate is 0/1 */
+ uint32_t views; /**< the number of views in this video */
+ enum spa_video_interlace_mode interlace_mode; /**< the interlace mode */
+ struct spa_fraction pixel_aspect_ratio; /**< the pixel aspect ratio */
+ enum spa_video_multiview_mode multiview_mode; /**< multiview mode */
+ enum spa_video_multiview_flags multiview_flags; /**< multiview flags */
+ enum spa_video_chroma_site chroma_site; /**< the chroma siting */
+ enum spa_video_color_range color_range; /**< the color range. This is the valid range for the samples.
+ * It is used to convert the samples to Y'PbPr values. */
+ enum spa_video_color_matrix color_matrix; /**< the color matrix. Used to convert between Y'PbPr and
+ * non-linear RGB (R'G'B') */
+ enum spa_video_transfer_function transfer_function; /**< the transfer function. used to convert between R'G'B' and RGB */
+ enum spa_video_color_primaries color_primaries; /**< color primaries. used to convert between R'G'B' and CIE XYZ */
+};
+
+#define SPA_VIDEO_INFO_RAW_INIT(...) ((struct spa_video_info_raw) { __VA_ARGS__ })
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_VIDEO_RAW_H */
diff --git a/spa/include/spa/param/video/type-info.h b/spa/include/spa/param/video/type-info.h
new file mode 100644
index 0000000..0bc0f24
--- /dev/null
+++ b/spa/include/spa/param/video/type-info.h
@@ -0,0 +1,30 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_VIDEO_TYPES_H
+#define SPA_VIDEO_TYPES_H
+
+#include <spa/param/video/raw-types.h>
+
+#endif /* SPA_VIDEO_TYPES_H */
diff --git a/spa/include/spa/pod/builder.h b/spa/include/spa/pod/builder.h
new file mode 100644
index 0000000..860673a
--- /dev/null
+++ b/spa/include/spa/pod/builder.h
@@ -0,0 +1,701 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_POD_BUILDER_H
+#define SPA_POD_BUILDER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup spa_pod POD
+ * Binary data serialization format
+ */
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+#include <stdarg.h>
+
+#include <spa/utils/hook.h>
+#include <spa/pod/iter.h>
+#include <spa/pod/vararg.h>
+
+struct spa_pod_builder_state {
+ uint32_t offset;
+#define SPA_POD_BUILDER_FLAG_BODY (1<<0)
+#define SPA_POD_BUILDER_FLAG_FIRST (1<<1)
+ uint32_t flags;
+ struct spa_pod_frame *frame;
+};
+
+struct spa_pod_builder;
+
+struct spa_pod_builder_callbacks {
+#define SPA_VERSION_POD_BUILDER_CALLBACKS 0
+ uint32_t version;
+
+ int (*overflow) (void *data, uint32_t size);
+};
+
+struct spa_pod_builder {
+ void *data;
+ uint32_t size;
+ uint32_t _padding;
+ struct spa_pod_builder_state state;
+ struct spa_callbacks callbacks;
+};
+
+#define SPA_POD_BUILDER_INIT(buffer,size) ((struct spa_pod_builder){ (buffer), (size), 0, {}, {} })
+
+static inline void
+spa_pod_builder_get_state(struct spa_pod_builder *builder, struct spa_pod_builder_state *state)
+{
+ *state = builder->state;
+}
+
+static inline 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_pod_builder_reset(struct spa_pod_builder *builder, struct spa_pod_builder_state *state)
+{
+ struct spa_pod_frame *f;
+ uint32_t size = builder->state.offset - state->offset;
+ builder->state = *state;
+ for (f = builder->state.frame; f ; f = f->parent)
+ f->pod.size -= size;
+}
+
+static inline 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_pod_builder_deref(struct spa_pod_builder *builder, uint32_t offset)
+{
+ uint32_t size = builder->size;
+ if (offset + 8 <= size) {
+ struct spa_pod *pod = SPA_PTROFF(builder->data, offset, struct spa_pod);
+ if (offset + SPA_POD_SIZE(pod) <= size)
+ return pod;
+ }
+ return NULL;
+}
+
+static inline 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)
+ return SPA_PTROFF(builder->data, frame->offset, struct spa_pod);
+ return NULL;
+}
+
+static inline void
+spa_pod_builder_push(struct spa_pod_builder *builder,
+ struct spa_pod_frame *frame,
+ const struct spa_pod *pod,
+ uint32_t offset)
+{
+ frame->pod = *pod;
+ frame->offset = offset;
+ frame->parent = builder->state.frame;
+ frame->flags = builder->state.flags;
+ builder->state.frame = frame;
+
+ if (frame->pod.type == SPA_TYPE_Array || frame->pod.type == SPA_TYPE_Choice)
+ 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)
+{
+ int res = 0;
+ struct spa_pod_frame *f;
+ uint32_t offset = builder->state.offset;
+
+ if (offset + size > builder->size) {
+ 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)
+ memcpy(SPA_PTROFF(builder->data, offset, void), data, size);
+
+ builder->state.offset += size;
+
+ for (f = builder->state.frame; f ; f = f->parent)
+ f->pod.size += size;
+
+ return res;
+}
+
+static inline 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_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);
+ if ((r = spa_pod_builder_pad(builder, size)) < 0)
+ res = r;
+ return res;
+}
+
+static inline void *spa_pod_builder_pop(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
+{
+ struct spa_pod *pod;
+
+ if (SPA_FLAG_IS_SET(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST)) {
+ const struct spa_pod p = { 0, SPA_TYPE_None };
+ spa_pod_builder_raw(builder, &p, sizeof(p));
+ }
+ if ((pod = (struct spa_pod*)spa_pod_builder_frame(builder, frame)) != NULL)
+ *pod = frame->pod;
+
+ builder->state.frame = frame->parent;
+ builder->state.flags = frame->flags;
+ spa_pod_builder_pad(builder, builder->state.offset);
+ return pod;
+}
+
+static inline int
+spa_pod_builder_primitive(struct spa_pod_builder *builder, const struct spa_pod *p)
+{
+ const void *data;
+ uint32_t size;
+ int r, res;
+
+ if (builder->state.flags == SPA_POD_BUILDER_FLAG_BODY) {
+ data = SPA_POD_BODY_CONST(p);
+ size = SPA_POD_BODY_SIZE(p);
+ } else {
+ data = p;
+ size = SPA_POD_SIZE(p);
+ SPA_FLAG_CLEAR(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST);
+ }
+ res = spa_pod_builder_raw(builder, data, size);
+ if (builder->state.flags != SPA_POD_BUILDER_FLAG_BODY)
+ if ((r = spa_pod_builder_pad(builder, size)) < 0)
+ res = r;
+ return res;
+}
+
+#define SPA_POD_INIT(size,type) ((struct spa_pod) { (size), (type) })
+
+#define SPA_POD_INIT_None() SPA_POD_INIT(0, SPA_TYPE_None)
+
+static inline 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)
+{
+ const struct spa_pod p = SPA_POD_INIT(size,type);
+ SPA_FLAG_CLEAR(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST);
+ return spa_pod_builder_raw(builder, &p, sizeof(p));
+}
+
+#define SPA_POD_INIT_Bool(val) ((struct spa_pod_bool){ { sizeof(uint32_t), SPA_TYPE_Bool }, (val) ? 1 : 0, 0 })
+
+static inline 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);
+}
+
+#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)
+{
+ const struct spa_pod_id p = SPA_POD_INIT_Id(val);
+ return spa_pod_builder_primitive(builder, &p.pod);
+}
+
+#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)
+{
+ const struct spa_pod_int p = SPA_POD_INIT_Int(val);
+ return spa_pod_builder_primitive(builder, &p.pod);
+}
+
+#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)
+{
+ const struct spa_pod_long p = SPA_POD_INIT_Long(val);
+ return spa_pod_builder_primitive(builder, &p.pod);
+}
+
+#define SPA_POD_INIT_Float(val) ((struct spa_pod_float){ { sizeof(float), SPA_TYPE_Float }, (val), 0 })
+
+static inline 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);
+}
+
+#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)
+{
+ const struct spa_pod_double p = SPA_POD_INIT_Double(val);
+ return spa_pod_builder_primitive(builder, &p.pod);
+}
+
+#define SPA_POD_INIT_String(len) ((struct spa_pod_string){ { (len), SPA_TYPE_String } })
+
+static inline int
+spa_pod_builder_write_string(struct spa_pod_builder *builder, const char *str, uint32_t len)
+{
+ int r, res;
+ res = spa_pod_builder_raw(builder, str, len);
+ if ((r = spa_pod_builder_raw(builder, "", 1)) < 0)
+ res = r;
+ if ((r = spa_pod_builder_pad(builder, builder->state.offset)) < 0)
+ res = r;
+ return res;
+}
+
+static inline 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);
+ int r, res = spa_pod_builder_raw(builder, &p, sizeof(p));
+ if ((r = spa_pod_builder_write_string(builder, str, len)) < 0)
+ res = r;
+ return res;
+}
+
+static inline 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);
+}
+
+#define SPA_POD_INIT_Bytes(len) ((struct spa_pod_bytes){ { (len), SPA_TYPE_Bytes } })
+
+static inline int
+spa_pod_builder_bytes(struct spa_pod_builder *builder, const void *bytes, uint32_t len)
+{
+ const struct spa_pod_bytes p = SPA_POD_INIT_Bytes(len);
+ int r, res = spa_pod_builder_raw(builder, &p, sizeof(p));
+ if ((r = spa_pod_builder_raw_padded(builder, bytes, len)) < 0)
+ res = r;
+ return res;
+}
+static inline void *
+spa_pod_builder_reserve_bytes(struct spa_pod_builder *builder, uint32_t len)
+{
+ uint32_t offset = builder->state.offset;
+ if (spa_pod_builder_bytes(builder, NULL, len) < 0)
+ return NULL;
+ return SPA_POD_BODY(spa_pod_builder_deref(builder, offset));
+}
+
+#define SPA_POD_INIT_Pointer(type,value) ((struct spa_pod_pointer){ { sizeof(struct spa_pod_pointer_body), SPA_TYPE_Pointer }, { (type), 0, (value) } })
+
+static inline 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);
+ return spa_pod_builder_primitive(builder, &p.pod);
+}
+
+#define SPA_POD_INIT_Fd(fd) ((struct spa_pod_fd){ { sizeof(int64_t), SPA_TYPE_Fd }, (fd) })
+
+static inline 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);
+}
+
+#define SPA_POD_INIT_Rectangle(val) ((struct spa_pod_rectangle){ { sizeof(struct spa_rectangle), SPA_TYPE_Rectangle }, (val) })
+
+static inline int
+spa_pod_builder_rectangle(struct spa_pod_builder *builder, uint32_t width, uint32_t height)
+{
+ const struct spa_pod_rectangle p = SPA_POD_INIT_Rectangle(SPA_RECTANGLE(width, height));
+ return spa_pod_builder_primitive(builder, &p.pod);
+}
+
+#define SPA_POD_INIT_Fraction(val) ((struct spa_pod_fraction){ { sizeof(struct spa_fraction), SPA_TYPE_Fraction }, (val) })
+
+static inline 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_pod_builder_push_array(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
+{
+ const struct spa_pod_array p =
+ { {sizeof(struct spa_pod_array_body) - sizeof(struct spa_pod), SPA_TYPE_Array},
+ {{0, 0}} };
+ uint32_t offset = builder->state.offset;
+ int res = spa_pod_builder_raw(builder, &p, sizeof(p) - sizeof(struct spa_pod));
+ spa_pod_builder_push(builder, frame, &p.pod, offset);
+ return res;
+}
+
+static inline 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)
+{
+ const struct spa_pod_array p = {
+ {(uint32_t)(sizeof(struct spa_pod_array_body) + n_elems * child_size), SPA_TYPE_Array},
+ {{child_size, child_type}}
+ };
+ int r, res = spa_pod_builder_raw(builder, &p, sizeof(p));
+ if ((r = spa_pod_builder_raw_padded(builder, elems, child_size * n_elems)) < 0)
+ res = r;
+ return res;
+}
+
+#define SPA_POD_INIT_CHOICE_BODY(type, flags, child_size, child_type) \
+ ((struct spa_pod_choice_body) { (type), (flags), { (child_size), (child_type) }})
+
+#define SPA_POD_INIT_Choice(type, ctype, child_type, n_vals, ...) \
+ ((struct { struct spa_pod_choice choice; ctype vals[(n_vals)];}) \
+ { { { (n_vals) * sizeof(ctype) + sizeof(struct spa_pod_choice_body), SPA_TYPE_Choice }, \
+ { (type), 0, { sizeof(ctype), (child_type) } } }, { __VA_ARGS__ } })
+
+static inline int
+spa_pod_builder_push_choice(struct spa_pod_builder *builder, struct spa_pod_frame *frame,
+ uint32_t type, uint32_t flags)
+{
+ const struct spa_pod_choice p =
+ { {sizeof(struct spa_pod_choice_body) - sizeof(struct spa_pod), SPA_TYPE_Choice},
+ { type, flags, {0, 0}} };
+ uint32_t offset = builder->state.offset;
+ int res = spa_pod_builder_raw(builder, &p, sizeof(p) - sizeof(struct spa_pod));
+ spa_pod_builder_push(builder, frame, &p.pod, offset);
+ return res;
+}
+
+#define SPA_POD_INIT_Struct(size) ((struct spa_pod_struct){ { (size), SPA_TYPE_Struct } })
+
+static inline int
+spa_pod_builder_push_struct(struct spa_pod_builder *builder, struct spa_pod_frame *frame)
+{
+ const struct spa_pod_struct p = SPA_POD_INIT_Struct(0);
+ uint32_t offset = builder->state.offset;
+ int res = spa_pod_builder_raw(builder, &p, sizeof(p));
+ spa_pod_builder_push(builder, frame, &p.pod, offset);
+ return res;
+}
+
+#define SPA_POD_INIT_Object(size,type,id,...) ((struct spa_pod_object){ { (size), SPA_TYPE_Object }, { (type), (id) }, ##__VA_ARGS__ })
+
+static inline int
+spa_pod_builder_push_object(struct spa_pod_builder *builder, struct spa_pod_frame *frame,
+ uint32_t type, uint32_t id)
+{
+ const struct spa_pod_object p =
+ SPA_POD_INIT_Object(sizeof(struct spa_pod_object_body), type, id);
+ uint32_t offset = builder->state.offset;
+ int res = spa_pod_builder_raw(builder, &p, sizeof(p));
+ spa_pod_builder_push(builder, frame, &p.pod, offset);
+ return res;
+}
+
+#define SPA_POD_INIT_Prop(key,flags,size,type) \
+ ((struct spa_pod_prop){ (key), (flags), { (size), (type) } })
+
+static inline int
+spa_pod_builder_prop(struct spa_pod_builder *builder, uint32_t key, uint32_t flags)
+{
+ const struct { uint32_t key; uint32_t flags; } p = { key, flags };
+ return spa_pod_builder_raw(builder, &p, sizeof(p));
+}
+
+#define SPA_POD_INIT_Sequence(size,unit) \
+ ((struct spa_pod_sequence){ { (size), SPA_TYPE_Sequence}, {(unit), 0 } })
+
+static inline int
+spa_pod_builder_push_sequence(struct spa_pod_builder *builder, struct spa_pod_frame *frame, uint32_t unit)
+{
+ const struct spa_pod_sequence p =
+ SPA_POD_INIT_Sequence(sizeof(struct spa_pod_sequence_body), unit);
+ uint32_t offset = builder->state.offset;
+ int res = spa_pod_builder_raw(builder, &p, sizeof(p));
+ spa_pod_builder_push(builder, frame, &p.pod, offset);
+ return res;
+}
+
+static inline uint32_t
+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)
+{
+ switch (id) {
+ case 'r':
+ return SPA_CHOICE_Range;
+ case 's':
+ return SPA_CHOICE_Step;
+ case 'e':
+ return SPA_CHOICE_Enum;
+ case 'f':
+ return SPA_CHOICE_Flags;
+ case 'n':
+ default:
+ return SPA_CHOICE_None;
+ }
+}
+
+#define SPA_POD_BUILDER_COLLECT(builder,type,args) \
+do { \
+ switch (type) { \
+ case 'b': \
+ spa_pod_builder_bool(builder, !!va_arg(args, int)); \
+ break; \
+ case 'I': \
+ spa_pod_builder_id(builder, va_arg(args, uint32_t)); \
+ break; \
+ case 'i': \
+ spa_pod_builder_int(builder, va_arg(args, int)); \
+ break; \
+ case 'l': \
+ spa_pod_builder_long(builder, va_arg(args, int64_t)); \
+ break; \
+ case 'f': \
+ spa_pod_builder_float(builder, va_arg(args, double)); \
+ break; \
+ case 'd': \
+ spa_pod_builder_double(builder, va_arg(args, double)); \
+ break; \
+ case 's': \
+ { \
+ char *strval = va_arg(args, char *); \
+ if (strval != NULL) { \
+ size_t len = strlen(strval); \
+ spa_pod_builder_string_len(builder, strval, len); \
+ } \
+ else \
+ spa_pod_builder_none(builder); \
+ break; \
+ } \
+ case 'S': \
+ { \
+ char *strval = va_arg(args, char *); \
+ size_t len = va_arg(args, int); \
+ spa_pod_builder_string_len(builder, strval, len); \
+ break; \
+ } \
+ case 'y': \
+ { \
+ void *ptr = va_arg(args, void *); \
+ int len = va_arg(args, int); \
+ spa_pod_builder_bytes(builder, ptr, len); \
+ break; \
+ } \
+ case 'R': \
+ { \
+ struct spa_rectangle *rectval = \
+ va_arg(args, struct spa_rectangle *); \
+ spa_pod_builder_rectangle(builder, \
+ rectval->width, rectval->height); \
+ break; \
+ } \
+ case 'F': \
+ { \
+ struct spa_fraction *fracval = \
+ va_arg(args, struct spa_fraction *); \
+ spa_pod_builder_fraction(builder, fracval->num, fracval->denom);\
+ break; \
+ } \
+ case 'a': \
+ { \
+ int child_size = va_arg(args, int); \
+ int child_type = va_arg(args, int); \
+ int n_elems = va_arg(args, int); \
+ void *elems = va_arg(args, void *); \
+ spa_pod_builder_array(builder, child_size, \
+ child_type, n_elems, elems); \
+ break; \
+ } \
+ case 'p': \
+ { \
+ int t = va_arg(args, uint32_t); \
+ spa_pod_builder_pointer(builder, t, va_arg(args, void *)); \
+ break; \
+ } \
+ case 'h': \
+ spa_pod_builder_fd(builder, va_arg(args, int)); \
+ break; \
+ case 'P': \
+ case 'O': \
+ case 'T': \
+ case 'V': \
+ { \
+ struct spa_pod *pod = va_arg(args, struct spa_pod *); \
+ if (pod == NULL) \
+ spa_pod_builder_none(builder); \
+ else \
+ spa_pod_builder_primitive(builder, pod); \
+ break; \
+ } \
+ } \
+} while(false)
+
+static inline int
+spa_pod_builder_addv(struct spa_pod_builder *builder, va_list args)
+{
+ int res = 0;
+ struct spa_pod_frame *frame = builder->state.frame;
+ uint32_t ftype = frame ? frame->pod.type : (uint32_t)SPA_TYPE_None;
+
+ do {
+ const char *format;
+ int n_values = 1;
+ struct spa_pod_frame f;
+ bool choice;
+
+ switch (ftype) {
+ case SPA_TYPE_Object:
+ {
+ uint32_t key = va_arg(args, uint32_t);
+ if (key == 0)
+ goto exit;
+ spa_pod_builder_prop(builder, key, 0);
+ break;
+ }
+ case SPA_TYPE_Sequence:
+ {
+ uint32_t offset = va_arg(args, uint32_t);
+ uint32_t type = va_arg(args, uint32_t);
+ if (type == 0)
+ goto exit;
+ spa_pod_builder_control(builder, offset, type);
+ SPA_FALLTHROUGH
+ }
+ default:
+ break;
+ }
+ if ((format = va_arg(args, const char *)) == NULL)
+ break;
+
+ choice = *format == '?';
+ if (choice) {
+ uint32_t type = spa_choice_from_id(*++format);
+ if (*format != '\0')
+ format++;
+
+ spa_pod_builder_push_choice(builder, &f, type, 0);
+
+ n_values = va_arg(args, int);
+ }
+ while (n_values-- > 0)
+ SPA_POD_BUILDER_COLLECT(builder, *format, args);
+
+ if (choice)
+ spa_pod_builder_pop(builder, &f);
+ } while (true);
+
+ exit:
+ return res;
+}
+
+static inline int spa_pod_builder_add(struct spa_pod_builder *builder, ...)
+{
+ int res;
+ va_list args;
+
+ va_start(args, builder);
+ res = spa_pod_builder_addv(builder, args);
+ va_end(args);
+
+ return res;
+}
+
+#define spa_pod_builder_add_object(b,type,id,...) \
+({ \
+ struct spa_pod_builder *_b = (b); \
+ struct spa_pod_frame _f; \
+ spa_pod_builder_push_object(_b, &_f, type, id); \
+ spa_pod_builder_add(_b, ##__VA_ARGS__, 0); \
+ spa_pod_builder_pop(_b, &_f); \
+})
+
+#define spa_pod_builder_add_struct(b,...) \
+({ \
+ struct spa_pod_builder *_b = (b); \
+ struct spa_pod_frame _f; \
+ spa_pod_builder_push_struct(_b, &_f); \
+ spa_pod_builder_add(_b, ##__VA_ARGS__, NULL); \
+ spa_pod_builder_pop(_b, &_f); \
+})
+
+#define spa_pod_builder_add_sequence(b,unit,...) \
+({ \
+ struct spa_pod_builder *_b = (b); \
+ struct spa_pod_frame _f; \
+ spa_pod_builder_push_sequence(_b, &_f, unit); \
+ spa_pod_builder_add(_b, ##__VA_ARGS__, 0, 0); \
+ spa_pod_builder_pop(_b, &_f); \
+})
+
+/** Copy a pod structure */
+static inline struct spa_pod *
+spa_pod_copy(const struct spa_pod *pod)
+{
+ size_t size;
+ struct spa_pod *c;
+
+ size = SPA_POD_SIZE(pod);
+ if ((c = (struct spa_pod *) malloc(size)) == NULL)
+ return NULL;
+ return (struct spa_pod *) memcpy(c, pod, size);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_POD_BUILDER_H */
diff --git a/spa/include/spa/pod/command.h b/spa/include/spa/pod/command.h
new file mode 100644
index 0000000..3cd30a0
--- /dev/null
+++ b/spa/include/spa/pod/command.h
@@ -0,0 +1,69 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_COMMAND_H
+#define SPA_COMMAND_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/pod/pod.h>
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+struct spa_command_body {
+ struct spa_pod_object_body body;
+};
+
+struct spa_command {
+ struct spa_pod pod;
+ struct spa_command_body body;
+};
+
+#define SPA_COMMAND_TYPE(cmd) ((cmd)->body.body.type)
+#define SPA_COMMAND_ID(cmd,type) (SPA_COMMAND_TYPE(cmd) == (type) ? \
+ (cmd)->body.body.id : SPA_ID_INVALID)
+
+#define SPA_COMMAND_INIT_FULL(t,size,type,id,...) ((t) \
+ { { (size), SPA_TYPE_Object }, \
+ { { (type), (id) }, ##__VA_ARGS__ } })
+
+#define SPA_COMMAND_INIT(type,id) \
+ SPA_COMMAND_INIT_FULL(struct spa_command, \
+ sizeof(struct spa_command_body), type, id)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_COMMAND_H */
diff --git a/spa/include/spa/pod/compare.h b/spa/include/spa/pod/compare.h
new file mode 100644
index 0000000..3fd9d00
--- /dev/null
+++ b/spa/include/spa/pod/compare.h
@@ -0,0 +1,190 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_POD_COMPARE_H
+#define SPA_POD_COMPARE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <spa/param/props.h>
+#include <spa/pod/iter.h>
+#include <spa/pod/builder.h>
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+static inline int spa_pod_compare_value(uint32_t type, const void *r1, const void *r2, uint32_t size)
+{
+ switch (type) {
+ case SPA_TYPE_None:
+ return 0;
+ case SPA_TYPE_Bool:
+ case SPA_TYPE_Id:
+ return *(uint32_t *) r1 == *(uint32_t *) r2 ? 0 : 1;
+ case SPA_TYPE_Int:
+ return *(int32_t *) r1 - *(int32_t *) r2;
+ case SPA_TYPE_Long:
+ return *(int64_t *) r1 - *(int64_t *) r2;
+ case SPA_TYPE_Float:
+ return *(float *) r1 - *(float *) r2;
+ case SPA_TYPE_Double:
+ return *(double *) r1 - *(double *) r2;
+ case SPA_TYPE_String:
+ return strcmp((char *)r1, (char *)r2);
+ case SPA_TYPE_Bytes:
+ return memcmp((char *)r1, (char *)r2, size);
+ case SPA_TYPE_Rectangle:
+ {
+ const struct spa_rectangle *rec1 = (struct spa_rectangle *) r1,
+ *rec2 = (struct spa_rectangle *) r2;
+ if (rec1->width == rec2->width && rec1->height == rec2->height)
+ return 0;
+ else if (rec1->width < rec2->width || rec1->height < rec2->height)
+ return -1;
+ else
+ return 1;
+ }
+ case SPA_TYPE_Fraction:
+ {
+ const struct spa_fraction *f1 = (struct spa_fraction *) r1,
+ *f2 = (struct spa_fraction *) r2;
+ int64_t n1, n2;
+ n1 = ((int64_t) f1->num) * f2->denom;
+ n2 = ((int64_t) f2->num) * f1->denom;
+ if (n1 < n2)
+ return -1;
+ else if (n1 > n2)
+ return 1;
+ else
+ return 0;
+ }
+ default:
+ break;
+ }
+ return 0;
+}
+
+static inline int spa_pod_compare(const struct spa_pod *pod1,
+ const struct spa_pod *pod2)
+{
+ int res = 0;
+ uint32_t n_vals1, n_vals2;
+ uint32_t choice1, choice2;
+
+ spa_return_val_if_fail(pod1 != NULL, -EINVAL);
+ spa_return_val_if_fail(pod2 != NULL, -EINVAL);
+
+ pod1 = spa_pod_get_values(pod1, &n_vals1, &choice1);
+ pod2 = spa_pod_get_values(pod2, &n_vals2, &choice2);
+
+ if (n_vals1 != n_vals2)
+ return -EINVAL;
+
+ if (SPA_POD_TYPE(pod1) != SPA_POD_TYPE(pod2))
+ return -EINVAL;
+
+ switch (SPA_POD_TYPE(pod1)) {
+ case SPA_TYPE_Struct:
+ {
+ const struct spa_pod *p1, *p2;
+ size_t p1s, p2s;
+
+ p1 = (const struct spa_pod*)SPA_POD_BODY_CONST(pod1);
+ p1s = SPA_POD_BODY_SIZE(pod1);
+ p2 = (const struct spa_pod*)SPA_POD_BODY_CONST(pod2);
+ p2s = SPA_POD_BODY_SIZE(pod2);
+
+ while (true) {
+ if (!spa_pod_is_inside(pod1, p1s, p1) ||
+ !spa_pod_is_inside(pod2, p2s, p2))
+ return -EINVAL;
+
+ if ((res = spa_pod_compare(p1, p2)) != 0)
+ return res;
+
+ p1 = (const struct spa_pod*)spa_pod_next(p1);
+ p2 = (const struct spa_pod*)spa_pod_next(p2);
+ }
+ break;
+ }
+ case SPA_TYPE_Object:
+ {
+ const struct spa_pod_prop *p1, *p2;
+ const struct spa_pod_object *o1, *o2;
+
+ o1 = (const struct spa_pod_object*)pod1;
+ o2 = (const struct spa_pod_object*)pod2;
+
+ p2 = NULL;
+ SPA_POD_OBJECT_FOREACH(o1, p1) {
+ if ((p2 = spa_pod_object_find_prop(o2, p2, p1->key)) == NULL)
+ return 1;
+ if ((res = spa_pod_compare(&p1->value, &p2->value)) != 0)
+ return res;
+ }
+ p1 = NULL;
+ SPA_POD_OBJECT_FOREACH(o2, p2) {
+ if ((p1 = spa_pod_object_find_prop(o1, p1, p2->key)) == NULL)
+ return -1;
+ }
+ break;
+ }
+ case SPA_TYPE_Array:
+ {
+ if (SPA_POD_BODY_SIZE(pod1) != SPA_POD_BODY_SIZE(pod2))
+ return -EINVAL;
+ res = memcmp(SPA_POD_BODY(pod1), SPA_POD_BODY(pod2), SPA_POD_BODY_SIZE(pod2));
+ break;
+ }
+ default:
+ if (SPA_POD_BODY_SIZE(pod1) != SPA_POD_BODY_SIZE(pod2))
+ return -EINVAL;
+ res = spa_pod_compare_value(SPA_POD_TYPE(pod1),
+ SPA_POD_BODY(pod1), SPA_POD_BODY(pod2),
+ SPA_POD_BODY_SIZE(pod1));
+ break;
+ }
+ return res;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/spa/include/spa/pod/dynamic.h b/spa/include/spa/pod/dynamic.h
new file mode 100644
index 0000000..1fe77be
--- /dev/null
+++ b/spa/include/spa/pod/dynamic.h
@@ -0,0 +1,81 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_POD_DYNAMIC_H
+#define SPA_POD_DYNAMIC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/pod/builder.h>
+
+struct spa_pod_dynamic_builder {
+ struct spa_pod_builder b;
+ void *data;
+ uint32_t extend;
+ uint32_t _padding;
+};
+
+static int spa_pod_dynamic_builder_overflow(void *data, uint32_t size)
+{
+ struct spa_pod_dynamic_builder *d = (struct spa_pod_dynamic_builder*)data;
+ int32_t old_size = d->b.size;
+ int32_t new_size = SPA_ROUND_UP_N(size, d->extend);
+ void *old_data = d->b.data;
+
+ if (old_data == d->data)
+ d->b.data = NULL;
+ if ((d->b.data = realloc(d->b.data, new_size)) == NULL)
+ return -errno;
+ if (old_data == d->data && d->b.data != old_data && old_size > 0)
+ memcpy(d->b.data, old_data, old_size);
+ d->b.size = new_size;
+ return 0;
+}
+
+static inline 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 = {
+ SPA_VERSION_POD_BUILDER_CALLBACKS,
+ .overflow = spa_pod_dynamic_builder_overflow
+ };
+ builder->b = SPA_POD_BUILDER_INIT(data, size);
+ spa_pod_builder_set_callbacks(&builder->b, &spa_pod_dynamic_builder_callbacks, builder);
+ builder->extend = extend;
+ builder->data = data;
+}
+
+static inline void spa_pod_dynamic_builder_clean(struct spa_pod_dynamic_builder *builder)
+{
+ if (builder->data != builder->b.data)
+ free(builder->b.data);
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_POD_DYNAMIC_H */
diff --git a/spa/include/spa/pod/event.h b/spa/include/spa/pod/event.h
new file mode 100644
index 0000000..2328af7
--- /dev/null
+++ b/spa/include/spa/pod/event.h
@@ -0,0 +1,68 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_EVENT_H
+#define SPA_EVENT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/pod/pod.h>
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+struct spa_event_body {
+ struct spa_pod_object_body body;
+};
+
+struct spa_event {
+ struct spa_pod pod;
+ struct spa_event_body body;
+};
+
+#define SPA_EVENT_TYPE(ev) ((ev)->body.body.type)
+#define SPA_EVENT_ID(ev,type) (SPA_EVENT_TYPE(ev) == (type) ? \
+ (ev)->body.body.id : SPA_ID_INVALID)
+
+#define SPA_EVENT_INIT_FULL(t,size,type,id,...) ((t) \
+ { { (size), SPA_TYPE_OBJECT }, \
+ { { (type), (id) }, ##__VA_ARGS__ } }) \
+
+#define SPA_EVENT_INIT(type,id) \
+ SPA_EVENT_INIT_FULL(struct spa_event, \
+ sizeof(struct spa_event_body), type, id)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_EVENT_H */
diff --git a/spa/include/spa/pod/filter.h b/spa/include/spa/pod/filter.h
new file mode 100644
index 0000000..423a10b
--- /dev/null
+++ b/spa/include/spa/pod/filter.h
@@ -0,0 +1,481 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_POD_FILTER_H
+#define SPA_POD_FILTER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <spa/param/props.h>
+#include <spa/pod/iter.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/compare.h>
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+static inline int spa_pod_choice_fix_default(struct spa_pod_choice *choice)
+{
+ void *val, *alt;
+ int i, nvals;
+ uint32_t type, size;
+
+ nvals = SPA_POD_CHOICE_N_VALUES(choice);
+ type = SPA_POD_CHOICE_VALUE_TYPE(choice);
+ size = SPA_POD_CHOICE_VALUE_SIZE(choice);
+ alt = val = SPA_POD_CHOICE_VALUES(choice);
+
+ switch (choice->body.type) {
+ case SPA_CHOICE_None:
+ break;
+ case SPA_CHOICE_Range:
+ case SPA_CHOICE_Step:
+ if (nvals > 1) {
+ alt = SPA_PTROFF(alt, size, void);
+ if (spa_pod_compare_value(type, val, alt, size) < 0)
+ memcpy(val, alt, size);
+ }
+ if (nvals > 2) {
+ alt = SPA_PTROFF(alt, size, void);
+ if (spa_pod_compare_value(type, val, alt, size) > 0)
+ memcpy(val, alt, size);
+ }
+ break;
+ case SPA_CHOICE_Flags:
+ case SPA_CHOICE_Enum:
+ {
+ void *best = NULL;
+
+ for (i = 1; i < nvals; i++) {
+ alt = SPA_PTROFF(alt, size, void);
+ if (spa_pod_compare_value(type, val, alt, size) == 0) {
+ best = alt;
+ break;
+ }
+ if (best == NULL)
+ best = alt;
+ }
+ if (best)
+ memcpy(val, best, size);
+
+ if (nvals <= 1)
+ choice->body.type = SPA_CHOICE_None;
+ break;
+ }
+ }
+ return 0;
+}
+
+static inline int spa_pod_filter_flags_value(struct spa_pod_builder *b,
+ uint32_t type, const void *r1, const void *r2, uint32_t size)
+{
+ switch (type) {
+ case SPA_TYPE_Int:
+ {
+ int32_t val = (*(int32_t *) r1) & (*(int32_t *) r2);
+ if (val == 0)
+ return 0;
+ spa_pod_builder_int(b, val);
+ break;
+ }
+ case SPA_TYPE_Long:
+ {
+ int64_t val = (*(int64_t *) r1) & (*(int64_t *) r2);
+ if (val == 0)
+ return 0;
+ spa_pod_builder_long(b, val);
+ break;
+ }
+ default:
+ return -ENOTSUP;
+ }
+ return 1;
+}
+
+static inline int spa_pod_filter_is_step_of(uint32_t type, const void *r1,
+ const void *r2, uint32_t size)
+{
+ switch (type) {
+ case SPA_TYPE_Int:
+ return *(int32_t *) r1 % *(int32_t *) r2 == 0;
+ case SPA_TYPE_Long:
+ return *(int64_t *) r1 % *(int64_t *) r2 == 0;
+ case SPA_TYPE_Rectangle:
+ {
+ const struct spa_rectangle *rec1 = (struct spa_rectangle *) r1,
+ *rec2 = (struct spa_rectangle *) r2;
+
+ return (rec1->width % rec2->width == 0 &&
+ rec1->height % rec2->height == 0);
+ }
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static inline int
+spa_pod_filter_prop(struct spa_pod_builder *b,
+ const struct spa_pod_prop *p1,
+ const struct spa_pod_prop *p2)
+{
+ const struct spa_pod *v1, *v2;
+ struct spa_pod_choice *nc;
+ uint32_t j, k, nalt1, nalt2;
+ void *alt1, *alt2, *a1, *a2;
+ uint32_t type, size, p1c, p2c;
+ struct spa_pod_frame f;
+
+ v1 = spa_pod_get_values(&p1->value, &nalt1, &p1c);
+ alt1 = SPA_POD_BODY(v1);
+ v2 = spa_pod_get_values(&p2->value, &nalt2, &p2c);
+ alt2 = SPA_POD_BODY(v2);
+
+ type = v1->type;
+ size = v1->size;
+
+ /* incompatible property types */
+ if (type != v2->type || size != v2->size || p1->key != p2->key)
+ return -EINVAL;
+
+ if (p1c == SPA_CHOICE_None || p1c == SPA_CHOICE_Flags) {
+ nalt1 = 1;
+ } else {
+ alt1 = SPA_PTROFF(alt1, size, void);
+ nalt1--;
+ }
+
+ if (p2c == SPA_CHOICE_None || p2c == SPA_CHOICE_Flags) {
+ nalt2 = 1;
+ } else {
+ alt2 = SPA_PTROFF(alt2, size, void);
+ nalt2--;
+ }
+
+ /* start with copying the property */
+ spa_pod_builder_prop(b, p1->key, p1->flags & p2->flags);
+ spa_pod_builder_push_choice(b, &f, 0, 0);
+ nc = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f);
+
+ /* default value */
+ spa_pod_builder_primitive(b, v1);
+
+ if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_None) ||
+ (p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Enum) ||
+ (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_None) ||
+ (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Enum)) {
+ int n_copied = 0;
+ /* copy all equal values but don't copy the default value again */
+ for (j = 0, a1 = alt1; j < nalt1; j++, a1 = SPA_PTROFF(a1, size, void)) {
+ for (k = 0, a2 = alt2; k < nalt2; k++, a2 = SPA_PTROFF(a2,size,void)) {
+ if (spa_pod_compare_value(type, a1, a2, size) == 0) {
+ if (p1c == SPA_CHOICE_Enum || j > 0)
+ spa_pod_builder_raw(b, a1, size);
+ n_copied++;
+ }
+ }
+ }
+ if (n_copied == 0)
+ return -EINVAL;
+ nc->body.type = SPA_CHOICE_Enum;
+ }
+
+ if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Range) ||
+ (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Range)) {
+ int n_copied = 0;
+ /* copy all values inside the range */
+ for (j = 0, a1 = alt1, a2 = alt2; j < nalt1; j++, a1 = SPA_PTROFF(a1,size,void)) {
+ if (spa_pod_compare_value(type, a1, a2, size) < 0)
+ continue;
+ if (spa_pod_compare_value(type, a1, SPA_PTROFF(a2,size,void), size) > 0)
+ continue;
+ spa_pod_builder_raw(b, a1, size);
+ n_copied++;
+ }
+ if (n_copied == 0)
+ return -EINVAL;
+ nc->body.type = SPA_CHOICE_Enum;
+ }
+
+ if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Step) ||
+ (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Step)) {
+ int n_copied = 0;
+ for (j = 0, a1 = alt1, a2 = alt2; j < nalt1; j++, a1 = SPA_PTROFF(a1,size,void)) {
+ int res;
+ if (spa_pod_compare_value(type, a1, a2, size) < 0)
+ continue;
+ if (spa_pod_compare_value(type, a1, SPA_PTROFF(a2,size,void), size) > 0)
+ continue;
+
+ res = spa_pod_filter_is_step_of(type, a1, SPA_PTROFF(a2,size*2,void), size);
+ if (res == 0)
+ continue;
+ if (res == -ENOTSUP)
+ return -EINVAL;
+
+ spa_pod_builder_raw(b, a1, size);
+ n_copied++;
+ }
+ if (n_copied == 0)
+ return -EINVAL;
+ nc->body.type = SPA_CHOICE_Enum;
+ }
+
+ if ((p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_None) ||
+ (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Enum)) {
+ int n_copied = 0;
+ /* copy all values inside the range */
+ for (k = 0, a1 = alt1, a2 = alt2; k < nalt2; k++, a2 = SPA_PTROFF(a2,size,void)) {
+ if (spa_pod_compare_value(type, a2, a1, size) < 0)
+ continue;
+ if (spa_pod_compare_value(type, a2, SPA_PTROFF(a1,size,void), size) > 0)
+ continue;
+ spa_pod_builder_raw(b, a2, size);
+ n_copied++;
+ }
+ if (n_copied == 0)
+ return -EINVAL;
+ nc->body.type = SPA_CHOICE_Enum;
+ }
+
+ if ((p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Range) ||
+ (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Step) ||
+ (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Range) ||
+ (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Step)) {
+ if (spa_pod_compare_value(type, alt1, alt2, size) < 0)
+ spa_pod_builder_raw(b, alt2, size);
+ else
+ spa_pod_builder_raw(b, alt1, size);
+
+ alt1 = SPA_PTROFF(alt1,size,void);
+ alt2 = SPA_PTROFF(alt2,size,void);
+
+ if (spa_pod_compare_value(type, alt1, alt2, size) < 0)
+ spa_pod_builder_raw(b, alt1, size);
+ else
+ spa_pod_builder_raw(b, alt2, size);
+
+ nc->body.type = SPA_CHOICE_Range;
+ }
+
+ if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Flags) ||
+ (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_None) ||
+ (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Flags)) {
+ if (spa_pod_filter_flags_value(b, type, alt1, alt2, size) != 1)
+ return -EINVAL;
+ nc->body.type = SPA_CHOICE_Flags;
+ }
+
+ if (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Flags)
+ return -ENOTSUP;
+
+ if (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Flags)
+ return -ENOTSUP;
+
+ if ((p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_None) ||
+ (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Enum)) {
+ int n_copied = 0;
+ for (j = 0, a1 = alt1, a2 = alt2; j < nalt2; j++, a2 = SPA_PTROFF(a1,size,void)) {
+ int res;
+ if (spa_pod_compare_value(type, a2, a1, size) < 0)
+ continue;
+ if (spa_pod_compare_value(type, a2, SPA_PTROFF(a1,size,void), size) > 0)
+ continue;
+
+ res = spa_pod_filter_is_step_of(type, a2, SPA_PTROFF(a1,size*2,void), size);
+ if (res == 0)
+ continue;
+ if (res == -ENOTSUP)
+ return -EINVAL;
+
+ spa_pod_builder_raw(b, a2, size);
+ n_copied++;
+ }
+ if (n_copied == 0)
+ return -EINVAL;
+ nc->body.type = SPA_CHOICE_Enum;
+ }
+ if (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Flags)
+ return -ENOTSUP;
+
+ if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Range)
+ return -ENOTSUP;
+ if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Step)
+ return -ENOTSUP;
+ if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Enum)
+ return -ENOTSUP;
+
+ spa_pod_builder_pop(b, &f);
+ spa_pod_choice_fix_default(nc);
+
+ return 0;
+}
+
+static inline 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)
+{
+ const struct spa_pod *pp, *pf;
+ int res = 0;
+
+ pf = filter;
+
+ SPA_POD_FOREACH(pod, pod_size, pp) {
+ bool do_copy = false, do_advance = false;
+ uint32_t filter_offset = 0;
+ struct spa_pod_frame f;
+
+ switch (SPA_POD_TYPE(pp)) {
+ case SPA_TYPE_Object:
+ if (pf != NULL) {
+ struct spa_pod_object *op = (struct spa_pod_object *) pp;
+ struct spa_pod_object *of = (struct spa_pod_object *) pf;
+ const struct spa_pod_prop *p1, *p2;
+
+ if (SPA_POD_TYPE(pf) != SPA_POD_TYPE(pp))
+ return -EINVAL;
+
+ spa_pod_builder_push_object(b, &f, op->body.type, op->body.id);
+ p2 = NULL;
+ SPA_POD_OBJECT_FOREACH(op, p1) {
+ p2 = spa_pod_object_find_prop(of, p2, p1->key);
+ if (p2 != NULL)
+ res = spa_pod_filter_prop(b, p1, p2);
+ else if ((p1->flags & SPA_POD_PROP_FLAG_MANDATORY) != 0)
+ res = -EINVAL;
+ else
+ spa_pod_builder_raw_padded(b, p1, SPA_POD_PROP_SIZE(p1));
+ if (res < 0)
+ break;
+ }
+ if (res >= 0) {
+ p1 = NULL;
+ SPA_POD_OBJECT_FOREACH(of, p2) {
+ p1 = spa_pod_object_find_prop(op, p1, p2->key);
+ if (p1 != NULL)
+ continue;
+ if ((p2->flags & SPA_POD_PROP_FLAG_MANDATORY) != 0)
+ res = -EINVAL;
+ if (res < 0)
+ break;
+ spa_pod_builder_raw_padded(b, p2, SPA_POD_PROP_SIZE(p2));
+ }
+ }
+ spa_pod_builder_pop(b, &f);
+ do_advance = true;
+ }
+ else
+ do_copy = true;
+ break;
+
+ case SPA_TYPE_Struct:
+ if (pf != NULL) {
+ if (SPA_POD_TYPE(pf) != SPA_POD_TYPE(pp))
+ return -EINVAL;
+
+ filter_offset = sizeof(struct spa_pod_struct);
+ spa_pod_builder_push_struct(b, &f);
+ res = spa_pod_filter_part(b,
+ SPA_PTROFF(pp,filter_offset,const struct spa_pod),
+ SPA_POD_SIZE(pp) - filter_offset,
+ SPA_PTROFF(pf,filter_offset,const struct spa_pod),
+ SPA_POD_SIZE(pf) - filter_offset);
+ spa_pod_builder_pop(b, &f);
+ do_advance = true;
+ }
+ else
+ do_copy = true;
+ break;
+
+ default:
+ if (pf != NULL) {
+ if (SPA_POD_SIZE(pp) != SPA_POD_SIZE(pf))
+ return -EINVAL;
+ if (memcmp(pp, pf, SPA_POD_SIZE(pp)) != 0)
+ return -EINVAL;
+ do_advance = true;
+ }
+ do_copy = true;
+ break;
+ }
+ if (do_copy)
+ spa_pod_builder_raw_padded(b, pp, SPA_POD_SIZE(pp));
+ if (do_advance) {
+ pf = (const struct spa_pod*)spa_pod_next(pf);
+ if (!spa_pod_is_inside(filter, filter_size, pf))
+ pf = NULL;
+ }
+ if (res < 0)
+ break;
+ }
+ return res;
+}
+
+static inline int
+spa_pod_filter(struct spa_pod_builder *b,
+ struct spa_pod **result,
+ const struct spa_pod *pod,
+ const struct spa_pod *filter)
+{
+ int res;
+ struct spa_pod_builder_state state;
+
+ spa_return_val_if_fail(pod != NULL, -EINVAL);
+ spa_return_val_if_fail(b != NULL, -EINVAL);
+
+ spa_pod_builder_get_state(b, &state);
+ if (filter == NULL)
+ res = spa_pod_builder_raw_padded(b, pod, SPA_POD_SIZE(pod));
+ else
+ res = spa_pod_filter_part(b, pod, SPA_POD_SIZE(pod), filter, SPA_POD_SIZE(filter));
+
+ if (res < 0) {
+ spa_pod_builder_reset(b, &state);
+ } else if (result) {
+ *result = (struct spa_pod*)spa_pod_builder_deref(b, state.offset);
+ if (*result == NULL)
+ res = -ENOSPC;
+ }
+ return res;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_POD_FILTER_H */
diff --git a/spa/include/spa/pod/iter.h b/spa/include/spa/pod/iter.h
new file mode 100644
index 0000000..d3dcf13
--- /dev/null
+++ b/spa/include/spa/pod/iter.h
@@ -0,0 +1,475 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_POD_ITER_H
+#define SPA_POD_ITER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+#include <sys/types.h>
+
+#include <spa/pod/pod.h>
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+struct spa_pod_frame {
+ struct spa_pod pod;
+ struct spa_pod_frame *parent;
+ uint32_t offset;
+ uint32_t flags;
+};
+
+static inline bool spa_pod_is_inside(const void *pod, uint32_t size, const void *iter)
+{
+ return SPA_POD_BODY(iter) <= SPA_PTROFF(pod, size, void) &&
+ SPA_PTROFF(iter, SPA_POD_SIZE(iter), void) <= SPA_PTROFF(pod, size, void);
+}
+
+static inline 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)
+{
+ 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,
+ uint32_t size, const struct spa_pod_prop *iter)
+{
+ return SPA_POD_CONTENTS(struct spa_pod_prop, iter) <= SPA_PTROFF(body, size, void) &&
+ SPA_PTROFF(iter, SPA_POD_PROP_SIZE(iter), void) <= SPA_PTROFF(body, size, void);
+}
+
+static inline 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)
+{
+ 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,
+ uint32_t size, const struct spa_pod_control *iter)
+{
+ return SPA_POD_CONTENTS(struct spa_pod_control, iter) <= SPA_PTROFF(body, size, void) &&
+ SPA_PTROFF(iter, SPA_POD_CONTROL_SIZE(iter), void) <= SPA_PTROFF(body, size, void);
+}
+
+static inline 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);
+}
+
+#define SPA_POD_ARRAY_BODY_FOREACH(body, _size, iter) \
+ for ((iter) = (__typeof__(iter))SPA_PTROFF((body), sizeof(struct spa_pod_array_body), void); \
+ (iter) < (__typeof__(iter))SPA_PTROFF((body), (_size), void); \
+ (iter) = (__typeof__(iter))SPA_PTROFF((iter), (body)->child.size, void))
+
+#define SPA_POD_ARRAY_FOREACH(obj, iter) \
+ SPA_POD_ARRAY_BODY_FOREACH(&(obj)->body, SPA_POD_BODY_SIZE(obj), iter)
+
+#define SPA_POD_CHOICE_BODY_FOREACH(body, _size, iter) \
+ for ((iter) = (__typeof__(iter))SPA_PTROFF((body), sizeof(struct spa_pod_choice_body), void); \
+ (iter) < (__typeof__(iter))SPA_PTROFF((body), (_size), void); \
+ (iter) = (__typeof__(iter))SPA_PTROFF((iter), (body)->child.size, void))
+
+#define SPA_POD_CHOICE_FOREACH(obj, iter) \
+ SPA_POD_CHOICE_BODY_FOREACH(&(obj)->body, SPA_POD_BODY_SIZE(obj), iter)
+
+#define SPA_POD_FOREACH(pod, size, iter) \
+ for ((iter) = (pod); \
+ spa_pod_is_inside(pod, size, iter); \
+ (iter) = (__typeof__(iter))spa_pod_next(iter))
+
+#define SPA_POD_STRUCT_FOREACH(obj, iter) \
+ SPA_POD_FOREACH(SPA_POD_BODY(obj), SPA_POD_BODY_SIZE(obj), iter)
+
+#define SPA_POD_OBJECT_BODY_FOREACH(body, size, iter) \
+ for ((iter) = spa_pod_prop_first(body); \
+ spa_pod_prop_is_inside(body, size, iter); \
+ (iter) = spa_pod_prop_next(iter))
+
+#define SPA_POD_OBJECT_FOREACH(obj, iter) \
+ SPA_POD_OBJECT_BODY_FOREACH(&(obj)->body, SPA_POD_BODY_SIZE(obj), iter)
+
+#define SPA_POD_SEQUENCE_BODY_FOREACH(body, size, iter) \
+ for ((iter) = spa_pod_control_first(body); \
+ spa_pod_control_is_inside(body, size, iter); \
+ (iter) = spa_pod_control_next(iter))
+
+#define SPA_POD_SEQUENCE_FOREACH(seq, iter) \
+ 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)
+{
+ void *pod;
+ if (size < sizeof(struct spa_pod) || offset + size > maxsize)
+ return NULL;
+ pod = SPA_PTROFF(data, offset, void);
+ if (SPA_POD_SIZE(pod) > size)
+ return NULL;
+ return pod;
+}
+
+static inline 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)
+{
+ 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)
+{
+ if (!spa_pod_is_bool(pod))
+ return -EINVAL;
+ *value = !!SPA_POD_VALUE(struct spa_pod_bool, pod);
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_id(pod))
+ return -EINVAL;
+ *value = SPA_POD_VALUE(struct spa_pod_id, pod);
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_int(pod))
+ return -EINVAL;
+ *value = SPA_POD_VALUE(struct spa_pod_int, pod);
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_long(pod))
+ return -EINVAL;
+ *value = SPA_POD_VALUE(struct spa_pod_long, pod);
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_float(pod))
+ return -EINVAL;
+ *value = SPA_POD_VALUE(struct spa_pod_float, pod);
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_double(pod))
+ return -EINVAL;
+ *value = SPA_POD_VALUE(struct spa_pod_double, pod);
+ return 0;
+}
+
+static inline 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 &&
+ SPA_POD_BODY_SIZE(pod) > 0 &&
+ s[SPA_POD_BODY_SIZE(pod)-1] == '\0');
+}
+
+static inline int spa_pod_get_string(const struct spa_pod *pod, const char **value)
+{
+ if (!spa_pod_is_string(pod))
+ return -EINVAL;
+ *value = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod);
+ return 0;
+}
+
+static inline 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)
+ return -EINVAL;
+ strncpy(dest, s, maxlen-1);
+ dest[maxlen-1]= '\0';
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_bytes(pod))
+ return -EINVAL;
+ *value = (const void *)SPA_POD_CONTENTS(struct spa_pod_bytes, pod);
+ *len = SPA_POD_BODY_SIZE(pod);
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_pointer(pod))
+ return -EINVAL;
+ *type = ((struct spa_pod_pointer*)pod)->body.type;
+ *value = ((struct spa_pod_pointer*)pod)->body.value;
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_fd(pod))
+ return -EINVAL;
+ *value = SPA_POD_VALUE(struct spa_pod_fd, pod);
+ return 0;
+}
+
+static inline 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)
+{
+ if (!spa_pod_is_rectangle(pod))
+ return -EINVAL;
+ *value = SPA_POD_VALUE(struct spa_pod_rectangle, pod);
+ return 0;
+}
+
+static inline 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_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)
+{
+ 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)
+{
+ 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_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,
+ void *values, uint32_t max_values)
+{
+ uint32_t n_values;
+ void *v = spa_pod_get_array(pod, &n_values);
+ if (v == NULL || max_values == 0 || SPA_POD_ARRAY_VALUE_TYPE(pod) != type)
+ return 0;
+ n_values = SPA_MIN(n_values, max_values);
+ memcpy(values, v, SPA_POD_ARRAY_VALUE_SIZE(pod) * n_values);
+ return n_values;
+}
+
+static inline 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)
+{
+ if (pod->type == SPA_TYPE_Choice) {
+ *n_vals = SPA_POD_CHOICE_N_VALUES(pod);
+ if ((*choice = SPA_POD_CHOICE_TYPE(pod)) == SPA_CHOICE_None)
+ *n_vals = SPA_MIN(1u, SPA_POD_CHOICE_N_VALUES(pod));
+ return (struct spa_pod*)SPA_POD_CHOICE_CHILD(pod);
+ } else {
+ *n_vals = 1;
+ *choice = SPA_CHOICE_None;
+ return (struct spa_pod*)pod;
+ }
+}
+
+static inline 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)
+{
+ 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)
+{
+ 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)
+{
+ 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)
+{
+ 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,
+ const struct spa_pod_prop *start, uint32_t key)
+{
+ const struct spa_pod_prop *first, *res;
+
+ first = spa_pod_prop_first(&pod->body);
+ start = start ? spa_pod_prop_next(start) : first;
+
+ for (res = start; spa_pod_prop_is_inside(&pod->body, pod->pod.size, res);
+ res = spa_pod_prop_next(res)) {
+ if (res->key == key)
+ return res;
+ }
+ for (res = first; res != start; res = spa_pod_prop_next(res)) {
+ if (res->key == key)
+ return res;
+ }
+ return NULL;
+}
+
+static inline 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))
+ return NULL;
+ 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)
+{
+ struct spa_pod_prop *res;
+ SPA_POD_OBJECT_FOREACH(pod, res) {
+ if (res->value.type == SPA_TYPE_Choice &&
+ !SPA_FLAG_IS_SET(res->flags, SPA_POD_PROP_FLAG_DONT_FIXATE))
+ ((struct spa_pod_choice*)&res->value)->body.type = SPA_CHOICE_None;
+ }
+ return 0;
+}
+
+static inline 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)
+{
+ struct spa_pod_prop *res;
+ SPA_POD_OBJECT_FOREACH(pod, res) {
+ if (res->value.type == SPA_TYPE_Choice &&
+ ((struct spa_pod_choice*)&res->value)->body.type != SPA_CHOICE_None)
+ return 0;
+ }
+ return 1;
+}
+
+static inline int spa_pod_is_fixated(const struct spa_pod *pod)
+{
+ if (!spa_pod_is_object(pod))
+ return -EINVAL;
+ return spa_pod_object_is_fixated((const struct spa_pod_object *)pod);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_POD_H */
diff --git a/spa/include/spa/pod/parser.h b/spa/include/spa/pod/parser.h
new file mode 100644
index 0000000..e054675
--- /dev/null
+++ b/spa/include/spa/pod/parser.h
@@ -0,0 +1,594 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_POD_PARSER_H
+#define SPA_POD_PARSER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+#include <stdarg.h>
+
+#include <spa/pod/iter.h>
+#include <spa/pod/vararg.h>
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+struct spa_pod_parser_state {
+ uint32_t offset;
+ uint32_t flags;
+ struct spa_pod_frame *frame;
+};
+
+struct spa_pod_parser {
+ const void *data;
+ uint32_t size;
+ uint32_t _padding;
+ struct spa_pod_parser_state state;
+};
+
+#define SPA_POD_PARSER_INIT(buffer,size) ((struct spa_pod_parser){ (buffer), (size), 0, {} })
+
+static inline void spa_pod_parser_init(struct spa_pod_parser *parser,
+ const void *data, uint32_t size)
+{
+ *parser = SPA_POD_PARSER_INIT(data, size);
+}
+
+static inline 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_pod_parser_get_state(struct spa_pod_parser *parser, struct spa_pod_parser_state *state)
+{
+ *state = parser->state;
+}
+
+static inline 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_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. */
+ const uint64_t long_offset = (uint64_t)offset + 8;
+ if (long_offset <= size && (offset & 7) == 0) {
+ /* Use void* because creating a misaligned pointer is undefined. */
+ void *pod = SPA_PTROFF(parser->data, offset, void);
+ /*
+ * Check that the pointer is aligned and that the size (rounded
+ * to the next multiple of 8) is in bounds.
+ */
+ if (SPA_IS_ALIGNED(pod, __alignof__(struct spa_pod)) &&
+ long_offset + SPA_ROUND_UP_N((uint64_t)SPA_POD_BODY_SIZE(pod), 8) <= size)
+ return (struct spa_pod *)pod;
+ }
+ return NULL;
+}
+
+static inline 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,
+ struct spa_pod_frame *frame, const struct spa_pod *pod, uint32_t offset)
+{
+ frame->pod = *pod;
+ frame->offset = offset;
+ frame->parent = parser->state.frame;
+ frame->flags = parser->state.flags;
+ parser->state.frame = frame;
+}
+
+static inline 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)
+{
+ 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)
+{
+ struct spa_pod *pod = spa_pod_parser_current(parser);
+ if (pod)
+ spa_pod_parser_advance(parser, pod);
+ return pod;
+}
+
+static inline int spa_pod_parser_pop(struct spa_pod_parser *parser,
+ struct spa_pod_frame *frame)
+{
+ parser->state.frame = frame->parent;
+ parser->state.offset = frame->offset + SPA_ROUND_UP_N(SPA_POD_SIZE(&frame->pod), 8);
+ return 0;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_bool(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_id(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_int(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_long(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_float(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_double(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_string(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_bytes(pod, value, len)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_pointer(pod, type, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_fd(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_rectangle(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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);
+ if (pod != NULL && (res = spa_pod_get_fraction(pod, value)) >= 0)
+ spa_pod_parser_advance(parser, pod);
+ return res;
+}
+
+static inline 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)
+ return -EPIPE;
+ *value = pod;
+ spa_pod_parser_advance(parser, pod);
+ return 0;
+}
+static inline 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);
+ if (pod == NULL)
+ return -EPIPE;
+ if (!spa_pod_is_struct(pod))
+ return -EINVAL;
+ spa_pod_parser_push(parser, frame, pod, parser->state.offset);
+ parser->state.offset += sizeof(struct spa_pod_struct);
+ return 0;
+}
+
+static inline 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);
+ if (pod == NULL)
+ return -EPIPE;
+ if (!spa_pod_is_object(pod))
+ return -EINVAL;
+ if (type != SPA_POD_OBJECT_TYPE(pod))
+ return -EPROTO;
+ if (id != NULL)
+ *id = SPA_POD_OBJECT_ID(pod);
+ spa_pod_parser_push(parser, frame, pod, parser->state.offset);
+ parser->state.offset = parser->size;
+ return 0;
+}
+
+static inline bool spa_pod_parser_can_collect(const struct spa_pod *pod, char type)
+{
+ if (pod == NULL)
+ return false;
+
+ if (SPA_POD_TYPE(pod) == SPA_TYPE_Choice) {
+ if (!spa_pod_is_choice(pod))
+ return false;
+ if (type == 'V')
+ return true;
+ if (SPA_POD_CHOICE_TYPE(pod) != SPA_CHOICE_None)
+ return false;
+ pod = SPA_POD_CHOICE_CHILD(pod);
+ }
+
+ switch (type) {
+ case 'P':
+ return true;
+ case 'b':
+ return spa_pod_is_bool(pod);
+ case 'I':
+ return spa_pod_is_id(pod);
+ case 'i':
+ return spa_pod_is_int(pod);
+ case 'l':
+ return spa_pod_is_long(pod);
+ case 'f':
+ return spa_pod_is_float(pod);
+ case 'd':
+ return spa_pod_is_double(pod);
+ case 's':
+ return spa_pod_is_string(pod) || spa_pod_is_none(pod);
+ case 'S':
+ return spa_pod_is_string(pod);
+ case 'y':
+ return spa_pod_is_bytes(pod);
+ case 'R':
+ return spa_pod_is_rectangle(pod);
+ case 'F':
+ return spa_pod_is_fraction(pod);
+ case 'B':
+ return spa_pod_is_bitmap(pod);
+ case 'a':
+ return spa_pod_is_array(pod);
+ case 'p':
+ return spa_pod_is_pointer(pod);
+ case 'h':
+ return spa_pod_is_fd(pod);
+ case 'T':
+ return spa_pod_is_struct(pod) || spa_pod_is_none(pod);
+ case 'O':
+ return spa_pod_is_object(pod) || spa_pod_is_none(pod);
+ case 'V':
+ default:
+ return false;
+ }
+}
+
+#define SPA_POD_PARSER_COLLECT(pod,_type,args) \
+do { \
+ switch (_type) { \
+ case 'b': \
+ *va_arg(args, bool*) = SPA_POD_VALUE(struct spa_pod_bool, pod); \
+ break; \
+ case 'I': \
+ case 'i': \
+ *va_arg(args, int32_t*) = SPA_POD_VALUE(struct spa_pod_int, pod); \
+ break; \
+ case 'l': \
+ *va_arg(args, int64_t*) = SPA_POD_VALUE(struct spa_pod_long, pod); \
+ break; \
+ case 'f': \
+ *va_arg(args, float*) = SPA_POD_VALUE(struct spa_pod_float, pod); \
+ break; \
+ case 'd': \
+ *va_arg(args, double*) = SPA_POD_VALUE(struct spa_pod_double, pod); \
+ break; \
+ case 's': \
+ *va_arg(args, char**) = \
+ ((pod) == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None) \
+ ? NULL \
+ : (char *)SPA_POD_CONTENTS(struct spa_pod_string, pod)); \
+ break; \
+ case 'S': \
+ { \
+ char *dest = va_arg(args, char*); \
+ uint32_t maxlen = va_arg(args, uint32_t); \
+ strncpy(dest, (char *)SPA_POD_CONTENTS(struct spa_pod_string, pod), maxlen-1); \
+ dest[maxlen-1] = '\0'; \
+ break; \
+ } \
+ case 'y': \
+ *(va_arg(args, void **)) = SPA_POD_CONTENTS(struct spa_pod_bytes, pod); \
+ *(va_arg(args, uint32_t *)) = SPA_POD_BODY_SIZE(pod); \
+ break; \
+ case 'R': \
+ *va_arg(args, struct spa_rectangle*) = \
+ SPA_POD_VALUE(struct spa_pod_rectangle, pod); \
+ break; \
+ case 'F': \
+ *va_arg(args, struct spa_fraction*) = \
+ SPA_POD_VALUE(struct spa_pod_fraction, pod); \
+ break; \
+ case 'B': \
+ *va_arg(args, uint32_t **) = \
+ (uint32_t *) SPA_POD_CONTENTS(struct spa_pod_bitmap, pod); \
+ break; \
+ case 'a': \
+ *va_arg(args, uint32_t*) = SPA_POD_ARRAY_VALUE_SIZE(pod); \
+ *va_arg(args, uint32_t*) = SPA_POD_ARRAY_VALUE_TYPE(pod); \
+ *va_arg(args, uint32_t*) = SPA_POD_ARRAY_N_VALUES(pod); \
+ *va_arg(args, void**) = SPA_POD_ARRAY_VALUES(pod); \
+ break; \
+ case 'p': \
+ { \
+ struct spa_pod_pointer_body *b = \
+ (struct spa_pod_pointer_body *) SPA_POD_BODY(pod); \
+ *(va_arg(args, uint32_t *)) = b->type; \
+ *(va_arg(args, const void **)) = b->value; \
+ break; \
+ } \
+ case 'h': \
+ *va_arg(args, int64_t*) = SPA_POD_VALUE(struct spa_pod_fd, pod); \
+ break; \
+ case 'P': \
+ case 'T': \
+ case 'O': \
+ case 'V': \
+ { \
+ const struct spa_pod **d = va_arg(args, const struct spa_pod**); \
+ if (d) \
+ *d = ((pod) == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None) \
+ ? NULL : (pod)); \
+ break; \
+ } \
+ default: \
+ break; \
+ } \
+} while(false)
+
+#define SPA_POD_PARSER_SKIP(_type,args) \
+do { \
+ switch (_type) { \
+ case 'S': \
+ va_arg(args, char*); \
+ va_arg(args, uint32_t); \
+ break; \
+ case 'a': \
+ va_arg(args, void*); \
+ va_arg(args, void*); \
+ SPA_FALLTHROUGH \
+ case 'p': \
+ case 'y': \
+ va_arg(args, void*); \
+ SPA_FALLTHROUGH \
+ case 'b': \
+ case 'I': \
+ case 'i': \
+ case 'l': \
+ case 'f': \
+ case 'd': \
+ case 's': \
+ case 'R': \
+ case 'F': \
+ case 'B': \
+ case 'h': \
+ case 'V': \
+ case 'P': \
+ case 'T': \
+ case 'O': \
+ va_arg(args, void*); \
+ break; \
+ } \
+} while(false)
+
+static inline 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;
+ const struct spa_pod_prop *prop = NULL;
+ int count = 0;
+
+ do {
+ bool optional;
+ const struct spa_pod *pod = NULL;
+ const char *format;
+
+ if (ftype == SPA_TYPE_Object) {
+ uint32_t key = va_arg(args, uint32_t);
+ const struct spa_pod_object *object;
+
+ if (key == 0)
+ break;
+
+ object = (const struct spa_pod_object *)spa_pod_parser_frame(parser, f);
+ prop = spa_pod_object_find_prop(object, prop, key);
+ pod = prop ? &prop->value : NULL;
+ }
+
+ if ((format = va_arg(args, char *)) == NULL)
+ break;
+
+ if (ftype == SPA_TYPE_Struct)
+ pod = spa_pod_parser_next(parser);
+
+ if ((optional = (*format == '?')))
+ format++;
+
+ if (!spa_pod_parser_can_collect(pod, *format)) {
+ if (!optional) {
+ if (pod == NULL)
+ return -ESRCH;
+ else
+ return -EPROTO;
+ }
+ SPA_POD_PARSER_SKIP(*format, args);
+ } else {
+ if (pod->type == SPA_TYPE_Choice && *format != 'V')
+ pod = SPA_POD_CHOICE_CHILD(pod);
+
+ SPA_POD_PARSER_COLLECT(pod, *format, args);
+ count++;
+ }
+ } while (true);
+
+ return count;
+}
+
+static inline int spa_pod_parser_get(struct spa_pod_parser *parser, ...)
+{
+ int res;
+ va_list args;
+
+ va_start(args, parser);
+ res = spa_pod_parser_getv(parser, args);
+ va_end(args);
+
+ return res;
+}
+
+#define SPA_POD_OPT_Bool(val) "?" SPA_POD_Bool(val)
+#define SPA_POD_OPT_Id(val) "?" SPA_POD_Id(val)
+#define SPA_POD_OPT_Int(val) "?" SPA_POD_Int(val)
+#define SPA_POD_OPT_Long(val) "?" SPA_POD_Long(val)
+#define SPA_POD_OPT_Float(val) "?" SPA_POD_Float(val)
+#define SPA_POD_OPT_Double(val) "?" SPA_POD_Double(val)
+#define SPA_POD_OPT_String(val) "?" SPA_POD_String(val)
+#define SPA_POD_OPT_Stringn(val,len) "?" SPA_POD_Stringn(val,len)
+#define SPA_POD_OPT_Bytes(val,len) "?" SPA_POD_Bytes(val,len)
+#define SPA_POD_OPT_Rectangle(val) "?" SPA_POD_Rectangle(val)
+#define SPA_POD_OPT_Fraction(val) "?" SPA_POD_Fraction(val)
+#define SPA_POD_OPT_Array(csize,ctype,n_vals,vals) "?" SPA_POD_Array(csize,ctype,n_vals,vals)
+#define SPA_POD_OPT_Pointer(type,val) "?" SPA_POD_Pointer(type,val)
+#define SPA_POD_OPT_Fd(val) "?" SPA_POD_Fd(val)
+#define SPA_POD_OPT_Pod(val) "?" SPA_POD_Pod(val)
+#define SPA_POD_OPT_PodObject(val) "?" SPA_POD_PodObject(val)
+#define SPA_POD_OPT_PodStruct(val) "?" SPA_POD_PodStruct(val)
+#define SPA_POD_OPT_PodChoice(val) "?" SPA_POD_PodChoice(val)
+
+#define spa_pod_parser_get_object(p,type,id,...) \
+({ \
+ struct spa_pod_frame _f; \
+ int _res; \
+ if ((_res = spa_pod_parser_push_object(p, &_f, type, id)) == 0) { \
+ _res = spa_pod_parser_get(p,##__VA_ARGS__, 0); \
+ spa_pod_parser_pop(p, &_f); \
+ } \
+ _res; \
+})
+
+#define spa_pod_parser_get_struct(p,...) \
+({ \
+ struct spa_pod_frame _f; \
+ int _res; \
+ if ((_res = spa_pod_parser_push_struct(p, &_f)) == 0) { \
+ _res = spa_pod_parser_get(p,##__VA_ARGS__, NULL); \
+ spa_pod_parser_pop(p, &_f); \
+ } \
+ _res; \
+})
+
+#define spa_pod_parse_object(pod,type,id,...) \
+({ \
+ struct spa_pod_parser _p; \
+ spa_pod_parser_pod(&_p, pod); \
+ spa_pod_parser_get_object(&_p,type,id,##__VA_ARGS__); \
+})
+
+#define spa_pod_parse_struct(pod,...) \
+({ \
+ struct spa_pod_parser _p; \
+ spa_pod_parser_pod(&_p, pod); \
+ spa_pod_parser_get_struct(&_p,##__VA_ARGS__); \
+})
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_POD_PARSER_H */
diff --git a/spa/include/spa/pod/pod.h b/spa/include/spa/pod/pod.h
new file mode 100644
index 0000000..1864b19
--- /dev/null
+++ b/spa/include/spa/pod/pod.h
@@ -0,0 +1,246 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_POD_H
+#define SPA_POD_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/type.h>
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+#define SPA_POD_BODY_SIZE(pod) (((struct spa_pod*)(pod))->size)
+#define SPA_POD_TYPE(pod) (((struct spa_pod*)(pod))->type)
+#define SPA_POD_SIZE(pod) ((uint64_t)sizeof(struct spa_pod) + SPA_POD_BODY_SIZE(pod))
+#define SPA_POD_CONTENTS_SIZE(type,pod) (SPA_POD_SIZE(pod)-sizeof(type))
+
+#define SPA_POD_CONTENTS(type,pod) SPA_PTROFF((pod),sizeof(type),void)
+#define SPA_POD_CONTENTS_CONST(type,pod) SPA_PTROFF((pod),sizeof(type),const void)
+#define SPA_POD_BODY(pod) SPA_PTROFF((pod),sizeof(struct spa_pod),void)
+#define SPA_POD_BODY_CONST(pod) SPA_PTROFF((pod),sizeof(struct spa_pod),const void)
+
+struct spa_pod {
+ uint32_t size; /* size of the body */
+ uint32_t type; /* a basic id of enum spa_type */
+};
+
+#define SPA_POD_VALUE(type,pod) (((type*)(pod))->value)
+
+struct spa_pod_bool {
+ struct spa_pod pod;
+ int32_t value;
+ int32_t _padding;
+};
+
+struct spa_pod_id {
+ struct spa_pod pod;
+ uint32_t value;
+ int32_t _padding;
+};
+
+struct spa_pod_int {
+ struct spa_pod pod;
+ int32_t value;
+ int32_t _padding;
+};
+
+struct spa_pod_long {
+ struct spa_pod pod;
+ int64_t value;
+};
+
+struct spa_pod_float {
+ struct spa_pod pod;
+ float value;
+ int32_t _padding;
+};
+
+struct spa_pod_double {
+ struct spa_pod pod;
+ double value;
+};
+
+struct spa_pod_string {
+ struct spa_pod pod;
+ /* value here */
+};
+
+struct spa_pod_bytes {
+ struct spa_pod pod;
+ /* value here */
+};
+
+struct spa_pod_rectangle {
+ struct spa_pod pod;
+ struct spa_rectangle value;
+};
+
+struct spa_pod_fraction {
+ struct spa_pod pod;
+ struct spa_fraction value;
+};
+
+struct spa_pod_bitmap {
+ struct spa_pod pod;
+ /* array of uint8_t follows with the bitmap */
+};
+
+#define SPA_POD_ARRAY_CHILD(arr) (&((struct spa_pod_array*)(arr))->body.child)
+#define SPA_POD_ARRAY_VALUE_TYPE(arr) (SPA_POD_TYPE(SPA_POD_ARRAY_CHILD(arr)))
+#define SPA_POD_ARRAY_VALUE_SIZE(arr) (SPA_POD_BODY_SIZE(SPA_POD_ARRAY_CHILD(arr)))
+#define SPA_POD_ARRAY_N_VALUES(arr) (SPA_POD_ARRAY_VALUE_SIZE(arr) ? ((SPA_POD_BODY_SIZE(arr) - sizeof(struct spa_pod_array_body)) / SPA_POD_ARRAY_VALUE_SIZE(arr)) : 0)
+#define SPA_POD_ARRAY_VALUES(arr) SPA_POD_CONTENTS(struct spa_pod_array, arr)
+
+struct spa_pod_array_body {
+ struct spa_pod child;
+ /* array with elements of child.size follows */
+};
+
+struct spa_pod_array {
+ struct spa_pod pod;
+ struct spa_pod_array_body body;
+};
+
+#define SPA_POD_CHOICE_CHILD(choice) (&((struct spa_pod_choice*)(choice))->body.child)
+#define SPA_POD_CHOICE_TYPE(choice) (((struct spa_pod_choice*)(choice))->body.type)
+#define SPA_POD_CHOICE_FLAGS(choice) (((struct spa_pod_choice*)(choice))->body.flags)
+#define SPA_POD_CHOICE_VALUE_TYPE(choice) (SPA_POD_TYPE(SPA_POD_CHOICE_CHILD(choice)))
+#define SPA_POD_CHOICE_VALUE_SIZE(choice) (SPA_POD_BODY_SIZE(SPA_POD_CHOICE_CHILD(choice)))
+#define SPA_POD_CHOICE_N_VALUES(choice) (SPA_POD_CHOICE_VALUE_SIZE(choice) ? ((SPA_POD_BODY_SIZE(choice) - sizeof(struct spa_pod_choice_body)) / SPA_POD_CHOICE_VALUE_SIZE(choice)) : 0)
+#define SPA_POD_CHOICE_VALUES(choice) (SPA_POD_CONTENTS(struct spa_pod_choice, choice))
+
+enum spa_choice_type {
+ SPA_CHOICE_None, /**< no choice, first value is current */
+ SPA_CHOICE_Range, /**< range: default, min, max */
+ SPA_CHOICE_Step, /**< range with step: default, min, max, step */
+ SPA_CHOICE_Enum, /**< list: default, alternative,... */
+ SPA_CHOICE_Flags, /**< flags: default, possible flags,... */
+};
+
+struct spa_pod_choice_body {
+ uint32_t type; /**< type of choice, one of enum spa_choice_type */
+ uint32_t flags; /**< extra flags */
+ struct spa_pod child;
+ /* array with elements of child.size follows. Note that there might be more
+ * elements than required by \a type, which should be ignored. */
+};
+
+struct spa_pod_choice {
+ struct spa_pod pod;
+ struct spa_pod_choice_body body;
+};
+
+struct spa_pod_struct {
+ struct spa_pod pod;
+ /* one or more spa_pod follow */
+};
+
+#define SPA_POD_OBJECT_TYPE(obj) (((struct spa_pod_object*)(obj))->body.type)
+#define SPA_POD_OBJECT_ID(obj) (((struct spa_pod_object*)(obj))->body.id)
+
+struct spa_pod_object_body {
+ uint32_t type; /**< one of enum spa_type */
+ uint32_t id; /**< id of the object, depends on the object type */
+ /* contents follow, series of spa_pod_prop */
+};
+
+struct spa_pod_object {
+ struct spa_pod pod;
+ struct spa_pod_object_body body;
+};
+
+struct spa_pod_pointer_body {
+ uint32_t type; /**< pointer id, one of enum spa_type */
+ uint32_t _padding;
+ const void *value;
+};
+
+struct spa_pod_pointer {
+ struct spa_pod pod;
+ struct spa_pod_pointer_body body;
+};
+
+struct spa_pod_fd {
+ struct spa_pod pod;
+ int64_t value;
+};
+
+#define SPA_POD_PROP_SIZE(prop) (sizeof(struct spa_pod_prop) + (prop)->value.size)
+
+/* props can be inside an object */
+struct spa_pod_prop {
+ uint32_t key; /**< key of property, list of valid keys depends on the
+ * object type */
+#define SPA_POD_PROP_FLAG_READONLY (1u<<0) /**< is read-only */
+#define SPA_POD_PROP_FLAG_HARDWARE (1u<<1) /**< some sort of hardware parameter */
+#define SPA_POD_PROP_FLAG_HINT_DICT (1u<<2) /**< contains a dictionary struct as
+ * (Struct(
+ * Int : n_items,
+ * (String : key,
+ * String : value)*)) */
+#define SPA_POD_PROP_FLAG_MANDATORY (1u<<3) /**< is mandatory */
+#define SPA_POD_PROP_FLAG_DONT_FIXATE (1u<<4) /**< choices need no fixation */
+ uint32_t flags; /**< flags for property */
+ struct spa_pod value;
+ /* value follows */
+};
+
+#define SPA_POD_CONTROL_SIZE(ev) (sizeof(struct spa_pod_control) + (ev)->value.size)
+
+/* controls can be inside a sequence and mark timed values */
+struct spa_pod_control {
+ uint32_t offset; /**< media offset */
+ uint32_t type; /**< type of control, enum spa_control_type */
+ struct spa_pod value; /**< control value, depends on type */
+ /* value contents follow */
+};
+
+struct spa_pod_sequence_body {
+ uint32_t unit;
+ uint32_t pad;
+ /* series of struct spa_pod_control follows */
+};
+
+/** a sequence of timed controls */
+struct spa_pod_sequence {
+ struct spa_pod pod;
+ struct spa_pod_sequence_body body;
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_POD_H */
diff --git a/spa/include/spa/pod/vararg.h b/spa/include/spa/pod/vararg.h
new file mode 100644
index 0000000..53dc654
--- /dev/null
+++ b/spa/include/spa/pod/vararg.h
@@ -0,0 +1,113 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_POD_VARARG_H
+#define SPA_POD_VARARG_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+
+#include <spa/pod/pod.h>
+
+/**
+ * \addtogroup spa_pod
+ * \{
+ */
+
+#define SPA_POD_Prop(key,...) \
+ key, ##__VA_ARGS__
+
+#define SPA_POD_Control(offset,type,...) \
+ offset, type, ##__VA_ARGS__
+
+#define SPA_CHOICE_RANGE(def,min,max) 3,(def),(min),(max)
+#define SPA_CHOICE_STEP(def,min,max,step) 4,(def),(min),(max),(step)
+#define SPA_CHOICE_ENUM(n_vals,...) (n_vals),##__VA_ARGS__
+#define SPA_CHOICE_FLAGS(flags) 1, (flags)
+#define SPA_CHOICE_BOOL(def) 3,(def),(def),!(def)
+
+#define SPA_POD_Bool(val) "b", val
+#define SPA_POD_CHOICE_Bool(def) "?eb", SPA_CHOICE_BOOL(def)
+
+#define SPA_POD_Id(val) "I", val
+#define SPA_POD_CHOICE_ENUM_Id(n_vals,...) "?eI", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__)
+
+#define SPA_POD_Int(val) "i", val
+#define SPA_POD_CHOICE_ENUM_Int(n_vals,...) "?ei", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__)
+#define SPA_POD_CHOICE_RANGE_Int(def,min,max) "?ri", SPA_CHOICE_RANGE(def, min, max)
+#define SPA_POD_CHOICE_STEP_Int(def,min,max,step) "?si", SPA_CHOICE_STEP(def, min, max, step)
+#define SPA_POD_CHOICE_FLAGS_Int(flags) "?fi", SPA_CHOICE_FLAGS(flags)
+
+#define SPA_POD_Long(val) "l", val
+#define SPA_POD_CHOICE_ENUM_Long(n_vals,...) "?el", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__)
+#define SPA_POD_CHOICE_RANGE_Long(def,min,max) "?rl", SPA_CHOICE_RANGE(def, min, max)
+#define SPA_POD_CHOICE_STEP_Long(def,min,max,step) "?sl", SPA_CHOICE_STEP(def, min, max, step)
+#define SPA_POD_CHOICE_FLAGS_Long(flags) "?fl", SPA_CHOICE_FLAGS(flags)
+
+#define SPA_POD_Float(val) "f", val
+#define SPA_POD_CHOICE_ENUM_Float(n_vals,...) "?ef", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__)
+#define SPA_POD_CHOICE_RANGE_Float(def,min,max) "?rf", SPA_CHOICE_RANGE(def, min, max)
+#define SPA_POD_CHOICE_STEP_Float(def,min,max,step) "?sf", SPA_CHOICE_STEP(def, min, max, step)
+
+#define SPA_POD_Double(val) "d", val
+#define SPA_POD_CHOICE_ENUM_Double(n_vals,...) "?ed", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__)
+#define SPA_POD_CHOICE_RANGE_Double(def,min,max) "?rd", SPA_CHOICE_RANGE(def, min, max)
+#define SPA_POD_CHOICE_STEP_Double(def,min,max,step) "?sd", SPA_CHOICE_STEP(def, min, max, step)
+
+#define SPA_POD_String(val) "s",val
+#define SPA_POD_Stringn(val,len) "S",val,len
+
+#define SPA_POD_Bytes(val,len) "y",val,len
+
+#define SPA_POD_Rectangle(val) "R",val
+#define SPA_POD_CHOICE_ENUM_Rectangle(n_vals,...) "?eR", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__)
+#define SPA_POD_CHOICE_RANGE_Rectangle(def,min,max) "?rR", SPA_CHOICE_RANGE((def),(min),(max))
+#define SPA_POD_CHOICE_STEP_Rectangle(def,min,max,step) "?sR", SPA_CHOICE_STEP((def),(min),(max),(step))
+
+#define SPA_POD_Fraction(val) "F",val
+#define SPA_POD_CHOICE_ENUM_Fraction(n_vals,...) "?eF", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__)
+#define SPA_POD_CHOICE_RANGE_Fraction(def,min,max) "?rF", SPA_CHOICE_RANGE((def),(min),(max))
+#define SPA_POD_CHOICE_STEP_Fraction(def,min,max,step) "?sF", SPA_CHOICE_STEP(def, min, max, step)
+
+#define SPA_POD_Array(csize,ctype,n_vals,vals) "a", csize,ctype,n_vals,vals
+#define SPA_POD_Pointer(type,val) "p", type,val
+#define SPA_POD_Fd(val) "h", val
+#define SPA_POD_None() "P", NULL
+#define SPA_POD_Pod(val) "P", val
+#define SPA_POD_PodObject(val) "O", val
+#define SPA_POD_PodStruct(val) "T", val
+#define SPA_POD_PodChoice(val) "V", val
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_POD_VARARG_H */
diff --git a/spa/include/spa/support/cpu.h b/spa/include/spa/support/cpu.h
new file mode 100644
index 0000000..39a82f4
--- /dev/null
+++ b/spa/include/spa/support/cpu.h
@@ -0,0 +1,168 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_CPU_H
+#define SPA_CPU_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+/** \defgroup spa_cpu CPU
+ * Querying CPU properties
+ */
+
+/**
+ * \addtogroup spa_cpu
+ * \{
+ */
+
+/**
+ * The CPU features interface
+ */
+#define SPA_TYPE_INTERFACE_CPU SPA_TYPE_INFO_INTERFACE_BASE "CPU"
+
+#define SPA_VERSION_CPU 0
+struct spa_cpu { struct spa_interface iface; };
+
+/* x86 specific */
+#define SPA_CPU_FLAG_MMX (1<<0) /**< standard MMX */
+#define SPA_CPU_FLAG_MMXEXT (1<<1) /**< SSE integer or AMD MMX ext */
+#define SPA_CPU_FLAG_3DNOW (1<<2) /**< AMD 3DNOW */
+#define SPA_CPU_FLAG_SSE (1<<3) /**< SSE */
+#define SPA_CPU_FLAG_SSE2 (1<<4) /**< SSE2 */
+#define SPA_CPU_FLAG_3DNOWEXT (1<<5) /**< AMD 3DNowExt */
+#define SPA_CPU_FLAG_SSE3 (1<<6) /**< Prescott SSE3 */
+#define SPA_CPU_FLAG_SSSE3 (1<<7) /**< Conroe SSSE3 */
+#define SPA_CPU_FLAG_SSE41 (1<<8) /**< Penryn SSE4.1 */
+#define SPA_CPU_FLAG_SSE42 (1<<9) /**< Nehalem SSE4.2 */
+#define SPA_CPU_FLAG_AESNI (1<<10) /**< Advanced Encryption Standard */
+#define SPA_CPU_FLAG_AVX (1<<11) /**< AVX */
+#define SPA_CPU_FLAG_XOP (1<<12) /**< Bulldozer XOP */
+#define SPA_CPU_FLAG_FMA4 (1<<13) /**< Bulldozer FMA4 */
+#define SPA_CPU_FLAG_CMOV (1<<14) /**< supports cmov */
+#define SPA_CPU_FLAG_AVX2 (1<<15) /**< AVX2 */
+#define SPA_CPU_FLAG_FMA3 (1<<16) /**< Haswell FMA3 */
+#define SPA_CPU_FLAG_BMI1 (1<<17) /**< Bit Manipulation Instruction Set 1 */
+#define SPA_CPU_FLAG_BMI2 (1<<18) /**< Bit Manipulation Instruction Set 2 */
+#define SPA_CPU_FLAG_AVX512 (1<<19) /**< AVX-512 */
+#define SPA_CPU_FLAG_SLOW_UNALIGNED (1<<20) /**< unaligned loads/stores are slow */
+
+/* PPC specific */
+#define SPA_CPU_FLAG_ALTIVEC (1<<0) /**< standard */
+#define SPA_CPU_FLAG_VSX (1<<1) /**< ISA 2.06 */
+#define SPA_CPU_FLAG_POWER8 (1<<2) /**< ISA 2.07 */
+
+/* ARM specific */
+#define SPA_CPU_FLAG_ARMV5TE (1 << 0)
+#define SPA_CPU_FLAG_ARMV6 (1 << 1)
+#define SPA_CPU_FLAG_ARMV6T2 (1 << 2)
+#define SPA_CPU_FLAG_VFP (1 << 3)
+#define SPA_CPU_FLAG_VFPV3 (1 << 4)
+#define SPA_CPU_FLAG_NEON (1 << 5)
+#define SPA_CPU_FLAG_ARMV8 (1 << 6)
+
+#define SPA_CPU_FORCE_AUTODETECT ((uint32_t)-1)
+
+#define SPA_CPU_VM_NONE (0)
+#define SPA_CPU_VM_OTHER (1 << 0)
+#define SPA_CPU_VM_KVM (1 << 1)
+#define SPA_CPU_VM_QEMU (1 << 2)
+#define SPA_CPU_VM_BOCHS (1 << 3)
+#define SPA_CPU_VM_XEN (1 << 4)
+#define SPA_CPU_VM_UML (1 << 5)
+#define SPA_CPU_VM_VMWARE (1 << 6)
+#define SPA_CPU_VM_ORACLE (1 << 7)
+#define SPA_CPU_VM_MICROSOFT (1 << 8)
+#define SPA_CPU_VM_ZVM (1 << 9)
+#define SPA_CPU_VM_PARALLELS (1 << 10)
+#define SPA_CPU_VM_BHYVE (1 << 11)
+#define SPA_CPU_VM_QNX (1 << 12)
+#define SPA_CPU_VM_ACRN (1 << 13)
+#define SPA_CPU_VM_POWERVM (1 << 14)
+
+/**
+ * methods
+ */
+struct spa_cpu_methods {
+ /** the version of the methods. This can be used to expand this
+ structure in the future */
+#define SPA_VERSION_CPU_METHODS 2
+ uint32_t version;
+
+ /** get CPU flags */
+ uint32_t (*get_flags) (void *object);
+
+ /** force CPU flags, use SPA_CPU_FORCE_AUTODETECT to autodetect CPU flags */
+ int (*force_flags) (void *object, uint32_t flags);
+
+ /** get number of CPU cores */
+ uint32_t (*get_count) (void *object);
+
+ /** get maximum required alignment of data */
+ uint32_t (*get_max_align) (void *object);
+
+ /* check if running in a VM. Since:1 */
+ uint32_t (*get_vm_type) (void *object);
+
+ /* denormals will be handled as zero, either with FTZ or DAZ.
+ * Since:2 */
+ 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)
+
+/** keys can be given when initializing the cpu handle */
+#define SPA_KEY_CPU_FORCE "cpu.force" /**< force cpu flags */
+#define SPA_KEY_CPU_VM_TYPE "cpu.vm.type" /**< force a VM type */
+#define SPA_KEY_CPU_ZERO_DENORMALS "cpu.zero.denormals" /**< zero denormals */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_CPU_H */
diff --git a/spa/include/spa/support/dbus.h b/spa/include/spa/support/dbus.h
new file mode 100644
index 0000000..9ebb7d8
--- /dev/null
+++ b/spa/include/spa/support/dbus.h
@@ -0,0 +1,167 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DBUS_H
+#define SPA_DBUS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/support/loop.h>
+
+/** \defgroup spa_dbus DBus
+ * DBus communication
+ */
+
+/**
+ * \addtogroup spa_dbus
+ * \{
+ */
+
+#define SPA_TYPE_INTERFACE_DBus SPA_TYPE_INFO_INTERFACE_BASE "DBus"
+
+#define SPA_VERSION_DBUS 0
+struct spa_dbus { struct spa_interface iface; };
+
+enum spa_dbus_type {
+ SPA_DBUS_TYPE_SESSION, /**< The login session bus */
+ SPA_DBUS_TYPE_SYSTEM, /**< The systemwide bus */
+ SPA_DBUS_TYPE_STARTER /**< The bus that started us, if any */
+};
+
+#define SPA_DBUS_CONNECTION_EVENT_DESTROY 0
+#define SPA_DBUS_CONNECTION_EVENT_DISCONNECTED 1
+#define SPA_DBUS_CONNECTION_EVENT_NUM 2
+
+struct spa_dbus_connection_events {
+#define SPA_VERSION_DBUS_CONNECTION_EVENTS 0
+ uint32_t version;
+
+ /* a connection is destroyed */
+ void (*destroy) (void *data);
+
+ /* a connection disconnected */
+ void (*disconnected) (void *data);
+};
+
+struct spa_dbus_connection {
+#define SPA_VERSION_DBUS_CONNECTION 1
+ uint32_t version;
+ /**
+ * Get the DBusConnection from a wrapper
+ *
+ * Note that the returned handle is closed and unref'd by spa_dbus
+ * immediately before emitting the asynchronous "disconnected" event.
+ * The caller must either deal with the invalidation, or keep an extra
+ * ref on the handle returned.
+ *
+ * \param conn the spa_dbus_connection wrapper
+ * \return a pointer of type DBusConnection
+ */
+ void *(*get) (struct spa_dbus_connection *conn);
+ /**
+ * Destroy a dbus connection wrapper
+ *
+ * \param conn the wrapper to destroy
+ */
+ void (*destroy) (struct spa_dbus_connection *conn);
+
+ /**
+ * Add a listener for events
+ *
+ * Since version 1
+ */
+ void (*add_listener) (struct spa_dbus_connection *conn,
+ struct spa_hook *listener,
+ const struct spa_dbus_connection_events *events,
+ 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)
+/** \copydoc spa_dbus_connection.destroy
+ * \sa spa_dbus_connection.destroy */
+#define spa_dbus_connection_destroy(c) spa_dbus_connection_call(c,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__)
+
+struct spa_dbus_methods {
+#define SPA_VERSION_DBUS_METHODS 0
+ uint32_t version;
+
+ /**
+ * Get a new connection wrapper for the given bus type.
+ *
+ * The connection wrapper is completely configured to operate
+ * in the main context of the handle that manages the spa_dbus
+ * interface.
+ *
+ * \param dbus the dbus manager
+ * \param type the bus type to wrap
+ * \param error location for the DBusError
+ * \return a new dbus connection wrapper or NULL on error
+ */
+ struct spa_dbus_connection * (*get_connection) (void *object,
+ enum spa_dbus_type type);
+};
+
+/** \copydoc spa_dbus_methods.get_connection
+ * \sa spa_dbus_methods.get_connection
+ */
+static inline 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;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DBUS_H */
diff --git a/spa/include/spa/support/i18n.h b/spa/include/spa/support/i18n.h
new file mode 100644
index 0000000..fc45b07
--- /dev/null
+++ b/spa/include/spa/support/i18n.h
@@ -0,0 +1,107 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_I18N_H
+#define SPA_I18N_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+#include <spa/utils/defs.h>
+
+/** \defgroup spa_i18n I18N
+ * Gettext interface
+ */
+
+/**
+ * \addtogroup spa_i18n
+ * \{
+ */
+
+#define SPA_TYPE_INTERFACE_I18N SPA_TYPE_INFO_INTERFACE_BASE "I18N"
+
+#define SPA_VERSION_I18N 0
+struct spa_i18n { struct spa_interface iface; };
+
+struct spa_i18n_methods {
+#define SPA_VERSION_I18N_METHODS 0
+ uint32_t version;
+
+ /**
+ * Translate a message
+ *
+ * \param object the i18n interface
+ * \param msgid the message
+ * \return a translated message
+ */
+ const char *(*text) (void *object, const char *msgid);
+
+ /**
+ * Translate a message for a number
+ *
+ * \param object the i18n interface
+ * \param msgid the message to translate
+ * \param msgid_plural the plural form of \a msgid
+ * \param n a number
+ * \return a translated message for the number \a n
+ */
+ const char *(*ntext) (void *object, const char *msgid,
+ const char *msgid_plural, unsigned long int n);
+};
+
+SPA_FORMAT_ARG_FUNC(2)
+static inline 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;
+}
+
+static inline 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;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_I18N_H */
diff --git a/spa/include/spa/support/log-impl.h b/spa/include/spa/support/log-impl.h
new file mode 100644
index 0000000..e1ee21d
--- /dev/null
+++ b/spa/include/spa/support/log-impl.h
@@ -0,0 +1,143 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_LOG_IMPL_H
+#define SPA_LOG_IMPL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+#include <spa/utils/type.h>
+#include <spa/support/log.h>
+
+/**
+ * \addtogroup spa_log
+ * \{
+ */
+
+static inline SPA_PRINTF_FUNC(7, 0) void spa_log_impl_logtv(void *object,
+ 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)
+{
+ static const char * const levels[] = { "-", "E", "W", "I", "D", "T" };
+
+ const char *basename = strrchr(file, '/');
+ char text[512], location[1024];
+ char topicstr[32] = {0};
+
+ if (basename)
+ basename += 1; /* skip '/' */
+ else
+ basename = file; /* use whole string if no '/' is found */
+
+ if (topic && topic->topic)
+ snprintf(topicstr, sizeof(topicstr), " %s ", topic->topic);
+ vsnprintf(text, sizeof(text), fmt, args);
+ snprintf(location, sizeof(location), "[%s]%s[%s:%i %s()] %s\n",
+ levels[level],
+ topicstr,
+ basename, line, func, text);
+ fputs(location, stderr);
+}
+
+static inline SPA_PRINTF_FUNC(7,8) void spa_log_impl_logt(void *object,
+ 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;
+ va_start(args, fmt);
+ spa_log_impl_logtv(object, level, topic, file, line, func, fmt, args);
+ va_end(args);
+}
+
+static inline SPA_PRINTF_FUNC(6, 0) void spa_log_impl_logv(void *object,
+ enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt,
+ va_list args)
+{
+
+ spa_log_impl_logtv(object, level, NULL, file, line, func, fmt, args);
+}
+
+static inline SPA_PRINTF_FUNC(6,7) void spa_log_impl_log(void *object,
+ enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ spa_log_impl_logv(object, level, file, line, func, fmt, args);
+ va_end(args);
+}
+
+static inline void spa_log_impl_topic_init(void *object, struct spa_log_topic *topic)
+{
+ /* noop */
+}
+
+#define SPA_LOG_IMPL_DEFINE(name) \
+struct { \
+ struct spa_log log; \
+ struct spa_log_methods methods; \
+} name
+
+#define SPA_LOG_IMPL_INIT(name) \
+ { { { SPA_TYPE_INTERFACE_Log, SPA_VERSION_LOG, \
+ SPA_CALLBACKS_INIT(&(name).methods, &(name)) }, \
+ SPA_LOG_LEVEL_INFO, }, \
+ { SPA_VERSION_LOG_METHODS, \
+ spa_log_impl_log, \
+ spa_log_impl_logv, \
+ spa_log_impl_logt, \
+ spa_log_impl_logtv, \
+ } }
+
+#define SPA_LOG_IMPL(name) \
+ SPA_LOG_IMPL_DEFINE(name) = SPA_LOG_IMPL_INIT(name)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+#endif /* SPA_LOG_IMPL_H */
diff --git a/spa/include/spa/support/log.h b/spa/include/spa/support/log.h
new file mode 100644
index 0000000..74818a4
--- /dev/null
+++ b/spa/include/spa/support/log.h
@@ -0,0 +1,321 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_LOG_H
+#define SPA_LOG_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+
+#include <spa/utils/type.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+/** \defgroup spa_log Log
+ * Logging interface
+ */
+
+/**
+ * \addtogroup spa_log
+ * \{
+ */
+
+/** The default log topic. Redefine this in your code to
+ * allow for the spa_log_* macros to work correctly, e.g:
+ *
+ * \code{.c}
+ * struct spa_log_topic *mylogger;
+ * #undef SPA_LOG_TOPIC_DEFAULT
+ * #define SPA_LOG_TOPIC_DEFAULT mylogger
+ * \endcode
+ */
+#define SPA_LOG_TOPIC_DEFAULT NULL
+
+enum spa_log_level {
+ SPA_LOG_LEVEL_NONE = 0,
+ SPA_LOG_LEVEL_ERROR,
+ SPA_LOG_LEVEL_WARN,
+ SPA_LOG_LEVEL_INFO,
+ SPA_LOG_LEVEL_DEBUG,
+ SPA_LOG_LEVEL_TRACE,
+};
+
+/**
+ * The Log interface
+ */
+#define SPA_TYPE_INTERFACE_Log SPA_TYPE_INFO_INTERFACE_BASE "Log"
+
+
+struct spa_log {
+ /** the version of this log. This can be used to expand this
+ * structure in the future */
+#define SPA_VERSION_LOG 0
+ struct spa_interface iface;
+ /**
+ * Logging level, everything above this level is not logged
+ */
+ enum spa_log_level level;
+};
+
+/**
+ * \struct spa_log_topic
+ *
+ * Identifier for a topic. Topics are string-based filters that logically
+ * group messages together. An implementation may decide to filter different
+ * topics on different levels, for example the "protocol" topic may require
+ * debug level TRACE while the "core" topic defaults to debug level INFO.
+ *
+ * spa_log_topics require a spa_log_methods version of 1 or higher.
+ */
+struct spa_log_topic {
+#define SPA_VERSION_LOG_TOPIC 0
+ /** the version of this topic. This can be used to expand this
+ * structure in the future */
+ uint32_t version;
+ /** The string identifier for the topic */
+ const char *topic;
+ /** Logging level set for this topic */
+ enum spa_log_level level;
+ /** False if this topic follows the \ref spa_log level */
+ bool has_custom_level;
+};
+
+struct spa_log_methods {
+#define SPA_VERSION_LOG_METHODS 1
+ uint32_t version;
+ /**
+ * Log a message with the given log level.
+ *
+ * \note If compiled with this header, this function is only called
+ * for implementations of version 0. For versions 1 and above, see
+ * logt() instead.
+ *
+ * \param log a spa_log
+ * \param level a spa_log_level
+ * \param file the file name
+ * \param line the line number
+ * \param func the function name
+ * \param fmt printf style format
+ * \param ... format arguments
+ */
+ void (*log) (void *object,
+ enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt, ...) SPA_PRINTF_FUNC(6, 7);
+
+ /**
+ * Log a message with the given log level.
+ *
+ * \note If compiled with this header, this function is only called
+ * for implementations of version 0. For versions 1 and above, see
+ * logtv() instead.
+ *
+ * \param log a spa_log
+ * \param level a spa_log_level
+ * \param file the file name
+ * \param line the line number
+ * \param func the function name
+ * \param fmt printf style format
+ * \param args format arguments
+ */
+ void (*logv) (void *object,
+ enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt,
+ va_list args) SPA_PRINTF_FUNC(6, 0);
+ /**
+ * Log a message with the given log level for the given topic.
+ *
+ * \note Callers that do not use topic-based logging (version 0), the \a
+ * topic is NULL
+ *
+ * \param log a spa_log
+ * \param level a spa_log_level
+ * \param topic the topic for this message, may be NULL
+ * \param file the file name
+ * \param line the line number
+ * \param func the function name
+ * \param fmt printf style format
+ * \param ... format arguments
+ *
+ * \since 1
+ */
+ void (*logt) (void *object,
+ enum spa_log_level level,
+ const struct spa_log_topic *topic,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt, ...) SPA_PRINTF_FUNC(7, 8);
+
+ /**
+ * Log a message with the given log level for the given topic.
+ *
+ * \note For callers that do not use topic-based logging (version 0),
+ * the \a topic is NULL
+ *
+ * \param log a spa_log
+ * \param level a spa_log_level
+ * \param topic the topic for this message, may be NULL
+ * \param file the file name
+ * \param line the line number
+ * \param func the function name
+ * \param fmt printf style format
+ * \param args format arguments
+ *
+ * \since 1
+ */
+ void (*logtv) (void *object,
+ 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) SPA_PRINTF_FUNC(7, 0);
+
+ /**
+ * Initializes a \ref spa_log_topic to the correct logging level.
+ *
+ * \since 1
+ */
+ void (*topic_init) (void *object, struct spa_log_topic *topic);
+};
+
+
+#define SPA_LOG_TOPIC(v, t) \
+ (struct spa_log_topic){ .version = (v), .topic = (t)}
+
+#define spa_log_topic_init(l, topic) \
+do { \
+ struct spa_log *_l = l; \
+ if (SPA_LIKELY(_l)) { \
+ struct spa_interface *_if = &_l->iface; \
+ spa_interface_call(_if, struct spa_log_methods, \
+ topic_init, 1, topic); \
+ } \
+} while(0)
+
+/* Unused, left for backwards compat */
+#define spa_log_level_enabled(l,lev) ((l) && (l)->level >= (lev))
+
+#define spa_log_level_topic_enabled(l,topic,lev) \
+({ \
+ struct spa_log *_log = l; \
+ enum spa_log_level _lev = _log ? _log->level : SPA_LOG_LEVEL_NONE; \
+ struct spa_log_topic *_t = (struct spa_log_topic *)(topic); \
+ if (_t && _t->has_custom_level) \
+ _lev = _t->level; \
+ _lev >= (lev); \
+})
+
+/* Transparently calls to version 0 log if v1 is not supported */
+#define spa_log_logt(l,lev,topic,...) \
+({ \
+ struct spa_log *_l = l; \
+ struct spa_interface *_if = &_l->iface; \
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(_l, topic, lev))) { \
+ if (!spa_interface_call(_if, \
+ struct spa_log_methods, logt, 1, \
+ lev, topic, \
+ __VA_ARGS__)) \
+ spa_interface_call(_if, \
+ struct spa_log_methods, log, 0, \
+ lev, __VA_ARGS__); \
+ } \
+})
+
+/* Transparently calls to version 0 logv if v1 is not supported */
+#define spa_log_logtv(l,lev,topic,...) \
+({ \
+ struct spa_log *_l = l; \
+ struct spa_interface *_if = &_l->iface; \
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(_l, topic, lev))) { \
+ 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__); \
+ } \
+})
+
+#define spa_logt_lev(l,lev,t,...) \
+ spa_log_logt(l,lev,t,__FILE__,__LINE__,__func__,__VA_ARGS__)
+
+#define spa_log_lev(l,lev,...) \
+ spa_logt_lev(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__)
+
+#define spa_log_log(l,lev,...) \
+ spa_log_logt(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__)
+
+#define spa_log_logv(l,lev,...) \
+ spa_log_logtv(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__)
+
+#define spa_log_error(l,...) spa_log_lev(l,SPA_LOG_LEVEL_ERROR,__VA_ARGS__)
+#define spa_log_warn(l,...) spa_log_lev(l,SPA_LOG_LEVEL_WARN,__VA_ARGS__)
+#define spa_log_info(l,...) spa_log_lev(l,SPA_LOG_LEVEL_INFO,__VA_ARGS__)
+#define spa_log_debug(l,...) spa_log_lev(l,SPA_LOG_LEVEL_DEBUG,__VA_ARGS__)
+#define spa_log_trace(l,...) spa_log_lev(l,SPA_LOG_LEVEL_TRACE,__VA_ARGS__)
+
+#define spa_logt_error(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_ERROR,t,__VA_ARGS__)
+#define spa_logt_warn(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_WARN,t,__VA_ARGS__)
+#define spa_logt_info(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_INFO,t,__VA_ARGS__)
+#define spa_logt_debug(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_DEBUG,t,__VA_ARGS__)
+#define spa_logt_trace(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_TRACE,t,__VA_ARGS__)
+
+#ifndef FASTPATH
+#define spa_log_trace_fp(l,...) spa_log_lev(l,SPA_LOG_LEVEL_TRACE,__VA_ARGS__)
+#else
+#define spa_log_trace_fp(l,...)
+#endif
+
+
+/** \fn spa_log_error */
+
+/** keys can be given when initializing the logger handle */
+#define SPA_KEY_LOG_LEVEL "log.level" /**< the default log level */
+#define SPA_KEY_LOG_COLORS "log.colors" /**< enable colors in the logger */
+#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_LINE "log.line" /**< log file and line numbers */
+#define SPA_KEY_LOG_PATTERNS "log.patterns" /**< Spa:String:JSON array of [ {"pattern" : level}, ... ] */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+#endif /* SPA_LOG_H */
diff --git a/spa/include/spa/support/loop.h b/spa/include/spa/support/loop.h
new file mode 100644
index 0000000..3c93573
--- /dev/null
+++ b/spa/include/spa/support/loop.h
@@ -0,0 +1,327 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_LOOP_H
+#define SPA_LOOP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/support/system.h>
+
+/** \defgroup spa_loop Loop
+ * Event loop interface
+ */
+
+/**
+ * \addtogroup spa_loop
+ * \{
+ */
+
+#define SPA_TYPE_INTERFACE_Loop SPA_TYPE_INFO_INTERFACE_BASE "Loop"
+#define SPA_TYPE_INTERFACE_DataLoop SPA_TYPE_INFO_INTERFACE_BASE "DataLoop"
+#define SPA_VERSION_LOOP 0
+struct spa_loop { struct spa_interface iface; };
+
+#define SPA_TYPE_INTERFACE_LoopControl SPA_TYPE_INFO_INTERFACE_BASE "LoopControl"
+#define SPA_VERSION_LOOP_CONTROL 0
+struct spa_loop_control { struct spa_interface iface; };
+
+#define SPA_TYPE_INTERFACE_LoopUtils SPA_TYPE_INFO_INTERFACE_BASE "LoopUtils"
+#define SPA_VERSION_LOOP_UTILS 0
+struct spa_loop_utils { struct spa_interface iface; };
+
+struct spa_source;
+
+typedef void (*spa_source_func_t) (struct spa_source *source);
+
+struct spa_source {
+ struct spa_loop *loop;
+ spa_source_func_t func;
+ void *data;
+ int fd;
+ uint32_t mask;
+ uint32_t rmask;
+ /* private data for the loop implementer */
+ void *priv;
+};
+
+typedef int (*spa_invoke_func_t) (struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data);
+
+/**
+ * Register sources and work items to an event loop
+ */
+struct spa_loop_methods {
+ /* the version of this structure. This can be used to expand this
+ * structure in the future */
+#define SPA_VERSION_LOOP_METHODS 0
+ uint32_t version;
+
+ /** add a source to the loop */
+ int (*add_source) (void *object,
+ struct spa_source *source);
+
+ /** update the source io mask */
+ int (*update_source) (void *object,
+ struct spa_source *source);
+
+ /** remove a source from the loop */
+ int (*remove_source) (void *object,
+ struct spa_source *source);
+
+ /** invoke a function in the context of this loop */
+ int (*invoke) (void *object,
+ spa_invoke_func_t func,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ bool block,
+ 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__)
+
+
+/** Control hooks. These hooks can't be removed from their
+ * callbacks and must be removed from a safe place (when the loop
+ * is not running or when it is locked). */
+struct spa_loop_control_hooks {
+#define SPA_VERSION_LOOP_CONTROL_HOOKS 0
+ uint32_t version;
+ /** Executed right before waiting for events. It is typically used to
+ * release locks. */
+ void (*before) (void *data);
+ /** Executed right after waiting for events. It is typically used to
+ * reacquire locks. */
+ 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(&_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(&_h->cb, struct spa_loop_control_hooks, after, 0); \
+})
+
+/**
+ * Control an event loop
+ */
+struct spa_loop_control_methods {
+ /* the version of this structure. This can be used to expand this
+ * structure in the future */
+#define SPA_VERSION_LOOP_CONTROL_METHODS 0
+ uint32_t version;
+
+ int (*get_fd) (void *object);
+
+ /** Add a hook
+ * \param ctrl the control to change
+ * \param hooks the hooks to add
+ *
+ * Adds hooks to the loop controlled by \a ctrl.
+ */
+ void (*add_hook) (void *object,
+ struct spa_hook *hook,
+ const struct spa_loop_control_hooks *hooks,
+ void *data);
+
+ /** Enter a loop
+ * \param ctrl 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.
+ */
+ void (*enter) (void *object);
+ /** Leave a loop
+ * \param ctrl the control
+ *
+ * Ends the iteration of a loop. This should be called after calling
+ * iterate.
+ */
+ void (*leave) (void *object);
+
+ /** Perform one iteration of the loop.
+ * \param ctrl the control
+ * \param timeout an optional timeout in milliseconds.
+ * 0 for no timeout, -1 for infinite timeout.
+ *
+ * This function will block
+ * up to \a timeout milliseconds and then dispatch the fds with activity.
+ * The number of dispatched fds is returned.
+ */
+ int (*iterate) (void *object, int timeout);
+};
+
+#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_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__)
+
+typedef void (*spa_source_io_func_t) (void *data, int fd, uint32_t mask);
+typedef void (*spa_source_idle_func_t) (void *data);
+typedef void (*spa_source_event_func_t) (void *data, uint64_t count);
+typedef void (*spa_source_timer_func_t) (void *data, uint64_t expirations);
+typedef void (*spa_source_signal_func_t) (void *data, int signal_number);
+
+/**
+ * Create sources for an event loop
+ */
+struct spa_loop_utils_methods {
+ /* the version of this structure. This can be used to expand this
+ * structure in the future */
+#define SPA_VERSION_LOOP_UTILS_METHODS 0
+ uint32_t version;
+
+ struct spa_source *(*add_io) (void *object,
+ int fd,
+ uint32_t mask,
+ bool close,
+ spa_source_io_func_t func, void *data);
+
+ int (*update_io) (void *object, struct spa_source *source, uint32_t mask);
+
+ struct spa_source *(*add_idle) (void *object,
+ bool enabled,
+ spa_source_idle_func_t func, void *data);
+ int (*enable_idle) (void *object, struct spa_source *source, bool enabled);
+
+ struct spa_source *(*add_event) (void *object,
+ spa_source_event_func_t func, void *data);
+ int (*signal_event) (void *object, struct spa_source *source);
+
+ struct spa_source *(*add_timer) (void *object,
+ spa_source_timer_func_t func, void *data);
+ int (*update_timer) (void *object,
+ struct spa_source *source,
+ struct timespec *value,
+ struct timespec *interval,
+ bool absolute);
+ struct spa_source *(*add_signal) (void *object,
+ int signal_number,
+ spa_source_signal_func_t func, void *data);
+
+ /** destroy a source allocated with this interface. This function
+ * should only be called when the loop is not running or from the
+ * context of the running loop */
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_LOOP_H */
diff --git a/spa/include/spa/support/plugin-loader.h b/spa/include/spa/support/plugin-loader.h
new file mode 100644
index 0000000..ced58cb
--- /dev/null
+++ b/spa/include/spa/support/plugin-loader.h
@@ -0,0 +1,101 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PLUGIN_LOADER_H
+#define SPA_PLUGIN_LOADER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+#include <spa/utils/dict.h>
+
+/** \defgroup spa_plugin_loader Plugin Loader
+ * SPA plugin loader
+ */
+
+/**
+ * \addtogroup spa_plugin_loader
+ * \{
+ */
+
+#define SPA_TYPE_INTERFACE_PluginLoader SPA_TYPE_INFO_INTERFACE_BASE "PluginLoader"
+
+#define SPA_VERSION_PLUGIN_LOADER 0
+struct spa_plugin_loader { struct spa_interface iface; };
+
+struct spa_plugin_loader_methods {
+#define SPA_VERSION_PLUGIN_LOADER_METHODS 0
+ uint32_t version;
+
+ /**
+ * Load a SPA plugin.
+ *
+ * \param factory_name Plugin factory name
+ * \param info Info dictionary for plugin. NULL if none.
+ * \return plugin handle, or NULL on error
+ */
+ struct spa_handle *(*load) (void *object, const char *factory_name, const struct spa_dict *info);
+
+ /**
+ * Unload a SPA plugin.
+ *
+ * \param handle Plugin handle.
+ * \return 0 on success, < 0 on error
+ */
+ int (*unload)(void *object, struct spa_handle *handle);
+};
+
+static inline 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;
+}
+
+static inline 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;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PLUGIN_LOADER_H */
diff --git a/spa/include/spa/support/plugin.h b/spa/include/spa/support/plugin.h
new file mode 100644
index 0000000..80cadda
--- /dev/null
+++ b/spa/include/spa/support/plugin.h
@@ -0,0 +1,229 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_PLUGIN_H
+#define SPA_PLUGIN_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/dict.h>
+
+/**
+ * \defgroup spa_handle Plugin Handle
+ * SPA plugin handle and factory interfaces
+ */
+
+/**
+ * \addtogroup spa_handle
+ * \{
+ */
+
+struct spa_handle {
+ /** Version of this struct */
+#define SPA_VERSION_HANDLE 0
+ uint32_t version;
+
+ /**
+ * Get the interface provided by \a handle with \a type.
+ *
+ * \a interface is always a struct spa_interface but depending on
+ * \a type, the struct might contain other information.
+ *
+ * \param handle a spa_handle
+ * \param type the interface type
+ * \param interface 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);
+ /**
+ * Clean up the memory of \a handle. After this, \a handle should not be used
+ * anymore.
+ *
+ * \param handle a pointer to memory
+ * \return 0 on success
+ */
+ 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))
+
+/**
+ * This structure lists the information about available interfaces on
+ * handles.
+ */
+struct spa_interface_info {
+ const char *type; /*< the type of the interface, can be
+ * used to get the interface */
+};
+
+/**
+ * Extra supporting infrastructure passed to the init() function of
+ * a factory. It can be extra information or interfaces such as logging.
+ */
+struct spa_support {
+ const char *type; /*< the type of the support item */
+ void *data; /*< specific data for the item */
+};
+
+/** Find a support item of the given type */
+static inline void *spa_support_find(const struct spa_support *support,
+ uint32_t n_support,
+ const char *type)
+{
+ uint32_t i;
+ for (i = 0; i < n_support; i++) {
+ if (strcmp(support[i].type, type) == 0)
+ return support[i].data;
+ }
+ return NULL;
+}
+
+#define SPA_SUPPORT_INIT(type,data) ((struct spa_support) { (type), (data) })
+
+struct spa_handle_factory {
+ /** The version of this structure */
+#define SPA_VERSION_HANDLE_FACTORY 1
+ uint32_t version;
+ /**
+ * The name of the factory contains a logical name that describes
+ * the function of the handle. Other plugins might contain an alternative
+ * implementation with the same name.
+ *
+ * See utils/names.h for the list of standard names.
+ *
+ * Examples include:
+ *
+ * api.alsa.pcm.sink: an object to write PCM samples to an alsa PLAYBACK
+ * device
+ * api.v4l2.source: an object to read from a v4l2 source.
+ */
+ const char *name;
+ /**
+ * Extra information about the handles of this factory.
+ */
+ const struct spa_dict *info;
+ /**
+ * Get the size of handles from this factory.
+ *
+ * \param factory a spa_handle_factory
+ * \param params extra parameters that determine the size of the
+ * handle.
+ */
+ size_t (*get_size) (const struct spa_handle_factory *factory,
+ const struct spa_dict *params);
+
+ /**
+ * Initialize an instance of this factory. The caller should allocate
+ * memory at least size bytes and pass this as \a handle.
+ *
+ * \a support can optionally contain extra interfaces or data items that the
+ * plugin can use such as a logger.
+ *
+ * \param factory a spa_handle_factory
+ * \param handle a pointer to memory
+ * \param info extra handle specific information, usually obtained
+ * from a spa_device. This can be used to configure the handle.
+ * \param support support items
+ * \param n_support number of elements in \a support
+ * \return 0 on success
+ * < 0 errno type error
+ */
+ int (*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);
+
+ /**
+ * spa_handle_factory::enum_interface_info:
+ * \param factory: a #spa_handle_factory
+ * \param info: result to hold spa_interface_info.
+ * \param index: index to keep track of the enumeration, 0 for first item
+ *
+ * Enumerate the interface information for \a factory.
+ *
+ * \return 1 when an item is available
+ * 0 when no more items are available
+ * < 0 errno type error
+ */
+ int (*enum_interface_info) (const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ 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__)
+
+/**
+ * The function signature of the entry point in a plugin.
+ *
+ * \param factory a location to hold the factory result
+ * \param index index to keep track of the enumeration
+ * \return 1 on success
+ * 0 when there are no more factories
+ * -EINVAL when factory is NULL
+ */
+typedef int (*spa_handle_factory_enum_func_t) (const struct spa_handle_factory **factory,
+ uint32_t *index);
+
+#define SPA_HANDLE_FACTORY_ENUM_FUNC_NAME "spa_handle_factory_enum"
+
+/**
+ * The entry point in a plugin.
+ *
+ * \param factory a location to hold the factory result
+ * \param index index to keep track of the enumeration
+ * \return 1 on success
+ * 0 when no more items are available
+ * < 0 errno type error
+ */
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index);
+
+
+
+#define SPA_KEY_FACTORY_NAME "factory.name" /**< the name of a factory */
+#define SPA_KEY_FACTORY_AUTHOR "factory.author" /**< a comma separated list of factory authors */
+#define SPA_KEY_FACTORY_DESCRIPTION "factory.description" /**< description of a factory */
+#define SPA_KEY_FACTORY_USAGE "factory.usage" /**< usage of a factory */
+
+#define SPA_KEY_LIBRARY_NAME "library.name" /**< the name of a library. This is usually
+ * the filename of the plugin without the
+ * path or the plugin extension. */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_PLUGIN_H */
diff --git a/spa/include/spa/support/system.h b/spa/include/spa/support/system.h
new file mode 100644
index 0000000..79127d9
--- /dev/null
+++ b/spa/include/spa/support/system.h
@@ -0,0 +1,165 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_SYSTEM_H
+#define SPA_SYSTEM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct itimerspec;
+
+#include <time.h>
+#include <sys/types.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+/** \defgroup spa_system System
+ * I/O, clock, polling, timer, and signal interfaces
+ */
+
+/**
+ * \addtogroup spa_system
+ * \{
+ */
+
+/**
+ * a collection of core system functions
+ */
+#define SPA_TYPE_INTERFACE_System SPA_TYPE_INFO_INTERFACE_BASE "System"
+#define SPA_TYPE_INTERFACE_DataSystem SPA_TYPE_INFO_INTERFACE_BASE "DataSystem"
+
+#define SPA_VERSION_SYSTEM 0
+struct spa_system { struct spa_interface iface; };
+
+/* IO events */
+#define SPA_IO_IN (1 << 0)
+#define SPA_IO_OUT (1 << 2)
+#define SPA_IO_ERR (1 << 3)
+#define SPA_IO_HUP (1 << 4)
+
+/* flags */
+#define SPA_FD_CLOEXEC (1<<0)
+#define SPA_FD_NONBLOCK (1<<1)
+#define SPA_FD_EVENT_SEMAPHORE (1<<2)
+#define SPA_FD_TIMER_ABSTIME (1<<3)
+#define SPA_FD_TIMER_CANCEL_ON_SET (1<<4)
+
+struct spa_poll_event {
+ uint32_t events;
+ void *data;
+};
+
+struct spa_system_methods {
+#define SPA_VERSION_SYSTEM_METHODS 0
+ uint32_t version;
+
+ /* read/write/ioctl */
+ ssize_t (*read) (void *object, int fd, void *buf, size_t count);
+ ssize_t (*write) (void *object, int fd, const void *buf, size_t count);
+ int (*ioctl) (void *object, int fd, unsigned long request, ...);
+ int (*close) (void *object, int fd);
+
+ /* clock */
+ int (*clock_gettime) (void *object,
+ int clockid, struct timespec *value);
+ int (*clock_getres) (void *object,
+ int clockid, struct timespec *res);
+
+ /* poll */
+ int (*pollfd_create) (void *object, int flags);
+ int (*pollfd_add) (void *object, int pfd, int fd, uint32_t events, void *data);
+ int (*pollfd_mod) (void *object, int pfd, int fd, uint32_t events, void *data);
+ int (*pollfd_del) (void *object, int pfd, int fd);
+ int (*pollfd_wait) (void *object, int pfd,
+ struct spa_poll_event *ev, int n_ev, int timeout);
+
+ /* timers */
+ int (*timerfd_create) (void *object, int clockid, int flags);
+ int (*timerfd_settime) (void *object,
+ int fd, int flags,
+ const struct itimerspec *new_value,
+ struct itimerspec *old_value);
+ int (*timerfd_gettime) (void *object,
+ int fd, struct itimerspec *curr_value);
+ int (*timerfd_read) (void *object, int fd, uint64_t *expirations);
+
+ /* events */
+ int (*eventfd_create) (void *object, int flags);
+ int (*eventfd_write) (void *object, int fd, uint64_t count);
+ int (*eventfd_read) (void *object, int fd, uint64_t *count);
+
+ /* signals */
+ int (*signalfd_create) (void *object, int signal, int flags);
+ 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_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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_SYSTEM_H */
diff --git a/spa/include/spa/support/thread.h b/spa/include/spa/support/thread.h
new file mode 100644
index 0000000..ef3866f
--- /dev/null
+++ b/spa/include/spa/support/thread.h
@@ -0,0 +1,149 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_THREAD_H
+#define SPA_THREAD_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <string.h>
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/dict.h>
+
+/** \defgroup spa_thread Thread
+ * Threading utility interfaces
+ */
+
+/**
+ * \addtogroup spa_thread
+ * \{
+ */
+
+/** a thread object.
+ * This can be cast to a platform native thread, like pthread on posix systems
+ */
+#define SPA_TYPE_INFO_Thread SPA_TYPE_INFO_POINTER_BASE "Thread"
+struct spa_thread;
+
+#define SPA_TYPE_INTERFACE_ThreadUtils SPA_TYPE_INFO_INTERFACE_BASE "ThreadUtils"
+#define SPA_VERSION_THREAD_UTILS 0
+struct spa_thread_utils { struct spa_interface iface; };
+
+/** thread utils */
+struct spa_thread_utils_methods {
+#define SPA_VERSION_THREAD_UTILS_METHODS 0
+ uint32_t version;
+
+ /** create a new thread that runs \a start with \a arg */
+ struct spa_thread * (*create) (void *object, const struct spa_dict *props,
+ void *(*start)(void*), void *arg);
+ /** stop and join a thread */
+ int (*join)(void *object, struct spa_thread *thread, void **retval);
+
+ /** get realtime priority range for threads created with \a props */
+ int (*get_rt_range) (void *object, const struct spa_dict *props, int *min, int *max);
+ /** acquire realtime priority, a priority of -1 refers to the priority
+ * configured in the realtime module
+ */
+ int (*acquire_rt) (void *object, struct spa_thread *thread, int priority);
+ /** drop realtime priority */
+ int (*drop_rt) (void *object, struct spa_thread *thread);
+};
+
+/** \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,
+ 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,
+ 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,
+ struct spa_thread *thread, void **retval)
+{
+ int res = -ENOTSUP;
+ spa_interface_call_res(&o->iface,
+ struct spa_thread_utils_methods, res, 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,
+ 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,
+ 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,
+ struct spa_thread *thread, int priority)
+{
+ int res = -ENOTSUP;
+ spa_interface_call_res(&o->iface,
+ struct spa_thread_utils_methods, res, 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,
+ 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;
+}
+
+#define SPA_KEY_THREAD_NAME "thread.name" /* the thread name */
+#define SPA_KEY_THREAD_STACK_SIZE "thread.stack-size" /* the stack size of the thread */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_THREAD_H */
diff --git a/spa/include/spa/utils/ansi.h b/spa/include/spa/utils/ansi.h
new file mode 100644
index 0000000..83726f8
--- /dev/null
+++ b/spa/include/spa/utils/ansi.h
@@ -0,0 +1,114 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_UTILS_ANSI_H
+#define SPA_UTILS_ANSI_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \defgroup spa_ansi ANSI codes
+ * ANSI color code macros
+ */
+
+/**
+ * \addtogroup spa_ansi
+ * \{
+ */
+
+/**
+ * Ansi escape sequences. Note that the color names are approximate only and
+ * the actual rendering of the color depends on the terminal.
+ */
+
+#define SPA_ANSI_RESET "\x1B[0m"
+#define SPA_ANSI_BOLD "\x1B[1m"
+#define SPA_ANSI_ITALIC "\x1B[3m"
+#define SPA_ANSI_UNDERLINE "\x1B[4m"
+
+#define SPA_ANSI_BLACK "\x1B[0;30m"
+#define SPA_ANSI_RED "\x1B[0;31m"
+#define SPA_ANSI_GREEN "\x1B[0;32m"
+#define SPA_ANSI_YELLOW "\x1B[0;33m"
+#define SPA_ANSI_BLUE "\x1B[0;34m"
+#define SPA_ANSI_MAGENTA "\x1B[0;35m"
+#define SPA_ANSI_CYAN "\x1B[0;36m"
+#define SPA_ANSI_WHITE "\x1B[0;37m"
+#define SPA_ANSI_BRIGHT_BLACK "\x1B[90m"
+#define SPA_ANSI_BRIGHT_RED "\x1B[91m"
+#define SPA_ANSI_BRIGHT_GREEN "\x1B[92m"
+#define SPA_ANSI_BRIGHT_YELLOW "\x1B[93m"
+#define SPA_ANSI_BRIGHT_BLUE "\x1B[94m"
+#define SPA_ANSI_BRIGHT_MAGENTA "\x1B[95m"
+#define SPA_ANSI_BRIGHT_CYAN "\x1B[96m"
+#define SPA_ANSI_BRIGHT_WHITE "\x1B[97m"
+
+/* Shortcut because it's a common use-case and easier than combining both */
+#define SPA_ANSI_BOLD_BLACK "\x1B[1;30m"
+#define SPA_ANSI_BOLD_RED "\x1B[1;31m"
+#define SPA_ANSI_BOLD_GREEN "\x1B[1;32m"
+#define SPA_ANSI_BOLD_YELLOW "\x1B[1;33m"
+#define SPA_ANSI_BOLD_BLUE "\x1B[1;34m"
+#define SPA_ANSI_BOLD_MAGENTA "\x1B[1;35m"
+#define SPA_ANSI_BOLD_CYAN "\x1B[1;36m"
+#define SPA_ANSI_BOLD_WHITE "\x1B[1;37m"
+
+#define SPA_ANSI_DARK_BLACK "\x1B[2;30m"
+#define SPA_ANSI_DARK_RED "\x1B[2;31m"
+#define SPA_ANSI_DARK_GREEN "\x1B[2;32m"
+#define SPA_ANSI_DARK_YELLOW "\x1B[2;33m"
+#define SPA_ANSI_DARK_BLUE "\x1B[2;34m"
+#define SPA_ANSI_DARK_MAGENTA "\x1B[2;35m"
+#define SPA_ANSI_DARK_CYAN "\x1B[2;36m"
+#define SPA_ANSI_DARK_WHITE "\x1B[2;37m"
+
+/* Background colors */
+#define SPA_ANSI_BG_BLACK "\x1B[0;40m"
+#define SPA_ANSI_BG_RED "\x1B[0;41m"
+#define SPA_ANSI_BG_GREEN "\x1B[0;42m"
+#define SPA_ANSI_BG_YELLOW "\x1B[0;43m"
+#define SPA_ANSI_BG_BLUE "\x1B[0;44m"
+#define SPA_ANSI_BG_MAGENTA "\x1B[0;45m"
+#define SPA_ANSI_BG_CYAN "\x1B[0;46m"
+#define SPA_ANSI_BG_WHITE "\x1B[0;47m"
+#define SPA_ANSI_BG_BRIGHT_BLACK "\x1B[100m"
+#define SPA_ANSI_BG_BRIGHT_RED "\x1B[101m"
+#define SPA_ANSI_BG_BRIGHT_GREEN "\x1B[102m"
+#define SPA_ANSI_BG_BRIGHT_YELLOW "\x1B[103m"
+#define SPA_ANSI_BG_BRIGHT_BLUE "\x1B[104m"
+#define SPA_ANSI_BG_BRIGHT_MAGENTA "\x1B[105m"
+#define SPA_ANSI_BG_BRIGHT_CYAN "\x1B[106m"
+#define SPA_ANSI_BG_BRIGHT_WHITE "\x1B[107m"
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_UTILS_ANSI_H */
diff --git a/spa/include/spa/utils/defs.h b/spa/include/spa/utils/defs.h
new file mode 100644
index 0000000..3b48626
--- /dev/null
+++ b/spa/include/spa/utils/defs.h
@@ -0,0 +1,399 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_UTILS_DEFS_H
+#define SPA_UTILS_DEFS_H
+
+#ifdef __cplusplus
+extern "C" {
+# if __cplusplus >= 201103L
+# define SPA_STATIC_ASSERT static_assert
+# endif
+#else
+# include <stdbool.h>
+# if __STDC_VERSION__ >= 201112L
+# define SPA_STATIC_ASSERT _Static_assert
+# endif
+#endif
+#ifndef SPA_STATIC_ASSERT
+#define SPA_STATIC_ASSERT(a, b) \
+ ((void)sizeof(struct { int spa_static_assertion_failed : 2 * !!(a) - 1; }))
+#endif
+#include <inttypes.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stddef.h>
+#include <stdio.h>
+
+/**
+ * \defgroup spa_utils_defs Miscellaneous
+ * Helper macros and functions
+ */
+
+/**
+ * \addtogroup spa_utils_defs
+ * \{
+ */
+
+/**
+ * SPA_FALLTHROUGH is an annotation to suppress compiler warnings about switch
+ * cases that fall through without a break or return statement. SPA_FALLTHROUGH
+ * is only needed on cases that have code:
+ *
+ * switch (foo) {
+ * case 1: // These cases have no code. No fallthrough annotations are needed.
+ * case 2:
+ * case 3:
+ * foo = 4; // This case has code, so a fallthrough annotation is needed:
+ * SPA_FALLTHROUGH;
+ * default:
+ * return foo;
+ * }
+ */
+#if defined(__clang__) && defined(__cplusplus) && __cplusplus >= 201103L
+ /* clang's fallthrough annotations are only available starting in C++11. */
+# define SPA_FALLTHROUGH [[clang::fallthrough]];
+#elif __GNUC__ >= 7 || __clang_major__ >= 10
+# define SPA_FALLTHROUGH __attribute__ ((fallthrough));
+#else
+# define SPA_FALLTHROUGH /* FALLTHROUGH */
+#endif
+
+#define SPA_FLAG_MASK(field,mask,flag) (((field) & (mask)) == (flag))
+#define SPA_FLAG_IS_SET(field,flag) SPA_FLAG_MASK(field, flag, flag)
+
+#define SPA_FLAG_SET(field,flag) ((field) |= (flag))
+#define SPA_FLAG_CLEAR(field, flag) \
+({ \
+ SPA_STATIC_ASSERT(__builtin_constant_p(flag) ? \
+ (__typeof__(flag))(__typeof__(field))(__typeof__(flag))(flag) == (flag) : \
+ sizeof(field) >= sizeof(flag), \
+ "truncation problem when masking " #field \
+ " with ~" #flag); \
+ ((field) &= ~(__typeof__(field))(flag)); \
+})
+#define SPA_FLAG_UPDATE(field,flag,val) ((val) ? SPA_FLAG_SET((field),(flag)) : SPA_FLAG_CLEAR((field),(flag)))
+
+enum spa_direction {
+ SPA_DIRECTION_INPUT = 0,
+ SPA_DIRECTION_OUTPUT = 1,
+};
+
+#define SPA_DIRECTION_REVERSE(d) ((d) ^ 1)
+
+#define SPA_RECTANGLE(width,height) ((struct spa_rectangle){ (width), (height) })
+struct spa_rectangle {
+ uint32_t width;
+ uint32_t height;
+};
+
+#define SPA_POINT(x,y) ((struct spa_point){ (x), (y) })
+struct spa_point {
+ int32_t x;
+ int32_t y;
+};
+
+#define SPA_REGION(x,y,width,height) ((struct spa_region){ SPA_POINT(x,y), SPA_RECTANGLE(width,height) })
+struct spa_region {
+ struct spa_point position;
+ struct spa_rectangle size;
+};
+
+#define SPA_FRACTION(num,denom) ((struct spa_fraction){ (num), (denom) })
+struct spa_fraction {
+ uint32_t num;
+ uint32_t denom;
+};
+
+#define SPA_N_ELEMENTS(arr) (sizeof(arr) / sizeof((arr)[0]))
+/**
+ * Array iterator macro. Usage:
+ * ```c
+ * struct foo array[16];
+ * struct foo *f;
+ * SPA_FOR_EACH_ELEMENT(array, f) {
+ * f->bar = baz;
+ * }
+ * ```
+ */
+#define SPA_FOR_EACH_ELEMENT(arr, ptr) \
+ for ((ptr) = arr; (void*)(ptr) < SPA_PTROFF(arr, sizeof(arr), void); (ptr)++)
+
+#define SPA_FOR_EACH_ELEMENT_VAR(arr, var) \
+ for (__typeof__((arr)[0])* (var) = arr; (void*)(var) < SPA_PTROFF(arr, sizeof(arr), void); (var)++)
+
+#define SPA_ABS(a) \
+({ \
+ __typeof__(a) _a = (a); \
+ SPA_LIKELY(_a >= 0) ? _a : -_a; \
+})
+#define SPA_MIN(a,b) \
+({ \
+ __typeof__(a) _min_a = (a); \
+ __typeof__(b) _min_b = (b); \
+ SPA_LIKELY(_min_a <= _min_b) ? _min_a : _min_b; \
+})
+#define SPA_MAX(a,b) \
+({ \
+ __typeof__(a) _max_a = (a); \
+ __typeof__(b) _max_b = (b); \
+ SPA_LIKELY(_max_a >= _max_b) ? _max_a : _max_b; \
+})
+#define SPA_CLAMP(v,low,high) \
+({ \
+ __typeof__(v) _v = (v); \
+ __typeof__(low) _low = (low); \
+ __typeof__(high) _high = (high); \
+ SPA_MIN(SPA_MAX(_v, _low), _high); \
+})
+
+#define SPA_CLAMPF(v,low,high) \
+({ \
+ fminf(fmaxf(v, low), high); \
+})
+
+
+#define SPA_SWAP(a,b) \
+({ \
+ __typeof__(a) _t = (a); \
+ (a) = b; (b) = _t; \
+})
+
+#define SPA_TYPECHECK(type,x) \
+({ type _dummy; \
+ typeof(x) _dummy2; \
+ (void)(&_dummy == &_dummy2); \
+ x; \
+})
+
+/**
+ * Return the address (buffer + offset) as pointer of \a type
+ */
+#define SPA_PTROFF(ptr_,offset_,type_) ((type_*)((uintptr_t)(ptr_) + (ptrdiff_t)(offset_)))
+#define SPA_PTROFF_ALIGN(ptr_,offset_,alignment_,type_) \
+ SPA_PTR_ALIGN(SPA_PTROFF(ptr_,offset_,type_),alignment_,type_)
+
+
+/**
+ * Deprecated, use SPA_PTROFF and SPA_PTROFF_ALIGN instead
+ */
+#define SPA_MEMBER(b,o,t) SPA_PTROFF(b,o,t)
+#define SPA_MEMBER_ALIGN(b,o,a,t) SPA_PTROFF_ALIGN(b,o,a,t)
+
+#define SPA_CONTAINER_OF(p,t,m) ((t*)((uintptr_t)(p) - offsetof(t,m)))
+
+#define SPA_PTRDIFF(p1,p2) ((intptr_t)(p1) - (intptr_t)(p2))
+
+#define SPA_PTR_TO_INT(p) ((int) ((intptr_t) (p)))
+#define SPA_INT_TO_PTR(u) ((void*) ((intptr_t) (u)))
+
+#define SPA_PTR_TO_UINT32(p) ((uint32_t) ((uintptr_t) (p)))
+#define SPA_UINT32_TO_PTR(u) ((void*) ((uintptr_t) (u)))
+
+#define SPA_TIME_INVALID ((int64_t)INT64_MIN)
+#define SPA_IDX_INVALID ((unsigned int)-1)
+#define SPA_ID_INVALID ((uint32_t)0xffffffff)
+
+#define SPA_NSEC_PER_SEC (1000000000LL)
+#define SPA_NSEC_PER_MSEC (1000000ll)
+#define SPA_NSEC_PER_USEC (1000ll)
+#define SPA_USEC_PER_SEC (1000000ll)
+#define SPA_USEC_PER_MSEC (1000ll)
+#define SPA_MSEC_PER_SEC (1000ll)
+
+#define SPA_TIMESPEC_TO_NSEC(ts) ((ts)->tv_sec * SPA_NSEC_PER_SEC + (ts)->tv_nsec)
+#define SPA_TIMESPEC_TO_USEC(ts) ((ts)->tv_sec * SPA_USEC_PER_SEC + (ts)->tv_nsec / SPA_NSEC_PER_USEC)
+#define SPA_TIMEVAL_TO_NSEC(tv) ((tv)->tv_sec * SPA_NSEC_PER_SEC + (tv)->tv_usec * SPA_NSEC_PER_USEC)
+#define SPA_TIMEVAL_TO_USEC(tv) ((tv)->tv_sec * SPA_USEC_PER_SEC + (tv)->tv_usec)
+
+#ifdef __GNUC__
+#define SPA_PRINTF_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1)))
+#define SPA_FORMAT_ARG_FUNC(arg1) __attribute__((format_arg(arg1)))
+#define SPA_ALIGNED(align) __attribute__((aligned(align)))
+#define SPA_DEPRECATED __attribute__ ((deprecated))
+#define SPA_EXPORT __attribute__((visibility("default")))
+#define SPA_SENTINEL __attribute__((__sentinel__))
+#define SPA_UNUSED __attribute__ ((unused))
+#define SPA_NORETURN __attribute__ ((noreturn))
+#define SPA_WARN_UNUSED_RESULT __attribute__ ((warn_unused_result))
+#else
+#define SPA_PRINTF_FUNC(fmt, arg1)
+#define SPA_FORMAT_ARG_FUNC(arg1)
+#define SPA_ALIGNED(align)
+#define SPA_DEPRECATED
+#define SPA_EXPORT
+#define SPA_SENTINEL
+#define SPA_UNUSED
+#define SPA_NORETURN
+#define SPA_WARN_UNUSED_RESULT
+#endif
+
+#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
+#define SPA_RESTRICT restrict
+#elif defined(__GNUC__) && __GNUC__ >= 4
+#define SPA_RESTRICT __restrict__
+#else
+#define SPA_RESTRICT
+#endif
+
+#define SPA_ROUND_DOWN(num,value) \
+({ \
+ __typeof__(num) _num = (num); \
+ ((_num) - ((_num) % (value))); \
+})
+#define SPA_ROUND_UP(num,value) \
+({ \
+ __typeof__(value) _v = (value); \
+ ((((num) + (_v) - 1) / (_v)) * (_v)); \
+})
+
+#define SPA_ROUND_MASK(num,mask) ((__typeof__(num))((mask)-1))
+
+#define SPA_ROUND_DOWN_N(num,align) ((num) & ~SPA_ROUND_MASK(num, align))
+#define SPA_ROUND_UP_N(num,align) ((((num)-1) | SPA_ROUND_MASK(num, align))+1)
+
+#define SPA_SCALE32_UP(val,num,denom) \
+({ \
+ uint64_t _val = (val); \
+ uint64_t _denom = (denom); \
+ (uint32_t)(((_val) * (num) + (_denom)-1) / (_denom)); \
+})
+
+
+#define SPA_PTR_ALIGNMENT(p,align) ((intptr_t)(p) & ((align)-1))
+#define SPA_IS_ALIGNED(p,align) (SPA_PTR_ALIGNMENT(p,align) == 0)
+#define SPA_PTR_ALIGN(p,align,type) ((type*)SPA_ROUND_UP_N((intptr_t)(p), (intptr_t)(align)))
+
+#ifndef SPA_LIKELY
+#ifdef __GNUC__
+#define SPA_LIKELY(x) (__builtin_expect(!!(x),1))
+#define SPA_UNLIKELY(x) (__builtin_expect(!!(x),0))
+#else
+#define SPA_LIKELY(x) (x)
+#define SPA_UNLIKELY(x) (x)
+#endif
+#endif
+
+#define SPA_STRINGIFY_1(...) #__VA_ARGS__
+#define SPA_STRINGIFY(...) SPA_STRINGIFY_1(__VA_ARGS__)
+
+#define spa_return_if_fail(expr) \
+ do { \
+ if (SPA_UNLIKELY(!(expr))) { \
+ fprintf(stderr, "'%s' failed at %s:%u %s()\n", \
+ #expr , __FILE__, __LINE__, __func__); \
+ return; \
+ } \
+ } while(false)
+
+#define spa_return_val_if_fail(expr, val) \
+ do { \
+ if (SPA_UNLIKELY(!(expr))) { \
+ fprintf(stderr, "'%s' failed at %s:%u %s()\n", \
+ #expr , __FILE__, __LINE__, __func__); \
+ return (val); \
+ } \
+ } while(false)
+
+/* spa_assert_se() is an assert which guarantees side effects of x,
+ * i.e. is never optimized away, regardless of NDEBUG or FASTPATH. */
+#ifndef __COVERITY__
+#define spa_assert_se(expr) \
+ do { \
+ if (SPA_UNLIKELY(!(expr))) { \
+ fprintf(stderr, "'%s' failed at %s:%u %s()\n", \
+ #expr , __FILE__, __LINE__, __func__); \
+ abort(); \
+ } \
+ } while (false)
+#else
+#define spa_assert_se(expr) \
+ do { \
+ int _unique_var = (expr); \
+ if (!_unique_var) \
+ abort(); \
+ } while (false)
+#endif
+
+/* Does exactly nothing */
+#define spa_nop() do {} while (false)
+
+#ifdef NDEBUG
+#define spa_assert(expr) spa_nop()
+#elif defined (FASTPATH)
+#define spa_assert(expr) spa_assert_se(expr)
+#else
+#define spa_assert(expr) spa_assert_se(expr)
+#endif
+
+#ifdef NDEBUG
+#define spa_assert_not_reached() abort()
+#else
+#define spa_assert_not_reached() \
+ do { \
+ fprintf(stderr, "Code should not be reached at %s:%u %s()\n", \
+ __FILE__, __LINE__, __func__); \
+ abort(); \
+ } while (false)
+#endif
+
+#define spa_memzero(x,l) (memset((x), 0, (l)))
+#define spa_zero(x) (spa_memzero(&(x), sizeof(x)))
+
+#ifdef SPA_DEBUG_MEMCPY
+#define spa_memcpy(d,s,n) \
+({ \
+ fprintf(stderr, "%s:%u %s() memcpy(%p, %p, %zd)\n", \
+ __FILE__, __LINE__, __func__, (d), (s), (size_t)(n)); \
+ memcpy(d,s,n); \
+})
+#define spa_memmove(d,s,n) \
+({ \
+ fprintf(stderr, "%s:%u %s() memmove(%p, %p, %zd)\n", \
+ __FILE__, __LINE__, __func__, (d), (s), (size_t)(n)); \
+ memmove(d,s,n); \
+})
+#else
+#define spa_memcpy(d,s,n) memcpy(d,s,n)
+#define spa_memmove(d,s,n) memmove(d,s,n)
+#endif
+
+#define spa_aprintf(_fmt, ...) \
+({ \
+ char *_strp; \
+ if (asprintf(&(_strp), (_fmt), ## __VA_ARGS__ ) == -1) \
+ _strp = NULL; \
+ _strp; \
+})
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_UTILS_DEFS_H */
diff --git a/spa/include/spa/utils/dict.h b/spa/include/spa/utils/dict.h
new file mode 100644
index 0000000..f9a0b5b
--- /dev/null
+++ b/spa/include/spa/utils/dict.h
@@ -0,0 +1,120 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DICT_H
+#define SPA_DICT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <string.h>
+
+#include <spa/utils/defs.h>
+
+/**
+ * \defgroup spa_dict Dictionary
+ * Dictionary data structure
+ */
+
+/**
+ * \addtogroup spa_dict
+ * \{
+ */
+
+struct spa_dict_item {
+ const char *key;
+ const char *value;
+};
+
+#define SPA_DICT_ITEM_INIT(key,value) ((struct spa_dict_item) { (key), (value) })
+
+struct spa_dict {
+#define SPA_DICT_FLAG_SORTED (1<<0) /**< items are sorted */
+ uint32_t flags;
+ uint32_t n_items;
+ 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_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)
+{
+ 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)
+{
+ if (dict->n_items > 0)
+ qsort((void*)dict->items, dict->n_items, sizeof(struct spa_dict_item),
+ spa_dict_item_compare);
+ 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,
+ const char *key)
+{
+ const struct spa_dict_item *item;
+
+ if (SPA_FLAG_IS_SET(dict->flags, SPA_DICT_FLAG_SORTED) &&
+ dict->n_items > 0) {
+ struct spa_dict_item k = SPA_DICT_ITEM_INIT(key, NULL);
+ item = (const struct spa_dict_item *)bsearch(&k,
+ (const void *) dict->items, dict->n_items,
+ sizeof(struct spa_dict_item),
+ spa_dict_item_compare);
+ if (item != NULL)
+ return item;
+ } else {
+ spa_dict_for_each(item, dict) {
+ if (!strcmp(item->key, key))
+ return item;
+ }
+ }
+ return NULL;
+}
+
+static inline 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;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DICT_H */
diff --git a/spa/include/spa/utils/dll.h b/spa/include/spa/utils/dll.h
new file mode 100644
index 0000000..65d8cd6
--- /dev/null
+++ b/spa/include/spa/utils/dll.h
@@ -0,0 +1,71 @@
+/* Simple DLL
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_DLL_H
+#define SPA_DLL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <math.h>
+
+#define SPA_DLL_BW_MAX 0.128
+#define SPA_DLL_BW_MIN 0.016
+
+struct spa_dll {
+ double bw;
+ double z1, z2, z3;
+ double w0, w1, w2;
+};
+
+static inline 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)
+{
+ double w = 2 * M_PI * bw * period / rate;
+ dll->w0 = 1.0 - exp (-20.0 * w);
+ dll->w1 = w * 1.5 / period;
+ dll->w2 = w / 1.5;
+ dll->bw = bw;
+}
+
+static inline 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);
+ dll->z3 += dll->w2 * dll->z2;
+ return 1.0 - (dll->z2 + dll->z3);
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_DLL_H */
diff --git a/spa/include/spa/utils/enum-types.h b/spa/include/spa/utils/enum-types.h
new file mode 100644
index 0000000..030d8f2
--- /dev/null
+++ b/spa/include/spa/utils/enum-types.h
@@ -0,0 +1,65 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_ENUM_TYPES_H
+#define SPA_ENUM_TYPES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/type.h>
+
+#define SPA_TYPE_INFO_Direction SPA_TYPE_INFO_ENUM_BASE "Direction"
+#define SPA_TYPE_INFO_DIRECTION_BASE SPA_TYPE_INFO_Direction ":"
+
+static const struct spa_type_info spa_type_direction[] = {
+ { SPA_DIRECTION_INPUT, SPA_TYPE_Int, SPA_TYPE_INFO_DIRECTION_BASE "Input", NULL },
+ { SPA_DIRECTION_OUTPUT, SPA_TYPE_Int, SPA_TYPE_INFO_DIRECTION_BASE "Output", NULL },
+ { 0, 0, NULL, NULL }
+};
+
+#include <spa/pod/pod.h>
+
+#define SPA_TYPE_INFO_Choice SPA_TYPE_INFO_ENUM_BASE "Choice"
+#define SPA_TYPE_INFO_CHOICE_BASE SPA_TYPE_INFO_Choice ":"
+
+static const struct spa_type_info spa_type_choice[] = {
+ { SPA_CHOICE_None, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "None", NULL },
+ { SPA_CHOICE_Range, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "Range", NULL },
+ { SPA_CHOICE_Step, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "Step", NULL },
+ { SPA_CHOICE_Enum, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "Enum", NULL },
+ { SPA_CHOICE_Flags, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "Flags", NULL },
+ { 0, 0, NULL, NULL }
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_TYPE_INFO_H */
diff --git a/spa/include/spa/utils/hook.h b/spa/include/spa/utils/hook.h
new file mode 100644
index 0000000..edcb716
--- /dev/null
+++ b/spa/include/spa/utils/hook.h
@@ -0,0 +1,472 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_HOOK_H
+#define SPA_HOOK_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+
+/** \defgroup spa_interfaces Interfaces
+ *
+ * \brief Generic implementation of implementation-independent interfaces
+ *
+ * A SPA Interface is a generic struct that, together with a few macros,
+ * provides a generic way of invoking methods on objects without knowing the
+ * details of the implementation.
+ *
+ * The primary interaction with interfaces is through macros that expand into
+ * the right method call. For the implementation of an interface, we need two
+ * structs and a macro to invoke the `bar` method:
+ *
+ * \code{.c}
+ * // this struct must be public and defines the interface to a
+ * // struct foo
+ * struct foo_methods {
+ * uint32_t version;
+ * void (*bar)(void *object, const char *msg);
+ * };
+ *
+ * // this struct does not need to be public
+ * struct foo {
+ * struct spa_interface iface; // must be first element, see foo_bar()
+ * int some_other_field;
+ * ...
+ * };
+ *
+ * // if struct foo is private, we need to cast to a
+ * // generic spa_interface object
+ * #define foo_bar(obj, ...) ({ \
+ * struct foo *f = obj;
+ * spa_interface_call((struct spa_interface *)f, // pointer to spa_interface in foo
+ * struct foo_methods, // type of callbacks
+ * bar, // name of methods
+ * 0, // hardcoded version to match foo_methods->version
+ * __VA_ARGS__ // pass rest of args through
+ * );/
+ * })
+ * \endcode
+ *
+ * The `struct foo_methods` and the invocation macro `foo_bar()` must be
+ * available to the caller. The implementation of `struct foo` can be private.
+ *
+ * \code{.c}
+ * void main(void) {
+ * struct foo *myfoo = get_foo_from_somewhere();
+ * foo_bar(myfoo, "Invoking bar() on myfoo");
+ * }
+ * \endcode
+ * The expansion of `foo_bar()` resolves roughly into this code:
+ * \code{.c}
+ * void main(void) {
+ * struct foo *myfoo = get_foo_from_somewhere();
+ * // foo_bar(myfoo, "Invoking bar() on myfoo");
+ * const struct foo_methods *methods = ((struct spa_interface*)myfoo)->cb;
+ * if (0 >= methods->version && // version check
+ * methods->bar) // compile error if this function does not exist,
+ * methods->bar(myfoo, "Invoking bar() on myfoo");
+ * }
+ * \endcode
+ *
+ * The typecast used in `foo_bar()` allows `struct foo` to be opaque to the
+ * caller. The implementation may assign the callback methods at object
+ * instantiation, and the caller will transparently invoke the method on the
+ * given object. For example, the following code assigns a different `bar()` method on
+ * Mondays - the caller does not need to know this.
+ * \code{.c}
+ *
+ * static void bar_stdout(struct foo *f, const char *msg) {
+ * printf(msg);
+ * }
+ * static void bar_stderr(struct foo *f, const char *msg) {
+ * fprintf(stderr, msg);
+ * }
+ *
+ * struct foo* get_foo_from_somewhere() {
+ * struct foo *f = calloc(sizeof struct foo);
+ * // illustrative only, use SPA_INTERFACE_INIT()
+ * f->iface->cb = (struct foo_methods*) { .bar = bar_stdout };
+ * if (today_is_monday)
+ * f->iface->cb = (struct foo_methods*) { .bar = bar_stderr };
+ * return f;
+ * }
+ * \endcode
+ */
+
+/**
+ * \addtogroup spa_interfaces
+ * \{
+ */
+
+/** \struct spa_callbacks
+ * Callbacks, contains the structure with functions and the data passed
+ * to the functions. The structure should also contain a version field that
+ * is checked. */
+struct spa_callbacks {
+ const void *funcs;
+ void *data;
+};
+
+/** Check if a callback \a c is of at least version \a v */
+#define SPA_CALLBACK_VERSION_MIN(c,v) ((c) && ((v) == 0 || (c)->version > (v)-1))
+
+/** Check if a callback \a c has method \a m of version \a v */
+#define SPA_CALLBACK_CHECK(c,m,v) (SPA_CALLBACK_VERSION_MIN(c,v) && (c)->m)
+
+/**
+ * Initialize the set of functions \a funcs as a \ref spa_callbacks, together
+ * with \a _data.
+ */
+#define SPA_CALLBACKS_INIT(_funcs,_data) ((struct spa_callbacks){ (_funcs), (_data), })
+
+/** \struct spa_interface
+ */
+struct spa_interface {
+ const char *type;
+ uint32_t version;
+ struct spa_callbacks cb;
+};
+
+/**
+ * Initialize a \ref spa_interface.
+ *
+ * \code{.c}
+ * const static struct foo_methods foo_funcs = {
+ * .bar = some_bar_implementation,
+ * };
+ *
+ * struct foo *f = malloc(...);
+ * f->iface = SPA_INTERFACE_INIT("foo type", 0, foo_funcs, NULL);
+ * \endcode
+ *
+ */
+#define SPA_INTERFACE_INIT(_type,_version,_funcs,_data) \
+ ((struct spa_interface){ (_type), (_version), SPA_CALLBACKS_INIT(_funcs,_data), })
+
+/**
+ * Invoke method named \a method in the \a callbacks.
+ * The \a method_type defines the type of the method struct.
+ * Returns true if the method could be called, false otherwise.
+ */
+#define spa_callbacks_call(callbacks,type,method,vers,...) \
+({ \
+ 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__); \
+ _res; \
+})
+
+/**
+ * True if the \a callbacks are of version \a vers, false otherwise
+ */
+#define spa_callback_version_min(callbacks,type,vers) \
+({ \
+ const type *_f = (const type *) (callbacks)->funcs; \
+ SPA_CALLBACK_VERSION_MIN(_f,vers); \
+})
+
+/**
+ * True if the \a callbacks contains \a method of version
+ * \a vers, false otherwise
+ */
+#define spa_callback_check(callbacks,type,method,vers) \
+({ \
+ const type *_f = (const type *) (callbacks)->funcs; \
+ SPA_CALLBACK_CHECK(_f,method,vers); \
+})
+
+/**
+ * Invoke method named \a method in the \a callbacks.
+ * The \a method_type defines the type of the method struct.
+ *
+ * The return value is stored in \a res.
+ */
+#define spa_callbacks_call_res(callbacks,type,res,method,vers,...) \
+({ \
+ const type *_f = (const type *) (callbacks)->funcs; \
+ if (SPA_LIKELY(SPA_CALLBACK_CHECK(_f,method,vers))) \
+ res = _f->method((callbacks)->data, ## __VA_ARGS__); \
+ res; \
+})
+
+/**
+ * True if the \a iface's callbacks are of version \a vers, false otherwise
+ */
+#define spa_interface_callback_version_min(iface,method_type,vers) \
+ spa_callback_version_min(&(iface)->cb, method_type, vers)
+
+/**
+ * True if the \a iface's callback \a method is of version \a vers
+ * and exists, false otherwise
+ */
+#define spa_interface_callback_check(iface,method_type,method,vers) \
+ spa_callback_check(&(iface)->cb, method_type, method, vers)
+
+/**
+ * Invoke method named \a method in the callbacks on the given interface object.
+ * The \a method_type defines the type of the method struct, not the interface
+ * itself.
+ */
+#define spa_interface_call(iface,method_type,method,vers,...) \
+ spa_callbacks_call(&(iface)->cb,method_type,method,vers,##__VA_ARGS__)
+
+/**
+ * Invoke method named \a method in the callbacks on the given interface object.
+ * The \a method_type defines the type of the method struct, not the interface
+ * itself.
+ *
+ * The return value is stored in \a res.
+ */
+#define spa_interface_call_res(iface,method_type,res,method,vers,...) \
+ spa_callbacks_call_res(&(iface)->cb,method_type,res,method,vers,##__VA_ARGS__)
+
+/**
+ * \}
+ */
+
+/** \defgroup spa_hooks Hooks
+ *
+ * A SPA Hook is a data structure to keep track of callbacks. It is similar to
+ * the \ref spa_interfaces and typically used where an implementation allows
+ * for multiple external callback functions. For example, an implementation may
+ * use a hook list to implement signals with each caller using a hook to
+ * register callbacks to be invoked on those signals.
+ *
+ * The below (pseudo)code is a minimal example outlining the use of hooks:
+ * \code{.c}
+ * // the public interface
+ * #define VERSION_BAR_EVENTS 0 // version of the vtable
+ * struct bar_events {
+ * uint32_t version; // NOTE: an integral member named `version`
+ * // must be present in the vtable
+ * void (*boom)(void *data, const char *msg);
+ * };
+ *
+ * // private implementation
+ * struct party {
+ * struct spa_hook_list bar_list;
+ * };
+ *
+ * void party_add_event_listener(struct party *p, struct spa_hook *listener,
+ * const struct bar_events *events, void *data)
+ * {
+ * spa_hook_list_append(&p->bar_list, listener, events, data);
+ * }
+ *
+ * static void party_on(struct party *p)
+ * {
+ * // NOTE: this is a macro, it evaluates to an integer,
+ * // which is the number of hooks called
+ * spa_hook_list_call(&p->list,
+ * struct bar_events, // vtable type
+ * boom, // function name
+ * 0, // hardcoded version,
+ * // usually the version in which `boom`
+ * // has been added to the vtable
+ * "party on, wayne" // function argument(s)
+ * );
+ * }
+ * \endcode
+ *
+ * In the caller, the hooks can be used like this:
+ * \code{.c}
+ * static void boom_cb(void *data, const char *msg) {
+ * // data is userdata from main()
+ * printf("%s", msg);
+ * }
+ *
+ * static const struct bar_events events = {
+ * .version = VERSION_BAR_EVENTS, // version of the implemented interface
+ * .boom = boom_cb,
+ * };
+ *
+ * void main(void) {
+ * void *userdata = whatever;
+ * struct spa_hook hook;
+ * struct party *p = start_the_party();
+ *
+ * party_add_event_listener(p, &hook, &events, userdata);
+ *
+ * mainloop();
+ * return 0;
+ * }
+ *
+ * \endcode
+ */
+
+/**
+ * \addtogroup spa_hooks
+ * \{
+ */
+
+/** \struct spa_hook_list
+ * A list of hooks. This struct is primarily used by
+ * implementation that use multiple caller-provided \ref spa_hook. */
+struct spa_hook_list {
+ struct spa_list list;
+};
+
+
+/** \struct spa_hook
+ * A hook, contains the structure with functions and the data passed
+ * to the functions.
+ *
+ * A hook should be treated as opaque by the caller.
+ */
+struct spa_hook {
+ struct spa_list link;
+ struct spa_callbacks cb;
+ /** callback and data for the hook list, private to the
+ * hook_list implementor */
+ void (*removed) (struct spa_hook *hook);
+ void *priv;
+};
+
+/** Initialize a hook list to the empty list*/
+static inline 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)
+{
+ return spa_list_is_empty(&list->list);
+}
+
+/** Append a hook. */
+static inline void spa_hook_list_append(struct spa_hook_list *list,
+ struct spa_hook *hook,
+ const void *funcs, void *data)
+{
+ spa_zero(*hook);
+ hook->cb = SPA_CALLBACKS_INIT(funcs, data);
+ spa_list_append(&list->list, &hook->link);
+}
+
+/** Prepend a hook */
+static inline void spa_hook_list_prepend(struct spa_hook_list *list,
+ struct spa_hook *hook,
+ const void *funcs, void *data)
+{
+ spa_zero(*hook);
+ hook->cb = SPA_CALLBACKS_INIT(funcs, data);
+ spa_list_prepend(&list->list, &hook->link);
+}
+
+/** Remove a hook */
+static inline void spa_hook_remove(struct spa_hook *hook)
+{
+ if (spa_list_is_initialized(&hook->link))
+ spa_list_remove(&hook->link);
+ if (hook->removed)
+ hook->removed(hook);
+}
+
+/** Remove all hooks from the list */
+static inline 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_hook_list_isolate(struct spa_hook_list *list,
+ struct spa_hook_list *save,
+ struct spa_hook *hook,
+ const void *funcs, void *data)
+{
+ /* init save list and move hooks to it */
+ spa_hook_list_init(save);
+ spa_list_insert_list(&save->list, &list->list);
+ /* init hooks and add single hook */
+ spa_hook_list_init(list);
+ spa_hook_list_append(list, hook, funcs, data);
+}
+
+static inline void
+spa_hook_list_join(struct spa_hook_list *list,
+ struct spa_hook_list *save)
+{
+ spa_list_insert_list(&list->list, &save->list);
+}
+
+#define spa_hook_list_call_simple(l,type,method,vers,...) \
+({ \
+ struct spa_hook_list *_l = l; \
+ struct spa_hook *_h, *_t; \
+ spa_list_for_each_safe(_h, _t, &_l->list, link) \
+ spa_callbacks_call(&_h->cb,type,method,vers, ## __VA_ARGS__); \
+})
+
+/** Call all hooks in a list, starting from the given one and optionally stopping
+ * after calling the first non-NULL function, returns the number of methods
+ * called */
+#define spa_hook_list_do_call(l,start,type,method,vers,once,...) \
+({ \
+ struct spa_hook_list *_list = l; \
+ struct spa_list *_s = start ? (struct spa_list *)start : &_list->list; \
+ struct spa_hook _cursor = { 0 }, *_ci; \
+ int _count = 0; \
+ spa_list_cursor_start(_cursor, _s, link); \
+ spa_list_for_each_cursor(_ci, _cursor, &_list->list, link) { \
+ if (spa_callbacks_call(&_ci->cb,type,method,vers, ## __VA_ARGS__)) { \
+ _count++; \
+ if (once) \
+ break; \
+ } \
+ } \
+ spa_list_cursor_end(_cursor, link); \
+ _count; \
+})
+
+/**
+ * Call the method named \a m for each element in list \a l.
+ * \a t specifies the type of the callback struct.
+ */
+#define spa_hook_list_call(l,t,m,v,...) spa_hook_list_do_call(l,NULL,t,m,v,false,##__VA_ARGS__)
+/**
+ * Call the method named \a m for each element in list \a l, stopping after
+ * the first invocation.
+ * \a t specifies the type of the callback struct.
+ */
+#define spa_hook_list_call_once(l,t,m,v,...) spa_hook_list_do_call(l,NULL,t,m,v,true,##__VA_ARGS__)
+
+#define spa_hook_list_call_start(l,s,t,m,v,...) spa_hook_list_do_call(l,s,t,m,v,false,##__VA_ARGS__)
+#define spa_hook_list_call_once_start(l,s,t,m,v,...) spa_hook_list_do_call(l,s,t,m,v,true,##__VA_ARGS__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SPA_HOOK_H */
diff --git a/spa/include/spa/utils/json-pod.h b/spa/include/spa/utils/json-pod.h
new file mode 100644
index 0000000..34c7e08
--- /dev/null
+++ b/spa/include/spa/utils/json-pod.h
@@ -0,0 +1,177 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_UTILS_JSON_POD_H
+#define SPA_UTILS_JSON_POD_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/pod/pod.h>
+#include <spa/pod/builder.h>
+#include <spa/debug/types.h>
+
+/** \defgroup spa_json_pod JSON to POD
+ * JSON to POD conversion
+ */
+
+/**
+ * \addtogroup spa_json_pod
+ * \{
+ */
+
+static inline 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;
+ char key[256];
+ struct spa_pod_frame f[1];
+ struct spa_json it[1];
+ int l, res;
+ const char *v;
+ uint32_t type;
+
+ if (spa_json_is_object(value, len) && info != NULL) {
+ if ((ti = spa_debug_type_find(NULL, info->parent)) == NULL)
+ return -EINVAL;
+
+ 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) {
+ 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))
+ continue;
+ spa_pod_builder_prop(b, type, 0);
+ if ((res = spa_json_to_pod_part(b, flags, id, pi, &it[0], v, l)) < 0)
+ return res;
+ }
+ spa_pod_builder_pop(b, &f[0]);
+ }
+ else if (spa_json_is_array(value, len)) {
+ if (info == NULL || info->parent == SPA_TYPE_Struct) {
+ spa_pod_builder_push_struct(b, &f[0]);
+ } else {
+ spa_pod_builder_push_array(b, &f[0]);
+ info = info->values;
+ }
+ spa_json_enter(iter, &it[0]);
+ while ((l = spa_json_next(&it[0], &v)) > 0)
+ if ((res = spa_json_to_pod_part(b, flags, id, info, &it[0], v, l)) < 0)
+ return res;
+ spa_pod_builder_pop(b, &f[0]);
+ }
+ else if (spa_json_is_float(value, len)) {
+ float val = 0.0f;
+ spa_json_parse_float(value, len, &val);
+ switch (info ? info->parent : (uint32_t)SPA_TYPE_Struct) {
+ case SPA_TYPE_Bool:
+ spa_pod_builder_bool(b, val >= 0.5f);
+ break;
+ case SPA_TYPE_Id:
+ spa_pod_builder_id(b, val);
+ break;
+ case SPA_TYPE_Int:
+ spa_pod_builder_int(b, val);
+ break;
+ case SPA_TYPE_Long:
+ spa_pod_builder_long(b, val);
+ break;
+ case SPA_TYPE_Struct:
+ if (spa_json_is_int(value, len))
+ spa_pod_builder_int(b, val);
+ else
+ spa_pod_builder_float(b, val);
+ break;
+ case SPA_TYPE_Float:
+ spa_pod_builder_float(b, val);
+ break;
+ case SPA_TYPE_Double:
+ spa_pod_builder_double(b, val);
+ break;
+ default:
+ spa_pod_builder_none(b);
+ break;
+ }
+ }
+ else if (spa_json_is_bool(value, len)) {
+ bool val = false;
+ spa_json_parse_bool(value, len, &val);
+ spa_pod_builder_bool(b, val);
+ }
+ else if (spa_json_is_null(value, len)) {
+ spa_pod_builder_none(b);
+ }
+ else {
+ char *val = (char*)alloca(len+1);
+ spa_json_parse_stringn(value, len, val, len+1);
+ switch (info ? info->parent : (uint32_t)SPA_TYPE_Struct) {
+ case SPA_TYPE_Id:
+ if ((ti = spa_debug_type_find_short(info->values, val)) != NULL)
+ type = ti->type;
+ else if (!spa_atou32(val, &type, 0))
+ return -EINVAL;
+ spa_pod_builder_id(b, type);
+ break;
+ case SPA_TYPE_Struct:
+ case SPA_TYPE_String:
+ spa_pod_builder_string(b, val);
+ break;
+ default:
+ spa_pod_builder_none(b);
+ break;
+ }
+ }
+ 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)
+{
+ struct spa_json iter;
+ const char *val;
+
+ spa_json_init(&iter, value, len);
+ if ((len = spa_json_next(&iter, &val)) <= 0)
+ return -EINVAL;
+
+ return spa_json_to_pod_part(b, flags, info->type, info, &iter, val, len);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_UTILS_JSON_POD_H */
diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h
new file mode 100644
index 0000000..0f93149
--- /dev/null
+++ b/spa/include/spa/utils/json.h
@@ -0,0 +1,485 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#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>
+
+/** \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;
+ uint32_t state;
+ uint32_t depth;
+};
+
+#define SPA_JSON_INIT(data,size) ((struct spa_json) { (data), (data)+(size), })
+
+static inline void spa_json_init(struct spa_json * iter, const char *data, size_t size)
+{
+ *iter = SPA_JSON_INIT(data, size);
+}
+#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), })
+
+static inline void spa_json_enter(struct spa_json * iter, struct spa_json * sub)
+{
+ *sub = SPA_JSON_ENTER(iter);
+}
+
+#define SPA_JSON_SAVE(iter) ((struct spa_json) { (iter)->cur, (iter)->end, })
+
+/** Get the next token. \a value points to the token and the return value
+ * is the length. */
+static inline int spa_json_next(struct spa_json * iter, const char **value)
+{
+ int utf8_remain = 0;
+ enum { __NONE, __STRUCT, __BARE, __STRING, __UTF8, __ESC, __COMMENT };
+
+ *value = iter->cur;
+ for (; iter->cur < iter->end; iter->cur++) {
+ unsigned char cur = (unsigned char)*iter->cur;
+ again:
+ switch (iter->state) {
+ case __NONE:
+ iter->state = __STRUCT;
+ iter->depth = 0;
+ goto again;
+ case __STRUCT:
+ switch (cur) {
+ case '\0': case '\t': case ' ': case '\r': case '\n': case ':': case '=': case ',':
+ continue;
+ case '#':
+ iter->state = __COMMENT;
+ continue;
+ case '"':
+ *value = iter->cur;
+ iter->state = __STRING;
+ continue;
+ case '[': case '{':
+ *value = iter->cur;
+ if (++iter->depth > 1)
+ continue;
+ iter->cur++;
+ return 1;
+ case '}': case ']':
+ if (iter->depth == 0) {
+ if (iter->parent)
+ iter->parent->cur = iter->cur;
+ return 0;
+ }
+ --iter->depth;
+ continue;
+ default:
+ *value = iter->cur;
+ iter->state = __BARE;
+ }
+ continue;
+ case __BARE:
+ switch (cur) {
+ case '\t': case ' ': case '\r': case '\n':
+ case ':': case ',': case '=': case ']': case '}':
+ iter->state = __STRUCT;
+ if (iter->depth > 0)
+ goto again;
+ return iter->cur - *value;
+ }
+ continue;
+ case __STRING:
+ switch (cur) {
+ case '\\':
+ iter->state = __ESC;
+ continue;
+ case '"':
+ iter->state = __STRUCT;
+ 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;
+ continue;
+ default:
+ if (cur >= 32 && cur <= 126)
+ continue;
+ }
+ return -1;
+ case __UTF8:
+ switch (cur) {
+ case 128 ... 191:
+ if (--utf8_remain == 0)
+ iter->state = __STRING;
+ continue;
+ }
+ return -1;
+ case __ESC:
+ switch (cur) {
+ case '"': case '\\': case '/': case 'b': case 'f':
+ case 'n': case 'r': case 't': case 'u':
+ iter->state = __STRING;
+ continue;
+ }
+ return -1;
+ case __COMMENT:
+ switch (cur) {
+ case '\n': case '\r':
+ iter->state = __STRUCT;
+ }
+ }
+
+ }
+ if (iter->depth != 0)
+ return -1;
+ if (iter->state != __STRUCT) {
+ iter->state = __STRUCT;
+ return iter->cur - *value;
+ }
+ return 0;
+}
+
+static inline int spa_json_enter_container(struct spa_json *iter, struct spa_json *sub, char type)
+{
+ const char *value;
+ if (spa_json_next(iter, &value) <= 0 || *value != type)
+ return -1;
+ spa_json_enter(iter, sub);
+ return 1;
+}
+
+static inline int spa_json_is_container(const char *val, int len)
+{
+ return len > 0 && (*val == '{' || *val == '[');
+}
+
+static inline int spa_json_container_len(struct spa_json *iter, const char *value, int len)
+{
+ const char *val;
+ struct spa_json sub;
+ spa_json_enter(iter, &sub);
+ while (spa_json_next(&sub, &val) > 0);
+ return sub.cur + 1 - value;
+}
+
+/* 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)
+{
+ 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)
+{
+ char *end;
+ if (strspn(val, "+-0123456789.Ee") < (size_t)len)
+ return 0;
+ *result = spa_strtof(val, &end);
+ return len > 0 && end == val + len;
+}
+
+static inline bool spa_json_is_float(const char *val, int len)
+{
+ float dummy;
+ return spa_json_parse_float(val, len, &dummy);
+}
+static inline int spa_json_get_float(struct spa_json *iter, float *res)
+{
+ const char *value;
+ int len;
+ if ((len = spa_json_next(iter, &value)) <= 0)
+ return -1;
+ return spa_json_parse_float(value, len, res);
+}
+
+static inline char *spa_json_format_float(char *str, int size, float val)
+{
+ if (SPA_UNLIKELY(!isnormal(val))) {
+ if (val == INFINITY)
+ val = FLT_MAX;
+ else if (val == -INFINITY)
+ val = FLT_MIN;
+ else
+ val = 0.0f;
+ }
+ return spa_dtoa(str, size, val);
+}
+
+/* int */
+static inline int spa_json_parse_int(const char *val, int len, int *result)
+{
+ char *end;
+ *result = strtol(val, &end, 0);
+ return len > 0 && end == val + 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 -1;
+ return spa_json_parse_int(value, len, res);
+}
+
+/* bool */
+static inline bool spa_json_is_true(const char *val, int len)
+{
+ return len == 4 && strncmp(val, "true", 4) == 0;
+}
+
+static inline bool spa_json_is_false(const char *val, int len)
+{
+ return len == 5 && strncmp(val, "false", 5) == 0;
+}
+
+static inline bool spa_json_is_bool(const char *val, int len)
+{
+ return spa_json_is_true(val, len) || spa_json_is_false(val, len);
+}
+
+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 -1;
+ 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 == '"';
+}
+
+static inline 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;
+}
+
+static inline int spa_json_parse_stringn(const char *val, int len, char *result, int maxlen)
+{
+ const char *p;
+ if (maxlen <= len)
+ return -1;
+ if (!spa_json_is_string(val, len)) {
+ if (result != val)
+ strncpy(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;
+}
+
+static inline int spa_json_parse_string(const char *val, int len, char *result)
+{
+ return spa_json_parse_stringn(val, len, result, len+1);
+}
+
+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 -1;
+ return spa_json_parse_stringn(value, len, res, maxlen);
+}
+
+static inline 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/keys.h b/spa/include/spa/utils/keys.h
new file mode 100644
index 0000000..d7b3e29
--- /dev/null
+++ b/spa/include/spa/utils/keys.h
@@ -0,0 +1,151 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_UTILS_KEYS_H
+#define SPA_UTILS_KEYS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup spa_keys Key Names
+ * Key names used by SPA plugins
+ */
+
+/**
+ * \addtogroup spa_keys
+ * \{
+ */
+
+/** for objects */
+#define SPA_KEY_OBJECT_PATH "object.path" /**< a unique path to
+ * identity the object */
+
+#define SPA_KEY_MEDIA_CLASS "media.class" /**< Media class
+ * Ex. "Audio/Device",
+ * "Video/Source",... */
+#define SPA_KEY_MEDIA_ROLE "media.role" /**< Role: Movie, Music, Camera,
+ * Screen, Communication, Game,
+ * Notification, DSP, Production,
+ * Accessibility, Test */
+/** keys for udev api */
+#define SPA_KEY_API_UDEV "api.udev" /**< key for the udev api */
+#define SPA_KEY_API_UDEV_MATCH "api.udev.match" /**< udev subsystem match */
+
+/** keys for alsa api */
+#define SPA_KEY_API_ALSA "api.alsa" /**< key for the alsa api */
+#define SPA_KEY_API_ALSA_PATH "api.alsa.path" /**< alsa device path as can be
+ * used in snd_pcm_open() and
+ * snd_ctl_open(). */
+#define SPA_KEY_API_ALSA_CARD "api.alsa.card" /**< alsa card number */
+#define SPA_KEY_API_ALSA_USE_UCM "api.alsa.use-ucm" /**< if UCM should be used */
+#define SPA_KEY_API_ALSA_IGNORE_DB "api.alsa.ignore-dB" /**< if decibel info should be ignored */
+#define SPA_KEY_API_ALSA_OPEN_UCM "api.alsa.open.ucm" /**< if UCM should be opened card */
+#define SPA_KEY_API_ALSA_DISABLE_LONGNAME \
+ "api.alsa.disable-longname" /**< if card long name should not be passed to MIDI port */
+
+/** info from alsa card_info */
+#define SPA_KEY_API_ALSA_CARD_ID "api.alsa.card.id" /**< id from card_info */
+#define SPA_KEY_API_ALSA_CARD_COMPONENTS \
+ "api.alsa.card.components" /**< components from card_info */
+#define SPA_KEY_API_ALSA_CARD_DRIVER "api.alsa.card.driver" /**< driver from card_info */
+#define SPA_KEY_API_ALSA_CARD_NAME "api.alsa.card.name" /**< name from card_info */
+#define SPA_KEY_API_ALSA_CARD_LONGNAME "api.alsa.card.longname" /**< longname from card_info */
+#define SPA_KEY_API_ALSA_CARD_MIXERNAME "api.alsa.card.mixername" /**< mixername from card_info */
+
+/** info from alsa pcm_info */
+#define SPA_KEY_API_ALSA_PCM_ID "api.alsa.pcm.id" /**< id from pcm_info */
+#define SPA_KEY_API_ALSA_PCM_CARD "api.alsa.pcm.card" /**< card from pcm_info */
+#define SPA_KEY_API_ALSA_PCM_NAME "api.alsa.pcm.name" /**< name from pcm_info */
+#define SPA_KEY_API_ALSA_PCM_SUBNAME "api.alsa.pcm.subname" /**< subdevice_name from pcm_info */
+#define SPA_KEY_API_ALSA_PCM_STREAM "api.alsa.pcm.stream" /**< stream type from pcm_info */
+#define SPA_KEY_API_ALSA_PCM_CLASS "api.alsa.pcm.class" /**< class from pcm_info as string */
+#define SPA_KEY_API_ALSA_PCM_DEVICE "api.alsa.pcm.device" /**< device from pcm_info */
+#define SPA_KEY_API_ALSA_PCM_SUBDEVICE "api.alsa.pcm.subdevice" /**< subdevice from pcm_info */
+#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 */
+
+/** 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
+ * used in open() */
+
+/** keys for libcamera api */
+#define SPA_KEY_API_LIBCAMERA "api.libcamera" /**< key for the libcamera api */
+#define SPA_KEY_API_LIBCAMERA_PATH "api.libcamera.path" /**< libcamera device path as can be
+ * used in open() */
+#define SPA_KEY_API_LIBCAMERA_LOCATION "api.libcamera.location" /**< location of the camera:
+ * "front", "back" or "external" */
+
+/** info from libcamera_capability */
+#define SPA_KEY_API_LIBCAMERA_CAP_DRIVER "api.libcamera.cap.driver" /**< driver from capbility */
+#define SPA_KEY_API_LIBCAMERA_CAP_CARD "api.libcamera.cap.card" /**< caps from capability */
+#define SPA_KEY_API_LIBCAMERA_CAP_BUS_INFO "api.libcamera.cap.bus_info"/**< bus_info from capability */
+#define SPA_KEY_API_LIBCAMERA_CAP_VERSION "api.libcamera.cap.version" /**< version from capability as %u.%u.%u */
+#define SPA_KEY_API_LIBCAMERA_CAP_CAPABILITIES \
+ "api.libcamera.cap.capabilities" /**< capabilities from capability */
+#define SPA_KEY_API_LIBCAMERA_CAP_DEVICE_CAPS \
+ "api.libcamera.cap.device-caps" /**< device_caps from capability */
+/** info from v4l2_capability */
+#define SPA_KEY_API_V4L2_CAP_DRIVER "api.v4l2.cap.driver" /**< driver from capbility */
+#define SPA_KEY_API_V4L2_CAP_CARD "api.v4l2.cap.card" /**< caps from capability */
+#define SPA_KEY_API_V4L2_CAP_BUS_INFO "api.v4l2.cap.bus_info" /**< bus_info from capability */
+#define SPA_KEY_API_V4L2_CAP_VERSION "api.v4l2.cap.version" /**< version from capability as %u.%u.%u */
+#define SPA_KEY_API_V4L2_CAP_CAPABILITIES \
+ "api.v4l2.cap.capabilities" /**< capabilities from capability */
+#define SPA_KEY_API_V4L2_CAP_DEVICE_CAPS \
+ "api.v4l2.cap.device-caps" /**< device_caps from capability */
+
+
+/** keys for bluez5 api */
+#define SPA_KEY_API_BLUEZ5 "api.bluez5" /**< key for the bluez5 api */
+#define SPA_KEY_API_BLUEZ5_PATH "api.bluez5.path" /**< a bluez5 path */
+#define SPA_KEY_API_BLUEZ5_DEVICE "api.bluez5.device" /**< an internal bluez5 device */
+#define SPA_KEY_API_BLUEZ5_CONNECTION "api.bluez5.connection" /**< bluez5 device connection status */
+#define SPA_KEY_API_BLUEZ5_TRANSPORT "api.bluez5.transport" /**< an internal bluez5 transport */
+#define SPA_KEY_API_BLUEZ5_PROFILE "api.bluez5.profile" /**< a bluetooth profile */
+#define SPA_KEY_API_BLUEZ5_ADDRESS "api.bluez5.address" /**< a bluetooth address */
+#define SPA_KEY_API_BLUEZ5_CODEC "api.bluez5.codec" /**< a bluetooth codec */
+#define SPA_KEY_API_BLUEZ5_CLASS "api.bluez5.class" /**< a bluetooth class */
+#define SPA_KEY_API_BLUEZ5_ICON "api.bluez5.icon" /**< a bluetooth icon */
+#define SPA_KEY_API_BLUEZ5_ROLE "api.bluez5.role" /**< "client" or "server" */
+
+/** keys for jack api */
+#define SPA_KEY_API_JACK "api.jack" /**< key for the JACK api */
+#define SPA_KEY_API_JACK_SERVER "api.jack.server" /**< a jack server name */
+#define SPA_KEY_API_JACK_CLIENT "api.jack.client" /**< an internal jack client */
+
+/** keys for glib api */
+#define SPA_KEY_API_GLIB_MAINLOOP "api.glib.mainloop" /**< whether glib mainloop runs
+ * in same thread as PW loop */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_UTILS_KEYS_H */
diff --git a/spa/include/spa/utils/list.h b/spa/include/spa/utils/list.h
new file mode 100644
index 0000000..b57657a
--- /dev/null
+++ b/spa/include/spa/utils/list.h
@@ -0,0 +1,166 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_LIST_H
+#define SPA_LIST_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \defgroup spa_list List
+ * Doubly linked list data structure
+ */
+
+/**
+ * \addtogroup spa_list List
+ * \{
+ */
+
+struct spa_list {
+ struct spa_list *next;
+ struct spa_list *prev;
+};
+
+#define SPA_LIST_INIT(list) ((struct spa_list){ (list), (list) })
+
+static inline void spa_list_init(struct spa_list *list)
+{
+ *list = SPA_LIST_INIT(list);
+}
+
+static inline int spa_list_is_initialized(struct spa_list *list)
+{
+ return !!list->prev;
+}
+
+#define spa_list_is_empty(l) ((l)->next == (l))
+
+static inline void spa_list_insert(struct spa_list *list, struct spa_list *elem)
+{
+ elem->prev = list;
+ elem->next = list->next;
+ list->next = elem;
+ elem->next->prev = elem;
+}
+
+static inline void spa_list_insert_list(struct spa_list *list, struct spa_list *other)
+{
+ if (spa_list_is_empty(other))
+ return;
+ other->next->prev = list;
+ other->prev->next = list->next;
+ list->next->prev = other->prev;
+ list->next = other->next;
+}
+
+static inline void spa_list_remove(struct spa_list *elem)
+{
+ elem->prev->next = elem->next;
+ elem->next->prev = elem->prev;
+}
+
+#define spa_list_first(head, type, member) \
+ SPA_CONTAINER_OF((head)->next, type, member)
+
+#define spa_list_last(head, type, member) \
+ SPA_CONTAINER_OF((head)->prev, type, member)
+
+#define spa_list_append(list, item) \
+ spa_list_insert((list)->prev, item)
+
+#define spa_list_prepend(list, item) \
+ spa_list_insert(list, item)
+
+#define spa_list_is_end(pos, head, member) \
+ (&(pos)->member == (head))
+
+#define spa_list_next(pos, member) \
+ SPA_CONTAINER_OF((pos)->member.next, __typeof__(*(pos)), member)
+
+#define spa_list_prev(pos, member) \
+ SPA_CONTAINER_OF((pos)->member.prev, __typeof__(*(pos)), member)
+
+#define spa_list_consume(pos, head, member) \
+ for ((pos) = spa_list_first(head, __typeof__(*(pos)), member); \
+ !spa_list_is_empty(head); \
+ (pos) = spa_list_first(head, __typeof__(*(pos)), member))
+
+#define spa_list_for_each_next(pos, head, curr, member) \
+ for ((pos) = spa_list_first(curr, __typeof__(*(pos)), member); \
+ !spa_list_is_end(pos, head, member); \
+ (pos) = spa_list_next(pos, member))
+
+#define spa_list_for_each_prev(pos, head, curr, member) \
+ for ((pos) = spa_list_last(curr, __typeof__(*(pos)), member); \
+ !spa_list_is_end(pos, head, member); \
+ (pos) = spa_list_prev(pos, member))
+
+#define spa_list_for_each(pos, head, member) \
+ spa_list_for_each_next(pos, head, head, member)
+
+#define spa_list_for_each_reverse(pos, head, member) \
+ spa_list_for_each_prev(pos, head, head, member)
+
+#define spa_list_for_each_safe_next(pos, tmp, head, curr, member) \
+ for ((pos) = spa_list_first(curr, __typeof__(*(pos)), member); \
+ (tmp) = spa_list_next(pos, member), \
+ !spa_list_is_end(pos, head, member); \
+ (pos) = (tmp))
+
+#define spa_list_for_each_safe_prev(pos, tmp, head, curr, member) \
+ for ((pos) = spa_list_last(curr, __typeof__(*(pos)), member); \
+ (tmp) = spa_list_prev(pos, member), \
+ !spa_list_is_end(pos, head, member); \
+ (pos) = (tmp))
+
+#define spa_list_for_each_safe(pos, tmp, head, member) \
+ spa_list_for_each_safe_next(pos, tmp, head, head, member)
+
+#define spa_list_for_each_safe_reverse(pos, tmp, head, member) \
+ spa_list_for_each_safe_prev(pos, tmp, head, head, member)
+
+#define spa_list_cursor_start(cursor, head, member) \
+ spa_list_prepend(head, &(cursor).member)
+
+#define spa_list_for_each_cursor(pos, cursor, head, member) \
+ for((pos) = spa_list_first(&(cursor).member, __typeof__(*(pos)), member); \
+ spa_list_remove(&(pos)->member), \
+ spa_list_append(&(cursor).member, &(pos)->member), \
+ !spa_list_is_end(pos, head, member); \
+ (pos) = spa_list_next(&(cursor), member))
+
+#define spa_list_cursor_end(cursor, member) \
+ spa_list_remove(&(cursor).member)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_LIST_H */
diff --git a/spa/include/spa/utils/names.h b/spa/include/spa/utils/names.h
new file mode 100644
index 0000000..2ae04ed
--- /dev/null
+++ b/spa/include/spa/utils/names.h
@@ -0,0 +1,164 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_UTILS_NAMES_H
+#define SPA_UTILS_NAMES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup spa_names Factory Names
+ * SPA plugin factory names
+ */
+
+/**
+ * \addtogroup spa_names
+ * \{
+ */
+
+/** for factory names */
+#define SPA_NAME_SUPPORT_CPU "support.cpu" /**< A CPU interface */
+#define SPA_NAME_SUPPORT_DBUS "support.dbus" /**< A DBUS interface */
+#define SPA_NAME_SUPPORT_LOG "support.log" /**< A Log interface */
+#define SPA_NAME_SUPPORT_LOOP "support.loop" /**< A Loop/LoopControl/LoopUtils
+ * interface */
+#define SPA_NAME_SUPPORT_SYSTEM "support.system" /**< A System interface */
+
+#define SPA_NAME_SUPPORT_NODE_DRIVER "support.node.driver" /**< A dummy driver node */
+
+/* control mixer */
+#define SPA_NAME_CONTROL_MIXER "control.mixer" /**< mixes control streams */
+
+/* audio mixer */
+#define SPA_NAME_AUDIO_MIXER "audio.mixer" /**< mixes the raw audio on N input
+ * ports together on the output
+ * port */
+#define SPA_NAME_AUDIO_MIXER_DSP "audio.mixer.dsp" /**< mixes mono audio with fixed input
+ * and output buffer sizes. supported
+ * formats must include f32 and
+ * optionally f64 and s24_32 */
+
+/** audio processing */
+#define SPA_NAME_AUDIO_PROCESS_FORMAT "audio.process.format" /**< processes raw audio from one format
+ * to another */
+#define SPA_NAME_AUDIO_PROCESS_CHANNELMIX \
+ "audio.process.channelmix" /**< mixes raw audio channels and applies
+ * volume change. */
+#define SPA_NAME_AUDIO_PROCESS_RESAMPLE \
+ "audio.process.resample" /**< resamples raw audio */
+#define SPA_NAME_AUDIO_PROCESS_DEINTERLEAVE \
+ "audio.process.deinterleave" /**< deinterleave raw audio channels */
+#define SPA_NAME_AUDIO_PROCESS_INTERLEAVE \
+ "audio.process.interleave" /**< interleave raw audio channels */
+
+
+/** audio convert combines some of the audio processing */
+#define SPA_NAME_AUDIO_CONVERT "audio.convert" /**< converts raw audio from one format
+ * to another. Must include at least
+ * format, channelmix and resample
+ * processing */
+#define SPA_NAME_AUDIO_ADAPT "audio.adapt" /**< combination of a node and an
+ * audio.convert. Does clock slaving */
+
+#define SPA_NAME_AEC "audio.aec" /**< Echo canceling */
+
+/** video processing */
+#define SPA_NAME_VIDEO_PROCESS_FORMAT "video.process.format" /**< processes raw video from one format
+ * to another */
+#define SPA_NAME_VIDEO_PROCESS_SCALE "video.process.scale" /**< scales raw video */
+
+/** video convert combines some of the video processing */
+#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_ADAPT "video.adapt" /**< combination of a node and a
+ * video.convert. */
+/** keys for alsa factory names */
+#define SPA_NAME_API_ALSA_ENUM_UDEV "api.alsa.enum.udev" /**< an alsa udev Device interface */
+#define SPA_NAME_API_ALSA_PCM_DEVICE "api.alsa.pcm.device" /**< an alsa Device interface */
+#define SPA_NAME_API_ALSA_PCM_SOURCE "api.alsa.pcm.source" /**< an alsa Node interface for
+ * capturing PCM */
+#define SPA_NAME_API_ALSA_PCM_SINK "api.alsa.pcm.sink" /**< an alsa Node interface for
+ * playback PCM */
+#define SPA_NAME_API_ALSA_SEQ_DEVICE "api.alsa.seq.device" /**< an alsa Midi device */
+#define SPA_NAME_API_ALSA_SEQ_SOURCE "api.alsa.seq.source" /**< an alsa Node interface for
+ * capture of midi */
+#define SPA_NAME_API_ALSA_SEQ_SINK "api.alsa.seq.sink" /**< an alsa Node interface for
+ * playback of midi */
+#define SPA_NAME_API_ALSA_SEQ_BRIDGE "api.alsa.seq.bridge" /**< an alsa Node interface for
+ * bridging midi ports */
+#define SPA_NAME_API_ALSA_ACP_DEVICE "api.alsa.acp.device" /**< an alsa ACP Device interface */
+#define SPA_NAME_API_ALSA_COMPRESS_OFFLOAD_SINK "api.alsa.compress.offload.sink" /**< an alsa Node interface for
+ * compressed audio */
+
+/** keys for bluez5 factory names */
+#define SPA_NAME_API_BLUEZ5_ENUM_DBUS "api.bluez5.enum.dbus" /**< a dbus Device interface */
+#define SPA_NAME_API_BLUEZ5_DEVICE "api.bluez5.device" /**< a Device interface */
+#define SPA_NAME_API_BLUEZ5_MEDIA_SINK "api.bluez5.media.sink" /**< a playback Node interface for A2DP/BAP profiles */
+#define SPA_NAME_API_BLUEZ5_MEDIA_SOURCE "api.bluez5.media.source" /**< a capture Node interface for A2DP/BAP profiles */
+#define SPA_NAME_API_BLUEZ5_A2DP_SINK "api.bluez5.a2dp.sink" /**< alias for media.sink */
+#define SPA_NAME_API_BLUEZ5_A2DP_SOURCE "api.bluez5.a2dp.source" /**< alias for media.source */
+#define SPA_NAME_API_BLUEZ5_SCO_SINK "api.bluez5.sco.sink" /**< a playback Node interface for HSP/HFP profiles */
+#define SPA_NAME_API_BLUEZ5_SCO_SOURCE "api.bluez5.sco.source" /**< a capture Node interface for HSP/HFP profiles */
+#define SPA_NAME_API_BLUEZ5_MIDI_ENUM "api.bluez5.midi.enum" /**< a dbus midi Device interface */
+#define SPA_NAME_API_BLUEZ5_MIDI_NODE "api.bluez5.midi.node" /**< a midi Node interface */
+
+/** keys for codec factory names */
+#define SPA_NAME_API_CODEC_BLUEZ5_MEDIA "api.codec.bluez5.media" /**< Bluez5 Media codec plugin */
+
+/** keys for v4l2 factory names */
+#define SPA_NAME_API_V4L2_ENUM_UDEV "api.v4l2.enum.udev" /**< a v4l2 udev Device interface */
+#define SPA_NAME_API_V4L2_DEVICE "api.v4l2.device" /**< a v4l2 Device interface */
+#define SPA_NAME_API_V4L2_SOURCE "api.v4l2.source" /**< a v4l2 Node interface for
+ * capturing */
+
+/** keys for libcamera factory names */
+#define SPA_NAME_API_LIBCAMERA_ENUM_CLIENT "api.libcamera.enum.client" /**< a libcamera client Device interface */
+#define SPA_NAME_API_LIBCAMERA_ENUM_MANAGER "api.libcamera.enum.manager" /**< a libcamera manager Device interface */
+#define SPA_NAME_API_LIBCAMERA_DEVICE "api.libcamera.device" /**< a libcamera Device interface */
+#define SPA_NAME_API_LIBCAMERA_SOURCE "api.libcamera.source" /**< a libcamera Node interface for
+ * capturing */
+
+/** keys for jack factory names */
+#define SPA_NAME_API_JACK_DEVICE "api.jack.device" /**< a jack device. This is a
+ * client connected to a server */
+#define SPA_NAME_API_JACK_SOURCE "api.jack.source" /**< a jack source */
+#define SPA_NAME_API_JACK_SINK "api.jack.sink" /**< a jack sink */
+
+/** keys for vulkan factory names */
+#define SPA_NAME_API_VULKAN_COMPUTE_SOURCE \
+ "api.vulkan.compute.source" /**< a vulkan compute source. */
+#define SPA_NAME_API_VULKAN_COMPUTE_FILTER \
+ "api.vulkan.compute.filter" /**< a vulkan compute filter. */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_UTILS_NAMES_H */
diff --git a/spa/include/spa/utils/result.h b/spa/include/spa/utils/result.h
new file mode 100644
index 0000000..05a1ef9
--- /dev/null
+++ b/spa/include/spa/utils/result.h
@@ -0,0 +1,72 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_UTILS_RESULT_H
+#define SPA_UTILS_RESULT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \defgroup spa_result Result handling
+ * Asynchronous result utilities
+ */
+
+/**
+ * \addtogroup spa_result
+ * \{
+ */
+
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+
+#define SPA_ASYNC_BIT (1 << 30)
+#define SPA_ASYNC_SEQ_MASK (SPA_ASYNC_BIT - 1)
+#define SPA_ASYNC_MASK (~SPA_ASYNC_SEQ_MASK)
+
+#define SPA_RESULT_IS_OK(res) ((res) >= 0)
+#define SPA_RESULT_IS_ERROR(res) ((res) < 0)
+#define SPA_RESULT_IS_ASYNC(res) (((res) & SPA_ASYNC_MASK) == SPA_ASYNC_BIT)
+
+#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); \
+})
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_UTILS_RESULT_H */
diff --git a/spa/include/spa/utils/ringbuffer.h b/spa/include/spa/utils/ringbuffer.h
new file mode 100644
index 0000000..ed14939
--- /dev/null
+++ b/spa/include/spa/utils/ringbuffer.h
@@ -0,0 +1,188 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_RINGBUFFER_H
+#define SPA_RINGBUFFER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \defgroup spa_ringbuffer Ringbuffer
+ * Ring buffer implementation
+ */
+
+/**
+ * \addtogroup spa_ringbuffer
+ * \{
+ */
+
+struct spa_ringbuffer;
+
+#include <string.h>
+
+#include <spa/utils/defs.h>
+
+/**
+ * A ringbuffer type.
+ */
+struct spa_ringbuffer {
+ uint32_t readindex; /*< the current read index */
+ uint32_t writeindex; /*< the current write index */
+};
+
+#define SPA_RINGBUFFER_INIT() ((struct spa_ringbuffer) { 0, 0 })
+
+/**
+ * Initialize a spa_ringbuffer with \a size.
+ *
+ * \param rbuf a spa_ringbuffer
+ */
+static inline void spa_ringbuffer_init(struct spa_ringbuffer *rbuf)
+{
+ *rbuf = SPA_RINGBUFFER_INIT();
+}
+
+/**
+ * Sets the pointers so that the ringbuffer contains \a size bytes.
+ *
+ * \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)
+{
+ rbuf->readindex = 0;
+ rbuf->writeindex = size;
+}
+
+/**
+ * Get the read index and available bytes for reading.
+ *
+ * \param rbuf a spa_ringbuffer
+ * \param index the value of readindex, should be taken modulo the size of the
+ * ringbuffer memory to get the offset in the ringbuffer memory
+ * \return number of available bytes to read. values < 0 mean
+ * 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)
+{
+ *index = __atomic_load_n(&rbuf->readindex, __ATOMIC_RELAXED);
+ return (int32_t) (__atomic_load_n(&rbuf->writeindex, __ATOMIC_ACQUIRE) - *index);
+}
+
+/**
+ * Read \a len bytes from \a rbuf starting \a offset. \a offset must be taken
+ * modulo \a size and len should be smaller than \a size.
+ *
+ * \param rbuf a struct \ref spa_ringbuffer
+ * \param buffer memory to read from
+ * \param size the size of \a buffer
+ * \param offset offset in \a buffer to read from
+ * \param data destination memory
+ * \param len number of bytes to read
+ */
+static inline void
+spa_ringbuffer_read_data(struct spa_ringbuffer *rbuf,
+ const void *buffer, uint32_t size,
+ uint32_t offset, void *data, uint32_t len)
+{
+ uint32_t l0 = SPA_MIN(len, size - offset), l1 = len - l0;
+ spa_memcpy(data, SPA_PTROFF(buffer, offset, void), l0);
+ if (SPA_UNLIKELY(l1 > 0))
+ spa_memcpy(SPA_PTROFF(data, l0, void), buffer, l1);
+}
+
+/**
+ * Update the read pointer to \a index.
+ *
+ * \param rbuf a spa_ringbuffer
+ * \param index new index
+ */
+static inline void spa_ringbuffer_read_update(struct spa_ringbuffer *rbuf, int32_t index)
+{
+ __atomic_store_n(&rbuf->readindex, index, __ATOMIC_RELEASE);
+}
+
+/**
+ * Get the write index and the number of bytes inside the ringbuffer.
+ *
+ * \param rbuf a spa_ringbuffer
+ * \param index the value of writeindex, should be taken modulo the size of the
+ * ringbuffer memory to get the offset in the ringbuffer memory
+ * \return the fill level of \a rbuf. values < 0 mean
+ * there was an underrun. values > rbuf->size means there
+ * 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)
+{
+ *index = __atomic_load_n(&rbuf->writeindex, __ATOMIC_RELAXED);
+ return (int32_t) (*index - __atomic_load_n(&rbuf->readindex, __ATOMIC_ACQUIRE));
+}
+
+/**
+ * Write \a len bytes to \a buffer starting \a offset. \a offset must be taken
+ * modulo \a size and len should be smaller than \a size.
+ *
+ * \param rbuf a spa_ringbuffer
+ * \param buffer memory to write to
+ * \param size the size of \a buffer
+ * \param offset offset in \a buffer to write to
+ * \param data source memory
+ * \param len number of bytes to write
+ */
+static inline void
+spa_ringbuffer_write_data(struct spa_ringbuffer *rbuf,
+ void *buffer, uint32_t size,
+ uint32_t offset, const void *data, uint32_t len)
+{
+ uint32_t l0 = SPA_MIN(len, size - offset), l1 = len - l0;
+ spa_memcpy(SPA_PTROFF(buffer, offset, void), data, l0);
+ if (SPA_UNLIKELY(l1 > 0))
+ spa_memcpy(buffer, SPA_PTROFF(data, l0, void), l1);
+}
+
+/**
+ * Update the write pointer to \a index
+ *
+ * \param rbuf a spa_ringbuffer
+ * \param index new index
+ */
+static inline void spa_ringbuffer_write_update(struct spa_ringbuffer *rbuf, int32_t index)
+{
+ __atomic_store_n(&rbuf->writeindex, index, __ATOMIC_RELEASE);
+}
+
+/**
+ * \}
+ */
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_RINGBUFFER_H */
diff --git a/spa/include/spa/utils/string.h b/spa/include/spa/utils/string.h
new file mode 100644
index 0000000..787a116
--- /dev/null
+++ b/spa/include/spa/utils/string.h
@@ -0,0 +1,414 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_UTILS_STRING_H
+#define SPA_UTILS_STRING_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <locale.h>
+
+#include <spa/utils/defs.h>
+
+/**
+ * \defgroup spa_string String handling
+ * String handling utilities
+ */
+
+/**
+ * \addtogroup spa_string
+ * \{
+ */
+
+/**
+ * \return true if the two strings are equal, false otherwise
+ *
+ * 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)
+{
+ return SPA_LIKELY(s1 && s2) ? strcmp(s1, s2) == 0 : s1 == s2;
+}
+
+/**
+ * \return true if the two strings are equal, false otherwise
+ *
+ * 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)
+{
+ return SPA_LIKELY(s1 && s2) ? strncmp(s1, s2, len) == 0 : s1 == s2;
+}
+
+
+/**
+ * \return true if \a s starts with the \a prefix or false otherwise.
+ * 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)
+{
+ if (SPA_UNLIKELY(s == NULL))
+ return false;
+
+ spa_assert_se(prefix);
+
+ return strncmp(s, prefix, strlen(prefix)) == 0;
+}
+
+
+/**
+ * \return true if \a s ends with the \a suffix or false otherwise.
+ * 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)
+{
+ size_t l1, l2;
+
+ if (SPA_UNLIKELY(s == NULL))
+ return false;
+
+ spa_assert_se(suffix);
+
+ l1 = strlen(s);
+ l2 = strlen(suffix);
+ return l1 >= l2 && spa_streq(s + l1 - l2, suffix);
+}
+
+/**
+ * Convert \a str to an int32_t with the given \a base and store the
+ * result in \a val.
+ *
+ * On failure, the value of \a val is unmodified.
+ *
+ * \return true on success, false otherwise
+ */
+static inline bool spa_atoi32(const char *str, int32_t *val, int base)
+{
+ char *endptr;
+ long v;
+
+ if (!str || *str =='\0')
+ return false;
+
+ errno = 0;
+ v = strtol(str, &endptr, base);
+ if (errno != 0 || *endptr != '\0')
+ return false;
+
+ if (v != (int32_t)v)
+ return false;
+
+ *val = v;
+ return true;
+}
+
+/**
+ * Convert \a str to an uint32_t with the given \a base and store the
+ * result in \a val.
+ *
+ * On failure, the value of \a val is unmodified.
+ *
+ * \return true on success, false otherwise
+ */
+static inline bool spa_atou32(const char *str, uint32_t *val, int base)
+{
+ char *endptr;
+ unsigned long long v;
+
+ if (!str || *str =='\0')
+ return false;
+
+ errno = 0;
+ v = strtoull(str, &endptr, base);
+ if (errno != 0 || *endptr != '\0')
+ return false;
+
+ if (v != (uint32_t)v)
+ return false;
+
+ *val = v;
+ return true;
+}
+
+/**
+ * Convert \a str to an int64_t with the given \a base and store the
+ * result in \a val.
+ *
+ * On failure, the value of \a val is unmodified.
+ *
+ * \return true on success, false otherwise
+ */
+static inline bool spa_atoi64(const char *str, int64_t *val, int base)
+{
+ char *endptr;
+ long long v;
+
+ if (!str || *str =='\0')
+ return false;
+
+ errno = 0;
+ v = strtoll(str, &endptr, base);
+ if (errno != 0 || *endptr != '\0')
+ return false;
+
+ *val = v;
+ return true;
+}
+
+/**
+ * Convert \a str to an uint64_t with the given \a base and store the
+ * result in \a val.
+ *
+ * On failure, the value of \a val is unmodified.
+ *
+ * \return true on success, false otherwise
+ */
+static inline bool spa_atou64(const char *str, uint64_t *val, int base)
+{
+ char *endptr;
+ unsigned long long v;
+
+ if (!str || *str =='\0')
+ return false;
+
+ errno = 0;
+ v = strtoull(str, &endptr, base);
+ if (errno != 0 || *endptr != '\0')
+ return false;
+
+ *val = v;
+ return true;
+}
+
+/**
+ * Convert \a str to a boolean. Allowed boolean values are "true" and a
+ * literal "1", anything else is false.
+ *
+ * \return true on success, false otherwise
+ */
+static inline bool spa_atob(const char *str)
+{
+ return spa_streq(str, "true") || spa_streq(str, "1");
+}
+
+/**
+ * "Safe" version of vsnprintf. Exactly the same as vsnprintf but the
+ * returned value is clipped to `size - 1` and a negative or zero size
+ * will abort() the program.
+ *
+ * \return The number of bytes printed, capped to `size-1`, or a negative
+ * number on error.
+ */
+SPA_PRINTF_FUNC(3, 0)
+static inline int spa_vscnprintf(char *buffer, size_t size, const char *format, va_list args)
+{
+ int r;
+
+ spa_assert_se((ssize_t)size > 0);
+
+ r = vsnprintf(buffer, size, format, args);
+ if (SPA_UNLIKELY(r < 0))
+ buffer[0] = '\0';
+ if (SPA_LIKELY(r < (ssize_t)size))
+ return r;
+ return size - 1;
+}
+
+/**
+ * "Safe" version of snprintf. Exactly the same as snprintf but the
+ * returned value is clipped to `size - 1` and a negative or zero size
+ * will abort() the program.
+ *
+ * \return The number of bytes printed, capped to `size-1`, or a negative
+ * number on error.
+ */
+SPA_PRINTF_FUNC(3, 4)
+static inline int spa_scnprintf(char *buffer, size_t size, const char *format, ...)
+{
+ int r;
+ va_list args;
+
+ va_start(args, format);
+ r = spa_vscnprintf(buffer, size, format, args);
+ va_end(args);
+
+ return r;
+}
+
+/**
+ * Convert \a str to a float in the C locale.
+ *
+ * If \a endptr is not NULL, a pointer to the character after the last character
+ * used in the conversion is stored in the location referenced by endptr.
+ *
+ * \return the result float.
+ */
+static inline float spa_strtof(const char *str, char **endptr)
+{
+#ifndef __LOCALE_C_ONLY
+ static locale_t locale = NULL;
+ locale_t prev;
+#endif
+ float v;
+#ifndef __LOCALE_C_ONLY
+ if (SPA_UNLIKELY(locale == NULL))
+ locale = newlocale(LC_ALL_MASK, "C", NULL);
+ prev = uselocale(locale);
+#endif
+ v = strtof(str, endptr);
+#ifndef __LOCALE_C_ONLY
+ uselocale(prev);
+#endif
+ return v;
+}
+
+/**
+ * Convert \a str to a float and store the result in \a val.
+ *
+ * On failure, the value of \a val is unmodified.
+ *
+ * \return true on success, false otherwise
+ */
+static inline bool spa_atof(const char *str, float *val)
+{
+ char *endptr;
+ float v;
+
+ if (!str || *str =='\0')
+ return false;
+ errno = 0;
+ v = spa_strtof(str, &endptr);
+ if (errno != 0 || *endptr != '\0')
+ return false;
+
+ *val = v;
+ return true;
+}
+
+/**
+ * Convert \a str to a double in the C locale.
+ *
+ * If \a endptr is not NULL, a pointer to the character after the last character
+ * used in the conversion is stored in the location referenced by endptr.
+ *
+ * \return the result float.
+ */
+static inline double spa_strtod(const char *str, char **endptr)
+{
+#ifndef __LOCALE_C_ONLY
+ static locale_t locale = NULL;
+ locale_t prev;
+#endif
+ double v;
+#ifndef __LOCALE_C_ONLY
+ if (SPA_UNLIKELY(locale == NULL))
+ locale = newlocale(LC_ALL_MASK, "C", NULL);
+ prev = uselocale(locale);
+#endif
+ v = strtod(str, endptr);
+#ifndef __LOCALE_C_ONLY
+ uselocale(prev);
+#endif
+ return v;
+}
+
+/**
+ * Convert \a str to a double and store the result in \a val.
+ *
+ * On failure, the value of \a val is unmodified.
+ *
+ * \return true on success, false otherwise
+ */
+static inline bool spa_atod(const char *str, double *val)
+{
+ char *endptr;
+ double v;
+
+ if (!str || *str =='\0')
+ return false;
+
+ errno = 0;
+ v = spa_strtod(str, &endptr);
+ if (errno != 0 || *endptr != '\0')
+ return false;
+
+ *val = v;
+ return true;
+}
+
+static inline char *spa_dtoa(char *str, size_t size, double val)
+{
+ int i, l;
+ l = spa_scnprintf(str, size, "%f", val);
+ for (i = 0; i < l; i++)
+ if (str[i] == ',')
+ str[i] = '.';
+ return str;
+}
+
+struct spa_strbuf {
+ char *buffer;
+ size_t maxsize;
+ size_t pos;
+};
+
+static inline void spa_strbuf_init(struct spa_strbuf *buf, char *buffer, size_t maxsize)
+{
+ buf->buffer = buffer;
+ buf->maxsize = maxsize;
+ buf->pos = 0;
+}
+
+SPA_PRINTF_FUNC(2, 3)
+static inline int spa_strbuf_append(struct spa_strbuf *buf, const char *fmt, ...)
+{
+ size_t remain = buf->maxsize - buf->pos;
+ ssize_t written;
+ va_list args;
+ va_start(args, fmt);
+ written = vsnprintf(&buf->buffer[buf->pos], remain, fmt, args);
+ va_end(args);
+ if (written > 0)
+ buf->pos += SPA_MIN(remain, (size_t)written);
+ return written;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_UTILS_STRING_H */
diff --git a/spa/include/spa/utils/type-info.h b/spa/include/spa/utils/type-info.h
new file mode 100644
index 0000000..643a7ac
--- /dev/null
+++ b/spa/include/spa/utils/type-info.h
@@ -0,0 +1,118 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_TYPE_INFO_H
+#define SPA_TYPE_INFO_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+
+/**
+ * \addtogroup spa_types
+ * \{
+ */
+
+#ifndef SPA_TYPE_ROOT
+#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>
+
+#include <spa/monitor/type-info.h>
+#include <spa/node/type-info.h>
+#include <spa/param/type-info.h>
+#include <spa/control/type-info.h>
+
+static const struct spa_type_info spa_types[] = {
+ /* Basic types */
+ { SPA_TYPE_START, SPA_TYPE_START, SPA_TYPE_INFO_BASE, NULL },
+ { SPA_TYPE_None, SPA_TYPE_None, SPA_TYPE_INFO_BASE "None", NULL },
+ { SPA_TYPE_Bool, SPA_TYPE_Bool, SPA_TYPE_INFO_BASE "Bool", NULL },
+ { SPA_TYPE_Id, SPA_TYPE_Int, SPA_TYPE_INFO_BASE "Id", NULL },
+ { SPA_TYPE_Int, SPA_TYPE_Int, SPA_TYPE_INFO_BASE "Int", NULL },
+ { SPA_TYPE_Long, SPA_TYPE_Long, SPA_TYPE_INFO_BASE "Long", NULL },
+ { SPA_TYPE_Float, SPA_TYPE_Float, SPA_TYPE_INFO_BASE "Float", NULL },
+ { SPA_TYPE_Double, SPA_TYPE_Double, SPA_TYPE_INFO_BASE "Double", NULL },
+ { SPA_TYPE_String, SPA_TYPE_String, SPA_TYPE_INFO_BASE "String", NULL },
+ { SPA_TYPE_Bytes, SPA_TYPE_Bytes, SPA_TYPE_INFO_BASE "Bytes", NULL },
+ { SPA_TYPE_Rectangle, SPA_TYPE_Rectangle, SPA_TYPE_INFO_BASE "Rectangle", NULL },
+ { SPA_TYPE_Fraction, SPA_TYPE_Fraction, SPA_TYPE_INFO_BASE "Fraction", NULL },
+ { SPA_TYPE_Bitmap, SPA_TYPE_Bitmap, SPA_TYPE_INFO_BASE "Bitmap", NULL },
+ { SPA_TYPE_Array, SPA_TYPE_Array, SPA_TYPE_INFO_BASE "Array", NULL },
+ { SPA_TYPE_Pod, SPA_TYPE_Pod, SPA_TYPE_INFO_Pod, NULL },
+ { SPA_TYPE_Struct, SPA_TYPE_Pod, SPA_TYPE_INFO_Struct, NULL },
+ { SPA_TYPE_Object, SPA_TYPE_Pod, SPA_TYPE_INFO_Object, NULL },
+ { SPA_TYPE_Sequence, SPA_TYPE_Pod, SPA_TYPE_INFO_POD_BASE "Sequence", NULL },
+ { SPA_TYPE_Pointer, SPA_TYPE_Pointer, SPA_TYPE_INFO_Pointer, NULL },
+ { SPA_TYPE_Fd, SPA_TYPE_Fd, SPA_TYPE_INFO_BASE "Fd", NULL },
+ { SPA_TYPE_Choice, SPA_TYPE_Pod, SPA_TYPE_INFO_POD_BASE "Choice", NULL },
+
+ { SPA_TYPE_POINTER_START, SPA_TYPE_Pointer, SPA_TYPE_INFO_Pointer, NULL },
+ { SPA_TYPE_POINTER_Buffer, SPA_TYPE_Pointer, SPA_TYPE_INFO_POINTER_BASE "Buffer", NULL },
+ { SPA_TYPE_POINTER_Meta, SPA_TYPE_Pointer, SPA_TYPE_INFO_POINTER_BASE "Meta", NULL },
+ { SPA_TYPE_POINTER_Dict, SPA_TYPE_Pointer, SPA_TYPE_INFO_POINTER_BASE "Dict", NULL },
+
+ { SPA_TYPE_EVENT_START, SPA_TYPE_Object, SPA_TYPE_INFO_Event, NULL },
+ { SPA_TYPE_EVENT_Device, SPA_TYPE_Object, SPA_TYPE_INFO_EVENT_BASE "Device", spa_type_device_event },
+ { SPA_TYPE_EVENT_Node, SPA_TYPE_Object, SPA_TYPE_INFO_EVENT_BASE "Node", spa_type_node_event },
+
+ { SPA_TYPE_COMMAND_START, SPA_TYPE_Object, SPA_TYPE_INFO_Command, NULL },
+ { SPA_TYPE_COMMAND_Device, SPA_TYPE_Object, SPA_TYPE_INFO_COMMAND_BASE "Device", NULL },
+ { SPA_TYPE_COMMAND_Node, SPA_TYPE_Object, SPA_TYPE_INFO_COMMAND_BASE "Node", spa_type_node_command },
+
+ { SPA_TYPE_OBJECT_START, SPA_TYPE_Object, SPA_TYPE_INFO_Object, NULL },
+ { SPA_TYPE_OBJECT_PropInfo, SPA_TYPE_Object, SPA_TYPE_INFO_PropInfo, spa_type_prop_info, },
+ { SPA_TYPE_OBJECT_Props, SPA_TYPE_Object, SPA_TYPE_INFO_Props, spa_type_props },
+ { SPA_TYPE_OBJECT_Format, SPA_TYPE_Object, SPA_TYPE_INFO_Format, spa_type_format },
+ { SPA_TYPE_OBJECT_ParamBuffers, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_Buffers, spa_type_param_buffers, },
+ { SPA_TYPE_OBJECT_ParamMeta, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_Meta, spa_type_param_meta },
+ { SPA_TYPE_OBJECT_ParamIO, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_IO, spa_type_param_io },
+ { SPA_TYPE_OBJECT_ParamProfile, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_Profile, spa_type_param_profile },
+ { SPA_TYPE_OBJECT_ParamPortConfig, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_PortConfig, spa_type_param_port_config },
+ { SPA_TYPE_OBJECT_ParamRoute, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_Route, spa_type_param_route },
+ { SPA_TYPE_OBJECT_Profiler, SPA_TYPE_Object, SPA_TYPE_INFO_Profiler, spa_type_profiler },
+ { SPA_TYPE_OBJECT_ParamLatency, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_Latency, spa_type_param_latency },
+ { SPA_TYPE_OBJECT_ParamProcessLatency, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_ProcessLatency, spa_type_param_process_latency },
+
+ { 0, 0, NULL, NULL }
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_TYPE_INFO_H */
diff --git a/spa/include/spa/utils/type.h b/spa/include/spa/utils/type.h
new file mode 100644
index 0000000..31623c3
--- /dev/null
+++ b/spa/include/spa/utils/type.h
@@ -0,0 +1,153 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_TYPE_H
+#define SPA_TYPE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+
+/** \defgroup spa_types Types
+ * Data type information enumerations
+ */
+
+/**
+ * \addtogroup spa_types
+ * \{
+ */
+
+enum {
+ /* Basic types */
+ SPA_TYPE_START = 0x00000,
+ SPA_TYPE_None,
+ SPA_TYPE_Bool,
+ SPA_TYPE_Id,
+ SPA_TYPE_Int,
+ SPA_TYPE_Long,
+ SPA_TYPE_Float,
+ SPA_TYPE_Double,
+ SPA_TYPE_String,
+ SPA_TYPE_Bytes,
+ SPA_TYPE_Rectangle,
+ SPA_TYPE_Fraction,
+ SPA_TYPE_Bitmap,
+ SPA_TYPE_Array,
+ SPA_TYPE_Struct,
+ SPA_TYPE_Object,
+ SPA_TYPE_Sequence,
+ SPA_TYPE_Pointer,
+ SPA_TYPE_Fd,
+ SPA_TYPE_Choice,
+ SPA_TYPE_Pod,
+ _SPA_TYPE_LAST, /**< not part of ABI */
+
+ /* Pointers */
+ SPA_TYPE_POINTER_START = 0x10000,
+ SPA_TYPE_POINTER_Buffer,
+ SPA_TYPE_POINTER_Meta,
+ SPA_TYPE_POINTER_Dict,
+ _SPA_TYPE_POINTER_LAST, /**< not part of ABI */
+
+ /* Events */
+ SPA_TYPE_EVENT_START = 0x20000,
+ SPA_TYPE_EVENT_Device,
+ SPA_TYPE_EVENT_Node,
+ _SPA_TYPE_EVENT_LAST, /**< not part of ABI */
+
+ /* Commands */
+ SPA_TYPE_COMMAND_START = 0x30000,
+ SPA_TYPE_COMMAND_Device,
+ SPA_TYPE_COMMAND_Node,
+ _SPA_TYPE_COMMAND_LAST, /**< not part of ABI */
+
+ /* Objects */
+ SPA_TYPE_OBJECT_START = 0x40000,
+ SPA_TYPE_OBJECT_PropInfo,
+ SPA_TYPE_OBJECT_Props,
+ SPA_TYPE_OBJECT_Format,
+ SPA_TYPE_OBJECT_ParamBuffers,
+ SPA_TYPE_OBJECT_ParamMeta,
+ SPA_TYPE_OBJECT_ParamIO,
+ SPA_TYPE_OBJECT_ParamProfile,
+ SPA_TYPE_OBJECT_ParamPortConfig,
+ SPA_TYPE_OBJECT_ParamRoute,
+ SPA_TYPE_OBJECT_Profiler,
+ SPA_TYPE_OBJECT_ParamLatency,
+ SPA_TYPE_OBJECT_ParamProcessLatency,
+ _SPA_TYPE_OBJECT_LAST, /**< not part of ABI */
+
+ /* vendor extensions */
+ SPA_TYPE_VENDOR_PipeWire = 0x02000000,
+
+ SPA_TYPE_VENDOR_Other = 0x7f000000,
+};
+
+#define SPA_TYPE_INFO_BASE "Spa:"
+
+#define SPA_TYPE_INFO_Flags SPA_TYPE_INFO_BASE "Flags"
+#define SPA_TYPE_INFO_FLAGS_BASE SPA_TYPE_INFO_Flags ":"
+
+#define SPA_TYPE_INFO_Enum SPA_TYPE_INFO_BASE "Enum"
+#define SPA_TYPE_INFO_ENUM_BASE SPA_TYPE_INFO_Enum ":"
+
+#define SPA_TYPE_INFO_Pod SPA_TYPE_INFO_BASE "Pod"
+#define SPA_TYPE_INFO_POD_BASE SPA_TYPE_INFO_Pod ":"
+
+#define SPA_TYPE_INFO_Struct SPA_TYPE_INFO_POD_BASE "Struct"
+#define SPA_TYPE_INFO_STRUCT_BASE SPA_TYPE_INFO_Struct ":"
+
+#define SPA_TYPE_INFO_Object SPA_TYPE_INFO_POD_BASE "Object"
+#define SPA_TYPE_INFO_OBJECT_BASE SPA_TYPE_INFO_Object ":"
+
+#define SPA_TYPE_INFO_Pointer SPA_TYPE_INFO_BASE "Pointer"
+#define SPA_TYPE_INFO_POINTER_BASE SPA_TYPE_INFO_Pointer ":"
+
+#define SPA_TYPE_INFO_Interface SPA_TYPE_INFO_POINTER_BASE "Interface"
+#define SPA_TYPE_INFO_INTERFACE_BASE SPA_TYPE_INFO_Interface ":"
+
+#define SPA_TYPE_INFO_Event SPA_TYPE_INFO_OBJECT_BASE "Event"
+#define SPA_TYPE_INFO_EVENT_BASE SPA_TYPE_INFO_Event ":"
+
+#define SPA_TYPE_INFO_Command SPA_TYPE_INFO_OBJECT_BASE "Command"
+#define SPA_TYPE_INFO_COMMAND_BASE SPA_TYPE_INFO_Command ":"
+
+struct spa_type_info {
+ uint32_t type;
+ uint32_t parent;
+ const char *name;
+ const struct spa_type_info *values;
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_TYPE_H */
diff --git a/spa/meson.build b/spa/meson.build
new file mode 100644
index 0000000..995fbf8
--- /dev/null
+++ b/spa/meson.build
@@ -0,0 +1,101 @@
+#project('spa', 'c')
+
+#cc = meson.get_compiler('c')
+#dl_lib = cc.find_library('dl', required : false)
+#pthread_lib = dependencies('threads')
+#mathlib = cc.find_library('m', required : false)
+
+spa_dep = declare_dependency(
+ include_directories : [
+ include_directories('include'),
+ ],
+ dependencies : [atomic_dep],
+ version : spaversion,
+ variables : {
+ 'plugindir' : meson.current_build_dir() / 'plugins',
+ 'datadir' : meson.current_source_dir() / 'plugins',
+ },
+)
+
+meson.override_dependency('lib@0@'.format(spa_name), spa_dep)
+
+pkgconfig.generate(filebase : 'lib@0@'.format(spa_name),
+ name : 'libspa',
+ subdirs : spa_name,
+ description : 'Simple Plugin API',
+ version : spaversion,
+ extra_cflags : '-D_REENTRANT',
+ variables : ['plugindir=${libdir}/@0@'.format(spa_name)],
+ uninstalled_variables : ['plugindir=${prefix}/spa/plugins'],
+)
+
+subdir('include')
+
+if get_option('spa-plugins').allowed()
+ udevrulesdir = get_option('udevrulesdir')
+ if udevrulesdir == ''
+ # absolute path, otherwise meson prepends the prefix
+ udevrulesdir = '/lib/udev/rules.d'
+ endif
+
+ # plugin-specific dependencies
+ alsa_dep = dependency('alsa', 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'))
+ gio_dep = dependency('gio-2.0', required : get_option('bluez5'))
+ gio_unix_dep = dependency('gio-unix-2.0', required : get_option('bluez5'))
+ bluez_deps_found = bluez_dep.found() and gio_dep.found() and gio_unix_dep.found()
+ summary({'Bluetooth audio': bluez_deps_found}, bool_yn: true, section: 'Backend')
+ if bluez_deps_found
+ sbc_dep = dependency('sbc', required: get_option('bluez5'))
+ summary({'SBC': sbc_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+ ldac_dep = dependency('ldacBT-enc', required : get_option('bluez5-codec-ldac'))
+ summary({'LDAC': ldac_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+ ldac_abr_dep = dependency('ldacBT-abr', required : get_option('bluez5-codec-ldac'))
+ summary({'LDAC ABR': ldac_abr_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+ aptx_dep = dependency('libfreeaptx', required : get_option('bluez5-codec-aptx'))
+ summary({'aptX': aptx_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+ fdk_aac_dep = dependency('fdk-aac', required : get_option('bluez5-codec-aac'))
+ summary({'AAC': fdk_aac_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+ lc3plus_dep = dependency('lc3plus', required : false)
+ if not lc3plus_dep.found()
+ lc3plus_lc3plus_h_dep = cc.find_library('LC3plus', has_headers: ['lc3plus.h'], required : get_option('bluez5-codec-lc3plus'))
+ if lc3plus_lc3plus_h_dep.found()
+ lc3plus_dep = declare_dependency(compile_args : '-DHAVE_LC3PLUS_H', dependencies : [ lc3plus_lc3plus_h_dep ])
+ endif
+ endif
+ summary({'LC3plus': lc3plus_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+ opus_dep = dependency('opus', required : get_option('bluez5-codec-opus'))
+ summary({'Opus': opus_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+ lc3_dep = dependency('lc3', required : get_option('bluez5-codec-lc3'))
+ summary({'LC3': lc3_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs')
+ if get_option('bluez5-backend-hsp-native').allowed() or get_option('bluez5-backend-hfp-native').allowed()
+ mm_dep = dependency('ModemManager', version : '>= 1.10.0', required : get_option('bluez5-backend-native-mm'))
+ summary({'ModemManager': mm_dep.found()}, bool_yn: true, section: 'Bluetooth backends')
+ endif
+ endif
+ jack_dep = dependency('jack', version : '>= 1.9.10', required: get_option('jack'))
+ summary({'JACK2': jack_dep.found()}, bool_yn: true, section: 'Backend')
+ vulkan_dep = dependency('vulkan', disabler : true, version : '>= 1.1.69', required: get_option('vulkan'))
+ vulkan_headers = cc.has_header('vulkan/vulkan.h', dependencies : vulkan_dep)
+ #summary({'Vulkan': vulkan_headers}, bool_yn: true, section: 'Misc dependencies')
+
+ libcamera_dep = dependency('libcamera', required: get_option('libcamera'))
+ summary({'libcamera': libcamera_dep.found()}, bool_yn: true, section: 'Backend')
+
+ tinycompress_dep = cc.find_library('tinycompress', has_headers: ['tinycompress/tinycompress.h' ], required: get_option('compress-offload'))
+ summary({'Compress-Offload': tinycompress_dep.found()}, bool_yn: true, section: 'Backend')
+ cdata.set('HAVE_ALSA_COMPRESS_OFFLOAD', tinycompress_dep.found())
+
+ # common dependencies
+ libudev_dep = dependency('libudev', required: alsa_dep.found() or get_option('udev').enabled() or get_option('v4l2').enabled())
+ summary({'Udev': libudev_dep.found()}, bool_yn: true, section: 'Backend')
+
+ subdir('plugins')
+endif
+
+subdir('tools')
+subdir('tests')
+if get_option('examples').allowed()
+ subdir('examples')
+endif
diff --git a/spa/plugins/aec/aec-null.c b/spa/plugins/aec/aec-null.c
new file mode 100644
index 0000000..b8d5b85
--- /dev/null
+++ b/spa/plugins/aec/aec-null.c
@@ -0,0 +1,175 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/interfaces/audio/aec.h>
+#include <spa/support/log.h>
+#include <spa/utils/string.h>
+#include <spa/utils/names.h>
+#include <spa/support/plugin.h>
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_audio_aec aec;
+ struct spa_log *log;
+
+ struct spa_hook_list hooks_list;
+
+ uint32_t channels;
+};
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.aec.null");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+static int null_init(void *object, const struct spa_dict *args, const struct spa_audio_info_raw *info)
+{
+ struct impl *impl = object;
+ impl->channels = info->channels;
+ return 0;
+}
+
+static int null_run(void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples)
+{
+ struct impl *impl = object;
+ uint32_t i;
+ for (i = 0; i < impl->channels; i++)
+ memcpy(out[i], rec[i], n_samples * sizeof(float));
+ return 0;
+}
+
+static const struct spa_audio_aec_methods impl_aec = {
+ SPA_VERSION_AUDIO_AEC,
+ .init = null_init,
+ .run = null_run,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ struct impl *impl = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_AUDIO_AEC))
+ *interface = &impl->aec;
+ 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)
+{
+ 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;
+
+ struct impl *impl = (struct impl *) handle;
+
+ impl->aec.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_AUDIO_AEC,
+ SPA_VERSION_AUDIO_AEC,
+ &impl_aec, impl);
+ impl->aec.name = "null";
+ impl->aec.info = NULL;
+ impl->aec.latency = NULL;
+
+ impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(impl->log, &log_topic);
+
+ spa_hook_list_init(&impl->hooks_list);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_AUDIO_AEC,},
+};
+
+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_handle_factory spa_aec_null_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_AEC,
+ 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_aec_null_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/aec/aec-webrtc.cpp b/spa/plugins/aec/aec-webrtc.cpp
new file mode 100644
index 0000000..19a506e
--- /dev/null
+++ b/spa/plugins/aec/aec-webrtc.cpp
@@ -0,0 +1,276 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <memory>
+#include <utility>
+
+#include <spa/interfaces/audio/aec.h>
+#include <spa/support/log.h>
+#include <spa/utils/string.h>
+#include <spa/utils/names.h>
+#include <spa/support/plugin.h>
+
+#include <webrtc/modules/audio_processing/include/audio_processing.h>
+#include <webrtc/modules/interface/module_common_types.h>
+#include <webrtc/system_wrappers/include/trace.h>
+
+struct impl_data {
+ struct spa_handle handle;
+ struct spa_audio_aec aec;
+
+ struct spa_log *log;
+ std::unique_ptr<webrtc::AudioProcessing> apm;
+ spa_audio_info_raw info;
+ std::unique_ptr<float *[]> play_buffer, rec_buffer, out_buffer;
+};
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.eac.webrtc");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+static bool webrtc_get_spa_bool(const struct spa_dict *args, const char *key, bool default_value)
+{
+ if (auto str = spa_dict_lookup(args, key))
+ return spa_atob(str);
+
+ return default_value;
+}
+
+static int webrtc_init(void *object, const struct spa_dict *args, const struct spa_audio_info_raw *info)
+{
+ auto impl = static_cast<struct impl_data*>(object);
+
+ bool extended_filter = webrtc_get_spa_bool(args, "webrtc.extended_filter", true);
+ bool delay_agnostic = webrtc_get_spa_bool(args, "webrtc.delay_agnostic", true);
+ 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);
+
+ // Note: AGC seems to mess up with Agnostic Delay Detection, especially with speech,
+ // result in very poor performance, disable by default
+ bool gain_control = webrtc_get_spa_bool(args, "webrtc.gain_control", false);
+
+ // 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);
+
+ // FIXME: Intelligibility enhancer is not currently supported
+ // This filter will modify playback buffer (when calling ProcessReverseStream), but now
+ // playback buffer modifications are discarded.
+
+ webrtc::Config config;
+ config.Set<webrtc::ExtendedFilter>(new webrtc::ExtendedFilter(extended_filter));
+ config.Set<webrtc::DelayAgnostic>(new webrtc::DelayAgnostic(delay_agnostic));
+ config.Set<webrtc::ExperimentalAgc>(new webrtc::ExperimentalAgc(experimental_agc));
+ config.Set<webrtc::ExperimentalNs>(new webrtc::ExperimentalNs(experimental_ns));
+
+ webrtc::ProcessingConfig pconfig = {{
+ webrtc::StreamConfig(info->rate, info->channels, false), /* input stream */
+ webrtc::StreamConfig(info->rate, info->channels, false), /* output stream */
+ webrtc::StreamConfig(info->rate, info->channels, false), /* reverse input stream */
+ webrtc::StreamConfig(info->rate, info->channels, false), /* reverse output stream */
+ }};
+
+ auto apm = std::unique_ptr<webrtc::AudioProcessing>(webrtc::AudioProcessing::Create(config));
+ if (apm->Initialize(pconfig) != webrtc::AudioProcessing::kNoError) {
+ spa_log_error(impl->log, "Error initialising webrtc audio processing module");
+ return -1;
+ }
+
+ apm->high_pass_filter()->Enable(high_pass_filter);
+ // Always disable drift compensation since PipeWire will already do
+ // drift compensation on all sinks and sources linked to this echo-canceler
+ apm->echo_cancellation()->enable_drift_compensation(false);
+ apm->echo_cancellation()->Enable(true);
+ // TODO: wire up supression levels to args
+ apm->echo_cancellation()->set_suppression_level(webrtc::EchoCancellation::kHighSuppression);
+ apm->noise_suppression()->set_level(webrtc::NoiseSuppression::kHigh);
+ apm->noise_suppression()->Enable(noise_suppression);
+ apm->voice_detection()->Enable(voice_detection);
+ // TODO: wire up AGC parameters to args
+ apm->gain_control()->set_analog_level_limits(0, 255);
+ apm->gain_control()->set_mode(webrtc::GainControl::kAdaptiveDigital);
+ apm->gain_control()->Enable(gain_control);
+ impl->apm = std::move(apm);
+ impl->info = *info;
+ impl->play_buffer = std::make_unique<float *[]>(info->channels);
+ impl->rec_buffer = std::make_unique<float *[]>(info->channels);
+ impl->out_buffer = std::make_unique<float *[]>(info->channels);
+ return 0;
+}
+
+static int webrtc_run(void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples)
+{
+ auto impl = static_cast<struct impl_data*>(object);
+ webrtc::StreamConfig config =
+ webrtc::StreamConfig(impl->info.rate, impl->info.channels, false);
+ unsigned int num_blocks = n_samples * 1000 / impl->info.rate / 10;
+
+ if (n_samples * 1000 / impl->info.rate % 10 != 0) {
+ spa_log_error(impl->log, "Buffers must be multiples of 10ms in length (currently %u samples)", n_samples);
+ return -1;
+ }
+
+ for (size_t i = 0; i < num_blocks; i ++) {
+ for (size_t j = 0; j < impl->info.channels; j++) {
+ impl->play_buffer[j] = const_cast<float *>(play[j]) + config.num_frames() * i;
+ impl->rec_buffer[j] = const_cast<float *>(rec[j]) + config.num_frames() * i;
+ impl->out_buffer[j] = out[j] + config.num_frames() * i;
+ }
+ /* FIXME: ProcessReverseStream may change the playback buffer, in which
+ * case we should use that, if we ever expose the intelligibility
+ * enhancer */
+ if (impl->apm->ProcessReverseStream(impl->play_buffer.get(), config, config, impl->play_buffer.get()) !=
+ webrtc::AudioProcessing::kNoError) {
+ spa_log_error(impl->log, "Processing reverse stream failed");
+ }
+
+ // Extra delay introduced by multiple frames
+ impl->apm->set_stream_delay_ms((num_blocks - 1) * 10);
+
+ if (impl->apm->ProcessStream(impl->rec_buffer.get(), config, config, impl->out_buffer.get()) !=
+ webrtc::AudioProcessing::kNoError) {
+ spa_log_error(impl->log, "Processing stream failed");
+ }
+ }
+
+ return 0;
+}
+
+static const struct spa_audio_aec_methods impl_aec = {
+ SPA_VERSION_AUDIO_AEC_METHODS,
+ .add_listener = NULL,
+ .init = webrtc_init,
+ .run = webrtc_run,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ auto impl = reinterpret_cast<struct impl_data*>(handle);
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_AUDIO_AEC))
+ *interface = &impl->aec;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ auto impl = reinterpret_cast<struct impl_data*>(handle);
+ impl->~impl_data();
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl_data);
+}
+
+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)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ auto impl = new (handle) impl_data();
+
+ impl->handle.get_interface = impl_get_interface;
+ impl->handle.clear = impl_clear;
+
+ impl->aec.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_AUDIO_AEC,
+ SPA_VERSION_AUDIO_AEC,
+ &impl_aec, impl);
+ impl->aec.name = "webrtc",
+ impl->aec.info = NULL;
+ impl->aec.latency = "480/48000",
+
+ impl->log = static_cast<struct spa_log *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log));
+ spa_log_topic_init(impl->log, &log_topic);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_AUDIO_AEC,},
+};
+
+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_handle_factory spa_aec_webrtc_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_AEC,
+ 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_aec_webrtc_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/aec/meson.build b/spa/plugins/aec/meson.build
new file mode 100644
index 0000000..2b1a2c0
--- /dev/null
+++ b/spa/plugins/aec/meson.build
@@ -0,0 +1,16 @@
+aec_null = shared_library('spa-aec-null',
+ [ 'aec-null.c' ],
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'aec')
+
+if webrtc_dep.found()
+ aec_webrtc = shared_library('spa-aec-webrtc',
+ [ 'aec-webrtc.cpp' ],
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, webrtc_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'aec')
+endif
+
diff --git a/spa/plugins/alsa/90-pipewire-alsa.rules b/spa/plugins/alsa/90-pipewire-alsa.rules
new file mode 100644
index 0000000..54e0244
--- /dev/null
+++ b/spa/plugins/alsa/90-pipewire-alsa.rules
@@ -0,0 +1,214 @@
+# do not edit this file, it will be overwritten on update
+
+# This file is part of PipeWire adapted from 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
+# Lesser 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/>.
+
+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=="platform", DRIVERS=="thinkpad_acpi", ENV{ACP_IGNORE}="1"
+
+# Force enable speaker and internal mic for some laptops
+# This should only be necessary for kernels 3.3, 3.4 and 3.5 (as they are lacking the phantom jack kctls).
+# Acer AOA150
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x015b", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Acer Aspire 4810TZ
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x022a", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Packard bell dot m/a
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x028c", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Acer Aspire 1810TZ
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x029b", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Acer AOD260 and AO532h
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x0349", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell MXC051
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01b5", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Inspiron 6400 and E1505
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01bd", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Latitude D620
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01c2", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Latitude D820
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01cc", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Latitude D520
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01d4", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Latitude D420
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01d6", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Inspiron 1525
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x022f", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Inspiron 1011
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x02f4", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell XPS 14 (L401X)
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0468", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell XPS 15 (L501X)
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x046e", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell XPS 15 (L502X)
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x050e", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Inspiron 3420
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0553", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Inspiron 3520
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0555", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Vostro 2420
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0556", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Vostro 2520
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0558", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Inspiron One 2020
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0579", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Asus 904HA (1000H)
+ATTRS{subsystem_vendor}=="0x1043", ATTRS{subsystem_device}=="0x831a", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Asus T101MT
+ATTRS{subsystem_vendor}=="0x1043", ATTRS{subsystem_device}=="0x83ce", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Sony Vaio VGN-SR21M
+ATTRS{subsystem_vendor}=="0x104d", ATTRS{subsystem_device}=="0x9033", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Sony Vaio VPC-W115XG
+ATTRS{subsystem_vendor}=="0x104d", ATTRS{subsystem_device}=="0x9064", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Fujitsu Lifebook S7110
+ATTRS{subsystem_vendor}=="0x10cf", ATTRS{subsystem_device}=="0x1397", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Fujitsu Lifebook A530
+ATTRS{subsystem_vendor}=="0x10cf", ATTRS{subsystem_device}=="0x1531", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Toshiba A200
+ATTRS{subsystem_vendor}=="0x1179", ATTRS{subsystem_device}=="0xff00", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# MSI X360
+ATTRS{subsystem_vendor}=="0x1462", ATTRS{subsystem_device}=="0x1053", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Lenovo 3000 Y410
+ATTRS{subsystem_vendor}=="0x17aa", ATTRS{subsystem_device}=="0x384e", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+
+GOTO="pipewire_end"
+
+LABEL="pipewire_check_usb"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1978", ENV{ACP_PROFILE_SET}="native-instruments-audio8dj.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="0839", ENV{ACP_PROFILE_SET}="native-instruments-audio4dj.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="baff", ENV{ACP_PROFILE_SET}="native-instruments-traktorkontrol-s4.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="4711", ENV{ACP_PROFILE_SET}="native-instruments-korecontroller.conf"
+
+# This ID 17cc:041c is verified for the older Audio 2 DJ model (pre-2014 ish).
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="041c", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio2.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="041d", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio2.conf"
+
+# There appear to be two IDs in use for Traktor Audio 6 (or maybe 17cc:1011
+# is just incorrect - 17cc:1010 has been verified to be correct at least
+# for some hardware).
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1010", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio6.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1011", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio6.conf"
+
+
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1001", ENV{ACP_PROFILE_SET}="native-instruments-komplete-audio6.conf"
+# This entry is for the Komplete Audio 6 MK2, which has a different ID, but is functionally identical to the Komplete Audio 6.
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1870", ENV{ACP_PROFILE_SET}="native-instruments-komplete-audio6.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1021", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio10.conf"
+ATTRS{idVendor}=="0763", ATTRS{idProduct}=="2012", ENV{ACP_PROFILE_SET}="maudio-fasttrack-pro.conf"
+ATTRS{idVendor}=="045e", ATTRS{idProduct}=="02bb", ENV{ACP_PROFILE_SET}="kinect-audio.conf"
+ATTRS{idVendor}=="041e", ATTRS{idProduct}=="322c", ENV{ACP_PROFILE_SET}="sb-omni-surround-5.1.conf"
+ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="4014", ENV{ACP_PROFILE_SET}="dell-dock-tb16-usb-audio.conf"
+ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="402e", ENV{ACP_PROFILE_SET}="dell-dock-tb16-usb-audio.conf"
+ATTRS{idVendor}=="08bb", ATTRS{idProduct}=="2902", ENV{ACP_PROFILE_SET}="texas-instruments-pcm2902.conf"
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0269", ENV{ACP_PROFILE_SET}="hp-tbt-dock-120w-g2.conf"
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0567", ENV{ACP_PROFILE_SET}="hp-tbt-dock-audio-module.conf"
+
+# ID 1038:12ad is for the 2018 refresh of the Arctis 7.
+# ID 1038:1294 is for Arctis Pro Wireless (which works with the Arctis 7 configuration).
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1260", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12ad", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1294", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1730", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 1038:1282 is for SteelSeries GameDAC
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1282", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 1038:12c4 is for Arctis 9
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12c4", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# Lucidsound LS31
+ATTRS{idVendor}=="2f12", ATTRS{idProduct}=="0109", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 9886:002c is for the Astro A50 Gen4
+ATTRS{idVendor}=="9886", ATTRS{idProduct}=="002c", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 9886:0045 is for the Astro A20 Gen2
+ATTRS{idVendor}=="9886", ATTRS{idProduct}=="0045", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 1532:0520 is for the Razer Kraken Tournament Edition
+ATTRS{idVendor}=="1532", ATTRS{idProduct}=="0520", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+
+
+# ID 1038:1250 is for the Arctis 5
+# ID 1037:12aa is for the Arctis 5 2019
+# ID 1038:1252 is for the Arctis Pro 2019 edition
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1250", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12aa", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1252", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf"
+
+ATTRS{idVendor}=="147a", ATTRS{idProduct}=="e055", ENV{ACP_PROFILE_SET}="cmedia-high-speed-true-hdaudio.conf"
+
+# HyperX Cloud Orbit S has three modes. Each mode has a separate product ID.
+# ID_SERIAL for this device is the device name + mode repeated three times.
+# ID_SERIAL is used for the ID_ID property, and the ID_ID property is used in
+# the card name in PulseAudio. The resulting card name is too long for the name
+# length limit, so we set a more sensible ID_ID here (the same as the default
+# ID_ID, but without repetition in the serial part).
+ATTRS{idVendor}=="0951", ATTRS{idProduct}=="16ff", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_2Ch-$env{ID_USB_INTERFACE_NUM}"
+ATTRS{idVendor}=="0951", ATTRS{idProduct}=="1702", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_Hi-Res_2Ch-$env{ID_USB_INTERFACE_NUM}"
+ATTRS{idVendor}=="0951", ATTRS{idProduct}=="1703", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_3D_8Ch-$env{ID_USB_INTERFACE_NUM}"
+
+# OnePlus Type-C Bullets (ED117)
+ATTRS{idVendor}=="2a70", ATTRS{idProduct}=="1881", ENV{ACP_PROFILE_SET}="simple-headphones-mic.conf"
+
+# ID 1395:005e is for Sennheiser GSX 1000
+# ID 1395:00a0 is for Sennheiser GSX 1000
+# ID 1395:00b1 is for Sennheiser GSX 1000 v2
+# ID 1395:005f is for Sennheiser GSX 1200
+# ID 1395:00a1 is for Sennheiser GSX 1200
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="005e", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00a0", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00b1", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="005f", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00a1", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+
+# Sennheiser GSA 70 wireless USB dongle for GSP 670
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="0089", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# EPOS GSA 70 wireless USB dongle for GSP 670 (Sennheiser GSA 70 with updated firmware)
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="0300", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# Sennheiser GSP 670 USB headset
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="008a", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+
+# Audioengine HD3 powered speakers support IEC958 but don't actually
+# have any digital outputs.
+ATTRS{idVendor}=="0a12", ATTRS{idProduct}=="4007", ENV{ACP_PROFILE_SET}="analog-only.conf"
+
+# Asus Xonar SE
+ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="189d", ENV{ACP_PROFILE_SET}="asus-xonar-se.conf"
+
+GOTO="pipewire_end"
+
+LABEL="pipewire_check_pci"
+
+# Creative SoundBlaster Audigy-based cards
+# EMU10k2/CA0100/CA0102/CA10200
+ATTRS{vendor}=="0x1102", ATTRS{device}=="0x0004", ENV{ACP_PROFILE_SET}="audigy.conf"
+# CA0108/CA10300
+ATTRS{vendor}=="0x1102", ATTRS{device}=="0x0008", ENV{ACP_PROFILE_SET}="audigy.conf"
+
+GOTO="pipewire_end"
+
+LABEL="pipewire_firewire_quirk"
+
+# Focusrite Saffire Pro 10 i/o and Pro 26 i/o have a quirk to disappear from
+# IEEE 1394 bus when breaking connections. This brings an issue for PulseAudio
+# to continue the routine for ever that:
+# - detecting sound card
+# - starting PCM substream
+# - stopping the PCM substream
+# - the card disappears
+# - the card appears
+# In detail, see: https://bugzilla.kernel.org/show_bug.cgi?id=199365
+ATTRS{vendor}=="0x00130e", ATTRS{model}=="0x00000[36]", ATTRS{units}=="0x00a02d:0x010001", ENV{ACP_IGNORE}="1"
+
+LABEL="pipewire_end"
diff --git a/spa/plugins/alsa/acp-tool.c b/spa/plugins/alsa/acp-tool.c
new file mode 100644
index 0000000..0fc92a4
--- /dev/null
+++ b/spa/plugins/alsa/acp-tool.c
@@ -0,0 +1,786 @@
+/* ALSA card profile test
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+#include <stdbool.h>
+#include <getopt.h>
+
+#include <spa/utils/string.h>
+
+#include <acp/acp.h>
+
+#define WHITESPACE "\n\r\t "
+
+struct data {
+ int verbose;
+ int card_index;
+ char *properties;
+ struct acp_card *card;
+ bool quit;
+};
+
+static void acp_debug_dict(struct acp_dict *dict, int indent)
+{
+ const struct acp_dict_item *it;
+ fprintf(stderr, "%*sproperties: (%d)\n", indent, "", dict->n_items);
+ acp_dict_for_each(it, dict) {
+ fprintf(stderr, "%*s%s = \"%s\"\n", indent+4, "", it->key, it->value);
+ }
+}
+
+static char *split_walk(char *str, const char *delimiter, size_t *len, char **state)
+{
+ char *s = *state ? *state : str;
+
+ if (*s == '\0')
+ return NULL;
+
+ *len = strcspn(s, delimiter);
+ *state = s + *len;
+ *state += strspn(*state, delimiter);
+ return s;
+}
+
+static int split_ip(char *str, const char *delimiter, int max_tokens, char *tokens[])
+{
+ char *state = NULL, *s;
+ size_t len;
+ int n = 0;
+
+ while (true) {
+ if ((s = split_walk(str, delimiter, &len, &state)) == NULL)
+ break;
+ tokens[n++] = s;
+ if (n >= max_tokens)
+ break;
+ s[len] = '\0';
+ }
+ return n;
+}
+
+
+static void card_props_changed(void *data)
+{
+ struct data *d = data;
+ struct acp_card *card = d->card;
+ fprintf(stderr, "*** properties changed:\n");
+ acp_debug_dict(&card->props, 4);
+ fprintf(stderr, "***\n");
+}
+
+static void card_profile_changed(void *data, uint32_t old_index, uint32_t new_index)
+{
+ struct data *d = data;
+ struct acp_card *card = d->card;
+ struct acp_card_profile *op = card->profiles[old_index];
+ struct acp_card_profile *np = card->profiles[new_index];
+ fprintf(stderr, "*** profile changed from %s to %s\n", op->name, np->name);
+}
+
+static void card_profile_available(void *data, uint32_t index,
+ enum acp_available old, enum acp_available available)
+{
+ struct data *d = data;
+ struct acp_card *card = d->card;
+ struct acp_card_profile *p = card->profiles[index];
+ fprintf(stderr, "*** profile %s available %s\n", p->name, acp_available_str(available));
+}
+
+static void card_port_available(void *data, uint32_t index,
+ enum acp_available old, enum acp_available available)
+{
+ struct data *d = data;
+ struct acp_card *card = d->card;
+ struct acp_port *p = card->ports[index];
+ fprintf(stderr, "*** port %s available %s\n", p->name, acp_available_str(available));
+}
+
+static void on_volume_changed(void *data, struct acp_device *dev)
+{
+ float vol;
+ acp_device_get_volume(dev, &vol, 1);
+ fprintf(stderr, "*** volume %s changed to %f\n", dev->name, vol);
+}
+
+static void on_mute_changed(void *data, struct acp_device *dev)
+{
+ bool mute;
+ acp_device_get_mute(dev, &mute);
+ fprintf(stderr, "*** mute %s changed to %d\n", dev->name, mute);
+}
+
+static const struct acp_card_events card_events = {
+ ACP_VERSION_CARD_EVENTS,
+ .props_changed = card_props_changed,
+ .profile_changed = card_profile_changed,
+ .profile_available = card_profile_available,
+ .port_available = card_port_available,
+ .volume_changed = on_volume_changed,
+ .mute_changed = on_mute_changed,
+};
+
+static ACP_PRINTF_FUNC(6,0) void log_func(void *data,
+ int level, const char *file, int line, const char *func,
+ const char *fmt, va_list arg)
+{
+ vfprintf(stderr, fmt, arg);
+ fprintf(stderr, "\n");
+}
+
+static void show_prompt(struct data *data)
+{
+ fprintf(stderr, ">>>");
+}
+
+struct command {
+ const char *name;
+ const char *args;
+ const char *alias;
+ const char *description;
+ int (*func) (struct data *data, const struct command *cmd, int argc, char *argv[]);
+ void *extra;
+};
+
+static int cmd_help(struct data *data, const struct command *cmd, int argc, char *argv[]);
+
+static int cmd_quit(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ data->quit = true;
+ return 0;
+}
+
+static void print_profile(struct data *data, struct acp_card_profile *p, int indent, int level);
+static void print_device(struct data *data, struct acp_device *d, int indent, int level);
+
+static void print_port(struct data *data, struct acp_port *p, int indent, int level)
+{
+ uint32_t i;
+
+ fprintf(stderr, "%*s %c port %u: name:\"%s\" direction:%s prio:%d (available: %s)\n",
+ indent, "", p->flags & ACP_PORT_ACTIVE ? '*' : ' ', p->index,
+ p->name, acp_direction_str(p->direction), p->priority,
+ acp_available_str(p->available));
+ if (level > 0) {
+ acp_debug_dict(&p->props, indent + 8);
+ }
+ if (level > 1) {
+ fprintf(stderr, "%*sprofiles: (%d)\n", indent+8, "", p->n_profiles);
+ for (i = 0; i < p->n_profiles; i++) {
+ struct acp_card_profile *pr = p->profiles[i];
+ print_profile(data, pr, indent + 8, 0);
+ }
+ fprintf(stderr, "%*sdevices: (%d)\n", indent+8, "", p->n_devices);
+ for (i = 0; i < p->n_devices; i++) {
+ struct acp_device *d = p->devices[i];
+ print_device(data, d, indent + 8, 0);
+ }
+ }
+}
+
+static void print_device(struct data *data, struct acp_device *d, int indent, int level)
+{
+ const char **s;
+ uint32_t i;
+
+ fprintf(stderr, "%*s %c device %u: direction:%s name:\"%s\" prio:%d flags:%08x devices: ",
+ indent, "", d->flags & ACP_DEVICE_ACTIVE ? '*' : ' ', d->index,
+ acp_direction_str(d->direction), d->name, d->priority, d->flags);
+ for (s = d->device_strings; *s; s++)
+ fprintf(stderr, "\"%s\" ", *s);
+ fprintf(stderr, "\n");
+ if (level > 0) {
+ fprintf(stderr, "%*srate:%d channels:%d\n", indent+8, "",
+ d->format.rate_mask, d->format.channels);
+ acp_debug_dict(&d->props, indent + 8);
+ }
+ if (level > 1) {
+ fprintf(stderr, "%*sports: (%d)\n", indent+8, "", d->n_ports);
+ for (i = 0; i < d->n_ports; i++) {
+ struct acp_port *p = d->ports[i];
+ print_port(data, p, indent + 8, 0);
+ }
+ }
+}
+
+static void print_profile(struct data *data, struct acp_card_profile *p, int indent, int level)
+{
+ uint32_t i;
+
+ fprintf(stderr, "%*s %c profile %u: name:\"%s\" prio:%d (available: %s)\n",
+ indent, "", p->flags & ACP_PROFILE_ACTIVE ? '*' : ' ', p->index,
+ p->name, p->priority, acp_available_str(p->available));
+ if (level > 0) {
+ fprintf(stderr, "%*sdescription:\"%s\"\n",
+ indent+8, "", p->description);
+ }
+ if (level > 1) {
+ fprintf(stderr, "%*sdevices: (%d)\n", indent+8, "", p->n_devices);
+ for (i = 0; i < p->n_devices; i++) {
+ struct acp_device *d = p->devices[i];
+ print_device(data, d, indent + 8, 0);
+ }
+ }
+}
+
+static void print_card(struct data *data, struct acp_card *card, int indent, int level)
+{
+ fprintf(stderr, "%*scard %d: profiles:%d devices:%d ports:%d\n", indent, "",
+ card->index, card->n_profiles, card->n_devices, card->n_ports);
+ if (level > 0) {
+ acp_debug_dict(&card->props, 4);
+ }
+}
+
+static int cmd_info(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ print_card(data, card, 0, 2);
+ return 0;
+}
+
+static int cmd_card(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <card_index> missing\n");
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int cmd_list(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t i;
+ int level = 0;
+
+ if (spa_streq(cmd->name, "list-verbose"))
+ level = 2;
+
+ print_card(data, card, 0, level);
+ for (i = 0; i < card->n_profiles; i++)
+ print_profile(data, card->profiles[i], 0, level);
+
+ for (i = 0; i < card->n_ports; i++)
+ print_port(data, card->ports[i], 0, level);
+
+ for (i = 0; i < card->n_devices; i++)
+ print_device(data, card->devices[i], 0, level);
+
+ return 0;
+}
+
+static int cmd_list_profiles(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ uint32_t i;
+ struct acp_card *card = data->card;
+
+ if (argc > 1) {
+ i = atoi(argv[1]);
+ if (i >= card->n_profiles)
+ return -EINVAL;
+ print_profile(data, card->profiles[i], 0, 2);
+ } else {
+ for (i = 0; i < card->n_profiles; i++)
+ print_profile(data, card->profiles[i], 0, 0);
+ }
+ return 0;
+}
+
+static int cmd_set_profile(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t index;
+
+ if (argc > 1)
+ index = atoi(argv[1]);
+ else
+ index = card->active_profile_index;
+
+ return acp_card_set_profile(card, index, 0);
+}
+
+static int cmd_list_ports(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ uint32_t i;
+ struct acp_card *card = data->card;
+
+ if (argc > 1) {
+ i = atoi(argv[1]);
+ if (i >= card->n_ports)
+ return -EINVAL;
+ print_port(data, card->ports[i], 0, 2);
+ } else {
+ for (i = 0; i < card->n_ports; i++)
+ print_port(data, card->ports[i], 0, 0);
+ }
+ return 0;
+}
+
+static int cmd_set_port(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id, port_id;
+
+ if (argc < 3) {
+ fprintf(stderr, "arguments: <device_id> <port_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ port_id = atoi(argv[2]);
+
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ return acp_device_set_port(card->devices[dev_id], port_id, 0);
+}
+
+static int cmd_list_devices(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ uint32_t i;
+ struct acp_card *card = data->card;
+
+ if (argc > 1) {
+ i = atoi(argv[1]);
+ if (i >= card->n_devices)
+ return -EINVAL;
+ print_device(data, card->devices[i], 0, 2);
+ } else {
+ for (i = 0; i < card->n_devices; i++)
+ print_device(data, card->devices[i], 0, 0);
+ }
+ return 0;
+}
+
+static int cmd_get_volume(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ float vol;
+
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <device_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ acp_device_get_volume(card->devices[dev_id], &vol, 1);
+
+ fprintf(stderr, "volume: %f\n", vol);
+ return 0;
+}
+
+static int cmd_set_volume(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ float vol;
+
+ if (argc < 3) {
+ fprintf(stderr, "arguments: <device_id> <volume> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ vol = atof(argv[2]);
+
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ return acp_device_set_volume(card->devices[dev_id], &vol, 1);
+}
+
+static int adjust_volume(struct data *data, const struct command *cmd, int argc, char *argv[], float adjust)
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ float vol;
+
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <device_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+ acp_device_get_volume(card->devices[dev_id], &vol, 1);
+ vol += adjust;
+ acp_device_set_volume(card->devices[dev_id], &vol, 1);
+ fprintf(stderr, "volume: %f\n", vol);
+ return 0;
+}
+
+static int cmd_inc_volume(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ return adjust_volume(data, cmd, argc, argv, 0.2);
+}
+
+static int cmd_dec_volume(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ return adjust_volume(data, cmd, argc, argv, -0.2);
+}
+
+static int cmd_get_mute(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ bool mute;
+
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <device_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ acp_device_get_mute(card->devices[dev_id], &mute);
+
+ fprintf(stderr, "muted: %s\n", mute ? "yes" : "no");
+ return 0;
+}
+
+static int cmd_set_mute(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ bool mute;
+
+ if (argc < 3) {
+ fprintf(stderr, "arguments: <device_id> <mute> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ mute = atoi(argv[2]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ acp_device_set_mute(card->devices[dev_id], mute);
+ fprintf(stderr, "muted: %s\n", mute ? "yes" : "no");
+ return 0;
+}
+
+static int cmd_toggle_mute(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ bool mute;
+
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <device_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+ acp_device_get_mute(card->devices[dev_id], &mute);
+ mute = !mute;
+ acp_device_set_mute(card->devices[dev_id], mute);
+ fprintf(stderr, "muted: %s\n", mute ? "yes" : "no");
+ return 0;
+}
+
+static const struct command command_list[] = {
+ { "help", "", "h", "Show available commands", cmd_help },
+ { "quit", "", "q", "Quit", cmd_quit },
+ { "card", "<id>", "c", "Probe card", cmd_card },
+ { "info", "", "i", "List card info", cmd_info },
+ { "list", "", "l", "List all objects", cmd_list },
+ { "list-verbose", "", "lv", "List all data", cmd_list },
+ { "list-profiles", "[id]", "lpr", "List profiles", cmd_list_profiles },
+ { "set-profile", "<id>", "spr", "Activate a profile", cmd_set_profile },
+ { "list-ports", "[id]", "lp", "List ports", cmd_list_ports },
+ { "set-port", "<id>", "sp", "Activate a port", cmd_set_port },
+ { "list-devices", "[id]", "ld", "List available devices", cmd_list_devices },
+ { "get-volume", "<id>", "gv", "Get volume from device", cmd_get_volume },
+ { "set-volume", "<id> <vol>", "v", "Set volume on device", cmd_set_volume },
+ { "inc-volume", "<id>", "v+", "Increase volume on device", cmd_inc_volume },
+ { "dec-volume", "<id>", "v-", "Decrease volume on device", cmd_dec_volume },
+ { "get-mute", "<id>", "gm", "Get mute state from device", cmd_get_mute },
+ { "set-mute", "<id> <val>", "sm", "Set mute on device", cmd_set_mute },
+ { "toggle-mute", "<id>", "m", "Toggle mute on device", cmd_toggle_mute },
+};
+#define N_COMMANDS sizeof(command_list)/sizeof(command_list[0])
+
+static const struct command *find_command(struct data *data, const char *cmd)
+{
+ size_t i;
+ for (i = 0; i < N_COMMANDS; i++) {
+ if (spa_streq(command_list[i].name, cmd) ||
+ spa_streq(command_list[i].alias, cmd))
+ return &command_list[i];
+ }
+ return NULL;
+}
+
+static int cmd_help(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ size_t i;
+ fprintf(stderr, "Available commands:\n");
+ for (i = 0; i < N_COMMANDS; i++) {
+ fprintf(stdout, "\t%-15.15s %-10.10s\t%s (%s)\n",
+ command_list[i].name,
+ command_list[i].args,
+ command_list[i].description,
+ command_list[i].alias);
+ }
+ return 0;
+}
+
+static int run_command(struct data *data, int argc, char *argv[64])
+{
+ const struct command *command;
+ int res;
+
+ command = find_command(data, argv[0]);
+ if (command == NULL) {
+ fprintf(stderr, "unknown command %s\n", argv[0]);
+ cmd_help(data, NULL, argc, argv);
+ res = -EINVAL;
+ } else if (command->func) {
+ res = command->func(data, command, argc, argv);
+ if (res < 0) {
+ fprintf(stderr, "error: %s\n", strerror(-res));
+ }
+ } else {
+ res = -ENOTSUP;
+ }
+ return res;
+}
+
+static int handle_input(struct data *data)
+{
+ char buf[4096] = { 0, }, *p, *argv[64];
+ ssize_t r;
+ int res, argc;
+
+ if ((r = read(STDIN_FILENO, buf, sizeof(buf)-1)) < 0)
+ return -errno;
+ buf[r] = 0;
+
+ if (r == 0)
+ return -EPIPE;
+
+ if ((p = strchr(buf, '#')))
+ *p = '\0';
+
+ argc = split_ip(buf, WHITESPACE, 64, argv);
+ if (argc < 1)
+ return -EINVAL;
+
+ res = run_command(data, argc, argv);
+
+ if (!data->quit)
+ show_prompt(data);
+
+ return res;
+}
+
+static int do_probe(struct data *data)
+{
+ uint32_t n_items = 0;
+ struct acp_dict_item items[64];
+ struct acp_dict props;
+
+ 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;
+ items[n_items++] = ACP_DICT_ITEM_INIT(k, v);
+ if (n_items == 64)
+ break;
+ }
+ }
+ props = ACP_DICT_INIT(items, n_items);
+
+ data->card = acp_card_new(data->card_index, &props);
+ if (data->card == NULL)
+ return -errno;
+ return 0;
+}
+
+static int do_prompt(struct data *data)
+{
+ struct pollfd *pfds;
+ int err, count;
+
+ acp_card_add_listener(data->card, &card_events, data);
+
+ count = acp_card_poll_descriptors_count(data->card);
+ if (count == 0)
+ fprintf(stderr, "card has no events\n");
+
+ count++;
+ pfds = alloca(sizeof(struct pollfd) * count);
+ pfds[0].fd = STDIN_FILENO;
+ pfds[0].events = POLLIN;
+
+ print_card(data, data->card, 0, 0);
+
+ fprintf(stderr, "type 'help' for usage.\n");
+ show_prompt(data);
+
+ while (!data->quit) {
+ unsigned short revents;
+
+ err = acp_card_poll_descriptors(data->card, &pfds[1], count-1);
+ if (err < 0)
+ return err;
+
+ err = poll(pfds, (unsigned int) count, -1);
+ if (err < 0)
+ return -errno;
+
+ if (pfds[0].revents & POLLIN) {
+ if ((err = handle_input(data)) < 0) {
+ if (err == -EPIPE)
+ break;
+ }
+ }
+
+ if (count < 2)
+ continue;
+
+ err = acp_card_poll_descriptors_revents(data->card, &pfds[1], count-1, &revents);
+ if (err < 0)
+ return err;
+
+ if (revents)
+ acp_card_handle_events(data->card);
+ }
+ return 0;
+}
+
+#define OPTIONS "hvc:p:"
+static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h'},
+ { "verbose", no_argument, NULL, 'v'},
+
+ { "card", required_argument, NULL, 'c' },
+ { "properties", required_argument, NULL, 'p' },
+
+ { NULL, 0, NULL, 0 }
+};
+
+static void show_usage(struct data *data, const char *name, bool is_error)
+{
+ FILE *fp;
+
+ fp = is_error ? stderr : stdout;
+
+ fprintf(fp, "%s [options] [COMMAND]\n", name);
+ fprintf(fp,
+ " -h, --help Show this help\n"
+ " -v --verbose Be verbose\n"
+ " -c --card Card number\n"
+ " -p --properties Extra properties:\n"
+ " 'key=value ... '\n"
+ "\n");
+ cmd_help(data, NULL, 0, NULL);
+}
+
+int main(int argc, char *argv[])
+{
+ int c, res;
+ int longopt_index = 0, ret;
+ struct data data = { 0, };
+
+ data.verbose = 1;
+
+ while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) {
+ switch (c) {
+ case 'h':
+ show_usage(&data, argv[0], false);
+ return EXIT_SUCCESS;
+ case 'v':
+ data.verbose++;
+ break;
+ case 'c':
+ ret = atoi(optarg);
+ if (ret < 0) {
+ fprintf(stderr, "error: bad card %s\n", optarg);
+ goto error_usage;
+ }
+ data.card_index = ret;
+ break;
+ case 'p':
+ data.properties = strdup(optarg);
+ break;
+ default:
+ fprintf(stderr, "error: unknown option '%c'\n", c);
+ goto error_usage;
+ }
+ }
+
+ if ((res = do_probe(&data)) < 0) {
+ fprintf(stderr, "failed to probe card: %s\n", strerror(-res));
+ return res;
+ }
+
+ if (optind < argc)
+ run_command(&data, argc - optind, &argv[optind]);
+ else
+ do_prompt(&data);
+
+ if (data.card)
+ acp_card_destroy(data.card);
+
+ free(data.properties);
+
+ return 0;
+
+error_usage:
+ show_usage(&data, argv[0], true);
+ return EXIT_FAILURE;
+}
diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c
new file mode 100644
index 0000000..f9985b4
--- /dev/null
+++ b/spa/plugins/alsa/acp/acp.c
@@ -0,0 +1,1983 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "acp.h"
+#include "alsa-mixer.h"
+#include "alsa-ucm.h"
+
+#include <spa/utils/string.h>
+
+int _acp_log_level = 1;
+acp_log_func _acp_log_func;
+void *_acp_log_data;
+
+struct spa_i18n *acp_i18n;
+
+#define DEFAULT_RATE 48000
+
+#define VOLUME_ACCURACY (PA_VOLUME_NORM/100) /* don't require volume adjustments to be perfectly correct. don't necessarily extend granularity in software unless the differences get greater than this level */
+
+static const uint32_t channel_table[PA_CHANNEL_POSITION_MAX] = {
+ [PA_CHANNEL_POSITION_MONO] = ACP_CHANNEL_MONO,
+
+ [PA_CHANNEL_POSITION_FRONT_LEFT] = ACP_CHANNEL_FL,
+ [PA_CHANNEL_POSITION_FRONT_RIGHT] = ACP_CHANNEL_FR,
+ [PA_CHANNEL_POSITION_FRONT_CENTER] = ACP_CHANNEL_FC,
+
+ [PA_CHANNEL_POSITION_REAR_CENTER] = ACP_CHANNEL_RC,
+ [PA_CHANNEL_POSITION_REAR_LEFT] = ACP_CHANNEL_RL,
+ [PA_CHANNEL_POSITION_REAR_RIGHT] = ACP_CHANNEL_RR,
+
+ [PA_CHANNEL_POSITION_LFE] = ACP_CHANNEL_LFE,
+ [PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = ACP_CHANNEL_FLC,
+ [PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = ACP_CHANNEL_FRC,
+
+ [PA_CHANNEL_POSITION_SIDE_LEFT] = ACP_CHANNEL_SL,
+ [PA_CHANNEL_POSITION_SIDE_RIGHT] = ACP_CHANNEL_SR,
+
+ [PA_CHANNEL_POSITION_AUX0] = ACP_CHANNEL_START_Aux + 0,
+ [PA_CHANNEL_POSITION_AUX1] = ACP_CHANNEL_START_Aux + 1,
+ [PA_CHANNEL_POSITION_AUX2] = ACP_CHANNEL_START_Aux + 2,
+ [PA_CHANNEL_POSITION_AUX3] = ACP_CHANNEL_START_Aux + 3,
+ [PA_CHANNEL_POSITION_AUX4] = ACP_CHANNEL_START_Aux + 4,
+ [PA_CHANNEL_POSITION_AUX5] = ACP_CHANNEL_START_Aux + 5,
+ [PA_CHANNEL_POSITION_AUX6] = ACP_CHANNEL_START_Aux + 6,
+ [PA_CHANNEL_POSITION_AUX7] = ACP_CHANNEL_START_Aux + 7,
+ [PA_CHANNEL_POSITION_AUX8] = ACP_CHANNEL_START_Aux + 8,
+ [PA_CHANNEL_POSITION_AUX9] = ACP_CHANNEL_START_Aux + 9,
+ [PA_CHANNEL_POSITION_AUX10] = ACP_CHANNEL_START_Aux + 10,
+ [PA_CHANNEL_POSITION_AUX11] = ACP_CHANNEL_START_Aux + 11,
+ [PA_CHANNEL_POSITION_AUX12] = ACP_CHANNEL_START_Aux + 12,
+ [PA_CHANNEL_POSITION_AUX13] = ACP_CHANNEL_START_Aux + 13,
+ [PA_CHANNEL_POSITION_AUX14] = ACP_CHANNEL_START_Aux + 14,
+ [PA_CHANNEL_POSITION_AUX15] = ACP_CHANNEL_START_Aux + 15,
+ [PA_CHANNEL_POSITION_AUX16] = ACP_CHANNEL_START_Aux + 16,
+ [PA_CHANNEL_POSITION_AUX17] = ACP_CHANNEL_START_Aux + 17,
+ [PA_CHANNEL_POSITION_AUX18] = ACP_CHANNEL_START_Aux + 18,
+ [PA_CHANNEL_POSITION_AUX19] = ACP_CHANNEL_START_Aux + 19,
+ [PA_CHANNEL_POSITION_AUX20] = ACP_CHANNEL_START_Aux + 20,
+ [PA_CHANNEL_POSITION_AUX21] = ACP_CHANNEL_START_Aux + 21,
+ [PA_CHANNEL_POSITION_AUX22] = ACP_CHANNEL_START_Aux + 22,
+ [PA_CHANNEL_POSITION_AUX23] = ACP_CHANNEL_START_Aux + 23,
+ [PA_CHANNEL_POSITION_AUX24] = ACP_CHANNEL_START_Aux + 24,
+ [PA_CHANNEL_POSITION_AUX25] = ACP_CHANNEL_START_Aux + 25,
+ [PA_CHANNEL_POSITION_AUX26] = ACP_CHANNEL_START_Aux + 26,
+ [PA_CHANNEL_POSITION_AUX27] = ACP_CHANNEL_START_Aux + 27,
+ [PA_CHANNEL_POSITION_AUX28] = ACP_CHANNEL_START_Aux + 28,
+ [PA_CHANNEL_POSITION_AUX29] = ACP_CHANNEL_START_Aux + 29,
+ [PA_CHANNEL_POSITION_AUX30] = ACP_CHANNEL_START_Aux + 30,
+ [PA_CHANNEL_POSITION_AUX31] = ACP_CHANNEL_START_Aux + 31,
+
+ [PA_CHANNEL_POSITION_TOP_CENTER] = ACP_CHANNEL_TC,
+
+ [PA_CHANNEL_POSITION_TOP_FRONT_LEFT] = ACP_CHANNEL_TFL,
+ [PA_CHANNEL_POSITION_TOP_FRONT_RIGHT] = ACP_CHANNEL_TFR,
+ [PA_CHANNEL_POSITION_TOP_FRONT_CENTER] = ACP_CHANNEL_TFC,
+
+ [PA_CHANNEL_POSITION_TOP_REAR_LEFT] = ACP_CHANNEL_TRL,
+ [PA_CHANNEL_POSITION_TOP_REAR_RIGHT] = ACP_CHANNEL_TRR,
+ [PA_CHANNEL_POSITION_TOP_REAR_CENTER] = ACP_CHANNEL_TRC,
+};
+
+static const char *channel_names[] = {
+ [ACP_CHANNEL_UNKNOWN] = "UNK",
+ [ACP_CHANNEL_NA] = "NA",
+ [ACP_CHANNEL_MONO] = "MONO",
+ [ACP_CHANNEL_FL] = "FL",
+ [ACP_CHANNEL_FR] = "FR",
+ [ACP_CHANNEL_FC] = "FC",
+ [ACP_CHANNEL_LFE] = "LFE",
+ [ACP_CHANNEL_SL] = "SL",
+ [ACP_CHANNEL_SR] = "SR",
+ [ACP_CHANNEL_FLC] = "FLC",
+ [ACP_CHANNEL_FRC] = "FRC",
+ [ACP_CHANNEL_RC] = "RC",
+ [ACP_CHANNEL_RL] = "RL",
+ [ACP_CHANNEL_RR] = "RR",
+ [ACP_CHANNEL_TC] = "TC",
+ [ACP_CHANNEL_TFL] = "TFL",
+ [ACP_CHANNEL_TFC] = "TFC",
+ [ACP_CHANNEL_TFR] = "TFR",
+ [ACP_CHANNEL_TRL] = "TRL",
+ [ACP_CHANNEL_TRC] = "TRC",
+ [ACP_CHANNEL_TRR] = "TRR",
+ [ACP_CHANNEL_RLC] = "RLC",
+ [ACP_CHANNEL_RRC] = "RRC",
+ [ACP_CHANNEL_FLW] = "FLW",
+ [ACP_CHANNEL_FRW] = "FRW",
+ [ACP_CHANNEL_LFE2] = "LFE2",
+ [ACP_CHANNEL_FLH] = "FLH",
+ [ACP_CHANNEL_FCH] = "FCH",
+ [ACP_CHANNEL_FRH] = "FRH",
+ [ACP_CHANNEL_TFLC] = "TFLC",
+ [ACP_CHANNEL_TFRC] = "TFRC",
+ [ACP_CHANNEL_TSL] = "TSL",
+ [ACP_CHANNEL_TSR] = "TSR",
+ [ACP_CHANNEL_LLFE] = "LLFE",
+ [ACP_CHANNEL_RLFE] = "RLFE",
+ [ACP_CHANNEL_BC] = "BC",
+ [ACP_CHANNEL_BLC] = "BLC",
+ [ACP_CHANNEL_BRC] = "BRC",
+};
+
+#define ACP_N_ELEMENTS(arr) (sizeof(arr) / sizeof((arr)[0]))
+
+static inline uint32_t channel_pa2acp(pa_channel_position_t channel)
+{
+ if (channel < 0 || (size_t)channel >= ACP_N_ELEMENTS(channel_table))
+ return ACP_CHANNEL_UNKNOWN;
+ return channel_table[channel];
+}
+
+char *acp_channel_str(char *buf, size_t len, enum acp_channel ch)
+{
+ if (ch >= ACP_CHANNEL_START_Aux && ch <= ACP_CHANNEL_LAST_Aux) {
+ snprintf(buf, len, "AUX%d", ch - ACP_CHANNEL_START_Aux);
+ } else if (ch >= ACP_CHANNEL_UNKNOWN && ch <= ACP_CHANNEL_BRC) {
+ snprintf(buf, len, "%s", channel_names[ch]);
+ } else {
+ snprintf(buf, len, "UNK");
+ }
+ return buf;
+}
+
+
+const char *acp_available_str(enum acp_available status)
+{
+ switch (status) {
+ case ACP_AVAILABLE_UNKNOWN:
+ return "unknown";
+ case ACP_AVAILABLE_NO:
+ return "no";
+ case ACP_AVAILABLE_YES:
+ return "yes";
+ }
+ return "error";
+}
+
+const char *acp_direction_str(enum acp_direction direction)
+{
+ switch (direction) {
+ case ACP_DIRECTION_CAPTURE:
+ return "capture";
+ case ACP_DIRECTION_PLAYBACK:
+ return "playback";
+ }
+ return "error";
+}
+
+static void port_free(void *data)
+{
+ pa_device_port *dp = data;
+ pa_dynarray_clear(&dp->devices);
+ pa_dynarray_clear(&dp->prof);
+ pa_device_port_free(dp);
+}
+
+static void device_free(void *data)
+{
+ pa_alsa_device *dev = data;
+ pa_dynarray_clear(&dev->port_array);
+ pa_proplist_free(dev->proplist);
+ pa_hashmap_free(dev->ports);
+}
+
+static inline void channelmap_to_acp(pa_channel_map *m, uint32_t *map)
+{
+ uint32_t i, j;
+ for (i = 0; i < m->channels; i++) {
+ map[i] = channel_pa2acp(m->map[i]);
+ for (j = 0; j < i; j++) {
+ if (map[i] == map[j])
+ map[i] += 32;
+ }
+
+ }
+}
+
+static void init_device(pa_card *impl, pa_alsa_device *dev, pa_alsa_direction_t direction,
+ pa_alsa_mapping *m, uint32_t index)
+{
+ char **d;
+
+ dev->card = impl;
+ dev->mapping = m;
+ dev->device.index = index;
+ dev->device.name = m->name;
+ dev->device.description = m->description;
+ dev->device.priority = m->priority;
+ dev->device.device_strings = (const char **)m->device_strings;
+ dev->device.format.format_mask = m->sample_spec.format;
+ dev->device.format.rate_mask = m->sample_spec.rate;
+ dev->device.format.channels = m->channel_map.channels;
+ pa_cvolume_reset(&dev->real_volume, dev->device.format.channels);
+ pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels);
+ channelmap_to_acp(&m->channel_map, dev->device.format.map);
+ dev->direction = direction;
+ dev->proplist = pa_proplist_new();
+ pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->proplist);
+ if (direction == PA_ALSA_DIRECTION_OUTPUT) {
+ dev->mixer_path_set = m->output_path_set;
+ dev->pcm_handle = m->output_pcm;
+ dev->device.direction = ACP_DIRECTION_PLAYBACK;
+ pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->output_proplist);
+ } else {
+ dev->mixer_path_set = m->input_path_set;
+ dev->pcm_handle = m->input_pcm;
+ dev->device.direction = ACP_DIRECTION_CAPTURE;
+ pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->input_proplist);
+ }
+ 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);
+ pa_proplist_as_dict(dev->proplist, &dev->device.props);
+
+ dev->ports = pa_hashmap_new(pa_idxset_string_hash_func,
+ pa_idxset_string_compare_func);
+ if (m->ucm_context.ucm) {
+ dev->ucm_context = &m->ucm_context;
+ if (impl->ucm.alib_prefix != NULL) {
+ for (d = m->device_strings; *d; d++) {
+ if (pa_startswith(*d, impl->ucm.alib_prefix)) {
+ size_t plen = strlen(impl->ucm.alib_prefix);
+ size_t len = strlen(*d);
+ memmove(*d, (*d) + plen, len - plen + 1);
+ dev->device.flags |= ACP_DEVICE_UCM_DEVICE;
+ }
+ }
+ }
+ }
+ for (d = m->device_strings; *d; d++) {
+ if (pa_startswith(*d, "iec958") ||
+ pa_startswith(*d, "hdmi"))
+ dev->device.flags |= ACP_DEVICE_IEC958;
+ }
+ pa_dynarray_init(&dev->port_array, NULL);
+}
+
+static int compare_profile(const void *a, const void *b)
+{
+ const pa_hashmap_item *i1 = a;
+ const pa_hashmap_item *i2 = b;
+ const pa_alsa_profile *p1, *p2;
+ if (i1->key == NULL || i2->key == NULL)
+ return 0;
+ p1 = i1->value;
+ p2 = i2->value;
+ if (p1->profile.priority == 0 || p2->profile.priority == 0)
+ return 0;
+ return p2->profile.priority - p1->profile.priority;
+}
+
+static void profile_free(void *data)
+{
+ pa_alsa_profile *ap = data;
+ pa_dynarray_clear(&ap->out.devices);
+ if (ap->profile.flags & ACP_PROFILE_OFF) {
+ free(ap->name);
+ free(ap->description);
+ free(ap);
+ }
+}
+
+static int add_pro_profile(pa_card *impl, uint32_t index)
+{
+ snd_ctl_t *ctl_hndl;
+ int err, dev, count = 0;
+ pa_alsa_profile *ap;
+ pa_alsa_profile_set *ps = impl->profile_set;
+ pa_alsa_mapping *m;
+ char *device;
+ snd_pcm_info_t *pcminfo;
+ pa_sample_spec ss;
+ snd_pcm_uframes_t try_period_size, try_buffer_size;
+
+ ss.format = PA_SAMPLE_S32LE;
+ ss.rate = impl->rate;
+ ss.channels = 64;
+
+ ap = pa_xnew0(pa_alsa_profile, 1);
+ ap->profile_set = ps;
+ ap->profile.name = ap->name = pa_xstrdup("pro-audio");
+ ap->profile.description = ap->description = pa_xstrdup(_("Pro Audio"));
+ ap->profile.available = ACP_AVAILABLE_YES;
+ ap->profile.flags = ACP_PROFILE_PRO;
+ ap->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ ap->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ pa_hashmap_put(ps->profiles, ap->name, ap);
+
+ ap->output_name = pa_xstrdup("pro-output");
+ ap->input_name = pa_xstrdup("pro-input");
+ ap->priority = 1;
+
+ pa_assert_se(asprintf(&device, "hw:%d", index) >= 0);
+
+ if ((err = snd_ctl_open(&ctl_hndl, device, 0)) < 0) {
+ pa_log_error("can't open control for card %s: %s",
+ device, snd_strerror(err));
+ free(device);
+ return err;
+ }
+ free(device);
+
+ snd_pcm_info_alloca(&pcminfo);
+
+ dev = -1;
+ while (1) {
+ char desc[128], devstr[128], *name;
+
+ if ((err = snd_ctl_pcm_next_device(ctl_hndl, &dev)) < 0) {
+ pa_log_error("error iterating devices: %s", snd_strerror(err));
+ break;
+ }
+ if (dev < 0)
+ break;
+
+ snd_pcm_info_set_device(pcminfo, dev);
+ snd_pcm_info_set_subdevice(pcminfo, 0);
+
+ snprintf(devstr, sizeof(devstr), "hw:%d,%d", index, dev);
+ if (count++ == 0)
+ snprintf(desc, sizeof(desc), "Pro");
+ else
+ snprintf(desc, sizeof(desc), "Pro %d", dev);
+
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK);
+ if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
+ if (err != -ENOENT)
+ pa_log_error("error pcm info: %s", snd_strerror(err));
+ }
+ if (err >= 0) {
+ pa_assert_se(asprintf(&name, "Mapping pro-output-%d", dev) >= 0);
+ m = pa_alsa_mapping_get(ps, name);
+ m->description = pa_xstrdup(desc);
+ m->device_strings = pa_split_spaces_strv(devstr);
+
+ try_period_size = 1024;
+ try_buffer_size = 1024 * 64;
+ m->sample_spec = ss;
+
+ if ((m->output_pcm = pa_alsa_open_by_template(m->device_strings,
+ devstr, NULL, &m->sample_spec,
+ &m->channel_map, SND_PCM_STREAM_PLAYBACK,
+ &try_period_size, &try_buffer_size,
+ 0, NULL, NULL, false))) {
+ pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
+ pa_proplist_setf(m->output_proplist, "clock.name", "api.alsa.%u", index);
+ pa_alsa_close(&m->output_pcm);
+ m->supported = true;
+ pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX);
+ }
+ pa_idxset_put(ap->output_mappings, m, NULL);
+ free(name);
+ }
+
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE);
+ if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
+ if (err != -ENOENT)
+ pa_log_error("error pcm info: %s", snd_strerror(err));
+ }
+ if (err >= 0) {
+ pa_assert_se(asprintf(&name, "Mapping pro-input-%d", dev) >= 0);
+ m = pa_alsa_mapping_get(ps, name);
+ m->description = pa_xstrdup(desc);
+ m->device_strings = pa_split_spaces_strv(devstr);
+
+ try_period_size = 1024;
+ try_buffer_size = 1024 * 64;
+ m->sample_spec = ss;
+
+ if ((m->input_pcm = pa_alsa_open_by_template(m->device_strings,
+ devstr, NULL, &m->sample_spec,
+ &m->channel_map, SND_PCM_STREAM_CAPTURE,
+ &try_period_size, &try_buffer_size,
+ 0, NULL, NULL, false))) {
+ pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
+ pa_proplist_setf(m->input_proplist, "clock.name", "api.alsa.%u", index);
+ pa_alsa_close(&m->input_pcm);
+ m->supported = true;
+ pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX);
+ }
+ pa_idxset_put(ap->input_mappings, m, NULL);
+ free(name);
+ }
+ }
+ snd_ctl_close(ctl_hndl);
+
+ return 0;
+}
+
+
+static void add_profiles(pa_card *impl)
+{
+ pa_alsa_profile *ap;
+ void *state;
+ struct acp_card_profile *cp;
+ pa_device_port *dp;
+ pa_alsa_device *dev;
+ int n_profiles, n_ports, n_devices;
+ uint32_t idx;
+
+ n_devices = 0;
+ pa_dynarray_init(&impl->out.devices, device_free);
+
+ ap = pa_xnew0(pa_alsa_profile, 1);
+ ap->profile.name = ap->name = pa_xstrdup("off");
+ ap->profile.description = ap->description = pa_xstrdup(_("Off"));
+ ap->profile.available = ACP_AVAILABLE_YES;
+ ap->profile.flags = ACP_PROFILE_OFF;
+ pa_hashmap_put(impl->profiles, ap->name, ap);
+
+ add_pro_profile(impl, impl->card.index);
+
+ PA_HASHMAP_FOREACH(ap, impl->profile_set->profiles, state) {
+ pa_alsa_mapping *m;
+
+ cp = &ap->profile;
+ cp->name = ap->name;
+ cp->description = ap->description;
+ cp->priority = ap->priority ? ap->priority : 1;
+
+ pa_dynarray_init(&ap->out.devices, NULL);
+
+ if (ap->output_mappings) {
+ PA_IDXSET_FOREACH(m, ap->output_mappings, idx) {
+ dev = &m->output;
+ if (dev->mapping == NULL) {
+ init_device(impl, dev, PA_ALSA_DIRECTION_OUTPUT, m, n_devices++);
+ pa_dynarray_append(&impl->out.devices, dev);
+ }
+ if (impl->use_ucm) {
+ if (m->ucm_context.ucm_devices) {
+ pa_alsa_ucm_add_ports_combination(NULL, &m->ucm_context,
+ true, impl->ports, ap, NULL);
+ pa_alsa_ucm_add_ports(&dev->ports, m->proplist, &m->ucm_context,
+ true, impl, dev->pcm_handle, impl->profile_set->ignore_dB);
+ }
+ }
+ else
+ pa_alsa_path_set_add_ports(m->output_path_set, ap, impl->ports,
+ dev->ports, NULL);
+
+ pa_dynarray_append(&ap->out.devices, dev);
+ }
+ }
+
+ if (ap->input_mappings) {
+ PA_IDXSET_FOREACH(m, ap->input_mappings, idx) {
+ dev = &m->input;
+ if (dev->mapping == NULL) {
+ init_device(impl, dev, PA_ALSA_DIRECTION_INPUT, m, n_devices++);
+ pa_dynarray_append(&impl->out.devices, dev);
+ }
+
+ if (impl->use_ucm) {
+ if (m->ucm_context.ucm_devices) {
+ pa_alsa_ucm_add_ports_combination(NULL, &m->ucm_context,
+ false, impl->ports, ap, NULL);
+ pa_alsa_ucm_add_ports(&dev->ports, m->proplist, &m->ucm_context,
+ false, impl, dev->pcm_handle, impl->profile_set->ignore_dB);
+ }
+ } else
+ pa_alsa_path_set_add_ports(m->input_path_set, ap, impl->ports,
+ dev->ports, NULL);
+
+ pa_dynarray_append(&ap->out.devices, dev);
+ }
+ }
+ cp->n_devices = pa_dynarray_size(&ap->out.devices);
+ cp->devices = ap->out.devices.array.data;
+ pa_hashmap_put(impl->profiles, ap->name, cp);
+ }
+
+ pa_dynarray_init(&impl->out.ports, NULL);
+ n_ports = 0;
+ PA_HASHMAP_FOREACH(dp, impl->ports, state) {
+ void *state2;
+ dp->card = impl;
+ dp->port.index = n_ports++;
+ dp->port.priority = dp->priority;
+ pa_dynarray_init(&dp->prof, NULL);
+ pa_dynarray_init(&dp->devices, NULL);
+ n_profiles = 0;
+ PA_HASHMAP_FOREACH(cp, dp->profiles, state2) {
+ pa_dynarray_append(&dp->prof, cp);
+ n_profiles++;
+ }
+ dp->port.n_profiles = n_profiles;
+ dp->port.profiles = dp->prof.array.data;
+
+ pa_proplist_setf(dp->proplist, "card.profile.port", "%u", dp->port.index);
+ pa_proplist_as_dict(dp->proplist, &dp->port.props);
+ pa_dynarray_append(&impl->out.ports, dp);
+ }
+ PA_DYNARRAY_FOREACH(dev, &impl->out.devices, idx) {
+ PA_HASHMAP_FOREACH(dp, dev->ports, state) {
+ pa_dynarray_append(&dev->port_array, dp);
+ pa_dynarray_append(&dp->devices, dev);
+ }
+ dev->device.ports = dev->port_array.array.data;
+ dev->device.n_ports = pa_dynarray_size(&dev->port_array);
+ }
+ PA_HASHMAP_FOREACH(dp, impl->ports, state) {
+ dp->port.devices = dp->devices.array.data;
+ dp->port.n_devices = pa_dynarray_size(&dp->devices);
+ }
+
+ pa_hashmap_sort(impl->profiles, compare_profile);
+
+ n_profiles = 0;
+ pa_dynarray_init(&impl->out.profiles, NULL);
+ PA_HASHMAP_FOREACH(cp, impl->profiles, state) {
+ cp->index = n_profiles++;
+ pa_dynarray_append(&impl->out.profiles, cp);
+ }
+}
+
+static pa_available_t calc_port_state(pa_device_port *p, pa_card *impl)
+{
+ void *state;
+ pa_alsa_jack *jack;
+ pa_available_t pa = PA_AVAILABLE_UNKNOWN;
+ pa_device_port *port;
+
+ PA_HASHMAP_FOREACH(jack, impl->jacks, state) {
+ pa_available_t cpa;
+
+ if (impl->use_ucm)
+ port = pa_hashmap_get(impl->ports, jack->name);
+ else {
+ if (jack->path)
+ port = jack->path->port;
+ else
+ continue;
+ }
+
+ if (p != port)
+ continue;
+
+ cpa = jack->plugged_in ? jack->state_plugged : jack->state_unplugged;
+
+ if (cpa == PA_AVAILABLE_NO) {
+ /* If a plugged-in jack causes the availability to go to NO, it
+ * should override all other availability information (like a
+ * blacklist) so set and bail */
+ if (jack->plugged_in) {
+ pa = cpa;
+ break;
+ }
+
+ /* If the current availability is unknown go the more precise no,
+ * but otherwise don't change state */
+ if (pa == PA_AVAILABLE_UNKNOWN)
+ pa = cpa;
+ } else if (cpa == PA_AVAILABLE_YES) {
+ /* Output is available through at least one jack, so go to that
+ * level of availability. We still need to continue iterating through
+ * the jacks in case a jack is plugged in that forces the state to no
+ */
+ pa = cpa;
+ }
+ }
+ return pa;
+}
+
+static void profile_set_available(pa_card *impl, uint32_t index,
+ enum acp_available status, bool emit)
+{
+ struct acp_card_profile *p = impl->card.profiles[index];
+ enum acp_available old = p->available;
+
+ if (old != status)
+ pa_log_info("Profile %s available %s -> %s", p->name,
+ acp_available_str(old), acp_available_str(status));
+
+ p->available = status;
+
+ if (emit && impl->events && impl->events->profile_available)
+ impl->events->profile_available(impl->user_data, index,
+ old, status);
+}
+
+struct temp_port_avail {
+ pa_device_port *port;
+ pa_available_t avail;
+};
+
+static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask)
+{
+ pa_card *impl = snd_mixer_elem_get_callback_private(melem);
+ snd_hctl_elem_t *elem = snd_mixer_elem_get_private(melem);
+ snd_ctl_elem_value_t *elem_value;
+ bool plugged_in, any_input_port_available;
+ void *state;
+ pa_alsa_jack *jack;
+ struct temp_port_avail *tp, *tports;
+ pa_alsa_profile *profile;
+ enum acp_available active_available = ACP_AVAILABLE_UNKNOWN;
+ size_t size;
+
+#if 0
+ /* Changing the jack state may cause a port change, and a port change will
+ * make the sink or source change the mixer settings. If there are multiple
+ * users having pulseaudio running, the mixer changes done by inactive
+ * users may mess up the volume settings for the active users, because when
+ * the inactive users change the mixer settings, those changes are picked
+ * up by the active user's pulseaudio instance and the changes are
+ * interpreted as if the active user changed the settings manually e.g.
+ * with alsamixer. Even single-user systems suffer from this, because gdm
+ * runs its own pulseaudio instance.
+ *
+ * We rerun this function when being unsuspended to catch up on jack state
+ * changes */
+ if (u->card->suspend_cause & PA_SUSPEND_SESSION)
+ return 0;
+#endif
+
+ if (mask == SND_CTL_EVENT_MASK_REMOVE)
+ return 0;
+
+ snd_ctl_elem_value_alloca(&elem_value);
+ if (snd_hctl_elem_read(elem, elem_value) < 0) {
+ pa_log_warn("Failed to read jack detection from '%s'", pa_strnull(snd_hctl_elem_get_name(elem)));
+ return 0;
+ }
+
+ plugged_in = !!snd_ctl_elem_value_get_boolean(elem_value, 0);
+
+ pa_log_debug("Jack '%s' is now %s", pa_strnull(snd_hctl_elem_get_name(elem)),
+ plugged_in ? "plugged in" : "unplugged");
+
+ size = sizeof(struct temp_port_avail) * (pa_hashmap_size(impl->jacks)+1);
+ tports = tp = alloca(size);
+ memset(tports, 0, size);
+
+ PA_HASHMAP_FOREACH(jack, impl->jacks, state)
+ if (jack->melem == melem) {
+ pa_alsa_jack_set_plugged_in(jack, plugged_in);
+
+ if (impl->use_ucm) {
+ /* When using UCM, pa_alsa_jack_set_plugged_in() maps the jack
+ * state to port availability. */
+ continue;
+ }
+
+ /* When not using UCM, we have to do the jack state -> port
+ * availability mapping ourselves. */
+ pa_assert_se(tp->port = jack->path->port);
+ tp->avail = calc_port_state(tp->port, impl);
+ tp++;
+ }
+
+ /* Report available ports before unavailable ones: in case port 1
+ * becomes available when port 2 becomes unavailable,
+ * this prevents an unnecessary switch port 1 -> port 3 -> port 2 */
+
+ for (tp = tports; tp->port; tp++)
+ if (tp->avail != PA_AVAILABLE_NO)
+ pa_device_port_set_available(tp->port, tp->avail);
+ for (tp = tports; tp->port; tp++)
+ if (tp->avail == PA_AVAILABLE_NO)
+ pa_device_port_set_available(tp->port, tp->avail);
+
+ for (tp = tports; tp->port; tp++) {
+ pa_alsa_port_data *data;
+
+ data = PA_DEVICE_PORT_DATA(tp->port);
+
+ if (!data->suspend_when_unavailable)
+ continue;
+
+#if 0
+ pa_sink *sink;
+ uint32_t idx;
+ PA_IDXSET_FOREACH(sink, u->core->sinks, idx) {
+ if (sink->active_port == tp->port)
+ pa_sink_suspend(sink, tp->avail == PA_AVAILABLE_NO, PA_SUSPEND_UNAVAILABLE);
+ }
+#endif
+ }
+
+ /* Update profile availabilities. Ideally we would mark all profiles
+ * unavailable that contain unavailable devices. We can't currently do that
+ * in all cases, because if there are multiple sinks in a profile, and the
+ * profile contains a mix of available and unavailable ports, we don't know
+ * how the ports are distributed between the different sinks. It's possible
+ * that some sinks contain only unavailable ports, in which case we should
+ * mark the profile as unavailable, but it's also possible that all sinks
+ * contain at least one available port, in which case we should mark the
+ * profile as available. Until the data structures are improved so that we
+ * can distinguish between these two cases, we mark the problematic cases
+ * as available (well, "unknown" to be precise, but there's little
+ * practical difference).
+ *
+ * When all output ports are unavailable, we know that all sinks are
+ * unavailable, and therefore the profile is marked unavailable as well.
+ * The same applies to input ports as well, of course.
+ *
+ * If there are no output ports at all, but the profile contains at least
+ * one sink, then the output is considered to be available. */
+ if (impl->card.active_profile_index != ACP_INVALID_INDEX)
+ active_available = impl->card.profiles[impl->card.active_profile_index]->available;
+
+ /* First round - detect, if we have any input port available.
+ If the hardware can report the state for all I/O jacks, only speakers
+ may be plugged in. */
+ any_input_port_available = false;
+ PA_HASHMAP_FOREACH(profile, impl->profiles, state) {
+ pa_device_port *port;
+ void *state2;
+
+ if (profile->profile.flags & ACP_PROFILE_OFF)
+ continue;
+
+ PA_HASHMAP_FOREACH(port, impl->ports, state2) {
+ if (!pa_hashmap_get(port->profiles, profile->profile.name))
+ continue;
+
+ if (port->port.direction == ACP_DIRECTION_CAPTURE &&
+ port->port.available != ACP_AVAILABLE_NO) {
+ any_input_port_available = true;
+ goto input_port_found;
+ }
+ }
+ }
+input_port_found:
+
+ /* Second round */
+ PA_HASHMAP_FOREACH(profile, impl->profiles, state) {
+ pa_device_port *port;
+ void *state2;
+ bool has_input_port = false;
+ bool has_output_port = false;
+ bool found_available_input_port = false;
+ bool found_available_output_port = false;
+ enum acp_available available = ACP_AVAILABLE_UNKNOWN;
+
+ if (profile->profile.flags & ACP_PROFILE_OFF)
+ continue;
+
+ PA_HASHMAP_FOREACH(port, impl->ports, state2) {
+ if (!pa_hashmap_get(port->profiles, profile->profile.name))
+ continue;
+
+ if (port->port.direction == ACP_DIRECTION_CAPTURE) {
+ has_input_port = true;
+ if (port->port.available != ACP_AVAILABLE_NO)
+ found_available_input_port = true;
+ } else {
+ has_output_port = true;
+ if (port->port.available != ACP_AVAILABLE_NO)
+ found_available_output_port = true;
+ }
+ }
+
+ if ((has_input_port && !found_available_input_port) ||
+ (has_output_port && !found_available_output_port))
+ available = ACP_AVAILABLE_NO;
+
+ if (has_input_port && !has_output_port && found_available_input_port)
+ available = ACP_AVAILABLE_YES;
+ if (has_output_port && (!has_input_port || !any_input_port_available) && found_available_output_port)
+ available = ACP_AVAILABLE_YES;
+ if (has_output_port && has_input_port && found_available_output_port && found_available_input_port)
+ available = ACP_AVAILABLE_YES;
+
+ /* We want to update the active profile's status last, so logic that
+ * may change the active profile based on profile availability status
+ * has an updated view of all profiles' availabilities. */
+ if (profile->profile.index == impl->card.active_profile_index)
+ active_available = available;
+ else
+ profile_set_available(impl, profile->profile.index, available, false);
+ }
+
+ if (impl->card.active_profile_index != ACP_INVALID_INDEX)
+ profile_set_available(impl, impl->card.active_profile_index, active_available, true);
+
+ return 0;
+}
+
+static void init_jacks(pa_card *impl)
+{
+ void *state;
+ pa_alsa_path* path;
+ pa_alsa_jack* jack;
+ char buf[64];
+
+ impl->jacks = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ if (impl->use_ucm) {
+ PA_LLIST_FOREACH(jack, impl->ucm.jacks)
+ if (jack->has_control)
+ pa_hashmap_put(impl->jacks, jack, jack);
+ } else {
+ /* See if we have any jacks */
+ if (impl->profile_set->output_paths)
+ PA_HASHMAP_FOREACH(path, impl->profile_set->output_paths, state)
+ PA_LLIST_FOREACH(jack, path->jacks)
+ if (jack->has_control)
+ pa_hashmap_put(impl->jacks, jack, jack);
+
+ if (impl->profile_set->input_paths)
+ PA_HASHMAP_FOREACH(path, impl->profile_set->input_paths, state)
+ PA_LLIST_FOREACH(jack, path->jacks)
+ if (jack->has_control)
+ pa_hashmap_put(impl->jacks, jack, jack);
+ }
+
+ pa_log_debug("Found %d jacks.", pa_hashmap_size(impl->jacks));
+
+ if (pa_hashmap_size(impl->jacks) == 0)
+ return;
+
+ PA_HASHMAP_FOREACH(jack, impl->jacks, state) {
+ if (!jack->mixer_device_name) {
+ jack->mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, false);
+ if (!jack->mixer_handle) {
+ pa_log("Failed to open mixer for card %d for jack detection", impl->card.index);
+ continue;
+ }
+ } else {
+ jack->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, jack->mixer_device_name, false);
+ if (!jack->mixer_handle) {
+ pa_log("Failed to open mixer '%s' for jack detection", jack->mixer_device_name);
+ continue;
+ }
+ }
+
+ pa_alsa_mixer_use_for_poll(impl->ucm.mixers, jack->mixer_handle);
+ jack->melem = pa_alsa_mixer_find_card(jack->mixer_handle, &jack->alsa_id, 0);
+ if (!jack->melem) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &jack->alsa_id);
+ pa_log_warn("Jack '%s' seems to have disappeared.", buf);
+ pa_alsa_jack_set_has_control(jack, false);
+ continue;
+ }
+ snd_mixer_elem_set_callback(jack->melem, report_jack_state);
+ snd_mixer_elem_set_callback_private(jack->melem, impl);
+ report_jack_state(jack->melem, 0);
+ }
+}
+static pa_device_port* find_port_with_eld_device(pa_card *impl, int device)
+{
+ void *state;
+ pa_device_port *p;
+
+ if (impl->use_ucm) {
+ PA_HASHMAP_FOREACH(p, impl->ports, state) {
+ pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(p);
+ pa_assert(data->eld_mixer_device_name);
+ if (device == data->eld_device)
+ return p;
+ }
+ } else {
+ PA_HASHMAP_FOREACH(p, impl->ports, state) {
+ pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(p);
+ pa_assert(data->path);
+ if (device == data->path->eld_device)
+ return p;
+ }
+ }
+ return NULL;
+}
+
+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);
+ int device = snd_hctl_elem_get_device(elem);
+ const char *old_monitor_name;
+ pa_device_port *p;
+ pa_hdmi_eld eld;
+ bool changed = false;
+
+ if (mask == SND_CTL_EVENT_MASK_REMOVE)
+ return 0;
+
+ p = find_port_with_eld_device(impl, device);
+ if (p == NULL) {
+ pa_log_error("Invalid device changed in ALSA: %d", device);
+ return 0;
+ }
+
+ if (pa_alsa_get_hdmi_eld(elem, &eld) < 0)
+ memset(&eld, 0, sizeof(eld));
+
+ old_monitor_name = pa_proplist_gets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);
+ if (eld.monitor_name[0] == '\0') {
+ changed |= old_monitor_name != NULL;
+ pa_proplist_unset(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);
+ } else {
+ 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);
+ }
+ pa_proplist_as_dict(p->proplist, &p->port.props);
+
+ if (changed && mask != 0 && impl->events && impl->events->props_changed)
+ impl->events->props_changed(impl->user_data);
+ return 0;
+}
+
+static void init_eld_ctls(pa_card *impl)
+{
+ void *state;
+ pa_device_port *port;
+
+ /* The code in this function expects ports to have a pa_alsa_port_data
+ * struct as their data, but in UCM mode ports don't have any data. Hence,
+ * the ELD controls can't currently be used in UCM mode. */
+ PA_HASHMAP_FOREACH(port, impl->ports, state) {
+ snd_mixer_t *mixer_handle;
+ snd_mixer_elem_t* melem;
+ int device;
+
+ if (impl->use_ucm) {
+ pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(port);
+ device = data->eld_device;
+ if (device < 0 || !data->eld_mixer_device_name)
+ continue;
+
+ mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, data->eld_mixer_device_name, true);
+ } else {
+ pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(port);
+
+ pa_assert(data->path);
+
+ device = data->path->eld_device;
+ if (device < 0)
+ continue;
+
+ mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, true);
+ }
+
+ if (!mixer_handle)
+ continue;
+
+ melem = pa_alsa_mixer_find_pcm(mixer_handle, "ELD", device);
+ if (melem) {
+ pa_alsa_mixer_use_for_poll(impl->ucm.mixers, mixer_handle);
+ snd_mixer_elem_set_callback(melem, hdmi_eld_changed);
+ snd_mixer_elem_set_callback_private(melem, impl);
+ hdmi_eld_changed(melem, 0);
+ pa_log_info("ELD device found for port %s (%d).", port->port.name, device);
+ }
+ else
+ pa_log_debug("No ELD device found for port %s (%d).", port->port.name, device);
+ }
+}
+
+uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name)
+{
+ uint32_t i;
+ uint32_t best, best2, off;
+ struct acp_card_profile **profiles = card->profiles;
+
+ best = best2 = ACP_INVALID_INDEX;
+ off = 0;
+
+ for (i = 0; i < card->n_profiles; i++) {
+ struct acp_card_profile *p = profiles[i];
+
+ if (name) {
+ if (spa_streq(name, p->name))
+ best = i;
+ } else if (p->flags & ACP_PROFILE_OFF) {
+ off = i;
+ } else if (p->available == ACP_AVAILABLE_YES) {
+ if (best == ACP_INVALID_INDEX || p->priority > profiles[best]->priority)
+ best = i;
+ } else if (p->available != ACP_AVAILABLE_NO) {
+ if (best2 == ACP_INVALID_INDEX || p->priority > profiles[best2]->priority)
+ best2 = i;
+ }
+ }
+ if (best == ACP_INVALID_INDEX)
+ best = best2;
+ if (best == ACP_INVALID_INDEX)
+ best = off;
+ return best;
+}
+
+static void find_mixer(pa_card *impl, pa_alsa_device *dev, const char *element, bool ignore_dB)
+{
+ const char *mdev;
+ pa_alsa_mapping *mapping = dev->mapping;
+
+ if (!mapping && !element)
+ return;
+
+ if (!element && mapping && pa_alsa_path_set_is_empty(dev->mixer_path_set))
+ return;
+
+ mdev = pa_proplist_gets(mapping->proplist, "alsa.mixer_device");
+ if (mdev) {
+ dev->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, mdev, true);
+ } else {
+ dev->mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, true);
+ }
+ if (!dev->mixer_handle) {
+ pa_log_info("Failed to find a working mixer device.");
+ return;
+ }
+
+ if (element) {
+ if (!(dev->mixer_path = pa_alsa_path_synthesize(element, dev->direction)))
+ goto fail;
+
+ if (pa_alsa_path_probe(dev->mixer_path, NULL, dev->mixer_handle, ignore_dB) < 0)
+ goto fail;
+
+ pa_log_debug("Probed mixer path %s:", dev->mixer_path->name);
+ pa_alsa_path_dump(dev->mixer_path);
+ }
+ return;
+
+fail:
+ if (dev->mixer_path) {
+ pa_alsa_path_free(dev->mixer_path);
+ dev->mixer_path = NULL;
+ }
+ dev->mixer_handle = NULL;
+}
+
+static int mixer_callback(snd_mixer_elem_t *elem, unsigned int mask)
+{
+ pa_alsa_device *dev = snd_mixer_elem_get_callback_private(elem);
+
+ if (mask == SND_CTL_EVENT_MASK_REMOVE)
+ return 0;
+
+ pa_log_info("%p mixer changed %d", dev, mask);
+
+ if (mask & SND_CTL_EVENT_MASK_VALUE) {
+ if (dev->read_volume)
+ dev->read_volume(dev);
+ if (dev->read_mute)
+ dev->read_mute(dev);
+ }
+ return 0;
+}
+
+static int read_volume(pa_alsa_device *dev)
+{
+ pa_card *impl = dev->card;
+ pa_cvolume r;
+ uint32_t i;
+ int res;
+
+ if (!dev->mixer_handle)
+ return 0;
+
+ if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0)
+ return res;
+
+ /* Shift down by the base volume, so that 0dB becomes maximum volume */
+ pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
+
+ if (pa_cvolume_equal(&dev->hardware_volume, &r))
+ return 0;
+
+ dev->real_volume = dev->hardware_volume = r;
+
+ pa_log_info("New hardware volume: min:%d max:%d",
+ pa_cvolume_min(&r), pa_cvolume_max(&r));
+
+ for (i = 0; i < r.channels; i++)
+ pa_log_debug(" %d: %d", i, r.values[i]);
+
+ pa_cvolume_reset(&dev->soft_volume, r.channels);
+
+ if (impl->events && impl->events->volume_changed)
+ impl->events->volume_changed(impl->user_data, &dev->device);
+
+ return 0;
+}
+
+static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
+{
+ pa_cvolume r;
+
+ dev->real_volume = *v;
+
+ if (!dev->mixer_handle)
+ return;
+
+ /* Shift up by the base volume */
+ pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
+
+ if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
+ &r, false, true) < 0)
+ return;
+
+ /* Shift down by the base volume, so that 0dB becomes maximum volume */
+ pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
+
+ dev->hardware_volume = r;
+
+ if (dev->mixer_path->has_dB) {
+ pa_cvolume new_soft_volume;
+ bool accurate_enough;
+
+ /* Match exactly what the user requested by software */
+ pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume);
+
+ /* If the adjustment to do in software is only minimal we
+ * can skip it. That saves us CPU at the expense of a bit of
+ * accuracy */
+ accurate_enough =
+ (pa_cvolume_min(&new_soft_volume) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) &&
+ (pa_cvolume_max(&new_soft_volume) <= (PA_VOLUME_NORM + VOLUME_ACCURACY));
+
+ pa_log_debug("Requested volume: %d", pa_cvolume_max(&dev->real_volume));
+ pa_log_debug("Got hardware volume: %d", pa_cvolume_max(&dev->hardware_volume));
+ pa_log_debug("Calculated software volume: %d (accurate-enough=%s)",
+ pa_cvolume_max(&new_soft_volume),
+ pa_yes_no(accurate_enough));
+
+ if (accurate_enough)
+ pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels);
+
+ dev->soft_volume = new_soft_volume;
+ } else {
+ pa_log_debug("Wrote hardware volume: %d", pa_cvolume_max(&r));
+ /* We can't match exactly what the user requested, hence let's
+ * at least tell the user about it */
+ dev->real_volume = r;
+ }
+}
+
+static int read_mute(pa_alsa_device *dev)
+{
+ pa_card *impl = dev->card;
+ bool mute;
+ int res;
+
+ if (!dev->mixer_handle)
+ return 0;
+
+ if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0)
+ return res;
+
+ if (mute == dev->muted)
+ return 0;
+
+ dev->muted = mute;
+ pa_log_info("New hardware muted: %d", mute);
+
+ if (impl->events && impl->events->mute_changed)
+ impl->events->mute_changed(impl->user_data, &dev->device);
+
+ return 0;
+}
+
+static void set_mute(pa_alsa_device *dev, bool mute)
+{
+ dev->muted = mute;
+
+ if (!dev->mixer_handle)
+ return;
+
+ pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute);
+}
+
+static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev)
+{
+ pa_assert(dev);
+
+ if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) {
+ dev->read_volume = NULL;
+ dev->set_volume = NULL;
+ pa_log_info("Driver does not support hardware volume control, "
+ "falling back to software volume control.");
+ dev->base_volume = PA_VOLUME_NORM;
+ dev->n_volume_steps = PA_VOLUME_NORM+1;
+ dev->device.flags &= ~ACP_DEVICE_HW_VOLUME;
+ } else {
+ dev->read_volume = read_volume;
+ dev->set_volume = set_volume;
+ dev->device.flags |= ACP_DEVICE_HW_VOLUME;
+
+#if 0
+ if (u->mixer_path->has_dB && u->deferred_volume) {
+ pa_sink_set_write_volume_callback(u->sink, sink_write_volume_cb);
+ pa_log_info("Successfully enabled deferred volume.");
+ } else
+ pa_sink_set_write_volume_callback(u->sink, NULL);
+#endif
+
+ if (dev->mixer_path->has_dB) {
+ dev->decibel_volume = true;
+ pa_log_info("Hardware volume ranges from %0.2f dB to %0.2f dB.",
+ dev->mixer_path->min_dB, dev->mixer_path->max_dB);
+
+ dev->base_volume = pa_sw_volume_from_dB(-dev->mixer_path->max_dB);
+ dev->n_volume_steps = PA_VOLUME_NORM+1;
+
+ pa_log_info("Fixing base volume to %0.2f dB", pa_sw_volume_to_dB(dev->base_volume));
+ } else {
+ dev->decibel_volume = false;
+ pa_log_info("Hardware volume ranges from %li to %li.",
+ dev->mixer_path->min_volume, dev->mixer_path->max_volume);
+ dev->base_volume = PA_VOLUME_NORM;
+ dev->n_volume_steps = dev->mixer_path->max_volume - dev->mixer_path->min_volume + 1;
+ }
+ pa_log_info("Using hardware volume control. Hardware dB scale %s.",
+ dev->mixer_path->has_dB ? "supported" : "not supported");
+ }
+ dev->device.base_volume = pa_sw_volume_to_linear(dev->base_volume);
+ dev->device.volume_step = 1.0f / dev->n_volume_steps;
+
+ if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_mute) {
+ dev->read_mute = NULL;
+ dev->set_mute = NULL;
+ pa_log_info("Driver does not support hardware mute control, falling back to software mute control.");
+ dev->device.flags &= ~ACP_DEVICE_HW_MUTE;
+ } else {
+ dev->read_mute = read_mute;
+ dev->set_mute = set_mute;
+ pa_log_info("Using hardware mute control.");
+ dev->device.flags |= ACP_DEVICE_HW_MUTE;
+ }
+}
+
+
+static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
+{
+ int res;
+ bool need_mixer_callback = false;
+
+ /* This code is before the u->mixer_handle check, because if the UCM
+ * configuration doesn't specify volume or mute controls, u->mixer_handle
+ * will be NULL, but the UCM device enable sequence will still need to be
+ * executed. */
+ if (dev->active_port && dev->ucm_context) {
+ if ((res = pa_alsa_ucm_set_port(dev->ucm_context, dev->active_port,
+ dev->direction == PA_ALSA_DIRECTION_OUTPUT)) < 0)
+ return res;
+ }
+
+ if (!dev->mixer_handle)
+ return 0;
+
+ if (dev->active_port) {
+ if (!impl->use_ucm) {
+ pa_alsa_port_data *data;
+
+ /* We have a list of supported paths, so let's activate the
+ * one that has been chosen as active */
+ 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);
+ } else {
+ pa_alsa_ucm_port_data *data;
+
+ data = PA_DEVICE_PORT_DATA(dev->active_port);
+
+ /* 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);
+ }
+ }
+ } else {
+ if (!dev->mixer_path && dev->mixer_path_set)
+ dev->mixer_path = pa_hashmap_first(dev->mixer_path_set->paths);
+
+ 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);
+ } else
+ return 0;
+ }
+
+ mixer_volume_init(impl, dev);
+
+ /* Will we need to register callbacks? */
+ if (dev->mixer_path_set && dev->mixer_path_set->paths) {
+ pa_alsa_path *p;
+ void *state;
+
+ PA_HASHMAP_FOREACH(p, dev->mixer_path_set->paths, state) {
+ if (p->has_volume || p->has_mute)
+ need_mixer_callback = true;
+ }
+ }
+ else if (dev->mixer_path)
+ need_mixer_callback = dev->mixer_path->has_volume || dev->mixer_path->has_mute;
+
+ if (!impl->soft_mixer && need_mixer_callback) {
+ pa_alsa_mixer_use_for_poll(impl->ucm.mixers, dev->mixer_handle);
+ if (dev->mixer_path_set)
+ pa_alsa_path_set_set_callback(dev->mixer_path_set, dev->mixer_handle, mixer_callback, dev);
+ else
+ pa_alsa_path_set_callback(dev->mixer_path, dev->mixer_handle, mixer_callback, dev);
+ }
+ return 0;
+}
+
+static int device_disable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev)
+{
+ dev->device.flags &= ~ACP_DEVICE_ACTIVE;
+ if (dev->active_port) {
+ dev->active_port->port.flags &= ~ACP_PORT_ACTIVE;
+ dev->active_port = NULL;
+ }
+ return 0;
+}
+
+static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev)
+{
+ const char *mod_name;
+ uint32_t i, port_index;
+ int res;
+
+ if (impl->use_ucm &&
+ (mod_name = pa_proplist_gets(mapping->proplist, PA_ALSA_PROP_UCM_MODIFIER))) {
+ if (snd_use_case_set(impl->ucm.ucm_mgr, "_enamod", mod_name) < 0)
+ pa_log("Failed to enable ucm modifier %s", mod_name);
+ else
+ pa_log_debug("Enabled ucm modifier %s", mod_name);
+ }
+
+ pa_log_info("Device: %s mapping '%s' (%s).", dev->device.description,
+ mapping->description, mapping->name);
+
+ dev->device.flags |= ACP_DEVICE_ACTIVE;
+
+ find_mixer(impl, dev, NULL, impl->ignore_dB);
+
+ /* 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->port.priority = p->priority;
+ }
+
+ if (impl->auto_port)
+ port_index = acp_device_find_best_port_index(&dev->device, NULL);
+ else
+ port_index = ACP_INVALID_INDEX;
+
+ if (port_index == ACP_INVALID_INDEX)
+ dev->active_port = NULL;
+ else
+ dev->active_port = (pa_device_port*)impl->card.ports[port_index];
+
+ if (dev->active_port)
+ dev->active_port->port.flags |= ACP_PORT_ACTIVE;
+
+ if ((res = setup_mixer(impl, dev, impl->ignore_dB)) < 0)
+ return res;
+
+ if (dev->read_volume)
+ dev->read_volume(dev);
+ else {
+ pa_cvolume_reset(&dev->real_volume, dev->device.format.channels);
+ pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels);
+ }
+ if (dev->read_mute)
+ dev->read_mute(dev);
+ else
+ dev->muted = false;
+
+ return 0;
+}
+
+int acp_card_set_profile(struct acp_card *card, uint32_t new_index, uint32_t flags)
+{
+ pa_card *impl = (pa_card *)card;
+ pa_alsa_mapping *am;
+ uint32_t old_index = impl->card.active_profile_index;
+ struct acp_card_profile **profiles = card->profiles;
+ pa_alsa_profile *op, *np;
+ uint32_t idx;
+ int res;
+
+ if (new_index >= card->n_profiles)
+ return -EINVAL;
+
+ op = old_index != ACP_INVALID_INDEX ? (pa_alsa_profile*)profiles[old_index] : NULL;
+ np = (pa_alsa_profile*)profiles[new_index];
+
+ if (op == np)
+ return 0;
+
+ pa_log_info("activate profile: %s (%d)", np->profile.name, new_index);
+
+ if (op && op->output_mappings) {
+ PA_IDXSET_FOREACH(am, op->output_mappings, idx) {
+ if (np->output_mappings &&
+ pa_idxset_get_by_data(np->output_mappings, am, NULL))
+ continue;
+
+ device_disable(impl, am, &am->output);
+ }
+ }
+ if (op && op->input_mappings) {
+ PA_IDXSET_FOREACH(am, op->input_mappings, idx) {
+ if (np->input_mappings &&
+ pa_idxset_get_by_data(np->input_mappings, am, NULL))
+ continue;
+
+ device_disable(impl, am, &am->input);
+ }
+ }
+
+ /* if UCM is available for this card then update the verb */
+ if (impl->use_ucm && !(np->profile.flags & ACP_PROFILE_PRO)) {
+ if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl,
+ np->profile.flags & ACP_PROFILE_OFF ? NULL : np->profile.name,
+ op ? op->profile.name : NULL)) < 0) {
+ return res;
+ }
+ }
+
+ if (np->output_mappings) {
+ PA_IDXSET_FOREACH(am, np->output_mappings, idx) {
+ if (impl->use_ucm) {
+ /* Update ports priorities */
+ if (am->ucm_context.ucm_devices) {
+ pa_alsa_ucm_add_ports_combination(am->output.ports, &am->ucm_context,
+ true, impl->ports, np, NULL);
+ }
+ }
+ device_enable(impl, am, &am->output);
+ }
+ }
+
+ if (np->input_mappings) {
+ PA_IDXSET_FOREACH(am, np->input_mappings, idx) {
+ if (impl->use_ucm) {
+ /* Update ports priorities */
+ if (am->ucm_context.ucm_devices) {
+ pa_alsa_ucm_add_ports_combination(am->input.ports, &am->ucm_context,
+ false, impl->ports, np, NULL);
+ }
+ }
+ device_enable(impl, am, &am->input);
+ }
+ }
+ if (op)
+ op->profile.flags &= ~(ACP_PROFILE_ACTIVE | ACP_PROFILE_SAVE);
+ np->profile.flags |= ACP_PROFILE_ACTIVE | flags;
+ impl->card.active_profile_index = new_index;
+
+ if (impl->events && impl->events->profile_changed)
+ impl->events->profile_changed(impl->user_data, old_index,
+ new_index);
+ return 0;
+}
+
+static void prune_singleton_availability_groups(pa_hashmap *ports) {
+ pa_device_port *p;
+ pa_hashmap *group_counts;
+ void *state, *count;
+ const char *group;
+
+ /* Collect groups and erase those that don't have more than 1 path */
+ group_counts = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+
+ PA_HASHMAP_FOREACH(p, ports, state) {
+ if (p->availability_group) {
+ count = pa_hashmap_get(group_counts, p->availability_group);
+ pa_hashmap_remove(group_counts, p->availability_group);
+ pa_hashmap_put(group_counts, p->availability_group, PA_UINT_TO_PTR(PA_PTR_TO_UINT(count) + 1));
+ }
+ }
+
+ /* Now we have an availability_group -> count map, let's drop all groups
+ * that have only one member */
+ PA_HASHMAP_FOREACH_KV(group, count, group_counts, state) {
+ if (count == PA_UINT_TO_PTR(1))
+ pa_hashmap_remove(group_counts, group);
+ }
+
+ PA_HASHMAP_FOREACH(p, ports, state) {
+ if (p->availability_group && !pa_hashmap_get(group_counts, p->availability_group)) {
+ pa_log_debug("Pruned singleton availability group %s from port %s", p->availability_group, p->name);
+ pa_xfree(p->availability_group);
+ p->availability_group = NULL;
+ }
+ }
+
+ pa_hashmap_free(group_counts);
+}
+
+static const char *acp_dict_lookup(const struct acp_dict *dict, const char *key)
+{
+ const struct acp_dict_item *it;
+ acp_dict_for_each(it, dict) {
+ if (spa_streq(key, it->key))
+ return it->value;
+ }
+ return NULL;
+}
+
+struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
+{
+ pa_card *impl;
+ struct acp_card *card;
+ const char *s, *profile_set = NULL, *profile = NULL;
+ char device_id[16];
+ uint32_t profile_index;
+ int res;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL)
+ return NULL;
+
+ pa_alsa_refcnt_inc();
+
+ snprintf(device_id, sizeof(device_id), "%d", index);
+
+ impl->proplist = pa_proplist_new_dict(props);
+
+ card = &impl->card;
+ card->index = index;
+ card->active_profile_index = ACP_INVALID_INDEX;
+
+ impl->use_ucm = true;
+ impl->auto_profile = true;
+ impl->auto_port = true;
+ impl->ignore_dB = false;
+ impl->rate = DEFAULT_RATE;
+
+ if (props) {
+ if ((s = acp_dict_lookup(props, "api.alsa.use-ucm")) != NULL)
+ 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.ignore-dB")) != NULL)
+ impl->ignore_dB = spa_atob(s);
+ if ((s = acp_dict_lookup(props, "device.profile-set")) != NULL)
+ profile_set = s;
+ if ((s = acp_dict_lookup(props, "device.profile")) != NULL)
+ profile = s;
+ if ((s = acp_dict_lookup(props, "api.acp.auto-profile")) != NULL)
+ impl->auto_profile = spa_atob(s);
+ if ((s = acp_dict_lookup(props, "api.acp.auto-port")) != NULL)
+ impl->auto_port = spa_atob(s);
+ if ((s = acp_dict_lookup(props, "api.acp.probe-rate")) != NULL)
+ impl->rate = atoi(s);
+ }
+
+ impl->ucm.default_sample_spec.format = PA_SAMPLE_S16NE;
+ impl->ucm.default_sample_spec.rate = impl->rate;
+ impl->ucm.default_sample_spec.channels = 2;
+ pa_channel_map_init_extend(&impl->ucm.default_channel_map,
+ impl->ucm.default_sample_spec.channels, PA_CHANNEL_MAP_ALSA);
+ impl->ucm.default_n_fragments = 4;
+ impl->ucm.default_fragment_size_msec = 25;
+
+ impl->ucm.mixers = pa_hashmap_new_full(pa_idxset_string_hash_func,
+ pa_idxset_string_compare_func,
+ pa_xfree, (pa_free_cb_t) pa_alsa_mixer_free);
+ impl->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func,
+ pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) profile_free);
+ impl->ports = pa_hashmap_new_full(pa_idxset_string_hash_func,
+ pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) port_free);
+
+ snd_config_update_free_global();
+
+ res = impl->use_ucm ? pa_alsa_ucm_query_profiles(&impl->ucm, card->index) : -1;
+ if (res == -PA_ALSA_ERR_UCM_LINKED) {
+ res = -ENOENT;
+ goto error;
+ }
+ if (res == 0) {
+ pa_log_info("Found UCM profiles");
+ impl->profile_set = pa_alsa_ucm_add_profile_set(&impl->ucm, &impl->ucm.default_channel_map);
+ } else {
+ impl->use_ucm = false;
+ impl->profile_set = pa_alsa_profile_set_new(profile_set, &impl->ucm.default_channel_map);
+ }
+ if (impl->profile_set == NULL) {
+ res = -ENOTSUP;
+ goto error;
+ }
+
+ impl->profile_set->ignore_dB = impl->ignore_dB;
+
+ pa_alsa_profile_set_probe(impl->profile_set, impl->ucm.mixers,
+ device_id,
+ &impl->ucm.default_sample_spec,
+ impl->ucm.default_n_fragments,
+ impl->ucm.default_fragment_size_msec);
+
+ pa_alsa_init_proplist_card(NULL, impl->proplist, impl->card.index);
+ pa_proplist_sets(impl->proplist, PA_PROP_DEVICE_STRING, device_id);
+ pa_alsa_init_description(impl->proplist, NULL);
+
+ add_profiles(impl);
+ prune_singleton_availability_groups(impl->ports);
+
+ card->n_profiles = pa_dynarray_size(&impl->out.profiles);
+ card->profiles = impl->out.profiles.array.data;
+
+ card->n_ports = pa_dynarray_size(&impl->out.ports);
+ card->ports = impl->out.ports.array.data;
+
+ card->n_devices = pa_dynarray_size(&impl->out.devices);
+ card->devices = impl->out.devices.array.data;
+
+ pa_proplist_as_dict(impl->proplist, &card->props);
+
+ init_jacks(impl);
+
+ if (!impl->auto_profile && profile == NULL)
+ profile = "off";
+
+ 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();
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+void acp_card_add_listener(struct acp_card *card,
+ const struct acp_card_events *events, void *user_data)
+{
+ pa_card *impl = (pa_card *)card;
+ impl->events = events;
+ impl->user_data = user_data;
+}
+
+void acp_card_destroy(struct acp_card *card)
+{
+ pa_card *impl = (pa_card *)card;
+ if (impl->profiles)
+ pa_hashmap_free(impl->profiles);
+ if (impl->ports)
+ pa_hashmap_free(impl->ports);
+ pa_dynarray_clear(&impl->out.devices);
+ pa_dynarray_clear(&impl->out.profiles);
+ pa_dynarray_clear(&impl->out.ports);
+ if (impl->ucm.mixers)
+ pa_hashmap_free(impl->ucm.mixers);
+ if (impl->jacks)
+ pa_hashmap_free(impl->jacks);
+ if (impl->profile_set)
+ pa_alsa_profile_set_free(impl->profile_set);
+ pa_alsa_ucm_free(&impl->ucm);
+ pa_proplist_free(impl->proplist);
+ pa_alsa_refcnt_dec();
+ free(impl);
+}
+
+int acp_card_poll_descriptors_count(struct acp_card *card)
+{
+ pa_card *impl = (pa_card *)card;
+ void *state;
+ pa_alsa_mixer *pm;
+ int n, count = 0;
+
+ PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
+ if (!pm->used_for_poll)
+ continue;
+ n = snd_mixer_poll_descriptors_count(pm->mixer_handle);
+ if (n < 0)
+ return n;
+ count += n;
+ }
+ return count;
+}
+
+int acp_card_poll_descriptors(struct acp_card *card, struct pollfd *pfds, unsigned int space)
+{
+ pa_card *impl = (pa_card *)card;
+ void *state;
+ pa_alsa_mixer *pm;
+ int n, count = 0;
+
+ PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
+ if (!pm->used_for_poll)
+ continue;
+
+ n = snd_mixer_poll_descriptors(pm->mixer_handle, pfds, space);
+ if (n < 0)
+ return n;
+ if (space >= (unsigned int) n) {
+ count += n;
+ space -= n;
+ pfds += n;
+ } else
+ space = 0;
+ }
+ return count;
+}
+
+int acp_card_poll_descriptors_revents(struct acp_card *card, struct pollfd *pfds,
+ unsigned int nfds, unsigned short *revents)
+{
+ unsigned int idx;
+ unsigned short res;
+ if (nfds == 0)
+ return -EINVAL;
+ res = 0;
+ for (idx = 0; idx < nfds; idx++, pfds++)
+ res |= pfds->revents & (POLLIN|POLLERR|POLLNVAL);
+ *revents = res;
+ return 0;
+}
+
+int acp_card_handle_events(struct acp_card *card)
+{
+ pa_card *impl = (pa_card *)card;
+ void *state;
+ pa_alsa_mixer *pm;
+ int n, count = 0;
+
+ PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
+ if (!pm->used_for_poll)
+ continue;
+
+ n = snd_mixer_handle_events(pm->mixer_handle);
+ if (n < 0)
+ return n;
+ count += n;
+ }
+ return count;
+}
+
+static void sync_mixer(pa_alsa_device *d, pa_device_port *port)
+{
+ pa_alsa_setting *setting = NULL;
+
+ if (!d->mixer_path)
+ return;
+
+ /* port may be NULL, because if we use a synthesized mixer path, then the
+ * sink has no ports. */
+ if (port && !d->ucm_context) {
+ pa_alsa_port_data *data;
+ data = PA_DEVICE_PORT_DATA(port);
+ setting = data->setting;
+ }
+
+ if (d->mixer_handle)
+ pa_alsa_path_select(d->mixer_path, setting, d->mixer_handle, d->muted);
+
+ if (d->set_mute)
+ d->set_mute(d, d->muted);
+ if (d->set_volume)
+ d->set_volume(d, &d->real_volume);
+}
+
+
+uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *name)
+{
+ uint32_t i;
+ uint32_t best, best2, best3;
+ struct acp_port **ports = dev->ports;
+
+ best = best2 = best3 = ACP_INVALID_INDEX;
+
+ for (i = 0; i < dev->n_ports; i++) {
+ struct acp_port *p = ports[i];
+
+ if (name) {
+ if (spa_streq(name, p->name))
+ best = i;
+ } else if (p->available == ACP_AVAILABLE_YES) {
+ if (best == ACP_INVALID_INDEX || p->priority > ports[best]->priority)
+ best = i;
+ } else if (p->available != ACP_AVAILABLE_NO) {
+ if (best2 == ACP_INVALID_INDEX || p->priority > ports[best2]->priority)
+ best2 = i;
+ } else {
+ if (best3 == ACP_INVALID_INDEX || p->priority > ports[best3]->priority)
+ best3 = i;
+ }
+ }
+ if (best == ACP_INVALID_INDEX)
+ best = best2;
+ if (best == ACP_INVALID_INDEX)
+ best = best3;
+ if (best == ACP_INVALID_INDEX)
+ best = 0;
+ if (best < dev->n_ports)
+ return ports[best]->index;
+ else
+ return ACP_INVALID_INDEX;
+}
+
+int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ pa_card *impl = d->card;
+ pa_device_port *p, *old = d->active_port;
+ int res;
+
+ if (port_index >= impl->card.n_ports)
+ return -EINVAL;
+
+ p = (pa_device_port*)impl->card.ports[port_index];
+ if (!pa_hashmap_get(d->ports, p->name))
+ return -EINVAL;
+
+ p->port.flags = ACP_PORT_ACTIVE | flags;
+ if (p == old)
+ return 0;
+ if (old)
+ old->port.flags &= ~(ACP_PORT_ACTIVE | ACP_PORT_SAVE);
+ d->active_port = p;
+
+ if (impl->use_ucm) {
+ pa_alsa_ucm_port_data *data;
+
+ data = PA_DEVICE_PORT_DATA(p);
+ d->mixer_path = data->path;
+ mixer_volume_init(impl, d);
+
+ sync_mixer(d, p);
+ res = pa_alsa_ucm_set_port(d->ucm_context, p,
+ dev->direction == ACP_DIRECTION_PLAYBACK);
+ } else {
+ pa_alsa_port_data *data;
+
+ data = PA_DEVICE_PORT_DATA(p);
+ d->mixer_path = data->path;
+ mixer_volume_init(impl, d);
+
+ sync_mixer(d, p);
+ res = 0;
+#if 0
+ if (data->suspend_when_unavailable && p->available == PA_AVAILABLE_NO)
+ pa_sink_suspend(s, true, PA_SUSPEND_UNAVAILABLE);
+ else
+ pa_sink_suspend(s, false, PA_SUSPEND_UNAVAILABLE);
+#endif
+ }
+ if (impl->events && impl->events->port_changed)
+ impl->events->port_changed(impl->user_data,
+ old ? old->port.index : 0, p->port.index);
+ return res;
+}
+
+int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ pa_card *impl = d->card;
+ uint32_t i;
+ pa_cvolume v, old_volume;
+
+ if (n_volume == 0)
+ return -EINVAL;
+
+ old_volume = d->real_volume;
+
+ v.channels = d->mapping->channel_map.channels;
+ for (i = 0; i < v.channels; i++)
+ v.values[i] = pa_sw_volume_from_linear(volume[i % n_volume]);
+
+ pa_log_info("Set %s volume: min:%d max:%d",
+ d->set_volume ? "hardware" : "software",
+ pa_cvolume_min(&v), pa_cvolume_max(&v));
+
+ for (i = 0; i < v.channels; i++)
+ pa_log_debug(" %d: %d", i, v.values[i]);
+
+ if (d->set_volume) {
+ d->set_volume(d, &v);
+ } else {
+ d->real_volume = v;
+ d->soft_volume = v;
+ }
+ if (!pa_cvolume_equal(&d->real_volume, &old_volume))
+ if (impl->events && impl->events->volume_changed)
+ impl->events->volume_changed(impl->user_data, dev);
+ return 0;
+}
+
+static int get_volume(pa_cvolume *v, float *volume, uint32_t n_volume)
+{
+ uint32_t i;
+ if (v->channels == 0)
+ return -EIO;
+ for (i = 0; i < n_volume; i++)
+ volume[i] = pa_sw_volume_to_linear(v->values[i % v->channels]);
+ return 0;
+}
+
+int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ return get_volume(&d->soft_volume, volume, n_volume);
+}
+
+int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ return get_volume(&d->real_volume, volume, n_volume);
+}
+
+int acp_device_set_mute(struct acp_device *dev, bool mute)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ pa_card *impl = d->card;
+ bool old_muted = d->muted;
+
+ if (old_muted == mute)
+ return 0;
+
+ pa_log_info("Set %s mute: %d", d->set_mute ? "hardware" : "software", mute);
+
+ if (d->set_mute) {
+ d->set_mute(d, mute);
+ } else {
+ d->muted = mute;
+ }
+ if (old_muted != mute)
+ if (impl->events && impl->events->mute_changed)
+ impl->events->mute_changed(impl->user_data, dev);
+
+ return 0;
+}
+
+int acp_device_get_mute(struct acp_device *dev, bool *mute)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ *mute = d->muted;
+ return 0;
+}
+
+void acp_set_log_func(acp_log_func func, void *data)
+{
+ _acp_log_func = func;
+ _acp_log_data = data;
+}
+void acp_set_log_level(int level)
+{
+ _acp_log_level = level;
+}
diff --git a/spa/plugins/alsa/acp/acp.h b/spa/plugins/alsa/acp/acp.h
new file mode 100644
index 0000000..3fed6f6
--- /dev/null
+++ b/spa/plugins/alsa/acp/acp.h
@@ -0,0 +1,308 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef ACP_H
+#define ACP_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <poll.h>
+
+#ifdef __GNUC__
+#define ACP_PRINTF_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1)))
+#else
+#define ACP_PRINTF_FUNC(fmt, arg1)
+#endif
+
+#define ACP_INVALID_INDEX ((uint32_t)-1)
+#define ACP_MAX_CHANNELS 64
+
+struct acp_dict_item {
+ const char *key;
+ const char *value;
+};
+#define ACP_DICT_ITEM_INIT(key,value) ((struct acp_dict_item) { (key), (value) })
+
+struct acp_dict {
+ uint32_t flags;
+ uint32_t n_items;
+ const struct acp_dict_item *items;
+};
+
+enum acp_channel {
+ ACP_CHANNEL_UNKNOWN, /**< unspecified */
+ ACP_CHANNEL_NA, /**< N/A, silent */
+
+ ACP_CHANNEL_MONO, /**< mono stream */
+
+ ACP_CHANNEL_FL, /**< front left */
+ ACP_CHANNEL_FR, /**< front right */
+ ACP_CHANNEL_FC, /**< front center */
+ ACP_CHANNEL_LFE, /**< LFE */
+ ACP_CHANNEL_SL, /**< side left */
+ ACP_CHANNEL_SR, /**< side right */
+ ACP_CHANNEL_FLC, /**< front left center */
+ ACP_CHANNEL_FRC, /**< front right center */
+ ACP_CHANNEL_RC, /**< rear center */
+ ACP_CHANNEL_RL, /**< rear left */
+ ACP_CHANNEL_RR, /**< rear right */
+ ACP_CHANNEL_TC, /**< top center */
+ ACP_CHANNEL_TFL, /**< top front left */
+ ACP_CHANNEL_TFC, /**< top front center */
+ ACP_CHANNEL_TFR, /**< top front right */
+ ACP_CHANNEL_TRL, /**< top rear left */
+ ACP_CHANNEL_TRC, /**< top rear center */
+ ACP_CHANNEL_TRR, /**< top rear right */
+ ACP_CHANNEL_RLC, /**< rear left center */
+ ACP_CHANNEL_RRC, /**< rear right center */
+ ACP_CHANNEL_FLW, /**< front left wide */
+ ACP_CHANNEL_FRW, /**< front right wide */
+ ACP_CHANNEL_LFE2, /**< LFE 2 */
+ ACP_CHANNEL_FLH, /**< front left high */
+ ACP_CHANNEL_FCH, /**< front center high */
+ ACP_CHANNEL_FRH, /**< front right high */
+ ACP_CHANNEL_TFLC, /**< top front left center */
+ ACP_CHANNEL_TFRC, /**< top front right center */
+ ACP_CHANNEL_TSL, /**< top side left */
+ ACP_CHANNEL_TSR, /**< top side right */
+ ACP_CHANNEL_LLFE, /**< left LFE */
+ ACP_CHANNEL_RLFE, /**< right LFE */
+ ACP_CHANNEL_BC, /**< bottom center */
+ ACP_CHANNEL_BLC, /**< bottom left center */
+ ACP_CHANNEL_BRC, /**< bottom right center */
+
+ ACP_CHANNEL_START_Aux = 0x1000,
+ ACP_CHANNEL_LAST_Aux = 0x1fff,
+
+ ACP_CHANNEL_START_Custom = 0x10000,
+};
+
+char *acp_channel_str(char *buf, size_t len, enum acp_channel ch);
+
+struct acp_format {
+ uint32_t flags;
+ uint32_t format_mask;
+ uint32_t rate_mask;
+ uint32_t channels;
+ uint32_t map[ACP_MAX_CHANNELS];
+};
+
+#define ACP_DICT_INIT(items,n_items) ((struct acp_dict) { 0, (n_items), (items) })
+#define ACP_DICT_INIT_ARRAY(items) ((struct acp_dict) { 0, sizeof(items)/sizeof((items)[0]), (items) })
+
+#define acp_dict_for_each(item, dict) \
+ for ((item) = (dict)->items; \
+ (item) < &(dict)->items[(dict)->n_items]; \
+ (item)++)
+
+enum acp_direction {
+ ACP_DIRECTION_PLAYBACK = 1,
+ ACP_DIRECTION_CAPTURE = 2
+};
+
+const char *acp_direction_str(enum acp_direction direction);
+
+enum acp_available {
+ ACP_AVAILABLE_UNKNOWN = 0,
+ ACP_AVAILABLE_NO = 1,
+ ACP_AVAILABLE_YES = 2
+};
+
+const char *acp_available_str(enum acp_available status);
+
+#define ACP_KEY_PORT_TYPE "port.type" /**< a Port type, like "aux", "speaker", ... */
+#define ACP_KEY_PORT_AVAILABILITY_GROUP "port.availability-group"
+ /**< An identifier for the group of ports that share their availability status with
+ * each other. This is meant especially for handling cases where one 3.5 mm connector
+ * is used for headphones, headsets and microphones, and the hardware can only tell
+ * that something was plugged in but not what exactly. In this situation the ports for
+ * all those devices share their availability status, and ACP can't tell which
+ * one is actually plugged in, and some application may ask the user what was plugged
+ * in. Such applications should get a list of all card ports and compare their
+ * `available_group` fields. Ports that have the same group are those that need
+ * input from the user to determine which device was plugged in. The application should
+ * then activate the user-chosen port.
+ *
+ * May be NULL, in which case the port is not part of any availability group (which is
+ * the same as having a group with only one member).
+ *
+ * The group identifier must be treated as an opaque identifier. The string may look
+ * like an ALSA control name, but applications must not assume any such relationship.
+ * The group naming scheme can change without a warning.
+ */
+
+struct acp_device;
+
+struct acp_card_events {
+#define ACP_VERSION_CARD_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*props_changed) (void *data);
+
+ void (*profile_changed) (void *data, uint32_t old_index, uint32_t new_index);
+
+ void (*profile_available) (void *data, uint32_t index,
+ enum acp_available old, enum acp_available available);
+
+ void (*port_changed) (void *data, uint32_t old_index, uint32_t new_index);
+
+ void (*port_available) (void *data, uint32_t index,
+ enum acp_available old, enum acp_available available);
+
+ void (*volume_changed) (void *data, struct acp_device *dev);
+ void (*mute_changed) (void *data, struct acp_device *dev);
+};
+
+struct acp_port {
+ uint32_t index; /**< unique index for this port */
+#define ACP_PORT_ACTIVE (1<<0)
+#define ACP_PORT_SAVE (1<<1) /* if the port needs saving */
+ uint32_t flags; /**< extra port flags */
+
+ const char *name; /**< Name of this port */
+ const char *description; /**< Description of this port */
+ uint32_t priority; /**< The higher this value is, the more useful this port is as a default. */
+ enum acp_direction direction;
+ enum acp_available available; /**< A flags (see #acp_port_available), indicating availability status of this port. */
+ struct acp_dict props; /**< extra port properties */
+
+ uint32_t n_profiles; /**< number of elements in profiles array */
+ struct acp_card_profile **profiles; /**< array of profiles for this port */
+
+ uint32_t n_devices; /**< number of elements in devices array */
+ struct acp_device **devices; /**< array of devices */
+};
+
+struct acp_device {
+ uint32_t index;
+#define ACP_DEVICE_ACTIVE (1<<0)
+#define ACP_DEVICE_HW_VOLUME (1<<1)
+#define ACP_DEVICE_HW_MUTE (1<<2)
+#define ACP_DEVICE_UCM_DEVICE (1<<3)
+#define ACP_DEVICE_IEC958 (1<<4)
+ uint32_t flags;
+
+ const char *name;
+ const char *description;
+ uint32_t priority;
+ enum acp_direction direction;
+ struct acp_dict props;
+
+ const char **device_strings;
+ struct acp_format format;
+
+ float base_volume;
+ float volume_step;
+
+ uint32_t n_ports;
+ struct acp_port **ports;
+
+ int64_t latency_ns;
+ uint32_t codecs[32];
+ uint32_t n_codecs;
+};
+
+struct acp_card_profile {
+ uint32_t index;
+#define ACP_PROFILE_ACTIVE (1<<0)
+#define ACP_PROFILE_OFF (1<<1) /* the Off profile */
+#define ACP_PROFILE_SAVE (1<<2) /* if the profile needs saving */
+#define ACP_PROFILE_PRO (1<<3) /* the Pro profile */
+ uint32_t flags;
+
+ const char *name;
+ const char *description;
+ uint32_t priority;
+ enum acp_available available;
+ struct acp_dict props;
+
+ uint32_t n_devices;
+ struct acp_device **devices;
+};
+
+struct acp_card {
+ uint32_t index;
+ uint32_t flags;
+
+ struct acp_dict props;
+
+ uint32_t n_profiles;
+ uint32_t active_profile_index;
+ struct acp_card_profile **profiles;
+
+ uint32_t n_devices;
+ struct acp_device **devices;
+
+ uint32_t n_ports;
+ struct acp_port **ports;
+ uint32_t preferred_input_port_index;
+ uint32_t preferred_output_port_index;
+};
+
+struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props);
+
+void acp_card_add_listener(struct acp_card *card,
+ const struct acp_card_events *events, void *user_data);
+
+void acp_card_destroy(struct acp_card *card);
+
+int acp_card_poll_descriptors_count(struct acp_card *card);
+int acp_card_poll_descriptors(struct acp_card *card, struct pollfd *pfds, unsigned int space);
+int acp_card_poll_descriptors_revents(struct acp_card *card, struct pollfd *pfds,
+ unsigned int nfds, unsigned short *revents);
+int acp_card_handle_events(struct acp_card *card);
+
+uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name);
+int acp_card_set_profile(struct acp_card *card, uint32_t profile_index, uint32_t flags);
+
+uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *name);
+int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags);
+
+int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume);
+int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume);
+int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume);
+int acp_device_set_mute(struct acp_device *dev, bool mute);
+int acp_device_get_mute(struct acp_device *dev, bool *mute);
+
+typedef void (*acp_log_func) (void *data,
+ int level, const char *file, int line, const char *func,
+ const char *fmt, va_list arg) ACP_PRINTF_FUNC(6,0);
+
+void acp_set_log_func(acp_log_func, void *data);
+void acp_set_log_level(int level);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACP_H */
diff --git a/spa/plugins/alsa/acp/alsa-mixer.c b/spa/plugins/alsa/acp/alsa-mixer.c
new file mode 100644
index 0000000..86425fd
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-mixer.c
@@ -0,0 +1,5398 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2009 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <alsa/asoundlib.h>
+#include <math.h>
+
+#include <valgrind/memcheck.h>
+
+#include "conf-parser.h"
+#include "alsa-mixer.h"
+#include "alsa-util.h"
+
+static int setting_select(pa_alsa_setting *s, snd_mixer_t *m);
+
+struct description_map {
+ const char *key;
+ const char *description;
+};
+
+struct description2_map {
+ const char *key;
+ const char *description;
+ pa_device_port_type_t type;
+};
+
+char *pa_alsa_mixer_id_to_string(char *dst, size_t dst_len, pa_alsa_mixer_id *id) {
+ if (id->index > 0) {
+ snprintf(dst, dst_len, "'%s',%d", id->name, id->index);
+ } else {
+ snprintf(dst, dst_len, "'%s'", id->name);
+ }
+ return dst;
+}
+
+static int alsa_id_decode(const char *src, char *name, int *index) {
+ char *idx, c;
+ int i;
+
+ *index = 0;
+ c = src[0];
+ /* Strip quotes in entries such as 'Speaker',1 or "Speaker",1 */
+ if (c == '\'' || c == '"') {
+ strcpy(name, src + 1);
+ for (i = 0; name[i] != '\0' && name[i] != c; i++);
+ idx = NULL;
+ if (name[i]) {
+ name[i] = '\0';
+ idx = strchr(name + i + 1, ',');
+ }
+ } else {
+ strcpy(name, src);
+ idx = strchr(name, ',');
+ }
+ if (idx == NULL)
+ return 0;
+ *idx = '\0';
+ idx++;
+ if (*idx < '0' || *idx > '9') {
+ pa_log("Element %s: index value is invalid", src);
+ return 1;
+ }
+ *index = atoi(idx);
+ return 0;
+}
+
+pa_alsa_jack *pa_alsa_jack_new(pa_alsa_path *path, const char *mixer_device_name, const char *name, int index) {
+ pa_alsa_jack *jack;
+
+ pa_assert(name);
+
+ jack = pa_xnew0(pa_alsa_jack, 1);
+ jack->path = path;
+ jack->mixer_device_name = pa_xstrdup(mixer_device_name);
+ jack->name = pa_xstrdup(name);
+ jack->alsa_id.name = pa_sprintf_malloc("%s Jack", name);
+ jack->alsa_id.index = index;
+ jack->state_unplugged = PA_AVAILABLE_NO;
+ jack->state_plugged = PA_AVAILABLE_YES;
+ jack->ucm_devices = pa_dynarray_new(NULL);
+ jack->ucm_hw_mute_devices = pa_dynarray_new(NULL);
+
+ return jack;
+}
+
+void pa_alsa_jack_free(pa_alsa_jack *jack) {
+ pa_assert(jack);
+
+ pa_dynarray_free(jack->ucm_hw_mute_devices);
+ pa_dynarray_free(jack->ucm_devices);
+
+ pa_xfree(jack->alsa_id.name);
+ pa_xfree(jack->name);
+ pa_xfree(jack->mixer_device_name);
+ pa_xfree(jack);
+}
+
+void pa_alsa_jack_set_has_control(pa_alsa_jack *jack, bool has_control) {
+ pa_alsa_ucm_device *device;
+ unsigned idx;
+
+ pa_assert(jack);
+
+ if (has_control == jack->has_control)
+ return;
+
+ jack->has_control = has_control;
+
+ PA_DYNARRAY_FOREACH(device, jack->ucm_hw_mute_devices, idx)
+ pa_alsa_ucm_device_update_available(device);
+
+ PA_DYNARRAY_FOREACH(device, jack->ucm_devices, idx)
+ pa_alsa_ucm_device_update_available(device);
+}
+
+void pa_alsa_jack_set_plugged_in(pa_alsa_jack *jack, bool plugged_in) {
+ pa_alsa_ucm_device *device;
+ unsigned idx;
+
+ pa_assert(jack);
+
+ if (plugged_in == jack->plugged_in)
+ return;
+
+ jack->plugged_in = plugged_in;
+
+ /* XXX: If this is a headphone jack that mutes speakers when plugged in,
+ * and the headphones get unplugged, then the headphone device must be set
+ * to unavailable and the speaker device must be set to unknown. So far so
+ * good. But there's an ugly detail: we must first set the availability of
+ * the speakers and then the headphones. We shouldn't need to care about
+ * the order, but we have to, because module-switch-on-port-available gets
+ * separate events for the two devices, and the intermediate state between
+ * the two events is such that the second event doesn't trigger the desired
+ * port switch, if the event order is "wrong".
+ *
+ * These are the transitions when the event order is "right":
+ *
+ * speakers: 1) unavailable -> 2) unknown -> 3) unknown
+ * headphones: 1) available -> 2) available -> 3) unavailable
+ *
+ * In the 2 -> 3 transition, headphones become unavailable, and
+ * module-switch-on-port-available sees that speakers can be used, so the
+ * port gets changed as it should.
+ *
+ * These are the transitions when the event order is "wrong":
+ *
+ * speakers: 1) unavailable -> 2) unavailable -> 3) unknown
+ * headphones: 1) available -> 2) unavailable -> 3) unavailable
+ *
+ * In the 1 -> 2 transition, headphones become unavailable, and there are
+ * no available ports to use, so no port change happens. In the 2 -> 3
+ * transition, speaker availability becomes unknown, but that's not
+ * a strong enough signal for module-switch-on-port-available, so it still
+ * doesn't do the port switch.
+ *
+ * We should somehow merge the two events so that
+ * module-switch-on-port-available would handle both transitions in one go.
+ * If module-switch-on-port-available used a defer event to delay
+ * the port availability processing, that would probably do the trick. */
+
+ PA_DYNARRAY_FOREACH(device, jack->ucm_hw_mute_devices, idx)
+ pa_alsa_ucm_device_update_available(device);
+
+ PA_DYNARRAY_FOREACH(device, jack->ucm_devices, idx)
+ pa_alsa_ucm_device_update_available(device);
+}
+
+void pa_alsa_jack_add_ucm_device(pa_alsa_jack *jack, pa_alsa_ucm_device *device) {
+ pa_alsa_ucm_device *idevice;
+ unsigned idx, prio, iprio;
+
+ pa_assert(jack);
+ pa_assert(device);
+
+ /* store the ucm device with the sequence of priority from low to high. this
+ * could guarantee when the jack state is changed, the device with highest
+ * priority will send to the module-switch-on-port-available last */
+ prio = device->playback_priority ? device->playback_priority : device->capture_priority;
+
+ PA_DYNARRAY_FOREACH(idevice, jack->ucm_devices, idx) {
+ iprio = idevice->playback_priority ? idevice->playback_priority : idevice->capture_priority;
+ if (iprio > prio)
+ break;
+ }
+ pa_dynarray_insert_by_index(jack->ucm_devices, device, idx);
+}
+
+void pa_alsa_jack_add_ucm_hw_mute_device(pa_alsa_jack *jack, pa_alsa_ucm_device *device) {
+ pa_assert(jack);
+ pa_assert(device);
+
+ pa_dynarray_append(jack->ucm_hw_mute_devices, device);
+}
+
+static const char *lookup_description(const char *key, const struct description_map dm[], unsigned n) {
+ unsigned i;
+
+ if (!key)
+ return NULL;
+
+ for (i = 0; i < n; i++)
+ if (pa_streq(dm[i].key, key))
+ return _(dm[i].description);
+
+ return NULL;
+}
+
+static const struct description2_map *lookup_description2(const char *key, const struct description2_map dm[], unsigned n) {
+ unsigned i;
+
+ if (!key)
+ return NULL;
+
+ for (i = 0; i < n; i++)
+ if (pa_streq(dm[i].key, key))
+ return &dm[i];
+
+ return NULL;
+}
+
+void pa_alsa_mixer_use_for_poll(pa_hashmap *mixers, snd_mixer_t *mixer_handle)
+{
+ pa_alsa_mixer *pm;
+ void *state;
+
+ PA_HASHMAP_FOREACH(pm, mixers, state) {
+ if (pm->mixer_handle == mixer_handle) {
+ pm->used_for_probe_only = false;
+ pm->used_for_poll = true;
+ }
+ }
+}
+
+#if 0
+struct pa_alsa_fdlist {
+ unsigned num_fds;
+ struct pollfd *fds;
+ /* This is a temporary buffer used to avoid lots of mallocs */
+ struct pollfd *work_fds;
+
+ snd_mixer_t *mixer;
+ snd_hctl_t *hctl;
+
+ pa_mainloop_api *m;
+ pa_defer_event *defer;
+ pa_io_event **ios;
+
+ bool polled;
+
+ void (*cb)(void *userdata);
+ void *userdata;
+};
+
+static void io_cb(pa_mainloop_api *a, pa_io_event *e, int fd, pa_io_event_flags_t events, void *userdata) {
+
+ struct pa_alsa_fdlist *fdl = userdata;
+ int err;
+ unsigned i;
+ unsigned short revents;
+
+ pa_assert(a);
+ pa_assert(fdl);
+ pa_assert(fdl->mixer || fdl->hctl);
+ pa_assert(fdl->fds);
+ pa_assert(fdl->work_fds);
+
+ if (fdl->polled)
+ return;
+
+ fdl->polled = true;
+
+ memcpy(fdl->work_fds, fdl->fds, sizeof(struct pollfd) * fdl->num_fds);
+
+ for (i = 0; i < fdl->num_fds; i++) {
+ if (e == fdl->ios[i]) {
+ if (events & PA_IO_EVENT_INPUT)
+ fdl->work_fds[i].revents |= POLLIN;
+ if (events & PA_IO_EVENT_OUTPUT)
+ fdl->work_fds[i].revents |= POLLOUT;
+ if (events & PA_IO_EVENT_ERROR)
+ fdl->work_fds[i].revents |= POLLERR;
+ if (events & PA_IO_EVENT_HANGUP)
+ fdl->work_fds[i].revents |= POLLHUP;
+ break;
+ }
+ }
+
+ pa_assert(i != fdl->num_fds);
+
+ if (fdl->hctl)
+ err = snd_hctl_poll_descriptors_revents(fdl->hctl, fdl->work_fds, fdl->num_fds, &revents);
+ else
+ err = snd_mixer_poll_descriptors_revents(fdl->mixer, fdl->work_fds, fdl->num_fds, &revents);
+
+ if (err < 0) {
+ pa_log_error("Unable to get poll revent: %s", pa_alsa_strerror(err));
+ return;
+ }
+
+ a->defer_enable(fdl->defer, 1);
+
+ if (revents) {
+ if (fdl->hctl)
+ snd_hctl_handle_events(fdl->hctl);
+ else
+ snd_mixer_handle_events(fdl->mixer);
+ }
+}
+
+static void defer_cb(pa_mainloop_api *a, pa_defer_event *e, void *userdata) {
+ struct pa_alsa_fdlist *fdl = userdata;
+ unsigned num_fds, i;
+ int err, n;
+ struct pollfd *temp;
+
+ pa_assert(a);
+ pa_assert(fdl);
+ pa_assert(fdl->mixer || fdl->hctl);
+
+ a->defer_enable(fdl->defer, 0);
+
+ if (fdl->hctl)
+ n = snd_hctl_poll_descriptors_count(fdl->hctl);
+ else
+ n = snd_mixer_poll_descriptors_count(fdl->mixer);
+
+ if (n < 0) {
+ pa_log("snd_mixer_poll_descriptors_count() failed: %s", pa_alsa_strerror(n));
+ return;
+ }
+ else if (n == 0) {
+ pa_log_warn("Mixer has no poll descriptors. Please control mixer from PulseAudio only.");
+ return;
+ }
+ num_fds = (unsigned) n;
+
+ if (num_fds != fdl->num_fds) {
+ if (fdl->fds)
+ pa_xfree(fdl->fds);
+ if (fdl->work_fds)
+ pa_xfree(fdl->work_fds);
+ fdl->fds = pa_xnew0(struct pollfd, num_fds);
+ fdl->work_fds = pa_xnew(struct pollfd, num_fds);
+ }
+
+ memset(fdl->work_fds, 0, sizeof(struct pollfd) * num_fds);
+
+ if (fdl->hctl)
+ err = snd_hctl_poll_descriptors(fdl->hctl, fdl->work_fds, num_fds);
+ else
+ err = snd_mixer_poll_descriptors(fdl->mixer, fdl->work_fds, num_fds);
+
+ if (err < 0) {
+ pa_log_error("Unable to get poll descriptors: %s", pa_alsa_strerror(err));
+ return;
+ }
+
+ fdl->polled = false;
+
+ if (memcmp(fdl->fds, fdl->work_fds, sizeof(struct pollfd) * num_fds) == 0)
+ return;
+
+ if (fdl->ios) {
+ for (i = 0; i < fdl->num_fds; i++)
+ a->io_free(fdl->ios[i]);
+
+ if (num_fds != fdl->num_fds) {
+ pa_xfree(fdl->ios);
+ fdl->ios = NULL;
+ }
+ }
+
+ if (!fdl->ios)
+ fdl->ios = pa_xnew(pa_io_event*, num_fds);
+
+ /* Swap pointers */
+ temp = fdl->work_fds;
+ fdl->work_fds = fdl->fds;
+ fdl->fds = temp;
+
+ fdl->num_fds = num_fds;
+
+ for (i = 0;i < num_fds;i++)
+ fdl->ios[i] = a->io_new(a, fdl->fds[i].fd,
+ ((fdl->fds[i].events & POLLIN) ? PA_IO_EVENT_INPUT : 0) |
+ ((fdl->fds[i].events & POLLOUT) ? PA_IO_EVENT_OUTPUT : 0),
+ io_cb, fdl);
+}
+
+struct pa_alsa_fdlist *pa_alsa_fdlist_new(void) {
+ struct pa_alsa_fdlist *fdl;
+
+ fdl = pa_xnew0(struct pa_alsa_fdlist, 1);
+
+ return fdl;
+}
+
+void pa_alsa_fdlist_free(struct pa_alsa_fdlist *fdl) {
+ pa_assert(fdl);
+
+ if (fdl->defer) {
+ pa_assert(fdl->m);
+ fdl->m->defer_free(fdl->defer);
+ }
+
+ if (fdl->ios) {
+ unsigned i;
+ pa_assert(fdl->m);
+ for (i = 0; i < fdl->num_fds; i++)
+ fdl->m->io_free(fdl->ios[i]);
+ pa_xfree(fdl->ios);
+ }
+
+ if (fdl->fds)
+ pa_xfree(fdl->fds);
+ if (fdl->work_fds)
+ pa_xfree(fdl->work_fds);
+
+ pa_xfree(fdl);
+}
+
+/* We can listen to either a snd_hctl_t or a snd_mixer_t, but not both */
+int pa_alsa_fdlist_set_handle(struct pa_alsa_fdlist *fdl, snd_mixer_t *mixer_handle, snd_hctl_t *hctl_handle, pa_mainloop_api *m) {
+ pa_assert(fdl);
+ pa_assert(hctl_handle || mixer_handle);
+ pa_assert(!(hctl_handle && mixer_handle));
+ pa_assert(m);
+ pa_assert(!fdl->m);
+
+ fdl->hctl = hctl_handle;
+ fdl->mixer = mixer_handle;
+ fdl->m = m;
+ fdl->defer = m->defer_new(m, defer_cb, fdl);
+
+ return 0;
+}
+
+struct pa_alsa_mixer_pdata {
+ pa_rtpoll *rtpoll;
+ pa_rtpoll_item *poll_item;
+ snd_mixer_t *mixer;
+};
+
+struct pa_alsa_mixer_pdata *pa_alsa_mixer_pdata_new(void) {
+ struct pa_alsa_mixer_pdata *pd;
+
+ pd = pa_xnew0(struct pa_alsa_mixer_pdata, 1);
+
+ return pd;
+}
+
+void pa_alsa_mixer_pdata_free(struct pa_alsa_mixer_pdata *pd) {
+ pa_assert(pd);
+
+ if (pd->poll_item) {
+ pa_rtpoll_item_free(pd->poll_item);
+ }
+
+ pa_xfree(pd);
+}
+
+static int rtpoll_work_cb(pa_rtpoll_item *i) {
+ struct pa_alsa_mixer_pdata *pd;
+ struct pollfd *p;
+ unsigned n_fds;
+ unsigned short revents = 0;
+ int err, ret = 0;
+
+ pd = pa_rtpoll_item_get_work_userdata(i);
+ pa_assert_fp(pd);
+ pa_assert_fp(i == pd->poll_item);
+
+ p = pa_rtpoll_item_get_pollfd(i, &n_fds);
+
+ if ((err = snd_mixer_poll_descriptors_revents(pd->mixer, p, n_fds, &revents)) < 0) {
+ pa_log_error("Unable to get poll revent: %s", pa_alsa_strerror(err));
+ ret = -1;
+ goto fail;
+ }
+
+ if (revents) {
+ if (revents & (POLLNVAL | POLLERR)) {
+ pa_log_debug("Device disconnected, stopping poll on mixer");
+ goto fail;
+ } else if (revents & POLLERR) {
+ /* This shouldn't happen. */
+ pa_log_error("Got a POLLERR (revents = %04x), stopping poll on mixer", revents);
+ goto fail;
+ }
+
+ err = snd_mixer_handle_events(pd->mixer);
+
+ if (PA_LIKELY(err >= 0)) {
+ pa_rtpoll_item_free(i);
+ pa_alsa_set_mixer_rtpoll(pd, pd->mixer, pd->rtpoll);
+ } else {
+ pa_log_error("Error handling mixer event: %s", pa_alsa_strerror(err));
+ ret = -1;
+ goto fail;
+ }
+ }
+
+ return ret;
+
+fail:
+ pa_rtpoll_item_free(i);
+
+ pd->poll_item = NULL;
+ pd->rtpoll = NULL;
+ pd->mixer = NULL;
+
+ return ret;
+}
+
+int pa_alsa_set_mixer_rtpoll(struct pa_alsa_mixer_pdata *pd, snd_mixer_t *mixer, pa_rtpoll *rtp) {
+ pa_rtpoll_item *i;
+ struct pollfd *p;
+ int err, n;
+
+ pa_assert(pd);
+ pa_assert(mixer);
+ pa_assert(rtp);
+
+ if ((n = snd_mixer_poll_descriptors_count(mixer)) < 0) {
+ pa_log("snd_mixer_poll_descriptors_count() failed: %s", pa_alsa_strerror(n));
+ return -1;
+ }
+ else if (n == 0) {
+ pa_log_warn("Mixer has no poll descriptors. Please control mixer from PulseAudio only.");
+ return 0;
+ }
+
+ i = pa_rtpoll_item_new(rtp, PA_RTPOLL_LATE, (unsigned) n);
+
+ p = pa_rtpoll_item_get_pollfd(i, NULL);
+
+ memset(p, 0, sizeof(struct pollfd) * n);
+
+ if ((err = snd_mixer_poll_descriptors(mixer, p, (unsigned) n)) < 0) {
+ pa_log_error("Unable to get poll descriptors: %s", pa_alsa_strerror(err));
+ pa_rtpoll_item_free(i);
+ return -1;
+ }
+
+ pd->rtpoll = rtp;
+ pd->poll_item = i;
+ pd->mixer = mixer;
+
+ pa_rtpoll_item_set_work_callback(i, rtpoll_work_cb, pd);
+
+ return 0;
+}
+#endif
+
+static const snd_mixer_selem_channel_id_t alsa_channel_ids[PA_CHANNEL_POSITION_MAX] = {
+ [PA_CHANNEL_POSITION_MONO] = SND_MIXER_SCHN_MONO, /* The ALSA name is just an alias! */
+
+ [PA_CHANNEL_POSITION_FRONT_CENTER] = SND_MIXER_SCHN_FRONT_CENTER,
+ [PA_CHANNEL_POSITION_FRONT_LEFT] = SND_MIXER_SCHN_FRONT_LEFT,
+ [PA_CHANNEL_POSITION_FRONT_RIGHT] = SND_MIXER_SCHN_FRONT_RIGHT,
+
+ [PA_CHANNEL_POSITION_REAR_CENTER] = SND_MIXER_SCHN_REAR_CENTER,
+ [PA_CHANNEL_POSITION_REAR_LEFT] = SND_MIXER_SCHN_REAR_LEFT,
+ [PA_CHANNEL_POSITION_REAR_RIGHT] = SND_MIXER_SCHN_REAR_RIGHT,
+
+ [PA_CHANNEL_POSITION_LFE] = SND_MIXER_SCHN_WOOFER,
+
+ [PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+
+ [PA_CHANNEL_POSITION_SIDE_LEFT] = SND_MIXER_SCHN_SIDE_LEFT,
+ [PA_CHANNEL_POSITION_SIDE_RIGHT] = SND_MIXER_SCHN_SIDE_RIGHT,
+
+ [PA_CHANNEL_POSITION_AUX0] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX1] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX2] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX3] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX4] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX5] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX6] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX7] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX8] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX9] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX10] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX11] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX12] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX13] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX14] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX15] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX16] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX17] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX18] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX19] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX20] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX21] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX22] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX23] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX24] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX25] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX26] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX27] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX28] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX29] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX30] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX31] = SND_MIXER_SCHN_UNKNOWN,
+
+ [PA_CHANNEL_POSITION_TOP_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+
+ [PA_CHANNEL_POSITION_TOP_FRONT_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_TOP_FRONT_LEFT] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_TOP_FRONT_RIGHT] = SND_MIXER_SCHN_UNKNOWN,
+
+ [PA_CHANNEL_POSITION_TOP_REAR_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_TOP_REAR_LEFT] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_TOP_REAR_RIGHT] = SND_MIXER_SCHN_UNKNOWN
+};
+
+static snd_mixer_selem_channel_id_t alsa_channel_positions[POSITION_MASK_CHANNELS] = {
+ SND_MIXER_SCHN_FRONT_LEFT,
+ SND_MIXER_SCHN_FRONT_RIGHT,
+ SND_MIXER_SCHN_REAR_LEFT,
+ SND_MIXER_SCHN_REAR_RIGHT,
+ SND_MIXER_SCHN_FRONT_CENTER,
+ SND_MIXER_SCHN_WOOFER,
+ SND_MIXER_SCHN_SIDE_LEFT,
+ SND_MIXER_SCHN_SIDE_RIGHT,
+#if POSITION_MASK_CHANNELS > 8
+#error "Extend alsa_channel_positions[] array (9+)"
+#endif
+};
+
+static void setting_free(pa_alsa_setting *s) {
+ pa_assert(s);
+
+ if (s->options)
+ pa_idxset_free(s->options, NULL);
+
+ pa_xfree(s->name);
+ pa_xfree(s->description);
+ pa_xfree(s);
+}
+
+static void option_free(pa_alsa_option *o) {
+ pa_assert(o);
+
+ pa_xfree(o->alsa_name);
+ pa_xfree(o->name);
+ pa_xfree(o->description);
+ pa_xfree(o);
+}
+
+static void decibel_fix_free(pa_alsa_decibel_fix *db_fix) {
+ pa_assert(db_fix);
+
+ pa_xfree(db_fix->name);
+ pa_xfree(db_fix->db_values);
+
+ pa_xfree(db_fix->key);
+ pa_xfree(db_fix);
+}
+
+static void element_free(pa_alsa_element *e) {
+ pa_alsa_option *o;
+ pa_assert(e);
+
+ while ((o = e->options)) {
+ PA_LLIST_REMOVE(pa_alsa_option, e->options, o);
+ option_free(o);
+ }
+
+ if (e->db_fix)
+ decibel_fix_free(e->db_fix);
+
+ pa_xfree(e->alsa_id.name);
+ pa_xfree(e);
+}
+
+void pa_alsa_path_free(pa_alsa_path *p) {
+ pa_alsa_jack *j;
+ pa_alsa_element *e;
+ pa_alsa_setting *s;
+
+ pa_assert(p);
+
+ while ((j = p->jacks)) {
+ PA_LLIST_REMOVE(pa_alsa_jack, p->jacks, j);
+ pa_alsa_jack_free(j);
+ }
+
+ while ((e = p->elements)) {
+ PA_LLIST_REMOVE(pa_alsa_element, p->elements, e);
+ element_free(e);
+ }
+
+ while ((s = p->settings)) {
+ PA_LLIST_REMOVE(pa_alsa_setting, p->settings, s);
+ setting_free(s);
+ }
+
+ pa_proplist_free(p->proplist);
+ pa_xfree(p->availability_group);
+ pa_xfree(p->name);
+ pa_xfree(p->description);
+ pa_xfree(p->description_key);
+ pa_xfree(p);
+}
+
+void pa_alsa_path_set_free(pa_alsa_path_set *ps) {
+ pa_assert(ps);
+
+ if (ps->paths)
+ pa_hashmap_free(ps->paths);
+
+ pa_xfree(ps);
+}
+
+int pa_alsa_path_set_is_empty(pa_alsa_path_set *ps) {
+ if (ps && !pa_hashmap_isempty(ps->paths))
+ return 0;
+ return 1;
+}
+
+static long to_alsa_dB(pa_volume_t v) {
+ return lround(pa_sw_volume_to_dB(v) * 100.0);
+}
+
+static pa_volume_t from_alsa_dB(long v) {
+ return pa_sw_volume_from_dB((double) v / 100.0);
+}
+
+static long to_alsa_volume(pa_volume_t v, long min, long max) {
+ long w;
+
+ w = (long) round(((double) v * (double) (max - min)) / PA_VOLUME_NORM) + min;
+ return PA_CLAMP_UNLIKELY(w, min, max);
+}
+
+static pa_volume_t from_alsa_volume(long v, long min, long max) {
+ return (pa_volume_t) round(((double) (v - min) * PA_VOLUME_NORM) / (double) (max - min));
+}
+
+#define SELEM_INIT(sid, aid) \
+ do { \
+ snd_mixer_selem_id_alloca(&(sid)); \
+ snd_mixer_selem_id_set_name((sid), (aid)->name); \
+ snd_mixer_selem_id_set_index((sid), (aid)->index); \
+ } while(false)
+
+static int element_get_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+ snd_mixer_selem_channel_id_t c;
+ pa_channel_position_mask_t mask = 0;
+ char buf[64];
+ unsigned k;
+
+ pa_assert(m);
+ pa_assert(e);
+ pa_assert(cm);
+ pa_assert(v);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ pa_cvolume_mute(v, cm->channels);
+
+ /* We take the highest volume of all channels that match */
+
+ for (c = 0; c <= SND_MIXER_SCHN_LAST; c++) {
+ int r;
+ pa_volume_t f;
+
+ if (e->has_dB) {
+ long value = 0;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_has_playback_channel(me, c)) {
+ if (e->db_fix) {
+ if ((r = snd_mixer_selem_get_playback_volume(me, c, &value)) >= 0) {
+ /* If the channel volume is outside the limits set
+ * by the dB fix, we clamp the hw volume to be
+ * within the limits. */
+ if (value < e->db_fix->min_step) {
+ value = e->db_fix->min_step;
+ snd_mixer_selem_set_playback_volume(me, c, value);
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Playback volume for element %s channel %i was below the dB fix limit. "
+ "Volume reset to %0.2f dB.", buf, c,
+ e->db_fix->db_values[value - e->db_fix->min_step] / 100.0);
+ } else if (value > e->db_fix->max_step) {
+ value = e->db_fix->max_step;
+ snd_mixer_selem_set_playback_volume(me, c, value);
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Playback volume for element %s channel %i was over the dB fix limit. "
+ "Volume reset to %0.2f dB.", buf, c,
+ e->db_fix->db_values[value - e->db_fix->min_step] / 100.0);
+ }
+
+ /* Volume step -> dB value conversion. */
+ value = e->db_fix->db_values[value - e->db_fix->min_step];
+ }
+ } else
+ r = snd_mixer_selem_get_playback_dB(me, c, &value);
+ } else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c)) {
+ if (e->db_fix) {
+ if ((r = snd_mixer_selem_get_capture_volume(me, c, &value)) >= 0) {
+ /* If the channel volume is outside the limits set
+ * by the dB fix, we clamp the hw volume to be
+ * within the limits. */
+ if (value < e->db_fix->min_step) {
+ value = e->db_fix->min_step;
+ snd_mixer_selem_set_capture_volume(me, c, value);
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Capture volume for element %s channel %i was below the dB fix limit. "
+ "Volume reset to %0.2f dB.", buf, c,
+ e->db_fix->db_values[value - e->db_fix->min_step] / 100.0);
+ } else if (value > e->db_fix->max_step) {
+ value = e->db_fix->max_step;
+ snd_mixer_selem_set_capture_volume(me, c, value);
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Capture volume for element %s channel %i was over the dB fix limit. "
+ "Volume reset to %0.2f dB.", buf, c,
+ e->db_fix->db_values[value - e->db_fix->min_step] / 100.0);
+ }
+
+ /* Volume step -> dB value conversion. */
+ value = e->db_fix->db_values[value - e->db_fix->min_step];
+ }
+ } else
+ r = snd_mixer_selem_get_capture_dB(me, c, &value);
+ } else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ VALGRIND_MAKE_MEM_DEFINED(&value, sizeof(value));
+
+ f = from_alsa_dB(value);
+
+ } else {
+ long value = 0;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_has_playback_channel(me, c))
+ r = snd_mixer_selem_get_playback_volume(me, c, &value);
+ else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c))
+ r = snd_mixer_selem_get_capture_volume(me, c, &value);
+ else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ f = from_alsa_volume(value, e->min_volume, e->max_volume);
+ }
+
+ for (k = 0; k < cm->channels; k++)
+ if (e->masks[c][e->n_channels-1] & PA_CHANNEL_POSITION_MASK(cm->map[k]))
+ if (v->values[k] < f)
+ v->values[k] = f;
+
+ mask |= e->masks[c][e->n_channels-1];
+ }
+
+ for (k = 0; k < cm->channels; k++)
+ if (!(mask & PA_CHANNEL_POSITION_MASK(cm->map[k])))
+ v->values[k] = PA_VOLUME_NORM;
+
+ return 0;
+}
+
+int pa_alsa_path_get_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v) {
+ pa_alsa_element *e;
+
+ pa_assert(m);
+ pa_assert(p);
+ pa_assert(cm);
+ pa_assert(v);
+
+ if (!p->has_volume)
+ return -1;
+
+ pa_cvolume_reset(v, cm->channels);
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ pa_cvolume ev;
+
+ if (e->volume_use != PA_ALSA_VOLUME_MERGE)
+ continue;
+
+ pa_assert(!p->has_dB || e->has_dB);
+
+ if (element_get_volume(e, m, cm, &ev) < 0)
+ return -1;
+
+ /* If we have no dB information all we can do is take the first element and leave */
+ if (!p->has_dB) {
+ *v = ev;
+ return 0;
+ }
+
+ pa_sw_cvolume_multiply(v, v, &ev);
+ }
+
+ return 0;
+}
+
+static int element_get_switch(pa_alsa_element *e, snd_mixer_t *m, bool *b) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+ snd_mixer_selem_channel_id_t c;
+ char buf[64];
+
+ pa_assert(m);
+ pa_assert(e);
+ pa_assert(b);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ /* We return muted if at least one channel is muted */
+
+ for (c = 0; c <= SND_MIXER_SCHN_LAST; c++) {
+ int r;
+ int value = 0;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_has_playback_channel(me, c))
+ r = snd_mixer_selem_get_playback_switch(me, c, &value);
+ else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c))
+ r = snd_mixer_selem_get_capture_switch(me, c, &value);
+ else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ if (!value) {
+ *b = false;
+ return 0;
+ }
+ }
+
+ *b = true;
+ return 0;
+}
+
+int pa_alsa_path_get_mute(pa_alsa_path *p, snd_mixer_t *m, bool *muted) {
+ pa_alsa_element *e;
+
+ pa_assert(m);
+ pa_assert(p);
+ pa_assert(muted);
+
+ if (!p->has_mute)
+ return -1;
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ bool b;
+
+ if (e->switch_use != PA_ALSA_SWITCH_MUTE)
+ continue;
+
+ if (element_get_switch(e, m, &b) < 0)
+ return -1;
+
+ if (!b) {
+ *muted = true;
+ return 0;
+ }
+ }
+
+ *muted = false;
+ return 0;
+}
+
+/* Finds the closest item in db_fix->db_values and returns the corresponding
+ * step. *db_value is replaced with the value from the db_values table.
+ * Rounding is done based on the rounding parameter: -1 means rounding down and
+ * +1 means rounding up. */
+static long decibel_fix_get_step(pa_alsa_decibel_fix *db_fix, long *db_value, int rounding) {
+ unsigned i = 0;
+ unsigned max_i = 0;
+
+ pa_assert(db_fix);
+ pa_assert(db_value);
+ pa_assert(rounding != 0);
+
+ max_i = db_fix->max_step - db_fix->min_step;
+
+ if (rounding > 0) {
+ for (i = 0; i < max_i; i++) {
+ if (db_fix->db_values[i] >= *db_value)
+ break;
+ }
+ } else {
+ for (i = 0; i < max_i; i++) {
+ if (db_fix->db_values[i + 1] > *db_value)
+ break;
+ }
+ }
+
+ *db_value = db_fix->db_values[i];
+
+ return i + db_fix->min_step;
+}
+
+/* Alsa lib documentation says for snd_mixer_selem_set_playback_dB() direction argument,
+ * that "-1 = accurate or first below, 0 = accurate, 1 = accurate or first above".
+ * But even with accurate nearest dB volume step is not selected, so that is why we need
+ * this function. Returns 0 and nearest selectable volume in *value_dB on success or
+ * negative error code if fails. */
+static int element_get_nearest_alsa_dB(snd_mixer_elem_t *me, snd_mixer_selem_channel_id_t c, pa_alsa_direction_t d, long *value_dB) {
+
+ long alsa_val;
+ long value_high;
+ long value_low;
+ int r = -1;
+
+ pa_assert(me);
+ pa_assert(value_dB);
+
+ if (d == PA_ALSA_DIRECTION_OUTPUT) {
+ if ((r = snd_mixer_selem_ask_playback_dB_vol(me, *value_dB, +1, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value_high);
+
+ if (r < 0)
+ return r;
+
+ if (value_high == *value_dB)
+ return r;
+
+ if ((r = snd_mixer_selem_ask_playback_dB_vol(me, *value_dB, -1, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value_low);
+ } else {
+ if ((r = snd_mixer_selem_ask_capture_dB_vol(me, *value_dB, +1, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value_high);
+
+ if (r < 0)
+ return r;
+
+ if (value_high == *value_dB)
+ return r;
+
+ if ((r = snd_mixer_selem_ask_capture_dB_vol(me, *value_dB, -1, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value_low);
+ }
+
+ if (r < 0)
+ return r;
+
+ if (labs(value_high - *value_dB) < labs(value_low - *value_dB))
+ *value_dB = value_high;
+ else
+ *value_dB = value_low;
+
+ return r;
+}
+
+static int element_set_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, bool deferred_volume, bool write_to_hw) {
+
+ snd_mixer_selem_id_t *sid;
+ pa_cvolume rv;
+ snd_mixer_elem_t *me;
+ snd_mixer_selem_channel_id_t c;
+ pa_channel_position_mask_t mask = 0;
+ char buf[64];
+ unsigned k;
+
+ pa_assert(m);
+ pa_assert(e);
+ pa_assert(cm);
+ pa_assert(v);
+ pa_assert(pa_cvolume_compatible_with_channel_map(v, cm));
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ pa_cvolume_mute(&rv, cm->channels);
+
+ for (c = 0; c <= SND_MIXER_SCHN_LAST; c++) {
+ int r;
+ pa_volume_t f = PA_VOLUME_MUTED;
+ bool found = false;
+
+ for (k = 0; k < cm->channels; k++)
+ if (e->masks[c][e->n_channels-1] & PA_CHANNEL_POSITION_MASK(cm->map[k])) {
+ found = true;
+ if (v->values[k] > f)
+ f = v->values[k];
+ }
+
+ if (!found) {
+ /* Hmm, so this channel does not exist in the volume
+ * struct, so let's bind it to the overall max of the
+ * volume. */
+ f = pa_cvolume_max(v);
+ }
+
+ if (e->has_dB) {
+ long value = to_alsa_dB(f);
+ int rounding;
+
+ if (e->volume_limit >= 0 && value > (e->max_dB * 100))
+ value = e->max_dB * 100;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ /* If we call set_playback_volume() without checking first
+ * if the channel is available, ALSA behaves very
+ * strangely and doesn't fail the call */
+ if (snd_mixer_selem_has_playback_channel(me, c)) {
+ rounding = +1;
+ if (e->db_fix) {
+ if (write_to_hw)
+ r = snd_mixer_selem_set_playback_volume(me, c, decibel_fix_get_step(e->db_fix, &value, rounding));
+ else {
+ decibel_fix_get_step(e->db_fix, &value, rounding);
+ r = 0;
+ }
+
+ } else {
+ if (write_to_hw) {
+ if (deferred_volume) {
+ if ((r = element_get_nearest_alsa_dB(me, c, PA_ALSA_DIRECTION_OUTPUT, &value)) >= 0)
+ r = snd_mixer_selem_set_playback_dB(me, c, value, 0);
+ } else {
+ if ((r = snd_mixer_selem_set_playback_dB(me, c, value, rounding)) >= 0)
+ r = snd_mixer_selem_get_playback_dB(me, c, &value);
+ }
+ } else {
+ long alsa_val;
+ if ((r = snd_mixer_selem_ask_playback_dB_vol(me, value, rounding, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value);
+ }
+ }
+ } else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c)) {
+ rounding = -1;
+ if (e->db_fix) {
+ if (write_to_hw)
+ r = snd_mixer_selem_set_capture_volume(me, c, decibel_fix_get_step(e->db_fix, &value, rounding));
+ else {
+ decibel_fix_get_step(e->db_fix, &value, rounding);
+ r = 0;
+ }
+
+ } else {
+ if (write_to_hw) {
+ if (deferred_volume) {
+ if ((r = element_get_nearest_alsa_dB(me, c, PA_ALSA_DIRECTION_INPUT, &value)) >= 0)
+ r = snd_mixer_selem_set_capture_dB(me, c, value, 0);
+ } else {
+ if ((r = snd_mixer_selem_set_capture_dB(me, c, value, rounding)) >= 0)
+ r = snd_mixer_selem_get_capture_dB(me, c, &value);
+ }
+ } else {
+ long alsa_val;
+ if ((r = snd_mixer_selem_ask_capture_dB_vol(me, value, rounding, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value);
+ }
+ }
+ } else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ f = from_alsa_dB(value);
+
+ } else {
+ long value;
+
+ value = to_alsa_volume(f, e->min_volume, e->max_volume);
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_has_playback_channel(me, c)) {
+ if ((r = snd_mixer_selem_set_playback_volume(me, c, value)) >= 0)
+ r = snd_mixer_selem_get_playback_volume(me, c, &value);
+ } else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c)) {
+ if ((r = snd_mixer_selem_set_capture_volume(me, c, value)) >= 0)
+ r = snd_mixer_selem_get_capture_volume(me, c, &value);
+ } else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ f = from_alsa_volume(value, e->min_volume, e->max_volume);
+ }
+
+ for (k = 0; k < cm->channels; k++)
+ if (e->masks[c][e->n_channels-1] & PA_CHANNEL_POSITION_MASK(cm->map[k]))
+ if (rv.values[k] < f)
+ rv.values[k] = f;
+
+ mask |= e->masks[c][e->n_channels-1];
+ }
+
+ for (k = 0; k < cm->channels; k++)
+ if (!(mask & PA_CHANNEL_POSITION_MASK(cm->map[k])))
+ rv.values[k] = PA_VOLUME_NORM;
+
+ *v = rv;
+ return 0;
+}
+
+int pa_alsa_path_set_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, bool deferred_volume, bool write_to_hw) {
+
+ pa_alsa_element *e;
+ pa_cvolume rv;
+
+ pa_assert(m);
+ pa_assert(p);
+ pa_assert(cm);
+ pa_assert(v);
+ pa_assert(pa_cvolume_compatible_with_channel_map(v, cm));
+
+ if (!p->has_volume)
+ return -1;
+
+ rv = *v; /* Remaining adjustment */
+ pa_cvolume_reset(v, cm->channels); /* Adjustment done */
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ pa_cvolume ev;
+
+ if (e->volume_use != PA_ALSA_VOLUME_MERGE)
+ continue;
+
+ pa_assert(!p->has_dB || e->has_dB);
+
+ ev = rv;
+ if (element_set_volume(e, m, cm, &ev, deferred_volume, write_to_hw) < 0)
+ return -1;
+
+ if (!p->has_dB) {
+ *v = ev;
+ return 0;
+ }
+
+ pa_sw_cvolume_multiply(v, v, &ev);
+ pa_sw_cvolume_divide(&rv, &rv, &ev);
+ }
+
+ return 0;
+}
+
+static int element_set_switch(pa_alsa_element *e, snd_mixer_t *m, bool b) {
+ snd_mixer_elem_t *me;
+ snd_mixer_selem_id_t *sid;
+ char buf[64];
+ int r;
+
+ pa_assert(m);
+ pa_assert(e);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_set_playback_switch_all(me, b);
+ else
+ r = snd_mixer_selem_set_capture_switch_all(me, b);
+
+ if (r < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to set switch of %s: %s", buf, pa_alsa_strerror(errno));
+ }
+
+ return r;
+}
+
+int pa_alsa_path_set_mute(pa_alsa_path *p, snd_mixer_t *m, bool muted) {
+ pa_alsa_element *e;
+
+ pa_assert(m);
+ pa_assert(p);
+
+ if (!p->has_mute)
+ return -1;
+
+ PA_LLIST_FOREACH(e, p->elements) {
+
+ if (e->switch_use != PA_ALSA_SWITCH_MUTE)
+ continue;
+
+ if (element_set_switch(e, m, !muted) < 0)
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Depending on whether e->volume_use is _OFF, _ZERO or _CONSTANT, this
+ * function sets all channels of the volume element to e->min_volume, 0 dB or
+ * e->constant_volume. */
+static int element_set_constant_volume(pa_alsa_element *e, snd_mixer_t *m) {
+ snd_mixer_elem_t *me = NULL;
+ snd_mixer_selem_id_t *sid = NULL;
+ int r = 0;
+ long volume = -1;
+ bool volume_set = false;
+ char buf[64];
+
+ pa_assert(m);
+ pa_assert(e);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ switch (e->volume_use) {
+ case PA_ALSA_VOLUME_OFF:
+ volume = e->min_volume;
+ volume_set = true;
+ break;
+
+ case PA_ALSA_VOLUME_ZERO:
+ if (e->db_fix) {
+ long dB = 0;
+
+ volume = decibel_fix_get_step(e->db_fix, &dB, (e->direction == PA_ALSA_DIRECTION_OUTPUT ? +1 : -1));
+ volume_set = true;
+ }
+ break;
+
+ case PA_ALSA_VOLUME_CONSTANT:
+ volume = e->constant_volume;
+ volume_set = true;
+ break;
+
+ default:
+ pa_assert_not_reached();
+ }
+
+ if (volume_set) {
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_set_playback_volume_all(me, volume);
+ else
+ r = snd_mixer_selem_set_capture_volume_all(me, volume);
+ } else {
+ pa_assert(e->volume_use == PA_ALSA_VOLUME_ZERO);
+ pa_assert(!e->db_fix);
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_set_playback_dB_all(me, 0, +1);
+ else
+ r = snd_mixer_selem_set_capture_dB_all(me, 0, -1);
+ }
+
+ if (r < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to set volume of %s: %s", buf, pa_alsa_strerror(errno));
+ }
+
+ return r;
+}
+
+int pa_alsa_path_select(pa_alsa_path *p, pa_alsa_setting *s, snd_mixer_t *m, bool device_is_muted) {
+ pa_alsa_element *e;
+ int r = 0;
+
+ pa_assert(m);
+ pa_assert(p);
+
+ pa_log_info("Activating path %s", p->name);
+ pa_alsa_path_dump(p);
+
+ /* First turn on hw mute if available, to avoid noise
+ * when setting the mixer controls. */
+ if (p->mute_during_activation) {
+ PA_LLIST_FOREACH(e, p->elements) {
+ if (e->switch_use == PA_ALSA_SWITCH_MUTE)
+ /* If the muting fails here, that's not a critical problem for
+ * selecting a path, so we ignore the return value.
+ * element_set_switch() will print a warning anyway, so this
+ * won't be a silent failure either. */
+ (void) element_set_switch(e, m, false);
+ }
+ }
+
+ PA_LLIST_FOREACH(e, p->elements) {
+
+ switch (e->switch_use) {
+ case PA_ALSA_SWITCH_OFF:
+ r = element_set_switch(e, m, false);
+ break;
+
+ case PA_ALSA_SWITCH_ON:
+ r = element_set_switch(e, m, true);
+ break;
+
+ case PA_ALSA_SWITCH_MUTE:
+ case PA_ALSA_SWITCH_IGNORE:
+ case PA_ALSA_SWITCH_SELECT:
+ r = 0;
+ break;
+ }
+
+ if (r < 0)
+ return -1;
+
+ switch (e->volume_use) {
+ case PA_ALSA_VOLUME_OFF:
+ case PA_ALSA_VOLUME_ZERO:
+ case PA_ALSA_VOLUME_CONSTANT:
+ r = element_set_constant_volume(e, m);
+ break;
+
+ case PA_ALSA_VOLUME_MERGE:
+ case PA_ALSA_VOLUME_IGNORE:
+ r = 0;
+ break;
+ }
+
+ if (r < 0)
+ return -1;
+ }
+
+ if (s)
+ setting_select(s, m);
+
+ /* Finally restore hw mute to the device mute status. */
+ if (p->mute_during_activation) {
+ PA_LLIST_FOREACH(e, p->elements) {
+ if (e->switch_use == PA_ALSA_SWITCH_MUTE) {
+ if (element_set_switch(e, m, !device_is_muted) < 0)
+ return -1;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int check_required(pa_alsa_element *e, snd_mixer_elem_t *me) {
+ bool has_switch;
+ bool has_enumeration;
+ bool has_volume;
+
+ pa_assert(e);
+ pa_assert(me);
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ has_switch =
+ snd_mixer_selem_has_playback_switch(me) ||
+ (e->direction_try_other && snd_mixer_selem_has_capture_switch(me));
+ } else {
+ has_switch =
+ snd_mixer_selem_has_capture_switch(me) ||
+ (e->direction_try_other && snd_mixer_selem_has_playback_switch(me));
+ }
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ has_volume =
+ snd_mixer_selem_has_playback_volume(me) ||
+ (e->direction_try_other && snd_mixer_selem_has_capture_volume(me));
+ } else {
+ has_volume =
+ snd_mixer_selem_has_capture_volume(me) ||
+ (e->direction_try_other && snd_mixer_selem_has_playback_volume(me));
+ }
+
+ has_enumeration = snd_mixer_selem_is_enumerated(me);
+
+ if ((e->required == PA_ALSA_REQUIRED_SWITCH && !has_switch) ||
+ (e->required == PA_ALSA_REQUIRED_VOLUME && !has_volume) ||
+ (e->required == PA_ALSA_REQUIRED_ENUMERATION && !has_enumeration))
+ return -1;
+
+ if (e->required == PA_ALSA_REQUIRED_ANY && !(has_switch || has_volume || has_enumeration))
+ return -1;
+
+ if ((e->required_absent == PA_ALSA_REQUIRED_SWITCH && has_switch) ||
+ (e->required_absent == PA_ALSA_REQUIRED_VOLUME && has_volume) ||
+ (e->required_absent == PA_ALSA_REQUIRED_ENUMERATION && has_enumeration))
+ return -1;
+
+ if (e->required_absent == PA_ALSA_REQUIRED_ANY && (has_switch || has_volume || has_enumeration))
+ return -1;
+
+ if (e->required_any != PA_ALSA_REQUIRED_IGNORE) {
+ switch (e->required_any) {
+ case PA_ALSA_REQUIRED_VOLUME:
+ e->path->req_any_present |= (e->volume_use != PA_ALSA_VOLUME_IGNORE);
+ break;
+ case PA_ALSA_REQUIRED_SWITCH:
+ e->path->req_any_present |= (e->switch_use != PA_ALSA_SWITCH_IGNORE);
+ break;
+ case PA_ALSA_REQUIRED_ENUMERATION:
+ e->path->req_any_present |= (e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE);
+ break;
+ case PA_ALSA_REQUIRED_ANY:
+ e->path->req_any_present |=
+ (e->volume_use != PA_ALSA_VOLUME_IGNORE) ||
+ (e->switch_use != PA_ALSA_SWITCH_IGNORE) ||
+ (e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE);
+ break;
+ default:
+ pa_assert_not_reached();
+ }
+ }
+
+ if (e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) {
+ pa_alsa_option *o;
+ PA_LLIST_FOREACH(o, e->options) {
+ e->path->req_any_present |= (o->required_any != PA_ALSA_REQUIRED_IGNORE) &&
+ (o->alsa_idx >= 0);
+ if (o->required != PA_ALSA_REQUIRED_IGNORE && o->alsa_idx < 0)
+ return -1;
+ if (o->required_absent != PA_ALSA_REQUIRED_IGNORE && o->alsa_idx >= 0)
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static int element_ask_vol_dB(snd_mixer_elem_t *me, pa_alsa_direction_t dir, long value, long *dBvalue) {
+ if (dir == PA_ALSA_DIRECTION_OUTPUT)
+ return snd_mixer_selem_ask_playback_vol_dB(me, value, dBvalue);
+ else
+ return snd_mixer_selem_ask_capture_vol_dB(me, value, dBvalue);
+}
+
+static bool element_probe_volume(pa_alsa_element *e, snd_mixer_elem_t *me) {
+
+ long min_dB = 0, max_dB = 0;
+ int r;
+ bool is_mono;
+ pa_channel_position_t p;
+ char buf[64];
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (!snd_mixer_selem_has_playback_volume(me)) {
+ if (e->direction_try_other && snd_mixer_selem_has_capture_volume(me))
+ e->direction = PA_ALSA_DIRECTION_INPUT;
+ else
+ return false;
+ }
+ } else {
+ if (!snd_mixer_selem_has_capture_volume(me)) {
+ if (e->direction_try_other && snd_mixer_selem_has_playback_volume(me))
+ e->direction = PA_ALSA_DIRECTION_OUTPUT;
+ else
+ return false;
+ }
+ }
+
+ e->direction_try_other = false;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_get_playback_volume_range(me, &e->min_volume, &e->max_volume);
+ else
+ r = snd_mixer_selem_get_capture_volume_range(me, &e->min_volume, &e->max_volume);
+
+ if (r < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to get volume range of %s: %s", buf, pa_alsa_strerror(r));
+ return false;
+ }
+
+ if (e->min_volume >= e->max_volume) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Your kernel driver is broken for element %s: it reports a volume range from %li to %li which makes no sense.",
+ buf, e->min_volume, e->max_volume);
+ return false;
+ }
+ if (e->volume_use == PA_ALSA_VOLUME_CONSTANT && (e->min_volume > e->constant_volume || e->max_volume < e->constant_volume)) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Constant volume %li configured for element %s, but the available range is from %li to %li.",
+ e->constant_volume, buf, e->min_volume, e->max_volume);
+ return false;
+ }
+
+
+ if (e->db_fix && ((e->min_volume > e->db_fix->min_step) || (e->max_volume < e->db_fix->max_step))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("The step range of the decibel fix for element %s (%li-%li) doesn't fit to the "
+ "real hardware range (%li-%li). Disabling the decibel fix.", buf,
+ e->db_fix->min_step, e->db_fix->max_step, e->min_volume, e->max_volume);
+
+ decibel_fix_free(e->db_fix);
+ e->db_fix = NULL;
+ }
+
+ if (e->db_fix) {
+ e->has_dB = true;
+ e->min_volume = e->db_fix->min_step;
+ e->max_volume = e->db_fix->max_step;
+ min_dB = e->db_fix->db_values[0];
+ max_dB = e->db_fix->db_values[e->db_fix->max_step - e->db_fix->min_step];
+ } else if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ e->has_dB = snd_mixer_selem_get_playback_dB_range(me, &min_dB, &max_dB) >= 0;
+ else
+ e->has_dB = snd_mixer_selem_get_capture_dB_range(me, &min_dB, &max_dB) >= 0;
+
+ /* Assume decibel data to be incorrect if max_dB is negative. */
+ if (e->has_dB && max_dB < 0 && !e->db_fix) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("The decibel volume range for element %s (%li dB - %li dB) has negative maximum. "
+ "Disabling the decibel range.", buf, min_dB, max_dB);
+ e->has_dB = false;
+ }
+
+ /* Check that the kernel driver returns consistent limits with
+ * both _get_*_dB_range() and _ask_*_vol_dB(). */
+ if (e->has_dB && !e->db_fix) {
+ long min_dB_checked = 0;
+ long max_dB_checked = 0;
+
+ if (element_ask_vol_dB(me, e->direction, e->min_volume, &min_dB_checked) < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to query the dB value for %s at volume level %li", buf, e->min_volume);
+ return false;
+ }
+
+ if (element_ask_vol_dB(me, e->direction, e->max_volume, &max_dB_checked) < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to query the dB value for %s at volume level %li", buf, e->max_volume);
+ return false;
+ }
+
+ if (min_dB != min_dB_checked || max_dB != max_dB_checked) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Your kernel driver is broken: the reported dB range for %s (from %0.2f dB to %0.2f dB) "
+ "doesn't match the dB values at minimum and maximum volume levels: %0.2f dB at level %li, "
+ "%0.2f dB at level %li.", buf, min_dB / 100.0, max_dB / 100.0,
+ min_dB_checked / 100.0, e->min_volume, max_dB_checked / 100.0, e->max_volume);
+ return false;
+ }
+ }
+
+ if (e->has_dB) {
+ e->min_dB = ((double) min_dB) / 100.0;
+ e->max_dB = ((double) max_dB) / 100.0;
+
+ if (min_dB >= max_dB) {
+ pa_assert(!e->db_fix);
+ pa_log_warn("Your kernel driver is broken: it reports a volume range from %0.2f dB to %0.2f dB which makes no sense.",
+ e->min_dB, e->max_dB);
+ e->has_dB = false;
+ }
+ }
+
+ if (e->volume_limit >= 0) {
+ if (e->volume_limit <= e->min_volume || e->volume_limit > e->max_volume) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Volume limit for element %s of path %s is invalid: %li isn't within the valid range "
+ "%li-%li. The volume limit is ignored.",
+ buf, e->path->name, e->volume_limit, e->min_volume + 1, e->max_volume);
+ } else {
+ e->max_volume = e->volume_limit;
+
+ if (e->has_dB) {
+ if (e->db_fix) {
+ e->db_fix->max_step = e->max_volume;
+ e->max_dB = ((double) e->db_fix->db_values[e->db_fix->max_step - e->db_fix->min_step]) / 100.0;
+ } else if (element_ask_vol_dB(me, e->direction, e->max_volume, &max_dB) < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to get dB value of %s: %s", buf, pa_alsa_strerror(r));
+ e->has_dB = false;
+ } else
+ e->max_dB = ((double) max_dB) / 100.0;
+ }
+ }
+ }
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ is_mono = snd_mixer_selem_is_playback_mono(me) > 0;
+ else
+ is_mono = snd_mixer_selem_is_capture_mono(me) > 0;
+
+ if (is_mono) {
+ e->n_channels = 1;
+
+ if ((e->override_map & (1 << (e->n_channels-1))) && e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1] == 0) {
+ pa_log_warn("Override map for mono element %s is invalid, ignoring override map", e->path->name);
+ e->override_map &= ~(1 << (e->n_channels-1));
+ }
+ if (!(e->override_map & (1 << (e->n_channels-1)))) {
+ for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) {
+ if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN)
+ continue;
+ e->masks[alsa_channel_ids[p]][e->n_channels-1] = 0;
+ }
+ e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1] = PA_CHANNEL_POSITION_MASK_ALL;
+ }
+ e->merged_mask = e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1];
+ return true;
+ }
+
+ e->n_channels = 0;
+ for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) {
+ if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN)
+ continue;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ e->n_channels += snd_mixer_selem_has_playback_channel(me, alsa_channel_ids[p]) > 0;
+ else
+ e->n_channels += snd_mixer_selem_has_capture_channel(me, alsa_channel_ids[p]) > 0;
+ }
+
+ if (e->n_channels <= 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Volume element %s with no channels?", buf);
+ return false;
+ } else if (e->n_channels > POSITION_MASK_CHANNELS) {
+ /* FIXME: In some places code like this is used:
+ *
+ * e->masks[alsa_channel_ids[p]][e->n_channels-1]
+ *
+ * The definition of e->masks is
+ *
+ * pa_channel_position_mask_t masks[SND_MIXER_SCHN_LAST + 1][POSITION_MASK_CHANNELS];
+ *
+ * Since the array size is fixed at POSITION_MASK_CHANNELS, we obviously
+ * don't support elements with more than POSITION_MASK_CHANNELS
+ * channels... */
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Volume element %s has %u channels. That's too much! I can't handle that!", buf, e->n_channels);
+ return false;
+ }
+
+retry:
+ if (!(e->override_map & (1 << (e->n_channels-1)))) {
+ for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) {
+ bool has_channel;
+
+ if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN)
+ continue;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ has_channel = snd_mixer_selem_has_playback_channel(me, alsa_channel_ids[p]) > 0;
+ else
+ has_channel = snd_mixer_selem_has_capture_channel(me, alsa_channel_ids[p]) > 0;
+
+ e->masks[alsa_channel_ids[p]][e->n_channels-1] = has_channel ? PA_CHANNEL_POSITION_MASK(p) : 0;
+ }
+ }
+
+ e->merged_mask = 0;
+ for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) {
+ if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN)
+ continue;
+
+ e->merged_mask |= e->masks[alsa_channel_ids[p]][e->n_channels-1];
+ }
+
+ if (e->merged_mask == 0) {
+ if (!(e->override_map & (1 << (e->n_channels-1)))) {
+ pa_log_warn("Channel map for element %s is invalid", e->path->name);
+ return false;
+ }
+ pa_log_warn("Override map for element %s has empty result, ignoring override map", e->path->name);
+ e->override_map &= ~(1 << (e->n_channels-1));
+ goto retry;
+ }
+
+ return true;
+}
+
+static int element_probe(pa_alsa_element *e, snd_mixer_t *m) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+
+ pa_assert(m);
+ pa_assert(e);
+ pa_assert(e->path);
+
+ SELEM_INIT(sid, &e->alsa_id);
+
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+
+ if (e->required != PA_ALSA_REQUIRED_IGNORE)
+ return -1;
+
+ e->switch_use = PA_ALSA_SWITCH_IGNORE;
+ e->volume_use = PA_ALSA_VOLUME_IGNORE;
+ e->enumeration_use = PA_ALSA_ENUMERATION_IGNORE;
+
+ return 0;
+ }
+
+ if (e->switch_use != PA_ALSA_SWITCH_IGNORE) {
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+
+ if (!snd_mixer_selem_has_playback_switch(me)) {
+ if (e->direction_try_other && snd_mixer_selem_has_capture_switch(me))
+ e->direction = PA_ALSA_DIRECTION_INPUT;
+ else
+ e->switch_use = PA_ALSA_SWITCH_IGNORE;
+ }
+
+ } else {
+
+ if (!snd_mixer_selem_has_capture_switch(me)) {
+ if (e->direction_try_other && snd_mixer_selem_has_playback_switch(me))
+ e->direction = PA_ALSA_DIRECTION_OUTPUT;
+ else
+ e->switch_use = PA_ALSA_SWITCH_IGNORE;
+ }
+ }
+
+ if (e->switch_use != PA_ALSA_SWITCH_IGNORE)
+ e->direction_try_other = false;
+ }
+
+ if (!element_probe_volume(e, me))
+ e->volume_use = PA_ALSA_VOLUME_IGNORE;
+
+ if (e->switch_use == PA_ALSA_SWITCH_SELECT) {
+ pa_alsa_option *o;
+
+ PA_LLIST_FOREACH(o, e->options)
+ o->alsa_idx = pa_streq(o->alsa_name, "on") ? 1 : 0;
+ } else if (e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) {
+ int n;
+ pa_alsa_option *o;
+
+ if ((n = snd_mixer_selem_get_enum_items(me)) < 0) {
+ pa_log("snd_mixer_selem_get_enum_items() failed: %s", pa_alsa_strerror(n));
+ return -1;
+ }
+
+ PA_LLIST_FOREACH(o, e->options) {
+ int i;
+
+ for (i = 0; i < n; i++) {
+ char buf[128];
+
+ if (snd_mixer_selem_get_enum_item_name(me, i, sizeof(buf), buf) < 0)
+ continue;
+
+ if (!pa_streq(buf, o->alsa_name))
+ continue;
+
+ o->alsa_idx = i;
+ }
+ }
+ }
+
+ if (check_required(e, me) < 0)
+ return -1;
+
+ return 0;
+}
+
+static int jack_probe(pa_alsa_jack *j, pa_alsa_mapping *mapping, snd_mixer_t *m) {
+ bool has_control;
+
+ pa_assert(j);
+ pa_assert(j->path);
+
+ if (j->append_pcm_to_name) {
+ char *new_name;
+
+ if (!mapping) {
+ /* This could also be an assertion, because this should never
+ * happen. At the time of writing, mapping can only be NULL when
+ * module-alsa-sink/source synthesizes a path, and those
+ * synthesized paths never have any jacks, so jack_probe() should
+ * never be called with a NULL mapping. */
+ pa_log("Jack %s: append_pcm_to_name is set, but mapping is NULL. Can't use this jack.", j->name);
+ return -1;
+ }
+
+ new_name = pa_sprintf_malloc("%s,pcm=%i Jack", j->name, mapping->hw_device_index);
+ pa_xfree(j->alsa_id.name);
+ j->alsa_id.name = new_name;
+ j->append_pcm_to_name = false;
+ }
+
+ has_control = pa_alsa_mixer_find_card(m, &j->alsa_id, 0) != NULL;
+ pa_alsa_jack_set_has_control(j, has_control);
+
+ if (j->has_control) {
+ if (j->required_absent != PA_ALSA_REQUIRED_IGNORE)
+ return -1;
+ if (j->required_any != PA_ALSA_REQUIRED_IGNORE)
+ j->path->req_any_present = true;
+ } else {
+ if (j->required != PA_ALSA_REQUIRED_IGNORE)
+ return -1;
+ }
+
+ return 0;
+}
+
+pa_alsa_element * pa_alsa_element_get(pa_alsa_path *p, const char *section, bool prefixed) {
+ pa_alsa_element *e;
+ char *name;
+ int index;
+
+ pa_assert(p);
+ pa_assert(section);
+
+ if (prefixed) {
+ if (!pa_startswith(section, "Element "))
+ return NULL;
+
+ section += 8;
+ }
+
+ /* This is not an element section, but an enum section? */
+ if (strchr(section, ':'))
+ return NULL;
+
+ name = alloca(strlen(section) + 1);
+ if (alsa_id_decode(section, name, &index))
+ return NULL;
+
+ if (p->last_element && pa_streq(p->last_element->alsa_id.name, name) &&
+ p->last_element->alsa_id.index == index)
+ return p->last_element;
+
+ PA_LLIST_FOREACH(e, p->elements)
+ if (pa_streq(e->alsa_id.name, name) && e->alsa_id.index == index)
+ goto finish;
+
+ e = pa_xnew0(pa_alsa_element, 1);
+ e->path = p;
+ e->alsa_id.name = pa_xstrdup(name);
+ e->alsa_id.index = index;
+ e->direction = p->direction;
+ e->volume_limit = -1;
+
+ PA_LLIST_INSERT_AFTER(pa_alsa_element, p->elements, p->last_element, e);
+
+finish:
+ p->last_element = e;
+ return e;
+}
+
+static pa_alsa_jack* jack_get(pa_alsa_path *p, const char *section) {
+ pa_alsa_jack *j;
+ char *name;
+ int index;
+
+ if (!pa_startswith(section, "Jack "))
+ return NULL;
+ section += 5;
+
+ name = alloca(strlen(section) + 1);
+ if (alsa_id_decode(section, name, &index))
+ return NULL;
+
+ if (p->last_jack && pa_streq(p->last_jack->name, name) &&
+ p->last_jack->alsa_id.index == index)
+ return p->last_jack;
+
+ PA_LLIST_FOREACH(j, p->jacks)
+ if (pa_streq(j->name, name) && j->alsa_id.index == index)
+ goto finish;
+
+ j = pa_alsa_jack_new(p, NULL, name, index);
+ PA_LLIST_INSERT_AFTER(pa_alsa_jack, p->jacks, p->last_jack, j);
+
+finish:
+ p->last_jack = j;
+ return j;
+}
+
+static pa_alsa_option* option_get(pa_alsa_path *p, const char *section) {
+ char *en, *name;
+ const char *on;
+ pa_alsa_option *o;
+ pa_alsa_element *e;
+ size_t len;
+ int index;
+
+ if (!pa_startswith(section, "Option "))
+ return NULL;
+
+ section += 7;
+
+ /* This is not an enum section, but an element section? */
+ if (!(on = strchr(section, ':')))
+ return NULL;
+
+ len = on - section;
+ en = alloca(len + 1);
+ strncpy(en, section, len);
+ en[len] = '\0';
+
+ name = alloca(strlen(en) + 1);
+ if (alsa_id_decode(en, name, &index))
+ return NULL;
+
+ on++;
+
+ if (p->last_option &&
+ pa_streq(p->last_option->element->alsa_id.name, name) &&
+ p->last_option->element->alsa_id.index == index &&
+ pa_streq(p->last_option->alsa_name, on)) {
+ return p->last_option;
+ }
+
+ pa_assert_se(e = pa_alsa_element_get(p, en, false));
+
+ PA_LLIST_FOREACH(o, e->options)
+ if (pa_streq(o->alsa_name, on))
+ goto finish;
+
+ o = pa_xnew0(pa_alsa_option, 1);
+ o->element = e;
+ o->alsa_name = pa_xstrdup(on);
+ o->alsa_idx = -1;
+
+ if (p->last_option && p->last_option->element == e)
+ PA_LLIST_INSERT_AFTER(pa_alsa_option, e->options, p->last_option, o);
+ else
+ PA_LLIST_PREPEND(pa_alsa_option, e->options, o);
+
+finish:
+ p->last_option = o;
+ return o;
+}
+
+static int element_parse_switch(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Switch makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "ignore"))
+ e->switch_use = PA_ALSA_SWITCH_IGNORE;
+ else if (pa_streq(state->rvalue, "mute"))
+ e->switch_use = PA_ALSA_SWITCH_MUTE;
+ else if (pa_streq(state->rvalue, "off"))
+ e->switch_use = PA_ALSA_SWITCH_OFF;
+ else if (pa_streq(state->rvalue, "on"))
+ e->switch_use = PA_ALSA_SWITCH_ON;
+ else if (pa_streq(state->rvalue, "select"))
+ e->switch_use = PA_ALSA_SWITCH_SELECT;
+ else {
+ pa_log("[%s:%u] Switch invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int element_parse_volume(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Volume makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "ignore"))
+ e->volume_use = PA_ALSA_VOLUME_IGNORE;
+ else if (pa_streq(state->rvalue, "merge"))
+ e->volume_use = PA_ALSA_VOLUME_MERGE;
+ else if (pa_streq(state->rvalue, "off"))
+ e->volume_use = PA_ALSA_VOLUME_OFF;
+ else if (pa_streq(state->rvalue, "zero"))
+ e->volume_use = PA_ALSA_VOLUME_ZERO;
+ else {
+ uint32_t constant;
+
+ if (pa_atou(state->rvalue, &constant) >= 0) {
+ e->volume_use = PA_ALSA_VOLUME_CONSTANT;
+ e->constant_volume = constant;
+ } else {
+ pa_log("[%s:%u] Volume invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static int element_parse_enumeration(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Enumeration makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "ignore"))
+ e->enumeration_use = PA_ALSA_ENUMERATION_IGNORE;
+ else if (pa_streq(state->rvalue, "select"))
+ e->enumeration_use = PA_ALSA_ENUMERATION_SELECT;
+ else {
+ pa_log("[%s:%u] Enumeration invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int parse_type(pa_config_parser_state *state) {
+ struct device_port_types {
+ const char *name;
+ pa_device_port_type_t type;
+ } device_port_types[] = {
+ { "unknown", PA_DEVICE_PORT_TYPE_UNKNOWN },
+ { "aux", PA_DEVICE_PORT_TYPE_AUX },
+ { "speaker", PA_DEVICE_PORT_TYPE_SPEAKER },
+ { "headphones", PA_DEVICE_PORT_TYPE_HEADPHONES },
+ { "line", PA_DEVICE_PORT_TYPE_LINE },
+ { "mic", PA_DEVICE_PORT_TYPE_MIC },
+ { "headset", PA_DEVICE_PORT_TYPE_HEADSET },
+ { "handset", PA_DEVICE_PORT_TYPE_HANDSET },
+ { "earpiece", PA_DEVICE_PORT_TYPE_EARPIECE },
+ { "spdif", PA_DEVICE_PORT_TYPE_SPDIF },
+ { "hdmi", PA_DEVICE_PORT_TYPE_HDMI },
+ { "tv", PA_DEVICE_PORT_TYPE_TV },
+ { "radio", PA_DEVICE_PORT_TYPE_RADIO },
+ { "video", PA_DEVICE_PORT_TYPE_VIDEO },
+ { "usb", PA_DEVICE_PORT_TYPE_USB },
+ { "bluetooth", PA_DEVICE_PORT_TYPE_BLUETOOTH },
+ { "portable", PA_DEVICE_PORT_TYPE_PORTABLE },
+ { "handsfree", PA_DEVICE_PORT_TYPE_HANDSFREE },
+ { "car", PA_DEVICE_PORT_TYPE_CAR },
+ { "hifi", PA_DEVICE_PORT_TYPE_HIFI },
+ { "phone", PA_DEVICE_PORT_TYPE_PHONE },
+ { "network", PA_DEVICE_PORT_TYPE_NETWORK },
+ { "analog", PA_DEVICE_PORT_TYPE_ANALOG },
+ };
+ pa_alsa_path *path;
+ unsigned int idx;
+
+ path = state->userdata;
+
+ for (idx = 0; idx < PA_ELEMENTSOF(device_port_types); idx++)
+ if (pa_streq(state->rvalue, device_port_types[idx].name)) {
+ path->device_port_type = device_port_types[idx].type;
+ return 0;
+ }
+
+ pa_log("[%s:%u] Invalid value for option 'type': %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+}
+
+static int parse_eld_device(pa_config_parser_state *state) {
+ pa_alsa_path *path;
+ uint32_t eld_device;
+
+ path = state->userdata;
+
+ if (pa_atou(state->rvalue, &eld_device) >= 0) {
+ path->autodetect_eld_device = false;
+ path->eld_device = eld_device;
+ return 0;
+ }
+
+ if (pa_streq(state->rvalue, "auto")) {
+ path->autodetect_eld_device = true;
+ path->eld_device = -1;
+ return 0;
+ }
+
+ pa_log("[%s:%u] Invalid value for option 'eld-device': %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+}
+
+static int option_parse_priority(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_option *o;
+ uint32_t prio;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(o = option_get(p, state->section))) {
+ pa_log("[%s:%u] Priority makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_atou(state->rvalue, &prio) < 0) {
+ pa_log("[%s:%u] Priority invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ o->priority = prio;
+ return 0;
+}
+
+static int option_parse_name(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_option *o;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(o = option_get(p, state->section))) {
+ pa_log("[%s:%u] Name makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ pa_xfree(o->name);
+ o->name = pa_xstrdup(state->rvalue);
+
+ return 0;
+}
+
+static int element_parse_required(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ pa_alsa_option *o;
+ pa_alsa_jack *j;
+ pa_alsa_required_t req;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ e = pa_alsa_element_get(p, state->section, true);
+ o = option_get(p, state->section);
+ j = jack_get(p, state->section);
+ if (!e && !o && !j) {
+ pa_log("[%s:%u] Required makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "ignore"))
+ req = PA_ALSA_REQUIRED_IGNORE;
+ else if (pa_streq(state->rvalue, "switch") && e)
+ req = PA_ALSA_REQUIRED_SWITCH;
+ else if (pa_streq(state->rvalue, "volume") && e)
+ req = PA_ALSA_REQUIRED_VOLUME;
+ else if (pa_streq(state->rvalue, "enumeration"))
+ req = PA_ALSA_REQUIRED_ENUMERATION;
+ else if (pa_streq(state->rvalue, "any"))
+ req = PA_ALSA_REQUIRED_ANY;
+ else {
+ pa_log("[%s:%u] Required invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "required-absent")) {
+ if (e)
+ e->required_absent = req;
+ if (o)
+ o->required_absent = req;
+ if (j)
+ j->required_absent = req;
+ }
+ else if (pa_streq(state->lvalue, "required-any")) {
+ if (e) {
+ e->required_any = req;
+ e->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE);
+ }
+ if (o) {
+ o->required_any = req;
+ o->element->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE);
+ }
+ if (j) {
+ j->required_any = req;
+ j->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE);
+ }
+
+ }
+ else {
+ if (e)
+ e->required = req;
+ if (o)
+ o->required = req;
+ if (j)
+ j->required = req;
+ }
+
+ return 0;
+}
+
+static int element_parse_direction(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Direction makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "playback"))
+ e->direction = PA_ALSA_DIRECTION_OUTPUT;
+ else if (pa_streq(state->rvalue, "capture"))
+ e->direction = PA_ALSA_DIRECTION_INPUT;
+ else {
+ pa_log("[%s:%u] Direction invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int element_parse_direction_try_other(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ int yes;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Direction makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if ((yes = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Direction invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ e->direction_try_other = !!yes;
+ return 0;
+}
+
+static int element_parse_volume_limit(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ long volume_limit;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] volume-limit makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_atol(state->rvalue, &volume_limit) < 0 || volume_limit < 0) {
+ pa_log("[%s:%u] Invalid value for volume-limit", state->filename, state->lineno);
+ return -1;
+ }
+
+ e->volume_limit = volume_limit;
+ return 0;
+}
+
+static unsigned int parse_channel_position(const char *m)
+{
+ pa_channel_position_t p;
+
+ if ((p = pa_channel_position_from_string(m)) == PA_CHANNEL_POSITION_INVALID)
+ return SND_MIXER_SCHN_UNKNOWN;
+
+ return alsa_channel_ids[p];
+}
+
+static pa_channel_position_mask_t parse_mask(const char *m) {
+ pa_channel_position_mask_t v;
+
+ if (pa_streq(m, "all-left"))
+ v = PA_CHANNEL_POSITION_MASK_LEFT;
+ else if (pa_streq(m, "all-right"))
+ v = PA_CHANNEL_POSITION_MASK_RIGHT;
+ else if (pa_streq(m, "all-center"))
+ v = PA_CHANNEL_POSITION_MASK_CENTER;
+ else if (pa_streq(m, "all-front"))
+ v = PA_CHANNEL_POSITION_MASK_FRONT;
+ else if (pa_streq(m, "all-rear"))
+ v = PA_CHANNEL_POSITION_MASK_REAR;
+ else if (pa_streq(m, "all-side"))
+ v = PA_CHANNEL_POSITION_MASK_SIDE_OR_TOP_CENTER;
+ else if (pa_streq(m, "all-top"))
+ v = PA_CHANNEL_POSITION_MASK_TOP;
+ else if (pa_streq(m, "all-no-lfe"))
+ v = PA_CHANNEL_POSITION_MASK_ALL ^ PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_LFE);
+ else if (pa_streq(m, "all"))
+ v = PA_CHANNEL_POSITION_MASK_ALL;
+ else {
+ pa_channel_position_t p;
+
+ if ((p = pa_channel_position_from_string(m)) == PA_CHANNEL_POSITION_INVALID)
+ return 0;
+
+ v = PA_CHANNEL_POSITION_MASK(p);
+ }
+
+ return v;
+}
+
+static int element_parse_override_map(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ const char *split_state = NULL;
+ char *s;
+ unsigned i = 0;
+ int channel_count = 0;
+ char *n;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Override map makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ s = strstr(state->lvalue, ".");
+ if (s) {
+ pa_atoi(s + 1, &channel_count);
+ if (channel_count < 1 || channel_count > POSITION_MASK_CHANNELS) {
+ pa_log("[%s:%u] Override map index '%s' invalid in '%s'", state->filename, state->lineno, state->lvalue, state->section);
+ return 0;
+ }
+ } else {
+ pa_log("[%s:%u] Invalid override map syntax '%s' in '%s'", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ while ((n = pa_split(state->rvalue, ",", &split_state))) {
+ pa_channel_position_mask_t m;
+ snd_mixer_selem_channel_id_t channel_position;
+
+ if (i >= (unsigned)channel_count) {
+ pa_log("[%s:%u] Invalid override map size (>%d) in '%s'", state->filename, state->lineno, channel_count, state->section);
+ pa_xfree(n);
+ return -1;
+ }
+ channel_position = alsa_channel_positions[i];
+
+ if (!*n)
+ m = 0;
+ else {
+ s = strstr(n, ":");
+ if (s) {
+ *s = '\0';
+ s++;
+ channel_position = parse_channel_position(n);
+ if (channel_position == SND_MIXER_SCHN_UNKNOWN) {
+ pa_log("[%s:%u] Override map position '%s' invalid in '%s'", state->filename, state->lineno, n, state->section);
+ pa_xfree(n);
+ return -1;
+ }
+ }
+ if ((m = parse_mask(s ? s : n)) == 0) {
+ pa_log("[%s:%u] Override map '%s' invalid in '%s'", state->filename, state->lineno, s ? s : n, state->section);
+ pa_xfree(n);
+ return -1;
+ }
+ }
+
+ if (e->masks[channel_position][channel_count-1]) {
+ pa_log("[%s:%u] Override map '%s' duplicate position '%s' in '%s'", state->filename, state->lineno, s ? s : n, snd_mixer_selem_channel_name(channel_position), state->section);
+ pa_xfree(n);
+ return -1;
+ }
+ e->override_map |= (1 << (channel_count - 1));
+ e->masks[channel_position][channel_count-1] = m;
+ pa_xfree(n);
+ i++;
+ }
+
+ return 0;
+}
+
+static int jack_parse_state(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_jack *j;
+ pa_available_t pa;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(j = jack_get(p, state->section))) {
+ pa_log("[%s:%u] state makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "yes"))
+ pa = PA_AVAILABLE_YES;
+ else if (pa_streq(state->rvalue, "no"))
+ pa = PA_AVAILABLE_NO;
+ else if (pa_streq(state->rvalue, "unknown"))
+ pa = PA_AVAILABLE_UNKNOWN;
+ else {
+ pa_log("[%s:%u] state must be 'yes', 'no' or 'unknown' in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "state.unplugged"))
+ j->state_unplugged = pa;
+ else {
+ j->state_plugged = pa;
+ pa_assert(pa_streq(state->lvalue, "state.plugged"));
+ }
+
+ return 0;
+}
+
+static int jack_parse_append_pcm_to_name(pa_config_parser_state *state) {
+ pa_alsa_path *path;
+ pa_alsa_jack *jack;
+ int b;
+
+ pa_assert(state);
+
+ path = state->userdata;
+ if (!(jack = jack_get(path, state->section))) {
+ pa_log("[%s:%u] Option 'append_pcm_to_name' not expected in section '%s'",
+ state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ b = pa_parse_boolean(state->rvalue);
+ if (b < 0) {
+ pa_log("[%s:%u] Invalid value for 'append_pcm_to_name': %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ jack->append_pcm_to_name = b;
+ return 0;
+}
+
+static int element_set_option(pa_alsa_element *e, snd_mixer_t *m, int alsa_idx) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+ char buf[64];
+ int r;
+
+ pa_assert(e);
+ pa_assert(m);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ if (e->switch_use == PA_ALSA_SWITCH_SELECT) {
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_set_playback_switch_all(me, alsa_idx);
+ else
+ r = snd_mixer_selem_set_capture_switch_all(me, alsa_idx);
+
+ if (r < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to set switch of %s: %s", buf, pa_alsa_strerror(errno));
+ }
+
+ } else {
+ pa_assert(e->enumeration_use == PA_ALSA_ENUMERATION_SELECT);
+
+ if ((r = snd_mixer_selem_set_enum_item(me, 0, alsa_idx)) < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to set enumeration of %s: %s", buf, pa_alsa_strerror(errno));
+ }
+ }
+
+ return r;
+}
+
+static int setting_select(pa_alsa_setting *s, snd_mixer_t *m) {
+ pa_alsa_option *o;
+ uint32_t idx;
+
+ pa_assert(s);
+ pa_assert(m);
+
+ PA_IDXSET_FOREACH(o, s->options, idx)
+ element_set_option(o->element, m, o->alsa_idx);
+
+ return 0;
+}
+
+static int option_verify(pa_alsa_option *o) {
+ static const struct description_map well_known_descriptions[] = {
+ { "input", N_("Input") },
+ { "input-docking", N_("Docking Station Input") },
+ { "input-docking-microphone", N_("Docking Station Microphone") },
+ { "input-docking-linein", N_("Docking Station Line In") },
+ { "input-linein", N_("Line In") },
+ { "input-microphone", N_("Microphone") },
+ { "input-microphone-front", N_("Front Microphone") },
+ { "input-microphone-rear", N_("Rear Microphone") },
+ { "input-microphone-external", N_("External Microphone") },
+ { "input-microphone-internal", N_("Internal Microphone") },
+ { "input-radio", N_("Radio") },
+ { "input-video", N_("Video") },
+ { "input-agc-on", N_("Automatic Gain Control") },
+ { "input-agc-off", N_("No Automatic Gain Control") },
+ { "input-boost-on", N_("Boost") },
+ { "input-boost-off", N_("No Boost") },
+ { "output-amplifier-on", N_("Amplifier") },
+ { "output-amplifier-off", N_("No Amplifier") },
+ { "output-bass-boost-on", N_("Bass Boost") },
+ { "output-bass-boost-off", N_("No Bass Boost") },
+ { "output-speaker", N_("Speaker") },
+ { "output-headphones", N_("Headphones") }
+ };
+ char buf[64];
+
+ pa_assert(o);
+
+ if (!o->name) {
+ pa_log("No name set for option %s", o->alsa_name);
+ return -1;
+ }
+
+ if (o->element->enumeration_use != PA_ALSA_ENUMERATION_SELECT &&
+ o->element->switch_use != PA_ALSA_SWITCH_SELECT) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &o->element->alsa_id);
+ pa_log("Element %s of option %s not set for select.", buf, o->name);
+ return -1;
+ }
+
+ if (o->element->switch_use == PA_ALSA_SWITCH_SELECT &&
+ !pa_streq(o->alsa_name, "on") &&
+ !pa_streq(o->alsa_name, "off")) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &o->element->alsa_id);
+ pa_log("Switch %s options need be named off or on ", buf);
+ return -1;
+ }
+
+ if (!o->description)
+ o->description = pa_xstrdup(lookup_description(o->name,
+ well_known_descriptions,
+ PA_ELEMENTSOF(well_known_descriptions)));
+ if (!o->description)
+ o->description = pa_xstrdup(o->name);
+
+ return 0;
+}
+
+static int element_verify(pa_alsa_element *e) {
+ pa_alsa_option *o;
+ char buf[64];
+
+ pa_assert(e);
+
+// pa_log_debug("Element %s, path %s: r=%d, r-any=%d, r-abs=%d", e->alsa_name, e->path->name, e->required, e->required_any, e->required_absent);
+ if ((e->required != PA_ALSA_REQUIRED_IGNORE && e->required == e->required_absent) ||
+ (e->required_any != PA_ALSA_REQUIRED_IGNORE && e->required_any == e->required_absent) ||
+ (e->required_absent == PA_ALSA_REQUIRED_ANY && e->required_any != PA_ALSA_REQUIRED_IGNORE) ||
+ (e->required_absent == PA_ALSA_REQUIRED_ANY && e->required != PA_ALSA_REQUIRED_IGNORE)) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log("Element %s cannot be required and absent at the same time.", buf);
+ return -1;
+ }
+
+ if (e->switch_use == PA_ALSA_SWITCH_SELECT && e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log("Element %s cannot set select for both switch and enumeration.", buf);
+ return -1;
+ }
+
+ PA_LLIST_FOREACH(o, e->options)
+ if (option_verify(o) < 0)
+ return -1;
+
+ return 0;
+}
+
+static int path_verify(pa_alsa_path *p) {
+ static const struct description2_map well_known_descriptions[] = {
+ { "analog-input", N_("Analog Input"), PA_DEVICE_PORT_TYPE_ANALOG },
+ { "analog-input-microphone", N_("Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-front", N_("Front Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-rear", N_("Rear Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-dock", N_("Dock Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-internal", N_("Internal Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-headset", N_("Headset Microphone"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "analog-input-linein", N_("Line In"), PA_DEVICE_PORT_TYPE_LINE },
+ { "analog-input-radio", N_("Radio"), PA_DEVICE_PORT_TYPE_RADIO },
+ { "analog-input-video", N_("Video"), PA_DEVICE_PORT_TYPE_VIDEO },
+ { "analog-output", N_("Analog Output"), PA_DEVICE_PORT_TYPE_ANALOG },
+ { "analog-output-headphones", N_("Headphones"), PA_DEVICE_PORT_TYPE_HEADPHONES },
+ { "analog-output-headphones-2", N_("Headphones 2"), PA_DEVICE_PORT_TYPE_HEADPHONES },
+ { "analog-output-headphones-mono", N_("Headphones Mono Output"), PA_DEVICE_PORT_TYPE_HEADPHONES },
+ { "analog-output-lineout", N_("Line Out"), PA_DEVICE_PORT_TYPE_LINE },
+ { "analog-output-mono", N_("Analog Mono Output"), PA_DEVICE_PORT_TYPE_ANALOG },
+ { "analog-output-speaker", N_("Speakers"), PA_DEVICE_PORT_TYPE_SPEAKER },
+ { "hdmi-output", N_("HDMI / DisplayPort"), PA_DEVICE_PORT_TYPE_HDMI },
+ { "iec958-stereo-output", N_("Digital Output (S/PDIF)"), PA_DEVICE_PORT_TYPE_SPDIF },
+ { "iec958-stereo-input", N_("Digital Input (S/PDIF)"), PA_DEVICE_PORT_TYPE_SPDIF },
+ { "multichannel-input", N_("Multichannel Input"), PA_DEVICE_PORT_TYPE_LINE },
+ { "multichannel-output", N_("Multichannel Output"), PA_DEVICE_PORT_TYPE_LINE },
+ { "steelseries-arctis-output-game-common", N_("Game Output"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "steelseries-arctis-output-chat-common", N_("Chat Output"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "analog-chat-output", N_("Chat Output"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "analog-chat-input", N_("Chat Input"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "virtual-surround-7.1", N_("Virtual Surround 7.1"), PA_DEVICE_PORT_TYPE_HEADPHONES },
+ };
+
+ pa_alsa_element *e;
+ const char *key = p->description_key ? p->description_key : p->name;
+ const struct description2_map *map = lookup_description2(key,
+ well_known_descriptions,
+ PA_ELEMENTSOF(well_known_descriptions));
+
+ pa_assert(p);
+
+ PA_LLIST_FOREACH(e, p->elements)
+ if (element_verify(e) < 0)
+ return -1;
+
+ if (map) {
+ if (p->device_port_type == PA_DEVICE_PORT_TYPE_UNKNOWN)
+ p->device_port_type = map->type;
+ if (!p->description)
+ p->description = pa_xstrdup(_(map->description));
+ }
+
+ if (!p->description) {
+ if (p->description_key)
+ pa_log_warn("Path %s: Unrecognized description key: %s", p->name, p->description_key);
+
+ p->description = pa_xstrdup(p->name);
+ }
+
+ return 0;
+}
+
+static const char *get_default_paths_dir(void) {
+ const char *str;
+#ifdef HAVE_RUNNING_FROM_BUILD_TREE
+ if (pa_run_from_build_tree())
+ return PA_SRCDIR "mixer/paths";
+ else
+#endif
+ if (getenv("ACP_BUILDDIR") != NULL)
+ return "mixer/paths";
+ if ((str = getenv("ACP_PATHS_DIR")) != NULL)
+ return str;
+ return PA_ALSA_PATHS_DIR;
+}
+
+pa_alsa_path* pa_alsa_path_new(const char *paths_dir, const char *fname, pa_alsa_direction_t direction) {
+ pa_alsa_path *p;
+ char *fn;
+ int r;
+ const char *n;
+ bool mute_during_activation = false;
+
+ pa_config_item items[] = {
+ /* [General] */
+ { "priority", pa_config_parse_unsigned, NULL, "General" },
+ { "description-key", pa_config_parse_string, NULL, "General" },
+ { "description", pa_config_parse_string, NULL, "General" },
+ { "mute-during-activation", pa_config_parse_bool, NULL, "General" },
+ { "type", parse_type, NULL, "General" },
+ { "eld-device", parse_eld_device, NULL, "General" },
+
+ /* [Option ...] */
+ { "priority", option_parse_priority, NULL, NULL },
+ { "name", option_parse_name, NULL, NULL },
+
+ /* [Jack ...] */
+ { "state.plugged", jack_parse_state, NULL, NULL },
+ { "state.unplugged", jack_parse_state, NULL, NULL },
+ { "append-pcm-to-name", jack_parse_append_pcm_to_name, NULL, NULL },
+
+ /* [Element ...] */
+ { "switch", element_parse_switch, NULL, NULL },
+ { "volume", element_parse_volume, NULL, NULL },
+ { "enumeration", element_parse_enumeration, NULL, NULL },
+ { "override-map.1", element_parse_override_map, NULL, NULL },
+ { "override-map.2", element_parse_override_map, NULL, NULL },
+ { "override-map.3", element_parse_override_map, NULL, NULL },
+ { "override-map.4", element_parse_override_map, NULL, NULL },
+ { "override-map.5", element_parse_override_map, NULL, NULL },
+ { "override-map.6", element_parse_override_map, NULL, NULL },
+ { "override-map.7", element_parse_override_map, NULL, NULL },
+ { "override-map.8", element_parse_override_map, NULL, NULL },
+#if POSITION_MASK_CHANNELS > 8
+#error "Add override-map.9+ definitions"
+#endif
+ /* ... later on we might add override-map.3 and so on here ... */
+ { "required", element_parse_required, NULL, NULL },
+ { "required-any", element_parse_required, NULL, NULL },
+ { "required-absent", element_parse_required, NULL, NULL },
+ { "direction", element_parse_direction, NULL, NULL },
+ { "direction-try-other", element_parse_direction_try_other, NULL, NULL },
+ { "volume-limit", element_parse_volume_limit, NULL, NULL },
+ { NULL, NULL, NULL, NULL }
+ };
+
+ pa_assert(fname);
+
+ p = pa_xnew0(pa_alsa_path, 1);
+ n = pa_path_get_filename(fname);
+ p->name = pa_xstrndup(n, strcspn(n, "."));
+ p->proplist = pa_proplist_new();
+ p->direction = direction;
+ p->eld_device = -1;
+
+ items[0].data = &p->priority;
+ items[1].data = &p->description_key;
+ items[2].data = &p->description;
+ items[3].data = &mute_during_activation;
+
+ if (!paths_dir)
+ paths_dir = get_default_paths_dir();
+
+ fn = pa_maybe_prefix_path(fname, paths_dir);
+
+ r = pa_config_parse(fn, NULL, items, p->proplist, false, p);
+ pa_xfree(fn);
+
+ if (r < 0)
+ goto fail;
+
+ p->mute_during_activation = mute_during_activation;
+
+ if (path_verify(p) < 0)
+ goto fail;
+
+ if (p->description) {
+ char *tmp = p->description;
+ p->description = pa_xstrdup(_(tmp));
+ free(tmp);
+ }
+
+ return p;
+
+fail:
+ pa_alsa_path_free(p);
+ return NULL;
+}
+
+pa_alsa_path *pa_alsa_path_synthesize(const char *element, pa_alsa_direction_t direction) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ char *name;
+ int index;
+
+ pa_assert(element);
+
+ name = alloca(strlen(element) + 1);
+ if (alsa_id_decode(element, name, &index))
+ return NULL;
+
+ p = pa_xnew0(pa_alsa_path, 1);
+ p->name = pa_xstrdup(element);
+ p->direction = direction;
+ p->proplist = pa_proplist_new();
+
+ e = pa_xnew0(pa_alsa_element, 1);
+ e->path = p;
+ e->alsa_id.name = pa_xstrdup(name);
+ e->alsa_id.index = index;
+ e->direction = direction;
+ e->volume_limit = -1;
+
+ e->switch_use = PA_ALSA_SWITCH_MUTE;
+ e->volume_use = PA_ALSA_VOLUME_MERGE;
+
+ PA_LLIST_PREPEND(pa_alsa_element, p->elements, e);
+ p->last_element = e;
+ return p;
+}
+
+static bool element_drop_unsupported(pa_alsa_element *e) {
+ pa_alsa_option *o, *n;
+
+ pa_assert(e);
+
+ for (o = e->options; o; o = n) {
+ n = o->next;
+
+ if (o->alsa_idx < 0) {
+ PA_LLIST_REMOVE(pa_alsa_option, e->options, o);
+ option_free(o);
+ }
+ }
+
+ return
+ e->switch_use != PA_ALSA_SWITCH_IGNORE ||
+ e->volume_use != PA_ALSA_VOLUME_IGNORE ||
+ e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE;
+}
+
+static void path_drop_unsupported(pa_alsa_path *p) {
+ pa_alsa_element *e, *n;
+
+ pa_assert(p);
+
+ for (e = p->elements; e; e = n) {
+ n = e->next;
+
+ if (!element_drop_unsupported(e)) {
+ PA_LLIST_REMOVE(pa_alsa_element, p->elements, e);
+ element_free(e);
+ }
+ }
+}
+
+static void path_make_options_unique(pa_alsa_path *p) {
+ pa_alsa_element *e;
+ pa_alsa_option *o, *u;
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ PA_LLIST_FOREACH(o, e->options) {
+ unsigned i;
+ char *m;
+
+ for (u = o->next; u; u = u->next)
+ if (pa_streq(u->name, o->name))
+ break;
+
+ if (!u)
+ continue;
+
+ m = pa_xstrdup(o->name);
+
+ /* OK, this name is not unique, hence let's rename */
+ for (i = 1, u = o; u; u = u->next) {
+ char *nn, *nd;
+
+ if (!pa_streq(u->name, m))
+ continue;
+
+ nn = pa_sprintf_malloc("%s-%u", m, i);
+ pa_xfree(u->name);
+ u->name = nn;
+
+ nd = pa_sprintf_malloc("%s %u", u->description, i);
+ pa_xfree(u->description);
+ u->description = nd;
+
+ i++;
+ }
+
+ pa_xfree(m);
+ }
+ }
+}
+
+static bool element_create_settings(pa_alsa_element *e, pa_alsa_setting *template) {
+ pa_alsa_option *o;
+
+ for (; e; e = e->next)
+ if (e->switch_use == PA_ALSA_SWITCH_SELECT ||
+ e->enumeration_use == PA_ALSA_ENUMERATION_SELECT)
+ break;
+
+ if (!e)
+ return false;
+
+ for (o = e->options; o; o = o->next) {
+ pa_alsa_setting *s;
+
+ if (template) {
+ s = pa_xnewdup(pa_alsa_setting, template, 1);
+ s->options = pa_idxset_copy(template->options, NULL);
+ s->name = pa_sprintf_malloc("%s+%s", template->name, o->name);
+ s->description =
+ (template->description[0] && o->description[0])
+ ? pa_sprintf_malloc("%s / %s", template->description, o->description)
+ : (template->description[0]
+ ? pa_xstrdup(template->description)
+ : pa_xstrdup(o->description));
+
+ s->priority = PA_MAX(template->priority, o->priority);
+ } else {
+ s = pa_xnew0(pa_alsa_setting, 1);
+ s->options = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ s->name = pa_xstrdup(o->name);
+ s->description = pa_xstrdup(o->description);
+ s->priority = o->priority;
+ }
+
+ pa_idxset_put(s->options, o, NULL);
+
+ if (element_create_settings(e->next, s))
+ /* This is not a leaf, so let's get rid of it */
+ setting_free(s);
+ else {
+ /* This is a leaf, so let's add it */
+ PA_LLIST_INSERT_AFTER(pa_alsa_setting, e->path->settings, e->path->last_setting, s);
+
+ e->path->last_setting = s;
+ }
+ }
+
+ return true;
+}
+
+static void path_create_settings(pa_alsa_path *p) {
+ pa_assert(p);
+
+ element_create_settings(p->elements, NULL);
+}
+
+int pa_alsa_path_probe(pa_alsa_path *p, pa_alsa_mapping *mapping, snd_mixer_t *m, bool ignore_dB) {
+ pa_alsa_element *e;
+ pa_alsa_jack *j;
+ double min_dB[PA_CHANNEL_POSITION_MAX], max_dB[PA_CHANNEL_POSITION_MAX];
+ pa_channel_position_t t;
+ pa_channel_position_mask_t path_volume_channels = 0;
+ bool min_dB_set, max_dB_set;
+ char buf[64];
+
+ pa_assert(p);
+ pa_assert(m);
+
+ if (p->probed)
+ return p->supported ? 0 : -1;
+ p->probed = true;
+
+ pa_zero(min_dB);
+ pa_zero(max_dB);
+
+ pa_log_debug("Probing path '%s'", p->name);
+
+ PA_LLIST_FOREACH(j, p->jacks) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &j->alsa_id);
+ if (jack_probe(j, mapping, m) < 0) {
+ p->supported = false;
+ pa_log_debug("Probe of jack %s failed.", buf);
+ return -1;
+ }
+ pa_log_debug("Probe of jack %s succeeded (%s)", buf, j->has_control ? "found!" : "not found");
+ }
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ if (element_probe(e, m) < 0) {
+ p->supported = false;
+ pa_log_debug("Probe of element %s failed.", buf);
+ return -1;
+ }
+ pa_log_debug("Probe of element %s succeeded (volume=%d, switch=%d, enumeration=%d, has_dB=%d).", buf, e->volume_use, e->switch_use, e->enumeration_use, e->has_dB);
+
+ if (ignore_dB)
+ e->has_dB = false;
+
+ if (e->volume_use == PA_ALSA_VOLUME_MERGE) {
+
+ if (!p->has_volume) {
+ p->min_volume = e->min_volume;
+ p->max_volume = e->max_volume;
+ }
+
+ if (e->has_dB) {
+ if (!p->has_volume) {
+ for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++)
+ if (PA_CHANNEL_POSITION_MASK(t) & e->merged_mask) {
+ min_dB[t] = e->min_dB;
+ max_dB[t] = e->max_dB;
+ path_volume_channels |= PA_CHANNEL_POSITION_MASK(t);
+ }
+
+ p->has_dB = true;
+ } else {
+
+ if (p->has_dB) {
+ for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++)
+ if (PA_CHANNEL_POSITION_MASK(t) & e->merged_mask) {
+ min_dB[t] += e->min_dB;
+ max_dB[t] += e->max_dB;
+ path_volume_channels |= PA_CHANNEL_POSITION_MASK(t);
+ }
+ } else {
+ /* Hmm, there's another element before us
+ * which cannot do dB volumes, so we we need
+ * to 'neutralize' this slider */
+ e->volume_use = PA_ALSA_VOLUME_ZERO;
+ pa_log_info("Zeroing volume of %s on path '%s'", buf, p->name);
+ }
+ }
+ } else if (p->has_volume) {
+ /* We can't use this volume, so let's ignore it */
+ e->volume_use = PA_ALSA_VOLUME_IGNORE;
+ pa_log_info("Ignoring volume of %s on path '%s' (missing dB info)", buf, p->name);
+ }
+ p->has_volume = true;
+ }
+
+ if (e->switch_use == PA_ALSA_SWITCH_MUTE)
+ p->has_mute = true;
+ }
+
+ if (p->has_req_any && !p->req_any_present) {
+ p->supported = false;
+ pa_log_debug("Skipping path '%s', none of required-any elements preset.", p->name);
+ return -1;
+ }
+
+ path_drop_unsupported(p);
+ path_make_options_unique(p);
+ path_create_settings(p);
+
+ p->supported = true;
+
+ p->min_dB = INFINITY;
+ min_dB_set = false;
+ p->max_dB = -INFINITY;
+ max_dB_set = false;
+
+ for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++) {
+ if (path_volume_channels & PA_CHANNEL_POSITION_MASK(t)) {
+ if (p->min_dB > min_dB[t]) {
+ p->min_dB = min_dB[t];
+ min_dB_set = true;
+ }
+
+ if (p->max_dB < max_dB[t]) {
+ p->max_dB = max_dB[t];
+ max_dB_set = true;
+ }
+ }
+ }
+
+ /* this is probably a wrong prediction, but it should be safe */
+ if (!min_dB_set)
+ p->min_dB = -INFINITY;
+ if (!max_dB_set)
+ p->max_dB = 0;
+
+ return 0;
+}
+
+void pa_alsa_setting_dump(pa_alsa_setting *s) {
+ pa_assert(s);
+
+ pa_log_debug("Setting %s (%s) priority=%u",
+ s->name,
+ pa_strnull(s->description),
+ s->priority);
+}
+
+void pa_alsa_jack_dump(pa_alsa_jack *j) {
+ pa_assert(j);
+
+ pa_log_debug("Jack %s, alsa_name='%s', index='%d', detection %s", j->name, j->alsa_id.name, j->alsa_id.index, j->has_control ? "possible" : "unavailable");
+}
+
+void pa_alsa_option_dump(pa_alsa_option *o) {
+ pa_assert(o);
+
+ pa_log_debug("Option %s (%s/%s) index=%i, priority=%u",
+ o->alsa_name,
+ pa_strnull(o->name),
+ pa_strnull(o->description),
+ o->alsa_idx,
+ o->priority);
+}
+
+void pa_alsa_element_dump(pa_alsa_element *e) {
+ char buf[64];
+
+ pa_alsa_option *o;
+ pa_assert(e);
+
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Element %s, direction=%i, switch=%i, volume=%i, volume_limit=%li, enumeration=%i, required=%i, required_any=%i, required_absent=%i, mask=0x%llx, n_channels=%u, override_map=%02x",
+ buf,
+ e->direction,
+ e->switch_use,
+ e->volume_use,
+ e->volume_limit,
+ e->enumeration_use,
+ e->required,
+ e->required_any,
+ e->required_absent,
+ (long long unsigned) e->merged_mask,
+ e->n_channels,
+ e->override_map);
+
+ PA_LLIST_FOREACH(o, e->options)
+ pa_alsa_option_dump(o);
+}
+
+void pa_alsa_path_dump(pa_alsa_path *p) {
+ pa_alsa_element *e;
+ pa_alsa_jack *j;
+ pa_alsa_setting *s;
+ pa_assert(p);
+
+ pa_log_debug("Path %s (%s), direction=%i, priority=%u, probed=%s, supported=%s, has_mute=%s, has_volume=%s, "
+ "has_dB=%s, min_volume=%li, max_volume=%li, min_dB=%g, max_dB=%g",
+ p->name,
+ pa_strnull(p->description),
+ p->direction,
+ p->priority,
+ pa_yes_no(p->probed),
+ pa_yes_no(p->supported),
+ pa_yes_no(p->has_mute),
+ pa_yes_no(p->has_volume),
+ pa_yes_no(p->has_dB),
+ p->min_volume, p->max_volume,
+ p->min_dB, p->max_dB);
+
+ PA_LLIST_FOREACH(e, p->elements)
+ pa_alsa_element_dump(e);
+
+ PA_LLIST_FOREACH(j, p->jacks)
+ pa_alsa_jack_dump(j);
+
+ PA_LLIST_FOREACH(s, p->settings)
+ pa_alsa_setting_dump(s);
+}
+
+static void element_set_callback(pa_alsa_element *e, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+ char buf[64];
+
+ pa_assert(e);
+ pa_assert(m);
+ pa_assert(cb);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return;
+ }
+
+ snd_mixer_elem_set_callback(me, cb);
+ snd_mixer_elem_set_callback_private(me, userdata);
+}
+
+void pa_alsa_path_set_callback(pa_alsa_path *p, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) {
+ pa_alsa_element *e;
+
+ pa_assert(p);
+ pa_assert(m);
+ pa_assert(cb);
+
+ PA_LLIST_FOREACH(e, p->elements)
+ element_set_callback(e, m, cb, userdata);
+}
+
+void pa_alsa_path_set_set_callback(pa_alsa_path_set *ps, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) {
+ pa_alsa_path *p;
+ void *state;
+
+ pa_assert(ps);
+ pa_assert(m);
+ pa_assert(cb);
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state)
+ pa_alsa_path_set_callback(p, m, cb, userdata);
+}
+
+static pa_alsa_path *profile_set_get_path(pa_alsa_profile_set *ps, const char *path_name) {
+ pa_alsa_path *path;
+
+ pa_assert(ps);
+ pa_assert(path_name);
+
+ if ((path = pa_hashmap_get(ps->output_paths, path_name)))
+ return path;
+
+ return pa_hashmap_get(ps->input_paths, path_name);
+}
+
+static void profile_set_add_path(pa_alsa_profile_set *ps, pa_alsa_path *path) {
+ pa_assert(ps);
+ pa_assert(path);
+
+ switch (path->direction) {
+ case PA_ALSA_DIRECTION_OUTPUT:
+ pa_assert_se(pa_hashmap_put(ps->output_paths, path->name, path) >= 0);
+ break;
+
+ case PA_ALSA_DIRECTION_INPUT:
+ pa_assert_se(pa_hashmap_put(ps->input_paths, path->name, path) >= 0);
+ break;
+
+ default:
+ pa_assert_not_reached();
+ }
+}
+
+pa_alsa_path_set *pa_alsa_path_set_new(pa_alsa_mapping *m, pa_alsa_direction_t direction, const char *paths_dir) {
+ pa_alsa_path_set *ps;
+ char **pn = NULL, **en = NULL, **ie;
+ pa_alsa_decibel_fix *db_fix;
+ void *state, *state2;
+ char name[64];
+ int index;
+
+ pa_assert(m);
+ pa_assert(m->profile_set);
+ pa_assert(m->profile_set->decibel_fixes);
+ pa_assert(direction == PA_ALSA_DIRECTION_OUTPUT || direction == PA_ALSA_DIRECTION_INPUT);
+
+ if (m->direction != PA_ALSA_DIRECTION_ANY && m->direction != direction)
+ return NULL;
+
+ ps = pa_xnew0(pa_alsa_path_set, 1);
+ ps->direction = direction;
+ ps->paths = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ if (direction == PA_ALSA_DIRECTION_OUTPUT)
+ pn = m->output_path_names;
+ else
+ pn = m->input_path_names;
+
+ if (pn) {
+ char **in;
+
+ for (in = pn; *in; in++) {
+ pa_alsa_path *p = NULL;
+ bool duplicate = false;
+ char **kn;
+
+ for (kn = pn; kn < in; kn++)
+ if (pa_streq(*kn, *in)) {
+ duplicate = true;
+ break;
+ }
+
+ if (duplicate)
+ continue;
+
+ p = profile_set_get_path(m->profile_set, *in);
+
+ if (p && p->direction != direction) {
+ pa_log("Configuration error: Path %s is used both as an input and as an output path.", p->name);
+ goto fail;
+ }
+
+ if (!p) {
+ char *fn = pa_sprintf_malloc("%s.conf", *in);
+ p = pa_alsa_path_new(paths_dir, fn, direction);
+ pa_xfree(fn);
+ if (p)
+ profile_set_add_path(m->profile_set, p);
+ }
+
+ if (p)
+ pa_hashmap_put(ps->paths, p, p);
+
+ }
+
+ goto finish;
+ }
+
+ if (direction == PA_ALSA_DIRECTION_OUTPUT)
+ en = m->output_element;
+ else
+ en = m->input_element;
+
+ if (!en)
+ goto fail;
+
+ for (ie = en; *ie; ie++) {
+ char **je;
+ pa_alsa_path *p;
+
+ p = pa_alsa_path_synthesize(*ie, direction);
+
+ /* Mark all other passed elements for require-absent */
+ for (je = en; *je; je++) {
+ pa_alsa_element *e;
+
+ if (je == ie)
+ continue;
+
+ if (strlen(*je) + 1 >= sizeof(name)) {
+ pa_log("Element identifier %s is too long!", *je);
+ continue;
+ }
+
+ if (alsa_id_decode(*je, name, &index))
+ continue;
+
+ e = pa_xnew0(pa_alsa_element, 1);
+ e->path = p;
+ e->alsa_id.name = pa_xstrdup(name);
+ e->alsa_id.index = index;
+ e->direction = direction;
+ e->required_absent = PA_ALSA_REQUIRED_ANY;
+ e->volume_limit = -1;
+
+ PA_LLIST_INSERT_AFTER(pa_alsa_element, p->elements, p->last_element, e);
+ p->last_element = e;
+ }
+
+ pa_hashmap_put(ps->paths, *ie, p);
+ }
+
+finish:
+ /* Assign decibel fixes to elements. */
+ PA_HASHMAP_FOREACH(db_fix, m->profile_set->decibel_fixes, state) {
+ pa_alsa_path *p;
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state2) {
+ pa_alsa_element *e;
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ if (e->volume_use != PA_ALSA_VOLUME_IGNORE && pa_streq(db_fix->name, e->alsa_id.name) &&
+ db_fix->index == e->alsa_id.index) {
+ /* The profile set that contains the dB fix may be freed
+ * before the element, so we have to copy the dB fix
+ * object. */
+ e->db_fix = pa_xnewdup(pa_alsa_decibel_fix, db_fix, 1);
+ e->db_fix->profile_set = NULL;
+ e->db_fix->key = pa_xstrdup(db_fix->key);
+ e->db_fix->name = pa_xstrdup(db_fix->name);
+ e->db_fix->db_values = pa_xmemdup(db_fix->db_values, (db_fix->max_step - db_fix->min_step + 1) * sizeof(long));
+ }
+ }
+ }
+ }
+
+ return ps;
+
+fail:
+ if (ps)
+ pa_alsa_path_set_free(ps);
+
+ return NULL;
+}
+
+void pa_alsa_path_set_dump(pa_alsa_path_set *ps) {
+ pa_alsa_path *p;
+ void *state;
+ pa_assert(ps);
+
+ pa_log_debug("Path Set %p, direction=%i",
+ (void*) ps,
+ ps->direction);
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state)
+ pa_alsa_path_dump(p);
+}
+
+static bool options_have_option(pa_alsa_option *options, const char *alsa_name) {
+ pa_alsa_option *o;
+
+ pa_assert(options);
+ pa_assert(alsa_name);
+
+ PA_LLIST_FOREACH(o, options) {
+ if (pa_streq(o->alsa_name, alsa_name))
+ return true;
+ }
+ return false;
+}
+
+static bool enumeration_is_subset(pa_alsa_option *a_options, pa_alsa_option *b_options) {
+ pa_alsa_option *oa, *ob;
+
+ if (!a_options) return true;
+ if (!b_options) return false;
+
+ /* If there is an option A offers that B does not, then A is not a subset of B. */
+ PA_LLIST_FOREACH(oa, a_options) {
+ bool found = false;
+ PA_LLIST_FOREACH(ob, b_options) {
+ if (pa_streq(oa->alsa_name, ob->alsa_name)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Compares two elements to see if a is a subset of b
+ */
+static bool element_is_subset(pa_alsa_element *a, pa_alsa_element *b, snd_mixer_t *m) {
+ char buf[64];
+
+ pa_assert(a);
+ pa_assert(b);
+ pa_assert(m);
+
+ /* General rules:
+ * Every state is a subset of itself (with caveats for volume_limits and options)
+ * IGNORE is a subset of every other state */
+
+ /* Check the volume_use */
+ if (a->volume_use != PA_ALSA_VOLUME_IGNORE) {
+
+ /* "Constant" is subset of "Constant" only when their constant values are equal */
+ if (a->volume_use == PA_ALSA_VOLUME_CONSTANT && b->volume_use == PA_ALSA_VOLUME_CONSTANT && a->constant_volume != b->constant_volume)
+ return false;
+
+ /* Different volume uses when b is not "Merge" means we are definitely not a subset */
+ if (a->volume_use != b->volume_use && b->volume_use != PA_ALSA_VOLUME_MERGE)
+ return false;
+
+ /* "Constant" is a subset of "Merge", if there is not a "volume-limit" in "Merge" below the actual constant.
+ * "Zero" and "Off" are just special cases of "Constant" when comparing to "Merge"
+ * "Merge" with a "volume-limit" is a subset of "Merge" without a "volume-limit" or with a higher "volume-limit" */
+ if (b->volume_use == PA_ALSA_VOLUME_MERGE && b->volume_limit >= 0) {
+ long a_limit;
+
+ if (a->volume_use == PA_ALSA_VOLUME_CONSTANT)
+ a_limit = a->constant_volume;
+ else if (a->volume_use == PA_ALSA_VOLUME_ZERO) {
+ long dB = 0;
+
+ if (a->db_fix) {
+ int rounding = (a->direction == PA_ALSA_DIRECTION_OUTPUT ? +1 : -1);
+ a_limit = decibel_fix_get_step(a->db_fix, &dB, rounding);
+ } else {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+
+ SELEM_INIT(sid, &a->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &a->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return false;
+ }
+
+ if (a->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_ask_playback_dB_vol(me, dB, +1, &a_limit) < 0)
+ return false;
+ } else {
+ if (snd_mixer_selem_ask_capture_dB_vol(me, dB, -1, &a_limit) < 0)
+ return false;
+ }
+ }
+ } else if (a->volume_use == PA_ALSA_VOLUME_OFF)
+ a_limit = a->min_volume;
+ else if (a->volume_use == PA_ALSA_VOLUME_MERGE)
+ a_limit = a->volume_limit;
+ else
+ pa_assert_not_reached();
+
+ if (a_limit > b->volume_limit)
+ return false;
+ }
+
+ if (a->volume_use == PA_ALSA_VOLUME_MERGE) {
+ int s;
+ /* If override-maps are different, they're not subsets */
+ if (a->n_channels != b->n_channels)
+ return false;
+ for (s = 0; s <= SND_MIXER_SCHN_LAST; s++)
+ if (a->masks[s][a->n_channels-1] != b->masks[s][b->n_channels-1]) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &a->alsa_id);
+ pa_log_debug("Element %s is not a subset - mask a: 0x%" PRIx64 ", mask b: 0x%" PRIx64 ", at channel %d",
+ buf, a->masks[s][a->n_channels-1], b->masks[s][b->n_channels-1], s);
+ return false;
+ }
+ }
+ }
+
+ if (a->switch_use != PA_ALSA_SWITCH_IGNORE) {
+ /* "On" is a subset of "Mute".
+ * "Off" is a subset of "Mute".
+ * "On" is a subset of "Select", if there is an "Option:On" in B.
+ * "Off" is a subset of "Select", if there is an "Option:Off" in B.
+ * "Select" is a subset of "Select", if they have the same options (is this always true?). */
+
+ if (a->switch_use != b->switch_use) {
+
+ if (a->switch_use == PA_ALSA_SWITCH_SELECT || a->switch_use == PA_ALSA_SWITCH_MUTE
+ || b->switch_use == PA_ALSA_SWITCH_OFF || b->switch_use == PA_ALSA_SWITCH_ON)
+ return false;
+
+ if (b->switch_use == PA_ALSA_SWITCH_SELECT) {
+ if (a->switch_use == PA_ALSA_SWITCH_ON) {
+ if (!options_have_option(b->options, "on"))
+ return false;
+ } else if (a->switch_use == PA_ALSA_SWITCH_OFF) {
+ if (!options_have_option(b->options, "off"))
+ return false;
+ }
+ }
+ } else if (a->switch_use == PA_ALSA_SWITCH_SELECT) {
+ if (!enumeration_is_subset(a->options, b->options))
+ return false;
+ }
+ }
+
+ if (a->enumeration_use != PA_ALSA_ENUMERATION_IGNORE) {
+ if (b->enumeration_use == PA_ALSA_ENUMERATION_IGNORE)
+ return false;
+ if (!enumeration_is_subset(a->options, b->options))
+ return false;
+ }
+
+ return true;
+}
+
+static void path_set_condense(pa_alsa_path_set *ps, snd_mixer_t *m) {
+ pa_alsa_path *p;
+ void *state;
+
+ pa_assert(ps);
+ pa_assert(m);
+
+ /* If we only have one path, then don't bother */
+ if (pa_hashmap_size(ps->paths) < 2)
+ return;
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state) {
+ pa_alsa_path *p2;
+ void *state2;
+
+ PA_HASHMAP_FOREACH(p2, ps->paths, state2) {
+ pa_alsa_element *ea, *eb;
+ pa_alsa_jack *ja, *jb;
+ bool is_subset = true;
+
+ if (p == p2)
+ continue;
+
+ /* If a has a jack that b does not have, a is not a subset */
+ PA_LLIST_FOREACH(ja, p->jacks) {
+ bool exists = false;
+
+ if (!ja->has_control)
+ continue;
+
+ PA_LLIST_FOREACH(jb, p2->jacks) {
+ if (jb->has_control && pa_streq(ja->alsa_id.name, jb->alsa_id.name) &&
+ (ja->alsa_id.index == jb->alsa_id.index) &&
+ (ja->state_plugged == jb->state_plugged) &&
+ (ja->state_unplugged == jb->state_unplugged)) {
+ exists = true;
+ break;
+ }
+ }
+
+ if (!exists) {
+ is_subset = false;
+ break;
+ }
+ }
+
+ /* Compare the elements of each set... */
+ PA_LLIST_FOREACH(ea, p->elements) {
+ bool found_matching_element = false;
+
+ if (!is_subset)
+ break;
+
+ PA_LLIST_FOREACH(eb, p2->elements) {
+ if (pa_streq(ea->alsa_id.name, eb->alsa_id.name) &&
+ ea->alsa_id.index == eb->alsa_id.index) {
+ found_matching_element = true;
+ is_subset = element_is_subset(ea, eb, m);
+ break;
+ }
+ }
+
+ if (!found_matching_element)
+ is_subset = false;
+ }
+
+ if (is_subset) {
+ pa_log_debug("Removing path '%s' as it is a subset of '%s'.", p->name, p2->name);
+ pa_hashmap_remove(ps->paths, p);
+ break;
+ }
+ }
+ }
+}
+
+static pa_alsa_path* path_set_find_path_by_description(pa_alsa_path_set *ps, const char* description, pa_alsa_path *ignore) {
+ pa_alsa_path* p;
+ void *state;
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state)
+ if (p != ignore && pa_streq(p->description, description))
+ return p;
+
+ return NULL;
+}
+
+static void path_set_make_path_descriptions_unique(pa_alsa_path_set *ps) {
+ pa_alsa_path *p, *q;
+ void *state, *state2;
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state) {
+ unsigned i;
+ char *old_description;
+
+ q = path_set_find_path_by_description(ps, p->description, p);
+
+ if (!q)
+ continue;
+
+ old_description = pa_xstrdup(p->description);
+
+ /* OK, this description is not unique, hence let's rename */
+ i = 1;
+ PA_HASHMAP_FOREACH(q, ps->paths, state2) {
+ char *new_description;
+
+ if (!pa_streq(q->description, old_description))
+ continue;
+
+ new_description = pa_sprintf_malloc("%s %u", q->description, i);
+ pa_xfree(q->description);
+ q->description = new_description;
+
+ i++;
+ }
+
+ pa_xfree(old_description);
+ }
+}
+
+void pa_alsa_mapping_free(pa_alsa_mapping *m) {
+ pa_assert(m);
+
+ pa_xfree(m->name);
+ pa_xfree(m->description);
+ pa_xfree(m->description_key);
+
+ pa_proplist_free(m->proplist);
+
+ pa_xstrfreev(m->device_strings);
+ pa_xstrfreev(m->input_path_names);
+ pa_xstrfreev(m->output_path_names);
+ pa_xstrfreev(m->input_element);
+ pa_xstrfreev(m->output_element);
+ if (m->input_path_set)
+ pa_alsa_path_set_free(m->input_path_set);
+ if (m->output_path_set)
+ pa_alsa_path_set_free(m->output_path_set);
+
+ pa_proplist_free(m->input_proplist);
+ pa_proplist_free(m->output_proplist);
+
+ pa_assert(!m->input_pcm);
+ pa_assert(!m->output_pcm);
+
+ pa_alsa_ucm_mapping_context_free(&m->ucm_context);
+
+ pa_xfree(m);
+}
+
+void pa_alsa_profile_free(pa_alsa_profile *p) {
+ pa_assert(p);
+
+ pa_xfree(p->name);
+ pa_xfree(p->description);
+ pa_xfree(p->description_key);
+ pa_xfree(p->input_name);
+ pa_xfree(p->output_name);
+
+ pa_xstrfreev(p->input_mapping_names);
+ pa_xstrfreev(p->output_mapping_names);
+
+ if (p->input_mappings)
+ pa_idxset_free(p->input_mappings, NULL);
+
+ if (p->output_mappings)
+ pa_idxset_free(p->output_mappings, NULL);
+
+ pa_xfree(p);
+}
+
+void pa_alsa_profile_set_free(pa_alsa_profile_set *ps) {
+ pa_assert(ps);
+
+ if (ps->input_paths)
+ pa_hashmap_free(ps->input_paths);
+
+ if (ps->output_paths)
+ pa_hashmap_free(ps->output_paths);
+
+ if (ps->profiles)
+ pa_hashmap_free(ps->profiles);
+
+ if (ps->mappings)
+ pa_hashmap_free(ps->mappings);
+
+ if (ps->decibel_fixes)
+ pa_hashmap_free(ps->decibel_fixes);
+
+ pa_xfree(ps);
+}
+
+pa_alsa_mapping *pa_alsa_mapping_get(pa_alsa_profile_set *ps, const char *name) {
+ pa_alsa_mapping *m;
+
+ if (!pa_startswith(name, "Mapping "))
+ return NULL;
+
+ name += 8;
+
+ if ((m = pa_hashmap_get(ps->mappings, name)))
+ return m;
+
+ m = pa_xnew0(pa_alsa_mapping, 1);
+ m->profile_set = ps;
+ m->exact_channels = true;
+ m->name = pa_xstrdup(name);
+ pa_sample_spec_init(&m->sample_spec);
+ pa_channel_map_init(&m->channel_map);
+ m->proplist = pa_proplist_new();
+ m->hw_device_index = -1;
+ m->input_proplist = pa_proplist_new();
+ m->output_proplist = pa_proplist_new();
+
+ pa_hashmap_put(ps->mappings, m->name, m);
+
+ return m;
+}
+
+static pa_alsa_profile *profile_get(pa_alsa_profile_set *ps, const char *name) {
+ pa_alsa_profile *p;
+
+ if (!pa_startswith(name, "Profile "))
+ return NULL;
+
+ name += 8;
+
+ if ((p = pa_hashmap_get(ps->profiles, name)))
+ return p;
+
+ p = pa_xnew0(pa_alsa_profile, 1);
+ p->profile_set = ps;
+ p->name = pa_xstrdup(name);
+
+ pa_hashmap_put(ps->profiles, p->name, p);
+
+ return p;
+}
+
+static pa_alsa_decibel_fix *decibel_fix_get(pa_alsa_profile_set *ps, const char *alsa_id) {
+ pa_alsa_decibel_fix *db_fix;
+ char *name;
+ int index;
+
+ if (!pa_startswith(alsa_id, "DecibelFix "))
+ return NULL;
+
+ alsa_id += 11;
+
+ if ((db_fix = pa_hashmap_get(ps->decibel_fixes, alsa_id)))
+ return db_fix;
+
+ name = alloca(strlen(alsa_id) + 1);
+ if (alsa_id_decode(alsa_id, name, &index))
+ return NULL;
+
+ db_fix = pa_xnew0(pa_alsa_decibel_fix, 1);
+ db_fix->profile_set = ps;
+ db_fix->name = pa_xstrdup(name);
+ db_fix->index = index;
+ db_fix->key = pa_xstrdup(alsa_id);
+
+ pa_hashmap_put(ps->decibel_fixes, db_fix->key, db_fix);
+
+ return db_fix;
+}
+
+static int mapping_parse_device_strings(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ pa_xstrfreev(m->device_strings);
+ if (!(m->device_strings = pa_split_spaces_strv(state->rvalue))) {
+ pa_log("[%s:%u] Device string list empty of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_channel_map(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (!(pa_channel_map_parse(&m->channel_map, state->rvalue))) {
+ pa_log("[%s:%u] Channel map invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_paths(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "paths-input")) {
+ pa_xstrfreev(m->input_path_names);
+ m->input_path_names = pa_split_spaces_strv(state->rvalue);
+ } else {
+ pa_xstrfreev(m->output_path_names);
+ m->output_path_names = pa_split_spaces_strv(state->rvalue);
+ }
+
+ return 0;
+}
+
+static int mapping_parse_exact_channels(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+ int b;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if ((b = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] %s has invalid value '%s'", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ m->exact_channels = b;
+
+ return 0;
+}
+
+static int mapping_parse_element(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "element-input")) {
+ pa_xstrfreev(m->input_element);
+ m->input_element = pa_split_spaces_strv(state->rvalue);
+ } else {
+ pa_xstrfreev(m->output_element);
+ m->output_element = pa_split_spaces_strv(state->rvalue);
+ }
+
+ return 0;
+}
+
+static int mapping_parse_direction(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "input"))
+ m->direction = PA_ALSA_DIRECTION_INPUT;
+ else if (pa_streq(state->rvalue, "output"))
+ m->direction = PA_ALSA_DIRECTION_OUTPUT;
+ else if (pa_streq(state->rvalue, "any"))
+ m->direction = PA_ALSA_DIRECTION_ANY;
+ else {
+ pa_log("[%s:%u] Direction %s invalid.", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_description(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if ((m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_xfree(m->description);
+ m->description = pa_xstrdup(_(state->rvalue));
+ } else if ((p = profile_get(ps, state->section))) {
+ pa_xfree(p->description);
+ p->description = pa_xstrdup(_(state->rvalue));
+ } else {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_description_key(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if ((m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_xfree(m->description_key);
+ m->description_key = pa_xstrdup(state->rvalue);
+ } else if ((p = profile_get(ps, state->section))) {
+ pa_xfree(p->description_key);
+ p->description_key = pa_xstrdup(state->rvalue);
+ } else {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+
+static int mapping_parse_priority(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ uint32_t prio;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (pa_atou(state->rvalue, &prio) < 0) {
+ pa_log("[%s:%u] Priority invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if ((m = pa_alsa_mapping_get(ps, state->section)))
+ m->priority = prio;
+ else if ((p = profile_get(ps, state->section)))
+ p->priority = prio;
+ else {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_fallback(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ int k;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if ((k = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Fallback invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if ((m = pa_alsa_mapping_get(ps, state->section)))
+ m->fallback = k;
+ else if ((p = profile_get(ps, state->section)))
+ p->fallback_input = p->fallback_output = k;
+ else {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_intended_roles(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ pa_proplist_sets(m->proplist, PA_PROP_DEVICE_INTENDED_ROLES, state->rvalue);
+
+ return 0;
+}
+
+
+static int profile_parse_mappings(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(p = profile_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "input-mappings")) {
+ pa_xstrfreev(p->input_mapping_names);
+ p->input_mapping_names = pa_split_spaces_strv(state->rvalue);
+ } else {
+ pa_xstrfreev(p->output_mapping_names);
+ p->output_mapping_names = pa_split_spaces_strv(state->rvalue);
+ }
+
+ return 0;
+}
+
+static int profile_parse_skip_probe(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ int b;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(p = profile_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if ((b = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Skip probe invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ p->supported = b;
+
+ return 0;
+}
+
+static int decibel_fix_parse_db_values(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_decibel_fix *db_fix;
+ char **items;
+ char *item;
+ long *db_values;
+ unsigned n = 8; /* Current size of the db_values table. */
+ unsigned min_step = 0;
+ unsigned max_step = 0;
+ unsigned i = 0; /* Index to the items table. */
+ unsigned prev_step = 0;
+ double prev_db = 0;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(db_fix = decibel_fix_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (!(items = pa_split_spaces_strv(state->rvalue))) {
+ pa_log("[%s:%u] Value missing", state->filename, state->lineno);
+ return -1;
+ }
+
+ db_values = pa_xnew(long, n);
+
+ while ((item = items[i++])) {
+ char *s = item; /* Step value string. */
+ char *d = item; /* dB value string. */
+ uint32_t step;
+ double db;
+
+ /* Move d forward until it points to a colon or to the end of the item. */
+ for (; *d && *d != ':'; ++d);
+
+ if (d == s) {
+ /* item started with colon. */
+ pa_log("[%s:%u] No step value found in %s", state->filename, state->lineno, item);
+ goto fail;
+ }
+
+ if (!*d || !*(d + 1)) {
+ /* No colon found, or it was the last character in item. */
+ pa_log("[%s:%u] No dB value found in %s", state->filename, state->lineno, item);
+ goto fail;
+ }
+
+ /* pa_atou() needs a null-terminating string. Let's replace the colon
+ * with a zero byte. */
+ *d++ = '\0';
+
+ if (pa_atou(s, &step) < 0) {
+ pa_log("[%s:%u] Invalid step value: %s", state->filename, state->lineno, s);
+ goto fail;
+ }
+
+ if (pa_atod(d, &db) < 0) {
+ pa_log("[%s:%u] Invalid dB value: %s", state->filename, state->lineno, d);
+ goto fail;
+ }
+
+ if (step <= prev_step && i != 1) {
+ pa_log("[%s:%u] Step value %u not greater than the previous value %u", state->filename, state->lineno, step, prev_step);
+ goto fail;
+ }
+
+ if (db < prev_db && i != 1) {
+ pa_log("[%s:%u] Decibel value %0.2f less than the previous value %0.2f", state->filename, state->lineno, db, prev_db);
+ goto fail;
+ }
+
+ if (i == 1) {
+ min_step = step;
+ db_values[0] = (long) (db * 100.0);
+ prev_step = step;
+ prev_db = db;
+ } else {
+ /* Interpolate linearly. */
+ double db_increment = (db - prev_db) / (step - prev_step);
+
+ for (; prev_step < step; ++prev_step, prev_db += db_increment) {
+
+ /* Reallocate the db_values table if it's about to overflow. */
+ if (prev_step + 1 - min_step == n) {
+ n *= 2;
+ db_values = pa_xrenew(long, db_values, n);
+ }
+
+ db_values[prev_step + 1 - min_step] = (long) ((prev_db + db_increment) * 100.0);
+ }
+ }
+
+ max_step = step;
+ }
+
+ db_fix->min_step = min_step;
+ db_fix->max_step = max_step;
+ pa_xfree(db_fix->db_values);
+ db_fix->db_values = db_values;
+
+ pa_xstrfreev(items);
+
+ return 0;
+
+fail:
+ pa_xstrfreev(items);
+ pa_xfree(db_values);
+
+ return -1;
+}
+
+/* the logic is simple: if we see the jack in multiple paths */
+/* assign all those paths to one availability_group */
+static void profile_set_set_availability_groups(pa_alsa_profile_set *ps) {
+ pa_dynarray *paths;
+ pa_alsa_path *p;
+ void *state;
+ unsigned idx1;
+ uint32_t num = 1;
+
+ /* Merge ps->input_paths and ps->output_paths into one dynarray. */
+ paths = pa_dynarray_new(NULL);
+ PA_HASHMAP_FOREACH(p, ps->input_paths, state)
+ pa_dynarray_append(paths, p);
+ PA_HASHMAP_FOREACH(p, ps->output_paths, state)
+ pa_dynarray_append(paths, p);
+
+ PA_DYNARRAY_FOREACH(p, paths, idx1) {
+ pa_alsa_jack *j;
+ const char *found = NULL;
+ bool has_control = false;
+
+ PA_LLIST_FOREACH(j, p->jacks) {
+ pa_alsa_path *p2;
+ unsigned idx2;
+
+ if (!j->has_control || j->state_plugged == PA_AVAILABLE_NO)
+ continue;
+ has_control = true;
+ PA_DYNARRAY_FOREACH(p2, paths, idx2) {
+ pa_alsa_jack *j2;
+
+ if (p2 == p)
+ break;
+ PA_LLIST_FOREACH(j2, p2->jacks) {
+ if (!j2->has_control || j2->state_plugged == PA_AVAILABLE_NO)
+ continue;
+ if (pa_streq(j->alsa_id.name, j2->alsa_id.name) &&
+ j->alsa_id.index == j2->alsa_id.index) {
+ j->state_plugged = PA_AVAILABLE_UNKNOWN;
+ j2->state_plugged = PA_AVAILABLE_UNKNOWN;
+ found = p2->availability_group;
+ break;
+ }
+ }
+ }
+ if (found)
+ break;
+ }
+ if (!has_control)
+ continue;
+ if (!found) {
+ p->availability_group = pa_sprintf_malloc("Legacy %d", num);
+ } else {
+ p->availability_group = pa_xstrdup(found);
+ }
+ if (!found)
+ num++;
+ }
+
+ pa_dynarray_free(paths);
+}
+
+static void mapping_paths_probe(pa_alsa_mapping *m, pa_alsa_profile *profile,
+ pa_alsa_direction_t direction, pa_hashmap *used_paths,
+ pa_hashmap *mixers) {
+
+ pa_alsa_path *p;
+ void *state;
+ snd_pcm_t *pcm_handle;
+ pa_alsa_path_set *ps;
+ snd_mixer_t *mixer_handle;
+
+ if (direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (m->output_path_set)
+ return; /* Already probed */
+ m->output_path_set = ps = pa_alsa_path_set_new(m, direction, NULL); /* FIXME: Handle paths_dir */
+ pcm_handle = m->output_pcm;
+ } else {
+ if (m->input_path_set)
+ return; /* Already probed */
+ m->input_path_set = ps = pa_alsa_path_set_new(m, direction, NULL); /* FIXME: Handle paths_dir */
+ pcm_handle = m->input_pcm;
+ }
+
+ if (!ps)
+ return; /* No paths */
+
+ pa_assert(pcm_handle);
+
+ mixer_handle = pa_alsa_open_mixer_for_pcm(mixers, pcm_handle, true);
+ if (!mixer_handle) {
+ /* Cannot open mixer, remove all entries */
+ pa_hashmap_remove_all(ps->paths);
+ return;
+ }
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state) {
+ if (p->autodetect_eld_device)
+ p->eld_device = m->hw_device_index;
+
+ if (pa_alsa_path_probe(p, m, mixer_handle, m->profile_set->ignore_dB) < 0)
+ pa_hashmap_remove(ps->paths, p);
+ }
+
+ path_set_condense(ps, mixer_handle);
+ path_set_make_path_descriptions_unique(ps);
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state)
+ pa_hashmap_put(used_paths, p, p);
+
+ pa_log_debug("Available mixer paths (after tidying):");
+ pa_alsa_path_set_dump(ps);
+}
+
+static int mapping_verify(pa_alsa_mapping *m, const pa_channel_map *bonus) {
+
+ static const struct description_map well_known_descriptions[] = {
+ { "analog-mono", N_("Analog Mono") },
+ { "analog-mono-left", N_("Analog Mono (Left)") },
+ { "analog-mono-right", N_("Analog Mono (Right)") },
+ { "analog-stereo", N_("Analog Stereo") },
+ { "mono-fallback", N_("Mono") },
+ { "stereo-fallback", N_("Stereo") },
+ /* 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. */
+ { "analog-stereo-input", N_("Analog Stereo") },
+ { "analog-stereo-output", N_("Analog Stereo") },
+ { "analog-stereo-headset", N_("Headset") },
+ { "analog-stereo-speakerphone", N_("Speakerphone") },
+ { "multichannel-input", N_("Multichannel") },
+ { "multichannel-output", N_("Multichannel") },
+ { "analog-surround-21", N_("Analog Surround 2.1") },
+ { "analog-surround-30", N_("Analog Surround 3.0") },
+ { "analog-surround-31", N_("Analog Surround 3.1") },
+ { "analog-surround-40", N_("Analog Surround 4.0") },
+ { "analog-surround-41", N_("Analog Surround 4.1") },
+ { "analog-surround-50", N_("Analog Surround 5.0") },
+ { "analog-surround-51", N_("Analog Surround 5.1") },
+ { "analog-surround-61", N_("Analog Surround 6.0") },
+ { "analog-surround-61", N_("Analog Surround 6.1") },
+ { "analog-surround-70", N_("Analog Surround 7.0") },
+ { "analog-surround-71", N_("Analog Surround 7.1") },
+ { "iec958-stereo", N_("Digital Stereo (IEC958)") },
+ { "iec958-ac3-surround-40", N_("Digital Surround 4.0 (IEC958/AC3)") },
+ { "iec958-ac3-surround-51", N_("Digital Surround 5.1 (IEC958/AC3)") },
+ { "iec958-dts-surround-51", N_("Digital Surround 5.1 (IEC958/DTS)") },
+ { "hdmi-stereo", N_("Digital Stereo (HDMI)") },
+ { "hdmi-surround-51", N_("Digital Surround 5.1 (HDMI)") },
+ { "gaming-headset-chat", N_("Chat") },
+ { "gaming-headset-game", N_("Game") },
+ };
+ const char *description_key = m->description_key ? m->description_key : m->name;
+
+ pa_assert(m);
+
+ if (!pa_channel_map_valid(&m->channel_map)) {
+ pa_log("Mapping %s is missing channel map.", m->name);
+ return -1;
+ }
+
+ if (!m->device_strings) {
+ pa_log("Mapping %s is missing device strings.", m->name);
+ return -1;
+ }
+
+ if ((m->input_path_names && m->input_element) ||
+ (m->output_path_names && m->output_element)) {
+ pa_log("Mapping %s must have either mixer path or mixer element, not both.", m->name);
+ return -1;
+ }
+
+ if (!m->description)
+ m->description = pa_xstrdup(lookup_description(description_key,
+ well_known_descriptions,
+ PA_ELEMENTSOF(well_known_descriptions)));
+
+ if (!m->description)
+ m->description = pa_xstrdup(m->name);
+
+ if (bonus) {
+ if (pa_channel_map_equal(&m->channel_map, bonus))
+ m->priority += 50;
+ else if (m->channel_map.channels == bonus->channels)
+ m->priority += 30;
+ }
+
+ return 0;
+}
+
+void pa_alsa_mapping_dump(pa_alsa_mapping *m) {
+ char cm[PA_CHANNEL_MAP_SNPRINT_MAX];
+
+ pa_assert(m);
+
+ pa_log_debug("Mapping %s (%s), priority=%u, channel_map=%s, supported=%s, direction=%i",
+ m->name,
+ pa_strnull(m->description),
+ m->priority,
+ pa_channel_map_snprint(cm, sizeof(cm), &m->channel_map),
+ pa_yes_no(m->supported),
+ m->direction);
+}
+
+static void profile_set_add_auto_pair(
+ pa_alsa_profile_set *ps,
+ pa_alsa_mapping *m, /* output */
+ pa_alsa_mapping *n /* input */) {
+
+ char *name;
+ pa_alsa_profile *p;
+
+ pa_assert(ps);
+ pa_assert(m || n);
+
+ if (m && m->direction == PA_ALSA_DIRECTION_INPUT)
+ return;
+
+ if (n && n->direction == PA_ALSA_DIRECTION_OUTPUT)
+ return;
+
+ if (m && n)
+ name = pa_sprintf_malloc("output:%s+input:%s", m->name, n->name);
+ else if (m)
+ name = pa_sprintf_malloc("output:%s", m->name);
+ else
+ name = pa_sprintf_malloc("input:%s", n->name);
+
+ if (pa_hashmap_get(ps->profiles, name)) {
+ pa_xfree(name);
+ return;
+ }
+
+ p = pa_xnew0(pa_alsa_profile, 1);
+ p->profile_set = ps;
+ p->name = name;
+
+ if (m) {
+ p->output_name = pa_xstrdup(m->name);
+ p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ pa_idxset_put(p->output_mappings, m, NULL);
+ p->priority += m->priority * 100;
+ p->fallback_output = m->fallback;
+ }
+
+ if (n) {
+ p->input_name = pa_xstrdup(n->name);
+ p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ pa_idxset_put(p->input_mappings, n, NULL);
+ p->priority += n->priority;
+ p->fallback_input = n->fallback;
+ }
+
+ pa_hashmap_put(ps->profiles, p->name, p);
+}
+
+static void profile_set_add_auto(pa_alsa_profile_set *ps) {
+ pa_alsa_mapping *m, *n;
+ void *m_state, *n_state;
+
+ pa_assert(ps);
+
+ /* The order is important here:
+ 1) try single inputs and outputs before trying their
+ combination, because if the half-duplex test failed, we don't have
+ to try full duplex.
+ 2) try the output right before the input combinations with
+ that output, because then the output_pcm is not closed between tests.
+ */
+ PA_HASHMAP_FOREACH(n, ps->mappings, n_state)
+ profile_set_add_auto_pair(ps, NULL, n);
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, m_state) {
+ profile_set_add_auto_pair(ps, m, NULL);
+
+ PA_HASHMAP_FOREACH(n, ps->mappings, n_state)
+ profile_set_add_auto_pair(ps, m, n);
+ }
+
+}
+
+static int profile_verify(pa_alsa_profile *p) {
+
+ static const struct description_map well_known_descriptions[] = {
+ { "output:analog-mono+input:analog-mono", N_("Analog Mono Duplex") },
+ { "output:analog-stereo+input:analog-stereo", N_("Analog Stereo Duplex") },
+ { "output:analog-stereo-headset+input:analog-stereo-headset", N_("Headset") },
+ { "output:analog-stereo-speakerphone+input:analog-stereo-speakerphone", N_("Speakerphone") },
+ { "output:iec958-stereo+input:iec958-stereo", N_("Digital Stereo Duplex (IEC958)") },
+ { "output:multichannel-output+input:multichannel-input", N_("Multichannel Duplex") },
+ { "output:unknown-stereo+input:unknown-stereo", N_("Stereo Duplex") },
+ { "output:analog-output-surround71+output:analog-output-chat+input:analog-input", N_("Mono Chat + 7.1 Surround") },
+ { "off", N_("Off") }
+ };
+ const char *description_key = p->description_key ? p->description_key : p->name;
+
+ pa_assert(p);
+
+ /* Replace the output mapping names by the actual mappings */
+ if (p->output_mapping_names) {
+ char **name;
+
+ pa_assert(!p->output_mappings);
+ p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ for (name = p->output_mapping_names; *name; name++) {
+ pa_alsa_mapping *m;
+ char **in;
+ bool duplicate = false;
+
+ for (in = name + 1; *in; in++)
+ if (pa_streq(*name, *in)) {
+ duplicate = true;
+ break;
+ }
+
+ if (duplicate)
+ continue;
+
+ if (!(m = pa_hashmap_get(p->profile_set->mappings, *name)) || m->direction == PA_ALSA_DIRECTION_INPUT) {
+ pa_log("Profile '%s' refers to nonexistent mapping '%s'.", p->name, *name);
+ return -1;
+ }
+
+ pa_idxset_put(p->output_mappings, m, NULL);
+
+ if (p->supported)
+ m->supported++;
+ }
+
+ pa_xstrfreev(p->output_mapping_names);
+ p->output_mapping_names = NULL;
+ }
+
+ /* Replace the input mapping names by the actual mappings */
+ if (p->input_mapping_names) {
+ char **name;
+
+ pa_assert(!p->input_mappings);
+ p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ for (name = p->input_mapping_names; *name; name++) {
+ pa_alsa_mapping *m;
+ char **in;
+ bool duplicate = false;
+
+ for (in = name + 1; *in; in++)
+ if (pa_streq(*name, *in)) {
+ duplicate = true;
+ break;
+ }
+
+ if (duplicate)
+ continue;
+
+ if (!(m = pa_hashmap_get(p->profile_set->mappings, *name)) || m->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ pa_log("Profile '%s' refers to nonexistent mapping '%s'.", p->name, *name);
+ return -1;
+ }
+
+ pa_idxset_put(p->input_mappings, m, NULL);
+
+ if (p->supported)
+ m->supported++;
+ }
+
+ pa_xstrfreev(p->input_mapping_names);
+ p->input_mapping_names = NULL;
+ }
+
+ if (!p->input_mappings && !p->output_mappings) {
+ pa_log("Profile '%s' lacks mappings.", p->name);
+ return -1;
+ }
+
+ if (!p->description)
+ p->description = pa_xstrdup(lookup_description(description_key,
+ well_known_descriptions,
+ PA_ELEMENTSOF(well_known_descriptions)));
+
+ if (!p->description) {
+ uint32_t idx;
+ pa_alsa_mapping *m;
+ char *ptr;
+ size_t size;
+ FILE *f;
+ int count = 0;
+
+ f = open_memstream(&ptr, &size);
+ if (f == NULL) {
+ pa_log("failed to open memstream: %m");
+ return -1;
+ }
+
+ if (p->output_mappings)
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+ if (count++ > 0)
+ fprintf(f, " + ");
+ fprintf(f, _("%s Output"), m->description);
+ }
+
+ if (p->input_mappings)
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+ if (count++ > 0)
+ fprintf(f, " + ");
+ fprintf(f, _("%s Input"), m->description);
+ }
+
+ fclose(f);
+ p->description = ptr;
+ }
+
+ return 0;
+}
+
+void pa_alsa_profile_dump(pa_alsa_profile *p) {
+ uint32_t idx;
+ pa_alsa_mapping *m;
+ pa_assert(p);
+
+ pa_log_debug("Profile %s (%s), input=%s, output=%s priority=%u, supported=%s n_input_mappings=%u, n_output_mappings=%u",
+ p->name,
+ pa_strnull(p->description),
+ pa_strnull(p->input_name),
+ pa_strnull(p->output_name),
+ p->priority,
+ pa_yes_no(p->supported),
+ p->input_mappings ? pa_idxset_size(p->input_mappings) : 0,
+ p->output_mappings ? pa_idxset_size(p->output_mappings) : 0);
+
+ if (p->input_mappings)
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx)
+ pa_log_debug("Input %s", m->name);
+
+ if (p->output_mappings)
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx)
+ pa_log_debug("Output %s", m->name);
+}
+
+static int decibel_fix_verify(pa_alsa_decibel_fix *db_fix) {
+ pa_assert(db_fix);
+
+ /* Check that the dB mapping has been configured. Since "db-values" is
+ * currently the only option in the DecibelFix section, and decibel fix
+ * objects don't get created if a DecibelFix section is empty, this is
+ * actually a redundant check. Having this may prevent future bugs,
+ * however. */
+ if (!db_fix->db_values) {
+ pa_log("Decibel fix for element %s lacks the dB values.", db_fix->name);
+ return -1;
+ }
+
+ return 0;
+}
+
+void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix) {
+ char *db_values = NULL;
+
+ pa_assert(db_fix);
+
+ if (db_fix->db_values) {
+ unsigned long i, nsteps;
+ FILE *f;
+ char *ptr;
+ size_t size;
+
+ f = open_memstream(&ptr, &size);
+ if (f == NULL)
+ return;
+
+ pa_assert(db_fix->min_step <= db_fix->max_step);
+ nsteps = db_fix->max_step - db_fix->min_step + 1;
+
+ for (i = 0; i < nsteps; ++i)
+ fprintf(f, "[%li]:%0.2f ", i + db_fix->min_step, db_fix->db_values[i] / 100.0);
+
+ fclose(f);
+ db_values = ptr;
+ }
+
+ pa_log_debug("Decibel fix %s, min_step=%li, max_step=%li, db_values=%s",
+ db_fix->name, db_fix->min_step, db_fix->max_step, pa_strnull(db_values));
+
+ pa_xfree(db_values);
+}
+
+static const char *get_default_profile_dir(void) {
+ const char *str;
+#ifdef HAVE_RUNNING_FROM_BUILD_TREE
+ if (pa_run_from_build_tree())
+ return PA_SRCDIR "mixer/profile-sets";
+ else
+#endif
+ if (getenv("ACP_BUILDDIR") != NULL)
+ return "mixer/profile-sets";
+ if ((str = getenv("ACP_PROFILES_DIR")) != NULL)
+ return str;
+ return PA_ALSA_PROFILE_SETS_DIR;
+}
+
+pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ pa_alsa_decibel_fix *db_fix;
+ char *fn;
+ int r;
+ void *state;
+
+ static pa_config_item items[] = {
+ /* [General] */
+ { "auto-profiles", pa_config_parse_bool, NULL, "General" },
+
+ /* [Mapping ...] */
+ { "device-strings", mapping_parse_device_strings, NULL, NULL },
+ { "channel-map", mapping_parse_channel_map, NULL, NULL },
+ { "paths-input", mapping_parse_paths, NULL, NULL },
+ { "paths-output", mapping_parse_paths, NULL, NULL },
+ { "element-input", mapping_parse_element, NULL, NULL },
+ { "element-output", mapping_parse_element, NULL, NULL },
+ { "direction", mapping_parse_direction, NULL, NULL },
+ { "exact-channels", mapping_parse_exact_channels, NULL, NULL },
+ { "intended-roles", mapping_parse_intended_roles, NULL, NULL },
+
+ /* Shared by [Mapping ...] and [Profile ...] */
+ { "description", mapping_parse_description, NULL, NULL },
+ { "description-key", mapping_parse_description_key,NULL, NULL },
+ { "priority", mapping_parse_priority, NULL, NULL },
+ { "fallback", mapping_parse_fallback, NULL, NULL },
+
+ /* [Profile ...] */
+ { "input-mappings", profile_parse_mappings, NULL, NULL },
+ { "output-mappings", profile_parse_mappings, NULL, NULL },
+ { "skip-probe", profile_parse_skip_probe, NULL, NULL },
+
+ /* [DecibelFix ...] */
+ { "db-values", decibel_fix_parse_db_values, NULL, NULL },
+ { NULL, NULL, NULL, NULL }
+ };
+
+ ps = pa_xnew0(pa_alsa_profile_set, 1);
+ ps->mappings = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_mapping_free);
+ ps->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_profile_free);
+ ps->decibel_fixes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) decibel_fix_free);
+ ps->input_paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_path_free);
+ ps->output_paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_path_free);
+
+ items[0].data = &ps->auto_profiles;
+
+ fn = pa_maybe_prefix_path(fname ? fname : "default.conf",
+ get_default_profile_dir());
+ if ((r = access(fn, R_OK)) != 0) {
+ if (fname != NULL) {
+ pa_log_warn("profile-set '%s' can't be accessed: %m", fn);
+ fn = pa_maybe_prefix_path("default.conf",
+ get_default_profile_dir());
+ r = access(fn, R_OK);
+ }
+ if (r != 0) {
+ pa_log_warn("profile-set '%s' can't be accessed: %m", fn);
+ }
+ }
+ r = pa_config_parse(fn, NULL, items, NULL, false, ps);
+ pa_xfree(fn);
+
+ if (r < 0)
+ goto fail;
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, state)
+ if (mapping_verify(m, bonus) < 0)
+ goto fail;
+
+ if (ps->auto_profiles)
+ profile_set_add_auto(ps);
+
+ PA_HASHMAP_FOREACH(p, ps->profiles, state)
+ if (profile_verify(p) < 0)
+ goto fail;
+
+ PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state)
+ if (decibel_fix_verify(db_fix) < 0)
+ goto fail;
+
+ return ps;
+
+fail:
+ pa_alsa_profile_set_free(ps);
+ return NULL;
+}
+
+static void profile_finalize_probing(pa_alsa_profile *to_be_finalized, pa_alsa_profile *next) {
+ pa_alsa_mapping *m;
+ uint32_t idx;
+
+ if (!to_be_finalized)
+ return;
+
+ if (to_be_finalized->output_mappings)
+ PA_IDXSET_FOREACH(m, to_be_finalized->output_mappings, idx) {
+
+ if (!m->output_pcm)
+ continue;
+
+ if (to_be_finalized->supported)
+ m->supported++;
+
+ /* If this mapping is also in the next profile, we won't close the
+ * pcm handle here, because it would get immediately reopened
+ * anyway. */
+ if (next && next->output_mappings && pa_idxset_get_by_data(next->output_mappings, m, NULL))
+ continue;
+
+ pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
+ pa_alsa_close(&m->output_pcm);
+ }
+
+ if (to_be_finalized->input_mappings)
+ PA_IDXSET_FOREACH(m, to_be_finalized->input_mappings, idx) {
+
+ if (!m->input_pcm)
+ continue;
+
+ if (to_be_finalized->supported)
+ m->supported++;
+
+ /* If this mapping is also in the next profile, we won't close the
+ * pcm handle here, because it would get immediately reopened
+ * anyway. */
+ if (next && next->input_mappings && pa_idxset_get_by_data(next->input_mappings, m, NULL))
+ continue;
+
+ pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
+ pa_alsa_close(&m->input_pcm);
+ }
+}
+
+static snd_pcm_t* mapping_open_pcm(pa_alsa_mapping *m,
+ const pa_sample_spec *ss,
+ const char *dev_id,
+ bool exact_channels,
+ int mode,
+ unsigned default_n_fragments,
+ unsigned default_fragment_size_msec) {
+
+ snd_pcm_t* handle;
+ pa_sample_spec try_ss = *ss;
+ pa_channel_map try_map = m->channel_map;
+ snd_pcm_uframes_t try_period_size, try_buffer_size;
+
+ try_ss.channels = try_map.channels;
+
+ try_period_size =
+ pa_usec_to_bytes(default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) /
+ pa_frame_size(&try_ss);
+ try_buffer_size = default_n_fragments * try_period_size;
+
+ handle = pa_alsa_open_by_template(
+ m->device_strings, dev_id, NULL, &try_ss,
+ &try_map, mode, &try_period_size,
+ &try_buffer_size, 0, NULL, NULL, exact_channels);
+ if (handle && !exact_channels && m->channel_map.channels != try_map.channels) {
+ char buf[PA_CHANNEL_MAP_SNPRINT_MAX];
+ pa_log_debug("Channel map for mapping '%s' permanently changed to '%s'", m->name,
+ pa_channel_map_snprint(buf, sizeof(buf), &try_map));
+ m->channel_map = try_map;
+ }
+ return handle;
+}
+
+static void paths_drop_unused(pa_hashmap* h, pa_hashmap *keep) {
+
+ void* state = NULL;
+ const void* key;
+ pa_alsa_path* p;
+
+ pa_assert(h);
+ pa_assert(keep);
+
+ p = pa_hashmap_iterate(h, &state, &key);
+ while (p) {
+ if (pa_hashmap_get(keep, p) == NULL)
+ pa_hashmap_remove_and_free(h, key);
+ p = pa_hashmap_iterate(h, &state, &key);
+ }
+}
+
+static int add_profiles_to_probe(
+ pa_alsa_profile **list,
+ pa_hashmap *profiles,
+ bool fallback_output,
+ bool fallback_input) {
+
+ int i = 0;
+ void *state;
+ pa_alsa_profile *p;
+ PA_HASHMAP_FOREACH(p, profiles, state)
+ if (p->fallback_input == fallback_input && p->fallback_output == fallback_output) {
+ *list = p;
+ list++;
+ i++;
+ }
+ return i;
+}
+
+static void mapping_query_hw_device(pa_alsa_mapping *mapping, snd_pcm_t *pcm) {
+ int r;
+ snd_pcm_info_t* pcm_info;
+ snd_pcm_info_alloca(&pcm_info);
+
+ r = snd_pcm_info(pcm, pcm_info);
+ if (r < 0) {
+ pa_log("Mapping %s: snd_pcm_info() failed %s: ", mapping->name, pa_alsa_strerror(r));
+ return;
+ }
+
+ /* XXX: It's not clear what snd_pcm_info_get_device() does if the device is
+ * not backed by a hw device or if it's backed by multiple hw devices. We
+ * only use hw_device_index for HDMI devices, however, and for those the
+ * return value is expected to be always valid, so this shouldn't be a
+ * significant problem. */
+ mapping->hw_device_index = snd_pcm_info_get_device(pcm_info);
+}
+
+void pa_alsa_profile_set_probe(
+ pa_alsa_profile_set *ps,
+ pa_hashmap *mixers,
+ const char *dev_id,
+ const pa_sample_spec *ss,
+ unsigned default_n_fragments,
+ unsigned default_fragment_size_msec) {
+
+ bool found_output = false, found_input = false;
+
+ pa_alsa_profile *p, *last = NULL;
+ pa_alsa_profile **pp, **probe_order;
+ pa_alsa_mapping *m;
+ pa_hashmap *broken_inputs, *broken_outputs, *used_paths;
+ pa_alsa_mapping *selected_fallback_input = NULL, *selected_fallback_output = NULL;
+
+ pa_assert(ps);
+ pa_assert(dev_id);
+ pa_assert(ss);
+
+ if (ps->probed)
+ return;
+
+ broken_inputs = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ broken_outputs = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ used_paths = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ pp = probe_order = pa_xnew0(pa_alsa_profile *, pa_hashmap_size(ps->profiles) + 1);
+
+ pp += add_profiles_to_probe(pp, ps->profiles, false, false);
+ pp += add_profiles_to_probe(pp, ps->profiles, false, true);
+ pp += add_profiles_to_probe(pp, ps->profiles, true, false);
+ pp += add_profiles_to_probe(pp, ps->profiles, true, true);
+
+ for (pp = probe_order; *pp; pp++) {
+ uint32_t idx;
+ p = *pp;
+
+ /* Skip if fallback and already found something, but still probe already selected fallbacks.
+ * If UCM is used then both fallback_input and fallback_output flags are false.
+ * If UCM is not used then there will be only a single entry in mappings.
+ */
+ if (found_input && p->fallback_input)
+ if (selected_fallback_input == NULL || pa_idxset_get_by_index(p->input_mappings, 0) != selected_fallback_input)
+ continue;
+ if (found_output && p->fallback_output)
+ if (selected_fallback_output == NULL || pa_idxset_get_by_index(p->output_mappings, 0) != selected_fallback_output)
+ continue;
+
+ /* Skip if this is already marked that it is supported (i.e. from the config file) */
+ if (!p->supported) {
+
+ profile_finalize_probing(last, p);
+ p->supported = true;
+
+ if (p->output_mappings) {
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+ if (pa_hashmap_get(broken_outputs, m) == m) {
+ pa_log_debug("Skipping profile %s - will not be able to open output:%s", p->name, m->name);
+ p->supported = false;
+ break;
+ }
+ }
+ }
+
+ if (p->input_mappings && p->supported) {
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+ if (pa_hashmap_get(broken_inputs, m) == m) {
+ pa_log_debug("Skipping profile %s - will not be able to open input:%s", p->name, m->name);
+ p->supported = false;
+ break;
+ }
+ }
+ }
+
+ if (p->supported)
+ pa_log_debug("Looking at profile %s", p->name);
+
+ /* Check if we can open all new ones */
+ if (p->output_mappings && p->supported)
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+
+ if (m->output_pcm)
+ continue;
+
+ pa_log_debug("Checking for playback on %s (%s)", m->description, m->name);
+ if (!(m->output_pcm = mapping_open_pcm(m, ss, dev_id, m->exact_channels,
+ SND_PCM_STREAM_PLAYBACK,
+ default_n_fragments,
+ default_fragment_size_msec))) {
+ p->supported = false;
+ if (pa_idxset_size(p->output_mappings) == 1 &&
+ ((!p->input_mappings) || pa_idxset_size(p->input_mappings) == 0)) {
+ pa_log_debug("Caching failure to open output:%s", m->name);
+ pa_hashmap_put(broken_outputs, m, m);
+ }
+ break;
+ }
+
+ if (m->hw_device_index < 0)
+ mapping_query_hw_device(m, m->output_pcm);
+ }
+
+ if (p->input_mappings && p->supported)
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+
+ if (m->input_pcm)
+ continue;
+
+ pa_log_debug("Checking for recording on %s (%s)", m->description, m->name);
+ if (!(m->input_pcm = mapping_open_pcm(m, ss, dev_id, m->exact_channels,
+ SND_PCM_STREAM_CAPTURE,
+ default_n_fragments,
+ default_fragment_size_msec))) {
+ p->supported = false;
+ if (pa_idxset_size(p->input_mappings) == 1 &&
+ ((!p->output_mappings) || pa_idxset_size(p->output_mappings) == 0)) {
+ pa_log_debug("Caching failure to open input:%s", m->name);
+ pa_hashmap_put(broken_inputs, m, m);
+ }
+ break;
+ }
+
+ if (m->hw_device_index < 0)
+ mapping_query_hw_device(m, m->input_pcm);
+ }
+
+ last = p;
+
+ if (!p->supported)
+ continue;
+ }
+
+ pa_log_debug("Profile %s supported.", p->name);
+
+ if (p->output_mappings)
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx)
+ if (m->output_pcm) {
+ found_output = true;
+ if (p->fallback_output && selected_fallback_output == NULL) {
+ selected_fallback_output = m;
+ }
+ mapping_paths_probe(m, p, PA_ALSA_DIRECTION_OUTPUT, used_paths, mixers);
+ }
+
+ if (p->input_mappings)
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx)
+ if (m->input_pcm) {
+ found_input = true;
+ if (p->fallback_input && selected_fallback_input == NULL) {
+ selected_fallback_input = m;
+ }
+ mapping_paths_probe(m, p, PA_ALSA_DIRECTION_INPUT, used_paths, mixers);
+ }
+ }
+
+ /* Clean up */
+ profile_finalize_probing(last, NULL);
+
+ pa_alsa_profile_set_drop_unsupported(ps);
+
+ paths_drop_unused(ps->input_paths, used_paths);
+ paths_drop_unused(ps->output_paths, used_paths);
+ pa_hashmap_free(broken_inputs);
+ pa_hashmap_free(broken_outputs);
+ pa_hashmap_free(used_paths);
+ pa_xfree(probe_order);
+
+ profile_set_set_availability_groups(ps);
+
+ ps->probed = true;
+}
+
+void pa_alsa_profile_set_dump(pa_alsa_profile_set *ps) {
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ pa_alsa_decibel_fix *db_fix;
+ void *state;
+
+ pa_assert(ps);
+
+ pa_log_debug("Profile set %p, auto_profiles=%s, probed=%s, n_mappings=%u, n_profiles=%u, n_decibel_fixes=%u",
+ (void*)
+ ps,
+ pa_yes_no(ps->auto_profiles),
+ pa_yes_no(ps->probed),
+ pa_hashmap_size(ps->mappings),
+ pa_hashmap_size(ps->profiles),
+ pa_hashmap_size(ps->decibel_fixes));
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, state)
+ pa_alsa_mapping_dump(m);
+
+ PA_HASHMAP_FOREACH(p, ps->profiles, state)
+ pa_alsa_profile_dump(p);
+
+ PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state)
+ pa_alsa_decibel_fix_dump(db_fix);
+}
+
+void pa_alsa_profile_set_drop_unsupported(pa_alsa_profile_set *ps) {
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ void *state;
+
+ PA_HASHMAP_FOREACH(p, ps->profiles, state) {
+ if (!p->supported)
+ pa_hashmap_remove_and_free(ps->profiles, p->name);
+ }
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, state) {
+ if (m->supported <= 0)
+ pa_hashmap_remove_and_free(ps->mappings, m->name);
+ }
+}
+
+static pa_device_port* device_port_alsa_init(pa_hashmap *ports, /* card ports */
+ const char* name,
+ const char* description,
+ pa_alsa_path *path,
+ pa_alsa_setting *setting,
+ pa_card_profile *cp,
+ pa_hashmap *extra, /* sink/source ports */
+ pa_core *core) {
+
+ pa_device_port *p;
+
+ pa_assert(path);
+
+ p = pa_hashmap_get(ports, name);
+
+ if (!p) {
+ pa_alsa_port_data *data;
+ pa_device_port_new_data port_data;
+
+ pa_device_port_new_data_init(&port_data);
+ pa_device_port_new_data_set_name(&port_data, name);
+ pa_device_port_new_data_set_description(&port_data, description);
+ pa_device_port_new_data_set_direction(&port_data, path->direction == PA_ALSA_DIRECTION_OUTPUT ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT);
+ pa_device_port_new_data_set_type(&port_data, path->device_port_type);
+ pa_device_port_new_data_set_availability_group(&port_data, path->availability_group);
+
+ p = pa_device_port_new(core, &port_data, sizeof(pa_alsa_port_data));
+ pa_device_port_new_data_done(&port_data);
+ pa_assert(p);
+ pa_hashmap_put(ports, p->name, p);
+ pa_proplist_update(p->proplist, PA_UPDATE_REPLACE, path->proplist);
+
+ data = PA_DEVICE_PORT_DATA(p);
+ /* Ownership of the path and setting is not transferred to the port data, so we don't deal with freeing them */
+ data->path = path;
+ data->setting = setting;
+ path->port = p;
+ }
+
+ if (cp)
+ pa_hashmap_put(p->profiles, cp->name, cp);
+
+ if (extra) {
+ pa_hashmap_put(extra, p->name, p);
+ }
+
+ return p;
+}
+
+void pa_alsa_path_set_add_ports(
+ pa_alsa_path_set *ps,
+ pa_card_profile *cp,
+ pa_hashmap *ports, /* card ports */
+ pa_hashmap *extra, /* sink/source ports */
+ pa_core *core) {
+
+ pa_alsa_path *path;
+ void *state;
+
+ pa_assert(ports);
+
+ if (!ps)
+ return;
+
+ PA_HASHMAP_FOREACH(path, ps->paths, state) {
+ if (!path->settings || !path->settings->next) {
+ /* If there is no or just one setting we only need a
+ * single entry */
+ pa_device_port *port = device_port_alsa_init(ports, path->name,
+ path->description, path, path->settings, cp, extra, core);
+ port->priority = path->priority * 100;
+
+ } else {
+ pa_alsa_setting *s;
+ PA_LLIST_FOREACH(s, path->settings) {
+ pa_device_port *port;
+ char *n, *d;
+
+ n = pa_sprintf_malloc("%s;%s", path->name, s->name);
+
+ if (s->description[0])
+ d = pa_sprintf_malloc("%s / %s", path->description, s->description);
+ else
+ d = pa_xstrdup(path->description);
+
+ port = device_port_alsa_init(ports, n, d, path, s, cp, extra, core);
+ port->priority = path->priority * 100 + s->priority;
+
+ pa_xfree(n);
+ pa_xfree(d);
+ }
+ }
+ }
+}
+
+void pa_alsa_add_ports(pa_hashmap *ports, pa_alsa_path_set *ps, pa_card *card) {
+ pa_assert(ps);
+
+ if (ps->paths && pa_hashmap_size(ps->paths) > 0) {
+ pa_assert(card);
+ pa_alsa_path_set_add_ports(ps, NULL, card->ports, ports, card->core);
+ }
+
+ pa_log_debug("Added %u ports", pa_hashmap_size(ports));
+}
diff --git a/spa/plugins/alsa/acp/alsa-mixer.h b/spa/plugins/alsa/acp/alsa-mixer.h
new file mode 100644
index 0000000..643f03d
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-mixer.h
@@ -0,0 +1,459 @@
+#ifndef fooalsamixerhfoo
+#define fooalsamixerhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+#include <alsa/asoundlib.h>
+
+typedef struct pa_alsa_mixer pa_alsa_mixer;
+typedef struct pa_alsa_setting pa_alsa_setting;
+typedef struct pa_alsa_mixer_id pa_alsa_mixer_id;
+typedef struct pa_alsa_option pa_alsa_option;
+typedef struct pa_alsa_element pa_alsa_element;
+typedef struct pa_alsa_jack pa_alsa_jack;
+typedef struct pa_alsa_path pa_alsa_path;
+typedef struct pa_alsa_path_set pa_alsa_path_set;
+typedef struct pa_alsa_mapping pa_alsa_mapping;
+typedef struct pa_alsa_profile pa_alsa_profile;
+typedef struct pa_alsa_decibel_fix pa_alsa_decibel_fix;
+typedef struct pa_alsa_profile_set pa_alsa_profile_set;
+typedef struct pa_alsa_port_data pa_alsa_port_data;
+typedef struct pa_alsa_profile pa_alsa_profile;
+typedef struct pa_alsa_profile pa_card_profile;
+typedef struct pa_alsa_device pa_alsa_device;
+
+#define POSITION_MASK_CHANNELS 8
+
+typedef enum pa_alsa_switch_use {
+ PA_ALSA_SWITCH_IGNORE,
+ PA_ALSA_SWITCH_MUTE, /* make this switch follow mute status */
+ PA_ALSA_SWITCH_OFF, /* set this switch to 'off' unconditionally */
+ PA_ALSA_SWITCH_ON, /* set this switch to 'on' unconditionally */
+ PA_ALSA_SWITCH_SELECT /* allow the user to select switch status through a setting */
+} pa_alsa_switch_use_t;
+
+typedef enum pa_alsa_volume_use {
+ PA_ALSA_VOLUME_IGNORE,
+ PA_ALSA_VOLUME_MERGE, /* merge this volume slider into the global volume slider */
+ PA_ALSA_VOLUME_OFF, /* set this volume to minimal unconditionally */
+ PA_ALSA_VOLUME_ZERO, /* set this volume to 0dB unconditionally */
+ PA_ALSA_VOLUME_CONSTANT /* set this volume to a constant value unconditionally */
+} pa_alsa_volume_use_t;
+
+typedef enum pa_alsa_enumeration_use {
+ PA_ALSA_ENUMERATION_IGNORE,
+ PA_ALSA_ENUMERATION_SELECT
+} pa_alsa_enumeration_use_t;
+
+typedef enum pa_alsa_required {
+ PA_ALSA_REQUIRED_IGNORE,
+ PA_ALSA_REQUIRED_SWITCH,
+ PA_ALSA_REQUIRED_VOLUME,
+ PA_ALSA_REQUIRED_ENUMERATION,
+ PA_ALSA_REQUIRED_ANY
+} pa_alsa_required_t;
+
+typedef enum pa_alsa_direction {
+ PA_ALSA_DIRECTION_ANY,
+ PA_ALSA_DIRECTION_OUTPUT,
+ PA_ALSA_DIRECTION_INPUT
+} pa_alsa_direction_t;
+
+
+#include "acp.h"
+#include "device-port.h"
+#include "alsa-util.h"
+#include "alsa-ucm.h"
+#include "card.h"
+
+/* A setting combines a couple of options into a single entity that
+ * may be selected. Only one setting can be active at the same
+ * time. */
+struct pa_alsa_setting {
+ pa_alsa_path *path;
+ PA_LLIST_FIELDS(pa_alsa_setting);
+
+ pa_idxset *options;
+
+ char *name;
+ char *description;
+ unsigned priority;
+};
+
+/* An entry for one ALSA mixer */
+struct pa_alsa_mixer {
+ struct pa_alsa_mixer *alias;
+ snd_mixer_t *mixer_handle;
+ bool used_for_poll:1;
+ bool used_for_probe_only:1;
+};
+
+/* ALSA mixer element identifier */
+struct pa_alsa_mixer_id {
+ char *name;
+ int index;
+};
+
+char *pa_alsa_mixer_id_to_string(char *dst, size_t dst_len, pa_alsa_mixer_id *id);
+
+/* An option belongs to an element and refers to one enumeration item
+ * of the element is an enumeration item, or a switch status if the
+ * element is a switch item. */
+struct pa_alsa_option {
+ pa_alsa_element *element;
+ PA_LLIST_FIELDS(pa_alsa_option);
+
+ char *alsa_name;
+ int alsa_idx;
+
+ char *name;
+ char *description;
+ unsigned priority;
+
+ pa_alsa_required_t required;
+ pa_alsa_required_t required_any;
+ pa_alsa_required_t required_absent;
+};
+
+/* An element wraps one specific ALSA element. A series of elements
+ * make up a path (see below). If the element is an enumeration or switch
+ * element it may include a list of options. */
+struct pa_alsa_element {
+ pa_alsa_path *path;
+ PA_LLIST_FIELDS(pa_alsa_element);
+
+ struct pa_alsa_mixer_id alsa_id;
+ pa_alsa_direction_t direction;
+
+ pa_alsa_switch_use_t switch_use;
+ pa_alsa_volume_use_t volume_use;
+ pa_alsa_enumeration_use_t enumeration_use;
+
+ pa_alsa_required_t required;
+ pa_alsa_required_t required_any;
+ pa_alsa_required_t required_absent;
+
+ long constant_volume;
+
+ unsigned int override_map;
+ bool direction_try_other:1;
+
+ bool has_dB:1;
+ long min_volume, max_volume;
+ long volume_limit; /* -1 for no configured limit */
+ double min_dB, max_dB;
+
+ pa_channel_position_mask_t masks[SND_MIXER_SCHN_LAST + 1][POSITION_MASK_CHANNELS];
+ unsigned n_channels;
+
+ pa_channel_position_mask_t merged_mask;
+
+ PA_LLIST_HEAD(pa_alsa_option, options);
+
+ pa_alsa_decibel_fix *db_fix;
+};
+
+struct pa_alsa_jack {
+ pa_alsa_path *path;
+ PA_LLIST_FIELDS(pa_alsa_jack);
+
+ snd_mixer_t *mixer_handle;
+ char *mixer_device_name;
+
+ struct pa_alsa_mixer_id alsa_id;
+ char *name; /* E g "Headphone" */
+ bool has_control; /* is the jack itself present? */
+ bool plugged_in; /* is this jack currently plugged in? */
+ snd_mixer_elem_t *melem; /* Jack detection handle */
+ pa_available_t state_unplugged, state_plugged;
+
+ pa_alsa_required_t required;
+ pa_alsa_required_t required_any;
+ pa_alsa_required_t required_absent;
+
+ pa_dynarray *ucm_devices; /* pa_alsa_ucm_device */
+ pa_dynarray *ucm_hw_mute_devices; /* pa_alsa_ucm_device */
+
+ bool append_pcm_to_name;
+};
+
+pa_alsa_jack *pa_alsa_jack_new(pa_alsa_path *path, const char *mixer_device_name, const char *name, int index);
+void pa_alsa_jack_free(pa_alsa_jack *jack);
+void pa_alsa_jack_set_has_control(pa_alsa_jack *jack, bool has_control);
+void pa_alsa_jack_set_plugged_in(pa_alsa_jack *jack, bool plugged_in);
+void pa_alsa_jack_add_ucm_device(pa_alsa_jack *jack, pa_alsa_ucm_device *device);
+void pa_alsa_jack_add_ucm_hw_mute_device(pa_alsa_jack *jack, pa_alsa_ucm_device *device);
+
+/* A path wraps a series of elements into a single entity which can be
+ * used to control it as if it had a single volume slider, a single
+ * mute switch and a single list of selectable options. */
+struct pa_alsa_path {
+ pa_alsa_direction_t direction;
+ pa_device_port* port;
+
+ char *name;
+ char *description_key;
+ char *description;
+ char *availability_group;
+ pa_device_port_type_t device_port_type;
+ unsigned priority;
+ bool autodetect_eld_device;
+ pa_alsa_mixer *eld_mixer_handle;
+ int eld_device;
+ pa_proplist *proplist;
+
+ bool probed:1;
+ bool supported:1;
+ bool has_mute:1;
+ bool has_volume:1;
+ bool has_dB:1;
+ bool mute_during_activation:1;
+ /* These two are used during probing only */
+ bool has_req_any:1;
+ bool req_any_present:1;
+
+ long min_volume, max_volume;
+ double min_dB, max_dB;
+
+ /* This is used during parsing only, as a shortcut so that we
+ * don't have to iterate the list all the time */
+ pa_alsa_element *last_element;
+ pa_alsa_option *last_option;
+ pa_alsa_setting *last_setting;
+ pa_alsa_jack *last_jack;
+
+ PA_LLIST_HEAD(pa_alsa_element, elements);
+ PA_LLIST_HEAD(pa_alsa_setting, settings);
+ PA_LLIST_HEAD(pa_alsa_jack, jacks);
+};
+
+/* A path set is simply a set of paths that are applicable to a
+ * device */
+struct pa_alsa_path_set {
+ pa_hashmap *paths;
+ pa_alsa_direction_t direction;
+};
+
+void pa_alsa_setting_dump(pa_alsa_setting *s);
+
+void pa_alsa_option_dump(pa_alsa_option *o);
+void pa_alsa_jack_dump(pa_alsa_jack *j);
+void pa_alsa_element_dump(pa_alsa_element *e);
+
+pa_alsa_path *pa_alsa_path_new(const char *paths_dir, const char *fname, pa_alsa_direction_t direction);
+pa_alsa_path *pa_alsa_path_synthesize(const char *element, pa_alsa_direction_t direction);
+pa_alsa_element *pa_alsa_element_get(pa_alsa_path *p, const char *section, bool prefixed);
+int pa_alsa_path_probe(pa_alsa_path *p, pa_alsa_mapping *mapping, snd_mixer_t *m, bool ignore_dB);
+void pa_alsa_path_dump(pa_alsa_path *p);
+int pa_alsa_path_get_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v);
+int pa_alsa_path_get_mute(pa_alsa_path *path, snd_mixer_t *m, bool *muted);
+int pa_alsa_path_set_volume(pa_alsa_path *path, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, bool deferred_volume, bool write_to_hw);
+int pa_alsa_path_set_mute(pa_alsa_path *path, snd_mixer_t *m, bool muted);
+int pa_alsa_path_select(pa_alsa_path *p, pa_alsa_setting *s, snd_mixer_t *m, bool device_is_muted);
+void pa_alsa_path_set_callback(pa_alsa_path *p, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata);
+void pa_alsa_path_free(pa_alsa_path *p);
+
+pa_alsa_path_set *pa_alsa_path_set_new(pa_alsa_mapping *m, pa_alsa_direction_t direction, const char *paths_dir);
+void pa_alsa_path_set_dump(pa_alsa_path_set *s);
+void pa_alsa_path_set_set_callback(pa_alsa_path_set *ps, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata);
+void pa_alsa_path_set_free(pa_alsa_path_set *s);
+int pa_alsa_path_set_is_empty(pa_alsa_path_set *s);
+
+struct pa_alsa_device {
+ struct acp_device device;
+
+ pa_card *card;
+
+ pa_alsa_direction_t direction;
+ pa_proplist *proplist;
+
+ pa_alsa_mapping *mapping;
+ pa_alsa_ucm_mapping_context *ucm_context;
+
+ pa_hashmap *ports;
+ pa_dynarray port_array;
+ pa_device_port *active_port;
+
+ snd_mixer_t *mixer_handle;
+ pa_alsa_path_set *mixer_path_set;
+ pa_alsa_path *mixer_path;
+ snd_pcm_t *pcm_handle;
+
+ unsigned muted:1;
+ unsigned decibel_volume:1;
+ pa_cvolume real_volume;
+ pa_cvolume hardware_volume;
+ pa_cvolume soft_volume;
+
+ pa_volume_t base_volume;
+ unsigned n_volume_steps;
+
+ int (*read_volume)(pa_alsa_device *dev);
+ int (*read_mute)(pa_alsa_device *dev);
+
+ void (*set_volume)(pa_alsa_device *dev, const pa_cvolume *v);
+ void (*set_mute)(pa_alsa_device *dev, bool m);
+};
+
+struct pa_alsa_mapping {
+ pa_alsa_profile_set *profile_set;
+
+ char *name;
+ char *description;
+ char *description_key;
+ unsigned priority;
+ pa_alsa_direction_t direction;
+ /* These are copied over to the resultant sink/source */
+ pa_proplist *proplist;
+
+ pa_sample_spec sample_spec;
+ pa_channel_map channel_map;
+
+ char **device_strings;
+
+ char **input_path_names;
+ char **output_path_names;
+ char **input_element; /* list of fallbacks */
+ char **output_element;
+ pa_alsa_path_set *input_path_set;
+ pa_alsa_path_set *output_path_set;
+
+ unsigned supported;
+ bool exact_channels:1;
+ bool fallback:1;
+
+ /* The "y" in "hw:x,y". This is set to -1 before the device index has been
+ * queried, or if the query failed. */
+ int hw_device_index;
+
+ /* Temporarily used during probing */
+ snd_pcm_t *input_pcm;
+ snd_pcm_t *output_pcm;
+
+ pa_proplist *input_proplist;
+ pa_proplist *output_proplist;
+
+ pa_alsa_device output;
+ pa_alsa_device input;
+
+ /* ucm device context*/
+ pa_alsa_ucm_mapping_context ucm_context;
+};
+
+struct pa_alsa_profile {
+ struct acp_card_profile profile;
+
+ pa_alsa_profile_set *profile_set;
+
+ char *name;
+ char *description;
+ char *description_key;
+ unsigned priority;
+
+ char *input_name;
+ char *output_name;
+
+ bool supported:1;
+ bool fallback_input:1;
+ bool fallback_output:1;
+
+ char **input_mapping_names;
+ char **output_mapping_names;
+
+ pa_idxset *input_mappings;
+ pa_idxset *output_mappings;
+
+ struct {
+ pa_dynarray devices;
+ } out;
+};
+
+struct pa_alsa_decibel_fix {
+ char *key;
+
+ pa_alsa_profile_set *profile_set;
+
+ char *name; /* Alsa volume element name. */
+ int index; /* Alsa volume element index. */
+ long min_step;
+ long max_step;
+
+ /* An array that maps alsa volume element steps to decibels. The steps can
+ * be used as indices to this array, after subtracting min_step from the
+ * real value.
+ *
+ * The values are actually stored as integers representing millibels,
+ * because that's the format the alsa API uses. */
+ long *db_values;
+};
+
+struct pa_alsa_profile_set {
+ pa_hashmap *mappings;
+ pa_hashmap *profiles;
+ pa_hashmap *decibel_fixes;
+ pa_hashmap *input_paths;
+ pa_hashmap *output_paths;
+
+ bool auto_profiles;
+ bool ignore_dB:1;
+ bool probed:1;
+};
+
+void pa_alsa_mapping_dump(pa_alsa_mapping *m);
+void pa_alsa_profile_dump(pa_alsa_profile *p);
+void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix);
+pa_alsa_mapping *pa_alsa_mapping_get(pa_alsa_profile_set *ps, const char *name);
+void pa_alsa_mapping_free (pa_alsa_mapping *m);
+void pa_alsa_profile_free (pa_alsa_profile *p);
+
+pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus);
+void pa_alsa_profile_set_probe(pa_alsa_profile_set *ps, pa_hashmap *mixers, const char *dev_id, const pa_sample_spec *ss, unsigned default_n_fragments, unsigned default_fragment_size_msec);
+void pa_alsa_profile_set_free(pa_alsa_profile_set *s);
+void pa_alsa_profile_set_dump(pa_alsa_profile_set *s);
+void pa_alsa_profile_set_drop_unsupported(pa_alsa_profile_set *s);
+
+void pa_alsa_mixer_use_for_poll(pa_hashmap *mixers, snd_mixer_t *mixer_handle);
+
+#if 0
+pa_alsa_fdlist *pa_alsa_fdlist_new(void);
+void pa_alsa_fdlist_free(pa_alsa_fdlist *fdl);
+int pa_alsa_fdlist_set_handle(pa_alsa_fdlist *fdl, snd_mixer_t *mixer_handle, snd_hctl_t *hctl_handle, pa_mainloop_api* m);
+
+/* Alternative for handling alsa mixer events in io-thread. */
+
+pa_alsa_mixer_pdata *pa_alsa_mixer_pdata_new(void);
+void pa_alsa_mixer_pdata_free(pa_alsa_mixer_pdata *pd);
+int pa_alsa_set_mixer_rtpoll(struct pa_alsa_mixer_pdata *pd, snd_mixer_t *mixer, pa_rtpoll *rtp);
+#endif
+
+/* Data structure for inclusion in pa_device_port for alsa
+ * sinks/sources. This contains nothing that needs to be freed
+ * individually */
+struct pa_alsa_port_data {
+ pa_alsa_path *path;
+ pa_alsa_setting *setting;
+ bool suspend_when_unavailable;
+};
+
+void pa_alsa_add_ports(pa_hashmap *ports, pa_alsa_path_set *ps, pa_card *card);
+void pa_alsa_path_set_add_ports(pa_alsa_path_set *ps, pa_alsa_profile *cp, pa_hashmap *ports, pa_hashmap *extra, pa_core *core);
+
+#endif
diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c
new file mode 100644
index 0000000..f66b771
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-ucm.c
@@ -0,0 +1,2420 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2011 Wolfson Microelectronics PLC
+ Author Margarita Olaya <magi@slimlogic.co.uk>
+ Copyright 2012 Feng Wei <wei.feng@freescale.com>, Freescale Ltd.
+
+ 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/>.
+
+***/
+
+#include "config.h"
+
+#include <ctype.h>
+#include <sys/types.h>
+#include <limits.h>
+#include <alsa/asoundlib.h>
+
+#ifdef HAVE_VALGRIND_MEMCHECK_H
+#include <valgrind/memcheck.h>
+#endif
+
+#include "alsa-mixer.h"
+#include "alsa-util.h"
+#include "alsa-ucm.h"
+
+#define PA_UCM_PRE_TAG_OUTPUT "[Out] "
+#define PA_UCM_PRE_TAG_INPUT "[In] "
+
+#define PA_UCM_PLAYBACK_PRIORITY_UNSET(device) ((device)->playback_channels && !(device)->playback_priority)
+#define PA_UCM_CAPTURE_PRIORITY_UNSET(device) ((device)->capture_channels && !(device)->capture_priority)
+#define PA_UCM_DEVICE_PRIORITY_SET(device, priority) \
+ do { \
+ if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device)) (device)->playback_priority = (priority); \
+ if (PA_UCM_CAPTURE_PRIORITY_UNSET(device)) (device)->capture_priority = (priority); \
+ } while (0)
+#define PA_UCM_IS_MODIFIER_MAPPING(m) ((pa_proplist_gets((m)->proplist, PA_ALSA_PROP_UCM_MODIFIER)) != NULL)
+
+#ifdef HAVE_ALSA_UCM
+
+struct ucm_type {
+ const char *prefix;
+ pa_device_port_type_t type;
+};
+
+struct ucm_items {
+ const char *id;
+ const char *property;
+};
+
+struct ucm_info {
+ const char *id;
+ unsigned priority;
+};
+
+static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device);
+static void device_set_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack);
+static void device_add_hw_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack);
+
+static pa_alsa_ucm_device *verb_find_device(pa_alsa_ucm_verb *verb, const char *device_name);
+
+
+static void ucm_port_data_init(pa_alsa_ucm_port_data *port, pa_alsa_ucm_config *ucm, pa_device_port *core_port,
+ pa_alsa_ucm_device **devices, unsigned n_devices);
+static void ucm_port_data_free(pa_device_port *port);
+static void ucm_port_update_available(pa_alsa_ucm_port_data *port);
+
+static struct ucm_type types[] = {
+ {"None", PA_DEVICE_PORT_TYPE_UNKNOWN},
+ {"Speaker", PA_DEVICE_PORT_TYPE_SPEAKER},
+ {"Line", PA_DEVICE_PORT_TYPE_LINE},
+ {"Mic", PA_DEVICE_PORT_TYPE_MIC},
+ {"Headphones", PA_DEVICE_PORT_TYPE_HEADPHONES},
+ {"Headset", PA_DEVICE_PORT_TYPE_HEADSET},
+ {"Handset", PA_DEVICE_PORT_TYPE_HANDSET},
+ {"Bluetooth", PA_DEVICE_PORT_TYPE_BLUETOOTH},
+ {"Earpiece", PA_DEVICE_PORT_TYPE_EARPIECE},
+ {"SPDIF", PA_DEVICE_PORT_TYPE_SPDIF},
+ {"HDMI", PA_DEVICE_PORT_TYPE_HDMI},
+ {NULL, 0}
+};
+
+static struct ucm_items item[] = {
+ {"PlaybackPCM", PA_ALSA_PROP_UCM_SINK},
+ {"CapturePCM", PA_ALSA_PROP_UCM_SOURCE},
+ {"PlaybackCTL", PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE},
+ {"PlaybackVolume", PA_ALSA_PROP_UCM_PLAYBACK_VOLUME},
+ {"PlaybackSwitch", PA_ALSA_PROP_UCM_PLAYBACK_SWITCH},
+ {"PlaybackMixer", PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE},
+ {"PlaybackMixerElem", PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM},
+ {"PlaybackMasterElem", PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM},
+ {"PlaybackMasterType", PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE},
+ {"PlaybackPriority", PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY},
+ {"PlaybackRate", PA_ALSA_PROP_UCM_PLAYBACK_RATE},
+ {"PlaybackChannels", PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS},
+ {"CaptureCTL", PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE},
+ {"CaptureVolume", PA_ALSA_PROP_UCM_CAPTURE_VOLUME},
+ {"CaptureSwitch", PA_ALSA_PROP_UCM_CAPTURE_SWITCH},
+ {"CaptureMixer", PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE},
+ {"CaptureMixerElem", PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM},
+ {"CaptureMasterElem", PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM},
+ {"CaptureMasterType", PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE},
+ {"CapturePriority", PA_ALSA_PROP_UCM_CAPTURE_PRIORITY},
+ {"CaptureRate", PA_ALSA_PROP_UCM_CAPTURE_RATE},
+ {"CaptureChannels", PA_ALSA_PROP_UCM_CAPTURE_CHANNELS},
+ {"TQ", PA_ALSA_PROP_UCM_QOS},
+ {"JackCTL", PA_ALSA_PROP_UCM_JACK_DEVICE},
+ {"JackControl", PA_ALSA_PROP_UCM_JACK_CONTROL},
+ {"JackHWMute", PA_ALSA_PROP_UCM_JACK_HW_MUTE},
+ {NULL, NULL},
+};
+
+/* UCM verb info - this should eventually be part of policy management */
+static struct ucm_info verb_info[] = {
+ {SND_USE_CASE_VERB_INACTIVE, 0},
+ {SND_USE_CASE_VERB_HIFI, 8000},
+ {SND_USE_CASE_VERB_HIFI_LOW_POWER, 7000},
+ {SND_USE_CASE_VERB_VOICE, 6000},
+ {SND_USE_CASE_VERB_VOICE_LOW_POWER, 5000},
+ {SND_USE_CASE_VERB_VOICECALL, 4000},
+ {SND_USE_CASE_VERB_IP_VOICECALL, 4000},
+ {SND_USE_CASE_VERB_ANALOG_RADIO, 3000},
+ {SND_USE_CASE_VERB_DIGITAL_RADIO, 3000},
+ {NULL, 0}
+};
+
+/* UCM device info - should be overwritten by ucm property */
+static struct ucm_info dev_info[] = {
+ {SND_USE_CASE_DEV_SPEAKER, 100},
+ {SND_USE_CASE_DEV_LINE, 100},
+ {SND_USE_CASE_DEV_HEADPHONES, 100},
+ {SND_USE_CASE_DEV_HEADSET, 300},
+ {SND_USE_CASE_DEV_HANDSET, 200},
+ {SND_USE_CASE_DEV_BLUETOOTH, 400},
+ {SND_USE_CASE_DEV_EARPIECE, 100},
+ {SND_USE_CASE_DEV_SPDIF, 100},
+ {SND_USE_CASE_DEV_HDMI, 100},
+ {SND_USE_CASE_DEV_NONE, 100},
+ {NULL, 0}
+};
+
+
+static char *ucm_verb_value(
+ snd_use_case_mgr_t *uc_mgr,
+ const char *verb_name,
+ const char *id) {
+
+ const char *value;
+ char *_id = pa_sprintf_malloc("=%s//%s", id, verb_name);
+ int err = snd_use_case_get(uc_mgr, _id, &value);
+ pa_xfree(_id);
+ if (err < 0)
+ return NULL;
+ pa_log_debug("Got %s for verb %s: %s", id, verb_name, value);
+ /* Use the cast here to allow free() call without casting for callers.
+ * The snd_use_case_get() returns mallocated string.
+ * See the Note: in use-case.h for snd_use_case_get().
+ */
+ return (char *)value;
+}
+
+static int ucm_device_exists(pa_idxset *idxset, pa_alsa_ucm_device *dev) {
+ pa_alsa_ucm_device *d;
+ uint32_t idx;
+
+ PA_IDXSET_FOREACH(d, idxset, idx)
+ if (d == dev)
+ return 1;
+
+ return 0;
+}
+
+static void ucm_add_devices_to_idxset(
+ pa_idxset *idxset,
+ pa_alsa_ucm_device *me,
+ pa_alsa_ucm_device *devices,
+ const char **dev_names,
+ int n) {
+
+ pa_alsa_ucm_device *d;
+
+ PA_LLIST_FOREACH(d, devices) {
+ const char *name;
+ int i;
+
+ if (d == me)
+ continue;
+
+ name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ for (i = 0; i < n; i++)
+ if (pa_streq(dev_names[i], name))
+ pa_idxset_put(idxset, d, NULL);
+ }
+}
+
+/* Split a string into words. Like pa_split_spaces() but handle '' and "". */
+static char *ucm_split_devnames(const char *c, const char **state) {
+ const char *current = *state ? *state : c;
+ char h;
+ size_t l;
+
+ if (!*current || *c == 0)
+ return NULL;
+
+ current += strspn(current, "\n\r \t");
+ h = *current;
+ if (h == '\'' || h =='"') {
+ c = ++current;
+ for (l = 0; *c && *c != h; l++) c++;
+ if (*c != h)
+ return NULL;
+ *state = c + 1;
+ } else {
+ l = strcspn(current, "\n\r \t");
+ *state = current+l;
+ }
+
+ return pa_xstrndup(current, l);
+}
+
+
+static void ucm_volume_free(pa_alsa_ucm_volume *vol) {
+ pa_assert(vol);
+ pa_xfree(vol->mixer_elem);
+ pa_xfree(vol->master_elem);
+ pa_xfree(vol->master_type);
+ pa_xfree(vol);
+}
+
+/* Get the volume identifier */
+static char *ucm_get_mixer_id(
+ pa_alsa_ucm_device *device,
+ const char *mprop,
+ const char *cprop,
+ const char *cid)
+{
+#if SND_LIB_VERSION >= 0x10201 /* alsa-lib-1.2.1+ check */
+ snd_ctl_elem_id_t *ctl;
+ int err;
+#endif
+ const char *value;
+ char *value2;
+ int index;
+
+ /* mixer element as first, if it's found, return it without modifications */
+ value = pa_proplist_gets(device->proplist, mprop);
+ if (value)
+ return pa_xstrdup(value);
+ /* fallback, get the control element identifier */
+ /* and try to do some heuristic to determine the mixer element name */
+ value = pa_proplist_gets(device->proplist, cprop);
+ if (value == NULL)
+ return NULL;
+#if SND_LIB_VERSION >= 0x10201 /* alsa-lib-1.2.1+ check */
+ /* The new parser may return also element index. */
+ snd_ctl_elem_id_alloca(&ctl);
+ err = snd_use_case_parse_ctl_elem_id(ctl, cid, value);
+ if (err < 0)
+ return NULL;
+ value = snd_ctl_elem_id_get_name(ctl);
+ index = snd_ctl_elem_id_get_index(ctl);
+#else
+#warning "Upgrade to alsa-lib 1.2.1!"
+ index = 0;
+#endif
+ if (!(value2 = pa_str_strip_suffix(value, " Playback Volume")))
+ if (!(value2 = pa_str_strip_suffix(value, " Capture Volume")))
+ if (!(value2 = pa_str_strip_suffix(value, " Volume")))
+ value2 = pa_xstrdup(value);
+ if (index > 0) {
+ char *mix = pa_sprintf_malloc("'%s',%d", value2, index);
+ pa_xfree(value2);
+ return mix;
+ }
+ return value2;
+}
+
+/* Get the volume identifier */
+static pa_alsa_ucm_volume *ucm_get_mixer_volume(
+ pa_alsa_ucm_device *device,
+ const char *mprop,
+ const char *cprop,
+ const char *cid,
+ const char *masterid,
+ const char *mastertype)
+{
+ pa_alsa_ucm_volume *vol;
+ char *mixer_elem;
+
+ mixer_elem = ucm_get_mixer_id(device, mprop, cprop, cid);
+ if (mixer_elem == NULL)
+ return NULL;
+ vol = pa_xnew0(pa_alsa_ucm_volume, 1);
+ if (vol == NULL) {
+ pa_xfree(mixer_elem);
+ return NULL;
+ }
+ vol->mixer_elem = mixer_elem;
+ vol->master_elem = pa_xstrdup(pa_proplist_gets(device->proplist, masterid));
+ vol->master_type = pa_xstrdup(pa_proplist_gets(device->proplist, mastertype));
+ return vol;
+}
+
+/* Get the ALSA mixer device for the UCM device */
+static const char *get_mixer_device(pa_alsa_ucm_device *dev, bool is_sink)
+{
+ const char *dev_name;
+
+ if (is_sink) {
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE);
+ if (!dev_name)
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE);
+ } else {
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE);
+ if (!dev_name)
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE);
+ }
+ return dev_name;
+}
+
+/* Get the ALSA mixer device for the UCM jack */
+static const char *get_jack_mixer_device(pa_alsa_ucm_device *dev, bool is_sink) {
+ const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_JACK_DEVICE);
+ if (!dev_name)
+ return get_mixer_device(dev, is_sink);
+ return dev_name;
+}
+
+/* Create a property list for this ucm device */
+static int ucm_get_device_property(
+ pa_alsa_ucm_device *device,
+ snd_use_case_mgr_t *uc_mgr,
+ pa_alsa_ucm_verb *verb,
+ const char *device_name) {
+
+ const char *value;
+ const char **devices;
+ char *id, *s;
+ int i;
+ int err;
+ uint32_t ui;
+ int n_confdev, n_suppdev;
+ pa_alsa_ucm_volume *vol;
+
+ /* determine the device type */
+ device->type = PA_DEVICE_PORT_TYPE_UNKNOWN;
+ id = s = pa_xstrdup(device_name);
+ while (s && *s && isalpha(*s)) s++;
+ if (s)
+ *s = '\0';
+ for (i = 0; types[i].prefix; i++)
+ if (pa_streq(id, types[i].prefix)) {
+ device->type = types[i].type;
+ break;
+ }
+ pa_xfree(id);
+
+ /* set properties */
+ for (i = 0; item[i].id; i++) {
+ id = pa_sprintf_malloc("%s/%s", item[i].id, device_name);
+ err = snd_use_case_get(uc_mgr, id, &value);
+ pa_xfree(id);
+ if (err < 0)
+ continue;
+
+ pa_log_debug("Got %s for device %s: %s", item[i].id, device_name, value);
+ pa_proplist_sets(device->proplist, item[i].property, value);
+ free((void*)value);
+ }
+
+ /* get direction and channels */
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS);
+ if (value) { /* output */
+ /* get channels */
+ if (pa_atou(value, &ui) == 0 && pa_channels_valid(ui))
+ device->playback_channels = ui;
+ else
+ pa_log("UCM playback channels %s for device %s out of range", value, device_name);
+
+ /* get pcm */
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK);
+ if (!value) /* take pcm from verb playback default */
+ pa_log("UCM playback device %s fetch pcm failed", device_name);
+ }
+
+ if (pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK) &&
+ device->playback_channels == 0) {
+ pa_log_info("UCM file does not specify 'PlaybackChannels' "
+ "for device %s, assuming stereo.", device_name);
+ device->playback_channels = 2;
+ }
+
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_CHANNELS);
+ if (value) { /* input */
+ /* get channels */
+ if (pa_atou(value, &ui) == 0 && pa_channels_valid(ui))
+ device->capture_channels = ui;
+ else
+ pa_log("UCM capture channels %s for device %s out of range", value, device_name);
+
+ /* get pcm */
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE);
+ if (!value) /* take pcm from verb capture default */
+ pa_log("UCM capture device %s fetch pcm failed", device_name);
+ }
+
+ if (pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE) &&
+ device->capture_channels == 0) {
+ pa_log_info("UCM file does not specify 'CaptureChannels' "
+ "for device %s, assuming stereo.", device_name);
+ device->capture_channels = 2;
+ }
+
+ /* get rate and priority of device */
+ if (device->playback_channels) { /* sink device */
+ /* get rate */
+ if ((value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_RATE))) {
+ if (pa_atou(value, &ui) == 0 && pa_sample_rate_valid(ui)) {
+ pa_log_debug("UCM playback device %s rate %d", device_name, ui);
+ device->playback_rate = ui;
+ } else
+ pa_log_debug("UCM playback device %s has bad rate %s", device_name, value);
+ }
+
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY);
+ if (value) {
+ /* get priority from ucm config */
+ if (pa_atou(value, &ui) == 0)
+ device->playback_priority = ui;
+ else
+ pa_log_debug("UCM playback priority %s for device %s error", value, device_name);
+ }
+
+ vol = ucm_get_mixer_volume(device,
+ PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM,
+ PA_ALSA_PROP_UCM_PLAYBACK_VOLUME,
+ "PlaybackVolume",
+ PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM,
+ PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE);
+ if (vol)
+ pa_hashmap_put(device->playback_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol);
+ }
+
+ if (device->capture_channels) { /* source device */
+ /* get rate */
+ if ((value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_RATE))) {
+ if (pa_atou(value, &ui) == 0 && pa_sample_rate_valid(ui)) {
+ pa_log_debug("UCM capture device %s rate %d", device_name, ui);
+ device->capture_rate = ui;
+ } else
+ pa_log_debug("UCM capture device %s has bad rate %s", device_name, value);
+ }
+
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_PRIORITY);
+ if (value) {
+ /* get priority from ucm config */
+ if (pa_atou(value, &ui) == 0)
+ device->capture_priority = ui;
+ else
+ pa_log_debug("UCM capture priority %s for device %s error", value, device_name);
+ }
+
+ vol = ucm_get_mixer_volume(device,
+ PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM,
+ PA_ALSA_PROP_UCM_CAPTURE_VOLUME,
+ "CaptureVolume",
+ PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM,
+ PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE);
+ if (vol)
+ pa_hashmap_put(device->capture_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol);
+ }
+
+ 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++) {
+ if (strcasecmp(dev_info[i].id, device_name) == 0) {
+ PA_UCM_DEVICE_PRIORITY_SET(device, dev_info[i].priority);
+ break;
+ }
+ }
+ }
+
+ if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device)) {
+ /* fall through to default priority */
+ device->playback_priority = 100;
+ }
+
+ if (PA_UCM_CAPTURE_PRIORITY_UNSET(device)) {
+ /* fall through to default priority */
+ device->capture_priority = 100;
+ }
+
+ id = pa_sprintf_malloc("%s/%s", "_conflictingdevs", device_name);
+ n_confdev = snd_use_case_get_list(uc_mgr, id, &devices);
+ pa_xfree(id);
+
+ if (n_confdev <= 0)
+ pa_log_debug("No %s for device %s", "_conflictingdevs", device_name);
+ else {
+ device->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ ucm_add_devices_to_idxset(device->conflicting_devices, device, verb->devices, devices, n_confdev);
+ snd_use_case_free_list(devices, n_confdev);
+ }
+
+ id = pa_sprintf_malloc("%s/%s", "_supporteddevs", device_name);
+ n_suppdev = snd_use_case_get_list(uc_mgr, id, &devices);
+ pa_xfree(id);
+
+ if (n_suppdev <= 0)
+ pa_log_debug("No %s for device %s", "_supporteddevs", device_name);
+ else {
+ device->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ ucm_add_devices_to_idxset(device->supported_devices, device, verb->devices, devices, n_suppdev);
+ snd_use_case_free_list(devices, n_suppdev);
+ }
+
+ return 0;
+};
+
+/* Create a property list for this ucm modifier */
+static int ucm_get_modifier_property(pa_alsa_ucm_modifier *modifier, snd_use_case_mgr_t *uc_mgr, const char *modifier_name) {
+ const char *value;
+ char *id;
+ int i;
+
+ for (i = 0; item[i].id; i++) {
+ int err;
+
+ id = pa_sprintf_malloc("=%s/%s", item[i].id, modifier_name);
+ err = snd_use_case_get(uc_mgr, id, &value);
+ pa_xfree(id);
+ if (err < 0)
+ continue;
+
+ pa_log_debug("Got %s for modifier %s: %s", item[i].id, modifier_name, value);
+ pa_proplist_sets(modifier->proplist, item[i].property, value);
+ free((void*)value);
+ }
+
+ id = pa_sprintf_malloc("%s/%s", "_conflictingdevs", modifier_name);
+ modifier->n_confdev = snd_use_case_get_list(uc_mgr, id, &modifier->conflicting_devices);
+ pa_xfree(id);
+ if (modifier->n_confdev < 0)
+ pa_log_debug("No %s for modifier %s", "_conflictingdevs", modifier_name);
+
+ id = pa_sprintf_malloc("%s/%s", "_supporteddevs", modifier_name);
+ modifier->n_suppdev = snd_use_case_get_list(uc_mgr, id, &modifier->supported_devices);
+ pa_xfree(id);
+ if (modifier->n_suppdev < 0)
+ pa_log_debug("No %s for modifier %s", "_supporteddevs", modifier_name);
+
+ return 0;
+};
+
+/* Create a list of devices for this verb */
+static int ucm_get_devices(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) {
+ const char **dev_list;
+ int num_dev, i;
+
+ num_dev = snd_use_case_get_list(uc_mgr, "_devices", &dev_list);
+ if (num_dev < 0)
+ return num_dev;
+
+ for (i = 0; i < num_dev; i += 2) {
+ pa_alsa_ucm_device *d = pa_xnew0(pa_alsa_ucm_device, 1);
+
+ d->proplist = pa_proplist_new();
+ pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(dev_list[i]));
+ pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(dev_list[i + 1]));
+ d->ucm_ports = pa_dynarray_new(NULL);
+ d->hw_mute_jacks = pa_dynarray_new(NULL);
+ d->available = PA_AVAILABLE_UNKNOWN;
+
+ d->playback_volumes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree,
+ (pa_free_cb_t) ucm_volume_free);
+ d->capture_volumes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree,
+ (pa_free_cb_t) ucm_volume_free);
+
+ PA_LLIST_PREPEND(pa_alsa_ucm_device, verb->devices, d);
+ }
+
+ snd_use_case_free_list(dev_list, num_dev);
+
+ return 0;
+};
+
+static int ucm_get_modifiers(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) {
+ const char **mod_list;
+ int num_mod, i;
+
+ num_mod = snd_use_case_get_list(uc_mgr, "_modifiers", &mod_list);
+ if (num_mod < 0)
+ return num_mod;
+
+ for (i = 0; i < num_mod; i += 2) {
+ pa_alsa_ucm_modifier *m;
+
+ if (!mod_list[i]) {
+ pa_log_warn("Got a modifier with a null name. Skipping.");
+ continue;
+ }
+
+ m = pa_xnew0(pa_alsa_ucm_modifier, 1);
+ m->proplist = pa_proplist_new();
+
+ pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_NAME, mod_list[i]);
+ pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(mod_list[i + 1]));
+
+ PA_LLIST_PREPEND(pa_alsa_ucm_modifier, verb->modifiers, m);
+ }
+
+ snd_use_case_free_list(mod_list, num_mod);
+
+ return 0;
+};
+
+static void add_role_to_device(pa_alsa_ucm_device *dev, const char *dev_name, const char *role_name, const char *role) {
+ const char *cur = pa_proplist_gets(dev->proplist, role_name);
+
+ if (!cur)
+ pa_proplist_sets(dev->proplist, role_name, role);
+ else if (!pa_str_in_list_spaces(cur, role)) { /* does not exist */
+ char *value = pa_sprintf_malloc("%s %s", cur, role);
+
+ pa_proplist_sets(dev->proplist, role_name, value);
+ pa_xfree(value);
+ }
+
+ pa_log_info("Add role %s to device %s(%s), result %s", role, dev_name, role_name, pa_proplist_gets(dev->proplist,
+ role_name));
+}
+
+static void add_media_role(const char *name, pa_alsa_ucm_device *list, const char *role_name, const char *role, bool is_sink) {
+ pa_alsa_ucm_device *d;
+
+ PA_LLIST_FOREACH(d, list) {
+ const char *dev_name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ if (pa_streq(dev_name, name)) {
+ 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 (is_sink && sink)
+ add_role_to_device(d, dev_name, role_name, role);
+ else if (!is_sink && source)
+ add_role_to_device(d, dev_name, role_name, role);
+ break;
+ }
+ }
+}
+
+static char *modifier_name_to_role(const char *mod_name, bool *is_sink) {
+ char *sub = NULL, *tmp;
+
+ *is_sink = false;
+
+ if (pa_startswith(mod_name, "Play")) {
+ *is_sink = true;
+ sub = pa_xstrdup(mod_name + 4);
+ } else if (pa_startswith(mod_name, "Capture"))
+ sub = pa_xstrdup(mod_name + 7);
+
+ if (!sub || !*sub) {
+ pa_xfree(sub);
+ pa_log_warn("Can't match media roles for modifier %s", mod_name);
+ return NULL;
+ }
+
+ tmp = sub;
+
+ do {
+ *tmp = tolower(*tmp);
+ } while (*(++tmp));
+
+ return sub;
+}
+
+static void ucm_set_media_roles(pa_alsa_ucm_modifier *modifier, pa_alsa_ucm_device *list, const char *mod_name) {
+ int i;
+ bool is_sink = false;
+ char *sub = NULL;
+ const char *role_name;
+
+ sub = modifier_name_to_role(mod_name, &is_sink);
+ if (!sub)
+ return;
+
+ modifier->action_direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT;
+ modifier->media_role = sub;
+
+ role_name = is_sink ? PA_ALSA_PROP_UCM_PLAYBACK_ROLES : PA_ALSA_PROP_UCM_CAPTURE_ROLES;
+ for (i = 0; i < modifier->n_suppdev; i++) {
+ /* if modifier has no specific pcm, we add role intent to its supported devices */
+ if (!pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_SINK) &&
+ !pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_SOURCE))
+ add_media_role(modifier->supported_devices[i], list, role_name, sub, is_sink);
+ }
+}
+
+static void append_lost_relationship(pa_alsa_ucm_device *dev) {
+ uint32_t idx;
+ pa_alsa_ucm_device *d;
+
+ if (dev->conflicting_devices) {
+ PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) {
+ if (!d->conflicting_devices)
+ d->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ if (pa_idxset_put(d->conflicting_devices, dev, NULL) == 0)
+ pa_log_warn("Add lost conflicting device %s to %s",
+ pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME),
+ pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME));
+ }
+ }
+
+ if (dev->supported_devices) {
+ PA_IDXSET_FOREACH(d, dev->supported_devices, idx) {
+ if (!d->supported_devices)
+ d->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ if (pa_idxset_put(d->supported_devices, dev, NULL) == 0)
+ pa_log_warn("Add lost supported device %s to %s",
+ pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME),
+ pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME));
+ }
+ }
+}
+
+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;
+
+ /* support multiple card instances, address card directly by index */
+ card_name = pa_sprintf_malloc("hw:%i", 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) {
+ /* fallback longname: is UCM available for this card ? */
+ pa_xfree(card_name);
+ err = snd_card_get_name(card_index, &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;
+ }
+
+ err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name);
+ if (err < 0) {
+ pa_log_info("UCM not available for card %s", card_name);
+ err = -PA_ALSA_ERR_UCM_OPEN;
+ goto ucm_mgr_fail;
+ }
+ }
+
+ err = snd_use_case_get(ucm->ucm_mgr, "=Linked", &value);
+ if (err >= 0) {
+ if (strcasecmp(value, "true") == 0 || strcasecmp(value, "1") == 0) {
+ free((void *)value);
+ pa_log_info("Empty (linked) UCM for card %s", card_name);
+ err = -PA_ALSA_ERR_UCM_LINKED;
+ goto ucm_verb_fail;
+ }
+ free((void *)value);
+ }
+
+ pa_log_info("UCM available for card %s", card_name);
+
+ if (snd_use_case_get(ucm->ucm_mgr, "_alibpref", &value) == 0) {
+ if (value[0]) {
+ ucm->alib_prefix = pa_xstrdup(value);
+ pa_log_debug("UCM _alibpref=%s", ucm->alib_prefix);
+ }
+ free((void *)value);
+ }
+
+ /* get a list of all UCM verbs (profiles) for this card */
+ num_verbs = snd_use_case_verb_list(ucm->ucm_mgr, &verb_list);
+ if (num_verbs < 0) {
+ pa_log("UCM verb list not found for %s", card_name);
+ err = -PA_ALSA_ERR_UNSPECIFIED;
+ goto ucm_verb_fail;
+ }
+
+ /* get the properties of each UCM verb */
+ for (i = 0; i < num_verbs; i += 2) {
+ pa_alsa_ucm_verb *verb;
+
+ /* Get devices and modifiers for each verb */
+ err = pa_alsa_ucm_get_verb(ucm->ucm_mgr, verb_list[i], verb_list[i+1], &verb);
+ if (err < 0) {
+ pa_log("Failed to get the verb %s", verb_list[i]);
+ continue;
+ }
+
+ PA_LLIST_PREPEND(pa_alsa_ucm_verb, ucm->verbs, verb);
+ }
+
+ if (!ucm->verbs) {
+ pa_log("No UCM verb is valid for %s", card_name);
+ err = -PA_ALSA_ERR_UCM_NO_VERB;
+ }
+
+ snd_use_case_free_list(verb_list, num_verbs);
+
+ucm_verb_fail:
+ if (err < 0) {
+ snd_use_case_mgr_close(ucm->ucm_mgr);
+ ucm->ucm_mgr = NULL;
+ }
+
+ucm_mgr_fail:
+ pa_xfree(card_name);
+
+name_fail:
+ return err;
+}
+
+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;
+ pa_alsa_ucm_verb *verb;
+ char *value;
+ unsigned ui;
+ int err = 0;
+
+ *p_verb = NULL;
+ pa_log_info("Set UCM verb to %s", verb_name);
+ err = snd_use_case_set(uc_mgr, "_verb", verb_name);
+ if (err < 0)
+ return err;
+
+ verb = pa_xnew0(pa_alsa_ucm_verb, 1);
+ verb->proplist = pa_proplist_new();
+
+ pa_proplist_sets(verb->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(verb_name));
+ pa_proplist_sets(verb->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(verb_desc));
+
+ value = ucm_verb_value(uc_mgr, verb_name, "Priority");
+ if (value && !pa_atou(value, &ui))
+ verb->priority = ui > 10000 ? 10000 : ui;
+ free(value);
+
+ err = ucm_get_devices(verb, uc_mgr);
+ if (err < 0)
+ pa_log("No UCM devices for verb %s", verb_name);
+
+ err = ucm_get_modifiers(verb, uc_mgr);
+ if (err < 0)
+ pa_log("No UCM modifiers for verb %s", verb_name);
+
+ PA_LLIST_FOREACH(d, verb->devices) {
+ const char *dev_name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ /* Devices properties */
+ ucm_get_device_property(d, uc_mgr, verb, dev_name);
+ }
+ /* make conflicting or supported device mutual */
+ PA_LLIST_FOREACH(d, verb->devices)
+ append_lost_relationship(d);
+
+ PA_LLIST_FOREACH(mod, verb->modifiers) {
+ const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ /* Modifier properties */
+ ucm_get_modifier_property(mod, uc_mgr, mod_name);
+
+ /* Set PA_PROP_DEVICE_INTENDED_ROLES property to devices */
+ pa_log_debug("Set media roles for verb %s, modifier %s", verb_name, mod_name);
+ ucm_set_media_roles(mod, verb->devices, mod_name);
+ }
+
+ *p_verb = verb;
+ return 0;
+}
+
+static int pa_alsa_ucm_device_cmp(const void *a, const void *b) {
+ const pa_alsa_ucm_device *d1 = *(pa_alsa_ucm_device **)a;
+ const pa_alsa_ucm_device *d2 = *(pa_alsa_ucm_device **)b;
+
+ return strcmp(pa_proplist_gets(d1->proplist, PA_ALSA_PROP_UCM_NAME), pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_NAME));
+}
+
+static void set_eld_devices(pa_hashmap *hash)
+{
+ pa_device_port *port;
+ pa_alsa_ucm_port_data *data;
+ pa_alsa_ucm_device *dev;
+ const char *eld_mixer_device_name;
+ void *state;
+ int idx, eld_device;
+
+ PA_HASHMAP_FOREACH(port, hash, state) {
+ data = PA_DEVICE_PORT_DATA(port);
+ eld_mixer_device_name = NULL;
+ eld_device = -1;
+ PA_DYNARRAY_FOREACH(dev, data->devices, idx) {
+ if (dev->eld_device >= 0 && dev->eld_mixer_device_name) {
+ if (eld_device >= 0 && eld_device != dev->eld_device) {
+ pa_log_error("The ELD device is already set!");
+ } else if (eld_mixer_device_name && pa_streq(dev->eld_mixer_device_name, eld_mixer_device_name)) {
+ pa_log_error("The ELD mixer device is already set (%s, %s)!", dev->eld_mixer_device_name, dev->eld_mixer_device_name);
+ } else {
+ eld_mixer_device_name = dev->eld_mixer_device_name;
+ eld_device = dev->eld_device;
+ }
+ }
+ }
+ data->eld_device = eld_device;
+ if (data->eld_mixer_device_name)
+ pa_xfree(data->eld_mixer_device_name);
+ data->eld_mixer_device_name = pa_xstrdup(eld_mixer_device_name);
+ }
+}
+
+static void update_mixer_paths(pa_hashmap *ports, const char *profile) {
+ pa_device_port *port;
+ pa_alsa_ucm_port_data *data;
+ void *state;
+
+ /* select volume controls on ports */
+ PA_HASHMAP_FOREACH(port, ports, state) {
+ pa_log_info("Updating mixer path for %s: %s", profile, port->name);
+ data = PA_DEVICE_PORT_DATA(port);
+ data->path = pa_hashmap_get(data->paths, profile);
+ }
+}
+
+static void probe_volumes(pa_hashmap *hash, bool is_sink, snd_pcm_t *pcm_handle, pa_hashmap *mixers, bool ignore_dB) {
+ pa_device_port *port;
+ pa_alsa_path *path;
+ pa_alsa_ucm_port_data *data;
+ pa_alsa_ucm_device *dev;
+ snd_mixer_t *mixer_handle;
+ const char *profile, *mdev, *mdev2;
+ void *state, *state2;
+ int idx;
+
+ PA_HASHMAP_FOREACH(port, hash, state) {
+ data = PA_DEVICE_PORT_DATA(port);
+
+ mdev = NULL;
+ PA_DYNARRAY_FOREACH(dev, data->devices, idx) {
+ mdev2 = get_mixer_device(dev, is_sink);
+ if (mdev && mdev2 && !pa_streq(mdev, mdev2)) {
+ pa_log_error("Two mixer device names found ('%s', '%s'), using s/w volume", mdev, mdev2);
+ goto fail;
+ }
+ if (mdev2)
+ mdev = mdev2;
+ }
+
+ if (mdev == NULL || !(mixer_handle = pa_alsa_open_mixer_by_name(mixers, mdev, true))) {
+ pa_log_error("Failed to find a working mixer device (%s).", mdev);
+ goto fail;
+ }
+
+ PA_HASHMAP_FOREACH_KV(profile, path, data->paths, state2) {
+ if (pa_alsa_path_probe(path, NULL, mixer_handle, ignore_dB) < 0) {
+ pa_log_warn("Could not probe path: %s, using s/w volume", path->name);
+ pa_hashmap_remove(data->paths, profile);
+ } else if (!path->has_volume && !path->has_mute) {
+ pa_log_warn("Path %s is not a volume or mute control", path->name);
+ pa_hashmap_remove(data->paths, profile);
+ } else
+ pa_log_debug("Set up h/w %s using '%s' for %s:%s", path->has_volume ? "volume" : "mute",
+ path->name, profile, port->name);
+ }
+ }
+
+ return;
+
+fail:
+ /* We could not probe the paths we created. Free them and revert to software volumes. */
+ PA_HASHMAP_FOREACH(port, hash, state) {
+ data = PA_DEVICE_PORT_DATA(port);
+ pa_hashmap_remove_all(data->paths);
+ }
+}
+
+static void ucm_add_port_combination(
+ pa_hashmap *hash,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_alsa_ucm_device **pdevices,
+ int num,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core) {
+
+ pa_device_port *port;
+ int i;
+ unsigned priority;
+ double prio2;
+ char *name, *desc;
+ const char *dev_name;
+ const char *direction;
+ const char *profile;
+ pa_alsa_ucm_device *sorted[num], *dev;
+ pa_alsa_ucm_port_data *data;
+ pa_alsa_ucm_volume *vol;
+ pa_alsa_jack *jack, *jack2;
+ pa_device_port_type_t type, type2;
+ void *state;
+
+ for (i = 0; i < num; i++)
+ sorted[i] = pdevices[i];
+
+ /* Sort by alphabetical order so as to have a deterministic naming scheme
+ * for combination ports */
+ qsort(&sorted[0], num, sizeof(pa_alsa_ucm_device *), pa_alsa_ucm_device_cmp);
+
+ dev = sorted[0];
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ name = pa_sprintf_malloc("%s%s", is_sink ? PA_UCM_PRE_TAG_OUTPUT : PA_UCM_PRE_TAG_INPUT, dev_name);
+ desc = num == 1 ? pa_xstrdup(pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_DESCRIPTION))
+ : pa_sprintf_malloc("Combination port for %s", dev_name);
+
+ priority = is_sink ? dev->playback_priority : dev->capture_priority;
+ prio2 = (priority == 0 ? 0 : 1.0/priority);
+ jack = ucm_get_jack(context->ucm, dev);
+ type = dev->type;
+
+ for (i = 1; i < num; i++) {
+ char *tmp;
+
+ dev = sorted[i];
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ tmp = pa_sprintf_malloc("%s+%s", name, dev_name);
+ pa_xfree(name);
+ name = tmp;
+
+ tmp = pa_sprintf_malloc("%s,%s", desc, dev_name);
+ pa_xfree(desc);
+ desc = tmp;
+
+ priority = is_sink ? dev->playback_priority : dev->capture_priority;
+ if (priority != 0 && prio2 > 0)
+ prio2 += 1.0/priority;
+
+ jack2 = ucm_get_jack(context->ucm, dev);
+ if (jack2) {
+ if (jack && jack != jack2)
+ pa_log_warn("Multiple jacks per combined device '%s': '%s' '%s'", name, jack->name, jack2->name);
+ jack = jack2;
+ }
+
+ type2 = dev->type;
+ if (type2 != PA_DEVICE_PORT_TYPE_UNKNOWN) {
+ if (type != PA_DEVICE_PORT_TYPE_UNKNOWN && type != type2)
+ pa_log_warn("Multiple device types per combined device '%s': %d %d", name, type, type2);
+ type = type2;
+ }
+ }
+
+ /* Make combination ports always have lower priority, and use the formula
+ 1/p = 1/p1 + 1/p2 + ... 1/pn.
+ This way, the result will always be less than the individual components,
+ yet higher components will lead to higher result. */
+
+ if (num > 1)
+ priority = prio2 > 0 ? 1.0/prio2 : 0;
+
+ port = pa_hashmap_get(ports, name);
+ if (!port) {
+ pa_device_port_new_data port_data;
+
+ pa_device_port_new_data_init(&port_data);
+ pa_device_port_new_data_set_name(&port_data, name);
+ pa_device_port_new_data_set_description(&port_data, desc);
+ pa_device_port_new_data_set_type(&port_data, type);
+ pa_device_port_new_data_set_direction(&port_data, is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT);
+ if (jack)
+ pa_device_port_new_data_set_availability_group(&port_data, jack->name);
+
+ port = pa_device_port_new(core, &port_data, sizeof(pa_alsa_ucm_port_data));
+ pa_device_port_new_data_done(&port_data);
+
+ data = PA_DEVICE_PORT_DATA(port);
+ ucm_port_data_init(data, context->ucm, port, pdevices, num);
+ port->impl_free = ucm_port_data_free;
+
+ pa_hashmap_put(ports, port->name, port);
+ pa_log_debug("Add port %s: %s", port->name, port->description);
+
+ if (num == 1) {
+ /* To keep things simple and not worry about stacking controls, we only support hardware volumes on non-combination
+ * ports. */
+ PA_HASHMAP_FOREACH_KV(profile, 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;
+ }
+
+ pa_hashmap_put(data->paths, pa_xstrdup(profile), path);
+
+ /* Add path also to already created empty path set */
+ dev = sorted[0];
+ 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);
+ }
+ }
+ }
+ }
+
+ port->priority = priority;
+
+ pa_xfree(name);
+ pa_xfree(desc);
+
+ direction = is_sink ? "output" : "input";
+ pa_log_debug("Port %s direction %s, priority %d", port->name, direction, priority);
+
+ if (cp) {
+ pa_log_debug("Adding profile %s to port %s.", cp->name, port->name);
+ pa_hashmap_put(port->profiles, cp->name, cp);
+ }
+
+ if (hash) {
+ pa_hashmap_put(hash, port->name, port);
+ }
+}
+
+static int ucm_port_contains(const char *port_name, const char *dev_name, bool is_sink) {
+ int ret = 0;
+ const char *r;
+ const char *state = NULL;
+ size_t len;
+
+ if (!port_name || !dev_name)
+ return false;
+
+ port_name += is_sink ? strlen(PA_UCM_PRE_TAG_OUTPUT) : strlen(PA_UCM_PRE_TAG_INPUT);
+
+ while ((r = pa_split_in_place(port_name, "+", &len, &state))) {
+ if (strlen(dev_name) == len && !strncmp(r, dev_name, len)) {
+ ret = 1;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static int ucm_check_conformance(
+ pa_alsa_ucm_mapping_context *context,
+ pa_alsa_ucm_device **pdevices,
+ int dev_num,
+ pa_alsa_ucm_device *dev) {
+
+ uint32_t idx;
+ pa_alsa_ucm_device *d;
+ int i;
+
+ pa_assert(dev);
+
+ pa_log_debug("Check device %s conformance with %d other devices",
+ pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME), dev_num);
+ if (dev_num == 0) {
+ pa_log_debug("First device in combination, number 1");
+ return 1;
+ }
+
+ if (dev->conflicting_devices) { /* the device defines conflicting devices */
+ PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) {
+ for (i = 0; i < dev_num; i++) {
+ if (pdevices[i] == d) {
+ pa_log_debug("Conflicting device found");
+ return 0;
+ }
+ }
+ }
+ } else if (dev->supported_devices) { /* the device defines supported devices */
+ for (i = 0; i < dev_num; i++) {
+ if (!ucm_device_exists(dev->supported_devices, pdevices[i])) {
+ pa_log_debug("Supported device not found");
+ return 0;
+ }
+ }
+ } else { /* not support any other devices */
+ pa_log_debug("Not support any other devices");
+ return 0;
+ }
+
+ pa_log_debug("Device added to combination, number %d", dev_num + 1);
+ return 1;
+}
+
+static inline pa_alsa_ucm_device *get_next_device(pa_idxset *idxset, uint32_t *idx) {
+ pa_alsa_ucm_device *dev;
+
+ if (*idx == PA_IDXSET_INVALID)
+ dev = pa_idxset_first(idxset, idx);
+ else
+ dev = pa_idxset_next(idxset, idx);
+
+ return dev;
+}
+
+static void ucm_add_ports_combination(
+ pa_hashmap *hash,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_alsa_ucm_device **pdevices,
+ int dev_num,
+ uint32_t map_index,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core) {
+
+ pa_alsa_ucm_device *dev;
+ uint32_t idx = map_index;
+
+ if ((dev = get_next_device(context->ucm_devices, &idx)) == NULL)
+ return;
+
+ /* check if device at map_index can combine with existing devices combination */
+ if (ucm_check_conformance(context, pdevices, dev_num, dev)) {
+ /* add device at map_index to devices combination */
+ pdevices[dev_num] = dev;
+ /* add current devices combination as a new port */
+ ucm_add_port_combination(hash, context, is_sink, pdevices, dev_num + 1, ports, cp, core);
+ /* try more elements combination */
+ ucm_add_ports_combination(hash, context, is_sink, pdevices, dev_num + 1, idx, ports, cp, core);
+ }
+
+ /* try other device with current elements number */
+ ucm_add_ports_combination(hash, context, is_sink, pdevices, dev_num, idx, ports, cp, core);
+}
+
+static char* merge_roles(const char *cur, const char *add) {
+ char *r, *ret;
+ const char *state = NULL;
+
+ if (add == NULL)
+ return pa_xstrdup(cur);
+ else if (cur == NULL)
+ return pa_xstrdup(add);
+
+ ret = pa_xstrdup(cur);
+
+ while ((r = pa_split_spaces(add, &state))) {
+ char *value;
+
+ if (!pa_str_in_list_spaces(ret, r))
+ value = pa_sprintf_malloc("%s %s", ret, r);
+ else {
+ pa_xfree(r);
+ continue;
+ }
+
+ pa_xfree(ret);
+ ret = value;
+ pa_xfree(r);
+ }
+
+ return ret;
+}
+
+void pa_alsa_ucm_add_ports_combination(
+ pa_hashmap *p,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core) {
+
+ pa_alsa_ucm_device **pdevices;
+
+ pa_assert(context->ucm_devices);
+
+ if (pa_idxset_size(context->ucm_devices) > 0) {
+ pdevices = pa_xnew(pa_alsa_ucm_device *, pa_idxset_size(context->ucm_devices));
+ ucm_add_ports_combination(p, context, is_sink, pdevices, 0, PA_IDXSET_INVALID, ports, cp, core);
+ pa_xfree(pdevices);
+ }
+
+ /* ELD devices */
+ set_eld_devices(ports);
+}
+
+void pa_alsa_ucm_add_ports(
+ pa_hashmap **p,
+ pa_proplist *proplist,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_card *card,
+ snd_pcm_t *pcm_handle,
+ bool ignore_dB) {
+
+ uint32_t idx;
+ char *merged_roles;
+ const char *role_name = is_sink ? PA_ALSA_PROP_UCM_PLAYBACK_ROLES : PA_ALSA_PROP_UCM_CAPTURE_ROLES;
+ pa_alsa_ucm_device *dev;
+ pa_alsa_ucm_modifier *mod;
+ char *tmp;
+
+ pa_assert(p);
+ pa_assert(*p);
+
+ /* add ports first */
+ pa_alsa_ucm_add_ports_combination(*p, context, is_sink, card->ports, NULL, card->core);
+
+ /* now set up volume paths if any */
+ probe_volumes(*p, is_sink, pcm_handle, context->ucm->mixers, ignore_dB);
+
+ /* probe_volumes() removes per-profile paths from ports if probing them
+ * fails. The path for the current profile is cached in
+ * pa_alsa_ucm_port_data.path, which is not cleared by probe_volumes() if
+ * the path gets removed, so we have to call update_mixer_paths() here to
+ * unset the cached path if needed. */
+ if (card->card.active_profile_index < card->card.n_profiles)
+ update_mixer_paths(*p, card->card.profiles[card->card.active_profile_index]->name);
+
+ /* then set property PA_PROP_DEVICE_INTENDED_ROLES */
+ merged_roles = pa_xstrdup(pa_proplist_gets(proplist, PA_PROP_DEVICE_INTENDED_ROLES));
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ const char *roles = pa_proplist_gets(dev->proplist, role_name);
+ tmp = merge_roles(merged_roles, roles);
+ pa_xfree(merged_roles);
+ merged_roles = tmp;
+ }
+
+ if (context->ucm_modifiers)
+ PA_IDXSET_FOREACH(mod, context->ucm_modifiers, idx) {
+ tmp = merge_roles(merged_roles, mod->media_role);
+ pa_xfree(merged_roles);
+ merged_roles = tmp;
+ }
+
+ if (merged_roles)
+ pa_proplist_sets(proplist, PA_PROP_DEVICE_INTENDED_ROLES, merged_roles);
+
+ pa_log_info("ALSA device %s roles: %s", pa_proplist_gets(proplist, PA_PROP_DEVICE_STRING), pa_strnull(merged_roles));
+ pa_xfree(merged_roles);
+}
+
+/* Change UCM verb and device to match selected card profile */
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) {
+ int ret = 0;
+ const char *profile;
+ pa_alsa_ucm_verb *verb;
+
+ if (new_profile == old_profile)
+ return ret;
+ else if (new_profile == NULL || old_profile == NULL)
+ profile = new_profile ? new_profile : SND_USE_CASE_VERB_INACTIVE;
+ else if (!pa_streq(new_profile, old_profile))
+ profile = new_profile;
+ else
+ return ret;
+
+ /* change verb */
+ pa_log_info("Set UCM verb to %s", profile);
+ if ((ret = snd_use_case_set(ucm->ucm_mgr, "_verb", profile)) < 0) {
+ pa_log("Failed to set verb %s: %s", profile, snd_strerror(ret));
+ }
+
+ /* find active verb */
+ ucm->active_verb = NULL;
+ PA_LLIST_FOREACH(verb, ucm->verbs) {
+ const char *verb_name;
+ verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME);
+ if (pa_streq(verb_name, profile)) {
+ ucm->active_verb = verb;
+ break;
+ }
+ }
+
+ update_mixer_paths(card->ports, profile);
+ return ret;
+}
+
+int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink) {
+ int i;
+ int ret = 0;
+ pa_alsa_ucm_config *ucm;
+ const char **enable_devs;
+ int enable_num = 0;
+ uint32_t idx;
+ pa_alsa_ucm_device *dev;
+
+ pa_assert(context && context->ucm);
+
+ ucm = context->ucm;
+ pa_assert(ucm->ucm_mgr);
+
+ enable_devs = pa_xnew(const char *, pa_idxset_size(context->ucm_devices));
+
+ /* first disable then enable */
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ if (ucm_port_contains(port->name, dev_name, is_sink))
+ enable_devs[enable_num++] = dev_name;
+ else {
+ pa_log_debug("Disable ucm device %s", dev_name);
+ if (snd_use_case_set(ucm->ucm_mgr, "_disdev", dev_name) > 0) {
+ pa_log("Failed to disable ucm device %s", dev_name);
+ ret = -1;
+ break;
+ }
+ }
+ }
+
+ for (i = 0; i < enable_num; i++) {
+ pa_log_debug("Enable ucm device %s", enable_devs[i]);
+ if (snd_use_case_set(ucm->ucm_mgr, "_enadev", enable_devs[i]) < 0) {
+ pa_log("Failed to enable ucm device %s", enable_devs[i]);
+ ret = -1;
+ break;
+ }
+ }
+
+ pa_xfree(enable_devs);
+
+ return ret;
+}
+
+static void ucm_add_mapping(pa_alsa_profile *p, pa_alsa_mapping *m) {
+
+ pa_alsa_path_set *ps;
+
+ /* create empty path set for the future path additions */
+ ps = pa_xnew0(pa_alsa_path_set, 1);
+ ps->direction = m->direction;
+ ps->paths = pa_hashmap_new_full(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func, pa_xfree,
+ (pa_free_cb_t) pa_alsa_path_free);
+
+ switch (m->direction) {
+ case PA_ALSA_DIRECTION_ANY:
+ pa_idxset_put(p->output_mappings, m, NULL);
+ pa_idxset_put(p->input_mappings, m, NULL);
+ m->output_path_set = ps;
+ m->input_path_set = ps;
+ break;
+ case PA_ALSA_DIRECTION_OUTPUT:
+ pa_idxset_put(p->output_mappings, m, NULL);
+ m->output_path_set = ps;
+ break;
+ case PA_ALSA_DIRECTION_INPUT:
+ pa_idxset_put(p->input_mappings, m, NULL);
+ m->input_path_set = ps;
+ break;
+ }
+}
+
+static void alsa_mapping_add_ucm_device(pa_alsa_mapping *m, pa_alsa_ucm_device *device) {
+ char *cur_desc;
+ const char *new_desc, *mdev;
+ bool is_sink = m->direction == PA_ALSA_DIRECTION_OUTPUT;
+
+ pa_idxset_put(m->ucm_context.ucm_devices, device, NULL);
+
+ new_desc = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_DESCRIPTION);
+ cur_desc = m->description;
+ if (cur_desc)
+ m->description = pa_sprintf_malloc("%s + %s", cur_desc, new_desc);
+ else
+ m->description = pa_xstrdup(new_desc);
+ pa_xfree(cur_desc);
+
+ /* walk around null case */
+ m->description = m->description ? m->description : pa_xstrdup("");
+
+ /* save mapping to ucm device */
+ if (is_sink)
+ device->playback_mapping = m;
+ else
+ device->capture_mapping = m;
+
+ mdev = get_mixer_device(device, is_sink);
+ if (mdev)
+ pa_proplist_sets(m->proplist, "alsa.mixer_device", mdev);
+}
+
+static void alsa_mapping_add_ucm_modifier(pa_alsa_mapping *m, pa_alsa_ucm_modifier *modifier) {
+ char *cur_desc;
+ const char *new_desc, *mod_name, *channel_str;
+ uint32_t channels = 0;
+
+ pa_idxset_put(m->ucm_context.ucm_modifiers, modifier, NULL);
+
+ new_desc = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_DESCRIPTION);
+ cur_desc = m->description;
+ if (cur_desc)
+ m->description = pa_sprintf_malloc("%s + %s", cur_desc, new_desc);
+ else
+ m->description = pa_xstrdup(new_desc);
+ pa_xfree(cur_desc);
+
+ m->description = m->description ? m->description : pa_xstrdup("");
+
+ /* Modifier sinks should not be routed to by default */
+ m->priority = 0;
+
+ mod_name = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_NAME);
+ pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_MODIFIER, mod_name);
+
+ /* save mapping to ucm modifier */
+ if (m->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ modifier->playback_mapping = m;
+ channel_str = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS);
+ } else {
+ modifier->capture_mapping = m;
+ channel_str = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_CAPTURE_CHANNELS);
+ }
+
+ if (channel_str) {
+ /* FIXME: channel_str is unsanitized input from the UCM configuration,
+ * we should do proper error handling instead of asserting.
+ * https://bugs.freedesktop.org/show_bug.cgi?id=71823 */
+ pa_assert_se(pa_atou(channel_str, &channels) == 0 && pa_channels_valid(channels));
+ pa_log_debug("Got channel count %" PRIu32 " for modifier", channels);
+ }
+
+ if (channels)
+ pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA);
+ else
+ pa_channel_map_init(&m->channel_map);
+}
+
+static pa_alsa_mapping* ucm_alsa_mapping_get(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *ps, const char *verb_name, const char *device_str, bool is_sink) {
+ pa_alsa_mapping *m;
+ char *mapping_name;
+ size_t ucm_alibpref_len = 0;
+
+ /* find private alsa-lib's configuration device prefix */
+
+ if (ucm->alib_prefix && pa_startswith(device_str, ucm->alib_prefix))
+ ucm_alibpref_len = strlen(ucm->alib_prefix);
+
+ mapping_name = pa_sprintf_malloc("Mapping %s: %s: %s", verb_name, device_str + ucm_alibpref_len, is_sink ? "sink" : "source");
+
+ m = pa_alsa_mapping_get(ps, mapping_name);
+
+ if (!m)
+ pa_log("No mapping for %s", mapping_name);
+
+ pa_xfree(mapping_name);
+
+ return m;
+}
+
+static int ucm_create_mapping_direction(
+ pa_alsa_ucm_config *ucm,
+ pa_alsa_profile_set *ps,
+ pa_alsa_profile *p,
+ pa_alsa_ucm_device *device,
+ const char *verb_name,
+ const char *device_name,
+ const char *device_str,
+ bool is_sink) {
+
+ pa_alsa_mapping *m;
+ unsigned priority, rate, channels;
+
+ m = ucm_alsa_mapping_get(ucm, ps, verb_name, device_str, is_sink);
+
+ if (!m)
+ return -1;
+
+ pa_log_debug("UCM mapping: %s dev %s", m->name, device_name);
+
+ priority = is_sink ? device->playback_priority : device->capture_priority;
+ rate = is_sink ? device->playback_rate : device->capture_rate;
+ channels = is_sink ? device->playback_channels : device->capture_channels;
+
+ if (!m->ucm_context.ucm_devices) { /* new mapping */
+ m->ucm_context.ucm_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ m->ucm_context.ucm = ucm;
+ m->ucm_context.direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT;
+
+ m->device_strings = pa_xnew0(char*, 2);
+ m->device_strings[0] = pa_xstrdup(device_str);
+ m->direction = is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT;
+
+ ucm_add_mapping(p, m);
+ if (rate)
+ m->sample_spec.rate = rate;
+ pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA);
+ }
+
+ /* mapping priority is the highest one of ucm devices */
+ if (priority > m->priority)
+ m->priority = priority;
+
+ /* mapping channels is the lowest one of ucm devices */
+ if (channels < m->channel_map.channels)
+ pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA);
+
+ alsa_mapping_add_ucm_device(m, device);
+
+ return 0;
+}
+
+static int ucm_create_mapping_for_modifier(
+ pa_alsa_ucm_config *ucm,
+ pa_alsa_profile_set *ps,
+ pa_alsa_profile *p,
+ pa_alsa_ucm_modifier *modifier,
+ const char *verb_name,
+ const char *mod_name,
+ const char *device_str,
+ bool is_sink) {
+
+ pa_alsa_mapping *m;
+
+ m = ucm_alsa_mapping_get(ucm, ps, verb_name, device_str, is_sink);
+
+ if (!m)
+ return -1;
+
+ pa_log_info("UCM mapping: %s modifier %s", m->name, mod_name);
+
+ if (!m->ucm_context.ucm_devices && !m->ucm_context.ucm_modifiers) { /* new mapping */
+ m->ucm_context.ucm_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ m->ucm_context.ucm_modifiers = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ m->ucm_context.ucm = ucm;
+ m->ucm_context.direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT;
+
+ m->device_strings = pa_xnew0(char*, 2);
+ m->device_strings[0] = pa_xstrdup(device_str);
+ m->direction = is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT;
+ /* Modifier sinks should not be routed to by default */
+ m->priority = 0;
+
+ ucm_add_mapping(p, m);
+ } else if (!m->ucm_context.ucm_modifiers) /* share pcm with device */
+ m->ucm_context.ucm_modifiers = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ alsa_mapping_add_ucm_modifier(m, modifier);
+
+ return 0;
+}
+
+static int ucm_create_mapping(
+ pa_alsa_ucm_config *ucm,
+ pa_alsa_profile_set *ps,
+ pa_alsa_profile *p,
+ pa_alsa_ucm_device *device,
+ const char *verb_name,
+ const char *device_name,
+ const char *sink,
+ const char *source) {
+
+ int ret = 0;
+
+ if (!sink && !source) {
+ pa_log("No sink and source at %s: %s", verb_name, device_name);
+ return -1;
+ }
+
+ if (sink)
+ ret = ucm_create_mapping_direction(ucm, ps, p, device, verb_name, device_name, sink, true);
+ if (ret == 0 && source)
+ ret = ucm_create_mapping_direction(ucm, ps, p, device, verb_name, device_name, source, false);
+
+ return ret;
+}
+
+static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device) {
+ pa_alsa_jack *j;
+ const char *device_name;
+ const char *jack_control;
+ const char *mixer_device_name;
+ char *name;
+
+ pa_assert(ucm);
+ pa_assert(device);
+
+ device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ jack_control = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_JACK_CONTROL);
+ if (jack_control) {
+#if SND_LIB_VERSION >= 0x10201
+ snd_ctl_elem_id_t *ctl;
+ int err, index;
+ snd_ctl_elem_id_alloca(&ctl);
+ err = snd_use_case_parse_ctl_elem_id(ctl, "JackControl", jack_control);
+ if (err < 0)
+ return NULL;
+ jack_control = snd_ctl_elem_id_get_name(ctl);
+ index = snd_ctl_elem_id_get_index(ctl);
+ if (index > 0) {
+ pa_log("[%s] Invalid JackControl index value: \"%s\",%d", device_name, jack_control, index);
+ return NULL;
+ }
+#else
+#warning "Upgrade to alsa-lib 1.2.1!"
+#endif
+ if (!pa_endswith(jack_control, " Jack")) {
+ pa_log("[%s] Invalid JackControl value: \"%s\"", device_name, jack_control);
+ return NULL;
+ }
+
+ /* pa_alsa_jack_new() expects a jack name without " Jack" at the
+ * end, so drop the trailing " Jack". */
+ name = pa_xstrndup(jack_control, strlen(jack_control) - 5);
+ } else {
+ /* The jack control hasn't been explicitly configured, fail. */
+ return NULL;
+ }
+
+ PA_LLIST_FOREACH(j, ucm->jacks)
+ if (pa_streq(j->name, name))
+ goto finish;
+
+ mixer_device_name = get_jack_mixer_device(device, true);
+ if (!mixer_device_name)
+ mixer_device_name = get_jack_mixer_device(device, false);
+ if (!mixer_device_name) {
+ pa_log("[%s] No mixer device name for JackControl \"%s\"", device_name, jack_control);
+ j = NULL;
+ goto finish;
+ }
+ j = pa_alsa_jack_new(NULL, mixer_device_name, name, 0);
+ PA_LLIST_PREPEND(pa_alsa_jack, ucm->jacks, j);
+
+finish:
+ pa_xfree(name);
+
+ return j;
+}
+
+static int ucm_create_profile(
+ pa_alsa_ucm_config *ucm,
+ pa_alsa_profile_set *ps,
+ pa_alsa_ucm_verb *verb,
+ const char *verb_name,
+ const char *verb_desc) {
+
+ pa_alsa_profile *p;
+ pa_alsa_ucm_device *dev;
+ pa_alsa_ucm_modifier *mod;
+ int i = 0;
+ const char *name, *sink, *source;
+ unsigned int priority;
+
+ pa_assert(ps);
+
+ if (pa_hashmap_get(ps->profiles, verb_name)) {
+ pa_log("Verb %s already exists", verb_name);
+ return -1;
+ }
+
+ p = pa_xnew0(pa_alsa_profile, 1);
+ p->profile_set = ps;
+ p->name = pa_xstrdup(verb_name);
+ p->description = pa_xstrdup(verb_desc);
+
+ p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ p->supported = true;
+ pa_hashmap_put(ps->profiles, p->name, p);
+
+ /* TODO: get profile priority from policy management */
+ priority = verb->priority;
+
+ if (priority == 0) {
+ char *verb_cmp, *c;
+ c = verb_cmp = pa_xstrdup(verb_name);
+ while (*c) {
+ if (*c == '_') *c = ' ';
+ c++;
+ }
+ for (i = 0; verb_info[i].id; i++) {
+ if (strcasecmp(verb_info[i].id, verb_cmp) == 0) {
+ priority = verb_info[i].priority;
+ break;
+ }
+ }
+ pa_xfree(verb_cmp);
+ }
+
+ p->priority = priority;
+
+ PA_LLIST_FOREACH(dev, verb->devices) {
+ pa_alsa_jack *jack;
+ const char *jack_hw_mute;
+
+ name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK);
+ source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE);
+
+ ucm_create_mapping(ucm, ps, p, dev, verb_name, name, sink, source);
+
+ jack = ucm_get_jack(ucm, dev);
+ if (jack)
+ device_set_jack(dev, jack);
+
+ /* JackHWMute contains a list of device names. Each listed device must
+ * be associated with the jack object that we just created. */
+ jack_hw_mute = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_JACK_HW_MUTE);
+ if (jack_hw_mute && !jack) {
+ pa_log("[%s] JackHWMute set, but JackControl is missing", name);
+ jack_hw_mute = NULL;
+ }
+ if (jack_hw_mute) {
+ char *hw_mute_device_name;
+ const char *state = NULL;
+
+ while ((hw_mute_device_name = ucm_split_devnames(jack_hw_mute, &state))) {
+ pa_alsa_ucm_verb *verb2;
+ bool device_found = false;
+
+ /* Search the referenced device from all verbs. If there are
+ * multiple verbs that have a device with this name, we add the
+ * hw mute association to each of those devices. */
+ PA_LLIST_FOREACH(verb2, ucm->verbs) {
+ pa_alsa_ucm_device *hw_mute_device;
+
+ hw_mute_device = verb_find_device(verb2, hw_mute_device_name);
+ if (hw_mute_device) {
+ device_found = true;
+ device_add_hw_mute_jack(hw_mute_device, jack);
+ }
+ }
+
+ if (!device_found)
+ pa_log("[%s] JackHWMute references an unknown device: %s", name, hw_mute_device_name);
+
+ pa_xfree(hw_mute_device_name);
+ }
+ }
+ }
+
+ /* Now find modifiers that have their own PlaybackPCM and create
+ * separate sinks for them. */
+ PA_LLIST_FOREACH(mod, verb->modifiers) {
+ name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ sink = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_SINK);
+ source = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_SOURCE);
+
+ if (sink)
+ ucm_create_mapping_for_modifier(ucm, ps, p, mod, verb_name, name, sink, true);
+ else if (source)
+ ucm_create_mapping_for_modifier(ucm, ps, p, mod, verb_name, name, source, false);
+ }
+
+ pa_alsa_profile_dump(p);
+
+ return 0;
+}
+
+static void mapping_init_eld(pa_alsa_mapping *m, snd_pcm_t *pcm)
+{
+ pa_alsa_ucm_mapping_context *context = &m->ucm_context;
+ pa_alsa_ucm_device *dev;
+ uint32_t idx;
+ char *mdev, *alib_prefix;
+ snd_pcm_info_t *info;
+ int pcm_card, pcm_device;
+
+ snd_pcm_info_alloca(&info);
+ if (snd_pcm_info(pcm, info) < 0)
+ return;
+
+ if ((pcm_card = snd_pcm_info_get_card(info)) < 0)
+ return;
+ if ((pcm_device = snd_pcm_info_get_device(info)) < 0)
+ return;
+
+ alib_prefix = context->ucm->alib_prefix;
+
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ mdev = pa_sprintf_malloc("%shw:%i", alib_prefix ? alib_prefix : "", pcm_card);
+ if (mdev == NULL)
+ continue;
+ dev->eld_mixer_device_name = mdev;
+ dev->eld_device = pcm_device;
+ }
+}
+
+static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, int mode) {
+ snd_pcm_t* pcm;
+ pa_sample_spec try_ss = ucm->default_sample_spec;
+ pa_channel_map try_map;
+ 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);
+
+ try_period_size =
+ pa_usec_to_bytes(ucm->default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) /
+ pa_frame_size(&try_ss);
+ try_buffer_size = ucm->default_n_fragments * try_period_size;
+
+ pcm = pa_alsa_open_by_device_string(m->device_strings[0], NULL, &try_ss,
+ &try_map, mode, &try_period_size, &try_buffer_size, 0, NULL, NULL, exact_channels);
+
+ if (pcm) {
+ if (!exact_channels)
+ m->channel_map = try_map;
+ mapping_init_eld(m, pcm);
+ }
+
+ return pcm;
+}
+
+static void profile_finalize_probing(pa_alsa_profile *p) {
+ pa_alsa_mapping *m;
+ uint32_t idx;
+
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+ if (p->supported)
+ m->supported++;
+
+ if (!m->output_pcm)
+ continue;
+
+ pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
+ pa_alsa_close(&m->output_pcm);
+ }
+
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+ if (p->supported)
+ m->supported++;
+
+ if (!m->input_pcm)
+ continue;
+
+ pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
+ pa_alsa_close(&m->input_pcm);
+ }
+}
+
+static void ucm_mapping_jack_probe(pa_alsa_mapping *m, pa_hashmap *mixers) {
+ snd_mixer_t *mixer_handle;
+ pa_alsa_ucm_mapping_context *context = &m->ucm_context;
+ pa_alsa_ucm_device *dev;
+ uint32_t idx;
+
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ bool has_control;
+
+ if (!dev->jack || !dev->jack->mixer_device_name)
+ continue;
+
+ mixer_handle = pa_alsa_open_mixer_by_name(mixers, dev->jack->mixer_device_name, true);
+ if (!mixer_handle) {
+ pa_log_error("Unable to determine open mixer device '%s' for jack %s", dev->jack->mixer_device_name, dev->jack->name);
+ continue;
+ }
+
+ has_control = pa_alsa_mixer_find_card(mixer_handle, &dev->jack->alsa_id, 0) != NULL;
+ pa_alsa_jack_set_has_control(dev->jack, has_control);
+ pa_log_info("UCM jack %s has_control=%d", dev->jack->name, dev->jack->has_control);
+ }
+}
+
+static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *ps) {
+ void *state;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ uint32_t idx;
+
+ PA_HASHMAP_FOREACH(p, ps->profiles, state) {
+ /* change verb */
+ pa_log_info("Set ucm verb to %s", p->name);
+
+ if ((snd_use_case_set(ucm->ucm_mgr, "_verb", p->name)) < 0) {
+ pa_log("Failed to set verb %s", p->name);
+ p->supported = false;
+ continue;
+ }
+
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+ if (PA_UCM_IS_MODIFIER_MAPPING(m)) {
+ /* Skip jack probing on modifier PCMs since we expect this to
+ * only be controlled on the main device/verb PCM. */
+ continue;
+ }
+
+ m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK);
+ if (!m->output_pcm) {
+ p->supported = false;
+ break;
+ }
+ }
+
+ if (p->supported) {
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+ if (PA_UCM_IS_MODIFIER_MAPPING(m)) {
+ /* Skip jack probing on modifier PCMs since we expect this to
+ * only be controlled on the main device/verb PCM. */
+ continue;
+ }
+
+ m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE);
+ if (!m->input_pcm) {
+ p->supported = false;
+ break;
+ }
+ }
+ }
+
+ if (!p->supported) {
+ profile_finalize_probing(p);
+ continue;
+ }
+
+ pa_log_debug("Profile %s supported.", p->name);
+
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx)
+ if (!PA_UCM_IS_MODIFIER_MAPPING(m))
+ ucm_mapping_jack_probe(m, ucm->mixers);
+
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx)
+ if (!PA_UCM_IS_MODIFIER_MAPPING(m))
+ ucm_mapping_jack_probe(m, ucm->mixers);
+
+ profile_finalize_probing(p);
+ }
+
+ /* restore ucm state */
+ snd_use_case_set(ucm->ucm_mgr, "_verb", SND_USE_CASE_VERB_INACTIVE);
+
+ pa_alsa_profile_set_drop_unsupported(ps);
+}
+
+pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map) {
+ pa_alsa_ucm_verb *verb;
+ pa_alsa_profile_set *ps;
+
+ ps = pa_xnew0(pa_alsa_profile_set, 1);
+ ps->mappings = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) pa_alsa_mapping_free);
+ ps->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) pa_alsa_profile_free);
+ ps->decibel_fixes = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+
+ /* create a profile for each verb */
+ PA_LLIST_FOREACH(verb, ucm->verbs) {
+ const char *verb_name;
+ const char *verb_desc;
+
+ verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME);
+ verb_desc = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_DESCRIPTION);
+ if (verb_name == NULL) {
+ pa_log("Verb with no name");
+ continue;
+ }
+
+ ucm_create_profile(ucm, ps, verb, verb_name, verb_desc);
+ }
+
+ ucm_probe_profile_set(ucm, ps);
+ ps->probed = true;
+
+ return ps;
+}
+
+static void free_verb(pa_alsa_ucm_verb *verb) {
+ pa_alsa_ucm_device *di, *dn;
+ pa_alsa_ucm_modifier *mi, *mn;
+
+ PA_LLIST_FOREACH_SAFE(di, dn, verb->devices) {
+ PA_LLIST_REMOVE(pa_alsa_ucm_device, verb->devices, di);
+
+ if (di->hw_mute_jacks)
+ pa_dynarray_free(di->hw_mute_jacks);
+
+ if (di->ucm_ports)
+ pa_dynarray_free(di->ucm_ports);
+
+ if (di->playback_volumes)
+ pa_hashmap_free(di->playback_volumes);
+ if (di->capture_volumes)
+ pa_hashmap_free(di->capture_volumes);
+
+ pa_proplist_free(di->proplist);
+
+ if (di->conflicting_devices)
+ pa_idxset_free(di->conflicting_devices, NULL);
+ if (di->supported_devices)
+ pa_idxset_free(di->supported_devices, NULL);
+
+ pa_xfree(di->eld_mixer_device_name);
+
+ pa_xfree(di);
+ }
+
+ PA_LLIST_FOREACH_SAFE(mi, mn, verb->modifiers) {
+ PA_LLIST_REMOVE(pa_alsa_ucm_modifier, verb->modifiers, mi);
+ pa_proplist_free(mi->proplist);
+ if (mi->n_suppdev > 0)
+ snd_use_case_free_list(mi->supported_devices, mi->n_suppdev);
+ if (mi->n_confdev > 0)
+ snd_use_case_free_list(mi->conflicting_devices, mi->n_confdev);
+ pa_xfree(mi->media_role);
+ pa_xfree(mi);
+ }
+ pa_proplist_free(verb->proplist);
+ pa_xfree(verb);
+}
+
+static pa_alsa_ucm_device *verb_find_device(pa_alsa_ucm_verb *verb, const char *device_name) {
+ pa_alsa_ucm_device *device;
+
+ pa_assert(verb);
+ pa_assert(device_name);
+
+ PA_LLIST_FOREACH(device, verb->devices) {
+ const char *name;
+
+ name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME);
+ if (pa_streq(name, device_name))
+ return device;
+ }
+
+ return NULL;
+}
+
+void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) {
+ pa_alsa_ucm_verb *vi, *vn;
+ pa_alsa_jack *ji, *jn;
+
+ PA_LLIST_FOREACH_SAFE(vi, vn, ucm->verbs) {
+ PA_LLIST_REMOVE(pa_alsa_ucm_verb, ucm->verbs, vi);
+ free_verb(vi);
+ }
+ PA_LLIST_FOREACH_SAFE(ji, jn, ucm->jacks) {
+ PA_LLIST_REMOVE(pa_alsa_jack, ucm->jacks, ji);
+ pa_alsa_jack_free(ji);
+ }
+ if (ucm->ucm_mgr) {
+ snd_use_case_mgr_close(ucm->ucm_mgr);
+ ucm->ucm_mgr = NULL;
+ }
+ pa_xfree(ucm->alib_prefix);
+ ucm->alib_prefix = NULL;
+}
+
+void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) {
+ pa_alsa_ucm_device *dev;
+ pa_alsa_ucm_modifier *mod;
+ uint32_t idx;
+
+ if (context->ucm_devices) {
+ /* clear ucm device pointer to mapping */
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ if (context->direction == PA_DIRECTION_OUTPUT)
+ dev->playback_mapping = NULL;
+ else
+ dev->capture_mapping = NULL;
+ }
+
+ pa_idxset_free(context->ucm_devices, NULL);
+ }
+
+ if (context->ucm_modifiers) {
+ PA_IDXSET_FOREACH(mod, context->ucm_modifiers, idx) {
+ if (context->direction == PA_DIRECTION_OUTPUT)
+ mod->playback_mapping = NULL;
+ else
+ mod->capture_mapping = NULL;
+ }
+
+ pa_idxset_free(context->ucm_modifiers, NULL);
+ }
+}
+
+/* Enable the modifier when the first stream with matched role starts */
+void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) {
+ pa_alsa_ucm_modifier *mod;
+
+ if (!ucm->active_verb)
+ return;
+
+ PA_LLIST_FOREACH(mod, ucm->active_verb->modifiers) {
+ if ((mod->action_direction == dir) && (pa_streq(mod->media_role, role))) {
+ if (mod->enabled_counter == 0) {
+ const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ pa_log_info("Enable ucm modifier %s", mod_name);
+ if (snd_use_case_set(ucm->ucm_mgr, "_enamod", mod_name) < 0) {
+ pa_log("Failed to enable ucm modifier %s", mod_name);
+ }
+ }
+
+ mod->enabled_counter++;
+ break;
+ }
+ }
+}
+
+/* Disable the modifier when the last stream with matched role ends */
+void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) {
+ pa_alsa_ucm_modifier *mod;
+
+ if (!ucm->active_verb)
+ return;
+
+ PA_LLIST_FOREACH(mod, ucm->active_verb->modifiers) {
+ if ((mod->action_direction == dir) && (pa_streq(mod->media_role, role))) {
+
+ mod->enabled_counter--;
+ if (mod->enabled_counter == 0) {
+ const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ pa_log_info("Disable ucm modifier %s", mod_name);
+ if (snd_use_case_set(ucm->ucm_mgr, "_dismod", mod_name) < 0) {
+ pa_log("Failed to disable ucm modifier %s", mod_name);
+ }
+ }
+
+ break;
+ }
+ }
+}
+
+static void device_add_ucm_port(pa_alsa_ucm_device *device, pa_alsa_ucm_port_data *port) {
+ pa_assert(device);
+ pa_assert(port);
+
+ pa_dynarray_append(device->ucm_ports, port);
+}
+
+static void device_set_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) {
+ pa_assert(device);
+ pa_assert(jack);
+
+ device->jack = jack;
+ pa_alsa_jack_add_ucm_device(jack, device);
+
+ pa_alsa_ucm_device_update_available(device);
+}
+
+static void device_add_hw_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) {
+ pa_assert(device);
+ pa_assert(jack);
+
+ pa_dynarray_append(device->hw_mute_jacks, jack);
+ pa_alsa_jack_add_ucm_hw_mute_device(jack, device);
+
+ pa_alsa_ucm_device_update_available(device);
+}
+
+static void device_set_available(pa_alsa_ucm_device *device, pa_available_t available) {
+ pa_alsa_ucm_port_data *port;
+ unsigned idx;
+
+ pa_assert(device);
+
+ if (available == device->available)
+ return;
+
+ device->available = available;
+
+ PA_DYNARRAY_FOREACH(port, device->ucm_ports, idx)
+ ucm_port_update_available(port);
+}
+
+void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device) {
+ pa_available_t available = PA_AVAILABLE_UNKNOWN;
+ pa_alsa_jack *jack;
+ unsigned idx;
+
+ pa_assert(device);
+
+ if (device->jack && device->jack->has_control)
+ available = device->jack->plugged_in ? PA_AVAILABLE_YES : PA_AVAILABLE_NO;
+
+ PA_DYNARRAY_FOREACH(jack, device->hw_mute_jacks, idx) {
+ if (jack->plugged_in) {
+ available = PA_AVAILABLE_NO;
+ break;
+ }
+ }
+
+ device_set_available(device, available);
+}
+
+static void ucm_port_data_init(pa_alsa_ucm_port_data *port, pa_alsa_ucm_config *ucm, pa_device_port *core_port,
+ pa_alsa_ucm_device **devices, unsigned n_devices) {
+ unsigned i;
+
+ pa_assert(ucm);
+ pa_assert(core_port);
+ pa_assert(devices);
+
+ port->ucm = ucm;
+ port->core_port = core_port;
+ port->devices = pa_dynarray_new(NULL);
+ port->eld_device = -1;
+
+ for (i = 0; i < n_devices; i++) {
+ pa_dynarray_append(port->devices, devices[i]);
+ device_add_ucm_port(devices[i], port);
+ }
+
+ port->paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, NULL);
+
+ ucm_port_update_available(port);
+}
+
+static void ucm_port_data_free(pa_device_port *port) {
+ pa_alsa_ucm_port_data *ucm_port;
+
+ pa_assert(port);
+
+ ucm_port = PA_DEVICE_PORT_DATA(port);
+
+ if (ucm_port->devices)
+ pa_dynarray_free(ucm_port->devices);
+
+ if (ucm_port->paths)
+ pa_hashmap_free(ucm_port->paths);
+
+ pa_xfree(ucm_port->eld_mixer_device_name);
+}
+
+static void ucm_port_update_available(pa_alsa_ucm_port_data *port) {
+ pa_alsa_ucm_device *device;
+ unsigned idx;
+ pa_available_t available = PA_AVAILABLE_YES;
+
+ pa_assert(port);
+
+ PA_DYNARRAY_FOREACH(device, port->devices, idx) {
+ if (device->available == PA_AVAILABLE_UNKNOWN)
+ available = PA_AVAILABLE_UNKNOWN;
+ else if (device->available == PA_AVAILABLE_NO) {
+ available = PA_AVAILABLE_NO;
+ break;
+ }
+ }
+
+ pa_device_port_set_available(port->core_port, available);
+}
+
+#else /* HAVE_ALSA_UCM */
+
+/* Dummy functions for systems without UCM support */
+
+int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) {
+ pa_log_info("UCM not available.");
+ return -1;
+}
+
+pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map) {
+ return NULL;
+}
+
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) {
+ return -1;
+}
+
+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) {
+ return -1;
+}
+
+void pa_alsa_ucm_add_ports(
+ pa_hashmap **hash,
+ pa_proplist *proplist,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_card *card,
+ snd_pcm_t *pcm_handle,
+ bool ignore_dB) {
+}
+
+void pa_alsa_ucm_add_ports_combination(
+ pa_hashmap *hash,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core) {
+}
+
+int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink) {
+ return -1;
+}
+
+void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) {
+}
+
+void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) {
+}
+
+void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) {
+}
+
+void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) {
+}
+
+#endif
diff --git a/spa/plugins/alsa/acp/alsa-ucm.h b/spa/plugins/alsa/acp/alsa-ucm.h
new file mode 100644
index 0000000..696209e
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-ucm.h
@@ -0,0 +1,301 @@
+#ifndef fooalsaucmhfoo
+#define fooalsaucmhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2011 Wolfson Microelectronics PLC
+ Author Margarita Olaya <magi@slimlogic.co.uk>
+ Copyright 2012 Feng Wei <wei.feng@freescale.com>, Freescale Ltd.
+
+ 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/>.
+***/
+
+#ifdef HAVE_ALSA_UCM
+#include <alsa/use-case.h>
+#else
+typedef void snd_use_case_mgr_t;
+#endif
+
+#include "compat.h"
+
+#include "alsa-mixer.h"
+
+/** For devices: List of verbs, devices or modifiers available */
+#define PA_ALSA_PROP_UCM_NAME "alsa.ucm.name"
+
+/** For devices: List of supported devices per verb*/
+#define PA_ALSA_PROP_UCM_DESCRIPTION "alsa.ucm.description"
+
+/** For devices: Playback device name e.g PlaybackPCM */
+#define PA_ALSA_PROP_UCM_SINK "alsa.ucm.sink"
+
+/** For devices: Capture device name e.g CapturePCM*/
+#define PA_ALSA_PROP_UCM_SOURCE "alsa.ucm.source"
+
+/** For devices: Playback roles */
+#define PA_ALSA_PROP_UCM_PLAYBACK_ROLES "alsa.ucm.playback.roles"
+
+/** For devices: Playback control device name */
+#define PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE "alsa.ucm.playback.ctldev"
+
+/** For devices: Playback control volume ID string. e.g PlaybackVolume */
+#define PA_ALSA_PROP_UCM_PLAYBACK_VOLUME "alsa.ucm.playback.volume"
+
+/** For devices: Playback switch e.g PlaybackSwitch */
+#define PA_ALSA_PROP_UCM_PLAYBACK_SWITCH "alsa.ucm.playback.switch"
+
+/** For devices: Playback mixer device name */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE "alsa.ucm.playback.mixer.device"
+
+/** For devices: Playback mixer identifier */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM "alsa.ucm.playback.mixer.element"
+
+/** For devices: Playback mixer master identifier */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM "alsa.ucm.playback.master.element"
+
+/** For devices: Playback mixer master type */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE "alsa.ucm.playback.master.type"
+
+/** For devices: Playback mixer master identifier */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ID "alsa.ucm.playback.master.id"
+
+/** For devices: Playback mixer master type */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE "alsa.ucm.playback.master.type"
+
+/** For devices: Playback priority */
+#define PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY "alsa.ucm.playback.priority"
+
+/** For devices: Playback rate */
+#define PA_ALSA_PROP_UCM_PLAYBACK_RATE "alsa.ucm.playback.rate"
+
+/** For devices: Playback channels */
+#define PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS "alsa.ucm.playback.channels"
+
+/** For devices: Capture roles */
+#define PA_ALSA_PROP_UCM_CAPTURE_ROLES "alsa.ucm.capture.roles"
+
+/** For devices: Capture control device name */
+#define PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE "alsa.ucm.capture.ctldev"
+
+/** For devices: Capture controls volume ID string. e.g CaptureVolume */
+#define PA_ALSA_PROP_UCM_CAPTURE_VOLUME "alsa.ucm.capture.volume"
+
+/** For devices: Capture switch e.g CaptureSwitch */
+#define PA_ALSA_PROP_UCM_CAPTURE_SWITCH "alsa.ucm.capture.switch"
+
+/** For devices: Capture mixer device name */
+#define PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE "alsa.ucm.capture.mixer.device"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM "alsa.ucm.capture.mixer.element"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM "alsa.ucm.capture.master.element"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE "alsa.ucm.capture.master.type"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MASTER_ID "alsa.ucm.capture.master.id"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE "alsa.ucm.capture.master.type"
+
+/** For devices: Capture priority */
+#define PA_ALSA_PROP_UCM_CAPTURE_PRIORITY "alsa.ucm.capture.priority"
+
+/** For devices: Capture rate */
+#define PA_ALSA_PROP_UCM_CAPTURE_RATE "alsa.ucm.capture.rate"
+
+/** For devices: Capture channels */
+#define PA_ALSA_PROP_UCM_CAPTURE_CHANNELS "alsa.ucm.capture.channels"
+
+/** For devices: Quality of Service */
+#define PA_ALSA_PROP_UCM_QOS "alsa.ucm.qos"
+
+/** For devices: The modifier (if any) that this device corresponds to */
+#define PA_ALSA_PROP_UCM_MODIFIER "alsa.ucm.modifier"
+
+/* Corresponds to the "JackCTL" UCM value. */
+#define PA_ALSA_PROP_UCM_JACK_DEVICE "alsa.ucm.jack_device"
+
+/* Corresponds to the "JackControl" UCM value. */
+#define PA_ALSA_PROP_UCM_JACK_CONTROL "alsa.ucm.jack_control"
+
+/* Corresponds to the "JackHWMute" UCM value. */
+#define PA_ALSA_PROP_UCM_JACK_HW_MUTE "alsa.ucm.jack_hw_mute"
+
+typedef struct pa_alsa_ucm_verb pa_alsa_ucm_verb;
+typedef struct pa_alsa_ucm_modifier pa_alsa_ucm_modifier;
+typedef struct pa_alsa_ucm_device pa_alsa_ucm_device;
+typedef struct pa_alsa_ucm_config pa_alsa_ucm_config;
+typedef struct pa_alsa_ucm_mapping_context pa_alsa_ucm_mapping_context;
+typedef struct pa_alsa_ucm_port_data pa_alsa_ucm_port_data;
+typedef struct pa_alsa_ucm_volume pa_alsa_ucm_volume;
+
+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);
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile);
+
+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);
+
+void pa_alsa_ucm_add_ports(
+ pa_hashmap **hash,
+ pa_proplist *proplist,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_card *card,
+ snd_pcm_t *pcm_handle,
+ bool ignore_dB);
+void pa_alsa_ucm_add_ports_combination(
+ pa_hashmap *hash,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core);
+int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink);
+
+void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm);
+void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context);
+
+void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir);
+void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir);
+
+/* UCM - Use Case Manager is available on some audio cards */
+
+struct pa_alsa_ucm_device {
+ PA_LLIST_FIELDS(pa_alsa_ucm_device);
+
+ pa_proplist *proplist;
+
+ pa_device_port_type_t type;
+
+ unsigned playback_priority;
+ unsigned capture_priority;
+
+ unsigned playback_rate;
+ unsigned capture_rate;
+
+ unsigned playback_channels;
+ unsigned capture_channels;
+
+ /* These may be different per verb, so we store this as a hashmap of verb -> volume_control. We might eventually want to
+ * make this a hashmap of verb -> per-verb-device-properties-struct. */
+ pa_hashmap *playback_volumes;
+ pa_hashmap *capture_volumes;
+
+ pa_alsa_mapping *playback_mapping;
+ pa_alsa_mapping *capture_mapping;
+
+ pa_idxset *conflicting_devices;
+ pa_idxset *supported_devices;
+
+ /* One device may be part of multiple ports, since each device has
+ * a dedicated port, and in addition to that we sometimes generate ports
+ * that represent combinations of devices. */
+ pa_dynarray *ucm_ports; /* struct ucm_port */
+
+ pa_alsa_jack *jack;
+ pa_dynarray *hw_mute_jacks; /* pa_alsa_jack */
+ pa_available_t available;
+
+ char *eld_mixer_device_name;
+ int eld_device;
+};
+
+void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device);
+
+struct pa_alsa_ucm_modifier {
+ PA_LLIST_FIELDS(pa_alsa_ucm_modifier);
+
+ pa_proplist *proplist;
+
+ int n_confdev;
+ int n_suppdev;
+
+ const char **conflicting_devices;
+ const char **supported_devices;
+
+ pa_direction_t action_direction;
+
+ char *media_role;
+
+ /* Non-NULL if the modifier has its own PlaybackPCM/CapturePCM */
+ pa_alsa_mapping *playback_mapping;
+ pa_alsa_mapping *capture_mapping;
+
+ /* Count how many role matched streams are running */
+ int enabled_counter;
+};
+
+struct pa_alsa_ucm_verb {
+ PA_LLIST_FIELDS(pa_alsa_ucm_verb);
+
+ pa_proplist *proplist;
+ unsigned priority;
+
+ PA_LLIST_HEAD(pa_alsa_ucm_device, devices);
+ PA_LLIST_HEAD(pa_alsa_ucm_modifier, modifiers);
+};
+
+struct pa_alsa_ucm_config {
+ pa_sample_spec default_sample_spec;
+ pa_channel_map default_channel_map;
+ unsigned default_fragment_size_msec;
+ unsigned default_n_fragments;
+
+ snd_use_case_mgr_t *ucm_mgr;
+ pa_alsa_ucm_verb *active_verb;
+ char *alib_prefix;
+
+ pa_hashmap *mixers;
+ PA_LLIST_HEAD(pa_alsa_ucm_verb, verbs);
+ PA_LLIST_HEAD(pa_alsa_jack, jacks);
+};
+
+struct pa_alsa_ucm_mapping_context {
+ pa_alsa_ucm_config *ucm;
+ pa_direction_t direction;
+
+ pa_idxset *ucm_devices;
+ pa_idxset *ucm_modifiers;
+};
+
+struct pa_alsa_ucm_port_data {
+ pa_alsa_ucm_config *ucm;
+ pa_device_port *core_port;
+
+ /* A single port will be associated with multiple devices if it represents
+ * a combination of devices. */
+ pa_dynarray *devices; /* pa_alsa_ucm_device */
+
+ /* profile name -> pa_alsa_path for volume control */
+ pa_hashmap *paths;
+ /* Current path, set when activating profile */
+ pa_alsa_path *path;
+
+ /* ELD info */
+ char *eld_mixer_device_name;
+ int eld_device; /* PCM device number */
+};
+
+struct pa_alsa_ucm_volume {
+ char *mixer_elem; /* mixer element identifier */
+ char *master_elem; /* master mixer element identifier */
+ char *master_type;
+};
+
+#endif
diff --git a/spa/plugins/alsa/acp/alsa-util.c b/spa/plugins/alsa/acp/alsa-util.c
new file mode 100644
index 0000000..c76cef3
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-util.c
@@ -0,0 +1,1904 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2009 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <alsa/asoundlib.h>
+
+#include "alsa-util.h"
+#include "alsa-mixer.h"
+
+#ifdef HAVE_UDEV
+#include <modules/udev-util.h>
+#endif
+
+static int set_format(snd_pcm_t *pcm_handle, snd_pcm_hw_params_t *hwparams, pa_sample_format_t *f) {
+
+ static const snd_pcm_format_t format_trans[] = {
+ [PA_SAMPLE_U8] = SND_PCM_FORMAT_U8,
+ [PA_SAMPLE_ALAW] = SND_PCM_FORMAT_A_LAW,
+ [PA_SAMPLE_ULAW] = SND_PCM_FORMAT_MU_LAW,
+ [PA_SAMPLE_S16LE] = SND_PCM_FORMAT_S16_LE,
+ [PA_SAMPLE_S16BE] = SND_PCM_FORMAT_S16_BE,
+ [PA_SAMPLE_FLOAT32LE] = SND_PCM_FORMAT_FLOAT_LE,
+ [PA_SAMPLE_FLOAT32BE] = SND_PCM_FORMAT_FLOAT_BE,
+ [PA_SAMPLE_S32LE] = SND_PCM_FORMAT_S32_LE,
+ [PA_SAMPLE_S32BE] = SND_PCM_FORMAT_S32_BE,
+ [PA_SAMPLE_S24LE] = SND_PCM_FORMAT_S24_3LE,
+ [PA_SAMPLE_S24BE] = SND_PCM_FORMAT_S24_3BE,
+ [PA_SAMPLE_S24_32LE] = SND_PCM_FORMAT_S24_LE,
+ [PA_SAMPLE_S24_32BE] = SND_PCM_FORMAT_S24_BE,
+ };
+
+ static const pa_sample_format_t try_order[] = {
+ PA_SAMPLE_FLOAT32NE,
+ PA_SAMPLE_FLOAT32RE,
+ PA_SAMPLE_S32NE,
+ PA_SAMPLE_S32RE,
+ PA_SAMPLE_S24_32NE,
+ PA_SAMPLE_S24_32RE,
+ PA_SAMPLE_S24NE,
+ PA_SAMPLE_S24RE,
+ PA_SAMPLE_S16NE,
+ PA_SAMPLE_S16RE,
+ PA_SAMPLE_ALAW,
+ PA_SAMPLE_ULAW,
+ PA_SAMPLE_U8
+ };
+
+ unsigned i;
+ int ret;
+
+ pa_assert(pcm_handle);
+ pa_assert(hwparams);
+ pa_assert(f);
+
+ if ((ret = snd_pcm_hw_params_set_format(pcm_handle, hwparams, format_trans[*f])) >= 0)
+ return ret;
+
+ pa_log_debug("snd_pcm_hw_params_set_format(%s) failed: %s",
+ snd_pcm_format_description(format_trans[*f]),
+ pa_alsa_strerror(ret));
+
+ if (*f == PA_SAMPLE_FLOAT32BE)
+ *f = PA_SAMPLE_FLOAT32LE;
+ else if (*f == PA_SAMPLE_FLOAT32LE)
+ *f = PA_SAMPLE_FLOAT32BE;
+ else if (*f == PA_SAMPLE_S24BE)
+ *f = PA_SAMPLE_S24LE;
+ else if (*f == PA_SAMPLE_S24LE)
+ *f = PA_SAMPLE_S24BE;
+ else if (*f == PA_SAMPLE_S24_32BE)
+ *f = PA_SAMPLE_S24_32LE;
+ else if (*f == PA_SAMPLE_S24_32LE)
+ *f = PA_SAMPLE_S24_32BE;
+ else if (*f == PA_SAMPLE_S16BE)
+ *f = PA_SAMPLE_S16LE;
+ else if (*f == PA_SAMPLE_S16LE)
+ *f = PA_SAMPLE_S16BE;
+ else if (*f == PA_SAMPLE_S32BE)
+ *f = PA_SAMPLE_S32LE;
+ else if (*f == PA_SAMPLE_S32LE)
+ *f = PA_SAMPLE_S32BE;
+ else
+ goto try_auto;
+
+ if ((ret = snd_pcm_hw_params_set_format(pcm_handle, hwparams, format_trans[*f])) >= 0)
+ return ret;
+
+ pa_log_debug("snd_pcm_hw_params_set_format(%s) failed: %s",
+ snd_pcm_format_description(format_trans[*f]),
+ pa_alsa_strerror(ret));
+
+try_auto:
+
+ for (i = 0; i < PA_ELEMENTSOF(try_order); i++) {
+ *f = try_order[i];
+
+ if ((ret = snd_pcm_hw_params_set_format(pcm_handle, hwparams, format_trans[*f])) >= 0)
+ return ret;
+
+ pa_log_debug("snd_pcm_hw_params_set_format(%s) failed: %s",
+ snd_pcm_format_description(format_trans[*f]),
+ pa_alsa_strerror(ret));
+ }
+
+ return -1;
+}
+
+static int set_period_size(snd_pcm_t *pcm_handle, snd_pcm_hw_params_t *hwparams, snd_pcm_uframes_t size) {
+ snd_pcm_uframes_t s;
+ int d, ret;
+
+ pa_assert(pcm_handle);
+ pa_assert(hwparams);
+
+ s = size;
+ d = 0;
+ if (snd_pcm_hw_params_set_period_size_near(pcm_handle, hwparams, &s, &d) < 0) {
+ s = size;
+ d = -1;
+ if (snd_pcm_hw_params_set_period_size_near(pcm_handle, hwparams, &s, &d) < 0) {
+ s = size;
+ d = 1;
+ if ((ret = snd_pcm_hw_params_set_period_size_near(pcm_handle, hwparams, &s, &d)) < 0) {
+ pa_log_info("snd_pcm_hw_params_set_period_size_near() failed: %s", pa_alsa_strerror(ret));
+ return ret;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int set_buffer_size(snd_pcm_t *pcm_handle, snd_pcm_hw_params_t *hwparams, snd_pcm_uframes_t size) {
+ int ret;
+
+ pa_assert(pcm_handle);
+ pa_assert(hwparams);
+
+ if ((ret = snd_pcm_hw_params_set_buffer_size_near(pcm_handle, hwparams, &size)) < 0) {
+ pa_log_info("snd_pcm_hw_params_set_buffer_size_near() failed: %s", pa_alsa_strerror(ret));
+ return ret;
+ }
+
+ return 0;
+}
+
+static void check_access(snd_pcm_t *pcm_handle, snd_pcm_hw_params_t *hwparams, bool use_mmap) {
+ if ((use_mmap && !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_INTERLEAVED)) ||
+ !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED))
+ pa_log_error("Weird, PCM claims to support interleaved access, but snd_pcm_hw_params_set_access() failed.");
+
+ if ((use_mmap && !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_NONINTERLEAVED)) ||
+ !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_NONINTERLEAVED))
+ pa_log_debug("PCM seems to support non-interleaved access, but PA doesn't.");
+ else if (use_mmap && !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_COMPLEX)) {
+ pa_log_debug("PCM seems to support mmapped complex access, but PA doesn't.");
+ }
+}
+
+/* Set the hardware parameters of the given ALSA device. Returns the
+ * selected fragment settings in *buffer_size and *period_size. Determine
+ * whether mmap and tsched mode can be enabled. */
+int pa_alsa_set_hw_params(
+ snd_pcm_t *pcm_handle,
+ pa_sample_spec *ss,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ bool require_exact_channel_number) {
+
+ int ret = -1;
+ snd_pcm_hw_params_t *hwparams, *hwparams_copy;
+ int dir;
+ snd_pcm_uframes_t _period_size = period_size ? *period_size : 0;
+ snd_pcm_uframes_t _buffer_size = buffer_size ? *buffer_size : 0;
+ bool _use_mmap = use_mmap && *use_mmap;
+ bool _use_tsched = use_tsched && *use_tsched;
+ pa_sample_spec _ss = *ss;
+
+ pa_assert(pcm_handle);
+ pa_assert(ss);
+
+ snd_pcm_hw_params_alloca(&hwparams);
+ snd_pcm_hw_params_alloca(&hwparams_copy);
+
+ if ((ret = snd_pcm_hw_params_any(pcm_handle, hwparams)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ if ((ret = snd_pcm_hw_params_set_rate_resample(pcm_handle, hwparams, 0)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_rate_resample() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ if (_use_mmap) {
+
+ if (snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_INTERLEAVED) < 0) {
+
+ /* mmap() didn't work, fall back to interleaved */
+
+ if ((ret = snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_access() failed: %s", pa_alsa_strerror(ret));
+ check_access(pcm_handle, hwparams, true);
+ goto finish;
+ }
+
+ _use_mmap = false;
+ }
+
+ } else if ((ret = snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_access() failed: %s", pa_alsa_strerror(ret));
+ check_access(pcm_handle, hwparams, false);
+ goto finish;
+ }
+
+ if (!_use_mmap)
+ _use_tsched = false;
+
+ if (!pa_alsa_pcm_is_hw(pcm_handle))
+ _use_tsched = false;
+
+ /* The PCM pointer is only updated with period granularity */
+ if (snd_pcm_hw_params_is_batch(hwparams)) {
+ bool is_usb = false;
+ const char *id;
+ snd_pcm_info_t* pcm_info;
+ snd_pcm_info_alloca(&pcm_info);
+
+ if (snd_pcm_info(pcm_handle, pcm_info) == 0 &&
+ (id = snd_pcm_info_get_id(pcm_info))) {
+ /* This horrible hack makes sure we don't disable tsched on USB
+ * devices, which have a low enough transfer size for timer-based
+ * scheduling to work. This can go away when the ALSA API supports
+ * querying the block transfer size. */
+ if (pa_streq(id, "USB Audio"))
+ is_usb = true;
+ }
+
+ if (!is_usb) {
+ pa_log_info("Disabling tsched mode since BATCH flag is set");
+ _use_tsched = false;
+ }
+ }
+
+#if (SND_LIB_VERSION >= ((1<<16)|(0<<8)|24)) /* API additions in 1.0.24 */
+ if (_use_tsched) {
+
+ /* try to disable period wakeups if hardware can do so */
+ if (snd_pcm_hw_params_can_disable_period_wakeup(hwparams)) {
+
+ if ((ret = snd_pcm_hw_params_set_period_wakeup(pcm_handle, hwparams, false)) < 0)
+ /* don't bail, keep going with default mode with period wakeups */
+ pa_log_debug("snd_pcm_hw_params_set_period_wakeup() failed: %s", pa_alsa_strerror(ret));
+ else
+ pa_log_info("Trying to disable ALSA period wakeups, using timers only");
+ } else
+ pa_log_info("Cannot disable ALSA period wakeups");
+ }
+#endif
+
+ if ((ret = set_format(pcm_handle, hwparams, &_ss.format)) < 0)
+ goto finish;
+
+ if ((ret = snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams, &_ss.rate, NULL)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_rate_near() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ /* We ignore very small sampling rate deviations */
+ if (_ss.rate >= ss->rate*.95 && _ss.rate <= ss->rate*1.05)
+ _ss.rate = ss->rate;
+
+ if (require_exact_channel_number) {
+ if ((ret = snd_pcm_hw_params_set_channels(pcm_handle, hwparams, _ss.channels)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_channels(%u) failed: %s", _ss.channels, pa_alsa_strerror(ret));
+ goto finish;
+ }
+ } else {
+ unsigned int c = _ss.channels;
+
+ if ((ret = snd_pcm_hw_params_set_channels_near(pcm_handle, hwparams, &c)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_channels_near(%u) failed: %s", _ss.channels, pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ _ss.channels = c;
+ }
+
+ if (_use_tsched && tsched_size > 0) {
+ _buffer_size = (snd_pcm_uframes_t) (((uint64_t) tsched_size * _ss.rate) / ss->rate);
+ _period_size = _buffer_size;
+ } else {
+ _period_size = (snd_pcm_uframes_t) (((uint64_t) _period_size * _ss.rate) / ss->rate);
+ _buffer_size = (snd_pcm_uframes_t) (((uint64_t) _buffer_size * _ss.rate) / ss->rate);
+ }
+
+ if (_buffer_size > 0 || _period_size > 0) {
+ snd_pcm_uframes_t max_frames = 0;
+
+ if ((ret = snd_pcm_hw_params_get_buffer_size_max(hwparams, &max_frames)) < 0)
+ pa_log_warn("snd_pcm_hw_params_get_buffer_size_max() failed: %s", pa_alsa_strerror(ret));
+ else
+ pa_log_debug("Maximum hw buffer size is %lu ms", (long unsigned) (max_frames * PA_MSEC_PER_SEC / _ss.rate));
+
+ /* Some ALSA drivers really don't like if we set the buffer
+ * size first and the number of periods second (which would
+ * make a lot more sense to me). So, try a few combinations
+ * before we give up. */
+
+ if (_buffer_size > 0 && _period_size > 0) {
+ snd_pcm_hw_params_copy(hwparams_copy, hwparams);
+
+ /* First try: set buffer size first, followed by period size */
+ if (set_buffer_size(pcm_handle, hwparams_copy, _buffer_size) >= 0 &&
+ set_period_size(pcm_handle, hwparams_copy, _period_size) >= 0 &&
+ snd_pcm_hw_params(pcm_handle, hwparams_copy) >= 0) {
+ pa_log_debug("Set buffer size first (to %lu samples), period size second (to %lu samples).", (unsigned long) _buffer_size, (unsigned long) _period_size);
+ goto success;
+ }
+
+ snd_pcm_hw_params_copy(hwparams_copy, hwparams);
+ /* Second try: set period size first, followed by buffer size */
+ if (set_period_size(pcm_handle, hwparams_copy, _period_size) >= 0 &&
+ set_buffer_size(pcm_handle, hwparams_copy, _buffer_size) >= 0 &&
+ snd_pcm_hw_params(pcm_handle, hwparams_copy) >= 0) {
+ pa_log_debug("Set period size first (to %lu samples), buffer size second (to %lu samples).", (unsigned long) _period_size, (unsigned long) _buffer_size);
+ goto success;
+ }
+ }
+
+ if (_buffer_size > 0) {
+ snd_pcm_hw_params_copy(hwparams_copy, hwparams);
+
+ /* Third try: set only buffer size */
+ if (set_buffer_size(pcm_handle, hwparams_copy, _buffer_size) >= 0 &&
+ snd_pcm_hw_params(pcm_handle, hwparams_copy) >= 0) {
+ pa_log_debug("Set only buffer size (to %lu samples).", (unsigned long) _buffer_size);
+ goto success;
+ }
+ }
+
+ if (_period_size > 0) {
+ snd_pcm_hw_params_copy(hwparams_copy, hwparams);
+
+ /* Fourth try: set only period size */
+ if (set_period_size(pcm_handle, hwparams_copy, _period_size) >= 0 &&
+ snd_pcm_hw_params(pcm_handle, hwparams_copy) >= 0) {
+ pa_log_debug("Set only period size (to %lu samples).", (unsigned long) _period_size);
+ goto success;
+ }
+ }
+ }
+
+ pa_log_debug("Set neither period nor buffer size.");
+
+ /* Last chance, set nothing */
+ if ((ret = snd_pcm_hw_params(pcm_handle, hwparams)) < 0) {
+ pa_log_info("snd_pcm_hw_params failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+success:
+
+ if (ss->rate != _ss.rate)
+ pa_log_info("Device %s doesn't support %u Hz, changed to %u Hz.", snd_pcm_name(pcm_handle), ss->rate, _ss.rate);
+
+ if (ss->channels != _ss.channels)
+ pa_log_info("Device %s doesn't support %u channels, changed to %u.", snd_pcm_name(pcm_handle), ss->channels, _ss.channels);
+
+ if (ss->format != _ss.format)
+ pa_log_info("Device %s doesn't support sample format %s, changed to %s.", snd_pcm_name(pcm_handle), pa_sample_format_to_string(ss->format), pa_sample_format_to_string(_ss.format));
+
+ if ((ret = snd_pcm_hw_params_current(pcm_handle, hwparams)) < 0) {
+ pa_log_info("snd_pcm_hw_params_current() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ if ((ret = snd_pcm_hw_params_get_period_size(hwparams, &_period_size, &dir)) < 0 ||
+ (ret = snd_pcm_hw_params_get_buffer_size(hwparams, &_buffer_size)) < 0) {
+ pa_log_info("snd_pcm_hw_params_get_{period|buffer}_size() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+#if (SND_LIB_VERSION >= ((1<<16)|(0<<8)|24)) /* API additions in 1.0.24 */
+ if (_use_tsched) {
+ unsigned int no_wakeup;
+ /* see if period wakeups were disabled */
+ snd_pcm_hw_params_get_period_wakeup(pcm_handle, hwparams, &no_wakeup);
+ if (no_wakeup == 0)
+ pa_log_info("ALSA period wakeups disabled");
+ else
+ pa_log_info("ALSA period wakeups were not disabled");
+ }
+#endif
+
+ ss->rate = _ss.rate;
+ ss->channels = _ss.channels;
+ ss->format = _ss.format;
+
+ pa_assert(_period_size > 0);
+ pa_assert(_buffer_size > 0);
+
+ if (buffer_size)
+ *buffer_size = _buffer_size;
+
+ if (period_size)
+ *period_size = _period_size;
+
+ if (use_mmap)
+ *use_mmap = _use_mmap;
+
+ if (use_tsched)
+ *use_tsched = _use_tsched;
+
+ ret = 0;
+
+finish:
+
+ return ret;
+}
+
+int pa_alsa_set_sw_params(snd_pcm_t *pcm, snd_pcm_uframes_t avail_min, bool period_event) {
+ snd_pcm_sw_params_t *swparams;
+ snd_pcm_uframes_t boundary;
+ int err;
+
+ pa_assert(pcm);
+
+ snd_pcm_sw_params_alloca(&swparams);
+
+ if ((err = snd_pcm_sw_params_current(pcm, swparams)) < 0) {
+ pa_log_warn("Unable to determine current swparams: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_period_event(pcm, swparams, period_event)) < 0) {
+ pa_log_warn("Unable to disable period event: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_tstamp_mode(pcm, swparams, SND_PCM_TSTAMP_ENABLE)) < 0) {
+ pa_log_warn("Unable to enable time stamping: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_get_boundary(swparams, &boundary)) < 0) {
+ pa_log_warn("Unable to get boundary: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_stop_threshold(pcm, swparams, boundary)) < 0) {
+ pa_log_warn("Unable to set stop threshold: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_start_threshold(pcm, swparams, (snd_pcm_uframes_t) -1)) < 0) {
+ pa_log_warn("Unable to set start threshold: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, avail_min)) < 0) {
+ pa_log_error("snd_pcm_sw_params_set_avail_min() failed: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params(pcm, swparams)) < 0) {
+ pa_log_warn("Unable to set sw params: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ return 0;
+}
+
+#if 0
+snd_pcm_t *pa_alsa_open_by_device_id_auto(
+ const char *dev_id,
+ char **dev,
+ pa_sample_spec *ss,
+ pa_channel_map* map,
+ int mode,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ pa_alsa_profile_set *ps,
+ pa_alsa_mapping **mapping) {
+
+ char *d;
+ snd_pcm_t *pcm_handle;
+ void *state;
+ pa_alsa_mapping *m;
+
+ pa_assert(dev_id);
+ pa_assert(dev);
+ pa_assert(ss);
+ pa_assert(map);
+ pa_assert(ps);
+
+ /* First we try to find a device string with a superset of the
+ * requested channel map. We iterate through our device table from
+ * top to bottom and take the first that matches. If we didn't
+ * find a working device that way, we iterate backwards, and check
+ * all devices that do not provide a superset of the requested
+ * channel map.*/
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, state) {
+ if (!pa_channel_map_superset(&m->channel_map, map))
+ continue;
+
+ pa_log_debug("Checking for superset %s (%s)", m->name, m->device_strings[0]);
+
+ pcm_handle = pa_alsa_open_by_device_id_mapping(
+ dev_id,
+ dev,
+ ss,
+ map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ m);
+
+ if (pcm_handle) {
+ if (mapping)
+ *mapping = m;
+
+ return pcm_handle;
+ }
+ }
+
+ PA_HASHMAP_FOREACH_BACKWARDS(m, ps->mappings, state) {
+ if (pa_channel_map_superset(&m->channel_map, map))
+ continue;
+
+ pa_log_debug("Checking for subset %s (%s)", m->name, m->device_strings[0]);
+
+ pcm_handle = pa_alsa_open_by_device_id_mapping(
+ dev_id,
+ dev,
+ ss,
+ map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ m);
+
+ if (pcm_handle) {
+ if (mapping)
+ *mapping = m;
+
+ return pcm_handle;
+ }
+ }
+
+ /* OK, we didn't find any good device, so let's try the raw hw: stuff */
+ d = pa_sprintf_malloc("hw:%s", dev_id);
+ pa_log_debug("Trying %s as last resort...", d);
+ pcm_handle = pa_alsa_open_by_device_string(
+ d,
+ dev,
+ ss,
+ map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ false);
+ pa_xfree(d);
+
+ if (pcm_handle && mapping)
+ *mapping = NULL;
+
+ return pcm_handle;
+}
+#endif
+
+snd_pcm_t *pa_alsa_open_by_device_id_mapping(
+ const char *dev_id,
+ char **dev,
+ pa_sample_spec *ss,
+ pa_channel_map* map,
+ int mode,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ pa_alsa_mapping *m) {
+
+ snd_pcm_t *pcm_handle;
+ pa_sample_spec try_ss;
+ pa_channel_map try_map;
+
+ pa_assert(dev_id);
+ pa_assert(dev);
+ pa_assert(ss);
+ pa_assert(map);
+ pa_assert(m);
+
+ try_ss.channels = m->channel_map.channels;
+ try_ss.rate = ss->rate;
+ try_ss.format = ss->format;
+ try_map = m->channel_map;
+
+ pcm_handle = pa_alsa_open_by_template(
+ m->device_strings,
+ dev_id,
+ dev,
+ &try_ss,
+ &try_map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ pa_channel_map_valid(&m->channel_map) /* Query the channel count if we don't know what we want */);
+
+ if (!pcm_handle)
+ return NULL;
+
+ *ss = try_ss;
+ *map = try_map;
+ pa_assert(map->channels == ss->channels);
+
+ return pcm_handle;
+}
+
+int pa_alsa_close(snd_pcm_t **pcm)
+{
+ int err;
+ pa_assert(pcm);
+ pa_log_info("ALSA device close %p", *pcm);
+ if (*pcm == NULL)
+ return 0;
+ if ((err = snd_pcm_close(*pcm)) < 0) {
+ pa_log_warn("ALSA close failed: %s", snd_strerror(err));
+ }
+ *pcm = NULL;
+ return err;
+}
+
+snd_pcm_t *pa_alsa_open_by_device_string(
+ const char *device,
+ char **dev,
+ pa_sample_spec *ss,
+ pa_channel_map* map,
+ int mode,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ bool require_exact_channel_number) {
+
+ int err;
+ char *d;
+ snd_pcm_t *pcm_handle;
+ bool reformat = false;
+
+ pa_assert(device);
+ pa_assert(ss);
+ pa_assert(map);
+
+ d = pa_xstrdup(device);
+
+ for (;;) {
+ pa_log_debug("Trying %s %s SND_PCM_NO_AUTO_FORMAT ...", d, reformat ? "without" : "with");
+
+ if ((err = snd_pcm_open(&pcm_handle, d, mode,
+ SND_PCM_NONBLOCK|
+ SND_PCM_NO_AUTO_RESAMPLE|
+ SND_PCM_NO_AUTO_CHANNELS|
+ (reformat ? 0 : SND_PCM_NO_AUTO_FORMAT))) < 0) {
+ pa_log_info("Error opening PCM device %s: %s", d, pa_alsa_strerror(err));
+ goto fail;
+ }
+ pa_log_info("ALSA device open '%s' %s: %p", d,
+ mode == SND_PCM_STREAM_CAPTURE ? "capture" : "playback", pcm_handle);
+
+ if ((err = pa_alsa_set_hw_params(
+ pcm_handle,
+ ss,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ require_exact_channel_number)) < 0) {
+
+ if (!reformat) {
+ reformat = true;
+
+ pa_alsa_close(&pcm_handle);
+ continue;
+ }
+
+ /* Hmm, some hw is very exotic, so we retry with plug, if without it didn't work */
+ if (!pa_startswith(d, "plug:") && !pa_startswith(d, "plughw:")) {
+ char *t;
+
+ t = pa_sprintf_malloc("plug:SLAVE='%s'", d);
+ pa_xfree(d);
+ d = t;
+
+ reformat = false;
+
+ pa_alsa_close(&pcm_handle);
+ continue;
+ }
+
+ pa_log_info("Failed to set hardware parameters on %s: %s", d, pa_alsa_strerror(err));
+ pa_alsa_close(&pcm_handle);
+
+ goto fail;
+ }
+
+ if (ss->channels > PA_CHANNELS_MAX) {
+ pa_log("Device %s has %u channels, but PulseAudio supports only %u channels. Unable to use the device.",
+ d, ss->channels, PA_CHANNELS_MAX);
+ pa_alsa_close(&pcm_handle);
+ goto fail;
+ }
+
+ if (dev)
+ *dev = d;
+ else
+ pa_xfree(d);
+
+ if (ss->channels != map->channels)
+ pa_channel_map_init_extend(map, ss->channels, PA_CHANNEL_MAP_ALSA);
+
+ return pcm_handle;
+ }
+
+fail:
+ pa_xfree(d);
+
+ return NULL;
+}
+
+snd_pcm_t *pa_alsa_open_by_template(
+ char **template,
+ const char *dev_id,
+ char **dev,
+ pa_sample_spec *ss,
+ pa_channel_map* map,
+ int mode,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ bool require_exact_channel_number) {
+
+ snd_pcm_t *pcm_handle;
+ char **i;
+
+ for (i = template; *i; i++) {
+ char *d;
+
+ d = pa_replace(*i, "%f", dev_id);
+
+ pcm_handle = pa_alsa_open_by_device_string(
+ d,
+ dev,
+ ss,
+ map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ require_exact_channel_number);
+
+ pa_xfree(d);
+
+ if (pcm_handle)
+ return pcm_handle;
+ }
+
+ return NULL;
+}
+
+void pa_alsa_dump(pa_log_level_t level, snd_pcm_t *pcm) {
+ int err;
+ snd_output_t *out;
+
+ pa_assert(pcm);
+
+ pa_assert_se(snd_output_buffer_open(&out) == 0);
+
+ if ((err = snd_pcm_dump(pcm, out)) < 0)
+ pa_logl(level, "snd_pcm_dump(): %s", pa_alsa_strerror(err));
+ else {
+ char *s = NULL;
+ snd_output_buffer_string(out, &s);
+ pa_logl(level, "snd_pcm_dump():\n%s", pa_strnull(s));
+ }
+
+ pa_assert_se(snd_output_close(out) == 0);
+}
+
+#if 0
+void pa_alsa_dump_status(snd_pcm_t *pcm) {
+ int err;
+ snd_output_t *out;
+ snd_pcm_status_t *status;
+ char *s = NULL;
+
+ pa_assert(pcm);
+
+ snd_pcm_status_alloca(&status);
+
+ if ((err = snd_output_buffer_open(&out)) < 0) {
+ pa_log_debug("snd_output_buffer_open() failed: %s", pa_cstrerror(err));
+ return;
+ }
+
+ if ((err = snd_pcm_status(pcm, status)) < 0) {
+ pa_log_debug("snd_pcm_status() failed: %s", pa_cstrerror(err));
+ goto finish;
+ }
+
+ if ((err = snd_pcm_status_dump(status, out)) < 0) {
+ pa_log_debug("snd_pcm_status_dump(): %s", pa_alsa_strerror(err));
+ goto finish;
+ }
+
+ snd_output_buffer_string(out, &s);
+ pa_log_debug("snd_pcm_status_dump():\n%s", pa_strnull(s));
+
+finish:
+
+ snd_output_close(out);
+}
+#endif
+
+static PA_PRINTF_FUNC(5,6) void alsa_error_handler(const char *file, int line, const char *function, int err, const char *fmt,...) {
+ va_list ap;
+// char *alsa_file;
+
+// alsa_file = pa_sprintf_malloc("(alsa-lib)%s", file);
+
+ va_start(ap, fmt);
+
+ pa_log_levelv_meta(PA_LOG_INFO, file, line, function, fmt, ap);
+
+ va_end(ap);
+
+// pa_xfree(alsa_file);
+}
+
+static int n_error_handler_installed = 0;
+
+typedef void (*snd_lib2_error_handler_t)(const char *file, int line, const char *function, int err, const char *fmt, ...) PA_PRINTF_FUNC(5,6) /* __attribute__ ((format (printf, 5, 6))) */;
+
+extern int snd_lib_error_set_handler(snd_lib2_error_handler_t handler);
+
+void pa_alsa_refcnt_inc(void) {
+ /* This is not really thread safe, but we do our best */
+ if (n_error_handler_installed++ == 0)
+ snd_lib_error_set_handler(alsa_error_handler);
+}
+
+void pa_alsa_refcnt_dec(void) {
+ int r;
+
+ pa_assert_se((r = n_error_handler_installed--) >= 1);
+
+ if (r == 1) {
+ snd_lib_error_set_handler(NULL);
+ snd_config_update_free_global();
+ }
+}
+
+bool pa_alsa_init_description(pa_proplist *p, pa_card *card) {
+ const char *d, *k;
+ pa_assert(p);
+
+ if (pa_alsa_device_init_description(p, card))
+ return true;
+
+ if (!(d = pa_proplist_gets(p, "alsa.card_name")))
+ d = pa_proplist_gets(p, "alsa.name");
+
+ if (!d)
+ return false;
+
+ k = pa_proplist_gets(p, PA_PROP_DEVICE_PROFILE_DESCRIPTION);
+
+ if (d && k)
+ pa_proplist_setf(p, PA_PROP_DEVICE_DESCRIPTION, "%s %s", d, k);
+ else if (d)
+ pa_proplist_sets(p, PA_PROP_DEVICE_DESCRIPTION, d);
+
+ return false;
+}
+
+void pa_alsa_init_proplist_card(pa_core *c, pa_proplist *p, int card) {
+ char *cn, *lcn, *dn;
+
+ pa_assert(p);
+ pa_assert(card >= 0);
+
+ pa_proplist_setf(p, "alsa.card", "%i", card);
+
+ if (snd_card_get_name(card, &cn) >= 0) {
+ pa_proplist_sets(p, "alsa.card_name", pa_strip(cn));
+ free(cn);
+ }
+
+ if (snd_card_get_longname(card, &lcn) >= 0) {
+ pa_proplist_sets(p, "alsa.long_card_name", pa_strip(lcn));
+ free(lcn);
+ }
+
+ if ((dn = pa_alsa_get_driver_name(card))) {
+ pa_proplist_sets(p, "alsa.driver_name", dn);
+ pa_xfree(dn);
+ }
+
+#ifdef HAVE_UDEV
+ pa_udev_get_info(card, p);
+#endif
+}
+
+void pa_alsa_init_proplist_pcm_info(pa_core *c, pa_proplist *p, snd_pcm_info_t *pcm_info) {
+
+ static const char * const alsa_class_table[SND_PCM_CLASS_LAST+1] = {
+ [SND_PCM_CLASS_GENERIC] = "generic",
+ [SND_PCM_CLASS_MULTI] = "multi",
+ [SND_PCM_CLASS_MODEM] = "modem",
+ [SND_PCM_CLASS_DIGITIZER] = "digitizer"
+ };
+ static const char * const class_table[SND_PCM_CLASS_LAST+1] = {
+ [SND_PCM_CLASS_GENERIC] = "sound",
+ [SND_PCM_CLASS_MULTI] = NULL,
+ [SND_PCM_CLASS_MODEM] = "modem",
+ [SND_PCM_CLASS_DIGITIZER] = NULL
+ };
+ static const char * const alsa_subclass_table[SND_PCM_SUBCLASS_LAST+1] = {
+ [SND_PCM_SUBCLASS_GENERIC_MIX] = "generic-mix",
+ [SND_PCM_SUBCLASS_MULTI_MIX] = "multi-mix"
+ };
+
+ snd_pcm_class_t class;
+ snd_pcm_subclass_t subclass;
+ const char *n, *id, *sdn;
+ int card;
+
+ pa_assert(p);
+ pa_assert(pcm_info);
+
+ pa_proplist_sets(p, PA_PROP_DEVICE_API, "alsa");
+
+ if ((class = snd_pcm_info_get_class(pcm_info)) <= SND_PCM_CLASS_LAST) {
+ if (class_table[class])
+ pa_proplist_sets(p, PA_PROP_DEVICE_CLASS, class_table[class]);
+ if (alsa_class_table[class])
+ pa_proplist_sets(p, "alsa.class", alsa_class_table[class]);
+ }
+
+ if ((subclass = snd_pcm_info_get_subclass(pcm_info)) <= SND_PCM_SUBCLASS_LAST)
+ if (alsa_subclass_table[subclass])
+ pa_proplist_sets(p, "alsa.subclass", alsa_subclass_table[subclass]);
+
+ if ((n = snd_pcm_info_get_name(pcm_info))) {
+ char *t = pa_xstrdup(n);
+ pa_proplist_sets(p, "alsa.name", pa_strip(t));
+ pa_xfree(t);
+ }
+
+ if ((id = snd_pcm_info_get_id(pcm_info)))
+ pa_proplist_sets(p, "alsa.id", id);
+
+ pa_proplist_setf(p, "alsa.subdevice", "%u", snd_pcm_info_get_subdevice(pcm_info));
+ if ((sdn = snd_pcm_info_get_subdevice_name(pcm_info)))
+ pa_proplist_sets(p, "alsa.subdevice_name", sdn);
+
+ pa_proplist_setf(p, "alsa.device", "%u", snd_pcm_info_get_device(pcm_info));
+
+ if ((card = snd_pcm_info_get_card(pcm_info)) >= 0)
+ pa_alsa_init_proplist_card(c, p, card);
+}
+
+void pa_alsa_init_proplist_pcm(pa_core *c, pa_proplist *p, snd_pcm_t *pcm) {
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_info_t *info;
+ int bits, err;
+
+ snd_pcm_hw_params_alloca(&hwparams);
+ snd_pcm_info_alloca(&info);
+
+ if ((err = snd_pcm_hw_params_current(pcm, hwparams)) < 0)
+ pa_log_warn("Error fetching hardware parameter info: %s", pa_alsa_strerror(err));
+ else {
+
+ if ((bits = snd_pcm_hw_params_get_sbits(hwparams)) >= 0)
+ pa_proplist_setf(p, "alsa.resolution_bits", "%i", bits);
+ }
+
+ if ((err = snd_pcm_info(pcm, info)) < 0)
+ pa_log_warn("Error fetching PCM info: %s", pa_alsa_strerror(err));
+ else
+ pa_alsa_init_proplist_pcm_info(c, p, info);
+}
+
+#if 0
+void pa_alsa_init_proplist_ctl(pa_proplist *p, const char *name) {
+ int err;
+ snd_ctl_t *ctl;
+ snd_ctl_card_info_t *info;
+ const char *t;
+
+ pa_assert(p);
+
+ snd_ctl_card_info_alloca(&info);
+
+ if ((err = snd_ctl_open(&ctl, name, 0)) < 0) {
+ pa_log_warn("Error opening low-level control device '%s': %s", name, snd_strerror(err));
+ return;
+ }
+
+ if ((err = snd_ctl_card_info(ctl, info)) < 0) {
+ pa_log_warn("Control device %s card info: %s", name, snd_strerror(err));
+ snd_ctl_close(ctl);
+ return;
+ }
+
+ if ((t = snd_ctl_card_info_get_mixername(info)) && *t)
+ pa_proplist_sets(p, "alsa.mixer_name", t);
+
+ if ((t = snd_ctl_card_info_get_components(info)) && *t)
+ pa_proplist_sets(p, "alsa.components", t);
+
+ snd_ctl_close(ctl);
+}
+
+int pa_alsa_recover_from_poll(snd_pcm_t *pcm, int revents) {
+ snd_pcm_state_t state;
+ snd_pcm_hw_params_t *hwparams;
+ int err;
+
+ pa_assert(pcm);
+
+ if (revents & POLLERR)
+ pa_log_debug("Got POLLERR from ALSA");
+ if (revents & POLLNVAL)
+ pa_log_warn("Got POLLNVAL from ALSA");
+ if (revents & POLLHUP)
+ pa_log_warn("Got POLLHUP from ALSA");
+ if (revents & POLLPRI)
+ pa_log_warn("Got POLLPRI from ALSA");
+ if (revents & POLLIN)
+ pa_log_debug("Got POLLIN from ALSA");
+ if (revents & POLLOUT)
+ pa_log_debug("Got POLLOUT from ALSA");
+
+ state = snd_pcm_state(pcm);
+ pa_log_debug("PCM state is %s", snd_pcm_state_name(state));
+
+ /* Try to recover from this error */
+
+ switch (state) {
+
+ case SND_PCM_STATE_DISCONNECTED:
+ /* Do not try to recover */
+ pa_log_info("Device disconnected.");
+ return -1;
+
+ case SND_PCM_STATE_XRUN:
+ if ((err = snd_pcm_recover(pcm, -EPIPE, 1)) != 0) {
+ pa_log_warn("Could not recover from POLLERR|POLLNVAL|POLLHUP and XRUN: %s", pa_alsa_strerror(err));
+ return -1;
+ }
+ break;
+
+ case SND_PCM_STATE_SUSPENDED:
+ snd_pcm_hw_params_alloca(&hwparams);
+
+ if ((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(err));
+ return -1;
+ }
+
+ if (snd_pcm_hw_params_can_resume(hwparams)) {
+ /* Retry resume 3 times before giving up, then fallback to restarting the stream. */
+ for (int i = 0; i < 3; i++) {
+ if ((err = snd_pcm_resume(pcm)) == 0)
+ return 0;
+ if (err != -EAGAIN)
+ break;
+ pa_msleep(25);
+ }
+ pa_log_warn("Could not recover alsa device from SUSPENDED state, trying to restart PCM");
+ }
+ /* Fall through */
+
+ default:
+
+ snd_pcm_drop(pcm);
+ return 1;
+ }
+
+ return 0;
+}
+
+pa_rtpoll_item* pa_alsa_build_pollfd(snd_pcm_t *pcm, pa_rtpoll *rtpoll) {
+ int n, err;
+ struct pollfd *pollfd;
+ pa_rtpoll_item *item;
+
+ pa_assert(pcm);
+
+ if ((n = snd_pcm_poll_descriptors_count(pcm)) < 0) {
+ pa_log("snd_pcm_poll_descriptors_count() failed: %s", pa_alsa_strerror(n));
+ return NULL;
+ }
+
+ item = pa_rtpoll_item_new(rtpoll, PA_RTPOLL_NEVER, (unsigned) n);
+ pollfd = pa_rtpoll_item_get_pollfd(item, NULL);
+
+ if ((err = snd_pcm_poll_descriptors(pcm, pollfd, (unsigned) n)) < 0) {
+ pa_log("snd_pcm_poll_descriptors() failed: %s", pa_alsa_strerror(err));
+ pa_rtpoll_item_free(item);
+ return NULL;
+ }
+
+ return item;
+}
+
+snd_pcm_sframes_t pa_alsa_safe_avail(snd_pcm_t *pcm, size_t hwbuf_size, const pa_sample_spec *ss) {
+ snd_pcm_sframes_t n;
+ size_t k;
+
+ pa_assert(pcm);
+ pa_assert(hwbuf_size > 0);
+ pa_assert(ss);
+
+ /* Some ALSA driver expose weird bugs, let's inform the user about
+ * what is going on */
+
+ n = snd_pcm_avail(pcm);
+
+ if (n <= 0)
+ return n;
+
+ k = (size_t) n * pa_frame_size(ss);
+
+ if (PA_UNLIKELY(k >= hwbuf_size * 5 ||
+ k >= pa_bytes_per_second(ss)*10)) {
+
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log_debug(ngettext("snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ "snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ (unsigned long) k),
+ (unsigned long) k,
+ (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC),
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_DEBUG, pcm);
+ } PA_ONCE_END;
+
+ /* Mhmm, let's try not to fail completely */
+ n = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss));
+ }
+
+ return n;
+}
+
+int pa_alsa_safe_delay(snd_pcm_t *pcm, snd_pcm_status_t *status, snd_pcm_sframes_t *delay, size_t hwbuf_size, const pa_sample_spec *ss,
+ bool capture) {
+ ssize_t k;
+ size_t abs_k;
+ int err;
+ snd_pcm_sframes_t avail = 0;
+#if (SND_LIB_VERSION >= ((1<<16)|(1<<8)|0)) /* API additions in 1.1.0 */
+ snd_pcm_audio_tstamp_config_t tstamp_config;
+#endif
+
+ pa_assert(pcm);
+ pa_assert(delay);
+ pa_assert(hwbuf_size > 0);
+ pa_assert(ss);
+
+ /* Some ALSA driver expose weird bugs, let's inform the user about
+ * what is going on. We're going to get both the avail and delay values so
+ * that we can compare and check them for capture.
+ * This is done with snd_pcm_status() which provides
+ * avail, delay and timestamp values in a single kernel call to improve
+ * timer-based scheduling */
+
+#if (SND_LIB_VERSION >= ((1<<16)|(1<<8)|0)) /* API additions in 1.1.0 */
+
+ /* The time stamp configuration needs to be set so that the
+ * ALSA code will use the internal delay reported by the driver.
+ * The time stamp configuration was introduced in alsa version 1.1.0. */
+ tstamp_config.type_requested = 1; /* ALSA default time stamp type */
+ tstamp_config.report_delay = 1;
+ snd_pcm_status_set_audio_htstamp_config(status, &tstamp_config);
+#endif
+
+ if ((err = snd_pcm_status(pcm, status)) < 0)
+ return err;
+
+ avail = snd_pcm_status_get_avail(status);
+ *delay = snd_pcm_status_get_delay(status);
+
+ k = (ssize_t) *delay * (ssize_t) pa_frame_size(ss);
+
+ abs_k = k >= 0 ? (size_t) k : (size_t) -k;
+
+ if (PA_UNLIKELY(abs_k >= hwbuf_size * 5 ||
+ abs_k >= pa_bytes_per_second(ss)*10)) {
+
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log_debug(ngettext("snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ "snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ (signed long) k),
+ (signed long) k,
+ k < 0 ? "-" : "",
+ (unsigned long) (pa_bytes_to_usec(abs_k, ss) / PA_USEC_PER_MSEC),
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_DEBUG, pcm);
+ } PA_ONCE_END;
+
+ /* Mhmm, let's try not to fail completely */
+ if (k < 0)
+ *delay = -(snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss));
+ else
+ *delay = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss));
+ }
+
+ if (capture) {
+ abs_k = (size_t) avail * pa_frame_size(ss);
+
+ if (PA_UNLIKELY(abs_k >= hwbuf_size * 5 ||
+ abs_k >= pa_bytes_per_second(ss)*10)) {
+
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log_debug(ngettext("snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ "snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ (unsigned long) k),
+ (unsigned long) k,
+ (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC),
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_DEBUG, pcm);
+ } PA_ONCE_END;
+
+ /* Mhmm, let's try not to fail completely */
+ avail = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss));
+ }
+
+ if (PA_UNLIKELY(*delay < avail)) {
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log(_("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."),
+ (unsigned long) *delay,
+ (unsigned long) avail,
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_ERROR, pcm);
+ } PA_ONCE_END;
+
+ /* try to fixup */
+ *delay = avail;
+ }
+ }
+
+ return 0;
+}
+
+int pa_alsa_safe_mmap_begin(snd_pcm_t *pcm, const snd_pcm_channel_area_t **areas, snd_pcm_uframes_t *offset, snd_pcm_uframes_t *frames, size_t hwbuf_size, const pa_sample_spec *ss) {
+ int r;
+ snd_pcm_uframes_t before;
+ size_t k;
+
+ pa_assert(pcm);
+ pa_assert(areas);
+ pa_assert(offset);
+ pa_assert(frames);
+ pa_assert(hwbuf_size > 0);
+ pa_assert(ss);
+
+ before = *frames;
+
+ r = snd_pcm_mmap_begin(pcm, areas, offset, frames);
+
+ if (r < 0)
+ return r;
+
+ k = (size_t) *frames * pa_frame_size(ss);
+
+ if (PA_UNLIKELY(*frames > before ||
+ k >= hwbuf_size * 3 ||
+ k >= pa_bytes_per_second(ss)*10))
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log_debug(ngettext("snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ (unsigned long) k),
+ (unsigned long) k,
+ (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC),
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_DEBUG, pcm);
+ } PA_ONCE_END;
+
+ return r;
+}
+#endif
+
+char *pa_alsa_get_driver_name(int card) {
+ char *t, *m, *n;
+
+ pa_assert(card >= 0);
+
+ t = pa_sprintf_malloc("/sys/class/sound/card%i/device/driver/module", card);
+ m = pa_readlink(t);
+ pa_xfree(t);
+
+ if (!m)
+ return NULL;
+
+ n = pa_xstrdup(pa_path_get_filename(m));
+ pa_xfree(m);
+
+ return n;
+}
+
+char *pa_alsa_get_driver_name_by_pcm(snd_pcm_t *pcm) {
+ int card;
+ snd_pcm_info_t* info;
+ snd_pcm_info_alloca(&info);
+
+ pa_assert(pcm);
+
+ if (snd_pcm_info(pcm, info) < 0)
+ return NULL;
+
+ if ((card = snd_pcm_info_get_card(info)) < 0)
+ return NULL;
+
+ return pa_alsa_get_driver_name(card);
+}
+
+#if 0
+char *pa_alsa_get_reserve_name(const char *device) {
+ const char *t;
+ int i;
+
+ pa_assert(device);
+
+ if ((t = strchr(device, ':')))
+ device = t+1;
+
+ if ((i = snd_card_get_index(device)) < 0) {
+ int32_t k;
+
+ if (pa_atoi(device, &k) < 0)
+ return NULL;
+
+ i = (int) k;
+ }
+
+ return pa_sprintf_malloc("Audio%i", i);
+}
+
+unsigned int *pa_alsa_get_supported_rates(snd_pcm_t *pcm, unsigned int fallback_rate) {
+ static unsigned int all_rates[] = { 8000, 11025, 12000,
+ 16000, 22050, 24000,
+ 32000, 44100, 48000,
+ 64000, 88200, 96000,
+ 128000, 176400, 192000,
+ 384000 };
+ bool supported[PA_ELEMENTSOF(all_rates)] = { false, };
+ snd_pcm_hw_params_t *hwparams;
+ unsigned int i, j, n, *rates = NULL;
+ int ret;
+
+ snd_pcm_hw_params_alloca(&hwparams);
+
+ if ((ret = snd_pcm_hw_params_any(pcm, hwparams)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(ret));
+ return NULL;
+ }
+
+ for (i = 0, n = 0; i < PA_ELEMENTSOF(all_rates); i++) {
+ if (snd_pcm_hw_params_test_rate(pcm, hwparams, all_rates[i], 0) == 0) {
+ supported[i] = true;
+ n++;
+ }
+ }
+
+ if (n > 0) {
+ rates = pa_xnew(unsigned int, n + 1);
+
+ for (i = 0, j = 0; i < PA_ELEMENTSOF(all_rates); i++) {
+ if (supported[i])
+ rates[j++] = all_rates[i];
+ }
+
+ rates[j] = 0;
+ } else {
+ rates = pa_xnew(unsigned int, 2);
+
+ rates[0] = fallback_rate;
+ if ((ret = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rates[0], NULL)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_rate_near() failed: %s", pa_alsa_strerror(ret));
+ pa_xfree(rates);
+ return NULL;
+ }
+
+ rates[1] = 0;
+ }
+
+ return rates;
+}
+
+pa_sample_format_t *pa_alsa_get_supported_formats(snd_pcm_t *pcm, pa_sample_format_t fallback_format) {
+ static const snd_pcm_format_t format_trans_to_pa[] = {
+ [SND_PCM_FORMAT_U8] = PA_SAMPLE_U8,
+ [SND_PCM_FORMAT_A_LAW] = PA_SAMPLE_ALAW,
+ [SND_PCM_FORMAT_MU_LAW] = PA_SAMPLE_ULAW,
+ [SND_PCM_FORMAT_S16_LE] = PA_SAMPLE_S16LE,
+ [SND_PCM_FORMAT_S16_BE] = PA_SAMPLE_S16BE,
+ [SND_PCM_FORMAT_FLOAT_LE] = PA_SAMPLE_FLOAT32LE,
+ [SND_PCM_FORMAT_FLOAT_BE] = PA_SAMPLE_FLOAT32BE,
+ [SND_PCM_FORMAT_S32_LE] = PA_SAMPLE_S32LE,
+ [SND_PCM_FORMAT_S32_BE] = PA_SAMPLE_S32BE,
+ [SND_PCM_FORMAT_S24_3LE] = PA_SAMPLE_S24LE,
+ [SND_PCM_FORMAT_S24_3BE] = PA_SAMPLE_S24BE,
+ [SND_PCM_FORMAT_S24_LE] = PA_SAMPLE_S24_32LE,
+ [SND_PCM_FORMAT_S24_BE] = PA_SAMPLE_S24_32BE,
+ };
+ static const snd_pcm_format_t all_formats[] = {
+ SND_PCM_FORMAT_U8,
+ SND_PCM_FORMAT_A_LAW,
+ SND_PCM_FORMAT_MU_LAW,
+ SND_PCM_FORMAT_S16_LE,
+ SND_PCM_FORMAT_S16_BE,
+ SND_PCM_FORMAT_FLOAT_LE,
+ SND_PCM_FORMAT_FLOAT_BE,
+ SND_PCM_FORMAT_S32_LE,
+ SND_PCM_FORMAT_S32_BE,
+ SND_PCM_FORMAT_S24_3LE,
+ SND_PCM_FORMAT_S24_3BE,
+ SND_PCM_FORMAT_S24_LE,
+ SND_PCM_FORMAT_S24_BE,
+ };
+ bool supported[PA_ELEMENTSOF(all_formats)] = {
+ false,
+ };
+ snd_pcm_hw_params_t *hwparams;
+ unsigned int i, j, n;
+ pa_sample_format_t *formats = NULL;
+ int ret;
+
+ snd_pcm_hw_params_alloca(&hwparams);
+
+ if ((ret = snd_pcm_hw_params_any(pcm, hwparams)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(ret));
+ return NULL;
+ }
+
+ for (i = 0, n = 0; i < PA_ELEMENTSOF(all_formats); i++) {
+ if (snd_pcm_hw_params_test_format(pcm, hwparams, all_formats[i]) == 0) {
+ supported[i] = true;
+ n++;
+ }
+ }
+
+ if (n > 0) {
+ formats = pa_xnew(pa_sample_format_t, n + 1);
+
+ for (i = 0, j = 0; i < PA_ELEMENTSOF(all_formats); i++) {
+ if (supported[i])
+ formats[j++] = format_trans_to_pa[all_formats[i]];
+ }
+
+ formats[j] = PA_SAMPLE_MAX;
+ } else {
+ formats = pa_xnew(pa_sample_format_t, 2);
+
+ formats[0] = fallback_format;
+ if ((ret = snd_pcm_hw_params_set_format(pcm, hwparams, format_trans_to_pa[formats[0]])) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_format() failed: %s", pa_alsa_strerror(ret));
+ pa_xfree(formats);
+ return NULL;
+ }
+
+ formats[1] = PA_SAMPLE_MAX;
+ }
+
+ return formats;
+}
+#endif
+
+bool pa_alsa_pcm_is_hw(snd_pcm_t *pcm) {
+ snd_pcm_info_t* info;
+ snd_pcm_info_alloca(&info);
+
+ pa_assert(pcm);
+
+ if (snd_pcm_info(pcm, info) < 0)
+ return false;
+
+ return snd_pcm_info_get_card(info) >= 0;
+}
+
+bool pa_alsa_pcm_is_modem(snd_pcm_t *pcm) {
+ snd_pcm_info_t* info;
+ snd_pcm_info_alloca(&info);
+
+ pa_assert(pcm);
+
+ if (snd_pcm_info(pcm, info) < 0)
+ return false;
+
+ return snd_pcm_info_get_class(info) == SND_PCM_CLASS_MODEM;
+}
+
+const char* pa_alsa_strerror(int errnum) {
+ return snd_strerror(errnum);
+}
+
+#if 0
+bool pa_alsa_may_tsched(bool want) {
+
+ if (!want)
+ return false;
+
+ if (!pa_rtclock_hrtimer()) {
+ /* We cannot depend on being woken up in time when the timers
+ are inaccurate, so let's fallback to classic IO based playback
+ then. */
+ pa_log_notice("Disabling timer-based scheduling because high-resolution timers are not available from the kernel.");
+ return false; }
+
+ if (pa_running_in_vm()) {
+ /* We cannot depend on being woken up when we ask for in a VM,
+ * so let's fallback to classic IO based playback then. */
+ pa_log_notice("Disabling timer-based scheduling because running inside a VM.");
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+#define SND_MIXER_ELEM_PULSEAUDIO (SND_MIXER_ELEM_LAST + 10)
+
+static snd_mixer_elem_t *pa_alsa_mixer_find(snd_mixer_t *mixer,
+ snd_ctl_elem_iface_t iface,
+ const char *name,
+ unsigned int index,
+ unsigned int device) {
+ snd_mixer_elem_t *elem;
+
+ for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) {
+ snd_hctl_elem_t *helem;
+ if (snd_mixer_elem_get_type(elem) != SND_MIXER_ELEM_PULSEAUDIO)
+ continue;
+ helem = snd_mixer_elem_get_private(elem);
+ if (snd_hctl_elem_get_interface(helem) != iface)
+ continue;
+ if (!pa_streq(snd_hctl_elem_get_name(helem), name))
+ continue;
+ if (snd_hctl_elem_get_index(helem) != index)
+ continue;
+ if (snd_hctl_elem_get_device(helem) != device)
+ continue;
+ return elem;
+ }
+ return NULL;
+}
+
+snd_mixer_elem_t *pa_alsa_mixer_find_card(snd_mixer_t *mixer, struct pa_alsa_mixer_id *alsa_id, unsigned int device) {
+ return pa_alsa_mixer_find(mixer, SND_CTL_ELEM_IFACE_CARD, alsa_id->name, alsa_id->index, device);
+}
+
+snd_mixer_elem_t *pa_alsa_mixer_find_pcm(snd_mixer_t *mixer, const char *name, unsigned int device) {
+ return pa_alsa_mixer_find(mixer, SND_CTL_ELEM_IFACE_PCM, name, 0, device);
+}
+
+static int mixer_class_compare(const snd_mixer_elem_t *c1, const snd_mixer_elem_t *c2)
+{
+ /* Dummy compare function */
+ return c1 == c2 ? 0 : (c1 > c2 ? 1 : -1);
+}
+
+static int mixer_class_event(snd_mixer_class_t *class, unsigned int mask,
+ snd_hctl_elem_t *helem, snd_mixer_elem_t *melem)
+{
+ int err;
+ const char *name = snd_hctl_elem_get_name(helem);
+ // NOTE: The remove event defined as '~0U`.
+ if (mask == SND_CTL_EVENT_MASK_REMOVE) {
+ // NOTE: unless remove pointer to melem from link-list at private_data of helem, hits
+ // assersion in alsa-lib since the list is not empty.
+ snd_mixer_elem_detach(melem, helem);
+ } else if (mask & SND_CTL_EVENT_MASK_ADD) {
+ snd_ctl_elem_iface_t iface = snd_hctl_elem_get_interface(helem);
+ if (iface == SND_CTL_ELEM_IFACE_CARD || iface == SND_CTL_ELEM_IFACE_PCM) {
+ snd_mixer_elem_t *new_melem;
+
+ /* Put the hctl pointer as our private data - it will be useful for callbacks */
+ if ((err = snd_mixer_elem_new(&new_melem, SND_MIXER_ELEM_PULSEAUDIO, 0, helem, NULL)) < 0) {
+ pa_log_warn("snd_mixer_elem_new failed: %s", pa_alsa_strerror(err));
+ return 0;
+ }
+
+ if ((err = snd_mixer_elem_attach(new_melem, helem)) < 0) {
+ pa_log_warn("snd_mixer_elem_attach failed: %s", pa_alsa_strerror(err));
+ snd_mixer_elem_free(melem);
+ return 0;
+ }
+
+ if ((err = snd_mixer_elem_add(new_melem, class)) < 0) {
+ pa_log_warn("snd_mixer_elem_add failed: %s", pa_alsa_strerror(err));
+ return 0;
+ }
+ }
+ }
+ else if (mask & SND_CTL_EVENT_MASK_VALUE) {
+ snd_mixer_elem_value(melem); /* Calls the element callback */
+ return 0;
+ }
+ else
+ pa_log_info("Got an unknown mixer class event for %s: mask 0x%x", name, mask);
+
+ return 0;
+}
+
+static int prepare_mixer(snd_mixer_t *mixer, const char *dev, snd_hctl_t *hctl) {
+ int err;
+ snd_mixer_class_t *class;
+
+ pa_assert(mixer);
+ pa_assert(dev);
+
+ if ((err = snd_mixer_attach_hctl(mixer, hctl)) < 0) {
+ pa_log_info("Unable to attach to mixer %s: %s", dev, pa_alsa_strerror(err));
+ return -1;
+ }
+
+ if (snd_mixer_class_malloc(&class)) {
+ pa_log_info("Failed to allocate mixer class for %s", dev);
+ return -1;
+ }
+ snd_mixer_class_set_event(class, mixer_class_event);
+ snd_mixer_class_set_compare(class, mixer_class_compare);
+ if ((err = snd_mixer_class_register(class, mixer)) < 0) {
+ pa_log_info("Unable register mixer class for %s: %s", dev, pa_alsa_strerror(err));
+ snd_mixer_class_free(class);
+ return -1;
+ }
+ /* From here on, the mixer class is deallocated by alsa on snd_mixer_close/free. */
+
+ if ((err = snd_mixer_selem_register(mixer, NULL, NULL)) < 0) {
+ pa_log_warn("Unable to register mixer: %s", pa_alsa_strerror(err));
+ return -1;
+ }
+
+ if ((err = snd_mixer_load(mixer)) < 0) {
+ pa_log_warn("Unable to load mixer: %s", pa_alsa_strerror(err));
+ return -1;
+ }
+
+ pa_log_info("Successfully attached to mixer '%s'", dev);
+ return 0;
+}
+
+snd_mixer_t *pa_alsa_open_mixer(pa_hashmap *mixers, int alsa_card_index, bool probe) {
+ char *md = pa_sprintf_malloc("hw:%i", alsa_card_index);
+ snd_mixer_t *m = pa_alsa_open_mixer_by_name(mixers, md, probe);
+ pa_xfree(md);
+ return m;
+}
+
+pa_alsa_mixer *pa_alsa_create_mixer(pa_hashmap *mixers, const char *dev, snd_mixer_t *m, bool probe) {
+ pa_alsa_mixer *pm;
+
+ pm = pa_xnew0(pa_alsa_mixer, 1);
+ if (pm == NULL)
+ return NULL;
+
+ pm->used_for_probe_only = probe;
+ pm->mixer_handle = m;
+ pa_hashmap_put(mixers, pa_xstrdup(dev), pm);
+ return pm;
+}
+
+snd_mixer_t *pa_alsa_open_mixer_by_name(pa_hashmap *mixers, const char *dev, bool probe) {
+ int err;
+ snd_mixer_t *m;
+ snd_hctl_t *hctl;
+ pa_alsa_mixer *pm;
+
+ pa_assert(mixers);
+ pa_assert(dev);
+
+ pm = pa_hashmap_get(mixers, dev);
+ if (pm) {
+ if (!probe)
+ pm->used_for_probe_only = false;
+ return pm->mixer_handle;
+ }
+
+ if ((err = snd_mixer_open(&m, 0)) < 0) {
+ pa_log("Error opening mixer: %s", pa_alsa_strerror(err));
+ return NULL;
+ }
+
+ err = snd_hctl_open(&hctl, dev, 0);
+ if (err < 0) {
+ pa_log("Error opening hctl device: %s", pa_alsa_strerror(err));
+ goto __close;
+ }
+
+ if (prepare_mixer(m, dev, hctl) >= 0) {
+ /* get the ALSA card number (index) and ID (alias) and create two identical mixers */
+ char *p, *dev2, *dev_idx, *dev_id;
+ snd_ctl_card_info_t *info;
+ snd_ctl_card_info_alloca(&info);
+ err = snd_ctl_card_info(snd_hctl_ctl(hctl), info);
+ if (err < 0)
+ goto __std;
+ dev2 = pa_xstrdup(dev);
+ if (dev2 == NULL)
+ goto __close;
+ p = strchr(dev2, ':');
+ /* sanity check - only hw: devices */
+ if (p == NULL || (p - dev2) < 2 || !pa_strneq(p - 2, "hw:", 3)) {
+ pa_xfree(dev2);
+ goto __std;
+ }
+ *p = '\0';
+ dev_idx = pa_sprintf_malloc("%s:%u", dev2, snd_ctl_card_info_get_card(info));
+ dev_id = pa_sprintf_malloc("%s:%s", dev2, snd_ctl_card_info_get_id(info));
+ pa_log_debug("ALSA alias mixers: %s = %s", dev_idx, dev_id);
+ if (dev_idx && dev_id && (strcmp(dev, dev_idx) == 0 || strcmp(dev, dev_id) == 0)) {
+ pm = pa_alsa_create_mixer(mixers, dev_idx, m, probe);
+ if (pm) {
+ pa_alsa_mixer *pm2;
+ pm2 = pa_alsa_create_mixer(mixers, dev_id, m, probe);
+ if (pm2) {
+ pm->alias = pm2;
+ pm2->alias = pm;
+ }
+ }
+ }
+ pa_xfree(dev_id);
+ pa_xfree(dev_idx);
+ pa_xfree(dev2);
+ __std:
+ if (pm == NULL)
+ pm = pa_alsa_create_mixer(mixers, dev, m, probe);
+ if (pm)
+ return m;
+ }
+
+__close:
+ snd_mixer_close(m);
+ return NULL;
+}
+
+snd_mixer_t *pa_alsa_open_mixer_for_pcm(pa_hashmap *mixers, snd_pcm_t *pcm, bool probe) {
+ snd_pcm_info_t* info;
+ snd_pcm_info_alloca(&info);
+
+ pa_assert(pcm);
+
+ if (snd_pcm_info(pcm, info) >= 0) {
+ int card_idx;
+
+ if ((card_idx = snd_pcm_info_get_card(info)) >= 0)
+ return pa_alsa_open_mixer(mixers, card_idx, probe);
+ }
+
+ return NULL;
+}
+
+#if 0
+void pa_alsa_mixer_set_fdlist(pa_hashmap *mixers, snd_mixer_t *mixer_handle, pa_mainloop_api *ml)
+{
+ pa_alsa_mixer *pm;
+ void *state;
+
+ PA_HASHMAP_FOREACH(pm, mixers, state)
+ if (pm->mixer_handle == mixer_handle) {
+ pm->used_for_probe_only = false;
+ if (!pm->fdl) {
+ pm->fdl = pa_alsa_fdlist_new();
+ if (pm->fdl)
+ pa_alsa_fdlist_set_handle(pm->fdl, pm->mixer_handle, NULL, ml);
+ }
+ }
+}
+#endif
+
+void pa_alsa_mixer_free(pa_alsa_mixer *mixer)
+{
+ if (mixer->mixer_handle && mixer->alias == NULL)
+ snd_mixer_close(mixer->mixer_handle);
+ if (mixer->alias)
+ mixer->alias->alias = NULL;
+ pa_xfree(mixer);
+}
+
+int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld) {
+
+ /* The ELD format is specific to HDA Intel sound cards and defined in the
+ HDA specification: http://www.intel.com/content/www/us/en/standards/high-definition-audio-specification.html */
+ int err;
+ snd_ctl_elem_info_t *info;
+ snd_ctl_elem_value_t *value;
+ uint8_t *elddata;
+ unsigned int eldsize, mnl;
+ unsigned int device;
+
+ pa_assert(eld != NULL);
+ pa_assert(elem != NULL);
+
+ /* Does it have any contents? */
+ snd_ctl_elem_info_alloca(&info);
+ snd_ctl_elem_value_alloca(&value);
+ if ((err = snd_hctl_elem_info(elem, info)) < 0 ||
+ (err = snd_hctl_elem_read(elem, value)) < 0) {
+ pa_log_warn("Accessing ELD control failed with error %s", snd_strerror(err));
+ return -1;
+ }
+
+ device = snd_hctl_elem_get_device(elem);
+ eldsize = snd_ctl_elem_info_get_count(info);
+ elddata = (unsigned char *) snd_ctl_elem_value_get_bytes(value);
+ if (elddata == NULL || eldsize == 0) {
+ pa_log_debug("ELD info empty (for device=%d)", device);
+ return -1;
+ }
+ if (eldsize < 20 || eldsize > 256) {
+ pa_log_debug("ELD info has wrong size (for device=%d)", device);
+ return -1;
+ }
+
+ /* Try to fetch monitor name */
+ mnl = elddata[4] & 0x1f;
+ if (mnl == 0 || mnl > 16 || 20 + mnl > eldsize) {
+ pa_log_debug("No monitor name in ELD info (for device=%d)", device);
+ mnl = 0;
+ }
+ memcpy(eld->monitor_name, &elddata[20], mnl);
+ eld->monitor_name[mnl] = '\0';
+ if (mnl)
+ pa_log_debug("Monitor name in ELD info is '%s' (for device=%d)", eld->monitor_name, device);
+
+ return 0;
+}
diff --git a/spa/plugins/alsa/acp/alsa-util.h b/spa/plugins/alsa/acp/alsa-util.h
new file mode 100644
index 0000000..b18b98d
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-util.h
@@ -0,0 +1,176 @@
+#ifndef fooalsautilhfoo
+#define fooalsautilhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+#include <stdbool.h>
+
+#include <alsa/asoundlib.h>
+
+#include "compat.h"
+
+#include "alsa-mixer.h"
+
+enum {
+ PA_ALSA_ERR_UNSPECIFIED = 1,
+ PA_ALSA_ERR_UCM_OPEN = 1000,
+ PA_ALSA_ERR_UCM_NO_VERB = 1001,
+ PA_ALSA_ERR_UCM_LINKED = 1002
+};
+
+int pa_alsa_set_hw_params(
+ snd_pcm_t *pcm_handle,
+ pa_sample_spec *ss, /* modified at return */
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ bool require_exact_channel_number);
+
+int pa_alsa_set_sw_params(
+ snd_pcm_t *pcm,
+ snd_pcm_uframes_t avail_min,
+ bool period_event);
+
+#if 0
+/* Picks a working mapping from the profile set based on the specified ss/map */
+snd_pcm_t *pa_alsa_open_by_device_id_auto(
+ const char *dev_id,
+ char **dev, /* modified at return */
+ pa_sample_spec *ss, /* modified at return */
+ pa_channel_map* map, /* modified at return */
+ int mode,
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ pa_alsa_profile_set *ps,
+ pa_alsa_mapping **mapping); /* modified at return */
+#endif
+
+/* Uses the specified mapping */
+snd_pcm_t *pa_alsa_open_by_device_id_mapping(
+ const char *dev_id,
+ char **dev, /* modified at return */
+ pa_sample_spec *ss, /* modified at return */
+ pa_channel_map* map, /* modified at return */
+ int mode,
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ pa_alsa_mapping *mapping);
+
+/* Opens the explicit ALSA device */
+snd_pcm_t *pa_alsa_open_by_device_string(
+ const char *dir,
+ char **dev, /* modified at return */
+ pa_sample_spec *ss, /* modified at return */
+ pa_channel_map* map, /* modified at return */
+ int mode,
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ bool require_exact_channel_number);
+
+/* Opens the explicit ALSA device with a fallback list */
+snd_pcm_t *pa_alsa_open_by_template(
+ char **template,
+ const char *dev_id,
+ char **dev, /* modified at return */
+ pa_sample_spec *ss, /* modified at return */
+ pa_channel_map* map, /* modified at return */
+ int mode,
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ bool require_exact_channel_number);
+
+#if 0
+void pa_alsa_dump(pa_log_level_t level, snd_pcm_t *pcm);
+void pa_alsa_dump_status(snd_pcm_t *pcm);
+#endif
+int pa_alsa_close(snd_pcm_t **pcm);
+
+void pa_alsa_refcnt_inc(void);
+void pa_alsa_refcnt_dec(void);
+
+void pa_alsa_init_proplist_pcm_info(pa_core *c, pa_proplist *p, snd_pcm_info_t *pcm_info);
+void pa_alsa_init_proplist_card(pa_core *c, pa_proplist *p, int card);
+void pa_alsa_init_proplist_pcm(pa_core *c, pa_proplist *p, snd_pcm_t *pcm);
+#if 0
+void pa_alsa_init_proplist_ctl(pa_proplist *p, const char *name);
+#endif
+bool pa_alsa_init_description(pa_proplist *p, pa_card *card);
+
+#if 0
+int pa_alsa_recover_from_poll(snd_pcm_t *pcm, int revents);
+
+pa_rtpoll_item* pa_alsa_build_pollfd(snd_pcm_t *pcm, pa_rtpoll *rtpoll);
+
+snd_pcm_sframes_t pa_alsa_safe_avail(snd_pcm_t *pcm, size_t hwbuf_size, const pa_sample_spec *ss);
+int pa_alsa_safe_delay(snd_pcm_t *pcm, snd_pcm_status_t *status, snd_pcm_sframes_t *delay, size_t hwbuf_size, const pa_sample_spec *ss, bool capture);
+int pa_alsa_safe_mmap_begin(snd_pcm_t *pcm, const snd_pcm_channel_area_t **areas, snd_pcm_uframes_t *offset, snd_pcm_uframes_t *frames, size_t hwbuf_size, const pa_sample_spec *ss);
+#endif
+
+char *pa_alsa_get_driver_name(int card);
+char *pa_alsa_get_driver_name_by_pcm(snd_pcm_t *pcm);
+
+char *pa_alsa_get_reserve_name(const char *device);
+
+unsigned int *pa_alsa_get_supported_rates(snd_pcm_t *pcm, unsigned int fallback_rate);
+pa_sample_format_t *pa_alsa_get_supported_formats(snd_pcm_t *pcm, pa_sample_format_t fallback_format);
+
+bool pa_alsa_pcm_is_hw(snd_pcm_t *pcm);
+bool pa_alsa_pcm_is_modem(snd_pcm_t *pcm);
+
+const char* pa_alsa_strerror(int errnum);
+
+#if 0
+bool pa_alsa_may_tsched(bool want);
+#endif
+
+snd_mixer_elem_t *pa_alsa_mixer_find_card(snd_mixer_t *mixer, struct pa_alsa_mixer_id *alsa_id, unsigned int device);
+snd_mixer_elem_t *pa_alsa_mixer_find_pcm(snd_mixer_t *mixer, const char *name, unsigned int device);
+
+snd_mixer_t *pa_alsa_open_mixer(pa_hashmap *mixers, int alsa_card_index, bool probe);
+snd_mixer_t *pa_alsa_open_mixer_by_name(pa_hashmap *mixers, const char *dev, bool probe);
+snd_mixer_t *pa_alsa_open_mixer_for_pcm(pa_hashmap *mixers, snd_pcm_t *pcm, bool probe);
+#if 0
+void pa_alsa_mixer_set_fdlist(pa_hashmap *mixers, snd_mixer_t *mixer, pa_mainloop_api *ml);
+#endif
+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];
+};
+
+int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld);
+
+#endif
diff --git a/spa/plugins/alsa/acp/array.h b/spa/plugins/alsa/acp/array.h
new file mode 100644
index 0000000..8a976ca
--- /dev/null
+++ b/spa/plugins/alsa/acp/array.h
@@ -0,0 +1,150 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_ARRAY_H
+#define PA_ARRAY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+#include <string.h>
+
+typedef struct pa_array {
+ void *data; /**< pointer to array data */
+ size_t size; /**< length of array in bytes */
+ size_t alloc; /**< number of allocated memory in \a data */
+ size_t extend; /**< number of bytes to extend with */
+} pa_array;
+
+#define PW_ARRAY_INIT(extend) ((struct pa_array) { NULL, 0, 0, (extend) })
+
+#define pa_array_get_len_s(a,s) ((a)->size / (s))
+#define pa_array_get_unchecked_s(a,idx,s,t) (t*)((uint8_t*)(a)->data + (int)((idx)*(s)))
+#define pa_array_check_index_s(a,idx,s) ((idx) < pa_array_get_len_s(a,s))
+
+#define pa_array_get_len(a,t) pa_array_get_len_s(a,sizeof(t))
+#define pa_array_get_unchecked(a,idx,t) pa_array_get_unchecked_s(a,idx,sizeof(t),t)
+#define pa_array_check_index(a,idx,t) pa_array_check_index_s(a,idx,sizeof(t))
+
+#define pa_array_first(a) ((a)->data)
+#define pa_array_end(a) (void*)((uint8_t*)(a)->data + (int)(a)->size)
+#define pa_array_check(a,p) ((void*)((uint8_t*)p + (int)sizeof(*p)) <= pa_array_end(a))
+
+#define pa_array_for_each(pos, array) \
+ for (pos = (__typeof__(pos)) pa_array_first(array); \
+ pa_array_check(array, pos); \
+ (pos)++)
+
+#define pa_array_consume(pos, array) \
+ while (pos = (__typeof__(pos)) pa_array_first(array) && \
+ pa_array_check(array, pos)
+
+#define pa_array_remove(a,p) \
+({ \
+ (a)->size -= sizeof(*(p)); \
+ memmove(p, ((uint8_t*)(p) + (int)sizeof(*(p))), \
+ (uint8_t*)pa_array_end(a) - (uint8_t*)(p)); \
+})
+
+static inline void pa_array_init(pa_array *arr, size_t extend)
+{
+ arr->data = NULL;
+ arr->size = arr->alloc = 0;
+ arr->extend = extend;
+}
+
+static inline void pa_array_clear(pa_array *arr)
+{
+ free(arr->data);
+}
+
+static inline void pa_array_reset(pa_array *arr)
+{
+ arr->size = 0;
+}
+
+static inline int pa_array_ensure_size(pa_array *arr, size_t size)
+{
+ size_t alloc, need;
+
+ alloc = arr->alloc;
+ need = arr->size + size;
+
+ if (alloc < need) {
+ void *data;
+ alloc = alloc > arr->extend ? alloc : arr->extend;
+ while (alloc < need)
+ alloc *= 2;
+ if ((data = realloc(arr->data, alloc)) == NULL)
+ return -errno;
+ arr->data = data;
+ arr->alloc = alloc;
+ }
+ return 0;
+}
+
+static inline void *pa_array_add(pa_array *arr, size_t size)
+{
+ void *p;
+
+ if (pa_array_ensure_size(arr, size) < 0)
+ return NULL;
+
+ p = (void*)((uint8_t*)arr->data + (int)arr->size);
+ arr->size += size;
+
+ return p;
+}
+
+static inline void *pa_array_add_fixed(pa_array *arr, size_t size)
+{
+ void *p;
+ if (arr->alloc < arr->size + size) {
+ errno = ENOSPC;
+ return NULL;
+ }
+ p = ((uint8_t*)arr->data + (int)arr->size);
+ arr->size += size;
+ return p;
+}
+
+#define pa_array_add_ptr(a,p) \
+ *((void**) pa_array_add(a, sizeof(void*))) = (p)
+
+static inline int pa_array_add_data(pa_array *arr, const void *data, size_t size)
+{
+ void *d;
+ if ((d = pa_array_add(arr, size)) == NULL)
+ return -1;
+ memcpy(d, data, size);
+ return size;
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_ARRAY_H */
diff --git a/spa/plugins/alsa/acp/card.h b/spa/plugins/alsa/acp/card.h
new file mode 100644
index 0000000..5a94064
--- /dev/null
+++ b/spa/plugins/alsa/acp/card.h
@@ -0,0 +1,75 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+
+#ifndef PULSE_CARD_H
+#define PULSE_CARD_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+
+#include "compat.h"
+
+typedef struct pa_card pa_card;
+
+struct pa_card {
+ struct acp_card card;
+
+ pa_core *core;
+
+ char *name;
+ char *driver;
+
+ pa_proplist *proplist;
+
+ bool use_ucm;
+ bool soft_mixer;
+ bool auto_profile;
+ bool auto_port;
+ bool ignore_dB;
+ uint32_t rate;
+
+ pa_alsa_ucm_config ucm;
+ pa_alsa_profile_set *profile_set;
+
+ pa_hashmap *ports;
+ pa_hashmap *profiles;
+ pa_hashmap *jacks;
+
+ struct {
+ pa_dynarray ports;
+ pa_dynarray profiles;
+ pa_dynarray devices;
+ } out;
+
+ const struct acp_card_events *events;
+ void *user_data;
+};
+
+bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PULSE_CARD_H */
diff --git a/spa/plugins/alsa/acp/channelmap.h b/spa/plugins/alsa/acp/channelmap.h
new file mode 100644
index 0000000..adb4868
--- /dev/null
+++ b/spa/plugins/alsa/acp/channelmap.h
@@ -0,0 +1,476 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+#ifndef PULSE_CHANNELMAP_H
+#define PULSE_CHANNELMAP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "spa/utils/defs.h"
+
+#define PA_CHANNELS_MAX 64
+
+#define PA_CHANNEL_MAP_SNPRINT_MAX (PA_CHANNELS_MAX * 32)
+
+typedef enum pa_channel_map_def {
+ PA_CHANNEL_MAP_AIFF,
+ PA_CHANNEL_MAP_ALSA,
+ PA_CHANNEL_MAP_AUX,
+ PA_CHANNEL_MAP_WAVEEX,
+ PA_CHANNEL_MAP_OSS,
+ PA_CHANNEL_MAP_DEF_MAX,
+ PA_CHANNEL_MAP_DEFAULT = PA_CHANNEL_MAP_AIFF
+} pa_channel_map_def_t;
+
+typedef enum pa_channel_position {
+ PA_CHANNEL_POSITION_INVALID = -1,
+ PA_CHANNEL_POSITION_MONO = 0,
+
+ PA_CHANNEL_POSITION_FRONT_LEFT, /**< Apple, Dolby call this 'Left' */
+ PA_CHANNEL_POSITION_FRONT_RIGHT, /**< Apple, Dolby call this 'Right' */
+ PA_CHANNEL_POSITION_FRONT_CENTER, /**< Apple, Dolby call this 'Center' */
+
+/** \cond fulldocs */
+ PA_CHANNEL_POSITION_LEFT = PA_CHANNEL_POSITION_FRONT_LEFT,
+ PA_CHANNEL_POSITION_RIGHT = PA_CHANNEL_POSITION_FRONT_RIGHT,
+ PA_CHANNEL_POSITION_CENTER = PA_CHANNEL_POSITION_FRONT_CENTER,
+/** \endcond */
+
+ PA_CHANNEL_POSITION_REAR_CENTER, /**< Microsoft calls this 'Back Center', Apple calls this 'Center Surround', Dolby calls this 'Surround Rear Center' */
+ PA_CHANNEL_POSITION_REAR_LEFT, /**< Microsoft calls this 'Back Left', Apple calls this 'Left Surround' (!), Dolby calls this 'Surround Rear Left' */
+ PA_CHANNEL_POSITION_REAR_RIGHT, /**< Microsoft calls this 'Back Right', Apple calls this 'Right Surround' (!), Dolby calls this 'Surround Rear Right' */
+
+ PA_CHANNEL_POSITION_LFE, /**< Microsoft calls this 'Low Frequency', Apple calls this 'LFEScreen' */
+/** \cond fulldocs */
+ PA_CHANNEL_POSITION_SUBWOOFER = PA_CHANNEL_POSITION_LFE,
+/** \endcond */
+
+ PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER, /**< Apple, Dolby call this 'Left Center' */
+ PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER, /**< Apple, Dolby call this 'Right Center */
+
+ PA_CHANNEL_POSITION_SIDE_LEFT, /**< Apple calls this 'Left Surround Direct', Dolby calls this 'Surround Left' (!) */
+ PA_CHANNEL_POSITION_SIDE_RIGHT, /**< Apple calls this 'Right Surround Direct', Dolby calls this 'Surround Right' (!) */
+ PA_CHANNEL_POSITION_AUX0,
+ PA_CHANNEL_POSITION_AUX1,
+ PA_CHANNEL_POSITION_AUX2,
+ PA_CHANNEL_POSITION_AUX3,
+ PA_CHANNEL_POSITION_AUX4,
+ PA_CHANNEL_POSITION_AUX5,
+ PA_CHANNEL_POSITION_AUX6,
+ PA_CHANNEL_POSITION_AUX7,
+ PA_CHANNEL_POSITION_AUX8,
+ PA_CHANNEL_POSITION_AUX9,
+ PA_CHANNEL_POSITION_AUX10,
+ PA_CHANNEL_POSITION_AUX11,
+ PA_CHANNEL_POSITION_AUX12,
+ PA_CHANNEL_POSITION_AUX13,
+ PA_CHANNEL_POSITION_AUX14,
+ PA_CHANNEL_POSITION_AUX15,
+ PA_CHANNEL_POSITION_AUX16,
+ PA_CHANNEL_POSITION_AUX17,
+ PA_CHANNEL_POSITION_AUX18,
+ PA_CHANNEL_POSITION_AUX19,
+ PA_CHANNEL_POSITION_AUX20,
+ PA_CHANNEL_POSITION_AUX21,
+ PA_CHANNEL_POSITION_AUX22,
+ PA_CHANNEL_POSITION_AUX23,
+ PA_CHANNEL_POSITION_AUX24,
+ PA_CHANNEL_POSITION_AUX25,
+ PA_CHANNEL_POSITION_AUX26,
+ PA_CHANNEL_POSITION_AUX27,
+ PA_CHANNEL_POSITION_AUX28,
+ PA_CHANNEL_POSITION_AUX29,
+ PA_CHANNEL_POSITION_AUX30,
+ PA_CHANNEL_POSITION_AUX31,
+
+ PA_CHANNEL_POSITION_TOP_CENTER, /**< Apple calls this 'Top Center Surround' */
+
+ PA_CHANNEL_POSITION_TOP_FRONT_LEFT, /**< Apple calls this 'Vertical Height Left' */
+ PA_CHANNEL_POSITION_TOP_FRONT_RIGHT, /**< Apple calls this 'Vertical Height Right' */
+ PA_CHANNEL_POSITION_TOP_FRONT_CENTER, /**< Apple calls this 'Vertical Height Center' */
+
+ PA_CHANNEL_POSITION_TOP_REAR_LEFT, /**< Microsoft and Apple call this 'Top Back Left' */
+ PA_CHANNEL_POSITION_TOP_REAR_RIGHT, /**< Microsoft and Apple call this 'Top Back Right' */
+ PA_CHANNEL_POSITION_TOP_REAR_CENTER, /**< Microsoft and Apple call this 'Top Back Center' */
+
+ PA_CHANNEL_POSITION_MAX
+} pa_channel_position_t;
+
+typedef struct pa_channel_map {
+ uint8_t channels;
+ pa_channel_position_t map[PA_CHANNELS_MAX];
+} pa_channel_map;
+
+static inline int pa_channels_valid(uint8_t channels)
+{
+ return channels > 0 && channels <= PA_CHANNELS_MAX;
+}
+
+static inline int pa_channel_map_valid(const pa_channel_map *map)
+{
+ unsigned c;
+ if (!pa_channels_valid(map->channels))
+ return 0;
+ for (c = 0; c < map->channels; c++)
+ if (map->map[c] < 0 || map->map[c] >= PA_CHANNEL_POSITION_MAX)
+ return 0;
+ return 1;
+}
+static inline pa_channel_map* pa_channel_map_init(pa_channel_map *m)
+{
+ unsigned c;
+ m->channels = 0;
+ for (c = 0; c < PA_CHANNELS_MAX; c++)
+ m->map[c] = PA_CHANNEL_POSITION_INVALID;
+ return m;
+}
+
+static inline pa_channel_map* pa_channel_map_init_auto(pa_channel_map *m, unsigned channels, pa_channel_map_def_t def)
+{
+ unsigned i;
+
+ pa_assert(m);
+ pa_assert(pa_channels_valid(channels));
+ pa_assert(def < PA_CHANNEL_MAP_DEF_MAX);
+
+ pa_channel_map_init(m);
+
+ m->channels = (uint8_t) channels;
+
+ switch (def) {
+ case PA_CHANNEL_MAP_ALSA:
+ switch (channels) {
+ case 1:
+ m->map[0] = PA_CHANNEL_POSITION_MONO;
+ return m;
+ case 8:
+ m->map[6] = PA_CHANNEL_POSITION_SIDE_LEFT;
+ m->map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT;
+ SPA_FALLTHROUGH;
+ case 6:
+ m->map[5] = PA_CHANNEL_POSITION_LFE;
+ SPA_FALLTHROUGH;
+ case 5:
+ m->map[4] = PA_CHANNEL_POSITION_FRONT_CENTER;
+ SPA_FALLTHROUGH;
+ case 4:
+ m->map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ m->map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ SPA_FALLTHROUGH;
+ case 2:
+ m->map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ m->map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ return m;
+ default:
+ return NULL;
+ }
+ case PA_CHANNEL_MAP_AUX:
+ for (i = 0; i < channels; i++)
+ m->map[i] = PA_CHANNEL_POSITION_AUX0 + (i & 31);
+ return m;
+ default:
+ break;
+ }
+ return NULL;
+}
+
+static inline pa_channel_map* pa_channel_map_init_extend(pa_channel_map *m,
+ unsigned channels, pa_channel_map_def_t def)
+{
+ pa_channel_map *r;
+ if ((r = pa_channel_map_init_auto(m, channels, def)) != NULL)
+ return r;
+ return pa_channel_map_init_auto(m, channels, PA_CHANNEL_MAP_AUX);
+}
+
+typedef uint64_t pa_channel_position_mask_t;
+
+#define PA_CHANNEL_POSITION_MASK(f) ((pa_channel_position_mask_t) (1ULL << (f)))
+
+#define PA_CHANNEL_POSITION_MASK_LEFT \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_SIDE_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_LEFT)) \
+
+#define PA_CHANNEL_POSITION_MASK_RIGHT \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_SIDE_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_RIGHT))
+
+#define PA_CHANNEL_POSITION_MASK_CENTER \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_FRONT \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_REAR \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_LFE \
+ PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_LFE)
+
+#define PA_CHANNEL_POSITION_MASK_HFE \
+ (PA_CHANNEL_POSITION_MASK_REAR | PA_CHANNEL_POSITION_MASK_FRONT \
+ | PA_CHANNEL_POSITION_MASK_LEFT | PA_CHANNEL_POSITION_MASK_RIGHT \
+ | PA_CHANNEL_POSITION_MASK_CENTER)
+
+#define PA_CHANNEL_POSITION_MASK_SIDE_OR_TOP_CENTER \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_SIDE_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_SIDE_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_TOP \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_ALL \
+ ((pa_channel_position_mask_t) (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_MAX)-1))
+
+static const char *const pa_position_table[PA_CHANNEL_POSITION_MAX] = {
+ [PA_CHANNEL_POSITION_MONO] = "mono",
+ [PA_CHANNEL_POSITION_FRONT_CENTER] = "front-center",
+ [PA_CHANNEL_POSITION_FRONT_LEFT] = "front-left",
+ [PA_CHANNEL_POSITION_FRONT_RIGHT] = "front-right",
+ [PA_CHANNEL_POSITION_REAR_CENTER] = "rear-center",
+ [PA_CHANNEL_POSITION_REAR_LEFT] = "rear-left",
+ [PA_CHANNEL_POSITION_REAR_RIGHT] = "rear-right",
+ [PA_CHANNEL_POSITION_LFE] = "lfe",
+ [PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = "front-left-of-center",
+ [PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = "front-right-of-center",
+ [PA_CHANNEL_POSITION_SIDE_LEFT] = "side-left",
+ [PA_CHANNEL_POSITION_SIDE_RIGHT] = "side-right",
+ [PA_CHANNEL_POSITION_AUX0] = "aux0",
+ [PA_CHANNEL_POSITION_AUX1] = "aux1",
+ [PA_CHANNEL_POSITION_AUX2] = "aux2",
+ [PA_CHANNEL_POSITION_AUX3] = "aux3",
+ [PA_CHANNEL_POSITION_AUX4] = "aux4",
+ [PA_CHANNEL_POSITION_AUX5] = "aux5",
+ [PA_CHANNEL_POSITION_AUX6] = "aux6",
+ [PA_CHANNEL_POSITION_AUX7] = "aux7",
+ [PA_CHANNEL_POSITION_AUX8] = "aux8",
+ [PA_CHANNEL_POSITION_AUX9] = "aux9",
+ [PA_CHANNEL_POSITION_AUX10] = "aux10",
+ [PA_CHANNEL_POSITION_AUX11] = "aux11",
+ [PA_CHANNEL_POSITION_AUX12] = "aux12",
+ [PA_CHANNEL_POSITION_AUX13] = "aux13",
+ [PA_CHANNEL_POSITION_AUX14] = "aux14",
+ [PA_CHANNEL_POSITION_AUX15] = "aux15",
+ [PA_CHANNEL_POSITION_AUX16] = "aux16",
+ [PA_CHANNEL_POSITION_AUX17] = "aux17",
+ [PA_CHANNEL_POSITION_AUX18] = "aux18",
+ [PA_CHANNEL_POSITION_AUX19] = "aux19",
+ [PA_CHANNEL_POSITION_AUX20] = "aux20",
+ [PA_CHANNEL_POSITION_AUX21] = "aux21",
+ [PA_CHANNEL_POSITION_AUX22] = "aux22",
+ [PA_CHANNEL_POSITION_AUX23] = "aux23",
+ [PA_CHANNEL_POSITION_AUX24] = "aux24",
+ [PA_CHANNEL_POSITION_AUX25] = "aux25",
+ [PA_CHANNEL_POSITION_AUX26] = "aux26",
+ [PA_CHANNEL_POSITION_AUX27] = "aux27",
+ [PA_CHANNEL_POSITION_AUX28] = "aux28",
+ [PA_CHANNEL_POSITION_AUX29] = "aux29",
+ [PA_CHANNEL_POSITION_AUX30] = "aux30",
+ [PA_CHANNEL_POSITION_AUX31] = "aux31",
+
+ [PA_CHANNEL_POSITION_TOP_CENTER] = "top-center",
+ [PA_CHANNEL_POSITION_TOP_FRONT_CENTER] = "top-front-center",
+ [PA_CHANNEL_POSITION_TOP_FRONT_LEFT] = "top-front-left",
+ [PA_CHANNEL_POSITION_TOP_FRONT_RIGHT] = "top-front-right",
+ [PA_CHANNEL_POSITION_TOP_REAR_CENTER] = "top-rear-center",
+ [PA_CHANNEL_POSITION_TOP_REAR_LEFT] = "top-rear-left",
+ [PA_CHANNEL_POSITION_TOP_REAR_RIGHT] = "top-rear-right"
+};
+
+static inline pa_channel_position_t pa_channel_position_from_string(const char *p)
+{
+ pa_channel_position_t i;
+ /* Some special aliases */
+ if (pa_streq(p, "left"))
+ return PA_CHANNEL_POSITION_LEFT;
+ else if (pa_streq(p, "right"))
+ return PA_CHANNEL_POSITION_RIGHT;
+ else if (pa_streq(p, "center"))
+ return PA_CHANNEL_POSITION_CENTER;
+ else if (pa_streq(p, "subwoofer"))
+ return PA_CHANNEL_POSITION_SUBWOOFER;
+ for (i = 0; i < PA_CHANNEL_POSITION_MAX; i++)
+ if (pa_streq(p, pa_position_table[i]))
+ return i;
+ return PA_CHANNEL_POSITION_INVALID;
+}
+
+static inline pa_channel_map *pa_channel_map_parse(pa_channel_map *rmap, const char *s)
+{
+ const char *state;
+ pa_channel_map map;
+ char *p;
+ pa_channel_map_init(&map);
+ if (pa_streq(s, "stereo")) {
+ map.channels = 2;
+ map.map[0] = PA_CHANNEL_POSITION_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_RIGHT;
+ goto finish;
+ } else if (pa_streq(s, "surround-21")) {
+ map.channels = 3;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_LFE;
+ goto finish;
+ } else if (pa_streq(s, "surround-40")) {
+ map.channels = 4;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ goto finish;
+ } else if (pa_streq(s, "surround-41")) {
+ map.channels = 5;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ map.map[4] = PA_CHANNEL_POSITION_LFE;
+ goto finish;
+ } else if (pa_streq(s, "surround-50")) {
+ map.channels = 5;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ map.map[4] = PA_CHANNEL_POSITION_FRONT_CENTER;
+ goto finish;
+ } else if (pa_streq(s, "surround-51")) {
+ map.channels = 6;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ map.map[4] = PA_CHANNEL_POSITION_FRONT_CENTER;
+ map.map[5] = PA_CHANNEL_POSITION_LFE;
+ goto finish;
+ } else if (pa_streq(s, "surround-71")) {
+ map.channels = 8;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ map.map[4] = PA_CHANNEL_POSITION_FRONT_CENTER;
+ map.map[5] = PA_CHANNEL_POSITION_LFE;
+ map.map[6] = PA_CHANNEL_POSITION_SIDE_LEFT;
+ map.map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT;
+ goto finish;
+ }
+ state = NULL;
+ map.channels = 0;
+ while ((p = pa_split(s, ",", &state))) {
+ pa_channel_position_t f;
+
+ if (map.channels >= PA_CHANNELS_MAX) {
+ pa_xfree(p);
+ return NULL;
+ }
+ if ((f = pa_channel_position_from_string(p)) == PA_CHANNEL_POSITION_INVALID) {
+ pa_xfree(p);
+ return NULL;
+ }
+ map.map[map.channels++] = f;
+ pa_xfree(p);
+ }
+finish:
+ if (!pa_channel_map_valid(&map))
+ return NULL;
+ *rmap = map;
+ return rmap;
+}
+
+static inline const char* pa_channel_position_to_string(pa_channel_position_t pos) {
+
+ if (pos < 0 || pos >= PA_CHANNEL_POSITION_MAX)
+ return NULL;
+ return pa_position_table[pos];
+}
+
+static inline int pa_channel_map_equal(const pa_channel_map *a, const pa_channel_map *b)
+{
+ unsigned c;
+ if (PA_UNLIKELY(a == b))
+ return 1;
+ if (a->channels != b->channels)
+ return 0;
+ for (c = 0; c < a->channels; c++)
+ if (a->map[c] != b->map[c])
+ return 0;
+ return 1;
+}
+
+static inline char* pa_channel_map_snprint(char *s, size_t l, const pa_channel_map *map) {
+ unsigned channel;
+ bool first = true;
+ char *e;
+ if (!pa_channel_map_valid(map)) {
+ pa_snprintf(s, l, "%s", _("(invalid)"));
+ return s;
+ }
+ *(e = s) = 0;
+ for (channel = 0; channel < map->channels && l > 1; channel++) {
+ l -= pa_snprintf(e, l, "%s%s",
+ first ? "" : ",",
+ pa_channel_position_to_string(map->map[channel]));
+ e = strchr(e, 0);
+ first = false;
+ }
+
+ return s;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PULSE_CHANNELMAP_H */
diff --git a/spa/plugins/alsa/acp/compat.c b/spa/plugins/alsa/acp/compat.c
new file mode 100644
index 0000000..7703444
--- /dev/null
+++ b/spa/plugins/alsa/acp/compat.c
@@ -0,0 +1,210 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2009 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+#include "compat.h"
+#include "device-port.h"
+#include "alsa-mixer.h"
+
+static const char *port_types[] = {
+ [PA_DEVICE_PORT_TYPE_UNKNOWN] = "unknown",
+ [PA_DEVICE_PORT_TYPE_AUX] = "aux",
+ [PA_DEVICE_PORT_TYPE_SPEAKER] = "speaker",
+ [PA_DEVICE_PORT_TYPE_HEADPHONES] = "headphones",
+ [PA_DEVICE_PORT_TYPE_LINE] = "line",
+ [PA_DEVICE_PORT_TYPE_MIC] = "mic",
+ [PA_DEVICE_PORT_TYPE_HEADSET] = "headset",
+ [PA_DEVICE_PORT_TYPE_HANDSET] = "handset",
+ [PA_DEVICE_PORT_TYPE_EARPIECE] = "earpiece",
+ [PA_DEVICE_PORT_TYPE_SPDIF] = "spdif",
+ [PA_DEVICE_PORT_TYPE_HDMI] = "hdmi",
+ [PA_DEVICE_PORT_TYPE_TV] = "tv",
+ [PA_DEVICE_PORT_TYPE_RADIO] = "radio",
+ [PA_DEVICE_PORT_TYPE_VIDEO] = "video",
+ [PA_DEVICE_PORT_TYPE_USB] = "usb",
+ [PA_DEVICE_PORT_TYPE_BLUETOOTH] = "bluetooth",
+ [PA_DEVICE_PORT_TYPE_PORTABLE] = "portable",
+ [PA_DEVICE_PORT_TYPE_HANDSFREE] = "handsfree",
+ [PA_DEVICE_PORT_TYPE_CAR] = "car",
+ [PA_DEVICE_PORT_TYPE_HIFI] = "hifi",
+ [PA_DEVICE_PORT_TYPE_PHONE] = "phone",
+ [PA_DEVICE_PORT_TYPE_NETWORK] = "network",
+ [PA_DEVICE_PORT_TYPE_ANALOG] = "analog",
+};
+
+static const char *str_port_type(pa_device_port_type_t type)
+{
+ int idx = (type < PA_ELEMENTSOF(port_types)) ? type : 0;
+ return port_types[idx];
+}
+
+pa_device_port_new_data *pa_device_port_new_data_init(pa_device_port_new_data *data)
+{
+ pa_assert(data);
+ pa_zero(*data);
+ data->type = PA_DEVICE_PORT_TYPE_UNKNOWN;
+ data->available = PA_AVAILABLE_UNKNOWN;
+ return data;
+}
+
+void pa_device_port_new_data_set_name(pa_device_port_new_data *data, const char *name)
+{
+ pa_assert(data);
+ pa_xfree(data->name);
+ data->name = pa_xstrdup(name);
+}
+
+void pa_device_port_new_data_set_description(pa_device_port_new_data *data, const char *description)
+{
+ pa_assert(data);
+ pa_xfree(data->description);
+ data->description = pa_xstrdup(description);
+}
+
+void pa_device_port_new_data_set_available(pa_device_port_new_data *data, pa_available_t available)
+{
+ pa_assert(data);
+ data->available = available;
+}
+
+void pa_device_port_new_data_set_availability_group(pa_device_port_new_data *data, const char *group)
+{
+ pa_assert(data);
+ pa_xfree(data->availability_group);
+ data->availability_group = pa_xstrdup(group);
+}
+
+void pa_device_port_new_data_set_direction(pa_device_port_new_data *data, pa_direction_t direction)
+{
+ pa_assert(data);
+ data->direction = direction;
+}
+
+void pa_device_port_new_data_set_type(pa_device_port_new_data *data, pa_device_port_type_t type)
+{
+ pa_assert(data);
+ data->type = type;
+}
+
+void pa_device_port_new_data_done(pa_device_port_new_data *data)
+{
+ pa_assert(data);
+ pa_xfree(data->name);
+ pa_xfree(data->description);
+ pa_xfree(data->availability_group);
+}
+
+pa_device_port *pa_device_port_new(pa_core *c, pa_device_port_new_data *data, size_t extra)
+{
+ pa_device_port *p;
+
+ pa_assert(data);
+ pa_assert(data->name);
+ pa_assert(data->description);
+ pa_assert(data->direction == PA_DIRECTION_OUTPUT || data->direction == PA_DIRECTION_INPUT);
+
+ p = calloc(1, sizeof(pa_device_port) + extra);
+
+ p->port.name = p->name = data->name;
+ data->name = NULL;
+ p->port.description = p->description = data->description;
+ data->description = NULL;
+ p->priority = p->port.priority = 0;
+ p->available = data->available;
+ p->port.available = (enum acp_available) data->available;
+ p->availability_group = data->availability_group;
+ data->availability_group = NULL;
+ p->profiles = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+ p->direction = data->direction;
+ p->port.direction = data->direction == PA_DIRECTION_OUTPUT ?
+ ACP_DIRECTION_PLAYBACK : ACP_DIRECTION_CAPTURE;
+ p->type = data->type;
+
+ p->proplist = pa_proplist_new();
+ pa_proplist_sets(p->proplist, ACP_KEY_PORT_TYPE, str_port_type(data->type));
+ if (p->availability_group)
+ pa_proplist_sets(p->proplist, ACP_KEY_PORT_AVAILABILITY_GROUP, p->availability_group);
+
+ p->user_data = (void*)((uint8_t*)p + sizeof(pa_device_port));
+
+ return p;
+}
+
+void pa_device_port_free(pa_device_port *port)
+{
+ pa_xfree(port->name);
+ pa_xfree(port->description);
+ pa_xfree(port->availability_group);
+ pa_hashmap_free(port->profiles);
+ pa_proplist_free(port->proplist);
+ if (port->impl_free)
+ port->impl_free (port);
+ free(port);
+}
+
+void pa_device_port_set_available(pa_device_port *p, pa_available_t status)
+{
+ pa_available_t old = p->available;
+
+ if (old == status)
+ return;
+ p->available = status;
+ p->port.available = (enum acp_available) status;
+
+ if (p->card && p->card->events && p->card->events->port_available)
+ p->card->events->port_available(p->card->user_data, p->port.index,
+ (enum acp_available)old, p->port.available);
+}
+
+bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card) {
+ const char *s, *d = NULL, *k;
+ pa_assert(p);
+
+ if (pa_proplist_contains(p, PA_PROP_DEVICE_DESCRIPTION))
+ return true;
+
+ if (card)
+ if ((s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_DESCRIPTION)))
+ d = s;
+
+ if (!d)
+ if ((s = pa_proplist_gets(p, PA_PROP_DEVICE_FORM_FACTOR)))
+ if (pa_streq(s, "internal"))
+ d = _("Built-in Audio");
+
+ if (!d)
+ if ((s = pa_proplist_gets(p, PA_PROP_DEVICE_CLASS)))
+ if (pa_streq(s, "modem"))
+ d = _("Modem");
+
+ if (!d)
+ d = pa_proplist_gets(p, PA_PROP_DEVICE_PRODUCT_NAME);
+
+ if (!d)
+ return false;
+
+ k = pa_proplist_gets(p, PA_PROP_DEVICE_PROFILE_DESCRIPTION);
+
+ if (d && k)
+ pa_proplist_setf(p, PA_PROP_DEVICE_DESCRIPTION, "%s %s", d, k);
+ else if (d)
+ pa_proplist_sets(p, PA_PROP_DEVICE_DESCRIPTION, d);
+
+ return true;
+}
diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h
new file mode 100644
index 0000000..46ef1fd
--- /dev/null
+++ b/spa/plugins/alsa/acp/compat.h
@@ -0,0 +1,656 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+
+#ifndef PULSE_COMPAT_H
+#define PULSE_COMPAT_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <math.h>
+
+#include <spa/utils/string.h>
+
+typedef struct pa_core pa_core;
+
+typedef void *(*pa_copy_func_t)(const void *p);
+typedef void (*pa_free_cb_t)(void *p);
+
+#ifdef __GNUC__
+#define PA_LIKELY(x) (__builtin_expect(!!(x),1))
+#define PA_UNLIKELY(x) (__builtin_expect(!!(x),0))
+#define PA_PRINTF_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1)))
+#else
+#define PA_LIKELY(x) (x)
+#define PA_UNLIKELY(x) (x)
+#define PA_PRINTF_FUNC(fmt, arg1)
+#endif
+
+#define PA_MIN(a,b) \
+({ \
+ __typeof__(a) _a = (a); \
+ __typeof__(b) _b = (b); \
+ PA_LIKELY(_a < _b) ? _a : _b; \
+})
+#define PA_MAX(a,b) \
+({ \
+ __typeof__(a) _a = (a); \
+ __typeof__(b) _b = (b); \
+ PA_LIKELY(_a > _b) ? _a : _b; \
+})
+#define PA_CLAMP_UNLIKELY(v,low,high) \
+({ \
+ __typeof__(v) _v = (v); \
+ __typeof__(low) _low = (low); \
+ __typeof__(high) _high = (high); \
+ PA_MIN(PA_MAX(_v, _low), _high); \
+})
+
+#define PA_PTR_TO_UINT(p) ((unsigned int) ((uintptr_t) (p)))
+#define PA_UINT_TO_PTR(u) ((void*) ((uintptr_t) (u)))
+
+#include "array.h"
+#include "llist.h"
+#include "hashmap.h"
+#include "dynarray.h"
+#include "idxset.h"
+#include "proplist.h"
+
+typedef enum pa_direction {
+ PA_DIRECTION_OUTPUT = 0x0001U, /**< Output direction */
+ PA_DIRECTION_INPUT = 0x0002U /**< Input direction */
+} pa_direction_t;
+
+/* This enum replaces pa_port_available_t (defined in pulse/def.h) for
+ * internal use, so make sure both enum types stay in sync. */
+typedef enum pa_available {
+ PA_AVAILABLE_UNKNOWN = 0,
+ PA_AVAILABLE_NO = 1,
+ PA_AVAILABLE_YES = 2,
+} pa_available_t;
+
+#define PA_RATE_MAX (48000U*8U)
+
+typedef enum pa_sample_format {
+ PA_SAMPLE_U8, /**< Unsigned 8 Bit PCM */
+ PA_SAMPLE_ALAW, /**< 8 Bit a-Law */
+ PA_SAMPLE_ULAW, /**< 8 Bit mu-Law */
+ PA_SAMPLE_S16LE, /**< Signed 16 Bit PCM, little endian (PC) */
+ PA_SAMPLE_S16BE, /**< Signed 16 Bit PCM, big endian */
+ PA_SAMPLE_FLOAT32LE, /**< 32 Bit IEEE floating point, little endian (PC), range -1.0 to 1.0 */
+ PA_SAMPLE_FLOAT32BE, /**< 32 Bit IEEE floating point, big endian, range -1.0 to 1.0 */
+ PA_SAMPLE_S32LE, /**< Signed 32 Bit PCM, little endian (PC) */
+ PA_SAMPLE_S32BE, /**< Signed 32 Bit PCM, big endian */
+ PA_SAMPLE_S24LE, /**< Signed 24 Bit PCM packed, little endian (PC). \since 0.9.15 */
+ PA_SAMPLE_S24BE, /**< Signed 24 Bit PCM packed, big endian. \since 0.9.15 */
+ PA_SAMPLE_S24_32LE, /**< Signed 24 Bit PCM in LSB of 32 Bit words, little endian (PC). \since 0.9.15 */
+ PA_SAMPLE_S24_32BE, /**< Signed 24 Bit PCM in LSB of 32 Bit words, big endian. \since 0.9.15 */
+ PA_SAMPLE_MAX, /**< Upper limit of valid sample types */
+ PA_SAMPLE_INVALID = -1 /**< An invalid value */
+} pa_sample_format_t;
+
+static inline int pa_sample_format_valid(unsigned format)
+{
+ return format < PA_SAMPLE_MAX;
+}
+
+#ifdef WORDS_BIGENDIAN
+#define PA_SAMPLE_S16NE PA_SAMPLE_S16BE
+#define PA_SAMPLE_FLOAT32NE PA_SAMPLE_FLOAT32BE
+#define PA_SAMPLE_S32NE PA_SAMPLE_S32BE
+#define PA_SAMPLE_S24NE PA_SAMPLE_S24BE
+#define PA_SAMPLE_S24_32NE PA_SAMPLE_S24_32BE
+#define PA_SAMPLE_S16RE PA_SAMPLE_S16LE
+#define PA_SAMPLE_FLOAT32RE PA_SAMPLE_FLOAT32LE
+#define PA_SAMPLE_S32RE PA_SAMPLE_S32LE
+#define PA_SAMPLE_S24RE PA_SAMPLE_S24LE
+#define PA_SAMPLE_S24_32RE PA_SAMPLE_S24_32LE
+#else
+#define PA_SAMPLE_S16NE PA_SAMPLE_S16LE
+#define PA_SAMPLE_FLOAT32NE PA_SAMPLE_FLOAT32LE
+#define PA_SAMPLE_S32NE PA_SAMPLE_S32LE
+#define PA_SAMPLE_S24NE PA_SAMPLE_S24LE
+#define PA_SAMPLE_S24_32NE PA_SAMPLE_S24_32LE
+#define PA_SAMPLE_S16RE PA_SAMPLE_S16BE
+#define PA_SAMPLE_FLOAT32RE PA_SAMPLE_FLOAT32BE
+#define PA_SAMPLE_S32RE PA_SAMPLE_S32BE
+#define PA_SAMPLE_S24RE PA_SAMPLE_S24BE
+#define PA_SAMPLE_S24_32RE PA_SAMPLE_S24_32BE
+#endif
+
+static const size_t pa_sample_size_table[] = {
+ [PA_SAMPLE_U8] = 1,
+ [PA_SAMPLE_ULAW] = 1,
+ [PA_SAMPLE_ALAW] = 1,
+ [PA_SAMPLE_S16LE] = 2,
+ [PA_SAMPLE_S16BE] = 2,
+ [PA_SAMPLE_FLOAT32LE] = 4,
+ [PA_SAMPLE_FLOAT32BE] = 4,
+ [PA_SAMPLE_S32LE] = 4,
+ [PA_SAMPLE_S32BE] = 4,
+ [PA_SAMPLE_S24LE] = 3,
+ [PA_SAMPLE_S24BE] = 3,
+ [PA_SAMPLE_S24_32LE] = 4,
+ [PA_SAMPLE_S24_32BE] = 4
+};
+
+static inline const char *pa_sample_format_to_string(pa_sample_format_t f)
+{
+ static const char* const table[]= {
+ [PA_SAMPLE_U8] = "u8",
+ [PA_SAMPLE_ALAW] = "aLaw",
+ [PA_SAMPLE_ULAW] = "uLaw",
+ [PA_SAMPLE_S16LE] = "s16le",
+ [PA_SAMPLE_S16BE] = "s16be",
+ [PA_SAMPLE_FLOAT32LE] = "float32le",
+ [PA_SAMPLE_FLOAT32BE] = "float32be",
+ [PA_SAMPLE_S32LE] = "s32le",
+ [PA_SAMPLE_S32BE] = "s32be",
+ [PA_SAMPLE_S24LE] = "s24le",
+ [PA_SAMPLE_S24BE] = "s24be",
+ [PA_SAMPLE_S24_32LE] = "s24-32le",
+ [PA_SAMPLE_S24_32BE] = "s24-32be",
+ };
+
+ if (!pa_sample_format_valid(f))
+ return NULL;
+ return table[f];
+}
+
+typedef struct pa_sample_spec {
+ pa_sample_format_t format;
+ uint32_t rate;
+ uint8_t channels;
+} pa_sample_spec;
+
+typedef uint64_t pa_usec_t;
+#define PA_MSEC_PER_SEC ((pa_usec_t) 1000ULL)
+#define PA_USEC_PER_SEC ((pa_usec_t) 1000000ULL)
+#define PA_USEC_PER_MSEC ((pa_usec_t) 1000ULL)
+
+static inline size_t pa_usec_to_bytes(pa_usec_t t, const pa_sample_spec *spec) {
+ return (size_t) (((t * spec->rate) / PA_USEC_PER_SEC)) *
+ (pa_sample_size_table[spec->format] * spec->channels);
+}
+
+static inline int pa_sample_rate_valid(uint32_t rate) {
+ return rate > 0 && rate <= PA_RATE_MAX * 101 / 100;
+}
+
+static inline size_t pa_frame_size(const pa_sample_spec *spec) {
+ return pa_sample_size_table[spec->format] * spec->channels;
+}
+
+typedef enum pa_log_level {
+ PA_LOG_ERROR = 0, /* Error messages */
+ PA_LOG_WARN = 1, /* Warning messages */
+ PA_LOG_NOTICE = 2, /* Notice messages */
+ PA_LOG_INFO = 3, /* Info messages */
+ PA_LOG_DEBUG = 4, /* Debug messages */
+ PA_LOG_LEVEL_MAX
+} pa_log_level_t;
+
+extern int _acp_log_level;
+extern acp_log_func _acp_log_func;
+extern void * _acp_log_data;
+
+#define pa_log_level_enabled(lev) (_acp_log_level >= (int)(lev))
+
+#define pa_log_levelv_meta(lev,f,l,func,fmt,ap) \
+({ \
+ if (pa_log_level_enabled (lev) && _acp_log_func) \
+ _acp_log_func(_acp_log_data,lev,f,l,func,fmt,ap); \
+})
+
+static inline PA_PRINTF_FUNC(5, 6) void pa_log_level_meta(enum pa_log_level level,
+ const char *file, int line, const char *func,
+ const char *fmt, ...)
+{
+ va_list args;
+ va_start(args,fmt);
+ pa_log_levelv_meta(level,file,line,func,fmt,args);
+ va_end(args);
+}
+
+#define pa_logl(lev,fmt,...) pa_log_level_meta(lev,__FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)
+#define pa_log_error(fmt,...) pa_logl(PA_LOG_ERROR, fmt, ##__VA_ARGS__)
+#define pa_log_warn(fmt,...) pa_logl(PA_LOG_WARN, fmt, ##__VA_ARGS__)
+#define pa_log_notice(fmt,...) pa_logl(PA_LOG_NOTICE, fmt, ##__VA_ARGS__)
+#define pa_log_info(fmt,...) pa_logl(PA_LOG_INFO, fmt, ##__VA_ARGS__)
+#define pa_log_debug(fmt,...) pa_logl(PA_LOG_DEBUG, fmt, ##__VA_ARGS__)
+#define pa_log pa_log_error
+
+#define pa_assert_se(expr) \
+ do { \
+ if (PA_UNLIKELY(!(expr))) { \
+ fprintf(stderr, "'%s' failed at %s:%u %s()\n", \
+ #expr , __FILE__, __LINE__, __func__); \
+ abort(); \
+ } \
+ } while (false)
+
+#define pa_assert(expr) \
+ do { \
+ if (PA_UNLIKELY(!(expr))) { \
+ fprintf(stderr, "'%s' failed at %s:%u %s()\n", \
+ #expr , __FILE__, __LINE__, __func__); \
+ abort(); \
+ } \
+ } while (false)
+
+#define pa_assert_not_reached() \
+ do { \
+ fprintf(stderr, "Code should not be reached at %s:%u %s()\n", \
+ __FILE__, __LINE__, __func__); \
+ abort(); \
+ } while (false)
+
+
+#define pa_memzero(x,l) (memset((x), 0, (l)))
+#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))
+
+#define PA_ELEMENTSOF(x) (sizeof(x)/sizeof((x)[0]))
+
+#define pa_streq(a,b) (!strcmp((a),(b)))
+#define pa_strneq(a,b,n) (!strncmp((a),(b),(n)))
+#define pa_strnull(s) ((s) ? (s) : "null")
+#define pa_startswith(s,pfx) (strstr(s, pfx) == s)
+
+PA_PRINTF_FUNC(3, 0)
+static inline size_t pa_vsnprintf(char *str, size_t size, const char *format, va_list ap)
+{
+ int ret;
+
+ pa_assert(str);
+ pa_assert(size > 0);
+ pa_assert(format);
+
+ ret = vsnprintf(str, size, format, ap);
+
+ str[size-1] = 0;
+
+ if (ret < 0)
+ return strlen(str);
+
+ if ((size_t) ret > size-1)
+ return size-1;
+
+ return (size_t) ret;
+}
+
+PA_PRINTF_FUNC(3, 4)
+static inline size_t pa_snprintf(char *str, size_t size, const char *format, ...)
+{
+ size_t ret;
+ va_list ap;
+
+ pa_assert(str);
+ pa_assert(size > 0);
+ pa_assert(format);
+
+ va_start(ap, format);
+ ret = pa_vsnprintf(str, size, format, ap);
+ va_end(ap);
+
+ return ret;
+}
+
+#define pa_xstrdup(s) ((s) != NULL ? strdup(s) : NULL)
+#define pa_xstrndup(s,n) ((s) != NULL ? strndup(s,n) : NULL)
+#define pa_xfree free
+#define pa_xmalloc malloc
+#define pa_xnew0(t,n) calloc(n, sizeof(t))
+#define pa_xnew(t,n) pa_xnew0(t,n)
+#define pa_xrealloc realloc
+#define pa_xrenew(t,p,n) ((t*) realloc(p, (n)*sizeof(t)))
+
+static inline void* pa_xmemdup(const void *p, size_t l) {
+ return memcpy(malloc(l), p, l);
+
+}
+#define pa_xnewdup(t,p,n) ((t*) pa_xmemdup((p), (n)*sizeof(t)))
+
+static inline void pa_xfreev(void**a)
+{
+ int i;
+ for (i = 0; a && a[i]; i++)
+ free(a[i]);
+ free(a);
+}
+static inline void pa_xstrfreev(char **a) {
+ pa_xfreev((void**)a);
+}
+
+
+#define pa_cstrerror strerror
+
+#define PA_PATH_SEP "/"
+#define PA_PATH_SEP_CHAR '/'
+
+#define PA_WHITESPACE "\n\r \t"
+
+static PA_PRINTF_FUNC(1,2) inline char *pa_sprintf_malloc(const char *fmt, ...)
+{
+ char *res;
+ va_list args;
+ va_start(args, fmt);
+ if (vasprintf(&res, fmt, args) < 0)
+ res = NULL;
+ va_end(args);
+ return res;
+}
+
+#define pa_fopen_cloexec(f,m) fopen(f,m"e")
+
+static inline char *pa_path_get_filename(const char *p)
+{
+ char *fn;
+ if (!p)
+ return NULL;
+ if ((fn = strrchr(p, PA_PATH_SEP_CHAR)))
+ return fn+1;
+ return (char*) p;
+}
+
+static inline bool pa_is_path_absolute(const char *fn)
+{
+ return *fn == PA_PATH_SEP_CHAR;
+}
+
+static inline char* pa_maybe_prefix_path(const char *path, const char *prefix)
+{
+ if (pa_is_path_absolute(path))
+ return pa_xstrdup(path);
+ return pa_sprintf_malloc("%s" PA_PATH_SEP "%s", prefix, path);
+}
+
+static inline bool pa_endswith(const char *s, const char *sfx)
+{
+ size_t l1, l2;
+ l1 = strlen(s);
+ l2 = strlen(sfx);
+ return l1 >= l2 && pa_streq(s + l1 - l2, sfx);
+}
+
+static inline char *pa_replace(const char*s, const char*a, const char *b)
+{
+ struct pa_array res;
+ size_t an, bn;
+
+ an = strlen(a);
+ bn = strlen(b);
+ pa_array_init(&res, an);
+
+ for (;;) {
+ const char *p;
+
+ if (!(p = strstr(s, a)))
+ break;
+
+ pa_array_add_data(&res, s, p-s);
+ pa_array_add_data(&res, b, bn);
+ s = p + an;
+ }
+ pa_array_add_data(&res, s, strlen(s) + 1);
+ return res.data;
+}
+
+static inline char *pa_split(const char *c, const char *delimiter, const char**state)
+{
+ const char *current = *state ? *state : c;
+ size_t l;
+ if (!*current)
+ return NULL;
+ l = strcspn(current, delimiter);
+ *state = current+l;
+ if (**state)
+ (*state)++;
+ return pa_xstrndup(current, l);
+}
+
+static inline char *pa_split_spaces(const char *c, const char **state)
+{
+ const char *current = *state ? *state : c;
+ size_t l;
+ if (!*current || *c == 0)
+ return NULL;
+ current += strspn(current, PA_WHITESPACE);
+ l = strcspn(current, PA_WHITESPACE);
+ *state = current+l;
+ return pa_xstrndup(current, l);
+}
+
+static inline char **pa_split_spaces_strv(const char *s)
+{
+ char **t, *e;
+ unsigned i = 0, n = 8;
+ const char *state = NULL;
+
+ t = pa_xnew(char*, n);
+ while ((e = pa_split_spaces(s, &state))) {
+ t[i++] = e;
+ if (i >= n) {
+ n *= 2;
+ t = pa_xrenew(char*, t, n);
+ }
+ }
+ if (i <= 0) {
+ pa_xfree(t);
+ return NULL;
+ }
+ t[i] = NULL;
+ return t;
+}
+
+static inline char* pa_str_strip_suffix(const char *str, const char *suffix)
+{
+ size_t str_l, suf_l, prefix;
+ char *ret;
+
+ str_l = strlen(str);
+ suf_l = strlen(suffix);
+
+ if (str_l < suf_l)
+ return NULL;
+ prefix = str_l - suf_l;
+ if (!pa_streq(&str[prefix], suffix))
+ return NULL;
+ ret = pa_xmalloc(prefix + 1);
+ memcpy(ret, str, prefix);
+ ret[prefix] = '\0';
+ return ret;
+}
+
+static inline const char *pa_split_in_place(const char *c, const char *delimiter, size_t *n, const char**state)
+{
+ const char *current = *state ? *state : c;
+ size_t l;
+ if (!*current)
+ return NULL;
+ l = strcspn(current, delimiter);
+ *state = current+l;
+ if (**state)
+ (*state)++;
+ *n = l;
+ return current;
+}
+
+static inline const char *pa_split_spaces_in_place(const char *c, size_t *n, const char **state)
+{
+ const char *current = *state ? *state : c;
+ size_t l;
+ if (!*current || *c == 0)
+ return NULL;
+ current += strspn(current, PA_WHITESPACE);
+ l = strcspn(current, PA_WHITESPACE);
+ *state = current+l;
+ *n = l;
+ return current;
+}
+
+static inline bool pa_str_in_list_spaces(const char *haystack, const char *needle)
+{
+ const char *s;
+ size_t n;
+ const char *state = NULL;
+
+ if (!haystack || !needle)
+ return false;
+
+ while ((s = pa_split_spaces_in_place(haystack, &n, &state))) {
+ if (pa_strneq(needle, s, n))
+ return true;
+ }
+
+ return false;
+}
+
+static inline char *pa_strip(char *s)
+{
+ char *e, *l = NULL;
+ s += strspn(s, PA_WHITESPACE);
+ for (e = s; *e; e++)
+ if (!strchr(PA_WHITESPACE, *e))
+ l = e;
+ if (l)
+ *(l+1) = 0;
+ else
+ *s = 0;
+ return s;
+}
+
+static inline int pa_atod(const char *s, double *ret_d)
+{
+ if (spa_atod(s, ret_d) && !isnan(*ret_d))
+ return 0;
+ errno = EINVAL;
+ return -1;
+}
+static inline int pa_atoi(const char *s, int32_t *ret_i)
+{
+ if (spa_atoi32(s, ret_i, 0))
+ return 0;
+ errno = EINVAL;
+ return -1;
+}
+static inline int pa_atou(const char *s, uint32_t *ret_u)
+{
+ if (spa_atou32(s, ret_u, 0))
+ return 0;
+ errno = EINVAL;
+ return -1;
+}
+static inline int pa_atol(const char *s, long *ret_l)
+{
+ int64_t res;
+ if (spa_atoi64(s, &res, 0)) {
+ *ret_l = res;
+ if (*ret_l == res)
+ return 0;
+ }
+ errno = EINVAL;
+ return -1;
+}
+
+static inline int pa_parse_boolean(const char *v)
+{
+ if (pa_streq(v, "1") || !strcasecmp(v, "y") || !strcasecmp(v, "t")
+ || !strcasecmp(v, "yes") || !strcasecmp(v, "true") || !strcasecmp(v, "on"))
+ return 1;
+ else if (pa_streq(v, "0") || !strcasecmp(v, "n") || !strcasecmp(v, "f")
+ || !strcasecmp(v, "no") || !strcasecmp(v, "false") || !strcasecmp(v, "off"))
+ return 0;
+ errno = EINVAL;
+ return -1;
+}
+
+static inline const char *pa_yes_no(bool b) {
+ return b ? "yes" : "no";
+}
+
+static inline const char *pa_strna(const char *x) {
+ return x ? x : "n/a";
+}
+
+static inline pa_sample_spec* pa_sample_spec_init(pa_sample_spec *spec)
+{
+ spec->format = PA_SAMPLE_INVALID;
+ spec->rate = 0;
+ spec->channels = 0;
+ return spec;
+}
+
+static inline char *pa_readlink(const char *p) {
+#ifdef HAVE_READLINK
+ size_t l = 100;
+
+ for (;;) {
+ char *c;
+ ssize_t n;
+
+ c = pa_xmalloc(l);
+
+ if ((n = readlink(p, c, l-1)) < 0) {
+ pa_xfree(c);
+ return NULL;
+ }
+
+ if ((size_t) n < l-1) {
+ c[n] = 0;
+ return c;
+ }
+
+ pa_xfree(c);
+ l *= 2;
+ }
+#else
+ return NULL;
+#endif
+}
+
+#include <spa/support/i18n.h>
+
+extern struct spa_i18n *acp_i18n;
+
+#define _(String) spa_i18n_text(acp_i18n, String)
+#ifdef gettext_noop
+#define N_(String) gettext_noop(String)
+#else
+#define N_(String) (String)
+#endif
+
+#include "channelmap.h"
+#include "volume.h"
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PULSE_COMPAT_H */
diff --git a/spa/plugins/alsa/acp/conf-parser.c b/spa/plugins/alsa/acp/conf-parser.c
new file mode 100644
index 0000000..b4d4d47
--- /dev/null
+++ b/spa/plugins/alsa/acp/conf-parser.c
@@ -0,0 +1,387 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+
+ 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/>.
+***/
+
+#include "config.h"
+
+#include <dirent.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include "compat.h"
+
+#include "conf-parser.h"
+
+#define WHITESPACE " \t\n"
+#define COMMENTS "#;\n"
+
+/* Run the user supplied parser for an assignment */
+static int normal_assignment(pa_config_parser_state *state) {
+ const pa_config_item *item;
+
+ pa_assert(state);
+
+ for (item = state->item_table; item->parse; item++) {
+
+ if (item->lvalue && !pa_streq(state->lvalue, item->lvalue))
+ continue;
+
+ if (item->section && !state->section)
+ continue;
+
+ if (item->section && !pa_streq(state->section, item->section))
+ continue;
+
+ state->data = item->data;
+
+ return item->parse(state);
+ }
+
+ pa_log("[%s:%u] Unknown lvalue '%s' in section '%s'.", state->filename, state->lineno, state->lvalue, pa_strna(state->section));
+
+ return -1;
+}
+
+/* Parse a proplist entry. */
+static int proplist_assignment(pa_config_parser_state *state) {
+ pa_assert(state);
+ pa_assert(state->proplist);
+
+ if (pa_proplist_sets(state->proplist, state->lvalue, state->rvalue) < 0) {
+ pa_log("[%s:%u] Failed to parse a proplist entry: %s = %s", state->filename, state->lineno, state->lvalue, state->rvalue);
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Parse a variable assignment line */
+static int parse_line(pa_config_parser_state *state) {
+ char *c;
+
+ state->lvalue = state->buf + strspn(state->buf, WHITESPACE);
+
+ if ((c = strpbrk(state->lvalue, COMMENTS)))
+ *c = 0;
+
+ if (!*state->lvalue)
+ return 0;
+
+ if (pa_startswith(state->lvalue, ".include ")) {
+ char *path = NULL, *fn;
+ int r;
+
+ fn = pa_strip(state->lvalue + 9);
+ if (!pa_is_path_absolute(fn)) {
+ const char *k;
+ if ((k = strrchr(state->filename, '/'))) {
+ char *dir = pa_xstrndup(state->filename, k - state->filename);
+ fn = path = pa_sprintf_malloc("%s" PA_PATH_SEP "%s", dir, fn);
+ pa_xfree(dir);
+ }
+ }
+
+ r = pa_config_parse(fn, NULL, state->item_table, state->proplist, false, state->userdata);
+ pa_xfree(path);
+ return r;
+ }
+
+ if (*state->lvalue == '[') {
+ size_t k;
+
+ k = strlen(state->lvalue);
+ pa_assert(k > 0);
+
+ if (state->lvalue[k-1] != ']') {
+ pa_log("[%s:%u] Invalid section header.", state->filename, state->lineno);
+ return -1;
+ }
+
+ pa_xfree(state->section);
+ state->section = pa_xstrndup(state->lvalue + 1, k-2);
+
+ if (pa_streq(state->section, "Properties")) {
+ if (!state->proplist) {
+ pa_log("[%s:%u] \"Properties\" section is not allowed in this file.", state->filename, state->lineno);
+ return -1;
+ }
+
+ state->in_proplist = true;
+ } else
+ state->in_proplist = false;
+
+ return 0;
+ }
+
+ if (!(state->rvalue = strchr(state->lvalue, '='))) {
+ pa_log("[%s:%u] Missing '='.", state->filename, state->lineno);
+ return -1;
+ }
+
+ *state->rvalue = 0;
+ state->rvalue++;
+
+ state->lvalue = pa_strip(state->lvalue);
+ state->rvalue = pa_strip(state->rvalue);
+
+ if (state->in_proplist)
+ return proplist_assignment(state);
+ else
+ return normal_assignment(state);
+}
+
+#ifndef OS_IS_WIN32
+static int conf_filter(const struct dirent *entry) {
+ return pa_endswith(entry->d_name, ".conf");
+}
+#endif
+
+/* Go through the file and parse each line */
+int pa_config_parse(const char *filename, FILE *f, const pa_config_item *t, pa_proplist *proplist, bool use_dot_d,
+ void *userdata) {
+ int r = -1;
+ bool do_close = !f;
+ pa_config_parser_state state;
+
+ pa_assert(filename);
+ pa_assert(t);
+
+ pa_zero(state);
+
+ if (!f && !(f = pa_fopen_cloexec(filename, "r"))) {
+ if (errno == ENOENT) {
+ pa_log_debug("Failed to open configuration file '%s': %s", filename, pa_cstrerror(errno));
+ r = 0;
+ goto finish;
+ }
+
+ pa_log_warn("Failed to open configuration file '%s': %s", filename, pa_cstrerror(errno));
+ goto finish;
+ }
+ pa_log_debug("Parsing configuration file '%s'", filename);
+
+ state.filename = filename;
+ state.item_table = t;
+ state.userdata = userdata;
+
+ if (proplist)
+ state.proplist = pa_proplist_new();
+
+ while (!feof(f)) {
+ if (!fgets(state.buf, sizeof(state.buf), f)) {
+ if (feof(f))
+ break;
+
+ pa_log_warn("Failed to read configuration file '%s': %s", filename, pa_cstrerror(errno));
+ goto finish;
+ }
+
+ state.lineno++;
+
+ if (parse_line(&state) < 0)
+ goto finish;
+ }
+
+ if (proplist)
+ pa_proplist_update(proplist, PA_UPDATE_REPLACE, state.proplist);
+
+ r = 0;
+
+finish:
+ if (state.proplist)
+ pa_proplist_free(state.proplist);
+
+ pa_xfree(state.section);
+
+ if (do_close && f)
+ fclose(f);
+
+ if (use_dot_d) {
+#ifdef OS_IS_WIN32
+ char *dir_name = pa_sprintf_malloc("%s.d", filename);
+ char *pattern = pa_sprintf_malloc("%s\\*.conf", dir_name);
+ HANDLE fh;
+ WIN32_FIND_DATA wfd;
+
+ fh = FindFirstFile(pattern, &wfd);
+ if (fh != INVALID_HANDLE_VALUE) {
+ do {
+ if (!(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
+ char *filename2 = pa_sprintf_malloc("%s\\%s", dir_name, wfd.cFileName);
+ pa_config_parse(filename2, NULL, t, proplist, false, userdata);
+ pa_xfree(filename2);
+ }
+ } while (FindNextFile(fh, &wfd));
+ FindClose(fh);
+ } else {
+ DWORD err = GetLastError();
+
+ if (err == ERROR_PATH_NOT_FOUND) {
+ pa_log_debug("Pattern %s did not match any files, ignoring.", pattern);
+ } else {
+ LPVOID msgbuf;
+ DWORD fret = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&msgbuf, 0, NULL);
+
+ if (fret != 0) {
+ pa_log_warn("FindFirstFile(%s) failed with error %ld (%s), ignoring.", pattern, err, (char*)msgbuf);
+ LocalFree(msgbuf);
+ } else {
+ pa_log_warn("FindFirstFile(%s) failed with error %ld, ignoring.", pattern, err);
+ pa_log_warn("FormatMessage failed with error %ld", GetLastError());
+ }
+ }
+ }
+
+ pa_xfree(pattern);
+ pa_xfree(dir_name);
+#else
+ char *dir_name;
+ int n;
+ struct dirent **entries = NULL;
+
+ dir_name = pa_sprintf_malloc("%s.d", filename);
+
+ n = scandir(dir_name, &entries, conf_filter, alphasort);
+ if (n >= 0) {
+ int i;
+
+ for (i = 0; i < n; i++) {
+ char *filename2;
+
+ filename2 = pa_sprintf_malloc("%s" PA_PATH_SEP "%s", dir_name, entries[i]->d_name);
+ pa_config_parse(filename2, NULL, t, proplist, false, userdata);
+ pa_xfree(filename2);
+
+ free(entries[i]);
+ }
+
+ free(entries);
+ } else {
+ if (errno == ENOENT)
+ pa_log_debug("%s does not exist, ignoring.", dir_name);
+ else
+ pa_log_warn("scandir(\"%s\") failed: %s", dir_name, pa_cstrerror(errno));
+ }
+
+ pa_xfree(dir_name);
+#endif
+ }
+
+ return r;
+}
+
+int pa_config_parse_int(pa_config_parser_state *state) {
+ int *i;
+ int32_t k;
+
+ pa_assert(state);
+
+ i = state->data;
+
+ if (pa_atoi(state->rvalue, &k) < 0) {
+ pa_log("[%s:%u] Failed to parse numeric value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *i = (int) k;
+ return 0;
+}
+
+int pa_config_parse_unsigned(pa_config_parser_state *state) {
+ unsigned *u;
+ uint32_t k;
+
+ pa_assert(state);
+
+ u = state->data;
+
+ if (pa_atou(state->rvalue, &k) < 0) {
+ pa_log("[%s:%u] Failed to parse numeric value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *u = (unsigned) k;
+ return 0;
+}
+
+int pa_config_parse_size(pa_config_parser_state *state) {
+ size_t *i;
+ uint32_t k;
+
+ pa_assert(state);
+
+ i = state->data;
+
+ if (pa_atou(state->rvalue, &k) < 0) {
+ pa_log("[%s:%u] Failed to parse numeric value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *i = (size_t) k;
+ return 0;
+}
+
+int pa_config_parse_bool(pa_config_parser_state *state) {
+ int k;
+ bool *b;
+
+ pa_assert(state);
+
+ b = state->data;
+
+ if ((k = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Failed to parse boolean value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *b = !!k;
+
+ return 0;
+}
+
+int pa_config_parse_not_bool(pa_config_parser_state *state) {
+ int k;
+ bool *b;
+
+ pa_assert(state);
+
+ b = state->data;
+
+ if ((k = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Failed to parse boolean value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *b = !k;
+
+ return 0;
+}
+
+int pa_config_parse_string(pa_config_parser_state *state) {
+ char **s;
+
+ pa_assert(state);
+
+ s = state->data;
+
+ pa_xfree(*s);
+ *s = *state->rvalue ? pa_xstrdup(state->rvalue) : NULL;
+ return 0;
+}
diff --git a/spa/plugins/alsa/acp/conf-parser.h b/spa/plugins/alsa/acp/conf-parser.h
new file mode 100644
index 0000000..4d19d7b
--- /dev/null
+++ b/spa/plugins/alsa/acp/conf-parser.h
@@ -0,0 +1,88 @@
+#ifndef fooconfparserhfoo
+#define fooconfparserhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+
+ 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/>.
+***/
+
+#include <stdio.h>
+#include <stdbool.h>
+
+#include "compat.h"
+
+/* An abstract parser for simple, line based, shallow configuration
+ * files consisting of variable assignments only. */
+
+typedef struct pa_config_parser_state pa_config_parser_state;
+
+typedef int (*pa_config_parser_cb_t)(pa_config_parser_state *state);
+
+/* Wraps info for parsing a specific configuration variable */
+typedef struct pa_config_item {
+ const char *lvalue; /* name of the variable */
+ pa_config_parser_cb_t parse; /* Function that is called to parse the variable's value */
+ void *data; /* Where to store the variable's data */
+ const char *section;
+} pa_config_item;
+
+struct pa_config_parser_state {
+ const char *filename;
+ unsigned lineno;
+ char *section;
+ char *lvalue;
+ char *rvalue;
+ void *data; /* The data pointer of the current pa_config_item. */
+ void *userdata; /* The pointer that was given to pa_config_parse(). */
+
+ /* Private data to be used only by conf-parser.c. */
+ const pa_config_item *item_table;
+ char buf[4096];
+ pa_proplist *proplist;
+ bool in_proplist;
+};
+
+/* The configuration file parsing routine. Expects a table of
+ * pa_config_items in *t that is terminated by an item where lvalue is
+ * NULL.
+ *
+ * If use_dot_d is true, then after parsing the file named by the filename
+ * argument, the function will parse all files ending with ".conf" in
+ * alphabetical order from a directory whose name is filename + ".d", if such
+ * directory exists.
+ *
+ * Some configuration files may contain a Properties section, which
+ * is a bit special. Normally all accepted lvalues must be predefined
+ * in the pa_config_item table, but in the Properties section the
+ * pa_config_item table is ignored, and all lvalues are accepted (as
+ * long as they are valid proplist keys). If the proplist pointer is
+ * non-NULL, the parser will parse any section named "Properties" as
+ * properties, and those properties will be merged into the given
+ * proplist. If proplist is NULL, then sections named "Properties"
+ * are not allowed at all in the configuration file. */
+int pa_config_parse(const char *filename, FILE *f, const pa_config_item *t, pa_proplist *proplist, bool use_dot_d,
+ void *userdata);
+
+/* Generic parsers for integers, size_t, booleans and strings */
+int pa_config_parse_int(pa_config_parser_state *state);
+int pa_config_parse_unsigned(pa_config_parser_state *state);
+int pa_config_parse_size(pa_config_parser_state *state);
+int pa_config_parse_bool(pa_config_parser_state *state);
+int pa_config_parse_not_bool(pa_config_parser_state *state);
+int pa_config_parse_string(pa_config_parser_state *state);
+
+#endif
diff --git a/spa/plugins/alsa/acp/device-port.h b/spa/plugins/alsa/acp/device-port.h
new file mode 100644
index 0000000..d6bdc22
--- /dev/null
+++ b/spa/plugins/alsa/acp/device-port.h
@@ -0,0 +1,119 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+
+#ifndef PULSE_DEVICE_PORT_H
+#define PULSE_DEVICE_PORT_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+
+#include "compat.h"
+
+typedef struct pa_card pa_card;
+typedef struct pa_device_port pa_device_port;
+
+/** Port type. \since 14.0 */
+typedef enum pa_device_port_type {
+ PA_DEVICE_PORT_TYPE_UNKNOWN = 0,
+ PA_DEVICE_PORT_TYPE_AUX = 1,
+ PA_DEVICE_PORT_TYPE_SPEAKER = 2,
+ PA_DEVICE_PORT_TYPE_HEADPHONES = 3,
+ PA_DEVICE_PORT_TYPE_LINE = 4,
+ PA_DEVICE_PORT_TYPE_MIC = 5,
+ PA_DEVICE_PORT_TYPE_HEADSET = 6,
+ PA_DEVICE_PORT_TYPE_HANDSET = 7,
+ PA_DEVICE_PORT_TYPE_EARPIECE = 8,
+ PA_DEVICE_PORT_TYPE_SPDIF = 9,
+ PA_DEVICE_PORT_TYPE_HDMI = 10,
+ PA_DEVICE_PORT_TYPE_TV = 11,
+ PA_DEVICE_PORT_TYPE_RADIO = 12,
+ PA_DEVICE_PORT_TYPE_VIDEO = 13,
+ PA_DEVICE_PORT_TYPE_USB = 14,
+ PA_DEVICE_PORT_TYPE_BLUETOOTH = 15,
+ PA_DEVICE_PORT_TYPE_PORTABLE = 16,
+ PA_DEVICE_PORT_TYPE_HANDSFREE = 17,
+ PA_DEVICE_PORT_TYPE_CAR = 18,
+ PA_DEVICE_PORT_TYPE_HIFI = 19,
+ PA_DEVICE_PORT_TYPE_PHONE = 20,
+ PA_DEVICE_PORT_TYPE_NETWORK = 21,
+ PA_DEVICE_PORT_TYPE_ANALOG = 22,
+} pa_device_port_type_t;
+
+struct pa_device_port {
+ struct acp_port port;
+
+ pa_card *card;
+
+ char *name;
+ char *description;
+ char *preferred_profile;
+ pa_device_port_type_t type;
+
+ unsigned priority;
+ pa_available_t available; /* PA_AVAILABLE_UNKNOWN, PA_AVAILABLE_NO or PA_AVAILABLE_YES */
+ char *availability_group; /* a string identifier which determine the group of devices handling the available state simultaneously */
+
+ pa_direction_t direction;
+ int64_t latency_offset;
+
+ pa_proplist *proplist;
+ pa_hashmap *profiles;
+ pa_dynarray prof;
+
+ pa_dynarray devices;
+
+ void (*impl_free)(struct pa_device_port *port);
+ void *user_data;
+};
+
+#define PA_DEVICE_PORT_DATA(p) (p->user_data);
+
+typedef struct pa_device_port_new_data {
+ char *name;
+ char *description;
+ pa_available_t available;
+ char *availability_group;
+ pa_direction_t direction;
+ pa_device_port_type_t type;
+} pa_device_port_new_data;
+
+pa_device_port_new_data *pa_device_port_new_data_init(pa_device_port_new_data *data);
+void pa_device_port_new_data_set_name(pa_device_port_new_data *data, const char *name);
+void pa_device_port_new_data_set_description(pa_device_port_new_data *data, const char *description);
+void pa_device_port_new_data_set_available(pa_device_port_new_data *data, pa_available_t available);
+void pa_device_port_new_data_set_availability_group(pa_device_port_new_data *data, const char *group);
+void pa_device_port_new_data_set_direction(pa_device_port_new_data *data, pa_direction_t direction);
+void pa_device_port_new_data_set_type(pa_device_port_new_data *data, pa_device_port_type_t type);
+void pa_device_port_new_data_done(pa_device_port_new_data *data);
+
+pa_device_port *pa_device_port_new(pa_core *c, pa_device_port_new_data *data, size_t extra);
+void pa_device_port_free(pa_device_port *port);
+
+void pa_device_port_set_available(pa_device_port *p, pa_available_t status);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PULSE_DEVICE_PORT_H */
diff --git a/spa/plugins/alsa/acp/dynarray.h b/spa/plugins/alsa/acp/dynarray.h
new file mode 100644
index 0000000..762c84c
--- /dev/null
+++ b/spa/plugins/alsa/acp/dynarray.h
@@ -0,0 +1,159 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_DYNARRAY_H
+#define PA_DYNARRAY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "array.h"
+
+typedef struct pa_dynarray_item {
+ void *ptr;
+} pa_dynarray_item;
+
+typedef struct pa_dynarray {
+ pa_array array;
+ pa_free_cb_t free_cb;
+} pa_dynarray;
+
+static inline void pa_dynarray_init(pa_dynarray *array, pa_free_cb_t free_cb)
+{
+ pa_array_init(&array->array, 16);
+ array->free_cb = free_cb;
+}
+
+static inline void pa_dynarray_item_free(pa_dynarray *array, pa_dynarray_item *item)
+{
+ if (array->free_cb)
+ array->free_cb(item->ptr);
+}
+
+static inline void pa_dynarray_clear(pa_dynarray *array)
+{
+ pa_dynarray_item *item;
+ pa_array_for_each(item, &array->array)
+ pa_dynarray_item_free(array, item);
+ pa_array_clear(&array->array);
+}
+
+static inline pa_dynarray* pa_dynarray_new(pa_free_cb_t free_cb)
+{
+ pa_dynarray *d = calloc(1, sizeof(*d));
+ pa_dynarray_init(d, free_cb);
+ return d;
+}
+
+static inline void pa_dynarray_free(pa_dynarray *array)
+{
+ pa_dynarray_clear(array);
+ free(array);
+}
+
+static inline void pa_dynarray_append(pa_dynarray *array, void *p)
+{
+ pa_dynarray_item *item = pa_array_add(&array->array, sizeof(*item));
+ item->ptr = p;
+}
+
+static inline pa_dynarray_item *pa_dynarray_find_item(pa_dynarray *array, void *p)
+{
+ pa_dynarray_item *item;
+ pa_array_for_each(item, &array->array) {
+ if (item->ptr == p)
+ return item;
+ }
+ return NULL;
+}
+
+static inline pa_dynarray_item *pa_dynarray_get_item(pa_dynarray *array, unsigned i)
+{
+ if (!pa_array_check_index(&array->array, i, pa_dynarray_item))
+ return NULL;
+ return pa_array_get_unchecked(&array->array, i, pa_dynarray_item);
+}
+
+static inline void *pa_dynarray_get(pa_dynarray *array, unsigned i)
+{
+ pa_dynarray_item *item = pa_dynarray_get_item(array, i);
+ if (item == NULL)
+ return NULL;
+ return item->ptr;
+}
+
+static inline int pa_dynarray_insert_by_index(pa_dynarray *array, void *p, unsigned i)
+{
+ unsigned j, len;
+ pa_dynarray_item *item;
+
+ len = pa_array_get_len(&array->array, pa_dynarray_item);
+
+ if (i > len)
+ return -EINVAL;
+
+ item = pa_array_add(&array->array, sizeof(*item));
+ for (j = len; j > i; j--) {
+ item--;
+ item[1].ptr = item[0].ptr;
+ }
+ item->ptr = p;
+ return 0;
+}
+
+static inline int pa_dynarray_remove_by_index(pa_dynarray *array, unsigned i)
+{
+ pa_dynarray_item *item = pa_dynarray_get_item(array, i);
+ if (item == NULL)
+ return -ENOENT;
+ pa_dynarray_item_free(array, item);
+ pa_array_remove(&array->array, item);
+ return 0;
+}
+
+static inline int pa_dynarray_remove_by_data(pa_dynarray *array, void *p)
+{
+ pa_dynarray_item *item = pa_dynarray_find_item(array, p);
+ if (item == NULL)
+ return -ENOENT;
+ pa_dynarray_item_free(array, item);
+ pa_array_remove(&array->array, item);
+ return 0;
+}
+
+static inline unsigned pa_dynarray_size(pa_dynarray *array)
+{
+ return pa_array_get_len(&array->array, pa_dynarray_item);
+}
+
+#define PA_DYNARRAY_FOREACH(elem, array, idx) \
+ for ((idx) = 0; ((elem) = pa_dynarray_get(array, idx)); (idx)++)
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_DYNARRAY_H */
diff --git a/spa/plugins/alsa/acp/hashmap.h b/spa/plugins/alsa/acp/hashmap.h
new file mode 100644
index 0000000..d8dbadb
--- /dev/null
+++ b/spa/plugins/alsa/acp/hashmap.h
@@ -0,0 +1,219 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_HASHMAP_H
+#define PA_HASHMAP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "array.h"
+
+typedef unsigned (*pa_hash_func_t)(const void *p);
+typedef int (*pa_compare_func_t)(const void *a, const void *b);
+
+typedef struct pa_hashmap_item {
+ void *key;
+ void *value;
+} pa_hashmap_item;
+
+typedef struct pa_hashmap {
+ pa_array array;
+ pa_hash_func_t hash_func;
+ pa_compare_func_t compare_func;
+ pa_free_cb_t key_free_func;
+ pa_free_cb_t value_free_func;
+} pa_hashmap;
+
+static inline pa_hashmap *pa_hashmap_new(pa_hash_func_t hash_func, pa_compare_func_t compare_func)
+{
+ pa_hashmap *m = calloc(1, sizeof(pa_hashmap));
+ pa_array_init(&m->array, 16);
+ m->hash_func = hash_func;
+ m->compare_func = compare_func;
+ return m;
+}
+
+static inline pa_hashmap *pa_hashmap_new_full(pa_hash_func_t hash_func, pa_compare_func_t compare_func,
+ pa_free_cb_t key_free_func, pa_free_cb_t value_free_func)
+{
+ pa_hashmap *m = pa_hashmap_new(hash_func, compare_func);
+ m->key_free_func = key_free_func;
+ m->value_free_func = value_free_func;
+ return m;
+}
+
+static inline void pa_hashmap_item_free(pa_hashmap *h, pa_hashmap_item *item)
+{
+ if (h->key_free_func && item->key)
+ h->key_free_func(item->key);
+ if (h->value_free_func && item->value)
+ h->value_free_func(item->value);
+}
+
+static inline void pa_hashmap_remove_all(pa_hashmap *h)
+{
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array)
+ pa_hashmap_item_free(h, item);
+ pa_array_reset(&h->array);
+}
+
+static inline void pa_hashmap_free(pa_hashmap *h)
+{
+ pa_hashmap_remove_all(h);
+ pa_array_clear(&h->array);
+ free(h);
+}
+
+static inline pa_hashmap_item* pa_hashmap_find_free(pa_hashmap *h)
+{
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array) {
+ if (item->key == NULL)
+ return item;
+ }
+ return pa_array_add(&h->array, sizeof(*item));
+}
+
+static inline pa_hashmap_item* pa_hashmap_find(const pa_hashmap *h, const void *key)
+{
+ pa_hashmap_item *item = NULL;
+ pa_array_for_each(item, &h->array) {
+ if (item->key != NULL && h->compare_func(item->key, key) == 0)
+ return item;
+ }
+ return NULL;
+}
+
+static inline void* pa_hashmap_get(const pa_hashmap *h, const void *key)
+{
+ const pa_hashmap_item *item = pa_hashmap_find(h, key);
+ if (item == NULL)
+ return NULL;
+ return item->value;
+}
+
+static inline int pa_hashmap_put(pa_hashmap *h, void *key, void *value)
+{
+ pa_hashmap_item *item = pa_hashmap_find(h, key);
+ if (item != NULL)
+ return -1;
+ item = pa_hashmap_find_free(h);
+ item->key = key;
+ item->value = value;
+ return 0;
+}
+
+static inline void* pa_hashmap_remove(pa_hashmap *h, const void *key)
+{
+ pa_hashmap_item *item = pa_hashmap_find(h, key);
+ void *value;
+ if (item == NULL)
+ return NULL;
+ value = item->value;
+ if (h->key_free_func)
+ h->key_free_func(item->key);
+ item->key = NULL;
+ item->value = NULL;
+ return value;
+}
+
+static inline int pa_hashmap_remove_and_free(pa_hashmap *h, const void *key)
+{
+ void *val = pa_hashmap_remove(h, key);
+ if (val && h->value_free_func)
+ h->value_free_func(val);
+ return val ? 0 : -1;
+}
+
+static inline void *pa_hashmap_first(const pa_hashmap *h)
+{
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array) {
+ if (item->key != NULL)
+ return item->value;
+ }
+ return NULL;
+}
+
+static inline void *pa_hashmap_iterate(const pa_hashmap *h, void **state, const void **key)
+{
+ pa_hashmap_item *it = *state;
+ if (it == NULL)
+ *state = pa_array_first(&h->array);
+ do {
+ it = *state;
+ if (!pa_array_check(&h->array, it))
+ return NULL;
+ *state = it + 1;
+ } while (it->key == NULL);
+ if (key)
+ *key = it->key;
+ return it->value;
+}
+
+static inline bool pa_hashmap_isempty(const pa_hashmap *h)
+{
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array)
+ if (item->key != NULL)
+ return false;
+ return true;
+}
+
+static inline unsigned pa_hashmap_size(const pa_hashmap *h)
+{
+ unsigned count = 0;
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array)
+ if (item->key != NULL)
+ count++;
+ return count;
+}
+
+static inline void pa_hashmap_sort(pa_hashmap *h,
+ int (*compar)(const void *, const void *))
+{
+ qsort((void*)h->array.data,
+ pa_array_get_len(&h->array, pa_hashmap_item),
+ sizeof(pa_hashmap_item), compar);
+}
+
+#define PA_HASHMAP_FOREACH(e, h, state) \
+ for ((state) = NULL, (e) = pa_hashmap_iterate((h), &(state), NULL); \
+ (e); (e) = pa_hashmap_iterate((h), &(state), NULL))
+
+/* A macro to ease iteration through all key, value pairs */
+#define PA_HASHMAP_FOREACH_KV(k, e, h, state) \
+ for ((state) = NULL, (e) = pa_hashmap_iterate((h), &(state), (const void **) &(k)); \
+ (e); (e) = pa_hashmap_iterate((h), &(state), (const void **) &(k)))
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_HASHMAP_H */
diff --git a/spa/plugins/alsa/acp/idxset.h b/spa/plugins/alsa/acp/idxset.h
new file mode 100644
index 0000000..6e88a84
--- /dev/null
+++ b/spa/plugins/alsa/acp/idxset.h
@@ -0,0 +1,200 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_IDXSET_H
+#define PA_IDXSET_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "array.h"
+
+#define PA_IDXSET_INVALID ((uint32_t) -1)
+
+typedef unsigned (*pa_hash_func_t)(const void *p);
+typedef int (*pa_compare_func_t)(const void *a, const void *b);
+
+typedef struct pa_idxset_item {
+ void *ptr;
+} pa_idxset_item;
+
+typedef struct pa_idxset {
+ pa_array array;
+ pa_hash_func_t hash_func;
+ pa_compare_func_t compare_func;
+} pa_idxset;
+
+static inline unsigned pa_idxset_trivial_hash_func(const void *p)
+{
+ return PA_PTR_TO_UINT(p);
+}
+
+static inline int pa_idxset_trivial_compare_func(const void *a, const void *b)
+{
+ return a < b ? -1 : (a > b ? 1 : 0);
+}
+
+static inline unsigned pa_idxset_string_hash_func(const void *p)
+{
+ unsigned hash = 0;
+ const char *c;
+ for (c = p; *c; c++)
+ hash = 31 * hash + (unsigned) *c;
+ return hash;
+}
+
+static inline int pa_idxset_string_compare_func(const void *a, const void *b)
+{
+ return strcmp(a, b);
+}
+
+static inline pa_idxset *pa_idxset_new(pa_hash_func_t hash_func, pa_compare_func_t compare_func)
+{
+ pa_idxset *s = calloc(1, sizeof(pa_idxset));
+ pa_array_init(&s->array, 16);
+ s->hash_func = hash_func;
+ s->compare_func = compare_func;
+ return s;
+}
+
+static inline void pa_idxset_free(pa_idxset *s, pa_free_cb_t free_cb)
+{
+ if (free_cb) {
+ pa_idxset_item *item;
+ pa_array_for_each(item, &s->array)
+ free_cb(item->ptr);
+ }
+ pa_array_clear(&s->array);
+ free(s);
+}
+
+static inline pa_idxset_item* pa_idxset_find(const pa_idxset *s, const void *ptr)
+{
+ pa_idxset_item *item;
+ pa_array_for_each(item, &s->array) {
+ if (item->ptr == ptr)
+ return item;
+ }
+ return NULL;
+}
+
+static inline int pa_idxset_put(pa_idxset*s, void *p, uint32_t *idx)
+{
+ pa_idxset_item *item = pa_idxset_find(s, p);
+ int res = item ? -1 : 0;
+ if (item == NULL) {
+ item = pa_idxset_find(s, NULL);
+ if (item == NULL)
+ item = pa_array_add(&s->array, sizeof(*item));
+ item->ptr = p;
+ }
+ if (idx)
+ *idx = item - (pa_idxset_item*)s->array.data;
+ return res;
+}
+
+static inline pa_idxset *pa_idxset_copy(pa_idxset *s, pa_copy_func_t copy_func)
+{
+ pa_idxset_item *item;
+ pa_idxset *copy = pa_idxset_new(s->hash_func, s->compare_func);
+ pa_array_for_each(item, &s->array) {
+ if (item->ptr)
+ pa_idxset_put(copy, copy_func ? copy_func(item->ptr) : item->ptr, NULL);
+ }
+ return copy;
+}
+
+static inline bool pa_idxset_isempty(const pa_idxset *s)
+{
+ pa_idxset_item *item;
+ pa_array_for_each(item, &s->array)
+ if (item->ptr != NULL)
+ return false;
+ return true;
+}
+static inline unsigned pa_idxset_size(pa_idxset*s)
+{
+ unsigned count = 0;
+ pa_idxset_item *item;
+ pa_array_for_each(item, &s->array)
+ if (item->ptr != NULL)
+ count++;
+ return count;
+}
+
+static inline void *pa_idxset_search(pa_idxset *s, uint32_t *idx)
+{
+ pa_idxset_item *item;
+ for (item = pa_array_get_unchecked(&s->array, *idx, pa_idxset_item);
+ pa_array_check(&s->array, item); item++, (*idx)++) {
+ if (item->ptr != NULL)
+ return item->ptr;
+ }
+ *idx = PA_IDXSET_INVALID;
+ return NULL;
+}
+
+static inline void *pa_idxset_next(pa_idxset *s, uint32_t *idx)
+{
+ (*idx)++;;
+ return pa_idxset_search(s, idx);
+}
+
+static inline void* pa_idxset_first(pa_idxset *s, uint32_t *idx)
+{
+ uint32_t i = 0;
+ void *ptr = pa_idxset_search(s, &i);
+ if (idx)
+ *idx = i;
+ return ptr;
+}
+
+static inline void* pa_idxset_get_by_data(pa_idxset*s, const void *p, uint32_t *idx)
+{
+ pa_idxset_item *item = pa_idxset_find(s, p);
+ if (item == NULL)
+ return NULL;
+ if (idx)
+ *idx = item - (pa_idxset_item*)s->array.data;
+ return item->ptr;
+}
+
+static inline void* pa_idxset_get_by_index(pa_idxset*s, uint32_t idx)
+{
+ pa_idxset_item *item;
+ if (!pa_array_check_index(&s->array, idx, pa_idxset_item))
+ return NULL;
+ item = pa_array_get_unchecked(&s->array, idx, pa_idxset_item);
+ return item->ptr;
+}
+
+#define PA_IDXSET_FOREACH(e, s, idx) \
+ for ((e) = pa_idxset_first((s), &(idx)); (e); (e) = pa_idxset_next((s), &(idx)))
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_IDXSET_H */
diff --git a/spa/plugins/alsa/acp/llist.h b/spa/plugins/alsa/acp/llist.h
new file mode 100644
index 0000000..950375f
--- /dev/null
+++ b/spa/plugins/alsa/acp/llist.h
@@ -0,0 +1,109 @@
+#ifndef foollistfoo
+#define foollistfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1 of the
+ License, or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+/* Some macros for maintaining doubly linked lists */
+
+/* The head of the linked list. Use this in the structure that shall
+ * contain the head of the linked list */
+#define PA_LLIST_HEAD(t,name) \
+ t *name
+
+/* The pointers in the linked list's items. Use this in the item structure */
+#define PA_LLIST_FIELDS(t) \
+ t *next, *prev
+
+/* Initialize the list's head */
+#define PA_LLIST_HEAD_INIT(t,item) \
+ do { \
+ (item) = (t*) NULL; } \
+ while(0)
+
+/* Initialize a list item */
+#define PA_LLIST_INIT(t,item) \
+ do { \
+ t *_item = (item); \
+ pa_assert(_item); \
+ _item->prev = _item->next = NULL; \
+ } while(0)
+
+/* Prepend an item to the list */
+#define PA_LLIST_PREPEND(t,head,item) \
+ do { \
+ t **_head = &(head), *_item = (item); \
+ pa_assert(_item); \
+ if ((_item->next = *_head)) \
+ _item->next->prev = _item; \
+ _item->prev = NULL; \
+ *_head = _item; \
+ } while (0)
+
+/* Remove an item from the list */
+#define PA_LLIST_REMOVE(t,head,item) \
+ do { \
+ t **_head = &(head), *_item = (item); \
+ pa_assert(_item); \
+ if (_item->next) \
+ _item->next->prev = _item->prev; \
+ if (_item->prev) \
+ _item->prev->next = _item->next; \
+ else { \
+ pa_assert(*_head == _item); \
+ *_head = _item->next; \
+ } \
+ _item->next = _item->prev = NULL; \
+ } while(0)
+
+/* Find the head of the list */
+#define PA_LLIST_FIND_HEAD(t,item,head) \
+ do { \
+ t **_head = (head), *_item = (item); \
+ *_head = _item; \
+ pa_assert(_head); \
+ while ((*_head)->prev) \
+ *_head = (*_head)->prev; \
+ } while (0)
+
+/* Insert an item after another one (a = where, b = what) */
+#define PA_LLIST_INSERT_AFTER(t,head,a,b) \
+ do { \
+ t **_head = &(head), *_a = (a), *_b = (b); \
+ pa_assert(_b); \
+ if (!_a) { \
+ if ((_b->next = *_head)) \
+ _b->next->prev = _b; \
+ _b->prev = NULL; \
+ *_head = _b; \
+ } else { \
+ if ((_b->next = _a->next)) \
+ _b->next->prev = _b; \
+ _b->prev = _a; \
+ _a->next = _b; \
+ } \
+ } while (0)
+
+#define PA_LLIST_FOREACH(i,head) \
+ for (i = (head); i; i = i->next)
+
+#define PA_LLIST_FOREACH_SAFE(i,n,head) \
+ for (i = (head); i && ((n = i->next), 1); i = n)
+
+#endif
diff --git a/spa/plugins/alsa/acp/meson.build b/spa/plugins/alsa/acp/meson.build
new file mode 100644
index 0000000..0ec97e2
--- /dev/null
+++ b/spa/plugins/alsa/acp/meson.build
@@ -0,0 +1,22 @@
+acp_sources = [
+ 'acp.c',
+ 'compat.c',
+ 'alsa-mixer.c',
+ 'alsa-ucm.c',
+ 'alsa-util.c',
+ 'conf-parser.c',
+]
+
+acp_c_args = [
+ '-DHAVE_ALSA_UCM',
+ '-DHAVE_READLINK',
+]
+
+acp_lib = static_library(
+ 'acp',
+ acp_sources,
+ c_args : acp_c_args,
+ include_directories : [configinc, includes_inc ],
+ dependencies : [ spa_dep, alsa_dep, mathlib, ]
+ )
+acp_dep = declare_dependency(link_with: acp_lib)
diff --git a/spa/plugins/alsa/acp/proplist.h b/spa/plugins/alsa/acp/proplist.h
new file mode 100644
index 0000000..ed4cc5d
--- /dev/null
+++ b/spa/plugins/alsa/acp/proplist.h
@@ -0,0 +1,211 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_PROPLIST_H
+#define PA_PROPLIST_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+#include "array.h"
+#include "acp.h"
+
+#define PA_PROP_DEVICE_DESCRIPTION "device.description"
+
+#define PA_PROP_DEVICE_CLASS "device.class"
+
+#define PA_PROP_DEVICE_FORM_FACTOR "device.form_factor"
+
+#define PA_PROP_DEVICE_INTENDED_ROLES "device.intended_roles"
+
+#define PA_PROP_DEVICE_PROFILE_NAME "device.profile.name"
+
+#define PA_PROP_DEVICE_STRING "device.string"
+
+#define PA_PROP_DEVICE_API "device.api"
+
+#define PA_PROP_DEVICE_PRODUCT_NAME "device.product.name"
+
+#define PA_PROP_DEVICE_PROFILE_DESCRIPTION "device.profile.description"
+
+typedef struct pa_proplist_item {
+ char *key;
+ char *value;
+} pa_proplist_item;
+
+typedef struct pa_proplist {
+ struct pa_array array;
+} pa_proplist;
+
+static inline pa_proplist* pa_proplist_new(void)
+{
+ pa_proplist *p = calloc(1, sizeof(*p));
+ pa_array_init(&p->array, 16);
+ return p;
+}
+static inline pa_proplist_item* pa_proplist_item_find(const pa_proplist *p, const void *key)
+{
+ pa_proplist_item *item;
+ pa_array_for_each(item, &p->array) {
+ if (strcmp(key, item->key) == 0)
+ return item;
+ }
+ return NULL;
+}
+
+static inline void pa_proplist_item_free(pa_proplist_item* it)
+{
+ free(it->key);
+ free(it->value);
+}
+
+static inline void pa_proplist_clear(pa_proplist* p)
+{
+ pa_proplist_item *item;
+ pa_array_for_each(item, &p->array)
+ pa_proplist_item_free(item);
+ pa_array_reset(&p->array);
+}
+
+static inline void pa_proplist_free(pa_proplist* p)
+{
+ pa_proplist_clear(p);
+ pa_array_clear(&p->array);
+ free(p);
+}
+
+static inline unsigned pa_proplist_size(const pa_proplist *p)
+{
+ return pa_array_get_len(&p->array, pa_proplist_item);
+}
+
+static inline int pa_proplist_contains(const pa_proplist *p, const char *key)
+{
+ return pa_proplist_item_find(p, key) ? 1 : 0;
+}
+
+static inline int pa_proplist_sets(pa_proplist *p, const char *key, const char *value)
+{
+ pa_proplist_item *item = pa_proplist_item_find(p, key);
+ if (item != NULL)
+ pa_proplist_item_free(item);
+ else
+ item = pa_array_add(&p->array, sizeof(*item));
+ item->key = strdup(key);
+ item->value = strdup(value);
+ return 0;
+}
+
+static inline int pa_proplist_unset(pa_proplist *p, const char *key)
+{
+ pa_proplist_item *item = pa_proplist_item_find(p, key);
+ if (item == NULL)
+ return -ENOENT;
+ pa_proplist_item_free(item);
+ pa_array_remove(&p->array, item);
+ return 0;
+}
+
+static PA_PRINTF_FUNC(3,4) inline int pa_proplist_setf(pa_proplist *p, const char *key, const char *format, ...)
+{
+ pa_proplist_item *item = pa_proplist_item_find(p, key);
+ va_list args;
+ int res;
+
+ va_start(args, format);
+ if (item != NULL)
+ pa_proplist_item_free(item);
+ else
+ item = pa_array_add(&p->array, sizeof(*item));
+ item->key = strdup(key);
+ if ((res = vasprintf(&item->value, format, args)) < 0)
+ res = -errno;
+ va_end(args);
+ return res;
+}
+
+static inline const char *pa_proplist_gets(const pa_proplist *p, const char *key)
+{
+ pa_proplist_item *item = pa_proplist_item_find(p, key);
+ return item ? item->value : NULL;
+}
+
+typedef enum pa_update_mode {
+ PA_UPDATE_SET
+ /**< Replace the entire property list with the new one. Don't keep
+ * any of the old data around. */,
+ PA_UPDATE_MERGE
+ /**< Merge new property list into the existing one, not replacing
+ * any old entries if they share a common key with the new
+ * property list. */,
+ PA_UPDATE_REPLACE
+ /**< Merge new property list into the existing one, replacing all
+ * old entries that share a common key with the new property
+ * list. */
+} pa_update_mode_t;
+
+
+static inline void pa_proplist_update(pa_proplist *p, pa_update_mode_t mode, const pa_proplist *other)
+{
+ pa_proplist_item *item;
+
+ if (mode == PA_UPDATE_SET)
+ pa_proplist_clear(p);
+
+ pa_array_for_each(item, &other->array) {
+ if (mode == PA_UPDATE_MERGE && pa_proplist_contains(p, item->key))
+ continue;
+ pa_proplist_sets(p, item->key, item->value);
+ }
+}
+
+static inline pa_proplist* pa_proplist_new_dict(const struct acp_dict *dict)
+{
+ pa_proplist *p = pa_proplist_new();
+ if (dict) {
+ const struct acp_dict_item *item;
+ struct acp_dict_item *it;
+ acp_dict_for_each(item, dict) {
+ it = pa_array_add(&p->array, sizeof(*it));
+ it->key = strdup(item->key);
+ it->value = strdup(item->value);
+ }
+ }
+ return p;
+}
+
+static inline void pa_proplist_as_dict(const pa_proplist *p, struct acp_dict *dict)
+{
+ dict->n_items = pa_proplist_size(p);
+ dict->items = p->array.data;
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_PROPLIST_H */
diff --git a/spa/plugins/alsa/acp/volume.h b/spa/plugins/alsa/acp/volume.h
new file mode 100644
index 0000000..b916bd2
--- /dev/null
+++ b/spa/plugins/alsa/acp/volume.h
@@ -0,0 +1,207 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ 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/>.
+***/
+
+#ifndef PA_VOLUME_H
+#define PA_VOLUME_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <math.h>
+
+typedef uint32_t pa_volume_t;
+
+#define PA_VOLUME_MUTED ((pa_volume_t) 0U)
+#define PA_VOLUME_NORM ((pa_volume_t) 0x10000U)
+#define PA_VOLUME_MAX ((pa_volume_t) UINT32_MAX/2)
+
+#ifdef INFINITY
+#define PA_DECIBEL_MININFTY ((double) -INFINITY)
+#else
+#define PA_DECIBEL_MININFTY ((double) -200.0)
+#endif
+
+#define PA_CLAMP_VOLUME(v) (PA_CLAMP_UNLIKELY((v), PA_VOLUME_MUTED, PA_VOLUME_MAX))
+
+typedef struct pa_cvolume {
+ uint32_t channels; /**< Number of channels */
+ pa_volume_t values[PA_CHANNELS_MAX]; /**< Per-channel volume */
+} pa_cvolume;
+
+static inline double pa_volume_linear_to_dB(double v)
+{
+ return 20.0 * log10(v);
+}
+
+static inline double pa_sw_volume_to_linear(pa_volume_t v)
+{
+ double f;
+ if (v <= PA_VOLUME_MUTED)
+ return 0.0;
+ if (v == PA_VOLUME_NORM)
+ return 1.0;
+ f = ((double) v / PA_VOLUME_NORM);
+ return f*f*f;
+}
+
+static inline double pa_sw_volume_to_dB(pa_volume_t v)
+{
+ if (v <= PA_VOLUME_MUTED)
+ return PA_DECIBEL_MININFTY;
+ return pa_volume_linear_to_dB(pa_sw_volume_to_linear(v));
+}
+
+static inline double pa_volume_dB_to_linear(double v)
+{
+ return pow(10.0, v / 20.0);
+}
+
+static inline pa_volume_t pa_sw_volume_from_linear(double v)
+{
+ if (v <= 0.0)
+ return PA_VOLUME_MUTED;
+ return (pa_volume_t) PA_CLAMP_VOLUME((uint64_t) lround(cbrt(v) * PA_VOLUME_NORM));
+}
+
+static inline pa_volume_t pa_sw_volume_from_dB(double dB)
+{
+ if (isinf(dB) < 0 || dB <= PA_DECIBEL_MININFTY)
+ return PA_VOLUME_MUTED;
+ return pa_sw_volume_from_linear(pa_volume_dB_to_linear(dB));
+}
+
+static inline pa_cvolume* pa_cvolume_set(pa_cvolume *a, unsigned channels, pa_volume_t v)
+{
+ uint32_t i;
+ a->channels = (uint8_t) channels;
+ for (i = 0; i < a->channels; i++)
+ a->values[i] = PA_CLAMP_VOLUME(v);
+ return a;
+}
+
+static inline int pa_cvolume_equal(const pa_cvolume *a, const pa_cvolume *b)
+{
+ uint32_t i;
+ if (PA_UNLIKELY(a == b))
+ return 1;
+ if (a->channels != b->channels)
+ return 0;
+ for (i = 0; i < a->channels; i++)
+ if (a->values[i] != b->values[i])
+ return 0;
+ return 1;
+}
+
+static inline pa_volume_t pa_sw_volume_multiply(pa_volume_t a, pa_volume_t b)
+{
+ uint64_t result;
+ result = ((uint64_t) a * (uint64_t) b + (uint64_t) PA_VOLUME_NORM / 2ULL) /
+ (uint64_t) PA_VOLUME_NORM;
+ if (result > (uint64_t)PA_VOLUME_MAX)
+ pa_log_warn("pa_sw_volume_multiply: Volume exceeds maximum allowed value and will be clipped. Please check your volume settings.");
+ return (pa_volume_t) PA_CLAMP_VOLUME(result);
+}
+
+static inline pa_cvolume *pa_sw_cvolume_multiply(pa_cvolume *dest,
+ const pa_cvolume *a, const pa_cvolume *b)
+{
+ unsigned i;
+ dest->channels = PA_MIN(a->channels, b->channels);
+ for (i = 0; i < dest->channels; i++)
+ dest->values[i] = pa_sw_volume_multiply(a->values[i], b->values[i]);
+ return dest;
+}
+
+static inline pa_cvolume *pa_sw_cvolume_multiply_scalar(pa_cvolume *dest,
+ const pa_cvolume *a, pa_volume_t b)
+{
+ unsigned i;
+ for (i = 0; i < a->channels; i++)
+ dest->values[i] = pa_sw_volume_multiply(a->values[i], b);
+ dest->channels = (uint8_t) i;
+ return dest;
+}
+
+static inline pa_volume_t pa_sw_volume_divide(pa_volume_t a, pa_volume_t b)
+{
+ uint64_t result;
+ if (b <= PA_VOLUME_MUTED)
+ return 0;
+ result = ((uint64_t) a * (uint64_t) PA_VOLUME_NORM + (uint64_t) b / 2ULL) / (uint64_t) b;
+ if (result > (uint64_t)PA_VOLUME_MAX)
+ pa_log_warn("pa_sw_volume_divide: Volume exceeds maximum allowed value and will be clipped. Please check your volume settings.");
+ return (pa_volume_t) PA_CLAMP_VOLUME(result);
+}
+
+static inline pa_cvolume *pa_sw_cvolume_divide_scalar(pa_cvolume *dest,
+ const pa_cvolume *a, pa_volume_t b) {
+ unsigned i;
+ for (i = 0; i < a->channels; i++)
+ dest->values[i] = pa_sw_volume_divide(a->values[i], b);
+ dest->channels = (uint8_t) i;
+ return dest;
+}
+
+static inline pa_cvolume *pa_sw_cvolume_divide(pa_cvolume *dest,
+ const pa_cvolume *a, const pa_cvolume *b)
+{
+ unsigned i;
+ dest->channels = PA_MIN(a->channels, b->channels);
+ for (i = 0; i < dest->channels; i++)
+ dest->values[i] = pa_sw_volume_divide(a->values[i], b->values[i]);
+ return dest;
+}
+
+#define pa_cvolume_reset(a, n) pa_cvolume_set((a), (n), PA_VOLUME_NORM)
+#define pa_cvolume_mute(a, n) pa_cvolume_set((a), (n), PA_VOLUME_MUTED)
+
+static inline int pa_cvolume_compatible_with_channel_map(const pa_cvolume *v,
+ const pa_channel_map *cm)
+{
+ return v->channels == cm->channels;
+}
+
+static inline pa_volume_t pa_cvolume_max(const pa_cvolume *a)
+{
+ pa_volume_t m = PA_VOLUME_MUTED;
+ unsigned c;
+ for (c = 0; c < a->channels; c++)
+ if (a->values[c] > m)
+ m = a->values[c];
+ return m;
+}
+
+static inline pa_volume_t pa_cvolume_min(const pa_cvolume *a)
+{
+ pa_volume_t m = PA_VOLUME_MAX;
+ unsigned c;
+ for (c = 0; c < a->channels; c++)
+ if (a->values[c] < m)
+ m = a->values[c];
+ return m;
+}
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_VOLUME_H */
diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c
new file mode 100644
index 0000000..bcd7491
--- /dev/null
+++ b/spa/plugins/alsa/alsa-acp-device.c
@@ -0,0 +1,1133 @@
+/* Spa ALSA Device
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/support/i18n.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/monitor/event.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#include "alsa.h"
+
+#include "acp/acp.h"
+
+extern struct spa_i18n *acp_i18n;
+
+#define MAX_POLL 16
+
+#define DEFAULT_DEVICE "hw:0"
+#define DEFAULT_AUTO_PROFILE true
+#define DEFAULT_AUTO_PORT true
+
+struct props {
+ char device[64];
+ bool auto_profile;
+ bool auto_port;
+};
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, DEFAULT_DEVICE, 64);
+ props->auto_profile = DEFAULT_AUTO_PROFILE;
+ props->auto_port = DEFAULT_AUTO_PORT;
+}
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_loop *loop;
+
+ uint32_t info_all;
+ struct spa_device_info info;
+#define IDX_EnumProfile 0
+#define IDX_Profile 1
+#define IDX_EnumRoute 2
+#define IDX_Route 3
+ struct spa_param_info params[4];
+
+ struct spa_hook_list hooks;
+
+ struct props props;
+
+ uint32_t profile;
+
+ struct acp_card *card;
+ struct pollfd pfds[MAX_POLL];
+ int n_pfds;
+ struct spa_source sources[MAX_POLL];
+};
+
+static int emit_info(struct impl *this, bool full);
+
+static void handle_acp_poll(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ int i;
+
+ for (i = 0; i < this->n_pfds; i++)
+ this->pfds[i].revents = this->sources[i].rmask;
+ acp_card_handle_events(this->card);
+ for (i = 0; i < this->n_pfds; i++)
+ this->sources[i].rmask = 0;
+ emit_info(this, false);
+}
+
+static void remove_sources(struct impl *this)
+{
+ int i;
+ for (i = 0; i < this->n_pfds; i++) {
+ spa_loop_remove_source(this->loop, &this->sources[i]);
+ }
+ this->n_pfds = 0;
+}
+
+static int setup_sources(struct impl *this)
+{
+ int i;
+
+ remove_sources(this);
+
+ this->n_pfds = acp_card_poll_descriptors(this->card, this->pfds, MAX_POLL);
+
+ for (i = 0; i < this->n_pfds; i++) {
+ this->sources[i].func = handle_acp_poll;
+ this->sources[i].data = this;
+ this->sources[i].fd = this->pfds[i].fd;
+ this->sources[i].mask = this->pfds[i].events;
+ this->sources[i].rmask = 0;
+ spa_loop_add_source(this->loop, &this->sources[i]);
+ }
+ return 0;
+}
+
+static int emit_node(struct impl *this, struct acp_device *dev)
+{
+ struct spa_dict_item *items;
+ const struct acp_dict_item *it;
+ uint32_t n_items, i;
+ char device_name[128], path[180], channels[16], ch[12], routes[16];
+ char card_index[16], *p;
+ char positions[SPA_AUDIO_MAX_CHANNELS * 12];
+ struct spa_device_object_info info;
+ struct acp_card *card = this->card;
+ const char *stream, *devstr;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+
+ if (dev->direction == ACP_DIRECTION_PLAYBACK) {
+ info.factory_name = SPA_NAME_API_ALSA_PCM_SINK;
+ stream = "playback";
+ } else {
+ info.factory_name = SPA_NAME_API_ALSA_PCM_SOURCE;
+ stream = "capture";
+ }
+
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+
+ items = alloca((dev->props.n_items + 8) * sizeof(*items));
+ n_items = 0;
+
+ snprintf(card_index, sizeof(card_index), "%d", card->index);
+
+ devstr = dev->device_strings[0];
+ p = strstr(devstr, "%f");
+ if (p) {
+ snprintf(device_name, sizeof(device_name), "%.*s%d%s",
+ (int)SPA_PTRDIFF(p, devstr), devstr,
+ card->index, p+2);
+ } else {
+ snprintf(device_name, sizeof(device_name), "%s", devstr);
+ }
+ snprintf(path, sizeof(path), "alsa:pcm:%s:%s:%s", card_index, device_name, stream);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, device_name);
+ if (dev->flags & ACP_DEVICE_UCM_DEVICE)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_OPEN_UCM, "true");
+ 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);
+
+ snprintf(channels, sizeof(channels), "%d", dev->format.channels);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNELS, channels);
+
+ p = positions;
+ for (i = 0; i < dev->format.channels; i++) {
+ p += snprintf(p, 12, "%s%s", i == 0 ? "" : ",",
+ acp_channel_str(ch, sizeof(ch), dev->format.map[i]));
+ }
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_POSITION, positions);
+
+ snprintf(routes, sizeof(routes), "%d", dev->n_ports);
+ items[n_items++] = SPA_DICT_ITEM_INIT("device.routes", routes);
+
+ acp_dict_for_each(it, &dev->props)
+ items[n_items++] = SPA_DICT_ITEM_INIT(it->key, it->value);
+
+ info.props = &SPA_DICT_INIT(items, n_items);
+
+ spa_device_emit_object_info(&this->hooks, dev->index, &info);
+
+ return 0;
+}
+
+static int emit_info(struct impl *this, bool full)
+{
+ int err = 0;
+ struct spa_dict_item *items;
+ uint32_t n_items;
+ const struct acp_dict_item *it;
+ struct acp_card *card = this->card;
+ char path[128];
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ n_items = card->props.n_items + 4;
+ items = alloca(n_items * sizeof(*items));
+
+ n_items = 0;
+#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value)
+ snprintf(path, sizeof(path), "alsa:pcm:%d", card->index);
+ ADD_ITEM(SPA_KEY_OBJECT_PATH, path);
+ ADD_ITEM(SPA_KEY_DEVICE_API, "alsa:pcm");
+ ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ ADD_ITEM(SPA_KEY_API_ALSA_PATH, (char *)this->props.device);
+ acp_dict_for_each(it, &card->props)
+ ADD_ITEM(it->key, it->value);
+ this->info.props = &SPA_DICT_INIT(items, n_items);
+#undef ADD_ITEM
+
+ if (this->info.change_mask & SPA_DEVICE_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_device_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+ return err;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ struct acp_card *card;
+ struct acp_card_profile *profile;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ card = this->card;
+ if (card->active_profile_index < card->n_profiles)
+ profile = card->profiles[card->active_profile_index];
+ else
+ profile = NULL;
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ if (events->info || events->object_info)
+ emit_info(this, true);
+
+ if (profile) {
+ for (i = 0; i < profile->n_devices; i++)
+ emit_node(this, profile->devices[i]);
+ }
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static struct spa_pod *build_profile(struct spa_pod_builder *b, uint32_t id,
+ struct acp_card_profile *pr, bool current)
+{
+ struct spa_pod_frame f[2];
+ uint32_t i, n_classes, n_capture = 0, n_playback = 0;
+ uint32_t *capture, *playback;
+
+ capture = alloca(sizeof(uint32_t) * pr->n_devices);
+ playback = alloca(sizeof(uint32_t) * pr->n_devices);
+
+ for (i = 0; i < pr->n_devices; i++) {
+ struct acp_device *dev = pr->devices[i];
+ switch (dev->direction) {
+ case ACP_DIRECTION_PLAYBACK:
+ playback[n_playback++] = dev->index;
+ break;
+ case ACP_DIRECTION_CAPTURE:
+ capture[n_capture++] = dev->index;
+ break;
+ }
+ }
+ n_classes = n_capture > 0 ? 1 : 0;
+ n_classes += n_playback > 0 ? 1 : 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(pr->index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(pr->name),
+ SPA_PARAM_PROFILE_description, SPA_POD_String(pr->description),
+ SPA_PARAM_PROFILE_priority, SPA_POD_Int(pr->priority),
+ SPA_PARAM_PROFILE_available, SPA_POD_Id(pr->available),
+ 0);
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, n_classes);
+ if (n_capture > 0) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Source"),
+ SPA_POD_Int(n_capture),
+ SPA_POD_String("card.profile.devices"),
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int,
+ n_capture, capture));
+ }
+ if (n_playback > 0) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Sink"),
+ SPA_POD_Int(n_playback),
+ SPA_POD_String("card.profile.devices"),
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int,
+ n_playback, playback));
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ if (current) {
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_save, 0);
+ spa_pod_builder_bool(b, SPA_FLAG_IS_SET(pr->flags, ACP_PROFILE_SAVE));
+ }
+
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static struct spa_pod *build_route(struct spa_pod_builder *b, uint32_t id,
+ struct acp_port *p, struct acp_device *dev, uint32_t profile)
+{
+ struct spa_pod_frame f[2];
+ const struct acp_dict_item *item;
+ uint32_t i;
+ enum spa_direction direction;
+
+ switch (p->direction) {
+ case ACP_DIRECTION_PLAYBACK:
+ direction = SPA_DIRECTION_OUTPUT;
+ break;
+ case ACP_DIRECTION_CAPTURE:
+ direction = SPA_DIRECTION_INPUT;
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(p->index),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(direction),
+ SPA_PARAM_ROUTE_name, SPA_POD_String(p->name),
+ SPA_PARAM_ROUTE_description, SPA_POD_String(p->description),
+ SPA_PARAM_ROUTE_priority, SPA_POD_Int(p->priority),
+ SPA_PARAM_ROUTE_available, SPA_POD_Id(p->available),
+ 0);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_info, SPA_POD_PROP_FLAG_HINT_DICT);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, p->props.n_items + (dev ? 2 : 0));
+ acp_dict_for_each(item, &p->props) {
+ spa_pod_builder_add(b,
+ SPA_POD_String(item->key),
+ SPA_POD_String(item->value),
+ NULL);
+ }
+ if (dev != NULL) {
+ const char *str;
+ str = SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_MUTE) ? "true" : "false";
+ spa_pod_builder_add(b,
+ SPA_POD_String("route.hw-mute"),
+ SPA_POD_String(str), NULL);
+
+ str = SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_VOLUME) ? "true" : "false";
+ spa_pod_builder_add(b,
+ SPA_POD_String("route.hw-volume"),
+ SPA_POD_String(str), NULL);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0);
+ spa_pod_builder_push_array(b, &f[1]);
+ for (i = 0; i < p->n_profiles; i++)
+ spa_pod_builder_int(b, p->profiles[i]->index);
+ spa_pod_builder_pop(b, &f[1]);
+ if (dev != NULL) {
+ uint32_t channels = dev->format.channels;
+ float volumes[channels];
+ float soft_volumes[channels];
+ bool mute;
+
+ acp_device_get_mute(dev, &mute);
+ spa_zero(volumes);
+ spa_zero(soft_volumes);
+ acp_device_get_volume(dev, volumes, channels);
+ acp_device_get_soft_volume(dev, soft_volumes, channels);
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_device, 0);
+ spa_pod_builder_int(b, dev->index);
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_props, 0);
+ spa_pod_builder_push_object(b, &f[1], SPA_TYPE_OBJECT_Props, id);
+
+ spa_pod_builder_prop(b, SPA_PROP_mute,
+ SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_MUTE) ?
+ SPA_POD_PROP_FLAG_HARDWARE : 0);
+ spa_pod_builder_bool(b, mute);
+
+ spa_pod_builder_prop(b, SPA_PROP_channelVolumes,
+ SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_VOLUME) ?
+ SPA_POD_PROP_FLAG_HARDWARE : 0);
+ spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float,
+ channels, volumes);
+
+ spa_pod_builder_prop(b, SPA_PROP_volumeBase, SPA_POD_PROP_FLAG_READONLY);
+ spa_pod_builder_float(b, dev->base_volume);
+ spa_pod_builder_prop(b, SPA_PROP_volumeStep, SPA_POD_PROP_FLAG_READONLY);
+ spa_pod_builder_float(b, dev->volume_step);
+
+ spa_pod_builder_prop(b, SPA_PROP_channelMap, 0);
+ spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id,
+ channels, dev->format.map);
+
+ spa_pod_builder_prop(b, SPA_PROP_softVolumes, 0);
+ spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float,
+ channels, soft_volumes);
+
+ spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0);
+ spa_pod_builder_long(b, dev->latency_ns);
+
+ if (SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_IEC958)) {
+ spa_pod_builder_prop(b, SPA_PROP_iec958Codecs, 0);
+ spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id,
+ dev->n_codecs, dev->codecs);
+ }
+
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
+ spa_pod_builder_push_array(b, &f[1]);
+ for (i = 0; i < p->n_devices; i++)
+ spa_pod_builder_int(b, p->devices[i]->index);
+ 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);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_save, 0);
+ spa_pod_builder_bool(b, SPA_FLAG_IS_SET(p->flags, ACP_PORT_SAVE));
+ }
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static struct acp_port *find_port_for_device(struct acp_card *card, struct acp_device *dev)
+{
+ uint32_t i;
+ for (i = 0; i < dev->n_ports; i++) {
+ struct acp_port *p = dev->ports[i];
+ if (SPA_FLAG_IS_SET(p->flags, ACP_PORT_ACTIVE))
+ return p;
+ }
+ return NULL;
+}
+
+static int impl_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_device_params result;
+ uint32_t count = 0;
+ struct acp_card *card;
+ struct acp_card_profile *pr;
+ struct acp_port *p;
+ struct acp_device *dev;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ card = this->card;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumProfile:
+ if (result.index >= card->n_profiles)
+ return 0;
+
+ pr = card->profiles[result.index];
+ param = build_profile(&b, id, pr, false);
+ break;
+
+ case SPA_PARAM_Profile:
+ if (result.index > 0 || card->active_profile_index >= card->n_profiles)
+ return 0;
+
+ pr = card->profiles[card->active_profile_index];
+ param = build_profile(&b, id, pr, true);
+ break;
+
+ case SPA_PARAM_EnumRoute:
+ if (result.index >= card->n_ports)
+ return 0;
+
+ p = card->ports[result.index];
+ param = build_route(&b, id, p, NULL, SPA_ID_INVALID);
+ break;
+
+ case SPA_PARAM_Route:
+ while (true) {
+ if (result.index >= card->n_devices)
+ return 0;
+
+ dev = card->devices[result.index];
+ if (SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_ACTIVE) &&
+ (p = find_port_for_device(card, dev)) != NULL)
+ break;
+
+ result.index++;
+ }
+ result.next = result.index + 1;
+ param = build_route(&b, id, p, dev, card->active_profile_index);
+ if (param == NULL)
+ return -errno;
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_device_emit_result(&this->hooks, seq, 0,
+ SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static void on_latency_changed(void *data, struct acp_device *dev)
+{
+ struct impl *this = data;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+
+ spa_log_info(this->log, "device %s latency changed", dev->name);
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, dev->index);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(dev->latency_ns));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static void on_codecs_changed(void *data, struct acp_device *dev)
+{
+ struct impl *this = data;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+
+ spa_log_info(this->log, "device %s codecs changed", dev->name);
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, dev->index);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_iec958Codecs, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, dev->n_codecs, dev->codecs));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static int apply_device_props(struct impl *this, struct acp_device *dev, struct spa_pod *props)
+{
+ float volume = 0;
+ bool mute = 0;
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) props;
+ int changed = 0;
+ float volumes[ACP_MAX_CHANNELS];
+ uint32_t channels[ACP_MAX_CHANNELS];
+ uint32_t n_volumes = 0;
+
+ if (!spa_pod_is_object_type(props, SPA_TYPE_OBJECT_Props))
+ return -EINVAL;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &volume) == 0) {
+ acp_device_set_volume(dev, &volume, 1);
+ changed++;
+ }
+ break;
+ case SPA_PROP_mute:
+ if (spa_pod_get_bool(&prop->value, &mute) == 0) {
+ acp_device_set_mute(dev, mute);
+ changed++;
+ }
+ break;
+ case SPA_PROP_channelVolumes:
+ if ((n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ volumes, ACP_MAX_CHANNELS)) > 0) {
+ changed++;
+ }
+ break;
+ case SPA_PROP_channelMap:
+ if (spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ channels, ACP_MAX_CHANNELS) > 0) {
+ changed++;
+ }
+ break;
+ case SPA_PROP_latencyOffsetNsec:
+ {
+ int64_t latency_ns;
+ if (spa_pod_get_long(&prop->value, &latency_ns) == 0) {
+ if (dev->latency_ns != latency_ns) {
+ dev->latency_ns = latency_ns;
+ on_latency_changed(this, dev);
+ changed++;
+ }
+ }
+ break;
+ }
+ case SPA_PROP_iec958Codecs:
+ {
+ uint32_t codecs[32], n_codecs;
+
+ n_codecs = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ codecs, SPA_N_ELEMENTS(codecs));
+ if (n_codecs != dev->n_codecs ||
+ memcmp(dev->codecs, codecs, n_codecs * sizeof(uint32_t)) != 0) {
+ memcpy(dev->codecs, codecs, n_codecs * sizeof(uint32_t));
+ dev->n_codecs = n_codecs;
+ on_codecs_changed(this, dev);
+ changed++;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ if (n_volumes > 0)
+ acp_device_set_volume(dev, volumes, n_volumes);
+
+ return changed;
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Profile:
+ {
+ uint32_t idx;
+ bool save = false;
+
+ if (param == NULL) {
+ idx = acp_card_find_best_profile_index(this->card, NULL);
+ save = true;
+ } else if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_PROFILE_save, SPA_POD_OPT_Bool(&save))) < 0) {
+ spa_log_warn(this->log, "can't parse profile");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+
+ acp_card_set_profile(this->card, idx, save ? ACP_PROFILE_SAVE : 0);
+ emit_info(this, false);
+ break;
+ }
+ case SPA_PARAM_Route:
+ {
+ uint32_t idx, device;
+ struct spa_pod *props = NULL;
+ struct acp_device *dev;
+ bool save = false;
+
+ if (param == NULL)
+ return -EINVAL;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&device),
+ SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props),
+ SPA_PARAM_ROUTE_save, SPA_POD_OPT_Bool(&save))) < 0) {
+ spa_log_warn(this->log, "can't parse route");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+ if (device >= this->card->n_devices)
+ return -EINVAL;
+
+ dev = this->card->devices[device];
+ acp_device_set_port(dev, idx, save ? ACP_PORT_SAVE : 0);
+ if (props)
+ apply_device_props(this, dev, props);
+ emit_info(this, false);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+static void card_props_changed(void *data)
+{
+ struct impl *this = data;
+ spa_log_info(this->log, "card properties changed");
+}
+
+static bool has_device(struct acp_card_profile *pr, uint32_t index)
+{
+ uint32_t i;
+
+ for (i = 0; i < pr->n_devices; i++)
+ if (pr->devices[i]->index == index)
+ return true;
+ return false;
+}
+
+static void card_profile_changed(void *data, uint32_t old_index, uint32_t new_index)
+{
+ struct impl *this = data;
+ struct acp_card *card = this->card;
+ struct acp_card_profile *op = card->profiles[old_index];
+ struct acp_card_profile *np = card->profiles[new_index];
+ uint32_t i;
+
+ spa_log_info(this->log, "card profile changed from %s to %s",
+ op->name, np->name);
+
+ for (i = 0; i < op->n_devices; i++) {
+ uint32_t index = op->devices[i]->index;
+ if (has_device(np, index))
+ continue;
+ spa_device_emit_object_info(&this->hooks, index, NULL);
+ }
+ for (i = 0; i < np->n_devices; i++) {
+ emit_node(this, np->devices[i]);
+ }
+ setup_sources(this);
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Profile].user++;
+ this->params[IDX_Route].user++;
+ this->params[IDX_EnumRoute].user++;
+}
+
+static void card_profile_available(void *data, uint32_t index,
+ enum acp_available old, enum acp_available available)
+{
+ struct impl *this = data;
+ struct acp_card *card = this->card;
+ struct acp_card_profile *p = card->profiles[index];
+
+ spa_log_info(this->log, "card profile %s available %s -> %s", p->name,
+ acp_available_str(old), acp_available_str(available));
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_EnumProfile].user++;
+ this->params[IDX_Profile].user++;
+
+ if (this->props.auto_profile) {
+ uint32_t best = acp_card_find_best_profile_index(card, NULL);
+ acp_card_set_profile(card, best, 0);
+ }
+}
+
+static void card_port_changed(void *data, uint32_t old_index, uint32_t new_index)
+{
+ struct impl *this = data;
+ struct acp_card *card = this->card;
+ struct acp_port *op = card->ports[old_index];
+ struct acp_port *np = card->ports[new_index];
+
+ spa_log_info(this->log, "card port changed from %s to %s",
+ op->name, np->name);
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+}
+
+static void card_port_available(void *data, uint32_t index,
+ enum acp_available old, enum acp_available available)
+{
+ struct impl *this = data;
+ struct acp_card *card = this->card;
+ struct acp_port *p = card->ports[index];
+
+ spa_log_info(this->log, "card port %s available %s -> %s", p->name,
+ acp_available_str(old), acp_available_str(available));
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_EnumRoute].user++;
+ this->params[IDX_Route].user++;
+
+ if (this->props.auto_port) {
+ uint32_t i;
+
+ for (i = 0; i < p->n_devices; i++) {
+ struct acp_device *d = p->devices[i];
+ uint32_t best;
+
+ if (!(d->flags & ACP_DEVICE_ACTIVE))
+ continue;
+
+ best = acp_device_find_best_port_index(d, NULL);
+ acp_device_set_port(d, best, 0);
+ }
+ }
+}
+
+static void on_volume_changed(void *data, struct acp_device *dev)
+{
+ struct impl *this = data;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ uint32_t n_volume = dev->format.channels;
+ float volume[n_volume];
+ float soft_volume[n_volume];
+
+ spa_log_info(this->log, "device %s volume changed", dev->name);
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+
+ spa_zero(volume);
+ spa_zero(soft_volume);
+ acp_device_get_volume(dev, volume, n_volume);
+ acp_device_get_soft_volume(dev, soft_volume, n_volume);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, dev->index);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float, n_volume, volume),
+ SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, dev->format.channels,
+ dev->format.map),
+ SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float, n_volume, soft_volume));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static void on_mute_changed(void *data, struct acp_device *dev)
+{
+ struct impl *this = data;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ bool mute;
+
+ spa_log_info(this->log, "device %s mute changed", dev->name);
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+
+ acp_device_get_mute(dev, &mute);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, dev->index);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_mute, SPA_POD_Bool(mute),
+ SPA_PROP_softMute, SPA_POD_Bool(mute));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static const struct acp_card_events card_events = {
+ ACP_VERSION_CARD_EVENTS,
+ .props_changed = card_props_changed,
+ .profile_changed = card_profile_changed,
+ .profile_available = card_profile_available,
+ .port_changed = card_port_changed,
+ .port_available = card_port_available,
+ .volume_changed = on_volume_changed,
+ .mute_changed = on_mute_changed,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static SPA_PRINTF_FUNC(6,0) void impl_acp_log_func(void *data,
+ int level, const char *file, int line, const char *func,
+ const char *fmt, va_list arg)
+{
+ struct spa_log *log = data;
+ spa_log_logv(log, (enum spa_log_level)level, file, line, func, fmt, arg);
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ remove_sources(this);
+ if (this->card) {
+ acp_card_destroy(this->card);
+ this->card = NULL;
+ }
+ 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;
+ const char *str;
+ struct acp_dict_item *items = NULL;
+ const struct spa_dict_item *it;
+ uint32_t n_items = 0;
+
+ 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);
+ alsa_log_topic_init(this->log);
+
+ this->loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ acp_i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N);
+ if (this->loop == NULL) {
+ spa_log_error(this->log, "a Loop interface is needed");
+ return -EINVAL;
+ }
+
+ acp_set_log_func(impl_acp_log_func, this->log);
+ acp_set_log_level(6);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ if (info) {
+ if ((str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH)) != NULL)
+ snprintf(this->props.device, sizeof(this->props.device), "%s", str);
+ if ((str = spa_dict_lookup(info, "api.acp.auto-port")) != NULL)
+ this->props.auto_port = spa_atob(str);
+ if ((str = spa_dict_lookup(info, "api.acp.auto-profile")) != NULL)
+ this->props.auto_profile = spa_atob(str);
+
+ items = alloca((info->n_items) * sizeof(*items));
+ spa_dict_for_each(it, info)
+ items[n_items++] = ACP_DICT_ITEM_INIT(it->key, it->value);
+ }
+
+ spa_log_debug(this->log, "probe card %s", this->props.device);
+ if ((str = strchr(this->props.device, ':')) == NULL)
+ return -EINVAL;
+
+ this->card = acp_card_new(atoi(str+1), &ACP_DICT_INIT(items, n_items));
+ if (this->card == NULL)
+ return -errno;
+
+ setup_sources(this);
+
+ acp_card_add_listener(this->card, &card_events, this);
+
+ this->info = SPA_DEVICE_INFO_INIT();
+ this->info_all = SPA_DEVICE_CHANGE_MASK_PROPS |
+ SPA_DEVICE_CHANGE_MASK_PARAMS;
+
+ this->params[IDX_EnumProfile] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ this->params[IDX_Profile] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_EnumRoute] = SPA_PARAM_INFO(SPA_PARAM_EnumRoute, SPA_PARAM_INFO_READ);
+ this->params[IDX_Route] = SPA_PARAM_INFO(SPA_PARAM_Route, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 4;
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_alsa_acp_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_ACP_DEVICE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-compress-offload-sink.c b/spa/plugins/alsa/alsa-compress-offload-sink.c
new file mode 100644
index 0000000..966e680
--- /dev/null
+++ b/spa/plugins/alsa/alsa-compress-offload-sink.c
@@ -0,0 +1,1143 @@
+/* Spa ALSA Compress-Offload sink
+ *
+ * Copyright © 2022 Wim Taymans
+ * © 2022 Asymptotic Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+#include <fcntl.h>
+
+#include <spa/monitor/device.h>
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/json.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/debug/types.h>
+#include <spa/debug/mem.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/control/control.h>
+
+#include <sound/compress_params.h>
+#include <tinycompress/tinycompress.h>
+
+/*
+ * This creates a PipeWire sink node which uses the tinycompress user space
+ * library to use the ALSA Compress-Offload API for writing compressed data
+ * like MP3, FLAC etc. to an DSP that can handle such data directly.
+ *
+ * These show up under /dev/snd like comprCxDx, as opposed to regular
+ * ALSA PCM devices.
+ *
+ * root@dragonboard-845c:~# ls /dev/snd
+ * by-path comprC0D3 controlC0 pcmC0D0c pcmC0D0p pcmC0D1c pcmC0D1p pcmC0D2c pcmC0D2p timer
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.objects = [
+ * { factory = spa-node-factory
+ * args = {
+ * factory.name = api.alsa.compress.offload.sink
+ * node.name = Compress-Offload-Sink
+ * media.class = "Audio/Sink"
+ * api.alsa.path = "hw:0,3"
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ * TODO:
+ * - Clocking
+ * - Implement pause and resume
+ * - Having a better wait mechanism
+ * - Automatic loading using alsa-udev
+ *
+ */
+
+#define NAME "compress-offload-audio-sink"
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_RATE 44100
+#define MAX_BUFFERS 4
+#define MAX_PORTS 1
+#define MAX_CODECS 32 /* See include/sound/compress_params.h */
+
+#define MIN_FRAGMENT_SIZE (4 * 1024)
+#define MAX_FRAGMENT_SIZE (64 * 1024)
+#define MIN_NUM_FRAGMENTS (4)
+#define MAX_NUM_FRAGMENTS (8 * 4)
+
+struct props {
+ uint32_t channels;
+ uint32_t rate;
+ uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
+ char device[64];
+};
+
+static void reset_props(struct props *props)
+{
+ props->channels = 0;
+ props->rate = 0;
+}
+
+struct buffer {
+ uint32_t id;
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+};
+
+struct impl;
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+
+ bool have_format;
+ struct spa_audio_info current_format;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+ uint32_t written;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+ struct spa_log *log;
+ struct props props;
+
+ struct spa_node_info info;
+ struct spa_param_info params[1];
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+ struct port port;
+
+ unsigned int started_node:1;
+ unsigned int started_compress:1;
+ uint64_t info_all;
+ uint32_t quantum_limit;
+
+ struct compr_config compr_conf;
+ struct snd_codec codec;
+ struct compress *compress;
+
+ int32_t codecs_supported[MAX_CODECS];
+ uint32_t num_codecs;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS)
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "alsa" },
+ { SPA_KEY_MEDIA_CLASS, "Audio/Sink" },
+ { SPA_KEY_NODE_DRIVER, "false" },
+ { SPA_KEY_NODE_PAUSE_ON_IDLE, "false" },
+};
+
+static const struct codec_id {
+ uint32_t codec_id;
+} codec_info[] = {
+ { SND_AUDIOCODEC_MP3, },
+ { SND_AUDIOCODEC_AAC, },
+ { SND_AUDIOCODEC_WMA, },
+ { SND_AUDIOCODEC_VORBIS, },
+ { SND_AUDIOCODEC_FLAC, },
+ { SND_AUDIOCODEC_ALAC, },
+ { SND_AUDIOCODEC_APE, },
+ { SND_AUDIOCODEC_REAL, },
+ { SND_AUDIOCODEC_AMR, },
+ { SND_AUDIOCODEC_AMRWB, },
+};
+
+static int
+open_compress(struct impl *this)
+{
+ struct compress *compress;
+
+ compress = compress_open_by_name(this->props.device, COMPRESS_IN, &this->compr_conf);
+ if (!compress || !is_compress_ready(compress)) {
+ spa_log_error(this->log, NAME " %p: Unable to open compress device", this);
+ return -EINVAL;
+ }
+
+ this->compress = compress;
+
+ compress_nonblock(this->compress, 1);
+
+ return 0;
+}
+
+static int
+write_compress(struct impl *this, void *buf, int32_t size)
+{
+ int32_t wrote;
+ int32_t to_write = size;
+ struct port *port = &this->port;
+
+retry:
+ wrote = compress_write(this->compress, buf, to_write);
+ if (wrote < 0) {
+ spa_log_error(this->log, NAME " %p: Error playing sample: %s",
+ this, compress_get_error(this->compress));
+ return wrote;
+ }
+ port->written += wrote;
+
+ spa_log_debug(this->log, NAME " %p: We wrote %d, DSP accepted %d\n", this, size, wrote);
+
+ if (wrote < to_write) {
+ /*
+ * The choice of 20ms as the time to wait is
+ * completely arbitrary.
+ */
+ compress_wait(this->compress, 20);
+ buf = (uint8_t *)buf + wrote;
+ to_write = to_write - wrote;
+ goto retry;
+ }
+
+ /*
+ * One write has to happen before starting the compressed node. Calling
+ * compress_start before writing MIN_NUM_FRAGMENTS * MIN_FRAGMENT_SIZE
+ * will result in a distorted audio playback.
+ */
+ if (!this->started_compress &&
+ (port->written >= (MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS))) {
+ compress_start(this->compress);
+ this->started_compress = true;
+ }
+
+ return size;
+}
+
+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);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &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[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:
+ case SPA_PARAM_PortConfig:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, id,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_passthrough));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int do_start(struct impl *this)
+{
+ if (this->started_node)
+ return 0;
+
+ spa_log_debug(this->log, "Open compressed device: %s", this->props.device);
+ if (open_compress(this) < 0)
+ return -EINVAL;
+
+ this->started_node = true;
+ this->started_compress = false;
+
+ return 0;
+}
+
+static int do_drain(struct impl *this)
+{
+ if (!this->started_node)
+ return 0;
+
+ if (this->started_compress) {
+ spa_log_debug(this->log, NAME " %p: Issuing drain command", this);
+ compress_drain(this->compress);
+ spa_log_debug(this->log, NAME " %p: Finished drain", this);
+ }
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ if (!this->started_node)
+ return 0;
+
+ compress_stop(this->compress);
+ compress_close(this->compress);
+ spa_log_info(this->log, NAME " %p: Closed compress device", this);
+
+ this->compress = NULL;
+ this->started_node = false;
+ this->started_compress = false;
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ do_start(this);
+ break;
+ }
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ do_drain(this);
+ do_stop(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;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+port_enum_formats(struct impl *this,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct spa_audio_info info;
+ uint32_t codec;
+
+ if (index >= this->num_codecs)
+ return 0;
+
+ codec = this->codecs_supported[index];
+
+ spa_zero(info);
+ info.media_type = SPA_MEDIA_TYPE_audio;
+
+ switch (codec) {
+ case SND_AUDIOCODEC_MP3:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_mp3;
+ info.info.mp3.rate = this->props.rate;
+ info.info.mp3.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_AAC:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_aac;
+ info.info.aac.rate = this->props.rate;
+ info.info.aac.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_WMA:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_wma;
+ info.info.wma.rate = this->props.rate;
+ info.info.wma.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_VORBIS:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_vorbis;
+ info.info.vorbis.rate = this->props.rate;
+ info.info.vorbis.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_FLAC:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_flac;
+ info.info.flac.rate = this->props.rate;
+ info.info.flac.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_ALAC:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_alac;
+ info.info.alac.rate = this->props.rate;
+ info.info.alac.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_APE:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_ape;
+ info.info.ape.rate = this->props.rate;
+ info.info.ape.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_REAL:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_ra;
+ info.info.ra.rate = this->props.rate;
+ info.info.ra.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_AMR:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_amr;
+ info.info.amr.rate = this->props.rate;
+ info.info.amr.channels = this->props.channels;
+ info.info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_NB;
+ break;
+ case SND_AUDIOCODEC_AMRWB:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_amr;
+ info.info.amr.rate = this->props.rate;
+ info.info.amr.channels = this->props.channels;
+ info.info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_WB;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ if ((*param = spa_format_audio_build(builder, SPA_PARAM_EnumFormat, &info)) == NULL)
+ return -errno;
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(this, direction, port_id,
+ result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_build(&b, id, &port->current_format);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(0),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS,
+ MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS,
+ MAX_FRAGMENT_SIZE),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(0));
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_info(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ this->started_node = false;
+ }
+ return 0;
+}
+
+static int
+compress_setup(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate)
+{
+ struct compr_config *config;
+ struct snd_codec *codec;
+ uint32_t channels, rate;
+
+ memset(&this->codec, 0, sizeof(this->codec));
+ memset(&this->compr_conf, 0, sizeof(this->compr_conf));
+
+ config = &this->compr_conf;
+ codec = &this->codec;
+
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_vorbis:
+ codec->id = SND_AUDIOCODEC_VORBIS;
+ rate = info->info.vorbis.rate;
+ channels = info->info.vorbis.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_mp3:
+ codec->id = SND_AUDIOCODEC_MP3;
+ rate = info->info.mp3.rate;
+ channels = info->info.mp3.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_aac:
+ codec->id = SND_AUDIOCODEC_AAC;
+ rate = info->info.aac.rate;
+ channels = info->info.aac.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_flac:
+ codec->id = SND_AUDIOCODEC_FLAC;
+ /*
+ * Taken from the fcplay utility in tinycompress. Required for
+ * FLAC to work.
+ */
+ codec->options.flac_d.sample_size = 16;
+ codec->options.flac_d.min_blk_size = 16;
+ codec->options.flac_d.max_blk_size = 65535;
+ codec->options.flac_d.min_frame_size = 11;
+ codec->options.flac_d.max_frame_size = 8192 * 4;
+ rate = info->info.flac.rate;
+ channels = info->info.flac.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_wma:
+ codec->id = SND_AUDIOCODEC_WMA;
+ /*
+ * WMA does not work with Compress-Offload if codec profile
+ * is not set.
+ */
+ switch (info->info.wma.profile) {
+ case SPA_AUDIO_WMA_PROFILE_WMA9:
+ codec->profile = SND_AUDIOPROFILE_WMA9;
+ break;
+ case SPA_AUDIO_WMA_PROFILE_WMA9_PRO:
+ codec->profile = SND_AUDIOPROFILE_WMA9_PRO;
+ break;
+ case SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS:
+ codec->profile = SND_AUDIOPROFILE_WMA9_LOSSLESS;
+ break;
+ case SPA_AUDIO_WMA_PROFILE_WMA10:
+ codec->profile = SND_AUDIOPROFILE_WMA10;
+ break;
+ case SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS:
+ codec->profile = SND_AUDIOPROFILE_WMA10_LOSSLESS;
+ break;
+ default:
+ spa_log_error(this->log, NAME " %p: Invalid WMA codec profile", this);
+ return -EINVAL;
+ }
+ codec->bit_rate = info->info.wma.bitrate;
+ codec->align = info->info.wma.block_align;
+ rate = info->info.wma.rate;
+ channels = info->info.wma.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_alac:
+ codec->id = SND_AUDIOCODEC_ALAC;
+ rate = info->info.alac.rate;
+ channels = info->info.alac.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_ape:
+ codec->id = SND_AUDIOCODEC_APE;
+ rate = info->info.ape.rate;
+ channels = info->info.ape.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_ra:
+ codec->id = SND_AUDIOCODEC_REAL;
+ rate = info->info.ra.rate;
+ channels = info->info.ra.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_amr:
+ if (info->info.amr.band_mode == SPA_AUDIO_AMR_BAND_MODE_WB)
+ codec->id = SND_AUDIOCODEC_AMRWB;
+ else
+ codec->id = SND_AUDIOCODEC_AMR;
+ rate = info->info.amr.rate;
+ channels = info->info.amr.channels;
+ break;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ codec->ch_in = channels;
+ codec->ch_out = channels;
+ codec->sample_rate = rate;
+ *out_rate = rate;
+
+ codec->rate_control = 0;
+ codec->level = 0;
+ codec->ch_mode = 0;
+ codec->format = 0;
+
+ spa_log_info(this->log, NAME " %p: Codec info, profile: %d align: %d rate: %d bitrate: %d",
+ this, codec->profile, codec->align, codec->sample_rate, codec->bit_rate);
+
+ if (!is_codec_supported_by_name(this->props.device, 0, codec)) {
+ spa_log_error(this->log, NAME " %p: Requested codec is not supported by DSP", this);
+ return -EINVAL;
+ }
+
+ config->codec = codec;
+ config->fragment_size = MIN_FRAGMENT_SIZE;
+ config->fragments = MIN_NUM_FRAGMENTS;
+
+ 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)
+{
+ int res;
+ struct port *port = &this->port;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+ uint32_t rate;
+
+ if ((res = spa_format_audio_parse(format, &info)) < 0) {
+ spa_log_error(this->log, NAME " %p: format parse error: %s", this,
+ spa_strerror(res));
+ return res;
+ }
+
+ if ((res = compress_setup(this, &info, &rate)) < 0) {
+ spa_log_error(this->log, NAME " %p: can't setup compress: %s",
+ this, spa_strerror(res));
+ return res;
+ }
+
+ port->current_format = info;
+ port->have_format = true;
+ port->info.rate = SPA_FRACTION(1, rate);
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS;
+ this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE;
+ emit_node_info(this, false);
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+
+ if (port->have_format) {
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[3] = 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_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;
+ }
+ return 0;
+}
+
+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;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ if (!port->have_format)
+ return -EIO;
+
+ clear_buffers(this, port);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->flags = 0;
+ b->outbuf = buffers[i];
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+ struct buffer *b;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+
+ io = port->io;
+ spa_return_val_if_fail(io != NULL, -EIO);
+
+ if (io->status != SPA_STATUS_HAVE_DATA)
+ return io->status;
+
+ if (io->buffer_id >= port->n_buffers) {
+ io->status = -EINVAL;
+ return io->status;
+ }
+
+ b = &port->buffers[io->buffer_id];
+
+ for (i = 0; i < b->outbuf->n_datas; i++) {
+ int32_t offs, size;
+ int32_t wrote;
+ void *buf;
+
+ struct spa_data *d = b->outbuf->datas;
+ d = b->outbuf->datas;
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->maxsize - offs, d->chunk->size);
+ buf = SPA_PTROFF(d[0].data, offs, void);
+
+ wrote = write_compress(this, buf, size);
+ if (wrote < 0) {
+ spa_log_error(this->log, NAME " %p: Error playing sample: %s",
+ this, compress_get_error(this->compress));
+ io->status = wrote;
+ return SPA_STATUS_STOPPED;
+ }
+ }
+
+ io->status = SPA_STATUS_OK;
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .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,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = MAX_PORTS;
+ this->info.max_output_ports = 0;
+ this->info.flags = SPA_NODE_FLAG_RT |
+ SPA_NODE_FLAG_IN_PORT_CONFIG |
+ SPA_NODE_FLAG_NEED_CONFIGURE;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = 1;
+ reset_props(&this->props);
+
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF |
+ SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 4;
+ port->written = 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, SPA_KEY_AUDIO_CHANNELS)) {
+ this->props.channels = atoi(s);
+ } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) {
+ this->props.rate = atoi(s);
+ }
+ }
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH))) {
+ if ((str[0] == 'h') || (str[1] == 'w') || (str[2] == ':')) {
+ snprintf(this->props.device, sizeof(this->props.device), "%s", str);
+ } else {
+ spa_log_error(this->log, NAME " %p: Invalid Compress-Offload hw %s", this, str);
+ return -EINVAL;
+ }
+ } else {
+ spa_log_error(this->log, NAME " %p: Invalid compress hw", this);
+ return -EINVAL;
+ }
+
+ /*
+ * TODO:
+ *
+ * Move this to use new compress_get_supported_codecs_by_name API once
+ * merged upstream.
+ *
+ * Right now, we pretend all codecs are supported and then error out
+ * at runtime in port_set_format during compress_setup if not
+ * supported.
+ */
+ this->num_codecs = SPA_N_ELEMENTS (codec_info);
+ for (i = 0; i < this->num_codecs; i++) {
+ this->codecs_supported[i] = codec_info[i].codec_id;
+ }
+
+ spa_log_info(this->log, NAME " %p: Initialized Compress-Offload sink %s",
+ this, this->props.device);
+
+ 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, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play compressed audio (like MP3 or AAC) with the ALSA Compress-Offload API" },
+ { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<path>]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_alsa_compress_offload_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_COMPRESS_OFFLOAD_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-pcm-device.c b/spa/plugins/alsa/alsa-pcm-device.c
new file mode 100644
index 0000000..984f1d0
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm-device.c
@@ -0,0 +1,581 @@
+/* Spa ALSA Device
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#include "alsa.h"
+
+#define MAX_DEVICES 64
+
+static const char default_device[] = "hw:0";
+
+struct props {
+ char device[64];
+};
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, default_device, 64);
+}
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+
+ struct spa_hook_list hooks;
+
+ struct props props;
+ uint32_t n_nodes;
+ uint32_t n_capture;
+ uint32_t n_playback;
+
+ uint32_t profile;
+};
+
+static const char *get_stream(snd_pcm_info_t *pcminfo)
+{
+ switch (snd_pcm_info_get_stream(pcminfo)) {
+ case SND_PCM_STREAM_PLAYBACK:
+ return "playback";
+ case SND_PCM_STREAM_CAPTURE:
+ return "capture";
+ default:
+ return "unknown";
+ }
+}
+
+static const char *get_class(snd_pcm_info_t *pcminfo)
+{
+ switch (snd_pcm_info_get_class(pcminfo)) {
+ case SND_PCM_CLASS_GENERIC:
+ return "generic";
+ case SND_PCM_CLASS_MULTI:
+ return "multichannel";
+ case SND_PCM_CLASS_MODEM:
+ return "modem";
+ case SND_PCM_CLASS_DIGITIZER:
+ return "digitizer";
+ default:
+ return "unknown";
+ }
+}
+
+static const char *get_subclass(snd_pcm_info_t *pcminfo)
+{
+ switch (snd_pcm_info_get_subclass(pcminfo)) {
+ case SND_PCM_SUBCLASS_GENERIC_MIX:
+ return "generic-mix";
+ case SND_PCM_SUBCLASS_MULTI_MIX:
+ return "multichannel-mix";
+ default:
+ return "unknown";
+ }
+}
+
+static int emit_node(struct impl *this, snd_ctl_card_info_t *cardinfo, snd_pcm_info_t *pcminfo, uint32_t id)
+{
+ struct spa_dict_item items[12];
+ char device_name[128], path[180];
+ char sync_name[128], dev[16], subdev[16], card[16];
+ struct spa_device_object_info info;
+ snd_pcm_sync_id_t sync_id;
+ const char *stream;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+
+ if (snd_pcm_info_get_stream(pcminfo) == SND_PCM_STREAM_PLAYBACK) {
+ info.factory_name = SPA_NAME_API_ALSA_PCM_SINK;
+ stream = "playback";
+ } else {
+ info.factory_name = SPA_NAME_API_ALSA_PCM_SOURCE;
+ stream = "capture";
+ }
+
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+
+ snprintf(card, sizeof(card), "%d", snd_pcm_info_get_card(pcminfo));
+ snprintf(dev, sizeof(dev), "%d", snd_pcm_info_get_device(pcminfo));
+ snprintf(subdev, sizeof(subdev), "%d", snd_pcm_info_get_subdevice(pcminfo));
+ snprintf(device_name, sizeof(device_name), "%s,%s", this->props.device, dev);
+ snprintf(path, sizeof(path), "alsa:pcm:%s:%s:%s", snd_ctl_card_info_get_id(cardinfo), dev, stream);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path);
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, device_name);
+ items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CARD, card);
+ items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_DEVICE, dev);
+ items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBDEVICE, subdev);
+ items[5] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_STREAM, get_stream(pcminfo));
+ items[6] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_ID, snd_pcm_info_get_id(pcminfo));
+ items[7] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_NAME, snd_pcm_info_get_name(pcminfo));
+ items[8] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBNAME, snd_pcm_info_get_subdevice_name(pcminfo));
+ items[9] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CLASS, get_class(pcminfo));
+ items[10] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBCLASS, get_subclass(pcminfo));
+ sync_id = snd_pcm_info_get_sync(pcminfo);
+ snprintf(sync_name, sizeof(sync_name), "%08x:%08x:%08x:%08x",
+ sync_id.id32[0], sync_id.id32[1], sync_id.id32[2], sync_id.id32[3]);
+ items[11] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SYNC_ID, sync_name);
+ info.props = &SPA_DICT_INIT_ARRAY(items);
+
+ spa_device_emit_object_info(&this->hooks, id, &info);
+
+ return 0;
+}
+
+static int activate_profile(struct impl *this, snd_ctl_t *ctl_hndl, uint32_t id)
+{
+ int err = 0, dev;
+ uint32_t i, n_cap, n_play;
+ snd_pcm_info_t *pcminfo;
+ snd_ctl_card_info_t *cardinfo;
+
+ spa_log_debug(this->log, "profile %d", id);
+ this->profile = id;
+
+ snd_ctl_card_info_alloca(&cardinfo);
+ if ((err = snd_ctl_card_info(ctl_hndl, cardinfo)) < 0) {
+ spa_log_error(this->log, "error card info: %s", snd_strerror(err));
+ return err;
+ }
+
+ for (i = 0; i < this->n_nodes; i++)
+ spa_device_emit_object_info(&this->hooks, i, NULL);
+
+ this->n_nodes = this->n_capture = this->n_playback = 0;
+
+ if (id == 0)
+ return 0;
+
+ snd_pcm_info_alloca(&pcminfo);
+ dev = -1;
+ i = n_cap = n_play = 0;
+ while (1) {
+ if ((err = snd_ctl_pcm_next_device(ctl_hndl, &dev)) < 0) {
+ spa_log_error(this->log, "error iterating devices: %s", snd_strerror(err));
+ break;
+ }
+ if (dev < 0)
+ break;
+
+ snd_pcm_info_set_device(pcminfo, dev);
+ snd_pcm_info_set_subdevice(pcminfo, 0);
+
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK);
+ if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
+ if (err != -ENOENT)
+ spa_log_error(this->log, "error pcm info: %s", snd_strerror(err));
+ }
+ if (err >= 0) {
+ n_play++;
+ emit_node(this, cardinfo, pcminfo, i++);
+ }
+
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE);
+ if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
+ if (err != -ENOENT)
+ spa_log_error(this->log, "error pcm info: %s", snd_strerror(err));
+ }
+ if (err >= 0) {
+ n_cap++;
+ emit_node(this, cardinfo, pcminfo, i++);
+ }
+ }
+ this->n_capture = n_cap;
+ this->n_playback = n_play;
+ this->n_nodes = i;
+ return err;
+}
+
+static int set_profile(struct impl *this, uint32_t id)
+{
+ snd_ctl_t *ctl_hndl;
+ int err;
+
+ spa_log_debug(this->log, "open card %s", this->props.device);
+ if ((err = snd_ctl_open(&ctl_hndl, this->props.device, 0)) < 0) {
+ spa_log_error(this->log, "can't open control for card %s: %s",
+ this->props.device, snd_strerror(err));
+ return err;
+ }
+
+ err = activate_profile(this, ctl_hndl, id);
+
+ spa_log_debug(this->log, "close card %s", this->props.device);
+ snd_ctl_close(ctl_hndl);
+
+ return err;
+}
+
+static int emit_info(struct impl *this, bool full)
+{
+ int err = 0;
+ struct spa_dict_item items[20];
+ uint32_t n_items = 0;
+ snd_ctl_t *ctl_hndl;
+ snd_ctl_card_info_t *info;
+ struct spa_device_info dinfo;
+ struct spa_param_info params[2];
+ char path[128];
+
+ spa_log_debug(this->log, "open card %s", this->props.device);
+ if ((err = snd_ctl_open(&ctl_hndl, this->props.device, 0)) < 0) {
+ spa_log_error(this->log, "can't open control for card %s: %s",
+ this->props.device, snd_strerror(err));
+ return err;
+ }
+
+ snd_ctl_card_info_alloca(&info);
+ if ((err = snd_ctl_card_info(ctl_hndl, info)) < 0) {
+ spa_log_error(this->log, "error hardware info: %s", snd_strerror(err));
+ goto exit;
+ }
+
+ dinfo = SPA_DEVICE_INFO_INIT();
+
+ dinfo.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), "alsa:pcm:%s", snd_ctl_card_info_get_id(info));
+ ADD_ITEM(SPA_KEY_OBJECT_PATH, path);
+ ADD_ITEM(SPA_KEY_DEVICE_API, "alsa:pcm");
+ ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ ADD_ITEM(SPA_KEY_API_ALSA_PATH, (char *)this->props.device);
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_ID, snd_ctl_card_info_get_id(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_COMPONENTS, snd_ctl_card_info_get_components(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_DRIVER, snd_ctl_card_info_get_driver(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_NAME, snd_ctl_card_info_get_name(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_LONGNAME, snd_ctl_card_info_get_longname(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_MIXERNAME, snd_ctl_card_info_get_mixername(info));
+ dinfo.props = &SPA_DICT_INIT(items, n_items);
+#undef ADD_ITEM
+
+ dinfo.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE);
+ dinfo.n_params = SPA_N_ELEMENTS(params);
+ dinfo.params = params;
+
+ spa_device_emit_info(&this->hooks, &dinfo);
+
+ exit:
+ spa_log_debug(this->log, "close card %s", this->props.device);
+ snd_ctl_close(ctl_hndl);
+ return err;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ if (events->info || events->object_info)
+ emit_info(this, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b,
+ uint32_t id, uint32_t index)
+{
+ struct spa_pod_frame f[2];
+ const char *name, *desc;
+
+ switch (index) {
+ case 0:
+ name = "off";
+ desc = "Off";
+ break;
+ case 1:
+ name = "on";
+ desc = "On";
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(name),
+ SPA_PARAM_PROFILE_description, SPA_POD_String(desc),
+ 0);
+ if (index == 1) {
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ if (this->n_capture) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Source"),
+ SPA_POD_Int(this->n_capture));
+ }
+ if (this->n_playback) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Sink"),
+ SPA_POD_Int(this->n_playback));
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ return spa_pod_builder_pop(b, &f[0]);
+
+}
+static int impl_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_device_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_EnumProfile:
+ {
+ switch (result.index) {
+ case 0:
+ case 1:
+ param = build_profile(this, &b, id, result.index);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Profile:
+ {
+ switch (result.index) {
+ case 0:
+ param = build_profile(this, &b, id, this->profile);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_device_emit_result(&this->hooks, seq, 0,
+ SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Profile:
+ {
+ uint32_t idx;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx))) < 0) {
+ spa_log_warn(this->log, "can't parse profile");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+
+ set_profile(this, idx);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static 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;
+ const char *str;
+
+ 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);
+ alsa_log_topic_init(this->log);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ snd_config_update_free_global();
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH)))
+ snprintf(this->props.device, 64, "%s", str);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_alsa_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_PCM_DEVICE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-pcm-sink.c b/spa/plugins/alsa/alsa-pcm-sink.c
new file mode 100644
index 0000000..a8c40b1
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm-sink.c
@@ -0,0 +1,1014 @@
+/* Spa ALSA Sink
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/monitor/device.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/param/audio/format.h>
+#include <spa/pod/filter.h>
+
+#include "alsa-pcm.h"
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+
+static const char default_device[] = "hw:0";
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, default_device, 64);
+ props->use_chmap = DEFAULT_USE_CHMAP;
+}
+
+static void emit_node_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ struct spa_dict_item items[7];
+ uint32_t i, n_items = 0;
+ char latency[64], period[64], nperiods[64], headroom[64];
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Sink");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+ if (this->have_format) {
+ snprintf(latency, sizeof(latency), "%lu/%d", this->buffer_frames / 2, this->rate);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency);
+ snprintf(period, sizeof(period), "%lu", this->period_frames);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period);
+ snprintf(nperiods, sizeof(nperiods), "%lu", this->buffer_frames / this->period_frames);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods);
+ snprintf(headroom, sizeof(headroom), "%u", this->headroom);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom);
+ }
+ this->info.props = &SPA_DICT_INIT(items, n_items);
+
+ if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->info.n_params; i++) {
+ if (this->params[i].user > 0) {
+ this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_info(&this->hooks, &this->info);
+
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->port_info.change_mask : 0;
+
+ if (full)
+ this->port_info.change_mask = this->port_info_all;
+ if (this->port_info.change_mask) {
+ uint32_t i;
+
+ if (this->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->port_info.n_params; i++) {
+ if (this->port_params[i].user > 0) {
+ this->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->port_params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &this->port_info);
+ this->port_info.change_mask = old;
+ }
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_API_ALSA_PATH),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_cardName),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA card name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)));
+ break;
+ case 3:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
+ SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, 0LL, 2 * SPA_NSEC_PER_SEC));
+ break;
+ case 4:
+ if (!this->is_iec958 && !this->is_hdmi)
+ goto next;
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_iec958Codecs),
+ SPA_PROP_INFO_name, SPA_POD_String("iec958.codecs"),
+ SPA_PROP_INFO_description, SPA_POD_String("Enabled IEC958 (S/PDIF) codecs"),
+ SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_IEC958_CODEC_UNKNOWN),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true),
+ SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+ break;
+ default:
+ param = spa_alsa_enum_propinfo(this, result.index - 5, &b);
+ if (param == NULL)
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod_frame f;
+ uint32_t codecs[16], n_codecs;
+
+ switch (result.index) {
+ case 0:
+ spa_pod_builder_push_object(&b, &f,
+ SPA_TYPE_OBJECT_Props, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device)),
+ SPA_PROP_deviceName, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)),
+ SPA_PROP_cardName, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(this->process_latency.ns),
+ 0);
+
+ if (this->is_iec958 || this->is_hdmi) {
+ n_codecs = spa_alsa_get_iec958_codecs(this, codecs, SPA_N_ELEMENTS(codecs));
+ spa_pod_builder_prop(&b, SPA_PROP_iec958Codecs, 0);
+ spa_pod_builder_array(&b, sizeof(uint32_t), SPA_TYPE_Id,
+ n_codecs, codecs);
+ }
+ spa_alsa_add_prop_params(this, &b);
+ param = spa_pod_builder_pop(&b, &f);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_ProcessLatency:
+ switch (result.index) {
+ case 0:
+ param = spa_process_latency_build(&b, id, &this->process_latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ spa_alsa_reassign_follower(this);
+
+ return 0;
+}
+
+static void handle_process_latency(struct state *this,
+ const struct spa_process_latency_info *info)
+{
+ bool ns_changed = this->process_latency.ns != info->ns;
+
+ if (this->process_latency.quantum == info->quantum &&
+ this->process_latency.rate == info->rate &&
+ !ns_changed)
+ return;
+
+ this->process_latency = *info;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ if (ns_changed)
+ this->params[NODE_Props].user++;
+ this->params[NODE_ProcessLatency].user++;
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_Latency].user++;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod *iec958_codecs = NULL, *params = NULL;
+ int64_t lat_ns = -1;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&lat_ns),
+ SPA_PROP_iec958Codecs, SPA_POD_OPT_Pod(&iec958_codecs),
+ SPA_PROP_params, SPA_POD_OPT_Pod(&params));
+
+ if ((this->is_iec958 || this->is_hdmi) && iec958_codecs != NULL) {
+ uint32_t i, codecs[16], n_codecs;
+ n_codecs = spa_pod_copy_array(iec958_codecs, SPA_TYPE_Id,
+ codecs, SPA_N_ELEMENTS(codecs));
+ this->iec958_codecs = 1ULL << SPA_AUDIO_IEC958_CODEC_PCM;
+ for (i = 0; i < n_codecs; i++)
+ this->iec958_codecs |= 1ULL << codecs[i];
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[NODE_Props].user++;
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_EnumFormat].user++;
+ }
+ spa_alsa_parse_prop_params(this, params);
+ if (lat_ns != -1) {
+ struct spa_process_latency_info info;
+ info = this->process_latency;
+ info.ns = lat_ns;
+ handle_process_latency(this, &info);
+ }
+ emit_node_info(this, false);
+ emit_port_info(this, false);
+ break;
+ }
+ case SPA_PARAM_ProcessLatency:
+ {
+ struct spa_process_latency_info info;
+ if (param == NULL)
+ spa_zero(info);
+ else if ((res = spa_process_latency_parse(param, &info)) < 0)
+ return res;
+
+ handle_process_latency(this, &info);
+
+ emit_node_info(this, false);
+ emit_port_info(this, false);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_ParamBegin:
+ if ((res = spa_alsa_open(this, NULL)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_ParamEnd:
+ if (this->have_format)
+ return 0;
+ if ((res = spa_alsa_close(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Start:
+ if (!this->have_format)
+ return -EIO;
+ if (this->n_buffers == 0)
+ return -EIO;
+
+ if ((res = spa_alsa_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = spa_alsa_pause(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct state *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ return spa_alsa_enum_format(this, seq, start, num, filter);
+
+ case SPA_PARAM_Format:
+ if (!this->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ switch (this->current_format.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ param = spa_format_audio_raw_build(&b, id,
+ &this->current_format.info.raw);
+ break;
+ case SPA_MEDIA_SUBTYPE_iec958:
+ param = spa_format_audio_iec958_build(&b, id,
+ &this->current_format.info.iec958);
+ break;
+ case SPA_MEDIA_SUBTYPE_dsd:
+ param = spa_format_audio_dsd_build(&b, id,
+ &this->current_format.info.dsd);
+ break;
+ default:
+ return -EIO;
+ }
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!this->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->frame_size * this->frame_scale,
+ 16 * this->frame_size * this->frame_scale,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ {
+ struct spa_latency_info latency = this->latency[result.index];
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ spa_process_latency_info_add(&this->process_latency, &latency);
+ param = spa_latency_build(&b, id, &latency);
+ break;
+ }
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct state *this)
+{
+ if (this->n_buffers > 0) {
+ spa_list_init(&this->ready);
+ this->n_buffers = 0;
+ }
+ 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 state *this = object;
+ int err = 0;
+
+ if (format == NULL) {
+ if (!this->have_format)
+ return 0;
+
+ spa_log_debug(this->log, "clear format");
+ spa_alsa_close(this);
+ clear_buffers(this);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio)
+ return -EINVAL;
+
+ switch (info.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+ break;
+ case SPA_MEDIA_SUBTYPE_iec958:
+ if (spa_format_audio_iec958_parse(format, &info.info.iec958) < 0)
+ return -EINVAL;
+ break;
+ case SPA_MEDIA_SUBTYPE_dsd:
+ if (spa_format_audio_dsd_parse(format, &info.info.dsd) < 0)
+ return -EINVAL;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if ((err = spa_alsa_set_format(this, &info, flags)) < 0)
+ return err;
+
+ this->current_format = info;
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ emit_node_info(this, false);
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ this->port_info.rate = SPA_FRACTION(1, this->rate);
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (this->have_format) {
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Latency].user++;
+ } else {
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, false);
+
+ return err;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, direction, port_id, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if (param == NULL)
+ info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(direction));
+ else if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ this->latency[info.direction] = info;
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_Latency].user++;
+ emit_port_info(this, false);
+ res = 0;
+ break;
+ }
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct state *this = object;
+ uint32_t i;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers);
+
+ if (this->n_buffers > 0) {
+ spa_alsa_pause(this);
+ if ((res = clear_buffers(this)) < 0)
+ return res;
+ }
+ if (n_buffers > 0 && !this->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &this->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->flags = BUFFER_FLAG_OUT;
+
+ b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data);
+ }
+ this->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ this->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ this->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct state *this = object;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if ((io = this->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, io->status,
+ io->buffer_id, this->n_buffers);
+
+ if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
+ io->status = SPA_STATUS_NEED_DATA;
+ return SPA_STATUS_HAVE_DATA;
+ }
+ if (io->status == SPA_STATUS_HAVE_DATA &&
+ io->buffer_id < this->n_buffers) {
+ struct buffer *b = &this->buffers[io->buffer_id];
+
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_warn(this->log, "%p: buffer %u in use",
+ this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+ spa_log_trace_fp(this->log, "%p: queue buffer %u", this, io->buffer_id);
+ spa_list_append(&this->ready, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ io->buffer_id = SPA_ID_INVALID;
+
+ spa_alsa_write(this);
+
+ io->status = SPA_STATUS_OK;
+ }
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct state *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct state *this;
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ this = (struct state *) handle;
+ spa_alsa_close(this);
+ spa_alsa_clear(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct state *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->stream = SND_PCM_STREAM_PLAYBACK;
+ this->port_direction = SPA_DIRECTION_INPUT;
+ this->latency[this->port_direction] = SPA_LATENCY_INFO(
+ this->port_direction,
+ .min_quantum = 1.0f,
+ .max_quantum = 1.0f);
+ this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ reset_props(&this->props);
+
+ this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_info = SPA_PORT_INFO_INIT();
+ this->port_info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ this->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ this->port_info.params = this->port_params;
+ this->port_info.n_params = N_PORT_PARAMS;
+
+ spa_list_init(&this->ready);
+
+ return spa_alsa_init(this, info);
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the alsa API" },
+ { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<path>]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_alsa_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_PCM_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-pcm-source.c b/spa/plugins/alsa/alsa-pcm-source.c
new file mode 100644
index 0000000..f079bf6
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm-source.c
@@ -0,0 +1,964 @@
+/* Spa ALSA Source
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/list.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+#include <spa/param/audio/format.h>
+#include <spa/pod/filter.h>
+
+#include "alsa.h"
+
+#include "alsa-pcm.h"
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+
+static const char default_device[] = "hw:0";
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, default_device, 64);
+ props->use_chmap = DEFAULT_USE_CHMAP;
+}
+
+static void emit_node_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ struct spa_dict_item items[7];
+ uint32_t i, n_items = 0;
+ char latency[64], period[64], nperiods[64], headroom[64];
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Source");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+ if (this->have_format) {
+ snprintf(latency, sizeof(latency), "%lu/%d", this->buffer_frames / 2, this->rate);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency);
+ snprintf(period, sizeof(period), "%lu", this->period_frames);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period);
+ snprintf(nperiods, sizeof(nperiods), "%lu", this->buffer_frames / this->period_frames);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods);
+ snprintf(headroom, sizeof(headroom), "%u", this->headroom);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom);
+ }
+ this->info.props = &SPA_DICT_INIT(items, n_items);
+
+ if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->info.n_params; i++) {
+ if (this->params[i].user > 0) {
+ this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->port_info.change_mask : 0;
+ if (full)
+ this->port_info.change_mask = this->port_info_all;
+ if (this->port_info.change_mask) {
+ uint32_t i;
+
+ if (this->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->port_info.n_params; i++) {
+ if (this->port_params[i].user > 0) {
+ this->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->port_params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &this->port_info);
+ this->port_info.change_mask = old;
+ }
+}
+
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct state *this = object;
+ struct spa_pod *param;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct props *p;
+ 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);
+
+ p = &this->props;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_API_ALSA_PATH),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_cardName),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA card name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)));
+ break;
+ case 3:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
+ SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, 0LL, 2 * SPA_NSEC_PER_SEC));
+ break;
+ default:
+ param = spa_alsa_enum_propinfo(this, result.index - 4, &b);
+ if (param == NULL)
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Props:
+ {
+ struct spa_pod_frame f;
+
+ switch (result.index) {
+ case 0:
+ spa_pod_builder_push_object(&b, &f,
+ SPA_TYPE_OBJECT_Props, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device)),
+ SPA_PROP_deviceName, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)),
+ SPA_PROP_cardName, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(this->process_latency.ns),
+ 0);
+ spa_alsa_add_prop_params(this, &b);
+ param = spa_pod_builder_pop(&b, &f);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_ProcessLatency:
+ switch (result.index) {
+ case 0:
+ param = spa_process_latency_build(&b, id, &this->process_latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ if (size > 0 && size < sizeof(struct spa_io_clock))
+ return -EINVAL;
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ spa_alsa_reassign_follower(this);
+ return 0;
+}
+
+static void handle_process_latency(struct state *this,
+ const struct spa_process_latency_info *info)
+{
+ bool ns_changed = this->process_latency.ns != info->ns;
+
+ if (this->process_latency.quantum == info->quantum &&
+ this->process_latency.rate == info->rate &&
+ !ns_changed)
+ return;
+
+ this->process_latency = *info;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ if (ns_changed)
+ this->params[NODE_Props].user++;
+ this->params[NODE_ProcessLatency].user++;
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_Latency].user++;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod *params = NULL;
+ int64_t lat_ns = -1;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&lat_ns),
+ SPA_PROP_params, SPA_POD_OPT_Pod(&params));
+
+ spa_alsa_parse_prop_params(this, params);
+ if (lat_ns != -1) {
+ struct spa_process_latency_info info;
+ info = this->process_latency;
+ info.ns = lat_ns;
+ handle_process_latency(this, &info);
+ }
+
+ emit_node_info(this, false);
+ emit_port_info(this, false);
+ break;
+ }
+ case SPA_PARAM_ProcessLatency:
+ {
+ struct spa_process_latency_info info;
+ if (param == NULL)
+ spa_zero(info);
+ else if ((res = spa_process_latency_parse(param, &info)) < 0)
+ return res;
+
+ handle_process_latency(this, &info);
+
+ emit_node_info(this, false);
+ emit_port_info(this, false);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_ParamBegin:
+ if ((res = spa_alsa_open(this, NULL)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_ParamEnd:
+ if (this->have_format)
+ return 0;
+ if ((res = spa_alsa_close(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Start:
+ if (!this->have_format)
+ return -EIO;
+ if (this->n_buffers == 0)
+ return -EIO;
+
+ if ((res = spa_alsa_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = spa_alsa_pause(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct state *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ return spa_alsa_enum_format(this, seq, start, num, filter);
+
+ case SPA_PARAM_Format:
+ if (!this->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &this->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!this->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->frame_size,
+ 16 * this->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ {
+ struct spa_latency_info latency = this->latency[result.index];
+ if (latency.direction == SPA_DIRECTION_OUTPUT)
+ spa_process_latency_info_add(&this->process_latency, &latency);
+ param = spa_latency_build(&b, id, &latency);
+ break;
+ }
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct state *this)
+{
+ if (this->n_buffers > 0) {
+ spa_list_init(&this->free);
+ spa_list_init(&this->ready);
+ this->n_buffers = 0;
+ }
+ 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 state *this = object;
+ int err = 0;
+
+ if (format == NULL) {
+ if (!this->have_format)
+ return 0;
+
+ spa_log_debug(this->log, "clear format");
+ spa_alsa_close(this);
+ clear_buffers(this);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if ((err = spa_alsa_set_format(this, &info, flags)) < 0)
+ return err;
+
+ this->current_format = info;
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ emit_node_info(this, false);
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ this->port_info.rate = SPA_FRACTION(1, this->rate);
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (this->have_format) {
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Latency].user++;
+ } else {
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, false);
+
+ return err;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, direction, port_id, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if (param == NULL)
+ info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(direction));
+ else if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ this->latency[info.direction] = info;
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_Latency].user++;
+ emit_port_info(this, false);
+ break;
+ }
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct state *this = object;
+ int res;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers);
+
+ if (this->n_buffers > 0) {
+ spa_alsa_pause(this);
+ if ((res = clear_buffers(this)) < 0)
+ return res;
+ }
+ if (n_buffers > 0 && !this->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &this->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->flags = 0;
+
+ b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_list_append(&this->free, &b->link);
+ }
+ this->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ this->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ this->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 state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+
+ if (this->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= this->n_buffers)
+ return -EINVAL;
+
+ spa_alsa_recycle_buffer(this, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct state *this = object;
+ struct spa_io_buffers *io;
+ struct buffer *b;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if ((io = this->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, "%p; status %d", this, io->status);
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (io->buffer_id < this->n_buffers) {
+ spa_alsa_recycle_buffer(this, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (spa_list_is_empty(&this->ready) && this->following) {
+ if (this->freewheel)
+ spa_alsa_skip(this);
+ else
+ spa_alsa_read(this);
+ }
+ if (spa_list_is_empty(&this->ready) || !this->following)
+ return SPA_STATUS_OK;
+
+ b = spa_list_first(&this->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct state *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct state *this;
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ this = (struct state *) handle;
+ spa_alsa_close(this);
+ spa_alsa_clear(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct state *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "%p: a data loop is needed", this);
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "%p: a data system is needed", this);
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Node, SPA_VERSION_NODE, &impl_node, this);
+
+ spa_hook_list_init(&this->hooks);
+ this->stream = SND_PCM_STREAM_CAPTURE;
+ this->port_direction = SPA_DIRECTION_OUTPUT;
+ this->latency[this->port_direction] = SPA_LATENCY_INFO(
+ this->port_direction,
+ .min_quantum = 1.0f,
+ .max_quantum = 1.0f);
+ this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+ reset_props(&this->props);
+
+ this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_info = SPA_PORT_INFO_INIT();
+ this->port_info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ this->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ this->port_info.params = this->port_params;
+ this->port_info.n_params = N_PORT_PARAMS;
+
+ spa_list_init(&this->free);
+ spa_list_init(&this->ready);
+
+ return spa_alsa_init(this, info);
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Record audio with the alsa API" },
+ { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<device>]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_alsa_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_PCM_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c
new file mode 100644
index 0000000..012b460
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm.c
@@ -0,0 +1,2696 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sched.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/time.h>
+#include <math.h>
+#include <limits.h>
+
+#include <spa/pod/filter.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+#include <spa/support/system.h>
+#include <spa/utils/keys.h>
+
+#include "alsa-pcm.h"
+
+static struct spa_list cards = SPA_LIST_INIT(&cards);
+
+static struct card *find_card(uint32_t index)
+{
+ struct card *c;
+ spa_list_for_each(c, &cards, link) {
+ if (c->index == index) {
+ c->ref++;
+ return c;
+ }
+ }
+ return NULL;
+}
+
+static struct card *ensure_card(uint32_t index, bool ucm)
+{
+ struct card *c;
+ char card_name[64];
+ const char *alibpref = NULL;
+ int err;
+
+ if ((c = find_card(index)) != NULL)
+ return c;
+
+ c = calloc(1, sizeof(*c));
+ c->ref = 1;
+ c->index = index;
+
+ if (ucm) {
+ snprintf(card_name, sizeof(card_name), "hw:%i", index);
+ err = snd_use_case_mgr_open(&c->ucm, card_name);
+ if (err < 0) {
+ char *name;
+ err = snd_card_get_name(index, &name);
+ if (err < 0)
+ goto error;
+
+ snprintf(card_name, sizeof(card_name), "%s", name);
+ free(name);
+
+ err = snd_use_case_mgr_open(&c->ucm, card_name);
+ if (err < 0)
+ goto error;
+ }
+ if ((snd_use_case_get(c->ucm, "_alibpref", &alibpref) != 0))
+ alibpref = NULL;
+ c->ucm_prefix = (char*)alibpref;
+ }
+ spa_list_append(&cards, &c->link);
+
+ return c;
+error:
+ free(c);
+ errno = -err;
+ return NULL;
+}
+
+static void release_card(struct card *c)
+{
+ spa_assert(c->ref > 0);
+
+ if (--c->ref > 0)
+ return;
+
+ spa_list_remove(&c->link);
+ if (c->ucm) {
+ free(c->ucm_prefix);
+ snd_use_case_mgr_close(c->ucm);
+ }
+ free(c);
+}
+
+static int alsa_set_param(struct state *state, const char *k, const char *s)
+{
+ int fmt_change = 0;
+ if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) {
+ state->default_channels = atoi(s);
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) {
+ state->default_rate = atoi(s);
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_FORMAT)) {
+ state->default_format = spa_alsa_format_from_name(s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
+ spa_alsa_parse_position(&state->default_pos, s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) {
+ state->n_allowed_rates = spa_alsa_parse_rates(state->allowed_rates,
+ MAX_RATES, s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, "iec958.codecs")) {
+ spa_alsa_parse_iec958_codecs(&state->iec958_codecs, s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, "api.alsa.period-size")) {
+ state->default_period_size = atoi(s);
+ } else if (spa_streq(k, "api.alsa.period-num")) {
+ state->default_period_num = atoi(s);
+ } else if (spa_streq(k, "api.alsa.headroom")) {
+ state->default_headroom = atoi(s);
+ } else if (spa_streq(k, "api.alsa.start-delay")) {
+ state->default_start_delay = atoi(s);
+ } else if (spa_streq(k, "api.alsa.disable-mmap")) {
+ state->disable_mmap = spa_atob(s);
+ } else if (spa_streq(k, "api.alsa.disable-batch")) {
+ state->disable_batch = spa_atob(s);
+ } else if (spa_streq(k, "api.alsa.use-chmap")) {
+ state->props.use_chmap = spa_atob(s);
+ } else if (spa_streq(k, "api.alsa.multi-rate")) {
+ state->multi_rate = spa_atob(s);
+ } else if (spa_streq(k, "latency.internal.rate")) {
+ state->process_latency.rate = atoi(s);
+ } else if (spa_streq(k, "latency.internal.ns")) {
+ state->process_latency.ns = atoi(s);
+ } else if (spa_streq(k, "clock.name")) {
+ spa_scnprintf(state->clock_name,
+ sizeof(state->clock_name), "%s", s);
+ } else
+ return 0;
+
+ if (fmt_change > 0) {
+ state->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ state->port_params[PORT_EnumFormat].user++;
+ }
+ return 1;
+}
+
+static int position_to_string(struct channel_map *map, char *val, size_t len)
+{
+ uint32_t i, o = 0;
+ int r;
+ o += snprintf(val, len, "[ ");
+ for (i = 0; i < map->channels; i++) {
+ r = snprintf(val+o, len-o, "%s%s", i == 0 ? "" : ", ",
+ spa_debug_type_find_short_name(spa_type_audio_channel,
+ map->pos[i]));
+ if (r < 0 || o + r >= len)
+ return -ENOSPC;
+ o += r;
+ }
+ if (len > o)
+ o += snprintf(val+o, len-o, " ]");
+ return 0;
+}
+
+static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, size_t len)
+{
+ uint32_t i, o = 0;
+ int r;
+ o += snprintf(val, len, "[ ");
+ for (i = 0; i < n_vals; i++) {
+ r = snprintf(val+o, len-o, "%s%d", i == 0 ? "" : ", ", vals[i]);
+ if (r < 0 || o + r >= len)
+ return -ENOSPC;
+ o += r;
+ }
+ if (len > o)
+ o += snprintf(val+o, len-o, " ]");
+ return 0;
+}
+
+struct spa_pod *spa_alsa_enum_propinfo(struct state *state,
+ uint32_t idx, struct spa_pod_builder *b)
+{
+ struct spa_pod *param;
+
+ switch (idx) {
+ case 0:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_CHANNELS),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Channels"),
+ SPA_PROP_INFO_type, SPA_POD_Int(state->default_channels),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_RATE),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Rate"),
+ SPA_PROP_INFO_type, SPA_POD_Int(state->default_rate),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_FORMAT),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Format"),
+ SPA_PROP_INFO_type, SPA_POD_String(
+ spa_debug_type_find_short_name(spa_type_audio_format,
+ state->default_format)),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 3:
+ {
+ char buf[1024];
+ position_to_string(&state->default_pos, buf, sizeof(buf));
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_POSITION),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Position"),
+ SPA_PROP_INFO_type, SPA_POD_String(buf),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ }
+ case 4:
+ {
+ char buf[1024];
+ uint32_array_to_string(state->allowed_rates, state->n_allowed_rates, buf, sizeof(buf));
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_ALLOWED_RATES),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Allowed Rates"),
+ SPA_PROP_INFO_type, SPA_POD_String(buf),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ }
+ case 5:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.period-size"),
+ SPA_PROP_INFO_description, SPA_POD_String("Period Size"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_period_size, 0, 8192),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 6:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.period-num"),
+ SPA_PROP_INFO_description, SPA_POD_String("Number of Periods"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_period_num, 0, 1024),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 7:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.headroom"),
+ SPA_PROP_INFO_description, SPA_POD_String("Headroom"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_headroom, 0, 8192),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 8:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.start-delay"),
+ SPA_PROP_INFO_description, SPA_POD_String("Start Delay"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_start_delay, 0, 8192),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 9:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.disable-mmap"),
+ SPA_PROP_INFO_description, SPA_POD_String("Disable MMAP"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->disable_mmap),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 10:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.disable-batch"),
+ SPA_PROP_INFO_description, SPA_POD_String("Disable Batch"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->disable_batch),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 11:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.use-chmap"),
+ SPA_PROP_INFO_description, SPA_POD_String("Use the driver channelmap"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->props.use_chmap),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 12:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.multi-rate"),
+ SPA_PROP_INFO_description, SPA_POD_String("Support multiple rates"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->multi_rate),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 13:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("latency.internal.rate"),
+ SPA_PROP_INFO_description, SPA_POD_String("Internal latency in samples"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->process_latency.rate,
+ 0, 65536),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 14:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("latency.internal.ns"),
+ SPA_PROP_INFO_description, SPA_POD_String("Internal latency in nanoseconds"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(state->process_latency.ns,
+ 0LL, 2 * SPA_NSEC_PER_SEC),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 15:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("clock.name"),
+ SPA_PROP_INFO_description, SPA_POD_String("The name of the clock"),
+ SPA_PROP_INFO_type, SPA_POD_String(state->clock_name),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ default:
+ return NULL;
+ }
+ return param;
+}
+
+int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b)
+{
+ struct spa_pod_frame f[1];
+ char buf[1024];
+
+ spa_pod_builder_prop(b, SPA_PROP_params, 0);
+ spa_pod_builder_push_struct(b, &f[0]);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_CHANNELS);
+ spa_pod_builder_int(b, state->default_channels);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_RATE);
+ spa_pod_builder_int(b, state->default_rate);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_FORMAT);
+ spa_pod_builder_string(b,
+ spa_debug_type_find_short_name(spa_type_audio_format,
+ state->default_format));
+
+ position_to_string(&state->default_pos, buf, sizeof(buf));
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_POSITION);
+ spa_pod_builder_string(b, buf);
+
+ uint32_array_to_string(state->allowed_rates, state->n_allowed_rates,
+ buf, sizeof(buf));
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_ALLOWED_RATES);
+ spa_pod_builder_string(b, buf);
+
+ spa_pod_builder_string(b, "api.alsa.period-size");
+ spa_pod_builder_int(b, state->default_period_size);
+
+ spa_pod_builder_string(b, "api.alsa.period-num");
+ spa_pod_builder_int(b, state->default_period_num);
+
+ spa_pod_builder_string(b, "api.alsa.headroom");
+ spa_pod_builder_int(b, state->default_headroom);
+
+ spa_pod_builder_string(b, "api.alsa.start-delay");
+ spa_pod_builder_int(b, state->default_start_delay);
+
+ spa_pod_builder_string(b, "api.alsa.disable-mmap");
+ spa_pod_builder_bool(b, state->disable_mmap);
+
+ spa_pod_builder_string(b, "api.alsa.disable-batch");
+ spa_pod_builder_bool(b, state->disable_batch);
+
+ spa_pod_builder_string(b, "api.alsa.use-chmap");
+ spa_pod_builder_bool(b, state->props.use_chmap);
+
+ spa_pod_builder_string(b, "api.alsa.multi-rate");
+ spa_pod_builder_bool(b, state->multi_rate);
+
+ spa_pod_builder_string(b, "latency.internal.rate");
+ spa_pod_builder_int(b, state->process_latency.rate);
+
+ spa_pod_builder_string(b, "latency.internal.ns");
+ spa_pod_builder_long(b, state->process_latency.ns);
+
+ spa_pod_builder_string(b, "clock.name");
+ spa_pod_builder_string(b, state->clock_name);
+
+ spa_pod_builder_pop(b, &f[0]);
+ return 0;
+}
+
+int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params)
+{
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ int changed = 0;
+
+ if (params == NULL)
+ return 0;
+
+ spa_pod_parser_pod(&prs, params);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0)
+ return 0;
+
+ while (true) {
+ const char *name;
+ struct spa_pod *pod;
+ char value[512];
+
+ if (spa_pod_parser_get_string(&prs, &name) < 0)
+ break;
+
+ if (spa_pod_parser_get_pod(&prs, &pod) < 0)
+ break;
+ if (spa_pod_is_string(pod)) {
+ spa_pod_copy_string(pod, sizeof(value), value);
+ } else if (spa_pod_is_int(pod)) {
+ snprintf(value, sizeof(value), "%d",
+ SPA_POD_VALUE(struct spa_pod_int, pod));
+ } else if (spa_pod_is_long(pod)) {
+ snprintf(value, sizeof(value), "%"PRIi64,
+ SPA_POD_VALUE(struct spa_pod_long, pod));
+ } else if (spa_pod_is_bool(pod)) {
+ snprintf(value, sizeof(value), "%s",
+ SPA_POD_VALUE(struct spa_pod_bool, pod) ?
+ "true" : "false");
+ } else
+ continue;
+
+ spa_log_info(state->log, "key:'%s' val:'%s'", name, value);
+ alsa_set_param(state, name, value);
+ changed++;
+ }
+ if (changed > 0) {
+ state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ state->params[NODE_Props].user++;
+ }
+ return changed;
+}
+
+#define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; }
+
+static ssize_t log_write(void *cookie, const char *buf, size_t size)
+{
+ struct state *state = cookie;
+ int len;
+
+ while (size > 0) {
+ len = strcspn(buf, "\n");
+ if (len > 0)
+ spa_log_debug(state->log, "%.*s", (int)len, buf);
+ buf += len + 1;
+ size -= len + 1;
+ }
+ return size;
+}
+
+static cookie_io_functions_t io_funcs = {
+ .write = log_write,
+};
+
+int spa_alsa_init(struct state *state, const struct spa_dict *info)
+{
+ uint32_t i;
+ int err;
+
+ snd_config_update_free_global();
+
+ state->multi_rate = true;
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, SPA_KEY_API_ALSA_PATH)) {
+ snprintf(state->props.device, 63, "%s", s);
+ } else if (spa_streq(k, SPA_KEY_API_ALSA_PCM_CARD)) {
+ state->card_index = atoi(s);
+ } else if (spa_streq(k, SPA_KEY_API_ALSA_OPEN_UCM)) {
+ state->open_ucm = spa_atob(s);
+ } else if (spa_streq(k, "clock.quantum-limit")) {
+ spa_atou32(s, &state->quantum_limit, 0);
+ } else {
+ alsa_set_param(state, k, s);
+ }
+ }
+ if (state->clock_name[0] == '\0')
+ snprintf(state->clock_name, sizeof(state->clock_name),
+ "api.alsa.%s-%u",
+ state->stream == SND_PCM_STREAM_PLAYBACK ? "p" : "c",
+ state->card_index);
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK) {
+ state->is_iec958 = spa_strstartswith(state->props.device, "iec958");
+ state->is_hdmi = spa_strstartswith(state->props.device, "hdmi");
+ 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->log_file = fopencookie(state, "w", io_funcs);
+ if (state->log_file == NULL) {
+ spa_log_error(state->log, "can't create log file");
+ return -errno;
+ }
+ CHECK(snd_output_stdio_attach(&state->output, state->log_file, 0), "attach failed");
+
+ state->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+ state->rate_limit.burst = 1;
+
+ return 0;
+}
+
+int spa_alsa_clear(struct state *state)
+{
+ int err;
+
+ release_card(state->card);
+
+ state->card = NULL;
+ state->card_index = SPA_ID_INVALID;
+
+ if ((err = snd_output_close(state->output)) < 0)
+ spa_log_warn(state->log, "output close failed: %s", snd_strerror(err));
+ fclose(state->log_file);
+
+ return err;
+}
+
+int spa_alsa_open(struct state *state, const char *params)
+{
+ int err;
+ struct props *props = &state->props;
+ char device_name[256];
+
+ if (state->opened)
+ return 0;
+
+ spa_scnprintf(device_name, sizeof(device_name), "%s%s%s",
+ state->card->ucm_prefix ? state->card->ucm_prefix : "",
+ props->device, params ? params : "");
+
+ spa_log_info(state->log, "%p: ALSA device open '%s' %s", state, device_name,
+ state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback");
+ CHECK(snd_pcm_open(&state->hndl,
+ device_name,
+ state->stream,
+ SND_PCM_NONBLOCK |
+ SND_PCM_NO_AUTO_RESAMPLE |
+ SND_PCM_NO_AUTO_CHANNELS | SND_PCM_NO_AUTO_FORMAT), "'%s': %s open failed",
+ device_name,
+ state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback");
+
+ if ((err = spa_system_timerfd_create(state->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_exit_close;
+
+ state->timerfd = err;
+
+ if (state->clock)
+ spa_scnprintf(state->clock->name, sizeof(state->clock->name),
+ "%s", state->clock_name);
+ state->opened = true;
+ state->sample_count = 0;
+ state->sample_time = 0;
+
+ return 0;
+
+error_exit_close:
+ spa_log_info(state->log, "%p: Device '%s' closing: %s", state, state->props.device,
+ spa_strerror(err));
+ snd_pcm_close(state->hndl);
+ return err;
+}
+
+int spa_alsa_close(struct state *state)
+{
+ int err = 0;
+
+ if (!state->opened)
+ return 0;
+
+ spa_alsa_pause(state);
+
+ spa_log_info(state->log, "%p: Device '%s' closing", state, state->props.device);
+ if ((err = snd_pcm_close(state->hndl)) < 0)
+ spa_log_warn(state->log, "%s: close failed: %s", state->props.device,
+ snd_strerror(err));
+
+ spa_system_close(state->data_system, state->timerfd);
+
+ if (state->have_format)
+ state->card->format_ref--;
+
+ state->have_format = false;
+ state->opened = false;
+
+ return err;
+}
+
+struct format_info {
+ uint32_t spa_format;
+ uint32_t spa_pformat;
+ snd_pcm_format_t format;
+};
+
+static const struct format_info format_info[] = {
+ { SPA_AUDIO_FORMAT_UNKNOWN, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_UNKNOWN},
+ { SPA_AUDIO_FORMAT_F32_LE, SPA_AUDIO_FORMAT_F32P, SND_PCM_FORMAT_FLOAT_LE},
+ { SPA_AUDIO_FORMAT_F32_BE, SPA_AUDIO_FORMAT_F32P, SND_PCM_FORMAT_FLOAT_BE},
+ { SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_S32P, SND_PCM_FORMAT_S32_LE},
+ { SPA_AUDIO_FORMAT_S32_BE, SPA_AUDIO_FORMAT_S32P, SND_PCM_FORMAT_S32_BE},
+ { SPA_AUDIO_FORMAT_S24_32_LE, SPA_AUDIO_FORMAT_S24_32P, SND_PCM_FORMAT_S24_LE},
+ { SPA_AUDIO_FORMAT_S24_32_BE, SPA_AUDIO_FORMAT_S24_32P, SND_PCM_FORMAT_S24_BE},
+ { SPA_AUDIO_FORMAT_S24_LE, SPA_AUDIO_FORMAT_S24P, SND_PCM_FORMAT_S24_3LE},
+ { SPA_AUDIO_FORMAT_S24_BE, SPA_AUDIO_FORMAT_S24P, SND_PCM_FORMAT_S24_3BE},
+ { SPA_AUDIO_FORMAT_S16_LE, SPA_AUDIO_FORMAT_S16P, SND_PCM_FORMAT_S16_LE},
+ { SPA_AUDIO_FORMAT_S16_BE, SPA_AUDIO_FORMAT_S16P, SND_PCM_FORMAT_S16_BE},
+ { SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_S8},
+ { SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_U8P, SND_PCM_FORMAT_U8},
+ { SPA_AUDIO_FORMAT_U16_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U16_LE},
+ { SPA_AUDIO_FORMAT_U16_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U16_BE},
+ { SPA_AUDIO_FORMAT_U24_32_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_LE},
+ { SPA_AUDIO_FORMAT_U24_32_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_BE},
+ { SPA_AUDIO_FORMAT_U24_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_3LE},
+ { SPA_AUDIO_FORMAT_U24_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_3BE},
+ { SPA_AUDIO_FORMAT_U32_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U32_LE},
+ { SPA_AUDIO_FORMAT_U32_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U32_BE},
+ { SPA_AUDIO_FORMAT_F64_LE, SPA_AUDIO_FORMAT_F64P, SND_PCM_FORMAT_FLOAT64_LE},
+ { SPA_AUDIO_FORMAT_F64_BE, SPA_AUDIO_FORMAT_F64P, SND_PCM_FORMAT_FLOAT64_BE},
+};
+
+static snd_pcm_format_t spa_format_to_alsa(uint32_t format, bool *planar)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i) {
+ *planar = i->spa_pformat == format;
+ if (i->spa_format == format || *planar)
+ return i->format;
+ }
+ return SND_PCM_FORMAT_UNKNOWN;
+}
+
+struct chmap_info {
+ enum snd_pcm_chmap_position pos;
+ enum spa_audio_channel channel;
+};
+
+static const struct chmap_info chmap_info[] = {
+ [SND_CHMAP_UNKNOWN] = { SND_CHMAP_UNKNOWN, SPA_AUDIO_CHANNEL_UNKNOWN },
+ [SND_CHMAP_NA] = { SND_CHMAP_NA, SPA_AUDIO_CHANNEL_NA },
+ [SND_CHMAP_MONO] = { SND_CHMAP_MONO, SPA_AUDIO_CHANNEL_MONO },
+ [SND_CHMAP_FL] = { SND_CHMAP_FL, SPA_AUDIO_CHANNEL_FL },
+ [SND_CHMAP_FR] = { SND_CHMAP_FR, SPA_AUDIO_CHANNEL_FR },
+ [SND_CHMAP_RL] = { SND_CHMAP_RL, SPA_AUDIO_CHANNEL_RL },
+ [SND_CHMAP_RR] = { SND_CHMAP_RR, SPA_AUDIO_CHANNEL_RR },
+ [SND_CHMAP_FC] = { SND_CHMAP_FC, SPA_AUDIO_CHANNEL_FC },
+ [SND_CHMAP_LFE] = { SND_CHMAP_LFE, SPA_AUDIO_CHANNEL_LFE },
+ [SND_CHMAP_SL] = { SND_CHMAP_SL, SPA_AUDIO_CHANNEL_SL },
+ [SND_CHMAP_SR] = { SND_CHMAP_SR, SPA_AUDIO_CHANNEL_SR },
+ [SND_CHMAP_RC] = { SND_CHMAP_RC, SPA_AUDIO_CHANNEL_RC },
+ [SND_CHMAP_FLC] = { SND_CHMAP_FLC, SPA_AUDIO_CHANNEL_FLC },
+ [SND_CHMAP_FRC] = { SND_CHMAP_FRC, SPA_AUDIO_CHANNEL_FRC },
+ [SND_CHMAP_RLC] = { SND_CHMAP_RLC, SPA_AUDIO_CHANNEL_RLC },
+ [SND_CHMAP_RRC] = { SND_CHMAP_RRC, SPA_AUDIO_CHANNEL_RRC },
+ [SND_CHMAP_FLW] = { SND_CHMAP_FLW, SPA_AUDIO_CHANNEL_FLW },
+ [SND_CHMAP_FRW] = { SND_CHMAP_FRW, SPA_AUDIO_CHANNEL_FRW },
+ [SND_CHMAP_FLH] = { SND_CHMAP_FLH, SPA_AUDIO_CHANNEL_FLH },
+ [SND_CHMAP_FCH] = { SND_CHMAP_FCH, SPA_AUDIO_CHANNEL_FCH },
+ [SND_CHMAP_FRH] = { SND_CHMAP_FRH, SPA_AUDIO_CHANNEL_FRH },
+ [SND_CHMAP_TC] = { SND_CHMAP_TC, SPA_AUDIO_CHANNEL_TC },
+ [SND_CHMAP_TFL] = { SND_CHMAP_TFL, SPA_AUDIO_CHANNEL_TFL },
+ [SND_CHMAP_TFR] = { SND_CHMAP_TFR, SPA_AUDIO_CHANNEL_TFR },
+ [SND_CHMAP_TFC] = { SND_CHMAP_TFC, SPA_AUDIO_CHANNEL_TFC },
+ [SND_CHMAP_TRL] = { SND_CHMAP_TRL, SPA_AUDIO_CHANNEL_TRL },
+ [SND_CHMAP_TRR] = { SND_CHMAP_TRR, SPA_AUDIO_CHANNEL_TRR },
+ [SND_CHMAP_TRC] = { SND_CHMAP_TRC, SPA_AUDIO_CHANNEL_TRC },
+ [SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, SPA_AUDIO_CHANNEL_TFLC },
+ [SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, SPA_AUDIO_CHANNEL_TFRC },
+ [SND_CHMAP_TSL] = { SND_CHMAP_TSL, SPA_AUDIO_CHANNEL_TSL },
+ [SND_CHMAP_TSR] = { SND_CHMAP_TSR, SPA_AUDIO_CHANNEL_TSR },
+ [SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, SPA_AUDIO_CHANNEL_LLFE },
+ [SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, SPA_AUDIO_CHANNEL_RLFE },
+ [SND_CHMAP_BC] = { SND_CHMAP_BC, SPA_AUDIO_CHANNEL_BC },
+ [SND_CHMAP_BLC] = { SND_CHMAP_BLC, SPA_AUDIO_CHANNEL_BLC },
+ [SND_CHMAP_BRC] = { SND_CHMAP_BRC, SPA_AUDIO_CHANNEL_BRC },
+};
+
+#define _M(ch) (1LL << SND_CHMAP_ ##ch)
+
+struct def_mask {
+ int channels;
+ uint64_t mask;
+};
+
+static const struct def_mask default_layouts[] = {
+ { 0, 0 },
+ { 1, _M(MONO) },
+ { 2, _M(FL) | _M(FR) },
+ { 3, _M(FL) | _M(FR) | _M(LFE) },
+ { 4, _M(FL) | _M(FR) | _M(RL) |_M(RR) },
+ { 5, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(FC) },
+ { 6, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(FC) | _M(LFE) },
+ { 7, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(SL) | _M(SR) | _M(FC) },
+ { 8, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(SL) | _M(SR) | _M(FC) | _M(LFE) },
+};
+
+#define _C(ch) (SPA_AUDIO_CHANNEL_ ##ch)
+
+static const struct channel_map default_map[] = {
+ { 0, { 0, } } ,
+ { 1, { _C(MONO), } },
+ { 2, { _C(FL), _C(FR), } },
+ { 3, { _C(FL), _C(FR), _C(LFE) } },
+ { 4, { _C(FL), _C(FR), _C(RL), _C(RR), } },
+ { 5, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC) } },
+ { 6, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(LFE), } },
+ { 7, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(SL), _C(SR), } },
+ { 8, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(LFE), _C(SL), _C(SR), } },
+};
+
+static enum spa_audio_channel chmap_position_to_channel(enum snd_pcm_chmap_position pos)
+{
+ return chmap_info[pos].channel;
+}
+
+static void sanitize_map(snd_pcm_chmap_t* map)
+{
+ uint64_t mask = 0, p, dup = 0;
+ const struct def_mask *def;
+ uint32_t i, j, pos;
+
+ for (i = 0; i < map->channels; i++) {
+ if (map->pos[i] > SND_CHMAP_LAST)
+ map->pos[i] = SND_CHMAP_UNKNOWN;
+
+ p = 1LL << map->pos[i];
+ if (mask & p) {
+ /* duplicate channel */
+ for (j = 0; j <= i; j++)
+ if (map->pos[j] == map->pos[i])
+ map->pos[j] = SND_CHMAP_UNKNOWN;
+ dup |= p;
+ p = 1LL << SND_CHMAP_UNKNOWN;
+ }
+ mask |= p;
+ }
+ if ((mask & (1LL << SND_CHMAP_UNKNOWN)) == 0)
+ return;
+
+ def = &default_layouts[map->channels];
+
+ /* remove duplicates */
+ mask &= ~dup;
+ /* keep unassigned channels */
+ mask = def->mask & ~mask;
+
+ pos = 0;
+ for (i = 0; i < map->channels; i++) {
+ if (map->pos[i] == SND_CHMAP_UNKNOWN) {
+ do {
+ mask >>= 1;
+ pos++;
+ }
+ while (mask != 0 && (mask & 1) == 0);
+ map->pos[i] = mask ? pos : 0;
+ }
+
+ }
+}
+
+static bool uint32_array_contains(uint32_t *vals, uint32_t n_vals, uint32_t val)
+{
+ uint32_t i;
+ for (i = 0; i < n_vals; i++)
+ if (vals[i] == val)
+ return true;
+ return false;
+}
+
+static int add_rate(struct state *state, uint32_t scale, uint32_t interleave, bool all, uint32_t index, uint32_t *next,
+ uint32_t min_allowed_rate, snd_pcm_hw_params_t *params, struct spa_pod_builder *b)
+{
+ struct spa_pod_frame f[1];
+ int err, dir;
+ unsigned int min, max;
+ struct spa_pod_choice *choice;
+ uint32_t rate;
+
+ CHECK(snd_pcm_hw_params_get_rate_min(params, &min, &dir), "get_rate_min");
+ CHECK(snd_pcm_hw_params_get_rate_max(params, &max, &dir), "get_rate_max");
+
+ spa_log_debug(state->log, "min:%u max:%u min-allowed:%u scale:%u interleave:%u all:%d",
+ min, max, min_allowed_rate, scale, interleave, all);
+
+ min = SPA_MAX(min_allowed_rate * scale / interleave, min) * interleave / scale;
+ max = max * interleave / scale;
+ if (max < min)
+ return 0;
+
+ if (!state->multi_rate && state->card->format_ref > 0)
+ rate = state->card->rate;
+ else
+ rate = state->default_rate;
+
+ if (rate < min || rate > max)
+ rate = 0;
+
+ if (rate != 0 && !all)
+ min = max = rate;
+
+ if (rate == 0)
+ rate = state->position ? state->position->clock.rate.denom : DEFAULT_RATE;
+
+ rate = SPA_CLAMP(rate, min, max);
+
+ spa_log_debug(state->log, "rate:%u multi:%d card:%d def:%d",
+ rate, state->multi_rate, state->card->rate, state->default_rate);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[0]);
+
+ if (state->n_allowed_rates > 0) {
+ uint32_t i, v, last = 0, count = 0;
+
+ if (uint32_array_contains(state->allowed_rates, state->n_allowed_rates, rate)) {
+ spa_pod_builder_int(b, rate * scale);
+ count++;
+ }
+ for (i = 0; i < state->n_allowed_rates; i++) {
+ v = SPA_CLAMP(state->allowed_rates[i], min, max);
+ if (v != last &&
+ uint32_array_contains(state->allowed_rates, state->n_allowed_rates, v)) {
+ spa_pod_builder_int(b, v * scale);
+ if (count == 0)
+ spa_pod_builder_int(b, v * scale);
+ count++;
+ }
+ last = v;
+ }
+ if (count > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ } else {
+ spa_pod_builder_int(b, rate * scale);
+
+ if (min != max) {
+ spa_pod_builder_int(b, min * scale);
+ spa_pod_builder_int(b, max * scale);
+ choice->body.type = SPA_CHOICE_Range;
+ }
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ return 1;
+}
+
+static int add_channels(struct state *state, bool all, uint32_t index, uint32_t *next,
+ snd_pcm_hw_params_t *params, struct spa_pod_builder *b)
+{
+ struct spa_pod_frame f[1];
+ size_t i;
+ int err;
+ snd_pcm_t *hndl = state->hndl;
+ snd_pcm_chmap_query_t **maps;
+ unsigned int min, max;
+
+ CHECK(snd_pcm_hw_params_get_channels_min(params, &min), "get_channels_min");
+ CHECK(snd_pcm_hw_params_get_channels_max(params, &max), "get_channels_max");
+ spa_log_debug(state->log, "channels (%d %d) default:%d all:%d",
+ min, max, state->default_channels, all);
+
+ if (state->default_channels != 0 && !all) {
+ if (min < state->default_channels)
+ min = state->default_channels;
+ if (max > state->default_channels)
+ max = state->default_channels;
+ }
+ min = SPA_MIN(min, SPA_AUDIO_MAX_CHANNELS);
+ max = SPA_MIN(max, SPA_AUDIO_MAX_CHANNELS);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_channels, 0);
+
+ if (state->props.use_chmap && (maps = snd_pcm_query_chmaps(hndl)) != NULL) {
+ uint32_t channel;
+ snd_pcm_chmap_t* map;
+
+skip_channels:
+ if (maps[index] == NULL) {
+ snd_pcm_free_chmaps(maps);
+ return 0;
+ }
+ map = &maps[index]->map;
+
+ spa_log_debug(state->log, "map %d channels (%d %d)", map->channels, min, max);
+
+ if (map->channels < min || map->channels > max) {
+ index = (*next)++;
+ goto skip_channels;
+ }
+
+ sanitize_map(map);
+ spa_pod_builder_int(b, map->channels);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_position, 0);
+ spa_pod_builder_push_array(b, &f[0]);
+ for (i = 0; i < map->channels; i++) {
+ spa_log_debug(state->log, "%p: position %zd %d", state, i, map->pos[i]);
+ channel = chmap_position_to_channel(map->pos[i]);
+ spa_pod_builder_id(b, channel);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ snd_pcm_free_chmaps(maps);
+ }
+ else {
+ const struct channel_map *map = NULL;
+ struct spa_pod_choice *choice;
+
+ if (index > 0)
+ return 0;
+
+ spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[0]);
+ spa_pod_builder_int(b, max);
+ if (min != max) {
+ spa_pod_builder_int(b, min);
+ spa_pod_builder_int(b, max);
+ choice->body.type = SPA_CHOICE_Range;
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ if (min == max) {
+ if (state->default_pos.channels == min)
+ map = &state->default_pos;
+ else if (min == max && min <= 8)
+ map = &default_map[min];
+ }
+ if (map) {
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_position, 0);
+ spa_pod_builder_push_array(b, &f[0]);
+ for (i = 0; i < map->channels; i++) {
+ spa_log_debug(state->log, "%p: position %zd %d", state, i, map->pos[i]);
+ spa_pod_builder_id(b, map->pos[i]);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+ }
+ }
+ return 1;
+}
+
+static void debug_hw_params(struct state *state, const char *prefix, snd_pcm_hw_params_t *params)
+{
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) {
+ spa_log_debug(state->log, "%s:", prefix);
+ snd_pcm_hw_params_dump(params, state->output);
+ fflush(state->log_file);
+ }
+}
+static int enum_pcm_formats(struct state *state, uint32_t index, uint32_t *next,
+ struct spa_pod **result, struct spa_pod_builder *b)
+{
+ int res, err;
+ size_t j;
+ snd_pcm_t *hndl;
+ snd_pcm_hw_params_t *params;
+ struct spa_pod_frame f[2];
+ snd_pcm_format_mask_t *fmask;
+ snd_pcm_access_mask_t *amask;
+ unsigned int rrate, rchannels;
+ struct spa_pod_choice *choice;
+
+ hndl = state->hndl;
+ snd_pcm_hw_params_alloca(&params);
+ CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
+
+ debug_hw_params(state, __func__, params);
+
+ CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
+
+ if (state->default_channels != 0) {
+ rchannels = state->default_channels;
+ CHECK(snd_pcm_hw_params_set_channels_near(hndl, params, &rchannels), "set_channels");
+ if (state->default_channels != rchannels) {
+ spa_log_warn(state->log, "%s: Channels doesn't match (requested %u, got %u)",
+ state->props.device, state->default_channels, rchannels);
+ }
+ }
+ if (state->default_rate != 0) {
+ rrate = state->default_rate;
+ CHECK(snd_pcm_hw_params_set_rate_near(hndl, params, &rrate, 0), "set_rate_near");
+ if (state->default_rate != rrate) {
+ spa_log_warn(state->log, "%s: Rate doesn't match (requested %u, got %u)",
+ state->props.device, state->default_rate, rrate);
+ }
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ 0);
+
+ snd_pcm_format_mask_alloca(&fmask);
+ snd_pcm_hw_params_get_format_mask(params, fmask);
+
+ snd_pcm_access_mask_alloca(&amask);
+ snd_pcm_hw_params_get_access_mask(params, amask);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_format, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+
+ j = 0;
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, fi) {
+ if (fi->format == SND_PCM_FORMAT_UNKNOWN)
+ continue;
+
+ if (snd_pcm_format_mask_test(fmask, fi->format)) {
+ if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED) ||
+ snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_RW_NONINTERLEAVED)) &&
+ fi->spa_pformat != SPA_AUDIO_FORMAT_UNKNOWN &&
+ (state->default_format == 0 || state->default_format == fi->spa_pformat)) {
+ if (j++ == 0)
+ spa_pod_builder_id(b, fi->spa_pformat);
+ spa_pod_builder_id(b, fi->spa_pformat);
+ }
+ if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_INTERLEAVED) ||
+ snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_RW_INTERLEAVED)) &&
+ (state->default_format == 0 || state->default_format == fi->spa_format)) {
+ if (j++ == 0)
+ spa_pod_builder_id(b, fi->spa_format);
+ spa_pod_builder_id(b, fi->spa_format);
+ }
+ }
+ }
+ if (j == 0) {
+ char buf[1024];
+ int i, r, offs;
+
+ for (i = 0, offs = 0; i <= SND_PCM_FORMAT_LAST; i++) {
+ if (snd_pcm_format_mask_test(fmask, (snd_pcm_format_t)i)) {
+ r = snprintf(&buf[offs], sizeof(buf) - offs,
+ "%s ", snd_pcm_format_name((snd_pcm_format_t)i));
+ if (r < 0 || r + offs >= (int)sizeof(buf))
+ return -ENOSPC;
+ offs += r;
+ }
+ }
+ spa_log_warn(state->log, "%s: no format found (def:%d) formats:%s",
+ state->props.device, state->default_format, buf);
+
+ for (i = 0, offs = 0; i <= SND_PCM_ACCESS_LAST; i++) {
+ if (snd_pcm_access_mask_test(amask, (snd_pcm_access_t)i)) {
+ r = snprintf(&buf[offs], sizeof(buf) - offs,
+ "%s ", snd_pcm_access_name((snd_pcm_access_t)i));
+ if (r < 0 || r + offs >= (int)sizeof(buf))
+ return -ENOSPC;
+ offs += r;
+ }
+ }
+ spa_log_warn(state->log, "%s: access:%s", state->props.device, buf);
+ return -ENOTSUP;
+ }
+ if (j > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if ((res = add_rate(state, 1, 1, false, index & 0xffff, next, 0, params, b)) != 1)
+ return res;
+
+ if ((res = add_channels(state, false, index & 0xffff, next, params, b)) != 1)
+ return res;
+
+ *result = spa_pod_builder_pop(b, &f[0]);
+ return 1;
+}
+
+static bool codec_supported(uint32_t codec, unsigned int chmax, unsigned int rmax)
+{
+ switch (codec) {
+ case SPA_AUDIO_IEC958_CODEC_PCM:
+ case SPA_AUDIO_IEC958_CODEC_DTS:
+ case SPA_AUDIO_IEC958_CODEC_AC3:
+ case SPA_AUDIO_IEC958_CODEC_MPEG:
+ case SPA_AUDIO_IEC958_CODEC_MPEG2_AAC:
+ if (chmax >= 2)
+ return true;
+ break;
+ case SPA_AUDIO_IEC958_CODEC_EAC3:
+ if (rmax >= 48000 * 4 && chmax >= 2)
+ return true;
+ break;
+ case SPA_AUDIO_IEC958_CODEC_TRUEHD:
+ case SPA_AUDIO_IEC958_CODEC_DTSHD:
+ if (chmax >= 8)
+ return true;
+ break;
+ }
+ return false;
+}
+
+static int enum_iec958_formats(struct state *state, uint32_t index, uint32_t *next,
+ struct spa_pod **result, struct spa_pod_builder *b)
+{
+ int res, err, dir;
+ snd_pcm_t *hndl;
+ snd_pcm_hw_params_t *params;
+ struct spa_pod_frame f[2];
+ unsigned int rmin, rmax;
+ unsigned int chmin, chmax;
+ uint32_t i, c, codecs[16], n_codecs;
+
+ if ((index & 0xffff) > 0)
+ return 0;
+
+ if (!(state->is_iec958 || state->is_hdmi))
+ return 0;
+ if (state->iec958_codecs == 0)
+ return 0;
+
+ hndl = state->hndl;
+ snd_pcm_hw_params_alloca(&params);
+ CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
+
+ debug_hw_params(state, __func__, params);
+
+ CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_iec958),
+ 0);
+
+ CHECK(snd_pcm_hw_params_get_channels_min(params, &chmin), "get_channels_min");
+ CHECK(snd_pcm_hw_params_get_channels_max(params, &chmax), "get_channels_max");
+ spa_log_debug(state->log, "channels (%d %d)", chmin, chmax);
+
+ CHECK(snd_pcm_hw_params_get_rate_min(params, &rmin, &dir), "get_rate_min");
+ CHECK(snd_pcm_hw_params_get_rate_max(params, &rmax, &dir), "get_rate_max");
+ spa_log_debug(state->log, "rate (%d %d)", rmin, rmax);
+
+ if (state->default_rate != 0) {
+ if (rmin < state->default_rate)
+ rmin = state->default_rate;
+ if (rmax > state->default_rate)
+ rmax = state->default_rate;
+ }
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_iec958Codec, 0);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+
+ n_codecs = spa_alsa_get_iec958_codecs(state, codecs, SPA_N_ELEMENTS(codecs));
+ for (i = 0, c = 0; i < n_codecs; i++) {
+ if (!codec_supported(codecs[i], chmax, rmax))
+ continue;
+ if (c++ == 0)
+ spa_pod_builder_id(b, codecs[i]);
+ spa_pod_builder_id(b, codecs[i]);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+
+ if ((res = add_rate(state, 1, 1, true, index & 0xffff, next, 0, params, b)) != 1)
+ return res;
+
+ (*next)++;
+ *result = spa_pod_builder_pop(b, &f[0]);
+ return 1;
+}
+
+static int enum_dsd_formats(struct state *state, uint32_t index, uint32_t *next,
+ struct spa_pod **result, struct spa_pod_builder *b)
+{
+ int res, err;
+ snd_pcm_t *hndl;
+ snd_pcm_hw_params_t *params;
+ snd_pcm_format_mask_t *fmask;
+ struct spa_pod_frame f[2];
+ int32_t interleave;
+
+ if ((index & 0xffff) > 0)
+ return 0;
+
+ hndl = state->hndl;
+ snd_pcm_hw_params_alloca(&params);
+ CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
+
+ debug_hw_params(state, __func__, params);
+
+ snd_pcm_format_mask_alloca(&fmask);
+ snd_pcm_hw_params_get_format_mask(params, fmask);
+
+ if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U32_BE))
+ interleave = 4;
+ else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U32_LE))
+ interleave = -4;
+ else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U16_BE))
+ interleave = 2;
+ else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U16_LE))
+ interleave = -2;
+ else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U8))
+ interleave = 1;
+ else
+ return 0;
+
+ CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsd),
+ 0);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_bitorder, 0);
+ spa_pod_builder_id(b, SPA_PARAM_BITORDER_msb);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_interleave, 0);
+ spa_pod_builder_int(b, interleave);
+
+ /* Use a lower rate limit of 352800 (= 44100 * 64 / 8). This is because in
+ * PipeWire, DSD rates are given in bytes, not bits, so 352800 corresponds
+ * to the bit rate of DSD64. (The "64" in DSD64 means "64 times the rate
+ * of 44.1 kHz".) Some hardware may report rates lower than that, for example
+ * 176400. This would correspond to "DSD32" (which does not exist). Trying
+ * to use such a rate with DSD hardware does not work and may cause undefined
+ * behavior in said hardware. */
+ if ((res = add_rate(state, 8, SPA_ABS(interleave), true, index & 0xffff,
+ next, 44100, params, b)) != 1)
+ return res;
+
+ if ((res = add_channels(state, true, index & 0xffff, next, params, b)) != 1)
+ return res;
+
+ *result = spa_pod_builder_pop(b, &f[0]);
+ return 1;
+}
+
+int
+spa_alsa_enum_format(struct state *state, int seq, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod *fmt;
+ int err, res;
+ bool opened;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened,
+ state->have_format, state->started);
+
+ opened = state->opened;
+ if (!state->started && state->have_format)
+ spa_alsa_close(state);
+ if ((err = spa_alsa_open(state, NULL)) < 0)
+ return err;
+
+ result.id = SPA_PARAM_EnumFormat;
+ result.next = start;
+
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ if (result.index < 0x10000) {
+ if ((res = enum_pcm_formats(state, result.index, &result.next, &fmt, &b)) != 1) {
+ result.next = 0x10000;
+ goto next;
+ }
+ }
+ else if (result.index < 0x20000) {
+ if ((res = enum_iec958_formats(state, result.index, &result.next, &fmt, &b)) != 1) {
+ result.next = 0x20000;
+ goto next;
+ }
+ }
+ else if (result.index < 0x30000) {
+ if ((res = enum_dsd_formats(state, result.index, &result.next, &fmt, &b)) != 1) {
+ result.next = 0x30000;
+ goto next;
+ }
+ }
+ else
+ goto enum_end;
+
+ if (spa_pod_filter(&b, &result.param, fmt, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&state->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ enum_end:
+ res = 0;
+ if (!opened)
+ spa_alsa_close(state);
+ return res;
+}
+
+int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags)
+{
+ unsigned int rrate, rchannels, val, rscale = 1;
+ snd_pcm_uframes_t period_size;
+ int err, dir;
+ snd_pcm_hw_params_t *params;
+ snd_pcm_format_t rformat;
+ snd_pcm_access_mask_t *amask;
+ snd_pcm_t *hndl;
+ unsigned int periods;
+ bool match = true, planar = false, is_batch;
+ char spdif_params[128] = "";
+
+ spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened,
+ state->have_format, state->started);
+
+ state->use_mmap = !state->disable_mmap;
+
+ switch (fmt->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ {
+ struct spa_audio_info_raw *f = &fmt->info.raw;
+ rrate = f->rate;
+ rchannels = f->channels;
+ rformat = spa_format_to_alsa(f->format, &planar);
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_iec958:
+ {
+ struct spa_audio_info_iec958 *f = &fmt->info.iec958;
+ 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),
+ f->rate);
+
+ rformat = SND_PCM_FORMAT_S16_LE;
+ rchannels = 2;
+ rrate = f->rate;
+
+ switch (f->codec) {
+ case SPA_AUDIO_IEC958_CODEC_PCM:
+ case SPA_AUDIO_IEC958_CODEC_DTS:
+ case SPA_AUDIO_IEC958_CODEC_AC3:
+ case SPA_AUDIO_IEC958_CODEC_MPEG:
+ case SPA_AUDIO_IEC958_CODEC_MPEG2_AAC:
+ break;
+ case SPA_AUDIO_IEC958_CODEC_EAC3:
+ /* EAC3 has 3 rates, 32, 44.1 and 48KHz. We need to
+ * open the device in 4x that rate. Some clients
+ * already multiply (mpv,..) others don't (vlc). */
+ if (rrate <= 48000)
+ rrate *= 4;
+ break;
+ case SPA_AUDIO_IEC958_CODEC_TRUEHD:
+ case SPA_AUDIO_IEC958_CODEC_DTSHD:
+ rchannels = 8;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ switch (rrate) {
+ case 22050: aes3 = IEC958_AES3_CON_FS_22050; break;
+ case 24000: aes3 = IEC958_AES3_CON_FS_24000; break;
+ case 32000: aes3 = IEC958_AES3_CON_FS_32000; break;
+ case 44100: aes3 = IEC958_AES3_CON_FS_44100; break;
+ case 48000: aes3 = IEC958_AES3_CON_FS_48000; break;
+ case 88200: aes3 = IEC958_AES3_CON_FS_88200; break;
+ case 96000: aes3 = IEC958_AES3_CON_FS_96000; break;
+ case 176400: aes3 = IEC958_AES3_CON_FS_176400; break;
+ case 192000: aes3 = IEC958_AES3_CON_FS_192000; break;
+ case 768000: aes3 = IEC958_AES3_CON_FS_768000; break;
+ default: aes3 = IEC958_AES3_CON_FS_NOTID; break;
+ }
+ spa_scnprintf(spdif_params, sizeof(spdif_params),
+ ",AES0=0x%x,AES1=0x%x,AES2=0x%x,AES3=0x%x",
+ IEC958_AES0_CON_EMPHASIS_NONE | IEC958_AES0_NONAUDIO,
+ IEC958_AES1_CON_ORIGINAL | IEC958_AES1_CON_PCM_CODER,
+ 0, aes3);
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_dsd:
+ {
+ struct spa_audio_info_dsd *f = &fmt->info.dsd;
+
+ rrate = f->rate;
+ rchannels = f->channels;
+
+ switch (f->interleave) {
+ case 4:
+ rformat = SND_PCM_FORMAT_DSD_U32_BE;
+ rrate /= 4;
+ rscale = 4;
+ break;
+ case -4:
+ rformat = SND_PCM_FORMAT_DSD_U32_LE;
+ rrate /= 4;
+ rscale = 4;
+ break;
+ case 2:
+ rformat = SND_PCM_FORMAT_DSD_U16_BE;
+ rrate /= 2;
+ rscale = 2;
+ break;
+ case -2:
+ rformat = SND_PCM_FORMAT_DSD_U16_LE;
+ rrate /= 2;
+ rscale = 2;
+ break;
+ case 1:
+ rformat = SND_PCM_FORMAT_DSD_U8;
+ rscale = 1;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ break;
+ }
+ default:
+ return -ENOTSUP;
+ }
+
+ if (rformat == SND_PCM_FORMAT_UNKNOWN) {
+ spa_log_warn(state->log, "%s: unknown format",
+ state->props.device);
+ return -EINVAL;
+ }
+
+ if (!state->started && state->have_format)
+ spa_alsa_close(state);
+ if ((err = spa_alsa_open(state, spdif_params)) < 0)
+ return err;
+
+ hndl = state->hndl;
+
+ snd_pcm_hw_params_alloca(&params);
+ /* choose all parameters */
+ CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration for playback: no configurations available");
+
+ debug_hw_params(state, __func__, params);
+
+ /* set hardware resampling, no resample */
+ CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
+
+ /* set the interleaved/planar read/write format */
+ snd_pcm_access_mask_alloca(&amask);
+ snd_pcm_hw_params_get_access_mask(params, amask);
+
+ if (state->use_mmap) {
+ if ((err = snd_pcm_hw_params_set_access(hndl, params,
+ planar ? SND_PCM_ACCESS_MMAP_NONINTERLEAVED
+ : SND_PCM_ACCESS_MMAP_INTERLEAVED)) < 0) {
+ spa_log_debug(state->log, "%p: MMAP not possible: %s", state,
+ snd_strerror(err));
+ state->use_mmap = false;
+ }
+ }
+ if (!state->use_mmap) {
+ if ((err = snd_pcm_hw_params_set_access(hndl, params,
+ planar ? SND_PCM_ACCESS_RW_NONINTERLEAVED
+ : SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
+ spa_log_error(state->log, "%s: RW not possible: %s",
+ state->props.device, snd_strerror(err));
+ return err;
+ }
+ }
+
+ /* set the sample format */
+ spa_log_debug(state->log, "%p: Stream parameters are %iHz fmt:%s access:%s-%s channels:%i",
+ state, rrate, snd_pcm_format_name(rformat),
+ state->use_mmap ? "mmap" : "rw",
+ planar ? "planar" : "interleaved", rchannels);
+ CHECK(snd_pcm_hw_params_set_format(hndl, params, rformat), "set_format");
+
+ /* set the count of channels */
+ val = rchannels;
+ CHECK(snd_pcm_hw_params_set_channels_near(hndl, params, &val), "set_channels");
+ if (rchannels != val) {
+ spa_log_warn(state->log, "%s: Channels doesn't match (requested %u, got %u)",
+ state->props.device, rchannels, val);
+ if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST))
+ return -EINVAL;
+ if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+ rchannels = val;
+ fmt->info.raw.channels = rchannels;
+ match = false;
+ }
+
+ if (!state->multi_rate &&
+ state->card->format_ref > 0 &&
+ state->card->rate != rrate) {
+ spa_log_error(state->log, "%p: card already opened at rate:%i",
+ state, state->card->rate);
+ return -EINVAL;
+ }
+
+ /* set the stream rate */
+ val = rrate;
+ CHECK(snd_pcm_hw_params_set_rate_near(hndl, params, &val, 0), "set_rate_near");
+ if (rrate != val) {
+ spa_log_warn(state->log, "%s: Rate doesn't match (requested %iHz, got %iHz)",
+ state->props.device, rrate, val);
+ if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST))
+ return -EINVAL;
+ if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+ rrate = val;
+ fmt->info.raw.rate = rrate;
+ match = false;
+ }
+ if (rchannels == 0 || rrate == 0) {
+ spa_log_error(state->log, "%s: invalid channels:%d or rate:%d",
+ state->props.device, rchannels, rrate);
+ return -EIO;
+ }
+
+ state->format = rformat;
+ state->channels = rchannels;
+ state->rate = rrate;
+ state->frame_size = snd_pcm_format_physical_width(rformat) / 8;
+ state->frame_scale = rscale;
+ state->planar = planar;
+ state->blocks = 1;
+ if (planar)
+ state->blocks *= rchannels;
+ else
+ state->frame_size *= rchannels;
+
+ state->have_format = true;
+ if (state->card->format_ref++ == 0)
+ state->card->rate = rrate;
+
+ dir = 0;
+ period_size = state->default_period_size;
+ is_batch = snd_pcm_hw_params_is_batch(params) &&
+ !state->disable_batch;
+
+ if (is_batch) {
+ if (period_size == 0)
+ period_size = state->position ? state->position->clock.duration : DEFAULT_PERIOD;
+ if (period_size == 0)
+ period_size = DEFAULT_PERIOD;
+ /* batch devices get their hw pointers updated every period. Make
+ * the period smaller and add one period of headroom. Limit the
+ * period size to our default so that we don't create too much
+ * headroom. */
+ period_size = SPA_MIN(period_size, DEFAULT_PERIOD) / 2;
+ spa_log_info(state->log, "%s: batch mode, period_size:%ld",
+ state->props.device, period_size);
+ } else {
+ if (period_size == 0)
+ period_size = DEFAULT_PERIOD;
+ /* disable ALSA wakeups, we use a timer */
+ if (snd_pcm_hw_params_can_disable_period_wakeup(params))
+ CHECK(snd_pcm_hw_params_set_period_wakeup(hndl, params, 0), "set_period_wakeup");
+ }
+
+ CHECK(snd_pcm_hw_params_set_period_size_near(hndl, params, &period_size, &dir), "set_period_size_near");
+
+ if (period_size == 0) {
+ spa_log_error(state->log, "%s: invalid period_size 0 (driver error?)", state->props.device);
+ return -EIO;
+ }
+
+ state->period_frames = period_size;
+
+ if (state->default_period_num != 0) {
+ periods = state->default_period_num;
+ CHECK(snd_pcm_hw_params_set_periods_near(hndl, params, &periods, &dir), "set_periods");
+ state->buffer_frames = period_size * periods;
+ } else {
+ CHECK(snd_pcm_hw_params_get_buffer_size_max(params, &state->buffer_frames), "get_buffer_size_max");
+
+ state->buffer_frames = SPA_MIN(state->buffer_frames, state->quantum_limit * 4)* state->frame_scale;
+
+ CHECK(snd_pcm_hw_params_set_buffer_size_min(hndl, params, &state->buffer_frames), "set_buffer_size_min");
+ CHECK(snd_pcm_hw_params_set_buffer_size_near(hndl, params, &state->buffer_frames), "set_buffer_size_near");
+ periods = state->buffer_frames / period_size;
+ }
+ if (state->buffer_frames == 0) {
+ spa_log_error(state->log, "%s: invalid buffer_frames 0 (driver error?)", state->props.device);
+ return -EIO;
+ }
+
+ state->headroom = state->default_headroom;
+ if (is_batch)
+ state->headroom += period_size;
+
+ if (spa_strstartswith(state->props.device, "a52") ||
+ spa_strstartswith(state->props.device, "dca"))
+ state->min_delay = SPA_MIN(2048u, state->buffer_frames);
+ else
+ state->min_delay = 0;
+
+ state->headroom = SPA_MIN(state->headroom, state->buffer_frames);
+ state->start_delay = state->default_start_delay;
+
+ state->latency[state->port_direction].min_rate =
+ state->latency[state->port_direction].max_rate =
+ SPA_MAX(state->min_delay, state->headroom);
+
+ spa_log_info(state->log, "%s (%s): format:%s access:%s-%s rate:%d channels:%d "
+ "buffer frames %lu, period frames %lu, periods %u, frame_size %zd "
+ "headroom %u start-delay:%u",
+ state->props.device,
+ state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback",
+ snd_pcm_format_name(state->format),
+ state->use_mmap ? "mmap" : "rw",
+ planar ? "planar" : "interleaved",
+ state->rate, state->channels, state->buffer_frames, state->period_frames,
+ periods, state->frame_size, state->headroom, state->start_delay);
+
+ /* write the parameters to device */
+ CHECK(snd_pcm_hw_params(hndl, params), "set_hw_params");
+
+ return match ? 0 : 1;
+}
+
+static int set_swparams(struct state *state)
+{
+ snd_pcm_t *hndl = state->hndl;
+ int err = 0;
+ snd_pcm_sw_params_t *params;
+
+ snd_pcm_sw_params_alloca(&params);
+
+ /* get the current params */
+ CHECK(snd_pcm_sw_params_current(hndl, params), "sw_params_current");
+
+ CHECK(snd_pcm_sw_params_set_tstamp_mode(hndl, params, SND_PCM_TSTAMP_ENABLE),
+ "sw_params_set_tstamp_mode");
+ CHECK(snd_pcm_sw_params_set_tstamp_type(hndl, params, SND_PCM_TSTAMP_TYPE_MONOTONIC),
+ "sw_params_set_tstamp_type");
+#if 0
+ snd_pcm_uframes_t boundary;
+ CHECK(snd_pcm_sw_params_get_boundary(params, &boundary), "get_boundary");
+
+ CHECK(snd_pcm_sw_params_set_stop_threshold(hndl, params, boundary), "set_stop_threshold");
+#endif
+
+ /* start the transfer */
+ CHECK(snd_pcm_sw_params_set_start_threshold(hndl, params, LONG_MAX), "set_start_threshold");
+
+ CHECK(snd_pcm_sw_params_set_period_event(hndl, params, 0), "set_period_event");
+
+ /* write the parameters to the playback device */
+ CHECK(snd_pcm_sw_params(hndl, params), "sw_params");
+
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) {
+ spa_log_debug(state->log, "state after sw_params:");
+ snd_pcm_dump(hndl, state->output);
+ fflush(state->log_file);
+ }
+
+ return 0;
+}
+
+static int set_timeout(struct state *state, uint64_t time)
+{
+ struct itimerspec ts;
+
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(state->data_system,
+ state->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+ return 0;
+}
+
+int spa_alsa_silence(struct state *state, snd_pcm_uframes_t silence)
+{
+ snd_pcm_t *hndl = state->hndl;
+ const snd_pcm_channel_area_t *my_areas;
+ snd_pcm_uframes_t frames, offset;
+ int i, res;
+
+ if (state->use_mmap) {
+ frames = state->buffer_frames;
+
+ if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ silence = SPA_MIN(silence, frames);
+
+ spa_log_trace_fp(state->log, "%p: frames:%ld offset:%ld silence %ld",
+ state, frames, offset, silence);
+ snd_pcm_areas_silence(my_areas, offset, state->channels, silence, state->format);
+
+ if (SPA_UNLIKELY((res = snd_pcm_mmap_commit(hndl, offset, silence)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ } else {
+ uint8_t buffer[silence * state->frame_size];
+ memset(buffer, 0, silence * state->frame_size);
+
+ if (state->planar) {
+ void *bufs[state->channels];
+ for (i = 0; i < state->channels; i++)
+ bufs[i] = buffer;
+ snd_pcm_writen(hndl, bufs, silence);
+ } else {
+ snd_pcm_writei(hndl, buffer, silence);
+ }
+ }
+ return 0;
+}
+
+static inline int do_start(struct state *state)
+{
+ int res;
+ if (SPA_UNLIKELY(!state->alsa_started)) {
+ spa_log_trace(state->log, "%p: snd_pcm_start", state);
+ if ((res = snd_pcm_start(state->hndl)) < 0) {
+ spa_log_error(state->log, "%s: snd_pcm_start: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ state->alsa_started = true;
+ }
+ return 0;
+}
+
+static int alsa_recover(struct state *state, int err)
+{
+ int res, st;
+ snd_pcm_status_t *status;
+
+ snd_pcm_status_alloca(&status);
+ if (SPA_UNLIKELY((res = snd_pcm_status(state->hndl, status)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_status error: %s",
+ state->props.device, snd_strerror(res));
+ goto recover;
+ }
+
+ st = snd_pcm_status_get_state(status);
+ switch (st) {
+ case SND_PCM_STATE_XRUN:
+ {
+ struct timeval now, trigger, diff;
+ uint64_t delay, missing;
+
+ snd_pcm_status_get_tstamp (status, &now);
+ snd_pcm_status_get_trigger_tstamp (status, &trigger);
+ timersub(&now, &trigger, &diff);
+
+ delay = SPA_TIMEVAL_TO_USEC(&diff);
+ missing = delay * state->rate / SPA_USEC_PER_SEC;
+
+ spa_log_trace(state->log, "%p: xrun of %"PRIu64" usec %"PRIu64,
+ state, delay, missing);
+
+ spa_node_call_xrun(&state->callbacks,
+ SPA_TIMEVAL_TO_USEC(&trigger), delay, NULL);
+
+ state->sample_count += missing ? missing : state->threshold;
+ break;
+ }
+ case SND_PCM_STATE_SUSPENDED:
+ spa_log_info(state->log, "%s: recover from state %s",
+ state->props.device, snd_pcm_state_name(st));
+ res = snd_pcm_resume(state->hndl);
+ if (res >= 0)
+ return res;
+ err = -ESTRPIPE;
+ break;
+ default:
+ spa_log_error(state->log, "%s: recover from error state %s",
+ state->props.device, snd_pcm_state_name(st));
+ break;
+ }
+
+recover:
+ if (SPA_UNLIKELY((res = snd_pcm_recover(state->hndl, err, true)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_recover error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ spa_dll_init(&state->dll);
+ state->alsa_recovering = true;
+ state->alsa_started = false;
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom);
+
+ return do_start(state);
+}
+
+static int get_avail(struct state *state, uint64_t current_time)
+{
+ int res, missed;
+ snd_pcm_sframes_t avail;
+
+ if (SPA_UNLIKELY((avail = snd_pcm_avail(state->hndl)) < 0)) {
+ if ((res = alsa_recover(state, avail)) < 0)
+ return res;
+ if ((avail = snd_pcm_avail(state->hndl)) < 0) {
+ if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+ spa_log_warn(state->log, "%s: (%d missed) snd_pcm_avail after recover: %s",
+ state->props.device, missed, snd_strerror(avail));
+ }
+ avail = state->threshold * 2;
+ }
+ } else {
+ state->alsa_recovering = false;
+ }
+ return avail;
+}
+
+#if 0
+static int get_avail_htimestamp(struct state *state, uint64_t current_time)
+{
+ int res, missed;
+ snd_pcm_uframes_t avail;
+ snd_htimestamp_t tstamp;
+ uint64_t then;
+
+ if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) {
+ if ((res = alsa_recover(state, avail)) < 0)
+ return res;
+ if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) {
+ if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+ spa_log_warn(state->log, "%s: (%d missed) snd_pcm_htimestamp error: %s",
+ state->props.device, missed, snd_strerror(res));
+ }
+ avail = state->threshold * 2;
+ }
+ } else {
+ state->alsa_recovering = false;
+ }
+
+ if ((then = SPA_TIMESPEC_TO_NSEC(&tstamp)) != 0) {
+ if (then < current_time)
+ avail += (current_time - then) * state->rate / SPA_NSEC_PER_SEC;
+ else
+ avail -= (then - current_time) * state->rate / SPA_NSEC_PER_SEC;
+ }
+ return SPA_MIN(avail, state->buffer_frames);
+}
+#endif
+
+static int get_status(struct state *state, uint64_t current_time,
+ snd_pcm_uframes_t *delay, snd_pcm_uframes_t *target)
+{
+ int avail;
+
+ if ((avail = get_avail(state, current_time)) < 0)
+ return avail;
+
+ avail = SPA_MIN(avail, (int)state->buffer_frames);
+
+ *target = state->threshold + state->headroom;
+
+ if (state->resample && state->rate_match) {
+ state->delay = state->rate_match->delay;
+ state->read_size = state->rate_match->size;
+ } else {
+ state->delay = 0;
+ state->read_size = state->threshold;
+ }
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK) {
+ *delay = state->buffer_frames - avail;
+ } else {
+ *delay = avail;
+ *target = SPA_MAX(*target, state->read_size);
+ }
+ *target = SPA_CLAMP(*target, state->min_delay, state->buffer_frames);
+ return 0;
+}
+
+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;
+ int32_t diff;
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ err = delay - target;
+ else
+ err = target - delay;
+
+ if (SPA_UNLIKELY(state->dll.bw == 0.0)) {
+ spa_dll_set_bw(&state->dll, SPA_DLL_BW_MAX, state->threshold, state->rate);
+ state->next_time = current_time;
+ state->base_time = current_time;
+ }
+ diff = (int32_t) (state->last_threshold - state->threshold);
+
+ if (SPA_UNLIKELY(diff != 0)) {
+ err -= diff;
+ spa_log_trace(state->log, "%p: follower:%d quantum change %d -> %d (%d) %f",
+ state, follower, state->last_threshold, state->threshold, diff, err);
+ state->last_threshold = state->threshold;
+ state->alsa_sync = true;
+ state->alsa_sync_warning = false;
+ }
+ if (err > state->max_error) {
+ err = state->max_error;
+ state->alsa_sync = true;
+ } else if (err < -state->max_error) {
+ err = -state->max_error;
+ state->alsa_sync = true;
+ }
+
+ if (!follower || state->matching)
+ corr = spa_dll_update(&state->dll, err);
+ else
+ corr = 1.0;
+
+ if (diff < 0)
+ state->next_time += diff / corr * 1e9 / state->rate;
+
+ if (SPA_UNLIKELY((state->next_time - state->base_time) > BW_PERIOD)) {
+ state->base_time = state->next_time;
+
+ spa_log_debug(state->log, "%s: follower:%d match:%d rate:%f "
+ "bw:%f thr:%u del:%ld target:%ld err:%f max:%f",
+ state->props.device, follower, state->matching,
+ corr, state->dll.bw, state->threshold, delay, target,
+ err, state->max_error);
+ }
+
+ if (state->rate_match) {
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ state->rate_match->rate = corr;
+ else
+ state->rate_match->rate = 1.0/corr;
+
+ SPA_FLAG_UPDATE(state->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, state->matching);
+ }
+
+ state->next_time += state->threshold / corr * 1e9 / state->rate;
+
+ if (SPA_LIKELY(!follower && state->clock)) {
+ state->clock->nsec = current_time;
+ state->clock->position += state->duration;
+ state->clock->duration = state->duration;
+ state->clock->delay = delay + state->delay;
+ state->clock->rate_diff = corr;
+ state->clock->next_nsec = state->next_time;
+ }
+
+ spa_log_trace_fp(state->log, "%p: follower:%d %"PRIu64" %f %ld %f %f %u",
+ state, follower, current_time, corr, delay, err, state->threshold * corr,
+ state->threshold);
+
+ return 0;
+}
+
+static inline bool is_following(struct state *state)
+{
+ return state->position && state->clock && state->position->clock.id != state->clock->id;
+}
+
+static int setup_matching(struct state *state)
+{
+ state->matching = state->following;
+
+ if (state->position == NULL)
+ return -ENOTSUP;
+
+ spa_log_debug(state->log, "driver clock:'%s' our clock:'%s'",
+ state->position->clock.name, state->clock_name);
+
+ if (spa_streq(state->position->clock.name, state->clock_name))
+ state->matching = false;
+
+ state->resample = ((uint32_t)state->rate != state->rate_denom) || state->matching;
+
+ spa_log_info(state->log, "driver clock:'%s'@%d our clock:'%s'@%d matching:%d resample:%d",
+ state->position->clock.name, state->rate_denom,
+ state->clock_name, state->rate,
+ state->matching, state->resample);
+ return 0;
+}
+
+static inline void check_position_config(struct state *state)
+{
+ if (SPA_UNLIKELY(state->position == NULL))
+ return;
+
+ if (SPA_UNLIKELY((state->duration != state->position->clock.duration) ||
+ (state->rate_denom != state->position->clock.rate.denom))) {
+ state->duration = state->position->clock.duration;
+ state->rate_denom = state->position->clock.rate.denom;
+ state->threshold = SPA_SCALE32_UP(state->duration, state->rate, state->rate_denom);
+ state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f);
+ state->resample = ((uint32_t)state->rate != state->rate_denom) || state->matching;
+ state->alsa_sync = true;
+ }
+}
+
+int spa_alsa_write(struct state *state)
+{
+ snd_pcm_t *hndl = state->hndl;
+ const snd_pcm_channel_area_t *my_areas;
+ snd_pcm_uframes_t written, frames, offset, off, to_write, total_written, max_write;
+ snd_pcm_sframes_t commitres;
+ int res = 0, missed;
+ size_t frame_size = state->frame_size;
+
+ check_position_config(state);
+
+ max_write = state->buffer_frames;
+
+ if (state->following && state->alsa_started) {
+ uint64_t current_time;
+ snd_pcm_uframes_t delay, target;
+
+ current_time = state->position->clock.nsec;
+
+ if (SPA_UNLIKELY((res = get_status(state, current_time, &delay, &target)) < 0))
+ return res;
+
+ if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, true)) < 0))
+ return res;
+
+ if (SPA_UNLIKELY(state->alsa_sync)) {
+ enum spa_log_level lev;
+
+ if (SPA_UNLIKELY(state->alsa_sync_warning))
+ lev = SPA_LOG_LEVEL_WARN;
+ else
+ lev = SPA_LOG_LEVEL_INFO;
+
+ if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+ spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, "
+ "resync (%d missed)", state->props.device, delay,
+ target, state->threshold, missed);
+ }
+
+ if (delay > target)
+ snd_pcm_rewind(state->hndl, delay - target);
+ else if (delay < target)
+ spa_alsa_silence(state, target - delay);
+ delay = target;
+ state->alsa_sync = false;
+ } else
+ state->alsa_sync_warning = true;
+ }
+
+ total_written = 0;
+again:
+
+ frames = max_write;
+ if (state->use_mmap && frames > 0) {
+ if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ spa_log_trace_fp(state->log, "%p: begin %ld %ld %d",
+ state, offset, frames, state->threshold);
+ off = offset;
+ } else {
+ off = 0;
+ }
+
+ to_write = frames;
+ written = 0;
+
+ while (!spa_list_is_empty(&state->ready) && to_write > 0) {
+ size_t n_bytes, n_frames;
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t i, offs, size, last_offset;
+
+ b = spa_list_first(&state->ready, struct buffer, link);
+ d = b->buf->datas;
+
+ offs = d[0].chunk->offset + state->ready_offset;
+ last_offset = d[0].chunk->size;
+ size = last_offset - state->ready_offset;
+
+ offs = SPA_MIN(offs, d[0].maxsize);
+ size = SPA_MIN(d[0].maxsize - offs, size);
+
+ n_frames = SPA_MIN(size / frame_size, to_write);
+ n_bytes = n_frames * frame_size;
+
+ if (SPA_LIKELY(state->use_mmap)) {
+ for (i = 0; i < b->buf->n_datas; i++) {
+ spa_memcpy(SPA_PTROFF(my_areas[i].addr, off * frame_size, void),
+ SPA_PTROFF(d[i].data, offs, void), n_bytes);
+ }
+ } else {
+ void *bufs[b->buf->n_datas];
+ for (i = 0; i < b->buf->n_datas; i++)
+ bufs[i] = SPA_PTROFF(d[i].data, offs, void);
+
+ if (state->planar)
+ snd_pcm_writen(hndl, bufs, n_frames);
+ else
+ snd_pcm_writei(hndl, bufs[0], n_frames);
+ }
+
+ state->ready_offset += n_bytes;
+
+ if (state->ready_offset >= last_offset) {
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ state->io->buffer_id = b->id;
+ spa_log_trace_fp(state->log, "%p: reuse buffer %u", state, b->id);
+
+ spa_node_call_reuse_buffer(&state->callbacks, 0, b->id);
+
+ state->ready_offset = 0;
+ }
+ written += n_frames;
+ off += n_frames;
+ to_write -= n_frames;
+ }
+
+ spa_log_trace_fp(state->log, "%p: commit %ld %ld %"PRIi64,
+ state, offset, written, state->sample_count);
+ total_written += written;
+
+ 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->props.device, snd_strerror(commitres));
+ if (commitres != -EPIPE && commitres != -ESTRPIPE)
+ return res;
+ }
+ if (commitres > 0 && written != (snd_pcm_uframes_t) commitres) {
+ spa_log_warn(state->log, "%s: mmap_commit wrote %ld instead of %ld",
+ state->props.device, commitres, written);
+ }
+ }
+
+ if (!spa_list_is_empty(&state->ready) && written > 0)
+ goto again;
+
+ state->sample_count += total_written;
+
+ if (SPA_UNLIKELY(!state->alsa_started && (total_written > 0 || frames == 0)))
+ do_start(state);
+
+ return 0;
+}
+
+void spa_alsa_recycle_buffer(struct state *this, uint32_t buffer_id)
+{
+ struct buffer *b = &this->buffers[buffer_id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_trace_fp(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&this->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+}
+
+static snd_pcm_uframes_t
+push_frames(struct state *state,
+ const snd_pcm_channel_area_t *my_areas,
+ snd_pcm_uframes_t offset,
+ snd_pcm_uframes_t frames)
+{
+ snd_pcm_uframes_t total_frames = 0;
+
+ if (spa_list_is_empty(&state->free)) {
+ spa_log_warn(state->log, "%s: no more buffers", state->props.device);
+ total_frames = frames;
+ } else {
+ size_t n_bytes, left, frame_size = state->frame_size;
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t i, avail, l0, l1;
+
+ b = spa_list_first(&state->free, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ if (b->h) {
+ b->h->seq = state->sample_count;
+ b->h->pts = state->next_time;
+ b->h->dts_offset = 0;
+ }
+
+ d = b->buf->datas;
+
+ avail = d[0].maxsize / frame_size;
+ total_frames = SPA_MIN(avail, frames);
+ n_bytes = total_frames * frame_size;
+
+ if (my_areas) {
+ left = state->buffer_frames - offset;
+ l0 = SPA_MIN(n_bytes, left * frame_size);
+ l1 = n_bytes - l0;
+
+ for (i = 0; i < b->buf->n_datas; i++) {
+ spa_memcpy(d[i].data,
+ SPA_PTROFF(my_areas[i].addr, offset * frame_size, void),
+ l0);
+ if (SPA_UNLIKELY(l1 > 0))
+ spa_memcpy(SPA_PTROFF(d[i].data, l0, void),
+ my_areas[i].addr,
+ l1);
+ d[i].chunk->offset = 0;
+ d[i].chunk->size = n_bytes;
+ d[i].chunk->stride = frame_size;
+ }
+ } else {
+ void *bufs[b->buf->n_datas];
+ for (i = 0; i < b->buf->n_datas; i++) {
+ bufs[i] = d[i].data;
+ d[i].chunk->offset = 0;
+ d[i].chunk->size = n_bytes;
+ d[i].chunk->stride = frame_size;
+ }
+ if (state->planar) {
+ snd_pcm_readn(state->hndl, bufs, total_frames);
+ } else {
+ snd_pcm_readi(state->hndl, bufs[0], total_frames);
+ }
+ }
+ spa_log_trace_fp(state->log, "%p: wrote %ld frames into buffer %d",
+ state, total_frames, b->id);
+
+ spa_list_append(&state->ready, &b->link);
+ }
+ return total_frames;
+}
+
+
+int spa_alsa_read(struct state *state)
+{
+ snd_pcm_t *hndl = state->hndl;
+ snd_pcm_uframes_t total_read = 0, to_read, max_read;
+ const snd_pcm_channel_area_t *my_areas;
+ snd_pcm_uframes_t read, frames, offset;
+ snd_pcm_sframes_t commitres;
+ int res = 0, missed;
+
+ check_position_config(state);
+
+ max_read = state->buffer_frames;
+
+ if (state->following && state->alsa_started) {
+ uint64_t current_time;
+ snd_pcm_uframes_t avail, delay, target;
+
+ current_time = state->position->clock.nsec;
+
+ if ((res = get_status(state, current_time, &delay, &target)) < 0)
+ return res;
+
+ avail = delay;
+
+ if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, true)) < 0))
+ return res;
+
+ if (state->alsa_sync) {
+ enum spa_log_level lev;
+
+ if (SPA_UNLIKELY(state->alsa_sync_warning))
+ lev = SPA_LOG_LEVEL_WARN;
+ else
+ lev = SPA_LOG_LEVEL_INFO;
+
+ if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+ spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, "
+ "resync (%d missed)", state->props.device, delay,
+ target, state->threshold, missed);
+ }
+
+ if (delay < target)
+ max_read = target - delay;
+ else if (delay > target)
+ snd_pcm_forward(state->hndl, delay - target);
+ delay = target;
+ state->alsa_sync = false;
+ } else
+ state->alsa_sync_warning = true;
+
+ if (avail < state->read_size)
+ max_read = 0;
+ }
+
+ frames = SPA_MIN(max_read, state->read_size);
+
+ if (state->use_mmap) {
+ to_read = state->buffer_frames;
+ if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &to_read)) < 0) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ spa_log_trace_fp(state->log, "%p: begin offs:%ld frames:%ld to_read:%ld thres:%d", state,
+ offset, frames, to_read, state->threshold);
+ } else {
+ my_areas = NULL;
+ offset = 0;
+ }
+
+ if (frames > 0) {
+ read = push_frames(state, my_areas, offset, frames);
+ total_read += read;
+ } else {
+ spa_alsa_skip(state);
+ total_read += state->read_size;
+ read = 0;
+ }
+
+ if (state->use_mmap && read > 0) {
+ spa_log_trace_fp(state->log, "%p: commit offs:%ld read:%ld count:%"PRIi64, state,
+ offset, read, state->sample_count);
+ if ((commitres = snd_pcm_mmap_commit(hndl, offset, read)) < 0) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_commit error %lu %lu: %s",
+ state->props.device, frames, read, snd_strerror(commitres));
+ if (commitres != -EPIPE && commitres != -ESTRPIPE)
+ return res;
+ }
+ if (commitres > 0 && read != (snd_pcm_uframes_t) commitres) {
+ spa_log_warn(state->log, "%s: mmap_commit read %ld instead of %ld",
+ state->props.device, commitres, read);
+ }
+ }
+
+ state->sample_count += total_read;
+
+ return 0;
+}
+
+int spa_alsa_skip(struct state *state)
+{
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t i, avail, total_frames, n_bytes, frames;
+
+ if (spa_list_is_empty(&state->free)) {
+ spa_log_warn(state->log, "%s: no more buffers", state->props.device);
+ return -EPIPE;
+ }
+
+ frames = state->read_size;
+
+ b = spa_list_first(&state->free, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ d = b->buf->datas;
+
+ avail = d[0].maxsize / state->frame_size;
+ total_frames = SPA_MIN(avail, frames);
+ n_bytes = total_frames * state->frame_size;
+
+ for (i = 0; i < b->buf->n_datas; i++) {
+ memset(d[i].data, 0, n_bytes);
+ d[i].chunk->offset = 0;
+ d[i].chunk->size = n_bytes;
+ d[i].chunk->stride = state->frame_size;
+ }
+ spa_list_append(&state->ready, &b->link);
+
+ return 0;
+}
+
+
+static int handle_play(struct state *state, uint64_t current_time,
+ snd_pcm_uframes_t delay, snd_pcm_uframes_t target)
+{
+ int res;
+
+ if (state->alsa_started && SPA_UNLIKELY(delay > target + state->max_error)) {
+ spa_log_trace(state->log, "%p: early wakeup %lu %lu", state, delay, target);
+ if (delay > target * 3)
+ delay = target * 3;
+ state->next_time = current_time + (delay - target) * SPA_NSEC_PER_SEC / state->rate;
+ return -EAGAIN;
+ }
+
+ if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, false)) < 0))
+ return res;
+
+ if (spa_list_is_empty(&state->ready)) {
+ struct spa_io_buffers *io = state->io;
+
+ spa_log_trace_fp(state->log, "%p: %d", state, io->status);
+
+ io->status = SPA_STATUS_NEED_DATA;
+
+ res = spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA);
+ }
+ else {
+ res = spa_alsa_write(state);
+ }
+ return res;
+}
+
+static int handle_capture(struct state *state, uint64_t current_time,
+ snd_pcm_uframes_t delay, snd_pcm_uframes_t target)
+{
+ int res;
+ struct spa_io_buffers *io;
+
+ if (SPA_UNLIKELY(delay < target)) {
+ spa_log_trace(state->log, "%p: early wakeup %ld %ld", state, delay, target);
+ state->next_time = current_time + (target - delay) * SPA_NSEC_PER_SEC /
+ state->rate;
+ return -EAGAIN;
+ }
+
+ if (SPA_UNLIKELY(res = update_time(state, current_time, delay, target, false)) < 0)
+ return res;
+
+ if ((res = spa_alsa_read(state)) < 0)
+ return res;
+
+ if (spa_list_is_empty(&state->ready))
+ return 0;
+
+ io = state->io;
+ if (io != NULL &&
+ (io->status != SPA_STATUS_HAVE_DATA || state->rate_match != NULL)) {
+ struct buffer *b;
+
+ if (io->buffer_id < state->n_buffers)
+ spa_alsa_recycle_buffer(state, io->buffer_id);
+
+ b = spa_list_first(&state->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ spa_log_trace_fp(state->log, "%p: output buffer:%d", state, b->id);
+ }
+ spa_node_call_ready(&state->callbacks, SPA_STATUS_HAVE_DATA);
+ return 0;
+}
+
+static void alsa_on_timeout_event(struct spa_source *source)
+{
+ struct state *state = source->data;
+ snd_pcm_uframes_t delay, target;
+ uint64_t expire, current_time;
+ int res;
+
+ if (SPA_LIKELY(state->started)) {
+ if (SPA_UNLIKELY((res = spa_system_timerfd_read(state->data_system,
+ state->timerfd, &expire)) < 0)) {
+ /* we can get here when the timer is changed since the last
+ * timerfd wakeup, for example by do_reassign_follower() executed
+ * in the same epoll wakeup cycle */
+ if (res != -EAGAIN)
+ spa_log_warn(state->log, "%p: error reading timerfd: %s",
+ state, spa_strerror(res));
+ return;
+ }
+ }
+
+ check_position_config(state);
+
+ current_time = state->next_time;
+
+ if (SPA_UNLIKELY(get_status(state, current_time, &delay, &target) < 0)) {
+ spa_log_error(state->log, "get_status error");
+ state->next_time += state->threshold * 1e9 / state->rate;
+ goto done;
+ }
+
+#ifndef FASTPATH
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
+ struct timespec now;
+ uint64_t nsec;
+ if (spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now) < 0)
+ return;
+ nsec = SPA_TIMESPEC_TO_NSEC(&now);
+ spa_log_trace_fp(state->log, "%p: timeout %lu %lu %"PRIu64" %"PRIu64" %"PRIi64
+ " %d %"PRIi64, state, delay, target, nsec, nsec,
+ nsec - current_time, state->threshold, state->sample_count);
+ }
+#endif
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ handle_play(state, current_time, delay, target);
+ else
+ handle_capture(state, current_time, delay, target);
+
+done:
+ if (state->next_time > current_time + SPA_NSEC_PER_SEC ||
+ current_time > state->next_time + SPA_NSEC_PER_SEC) {
+ spa_log_error(state->log, "%s: impossible timeout %lu %lu %"PRIu64" %"PRIu64" %"PRIi64
+ " %d %"PRIi64, state->props.device, delay, target, current_time, state->next_time,
+ state->next_time - current_time, state->threshold, state->sample_count);
+ state->next_time = current_time + state->threshold * 1e9 / state->rate;
+ }
+ set_timeout(state, state->next_time);
+}
+
+static void reset_buffers(struct state *this)
+{
+ uint32_t i;
+
+ spa_list_init(&this->free);
+ spa_list_init(&this->ready);
+
+ for (i = 0; i < this->n_buffers; i++) {
+ struct buffer *b = &this->buffers[i];
+ if (this->stream == SND_PCM_STREAM_PLAYBACK) {
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ } else {
+ spa_list_append(&this->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+ }
+}
+
+static int set_timers(struct state *state)
+{
+ struct timespec now;
+ int res;
+
+ if ((res = spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now)) < 0)
+ return res;
+ state->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ if (state->following) {
+ set_timeout(state, 0);
+ } else {
+ set_timeout(state, state->next_time);
+ }
+ return 0;
+}
+
+int spa_alsa_start(struct state *state)
+{
+ int err;
+
+ if (state->started)
+ return 0;
+
+ if (state->position) {
+ state->duration = state->position->clock.duration;
+ state->rate_denom = state->position->clock.rate.denom;
+ }
+ else {
+ spa_log_warn(state->log, "%s: no position set, using defaults",
+ state->props.device);
+ state->duration = 1024;
+ state->rate_denom = state->rate;
+ }
+ if (state->rate_denom == 0) {
+ spa_log_error(state->log, "%s: unset rate_denom", state->props.device);
+ return -EIO;
+ }
+ if (state->duration == 0) {
+ spa_log_error(state->log, "%s: unset duration", state->props.device);
+ return -EIO;
+ }
+
+ state->following = is_following(state);
+ setup_matching(state);
+
+ spa_dll_init(&state->dll);
+ state->threshold = SPA_SCALE32_UP(state->duration, state->rate, state->rate_denom);
+ state->last_threshold = state->threshold;
+ state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f);
+
+ spa_log_debug(state->log, "%p: start %d duration:%d rate:%d follower:%d match:%d resample:%d",
+ state, state->threshold, state->duration, state->rate_denom,
+ state->following, state->matching, state->resample);
+
+ CHECK(set_swparams(state), "swparams");
+
+ if ((err = snd_pcm_prepare(state->hndl)) < 0 && err != -EBUSY) {
+ spa_log_error(state->log, "%s: snd_pcm_prepare error: %s",
+ state->props.device, snd_strerror(err));
+ return err;
+ }
+
+ state->source.func = alsa_on_timeout_event;
+ state->source.data = state;
+ state->source.fd = state->timerfd;
+ state->source.mask = SPA_IO_IN;
+ state->source.rmask = 0;
+ spa_loop_add_source(state->data_loop, &state->source);
+
+ reset_buffers(state);
+ state->alsa_sync = true;
+ state->alsa_sync_warning = false;
+ state->alsa_recovering = false;
+ state->alsa_started = false;
+
+ /* start capture now, playback will start after first write */
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom);
+ else if ((err = do_start(state)) < 0)
+ return err;
+
+ set_timers(state);
+
+ state->started = true;
+
+ return 0;
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct state *state = user_data;
+ set_timers(state);
+ spa_dll_init(&state->dll);
+ return 0;
+}
+
+int spa_alsa_reassign_follower(struct state *state)
+{
+ bool following, freewheel;
+
+ if (!state->started)
+ return 0;
+
+ following = is_following(state);
+ if (following != state->following) {
+ spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following);
+ state->following = following;
+ spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state);
+ }
+ setup_matching(state);
+
+ freewheel = state->position &&
+ SPA_FLAG_IS_SET(state->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL);
+
+ if (state->freewheel != freewheel) {
+ spa_log_debug(state->log, "%p: freewheel %d->%d", state, state->freewheel, freewheel);
+ state->freewheel = freewheel;
+ if (freewheel)
+ snd_pcm_pause(state->hndl, 1);
+ else
+ snd_pcm_pause(state->hndl, 0);
+ }
+
+ state->alsa_sync_warning = false;
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct state *state = user_data;
+ struct itimerspec ts;
+
+ spa_loop_remove_source(state->data_loop, &state->source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(state->data_system, state->timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+int spa_alsa_pause(struct state *state)
+{
+ int err;
+
+ if (!state->started)
+ return 0;
+
+ spa_log_debug(state->log, "%p: pause", state);
+
+ spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state);
+
+ if ((err = snd_pcm_drop(state->hndl)) < 0)
+ spa_log_error(state->log, "%s: snd_pcm_drop %s", state->props.device,
+ snd_strerror(err));
+
+ state->started = false;
+
+ return 0;
+}
diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h
new file mode 100644
index 0000000..9c4a868
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm.h
@@ -0,0 +1,374 @@
+/* Spa ALSA Sink
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_ALSA_UTILS_H
+#define SPA_ALSA_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <math.h>
+
+#include <alsa/asoundlib.h>
+#include <alsa/use-case.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/json.h>
+#include <spa/utils/dll.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/debug/types.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "alsa.h"
+
+
+#define MAX_RATES 16
+
+#define DEFAULT_PERIOD 1024u
+#define DEFAULT_RATE 48000u
+#define DEFAULT_CHANNELS 2u
+#define DEFAULT_USE_CHMAP false
+
+struct props {
+ char device[64];
+ char device_name[128];
+ char card_name[128];
+ bool use_chmap;
+};
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+#define BW_MAX 0.128
+#define BW_MED 0.064
+#define BW_MIN 0.016
+#define BW_PERIOD (3 * SPA_NSEC_PER_SEC)
+
+struct channel_map {
+ uint32_t channels;
+ uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct card {
+ struct spa_list link;
+ int ref;
+ uint32_t index;
+ snd_use_case_mgr_t *ucm;
+ char *ucm_prefix;
+ int format_ref;
+ uint32_t rate;
+};
+
+struct ratelimit {
+ uint64_t interval;
+ uint64_t begin;
+ unsigned burst;
+ unsigned n_printed, n_missed;
+};
+
+struct state {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_system *data_system;
+ struct spa_loop *data_loop;
+
+ FILE *log_file;
+ struct ratelimit rate_limit;
+
+ uint32_t card_index;
+ struct card *card;
+ snd_pcm_stream_t stream;
+ snd_output_t *output;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define NODE_PropInfo 0
+#define NODE_Props 1
+#define NODE_IO 2
+#define NODE_ProcessLatency 3
+#define N_NODE_PARAMS 4
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ bool opened;
+ snd_pcm_t *hndl;
+
+ bool have_format;
+ struct spa_audio_info current_format;
+
+ uint32_t default_period_size;
+ uint32_t default_period_num;
+ uint32_t default_headroom;
+ uint32_t default_start_delay;
+ uint32_t default_format;
+ unsigned int default_channels;
+ unsigned int default_rate;
+ uint32_t allowed_rates[MAX_RATES];
+ uint32_t n_allowed_rates;
+ struct channel_map default_pos;
+ unsigned int disable_mmap;
+ unsigned int disable_batch;
+ char clock_name[64];
+ uint32_t quantum_limit;
+
+ snd_pcm_uframes_t buffer_frames;
+ snd_pcm_uframes_t period_frames;
+ snd_pcm_format_t format;
+ int rate;
+ int channels;
+ size_t frame_size;
+ size_t frame_scale;
+ int blocks;
+ uint32_t rate_denom;
+ uint32_t delay;
+ uint32_t read_size;
+
+ uint64_t port_info_all;
+ struct spa_port_info port_info;
+#define PORT_EnumFormat 0
+#define PORT_Meta 1
+#define PORT_IO 2
+#define PORT_Format 3
+#define PORT_Buffers 4
+#define PORT_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info port_params[N_PORT_PARAMS];
+ enum spa_direction port_direction;
+ struct spa_io_buffers *io;
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+ struct spa_io_rate_match *rate_match;
+
+ struct buffer buffers[MAX_BUFFERS];
+ unsigned int n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ size_t ready_offset;
+
+ bool started;
+ struct spa_source source;
+ int timerfd;
+ uint32_t threshold;
+ uint32_t last_threshold;
+ uint32_t headroom;
+ uint32_t start_delay;
+ uint32_t min_delay;
+
+ uint32_t duration;
+ unsigned int alsa_started:1;
+ unsigned int alsa_sync:1;
+ unsigned int alsa_sync_warning:1;
+ unsigned int alsa_recovering:1;
+ unsigned int following:1;
+ unsigned int matching:1;
+ unsigned int resample:1;
+ unsigned int use_mmap:1;
+ unsigned int planar:1;
+ unsigned int freewheel:1;
+ unsigned int open_ucm:1;
+ unsigned int is_iec958:1;
+ unsigned int is_hdmi:1;
+ unsigned int multi_rate:1;
+
+ uint64_t iec958_codecs;
+
+ int64_t sample_count;
+
+ int64_t sample_time;
+ uint64_t next_time;
+ uint64_t base_time;
+
+ uint64_t underrun;
+
+ struct spa_dll dll;
+ double max_error;
+
+ struct spa_latency_info latency[2];
+ struct spa_process_latency_info process_latency;
+};
+
+struct spa_pod *spa_alsa_enum_propinfo(struct state *state,
+ uint32_t idx, struct spa_pod_builder *b);
+int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b);
+int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params);
+
+int spa_alsa_enum_format(struct state *state, int seq,
+ uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+
+int spa_alsa_set_format(struct state *state, struct spa_audio_info *info, uint32_t flags);
+
+int spa_alsa_init(struct state *state, const struct spa_dict *info);
+int spa_alsa_clear(struct state *state);
+
+int spa_alsa_open(struct state *state, const char *params);
+int spa_alsa_start(struct state *state);
+int spa_alsa_reassign_follower(struct state *state);
+int spa_alsa_pause(struct state *state);
+int spa_alsa_close(struct state *state);
+
+int spa_alsa_write(struct state *state);
+int spa_alsa_read(struct state *state);
+int spa_alsa_skip(struct state *state);
+
+void spa_alsa_recycle_buffer(struct state *state, uint32_t buffer_id);
+
+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);
+ }
+}
+
+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;
+}
+
+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;
+}
+
+static inline void spa_alsa_parse_iec958_codecs(uint64_t *codecs, 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);
+
+ *codecs = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0)
+ *codecs |= 1ULL << spa_alsa_iec958_codec_from_name(v);
+}
+
+static inline uint32_t spa_alsa_get_iec958_codecs(struct state *state, uint32_t *codecs,
+ uint32_t max_codecs)
+{
+ uint64_t mask = state->iec958_codecs;
+ uint32_t i = 0, j = 0;
+ if (!(state->is_iec958 || state->is_hdmi))
+ return 0;
+ while (mask && i < max_codecs) {
+ if (mask & 1)
+ codecs[i++] = j;
+ mask >>= 1;
+ j++;
+ }
+ return i;
+}
+
+static inline int ratelimit_test(struct ratelimit *r, uint64_t now)
+{
+ unsigned missed = 0;
+ if (r->begin + r->interval < now) {
+ missed = r->n_missed;
+ r->begin = now;
+ r->n_printed = 0;
+ r->n_missed = 0;
+ } else if (r->n_printed >= r->burst) {
+ r->n_missed++;
+ return -1;
+ }
+ r->n_printed++;
+ return missed;
+}
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_ALSA_UTILS_H */
diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c
new file mode 100644
index 0000000..ee50163
--- /dev/null
+++ b/spa/plugins/alsa/alsa-seq-bridge.c
@@ -0,0 +1,1006 @@
+/* Spa ALSA Source
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <ctype.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/list.h>
+#include <spa/monitor/device.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/latency-utils.h>
+#include <spa/pod/filter.h>
+
+#include "alsa-seq.h"
+
+#define DEFAULT_DEVICE "default"
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+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;
+}
+
+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 seq_state *this = object;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct props *p;
+ 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);
+
+ p = &this->props;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Props:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL)
+ spa_scnprintf(this->clock->name, sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ spa_alsa_seq_reassign_follower(this);
+ return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)));
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct seq_state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if ((res = spa_alsa_seq_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = spa_alsa_seq_pause(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "alsa" },
+ { SPA_KEY_MEDIA_CLASS, "Midi/Bridge" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+static void emit_node_info(struct seq_state *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static inline void clean_name(char *name)
+{
+ char *c;
+ for (c = name; *c; ++c) {
+ if (!isalnum(*c) && strchr(" /_:()[]", *c) == NULL)
+ *c = '-';
+ }
+}
+
+static void emit_port_info(struct seq_state *this, struct seq_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;
+ int id;
+ snd_seq_port_info_t *info;
+ snd_seq_client_info_t *client_info;
+ char card[8];
+ char name[256];
+ char path[128];
+ char alias[128];
+
+ snd_seq_port_info_alloca(&info);
+ snd_seq_get_any_port_info(this->sys.hndl,
+ port->addr.client, port->addr.port, info);
+
+ snd_seq_client_info_alloca(&client_info);
+ snd_seq_get_any_client_info(this->sys.hndl,
+ port->addr.client, client_info);
+
+ int card_id;
+
+ // 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));
+ }
+ }
+ clean_name(name);
+
+ snprintf(path, sizeof(path), "alsa:seq:%s:client_%d:%s_%d",
+ this->props.device,
+ port->addr.client,
+ port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
+ 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));
+ 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_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);
+ if ((id = snd_seq_client_info_get_card(client_info)) != -1) {
+ snprintf(card, sizeof(card), "%d", id);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, card);
+ }
+ port->info.props = &SPA_DICT_INIT(items, n_items);
+
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static void emit_stream_info(struct seq_state *this, struct seq_stream *stream, bool full)
+{
+ uint32_t i;
+
+ for (i = 0; i < MAX_PORTS; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (port->valid)
+ emit_port_info(this, port, full);
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct seq_state *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_stream_info(this, &this->streams[SPA_DIRECTION_INPUT], true);
+ emit_stream_info(this, &this->streams[SPA_DIRECTION_OUTPUT], 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 seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static struct seq_port *find_port(struct seq_state *state,
+ struct seq_stream *stream, const snd_seq_addr_t *addr)
+{
+ uint32_t i;
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (port->valid &&
+ port->addr.client == addr->client &&
+ port->addr.port == addr->port)
+ return port;
+ }
+ return NULL;
+}
+
+static struct seq_port *alloc_port(struct seq_state *state, struct seq_stream *stream)
+{
+ uint32_t i;
+ for (i = 0; i < MAX_PORTS; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (!port->valid) {
+ port->id = i;
+ port->direction = stream->direction;
+ port->valid = true;
+ if (stream->last_port < i + 1)
+ stream->last_port = i + 1;
+ return port;
+ }
+ }
+ return NULL;
+}
+
+static void free_port(struct seq_state *state, struct seq_stream *stream, struct seq_port *port)
+{
+ port->valid = false;
+
+ if (port->id + 1 == stream->last_port) {
+ int i;
+ for (i = stream->last_port - 1; i >= 0; i--)
+ if (stream->ports[i].valid)
+ break;
+ stream->last_port = i + 1;
+ }
+
+ spa_node_emit_port_info(&state->hooks,
+ port->direction, port->id, NULL);
+ spa_zero(*port);
+}
+
+static void init_port(struct seq_state *state, struct seq_port *port, const snd_seq_addr_t *addr,
+ unsigned int type)
+{
+ enum spa_direction reverse = SPA_DIRECTION_REVERSE(port->direction);
+
+ port->addr = *addr;
+ 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_LIVE;
+ if (type & (SND_SEQ_PORT_TYPE_HARDWARE|SND_SEQ_PORT_TYPE_PORT|SND_SEQ_PORT_TYPE_SPECIFIC))
+ port->info.flags |= SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL;
+ port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ port->latency[port->direction] = SPA_LATENCY_INFO(
+ port->direction,
+ .min_quantum = 1.0f,
+ .max_quantum = 1.0f);
+ port->latency[reverse] = SPA_LATENCY_INFO(reverse);
+
+ spa_alsa_seq_activate_port(state, port, true);
+
+ emit_port_info(state, port, true);
+}
+
+static void update_stream_port(struct seq_state *state, struct seq_stream *stream,
+ const snd_seq_addr_t *addr, unsigned int caps, const snd_seq_port_info_t *info)
+{
+ struct seq_port *port = find_port(state, stream, addr);
+
+ if (info == NULL) {
+ spa_log_debug(state->log, "free port %d.%d", addr->client, addr->port);
+ if (port)
+ free_port(state, stream, port);
+ } else {
+ if (port == NULL && (caps & stream->caps) == stream->caps) {
+ spa_log_debug(state->log, "new port %d.%d", addr->client, addr->port);
+ port = alloc_port(state, stream);
+ if (port == NULL)
+ return;
+ init_port(state, port, addr, snd_seq_port_info_get_type(info));
+ } else if (port != NULL) {
+ if ((caps & stream->caps) != stream->caps) {
+ spa_log_debug(state->log, "free port %d.%d", addr->client, addr->port);
+ free_port(state, stream, port);
+ }
+ else {
+ spa_log_debug(state->log, "update port %d.%d", addr->client, addr->port);
+ port->info.change_mask = SPA_PORT_CHANGE_MASK_PROPS;
+ emit_port_info(state, port, false);
+ }
+ }
+ }
+}
+
+static int on_port_info(void *data, const snd_seq_addr_t *addr, const snd_seq_port_info_t *info)
+{
+ struct seq_state *state = data;
+
+ if (info == NULL) {
+ update_stream_port(state, &state->streams[SPA_DIRECTION_INPUT], addr, 0, info);
+ update_stream_port(state, &state->streams[SPA_DIRECTION_OUTPUT], addr, 0, info);
+ } else {
+ unsigned int caps = snd_seq_port_info_get_capability(info);
+
+ if (caps & SND_SEQ_PORT_CAP_NO_EXPORT)
+ return 0;
+
+ update_stream_port(state, &state->streams[SPA_DIRECTION_INPUT], addr, caps, info);
+ update_stream_port(state, &state->streams[SPA_DIRECTION_OUTPUT], addr, caps, info);
+ }
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+ 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);
+
+ 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 (result.index > 0)
+ return 0;
+ 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));
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+ 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));
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ 4096, 4096, INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ param = spa_latency_build(&b, id, &port->latency[result.index]);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct seq_state *this, struct seq_port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(void *object, struct seq_port *port,
+ uint32_t flags, const struct spa_pod *format)
+{
+ struct seq_state *this = object;
+ int err;
+
+ if (format == NULL) {
+ if (!port->have_format)
+ return 0;
+
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_application ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_control)
+ return -EINVAL;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, 1);
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ port->latency[info.direction] = info;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[PORT_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_port_info(this, port, false);
+ break;
+ }
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: port %d.%d buffers:%d format:%d", this,
+ direction, port_id, n_buffers, port->have_format);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->flags = BUFFER_FLAG_OUT;
+
+ b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ if (direction == SPA_DIRECTION_OUTPUT)
+ spa_alsa_seq_recycle_buffer(this, port, i);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: io %d.%d %d %p %zd", this,
+ direction, port_id, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct seq_state *this = object;
+ struct seq_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_PORT(this, SPA_DIRECTION_OUTPUT, port_id);
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ spa_alsa_seq_recycle_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ return spa_alsa_seq_process(this);
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct seq_state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct seq_state *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct seq_state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct seq_state *) handle;
+
+ spa_alsa_seq_close(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct seq_state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct seq_state *this;
+ uint32_t i;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct seq_state *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Node, SPA_VERSION_NODE, &impl_node, this);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info.max_input_ports = MAX_PORTS;
+ this->info.max_output_ports = MAX_PORTS;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+ reset_props(&this->props);
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, SPA_KEY_API_ALSA_PATH)) {
+ spa_scnprintf(this->props.device,
+ sizeof(this->props.device), "%s", s);
+ } else if (spa_streq(k, "clock.name")) {
+ spa_scnprintf(this->props.clock_name,
+ sizeof(this->props.clock_name), "%s", s);
+ } else if (spa_streq(k, SPA_KEY_API_ALSA_DISABLE_LONGNAME)) {
+ this->props.disable_longname = spa_atob(s);
+ }
+ }
+
+ this->port_info = on_port_info;
+ this->port_info_data = this;
+
+ if ((res = spa_alsa_seq_open(this)) < 0)
+ return res;
+
+ 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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Bridge midi ports with the alsa sequencer API" },
+ { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<device>]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_alsa_seq_bridge_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_SEQ_BRIDGE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c
new file mode 100644
index 0000000..9cec44d
--- /dev/null
+++ b/spa/plugins/alsa/alsa-seq.c
@@ -0,0 +1,983 @@
+/* Spa ALSA Sequencer
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sched.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/time.h>
+#include <math.h>
+#include <limits.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/filter.h>
+#include <spa/support/system.h>
+#include <spa/control/control.h>
+
+#include "alsa.h"
+
+#include "alsa-seq.h"
+
+#define CHECK(s,msg,...) if ((res = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(res)); return res; }
+
+static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_queue)
+{
+ struct props *props = &state->props;
+ int res;
+
+ spa_log_debug(state->log, "%p: ALSA seq open '%s' duplex", state, props->device);
+
+ if ((res = snd_seq_open(&conn->hndl,
+ props->device,
+ SND_SEQ_OPEN_DUPLEX,
+ 0)) < 0) {
+ return res;
+ }
+ return 0;
+}
+
+static int seq_init(struct seq_state *state, struct seq_conn *conn, bool with_queue)
+{
+ struct pollfd pfd;
+ snd_seq_port_info_t *pinfo;
+ int res;
+
+ /* client id */
+ if ((res = snd_seq_client_id(conn->hndl)) < 0) {
+ spa_log_error(state->log, "failed to get client id: %d", res);
+ goto error_exit_close;
+ }
+ conn->addr.client = res;
+
+ /* queue */
+ if (with_queue) {
+ if ((res = snd_seq_alloc_queue(conn->hndl)) < 0) {
+ spa_log_error(state->log, "failed to create queue: %d", res);
+ goto error_exit_close;
+ }
+ conn->queue_id = res;
+ } else {
+ conn->queue_id = -1;
+ }
+
+ if ((res = snd_seq_nonblock(conn->hndl, 1)) < 0)
+ spa_log_warn(state->log, "can't set nonblock mode: %s", snd_strerror(res));
+
+ /* port for receiving */
+ snd_seq_port_info_alloca(&pinfo);
+ snd_seq_port_info_set_name(pinfo, "input");
+ snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC);
+ snd_seq_port_info_set_capability(pinfo,
+ SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_READ);
+ /* Enable timestamping for events sent by external subscribers. */
+ snd_seq_port_info_set_timestamping(pinfo, 1);
+ snd_seq_port_info_set_timestamp_real(pinfo, 1);
+ if (with_queue)
+ snd_seq_port_info_set_timestamp_queue(pinfo, conn->queue_id);
+
+ if ((res = snd_seq_create_port(conn->hndl, pinfo)) < 0) {
+ spa_log_error(state->log, "failed to create port: %s", snd_strerror(res));
+ goto error_exit_close;
+ }
+ conn->addr.port = snd_seq_port_info_get_port(pinfo);
+
+ spa_log_debug(state->log, "queue:%d client:%d port:%d",
+ conn->queue_id, conn->addr.client, conn->addr.port);
+
+ snd_seq_poll_descriptors(conn->hndl, &pfd, 1, POLLIN);
+ conn->source.fd = pfd.fd;
+ conn->source.mask = SPA_IO_IN;
+
+ return 0;
+
+error_exit_close:
+ snd_seq_close(conn->hndl);
+ return res;
+}
+
+static int seq_close(struct seq_state *state, struct seq_conn *conn)
+{
+ int res;
+ spa_log_debug(state->log, "%p: Device '%s' closing", state, state->props.device);
+ if ((res = snd_seq_close(conn->hndl)) < 0) {
+ spa_log_warn(state->log, "close failed: %s", snd_strerror(res));
+ }
+ return res;
+}
+
+static int init_stream(struct seq_state *state, enum spa_direction direction)
+{
+ struct seq_stream *stream = &state->streams[direction];
+ int res;
+ stream->direction = direction;
+ if (direction == SPA_DIRECTION_INPUT) {
+ stream->caps = SND_SEQ_PORT_CAP_SUBS_WRITE;
+ } else {
+ stream->caps = SND_SEQ_PORT_CAP_SUBS_READ;
+ }
+ if ((res = snd_midi_event_new(MAX_EVENT_SIZE, &stream->codec)) < 0) {
+ spa_log_error(state->log, "can make event decoder: %s",
+ snd_strerror(res));
+ return res;
+ }
+ snd_midi_event_no_status(stream->codec, 1);
+ memset(stream->ports, 0, sizeof(stream->ports));
+ return 0;
+}
+
+static int uninit_stream(struct seq_state *state, enum spa_direction direction)
+{
+ struct seq_stream *stream = &state->streams[direction];
+ if (stream->codec)
+ snd_midi_event_free(stream->codec);
+ stream->codec = NULL;
+ return 0;
+}
+
+static void init_ports(struct seq_state *state)
+{
+ snd_seq_addr_t addr;
+ snd_seq_client_info_t *client_info;
+ snd_seq_port_info_t *port_info;
+
+ snd_seq_client_info_alloca(&client_info);
+ snd_seq_port_info_alloca(&port_info);
+ snd_seq_client_info_set_client(client_info, -1);
+
+ while (snd_seq_query_next_client(state->sys.hndl, client_info) >= 0) {
+
+ addr.client = snd_seq_client_info_get_client(client_info);
+ if (addr.client == SND_SEQ_CLIENT_SYSTEM ||
+ addr.client == state->sys.addr.client ||
+ addr.client == state->event.addr.client)
+ continue;
+
+ snd_seq_port_info_set_client(port_info, addr.client);
+ snd_seq_port_info_set_port(port_info, -1);
+ while (snd_seq_query_next_port(state->sys.hndl, port_info) >= 0) {
+ addr.port = snd_seq_port_info_get_port(port_info);
+ state->port_info(state->port_info_data, &addr, port_info);
+ }
+ }
+}
+
+static void debug_event(struct seq_state *state, snd_seq_event_t *ev)
+{
+ if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE)))
+ return;
+
+ spa_log_trace(state->log, "event type:%d flags:0x%x", ev->type, ev->flags);
+ switch (ev->flags & SND_SEQ_TIME_STAMP_MASK) {
+ case SND_SEQ_TIME_STAMP_TICK:
+ spa_log_trace(state->log, " time: %d ticks", ev->time.tick);
+ break;
+ case SND_SEQ_TIME_STAMP_REAL:
+ spa_log_trace(state->log, " time = %d.%09d",
+ (int)ev->time.time.tv_sec,
+ (int)ev->time.time.tv_nsec);
+ break;
+ }
+ spa_log_trace(state->log, " source:%d.%d dest:%d.%d queue:%d",
+ ev->source.client,
+ ev->source.port,
+ ev->dest.client,
+ ev->dest.port,
+ ev->queue);
+}
+
+static void alsa_seq_on_sys(struct spa_source *source)
+{
+ struct seq_state *state = source->data;
+ snd_seq_event_t *ev;
+ int res;
+
+ while (snd_seq_event_input(state->sys.hndl, &ev) > 0) {
+ const snd_seq_addr_t *addr = &ev->data.addr;
+
+ if (addr->client == state->event.addr.client)
+ continue;
+
+ debug_event(state, ev);
+
+ switch (ev->type) {
+ case SND_SEQ_EVENT_CLIENT_START:
+ case SND_SEQ_EVENT_CLIENT_CHANGE:
+ spa_log_info(state->log, "client add/change %d", addr->client);
+ break;
+ case SND_SEQ_EVENT_CLIENT_EXIT:
+ spa_log_info(state->log, "client exit %d", addr->client);
+ break;
+
+ case SND_SEQ_EVENT_PORT_START:
+ case SND_SEQ_EVENT_PORT_CHANGE:
+ {
+ snd_seq_port_info_t *info;
+
+ snd_seq_port_info_alloca(&info);
+
+ if ((res = snd_seq_get_any_port_info(state->sys.hndl,
+ addr->client, addr->port, info)) < 0) {
+ spa_log_warn(state->log, "can't get port info %d.%d: %s",
+ addr->client, addr->port, snd_strerror(res));
+ } else {
+ spa_log_info(state->log, "port add/change %d:%d",
+ addr->client, addr->port);
+ state->port_info(state->port_info_data, addr, info);
+ }
+ break;
+ }
+ case SND_SEQ_EVENT_PORT_EXIT:
+ spa_log_info(state->log, "port_event: del %d:%d",
+ addr->client, addr->port);
+ state->port_info(state->port_info_data, addr, NULL);
+ break;
+ default:
+ spa_log_info(state->log, "unhandled event %d: %d:%d",
+ ev->type, addr->client, addr->port);
+ break;
+
+ }
+ snd_seq_free_event(ev);
+ }
+}
+
+int spa_alsa_seq_open(struct seq_state *state)
+{
+ int n, i, res;
+ snd_seq_port_subscribe_t *sub;
+ snd_seq_addr_t addr;
+ snd_seq_queue_timer_t *timer;
+ struct seq_conn reserve[16];
+
+ if (state->opened)
+ return 0;
+
+ init_stream(state, SPA_DIRECTION_INPUT);
+ init_stream(state, SPA_DIRECTION_OUTPUT);
+
+ spa_zero(reserve);
+ for (i = 0; i < 16; i++) {
+ spa_log_debug(state->log, "close %d", i);
+ if ((res = seq_open(state, &reserve[i], false)) < 0)
+ break;
+ }
+ if (i >= 2) {
+ state->event = reserve[--i];
+ state->sys = reserve[--i];
+ res = 0;
+ }
+ for (n = --i; n >= 0; n--) {
+ spa_log_debug(state->log, "close %d", n);
+ seq_close(state, &reserve[n]);
+ }
+ if (res < 0) {
+ spa_log_error(state->log, "open failed: %s", snd_strerror(res));
+ return res;
+ }
+
+ if ((res = seq_init(state, &state->sys, false)) < 0)
+ goto error_close;
+
+ snd_seq_set_client_name(state->sys.hndl, "PipeWire-System");
+
+ if ((res = seq_init(state, &state->event, true)) < 0)
+ goto error_close;
+
+ snd_seq_set_client_name(state->event.hndl, "PipeWire-RT-Event");
+
+ /* connect to system announce */
+ snd_seq_port_subscribe_alloca(&sub);
+ addr.client = SND_SEQ_CLIENT_SYSTEM;
+ addr.port = SND_SEQ_PORT_SYSTEM_ANNOUNCE;
+ snd_seq_port_subscribe_set_sender(sub, &addr);
+ snd_seq_port_subscribe_set_dest(sub, &state->sys.addr);
+ if ((res = snd_seq_subscribe_port(state->sys.hndl, sub)) < 0) {
+ spa_log_warn(state->log, "failed to connect announce port: %s", snd_strerror(res));
+ }
+
+ addr.client = SND_SEQ_CLIENT_SYSTEM;
+ addr.port = SND_SEQ_PORT_SYSTEM_TIMER;
+ snd_seq_port_subscribe_set_sender(sub, &addr);
+ if ((res = snd_seq_subscribe_port(state->sys.hndl, sub)) < 0) {
+ spa_log_warn(state->log, "failed to connect timer port: %s", snd_strerror(res));
+ }
+
+ state->sys.source.func = alsa_seq_on_sys;
+ state->sys.source.data = state;
+ spa_loop_add_source(state->main_loop, &state->sys.source);
+
+ /* increase event queue timer resolution */
+ snd_seq_queue_timer_alloca(&timer);
+ if ((res = snd_seq_get_queue_timer(state->event.hndl, state->event.queue_id, timer)) < 0) {
+ spa_log_warn(state->log, "failed to get queue timer: %s", snd_strerror(res));
+ }
+ snd_seq_queue_timer_set_resolution(timer, INT_MAX);
+ if ((res = snd_seq_set_queue_timer(state->event.hndl, state->event.queue_id, timer)) < 0) {
+ spa_log_warn(state->log, "failed to set queue timer: %s", snd_strerror(res));
+ }
+
+ init_ports(state);
+
+ if ((res = spa_system_timerfd_create(state->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_close;
+
+ state->timerfd = res;
+
+ state->opened = true;
+
+ return 0;
+
+error_close:
+ seq_close(state, &state->event);
+ seq_close(state, &state->sys);
+ return res;
+}
+
+int spa_alsa_seq_close(struct seq_state *state)
+{
+ int res = 0;
+
+ if (!state->opened)
+ return 0;
+
+ spa_loop_remove_source(state->main_loop, &state->sys.source);
+
+ seq_close(state, &state->sys);
+ seq_close(state, &state->event);
+
+ uninit_stream(state, SPA_DIRECTION_INPUT);
+ uninit_stream(state, SPA_DIRECTION_OUTPUT);
+
+ spa_system_close(state->data_system, state->timerfd);
+ state->opened = false;
+
+ return res;
+}
+
+static int set_timeout(struct seq_state *state, uint64_t time)
+{
+ struct itimerspec ts;
+
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(state->data_system,
+ state->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+ return 0;
+}
+
+static struct seq_port *find_port(struct seq_state *state,
+ struct seq_stream *stream, const snd_seq_addr_t *addr)
+{
+ uint32_t i;
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (port->valid &&
+ port->addr.client == addr->client &&
+ port->addr.port == addr->port)
+ return port;
+ }
+ return NULL;
+}
+
+int spa_alsa_seq_activate_port(struct seq_state *state, struct seq_port *port, bool active)
+{
+ int res;
+ snd_seq_port_subscribe_t* sub;
+
+ spa_log_debug(state->log, "activate: %d.%d: started:%d active:%d wanted:%d",
+ port->addr.client, port->addr.port, state->started, port->active, active);
+
+ if (active && !state->started)
+ return 0;
+ if (port->active == active)
+ return 0;
+
+ snd_seq_port_subscribe_alloca(&sub);
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ snd_seq_port_subscribe_set_sender(sub, &port->addr);
+ snd_seq_port_subscribe_set_dest(sub, &state->event.addr);
+ } else {
+ snd_seq_port_subscribe_set_sender(sub, &state->event.addr);
+ snd_seq_port_subscribe_set_dest(sub, &port->addr);
+ }
+
+ if (active) {
+ snd_seq_port_subscribe_set_time_update(sub, 1);
+ snd_seq_port_subscribe_set_time_real(sub, 1);
+ snd_seq_port_subscribe_set_queue(sub, state->event.queue_id);
+ if ((res = snd_seq_subscribe_port(state->event.hndl, sub)) < 0) {
+ spa_log_error(state->log, "can't subscribe to %d:%d - %s",
+ port->addr.client, port->addr.port, snd_strerror(res));
+ active = false;
+ }
+ spa_log_info(state->log, "subscribe: %s port %d.%d",
+ port->direction == SPA_DIRECTION_OUTPUT ? "output" : "input",
+ port->addr.client, port->addr.port);
+ } else {
+ if ((res = snd_seq_unsubscribe_port(state->event.hndl, sub)) < 0) {
+ spa_log_warn(state->log, "can't unsubscribe from %d:%d - %s",
+ port->addr.client, port->addr.port, snd_strerror(res));
+ }
+ spa_log_info(state->log, "unsubscribe: %s port %d.%d",
+ port->direction == SPA_DIRECTION_OUTPUT ? "output" : "input",
+ port->addr.client, port->addr.port);
+ }
+ port->active = active;
+ return res;
+}
+
+static struct buffer *peek_buffer(struct seq_state *state,
+ struct seq_port *port)
+{
+ if (spa_list_is_empty(&port->free))
+ return NULL;
+ return spa_list_first(&port->free, struct buffer, link);
+}
+
+int spa_alsa_seq_recycle_buffer(struct seq_state *state, struct seq_port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_trace_fp(state->log, "%p: recycle buffer port:%p buffer-id:%u",
+ state, port, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+ return 0;
+}
+
+static int prepare_buffer(struct seq_state *state, struct seq_port *port)
+{
+ if (port->buffer != NULL)
+ return 0;
+
+ if ((port->buffer = peek_buffer(state, port)) == NULL)
+ return -EPIPE;
+
+ spa_pod_builder_init(&port->builder,
+ port->buffer->buf->datas[0].data,
+ port->buffer->buf->datas[0].maxsize);
+ spa_pod_builder_push_sequence(&port->builder, &port->frame, 0);
+
+ return 0;
+}
+
+static int process_recycle(struct seq_state *state)
+{
+ struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT];
+ uint32_t i;
+
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ struct spa_io_buffers *io = port->io;
+
+ if (!port->valid || io == NULL)
+ continue;
+
+ if (io->status != SPA_STATUS_HAVE_DATA &&
+ io->buffer_id < port->n_buffers) {
+ spa_alsa_seq_recycle_buffer(state, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+ }
+ return 0;
+}
+
+#define NSEC_TO_CLOCK(r,n) (((n) * (r)->denom) / ((r)->num * SPA_NSEC_PER_SEC))
+#define NSEC_FROM_CLOCK(r,n) (((n) * (r)->num * SPA_NSEC_PER_SEC) / (r)->denom)
+
+static int process_read(struct seq_state *state)
+{
+ snd_seq_event_t *ev;
+ struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT];
+ uint32_t i;
+ long size;
+ uint8_t data[MAX_EVENT_SIZE];
+ int res;
+
+ /* copy all new midi events into their port buffers */
+ while (snd_seq_event_input(state->event.hndl, &ev) > 0) {
+ const snd_seq_addr_t *addr = &ev->source;
+ struct seq_port *port;
+ uint64_t ev_time, diff;
+ uint32_t offset;
+
+ debug_event(state, ev);
+
+ if ((port = find_port(state, stream, addr)) == NULL) {
+ spa_log_debug(state->log, "unknown port %d.%d",
+ addr->client, addr->port);
+ continue;
+ }
+ if (port->io == NULL || port->n_buffers == 0)
+ continue;
+
+ if ((res = prepare_buffer(state, port)) < 0) {
+ spa_log_debug(state->log, "can't prepare buffer port:%p %d.%d: %s",
+ port, addr->client, addr->port, spa_strerror(res));
+ 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;
+ }
+
+ /* queue_time is the estimated current time of the queue as calculated by
+ * the DLL. Calculate the age of the event. */
+ ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time);
+ if (state->queue_time > ev_time)
+ diff = state->queue_time - ev_time;
+ else
+ diff = 0;
+
+ /* convert the age to samples and convert to an offset */
+ offset = NSEC_TO_CLOCK(&state->rate, diff);
+ if (state->duration > offset)
+ offset = state->duration - 1 - offset;
+ else
+ offset = 0;
+
+ spa_log_trace_fp(state->log, "event time:%"PRIu64" offset:%d size:%ld port:%d.%d",
+ ev_time, offset, size, addr->client, addr->port);
+
+ spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_Midi);
+ spa_pod_builder_bytes(&port->builder, data, size);
+
+ snd_seq_free_event(ev);
+ }
+
+ /* prepare a buffer on each port, some ports might have their
+ * buffer filled above */
+ res = 0;
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ struct spa_io_buffers *io = port->io;
+
+ if (!port->valid || io == NULL)
+ continue;
+
+ if (prepare_buffer(state, port) >= 0) {
+ spa_pod_builder_pop(&port->builder, &port->frame);
+
+ port->buffer->buf->datas[0].chunk->offset = 0;
+ port->buffer->buf->datas[0].chunk->size = port->builder.state.offset;
+
+ /* move buffer to ready queue */
+ spa_list_remove(&port->buffer->link);
+ SPA_FLAG_SET(port->buffer->flags, BUFFER_FLAG_OUT);
+ spa_list_append(&port->ready, &port->buffer->link);
+ port->buffer = NULL;
+ }
+
+ /* if there is already data, continue */
+ if (io->status == SPA_STATUS_HAVE_DATA) {
+ res |= SPA_STATUS_HAVE_DATA;
+ continue;
+ }
+
+ if (io->buffer_id < port->n_buffers)
+ spa_alsa_seq_recycle_buffer(state, port, io->buffer_id);
+
+ if (spa_list_is_empty(&port->ready)) {
+ /* we have no ready buffers */
+ io->buffer_id = SPA_ID_INVALID;
+ io->status = -EPIPE;
+ } else {
+ struct buffer *b = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ /* dequeue ready buffer */
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ res |= SPA_STATUS_HAVE_DATA;
+ }
+ }
+ return res;
+}
+
+static int process_write(struct seq_state *state)
+{
+ struct seq_stream *stream = &state->streams[SPA_DIRECTION_INPUT];
+ uint32_t i;
+ int err, res = 0;
+
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ struct spa_io_buffers *io = port->io;
+ struct buffer *buffer;
+ struct spa_pod_sequence *pod;
+ struct spa_data *d;
+ struct spa_pod_control *c;
+ snd_seq_event_t ev;
+ uint64_t out_time;
+ snd_seq_real_time_t out_rt;
+
+ if (!port->valid || io == NULL)
+ continue;
+
+ if (io->status != SPA_STATUS_HAVE_DATA ||
+ io->buffer_id >= port->n_buffers)
+ continue;
+
+ buffer = &port->buffers[io->buffer_id];
+ d = &buffer->buf->datas[0];
+
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_reuse_buffer(&state->callbacks, i, io->buffer_id);
+ res |= SPA_STATUS_NEED_DATA;
+
+ pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size);
+ if (pod == NULL) {
+ spa_log_warn(state->log, "invalid sequence in buffer max:%u offset:%u size:%u",
+ d->maxsize, d->chunk->offset, d->chunk->size);
+ continue;
+ }
+
+ SPA_POD_SEQUENCE_FOREACH(pod, c) {
+ long size;
+
+ if (c->type != SPA_CONTROL_Midi)
+ continue;
+
+ snd_seq_ev_clear(&ev);
+
+ snd_midi_event_reset_encode(stream->codec);
+ if ((size = snd_midi_event_encode(stream->codec,
+ SPA_POD_BODY(&c->value),
+ SPA_POD_BODY_SIZE(&c->value), &ev)) <= 0) {
+ spa_log_warn(state->log, "failed to encode event: %s",
+ snd_strerror(size));
+ continue;
+ }
+
+ 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 time:%"PRIu64" offset:%d size:%ld port:%d.%d",
+ 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));
+ }
+ }
+ }
+ snd_seq_drain_output(state->event.hndl);
+
+ return res;
+}
+
+static void update_position(struct seq_state *state)
+{
+ if (state->position) {
+ struct spa_io_clock *clock = &state->position->clock;
+ state->rate = clock->rate;
+ if (state->rate.num == 0 || state->rate.denom == 0)
+ state->rate = SPA_FRACTION(1, 48000);
+ state->duration = clock->duration;
+ } else {
+ state->rate = SPA_FRACTION(1, 48000);
+ state->duration = 1024;
+ }
+ state->threshold = state->duration;
+}
+
+static int update_time(struct seq_state *state, uint64_t nsec, bool follower)
+{
+ snd_seq_queue_status_t *status;
+ const snd_seq_real_time_t* queue_time;
+ uint64_t queue_real;
+ double err, corr;
+ uint64_t queue_elapsed;
+
+ corr = 1.0 - (state->dll.z2 + state->dll.z3);
+
+ /* take queue time */
+ snd_seq_queue_status_alloca(&status);
+ snd_seq_get_queue_status(state->event.hndl, state->event.queue_id, status);
+ queue_time = snd_seq_queue_status_get_real_time(status);
+ queue_real = SPA_TIMESPEC_TO_NSEC(queue_time);
+
+ if (state->queue_time == 0)
+ queue_elapsed = 0;
+ else
+ queue_elapsed = (queue_real - state->queue_time) / corr;
+
+ state->queue_time = queue_real;
+
+ queue_elapsed = NSEC_TO_CLOCK(&state->rate, queue_elapsed);
+
+ err = ((int64_t)state->threshold - (int64_t) queue_elapsed);
+ err = SPA_CLAMP(err, -64, 64);
+
+ if (state->dll.bw == 0.0) {
+ spa_dll_set_bw(&state->dll, SPA_DLL_BW_MAX, state->threshold,
+ state->rate.denom);
+ state->next_time = nsec;
+ state->base_time = nsec;
+ }
+ corr = spa_dll_update(&state->dll, err);
+
+ if ((state->next_time - state->base_time) > BW_PERIOD) {
+ state->base_time = state->next_time;
+ spa_log_debug(state->log, "%p: follower:%d rate:%f bw:%f err:%f (%f %f %f)",
+ state, follower, corr, state->dll.bw, err,
+ state->dll.z1, state->dll.z2, state->dll.z3);
+ }
+
+ state->next_time += state->threshold / corr * 1e9 / state->rate.denom;
+
+ if (!follower && state->clock) {
+ state->clock->nsec = nsec;
+ state->clock->position += state->duration;
+ state->clock->duration = state->duration;
+ state->clock->delay = state->duration * corr;
+ state->clock->rate_diff = corr;
+ state->clock->next_nsec = state->next_time;
+ }
+
+ spa_log_trace_fp(state->log, "now:%"PRIu64" queue:%"PRIu64" err:%f corr:%f next:%"PRIu64" thr:%d",
+ nsec, queue_real, err, corr, state->next_time, state->threshold);
+
+ return 0;
+}
+
+int spa_alsa_seq_process(struct seq_state *state)
+{
+ int res;
+
+ update_position(state);
+
+ res = process_recycle(state);
+
+ if (state->following && state->position) {
+ update_time(state, state->position->clock.nsec, true);
+ res |= process_read(state);
+ }
+ res |= process_write(state);
+
+ return res;
+}
+
+static void alsa_on_timeout_event(struct spa_source *source)
+{
+ struct seq_state *state = source->data;
+ uint64_t expire;
+ int res;
+
+ if (state->started) {
+ if ((res = spa_system_timerfd_read(state->data_system, state->timerfd, &expire)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(state->log, "%p: error reading timerfd: %s",
+ state, spa_strerror(res));
+ return;
+ }
+ }
+
+ state->current_time = state->next_time;
+
+ spa_log_trace(state->log, "timeout %"PRIu64, state->current_time);
+
+ update_position(state);
+
+ update_time(state, state->current_time, false);
+
+ res = process_read(state);
+ if (res >= 0)
+ spa_node_call_ready(&state->callbacks, res | SPA_STATUS_NEED_DATA);
+
+ set_timeout(state, state->next_time);
+}
+
+static void reset_buffers(struct seq_state *this, struct seq_port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ if (port->direction == SPA_DIRECTION_INPUT) {
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ } else {
+ spa_list_append(&port->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+ }
+}
+static void reset_stream(struct seq_state *this, struct seq_stream *stream, bool active)
+{
+ uint32_t i;
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (port->valid) {
+ reset_buffers(this, port);
+ spa_alsa_seq_activate_port(this, port, active);
+ }
+ }
+}
+
+static int set_timers(struct seq_state *state)
+{
+ struct timespec now;
+ int res;
+
+ if ((res = spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now)) < 0)
+ return res;
+
+ state->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+ if (state->following) {
+ set_timeout(state, 0);
+ } else {
+ set_timeout(state, state->next_time);
+ }
+ return 0;
+}
+
+static inline bool is_following(struct seq_state *state)
+{
+ return state->position && state->clock && state->position->clock.id != state->clock->id;
+}
+
+int spa_alsa_seq_start(struct seq_state *state)
+{
+ int res;
+
+ if (state->started)
+ return 0;
+
+ state->following = is_following(state);
+
+ spa_log_debug(state->log, "alsa %p: start follower:%d", state, state->following);
+
+ if ((res = snd_seq_start_queue(state->event.hndl, state->event.queue_id, NULL)) < 0) {
+ spa_log_error(state->log, "failed to start queue: %s", snd_strerror(res));
+ return res;
+ }
+ while (snd_seq_drain_output(state->event.hndl) > 0)
+ sleep(1);
+
+ update_position(state);
+
+ state->started = true;
+
+ reset_stream(state, &state->streams[SPA_DIRECTION_INPUT], true);
+ reset_stream(state, &state->streams[SPA_DIRECTION_OUTPUT], true);
+
+ state->source.func = alsa_on_timeout_event;
+ state->source.data = state;
+ state->source.fd = state->timerfd;
+ state->source.mask = SPA_IO_IN;
+ state->source.rmask = 0;
+ spa_loop_add_source(state->data_loop, &state->source);
+
+ state->queue_time = 0;
+ spa_dll_init(&state->dll);
+ set_timers(state);
+
+ return 0;
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct seq_state *state = user_data;
+ set_timers(state);
+ return 0;
+}
+
+int spa_alsa_seq_reassign_follower(struct seq_state *state)
+{
+ bool following;
+
+ if (!state->started)
+ return 0;
+
+ following = is_following(state);
+ if (following != state->following) {
+ spa_log_debug(state->log, "alsa %p: reassign follower %d->%d", state, state->following, following);
+ state->following = following;
+ spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state);
+ }
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct seq_state *state = user_data;
+
+ spa_loop_remove_source(state->data_loop, &state->source);
+ set_timeout(state, 0);
+
+ return 0;
+}
+
+int spa_alsa_seq_pause(struct seq_state *state)
+{
+ int res;
+
+ if (!state->started)
+ return 0;
+
+ spa_log_debug(state->log, "alsa %p: pause", state);
+
+ spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state);
+
+ if ((res = snd_seq_stop_queue(state->event.hndl, state->event.queue_id, NULL)) < 0) {
+ spa_log_warn(state->log, "failed to stop queue: %s", snd_strerror(res));
+ }
+ while (snd_seq_drain_output(state->event.hndl) > 0)
+ sleep(1);
+
+ state->started = false;
+
+ reset_stream(state, &state->streams[SPA_DIRECTION_INPUT], false);
+ reset_stream(state, &state->streams[SPA_DIRECTION_OUTPUT], false);
+
+ return 0;
+}
diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h
new file mode 100644
index 0000000..5d5ed51
--- /dev/null
+++ b/spa/plugins/alsa/alsa-seq.h
@@ -0,0 +1,199 @@
+/* Spa ALSA Sequencer
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_ALSA_SEQ_H
+#define SPA_ALSA_SEQ_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <math.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/dll.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/latency-utils.h>
+
+#include "alsa.h"
+
+
+struct props {
+ char device[64];
+ char clock_name[64];
+ bool disable_longname;
+};
+
+#define MAX_EVENT_SIZE 1024
+#define MAX_PORTS 256
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct seq_port {
+ uint32_t id;
+ enum spa_direction direction;
+ snd_seq_addr_t addr;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+#define PORT_EnumFormat 0
+#define PORT_Meta 1
+#define PORT_IO 2
+#define PORT_Format 3
+#define PORT_Buffers 4
+#define PORT_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct spa_io_buffers *io;
+
+ struct buffer buffers[MAX_BUFFERS];
+ unsigned int n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ struct buffer *buffer;
+ struct spa_pod_builder builder;
+ struct spa_pod_frame frame;
+
+ struct spa_audio_info current_format;
+ unsigned int have_format:1;
+ unsigned int valid:1;
+ unsigned int active:1;
+
+ struct spa_latency_info latency[2];
+};
+
+struct seq_stream {
+ enum spa_direction direction;
+ unsigned int caps;
+ snd_midi_event_t *codec;
+ struct seq_port ports[MAX_PORTS];
+ uint32_t last_port;
+};
+
+struct seq_conn {
+ snd_seq_t *hndl;
+ snd_seq_addr_t addr;
+ int queue_id;
+ int fd;
+ struct spa_source source;
+};
+
+#define BW_PERIOD (3 * SPA_NSEC_PER_SEC)
+
+struct seq_state {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_system *data_system;
+ struct spa_loop *data_loop;
+ struct spa_loop *main_loop;
+
+ struct seq_conn sys;
+ struct seq_conn event;
+ int (*port_info) (void *data, const snd_seq_addr_t *addr, const snd_seq_port_info_t *info);
+ void *port_info_data;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define NODE_PropInfo 0
+#define NODE_Props 1
+#define NODE_IO 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ int rate_denom;
+ uint32_t duration;
+ uint32_t threshold;
+ struct spa_fraction rate;
+
+ struct spa_source source;
+ int timerfd;
+ uint64_t current_time;
+ uint64_t next_time;
+ uint64_t base_time;
+ uint64_t queue_time;
+
+ unsigned int opened:1;
+ unsigned int started:1;
+ unsigned int following:1;
+
+ struct seq_stream streams[2];
+
+ struct spa_dll dll;
+};
+
+#define VALID_DIRECTION(this,d) ((d) == SPA_DIRECTION_INPUT || (d) == SPA_DIRECTION_OUTPUT)
+#define VALID_PORT(this,d,p) ((p) < MAX_PORTS && this->streams[d].ports[p].id == (p))
+#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && VALID_PORT(this,d,p))
+#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && VALID_PORT(this,d,p))
+#define CHECK_PORT(this,d,p) (VALID_DIRECTION(this,d) && VALID_PORT(this,d,p))
+
+#define GET_PORT(this,d,p) (&this->streams[d].ports[p])
+
+int spa_alsa_seq_open(struct seq_state *state);
+int spa_alsa_seq_close(struct seq_state *state);
+
+int spa_alsa_seq_start(struct seq_state *state);
+int spa_alsa_seq_pause(struct seq_state *state);
+int spa_alsa_seq_reassign_follower(struct seq_state *state);
+
+int spa_alsa_seq_activate_port(struct seq_state *state, struct seq_port *port, bool active);
+int spa_alsa_seq_recycle_buffer(struct seq_state *state, struct seq_port *port, uint32_t buffer_id);
+
+int spa_alsa_seq_process(struct seq_state *state);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_ALSA_SEQ_H */
diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c
new file mode 100644
index 0000000..f89d863
--- /dev/null
+++ b/spa/plugins/alsa/alsa-udev.c
@@ -0,0 +1,1014 @@
+/* Spa ALSA udev
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/inotify.h>
+#include <fcntl.h>
+#include <dirent.h>
+
+#include <libudev.h>
+#include <alsa/asoundlib.h>
+
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+
+#include "alsa.h"
+
+#define MAX_DEVICES 64
+
+#define ACTION_ADD 0
+#define ACTION_REMOVE 1
+#define ACTION_DISABLE 2
+
+struct device {
+ uint32_t id;
+ struct udev_device *dev;
+ unsigned int unavailable:1;
+ unsigned int accessible:1;
+ unsigned int ignored:1;
+ unsigned int emitted:1;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_system *main_system;
+
+ struct spa_hook_list hooks;
+
+ uint64_t info_all;
+ struct spa_device_info info;
+
+ struct udev *udev;
+ struct udev_monitor *umonitor;
+
+ struct device devices[MAX_DEVICES];
+ uint32_t n_devices;
+
+ struct spa_source source;
+ struct spa_source notify;
+ unsigned int use_acp:1;
+};
+
+static int impl_udev_open(struct impl *this)
+{
+ if (this->udev == NULL) {
+ this->udev = udev_new();
+ if (this->udev == NULL)
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static int impl_udev_close(struct impl *this)
+{
+ if (this->udev != NULL)
+ udev_unref(this->udev);
+ this->udev = NULL;
+ return 0;
+}
+
+static struct device *add_device(struct impl *this, uint32_t id, struct udev_device *dev)
+{
+ struct device *device;
+
+ if (this->n_devices >= MAX_DEVICES)
+ return NULL;
+ device = &this->devices[this->n_devices++];
+ spa_zero(*device);
+ device->id = id;
+ udev_device_ref(dev);
+ device->dev = dev;
+ return device;
+}
+
+static struct device *find_device(struct impl *this, uint32_t id)
+{
+ uint32_t i;
+ for (i = 0; i < this->n_devices; i++) {
+ if (this->devices[i].id == id)
+ return &this->devices[i];
+ }
+ return NULL;
+}
+
+static void remove_device(struct impl *this, struct device *device)
+{
+ udev_device_unref(device->dev);
+ *device = this->devices[--this->n_devices];
+}
+
+static void clear_devices(struct impl *this)
+{
+ uint32_t i;
+ for (i = 0; i < this->n_devices; i++)
+ udev_device_unref(this->devices[i].dev);
+ this->n_devices = 0;
+}
+
+static uint32_t get_card_id(struct impl *this, struct udev_device *dev)
+{
+ const char *e, *str;
+
+ if (udev_device_get_property_value(dev, "ACP_IGNORE"))
+ return SPA_ID_INVALID;
+
+ if ((str = udev_device_get_property_value(dev, "SOUND_CLASS")) && spa_streq(str, "modem"))
+ return SPA_ID_INVALID;
+
+ if (udev_device_get_property_value(dev, "SOUND_INITIALIZED") == NULL)
+ return SPA_ID_INVALID;
+
+ if ((str = udev_device_get_property_value(dev, "DEVPATH")) == NULL)
+ return SPA_ID_INVALID;
+
+ if ((e = strrchr(str, '/')) == NULL)
+ return SPA_ID_INVALID;
+
+ if (strlen(e) <= 5 || strncmp(e, "/card", 5) != 0)
+ return SPA_ID_INVALID;
+
+ return atoi(e + 5);
+}
+
+static int dehex(char x)
+{
+ if (x >= '0' && x <= '9')
+ return x - '0';
+ if (x >= 'A' && x <= 'F')
+ return x - 'A' + 10;
+ if (x >= 'a' && x <= 'f')
+ return x - 'a' + 10;
+ return -1;
+}
+
+static void unescape(const char *src, char *dst)
+{
+ const char *s;
+ char *d;
+ int h1 = 0, h2 = 0;
+ enum { TEXT, BACKSLASH, EX, FIRST } state = TEXT;
+
+ for (s = src, d = dst; *s; s++) {
+ switch (state) {
+ case TEXT:
+ if (*s == '\\')
+ state = BACKSLASH;
+ else
+ *(d++) = *s;
+ break;
+
+ case BACKSLASH:
+ if (*s == 'x')
+ state = EX;
+ else {
+ *(d++) = '\\';
+ *(d++) = *s;
+ state = TEXT;
+ }
+ break;
+
+ case EX:
+ h1 = dehex(*s);
+ if (h1 < 0) {
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *s;
+ state = TEXT;
+ } else
+ state = FIRST;
+ break;
+
+ case FIRST:
+ h2 = dehex(*s);
+ if (h2 < 0) {
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *(s-1);
+ *(d++) = *s;
+ } else
+ *(d++) = (char) (h1 << 4) | h2;
+ state = TEXT;
+ break;
+ }
+ }
+ switch (state) {
+ case TEXT:
+ break;
+ case BACKSLASH:
+ *(d++) = '\\';
+ break;
+ case EX:
+ *(d++) = '\\';
+ *(d++) = 'x';
+ break;
+ case FIRST:
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *(s-1);
+ break;
+ }
+ *d = 0;
+}
+
+static int check_device_pcm_class(const char *devname)
+{
+ FILE *f;
+ char path[PATH_MAX];
+ char buf[16];
+ size_t sz;
+
+ /* Check device class */
+ spa_scnprintf(path, sizeof(path), "/sys/class/sound/%s/pcm_class",
+ devname);
+ f = fopen(path, "re");
+ if (f == NULL)
+ return -errno;
+ sz = fread(buf, 1, sizeof(buf) - 1, f);
+ buf[sz] = '\0';
+ fclose(f);
+ return spa_strstartswith(buf, "modem") ? -ENXIO : 0;
+}
+
+static int get_num_pcm_devices(unsigned int card_id)
+{
+ char prefix[32];
+ DIR *snd = NULL;
+ struct dirent *entry;
+ int num_dev = 0;
+ int res;
+
+ /* Check if card has PCM devices, without opening them */
+
+ spa_scnprintf(prefix, sizeof(prefix), "pcmC%uD", card_id);
+
+ if ((snd = opendir("/dev/snd")) == NULL)
+ return -errno;
+
+ while ((errno = 0, entry = readdir(snd)) != NULL) {
+ if (!(entry->d_type == DT_CHR &&
+ spa_strstartswith(entry->d_name, prefix)))
+ continue;
+
+ res = check_device_pcm_class(entry->d_name);
+ if (res != -ENXIO) {
+ /* count device also if sysfs status file not accessible */
+ ++num_dev;
+ }
+ }
+ if (errno != 0)
+ res = -errno;
+ else
+ res = num_dev;
+
+ closedir(snd);
+ return res;
+}
+
+static int check_device_available(struct impl *this, struct device *device, int *num_pcm)
+{
+ char path[PATH_MAX];
+ DIR *card = NULL, *pcm = NULL;
+ FILE *f;
+ char buf[16];
+ size_t sz;
+ struct dirent *entry, *entry_pcm;
+ int res;
+
+ res = get_num_pcm_devices(device->id);
+ if (res < 0) {
+ spa_log_error(this->log, "Error finding PCM devices for ALSA card %u: %s",
+ (unsigned int)device->id, spa_strerror(res));
+ return res;
+ }
+ *num_pcm = res;
+
+ spa_log_debug(this->log, "card %u has %d pcm device(s)", (unsigned int)device->id, *num_pcm);
+
+ /*
+ * Check if some pcm devices of the card are busy. Check it via /proc, as we
+ * don't want to actually open any devices using alsa-lib (generates uncontrolled
+ * number of inotify events), or replicate its subdevice logic.
+ *
+ * The /proc/asound directory might not exist if kernel is compiled with
+ * CONFIG_SND_PROCFS=n, and the pcmXX directories may be missing if compiled
+ * with CONFIG_SND_VERBOSE_PROCFS=n. In those cases, the busy check always succeeds.
+ */
+
+ res = 0;
+
+ spa_scnprintf(path, sizeof(path), "/proc/asound/card%u", (unsigned int)device->id);
+
+ if ((card = opendir(path)) == NULL)
+ goto done;
+
+ while ((errno = 0, entry = readdir(card)) != NULL) {
+ if (!(entry->d_type == DT_DIR &&
+ spa_strstartswith(entry->d_name, "pcm")))
+ continue;
+
+ spa_scnprintf(path, sizeof(path), "pcmC%uD%s",
+ (unsigned int)device->id, entry->d_name+3);
+ if (check_device_pcm_class(path) < 0)
+ continue;
+
+ /* Check busy status */
+ spa_scnprintf(path, sizeof(path), "/proc/asound/card%u/%s",
+ (unsigned int)device->id, entry->d_name);
+ if ((pcm = opendir(path)) == NULL)
+ goto done;
+
+ while ((errno = 0, entry_pcm = readdir(pcm)) != NULL) {
+ if (!(entry_pcm->d_type == DT_DIR &&
+ spa_strstartswith(entry_pcm->d_name, "sub")))
+ continue;
+
+ spa_scnprintf(path, sizeof(path), "/proc/asound/card%u/%s/%s/status",
+ (unsigned int)device->id, entry->d_name, entry_pcm->d_name);
+
+ f = fopen(path, "re");
+ if (f == NULL)
+ goto done;
+ sz = fread(buf, 1, 6, f);
+ buf[sz] = '\0';
+ fclose(f);
+
+ if (!spa_strstartswith(buf, "closed")) {
+ spa_log_debug(this->log, "card %u pcm device %s busy",
+ (unsigned int)device->id, entry->d_name);
+ res = -EBUSY;
+ goto done;
+ }
+ spa_log_debug(this->log, "card %u pcm device %s free",
+ (unsigned int)device->id, entry->d_name);
+ }
+ if (errno != 0)
+ goto done;
+
+ closedir(pcm);
+ pcm = NULL;
+ }
+ if (errno != 0)
+ goto done;
+
+done:
+ if (errno != 0) {
+ spa_log_info(this->log, "card %u: failed to find busy status (%s)",
+ (unsigned int)device->id, spa_strerror(-errno));
+ }
+ if (card)
+ closedir(card);
+ if (pcm)
+ closedir(pcm);
+ return res;
+}
+
+static int emit_object_info(struct impl *this, struct device *device)
+{
+ struct spa_device_object_info info;
+ uint32_t id = device->id;
+ struct udev_device *dev = device->dev;
+ const char *str;
+ char path[32], *cn = NULL, *cln = NULL;
+ struct spa_dict_item items[25];
+ uint32_t n_items = 0;
+ int res, pcm;
+
+ /*
+ * inotify close events under /dev/snd must not be emitted, except after setting
+ * device->emitted to true. alsalib functions can be used after that.
+ */
+
+ snprintf(path, sizeof(path), "hw:%u", id);
+
+ if ((res = check_device_available(this, device, &pcm)) < 0)
+ return res;
+ if (pcm == 0) {
+ spa_log_debug(this->log, "no pcm devices for %s", path);
+ device->ignored = true;
+ return -ENODEV;
+ }
+
+ spa_log_debug(this->log, "emitting card %s", path);
+ device->emitted = true;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+
+ info.type = SPA_TYPE_INTERFACE_Device;
+ info.factory_name = this->use_acp ?
+ SPA_NAME_API_ALSA_ACP_DEVICE :
+ SPA_NAME_API_ALSA_PCM_DEVICE;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.flags = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API, "udev");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, path+3);
+ if (snd_card_get_name(id, &cn) >= 0)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD_NAME, cn);
+ if (snd_card_get_longname(id, &cln) >= 0)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD_LONGNAME, cln);
+
+ if ((str = udev_device_get_property_value(dev, "ACP_NAME")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, str);
+
+ if ((str = udev_device_get_property_value(dev, "ACP_PROFILE_SET")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PROFILE_SET, str);
+
+ if ((str = udev_device_get_property_value(dev, "SOUND_CLASS")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_CLASS, str);
+
+ if ((str = udev_device_get_property_value(dev, "USEC_INITIALIZED")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PLUGGED_USEC, str);
+
+ str = udev_device_get_property_value(dev, "ID_PATH");
+ if (!(str && *str))
+ str = udev_device_get_syspath(dev);
+ if (str && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str);
+ }
+ if ((str = udev_device_get_devpath(dev)) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_ID")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_ID, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_BUS")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "SUBSYSTEM")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_VENDOR_ID")) && *str) {
+ int32_t val;
+ if (spa_atoi32(str, &val, 16)) {
+ char *dec = alloca(12); /* 0xffffffff is max */
+ snprintf(dec, 12, "0x%04x", val);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, dec);
+ }
+ }
+ str = udev_device_get_property_value(dev, "ID_VENDOR_FROM_DATABASE");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_VENDOR_ENC");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_VENDOR");
+ } else {
+ char *t = alloca(strlen(str) + 1);
+ unescape(str, t);
+ str = t;
+ }
+ }
+ if (str && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_NAME, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_MODEL_ID")) && *str) {
+ int32_t val;
+ if (spa_atoi32(str, &val, 16)) {
+ char *dec = alloca(12); /* 0xffffffff is max */
+ snprintf(dec, 12, "0x%04x", val);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, dec);
+ }
+ }
+ str = udev_device_get_property_value(dev, "ID_MODEL_FROM_DATABASE");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_MODEL_ENC");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_MODEL");
+ } else {
+ char *t = alloca(strlen(str) + 1);
+ unescape(str, t);
+ str = t;
+ }
+ }
+ if (str && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_NAME, str);
+
+ if ((str = udev_device_get_property_value(dev, "ID_SERIAL")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SERIAL, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "SOUND_FORM_FACTOR")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_FORM_FACTOR, str);
+ }
+ info.props = &SPA_DICT_INIT(items, n_items);
+
+ spa_device_emit_object_info(&this->hooks, id, &info);
+ free(cn);
+ free(cln);
+
+ return 1;
+}
+
+static bool check_access(struct impl *this, struct device *device)
+{
+ char path[128], prefix[32];
+ DIR *snd = NULL;
+ struct dirent *entry;
+ bool accessible = false;
+
+ snprintf(path, sizeof(path), "/dev/snd/controlC%u", device->id);
+ if (access(path, R_OK|W_OK) >= 0 && (snd = opendir("/dev/snd"))) {
+ /*
+ * It's possible that controlCX is accessible before pcmCX* or
+ * the other way around. Return true only if all devices are
+ * accessible.
+ */
+
+ accessible = true;
+ spa_scnprintf(prefix, sizeof(prefix), "pcmC%uD", device->id);
+ while ((entry = readdir(snd)) != NULL) {
+ if (!(entry->d_type == DT_CHR &&
+ spa_strstartswith(entry->d_name, prefix)))
+ continue;
+
+ snprintf(path, sizeof(path), "/dev/snd/%.32s", entry->d_name);
+ if (access(path, R_OK|W_OK) < 0) {
+ accessible = false;
+ break;
+ }
+ }
+ closedir(snd);
+ }
+
+ if (accessible != device->accessible)
+ spa_log_debug(this->log, "%s accessible:%u", path, accessible);
+ device->accessible = accessible;
+
+ return device->accessible;
+}
+
+static void process_device(struct impl *this, uint32_t action, struct udev_device *dev)
+{
+ uint32_t id;
+ struct device *device;
+ bool emitted;
+ int res;
+
+ if ((id = get_card_id(this, dev)) == SPA_ID_INVALID)
+ return;
+
+ device = find_device(this, id);
+ if (device && device->ignored)
+ return;
+
+ switch (action) {
+ case ACTION_ADD:
+ if (device == NULL)
+ device = add_device(this, id, dev);
+ if (device == NULL)
+ return;
+ if (!check_access(this, device))
+ return;
+ res = emit_object_info(this, device);
+ if (res < 0) {
+ if (device->ignored)
+ spa_log_info(this->log, "ALSA card %u unavailable (%s): it is ignored",
+ device->id, spa_strerror(res));
+ else if (!device->unavailable)
+ spa_log_info(this->log, "ALSA card %u unavailable (%s): wait for it",
+ device->id, spa_strerror(res));
+ else
+ spa_log_debug(this->log, "ALSA card %u still unavailable (%s)",
+ device->id, spa_strerror(res));
+ device->unavailable = true;
+ } else {
+ if (device->unavailable)
+ spa_log_info(this->log, "ALSA card %u now available",
+ device->id);
+ device->unavailable = false;
+ }
+ break;
+
+ case ACTION_REMOVE:
+ if (device == NULL)
+ return;
+ emitted = device->emitted;
+ remove_device(this, device);
+ if (emitted)
+ spa_device_emit_object_info(&this->hooks, id, NULL);
+ break;
+
+ case ACTION_DISABLE:
+ if (device == NULL)
+ return;
+ if (device->emitted) {
+ device->emitted = false;
+ spa_device_emit_object_info(&this->hooks, id, NULL);
+ }
+ break;
+ }
+}
+
+static int stop_inotify(struct impl *this)
+{
+ if (this->notify.fd == -1)
+ return 0;
+ spa_log_info(this->log, "stop inotify");
+ spa_loop_remove_source(this->main_loop, &this->notify);
+ close(this->notify.fd);
+ this->notify.fd = -1;
+ return 0;
+}
+
+static void impl_on_notify_events(struct spa_source *source)
+{
+ bool deleted = false;
+ struct impl *this = source->data;
+ union {
+ struct inotify_event e;
+ char name[NAME_MAX+1+sizeof(struct inotify_event)];
+ } buf;
+
+ while (true) {
+ ssize_t len;
+ const struct inotify_event *event;
+ void *p, *e;
+
+ len = read(source->fd, &buf, sizeof(buf));
+ if (len < 0 && errno != EAGAIN)
+ break;
+ if (len <= 0)
+ break;
+
+ e = SPA_PTROFF(&buf, len, void);
+
+ for (p = &buf; p < e;
+ p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) {
+ unsigned int id;
+ struct device *device;
+
+ event = (const struct inotify_event *) p;
+ spa_assert_se(SPA_PTRDIFF(e, p) >= (ptrdiff_t)sizeof(struct inotify_event) &&
+ SPA_PTRDIFF(e, p) - sizeof(struct inotify_event) >= event->len &&
+ "bad event from kernel");
+
+ /* Device becomes accessible or not busy */
+ if ((event->mask & (IN_ATTRIB | IN_CLOSE_WRITE))) {
+ bool access;
+ if (sscanf(event->name, "controlC%u", &id) != 1 &&
+ sscanf(event->name, "pcmC%uD", &id) != 1)
+ continue;
+ if ((device = find_device(this, id)) == NULL)
+ continue;
+
+ access = check_access(this, device);
+ if (access && !device->emitted)
+ process_device(this, ACTION_ADD, device->dev);
+ else if (!access && device->emitted)
+ process_device(this, ACTION_DISABLE, device->dev);
+ }
+ /* /dev/snd/ might have been removed */
+ if ((event->mask & (IN_DELETE_SELF | IN_MOVE_SELF)))
+ deleted = true;
+ }
+ }
+ if (deleted)
+ stop_inotify(this);
+}
+
+static int start_inotify(struct impl *this)
+{
+ int res, notify_fd;
+
+ if (this->notify.fd != -1)
+ return 0;
+
+ if ((notify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) < 0)
+ return -errno;
+
+ res = inotify_add_watch(notify_fd, "/dev/snd",
+ IN_ATTRIB | IN_CLOSE_WRITE | IN_DELETE_SELF | IN_MOVE_SELF);
+ if (res < 0) {
+ res = -errno;
+ close(notify_fd);
+
+ if (res == -ENOENT) {
+ spa_log_debug(this->log, "/dev/snd/ does not exist yet");
+ return 0;
+ }
+ spa_log_error(this->log, "inotify_add_watch() failed: %s", spa_strerror(res));
+ return res;
+ }
+ spa_log_info(this->log, "start inotify");
+ this->notify.func = impl_on_notify_events;
+ this->notify.data = this;
+ this->notify.fd = notify_fd;
+ this->notify.mask = SPA_IO_IN | SPA_IO_ERR;
+
+ spa_loop_add_source(this->main_loop, &this->notify);
+
+ return 0;
+}
+
+static void impl_on_fd_events(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct udev_device *dev;
+ const char *action;
+
+ dev = udev_monitor_receive_device(this->umonitor);
+ if (dev == NULL)
+ return;
+
+ if ((action = udev_device_get_action(dev)) == NULL)
+ action = "change";
+
+ spa_log_debug(this->log, "action %s", action);
+
+ start_inotify(this);
+
+ if (spa_streq(action, "change")) {
+ process_device(this, ACTION_ADD, dev);
+ } else if (spa_streq(action, "remove")) {
+ process_device(this, ACTION_REMOVE, dev);
+ }
+ udev_device_unref(dev);
+}
+
+static int start_monitor(struct impl *this)
+{
+ int res;
+
+ if (this->umonitor != NULL)
+ return 0;
+
+ this->umonitor = udev_monitor_new_from_netlink(this->udev, "udev");
+ if (this->umonitor == NULL)
+ return -ENOMEM;
+
+ udev_monitor_filter_add_match_subsystem_devtype(this->umonitor,
+ "sound", NULL);
+ udev_monitor_enable_receiving(this->umonitor);
+
+ this->source.func = impl_on_fd_events;
+ this->source.data = this;
+ this->source.fd = udev_monitor_get_fd(this->umonitor);
+ this->source.mask = SPA_IO_IN | SPA_IO_ERR;
+
+ spa_log_debug(this->log, "monitor %p", this->umonitor);
+ spa_loop_add_source(this->main_loop, &this->source);
+
+ if ((res = start_inotify(this)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int stop_monitor(struct impl *this)
+{
+ if (this->umonitor == NULL)
+ return 0;
+
+ clear_devices (this);
+
+ spa_loop_remove_source(this->main_loop, &this->source);
+ udev_monitor_unref(this->umonitor);
+ this->umonitor = NULL;
+
+ stop_inotify(this);
+
+ return 0;
+}
+
+static int enum_devices(struct impl *this)
+{
+ struct udev_enumerate *enumerate;
+ struct udev_list_entry *devices;
+
+ enumerate = udev_enumerate_new(this->udev);
+ if (enumerate == NULL)
+ return -ENOMEM;
+
+ udev_enumerate_add_match_subsystem(enumerate, "sound");
+ udev_enumerate_scan_devices(enumerate);
+
+ for (devices = udev_enumerate_get_list_entry(enumerate); devices;
+ devices = udev_list_entry_get_next(devices)) {
+ struct udev_device *dev;
+
+ dev = udev_device_new_from_syspath(this->udev, udev_list_entry_get_name(devices));
+ if (dev == NULL)
+ continue;
+
+ process_device(this, ACTION_ADD, dev);
+
+ udev_device_unref(dev);
+ }
+ udev_enumerate_unref(enumerate);
+
+ return 0;
+}
+
+static const struct spa_dict_item device_info_items[] = {
+ { SPA_KEY_DEVICE_API, "udev" },
+ { SPA_KEY_DEVICE_NICK, "alsa-udev" },
+ { SPA_KEY_API_UDEV_MATCH, "sound" },
+};
+
+static void emit_device_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(device_info_items);
+ spa_device_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void impl_hook_removed(struct spa_hook *hook)
+{
+ struct impl *this = hook->priv;
+ if (spa_hook_list_is_empty(&this->hooks)) {
+ stop_monitor(this);
+ impl_udev_close(this);
+ }
+}
+
+static int
+impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ int res;
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ if ((res = impl_udev_open(this)) < 0)
+ return res;
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_device_info(this, true);
+
+ if ((res = start_monitor(this)) < 0)
+ return res;
+
+ if ((res = enum_devices(this)) < 0)
+ return res;
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ listener->removed = impl_hook_removed;
+ listener->priv = this;
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ stop_monitor(this);
+ impl_udev_close(this);
+ 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;
+ const char *str;
+
+ 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->notify.fd = -1;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+
+ if (this->main_loop == NULL) {
+ spa_log_error(this->log, "a main-loop is needed");
+ return -EINVAL;
+ }
+ if (this->main_system == NULL) {
+ spa_log_error(this->log, "a main-system is needed");
+ return -EINVAL;
+ }
+ spa_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ this->info = SPA_DEVICE_INFO_INIT();
+ this->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS;
+ this->info.flags = 0;
+
+ if (info) {
+ if ((str = spa_dict_lookup(info, "alsa.use-acp")) != NULL)
+ this->use_acp = spa_atob(str);
+ }
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_alsa_udev_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_ENUM_UDEV,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa.c b/spa/plugins/alsa/alsa.c
new file mode 100644
index 0000000..ab4aa3a
--- /dev/null
+++ b/spa/plugins/alsa/alsa.c
@@ -0,0 +1,80 @@
+/* Spa ALSA support
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+
+extern const struct spa_handle_factory spa_alsa_source_factory;
+extern const struct spa_handle_factory spa_alsa_sink_factory;
+extern const struct spa_handle_factory spa_alsa_udev_factory;
+extern const struct spa_handle_factory spa_alsa_device_factory;
+extern const struct spa_handle_factory spa_alsa_seq_bridge_factory;
+extern const struct spa_handle_factory spa_alsa_acp_device_factory;
+#ifdef HAVE_ALSA_COMPRESS_OFFLOAD
+extern const struct spa_handle_factory spa_alsa_compress_offload_sink_factory;
+#endif
+
+struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.alsa");
+struct spa_log_topic *alsa_log_topic = &log_topic;
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *factory = &spa_alsa_source_factory;
+ break;
+ case 1:
+ *factory = &spa_alsa_sink_factory;
+ break;
+ case 2:
+ *factory = &spa_alsa_udev_factory;
+ break;
+ case 3:
+ *factory = &spa_alsa_device_factory;
+ break;
+ case 4:
+ *factory = &spa_alsa_seq_bridge_factory;
+ break;
+ case 5:
+ *factory = &spa_alsa_acp_device_factory;
+ break;
+#ifdef HAVE_ALSA_COMPRESS_OFFLOAD
+ case 6:
+ *factory = &spa_alsa_compress_offload_sink_factory;
+ break;
+#endif
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/alsa/alsa.h b/spa/plugins/alsa/alsa.h
new file mode 100644
index 0000000..ee18929
--- /dev/null
+++ b/spa/plugins/alsa/alsa.h
@@ -0,0 +1,39 @@
+/* Spa ALSA Source
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_ALSA_H
+#define SPA_ALSA_H
+
+#include <spa/support/log.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT alsa_log_topic
+extern struct spa_log_topic *alsa_log_topic;
+
+static inline void alsa_log_topic_init(struct spa_log *log)
+{
+ spa_log_topic_init(log, alsa_log_topic);
+}
+
+#endif /* SPA_ALSA_H */
diff --git a/spa/plugins/alsa/meson.build b/spa/plugins/alsa/meson.build
new file mode 100644
index 0000000..7dedc87
--- /dev/null
+++ b/spa/plugins/alsa/meson.build
@@ -0,0 +1,60 @@
+subdir('acp')
+subdir('mixer')
+
+spa_alsa_dependencies = [ spa_dep, alsa_dep, libudev_dep, mathlib, epoll_shim_dep, libinotify_dep ]
+
+spa_alsa_sources = ['alsa.c',
+ 'alsa.h',
+ 'alsa-udev.c',
+ 'alsa-acp-device.c',
+ 'alsa-pcm-device.c',
+ 'alsa-pcm-sink.c',
+ 'alsa-pcm-source.c',
+ 'alsa-pcm.c',
+ 'alsa-seq-bridge.c',
+ 'alsa-seq.c']
+
+if tinycompress_dep.found()
+ spa_alsa_sources += [ 'alsa-compress-offload-sink.c' ]
+ spa_alsa_dependencies += tinycompress_dep
+endif
+
+spa_alsa = shared_library(
+ 'spa-alsa',
+ [ spa_alsa_sources ],
+ c_args : acp_c_args,
+ include_directories : [configinc],
+ dependencies : spa_alsa_dependencies,
+ link_with : [ acp_lib ],
+ install : true,
+ install_dir : spa_plugindir / 'alsa'
+)
+
+alsa_udevrules = [
+ '90-pipewire-alsa.rules',
+]
+
+executable('spa-acp-tool',
+ [ 'acp-tool.c' ],
+ c_args : acp_c_args,
+ dependencies : [ spa_dep, alsa_dep, mathlib, acp_dep ],
+ install : true,
+)
+
+executable('test-timer',
+ [ 'test-timer.c' ],
+ dependencies : [ spa_dep, alsa_dep, mathlib, epoll_shim_dep ],
+ install : false,
+)
+
+executable('test-hw-params',
+ [ 'test-hw-params.c' ],
+ dependencies : [ spa_dep, alsa_dep, mathlib ],
+ install : false,
+)
+
+if libudev_dep.found()
+ install_data(alsa_udevrules,
+ install_dir : udevrulesdir,
+ )
+endif
diff --git a/spa/plugins/alsa/mixer/meson.build b/spa/plugins/alsa/mixer/meson.build
new file mode 100644
index 0000000..d4327b8
--- /dev/null
+++ b/spa/plugins/alsa/mixer/meson.build
@@ -0,0 +1,7 @@
+install_subdir('paths',
+ install_dir : alsadatadir
+)
+
+install_subdir('profile-sets',
+ install_dir : alsadatadir
+)
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-aux.conf b/spa/plugins/alsa/mixer/paths/analog-input-aux.conf
new file mode 100644
index 0000000..47e22c5
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-aux.conf
@@ -0,0 +1,65 @@
+# 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/>.
+
+; For devices where an 'Aux' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 80
+description-key = analog-input
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf
new file mode 100644
index 0000000..96861e7
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf
@@ -0,0 +1,104 @@
+# 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/>.
+
+; For devices where a 'Dock Mic' or 'Dock Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 78
+description-key = analog-input-microphone-dock
+
+[Jack Dock Mic]
+required-any = any
+
+[Jack Dock Mic Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Dock Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Dock Mic Boost:on]
+name = input-boost-on
+
+[Option Dock Mic Boost:off]
+name = input-boost-off
+
+[Element Dock Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Dock Mic]
+name = analog-input-microphone-dock
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Dock Mic]
+name = analog-input-microphone-dock
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-fm.conf b/spa/plugins/alsa/mixer/paths/analog-input-fm.conf
new file mode 100644
index 0000000..d3501a8
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-fm.conf
@@ -0,0 +1,65 @@
+# 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/>.
+
+; For devices where an 'FM' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 70
+description-key = analog-input-radio
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf
new file mode 100644
index 0000000..6e7775c
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf
@@ -0,0 +1,104 @@
+# 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/>.
+
+; For devices where a 'Front Mic' or 'Front Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 85
+description-key = analog-input-microphone-front
+
+[Jack Front Mic]
+required-any = any
+
+[Jack Front Mic Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Front Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Front Mic Boost:on]
+name = input-boost-on
+
+[Option Front Mic Boost:off]
+name = input-boost-off
+
+[Element Front Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Front Mic]
+name = analog-input-microphone-front
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Front Mic]
+name = analog-input-microphone-front
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf
new file mode 100644
index 0000000..eb5740a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf
@@ -0,0 +1,102 @@
+# 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/>.
+
+; For some ASUS netbooks that have one jack that can be either a Headphone
+; *or* a mic. This path will be active only when it is used as a mic.
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 87
+description-key = analog-input-microphone
+
+[Jack Headphone Mic]
+required-any = any
+state.plugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headphone Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headphone Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Headphone Mic]
+name = analog-input-microphone
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Headphone Mic]
+name = analog-input-microphone
+required-any = any
+
+; Make sure the internal speakers are not auto-muted when you plug a mic in
+[Element Auto-Mute Mode]
+enumeration = select
+
+[Option Auto-Mute Mode:Disabled]
+name = analog-input-microphone
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf
new file mode 100644
index 0000000..579db6b
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf
@@ -0,0 +1,114 @@
+# 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/>.
+
+; For devices where a 'Headset Mic' or 'Headset Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 88
+description-key = analog-input-microphone-headset
+
+[Jack Headset Mic]
+required-any = any
+
+[Jack Headset Mic Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Headphone]
+state.plugged = unknown
+
+[Jack Front Headphone]
+state.plugged = unknown
+
+[Jack Headphone Mic]
+state.plugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headset Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headset Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headset]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Headset Mic]
+name = Headset Microphone
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Headset Mic]
+name = Headset Microphone
+required-any = any
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf b/spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf
new file mode 100644
index 0000000..9e22008
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf
@@ -0,0 +1,133 @@
+# 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/>.
+
+; For devices where a 'Internal Mic' or 'Internal Mic Boost' element exists
+; 'Int Mic' and 'Int Mic Boost' are for compatibility with kernels < 2.6.38
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 89
+description-key = analog-input-microphone-internal
+
+[Jack Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Rear Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Internal Mic Boost]
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Internal Mic Boost:on]
+name = input-boost-on
+
+[Option Internal Mic Boost:off]
+name = input-boost-off
+
+[Element Int Mic Boost]
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Int Mic Boost:on]
+name = input-boost-on
+
+[Option Int Mic Boost:off]
+name = input-boost-off
+
+[Element Internal Mic]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Int Mic]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Internal Mic]
+name = analog-input-microphone-internal
+
+[Option Input Source:Int Mic]
+name = analog-input-microphone-internal
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Internal Mic]
+name = analog-input-microphone-internal
+
+[Option Capture Source:Int Mic]
+name = analog-input-microphone-internal
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf
new file mode 100644
index 0000000..898410a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf
@@ -0,0 +1,154 @@
+# 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/>.
+
+; For devices where a 'Internal Mic' or 'Internal Mic Boost' element exists
+; 'Int Mic' and 'Int Mic Boost' are for compatibility with kernels < 2.6.38
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 89
+description-key = analog-input-microphone-internal
+
+[Jack Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Rear Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Internal Mic Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Internal Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Internal Mic Boost:on]
+name = input-boost-on
+
+[Option Internal Mic Boost:off]
+name = input-boost-off
+
+[Element Int Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Int Mic Boost:on]
+name = input-boost-on
+
+[Option Int Mic Boost:off]
+name = input-boost-off
+
+[Element Internal Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Int Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Internal Mic]
+name = analog-input-microphone-internal
+required-any = any
+
+[Option Input Source:Int Mic]
+name = analog-input-microphone-internal
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Internal Mic]
+name = analog-input-microphone-internal
+required-any = any
+
+[Option Capture Source:Int Mic]
+name = analog-input-microphone-internal
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Headphone Mic]
+switch = off
+volume = off
+
+[Element Headphone Mic Boost]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-linein.conf b/spa/plugins/alsa/mixer/paths/analog-input-linein.conf
new file mode 100644
index 0000000..cf20790
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-linein.conf
@@ -0,0 +1,144 @@
+# 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/>.
+
+; For devices where a 'Line' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 81
+
+[Jack Line]
+required-any = any
+
+[Jack Line Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Line - Input]
+required-any = any
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Line Boost]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Line]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Line]
+name = analog-input-linein
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Line]
+name = analog-input-linein
+required-any = any
+
+[Element PCM Capture Source]
+enumeration = select
+
+[Option PCM Capture Source:Line]
+name = analog-input-linein
+required-any = any
+
+[Option PCM Capture Source:Line In]
+name = analog-input-linein
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+[Element Mic Jack Mode]
+enumeration = select
+
+[Option Mic Jack Mode:Line In]
+priority = 19
+name = input-linein
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf b/spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf
new file mode 100644
index 0000000..7147d20
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf
@@ -0,0 +1,66 @@
+# 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/>.
+
+; For devices where a 'Mic/Line' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 85
+description-key = analog-input
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+.include analog-input.conf.common
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-mic.conf
new file mode 100644
index 0000000..53c03c8
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-mic.conf
@@ -0,0 +1,141 @@
+# 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/>.
+
+; For devices where a 'Mic' or 'Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 87
+description-key = analog-input-microphone
+
+[Jack Mic]
+required-any = any
+
+[Jack Mic Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Mic - Input]
+required-any = any
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Mic Boost:on]
+name = input-boost-on
+
+[Option Mic Boost:off]
+name = input-boost-off
+
+[Element Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Mic]
+name = analog-input-microphone
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Mic]
+name = analog-input-microphone
+required-any = any
+
+[Element PCM Capture Source]
+enumeration = select
+
+[Option PCM Capture Source:Mic]
+name = analog-input-microphone
+required-any = any
+
+[Option PCM Capture Source:Mic-In/Mic Array]
+name = analog-input-microphone
+required-any = any
+
+;;; Some AC'97s have "Mic Select" and "Mic Boost (+20dB)"
+
+[Element Mic Select]
+enumeration = select
+
+[Option Mic Select:Mic1]
+name = input-microphone
+priority = 20
+
+[Option Mic Select:Mic2]
+name = input-microphone
+priority = 19
+
+[Element Mic Boost (+20dB)]
+switch = select
+volume = merge
+
+[Option Mic Boost (+20dB):on]
+name = input-boost-on
+
+[Option Mic Boost (+20dB):off]
+name = input-boost-off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common b/spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common
new file mode 100644
index 0000000..e5ced21
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common
@@ -0,0 +1,60 @@
+# 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/>.
+
+; Common element for all microphone inputs
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[Properties]
+device.icon_name = audio-input-microphone
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Line Boost]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+[Element Inverted Internal Mic]
+switch = off
+volume = off
+
+[Element Mic Jack Mode]
+enumeration = select
+
+[Option Mic Jack Mode:Mic In]
+priority = 19
+name = input-microphone
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf
new file mode 100644
index 0000000..a92f9d1
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf
@@ -0,0 +1,107 @@
+# 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/>.
+
+; For devices where a 'Rear Mic' or 'Rear Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 82
+description-key = analog-input-microphone-rear
+
+[Jack Rear Mic]
+required-any = any
+
+[Jack Rear Mic - Input]
+required-any = any
+
+[Jack Rear Mic Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Rear Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Rear Mic Boost:on]
+name = input-boost-on
+
+[Option Rear Mic Boost:off]
+name = input-boost-off
+
+[Element Rear Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Rear Mic]
+name = analog-input-microphone-rear
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Rear Mic]
+name = analog-input-microphone-rear
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf b/spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf
new file mode 100644
index 0000000..99d1d79
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf
@@ -0,0 +1,65 @@
+# 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/>.
+
+; For devices where a 'TV Tuner' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 70
+description-key = analog-input-video
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element FM]
+switch = off
+volume = off
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-video.conf b/spa/plugins/alsa/mixer/paths/analog-input-video.conf
new file mode 100644
index 0000000..50c999e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-video.conf
@@ -0,0 +1,64 @@
+# 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/>.
+
+; For devices where a 'Video' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 70
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input.conf b/spa/plugins/alsa/mixer/paths/analog-input.conf
new file mode 100644
index 0000000..c9db677
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input.conf
@@ -0,0 +1,102 @@
+# 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/>.
+
+; A fallback for devices that lack separate Mic/Line/Aux/Video/TV
+; Tuner/FM elements
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 100
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+required-absent = any
+
+[Element Mic Boost]
+required-absent = any
+
+[Element Dock Mic]
+required-absent = any
+
+[Element Dock Mic Boost]
+required-absent = any
+
+[Element Front Mic]
+required-absent = any
+
+[Element Front Mic Boost]
+required-absent = any
+
+[Element Int Mic]
+required-absent = any
+
+[Element Int Mic Boost]
+required-absent = any
+
+[Element Internal Mic]
+required-absent = any
+
+[Element Internal Mic Boost]
+required-absent = any
+
+[Element Rear Mic]
+required-absent = any
+
+[Element Rear Mic Boost]
+required-absent = any
+
+[Element Headset]
+required-absent = any
+
+[Element Headset Mic]
+required-absent = any
+
+[Element Headset Mic Boost]
+required-absent = any
+
+[Element Headphone Mic]
+required-absent = any
+
+[Element Headphone Mic Boost]
+required-absent = any
+
+[Element Line]
+required-absent = any
+
+[Element Line Boost]
+required-absent = any
+
+[Element Aux]
+required-absent = any
+
+[Element Video]
+required-absent = any
+
+[Element Mic/Line]
+required-absent = any
+
+[Element TV Tuner]
+required-absent = any
+
+[Element FM]
+required-absent = any
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input.conf.common b/spa/plugins/alsa/mixer/paths/analog-input.conf.common
new file mode 100644
index 0000000..201087e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input.conf.common
@@ -0,0 +1,289 @@
+# 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/>.
+
+; Mixer path for PulseAudio's ALSA backend, common elements for all
+; input paths. If multiple options by the same id are discovered they
+; will be suffixed with a number to distinguish them, in the same
+; order they appear here.
+;
+; Source selection should use the following names:
+;
+; input -- If we don't know the exact kind of input
+; input-microphone
+; input-microphone-internal
+; input-microphone-external
+; input-linein
+; input-video
+; input-radio
+; input-docking-microphone
+; input-docking-linein
+; input-docking
+;
+; We explicitly don't want to wrap the following sources:
+;
+; CD
+; Synth/MIDI
+; Phone
+; Mix
+; Digital/SPDIF
+; Master
+; PC Speaker
+;
+; See analog-output.conf.common for an explanation on the directives
+
+;;; 'Input Source Select'
+
+[Element Input Source Select]
+enumeration = select
+
+[Option Input Source Select:Input1]
+name = input
+priority = 10
+
+[Option Input Source Select:Input2]
+name = input
+priority = 5
+
+;;; 'Input Source'
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Digital Mic]
+name = input-microphone
+priority = 20
+
+[Option Input Source:Microphone]
+name = input-microphone
+priority = 20
+
+[Option Input Source:Front Microphone]
+name = input-microphone
+priority = 19
+
+[Option Input Source:Internal Mic 1]
+name = input-microphone
+priority = 19
+
+[Option Input Source:Line-In]
+name = input-linein
+priority = 18
+
+[Option Input Source:Line In]
+name = input-linein
+priority = 18
+
+[Option Input Source:Docking-Station]
+name = input-docking
+priority = 17
+
+[Option Input Source:AUX IN]
+name = input
+priority = 10
+
+;;; 'Capture Source'
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:TV Tuner]
+name = input-video
+
+[Option Capture Source:FM]
+name = input-radio
+
+[Option Capture Source:Mic/Line]
+name = input
+
+[Option Capture Source:Line/Mic]
+name = input
+
+[Option Capture Source:Microphone]
+name = input-microphone
+
+[Option Capture Source:Int DMic]
+name = input-microphone-internal
+
+[Option Capture Source:iMic]
+name = input-microphone-internal
+
+[Option Capture Source:i-Mic]
+name = input-microphone-internal
+
+[Option Capture Source:Internal Microphone]
+name = input-microphone-internal
+
+[Option Capture Source:Front Microphone]
+name = input-microphone
+
+[Option Capture Source:Mic1]
+name = input-microphone
+
+[Option Capture Source:Mic2]
+name = input-microphone
+
+[Option Capture Source:D-Mic]
+name = input-microphone
+
+[Option Capture Source:IntMic]
+name = input-microphone-internal
+
+[Option Capture Source:ExtMic]
+name = input-microphone-external
+
+[Option Capture Source:Ext Mic]
+name = input-microphone-external
+
+[Option Capture Source:E-Mic]
+name = input-microphone-external
+
+[Option Capture Source:e-Mic]
+name = input-microphone-external
+
+[Option Capture Source:LineIn]
+name = input-linein
+
+[Option Capture Source:Analog]
+name = input
+
+[Option Capture Source:Line-In]
+name = input-linein
+
+[Option Capture Source:Line In]
+name = input-linein
+
+[Option Capture Source:Video]
+name = input-video
+
+[Option Capture Source:Aux]
+name = input
+
+[Option Capture Source:Aux0]
+name = input
+
+[Option Capture Source:Aux1]
+name = input
+
+[Option Capture Source:Aux2]
+name = input
+
+[Option Capture Source:Aux3]
+name = input
+
+[Option Capture Source:AUX IN]
+name = input
+
+[Option Capture Source:Aux In]
+name = input
+
+[Option Capture Source:AOUT]
+name = input
+
+[Option Capture Source:AUX]
+name = input
+
+[Option Capture Source:Cam Mic]
+name = input-microphone
+
+[Option Capture Source:Digital Mic]
+name = input-microphone
+
+[Option Capture Source:Digital Mic 1]
+name = input-microphone
+
+[Option Capture Source:Digital Mic 2]
+name = input-microphone
+
+[Option Capture Source:Analog Inputs]
+name = input
+
+[Option Capture Source:Unknown1]
+name = input
+
+[Option Capture Source:Unknown2]
+name = input
+
+[Option Capture Source:Docking-Station]
+name = input-docking
+
+;;; 'Mic Jack Mode'
+
+[Element Mic Jack Mode]
+enumeration = select
+
+[Option Mic Jack Mode:Mic In]
+name = input-microphone
+
+[Option Mic Jack Mode:Line In]
+name = input-linein
+
+;;; 'Digital Input Source'
+
+[Element Digital Input Source]
+enumeration = select
+
+[Option Digital Input Source:Digital Mic 1]
+name = input-microphone
+
+[Option Digital Input Source:Analog Inputs]
+name = input
+
+[Option Digital Input Source:Digital Mic 2]
+name = input-microphone
+
+;;; 'Analog Source'
+
+[Element Analog Source]
+enumeration = select
+
+[Option Analog Source:Mic]
+name = input-microphone
+
+[Option Analog Source:Line in]
+name = input-linein
+
+[Option Analog Source:Aux]
+name = input
+
+;;; 'Shared Mic/Line in'
+
+[Element Shared Mic/Line in]
+enumeration = select
+
+[Option Shared Mic/Line in:Mic in]
+name = input-microphone
+
+[Option Shared Mic/Line in:Line in]
+name = input-linein
+
+;;; Various Boosts
+
+[Element Capture Boost]
+switch = select
+
+[Option Capture Boost:on]
+name = input-boost-on
+
+[Option Capture Boost:off]
+name = input-boost-off
+
+[Element Auto Gain Control]
+switch = select
+
+[Option Auto Gain Control:on]
+name = input-agc-on
+
+[Option Auto Gain Control:off]
+name = input-agc-off
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-chat.conf b/spa/plugins/alsa/mixer/paths/analog-output-chat.conf
new file mode 100644
index 0000000..360a1fc
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-chat.conf
@@ -0,0 +1,5 @@
+; Some gaming devices have a separate "chat" device, this is for voice chat
+; while playing games. This device is just a fairly standard analog mono
+; device, but it's nicer to make it clear that this is the "chat" device
+; as is mentioned in the marketing info and manual.
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf b/spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf
new file mode 100644
index 0000000..bda137d
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf
@@ -0,0 +1,118 @@
+# 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/>.
+
+; Path for the second headphone output on dual-headphone machines.
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 98
+
+[Properties]
+device.icon_name = audio-headphones
+
+; HP EliteDesk 800 SFF Headphone
+[Jack Front Headphone,1]
+required-any = any
+
+; HP EliteDesk 800 DM Headphone
+[Jack Front Headphone Surround]
+required-any = any
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+; This profile path is intended to control the second headphones, not
+; the first headphones. But it should not hurt if we leave the
+; headphone jack enabled nonetheless.
+[Element Headphone]
+switch = mute
+volume = zero
+
+[Element Headphone,1]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headphone+LO]
+switch = mute
+volume = zero
+
+[Element Speaker+LO]
+switch = off
+volume = off
+
+[Element Headphone2]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker]
+switch = off
+volume = off
+
+[Element Desktop Speaker]
+switch = off
+volume = off
+
+; On some machines, the Front Volume Control is shared by Headphone and Lineout,
+; or Headphone and Speaker, but they have independent Volume Switch. Here only
+; use switch to mute Lineout or Speaker.
+[Element Front]
+switch = off
+volume = zero
+
+[Element Rear]
+switch = off
+volume = off
+
+[Element Surround]
+switch = off
+volume = off
+
+[Element Side]
+switch = off
+volume = off
+
+[Element Center]
+switch = off
+volume = off
+
+[Element LFE]
+switch = off
+volume = off
+
+[Element Bass Speaker]
+switch = off
+volume = off
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-headphones.conf b/spa/plugins/alsa/mixer/paths/analog-output-headphones.conf
new file mode 100644
index 0000000..3c62c5e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-headphones.conf
@@ -0,0 +1,180 @@
+# 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/>.
+
+; Path for mixers that have a 'Headphone' control
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 99
+description-key = analog-output-headphones
+
+[Properties]
+device.icon_name = audio-headphones
+
+[Jack Dock Headphone]
+required-any = any
+
+[Jack Dock Headphone Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Front Headphone]
+required-any = any
+
+; HP EliteDesk 800 DM Headset
+[Jack Front Headphone Front]
+required-any = any
+
+[Jack Front Headphone Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Headphone]
+required-any = any
+
+[Jack Headphone Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+# This jack can be either a headphone *or* a mic. Used on some ASUS netbooks.
+[Jack Headphone Mic]
+required-any = any
+
+[Jack Headphone - Output]
+required-any = any
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+[Element Speaker+LO]
+switch = off
+volume = off
+
+[Element Headphone+LO]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headphone]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+; This path is intended to control the first headphones, not
+; the second headphones. But it should not hurt if we leave the second
+; headphone jack enabled nonetheless.
+[Element Headphone,1]
+switch = mute
+volume = zero
+
+[Element Headset]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Line HP Swap]
+switch = on
+required-any = any
+
+; This profile path is intended to control the first headphones, not
+; the second headphones. But it should not hurt if we leave the second
+; headphone jack enabled nonetheless.
+[Element Headphone2]
+switch = mute
+volume = zero
+
+[Element Speaker]
+switch = off
+volume = off
+
+[Element Desktop Speaker]
+switch = off
+volume = off
+
+; On some machines, the Front Volume Control is shared by Headphone and Lineout,
+; or Headphone and Speaker, but they have independent Volume Switch. Here only
+; use switch to mute Lineout or Speaker.
+[Element Front]
+switch = off
+volume = zero
+
+[Element Rear]
+switch = off
+volume = off
+
+[Element Surround]
+switch = off
+volume = off
+
+[Element Side]
+switch = off
+volume = off
+
+[Element Center]
+switch = off
+volume = off
+
+[Element LFE]
+switch = off
+volume = off
+
+[Element Bass Speaker]
+switch = off
+volume = off
+
+[Element Speaker Front]
+switch = off
+volume = off
+
+[Element Speaker Surround]
+switch = off
+volume = off
+
+[Element Speaker Side]
+switch = off
+volume = off
+
+[Element Speaker CLFE]
+switch = off
+volume = off
+
+[Element Speaker Center/LFE]
+switch = off
+volume = off
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-lineout.conf b/spa/plugins/alsa/mixer/paths/analog-output-lineout.conf
new file mode 100644
index 0000000..1ffce22
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-lineout.conf
@@ -0,0 +1,214 @@
+# 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/>.
+
+[General]
+priority = 90
+description-key = analog-output-lineout
+
+[Jack Line Out]
+required-any = any
+
+[Jack Line Out Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Front Line Out]
+required-any = any
+
+[Jack Front Line Out Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Rear Line Out]
+required-any = any
+
+[Jack Rear Line Out Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Front]
+required-any = any
+
+[Jack Line Out Front Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out CLFE]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out CLFE Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Surround]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Surround Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Side]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Side Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Dock Line Out]
+required-any = any
+
+[Jack Dock Line Out Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker+LO]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+required-any = any
+
+[Element Headphone+LO]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+required-any = any
+
+[Element Master Mono]
+switch = off
+volume = off
+
+[Element Line HP Swap]
+switch = off
+required-any = any
+
+; This profile path is intended to control line out, let's mute headphones
+; else there will be a spike when plugging in headphones
+[Element Headphone]
+switch = off
+volume = off
+
+[Element Headphone,1]
+switch = off
+volume = off
+
+[Element Headphone2]
+switch = off
+volume = off
+
+[Element Speaker]
+switch = off
+volume = off
+
+[Element Desktop Speaker]
+switch = off
+volume = off
+
+[Element Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Rear]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Center]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element LFE]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Center/LFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Bass Speaker]
+switch = off
+volume = off
+
+[Element Speaker Front]
+switch = off
+volume = off
+
+[Element Speaker Surround]
+switch = off
+volume = off
+
+[Element Speaker Side]
+switch = off
+volume = off
+
+[Element Speaker CLFE]
+switch = off
+volume = off
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-mono.conf b/spa/plugins/alsa/mixer/paths/analog-output-mono.conf
new file mode 100644
index 0000000..5e49405
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-mono.conf
@@ -0,0 +1,99 @@
+# 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/>.
+
+; Intended for usage on boards that have a separate Mono output plug.
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 50
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = off
+volume = off
+
+[Element Master Mono]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+; This profile path is intended to control the speaker, not the
+; headphones. But it should not hurt if we leave the headphone jack
+; enabled nonetheless.
+[Element Headphone]
+switch = mute
+volume = zero
+
+[Element Headphone,1]
+switch = mute
+volume = zero
+
+[Element Headphone+LO]
+switch = mute
+volume = zero
+
+[Element Headphone2]
+switch = mute
+volume = zero
+
+[Element Speaker]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker+LO]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Desktop Speaker]
+switch = off
+volume = off
+
+[Element Front]
+switch = off
+volume = off
+
+[Element Rear]
+switch = off
+volume = off
+
+[Element Surround]
+switch = off
+volume = off
+
+[Element Side]
+switch = off
+volume = off
+
+[Element Center]
+switch = off
+volume = off
+
+[Element LFE]
+switch = off
+volume = off
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf b/spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf
new file mode 100644
index 0000000..756afa9
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf
@@ -0,0 +1,187 @@
+# 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/>.
+
+; Path for mixers that don't have a 'Speaker' control, but where we
+; force enable the speaker paths nonetheless.
+; Needed for some older Dell laptops.
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 100
+description-key = analog-output-speaker
+
+[Properties]
+device.icon_name = audio-speakers
+
+[Jack Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Line Out Front]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Rear Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+; This profile path is intended to control the speaker, not the
+; headphones. But it should not hurt if we leave the headphone jack
+; enabled nonetheless.
+[Element Headphone]
+switch = mute
+volume = zero
+
+[Element Headphone,1]
+switch = mute
+volume = zero
+
+[Element Headphone2]
+switch = mute
+volume = zero
+
+[Element Headphone+LO]
+switch = off
+volume = off
+
+[Element Speaker+LO]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Desktop Speaker]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Front Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Rear]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Center]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element Center Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element LFE]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element LFE Speaker]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element Bass Speaker]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Center/LFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-speaker.conf b/spa/plugins/alsa/mixer/paths/analog-output-speaker.conf
new file mode 100644
index 0000000..72f928f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-speaker.conf
@@ -0,0 +1,246 @@
+# 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/>.
+
+; Path for mixers that have a 'Speaker' control
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 100
+description-key = analog-output-speaker
+
+[Properties]
+device.icon_name = audio-speakers
+
+[Jack Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Line Out Front]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Rear Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Speaker]
+required-any = any
+
+[Jack Speaker Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Speaker Front Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Speaker - Output]
+required-any = any
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+; Make sure the internal speakers are not auto-muted once the system has speakers
+[Element Auto-Mute Mode]
+enumeration = select
+
+[Option Auto-Mute Mode:Disabled]
+name = analog-output-speaker
+
+; This profile path is intended to control the speaker, let's mute headphones
+; else there will be a spike when plugging in headphones
+[Element Headphone]
+switch = off
+volume = off
+
+[Element Headphone,1]
+switch = off
+volume = off
+
+[Element Headphone2]
+switch = off
+volume = off
+
+[Element Headphone+LO]
+switch = off
+volume = off
+
+[Element Speaker+LO]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Desktop Speaker]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Front Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+required-any = any
+
+[Element Speaker Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+required-any = any
+
+[Element Rear]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+required-any = any
+
+[Element Speaker Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+required-any = any
+
+[Element Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Speaker Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Center]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element Center Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+required-any = any
+
+[Element LFE]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element LFE Speaker]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+required-any = any
+
+[Element Bass Speaker]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+required-any = any
+
+[Element CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Center/LFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Speaker CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output.conf b/spa/plugins/alsa/mixer/paths/analog-output.conf
new file mode 100644
index 0000000..0f6b5f5
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output.conf
@@ -0,0 +1,88 @@
+# 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/>.
+
+; Intended for the 'default' output. Note that a-o-speaker.conf has a
+; higher priority than this
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 99
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+[Element Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Rear]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Center]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element LFE]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Center/LFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output.conf.common b/spa/plugins/alsa/mixer/paths/analog-output.conf.common
new file mode 100644
index 0000000..028665d
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output.conf.common
@@ -0,0 +1,199 @@
+# 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/>.
+
+; Common part of all paths
+
+; So here's generally how mixer paths are used by PA: PA goes through
+; a mixer path file from top to bottom and checks if a mixer element
+; described therein exists. If so it is added to the list of mixer
+; elements PA will control, keeping the order it read them in. If a
+; mixer element described here has set the required= or
+; required-absent= directives a path might not be accepted as valid
+; and is ignored in its entirety (see below). However usually if a
+; element listed here is missing this one element is ignored but not
+; the entire path.
+;
+; When a device shall be muted/unmuted *all* elements listed in a path
+; file with "switch = mute" will be toggled.
+;
+; When a device shall change its volume, PA will got through the list
+; of all elements with "volume = merge" and set the volume on the
+; first element. If that element does not support dB volumes, this is
+; where the story ends. If it does support dB volumes, PA divides the
+; requested volume by the volume that was set on this element, and
+; then go on to the next element with "volume = merge" and then set
+; that there, and so on. That way the first volume element in the
+; path will be the one that does the 'biggest' part of the overall
+; volume adjustment, with the remaining elements usually being set to
+; some value next to 0dB. This logic makes sure we get the full range
+; over all volume sliders and a very high granularity of volumes
+; already in hardware.
+;
+; All switches and enumerations set to "select" are exposed via the
+; "port" functionality of sinks/sources. Basically every possible
+; switch setting and every possible enumeration setting will be
+; combined and made into a "port". So make sure you don't list too
+; many switches/enums for exposing, because the number of ports might
+; rise exponentially.
+;
+; Only one path can be selected at a time. All paths that are valid
+; for an audio device will be exposed as "port" for the sink/source.
+
+
+; [General]
+; type = ... # The device type. It's highly recommended to set a type for every path.
+; # See parse_type() in alsa-mixer.c for supported values.
+; priority = ... # Priority for this path
+; description-key = ... # The path description is looked up from a table in path_verify() in
+; # src/modules/alsa/alsa-mixer.c. By default the path name (i.e. the file name
+; # minus the ".conf" suffix) is used as the lookup key, but if this option is
+; # set, then the given string is used as the key instead. In any case the
+; # "description" option can be used to override the path description.
+; description = ... # Description for this path. Overrides the normal description lookup logic, as
+; # described in the "description-key" documentation above.
+; mute-during-activation = yes | no # If this path supports hardware mute, should the hw mute be used while activating this
+; # path? In some cases this can reduce extra noises during port switching, while in other
+; # cases this can increase such noises. Default: no.
+; eld-device = ... # If this is an HDMI port, set to "auto" so that PulseAudio will try to read
+; # the monitor ELD information from the ALSA mixer. By default the ELD information
+; # is not read, because it's only applicable with HDMI. Earlier the "auto" option
+; # didn't exist, and the hw device index had to be manually configured. For
+; # backwards compatibility, it's still possible to manually configure the device
+; # index using this option.
+;
+; [Properties] # Property list for this path. The list is merged into the port property list.
+; <key> = <value> # Each property is defined on its own line.
+; ...
+;
+; [Option ...:...] # For each option of an enumeration or switch element
+; # that shall be exposed as a sink/source port. Needs to
+; # be named after the Element, followed by a colon, followed
+; # by the option name, resp. on/off if the element is a switch.
+; name = ... # Logical name to use in the path identifier
+; priority = ... # Priority if this is made into a device port
+; required = ignore | enumeration | any # In this element, this option must exist or the path will be invalid. ("any" is an alias for "enumeration".)
+; required-any = ignore | enumeration | any # In this element, either this or another option must exist (or an element)
+; required-absent = ignore | enumeration | any # In this element, this option must not exist or the path will be invalid
+;
+; [Element ...] # For each element that we shall control. The "..." here is the element name,
+; # or name and index separated by a comma.
+; required = ignore | switch | volume | enumeration | any # If set, require this element to be of this kind and available,
+; # otherwise don't consider this path valid for the card
+; required-any = ignore | switch | volume | enumeration | any # If set, at least one of the elements or jacks with required-any in this
+; # path must be present, otherwise this path is invalid for the card
+; required-absent = ignore | switch | volume # If set, require this element to not be of this kind and not
+; # available, otherwise don't consider this path valid for the card
+;
+; switch = ignore | mute | off | on | select # What to do with this switch: ignore it, make it follow mute status,
+; # always set it to off, always to on, or make it selectable as port.
+; # If set to 'select' you need to define an Option section for on
+; # and off
+; volume = ignore | merge | off | zero | <volume step> # What to do with this volume: ignore it, merge it into the device
+; # volume slider, always set it to the lowest value possible, or always
+; # set it to 0 dB (for whatever that means), or always set it to
+; # <volume step> (this only makes sense in path configurations where
+; # the exact hardware and driver are known beforehand).
+; volume-limit = <volume step> # Limit the maximum volume by disabling the volume steps above <volume step>.
+; enumeration = ignore | select # What to do with this enumeration, ignore it or make it selectable
+; # via device ports. If set to 'select' you need to define an Option section
+; # for each of the items you want to expose
+; direction = playback | capture # Is this relevant only for playback or capture? If not set this will implicitly be
+; # set the direction of the PCM device is opened as. Generally this doesn't need to be set
+; # unless you have a broken driver that has playback controls marked for capture or vice
+; # versa
+; direction-try-other = no | yes # If the element does not supported what is requested, try the other direction, too?
+;
+; override-map.1 = ... # Override the channel mask of the mixer control if the control only exposes a single channel
+; override-map.2 = ... # Override the channel masks of the mixer control if the control only exposes two channels
+; # Override maps should list for each element channel which high-level channels it controls via a
+; # channel mask. A channel mask may either be the name of a single channel, or the words "all-left",
+; # "all-right", "all-center", "all-front", "all-rear", and "all" to encode a specific subset of
+; # channels in a mask
+; [Jack ...] # For each jack that we will use for jack detection
+; # The name 'Jack Foo' must match ALSA's 'Foo Jack' control.
+; required = ignore | any # If not set to ignore, make the path invalid if this jack control is not present.
+; required-absent = ignore | any # If not set to ignore, make the path invalid if this jack control is present.
+; required-any = ignore | any # If not set to ignore, make the path invalid if no jack controls and no elements with
+; # the required-any are present.
+; state.plugged = yes | no | unknown # Normally a plugged jack would mean the port becomes available, and an unplugged means it's
+; state.unplugged = yes | no | unknown # unavailable, but the port status can be overridden by specifying state.plugged and/or state.unplugged.
+; append-pcm-to-name = no | yes # Add ",pcm=N" to the jack name? N is the hw PCM device index. HDMI jacks have
+; # the PCM device index in their name, but different drivers use different
+; # numbering schemes, so we can't hardcode the full jack name in our configuration
+; # files.
+
+[Element PCM]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element External Amplifier]
+switch = select
+
+[Option External Amplifier:on]
+name = output-amplifier-on
+priority = 10
+
+[Option External Amplifier:off]
+name = output-amplifier-off
+priority = 0
+
+[Element Bass Boost]
+switch = select
+
+[Option Bass Boost:on]
+name = output-bass-boost-on
+priority = 0
+
+[Option Bass Boost:off]
+name = output-bass-boost-off
+priority = 10
+
+[Element IEC958]
+switch = off
+
+[Element IEC958 Optical Raw]
+switch = off
+
+;;; 'Analog Output'
+
+[Element Analog Output]
+enumeration = select
+
+[Option Analog Output:Speakers]
+name = output-speaker
+priority = 10
+
+[Option Analog Output:Headphones]
+name = output-headphones
+priority = 9
+
+[Option Analog Output:FP Headphones]
+name = output-headphones
+priority = 8
+
+;;; 'Output Select'
+
+[Element Output Select]
+enumeration = select
+
+[Option Output Select:Speakers]
+name = output-speaker
+priority = 10
+
+[Option Output Select:Headphone]
+name = output-headphones
+priority = 9
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf
new file mode 100644
index 0000000..bb3cec1
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort
+type = hdmi
+priority = 59
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf
new file mode 100644
index 0000000..3389a72
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 2
+type = hdmi
+priority = 58
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf
new file mode 100644
index 0000000..7607f8f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 11
+type = hdmi
+priority = 49
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf
new file mode 100644
index 0000000..316d810
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 3
+type = hdmi
+priority = 57
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf
new file mode 100644
index 0000000..0601ef7
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 4
+type = hdmi
+priority = 56
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf
new file mode 100644
index 0000000..ded155b
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 5
+type = hdmi
+priority = 55
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf
new file mode 100644
index 0000000..de31791
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 6
+type = hdmi
+priority = 54
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf
new file mode 100644
index 0000000..6d72176
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 7
+type = hdmi
+priority = 53
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf
new file mode 100644
index 0000000..d5d0771
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 8
+type = hdmi
+priority = 52
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf
new file mode 100644
index 0000000..0b8f9cd
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 9
+type = hdmi
+priority = 51
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf
new file mode 100644
index 0000000..f15797c
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 10
+type = hdmi
+priority = 50
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf b/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf
new file mode 100644
index 0000000..babc839
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf
@@ -0,0 +1,20 @@
+# 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/>.
+
+[Element PCM Capture Source]
+enumeration = select
+
+[Option PCM Capture Source:IEC958 In]
+name = iec958-input
diff --git a/spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf b/spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf
new file mode 100644
index 0000000..d47e5eb
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf
@@ -0,0 +1,18 @@
+# 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/>.
+
+
+[Element IEC958]
+switch = mute
diff --git a/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf b/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf
new file mode 100644
index 0000000..5842bfe
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf
@@ -0,0 +1,27 @@
+# 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/>.
+
+; Steelseries Arctis 5 USB headset stereo chat path. The headset has two
+; output devices. The first one is meant for voice audio, and the second
+; one meant for everything else. The purpose of this unusual design is to
+; provide separate volume controls for voice and other audio, which can be
+; useful in gaming.
+
+[General]
+priority = 50
+
+[Element Com Speaker]
+switch = mute
+volume = merge
diff --git a/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf b/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf
new file mode 100644
index 0000000..b758a6f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf
@@ -0,0 +1,27 @@
+# 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/>.
+
+; Steelseries Arctis 5 USB headset stereo game path. The headset has two
+; output devices. The first one is meant for voice audio, and the second
+; one meant for everything else. The purpose of this unusual design is to
+; provide separate volume controls for voice and other audio, which can be
+; useful in gaming.
+
+[General]
+priority = 99
+
+[Element PCM]
+switch = mute
+volume = merge
diff --git a/spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf
new file mode 100644
index 0000000..9fa7fe9
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf
@@ -0,0 +1,34 @@
+# 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/>.
+
+; USB gaming headset microphone input path. These headsets usually have two
+; output devices. The first one is mono, meant for voice audio, and the second
+; one is stereo, meant for everything else. The purpose of this unusual design
+; is to provide separate volume controls for voice and other audio, which can
+; be useful in gaming.
+;
+; Works with:
+; Steelseries Arctis 7
+; Steelseries Arctis Pro Wireless.
+; Lucidsound LS31
+
+[General]
+description-key = analog-input-microphone-headset
+
+[Element Headset]
+volume = merge
+switch = mute
+override-map.1 = all
+override-map.2 = all-left,all-right
diff --git a/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf
new file mode 100644
index 0000000..6df662f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf
@@ -0,0 +1,34 @@
+# 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/>.
+
+; USB gaming headset mono output path. These headsets usually have two
+; output devices. The first one is mono, meant for voice audio, and the second
+; one is stereo, meant for everything else. The purpose of this unusual design
+; is to provide separate volume controls for voice and other audio, which can
+; be useful in gaming.
+;
+; Works with:
+; Steelseries Arctis 7
+; Steelseries Arctis Pro Wireless.
+; Lucidsound LS31
+
+[General]
+description-key = analog-output-headphones-mono
+
+[Element PCM]
+volume = merge
+switch = mute
+override-map.1 = all
+override-map.2 = all-left,all-right
diff --git a/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf
new file mode 100644
index 0000000..1a1e794
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf
@@ -0,0 +1,34 @@
+# 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/>.
+
+; USB gaming headset mono output path. These headsets usually have two
+; output devices. The first one is mono, meant for voice audio, and the second
+; one is stereo, meant for everything else. The purpose of this unusual design
+; is to provide separate volume controls for voice and other audio, which can
+; be useful in gaming.
+;
+; Works with:
+; Steelseries Arctis 7
+; Steelseries Arctis Pro Wireless.
+; Lucidsound LS31
+
+[General]
+description-key = analog-output-headphones
+
+[Element PCM,1]
+volume = merge
+switch = mute
+override-map.1 = all
+override-map.2 = all-left,all-right
diff --git a/spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf b/spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf
new file mode 100644
index 0000000..7f111f2
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf
@@ -0,0 +1,5 @@
+[Element PCM,1]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
diff --git a/spa/plugins/alsa/mixer/profile-sets/analog-only.conf b/spa/plugins/alsa/mixer/profile-sets/analog-only.conf
new file mode 100644
index 0000000..badd5ec
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/analog-only.conf
@@ -0,0 +1,102 @@
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Some USB DACs appear to support IEC958, but don't physically have any
+; digital outputs.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+# If everything else fails, try to use hw:0 as a stereo device...
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+# ...and if even that fails, try to use hw:0 as a mono device.
+[Mapping mono-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic
+priority = 1
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping multichannel-output]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = output
+
+[Mapping multichannel-input]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = input
diff --git a/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf b/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf
new file mode 100644
index 0000000..3e42ea3
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf
@@ -0,0 +1,93 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; ASUS Xonar SE card.
+; This card has two devices for each rear and front panel jacks.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo-front]
+description = Analog Stereo Front
+device-strings = hw:%f,1
+channel-map = left,right
+paths-output = analog-output analog-output-headphones
+paths-input = analog-input-mic analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+[Mapping analog-stereo-rear]
+description = Analog Stereo Rear
+device-strings = hw:%f,0
+channel-map = left,right
+paths-output = analog-output analog-output-speaker
+paths-input = analog-input analog-input-mic analog-input-linein
+priority = 14
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
diff --git a/spa/plugins/alsa/mixer/profile-sets/audigy.conf b/spa/plugins/alsa/mixer/profile-sets/audigy.conf
new file mode 100644
index 0000000..043596e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/audigy.conf
@@ -0,0 +1,94 @@
+# 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/>.
+
+; Creative Sound Blaster Audigy product line
+;
+; These are just copies of the mappings we find in default.conf, with the
+; small change of making analog-stereo and analog-mono non-fallback mappings.
+; This is needed because these cards only support duplex profiles with mono
+; inputs, and in the default configuration, with stereo being a fallback
+; mapping, the mono mapping is never tried.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = yes
+
+# Based on stereo-fallback
+[Mapping analog-stereo]
+device-strings = hw:%f
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+# Based on mono-fallback
+[Mapping analog-mono]
+device-strings = hw:%f
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic
+priority = 1
+
+# The rest of these are identical to what's in default.conf
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-input = iec958-stereo-input
+paths-output = iec958-stereo-output
+priority = 5
diff --git a/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf b/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf
new file mode 100644
index 0000000..1b6f61c
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf
@@ -0,0 +1,66 @@
+# 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/>.
+
+# Config for CMEDIA USB2.0 High-Speed True HD Audio 147a:e055
+# Added by Jean-Philippe Guillemin <h1p8r10n@gmail.com>
+
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 10
+
+# If everything else fails, try to use hw:0 as a stereo device.
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 8
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 8
+direction = output
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 7
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = hw:%f,2 hw:%f,0
+channel-map = left,right
+paths-output = iec958-stereo-output
+paths-input = iec958-stereo-input
+priority = 5
diff --git a/spa/plugins/alsa/mixer/profile-sets/default.conf b/spa/plugins/alsa/mixer/profile-sets/default.conf
new file mode 100644
index 0000000..f0c9d2a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/default.conf
@@ -0,0 +1,580 @@
+# 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/>.
+
+; Default profile definitions for the ALSA backend of PulseAudio. This
+; is used as fallback for all cards that have no special mapping
+; assigned (and should be good enough for the vast majority of
+; cards). If you want to assign a different profile set than this one
+; to a device, either set the udev property ACP_PROFILE_SET for the
+; card, or use the "profile_set" module argument when loading
+; module-alsa-card.
+;
+; So what is this about? Simply, what we do here is map ALSA devices
+; to how they are exposed in PA. We say which ALSA device string to
+; use to open a device, which channel mapping to use then, and which
+; mixer path to use. This is encoded in a 'mapping'. Multiple of these
+; mappings can be bound together in a 'profile' which is then directly
+; exposed in the UI as a card profile. Each mapping assigned to a
+; profile will result in one sink/source to be created if the profile
+; is selected for the card.
+;
+; Additionally, the path set configuration files can describe the
+; decibel values assigned to the steps of the volume elements. This
+; can be used to work around situations when the alsa driver doesn't
+; provide any decibel information, or when the information is
+; incorrect.
+
+
+; [General]
+; auto-profiles = no | yes # Instead of defining all profiles manually, autogenerate
+; # them by combining every input mapping with every output mapping.
+;
+; [Mapping id]
+; device-strings = ... # ALSA device string. %f will be replaced by the card identifier.
+; channel-map = ... # Channel mapping to use for this device
+; description = ... # Description for the mapping. Note that it's better to set the description
+; # in the well_known_descriptions table in alsa-mixer.c than with this
+; # option, because the descriptions in alsa-mixer.c are translatable.
+; description-key = ... # A custom key for the well_known_descriptions table (by default the mapping
+; # name is used).
+; paths-input = ... # A list of mixer paths to use. Every path in this list will be probed.
+; # If multiple are found to be working they will be available as device ports
+; paths-output = ...
+; element-input = ... # Instead of configuring a full mixer path simply configure a single
+; # mixer element for volume/mute handling. The value can be an element
+; # name, or name and index separated by a comma.
+; element-output = ...
+; priority = ...
+; direction = any | input | output # Only useful for?
+;
+; exact-channels = yes | no # If no, and the exact number of channels is not supported,
+; # allow device to be opened with another channel count
+; fallback = no | yes # This mapping will only be considered if all non-fallback mappings fail
+; intended-roles = ... # Set the device.intended_roles property for the sink/source.
+;
+; [Profile id]
+; input-mappings = ... # Lists mappings for sources on this profile, those mapping must be
+; # defined in this file too
+; output-mappings = ... # Lists mappings for sinks on this profile, those mappings must be
+; # defined in this file too
+; description = ...
+; priority = ... # Numeric value to deduce priority for this profile
+; skip-probe = no | yes # Skip probing for availability? If this is yes then this profile
+; # will be assumed as working without probing. Makes initialization
+; # a bit faster but only works if the card is really known well.
+;
+; fallback = no | yes # This profile will only be considered if all non-fallback profiles fail
+; [DecibelFix element] # Decibel fixes can be used to work around missing or incorrect dB
+; # information from alsa. A decibel fix is a table that maps volume steps
+; # to decibel values for one volume element. The "element" part in the
+; # section title is the name of the volume element (or name and index
+; # separated by a comma).
+; #
+; # NOTE: This feature is meant just as a help for figuring out the correct
+; # decibel values. PulseAudio is not the correct place to maintain the
+; # decibel mappings!
+; #
+; # If you need this feature, then you should make sure that when you have
+; # the correct values figured out, the alsa driver developers get informed
+; # too, so that they can fix the driver.
+;
+; db-values = ... # The option value consists of pairs of step numbers and decibel values.
+; # The pairs are separated with whitespace, and steps are separated from
+; # the corresponding decibel values with a colon. The values must be in an
+; # increasing order. Here's an example of a valid string:
+; #
+; # "0:-40.50 1:-38.70 3:-33.00 11:0"
+; #
+; # The lowest step imposes a lower limit for hardware volume and the
+; # highest step correspondingly imposes a higher limit. That means that
+; # that the mixer will never be set outside those values - the rest of the
+; # volume scale is done using software volume.
+; #
+; # As can be seen in the example, you don't need to specify a dB value for
+; # each step. The dB values for skipped steps will be linearly interpolated
+; # using the nearest steps that are given.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+# If everything else fails, try to use hw:0 as a stereo device...
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+# ...and if even that fails, try to use hw:0 as a mono device.
+[Mapping mono-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic
+priority = 1
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-input = iec958-stereo-input
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping iec958-dts-surround-51]
+device-strings = dca:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping hdmi-stereo]
+description = Digital Stereo (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = left,right
+priority = 9
+direction = output
+
+[Mapping hdmi-surround]
+description = Digital Surround 5.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 8
+direction = output
+
+[Mapping hdmi-surround71]
+description = Digital Surround 7.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 8
+direction = output
+
+[Mapping hdmi-dts-surround]
+description = Digital Surround 5.1 (HDMI/DTS)
+device-strings = dcahdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra1]
+description = Digital Stereo (HDMI 2)
+device-strings = hdmi:%f,1
+paths-output = hdmi-output-1
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra1]
+description = Digital Surround 5.1 (HDMI 2)
+device-strings = hdmi:%f,1
+paths-output = hdmi-output-1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra1]
+description = Digital Surround 7.1 (HDMI 2)
+device-strings = hdmi:%f,1
+paths-output = hdmi-output-1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra1]
+description = Digital Surround 5.1 (HDMI 2/DTS)
+device-strings = dcahdmi:%f,1
+paths-output = hdmi-output-1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra2]
+description = Digital Stereo (HDMI 3)
+device-strings = hdmi:%f,2
+paths-output = hdmi-output-2
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra2]
+description = Digital Surround 5.1 (HDMI 3)
+device-strings = hdmi:%f,2
+paths-output = hdmi-output-2
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra2]
+description = Digital Surround 7.1 (HDMI 3)
+device-strings = hdmi:%f,2
+paths-output = hdmi-output-2
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra2]
+description = Digital Surround 5.1 (HDMI 3/DTS)
+device-strings = dcahdmi:%f,2
+paths-output = hdmi-output-2
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra3]
+description = Digital Stereo (HDMI 4)
+device-strings = hdmi:%f,3
+paths-output = hdmi-output-3
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra3]
+description = Digital Surround 5.1 (HDMI 4)
+device-strings = hdmi:%f,3
+paths-output = hdmi-output-3
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra3]
+description = Digital Surround 7.1 (HDMI 4)
+device-strings = hdmi:%f,3
+paths-output = hdmi-output-3
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra3]
+description = Digital Surround 5.1 (HDMI 4/DTS)
+device-strings = dcahdmi:%f,3
+paths-output = hdmi-output-3
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra4]
+description = Digital Stereo (HDMI 5)
+device-strings = hdmi:%f,4
+paths-output = hdmi-output-4
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra4]
+description = Digital Surround 5.1 (HDMI 5)
+device-strings = hdmi:%f,4
+paths-output = hdmi-output-4
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra4]
+description = Digital Surround 7.1 (HDMI 5)
+device-strings = hdmi:%f,4
+paths-output = hdmi-output-4
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra4]
+description = Digital Surround 5.1 (HDMI 5/DTS)
+device-strings = dcahdmi:%f,4
+paths-output = hdmi-output-4
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra5]
+description = Digital Stereo (HDMI 6)
+device-strings = hdmi:%f,5
+paths-output = hdmi-output-5
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra5]
+description = Digital Surround 5.1 (HDMI 6)
+device-strings = hdmi:%f,5
+paths-output = hdmi-output-5
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra5]
+description = Digital Surround 7.1 (HDMI 6)
+device-strings = hdmi:%f,5
+paths-output = hdmi-output-5
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra5]
+description = Digital Surround 5.1 (HDMI 6/DTS)
+device-strings = dcahdmi:%f,5
+paths-output = hdmi-output-5
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra6]
+description = Digital Stereo (HDMI 7)
+device-strings = hdmi:%f,6
+paths-output = hdmi-output-6
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra6]
+description = Digital Surround 5.1 (HDMI 7)
+device-strings = hdmi:%f,6
+paths-output = hdmi-output-6
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra6]
+description = Digital Surround 7.1 (HDMI 7)
+device-strings = hdmi:%f,6
+paths-output = hdmi-output-6
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra6]
+description = Digital Surround 5.1 (HDMI 7/DTS)
+device-strings = dcahdmi:%f,6
+paths-output = hdmi-output-6
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra7]
+description = Digital Stereo (HDMI 8)
+device-strings = hdmi:%f,7
+paths-output = hdmi-output-7
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra7]
+description = Digital Surround 5.1 (HDMI 8)
+device-strings = hdmi:%f,7
+paths-output = hdmi-output-7
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra7]
+description = Digital Surround 7.1 (HDMI 8)
+device-strings = hdmi:%f,7
+paths-output = hdmi-output-7
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra7]
+description = Digital Surround 5.1 (HDMI 8/DTS)
+device-strings = dcahdmi:%f,7
+paths-output = hdmi-output-7
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra8]
+description = Digital Stereo (HDMI 9)
+device-strings = hdmi:%f,8
+paths-output = hdmi-output-8
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra8]
+description = Digital Surround 5.1 (HDMI 9)
+device-strings = hdmi:%f,8
+paths-output = hdmi-output-8
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra8]
+description = Digital Surround 7.1 (HDMI 9)
+device-strings = hdmi:%f,8
+paths-output = hdmi-output-8
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra8]
+description = Digital Surround 5.1 (HDMI 9/DTS)
+device-strings = dcahdmi:%f,8
+paths-output = hdmi-output-8
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra9]
+description = Digital Stereo (HDMI 10)
+device-strings = hdmi:%f,9
+paths-output = hdmi-output-9
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra9]
+description = Digital Surround 5.1 (HDMI 10)
+device-strings = hdmi:%f,9
+paths-output = hdmi-output-9
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra9]
+description = Digital Surround 7.1 (HDMI 10)
+device-strings = hdmi:%f,9
+paths-output = hdmi-output-9
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra9]
+description = Digital Surround 5.1 (HDMI 10/DTS)
+device-strings = dcahdmi:%f,9
+paths-output = hdmi-output-9
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra10]
+description = Digital Stereo (HDMI 11)
+device-strings = hdmi:%f,10
+paths-output = hdmi-output-10
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra10]
+description = Digital Surround 5.1 (HDMI 11)
+device-strings = hdmi:%f,10
+paths-output = hdmi-output-10
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra10]
+description = Digital Surround 7.1 (HDMI 11)
+device-strings = hdmi:%f,10
+paths-output = hdmi-output-10
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra10]
+description = Digital Surround 5.1 (HDMI 11/DTS)
+device-strings = dcahdmi:%f,10
+paths-output = hdmi-output-10
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping multichannel-output]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = output
+
+[Mapping multichannel-input]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = input
+
+; An example for defining multiple-sink profiles
+#[Profile output:analog-stereo+output:iec958-stereo+input:analog-stereo]
+#description = Foobar
+#output-mappings = analog-stereo iec958-stereo
+#input-mappings = analog-stereo
diff --git a/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf b/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf
new file mode 100644
index 0000000..1186552
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf
@@ -0,0 +1,55 @@
+# 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/>.
+
+; Dell Dock TB16 USB audio
+;
+; This card has two stereo pairs of output, One Mono input.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-headphone]
+description = Headphone
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-speaker]
+description = Speaker
+device-strings = hw:%f,1,0
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-mic]
+description = Headset-Mic
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = input
+
+
+[Profile output:analog-stereo-speaker]
+description = Speaker
+output-mappings = analog-stereo-speaker
+priority = 60
+skip-probe = yes
+
+[Profile output:analog-stereo-headphone+input:analog-stereo-mic]
+description = Headset
+output-mappings = analog-stereo-headphone
+input-mappings = analog-stereo-mic
+priority = 80
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf b/spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf
new file mode 100644
index 0000000..41924f4
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf
@@ -0,0 +1,153 @@
+# 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/>.
+
+; This profile forces speaker and internal mic ports even if we have no way
+; of identifying those.
+; See default.conf for explanations.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-mono]
+device-strings = hw:%f
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker-always analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic-always analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line
+priority = 1
+
+[Mapping analog-stereo]
+device-strings = front:%f hw:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker-always analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic-always analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line
+priority = 10
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-4-channel-input]
+# Alsa doesn't currently provide any better device name than "hw" for 4-channel
+# input. If this causes trouble at some point, then we will need to get a new
+# device name standardized in alsa.
+device-strings = hw:%f
+channel-map = aux0,aux1,aux2,aux3
+priority = 1
+direction = input
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-input = iec958-stereo-input
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping iec958-dts-surround-51]
+device-strings = dca:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping hdmi-stereo]
+description = Digital Stereo (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = left,right
+priority = 4
+direction = output
+
+[Mapping hdmi-surround]
+description = Digital Surround 5.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 3
+direction = output
+
+[Mapping hdmi-surround71]
+description = Digital Surround 7.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 3
+direction = output
+
+[Mapping hdmi-dts-surround]
+description = Digital Surround 5.1 (HDMI/DTS)
+device-strings = dcahdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+; An example for defining multiple-sink profiles
+#[Profile output:analog-stereo+output:iec958-stereo+input:analog-stereo]
+#description = Foobar
+#output-mappings = analog-stereo iec958-stereo
+#input-mappings = analog-stereo
diff --git a/spa/plugins/alsa/mixer/profile-sets/force-speaker.conf b/spa/plugins/alsa/mixer/profile-sets/force-speaker.conf
new file mode 100644
index 0000000..dec57d5
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/force-speaker.conf
@@ -0,0 +1,152 @@
+# 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/>.
+
+; This profile forces a speaker port even if we have no way of identifying it.
+; See default.conf for explanations.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-mono]
+device-strings = hw:%f
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker-always analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line
+priority = 1
+
+[Mapping analog-stereo]
+device-strings = front:%f hw:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker-always analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line
+priority = 10
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-4-channel-input]
+# Alsa doesn't currently provide any better device name than "hw" for 4-channel
+# input. If this causes trouble at some point, then we will need to get a new
+# device name standardized in alsa.
+device-strings = hw:%f
+channel-map = aux0,aux1,aux2,aux3
+priority = 1
+direction = input
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-input = iec958-stereo-input
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping iec958-dts-surround-51]
+device-strings = dca:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping hdmi-stereo]
+description = Digital Stereo (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = left,right
+priority = 4
+direction = output
+
+[Mapping hdmi-surround]
+description = Digital Surround 5.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 3
+direction = output
+
+[Mapping hdmi-surround71]
+description = Digital Surround 7.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 3
+direction = output
+
+[Mapping hdmi-dts-surround]
+description = Digital Surround 5.1 (HDMI/DTS)
+device-strings = dcahdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+; An example for defining multiple-sink profiles
+#[Profile output:analog-stereo+output:iec958-stereo+input:analog-stereo]
+#description = Foobar
+#output-mappings = analog-stereo iec958-stereo
+#input-mappings = analog-stereo
diff --git a/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf b/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf
new file mode 100644
index 0000000..a683a4e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf
@@ -0,0 +1,35 @@
+# 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/>.
+
+; HP Thunderbolt Dock 120W G2
+;
+; This dock has a 3.5mm headset connector. Both input and output are stereo.
+;
+; There's a separate speakerphone module called "HP Thunderbolt Dock Audio
+; Module", which can be attached to this dock. The module will appear in ALSA
+; as a separate USB sound card, configuration for it is in
+; hp-tbt-dock-audio-module.conf.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-headset]
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Profile output:analog-stereo-headset+input:analog-stereo-headset]
+output-mappings = analog-stereo-headset
+input-mappings = analog-stereo-headset
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf b/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf
new file mode 100644
index 0000000..692ab8d
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf
@@ -0,0 +1,36 @@
+# 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/>.
+
+; HP Thunderbolt Dock Audio Module
+;
+; This device attaches to the "HP Thunderbolt Dock 120W G2" dock. The audio
+; module provides a speakerphone with echo cancellation and appears in ALSA as
+; a USB sound card with stereo input and output.
+;
+; The dock itself has a 3.5mm headset connector and appears as a separate USB
+; sound card, configuration for it is in hp-tbt-dock-120w-g2.conf.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-speakerphone]
+device-strings = hw:%f,0,0
+channel-map = left,right
+intended-roles = phone
+
+[Profile output:analog-stereo-speakerphone+input:analog-stereo-speakerphone]
+output-mappings = analog-stereo-speakerphone
+input-mappings = analog-stereo-speakerphone
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf b/spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf
new file mode 100644
index 0000000..d51fd17
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf
@@ -0,0 +1,38 @@
+# 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/>.
+
+; Audio profile for the Microsoft Kinect Sensor device in UAC mode.
+;
+; Copyright (C) 2011 Antonio Ospite <ospite@studenti.unina.it>
+;
+; This device has an array of four microphones, and no playback capability.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping input-4-channels]
+device-strings = hw:%f
+channel-map = front-left,front-right,rear-left,rear-right
+description = 4 Channels Input
+direction = input
+priority = 5
+
+[Profile input:mic-array]
+description = Microphone Array
+input-mappings = input-4-channels
+priority = 2
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf b/spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf
new file mode 100644
index 0000000..5122907
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf
@@ -0,0 +1,86 @@
+# 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/>.
+
+; M-Audio FastTrack Pro
+;
+; This card has one duplex stereo channel called A and an additional
+; stereo output channel called B.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-a-output]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = output
+
+; Try both device 0 and device 1 for input, see
+; http://mailman.alsa-project.org/pipermail/alsa-devel/2012-March/050701.html
+[Mapping analog-stereo-a-input]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0 hw:%f,1,0
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-b-output]
+description = Analog Stereo Channel B
+device-strings = hw:%f,1,0
+channel-map = left,right
+direction = output
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channel A, Analog Stereo output Channel B
+output-mappings = analog-stereo-a-output analog-stereo-b-output
+input-mappings = analog-stereo-a-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-a-output+input:analog-stereo-a-input]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-a-output
+input-mappings = analog-stereo-a-input
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Output Channel B
+output-mappings = analog-stereo-b-output
+input-mappings =
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-a]
+description = Analog Stereo Output Channel A
+output-mappings = analog-stereo-a-output
+priority = 5
+skip-probe = yes
+
+[Profile output:analog-stereo-b]
+description = Analog Stereo Output Channel B
+output-mappings = analog-stereo-b-output
+priority = 6
+skip-probe = yes
+
+[Profile input:analog-stereo-a]
+description = Analog Stereo Input Channel A
+input-mappings = analog-stereo-a-input
+priority = 2
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf
new file mode 100644
index 0000000..f7cbc15
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf
@@ -0,0 +1,90 @@
+# 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/>.
+
+; Native Instruments Audio 4 DJ
+;
+; This card has two stereo pairs of input and two stereo pairs of
+; output, named channels A and B. Channel B has an additional
+; Headphone connector.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-b-output]
+description = Analog Stereo Channel B (Headphones)
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-b-input]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channels A, B (Headphones)
+output-mappings = analog-stereo-a analog-stereo-b-output
+input-mappings = analog-stereo-a analog-stereo-b-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-a]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-a
+input-mappings = analog-stereo-a
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Duplex Channel B (Headphones)
+output-mappings = analog-stereo-b-output
+input-mappings = analog-stereo-b-input
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-a]
+description = Analog Stereo Output Channel A
+output-mappings = analog-stereo-a
+priority = 5
+skip-probe = yes
+
+[Profile output:analog-stereo-b]
+description = Analog Stereo Output Channel B (Headphones)
+output-mappings = analog-stereo-b-output
+priority = 6
+skip-probe = yes
+
+[Profile input:analog-stereo-a]
+description = Analog Stereo Input Channel A
+input-mappings = analog-stereo-a
+priority = 2
+skip-probe = yes
+
+[Profile input:analog-stereo-b]
+description = Analog Stereo Input Channel B
+input-mappings = analog-stereo-b-input
+priority = 1
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf
new file mode 100644
index 0000000..dc1b780
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf
@@ -0,0 +1,161 @@
+# 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/>.
+
+; Native Instruments Audio 8 DJ
+;
+; This card has four stereo pairs of input and four stereo pairs of
+; output, named channels A to D. Channel C has an additional Mic/Line
+; connector, channel D an additional Headphone connector.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+
+# Since we want to set a different description for channel C's/D's input
+# and output we define two separate mappings for them
+[Mapping analog-stereo-c-output]
+description = Analog Stereo Channel C
+device-strings = hw:%f,0,2
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-c-input]
+description = Analog Stereo Channel C (Line/Mic)
+device-strings = hw:%f,0,2
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-d-output]
+description = Analog Stereo Channel D (Headphones)
+device-strings = hw:%f,0,3
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-d-input]
+description = Analog Stereo Channel D
+device-strings = hw:%f,0,3
+channel-map = left,right
+direction = input
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channels A, B, C (Line/Mic), D (Headphones)
+output-mappings = analog-stereo-a analog-stereo-b analog-stereo-c-output analog-stereo-d-output
+input-mappings = analog-stereo-a analog-stereo-b analog-stereo-c-input analog-stereo-d-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-d+input:analog-stereo-c]
+description = Analog Stereo Channel D (Headphones) Output, Channel C (Line/Mic) Input
+output-mappings = analog-stereo-d-output
+input-mappings = analog-stereo-c-input
+priority = 90
+skip-probe = yes
+
+[Profile output:analog-stereo-c-d+input:analog-stereo-c-d]
+description = Analog Stereo Duplex Channels C (Line/Mic), D (Line/Mic)
+output-mappings = analog-stereo-c-output analog-stereo-d-output
+input-mappings = analog-stereo-c-input analog-stereo-d-input
+priority = 80
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-a]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-a
+input-mappings = analog-stereo-a
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Duplex Channel B
+output-mappings = analog-stereo-b
+input-mappings = analog-stereo-b
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-c+input:analog-stereo-c]
+description = Analog Stereo Duplex Channel C (Line/Mic)
+output-mappings = analog-stereo-c-output
+input-mappings = analog-stereo-c-input
+priority = 60
+skip-probe = yes
+
+[Profile output:analog-stereo-d+input:analog-stereo-d]
+description = Analog Stereo Duplex Channel D (Headphones)
+output-mappings = analog-stereo-d-output
+input-mappings = analog-stereo-d-input
+priority = 70
+skip-probe = yes
+
+[Profile output:analog-stereo-a]
+description = Analog Stereo Output Channel A
+output-mappings = analog-stereo-a
+priority = 6
+skip-probe = yes
+
+[Profile output:analog-stereo-b]
+description = Analog Stereo Output Channel B
+output-mappings = analog-stereo-b
+priority = 5
+skip-probe = yes
+
+[Profile output:analog-stereo-c]
+description = Analog Stereo Output Channel C
+output-mappings = analog-stereo-c-output
+priority = 7
+skip-probe = yes
+
+[Profile output:analog-stereo-d]
+description = Analog Stereo Output Channel D (Headphones)
+output-mappings = analog-stereo-d-output
+priority = 8
+skip-probe = yes
+
+[Profile input:analog-stereo-a]
+description = Analog Stereo Input Channel A
+input-mappings = analog-stereo-a
+priority = 2
+skip-probe = yes
+
+[Profile input:analog-stereo-b]
+description = Analog Stereo Input Channel B
+input-mappings = analog-stereo-b
+priority = 1
+skip-probe = yes
+
+[Profile input:analog-stereo-c]
+description = Analog Stereo Input Channel C (Line/Mic)
+input-mappings = analog-stereo-c-input
+priority = 4
+skip-probe = yes
+
+[Profile input:analog-stereo-d]
+description = Analog Stereo Input Channel D
+input-mappings = analog-stereo-d-input
+priority = 3
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.conf
new file mode 100644
index 0000000..60db1dd
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.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/>.
+
+; Native Instruments Komplete Audio 6
+;
+; This card has three stereo pairs of input and three stereo pairs of
+; output.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-out-ab]
+description = Analog Stereo 1/2
+device-strings = hw:%f,0,0
+channel-map = left,right,aux0,aux1,aux2,aux3
+direction = output
+
+[Mapping analog-stereo-out-cd]
+description = Analog Stereo 3/4
+device-strings = hw:%f,0,0
+channel-map = aux0,aux1,left,right,aux2,aux3
+direction = output
+
+[Mapping stereo-out-ef]
+description = Stereo 5/6 (S/PDIF)
+device-strings = hw:%f,0,0
+channel-map = aux0,aux1,aux2,aux3,left,right
+direction = output
+
+[Mapping analog-mono-in-a]
+description = Analog Mono Input 1
+device-strings = hw:%f,0,0
+channel-map = mono,aux0,aux1,aux2,aux3,aux4
+direction = input
+
+[Mapping analog-mono-in-b]
+description = Analog Mono Input 2
+device-strings = hw:%f,0,0
+channel-map = aux0,mono,aux1,aux2,aux3,aux4
+direction = input
+
+[Mapping analog-stereo-in-ab]
+description = Analog Stereo Input 1/2
+device-strings = hw:%f,0,0
+channel-map = left,right,aux0,aux1,aux2,aux3
+direction = input
+
+[Mapping analog-stereo-in-cd]
+description = Analog Stereo Input 3/4
+device-strings = hw:%f,0,0
+channel-map = aux0,aux1,left,right,aux2,aux3
+direction = input
+
+[Mapping stereo-in-ef]
+description = Stereo Input 5/6 (S/PDIF)
+device-strings = hw:%f,0,0
+channel-map = aux0,aux1,aux2,aux3,left,right
+direction = input
+
+[Profile output:analog-stereo-out-ab+input:analog-stereo-in-ab]
+description = Analog Stereo Output 1/2, Analog Stereo Input 1/2
+output-mappings = analog-stereo-out-ab
+input-mappings = analog-stereo-in-ab
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-out-ab+input:analog-mono-in-a]
+description = Analog Stereo Output 1/2, Analog Mono Input 1
+output-mappings = analog-stereo-out-ab
+input-mappings = analog-mono-in-a
+priority = 95
+skip-probe = yes
+
+[Profile output:analog-stereo-out-ab+input:analog-mono-in-b]
+description = Analog Stereo Output 1/2, Analog Mono Input 2
+output-mappings = analog-stereo-out-ab
+input-mappings = analog-mono-in-b
+priority = 90
+skip-probe = yes
+
+[Profile output:analog-stereo-out-cd+input:analog-stereo-in-cd]
+description = Analog Stereo Output 3/4, Analog Stereo Input 3/4
+output-mappings = analog-stereo-out-cd
+input-mappings = analog-stereo-in-cd
+priority = 80
+skip-probe = yes
+
+[Profile output:stereo-out-ef+input:stereo-in-ef]
+description = Stereo Output 5/6 (S/PDIF), Stereo Input 5/6 (S/PDIF)
+output-mappings = stereo-out-ef
+input-mappings = stereo-in-ef
+priority = 70
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf
new file mode 100644
index 0000000..35b3d06
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf
@@ -0,0 +1,84 @@
+# 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/>.
+
+; Native Instruments Kore Controller
+;
+; This card has one stereo pairs of input and two stereo pairs of
+; output, named "Master" and "Headphone". The master channel has
+; an additional Coax S/PDIF connector which is always on.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-master-out]
+description = Analog Stereo Master Channel
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-headphone-out]
+description = Analog Stereo Headphone Channel
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-input]
+description = Analog Stereo
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = input
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Master Output, Headphones Output
+output-mappings = analog-stereo-master-out analog-stereo-headphone-out
+input-mappings = analog-stereo-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-master+input:analog-stereo-input]
+description = Analog Stereo Duplex Master Output
+output-mappings = analog-stereo-master-out
+input-mappings = analog-stereo-input
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-headphone-out+input:analog-stereo-input]
+description = Analog Stereo Headphones Output
+output-mappings = analog-stereo-headphone-out
+input-mappings = analog-stereo-input
+priority = 30
+skip-probe = yes
+
+[Profile output:analog-stereo-master]
+description = Analog Stereo Master Output
+output-mappings = analog-stereo-master-out
+priority = 3
+skip-probe = yes
+
+[Profile output:analog-stereo-headphone]
+description = Analog Stereo Headphones Output
+output-mappings = analog-stereo-headphone-out
+priority = 2
+skip-probe = yes
+
+[Profile input:analog-stereo-input]
+description = Analog Stereo Input
+input-mappings = analog-stereo-input
+priority = 1
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf
new file mode 100644
index 0000000..c210297
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf
@@ -0,0 +1,130 @@
+# 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/>.
+
+; Native Instruments Audio 10 DJ
+;
+; This card has five stereo pairs of input and five stereo pairs of
+; output
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-out-main]
+description = Analog Stereo Main
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-out-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-out-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-out-c]
+description = Analog Stereo Channel C
+device-strings = hw:%f,0,2
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-out-d]
+description = Analog Stereo Channel D
+device-strings = hw:%f,0,3
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-in-main]
+description = Analog Stereo Main
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-in-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-in-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-in-c]
+description = Analog Stereo Channel C
+device-strings = hw:%f,0,2
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-in-d]
+description = Analog Stereo Channel D
+device-strings = hw:%f,0,3
+channel-map = left,right
+direction = input
+
+
+
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channels Main, A, B, C, D
+output-mappings = analog-stereo-out-main analog-stereo-out-a analog-stereo-out-b analog-stereo-out-c analog-stereo-out-d
+input-mappings = analog-stereo-in-main analog-stereo-in-a analog-stereo-in-b analog-stereo-in-c analog-stereo-in-d
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-main+input:analog-stereo-main]
+description = Analog Stereo Duplex Main
+output-mappings = analog-stereo-out-main
+input-mappings = analog-stereo-in-main
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-a]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-out-a
+input-mappings = analog-stereo-in-a
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Duplex Channel B
+output-mappings = analog-stereo-out-b
+input-mappings = analog-stereo-in-b
+priority = 30
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-c]
+description = Analog Stereo Duplex Channel C
+output-mappings = analog-stereo-out-c
+input-mappings = analog-stereo-in-c
+priority = 20
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-d]
+description = Analog Stereo Duplex Channel D
+output-mappings = analog-stereo-out-d
+input-mappings = analog-stereo-in-d
+priority = 10
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf
new file mode 100644
index 0000000..145dace
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf
@@ -0,0 +1,53 @@
+# 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/>.
+
+; Native Instruments Traktor Audio 2
+;
+; This card has two stereo pairs of output.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Profile output:analog-stereo-a]
+description = Analog Stereo Output Channel A
+output-mappings = analog-stereo-a
+priority = 60
+skip-probe = yes
+
+[Profile output:analog-stereo-b]
+description = Analog Stereo Output Channel B
+output-mappings = analog-stereo-b
+priority = 50
+skip-probe = yes
+
+[Profile analog-stereo-all]
+description = Analog Stereo Output Channels A & B
+output-mappings = analog-stereo-a analog-stereo-b
+priority = 100
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf
new file mode 100644
index 0000000..a08e9fc
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf
@@ -0,0 +1,91 @@
+# 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/>.
+
+; Native Instruments Audio 6 DJ
+;
+; This card has three stereo pairs of input and three stereo pairs of
+; output
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-out-main]
+description = Analog Stereo Main
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-out-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-out-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-in-main]
+description = Analog Stereo Main
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-in-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-in-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channels A, B (Headphones)
+output-mappings = analog-stereo-out-main analog-stereo-out-a analog-stereo-out-b
+input-mappings = analog-stereo-in-main analog-stereo-in-a analog-stereo-in-b
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-main+input:analog-stereo-main]
+description = Analog Stereo Duplex Channel Main
+output-mappings = analog-stereo-out-main
+input-mappings = analog-stereo-in-main
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-a]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-out-a
+input-mappings = analog-stereo-in-a
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Duplex Channel B
+output-mappings = analog-stereo-out-b
+input-mappings = analog-stereo-in-b
+priority = 30
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf
new file mode 100644
index 0000000..934965f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf
@@ -0,0 +1,80 @@
+# 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/>.
+
+; Native Instruments Traktor Kontrol S4
+;
+; This controller has two stereo pairs of input (named "Channel C" and
+; "Channel D") and two stereo pairs of output, one "Main Out" and
+; "Headphone Out".
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-output-main]
+description = Analog Stereo Main Out
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-output-headphone]
+description = Analog Stereo Headphones Out
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-c-input]
+description = Analog Stereo Channel C
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-d-input]
+description = Analog Stereo Channel D
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex
+output-mappings = analog-stereo-output-main analog-stereo-output-headphone
+input-mappings = analog-stereo-c-input analog-stereo-d-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-main]
+description = Analog Stereo Main Output
+output-mappings = analog-stereo-output-main
+priority = 4
+skip-probe = yes
+
+[Profile output:analog-stereo-headphone]
+description = Analog Stereo Output Headphones Out
+output-mappings = analog-stereo-output-headphone
+priority = 3
+skip-probe = yes
+
+[Profile input:analog-stereo-c]
+description = Analog Stereo Input Channel C
+input-mappings = analog-stereo-c-input
+priority = 2
+skip-probe = yes
+
+[Profile input:analog-stereo-d]
+description = Analog Stereo Input Channel D
+input-mappings = analog-stereo-d-input
+priority = 1
+skip-probe = yes
+
diff --git a/spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf b/spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf
new file mode 100644
index 0000000..d5d1d65
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf
@@ -0,0 +1,112 @@
+# 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/>.
+
+; Creative Sound Blaster Omni Surround 5.1
+;
+; This config supports Linux 4.3-rc1+.
+; By default there are some non-existing (physically) inputs and outputs that
+; are not present in this config.
+; Also in addition to natively supported modes (such as stereo, 5.1 and stereo
+; S/PDIF) following useful output modes are added: 2.1, 4.0, 4.1 and 5.0.
+;
+; NOTE: in 2.1 and 4.1 physical LFE output will be different than in 5.1 mode.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-input]
+device-strings = hw:%f
+channel-map = left,right
+paths-input = analog-input-mic analog-input-linein
+direction = input
+
+[Mapping analog-stereo-output]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-21]
+device-strings = surround51:%f
+channel-map = front-left,front-right,aux1,aux2,aux3,lfe
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,aux1,lfe
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-output = iec958-stereo-output
+direction = output
+
+[Profile output:analog-stereo-output+input:analog-stereo-input]
+output-mappings = analog-stereo-output
+input-mappings = analog-stereo-input
+priority = 7
+
+[Profile output:analog-surround-21+input:analog-stereo-input]
+output-mappings = analog-surround-21
+input-mappings = analog-stereo-input
+priority = 6
+
+[Profile output:analog-surround-40+input:analog-stereo-input]
+output-mappings = analog-surround-40
+input-mappings = analog-stereo-input
+priority = 5
+
+[Profile output:analog-surround-41+input:analog-stereo-input]
+output-mappings = analog-surround-41
+input-mappings = analog-stereo-input
+priority = 4
+
+[Profile output:analog-surround-50+input:analog-stereo-input]
+output-mappings = analog-surround-50
+input-mappings = analog-stereo-input
+priority = 3
+
+[Profile output:analog-surround-51+input:analog-stereo-input]
+output-mappings = analog-surround-51
+input-mappings = analog-stereo-input
+priority = 2
+
+[Profile output:iec958-stereo+input:analog-stereo-input]
+output-mappings = iec958-stereo
+input-mappings = analog-stereo-input
+priority = 1
diff --git a/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf b/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf
new file mode 100644
index 0000000..0ac1576
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf
@@ -0,0 +1,58 @@
+# 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/>.
+
+; USB Gaming DAC.
+; These devices have two output devices. The first one is mono, meant for
+; voice audio, and the second one is 7.1 surround, meant for everything
+; else. The 7.1 surround is mapped to headphones within the device.
+; The purpose of the mono/7.1 design is to provide separate volume
+; controls for voice and other audio, which can be useful in gaming.
+;
+; Works with:
+; Sennheiser GSX 1000
+; Sennheiser GSX 1200
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-chat-output]
+device-strings = hw:%f,0
+channel-map = mono
+paths-output = analog-chat-output
+direction = output
+priority = 4000
+intended-roles = phone
+
+[Mapping analog-output-surround71]
+device-strings = hw:%f,1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+paths-output = virtual-surround-7.1
+priority = 4100
+direction = output
+
+[Mapping analog-chat-input]
+device-strings = hw:%f,0
+channel-map = mono
+paths-input = analog-chat-input
+priority = 4100
+direction = input
+
+[Profile output:analog-output-surround71+output:analog-output-chat+input:analog-input]
+output-mappings = analog-output-surround71 analog-chat-output
+input-mappings = analog-chat-input
+priority = 5100
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf b/spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf
new file mode 100644
index 0000000..809d015
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf
@@ -0,0 +1,42 @@
+# 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/>.
+
+; This is a profile meant for simple (stereo + mic) headphones.
+; default.conf also works but using this one will hide some profiles
+; that don't make sense like IEC958 and multichannel inputs.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+# If everything else fails, try to use hw:0 as a stereo device...
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+[Mapping analog-mono]
+device-strings = hw:%f,0,0
+channel-map = mono
+direction = input
diff --git a/spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf b/spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf
new file mode 100644
index 0000000..0c58917
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf
@@ -0,0 +1,23 @@
+[General]
+auto-profiles = yes
+
+[Mapping analog-chat]
+description-key = gaming-headset-chat
+device-strings = hw:%f,0,0
+channel-map = left,right
+paths-input = analog-input-mic
+paths-output = steelseries-arctis-output-chat-common
+intended-roles = phone
+
+[Mapping analog-game]
+description-key = gaming-headset-game
+device-strings = hw:%f,1,0
+channel-map = left,right
+paths-output = steelseries-arctis-output-game-common
+direction = output
+
+[Profile output:analog-chat+output:analog-game+input:analog-chat]
+output-mappings = analog-chat analog-game
+input-mappings = analog-chat
+priority = 5100
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf b/spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf
new file mode 100644
index 0000000..3c2b398
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf
@@ -0,0 +1,75 @@
+# 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/>.
+
+; Texas Instruments PCM2902
+;
+; This is a generic chip used in multiple products, including at least
+; Behringer U-Phoria UMC22, Behringer Xenyx 302USB, Intopic Jazz-UB700 and
+; some unbranded "usb mini microphone".
+;
+; Behringer UMC22 has stereo input (representing two physical mono inputs),
+; others have mono input.
+;
+; Some devices have a mic input path, but at least Behringer Xenyx 302USB
+; doesn't have any input mixer controls.
+;
+; Since the UMC22 card has only stereo input PCM device but is commonly used
+; with mono mics, we define special mono mappings using "mono,aux1" and
+; "aux1,mono" channel maps. If we had only had the standard stereo input
+; mapping, the user would have to record stereo tracks with one channel silent,
+; which would be inconvenient.
+;
+; This config also removes default digital input/output mappings that do
+; not physically exist on cards that we've seen so far.
+;
+; Originally added by Nazar Mokrynskyi <nazar@mokrynskyi.com> for Behringer
+; UMC22.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo-input]
+device-strings = hw:%f
+channel-map = left,right
+paths-input = analog-input-mic analog-input
+direction = input
+priority = 4
+
+[Mapping analog-mono]
+device-strings = hw:%f
+channel-map = mono
+paths-input = analog-input-mic analog-input
+direction = input
+priority = 3
+
+[Mapping analog-mono-left]
+device-strings = hw:%f
+channel-map = mono,aux1
+paths-input = analog-input-mic analog-input
+direction = input
+priority = 2
+
+[Mapping analog-mono-right]
+device-strings = hw:%f
+channel-map = aux1,mono
+paths-input = analog-input-mic analog-input
+direction = input
+priority = 1
+
+[Mapping analog-stereo-output]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output
+direction = output
diff --git a/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf b/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf
new file mode 100644
index 0000000..adda54d
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf
@@ -0,0 +1,64 @@
+# 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/>.
+
+; USB gaming headset.
+; These headsets usually have two output devices. The first one is meant
+; for voice audio, and the second one is meant for everything else.
+; The purpose of this unusual design is to provide separate volume
+; controls for voice and other audio, which can be useful in gaming.
+;
+; Works with:
+; Steelseries Arctis 7
+; Steelseries Arctis Pro Wireless.
+; Lucidsound LS31
+; Astro A50
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = yes
+
+[Mapping mono-chat]
+description-key = gaming-headset-chat
+device-strings = hw:%f,0,0
+channel-map = mono
+paths-output = usb-gaming-headset-output-mono
+paths-input = usb-gaming-headset-input
+intended-roles = phone
+
+[Mapping stereo-chat]
+description-key = gaming-headset-chat
+device-strings = hw:%f,0,0
+channel-map = left,right
+paths-output = usb-gaming-headset-output-stereo
+paths-input = usb-gaming-headset-input
+intended-roles = phone
+
+[Mapping stereo-game]
+description-key = gaming-headset-game
+device-strings = hw:%f,1,0
+channel-map = left,right
+paths-output = usb-gaming-headset-output-stereo
+direction = output
+
+[Profile output:mono-chat+output:stereo-game+input:mono-chat]
+output-mappings = mono-chat stereo-game
+input-mappings = mono-chat
+priority = 5100
+
+[Profile output:stereo-game+output:stereo-chat+input:mono-chat]
+output-mappings = stereo-game stereo-chat
+input-mappings = mono-chat
+priority = 5100
diff --git a/spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0 b/spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0
new file mode 100644
index 0000000..082c9a1
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0
@@ -0,0 +1,150 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 29 [94%] [-3.00dB] [on]
+ Front Right: Playback 29 [94%] [-3.00dB] [on]
+Simple mixer control 'Master Mono',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 23 [74%] [0.00dB] [on]
+ Front Right: Playback 23 [74%] [0.00dB] [on]
+Simple mixer control 'Surround',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Surround Jack Mode',0
+ Capabilities: enum
+ Items: 'Shared' 'Independent'
+ Item0: 'Shared'
+Simple mixer control 'Center',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'LFE',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [on]
+ Front Right: Capture [on]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 31 [100%] [12.00dB] [off]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined cswitch cswitch-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Mono: Playback [off] Capture [off]
+Simple mixer control 'IEC958 Playback AC97-SPSA',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 3
+ Mono: 0 [0%]
+Simple mixer control 'IEC958 Playback Source',0
+ Capabilities: enum
+ Items: 'PCM' 'Analog In' 'IEC958 In'
+ Item0: 'PCM'
+Simple mixer control 'PC Speaker',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 15
+ Mono: Playback 0 [0%] [-45.00dB] [on]
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [on] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [on] Capture [off]
+Simple mixer control 'Mono Output Select',0
+ Capabilities: enum
+ Items: 'Mix' 'Mic'
+ Item0: 'Mix'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch cswitch-joined
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 12 [80%] [18.00dB] [on]
+ Front Right: Capture 12 [80%] [18.00dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Channel Mode',0
+ Capabilities: enum
+ Items: '2ch' '4ch' '6ch'
+ Item0: '2ch'
+Simple mixer control 'Duplicate Front',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
diff --git a/spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x b/spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x
new file mode 100644
index 0000000..b8f61fa
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x
@@ -0,0 +1,24 @@
+Simple mixer control 'FM',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
+Simple mixer control 'Mic/Line',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cvolume-joined
+ Capture channels: Mono
+ Limits: Capture 0 - 15
+ Mono: Capture 13 [87%]
+Simple mixer control 'Capture Boost',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
+Simple mixer control 'TV Tuner',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [on]
diff --git a/spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3 b/spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3
new file mode 100644
index 0000000..a500a81
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3
@@ -0,0 +1,135 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 63
+ Mono:
+ Front Left: Playback 63 [100%] [0.00dB] [on]
+ Front Right: Playback 63 [100%] [0.00dB] [on]
+Simple mixer control 'Master Mono',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Headphone',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control '3D Control - Center',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 15
+ Mono: 0 [0%]
+Simple mixer control '3D Control - Depth',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 15
+ Mono: 0 [0%]
+Simple mixer control '3D Control - Switch',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 23 [74%] [0.00dB] [on]
+ Front Right: Playback 23 [74%] [0.00dB] [on]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [on]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [on]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 23 [74%] [0.00dB] [on]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'PC Speaker',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 15
+ Mono: Playback 0 [0%] [-45.00dB] [off]
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mono Output Select',0
+ Capabilities: enum
+ Items: 'Mix' 'Mic'
+ Item0: 'Mic'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch cswitch-joined
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 15 [100%] [22.50dB] [on]
+ Front Right: Capture 15 [100%] [22.50dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
diff --git a/spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI b/spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI
new file mode 100644
index 0000000..244f24a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI
@@ -0,0 +1,4 @@
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
diff --git a/spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD1981 b/spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD1981
new file mode 100644
index 0000000..165522f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD1981
@@ -0,0 +1,62 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 63
+ Mono:
+ Front Left: Playback 63 [100%] [3.00dB] [on]
+ Front Right: Playback 63 [100%] [3.00dB] [on]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 23 [74%] [0.00dB] [on]
+ Front Right: Playback 23 [74%] [0.00dB] [on]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Capture [off]
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pswitch cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Capture [on]
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Mic Boost',0
+ Capabilities: volume
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: 0 - 3
+ Front Left: 0 [0%]
+ Front Right: 0 [0%]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Default PCM',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Playback Source',0
+ Capabilities: enum
+ Items: 'PCM' 'ADC'
+ Item0: 'PCM'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 0 [0%] [0.00dB] [on]
+ Front Right: Capture 0 [0%] [0.00dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
diff --git a/spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A b/spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A
new file mode 100644
index 0000000..28a2e73
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A
@@ -0,0 +1,113 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 64
+ Mono: Playback 64 [100%] [0.00dB] [on]
+Simple mixer control 'Headphone',0
+ Capabilities: pswitch
+ Playback channels: Front Left - Front Right
+ Mono:
+ Front Left: Playback [on]
+ Front Right: Playback [on]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 255
+ Mono:
+ Front Left: Playback 255 [100%] [0.00dB]
+ Front Right: Playback 255 [100%] [0.00dB]
+Simple mixer control 'Front',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 64
+ Mono:
+ Front Left: Playback 44 [69%] [-20.00dB] [on]
+ Front Right: Playback 44 [69%] [-20.00dB] [on]
+Simple mixer control 'Front Mic',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Front Mic Boost',0
+ Capabilities: volume
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: 0 - 3
+ Front Left: 0 [0%]
+ Front Right: 0 [0%]
+Simple mixer control 'Surround',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 64
+ Mono:
+ Front Left: Playback 0 [0%] [-64.00dB] [on]
+ Front Right: Playback 0 [0%] [-64.00dB] [on]
+Simple mixer control 'Center',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 64
+ Mono: Playback 0 [0%] [-64.00dB] [on]
+Simple mixer control 'LFE',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 64
+ Mono: Playback 0 [0%] [-64.00dB] [on]
+Simple mixer control 'Side',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 64
+ Mono:
+ Front Left: Playback 0 [0%] [-64.00dB] [on]
+ Front Right: Playback 0 [0%] [-64.00dB] [on]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Mic Boost',0
+ Capabilities: volume
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: 0 - 3
+ Front Left: 0 [0%]
+ Front Right: 0 [0%]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined cswitch cswitch-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Mono: Playback [on] Capture [on]
+Simple mixer control 'IEC958 Default PCM',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 46
+ Front Left: Capture 23 [50%] [7.00dB] [on]
+ Front Right: Capture 23 [50%] [7.00dB] [on]
+Simple mixer control 'Capture',1
+ Capabilities: cvolume cswitch
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 46
+ Front Left: Capture 0 [0%] [-16.00dB] [off]
+ Front Right: Capture 0 [0%] [-16.00dB] [off]
+Simple mixer control 'Input Source',0
+ Capabilities: cenum
+ Items: 'Mic' 'Front Mic' 'Line'
+ Item0: 'Mic'
+Simple mixer control 'Input Source',1
+ Capabilities: cenum
+ Items: 'Mic' 'Front Mic' 'Line'
+ Item0: 'Mic'
diff --git a/spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A b/spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A
new file mode 100644
index 0000000..3ddd8af
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A
@@ -0,0 +1,128 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 63
+ Mono:
+ Front Left: Playback 44 [70%] [-28.50dB] [on]
+ Front Right: Playback 60 [95%] [-4.50dB] [on]
+Simple mixer control 'Master Mono',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 17 [55%] [-21.00dB] [on]
+Simple mixer control '3D Control - Center',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 15
+ Mono: 0 [0%]
+Simple mixer control '3D Control - Depth',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 15
+ Mono: 0 [0%]
+Simple mixer control '3D Control - Switch',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 9 [29%] [-21.00dB] [on]
+ Front Right: Playback 9 [29%] [-21.00dB] [on]
+Simple mixer control 'PCM Out Path & Mute',0
+ Capabilities: enum
+ Items: 'pre 3D' 'post 3D'
+ Item0: 'pre 3D'
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 9 [29%] [-21.00dB] [on] Capture [off]
+ Front Right: Playback 9 [29%] [-21.00dB] [on] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [on]
+ Front Right: Capture [on]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'PC Speaker',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 15
+ Mono: Playback 8 [53%] [-21.00dB] [on]
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mono Output Select',0
+ Capabilities: enum
+ Items: 'Mix' 'Mic'
+ Item0: 'Mix'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch cswitch-joined
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 13 [87%] [19.50dB] [on]
+ Front Right: Capture 13 [87%] [19.50dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
diff --git a/spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer b/spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer
new file mode 100644
index 0000000..38cf677
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer
@@ -0,0 +1,27 @@
+Simple mixer control 'Bass',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 48
+ Mono: 22 [46%]
+Simple mixer control 'Bass Boost',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Treble',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 48
+ Mono: 25 [52%]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 44
+ Mono:
+ Front Left: Playback 10 [23%] [-31.00dB] [on]
+ Front Right: Playback 10 [23%] [-31.00dB] [on]
+Simple mixer control 'Auto Gain Control',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
diff --git a/spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer b/spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer
new file mode 100644
index 0000000..9cb4fa7
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer
@@ -0,0 +1,37 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 255
+ Mono: Playback 105 [41%] [-28.97dB] [on]
+Simple mixer control 'Line',0
+ Capabilities: pvolume cvolume pswitch pswitch-joined cswitch cswitch-joined
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 255 Capture 0 - 128
+ Front Left: Playback 191 [75%] [34.38dB] [off] Capture 0 [0%] [0.18dB] [off]
+ Front Right: Playback 191 [75%] [34.38dB] [off] Capture 0 [0%] [0.18dB] [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined cvolume cvolume-joined pswitch pswitch-joined cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: Playback 0 - 255 Capture 0 - 128
+ Mono: Playback 191 [75%] [34.38dB] [off] Capture 0 [0%] [0.18dB] [on]
+Simple mixer control 'Mic Capture',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 In',0
+ Capabilities: cswitch cswitch-joined
+ Capture channels: Mono
+ Mono: Capture [off]
+Simple mixer control 'Input 1',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
+Simple mixer control 'Input 2',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
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
new file mode 100644
index 0000000..783f826
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/USB Device 0x46d:0x9a4--USB Mixer
@@ -0,0 +1,5 @@
+Simple mixer control 'Mic',0
+ Capabilities: cvolume cvolume-joined cswitch cswitch-joined
+ Capture channels: Mono
+ Limits: Capture 0 - 3072
+ Mono: Capture 1536 [50%] [23.00dB] [on]
diff --git a/spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888 b/spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888
new file mode 100644
index 0000000..15e7b5a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888
@@ -0,0 +1,211 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [0.00dB] [on]
+ Front Right: Playback 31 [100%] [0.00dB] [on]
+Simple mixer control 'Master Mono',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Master Surround',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Headphone Jack Sense',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 23 [74%] [0.00dB] [on]
+ Front Right: Playback 23 [74%] [0.00dB] [on]
+Simple mixer control 'Surround',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Surround Jack Mode',0
+ Capabilities: enum
+ Items: 'Shared' 'Independent'
+ Item0: 'Shared'
+Simple mixer control 'Center',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 31 [100%] [0.00dB] [off]
+Simple mixer control 'LFE',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Line Jack Sense',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [on]
+ Front Right: Capture [on]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Output',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Playback AC97-SPSA',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 3
+ Mono: 3 [100%]
+Simple mixer control 'IEC958 Playback Source',0
+ Capabilities: enum
+ Items: 'AC-Link' 'A/D Converter'
+ Item0: 'AC-Link'
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 0 [0%] [0.00dB] [on]
+ Front Right: Capture 0 [0%] [0.00dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Channel Mode',0
+ Capabilities: enum
+ Items: '2ch' '4ch' '6ch'
+ Item0: '2ch'
+Simple mixer control 'Downmix',0
+ Capabilities: enum
+ Items: 'Off' '6 -> 4' '6 -> 2'
+ Item0: 'Off'
+Simple mixer control 'Exchange Front/Surround',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
+Simple mixer control 'High Pass Filter Enable',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Input Source Select',0
+ Capabilities: enum
+ Items: 'Input1' 'Input2'
+ Item0: 'Input1'
+Simple mixer control 'Input Source Select',1
+ Capabilities: enum
+ Items: 'Input1' 'Input2'
+ Item0: 'Input1'
+Simple mixer control 'Spread Front to Surround and Center/LFE',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'VIA DXS',0
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB]
+ Front Right: Playback 31 [100%] [-48.00dB]
+Simple mixer control 'VIA DXS',1
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB]
+ Front Right: Playback 31 [100%] [-48.00dB]
+Simple mixer control 'VIA DXS',2
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB]
+ Front Right: Playback 31 [100%] [-48.00dB]
+Simple mixer control 'VIA DXS',3
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB]
+ Front Right: Playback 31 [100%] [-48.00dB]
+Simple mixer control 'V_REFOUT Enable',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
diff --git a/spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+ b/spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+
new file mode 100644
index 0000000..d4f3db6
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+
@@ -0,0 +1,160 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB] [off]
+ Front Right: Playback 31 [100%] [-48.00dB] [off]
+Simple mixer control 'Surround',0
+ Capabilities: pswitch
+ Playback channels: Front Left - Front Right
+ Mono:
+ Front Left: Playback [off]
+ Front Right: Playback [off]
+Simple mixer control 'Surround Jack Mode',0
+ Capabilities: enum
+ Items: 'Shared' 'Independent'
+ Item0: 'Shared'
+Simple mixer control 'Center',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 31 [100%] [0.00dB] [off]
+Simple mixer control 'LFE',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [on]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [on]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined cswitch cswitch-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Mono: Playback [off] Capture [off]
+Simple mixer control 'IEC958 Capture Monitor',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Capture Valid',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Output',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Playback AC97-SPSA',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 3
+ Mono: 3 [100%]
+Simple mixer control 'IEC958 Playback Source',0
+ Capabilities: enum
+ Items: 'AC-Link' 'ADC' 'SPDIF-In'
+ Item0: 'AC-Link'
+Simple mixer control 'PC Speaker',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 15
+ Mono: Playback 0 [0%] [-45.00dB] [off]
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mono Output Select',0
+ Capabilities: enum
+ Items: 'Mix' 'Mic'
+ Item0: 'Mix'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch cswitch-joined
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 0 [0%] [0.00dB] [on]
+ Front Right: Capture 0 [0%] [0.00dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Channel Mode',0
+ Capabilities: enum
+ Items: '2ch' '4ch' '6ch'
+ Item0: '2ch'
+Simple mixer control 'DAC Clock Source',0
+ Capabilities: enum
+ Items: 'AC-Link' 'SPDIF-In' 'Both'
+ Item0: 'AC-Link'
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
+Simple mixer control 'Input Source Select',0
+ Capabilities: enum
+ Items: 'Input1' 'Input2'
+ Item0: 'Input1'
+Simple mixer control 'Input Source Select',1
+ Capabilities: enum
+ Items: 'Input1' 'Input2'
+ Item0: 'Input1'
diff --git a/spa/plugins/alsa/test-hw-params.c b/spa/plugins/alsa/test-hw-params.c
new file mode 100644
index 0000000..7315061
--- /dev/null
+++ b/spa/plugins/alsa/test-hw-params.c
@@ -0,0 +1,173 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <getopt.h>
+#include <math.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/utils/defs.h>
+
+#define DEFAULT_DEVICE "default"
+
+
+struct state {
+ const char *device;
+ snd_output_t *output;
+ snd_pcm_t *hndl;
+};
+
+#define CHECK(s,msg,...) { \
+ int __err; \
+ if ((__err = (s)) < 0) { \
+ fprintf(stderr, msg ": %s\n", ##__VA_ARGS__, snd_strerror(__err)); \
+ return __err; \
+ } \
+}
+
+static const char *get_class(snd_pcm_class_t c)
+{
+ switch (c) {
+ case SND_PCM_CLASS_GENERIC:
+ return "generic";
+ case SND_PCM_CLASS_MULTI:
+ return "multichannel";
+ case SND_PCM_CLASS_MODEM:
+ return "modem";
+ case SND_PCM_CLASS_DIGITIZER:
+ return "digitizer";
+ default:
+ return "unknown";
+ }
+}
+
+static const char *get_subclass(snd_pcm_subclass_t c)
+{
+ switch (c) {
+ case SND_PCM_SUBCLASS_GENERIC_MIX:
+ return "generic-mix";
+ case SND_PCM_SUBCLASS_MULTI_MIX:
+ return "multichannel-mix";
+ default:
+ return "unknown";
+ }
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " -D, --device device name (default '%s')\n"
+ " -C, --capture capture mode (default playback)\n",
+ name, DEFAULT_DEVICE);
+}
+
+int main(int argc, char *argv[])
+{
+ struct state state = { 0, };
+ snd_pcm_hw_params_t *hparams;
+ snd_pcm_info_t *info;
+ snd_pcm_sync_id_t sync;
+ snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;
+ snd_pcm_chmap_query_t **maps;
+ int c, i;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "device", required_argument, NULL, 'D' },
+ { "capture", no_argument, NULL, 'C' },
+ { NULL, 0, NULL, 0}
+ };
+ state.device = DEFAULT_DEVICE;
+
+ while ((c = getopt_long(argc, argv, "hD:C", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'D':
+ state.device = optarg;
+ break;
+ case 'C':
+ stream = SND_PCM_STREAM_CAPTURE;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ CHECK(snd_output_stdio_attach(&state.output, stdout, 0), "attach failed");
+
+ fprintf(stdout, "opening device: '%s'\n", state.device);
+
+ CHECK(snd_pcm_open(&state.hndl, state.device, stream, 0),
+ "open %s failed", state.device);
+
+ snd_pcm_info_alloca(&info);
+ snd_pcm_info(state.hndl, info);
+
+ fprintf(stdout, "info:\n");
+ fprintf(stdout, " device: %u\n", snd_pcm_info_get_device(info));
+ fprintf(stdout, " subdevice: %u\n", snd_pcm_info_get_subdevice(info));
+ fprintf(stdout, " stream: %s\n", snd_pcm_stream_name(snd_pcm_info_get_stream(info)));
+ fprintf(stdout, " card: %d\n", snd_pcm_info_get_card(info));
+ fprintf(stdout, " id: '%s'\n", snd_pcm_info_get_id(info));
+ fprintf(stdout, " name: '%s'\n", snd_pcm_info_get_name(info));
+ fprintf(stdout, " subdevice name: '%s'\n", snd_pcm_info_get_subdevice_name(info));
+ fprintf(stdout, " class: %s\n", get_class(snd_pcm_info_get_class(info)));
+ fprintf(stdout, " subclass: %s\n", get_subclass(snd_pcm_info_get_subclass(info)));
+ fprintf(stdout, " subdevice count: %u\n", snd_pcm_info_get_subdevices_count(info));
+ fprintf(stdout, " subdevice avail: %u\n", snd_pcm_info_get_subdevices_avail(info));
+ sync = snd_pcm_info_get_sync(info);
+ fprintf(stdout, " sync: %08x:%08x:%08x:%08x\n",
+ sync.id32[0], sync.id32[1], sync.id32[2],sync.id32[3]);
+
+ /* channel maps */
+ if ((maps = snd_pcm_query_chmaps(state.hndl)) != NULL) {
+ fprintf(stdout, "channels:\n");
+
+ for (i = 0; maps[i]; i++) {
+ snd_pcm_chmap_t* map = &maps[i]->map;
+ char buf[2048];
+
+ snd_pcm_chmap_print(map, sizeof(buf), buf);
+
+ fprintf(stdout, " %d: %s\n", map->channels, buf);
+ }
+ snd_pcm_free_chmaps(maps);
+ }
+
+ /* hw params */
+ snd_pcm_hw_params_alloca(&hparams);
+ snd_pcm_hw_params_any(state.hndl, hparams);
+
+ snd_pcm_hw_params_dump(hparams, state.output);
+
+ snd_pcm_close(state.hndl);
+
+ return EXIT_SUCCESS;
+}
diff --git a/spa/plugins/alsa/test-timer.c b/spa/plugins/alsa/test-timer.c
new file mode 100644
index 0000000..cc5e5f1
--- /dev/null
+++ b/spa/plugins/alsa/test-timer.c
@@ -0,0 +1,310 @@
+/* Spa
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <getopt.h>
+#include <math.h>
+#include <sys/timerfd.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/utils/dll.h>
+#include <spa/utils/defs.h>
+
+#define DEFAULT_DEVICE "hw:0"
+
+#define M_PI_M2 (M_PI + M_PI)
+
+#define BW_PERIOD (SPA_NSEC_PER_SEC * 3)
+
+struct state {
+ const char *device;
+ unsigned int format;
+ unsigned int rate;
+ unsigned int channels;
+ snd_pcm_uframes_t period;
+ snd_pcm_uframes_t buffer_frames;
+
+ snd_pcm_t *hndl;
+ int timerfd;
+
+ double max_error;
+ float accumulator;
+
+ uint64_t next_time;
+ uint64_t prev_time;
+
+ struct spa_dll dll;
+};
+
+static int set_timeout(struct state *state, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return timerfd_settime(state->timerfd, TFD_TIMER_ABSTIME, &ts, NULL);
+}
+
+#define CHECK(s,msg,...) { \
+ int __err; \
+ if ((__err = (s)) < 0) { \
+ fprintf(stderr, msg ": %s\n", ##__VA_ARGS__, snd_strerror(__err)); \
+ return __err; \
+ } \
+}
+
+#define LOOP(type,areas,scale) { \
+ uint32_t i, j; \
+ type *samples, v; \
+ samples = (type*)((uint8_t*)areas[0].addr + (areas[0].first + offset*areas[0].step) / 8); \
+ for (i = 0; i < frames; i++) { \
+ state->accumulator += M_PI_M2 * 440 / state->rate; \
+ if (state->accumulator >= M_PI_M2) \
+ state->accumulator -= M_PI_M2; \
+ v = sin(state->accumulator) * scale; \
+ for (j = 0; j < state->channels; j++) \
+ *samples++ = v; \
+ } \
+}
+
+static int write_period(struct state *state)
+{
+ snd_pcm_uframes_t frames = state->period;
+ snd_pcm_uframes_t offset;
+ const snd_pcm_channel_area_t* areas;
+
+ snd_pcm_mmap_begin(state->hndl, &areas, &offset, &frames);
+
+ switch (state->format) {
+ case SND_PCM_FORMAT_S32_LE:
+ LOOP(int32_t, areas, 0x7fffffff);
+ break;
+ case SND_PCM_FORMAT_S16_LE:
+ LOOP(int16_t, areas, 0x7fff);
+ break;
+ default:
+ break;
+ }
+
+ snd_pcm_mmap_commit(state->hndl, offset, frames) ;
+
+ return 0;
+}
+
+static int on_timer_wakeup(struct state *state)
+{
+ snd_pcm_sframes_t delay;
+ double error, corr;
+#if 1
+ snd_pcm_sframes_t avail;
+ CHECK(snd_pcm_avail_delay(state->hndl, &avail, &delay), "delay");
+#else
+ snd_pcm_uframes_t avail;
+ snd_htimestamp_t tstamp;
+ uint64_t then;
+
+ CHECK(snd_pcm_htimestamp(state->hndl, &avail, &tstamp), "htimestamp");
+ delay = state->buffer_frames - avail;
+
+ then = SPA_TIMESPEC_TO_NSEC(&tstamp);
+ if (then != 0) {
+ if (then < state->next_time) {
+ delay -= (state->next_time - then) * state->rate / SPA_NSEC_PER_SEC;
+ } else {
+ delay += (then - state->next_time) * state->rate / SPA_NSEC_PER_SEC;
+ }
+ }
+#endif
+
+ /* calculate the error, we want to have exactly 1 period of
+ * samples remaining in the device when we wakeup. */
+ error = (double)delay - (double)state->period;
+ if (error > state->max_error)
+ error = state->max_error;
+ else if (error < -state->max_error)
+ error = -state->max_error;
+
+ /* update the dll with the error, this gives a rate correction */
+ corr = spa_dll_update(&state->dll, error);
+
+ /* set our new adjusted timeout. alternatively, this value can
+ * instead be used to drive a resampler if this device is
+ * slaved. */
+ state->next_time += state->period / corr * 1e9 / state->rate;
+ set_timeout(state, state->next_time);
+
+ if (state->next_time - state->prev_time > BW_PERIOD) {
+ state->prev_time = state->next_time;
+ fprintf(stdout, "corr:%f error:%f bw:%f\n",
+ corr, error, state->dll.bw);
+ }
+ /* pull in new samples write a new period */
+ write_period(state);
+
+ return 0;
+}
+
+static unsigned int format_from_string(const char *str)
+{
+ if (strcmp(str, "S32_LE") == 0)
+ return SND_PCM_FORMAT_S32_LE;
+ else if (strcmp(str, "S32_BE") == 0)
+ return SND_PCM_FORMAT_S32_BE;
+ else if (strcmp(str, "S24_LE") == 0)
+ return SND_PCM_FORMAT_S24_LE;
+ else if (strcmp(str, "S24_BE") == 0)
+ return SND_PCM_FORMAT_S24_BE;
+ else if (strcmp(str, "S24_3LE") == 0)
+ return SND_PCM_FORMAT_S24_3LE;
+ else if (strcmp(str, "S24_3_BE") == 0)
+ return SND_PCM_FORMAT_S24_3BE;
+ else if (strcmp(str, "S16_LE") == 0)
+ return SND_PCM_FORMAT_S16_LE;
+ else if (strcmp(str, "S16_BE") == 0)
+ return SND_PCM_FORMAT_S16_BE;
+ return 0;
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " -D, --device device name (default %s)\n",
+ name, DEFAULT_DEVICE);
+}
+
+int main(int argc, char *argv[])
+{
+ struct state state = { 0, };
+ snd_pcm_hw_params_t *hparams;
+ snd_pcm_sw_params_t *sparams;
+ struct timespec now;
+ int c;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "device", required_argument, NULL, 'D' },
+ { "format", required_argument, NULL, 'f' },
+ { "rate", required_argument, NULL, 'r' },
+ { "channels", required_argument, NULL, 'c' },
+ { NULL, 0, NULL, 0}
+ };
+ state.device = DEFAULT_DEVICE;
+ state.format = SND_PCM_FORMAT_S16_LE;
+ state.rate = 44100;
+ state.channels = 2;
+ state.period = 1024;
+
+ while ((c = getopt_long(argc, argv, "hD:f:r:c:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'D':
+ state.device = optarg;
+ break;
+ case 'f':
+ state.format = format_from_string(optarg);
+ break;
+ case 'r':
+ state.rate = atoi(optarg);
+ break;
+ case 'c':
+ state.channels = atoi(optarg);
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ CHECK(snd_pcm_open(&state.hndl, state.device, SND_PCM_STREAM_PLAYBACK, 0),
+ "open %s failed", state.device);
+
+ /* hw params */
+ snd_pcm_hw_params_alloca(&hparams);
+ snd_pcm_hw_params_any(state.hndl, hparams);
+ CHECK(snd_pcm_hw_params_set_access(state.hndl, hparams,
+ SND_PCM_ACCESS_MMAP_INTERLEAVED), "set interleaved");
+ CHECK(snd_pcm_hw_params_set_format(state.hndl, hparams,
+ state.format), "set format");
+ CHECK(snd_pcm_hw_params_set_channels_near(state.hndl, hparams,
+ &state.channels), "set channels");
+ CHECK(snd_pcm_hw_params_set_rate_near(state.hndl, hparams,
+ &state.rate, 0), "set rate");
+ CHECK(snd_pcm_hw_params(state.hndl, hparams), "hw_params");
+
+ CHECK(snd_pcm_hw_params_get_buffer_size(hparams, &state.buffer_frames), "get_buffer_size_max");
+
+ fprintf(stdout, "opened format:%s rate:%u channels:%u\n",
+ snd_pcm_format_name(state.format),
+ state.rate, state.channels);
+
+ snd_pcm_sw_params_alloca(&sparams);
+#if 0
+ CHECK(snd_pcm_sw_params_current(state.hndl, sparams), "sw_params_current");
+ CHECK(snd_pcm_sw_params_set_tstamp_mode(state.hndl, sparams, SND_PCM_TSTAMP_ENABLE),
+ "sw_params_set_tstamp_type");
+ CHECK(snd_pcm_sw_params_set_tstamp_type(state.hndl, sparams, SND_PCM_TSTAMP_TYPE_MONOTONIC),
+ "sw_params_set_tstamp_type");
+ CHECK(snd_pcm_sw_params(state.hndl, sparams), "sw_params");
+#endif
+
+ spa_dll_init(&state.dll);
+ spa_dll_set_bw(&state.dll, SPA_DLL_BW_MAX, state.period, state.rate);
+ state.max_error = SPA_MAX(256.0, state.period / 2.0f);
+
+ if ((state.timerfd = timerfd_create(CLOCK_MONOTONIC, 0)) < 0)
+ perror("timerfd");
+
+ CHECK(snd_pcm_prepare(state.hndl), "prepare");
+
+ /* before we start, write one period */
+ write_period(&state);
+
+ /* set our first timeout for now */
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ state.prev_time = state.next_time = SPA_TIMESPEC_TO_NSEC(&now);
+ set_timeout(&state, state.next_time);
+
+ /* and start playback */
+ CHECK(snd_pcm_start(state.hndl), "start");
+
+ /* wait for timer to expire and call the wakeup function,
+ * this can be done in a poll loop as well */
+ while (true) {
+ uint64_t expirations;
+ CHECK(read(state.timerfd, &expirations, sizeof(expirations)), "read");
+ on_timer_wakeup(&state);
+ }
+
+ snd_pcm_drain(state.hndl);
+ snd_pcm_close(state.hndl);
+ close(state.timerfd);
+
+ return EXIT_SUCCESS;
+}
diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c
new file mode 100644
index 0000000..2ccc2b7
--- /dev/null
+++ b/spa/plugins/audioconvert/audioadapter.c
@@ -0,0 +1,1733 @@
+/* SPA
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/cpu.h>
+
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/buffer/alloc.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/latency-utils.h>
+#include <spa/debug/format.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT log_topic
+static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.audioadapter");
+
+#define DEFAULT_ALIGN 16
+
+#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1)
+
+/** \cond */
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_cpu *cpu;
+
+ uint32_t max_align;
+ enum spa_direction direction;
+
+ struct spa_node *target;
+
+ struct spa_node *follower;
+ struct spa_hook follower_listener;
+ uint32_t follower_flags;
+ struct spa_audio_info follower_current_format;
+ struct spa_audio_info default_format;
+
+ struct spa_handle *hnd_convert;
+ struct spa_node *convert;
+ struct spa_hook convert_listener;
+ uint32_t convert_flags;
+
+ uint32_t n_buffers;
+ struct spa_buffer **buffers;
+
+ struct spa_io_buffers io_buffers;
+ struct spa_io_rate_match io_rate_match;
+ struct spa_io_position *io_position;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_EnumFormat 0
+#define IDX_PropInfo 1
+#define IDX_Props 2
+#define IDX_Format 3
+#define IDX_EnumPortConfig 4
+#define IDX_PortConfig 5
+#define IDX_Latency 6
+#define IDX_ProcessLatency 7
+#define N_NODE_PARAMS 8
+ struct spa_param_info params[N_NODE_PARAMS];
+ uint32_t convert_params_flags[N_NODE_PARAMS];
+ uint32_t follower_params_flags[N_NODE_PARAMS];
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ unsigned int add_listener:1;
+ unsigned int have_format:1;
+ unsigned int started:1;
+ unsigned int driver:1;
+ unsigned int async:1;
+ unsigned int passthrough:1;
+ unsigned int follower_removing:1;
+};
+
+/** \endcond */
+
+static int follower_enum_params(struct impl *this,
+ 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) {
+ if ((res = spa_node_enum_params_sync(this->convert,
+ 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,
+ id, &result->next, filter, &result->param, builder)) == 1) {
+ result->next |= 0x100000;
+ return res;
+ }
+ result->next = 0x200000;
+ }
+ return 0;
+}
+
+static int convert_enum_port_config(struct impl *this,
+ int seq, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter, struct spa_pod_builder *builder)
+{
+ struct spa_pod *f1, *f2 = NULL;
+ int res;
+
+ f1 = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_ParamPortConfig, id,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction));
+
+ if (filter) {
+ if ((res = spa_pod_filter(builder, &f2, f1, filter)) < 0)
+ return res;
+ }
+ else {
+ f2 = f1;
+ }
+ return spa_node_enum_params(this->convert, seq, id, start, num, f2);
+}
+
+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;
+ uint8_t buffer[4096];
+ struct spa_pod_dynamic_builder b;
+ 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);
+
+ 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_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+
+ 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) {
+ switch (result.index) {
+ case 0:
+ result.param = spa_pod_builder_add_object(&b.b,
+ SPA_TYPE_OBJECT_ParamPortConfig, id,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(
+ SPA_PARAM_PORT_CONFIG_MODE_passthrough));
+ result.next++;
+ break;
+ default:
+ return 0;
+ }
+ } else {
+ return convert_enum_port_config(this, seq, id, start, num, filter, &b.b);
+ }
+ break;
+ case SPA_PARAM_PropInfo:
+ res = follower_enum_params(this,
+ id, IDX_PropInfo, &result, filter, &b.b);
+ break;
+ case SPA_PARAM_Props:
+ res = follower_enum_params(this,
+ id, IDX_Props, &result, filter, &b.b);
+ break;
+ case SPA_PARAM_ProcessLatency:
+ res = follower_enum_params(this,
+ id, IDX_ProcessLatency, &result, filter, &b.b);
+ break;
+ case SPA_PARAM_EnumFormat:
+ case SPA_PARAM_Format:
+ case SPA_PARAM_Latency:
+ res = spa_node_port_enum_params_sync(this->follower,
+ this->direction, 0,
+ id, &result.next, filter, &result.param, &b.b);
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (res == 1) {
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (res != 1)
+ return res;
+
+ if (count != num)
+ goto next;
+
+ return 0;
+}
+
+static int link_io(struct impl *this)
+{
+ int res;
+
+ if (this->convert == NULL)
+ return 0;
+
+ spa_log_debug(this->log, "%p: controls", this);
+
+ spa_zero(this->io_rate_match);
+ this->io_rate_match.rate = 1.0;
+
+ if ((res = spa_node_port_set_io(this->follower,
+ this->direction, 0,
+ SPA_IO_RateMatch,
+ &this->io_rate_match, sizeof(this->io_rate_match))) < 0) {
+ 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,
+ &this->io_rate_match, sizeof(this->io_rate_match))) < 0) {
+ spa_log_warn(this->log, "%p: set RateMatch on convert failed %d %s", this,
+ res, spa_strerror(res));
+ }
+
+ 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_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,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ SPA_IO_Buffers,
+ &this->io_buffers, sizeof(this->io_buffers))) < 0) {
+ spa_log_warn(this->log, "%p: set Buffers on convert failed %d %s", this,
+ res, spa_strerror(res));
+ return res;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint32_t i;
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ spa_log_debug(this->log, "%p: info full:%d change:%08"PRIx64,
+ this, full, this->info.change_mask);
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->info.n_params; i++) {
+ if (this->params[i].user > 0) {
+ this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[i].user = 0;
+ spa_log_debug(this->log, "param %d flags:%08x",
+ i, this->params[i].flags);
+ }
+ }
+ }
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static int debug_params(struct impl *this, struct spa_node *node,
+ enum spa_direction direction, uint32_t port_id, uint32_t id, struct spa_pod *filter,
+ const char *debug, int err)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ uint32_t state;
+ struct spa_pod *param;
+ int res, count = 0;
+
+ spa_log_error(this->log, "params %s: %d:%d (%s) %s",
+ spa_debug_type_find_name(spa_type_param, id),
+ direction, port_id, debug, err ? spa_strerror(err) : "no matching params");
+ if (err == -EBUSY)
+ return 0;
+
+ if (filter) {
+ spa_log_error(this->log, "with this filter:");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 2, NULL, filter);
+ } else {
+ spa_log_error(this->log, "there was no filter");
+ }
+
+ state = 0;
+ while (true) {
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ res = spa_node_port_enum_params_sync(node,
+ direction, port_id,
+ id, &state,
+ NULL, &param, &b);
+ if (res != 1) {
+ if (res < 0)
+ spa_log_error(this->log, " error: %s", spa_strerror(res));
+ break;
+ }
+ spa_log_error(this->log, "unmatched %s %d:", debug, count);
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 2, NULL, param);
+ count++;
+ }
+ if (count == 0)
+ spa_log_error(this->log, "could not get any %s", debug);
+
+ return 0;
+}
+
+static int negotiate_buffers(struct impl *this)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ uint32_t state;
+ struct spa_pod *param;
+ int res;
+ bool follower_alloc, conv_alloc;
+ uint32_t i, size, buffers, blocks, align, flags, stride = 0;
+ uint32_t *aligns;
+ struct spa_data *datas;
+ uint32_t follower_flags, conv_flags;
+
+ spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers);
+
+ if (this->target == this->follower)
+ return 0;
+
+ if (this->n_buffers > 0)
+ return 0;
+
+ state = 0;
+ param = NULL;
+ if ((res = spa_node_port_enum_params_sync(this->follower,
+ this->direction, 0,
+ SPA_PARAM_Buffers, &state,
+ param, &param, &b)) < 0) {
+ if (res == -ENOENT)
+ param = NULL;
+ else {
+ debug_params(this, this->follower, this->direction, 0,
+ SPA_PARAM_Buffers, param, "follower buffers", res);
+ return res;
+ }
+ }
+
+ state = 0;
+ if ((res = spa_node_port_enum_params_sync(this->convert,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ SPA_PARAM_Buffers, &state,
+ param, &param, &b)) != 1) {
+ debug_params(this, this->convert,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ SPA_PARAM_Buffers, param, "convert buffers", res);
+ return -ENOTSUP;
+ }
+ if (param == NULL)
+ return -ENOTSUP;
+
+ spa_pod_fixate(param);
+
+ follower_flags = this->follower_flags;
+ conv_flags = this->convert_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);
+
+ flags = 0;
+ if (conv_alloc || follower_alloc) {
+ flags |= SPA_BUFFER_ALLOC_FLAG_NO_DATA;
+ if (conv_alloc)
+ follower_alloc = false;
+ }
+
+ align = DEFAULT_ALIGN;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamBuffers, NULL,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(&buffers),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(&blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(&size),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(&stride),
+ SPA_PARAM_BUFFERS_align, SPA_POD_OPT_Int(&align))) < 0)
+ return res;
+
+ spa_log_debug(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d",
+ this, buffers, blocks, size, stride, align, follower_alloc, conv_alloc);
+
+ align = SPA_MAX(align, this->max_align);
+
+ datas = alloca(sizeof(struct spa_data) * blocks);
+ memset(datas, 0, sizeof(struct spa_data) * blocks);
+ aligns = alloca(sizeof(uint32_t) * blocks);
+ for (i = 0; i < blocks; i++) {
+ datas[i].type = SPA_DATA_MemPtr;
+ datas[i].flags = SPA_DATA_FLAG_READWRITE | SPA_DATA_FLAG_DYNAMIC;
+ datas[i].maxsize = size;
+ aligns[i] = align;
+ }
+
+ free(this->buffers);
+ this->buffers = spa_buffer_alloc_array(buffers, flags, 0, NULL, blocks, datas, aligns);
+ if (this->buffers == NULL)
+ return -errno;
+ this->n_buffers = buffers;
+
+ if ((res = spa_node_port_use_buffers(this->convert,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0,
+ this->buffers, this->n_buffers)) < 0)
+ return res;
+
+ if ((res = spa_node_port_use_buffers(this->follower,
+ this->direction, 0,
+ follower_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0,
+ this->buffers, this->n_buffers)) < 0)
+ return res;
+
+ return 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)
+ 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,
+ SPA_PARAM_Format, flags,
+ format)) < 0)
+ return res;
+
+ if (res > 0) {
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ uint32_t state = 0;
+ struct spa_pod *fmt;
+
+ /* format was changed to nearest compatible format */
+
+ if ((res = spa_node_port_enum_params_sync(this->follower,
+ this->direction, 0,
+ SPA_PARAM_Format, &state,
+ NULL, &fmt, &b)) != 1)
+ return -EIO;
+
+ format = fmt;
+ }
+
+ if (this->target != this->follower && this->convert) {
+ if ((res = spa_node_port_set_param(this->convert,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ SPA_PARAM_Format, flags,
+ format)) < 0)
+ return res;
+ }
+
+ this->have_format = format != NULL;
+ if (format == NULL) {
+ this->n_buffers = 0;
+ } else {
+ res = negotiate_buffers(this);
+ }
+
+ return res;
+}
+
+static int configure_convert(struct impl *this, uint32_t mode)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ 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,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode));
+
+ return spa_node_set_param(this->convert, SPA_PARAM_PortConfig, 0, param);
+}
+
+extern const struct spa_handle_factory spa_audioconvert_factory;
+
+static const struct spa_node_events follower_node_events;
+
+static int reconfigure_mode(struct impl *this, bool passthrough,
+ enum spa_direction direction, struct spa_pod *format)
+{
+ int res = 0;
+ struct spa_hook l;
+
+ spa_log_info(this->log, "%p: passthrough mode %d", this, passthrough);
+
+ if (this->passthrough != passthrough) {
+ if (passthrough) {
+ /* remove converter split/merge ports */
+ configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none);
+ } else {
+ /* remove follower ports */
+ this->follower_removing = true;
+ spa_zero(l);
+ spa_node_add_listener(this->follower, &l, &follower_node_events, this);
+ spa_hook_remove(&l);
+ this->follower_removing = false;
+ }
+ }
+
+ /* set new target */
+ this->target = passthrough ? this->follower : this->convert;
+
+ if ((res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format)) < 0)
+ return res;
+
+ if (this->passthrough != passthrough) {
+ this->passthrough = passthrough;
+ if (passthrough) {
+ /* add follower ports */
+ spa_zero(l);
+ spa_node_add_listener(this->follower, &l, &follower_node_events, this);
+ spa_hook_remove(&l);
+ } else {
+ /* add converter ports */
+ configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp);
+ link_io(this);
+ }
+ }
+
+ 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++;
+
+ emit_node_info(this, false);
+
+ return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ int res = 0, res2 = 0;
+ struct impl *this = object;
+ struct spa_audio_info info = { 0 };
+
+ spa_log_debug(this->log, "%p: set param %d", this, id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ if (this->started)
+ return -EIO;
+ 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)
+ return -EINVAL;
+
+ this->follower_current_format = info;
+ break;
+
+ case SPA_PARAM_PortConfig:
+ {
+ enum spa_direction dir;
+ enum spa_param_port_config_mode mode;
+ struct spa_pod *format = NULL;
+
+ if (this->started) {
+ spa_log_error(this->log, "was started");
+ return -EIO;
+ }
+
+ if (spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamPortConfig, NULL,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&dir),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0)
+ return -EINVAL;
+
+ if (format) {
+ struct spa_audio_info info;
+
+ spa_zero(info);
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -ENOTSUP;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) >= 0) {
+ info.info.raw.rate = 0;
+ this->default_format = info;
+ }
+ }
+
+ switch (mode) {
+ 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)
+ 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)
+ return res;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (this->target != this->follower) {
+ if ((res = spa_node_set_param(this->target, id, flags, param)) < 0)
+ return res;
+ }
+ break;
+ }
+
+ case SPA_PARAM_Props:
+ if (this->target != this->follower)
+ res = spa_node_set_param(this->target, id, flags, param);
+ res2 = spa_node_set_param(this->follower, id, flags, param);
+ if (res < 0 && res2 < 0)
+ return res;
+ res = 0;
+ break;
+ case SPA_PARAM_ProcessLatency:
+ res = spa_node_set_param(this->follower, id, flags, param);
+ break;
+ default:
+ res = -ENOTSUP;
+ break;
+ }
+ return res;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ int res = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Position:
+ this->io_position = data;
+ break;
+ default:
+ break;
+ }
+
+ if (this->target)
+ res = spa_node_set_io(this->target, id, data, size);
+
+ if (this->target != this->follower)
+ res = spa_node_set_io(this->follower, id, data, size);
+
+ return res;
+}
+
+static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder *b, uint32_t id,
+ struct spa_pod_object *o1, struct spa_pod_object *o2)
+{
+ const struct spa_pod_prop *p1, *p2;
+ struct spa_pod_frame f;
+ struct spa_pod_builder_state state;
+ int res = 0;
+
+ if (o2 == NULL || SPA_POD_TYPE(o1) != SPA_POD_TYPE(o2))
+ return (struct spa_pod*)o1;
+
+ spa_pod_builder_push_object(b, &f, o1->body.type, o1->body.id);
+ p2 = NULL;
+ SPA_POD_OBJECT_FOREACH(o1, p1) {
+ p2 = spa_pod_object_find_prop(o2, p2, p1->key);
+ if (p2 != NULL) {
+ spa_pod_builder_get_state(b, &state);
+ res = spa_pod_filter_prop(b, p1, p2);
+ if (res < 0)
+ spa_pod_builder_reset(b, &state);
+ }
+ if (p2 == NULL || res < 0)
+ spa_pod_builder_raw_padded(b, p1, SPA_POD_PROP_SIZE(p1));
+ }
+ p1 = NULL;
+ SPA_POD_OBJECT_FOREACH(o2, p2) {
+ p1 = spa_pod_object_find_prop(o1, p1, p2->key);
+ if (p1 != NULL)
+ continue;
+ spa_pod_builder_raw_padded(b, p2, SPA_POD_PROP_SIZE(p2));
+ }
+ return spa_pod_builder_pop(b, &f);
+}
+
+static int negotiate_format(struct impl *this)
+{
+ uint32_t state;
+ struct spa_pod *format, *def;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ int res;
+
+ spa_log_debug(this->log, "%p: have_format:%d", this, this->have_format);
+
+ if (this->have_format)
+ return 0;
+
+ if (this->target == this->follower)
+ return 0;
+
+ 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) {
+ if (res == -ENOENT)
+ format = NULL;
+ else {
+ debug_params(this, this->follower, this->direction, 0,
+ SPA_PARAM_EnumFormat, format, "follower format", res);
+ goto done;
+ }
+ }
+ if (this->convert) {
+ 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,
+ 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);
+
+ format = merge_objects(this, &b, SPA_PARAM_Format,
+ (struct spa_pod_object*)format,
+ (struct spa_pod_object*)def);
+
+ spa_pod_fixate(format);
+
+ res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format);
+
+done:
+ spa_node_send_command(this->follower,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamEnd));
+
+ return res;
+}
+
+
+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_log_debug(this->log, "%p: command %d", this, SPA_NODE_COMMAND_ID(command));
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ spa_log_debug(this->log, "%p: starting %d", this, this->started);
+ if (this->started)
+ return 0;
+ if ((res = negotiate_format(this)) < 0)
+ return res;
+ if ((res = negotiate_buffers(this)) < 0)
+ return res;
+ this->started = true;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ this->started = false;
+ spa_log_debug(this->log, "%p: suspending", this);
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ spa_log_debug(this->log, "%p: pausing", this);
+ break;
+ case SPA_NODE_COMMAND_Flush:
+ spa_log_debug(this->log, "%p: flushing", this);
+ this->io_buffers.status = SPA_STATUS_OK;
+ break;
+ default:
+ break;
+ }
+
+ if ((res = spa_node_send_command(this->target, command)) < 0) {
+ spa_log_error(this->log, "%p: can't send command %d: %s",
+ this, SPA_NODE_COMMAND_ID(command),
+ spa_strerror(res));
+ return res;
+ }
+
+ if (this->target != this->follower) {
+ if ((res = spa_node_send_command(this->follower, command)) < 0) {
+ spa_log_error(this->log, "%p: can't send command %d: %s",
+ this, SPA_NODE_COMMAND_ID(command),
+ spa_strerror(res));
+ return res;
+ }
+ }
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ spa_log_debug(this->log, "%p: started", this);
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ configure_format(this, 0, NULL);
+ spa_log_debug(this->log, "%p: suspended", this);
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ spa_log_debug(this->log, "%p: paused", this);
+ break;
+ case SPA_NODE_COMMAND_Flush:
+ spa_log_debug(this->log, "%p: flushed", this);
+ break;
+ }
+ return res;
+}
+
+static void convert_node_info(void *data, const struct spa_node_info *info)
+{
+ struct impl *this = data;
+ uint32_t i;
+
+ spa_log_debug(this->log, "%p: info change:%08"PRIx64, this,
+ info->change_mask);
+
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t idx;
+
+ switch (info->params[i].id) {
+ case SPA_PARAM_EnumPortConfig:
+ idx = IDX_EnumPortConfig;
+ break;
+ case SPA_PARAM_PortConfig:
+ idx = IDX_PortConfig;
+ break;
+ case SPA_PARAM_PropInfo:
+ idx = IDX_PropInfo;
+ break;
+ case SPA_PARAM_Props:
+ idx = IDX_Props;
+ break;
+ default:
+ continue;
+ }
+ if (!this->add_listener &&
+ this->convert_params_flags[idx] == info->params[i].flags)
+ continue;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->convert_params_flags[idx] = info->params[i].flags;
+ this->params[idx].flags =
+ (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
+ (info->params[i].flags & SPA_PARAM_INFO_READWRITE);
+
+ if (this->add_listener)
+ continue;
+
+ this->params[idx].user++;
+ spa_log_debug(this->log, "param %d changed", info->params[i].id);
+ }
+ }
+ emit_node_info(this, false);
+}
+
+static void convert_port_info(void *data,
+ enum spa_direction direction, uint32_t port_id,
+ const struct spa_port_info *info)
+{
+ struct impl *this = data;
+
+ if (direction != this->direction) {
+ if (port_id == 0)
+ return;
+ else
+ port_id--;
+ }
+
+ spa_log_debug(this->log, "%p: port info %d:%d", this,
+ direction, port_id);
+
+ if (this->target != this->follower)
+ spa_node_emit_port_info(&this->hooks, direction, port_id, info);
+}
+
+static void convert_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct impl *this = data;
+
+ if (this->target == this->follower)
+ return;
+
+ spa_log_trace(this->log, "%p: result %d %d", this, seq, res);
+ spa_node_emit_result(&this->hooks, seq, res, type, result);
+}
+
+static const struct spa_node_events convert_node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .info = convert_node_info,
+ .port_info = convert_port_info,
+ .result = convert_result,
+};
+
+static void follower_info(void *data, const struct spa_node_info *info)
+{
+ struct impl *this = data;
+ uint32_t i;
+
+ spa_log_debug(this->log, "%p: info change:%08"PRIx64, this,
+ info->change_mask);
+
+ if (this->follower_removing)
+ return;
+
+ this->async = (info->flags & SPA_NODE_FLAG_ASYNC) != 0;
+
+ if (info->max_input_ports > 0)
+ this->direction = SPA_DIRECTION_INPUT;
+ else
+ this->direction = SPA_DIRECTION_OUTPUT;
+
+ if (this->direction == SPA_DIRECTION_INPUT) {
+ this->info.flags |= SPA_NODE_FLAG_IN_PORT_CONFIG;
+ this->info.max_input_ports = MAX_PORTS;
+ } else {
+ this->info.flags |= SPA_NODE_FLAG_OUT_PORT_CONFIG;
+ this->info.max_output_ports = MAX_PORTS;
+ }
+
+ spa_log_debug(this->log, "%p: follower info %s", this,
+ this->direction == SPA_DIRECTION_INPUT ?
+ "Input" : "Output");
+
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PROPS) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ this->info.props = info->props;
+ }
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t idx;
+
+ switch (info->params[i].id) {
+ case SPA_PARAM_PropInfo:
+ idx = IDX_PropInfo;
+ break;
+ case SPA_PARAM_Props:
+ idx = IDX_Props;
+ break;
+ case SPA_PARAM_ProcessLatency:
+ idx = IDX_ProcessLatency;
+ break;
+ default:
+ continue;
+ }
+ if (!this->add_listener &&
+ this->follower_params_flags[idx] == info->params[i].flags)
+ continue;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->follower_params_flags[idx] = info->params[i].flags;
+ this->params[idx].flags =
+ (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
+ (info->params[i].flags & SPA_PARAM_INFO_READWRITE);
+
+ if (this->add_listener)
+ continue;
+
+ this->params[idx].user++;
+ spa_log_debug(this->log, "param %d changed", info->params[i].id);
+ }
+ }
+ emit_node_info(this, false);
+
+ spa_zero(this->info.props);
+ this->info.change_mask &= ~SPA_NODE_CHANGE_MASK_PROPS;
+
+}
+
+static int recalc_latency(struct impl *this, enum spa_direction direction, uint32_t port_id)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ uint32_t index = 0;
+ struct spa_latency_info latency;
+ int res;
+
+ spa_log_debug(this->log, "%p: ", this);
+
+ if (this->target == this->follower)
+ return 0;
+
+ while (true) {
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if ((res = spa_node_port_enum_params_sync(this->follower,
+ direction, port_id, SPA_PARAM_Latency,
+ &index, NULL, &param, &b)) != 1)
+ return res;
+ if ((res = spa_latency_parse(param, &latency)) < 0)
+ return res;
+ if (latency.direction == direction)
+ break;
+ }
+ if ((res = spa_node_port_set_param(this->target,
+ SPA_DIRECTION_REVERSE(direction), 0,
+ SPA_PARAM_Latency, 0, param)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void follower_port_info(void *data,
+ enum spa_direction direction, uint32_t port_id,
+ const struct spa_port_info *info)
+{
+ struct impl *this = data;
+ uint32_t i;
+ int res;
+
+ if (this->follower_removing) {
+ spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
+ return;
+ }
+
+ spa_log_debug(this->log, "%p: follower port info %s %p %08"PRIx64, this,
+ this->direction == SPA_DIRECTION_INPUT ?
+ "Input" : "Output", info, info->change_mask);
+
+ if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t idx;
+
+ switch (info->params[i].id) {
+ case SPA_PARAM_EnumFormat:
+ idx = IDX_EnumFormat;
+ break;
+ case SPA_PARAM_Format:
+ idx = IDX_Format;
+ break;
+ case SPA_PARAM_Latency:
+ idx = IDX_Latency;
+ break;
+ default:
+ continue;
+ }
+
+ if (!this->add_listener &&
+ this->follower_params_flags[idx] == info->params[i].flags)
+ continue;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->follower_params_flags[idx] = info->params[i].flags;
+ this->params[idx].flags =
+ (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
+ (info->params[i].flags & SPA_PARAM_INFO_READWRITE);
+
+ if (this->add_listener)
+ continue;
+
+ if (idx == IDX_Latency) {
+ res = recalc_latency(this, direction, port_id);
+ spa_log_debug(this->log, "latency: %d (%s)", res,
+ spa_strerror(res));
+ }
+ if (idx == IDX_EnumFormat) {
+ spa_log_debug(this->log, "new formats");
+ configure_format(this, 0, NULL);
+ }
+
+ this->params[idx].user++;
+ spa_log_debug(this->log, "param %d changed", info->params[i].id);
+ }
+ }
+ emit_node_info(this, false);
+
+ if (this->target == this->follower)
+ spa_node_emit_port_info(&this->hooks, direction, port_id, info);
+}
+
+static void follower_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct impl *this = data;
+
+ if (this->target != this->follower)
+ return;
+
+ spa_log_trace(this->log, "%p: result %d %d", this, seq, res);
+ spa_node_emit_result(&this->hooks, seq, res, type, result);
+}
+
+static void follower_event(void *data, const struct spa_event *event)
+{
+ struct impl *this = data;
+
+ spa_log_trace(this->log, "%p: event %d", this, SPA_EVENT_TYPE(event));
+
+ switch (SPA_NODE_EVENT_ID(event)) {
+ case SPA_NODE_EVENT_Error:
+ /* Forward errors */
+ spa_node_emit_event(&this->hooks, event);
+ break;
+ default:
+ /* Ignore other events */
+ break;
+ }
+}
+
+static const struct spa_node_events follower_node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .info = follower_info,
+ .port_info = follower_port_info,
+ .result = follower_result,
+ .event = follower_event,
+};
+
+static int follower_ready(void *data, int status)
+{
+ struct impl *this = data;
+
+ spa_log_trace_fp(this->log, "%p: ready %d", this, status);
+
+ if (!this->started) {
+ spa_log_info(this->log, "%p: ready stopped node", this);
+ return -EIO;
+ }
+
+ if (this->target != this->follower) {
+ this->driver = true;
+
+ if (this->direction == SPA_DIRECTION_OUTPUT) {
+ int retry = 8;
+ while (retry--) {
+ status = spa_node_process(this->convert);
+ if (status & SPA_STATUS_HAVE_DATA)
+ break;
+
+ if (status & SPA_STATUS_NEED_DATA) {
+ status = spa_node_process(this->follower);
+ if (!(status & SPA_STATUS_HAVE_DATA))
+ break;
+ }
+ }
+
+ }
+ }
+
+ return spa_node_call_ready(&this->callbacks, status);
+}
+
+static int follower_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_id)
+{
+ int res;
+ struct impl *this = data;
+
+ if (this->target != this->follower && this->convert)
+ res = spa_node_port_reuse_buffer(this->convert, port_id, buffer_id);
+ else
+ res = spa_node_call_reuse_buffer(&this->callbacks, port_id, buffer_id);
+
+ return res;
+}
+
+static int follower_xrun(void *data, uint64_t trigger, uint64_t delay, struct spa_pod *info)
+{
+ struct impl *this = data;
+ return spa_node_call_xrun(&this->callbacks, trigger, delay, info);
+}
+
+static const struct spa_node_callbacks follower_node_callbacks = {
+ SPA_VERSION_NODE_CALLBACKS,
+ .ready = follower_ready,
+ .reuse_buffer = follower_reuse_buffer,
+ .xrun = follower_xrun,
+};
+
+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 l;
+ 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);
+
+
+ if (events->info || events->port_info) {
+ this->add_listener = true;
+
+ spa_zero(l);
+ spa_node_add_listener(this->follower, &l, &follower_node_events, this);
+ spa_hook_remove(&l);
+
+ if (this->convert) {
+ spa_zero(l);
+ spa_node_add_listener(this->convert, &l, &convert_node_events, this);
+ spa_hook_remove(&l);
+ }
+ this->add_listener = false;
+
+ emit_node_info(this, 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_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ return spa_node_sync(this->follower, seq);
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (direction != this->direction)
+ return -EINVAL;
+
+ return spa_node_add_port(this->target, direction, port_id, props);
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (direction != this->direction)
+ return -EINVAL;
+
+ return spa_node_remove_port(this->target, direction, port_id);
+}
+
+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;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ if (direction != this->direction)
+ port_id++;
+
+ spa_log_debug(this->log, "%p: %d %u", this, seq, id);
+
+ return spa_node_port_enum_params(this->target, seq, direction, port_id, id,
+ start, num, filter);
+}
+
+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;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_debug(this->log, " %d %d %d %d", port_id, id, direction, this->direction);
+
+ if (direction != this->direction)
+ port_id++;
+
+ if ((res = spa_node_port_set_param(this->target, direction, port_id, id,
+ flags, param)) < 0)
+ return res;
+
+ if ((id == SPA_PARAM_Latency) &&
+ direction == this->direction) {
+ if ((res = spa_node_port_set_param(this->follower, direction, 0, id,
+ flags, param)) < 0)
+ return res;
+ }
+
+ return res;
+}
+
+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);
+
+ spa_log_debug(this->log, "set io %d %d %d %d", port_id, id, direction, this->direction);
+
+ if (direction != this->direction)
+ port_id++;
+
+ return spa_node_port_set_io(this->target, direction, port_id, id, data, size);
+}
+
+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;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (direction != this->direction)
+ port_id++;
+
+ spa_log_debug(this->log, "%p: %d %d:%d", this,
+ n_buffers, direction, port_id);
+
+ if ((res = spa_node_port_use_buffers(this->target,
+ direction, port_id, flags, buffers, n_buffers)) < 0)
+ return res;
+
+ return res;
+}
+
+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);
+
+ return spa_node_port_reuse_buffer(this->target, port_id, buffer_id);
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ int status = 0, fstatus, retry = 8;
+
+ if (!this->started) {
+ spa_log_warn(this->log, "%p: scheduling stopped node", this);
+ return -EIO;
+ }
+
+ spa_log_trace_fp(this->log, "%p: process convert:%p driver:%d",
+ this, this->convert, this->driver);
+
+ 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);
+ }
+
+ if (this->direction == SPA_DIRECTION_INPUT) {
+ /* an input node (sink).
+ * 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;
+ /* schedule the follower when the converter needed
+ * a recycled buffer */
+ if (status == -EPIPE || status == 0)
+ status = SPA_STATUS_HAVE_DATA;
+ else if (status < 0)
+ break;
+
+ 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);
+ if (fstatus < 0) {
+ status = fstatus;
+ break;
+ }
+ /* if the follower doesn't need more data or is
+ * drained we can stop */
+ if ((fstatus & SPA_STATUS_NEED_DATA) == 0 ||
+ (fstatus & SPA_STATUS_DRAINED))
+ break;
+ }
+ /* the converter needs more data */
+ if ((status & SPA_STATUS_NEED_DATA))
+ break;
+ }
+ } else if (!this->driver) {
+ bool done = false;
+ while (retry--) {
+ /* 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;
+ 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)
+ break;
+
+ if (status & SPA_STATUS_NEED_DATA) {
+ /* the converter needs more data, schedule the
+ * follower */
+ fstatus = spa_node_process(this->follower);
+ if (fstatus < 0) {
+ status = fstatus;
+ break;
+ }
+ /* if the follower didn't produce more data or is
+ * not drained we can stop now */
+ 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);
+ }
+ spa_log_trace_fp(this->log, "%p: process status:%d", this, status);
+
+ this->driver = false;
+
+ return status;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ spa_hook_remove(&this->follower_listener);
+ spa_node_set_callbacks(this->follower, NULL, NULL);
+
+ spa_handle_clear(this->hnd_convert);
+
+ if (this->buffers)
+ free(this->buffers);
+ this->buffers = NULL;
+
+ return 0;
+}
+
+
+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);
+
+ return size;
+}
+
+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;
+ void *iface;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(this->log, log_topic);
+
+ this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+
+ if (info == NULL ||
+ (str = spa_dict_lookup(info, "audio.adapt.follower")) == NULL)
+ return -EINVAL;
+
+ sscanf(str, "pointer:%p", &this->follower);
+ if (this->follower == NULL)
+ return -EINVAL;
+
+ if (this->cpu)
+ this->max_align = spa_cpu_get_max_align(this->cpu);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ 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);
+
+ spa_handle_get_interface(this->hnd_convert, SPA_TYPE_INTERFACE_Node, &iface);
+ this->convert = iface;
+ this->target = this->convert;
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.flags = SPA_NODE_FLAG_RT |
+ 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);
+ this->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ 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_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ spa_node_add_listener(this->follower,
+ &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);
+
+ configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp);
+
+ link_io(this);
+
+ 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_audioadapter_factory = {
+ .version = SPA_VERSION_HANDLE_FACTORY,
+ .name = SPA_NAME_AUDIO_ADAPT,
+ .get_size = impl_get_size,
+ .init = impl_init,
+ .enum_interface_info = impl_enum_interface_info,
+};
diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c
new file mode 100644
index 0000000..970df16
--- /dev/null
+++ b/spa/plugins/audioconvert/audioconvert.c
@@ -0,0 +1,2945 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <limits.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/cpu.h>
+#include <spa/support/log.h>
+#include <spa/utils/result.h>
+#include <spa/utils/list.h>
+#include <spa/utils/json.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/types.h>
+
+#include "volume-ops.h"
+#include "fmt-ops.h"
+#include "channelmix-ops.h"
+#include "resample.h"
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT log_topic
+static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.audioconvert");
+
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+
+#define MAX_ALIGN FMT_OPS_MAX_ALIGN
+#define MAX_BUFFERS 32
+#define MAX_DATAS SPA_AUDIO_MAX_CHANNELS
+#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1)
+
+#define DEFAULT_MUTE false
+#define DEFAULT_VOLUME VOLUME_NORM
+
+struct volumes {
+ bool mute;
+ uint32_t n_volumes;
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+};
+
+static void init_volumes(struct volumes *vol)
+{
+ uint32_t i;
+ vol->mute = DEFAULT_MUTE;
+ vol->n_volumes = 0;
+ for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
+ vol->volumes[i] = DEFAULT_VOLUME;
+}
+
+struct props {
+ float volume;
+ uint32_t n_channels;
+ uint32_t channel_map[SPA_AUDIO_MAX_CHANNELS];
+ struct volumes channel;
+ struct volumes soft;
+ struct volumes monitor;
+ unsigned int have_soft_volume:1;
+ unsigned int mix_disabled:1;
+ unsigned int resample_disabled:1;
+ unsigned int resample_quality;
+ double rate;
+};
+
+static void props_reset(struct props *props)
+{
+ uint32_t i;
+ props->volume = DEFAULT_VOLUME;
+ props->n_channels = 0;
+ for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
+ props->channel_map[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+ init_volumes(&props->channel);
+ init_volumes(&props->soft);
+ init_volumes(&props->monitor);
+ props->have_soft_volume = false;
+ props->mix_disabled = false;
+ props->resample_disabled = false;
+ props->resample_quality = RESAMPLE_DEFAULT_QUALITY;
+ props->rate = 1.0;
+}
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_QUEUED (1<<0)
+ uint32_t flags;
+ struct spa_list link;
+ struct spa_buffer *buf;
+ void *datas[MAX_DATAS];
+};
+
+struct port {
+ uint32_t direction;
+ uint32_t id;
+
+ struct spa_io_buffers *io;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+ char position[16];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_audio_info format;
+ unsigned int have_format:1;
+ unsigned int is_dsp:1;
+ unsigned int is_monitor:1;
+ unsigned int is_control:1;
+
+ uint32_t blocks;
+ uint32_t stride;
+
+ const struct spa_pod_sequence *ctrl;
+ uint32_t ctrl_offset;
+
+ struct spa_list queue;
+};
+
+struct dir {
+ struct port *ports[MAX_PORTS];
+ uint32_t n_ports;
+
+ enum spa_direction direction;
+ enum spa_param_port_config_mode mode;
+
+ struct spa_audio_info format;
+ unsigned int have_format:1;
+ unsigned int have_profile:1;
+ struct spa_latency_info latency;
+
+ uint32_t remap[MAX_PORTS];
+
+ struct convert conv;
+ unsigned int need_remap:1;
+ unsigned int is_passthrough:1;
+ unsigned int control:1;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_cpu *cpu;
+
+ uint32_t cpu_flags;
+ uint32_t max_align;
+ uint32_t quantum_limit;
+ enum spa_direction direction;
+
+ 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;
+ unsigned int monitor_channel_volumes:1;
+
+ struct dir dir[2];
+ struct channelmix mix;
+ struct resample resample;
+ struct volume volume;
+ double rate_scale;
+
+ uint32_t in_offset;
+ uint32_t out_offset;
+ unsigned int started:1;
+ unsigned int setup:1;
+ unsigned int resample_peaks:1;
+ unsigned int is_passthrough:1;
+ unsigned int drained:1;
+ unsigned int rate_adjust:1;
+
+ uint32_t empty_size;
+ float *empty;
+ float *scratch;
+ float *tmp[2];
+ float *tmp_datas[2][MAX_PORTS];
+};
+
+#define CHECK_PORT(this,d,p) ((p) < this->dir[d].n_ports)
+#define GET_PORT(this,d,p) (this->dir[d].ports[p])
+#define GET_IN_PORT(this,p) GET_PORT(this,SPA_DIRECTION_INPUT,p)
+#define GET_OUT_PORT(this,p) GET_PORT(this,SPA_DIRECTION_OUTPUT,p)
+
+#define PORT_IS_DSP(this,d,p) (GET_PORT(this,d,p)->is_dsp)
+#define PORT_IS_CONTROL(this,d,p) (GET_PORT(this,d,p)->is_control)
+
+static void set_volume(struct impl *this);
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ 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[3];
+ uint32_t n_items = 0;
+
+ if (PORT_IS_DSP(this, port->direction, port->id)) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNEL, port->position);
+ if (port->is_monitor)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_MONITOR, "true");
+ } else if (PORT_IS_CONTROL(this, port->direction, port->id)) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi");
+ }
+ port->info.props = &SPA_DICT_INIT(items, n_items);
+
+ 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,
+ uint32_t position, bool is_dsp, bool is_monitor, bool is_control)
+{
+ struct port *port = GET_PORT(this, direction, port_id);
+ const char *name;
+
+ 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;
+
+ name = spa_debug_type_find_short_name(spa_type_audio_channel, position);
+ snprintf(port->position, sizeof(port->position), "%s", name ? name : "UNK");
+
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF |
+ SPA_PORT_FLAG_DYNAMIC_DATA;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->n_buffers = 0;
+ port->have_format = false;
+ port->is_monitor = is_monitor;
+ port->is_dsp = is_dsp;
+ if (port->is_dsp) {
+ port->format.media_type = SPA_MEDIA_TYPE_audio;
+ port->format.media_subtype = SPA_MEDIA_SUBTYPE_dsp;
+ port->format.info.dsp.format = SPA_AUDIO_FORMAT_DSP_F32;
+ port->blocks = 1;
+ port->stride = 4;
+ }
+ port->is_control = is_control;
+ if (port->is_control) {
+ port->format.media_type = SPA_MEDIA_TYPE_application;
+ port->format.media_subtype = SPA_MEDIA_SUBTYPE_control;
+ port->blocks = 1;
+ port->stride = 1;
+ }
+ spa_list_init(&port->queue);
+
+ spa_log_info(this->log, "%p: add port %d:%d position:%s %d %d %d",
+ this, direction, port_id, port->position, is_dsp, is_monitor, is_control);
+ emit_port_info(this, port, true);
+
+ return 0;
+}
+
+static int 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_audio_raw_build(&b, SPA_PARAM_PORT_CONFIG_format,
+ &dir->format.info.raw);
+ }
+ param = spa_pod_builder_pop(&b, &f[0]);
+ break;
+ }
+ case SPA_PARAM_PropInfo:
+ {
+ struct props *p = &this->props;
+ struct spa_pod_frame f[2];
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume),
+ SPA_PROP_INFO_description, SPA_POD_String("Volume"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute),
+ SPA_PROP_INFO_description, SPA_POD_String("Mute"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->channel.mute));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_channelVolumes),
+ SPA_PROP_INFO_description, SPA_POD_String("Channel Volumes"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
+ SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+ break;
+ case 3:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_channelMap),
+ SPA_PROP_INFO_description, SPA_POD_String("Channel Map"),
+ SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_CHANNEL_UNKNOWN),
+ SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+ break;
+ case 4:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_monitorMute),
+ SPA_PROP_INFO_description, SPA_POD_String("Monitor Mute"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->monitor.mute));
+ break;
+ case 5:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_monitorVolumes),
+ SPA_PROP_INFO_description, SPA_POD_String("Monitor Volumes"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
+ SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+ break;
+ case 6:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_softMute),
+ SPA_PROP_INFO_description, SPA_POD_String("Soft Mute"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->soft.mute));
+ break;
+ case 7:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_softVolumes),
+ SPA_PROP_INFO_description, SPA_POD_String("Soft Volumes"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0),
+ SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+ break;
+ case 8:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("monitor.channel-volumes"),
+ SPA_PROP_INFO_description, SPA_POD_String("Monitor channel volume"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+ this->monitor_channel_volumes),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 9:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.disable"),
+ SPA_PROP_INFO_description, SPA_POD_String("Disable Channel mixing"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->mix_disabled),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 10:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.normalize"),
+ SPA_PROP_INFO_description, SPA_POD_String("Normalize Volumes"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+ SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_NORMALIZE)),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 11:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.mix-lfe"),
+ SPA_PROP_INFO_description, SPA_POD_String("Mix LFE into channels"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+ SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_MIX_LFE)),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 12:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix"),
+ SPA_PROP_INFO_description, SPA_POD_String("Enable upmixing"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+ SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_UPMIX)),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 13:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.lfe-cutoff"),
+ SPA_PROP_INFO_description, SPA_POD_String("LFE cutoff frequency"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+ this->mix.lfe_cutoff, 0.0, 1000.0),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 14:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.fc-cutoff"),
+ SPA_PROP_INFO_description, SPA_POD_String("FC cutoff frequency (Hz)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+ this->mix.fc_cutoff, 0.0, 48000.0),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 15:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.rear-delay"),
+ SPA_PROP_INFO_description, SPA_POD_String("Rear channels delay (ms)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+ this->mix.rear_delay, 0.0, 1000.0),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 16:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.stereo-widen"),
+ SPA_PROP_INFO_description, SPA_POD_String("Stereo widen"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+ this->mix.widen, 0.0, 1.0),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 17:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.hilbert-taps"),
+ SPA_PROP_INFO_description, SPA_POD_String("Taps for phase shift of rear"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(
+ this->mix.hilbert_taps, 0, MAX_TAPS),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 18:
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix-method"),
+ SPA_PROP_INFO_description, SPA_POD_String("Upmix method to use"),
+ SPA_PROP_INFO_type, SPA_POD_String(
+ channelmix_upmix_info[this->mix.upmix].label),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true),
+ 0);
+
+ spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+ spa_pod_builder_push_struct(&b, &f[1]);
+ SPA_FOR_EACH_ELEMENT_VAR(channelmix_upmix_info, i) {
+ spa_pod_builder_string(&b, i->label);
+ spa_pod_builder_string(&b, i->description);
+ }
+ spa_pod_builder_pop(&b, &f[1]);
+ param = spa_pod_builder_pop(&b, &f[0]);
+ break;
+ case 19:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_rate),
+ SPA_PROP_INFO_description, SPA_POD_String("Rate scaler"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Double(p->rate, 0.0, 10.0));
+ break;
+ case 20:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_quality),
+ SPA_PROP_INFO_name, SPA_POD_String("resample.quality"),
+ SPA_PROP_INFO_description, SPA_POD_String("Resample Quality"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->resample_quality, 0, 14),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 21:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("resample.disable"),
+ SPA_PROP_INFO_description, SPA_POD_String("Disable Resampling"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->resample_disabled),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 22:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_name, SPA_POD_String("dither.noise"),
+ SPA_PROP_INFO_description, SPA_POD_String("Add noise bits"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(this->dir[1].conv.noise_bits, 0, 16),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 23:
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_name, SPA_POD_String("dither.method"),
+ SPA_PROP_INFO_description, SPA_POD_String("The dithering method"),
+ SPA_PROP_INFO_type, SPA_POD_String(
+ dither_method_info[this->dir[1].conv.method].label),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true),
+ 0);
+ spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+ spa_pod_builder_push_struct(&b, &f[1]);
+ SPA_FOR_EACH_ELEMENT_VAR(dither_method_info, i) {
+ spa_pod_builder_string(&b, i->label);
+ spa_pod_builder_string(&b, i->description);
+ }
+ spa_pod_builder_pop(&b, &f[1]);
+ param = spa_pod_builder_pop(&b, &f[0]);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod_frame f[2];
+
+ switch (result.index) {
+ case 0:
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_Props, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_volume, SPA_POD_Float(p->volume),
+ SPA_PROP_mute, SPA_POD_Bool(p->channel.mute),
+ SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float,
+ p->channel.n_volumes,
+ p->channel.volumes),
+ SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id,
+ p->n_channels,
+ p->channel_map),
+ SPA_PROP_softMute, SPA_POD_Bool(p->soft.mute),
+ SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float,
+ p->soft.n_volumes,
+ p->soft.volumes),
+ SPA_PROP_monitorMute, SPA_POD_Bool(p->monitor.mute),
+ SPA_PROP_monitorVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float,
+ p->monitor.n_volumes,
+ p->monitor.volumes),
+ 0);
+ spa_pod_builder_prop(&b, SPA_PROP_params, 0);
+ spa_pod_builder_push_struct(&b, &f[1]);
+ spa_pod_builder_string(&b, "monitor.channel-volumes");
+ spa_pod_builder_bool(&b, this->monitor_channel_volumes);
+ spa_pod_builder_string(&b, "channelmix.disable");
+ spa_pod_builder_bool(&b, this->props.mix_disabled);
+ spa_pod_builder_string(&b, "channelmix.normalize");
+ spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
+ CHANNELMIX_OPTION_NORMALIZE));
+ spa_pod_builder_string(&b, "channelmix.mix-lfe");
+ spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
+ CHANNELMIX_OPTION_MIX_LFE));
+ spa_pod_builder_string(&b, "channelmix.upmix");
+ spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options,
+ CHANNELMIX_OPTION_UPMIX));
+ spa_pod_builder_string(&b, "channelmix.lfe-cutoff");
+ spa_pod_builder_float(&b, this->mix.lfe_cutoff);
+ spa_pod_builder_string(&b, "channelmix.fc-cutoff");
+ spa_pod_builder_float(&b, this->mix.fc_cutoff);
+ spa_pod_builder_string(&b, "channelmix.rear-delay");
+ spa_pod_builder_float(&b, this->mix.rear_delay);
+ spa_pod_builder_string(&b, "channelmix.stereo-widen");
+ spa_pod_builder_float(&b, this->mix.widen);
+ spa_pod_builder_string(&b, "channelmix.hilbert-taps");
+ spa_pod_builder_int(&b, this->mix.hilbert_taps);
+ spa_pod_builder_string(&b, "channelmix.upmix-method");
+ spa_pod_builder_string(&b, channelmix_upmix_info[this->mix.upmix].label);
+ spa_pod_builder_string(&b, "resample.quality");
+ spa_pod_builder_int(&b, p->resample_quality);
+ spa_pod_builder_string(&b, "resample.disable");
+ spa_pod_builder_bool(&b, p->resample_disabled);
+ spa_pod_builder_string(&b, "dither.noise");
+ spa_pod_builder_int(&b, this->dir[1].conv.noise_bits);
+ spa_pod_builder_string(&b, "dither.method");
+ spa_pod_builder_string(&b, dither_method_info[this->dir[1].conv.method].label);
+ spa_pod_builder_pop(&b, &f[1]);
+ param = spa_pod_builder_pop(&b, &f[0]);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return 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 audioconvert_set_param(struct impl *this, const char *k, const char *s)
+{
+ if (spa_streq(k, "monitor.channel-volumes"))
+ this->monitor_channel_volumes = spa_atob(s);
+ else if (spa_streq(k, "channelmix.disable"))
+ this->props.mix_disabled = spa_atob(s);
+ else if (spa_streq(k, "channelmix.normalize"))
+ SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_NORMALIZE, spa_atob(s));
+ else if (spa_streq(k, "channelmix.mix-lfe"))
+ SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_MIX_LFE, spa_atob(s));
+ else if (spa_streq(k, "channelmix.upmix"))
+ SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_UPMIX, spa_atob(s));
+ else if (spa_streq(k, "channelmix.lfe-cutoff"))
+ spa_atof(s, &this->mix.lfe_cutoff);
+ else if (spa_streq(k, "channelmix.fc-cutoff"))
+ spa_atof(s, &this->mix.fc_cutoff);
+ else if (spa_streq(k, "channelmix.rear-delay"))
+ spa_atof(s, &this->mix.rear_delay);
+ else if (spa_streq(k, "channelmix.stereo-widen"))
+ spa_atof(s, &this->mix.widen);
+ else if (spa_streq(k, "channelmix.hilbert-taps"))
+ spa_atou32(s, &this->mix.hilbert_taps, 0);
+ else if (spa_streq(k, "channelmix.upmix-method"))
+ this->mix.upmix = channelmix_upmix_from_label(s);
+ else if (spa_streq(k, "resample.quality"))
+ this->props.resample_quality = atoi(s);
+ else if (spa_streq(k, "resample.disable"))
+ this->props.resample_disabled = spa_atob(s);
+ else if (spa_streq(k, "dither.noise"))
+ spa_atou32(s, &this->dir[1].conv.noise_bits, 0);
+ else if (spa_streq(k, "dither.method"))
+ this->dir[1].conv.method = dither_method_from_label(s);
+ else
+ return 0;
+ return 1;
+}
+
+static 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 += audioconvert_set_param(this, name, value);
+ }
+ if (changed) {
+ channelmix_init(&this->mix);
+ }
+ return changed;
+}
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ struct props *p = &this->props;
+ bool have_channel_volume = false;
+ bool have_soft_volume = false;
+ int changed = 0;
+ uint32_t n;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &p->volume) == 0)
+ changed++;
+ break;
+ case SPA_PROP_mute:
+ if (spa_pod_get_bool(&prop->value, &p->channel.mute) == 0) {
+ have_channel_volume = true;
+ changed++;
+ }
+ break;
+ case SPA_PROP_channelVolumes:
+ if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ p->channel.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
+ have_channel_volume = true;
+ p->channel.n_volumes = n;
+ changed++;
+ }
+ break;
+ case SPA_PROP_channelMap:
+ if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ p->channel_map, SPA_AUDIO_MAX_CHANNELS)) > 0) {
+ p->n_channels = n;
+ changed++;
+ }
+ break;
+ case SPA_PROP_softMute:
+ if (spa_pod_get_bool(&prop->value, &p->soft.mute) == 0) {
+ have_soft_volume = true;
+ changed++;
+ }
+ break;
+ case SPA_PROP_softVolumes:
+ if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ p->soft.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
+ have_soft_volume = true;
+ p->soft.n_volumes = n;
+ changed++;
+ }
+ break;
+ case SPA_PROP_monitorMute:
+ if (spa_pod_get_bool(&prop->value, &p->monitor.mute) == 0)
+ changed++;
+ break;
+ case SPA_PROP_monitorVolumes:
+ if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ p->monitor.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) {
+ p->monitor.n_volumes = n;
+ changed++;
+ }
+ break;
+ case SPA_PROP_rate:
+ spa_pod_get_double(&prop->value, &p->rate);
+ if (!this->rate_adjust && p->rate != 1.0) {
+ this->rate_adjust = true;
+ spa_log_info(this->log, "%p: activating adaptive resampler",
+ this);
+ }
+ break;
+ case SPA_PROP_params:
+ changed += parse_prop_params(this, &prop->value);
+ break;
+ default:
+ break;
+ }
+ }
+ if (changed) {
+ if (have_soft_volume)
+ p->have_soft_volume = true;
+ else if (have_channel_volume)
+ p->have_soft_volume = false;
+
+ set_volume(this);
+ }
+ return changed;
+}
+
+static int apply_midi(struct impl *this, const struct spa_pod *value)
+{
+ const uint8_t *val = SPA_POD_BODY(value);
+ uint32_t size = SPA_POD_BODY_SIZE(value);
+ struct props *p = &this->props;
+
+ if (size < 3)
+ return -EINVAL;
+
+ if ((val[0] & 0xf0) != 0xb0 || val[1] != 7)
+ return 0;
+
+ p->volume = val[2] / 127.0f;
+ set_volume(this);
+ return 1;
+}
+
+static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode,
+ enum spa_direction direction, bool monitor, bool control, struct spa_audio_info *info)
+{
+ struct dir *dir;
+ uint32_t i;
+
+ 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_info(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++) {
+ spa_node_emit_port_info(&this->hooks, direction, i, NULL);
+ if (this->monitor && direction == SPA_DIRECTION_INPUT)
+ spa_node_emit_port_info(&this->hooks, SPA_DIRECTION_OUTPUT, i+1, NULL);
+ }
+
+ this->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 = info->info.raw.channels;
+ dir->format = *info;
+ dir->format.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32;
+ dir->format.info.raw.rate = 0;
+ dir->have_format = true;
+ } else {
+ 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, info->info.raw.position[i], true, false, false);
+ if (this->monitor && direction == SPA_DIRECTION_INPUT)
+ init_port(this, SPA_DIRECTION_OUTPUT, i+1,
+ info->info.raw.position[i], true, true, false);
+ }
+ break;
+ }
+ case SPA_PARAM_PORT_CONFIG_MODE_convert:
+ {
+ dir->n_ports = 1;
+ dir->have_format = false;
+ init_port(this, direction, 0, 0, false, false, false);
+ break;
+ }
+ 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, 0, false, false, true);
+ }
+
+ 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_audio_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_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.format == 0 ||
+ info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+
+ 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 int int32_cmp(const void *v1, const void *v2)
+{
+ int32_t a1 = *(int32_t*)v1;
+ int32_t a2 = *(int32_t*)v2;
+ if (a1 == 0 && a2 != 0)
+ return 1;
+ if (a2 == 0 && a1 != 0)
+ return -1;
+ return a1 - a2;
+}
+
+static int setup_in_convert(struct impl *this)
+{
+ uint32_t i, j;
+ struct dir *in = &this->dir[SPA_DIRECTION_INPUT];
+ struct spa_audio_info src_info, dst_info;
+ int res;
+ bool remap = false;
+
+ src_info = in->format;
+ dst_info = src_info;
+ dst_info.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32;
+
+ spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this,
+ spa_debug_type_find_name(spa_type_audio_format, src_info.info.raw.format),
+ src_info.info.raw.channels,
+ src_info.info.raw.rate,
+ spa_debug_type_find_name(spa_type_audio_format, dst_info.info.raw.format),
+ dst_info.info.raw.channels,
+ dst_info.info.raw.rate);
+
+ qsort(dst_info.info.raw.position, dst_info.info.raw.channels,
+ sizeof(uint32_t), int32_cmp);
+
+ for (i = 0; i < src_info.info.raw.channels; i++) {
+ for (j = 0; j < dst_info.info.raw.channels; j++) {
+ if (src_info.info.raw.position[i] !=
+ dst_info.info.raw.position[j])
+ continue;
+ in->remap[i] = j;
+ if (i != j)
+ remap = true;
+ spa_log_debug(this->log, "%p: channel %d (%d) -> %d (%s -> %s)", this,
+ i, in->remap[i], j,
+ spa_debug_type_find_short_name(spa_type_audio_channel,
+ src_info.info.raw.position[i]),
+ spa_debug_type_find_short_name(spa_type_audio_channel,
+ dst_info.info.raw.position[j]));
+ dst_info.info.raw.position[j] = -1;
+ break;
+ }
+ }
+ if (in->conv.free)
+ convert_free(&in->conv);
+
+ in->conv.src_fmt = src_info.info.raw.format;
+ in->conv.dst_fmt = dst_info.info.raw.format;
+ in->conv.n_channels = dst_info.info.raw.channels;
+ in->conv.cpu_flags = this->cpu_flags;
+ in->need_remap = remap;
+
+ if ((res = convert_init(&in->conv)) < 0)
+ return res;
+
+ spa_log_debug(this->log, "%p: got converter features %08x:%08x passthrough:%d remap:%d %s", this,
+ this->cpu_flags, in->conv.cpu_flags, in->conv.is_passthrough,
+ remap, in->conv.func_name);
+
+ return 0;
+}
+
+static void fix_volumes(struct impl *this, struct volumes *vols, uint32_t channels)
+{
+ float s;
+ uint32_t i;
+ spa_log_debug(this->log, "%p %d -> %d", this, vols->n_volumes, channels);
+ if (vols->n_volumes > 0) {
+ s = 0.0f;
+ for (i = 0; i < vols->n_volumes; i++)
+ s += vols->volumes[i];
+ s /= vols->n_volumes;
+ } else {
+ s = 1.0f;
+ }
+ vols->n_volumes = channels;
+ for (i = 0; i < vols->n_volumes; i++)
+ vols->volumes[i] = s;
+}
+
+static int remap_volumes(struct impl *this, const struct spa_audio_info *info)
+{
+ struct props *p = &this->props;
+ uint32_t i, j, target = info->info.raw.channels;
+
+ for (i = 0; i < p->n_channels; i++) {
+ for (j = i; j < target; j++) {
+ spa_log_debug(this->log, "%d %d: %d <-> %d", i, j,
+ p->channel_map[i], info->info.raw.position[j]);
+ if (p->channel_map[i] != info->info.raw.position[j])
+ continue;
+ if (i != j) {
+ SPA_SWAP(p->channel_map[i], p->channel_map[j]);
+ SPA_SWAP(p->channel.volumes[i], p->channel.volumes[j]);
+ SPA_SWAP(p->soft.volumes[i], p->soft.volumes[j]);
+ SPA_SWAP(p->monitor.volumes[i], p->monitor.volumes[j]);
+ }
+ break;
+ }
+ }
+ p->n_channels = target;
+ for (i = 0; i < p->n_channels; i++)
+ p->channel_map[i] = info->info.raw.position[i];
+
+ if (target == 0)
+ return 0;
+ if (p->channel.n_volumes != target)
+ fix_volumes(this, &p->channel, target);
+ if (p->soft.n_volumes != target)
+ fix_volumes(this, &p->soft, target);
+ if (p->monitor.n_volumes != target)
+ fix_volumes(this, &p->monitor, target);
+
+ return 1;
+}
+
+static void set_volume(struct impl *this)
+{
+ struct volumes *vol;
+ uint32_t i;
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+ struct dir *dir = &this->dir[this->direction];
+
+ spa_log_debug(this->log, "%p have_format:%d", this, dir->have_format);
+
+ if (dir->have_format)
+ remap_volumes(this, &dir->format);
+
+ if (this->mix.set_volume == NULL)
+ return;
+
+ if (this->props.have_soft_volume)
+ vol = &this->props.soft;
+ else
+ vol = &this->props.channel;
+
+ for (i = 0; i < vol->n_volumes; i++)
+ volumes[i] = vol->volumes[dir->remap[i]];
+
+ channelmix_set_volume(&this->mix, this->props.volume, vol->mute,
+ vol->n_volumes, volumes);
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].user++;
+}
+
+static char *format_position(char *str, size_t len, uint32_t channels, uint32_t *position)
+{
+ uint32_t i, idx = 0;
+ for (i = 0; i < channels; i++)
+ idx += snprintf(str + idx, len - idx, "%s%s", i == 0 ? "" : " ",
+ spa_debug_type_find_short_name(spa_type_audio_channel,
+ position[i]));
+ return str;
+}
+
+static int setup_channelmix(struct impl *this)
+{
+ struct dir *in = &this->dir[SPA_DIRECTION_INPUT];
+ struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT];
+ uint32_t i, src_chan, dst_chan, p;
+ uint64_t src_mask, dst_mask;
+ char str[1024];
+ int res;
+
+ src_chan = in->format.info.raw.channels;
+ dst_chan = out->format.info.raw.channels;
+
+ for (i = 0, src_mask = 0; i < src_chan; i++) {
+ p = in->format.info.raw.position[i];
+ src_mask |= 1ULL << (p < 64 ? p : 0);
+ }
+ for (i = 0, dst_mask = 0; i < dst_chan; i++) {
+ p = out->format.info.raw.position[i];
+ dst_mask |= 1ULL << (p < 64 ? p : 0);
+ }
+
+ spa_log_info(this->log, "in %s (%016"PRIx64")", format_position(str, sizeof(str),
+ src_chan, in->format.info.raw.position), src_mask);
+ spa_log_info(this->log, "out %s (%016"PRIx64")", format_position(str, sizeof(str),
+ dst_chan, out->format.info.raw.position), dst_mask);
+
+ spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d %08"PRIx64":%08"PRIx64, this,
+ spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
+ src_chan,
+ in->format.info.raw.rate,
+ spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
+ dst_chan,
+ in->format.info.raw.rate,
+ src_mask, dst_mask);
+
+ this->mix.src_chan = src_chan;
+ this->mix.src_mask = src_mask;
+ this->mix.dst_chan = dst_chan;
+ this->mix.dst_mask = dst_mask;
+ this->mix.cpu_flags = this->cpu_flags;
+ this->mix.log = this->log;
+ this->mix.freq = in->format.info.raw.rate;
+
+ if ((res = channelmix_init(&this->mix)) < 0)
+ return res;
+
+ set_volume(this);
+
+ spa_log_debug(this->log, "%p: got channelmix features %08x:%08x flags:%08x %s",
+ this, this->cpu_flags, this->mix.cpu_flags,
+ this->mix.flags, this->mix.func_name);
+ return 0;
+}
+
+static int setup_resample(struct impl *this)
+{
+ struct dir *in = &this->dir[SPA_DIRECTION_INPUT];
+ struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT];
+ int res;
+
+ spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this,
+ spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
+ out->format.info.raw.channels,
+ in->format.info.raw.rate,
+ spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32),
+ out->format.info.raw.channels,
+ out->format.info.raw.rate);
+
+ if (this->resample.free)
+ resample_free(&this->resample);
+
+ this->resample.channels = out->format.info.raw.channels;
+ this->resample.i_rate = in->format.info.raw.rate;
+ this->resample.o_rate = out->format.info.raw.rate;
+ this->resample.log = this->log;
+ this->resample.quality = this->props.resample_quality;
+ this->resample.cpu_flags = this->cpu_flags;
+
+ this->rate_adjust = this->props.rate != 1.0;
+
+ if (this->resample_peaks)
+ res = resample_peaks_init(&this->resample);
+ else
+ res = resample_native_init(&this->resample);
+
+ spa_log_debug(this->log, "%p: got resample features %08x:%08x %s",
+ this, this->cpu_flags, this->resample.cpu_flags,
+ this->resample.func_name);
+ return res;
+}
+
+static int calc_width(struct spa_audio_info *info)
+{
+ switch (info->info.raw.format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_U8P:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_S8P:
+ case SPA_AUDIO_FORMAT_ULAW:
+ case SPA_AUDIO_FORMAT_ALAW:
+ return 1;
+ case SPA_AUDIO_FORMAT_S16P:
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ return 2;
+ case SPA_AUDIO_FORMAT_S24P:
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ return 3;
+ case SPA_AUDIO_FORMAT_F64P:
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return 8;
+ default:
+ return 4;
+ }
+}
+
+static int setup_out_convert(struct impl *this)
+{
+ uint32_t i, j;
+ struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT];
+ struct spa_audio_info src_info, dst_info;
+ int res;
+ bool remap = false;
+
+ dst_info = out->format;
+ src_info = dst_info;
+ src_info.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32;
+
+ spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this,
+ spa_debug_type_find_name(spa_type_audio_format, src_info.info.raw.format),
+ src_info.info.raw.channels,
+ src_info.info.raw.rate,
+ spa_debug_type_find_name(spa_type_audio_format, dst_info.info.raw.format),
+ dst_info.info.raw.channels,
+ dst_info.info.raw.rate);
+
+ qsort(src_info.info.raw.position, src_info.info.raw.channels,
+ sizeof(uint32_t), int32_cmp);
+
+ for (i = 0; i < src_info.info.raw.channels; i++) {
+ for (j = 0; j < dst_info.info.raw.channels; j++) {
+ if (src_info.info.raw.position[i] !=
+ dst_info.info.raw.position[j])
+ continue;
+ out->remap[i] = j;
+ if (i != j)
+ remap = true;
+
+ spa_log_debug(this->log, "%p: channel %d (%d) -> %d (%s -> %s)", this,
+ i, out->remap[i], j,
+ spa_debug_type_find_short_name(spa_type_audio_channel,
+ src_info.info.raw.position[i]),
+ spa_debug_type_find_short_name(spa_type_audio_channel,
+ dst_info.info.raw.position[j]));
+ dst_info.info.raw.position[j] = -1;
+ break;
+ }
+ }
+ if (out->conv.free)
+ convert_free(&out->conv);
+
+ out->conv.src_fmt = src_info.info.raw.format;
+ out->conv.dst_fmt = dst_info.info.raw.format;
+ out->conv.rate = dst_info.info.raw.rate;
+ out->conv.n_channels = dst_info.info.raw.channels;
+ out->conv.cpu_flags = this->cpu_flags;
+ out->need_remap = remap;
+
+ if ((res = convert_init(&out->conv)) < 0)
+ return res;
+
+ spa_log_debug(this->log, "%p: got converter features %08x:%08x quant:%d:%d"
+ " passthrough:%d remap:%d %s", this,
+ this->cpu_flags, out->conv.cpu_flags, out->conv.method,
+ out->conv.noise_bits, out->conv.is_passthrough, remap, out->conv.func_name);
+
+ return 0;
+}
+
+static int setup_convert(struct impl *this)
+{
+ struct dir *in, *out;
+ uint32_t i, rate;
+ int res;
+
+ in = &this->dir[SPA_DIRECTION_INPUT];
+ out = &this->dir[SPA_DIRECTION_OUTPUT];
+
+ spa_log_debug(this->log, "%p: setup:%d in_format:%d out_format:%d", this,
+ this->setup, in->have_format, out->have_format);
+
+ if (this->setup)
+ return 0;
+
+ if (!in->have_format || !out->have_format)
+ return -EINVAL;
+
+ rate = this->io_position ? this->io_position->clock.rate.denom : DEFAULT_RATE;
+
+ /* in DSP mode we always convert to the DSP rate */
+ if (in->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp)
+ in->format.info.raw.rate = rate;
+ if (out->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp)
+ out->format.info.raw.rate = rate;
+
+ /* try to passthrough the rates */
+ if (in->format.info.raw.rate == 0)
+ in->format.info.raw.rate = out->format.info.raw.rate;
+ else if (out->format.info.raw.rate == 0)
+ out->format.info.raw.rate = in->format.info.raw.rate;
+
+ /* try to passthrough the channels */
+ if (in->format.info.raw.channels == 0)
+ in->format.info.raw.channels = out->format.info.raw.channels;
+ else if (out->format.info.raw.channels == 0)
+ out->format.info.raw.channels = in->format.info.raw.channels;
+
+ if (in->format.info.raw.rate == 0 || out->format.info.raw.rate == 0)
+ return -EINVAL;
+ if (in->format.info.raw.channels == 0 || out->format.info.raw.channels == 0)
+ return -EINVAL;
+
+ if ((res = setup_in_convert(this)) < 0)
+ return res;
+ if ((res = setup_channelmix(this)) < 0)
+ return res;
+ if ((res = setup_resample(this)) < 0)
+ return res;
+ if ((res = setup_out_convert(this)) < 0)
+ return res;
+
+ for (i = 0; i < MAX_PORTS; i++) {
+ this->tmp_datas[0][i] = SPA_PTROFF(this->tmp[0], this->empty_size * i, void);
+ this->tmp_datas[0][i] = SPA_PTR_ALIGN(this->tmp_datas[0][i], MAX_ALIGN, void);
+ this->tmp_datas[1][i] = SPA_PTROFF(this->tmp[1], this->empty_size * i, void);
+ this->tmp_datas[1][i] = SPA_PTR_ALIGN(this->tmp_datas[1][i], MAX_ALIGN, void);
+ }
+ this->setup = true;
+
+ emit_node_info(this, false);
+
+ return 0;
+}
+
+static void reset_node(struct impl *this)
+{
+ if (this->resample.reset)
+ resample_reset(&this->resample);
+ this->in_offset = 0;
+ this->out_offset = 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ int res;
+
+ 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;
+
+ 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++) {
+ emit_port_info(this, GET_IN_PORT(this, i), true);
+ }
+ for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) {
+ emit_port_info(this, GET_OUT_PORT(this, i), true);
+ }
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *user_data)
+{
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int port_enum_formats(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct impl *this = object;
+
+ switch (index) {
+ case 0:
+ if (PORT_IS_DSP(this, direction, port_id)) {
+ struct spa_audio_info_dsp info;
+ info.format = SPA_AUDIO_FORMAT_DSP_F32;
+ *param = spa_format_audio_dsp_build(builder,
+ SPA_PARAM_EnumFormat, &info);
+ } else if (PORT_IS_CONTROL(this, direction, port_id)) {
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+ } else {
+ uint32_t rate = this->io_position ?
+ this->io_position->clock.rate.denom : DEFAULT_RATE;
+
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(25,
+ SPA_AUDIO_FORMAT_F32P,
+ SPA_AUDIO_FORMAT_F32P,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F32_OE,
+ SPA_AUDIO_FORMAT_F64P,
+ SPA_AUDIO_FORMAT_F64,
+ SPA_AUDIO_FORMAT_F64_OE,
+ SPA_AUDIO_FORMAT_S32P,
+ SPA_AUDIO_FORMAT_S32,
+ SPA_AUDIO_FORMAT_S32_OE,
+ SPA_AUDIO_FORMAT_S24_32P,
+ SPA_AUDIO_FORMAT_S24_32,
+ SPA_AUDIO_FORMAT_S24_32_OE,
+ SPA_AUDIO_FORMAT_S24P,
+ SPA_AUDIO_FORMAT_S24,
+ SPA_AUDIO_FORMAT_S24_OE,
+ SPA_AUDIO_FORMAT_S16P,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_S16_OE,
+ SPA_AUDIO_FORMAT_S8P,
+ SPA_AUDIO_FORMAT_S8,
+ SPA_AUDIO_FORMAT_U8P,
+ SPA_AUDIO_FORMAT_U8,
+ SPA_AUDIO_FORMAT_ULAW,
+ SPA_AUDIO_FORMAT_ALAW),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(
+ rate, 1, INT32_MAX),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
+ DEFAULT_CHANNELS, 1, SPA_AUDIO_MAX_CHANNELS));
+ }
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[2048];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_log_debug(this->log, "%p: enum params port %d.%d %d %u",
+ this, direction, port_id, seq, id);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(object, direction, port_id, result.index, &param, &b)) <= 0)
+ return res;
+ break;
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ if (PORT_IS_DSP(this, direction, port_id))
+ param = spa_format_audio_dsp_build(&b, id, &port->format.info.dsp);
+ else if (PORT_IS_CONTROL(this, direction, port_id))
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, id,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+ else
+ param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
+ break;
+ case SPA_PARAM_Buffers:
+ {
+ uint32_t size;
+
+ if (!port->have_format)
+ return -EIO;
+ 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 {
+ uint32_t irate, orate;
+ struct dir *dir = &this->dir[direction];
+
+ /* Convert ports are scaled so that they can always
+ * provide one quantum of data */
+ irate = dir->format.info.raw.rate;
+
+ /* collect the other port rate */
+ dir = &this->dir[SPA_DIRECTION_REVERSE(direction)];
+ if (dir->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp)
+ orate = this->io_position ? this->io_position->clock.rate.denom : DEFAULT_RATE;
+ else
+ orate = dir->format.info.raw.rate;
+
+ /* always keep some extra room for adaptive resampling */
+ size = this->quantum_limit * 2;
+ /* scale the buffer size when we can. */
+ if (irate != 0 && orate != 0)
+ size = SPA_SCALE32_UP(size, irate, orate);
+ }
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ size * port->stride,
+ 16 * port->stride,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride));
+ break;
+ }
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ param = spa_latency_build(&b, id, &this->dir[result.index].latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, "%p: clear buffers %p", this, port);
+ port->n_buffers = 0;
+ spa_list_init(&port->queue);
+ }
+ return 0;
+}
+
+static int port_set_latency(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ const struct spa_pod *latency)
+{
+ struct impl *this = object;
+ struct port *port, *oport;
+ enum spa_direction other = SPA_DIRECTION_REVERSE(direction);
+ uint32_t i;
+
+ spa_log_debug(this->log, "%p: set latency direction:%d id:%d",
+ this, direction, port_id);
+
+ port = GET_PORT(this, direction, port_id);
+ if (port->is_monitor)
+ return 0;
+
+ if (latency == NULL) {
+ this->dir[other].latency = SPA_LATENCY_INFO(other);
+ } else {
+ struct spa_latency_info info;
+ if (spa_latency_parse(latency, &info) < 0 ||
+ info.direction != other)
+ return -EINVAL;
+ this->dir[other].latency = info;
+ }
+
+ for (i = 0; i < this->dir[other].n_ports; i++) {
+ oport = GET_PORT(this, other, i);
+ oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ oport->params[IDX_Latency].user++;
+ emit_port_info(this, oport, false);
+ }
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[IDX_Latency].user++;
+ emit_port_info(this, port, false);
+ return 0;
+}
+
+static int port_set_format(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: set format", this);
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) {
+ 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_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) {
+ spa_log_error(this->log, "unexpected types %d/%d",
+ info.media_type, info.media_subtype);
+ return -EINVAL;
+ }
+ if ((res = spa_format_audio_dsp_parse(format, &info.info.dsp)) < 0) {
+ spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
+ return res;
+ }
+ if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32) {
+ spa_log_error(this->log, "unexpected format %d<->%d",
+ info.info.dsp.format, SPA_AUDIO_FORMAT_DSP_F32);
+ return -EINVAL;
+ }
+ port->blocks = 1;
+ port->stride = 4;
+ }
+ else if (PORT_IS_CONTROL(this, direction, port_id)) {
+ if (info.media_type != SPA_MEDIA_TYPE_application ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_control) {
+ spa_log_error(this->log, "unexpected types %d/%d",
+ info.media_type, info.media_subtype);
+ return -EINVAL;
+ }
+ port->blocks = 1;
+ port->stride = 1;
+ }
+ else {
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw) {
+ spa_log_error(this->log, "unexpected types %d/%d",
+ info.media_type, info.media_subtype);
+ return -EINVAL;
+ }
+ if ((res = spa_format_audio_raw_parse(format, &info.info.raw)) < 0) {
+ spa_log_error(this->log, "can't parse format %s", spa_strerror(res));
+ return res;
+ }
+ if (info.info.raw.format == 0 ||
+ info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) {
+ spa_log_error(this->log, "invalid format:%d rate:%d channels:%d",
+ info.info.raw.format, info.info.raw.rate,
+ info.info.raw.channels);
+ return -EINVAL;
+ }
+ port->stride = calc_width(&info);
+ if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) {
+ port->blocks = info.info.raw.channels;
+ } else {
+ port->stride *= info.info.raw.channels;
+ port->blocks = 1;
+ }
+ this->dir[direction].format = info;
+ this->dir[direction].have_format = true;
+ 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_PARAMS;
+ if (port->have_format) {
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_debug(this->log, "%p: set param port %d.%d %u",
+ this, direction, port_id, id);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Latency:
+ return port_set_latency(this, direction, port_id, flags, param);
+ case SPA_PARAM_Format:
+ return port_set_format(this, direction, port_id, flags, param);
+ default:
+ return -ENOENT;
+ }
+}
+
+static void queue_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+ struct buffer *b = &port->buffers[id];
+
+ spa_log_trace_fp(this->log, "%p: queue buffer %d on port %d %d",
+ this, id, port->id, b->flags);
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
+ return;
+
+ spa_list_append(&port->queue, &b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED);
+}
+
+static struct buffer *peek_buffer(struct impl *this, struct port *port)
+{
+ struct buffer *b;
+
+ if (spa_list_is_empty(&port->queue))
+ return NULL;
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_log_trace_fp(this->log, "%p: peek buffer %d on port %d %u",
+ this, b->id, port->id, b->flags);
+ return b;
+}
+
+static void dequeue_buffer(struct impl *this, struct port *port, struct buffer *b)
+{
+ spa_list_remove(&b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED);
+ spa_log_trace_fp(this->log, "%p: dequeue buffer %d on port %d %u",
+ this, b->id, port->id, b->flags);
+}
+
+static int
+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;
+ }
+
+ for (j = 0; j < n_datas; j++) {
+ if (d[j].data == NULL) {
+ spa_log_error(this->log, "%p: invalid memory %d on buffer %d %d %p",
+ this, j, i, d[j].type, d[j].data);
+ return -EINVAL;
+ }
+ if (!SPA_IS_ALIGNED(d[j].data, this->max_align)) {
+ spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned",
+ this, j, i);
+ }
+ if (direction == SPA_DIRECTION_OUTPUT &&
+ !SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_DYNAMIC))
+ this->is_passthrough = false;
+
+ b->datas[j] = d[j].data;
+
+ maxsize = SPA_MAX(maxsize, d[j].maxsize);
+ }
+ if (direction == SPA_DIRECTION_OUTPUT)
+ queue_buffer(this, port, i);
+ }
+ if (maxsize > this->empty_size) {
+ this->empty = realloc(this->empty, maxsize + MAX_ALIGN);
+ this->scratch = realloc(this->scratch, maxsize + MAX_ALIGN);
+ this->tmp[0] = realloc(this->tmp[0], (maxsize + MAX_ALIGN) * MAX_PORTS);
+ this->tmp[1] = realloc(this->tmp[1], (maxsize + MAX_ALIGN) * MAX_PORTS);
+ if (this->empty == NULL || this->scratch == NULL ||
+ this->tmp[0] == NULL || this->tmp[1] == NULL)
+ return -errno;
+ memset(this->empty, 0, maxsize + MAX_ALIGN);
+ this->empty_size = maxsize;
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_debug(this->log, "%p: set io %d on port %d:%d %p",
+ this, id, direction, port_id, data);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ 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 channelmix_process_control(struct impl *this, struct port *ctrlport,
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ struct spa_pod_control *c, *prev = NULL;
+ uint32_t avail_samples = n_samples;
+ uint32_t i;
+ const float *s[MAX_PORTS], **ss = (const float**) src;
+ float *d[MAX_PORTS], **sd = (float **) dst;
+ const struct spa_pod_sequence_body *body = &(ctrlport->ctrl)->body;
+ uint32_t size = SPA_POD_BODY_SIZE(ctrlport->ctrl);
+ bool end = false;
+
+ c = spa_pod_control_first(body);
+ while (true) {
+ uint32_t chunk;
+
+ if (c == NULL || !spa_pod_control_is_inside(body, size, c)) {
+ c = NULL;
+ end = true;
+ }
+ if (avail_samples == 0)
+ break;
+
+ /* ignore old control offsets */
+ if (c != NULL) {
+ if (c->offset <= ctrlport->ctrl_offset) {
+ prev = c;
+ if (c != NULL)
+ c = spa_pod_control_next(c);
+ continue;
+ }
+ chunk = SPA_MIN(avail_samples, c->offset - ctrlport->ctrl_offset);
+ spa_log_trace_fp(this->log, "%p: process %d-%d %d/%d", this,
+ ctrlport->ctrl_offset, c->offset, chunk, avail_samples);
+ } else {
+ chunk = avail_samples;
+ spa_log_trace_fp(this->log, "%p: process remain %d", this, chunk);
+ }
+
+ if (prev) {
+ switch (prev->type) {
+ case SPA_CONTROL_Midi:
+ apply_midi(this, &prev->value);
+ break;
+ case SPA_CONTROL_Properties:
+ apply_props(this, &prev->value);
+ break;
+ default:
+ continue;
+ }
+ }
+ if (ss == (const float**)src && chunk != avail_samples) {
+ for (i = 0; i < this->mix.src_chan; i++)
+ s[i] = ss[i];
+ for (i = 0; i < this->mix.dst_chan; i++)
+ d[i] = sd[i];
+ ss = s;
+ sd = d;
+ }
+
+ channelmix_process(&this->mix, (void**)sd, (const void**)ss, chunk);
+
+ if (chunk != avail_samples) {
+ for (i = 0; i < this->mix.src_chan; i++)
+ ss[i] += chunk;
+ for (i = 0; i < this->mix.dst_chan; i++)
+ sd[i] += chunk;
+ }
+ avail_samples -= chunk;
+ ctrlport->ctrl_offset += chunk;
+ }
+ return end ? 1 : 0;
+}
+
+static uint32_t resample_get_in_size(struct impl *this, bool passthrough, uint32_t out_size)
+{
+ uint32_t match_size = passthrough ? out_size : resample_in_len(&this->resample, out_size);
+ spa_log_trace_fp(this->log, "%p: current match %u", this, match_size);
+ return match_size;
+}
+
+static uint32_t resample_update_rate_match(struct impl *this, bool passthrough, uint32_t out_size, uint32_t in_queued)
+{
+ uint32_t delay, match_size;
+
+ if (passthrough) {
+ delay = 0;
+ match_size = out_size;
+ } else {
+ double rate = this->rate_scale / this->props.rate;
+ if (this->io_rate_match &&
+ SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE))
+ rate *= this->io_rate_match->rate;
+ resample_update_rate(&this->resample, rate);
+ delay = resample_delay(&this->resample);
+ match_size = resample_in_len(&this->resample, out_size);
+ }
+ match_size -= SPA_MIN(match_size, in_queued);
+
+ spa_log_trace_fp(this->log, "%p: next match %u", this, match_size);
+
+ if (this->io_rate_match) {
+ this->io_rate_match->delay = delay;
+ this->io_rate_match->size = match_size;
+ }
+ return match_size;
+}
+
+static inline bool resample_is_passthrough(struct impl *this)
+{
+ return this->resample.i_rate == this->resample.o_rate && this->rate_scale == 1.0 &&
+ !this->rate_adjust && (this->io_rate_match == NULL ||
+ !SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE));
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ const void *src_datas[MAX_PORTS], **in_datas;
+ void *dst_datas[MAX_PORTS], *remap_src_datas[MAX_PORTS], *remap_dst_datas[MAX_PORTS];
+ void **out_datas, **dst_remap;
+ uint32_t i, j, n_src_datas = 0, n_dst_datas = 0, n_mon_datas = 0, remap;
+ uint32_t n_samples, max_in, n_out, max_out, quant_samples;
+ struct port *port, *ctrlport = NULL;
+ struct buffer *buf, *out_bufs[MAX_PORTS];
+ struct spa_data *bd;
+ struct dir *dir;
+ int tmp = 0, res = 0;
+ bool in_passthrough, mix_passthrough, resample_passthrough, out_passthrough;
+ bool in_avail = false, flush_in = false, flush_out = false, draining = false, in_empty = true;
+ struct spa_io_buffers *io, *ctrlio = NULL;
+ const struct spa_pod_sequence *ctrl = NULL;
+
+ /* calculate quantum scale, this is how many samples we need to produce or
+ * consume. Also update the rate scale, this is sent to the resampler to adjust
+ * the rate, either when the graph clock changed or when the user adjusted the
+ * rate. */
+ if (SPA_LIKELY(this->io_position)) {
+ double r = this->rate_scale;
+
+ quant_samples = this->io_position->clock.duration;
+ if (this->direction == SPA_DIRECTION_INPUT) {
+ if (this->io_position->clock.rate.denom != this->resample.o_rate)
+ r = (double) this->io_position->clock.rate.denom / this->resample.o_rate;
+ else
+ r = 1.0;
+ } else {
+ if (this->io_position->clock.rate.denom != this->resample.i_rate)
+ r = (double) this->resample.i_rate / this->io_position->clock.rate.denom;
+ else
+ r = 1.0;
+ }
+ if (this->rate_scale != r) {
+ spa_log_info(this->log, "scale %f->%f", this->rate_scale, r);
+ this->rate_scale = r;
+ }
+ }
+ else
+ quant_samples = this->quantum_limit;
+
+ dir = &this->dir[SPA_DIRECTION_INPUT];
+ in_passthrough = dir->conv.is_passthrough;
+ max_in = UINT32_MAX;
+
+ /* collect input port data */
+ for (i = 0; i < dir->n_ports; i++) {
+ port = GET_IN_PORT(this, i);
+
+ if (SPA_UNLIKELY((io = port->io) == NULL)) {
+ spa_log_trace_fp(this->log, "%p: no io on input port %d",
+ this, port->id);
+ buf = NULL;
+ } else if (SPA_UNLIKELY(io->status != SPA_STATUS_HAVE_DATA)) {
+ if (io->status & SPA_STATUS_DRAINED) {
+ spa_log_debug(this->log, "%p: port %d drained", this, port->id);
+ in_avail = flush_in = draining = true;
+ } else {
+ spa_log_trace_fp(this->log, "%p: empty input port %d %p %d %d %d",
+ this, port->id, io, io->status, io->buffer_id,
+ port->n_buffers);
+ this->drained = false;
+ }
+ buf = NULL;
+ } else if (SPA_UNLIKELY(io->buffer_id >= port->n_buffers)) {
+ spa_log_trace_fp(this->log, "%p: invalid input buffer port %d %p %d %d %d",
+ this, port->id, io, io->status, io->buffer_id,
+ port->n_buffers);
+ io->status = -EINVAL;
+ buf = NULL;
+ } else {
+ spa_log_trace_fp(this->log, "%p: input buffer port %d io:%p status:%d id:%d n:%d",
+ this, port->id, io, io->status, io->buffer_id,
+ port->n_buffers);
+ buf = &port->buffers[io->buffer_id];
+ }
+
+ if (SPA_UNLIKELY(buf == NULL)) {
+ for (j = 0; j < port->blocks; j++) {
+ if (port->is_control) {
+ spa_log_trace_fp(this->log, "%p: empty control %d", this,
+ i * port->blocks + j);
+ } else {
+ remap = n_src_datas++;
+ src_datas[remap] = SPA_PTR_ALIGN(this->empty, MAX_ALIGN, void);
+ spa_log_trace_fp(this->log, "%p: empty input %d->%d", this,
+ i * port->blocks + j, remap);
+ max_in = SPA_MIN(max_in, this->empty_size / port->stride);
+ }
+ }
+ } else {
+ in_avail = true;
+ for (j = 0; j < port->blocks; j++) {
+ uint32_t offs, size;
+
+ bd = &buf->buf->datas[j];
+
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->maxsize - offs, bd->chunk->size);
+ if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY))
+ in_empty = false;
+
+ if (SPA_UNLIKELY(port->is_control)) {
+ spa_log_trace_fp(this->log, "%p: control %d", this,
+ i * port->blocks + j);
+ ctrlport = port;
+ ctrlio = io;
+ ctrl = spa_pod_from_data(bd->data, bd->maxsize,
+ bd->chunk->offset, bd->chunk->size);
+ if (ctrl && !spa_pod_is_sequence(&ctrl->pod))
+ ctrl = NULL;
+ if (ctrl != ctrlport->ctrl) {
+ ctrlport->ctrl = ctrl;
+ ctrlport->ctrl_offset = 0;
+ }
+ } else {
+ max_in = SPA_MIN(max_in, size / port->stride);
+
+ remap = n_src_datas++;
+ offs += this->in_offset * port->stride;
+ src_datas[remap] = SPA_PTROFF(bd->data, offs, void);
+
+ spa_log_trace_fp(this->log, "%p: input %d:%d:%d %d %d %d->%d", this,
+ offs, size, port->stride, this->in_offset, max_in,
+ i * port->blocks + j, remap);
+ }
+ }
+ }
+ }
+
+ /* calculate how many samples we are going to produce. */
+ if (this->direction == SPA_DIRECTION_INPUT) {
+ /* in split mode we need to output exactly the size of the
+ * duration so we don't try to flush early */
+ max_out = quant_samples;
+ flush_out = false;
+ } else {
+ /* in merge mode we consume one duration of samples and
+ * always output the resulting data */
+ max_out = this->quantum_limit;
+ flush_out = true;
+ }
+
+ dir = &this->dir[SPA_DIRECTION_OUTPUT];
+ /* collect output ports and monitor ports data */
+ for (i = 0; i < dir->n_ports; i++) {
+ port = GET_OUT_PORT(this, i);
+
+ if (SPA_UNLIKELY((io = port->io) == NULL ||
+ io->status == SPA_STATUS_HAVE_DATA)) {
+ buf = NULL;
+ }
+ else {
+ if (SPA_LIKELY(io->buffer_id < port->n_buffers))
+ queue_buffer(this, port, io->buffer_id);
+
+ buf = peek_buffer(this, port);
+ }
+ out_bufs[i] = buf;
+
+ if (SPA_UNLIKELY(buf == NULL)) {
+ for (j = 0; j < port->blocks; j++) {
+ if (port->is_monitor) {
+ remap = n_mon_datas++;
+ spa_log_trace_fp(this->log, "%p: empty monitor %d", this,
+ remap);
+ } else if (port->is_control) {
+ spa_log_trace_fp(this->log, "%p: empty control %d", this, j);
+ } else {
+ remap = n_dst_datas++;
+ dst_datas[remap] = SPA_PTR_ALIGN(this->scratch, MAX_ALIGN, void);
+ spa_log_trace_fp(this->log, "%p: empty output %d->%d", this,
+ i * port->blocks + j, remap);
+ max_out = SPA_MIN(max_out, this->empty_size / port->stride);
+ }
+ }
+ } else {
+ for (j = 0; j < port->blocks; j++) {
+ bd = &buf->buf->datas[j];
+
+ bd->chunk->offset = 0;
+ bd->chunk->size = 0;
+ if (port->is_monitor) {
+ float volume;
+ uint32_t mon_max;
+
+ remap = n_mon_datas++;
+ volume = this->props.monitor.mute ? 0.0f : this->props.monitor.volumes[remap];
+ if (this->monitor_channel_volumes)
+ volume *= this->props.channel.mute ? 0.0f :
+ this->props.channel.volumes[remap];
+
+ mon_max = SPA_MIN(bd->maxsize / port->stride, max_in);
+
+ volume_process(&this->volume, bd->data, src_datas[remap],
+ volume, mon_max);
+
+ bd->chunk->size = mon_max * port->stride;
+ bd->chunk->stride = port->stride;
+
+ spa_log_trace_fp(this->log, "%p: monitor %d %d", this,
+ remap, mon_max);
+
+ dequeue_buffer(this, port, buf);
+ io->status = SPA_STATUS_HAVE_DATA;
+ io->buffer_id = buf->id;
+ res |= SPA_STATUS_HAVE_DATA;
+ } else if (SPA_UNLIKELY(port->is_control)) {
+ spa_log_trace_fp(this->log, "%p: control %d", this, j);
+ } else {
+ remap = n_dst_datas++;
+ dst_datas[remap] = SPA_PTROFF(bd->data,
+ this->out_offset * port->stride, void);
+ max_out = SPA_MIN(max_out, bd->maxsize / port->stride);
+
+ spa_log_trace_fp(this->log, "%p: output %d offs:%d %d->%d", this,
+ max_out, this->out_offset,
+ i * port->blocks + j, remap);
+ }
+ }
+ }
+ }
+
+
+ /* calculate how many samples at most we are going to consume. If we're
+ * draining, we consume as much as we can. Otherwise we consume what is
+ * left. */
+ if (SPA_UNLIKELY(draining))
+ n_samples = SPA_MIN(max_in, this->quantum_limit);
+ else {
+ n_samples = max_in - SPA_MIN(max_in, this->in_offset);
+ }
+ /* we only need to output the remaining samples */
+ n_out = max_out - SPA_MIN(max_out, this->out_offset);
+
+ resample_passthrough = resample_is_passthrough(this);
+
+ /* calculate how many samples we are going to consume. */
+ if (this->direction == SPA_DIRECTION_INPUT) {
+ if (!in_avail || this->drained) {
+ /* no input, ask for more, update rate-match first */
+ resample_update_rate_match(this, resample_passthrough, n_out, 0);
+ spa_log_trace_fp(this->log, "%p: no input drained:%d", this, this->drained);
+ res |= this->drained ? SPA_STATUS_DRAINED : SPA_STATUS_NEED_DATA;
+ return res;
+ }
+ /* else figure out how much input samples we need to consume */
+ n_samples = SPA_MIN(n_samples,
+ resample_get_in_size(this, resample_passthrough, n_out));
+ } else {
+ /* in merge mode we consume one duration of samples */
+ n_samples = SPA_MIN(n_samples, quant_samples);
+ flush_in = true;
+ }
+
+ mix_passthrough = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) &&
+ (ctrlport == NULL || ctrlport->ctrl == NULL);
+
+ 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;
+ }
+
+ 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_control(this, ctrlport, out_datas,
+ in_datas, n_samples) == 1) {
+ ctrlio->status = SPA_STATUS_OK;
+ ctrlport->ctrl = NULL;
+ }
+ } else {
+ channelmix_process(&this->mix, out_datas, in_datas, n_samples);
+ }
+ }
+ if (!resample_passthrough) {
+ uint32_t in_len, out_len;
+
+ in_datas = (const void**)out_datas;
+ if (out_passthrough)
+ out_datas = (void **)dst_remap;
+ else
+ out_datas = (void **)this->tmp_datas[(tmp++) & 1];
+
+ in_len = n_samples;
+ out_len = n_out;
+ resample_process(&this->resample, in_datas, &in_len, out_datas, &out_len);
+ spa_log_trace_fp(this->log, "%p: resample %d/%d -> %d/%d %d", this,
+ n_samples, in_len, n_out, out_len, out_passthrough);
+ this->in_offset += in_len;
+ n_samples = out_len;
+ } else {
+ n_samples = SPA_MIN(n_samples, n_out);
+ this->in_offset += n_samples;
+ }
+ this->out_offset += n_samples;
+
+ if (!out_passthrough) {
+ dir = &this->dir[SPA_DIRECTION_OUTPUT];
+ if (dir->need_remap) {
+ for (i = 0; i < dir->conv.n_channels; i++) {
+ remap_dst_datas[dir->remap[i]] = out_datas[i];
+ spa_log_trace_fp(this->log, "%p: output remap %d -> %d", this, i, dir->remap[i]);
+ }
+ in_datas = (const void**)remap_dst_datas;
+ } else {
+ in_datas = (const void**)out_datas;
+ }
+ spa_log_trace_fp(this->log, "%p: output convert %d", this, n_samples);
+ convert_process(&dir->conv, dst_datas, in_datas, n_samples);
+ }
+
+ spa_log_trace_fp(this->log, "%d/%d %d/%d %d->%d", this->in_offset, max_in,
+ this->out_offset, max_out, n_samples, n_out);
+
+ dir = &this->dir[SPA_DIRECTION_INPUT];
+ if (SPA_LIKELY(this->in_offset >= max_in || flush_in)) {
+ /* return input buffers */
+ for (i = 0; i < dir->n_ports; i++) {
+ port = GET_IN_PORT(this, i);
+ if (port->is_control)
+ continue;
+ if (SPA_UNLIKELY((io = port->io) == NULL))
+ continue;
+ spa_log_trace_fp(this->log, "return: input %d %d", port->id, io->buffer_id);
+ if (!draining)
+ io->status = SPA_STATUS_NEED_DATA;
+ }
+ this->in_offset = 0;
+ max_in = 0;
+ res |= SPA_STATUS_NEED_DATA;
+ }
+
+ dir = &this->dir[SPA_DIRECTION_OUTPUT];
+ if (SPA_LIKELY(n_samples > 0 && (this->out_offset >= max_out || flush_out))) {
+ /* queue output buffers */
+ for (i = 0; i < dir->n_ports; i++) {
+ port = GET_OUT_PORT(this, i);
+ if (SPA_UNLIKELY(port->is_monitor || port->is_control))
+ continue;
+ if (SPA_UNLIKELY((io = port->io) == NULL))
+ continue;
+
+ if (SPA_UNLIKELY((buf = out_bufs[i]) == NULL))
+ continue;
+
+ dequeue_buffer(this, port, buf);
+
+ for (j = 0; j < port->blocks; j++) {
+ bd = &buf->buf->datas[j];
+ bd->chunk->size = this->out_offset * port->stride;
+ bd->chunk->stride = port->stride;
+ SPA_FLAG_UPDATE(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY, in_empty);
+ spa_log_trace_fp(this->log, "out: offs:%d stride:%d size:%d",
+ this->out_offset, port->stride, bd->chunk->size);
+ }
+ io->status = SPA_STATUS_HAVE_DATA;
+ io->buffer_id = buf->id;
+ }
+ res |= SPA_STATUS_HAVE_DATA;
+ this->drained = draining;
+ this->out_offset = 0;
+ }
+ else if (n_samples == 0 && this->resample_peaks) {
+ for (i = 0; i < dir->n_ports; i++) {
+ port = GET_OUT_PORT(this, i);
+ if (port->is_monitor || port->is_control)
+ continue;
+ if (SPA_UNLIKELY((io = port->io) == NULL))
+ continue;
+
+ io->status = SPA_STATUS_HAVE_DATA;
+ io->buffer_id = SPA_ID_INVALID;
+ res |= SPA_STATUS_HAVE_DATA;
+ spa_log_trace_fp(this->log, "%p: no output buffer", this);
+ }
+ }
+ if (resample_update_rate_match(this, resample_passthrough,
+ max_out - this->out_offset,
+ max_in - this->in_offset) > 0)
+ res |= SPA_STATUS_NEED_DATA;
+
+ return res;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+ uint32_t i;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ for (i = 0; i < MAX_PORTS; i++)
+ free(this->dir[SPA_DIRECTION_INPUT].ports[i]);
+ for (i = 0; i < MAX_PORTS; i++)
+ free(this->dir[SPA_DIRECTION_OUTPUT].ports[i]);
+ free(this->empty);
+ free(this->scratch);
+ free(this->tmp[0]);
+ free(this->tmp[1]);
+
+ if (this->resample.free)
+ resample_free(&this->resample);
+ if (this->dir[0].conv.free)
+ convert_free(&this->dir[0].conv);
+ if (this->dir[1].conv.free)
+ convert_free(&this->dir[1].conv);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static inline uint32_t parse_position(uint32_t *pos, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+ uint32_t i = 0;
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ i < SPA_AUDIO_MAX_CHANNELS) {
+ pos[i++] = channel_from_name(v);
+ }
+ return i;
+}
+
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(this->log, log_topic);
+
+ this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+ if (this->cpu) {
+ this->cpu_flags = spa_cpu_get_flags(this->cpu);
+ this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
+ }
+
+ props_reset(&this->props);
+
+ this->mix.options = CHANNELMIX_OPTION_UPMIX | CHANNELMIX_OPTION_MIX_LFE;
+ this->mix.upmix = CHANNELMIX_UPMIX_PSD;
+ this->mix.log = this->log;
+ this->mix.lfe_cutoff = 150.0f;
+ this->mix.fc_cutoff = 12000.0f;
+ this->mix.rear_delay = 12.0f;
+ this->mix.widen = 0.0f;
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "clock.quantum-limit"))
+ spa_atou32(s, &this->quantum_limit, 0);
+ else if (spa_streq(k, "resample.peaks"))
+ this->resample_peaks = spa_atob(s);
+ else if (spa_streq(k, "resample.prefill"))
+ SPA_FLAG_UPDATE(this->resample.options,
+ RESAMPLE_OPTION_PREFILL, spa_atob(s));
+ else if (spa_streq(k, "factory.mode")) {
+ if (spa_streq(s, "merge"))
+ this->direction = SPA_DIRECTION_OUTPUT;
+ else
+ this->direction = SPA_DIRECTION_INPUT;
+ }
+ else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
+ if (s != NULL)
+ this->props.n_channels = parse_position(this->props.channel_map, s, strlen(s));
+ }
+ else
+ audioconvert_set_param(this, k, s);
+ }
+
+ this->props.channel.n_volumes = this->props.n_channels;
+ this->props.soft.n_volumes = this->props.n_channels;
+ this->props.monitor.n_volumes = this->props.n_channels;
+
+ this->dir[SPA_DIRECTION_INPUT].direction = SPA_DIRECTION_INPUT;
+ this->dir[SPA_DIRECTION_INPUT].latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ this->dir[SPA_DIRECTION_OUTPUT].direction = SPA_DIRECTION_OUTPUT;
+ this->dir[SPA_DIRECTION_OUTPUT].latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = MAX_PORTS;
+ this->info.max_output_ports = MAX_PORTS;
+ 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;
+
+ this->volume.cpu_flags = this->cpu_flags;
+ volume_init(&this->volume);
+
+ this->rate_scale = 1.0;
+
+ reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_INPUT, false, false, NULL);
+ reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_OUTPUT, false, false, NULL);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+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_audioconvert_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_AUDIO_CONVERT,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/audioconvert/benchmark-fmt-ops.c b/spa/plugins/audioconvert/benchmark-fmt-ops.c
new file mode 100644
index 0000000..2a0d4e8
--- /dev/null
+++ b/spa/plugins/audioconvert/benchmark-fmt-ops.c
@@ -0,0 +1,323 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include "test-helper.h"
+#include "fmt-ops.h"
+
+static uint32_t cpu_flags;
+
+typedef void (*convert_func_t) (struct convert *conv, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples);
+
+struct stats {
+ uint32_t n_samples;
+ uint32_t n_channels;
+ uint64_t perf;
+ const char *name;
+ const char *impl;
+};
+
+#define MAX_SAMPLES 4096
+#define MAX_CHANNELS 11
+
+#define MAX_COUNT 100
+
+static uint8_t samp_in[MAX_SAMPLES * MAX_CHANNELS * 4];
+static uint8_t samp_out[MAX_SAMPLES * MAX_CHANNELS * 4];
+
+static const int sample_sizes[] = { 0, 1, 128, 513, 4096 };
+static const int channel_counts[] = { 1, 2, 4, 6, 8, 11 };
+
+#define MAX_RESULTS SPA_N_ELEMENTS(sample_sizes) * SPA_N_ELEMENTS(channel_counts) * 70
+
+static uint32_t n_results = 0;
+static struct stats results[MAX_RESULTS];
+
+static void run_test1(const char *name, const char *impl, bool in_packed, bool out_packed,
+ convert_func_t func, int n_channels, int n_samples)
+{
+ int i, j;
+ const void *ip[n_channels];
+ void *op[n_channels];
+ struct timespec ts;
+ uint64_t count, t1, t2;
+ struct convert conv;
+
+ conv.n_channels = n_channels;
+
+ for (j = 0; j < n_channels; j++) {
+ ip[j] = &samp_in[j * n_samples * 4];
+ op[j] = &samp_out[j * n_samples * 4];
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ count = 0;
+ for (i = 0; i < MAX_COUNT; i++) {
+ func(&conv, op, ip, n_samples);
+ count++;
+ }
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ spa_assert(n_results < MAX_RESULTS);
+
+ results[n_results++] = (struct stats) {
+ .n_samples = n_samples,
+ .n_channels = n_channels,
+ .perf = count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1),
+ .name = name,
+ .impl = impl
+ };
+}
+
+static void run_testc(const char *name, const char *impl, bool in_packed, bool out_packed, convert_func_t func,
+ int channel_count)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(sample_sizes, s) {
+ run_test1(name, impl, in_packed, out_packed, func, channel_count,
+ (*s + (channel_count -1)) / channel_count);
+ }
+}
+
+static void run_test(const char *name, const char *impl, bool in_packed, bool out_packed, convert_func_t func)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(sample_sizes, s) {
+ SPA_FOR_EACH_ELEMENT_VAR(channel_counts, c) {
+ run_test1(name, impl, in_packed, out_packed, func, *c, (*s + (*c -1)) / *c);
+ }
+ }
+}
+
+static void test_f32_u8(void)
+{
+ run_test("test_f32_u8", "c", true, true, conv_f32_to_u8_c);
+ run_test("test_f32d_u8", "c", false, true, conv_f32d_to_u8_c);
+ run_test("test_f32_u8d", "c", true, false, conv_f32_to_u8d_c);
+ run_test("test_f32d_u8d", "c", false, false, conv_f32d_to_u8d_c);
+}
+
+static void test_u8_f32(void)
+{
+ run_test("test_u8_f32", "c", true, true, conv_u8_to_f32_c);
+ run_test("test_u8d_f32", "c", false, true, conv_u8d_to_f32_c);
+ run_test("test_u8_f32d", "c", true, false, conv_u8_to_f32d_c);
+ run_test("test_u8d_f32d", "c", false, false, conv_u8d_to_f32d_c);
+}
+
+static void test_f32_s16(void)
+{
+ run_test("test_f32_s16", "c", true, true, conv_f32_to_s16_c);
+ run_test("test_f32d_s16", "c", false, true, conv_f32d_to_s16_c);
+#if defined (HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_f32d_s16", "sse2", false, true, conv_f32d_to_s16_sse2);
+ run_testc("test_f32d_s16_2", "sse2", false, true, conv_f32d_to_s16_2_sse2, 2);
+ }
+#endif
+#if defined (HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_f32d_s16", "avx2", false, true, conv_f32d_to_s16_avx2);
+ 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
+ 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);
+}
+
+static void test_s16_f32(void)
+{
+ run_test("test_s16_f32", "c", true, true, conv_s16_to_f32_c);
+ run_test("test_s16d_f32", "c", false, true, conv_s16d_to_f32_c);
+ run_test("test_s16_f32d", "c", true, false, conv_s16_to_f32d_c);
+#if defined (HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_s16_f32d", "sse2", true, false, conv_s16_to_f32d_sse2);
+ run_testc("test_s16_f32d_2", "sse2", true, false, conv_s16_to_f32d_2_sse2, 2);
+ }
+#endif
+#if defined (HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ 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
+ run_test("test_s16d_f32d", "c", false, false, conv_s16d_to_f32d_c);
+}
+
+static void test_f32_s32(void)
+{
+ run_test("test_f32_s32", "c", true, true, conv_f32_to_s32_c);
+ run_test("test_f32d_s32", "c", false, true, conv_f32d_to_s32_c);
+#if defined (HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_f32d_s32", "sse2", false, true, conv_f32d_to_s32_sse2);
+ }
+#endif
+#if defined (HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_f32d_s32", "avx2", false, true, conv_f32d_to_s32_avx2);
+ }
+#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);
+}
+
+static void test_s32_f32(void)
+{
+ run_test("test_s32_f32", "c", true, true, conv_s32_to_f32_c);
+ run_test("test_s32d_f32", "c", false, true, conv_s32d_to_f32_c);
+#if defined (HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_s32_f32d", "sse2", true, false, conv_s32_to_f32d_sse2);
+ }
+#endif
+#if defined (HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_s32_f32d", "avx2", true, false, conv_s32_to_f32d_avx2);
+ }
+#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);
+}
+
+static void test_f32_s24(void)
+{
+ run_test("test_f32_s24", "c", true, true, conv_f32_to_s24_c);
+ run_test("test_f32d_s24", "c", false, true, conv_f32d_to_s24_c);
+ run_test("test_f32_s24d", "c", true, false, conv_f32_to_s24d_c);
+ run_test("test_f32d_s24d", "c", false, false, conv_f32d_to_s24d_c);
+}
+
+static void test_s24_f32(void)
+{
+ run_test("test_s24_f32", "c", true, true, conv_s24_to_f32_c);
+ run_test("test_s24d_f32", "c", false, true, conv_s24d_to_f32_c);
+ run_test("test_s24_f32d", "c", true, false, conv_s24_to_f32d_c);
+#if defined (HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_s24_f32d", "sse2", true, false, conv_s24_to_f32d_sse2);
+ }
+#endif
+#if defined (HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_s24_f32d", "avx2", true, false, conv_s24_to_f32d_avx2);
+ }
+#endif
+#if defined (HAVE_SSSE3)
+ if (cpu_flags & SPA_CPU_FLAG_SSSE3) {
+ run_test("test_s24_f32d", "ssse3", true, false, conv_s24_to_f32d_ssse3);
+ }
+#endif
+#if defined (HAVE_SSE41)
+ if (cpu_flags & SPA_CPU_FLAG_SSE41) {
+ run_test("test_s24_f32d", "sse41", true, false, conv_s24_to_f32d_sse41);
+ }
+#endif
+ run_test("test_s24d_f32d", "c", false, false, conv_s24d_to_f32d_c);
+}
+
+static void test_f32_s24_32(void)
+{
+ run_test("test_f32_s24_32", "c", true, true, conv_f32_to_s24_32_c);
+ run_test("test_f32d_s24_32", "c", false, true, conv_f32d_to_s24_32_c);
+ run_test("test_f32_s24_32d", "c", true, false, conv_f32_to_s24_32d_c);
+ run_test("test_f32d_s24_32d", "c", false, false, conv_f32d_to_s24_32d_c);
+}
+
+static void test_s24_32_f32(void)
+{
+ run_test("test_s24_32_f32", "c", true, true, conv_s24_32_to_f32_c);
+ run_test("test_s24_32d_f32", "c", false, true, conv_s24_32d_to_f32_c);
+ run_test("test_s24_32_f32d", "c", true, false, conv_s24_32_to_f32d_c);
+ run_test("test_s24_32d_f32d", "c", false, false, conv_s24_32d_to_f32d_c);
+}
+
+static void test_interleave(void)
+{
+ run_test("test_8d_to_8", "c", false, true, conv_8d_to_8_c);
+ run_test("test_16d_to_16", "c", false, true, conv_16d_to_16_c);
+ run_test("test_24d_to_24", "c", false, true, conv_24d_to_24_c);
+ run_test("test_32d_to_32", "c", false, true, conv_32d_to_32_c);
+}
+
+static void test_deinterleave(void)
+{
+ run_test("test_8_to_8d", "c", true, false, conv_8_to_8d_c);
+ run_test("test_16_to_16d", "c", true, false, conv_16_to_16d_c);
+ run_test("test_24_to_24d", "c", true, false, conv_24_to_24d_c);
+ run_test("test_32_to_32d", "c", true, false, conv_32_to_32d_c);
+}
+
+static int compare_func(const void *_a, const void *_b)
+{
+ const struct stats *a = _a, *b = _b;
+ int diff;
+ if ((diff = strcmp(a->name, b->name)) != 0) return diff;
+ if ((diff = a->n_samples - b->n_samples) != 0) return diff;
+ if ((diff = a->n_channels - b->n_channels) != 0) return diff;
+ if ((diff = b->perf - a->perf) != 0) return diff;
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ uint32_t i;
+
+ cpu_flags = get_cpu_flags();
+ printf("got get CPU flags %d\n", cpu_flags);
+
+ test_f32_u8();
+ test_u8_f32();
+ test_f32_s16();
+ test_s16_f32();
+ test_f32_s32();
+ test_s32_f32();
+ test_f32_s24();
+ test_s24_f32();
+ test_f32_s24_32();
+ test_s24_32_f32();
+ test_interleave();
+ test_deinterleave();
+
+ qsort(results, n_results, sizeof(struct stats), compare_func);
+
+ for (i = 0; i < n_results; i++) {
+ struct stats *s = &results[i];
+ fprintf(stderr, "%-12."PRIu64" \t%-32.32s %s \t samples %d, channels %d\n",
+ s->perf, s->name, s->impl, s->n_samples, s->n_channels);
+ }
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/benchmark-resample.c b/spa/plugins/audioconvert/benchmark-resample.c
new file mode 100644
index 0000000..597f9ac
--- /dev/null
+++ b/spa/plugins/audioconvert/benchmark-resample.c
@@ -0,0 +1,204 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include "test-helper.h"
+#include "resample.h"
+
+#define MAX_SAMPLES 4096
+#define MAX_CHANNELS 11
+
+#define MAX_COUNT 200
+
+static uint32_t cpu_flags;
+
+struct stats {
+ uint32_t in_rate;
+ uint32_t out_rate;
+ uint32_t n_samples;
+ uint32_t n_channels;
+ uint64_t perf;
+ const char *name;
+ const char *impl;
+};
+
+static float samp_in[MAX_SAMPLES * MAX_CHANNELS];
+static float samp_out[MAX_SAMPLES * MAX_CHANNELS];
+
+static const int sample_sizes[] = { 0, 1, 128, 513, 4096 };
+static const int in_rates[] = { 44100, 44100, 48000, 96000, 22050, 96000 };
+static const int out_rates[] = { 44100, 48000, 44100, 48000, 48000, 44100 };
+
+
+#define MAX_RESAMPLER 5
+#define MAX_SIZES SPA_N_ELEMENTS(sample_sizes)
+#define MAX_RATES SPA_N_ELEMENTS(in_rates)
+#define MAX_RESULTS MAX_RESAMPLER * MAX_SIZES * MAX_RATES
+
+static uint32_t n_results = 0;
+static struct stats results[MAX_RESULTS];
+
+static void run_test1(const char *name, const char *impl, struct resample *r, int n_samples)
+{
+ uint32_t i, j;
+ const void *ip[MAX_CHANNELS];
+ void *op[MAX_CHANNELS];
+ struct timespec ts;
+ uint64_t count, t1, t2;
+ uint32_t in_len, out_len;
+
+ for (j = 0; j < r->channels; j++) {
+ ip[j] = &samp_in[j * MAX_SAMPLES];
+ op[j] = &samp_out[j * MAX_SAMPLES];
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ count = 0;
+ for (i = 0; i < MAX_COUNT; i++) {
+ in_len = n_samples;
+ out_len = MAX_SAMPLES;
+ resample_process(r, ip, &in_len, op, &out_len);
+ count++;
+ }
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ spa_assert(n_results < MAX_RESULTS);
+
+ results[n_results++] = (struct stats) {
+ .in_rate = r->i_rate,
+ .out_rate = r->o_rate,
+ .n_samples = n_samples,
+ .n_channels = r->channels,
+ .perf = count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1),
+ .name = name,
+ .impl = impl
+ };
+}
+
+static void run_test(const char *name, const char *impl, struct resample *r)
+{
+ size_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(sample_sizes); i++)
+ run_test1(name, impl, r, sample_sizes[i]);
+}
+
+static int compare_func(const void *_a, const void *_b)
+{
+ const struct stats *a = _a, *b = _b;
+ int diff;
+
+ if ((diff = a->in_rate - b->in_rate) != 0) return diff;
+ if ((diff = a->out_rate - b->out_rate) != 0) return diff;
+ if ((diff = a->n_samples - b->n_samples) != 0) return diff;
+ if ((diff = a->n_channels - b->n_channels) != 0) return diff;
+ if ((diff = b->perf - a->perf) != 0) return diff;
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ struct resample r;
+ uint32_t i;
+
+ cpu_flags = get_cpu_flags();
+ printf("got get CPU flags %d\n", cpu_flags);
+
+ for (i = 0; i < SPA_N_ELEMENTS(in_rates); i++) {
+ spa_zero(r);
+ r.channels = 2;
+ r.cpu_flags = 0;
+ r.i_rate = in_rates[i];
+ r.o_rate = out_rates[i];
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+ run_test("native", "c", &r);
+ resample_free(&r);
+ }
+#if defined (HAVE_SSE)
+ if (cpu_flags & SPA_CPU_FLAG_SSE) {
+ for (i = 0; i < SPA_N_ELEMENTS(in_rates); i++) {
+ spa_zero(r);
+ r.channels = 2;
+ r.cpu_flags = SPA_CPU_FLAG_SSE;
+ r.i_rate = in_rates[i];
+ r.o_rate = out_rates[i];
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+ run_test("native", "sse", &r);
+ resample_free(&r);
+ }
+ }
+#endif
+#if defined (HAVE_SSSE3)
+ if (cpu_flags & SPA_CPU_FLAG_SSSE3) {
+ for (i = 0; i < SPA_N_ELEMENTS(in_rates); i++) {
+ spa_zero(r);
+ r.channels = 2;
+ r.cpu_flags = SPA_CPU_FLAG_SSSE3 | SPA_CPU_FLAG_SLOW_UNALIGNED;
+ r.i_rate = in_rates[i];
+ r.o_rate = out_rates[i];
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+ run_test("native", "ssse3", &r);
+ resample_free(&r);
+ }
+ }
+#endif
+#if defined (HAVE_AVX) && defined(HAVE_FMA)
+ if (SPA_FLAG_IS_SET(cpu_flags, SPA_CPU_FLAG_AVX | SPA_CPU_FLAG_FMA3)) {
+ for (i = 0; i < SPA_N_ELEMENTS(in_rates); i++) {
+ spa_zero(r);
+ r.channels = 2;
+ r.cpu_flags = SPA_CPU_FLAG_AVX | SPA_CPU_FLAG_FMA3;
+ r.i_rate = in_rates[i];
+ r.o_rate = out_rates[i];
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+ run_test("native", "avx", &r);
+ resample_free(&r);
+ }
+ }
+#endif
+
+ qsort(results, n_results, sizeof(struct stats), compare_func);
+
+ for (i = 0; i < n_results; i++) {
+ struct stats *s = &results[i];
+ fprintf(stderr, "%-12."PRIu64" \t%-16.16s %s \t%d->%d samples %d, channels %d\n",
+ s->perf, s->name, s->impl, s->in_rate, s->out_rate,
+ s->n_samples, s->n_channels);
+ }
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/biquad.c b/spa/plugins/audioconvert/biquad.c
new file mode 100644
index 0000000..409b673
--- /dev/null
+++ b/spa/plugins/audioconvert/biquad.c
@@ -0,0 +1,111 @@
+/* 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 <spa/utils/defs.h>
+
+#include <math.h>
+#include "biquad.h"
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+#ifndef M_SQRT2
+#define M_SQRT2 1.41421356237309504880
+#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 = b0 * a0_inv;
+ bq->b1 = b1 * a0_inv;
+ bq->b2 = b2 * a0_inv;
+ bq->a1 = a1 * a0_inv;
+ bq->a2 = a2 * a0_inv;
+}
+
+static void biquad_lowpass(struct biquad *bq, double cutoff)
+{
+ /* Limit cutoff to 0 to 1. */
+ cutoff = SPA_CLAMP(cutoff, 0.0, 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
+ * coefficients up correctly.
+ */
+ set_coefficient(bq, 0, 0, 0, 1, 0, 0);
+ }
+}
+
+static void biquad_highpass(struct biquad *bq, double cutoff)
+{
+ /* Limit cutoff to 0 to 1. */
+ cutoff = SPA_CLAMP(cutoff, 0.0, 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 {
+ /* 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, 0, 0, 1, 0, 0);
+ }
+}
+
+void biquad_set(struct biquad *bq, enum biquad_type type, double freq)
+{
+
+ switch (type) {
+ case BQ_LOWPASS:
+ biquad_lowpass(bq, freq);
+ break;
+ case BQ_HIGHPASS:
+ biquad_highpass(bq, freq);
+ break;
+ }
+}
diff --git a/spa/plugins/audioconvert/biquad.h b/spa/plugins/audioconvert/biquad.h
new file mode 100644
index 0000000..8b7eccc
--- /dev/null
+++ b/spa/plugins/audioconvert/biquad.h
@@ -0,0 +1,45 @@
+/* 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.
+ */
+
+#ifndef BIQUAD_H_
+#define BIQUAD_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* 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
+ * y2.
+ *
+ * We use double during the coefficients calculation for better accuracy, but
+ * float is used during the actual filtering for faster computation.
+ */
+struct biquad {
+ float b0, b1, b2;
+ float a1, a2;
+};
+
+/* The type of the biquad filters */
+enum biquad_type {
+ BQ_LOWPASS,
+ BQ_HIGHPASS,
+};
+
+/* Initialize a biquad filter parameters from its type and parameters.
+ * Args:
+ * bq - The biquad filter we want to set.
+ * 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.
+ */
+void biquad_set(struct biquad *bq, enum biquad_type type, double freq);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* BIQUAD_H_ */
diff --git a/spa/plugins/audioconvert/channelmix-ops-c.c b/spa/plugins/audioconvert/channelmix-ops-c.c
new file mode 100644
index 0000000..f12f35f
--- /dev/null
+++ b/spa/plugins/audioconvert/channelmix-ops-c.c
@@ -0,0 +1,533 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "channelmix-ops.h"
+
+static inline void clear_c(float *d, uint32_t n_samples)
+{
+ memset(d, 0, n_samples * sizeof(float));
+}
+
+static inline void copy_c(float *d, const float *s, uint32_t n_samples)
+{
+ spa_memcpy(d, s, n_samples * sizeof(float));
+}
+
+static inline void vol_c(float *d, const float *s, float vol, uint32_t n_samples)
+{
+ uint32_t n;
+ if (vol == 0.0f) {
+ clear_c(d, n_samples);
+ } else if (vol == 1.0f) {
+ copy_c(d, s, n_samples);
+ } else {
+ for (n = 0; n < n_samples; n++)
+ d[n] = s[n] * vol;
+ }
+}
+static inline void conv_c(float *d, const float **s, float *c, uint32_t n_c, uint32_t n_samples)
+{
+ uint32_t n, j;
+ for (n = 0; n < n_samples; n++) {
+ float sum = 0.0f;
+ for (j = 0; j < n_c; j++)
+ sum += s[j][n] * c[j];
+ d[n] = sum;
+ }
+}
+
+static inline void avg_c(float *d, const float *s0, const float *s1, uint32_t n_samples)
+{
+ uint32_t n;
+ for (n = 0; n < n_samples; n++)
+ d[n] = (s0[n] + s1[n]) * 0.5f;
+}
+
+static inline void sub_c(float *d, const float *s0, const float *s1, uint32_t n_samples)
+{
+ uint32_t n;
+ for (n = 0; n < n_samples; n++)
+ d[n] = s0[n] - s1[n];
+}
+
+void
+channelmix_copy_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ for (i = 0; i < n_dst; i++)
+ vol_c(d[i], s[i], mix->matrix[i][i], n_samples);
+}
+
+#define _M(ch) (1UL << SPA_AUDIO_CHANNEL_ ## ch)
+
+void
+channelmix_f32_n_m_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, j, n_dst = mix->dst_chan, n_src = mix->src_chan;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_COPY)) {
+ uint32_t copy = SPA_MIN(n_dst, n_src);
+ for (i = 0; i < copy; i++)
+ copy_c(d[i], s[i], n_samples);
+ for (; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ for (i = 0; i < n_dst; i++) {
+ float *di = d[i];
+ float mj[n_src];
+ const float *sj[n_src];
+ uint32_t n_j = 0;
+
+ for (j = 0; j < n_src; j++) {
+ if (mix->matrix[i][j] == 0.0f)
+ continue;
+ mj[n_j] = mix->matrix[i][j];
+ sj[n_j++] = s[j];
+ }
+ if (n_j == 0) {
+ clear_c(di, n_samples);
+ } else if (n_j == 1) {
+ lr4_process(&mix->lr4[i], di, sj[0], mj[0], n_samples);
+ } else {
+ conv_c(di, sj, mj, n_j, n_samples);
+ lr4_process(&mix->lr4[i], di, di, 1.0f, n_samples);
+ }
+ }
+ }
+}
+
+#define MASK_MONO _M(FC)|_M(MONO)|_M(UNKNOWN)
+#define MASK_STEREO _M(FL)|_M(FR)|_M(UNKNOWN)
+
+void
+channelmix_f32_1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][0];
+
+ vol_c(d[0], s[0], v0, n_samples);
+ vol_c(d[1], s[0], v1, n_samples);
+}
+
+void
+channelmix_f32_2_1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t n;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[0][1];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ clear_c(d[0], n_samples);
+ } else if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_EQUAL)) {
+ for (n = 0; n < n_samples; n++)
+ d[0][n] = (s[0][n] + s[1][n]) * v0;
+ }
+ else {
+ for (n = 0; n < n_samples; n++)
+ d[0][n] = s[0][n] * v0 + s[1][n] * v1;
+ }
+}
+
+void
+channelmix_f32_4_1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t n;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[0][1];
+ const float v2 = mix->matrix[0][2];
+ const float v3 = mix->matrix[0][3];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ clear_c(d[0], n_samples);
+ }
+ else if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_EQUAL)) {
+ for (n = 0; n < n_samples; n++)
+ d[0][n] = (s[0][n] + s[1][n] + s[2][n] + s[3][n]) * v0;
+ }
+ else {
+ for (n = 0; n < n_samples; n++)
+ d[0][n] = s[0][n] * v0 + s[1][n] * v1 +
+ s[2][n] * v2 + s[3][n] * v3;
+ }
+}
+
+#define MASK_QUAD _M(FL)|_M(FR)|_M(RL)|_M(RR)|_M(UNKNOWN)
+
+void
+channelmix_f32_2_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float v2 = mix->matrix[2][0];
+ const float v3 = mix->matrix[3][1];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ vol_c(d[0], s[0], v0, n_samples);
+ vol_c(d[1], s[1], v1, n_samples);
+ if (mix->upmix != CHANNELMIX_UPMIX_PSD) {
+ vol_c(d[2], s[0], v2, n_samples);
+ vol_c(d[3], s[1], v3, n_samples);
+ } else {
+ sub_c(d[2], s[0], s[1], n_samples);
+
+ delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[3], d[2], -v3, n_samples);
+ delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[2], d[2], v2, n_samples);
+ }
+ }
+}
+
+#define MASK_3_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)
+void
+channelmix_f32_2_3p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float v2 = (mix->matrix[2][0] + mix->matrix[2][1]) * 0.5f;
+ const float v3 = (mix->matrix[3][0] + mix->matrix[3][1]) * 0.5f;
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ if (mix->widen == 0.0f) {
+ vol_c(d[0], s[0], v0, n_samples);
+ vol_c(d[1], s[1], v1, n_samples);
+ avg_c(d[2], s[0], s[1], n_samples);
+ } else {
+ for (n = 0; n < n_samples; n++) {
+ float c = s[0][n] + s[1][n];
+ float w = c * mix->widen;
+ d[0][n] = (s[0][n] - w) * v0;
+ d[1][n] = (s[1][n] - w) * v1;
+ 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);
+ }
+}
+
+#define MASK_5_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR)
+void
+channelmix_f32_2_5p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v4 = mix->matrix[4][0];
+ const float v5 = mix->matrix[5][1];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ channelmix_f32_2_3p1_c(mix, dst, src, n_samples);
+
+ if (mix->upmix != CHANNELMIX_UPMIX_PSD) {
+ vol_c(d[4], s[0], v4, n_samples);
+ vol_c(d[5], s[1], v5, n_samples);
+ } else {
+ sub_c(d[4], s[0], s[1], n_samples);
+
+ delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[5], d[4], -v5, n_samples);
+ delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[4], d[4], v4, n_samples);
+ }
+ }
+}
+
+void
+channelmix_f32_2_7p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v4 = mix->matrix[4][0];
+ const float v5 = mix->matrix[5][1];
+ const float v6 = mix->matrix[6][0];
+ const float v7 = mix->matrix[7][1];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ channelmix_f32_2_3p1_c(mix, dst, src, n_samples);
+
+ vol_c(d[4], s[0], v4, n_samples);
+ vol_c(d[5], s[1], v5, n_samples);
+
+ if (mix->upmix != CHANNELMIX_UPMIX_PSD) {
+ vol_c(d[6], s[0], v6, n_samples);
+ vol_c(d[7], s[1], v7, n_samples);
+ } else {
+ sub_c(d[6], s[0], s[1], n_samples);
+
+ delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[7], d[6], -v7, n_samples);
+ delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[6], d[6], v6, n_samples);
+ }
+ }
+}
+
+/* FL+FR+FC+LFE -> FL+FR */
+void
+channelmix_f32_3p1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t n;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f;
+ const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f;
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ clear_c(d[0], n_samples);
+ clear_c(d[1], n_samples);
+ }
+ else {
+ for (n = 0; n < n_samples; n++) {
+ const float ctr = clev * s[2][n] + llev * s[3][n];
+ d[0][n] = s[0][n] * v0 + ctr;
+ d[1][n] = s[1][n] * v1 + ctr;
+ }
+ }
+}
+
+/* FL+FR+FC+LFE+SL+SR -> FL+FR */
+void
+channelmix_f32_5p1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t n;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f;
+ const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f;
+ const float slev0 = mix->matrix[0][4];
+ const float slev1 = mix->matrix[1][5];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ clear_c(d[0], n_samples);
+ clear_c(d[1], n_samples);
+ }
+ else {
+ for (n = 0; n < n_samples; n++) {
+ const float ctr = clev * s[2][n] + llev * s[3][n];
+ d[0][n] = s[0][n] * v0 + ctr + (slev0 * s[4][n]);
+ d[1][n] = s[1][n] * v1 + ctr + (slev1 * s[5][n]);
+ }
+ }
+}
+
+/* FL+FR+FC+LFE+SL+SR -> FL+FR+FC+LFE*/
+void
+channelmix_f32_5p1_3p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n, n_dst = mix->dst_chan;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float v2 = mix->matrix[2][2];
+ const float v3 = mix->matrix[3][3];
+ const float v4 = mix->matrix[0][4];
+ const float v5 = mix->matrix[1][5];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ for (n = 0; n < n_samples; n++) {
+ d[0][n] = s[0][n] * v0 + s[4][n] * v4;
+ d[1][n] = s[1][n] * v1 + s[5][n] * v5;
+ }
+ vol_c(d[2], s[2], v2, n_samples);
+ vol_c(d[3], s[3], v3, n_samples);
+ }
+}
+
+/* FL+FR+FC+LFE+SL+SR -> FL+FR+RL+RR*/
+void
+channelmix_f32_5p1_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float v4 = mix->matrix[2][4];
+ const float v5 = mix->matrix[3][5];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ channelmix_f32_3p1_2_c(mix, dst, src, n_samples);
+
+ vol_c(d[2], s[4], v4, n_samples);
+ vol_c(d[3], s[5], v5, n_samples);
+ }
+}
+
+#define MASK_7_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR)
+
+/* FL+FR+FC+LFE+SL+SR+RL+RR -> FL+FR */
+void
+channelmix_f32_7p1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t n;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f;
+ const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f;
+ const float slev0 = mix->matrix[0][4];
+ const float slev1 = mix->matrix[1][5];
+ const float rlev0 = mix->matrix[0][6];
+ const float rlev1 = mix->matrix[1][7];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ clear_c(d[0], n_samples);
+ clear_c(d[1], n_samples);
+ }
+ else {
+ for (n = 0; n < n_samples; n++) {
+ const float ctr = clev * s[2][n] + llev * s[3][n];
+ d[0][n] = s[0][n] * v0 + ctr + s[4][n] * slev0 + s[6][n] * rlev0;
+ d[1][n] = s[1][n] * v1 + ctr + s[5][n] * slev1 + s[7][n] * rlev1;
+ }
+ }
+}
+
+/* FL+FR+FC+LFE+SL+SR+RL+RR -> FL+FR+FC+LFE*/
+void
+channelmix_f32_7p1_3p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n, n_dst = mix->dst_chan;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float v2 = mix->matrix[2][2];
+ const float v3 = mix->matrix[3][3];
+ const float v4 = (mix->matrix[0][4] + mix->matrix[0][6]) * 0.5f;
+ const float v5 = (mix->matrix[1][5] + mix->matrix[1][7]) * 0.5f;
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ for (n = 0; n < n_samples; n++) {
+ d[0][n] = s[0][n] * v0 + (s[4][n] + s[6][n]) * v4;
+ d[1][n] = s[1][n] * v1 + (s[5][n] + s[7][n]) * v5;
+ }
+ vol_c(d[2], s[2], v2, n_samples);
+ vol_c(d[3], s[3], v3, n_samples);
+ }
+}
+
+/* FL+FR+FC+LFE+SL+SR+RL+RR -> FL+FR+RL+RR*/
+void
+channelmix_f32_7p1_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n, n_dst = mix->dst_chan;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f;
+ const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f;
+ const float slev0 = mix->matrix[2][4];
+ const float slev1 = mix->matrix[3][5];
+ const float rlev0 = mix->matrix[2][6];
+ const float rlev1 = mix->matrix[3][7];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_c(d[i], n_samples);
+ }
+ else {
+ for (n = 0; n < n_samples; n++) {
+ const float ctr = s[2][n] * clev + s[3][n] * llev;
+ const float sl = s[4][n] * slev0;
+ const float sr = s[5][n] * slev1;
+ d[0][n] = s[0][n] * v0 + ctr + sl;
+ d[1][n] = s[1][n] * v1 + ctr + sr;
+ d[2][n] = s[6][n] * rlev0 + sl;
+ d[3][n] = s[7][n] * rlev1 + sr;
+ }
+ }
+}
diff --git a/spa/plugins/audioconvert/channelmix-ops-sse.c b/spa/plugins/audioconvert/channelmix-ops-sse.c
new file mode 100644
index 0000000..8311fb4
--- /dev/null
+++ b/spa/plugins/audioconvert/channelmix-ops-sse.c
@@ -0,0 +1,522 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "channelmix-ops.h"
+
+#include <xmmintrin.h>
+
+static inline void clear_sse(float *d, uint32_t n_samples)
+{
+ memset(d, 0, n_samples * sizeof(float));
+}
+
+static inline void copy_sse(float *d, const float *s, uint32_t n_samples)
+{
+ spa_memcpy(d, s, n_samples * sizeof(float));
+}
+
+static inline void vol_sse(float *d, const float *s, float vol, uint32_t n_samples)
+{
+ uint32_t n, unrolled;
+ if (vol == 0.0f) {
+ clear_sse(d, n_samples);
+ } else if (vol == 1.0f) {
+ copy_sse(d, s, n_samples);
+ } else {
+ __m128 t[4];
+ const __m128 v = _mm_set1_ps(vol);
+
+ if (SPA_IS_ALIGNED(d, 16) &&
+ SPA_IS_ALIGNED(s, 16))
+ unrolled = n_samples & ~15;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 16) {
+ t[0] = _mm_load_ps(&s[n]);
+ t[1] = _mm_load_ps(&s[n+4]);
+ t[2] = _mm_load_ps(&s[n+8]);
+ t[3] = _mm_load_ps(&s[n+12]);
+ _mm_store_ps(&d[n], _mm_mul_ps(t[0], v));
+ _mm_store_ps(&d[n+4], _mm_mul_ps(t[1], v));
+ _mm_store_ps(&d[n+8], _mm_mul_ps(t[2], v));
+ _mm_store_ps(&d[n+12], _mm_mul_ps(t[3], v));
+ }
+ for(; n < n_samples; n++)
+ _mm_store_ss(&d[n], _mm_mul_ss(_mm_load_ss(&s[n]), v));
+ }
+}
+
+static inline void conv_sse(float *d, const float **s, float *c, uint32_t n_c, uint32_t n_samples)
+{
+ __m128 mi[n_c], sum[2];
+ uint32_t n, j, unrolled;
+ bool aligned = true;
+
+ for (j = 0; j < n_c; j++) {
+ mi[j] = _mm_set1_ps(c[j]);
+ aligned &= SPA_IS_ALIGNED(s[j], 16);
+ }
+
+ if (aligned && SPA_IS_ALIGNED(d, 16))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for (n = 0; n < unrolled; n += 8) {
+ sum[0] = sum[1] = _mm_setzero_ps();
+ for (j = 0; j < n_c; j++) {
+ sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(_mm_load_ps(&s[j][n + 0]), mi[j]));
+ sum[1] = _mm_add_ps(sum[1], _mm_mul_ps(_mm_load_ps(&s[j][n + 4]), mi[j]));
+ }
+ _mm_store_ps(&d[n + 0], sum[0]);
+ _mm_store_ps(&d[n + 4], sum[1]);
+ }
+ for (; n < n_samples; n++) {
+ sum[0] = _mm_setzero_ps();
+ for (j = 0; j < n_c; j++)
+ sum[0] = _mm_add_ss(sum[0], _mm_mul_ss(_mm_load_ss(&s[j][n]), mi[j]));
+ _mm_store_ss(&d[n], sum[0]);
+ }
+}
+
+static inline void avg_sse(float *d, const float *s0, const float *s1, uint32_t n_samples)
+{
+ uint32_t n, unrolled;
+ __m128 half = _mm_set1_ps(0.5f);
+
+ if (SPA_IS_ALIGNED(d, 16) &&
+ SPA_IS_ALIGNED(s0, 16) &&
+ SPA_IS_ALIGNED(s1, 16))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for (n = 0; n < unrolled; n += 8) {
+ _mm_store_ps(&d[n + 0],
+ _mm_mul_ps(
+ _mm_add_ps(
+ _mm_load_ps(&s0[n + 0]),
+ _mm_load_ps(&s1[n + 0])),
+ half));
+ _mm_store_ps(&d[n + 4],
+ _mm_mul_ps(
+ _mm_add_ps(
+ _mm_load_ps(&s0[n + 4]),
+ _mm_load_ps(&s1[n + 4])),
+ half));
+ }
+
+ for (; n < n_samples; n++)
+ _mm_store_ss(&d[n],
+ _mm_mul_ss(
+ _mm_add_ss(
+ _mm_load_ss(&s0[n]),
+ _mm_load_ss(&s1[n])),
+ half));
+}
+
+static inline void sub_sse(float *d, const float *s0, const float *s1, uint32_t n_samples)
+{
+ uint32_t n, unrolled;
+
+ if (SPA_IS_ALIGNED(d, 16) &&
+ SPA_IS_ALIGNED(s0, 16) &&
+ SPA_IS_ALIGNED(s1, 16))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for (n = 0; n < unrolled; n += 8) {
+ _mm_store_ps(&d[n + 0],
+ _mm_sub_ps(_mm_load_ps(&s0[n + 0]), _mm_load_ps(&s1[n + 0])));
+ _mm_store_ps(&d[n + 4],
+ _mm_sub_ps(_mm_load_ps(&s0[n + 4]), _mm_load_ps(&s1[n + 4])));
+ }
+ for (; n < n_samples; n++)
+ _mm_store_ss(&d[n],
+ _mm_sub_ss(_mm_load_ss(&s0[n]), _mm_load_ss(&s1[n])));
+}
+
+void channelmix_copy_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ for (i = 0; i < n_dst; i++)
+ vol_sse(d[i], s[i], mix->matrix[i][i], n_samples);
+}
+
+void
+channelmix_f32_n_m_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ uint32_t i, j, n_dst = mix->dst_chan, n_src = mix->src_chan;
+
+ for (i = 0; i < n_dst; i++) {
+ float *di = d[i];
+ float mj[n_src];
+ const float *sj[n_src];
+ uint32_t n_j = 0;
+
+ for (j = 0; j < n_src; j++) {
+ if (mix->matrix[i][j] == 0.0f)
+ continue;
+ mj[n_j] = mix->matrix[i][j];
+ sj[n_j++] = s[j];
+ }
+ if (n_j == 0) {
+ clear_sse(di, n_samples);
+ } else if (n_j == 1) {
+ if (mix->lr4[i].active)
+ lr4_process(&mix->lr4[i], di, sj[0], mj[0], n_samples);
+ else
+ vol_sse(di, sj[0], mj[0], n_samples);
+ } else {
+ conv_sse(di, sj, mj, n_j, n_samples);
+ lr4_process(&mix->lr4[i], di, di, 1.0f, n_samples);
+ }
+ }
+}
+
+void
+channelmix_f32_2_3p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n, unrolled, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v0 = mix->matrix[0][0];
+ const float v1 = mix->matrix[1][1];
+ const float v2 = (mix->matrix[2][0] + mix->matrix[2][1]) * 0.5f;
+ const float v3 = (mix->matrix[3][0] + mix->matrix[3][1]) * 0.5f;
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_sse(d[i], n_samples);
+ }
+ else {
+ if (mix->widen == 0.0f) {
+ vol_sse(d[0], s[0], v0, n_samples);
+ vol_sse(d[1], s[1], v1, n_samples);
+ avg_sse(d[2], s[0], s[1], n_samples);
+ } else {
+ const __m128 mv0 = _mm_set1_ps(mix->matrix[0][0]);
+ const __m128 mv1 = _mm_set1_ps(mix->matrix[1][1]);
+ const __m128 mw = _mm_set1_ps(mix->widen);
+ const __m128 mh = _mm_set1_ps(0.5f);
+ __m128 t0[1], t1[1], w[1], c[1];
+
+ if (SPA_IS_ALIGNED(s[0], 16) &&
+ SPA_IS_ALIGNED(s[1], 16) &&
+ SPA_IS_ALIGNED(d[0], 16) &&
+ SPA_IS_ALIGNED(d[1], 16) &&
+ SPA_IS_ALIGNED(d[2], 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ t0[0] = _mm_load_ps(&s[0][n]);
+ t1[0] = _mm_load_ps(&s[1][n]);
+ c[0] = _mm_add_ps(t0[0], t1[0]);
+ w[0] = _mm_mul_ps(c[0], mw);
+ _mm_store_ps(&d[0][n], _mm_mul_ps(_mm_sub_ps(t0[0], w[0]), mv0));
+ _mm_store_ps(&d[1][n], _mm_mul_ps(_mm_sub_ps(t1[0], w[0]), mv1));
+ _mm_store_ps(&d[2][n], _mm_mul_ps(c[0], mh));
+ }
+ for (; n < n_samples; n++) {
+ t0[0] = _mm_load_ss(&s[0][n]);
+ t1[0] = _mm_load_ss(&s[1][n]);
+ c[0] = _mm_add_ss(t0[0], t1[0]);
+ w[0] = _mm_mul_ss(c[0], mw);
+ _mm_store_ss(&d[0][n], _mm_mul_ss(_mm_sub_ss(t0[0], w[0]), mv0));
+ _mm_store_ss(&d[1][n], _mm_mul_ss(_mm_sub_ss(t1[0], w[0]), mv1));
+ _mm_store_ss(&d[2][n], _mm_mul_ss(c[0], mh));
+ }
+ }
+ lr4_process(&mix->lr4[3], d[3], d[2], v3, n_samples);
+ lr4_process(&mix->lr4[2], d[2], d[2], v2, n_samples);
+ }
+}
+
+void
+channelmix_f32_2_5p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v4 = mix->matrix[4][0];
+ const float v5 = mix->matrix[5][1];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_sse(d[i], n_samples);
+ }
+ else {
+ channelmix_f32_2_3p1_sse(mix, dst, src, n_samples);
+
+ if (mix->upmix != CHANNELMIX_UPMIX_PSD) {
+ vol_sse(d[4], s[0], v4, n_samples);
+ vol_sse(d[5], s[1], v5, n_samples);
+ } else {
+ sub_sse(d[4], s[0], s[1], n_samples);
+
+ delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[5], d[4], -v5, n_samples);
+ delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[4], d[4], v4, n_samples);
+ }
+ }
+}
+
+void
+channelmix_f32_2_7p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **)dst;
+ const float **s = (const float **)src;
+ const float v4 = mix->matrix[4][0];
+ const float v5 = mix->matrix[5][1];
+ const float v6 = mix->matrix[6][0];
+ const float v7 = mix->matrix[7][1];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_sse(d[i], n_samples);
+ }
+ else {
+ channelmix_f32_2_3p1_sse(mix, dst, src, n_samples);
+
+ vol_sse(d[4], s[0], v4, n_samples);
+ vol_sse(d[5], s[1], v5, n_samples);
+
+ if (mix->upmix != CHANNELMIX_UPMIX_PSD) {
+ vol_sse(d[6], s[0], v6, n_samples);
+ vol_sse(d[7], s[1], v7, n_samples);
+ } else {
+ sub_sse(d[6], s[0], s[1], n_samples);
+
+ delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[7], d[6], -v7, n_samples);
+ delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay,
+ mix->taps, mix->n_taps, d[6], d[6], v6, n_samples);
+ }
+ }
+}
+/* FL+FR+FC+LFE -> FL+FR */
+void
+channelmix_f32_3p1_2_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float m0 = mix->matrix[0][0];
+ const float m1 = mix->matrix[1][1];
+ const float m2 = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f;
+ const float m3 = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f;
+
+ if (m0 == 0.0f && m1 == 0.0f && m2 == 0.0f && m3 == 0.0f) {
+ clear_sse(d[0], n_samples);
+ clear_sse(d[1], n_samples);
+ }
+ else {
+ uint32_t n, unrolled;
+ const __m128 v0 = _mm_set1_ps(m0);
+ const __m128 v1 = _mm_set1_ps(m1);
+ const __m128 clev = _mm_set1_ps(m2);
+ const __m128 llev = _mm_set1_ps(m3);
+ __m128 ctr;
+
+ if (SPA_IS_ALIGNED(s[0], 16) &&
+ SPA_IS_ALIGNED(s[1], 16) &&
+ SPA_IS_ALIGNED(s[2], 16) &&
+ SPA_IS_ALIGNED(s[3], 16) &&
+ SPA_IS_ALIGNED(d[0], 16) &&
+ SPA_IS_ALIGNED(d[1], 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ ctr = _mm_add_ps(
+ _mm_mul_ps(_mm_load_ps(&s[2][n]), clev),
+ _mm_mul_ps(_mm_load_ps(&s[3][n]), llev));
+ _mm_store_ps(&d[0][n], _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[0][n]), v0), ctr));
+ _mm_store_ps(&d[1][n], _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[1][n]), v1), ctr));
+ }
+ for(; n < n_samples; n++) {
+ ctr = _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[2][n]), clev),
+ _mm_mul_ss(_mm_load_ss(&s[3][n]), llev));
+ _mm_store_ss(&d[0][n], _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[0][n]), v0), ctr));
+ _mm_store_ss(&d[1][n], _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[1][n]), v1), ctr));
+ }
+ }
+}
+
+/* FL+FR+FC+LFE+SL+SR -> FL+FR */
+void
+channelmix_f32_5p1_2_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t n, unrolled;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float m00 = mix->matrix[0][0];
+ const float m11 = mix->matrix[1][1];
+ const __m128 clev = _mm_set1_ps((mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f);
+ const __m128 llev = _mm_set1_ps((mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f);
+ const __m128 slev0 = _mm_set1_ps(mix->matrix[0][4]);
+ const __m128 slev1 = _mm_set1_ps(mix->matrix[1][5]);
+ __m128 in, ctr;
+
+ if (SPA_IS_ALIGNED(s[0], 16) &&
+ SPA_IS_ALIGNED(s[1], 16) &&
+ SPA_IS_ALIGNED(s[2], 16) &&
+ SPA_IS_ALIGNED(s[3], 16) &&
+ SPA_IS_ALIGNED(s[4], 16) &&
+ SPA_IS_ALIGNED(s[5], 16) &&
+ SPA_IS_ALIGNED(d[0], 16) &&
+ SPA_IS_ALIGNED(d[1], 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ clear_sse(d[0], n_samples);
+ clear_sse(d[1], n_samples);
+ }
+ else {
+ const __m128 v0 = _mm_set1_ps(m00);
+ const __m128 v1 = _mm_set1_ps(m11);
+ for(n = 0; n < unrolled; n += 4) {
+ ctr = _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[2][n]), clev),
+ _mm_mul_ps(_mm_load_ps(&s[3][n]), llev));
+ in = _mm_mul_ps(_mm_load_ps(&s[4][n]), slev0);
+ in = _mm_add_ps(in, ctr);
+ in = _mm_add_ps(in, _mm_mul_ps(_mm_load_ps(&s[0][n]), v0));
+ _mm_store_ps(&d[0][n], in);
+ in = _mm_mul_ps(_mm_load_ps(&s[5][n]), slev1);
+ in = _mm_add_ps(in, ctr);
+ in = _mm_add_ps(in, _mm_mul_ps(_mm_load_ps(&s[1][n]), v1));
+ _mm_store_ps(&d[1][n], in);
+ }
+ for(; n < n_samples; n++) {
+ ctr = _mm_mul_ss(_mm_load_ss(&s[2][n]), clev);
+ ctr = _mm_add_ss(ctr, _mm_mul_ss(_mm_load_ss(&s[3][n]), llev));
+ in = _mm_mul_ss(_mm_load_ss(&s[4][n]), slev0);
+ in = _mm_add_ss(in, ctr);
+ in = _mm_add_ss(in, _mm_mul_ss(_mm_load_ss(&s[0][n]), v0));
+ _mm_store_ss(&d[0][n], in);
+ in = _mm_mul_ss(_mm_load_ss(&s[5][n]), slev1);
+ in = _mm_add_ss(in, ctr);
+ in = _mm_add_ss(in, _mm_mul_ss(_mm_load_ss(&s[1][n]), v1));
+ _mm_store_ss(&d[1][n], in);
+ }
+ }
+}
+
+/* FL+FR+FC+LFE+SL+SR -> FL+FR+FC+LFE*/
+void
+channelmix_f32_5p1_3p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n, unrolled, n_dst = mix->dst_chan;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+
+ if (SPA_IS_ALIGNED(s[0], 16) &&
+ SPA_IS_ALIGNED(s[1], 16) &&
+ SPA_IS_ALIGNED(s[2], 16) &&
+ SPA_IS_ALIGNED(s[3], 16) &&
+ SPA_IS_ALIGNED(s[4], 16) &&
+ SPA_IS_ALIGNED(s[5], 16) &&
+ SPA_IS_ALIGNED(d[0], 16) &&
+ SPA_IS_ALIGNED(d[1], 16) &&
+ SPA_IS_ALIGNED(d[2], 16) &&
+ SPA_IS_ALIGNED(d[3], 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_sse(d[i], n_samples);
+ }
+ else {
+ const __m128 v0 = _mm_set1_ps(mix->matrix[0][0]);
+ const __m128 v1 = _mm_set1_ps(mix->matrix[1][1]);
+ const __m128 slev0 = _mm_set1_ps(mix->matrix[0][4]);
+ const __m128 slev1 = _mm_set1_ps(mix->matrix[1][5]);
+
+ for(n = 0; n < unrolled; n += 4) {
+ _mm_store_ps(&d[0][n], _mm_add_ps(
+ _mm_mul_ps(_mm_load_ps(&s[0][n]), v0),
+ _mm_mul_ps(_mm_load_ps(&s[4][n]), slev0)));
+
+ _mm_store_ps(&d[1][n], _mm_add_ps(
+ _mm_mul_ps(_mm_load_ps(&s[1][n]), v1),
+ _mm_mul_ps(_mm_load_ps(&s[5][n]), slev1)));
+ }
+ for(; n < n_samples; n++) {
+ _mm_store_ss(&d[0][n], _mm_add_ss(
+ _mm_mul_ss(_mm_load_ss(&s[0][n]), v0),
+ _mm_mul_ss(_mm_load_ss(&s[4][n]), slev0)));
+
+ _mm_store_ss(&d[1][n], _mm_add_ss(
+ _mm_mul_ss(_mm_load_ss(&s[1][n]), v1),
+ _mm_mul_ss(_mm_load_ss(&s[5][n]), slev1)));
+ }
+ vol_sse(d[2], s[2], mix->matrix[2][2], n_samples);
+ vol_sse(d[3], s[3], mix->matrix[3][3], n_samples);
+ }
+}
+
+/* FL+FR+FC+LFE+SL+SR -> FL+FR+RL+RR*/
+void
+channelmix_f32_5p1_4_sse(struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+{
+ uint32_t i, n_dst = mix->dst_chan;
+ float **d = (float **) dst;
+ const float **s = (const float **) src;
+ const float v4 = mix->matrix[2][4];
+ const float v5 = mix->matrix[3][5];
+
+ if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) {
+ for (i = 0; i < n_dst; i++)
+ clear_sse(d[i], n_samples);
+ }
+ else {
+ channelmix_f32_3p1_2_sse(mix, dst, src, n_samples);
+
+ vol_sse(d[2], s[4], v4, n_samples);
+ vol_sse(d[3], s[5], v5, n_samples);
+ }
+}
diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c
new file mode 100644
index 0000000..527763b
--- /dev/null
+++ b/spa/plugins/audioconvert/channelmix-ops.c
@@ -0,0 +1,668 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/support/cpu.h>
+#include <spa/support/log.h>
+#include <spa/utils/defs.h>
+#include <spa/debug/types.h>
+
+#include "channelmix-ops.h"
+#include "hilbert.h"
+
+#define ANY ((uint32_t)-1)
+#define EQ ((uint32_t)-2)
+
+typedef void (*channelmix_func_t) (struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples);
+
+#define MAKE(sc,sm,dc,dm,func,...) \
+ { sc, sm, dc, dm, func, #func, __VA_ARGS__ }
+
+static const struct channelmix_info {
+ uint32_t src_chan;
+ uint64_t src_mask;
+ uint32_t dst_chan;
+ uint64_t dst_mask;
+
+ channelmix_func_t process;
+ const char *name;
+
+ uint32_t cpu_flags;
+} channelmix_table[] =
+{
+#if defined (HAVE_SSE)
+ MAKE(2, MASK_MONO, 2, MASK_MONO, channelmix_copy_sse, SPA_CPU_FLAG_SSE),
+ MAKE(2, MASK_STEREO, 2, MASK_STEREO, channelmix_copy_sse, SPA_CPU_FLAG_SSE),
+ MAKE(EQ, 0, EQ, 0, channelmix_copy_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(2, MASK_MONO, 2, MASK_MONO, channelmix_copy_c),
+ MAKE(2, MASK_STEREO, 2, MASK_STEREO, channelmix_copy_c),
+ MAKE(EQ, 0, EQ, 0, channelmix_copy_c),
+
+ MAKE(1, MASK_MONO, 2, MASK_STEREO, channelmix_f32_1_2_c),
+ MAKE(2, MASK_STEREO, 1, MASK_MONO, channelmix_f32_2_1_c),
+ MAKE(4, MASK_QUAD, 1, MASK_MONO, channelmix_f32_4_1_c),
+ MAKE(4, MASK_3_1, 1, MASK_MONO, channelmix_f32_4_1_c),
+ MAKE(2, MASK_STEREO, 4, MASK_QUAD, channelmix_f32_2_4_c),
+#if defined (HAVE_SSE)
+ MAKE(2, MASK_STEREO, 4, MASK_3_1, channelmix_f32_2_3p1_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(2, MASK_STEREO, 4, MASK_3_1, channelmix_f32_2_3p1_c),
+#if defined (HAVE_SSE)
+ MAKE(2, MASK_STEREO, 6, MASK_5_1, channelmix_f32_2_5p1_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(2, MASK_STEREO, 6, MASK_5_1, channelmix_f32_2_5p1_c),
+#if defined (HAVE_SSE)
+ MAKE(2, MASK_STEREO, 8, MASK_7_1, channelmix_f32_2_7p1_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(2, MASK_STEREO, 8, MASK_7_1, channelmix_f32_2_7p1_c),
+#if defined (HAVE_SSE)
+ MAKE(4, MASK_3_1, 2, MASK_STEREO, channelmix_f32_3p1_2_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(4, MASK_3_1, 2, MASK_STEREO, channelmix_f32_3p1_2_c),
+#if defined (HAVE_SSE)
+ MAKE(6, MASK_5_1, 2, MASK_STEREO, channelmix_f32_5p1_2_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(6, MASK_5_1, 2, MASK_STEREO, channelmix_f32_5p1_2_c),
+#if defined (HAVE_SSE)
+ MAKE(6, MASK_5_1, 4, MASK_QUAD, channelmix_f32_5p1_4_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(6, MASK_5_1, 4, MASK_QUAD, channelmix_f32_5p1_4_c),
+
+#if defined (HAVE_SSE)
+ MAKE(6, MASK_5_1, 4, MASK_3_1, channelmix_f32_5p1_3p1_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(6, MASK_5_1, 4, MASK_3_1, channelmix_f32_5p1_3p1_c),
+
+ MAKE(8, MASK_7_1, 2, MASK_STEREO, channelmix_f32_7p1_2_c),
+ MAKE(8, MASK_7_1, 4, MASK_QUAD, channelmix_f32_7p1_4_c),
+ MAKE(8, MASK_7_1, 4, MASK_3_1, channelmix_f32_7p1_3p1_c),
+
+#if defined (HAVE_SSE)
+ MAKE(ANY, 0, ANY, 0, channelmix_f32_n_m_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(ANY, 0, ANY, 0, channelmix_f32_n_m_c),
+};
+#undef MAKE
+
+#define MATCH_CHAN(a,b) ((a) == ANY || (a) == (b))
+#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a)
+#define MATCH_MASK(a,b) ((a) == 0 || ((a) & (b)) == (b))
+
+static const struct channelmix_info *find_channelmix_info(uint32_t src_chan, uint64_t src_mask,
+ uint32_t dst_chan, uint64_t dst_mask, uint32_t cpu_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(channelmix_table, info) {
+ if (!MATCH_CPU_FLAGS(info->cpu_flags, cpu_flags))
+ continue;
+
+ if (src_chan == dst_chan && src_mask == dst_mask)
+ return info;
+
+ if (MATCH_CHAN(info->src_chan, src_chan) &&
+ MATCH_CHAN(info->dst_chan, dst_chan) &&
+ MATCH_MASK(info->src_mask, src_mask) &&
+ MATCH_MASK(info->dst_mask, dst_mask))
+ return info;
+ }
+ return NULL;
+}
+
+#define SQRT3_2 1.224744871f /* sqrt(3/2) */
+#define SQRT1_2 0.707106781f
+#define SQRT2 1.414213562f
+
+#define MATRIX_NORMAL 0
+#define MATRIX_DOLBY 1
+#define MATRIX_DPLII 2
+
+#define _CH(ch) ((SPA_AUDIO_CHANNEL_ ## ch)-3)
+#define _MASK(ch) (1ULL << _CH(ch))
+#define FRONT (_MASK(FC))
+#define STEREO (_MASK(FL)|_MASK(FR))
+#define REAR (_MASK(RL)|_MASK(RR))
+#define SIDE (_MASK(SL)|_MASK(SR))
+
+static int make_matrix(struct channelmix *mix)
+{
+ float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS] = {{ 0.0f }};
+ uint64_t src_mask = mix->src_mask;
+ uint64_t dst_mask = mix->dst_mask;
+ uint32_t src_chan = mix->src_chan;
+ uint32_t dst_chan = mix->dst_chan;
+ uint64_t unassigned, keep;
+ uint32_t i, j, ic, jc, matrix_encoding = MATRIX_NORMAL;
+ float clev = SQRT1_2;
+ float slev = SQRT1_2;
+ float llev = 0.5f;
+ float maxsum = 0.0f;
+ bool filter_fc = false, filter_lfe = false;
+#define _MATRIX(s,d) matrix[_CH(s)][_CH(d)]
+
+ spa_log_debug(mix->log, "src-mask:%08"PRIx64" dst-mask:%08"PRIx64
+ " options:%08x", src_mask, dst_mask, mix->options);
+
+ /* move the MONO mask to FRONT so that the lower bits can be shifted
+ * away. */
+ if ((src_mask & (1Ull << SPA_AUDIO_CHANNEL_MONO)) != 0) {
+ if (src_chan == 1)
+ src_mask = 0;
+ else
+ src_mask |= (1ULL << SPA_AUDIO_CHANNEL_FC);
+ }
+ if ((dst_mask & (1Ull << SPA_AUDIO_CHANNEL_MONO)) != 0)
+ dst_mask |= (1ULL << SPA_AUDIO_CHANNEL_FC);
+
+ /* shift so that bit 0 is FL */
+ src_mask >>= 3;
+ dst_mask >>= 3;
+
+ /* unknown channels or just 1 channel */
+ if (src_mask == 0 || dst_mask == 0) {
+ if (src_chan == 1) {
+ /* one FC/MONO src goes everywhere */
+ spa_log_debug(mix->log, "distribute FC/MONO (%f)", 1.0f);
+ for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
+ matrix[i][0]= 1.0f;
+ } else if (dst_chan == 1) {
+ /* one FC/MONO dst get average of everything */
+ spa_log_debug(mix->log, "average FC/MONO (%f)", 1.0f / src_chan);
+ for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
+ matrix[0][i]= 1.0f / src_chan;
+ } else {
+ /* just pair channels */
+ spa_log_debug(mix->log, "pairing channels (%f)", 1.0f);
+ for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++)
+ matrix[i][i]= 1.0f;
+ }
+ if (dst_mask & FRONT)
+ filter_fc = true;
+ if (dst_mask & _MASK(LFE))
+ filter_lfe = true;
+ src_mask = dst_mask = ~0LU;
+ goto done;
+ } else {
+ spa_log_debug(mix->log, "matching channels");
+ for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) {
+ if ((src_mask & dst_mask & (1ULL << i))) {
+ spa_log_debug(mix->log, "matched channel %u (%f)", i, 1.0f);
+ matrix[i][i]= 1.0f;
+ }
+ }
+ }
+
+ unassigned = src_mask & ~dst_mask;
+ keep = dst_mask & ~src_mask;
+
+ if (!SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_UPMIX)) {
+ keep = 0;
+ } else {
+ if (mix->upmix == CHANNELMIX_UPMIX_NONE)
+ keep = 0;
+ keep |= FRONT;
+ if (mix->lfe_cutoff > 0.0f)
+ keep |= _MASK(LFE);
+ else
+ keep &= ~_MASK(LFE);
+ }
+
+ spa_log_debug(mix->log, "unassigned downmix %08" PRIx64 " %08" PRIx64, unassigned, keep);
+
+ if (unassigned & FRONT){
+ if ((dst_mask & STEREO) == STEREO){
+ if(src_mask & STEREO) {
+ spa_log_debug(mix->log, "assign FC to STEREO (%f)", clev);
+ _MATRIX(FL,FC) += clev;
+ _MATRIX(FR,FC) += clev;
+ } else {
+ spa_log_debug(mix->log, "assign FC to STEREO (%f)", SQRT1_2);
+ _MATRIX(FL,FC) += SQRT1_2;
+ _MATRIX(FR,FC) += SQRT1_2;
+ }
+ } else {
+ spa_log_warn(mix->log, "can't assign FC");
+ }
+ }
+
+ if (unassigned & STEREO){
+ if (dst_mask & FRONT) {
+ spa_log_debug(mix->log, "assign STEREO to FC (%f)", SQRT1_2);
+ _MATRIX(FC,FL) += SQRT1_2;
+ _MATRIX(FC,FR) += SQRT1_2;
+ if (src_mask & FRONT) {
+ spa_log_debug(mix->log, "assign FC to FC (%f)", clev * SQRT2);
+ _MATRIX(FC,FC) = clev * SQRT2;
+ }
+ keep &= ~FRONT;
+ } else {
+ spa_log_warn(mix->log, "can't assign STEREO");
+ }
+ }
+
+ if (unassigned & _MASK(RC)) {
+ if (dst_mask & REAR){
+ spa_log_debug(mix->log, "assign RC to RL+RR (%f)", SQRT1_2);
+ _MATRIX(RL,RC) += SQRT1_2;
+ _MATRIX(RR,RC) += SQRT1_2;
+ } else if (dst_mask & SIDE) {
+ spa_log_debug(mix->log, "assign RC to SL+SR (%f)", SQRT1_2);
+ _MATRIX(SL,RC) += SQRT1_2;
+ _MATRIX(SR,RC) += SQRT1_2;
+ } else if(dst_mask & STEREO) {
+ spa_log_debug(mix->log, "assign RC to FL+FR");
+ if (matrix_encoding == MATRIX_DOLBY ||
+ matrix_encoding == MATRIX_DPLII) {
+ if (unassigned & (_MASK(RL)|_MASK(RR))) {
+ _MATRIX(FL,RC) -= slev * SQRT1_2;
+ _MATRIX(FR,RC) += slev * SQRT1_2;
+ } else {
+ _MATRIX(FL,RC) -= slev;
+ _MATRIX(FR,RC) += slev;
+ }
+ } else {
+ _MATRIX(FL,RC) += slev * SQRT1_2;
+ _MATRIX(FR,RC) += slev * SQRT1_2;
+ }
+ } else if (dst_mask & FRONT) {
+ spa_log_debug(mix->log, "assign RC to FC (%f)", slev * SQRT1_2);
+ _MATRIX(FC,RC) += slev * SQRT1_2;
+ } else {
+ spa_log_warn(mix->log, "can't assign RC");
+ }
+ }
+
+ if (unassigned & REAR) {
+ if (dst_mask & _MASK(RC)) {
+ spa_log_debug(mix->log, "assign RL+RR to RC");
+ _MATRIX(RC,RL) += SQRT1_2;
+ _MATRIX(RC,RR) += SQRT1_2;
+ } else if (dst_mask & SIDE) {
+ spa_log_debug(mix->log, "assign RL+RR to SL+SR");
+ if (src_mask & SIDE) {
+ _MATRIX(SL,RL) += SQRT1_2;
+ _MATRIX(SR,RR) += SQRT1_2;
+ } else {
+ _MATRIX(SL,RL) += 1.0f;
+ _MATRIX(SR,RR) += 1.0f;
+ }
+ keep &= ~SIDE;
+ } else if (dst_mask & STEREO) {
+ spa_log_debug(mix->log, "assign RL+RR to FL+FR (%f)", slev);
+ if (matrix_encoding == MATRIX_DOLBY) {
+ _MATRIX(FL,RL) -= slev * SQRT1_2;
+ _MATRIX(FL,RR) -= slev * SQRT1_2;
+ _MATRIX(FR,RL) += slev * SQRT1_2;
+ _MATRIX(FR,RR) += slev * SQRT1_2;
+ } else if (matrix_encoding == MATRIX_DPLII) {
+ _MATRIX(FL,RL) -= slev * SQRT3_2;
+ _MATRIX(FL,RR) -= slev * SQRT1_2;
+ _MATRIX(FR,RL) += slev * SQRT1_2;
+ _MATRIX(FR,RR) += slev * SQRT3_2;
+ } else {
+ _MATRIX(FL,RL) += slev;
+ _MATRIX(FR,RR) += slev;
+ }
+ } else if (dst_mask & FRONT) {
+ spa_log_debug(mix->log, "assign RL+RR to FC (%f)",
+ slev * SQRT1_2);
+ _MATRIX(FC,RL)+= slev * SQRT1_2;
+ _MATRIX(FC,RR)+= slev * SQRT1_2;
+ } else {
+ spa_log_warn(mix->log, "can't assign RL");
+ }
+ }
+
+ if (unassigned & SIDE) {
+ if (dst_mask & REAR) {
+ if (src_mask & _MASK(RL)) {
+ spa_log_debug(mix->log, "assign SL+SR to RL+RR (%f)", SQRT1_2);
+ _MATRIX(RL,SL) += SQRT1_2;
+ _MATRIX(RR,SR) += SQRT1_2;
+ } else {
+ spa_log_debug(mix->log, "assign SL+SR to RL+RR (%f)", 1.0f);
+ _MATRIX(RL,SL) += 1.0f;
+ _MATRIX(RR,SR) += 1.0f;
+ }
+ keep &= ~REAR;
+ } else if (dst_mask & _MASK(RC)) {
+ spa_log_debug(mix->log, "assign SL+SR to RC (%f)", SQRT1_2);
+ _MATRIX(RC,SL)+= SQRT1_2;
+ _MATRIX(RC,SR)+= SQRT1_2;
+ } else if (dst_mask & STEREO) {
+ if (matrix_encoding == MATRIX_DOLBY) {
+ spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f)",
+ slev * SQRT1_2);
+ _MATRIX(FL,SL) -= slev * SQRT1_2;
+ _MATRIX(FL,SR) -= slev * SQRT1_2;
+ _MATRIX(FR,SL) += slev * SQRT1_2;
+ _MATRIX(FR,SR) += slev * SQRT1_2;
+ } else if (matrix_encoding == MATRIX_DPLII) {
+ spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f / %f)",
+ slev * SQRT3_2, slev * SQRT1_2);
+ _MATRIX(FL,SL) -= slev * SQRT3_2;
+ _MATRIX(FL,SR) -= slev * SQRT1_2;
+ _MATRIX(FR,SL) += slev * SQRT1_2;
+ _MATRIX(FR,SR) += slev * SQRT3_2;
+ } else {
+ spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f)", slev);
+ _MATRIX(FL,SL) += slev;
+ _MATRIX(FR,SR) += slev;
+ }
+ } else if (dst_mask & FRONT) {
+ spa_log_debug(mix->log, "assign SL+SR to FC (%f)", slev * SQRT1_2);
+ _MATRIX(FC,SL) += slev * SQRT1_2;
+ _MATRIX(FC,SR) += slev * SQRT1_2;
+ } else {
+ spa_log_warn(mix->log, "can't assign SL");
+ }
+ }
+
+ if (unassigned & _MASK(FLC)) {
+ if (dst_mask & STEREO) {
+ spa_log_debug(mix->log, "assign FLC+FRC to FL+FR (%f)", 1.0f);
+ _MATRIX(FL,FLC)+= 1.0f;
+ _MATRIX(FR,FRC)+= 1.0f;
+ } else if(dst_mask & FRONT) {
+ spa_log_debug(mix->log, "assign FLC+FRC to FC (%f)", SQRT1_2);
+ _MATRIX(FC,FLC)+= SQRT1_2;
+ _MATRIX(FC,FRC)+= SQRT1_2;
+ } else {
+ spa_log_warn(mix->log, "can't assign FLC");
+ }
+ }
+ if (unassigned & _MASK(LFE) &&
+ SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_MIX_LFE)) {
+ if (dst_mask & FRONT) {
+ spa_log_debug(mix->log, "assign LFE to FC (%f)", llev);
+ _MATRIX(FC,LFE) += llev;
+ } else if (dst_mask & STEREO) {
+ spa_log_debug(mix->log, "assign LFE to FL+FR (%f)",
+ llev * SQRT1_2);
+ _MATRIX(FL,LFE) += llev * SQRT1_2;
+ _MATRIX(FR,LFE) += llev * SQRT1_2;
+ } else {
+ spa_log_warn(mix->log, "can't assign LFE");
+ }
+ }
+
+ unassigned = dst_mask & ~src_mask & keep;
+
+ spa_log_debug(mix->log, "unassigned upmix %08"PRIx64" lfe:%f",
+ unassigned, mix->lfe_cutoff);
+
+ if (unassigned & STEREO) {
+ if ((src_mask & FRONT) == FRONT) {
+ spa_log_debug(mix->log, "produce STEREO from FC (%f)", clev);
+ _MATRIX(FL,FC) += clev;
+ _MATRIX(FR,FC) += clev;
+ } else {
+ spa_log_warn(mix->log, "can't produce STEREO");
+ }
+ }
+ if (unassigned & FRONT) {
+ if ((src_mask & STEREO) == STEREO) {
+ spa_log_debug(mix->log, "produce FC from STEREO (%f)", clev);
+ _MATRIX(FC,FL) += clev;
+ _MATRIX(FC,FR) += clev;
+ filter_fc = true;
+ } else {
+ spa_log_warn(mix->log, "can't produce FC");
+ }
+ }
+ if (unassigned & _MASK(LFE)) {
+ if ((src_mask & STEREO) == STEREO) {
+ spa_log_debug(mix->log, "produce LFE from STEREO (%f)", llev);
+ _MATRIX(LFE,FL) += llev;
+ _MATRIX(LFE,FR) += llev;
+ filter_lfe = true;
+ } else if ((src_mask & FRONT) == FRONT) {
+ spa_log_debug(mix->log, "produce LFE from FC (%f)", llev);
+ _MATRIX(LFE,FC) += llev;
+ filter_lfe = true;
+ } else {
+ spa_log_warn(mix->log, "can't produce LFE");
+ }
+ }
+ if (unassigned & SIDE) {
+ if ((src_mask & REAR) == REAR) {
+ spa_log_debug(mix->log, "produce SIDE from REAR (%f)", 1.0f);
+ _MATRIX(SL,RL) += 1.0f;
+ _MATRIX(SR,RR) += 1.0f;
+ } else if ((src_mask & STEREO) == STEREO) {
+ spa_log_debug(mix->log, "produce SIDE from STEREO (%f)", slev);
+ _MATRIX(SL,FL) += slev;
+ _MATRIX(SR,FR) += slev;
+ } else if ((src_mask & FRONT) == FRONT &&
+ mix->upmix == CHANNELMIX_UPMIX_SIMPLE) {
+ spa_log_debug(mix->log, "produce SIDE from FC (%f)", clev);
+ _MATRIX(SL,FC) += clev;
+ _MATRIX(SR,FC) += clev;
+ } else {
+ spa_log_debug(mix->log, "won't produce SIDE");
+ }
+ }
+ if (unassigned & REAR) {
+ if ((src_mask & SIDE) == SIDE) {
+ spa_log_debug(mix->log, "produce REAR from SIDE (%f)", 1.0f);
+ _MATRIX(RL,SL) += 1.0f;
+ _MATRIX(RR,SR) += 1.0f;
+ } else if ((src_mask & STEREO) == STEREO) {
+ spa_log_debug(mix->log, "produce REAR from STEREO (%f)", slev);
+ _MATRIX(RL,FL) += slev;
+ _MATRIX(RR,FR) += slev;
+ } else if ((src_mask & FRONT) == FRONT &&
+ mix->upmix == CHANNELMIX_UPMIX_SIMPLE) {
+ spa_log_debug(mix->log, "produce REAR from FC (%f)", clev);
+ _MATRIX(RL,FC) += clev;
+ _MATRIX(RR,FC) += clev;
+ } else {
+ spa_log_debug(mix->log, "won't produce SIDE");
+ }
+ }
+ if (unassigned & _MASK(RC)) {
+ if ((src_mask & REAR) == REAR) {
+ spa_log_debug(mix->log, "produce RC from REAR (%f)", 0.5f);
+ _MATRIX(RC,RL) += 0.5f;
+ _MATRIX(RC,RR) += 0.5f;
+ } else if ((src_mask & SIDE) == SIDE) {
+ spa_log_debug(mix->log, "produce RC from SIDE (%f)", 0.5f);
+ _MATRIX(RC,SL) += 0.5f;
+ _MATRIX(RC,SR) += 0.5f;
+ } else if ((src_mask & STEREO) == STEREO) {
+ spa_log_debug(mix->log, "produce RC from STEREO (%f)", 0.5f);
+ _MATRIX(RC,FL) += 0.5f;
+ _MATRIX(RC,FR) += 0.5f;
+ } else if ((src_mask & FRONT) == FRONT &&
+ mix->upmix == CHANNELMIX_UPMIX_SIMPLE) {
+ spa_log_debug(mix->log, "produce RC from FC (%f)", slev);
+ _MATRIX(RC,FC) += slev;
+ } else {
+ spa_log_debug(mix->log, "won't produce RC");
+ }
+ }
+
+done:
+ for (jc = 0, ic = 0, i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) {
+ float sum = 0.0f;
+ char str[1024], str2[1024];
+ int idx = 0, idx2 = 0;
+ if ((dst_mask & (1UL << i)) == 0)
+ continue;
+ for (jc = 0, j = 0; j < SPA_AUDIO_MAX_CHANNELS; j++) {
+ if ((src_mask & (1UL << j)) == 0)
+ continue;
+ if (ic >= dst_chan || jc >= src_chan)
+ continue;
+
+ if (ic == 0)
+ idx2 += snprintf(str2 + idx2, sizeof(str2) - idx2, "%-4.4s ",
+ src_mask == ~0LU ? "MONO" :
+ spa_debug_type_find_short_name(spa_type_audio_channel, j + 3));
+
+ mix->matrix_orig[ic][jc++] = matrix[i][j];
+ sum += fabs(matrix[i][j]);
+
+ if (matrix[i][j] == 0.0f)
+ idx += snprintf(str + idx, sizeof(str) - idx, " ");
+ else
+ idx += snprintf(str + idx, sizeof(str) - idx, "%1.3f ", matrix[i][j]);
+ }
+ if (idx2 > 0)
+ spa_log_info(mix->log, " %s", str2);
+ if (idx > 0) {
+ spa_log_info(mix->log, "%-4.4s %s %f",
+ dst_mask == ~0LU ? "MONO" :
+ spa_debug_type_find_short_name(spa_type_audio_channel, i + 3),
+ str, sum);
+ }
+
+ maxsum = SPA_MAX(maxsum, sum);
+ if (i == _CH(LFE) && mix->lfe_cutoff > 0.0f && filter_lfe) {
+ spa_log_info(mix->log, "channel %d is LFE cutoff:%f", ic, mix->lfe_cutoff);
+ lr4_set(&mix->lr4[ic], BQ_LOWPASS, mix->lfe_cutoff / mix->freq);
+ } else if (i == _CH(FC) && mix->fc_cutoff > 0.0f && filter_fc) {
+ 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;
+ }
+ ic++;
+ }
+ if (SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_NORMALIZE) &&
+ maxsum > 1.0f) {
+ spa_log_debug(mix->log, "normalize %f", maxsum);
+ for (i = 0; i < dst_chan; i++)
+ for (j = 0; j < src_chan; j++)
+ mix->matrix_orig[i][j] /= maxsum;
+ }
+ return 0;
+}
+
+static void impl_channelmix_set_volume(struct channelmix *mix, float volume, bool mute,
+ uint32_t n_channel_volumes, float *channel_volumes)
+{
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+ float vol = mute ? 0.0f : volume, t;
+ uint32_t i, j;
+ uint32_t src_chan = mix->src_chan;
+ uint32_t dst_chan = mix->dst_chan;
+
+ spa_log_debug(mix->log, "volume:%f mute:%d n_volumes:%d", volume, mute, n_channel_volumes);
+
+ /** apply global volume to channels */
+ for (i = 0; i < n_channel_volumes; i++) {
+ volumes[i] = channel_volumes[i] * vol;
+ spa_log_debug(mix->log, "%d: %f * %f = %f", i, channel_volumes[i], vol, volumes[i]);
+ }
+
+ /** apply volumes per channel */
+ if (n_channel_volumes == src_chan) {
+ for (i = 0; i < dst_chan; i++) {
+ for (j = 0; j < src_chan; j++) {
+ mix->matrix[i][j] = mix->matrix_orig[i][j] * volumes[j];
+ }
+ }
+ } else if (n_channel_volumes == dst_chan) {
+ for (i = 0; i < dst_chan; i++) {
+ for (j = 0; j < src_chan; j++) {
+ mix->matrix[i][j] = mix->matrix_orig[i][j] * volumes[i];
+ }
+ }
+ } else if (n_channel_volumes == 0) {
+ for (i = 0; i < dst_chan; i++) {
+ for (j = 0; j < src_chan; j++) {
+ mix->matrix[i][j] = mix->matrix_orig[i][j] * vol;
+ }
+ }
+ }
+
+ SPA_FLAG_SET(mix->flags, CHANNELMIX_FLAG_ZERO);
+ SPA_FLAG_SET(mix->flags, CHANNELMIX_FLAG_EQUAL);
+ SPA_FLAG_SET(mix->flags, CHANNELMIX_FLAG_COPY);
+
+ t = 0.0;
+ for (i = 0; i < dst_chan; i++) {
+ for (j = 0; j < src_chan; j++) {
+ float v = mix->matrix[i][j];
+ spa_log_debug(mix->log, "%d %d: %f", i, j, v);
+ if (i == 0 && j == 0)
+ t = v;
+ else if (t != v)
+ SPA_FLAG_CLEAR(mix->flags, CHANNELMIX_FLAG_EQUAL);
+ if (v != 0.0)
+ SPA_FLAG_CLEAR(mix->flags, CHANNELMIX_FLAG_ZERO);
+ if ((i == j && v != 1.0f) ||
+ (i != j && v != 0.0f))
+ SPA_FLAG_CLEAR(mix->flags, CHANNELMIX_FLAG_COPY);
+ }
+ }
+ SPA_FLAG_UPDATE(mix->flags, CHANNELMIX_FLAG_IDENTITY,
+ dst_chan == src_chan && SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_COPY));
+
+ spa_log_debug(mix->log, "flags:%08x", mix->flags);
+}
+
+static void impl_channelmix_free(struct channelmix *mix)
+{
+ mix->process = NULL;
+}
+
+int channelmix_init(struct channelmix *mix)
+{
+ const struct channelmix_info *info;
+
+ if (mix->src_chan > SPA_AUDIO_MAX_CHANNELS ||
+ mix->dst_chan > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+
+ info = find_channelmix_info(mix->src_chan, mix->src_mask, mix->dst_chan, mix->dst_mask,
+ mix->cpu_flags);
+ if (info == NULL)
+ return -ENOTSUP;
+
+ mix->free = impl_channelmix_free;
+ mix->process = info->process;
+ mix->set_volume = impl_channelmix_set_volume;
+ mix->cpu_flags = info->cpu_flags;
+ mix->delay = mix->rear_delay * mix->freq / 1000.0f;
+ mix->func_name = info->name;
+
+ spa_log_debug(mix->log, "selected %s delay:%d options:%08x", info->name, mix->delay,
+ mix->options);
+
+ if (mix->hilbert_taps > 0) {
+ mix->n_taps = SPA_CLAMP(mix->hilbert_taps, 15u, 255u) | 1;
+ blackman_window(mix->taps, mix->n_taps);
+ hilbert_generate(mix->taps, mix->n_taps);
+ } else {
+ mix->n_taps = 1;
+ mix->taps[0] = 1.0f;
+ }
+ return make_matrix(mix);
+}
diff --git a/spa/plugins/audioconvert/channelmix-ops.h b/spa/plugins/audioconvert/channelmix-ops.h
new file mode 100644
index 0000000..c134a96
--- /dev/null
+++ b/spa/plugins/audioconvert/channelmix-ops.h
@@ -0,0 +1,160 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <spa/param/audio/raw.h>
+
+#include "crossover.h"
+#include "delay.h"
+
+#define VOLUME_MIN 0.0f
+#define VOLUME_NORM 1.0f
+
+#define _M(ch) (1UL << SPA_AUDIO_CHANNEL_ ## ch)
+#define MASK_MONO _M(FC)|_M(MONO)|_M(UNKNOWN)
+#define MASK_STEREO _M(FL)|_M(FR)|_M(UNKNOWN)
+#define MASK_QUAD _M(FL)|_M(FR)|_M(RL)|_M(RR)|_M(UNKNOWN)
+#define MASK_3_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)
+#define MASK_5_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR)
+#define MASK_7_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR)
+
+#define BUFFER_SIZE 4096
+#define MAX_TAPS 255
+
+struct channelmix {
+ uint32_t src_chan;
+ uint32_t dst_chan;
+ uint64_t src_mask;
+ uint64_t dst_mask;
+ uint32_t cpu_flags;
+#define CHANNELMIX_OPTION_MIX_LFE (1<<0) /**< mix LFE */
+#define CHANNELMIX_OPTION_NORMALIZE (1<<1) /**< normalize volumes */
+#define CHANNELMIX_OPTION_UPMIX (1<<2) /**< do simple upmixing */
+ uint32_t options;
+#define CHANNELMIX_UPMIX_NONE 0 /**< disable upmixing */
+#define CHANNELMIX_UPMIX_SIMPLE 1 /**< simple upmixing */
+#define CHANNELMIX_UPMIX_PSD 2 /**< Passive Surround Decoding upmixing */
+ uint32_t upmix;
+
+ struct spa_log *log;
+ const char *func_name;
+
+#define CHANNELMIX_FLAG_ZERO (1<<0) /**< all zero components */
+#define CHANNELMIX_FLAG_IDENTITY (1<<1) /**< identity matrix */
+#define CHANNELMIX_FLAG_EQUAL (1<<2) /**< all values are equal */
+#define CHANNELMIX_FLAG_COPY (1<<3) /**< 1 on diagonal, can be nxm */
+ uint32_t flags;
+ float matrix_orig[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS];
+ float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS];
+
+ float freq; /* sample frequency */
+ float lfe_cutoff; /* in Hz, 0 is disabled */
+ float fc_cutoff; /* in Hz, 0 is disabled */
+ float rear_delay; /* in ms, 0 is disabled */
+ float widen; /* stereo widen. 0 is disabled */
+ uint32_t hilbert_taps; /* to phase shift, 0 disabled */
+ struct lr4 lr4[SPA_AUDIO_MAX_CHANNELS];
+
+ float buffer[2][BUFFER_SIZE];
+ uint32_t pos[2];
+ uint32_t delay;
+ float taps[MAX_TAPS];
+ uint32_t n_taps;
+
+ void (*process) (struct channelmix *mix, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples);
+ void (*set_volume) (struct channelmix *mix, float volume, bool mute,
+ uint32_t n_channel_volumes, float *channel_volumes);
+ void (*free) (struct channelmix *mix);
+
+ void *data;
+};
+
+int channelmix_init(struct channelmix *mix);
+
+static const struct channelmix_upmix_info {
+ const char *label;
+ const char *description;
+ uint32_t upmix;
+} channelmix_upmix_info[] = {
+ [CHANNELMIX_UPMIX_NONE] = { "none", "Disabled", CHANNELMIX_UPMIX_NONE },
+ [CHANNELMIX_UPMIX_SIMPLE] = { "simple", "Simple upmixing", CHANNELMIX_UPMIX_SIMPLE },
+ [CHANNELMIX_UPMIX_PSD] = { "psd", "Passive Surround Decoding", CHANNELMIX_UPMIX_PSD }
+};
+
+static inline uint32_t channelmix_upmix_from_label(const char *label)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(channelmix_upmix_info, i) {
+ if (spa_streq(i->label, label))
+ return i->upmix;
+ }
+ return CHANNELMIX_UPMIX_NONE;
+}
+
+#define channelmix_process(mix,...) (mix)->process(mix, __VA_ARGS__)
+#define channelmix_set_volume(mix,...) (mix)->set_volume(mix, __VA_ARGS__)
+#define channelmix_free(mix) (mix)->free(mix)
+
+#define DEFINE_FUNCTION(name,arch) \
+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);
+DEFINE_FUNCTION(f32_2_1, c);
+DEFINE_FUNCTION(f32_4_1, c);
+DEFINE_FUNCTION(f32_2_4, c);
+DEFINE_FUNCTION(f32_2_3p1, c);
+DEFINE_FUNCTION(f32_2_5p1, c);
+DEFINE_FUNCTION(f32_2_7p1, c);
+DEFINE_FUNCTION(f32_3p1_2, c);
+DEFINE_FUNCTION(f32_5p1_2, c);
+DEFINE_FUNCTION(f32_5p1_3p1, c);
+DEFINE_FUNCTION(f32_5p1_4, c);
+DEFINE_FUNCTION(f32_7p1_2, c);
+DEFINE_FUNCTION(f32_7p1_3p1, c);
+DEFINE_FUNCTION(f32_7p1_4, c);
+
+#if defined (HAVE_SSE)
+DEFINE_FUNCTION(copy, sse);
+DEFINE_FUNCTION(f32_n_m, sse);
+DEFINE_FUNCTION(f32_2_3p1, sse);
+DEFINE_FUNCTION(f32_2_5p1, sse);
+DEFINE_FUNCTION(f32_2_7p1, sse);
+DEFINE_FUNCTION(f32_3p1_2, sse);
+DEFINE_FUNCTION(f32_5p1_2, sse);
+DEFINE_FUNCTION(f32_5p1_3p1, sse);
+DEFINE_FUNCTION(f32_5p1_4, sse);
+DEFINE_FUNCTION(f32_7p1_4, sse);
+#endif
+
+#undef DEFINE_FUNCTION
diff --git a/spa/plugins/audioconvert/crossover.c b/spa/plugins/audioconvert/crossover.c
new file mode 100644
index 0000000..7575833
--- /dev/null
+++ b/spa/plugins/audioconvert/crossover.c
@@ -0,0 +1,70 @@
+/* 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.
+ */
+
+#include <float.h>
+#include <string.h>
+
+#include "crossover.h"
+
+void lr4_set(struct lr4 *lr4, enum biquad_type type, float freq)
+{
+ biquad_set(&lr4->bq, type, freq);
+ 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 lx1 = lr4->x1;
+ float lx2 = lr4->x2;
+ float ly1 = lr4->y1;
+ float ly2 = lr4->y2;
+ float lz1 = lr4->z1;
+ float lz2 = lr4->z2;
+ float lb0 = lr4->bq.b0;
+ float lb1 = lr4->bq.b1;
+ float lb2 = lr4->bq.b2;
+ float la1 = lr4->bq.a1;
+ float la2 = lr4->bq.a2;
+ 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++) {
+ float x, y, z;
+ x = src[i];
+ y = lb0*x + lb1*lx1 + lb2*lx2 - la1*ly1 - la2*ly2;
+ z = lb0*y + lb1*ly1 + lb2*ly2 - la1*lz1 - la2*lz2;
+ lx2 = lx1;
+ lx1 = x;
+ ly2 = ly1;
+ ly1 = y;
+ lz2 = lz1;
+ lz1 = z;
+ dst[i] = z * vol;
+ }
+#define F(x) (-FLT_MIN < (x) && (x) < FLT_MIN ? 0.0f : (x))
+ lr4->x1 = F(lx1);
+ lr4->x2 = F(lx2);
+ lr4->y1 = F(ly1);
+ lr4->y2 = F(ly2);
+ lr4->z1 = F(lz1);
+ lr4->z2 = F(lz2);
+#undef F
+}
diff --git a/spa/plugins/audioconvert/crossover.h b/spa/plugins/audioconvert/crossover.h
new file mode 100644
index 0000000..b6f458b
--- /dev/null
+++ b/spa/plugins/audioconvert/crossover.h
@@ -0,0 +1,31 @@
+/* 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.
+ */
+
+#ifndef CROSSOVER_H_
+#define CROSSOVER_H_
+
+#include <stdbool.h>
+
+#include "biquad.h"
+/* An LR4 filter is two biquads with the same parameters connected in series:
+ *
+ * x -- [BIQUAD] -- y -- [BIQUAD] -- z
+ *
+ * Both biquad filter has the same parameter b[012] and a[12],
+ * The variable [xyz][12] keep the history values.
+ */
+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
new file mode 100644
index 0000000..16e189f
--- /dev/null
+++ b/spa/plugins/audioconvert/delay.h
@@ -0,0 +1,72 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#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
new file mode 100644
index 0000000..087f027
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops-avx2.c
@@ -0,0 +1,1043 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "fmt-ops.h"
+
+#include <immintrin.h>
+// GCC: workaround for missing AVX intrinsic: "_mm256_setr_m128()"
+// (see https://stackoverflow.com/questions/32630458/setting-m256i-to-the-value-of-two-m128i-values)
+#ifndef _mm256_setr_m128i
+# ifndef _mm256_set_m128i
+# define _mm256_set_m128i(v0, v1) _mm256_insertf128_si256(_mm256_castsi128_si256(v1), (v0), 1)
+# endif
+# define _mm256_setr_m128i(v0, v1) _mm256_set_m128i((v1), (v0))
+#endif
+
+#define _MM_CLAMP_PS(r,min,max) \
+ _mm_min_ps(_mm_max_ps(r, min), max)
+
+#define _MM256_CLAMP_PS(r,min,max) \
+ _mm256_min_ps(_mm256_max_ps(r, min), max)
+
+#define _MM_CLAMP_SS(r,min,max) \
+ _mm_min_ss(_mm_max_ss(r, min), max)
+
+static void
+conv_s16_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int16_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_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, s[0]);
+ out = _mm_mul_ss(out, factor);
+ _mm_store_ss(&d0[n], out);
+ s += n_channels;
+ }
+}
+
+void
+conv_s16_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int16_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i < n_channels; i++)
+ conv_s16_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)
+{
+ const int16_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));
+
+ 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, s[0]);
+ out[0] = _mm_mul_ss(out[0], factor);
+ out[1] = _mm_cvtsi32_ss(factor, s[1]);
+ out[1] = _mm_mul_ss(out[1], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ s += 2;
+ }
+}
+
+void
+conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int8_t *s = src;
+ float *d0 = dst[0];
+ uint32_t n, unrolled;
+ __m128i in;
+ __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
+ __m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels);
+
+ if (SPA_IS_ALIGNED(d0, 16) && n_samples > 0) {
+ unrolled = n_samples & ~3;
+ if ((n_samples & 3) == 0)
+ unrolled -= 4;
+ }
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in = _mm_i32gather_epi32((int*)s, mask1, 1);
+ in = _mm_slli_epi32(in, 8);
+ in = _mm_srai_epi32(in, 8);
+ out = _mm_cvtepi32_ps(in);
+ out = _mm_mul_ps(out, factor);
+ _mm_store_ps(&d0[n], out);
+ s += 12 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out = _mm_cvtsi32_ss(factor, s24_to_s32(*(int24_t*)s));
+ out = _mm_mul_ss(out, factor);
+ _mm_store_ss(&d0[n], out);
+ s += 3 * n_channels;
+ }
+}
+
+static void
+conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int8_t *s = src;
+ float *d0 = dst[0], *d1 = dst[1];
+ uint32_t n, unrolled;
+ __m128i in[2];
+ __m128 out[2], factor = _mm_set1_ps(1.0f / S24_SCALE);
+ __m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels);
+
+ if (SPA_IS_ALIGNED(d0, 16) &&
+ SPA_IS_ALIGNED(d1, 16) &&
+ n_samples > 0) {
+ unrolled = n_samples & ~3;
+ if ((n_samples & 3) == 0)
+ unrolled -= 4;
+ }
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_i32gather_epi32((int*)&s[0], mask1, 1);
+ in[1] = _mm_i32gather_epi32((int*)&s[3], mask1, 1);
+
+ in[0] = _mm_slli_epi32(in[0], 8);
+ in[1] = _mm_slli_epi32(in[1], 8);
+
+ in[0] = _mm_srai_epi32(in[0], 8);
+ in[1] = _mm_srai_epi32(in[1], 8);
+
+ out[0] = _mm_cvtepi32_ps(in[0]);
+ out[1] = _mm_cvtepi32_ps(in[1]);
+
+ out[0] = _mm_mul_ps(out[0], factor);
+ out[1] = _mm_mul_ps(out[1], factor);
+
+ _mm_store_ps(&d0[n], out[0]);
+ _mm_store_ps(&d1[n], out[1]);
+
+ s += 12 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+0)));
+ out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+1)));
+ out[0] = _mm_mul_ss(out[0], factor);
+ out[1] = _mm_mul_ss(out[1], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ s += 3 * n_channels;
+ }
+}
+static void
+conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int8_t *s = src;
+ float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
+ uint32_t n, unrolled;
+ __m128i in[4];
+ __m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE);
+ __m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels);
+
+ if (SPA_IS_ALIGNED(d0, 16) &&
+ SPA_IS_ALIGNED(d1, 16) &&
+ SPA_IS_ALIGNED(d2, 16) &&
+ SPA_IS_ALIGNED(d3, 16) &&
+ n_samples > 0) {
+ unrolled = n_samples & ~3;
+ if ((n_samples & 3) == 0)
+ unrolled -= 4;
+ }
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_i32gather_epi32((int*)&s[0], mask1, 1);
+ in[1] = _mm_i32gather_epi32((int*)&s[3], mask1, 1);
+ in[2] = _mm_i32gather_epi32((int*)&s[6], mask1, 1);
+ in[3] = _mm_i32gather_epi32((int*)&s[9], mask1, 1);
+
+ in[0] = _mm_slli_epi32(in[0], 8);
+ in[1] = _mm_slli_epi32(in[1], 8);
+ in[2] = _mm_slli_epi32(in[2], 8);
+ in[3] = _mm_slli_epi32(in[3], 8);
+
+ in[0] = _mm_srai_epi32(in[0], 8);
+ in[1] = _mm_srai_epi32(in[1], 8);
+ in[2] = _mm_srai_epi32(in[2], 8);
+ in[3] = _mm_srai_epi32(in[3], 8);
+
+ out[0] = _mm_cvtepi32_ps(in[0]);
+ out[1] = _mm_cvtepi32_ps(in[1]);
+ out[2] = _mm_cvtepi32_ps(in[2]);
+ out[3] = _mm_cvtepi32_ps(in[3]);
+
+ out[0] = _mm_mul_ps(out[0], factor);
+ out[1] = _mm_mul_ps(out[1], factor);
+ out[2] = _mm_mul_ps(out[2], factor);
+ out[3] = _mm_mul_ps(out[3], factor);
+
+ _mm_store_ps(&d0[n], out[0]);
+ _mm_store_ps(&d1[n], out[1]);
+ _mm_store_ps(&d2[n], out[2]);
+ _mm_store_ps(&d3[n], out[3]);
+
+ s += 12 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+0)));
+ out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+1)));
+ out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+2)));
+ out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)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);
+ out[3] = _mm_mul_ss(out[3], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ _mm_store_ss(&d2[n], out[2]);
+ _mm_store_ss(&d3[n], out[3]);
+ s += 3 * n_channels;
+ }
+}
+
+void
+conv_s24_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int8_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_s24_to_f32d_4s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples);
+ for(; i + 1 < n_channels; i += 2)
+ conv_s24_to_f32d_2s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_s24_to_f32d_1s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples);
+}
+
+
+void
+conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int32_t *s = src;
+ 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);
+ __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);
+
+ if (SPA_IS_ALIGNED(d0, 32) &&
+ SPA_IS_ALIGNED(d1, 32) &&
+ SPA_IS_ALIGNED(d2, 32) &&
+ SPA_IS_ALIGNED(d3, 32))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 8) {
+ in[0] = _mm256_i32gather_epi32((int*)&s[0], mask1, 4);
+ in[1] = _mm256_i32gather_epi32((int*)&s[1], mask1, 4);
+ 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]);
+ out[3] = _mm256_cvtepi32_ps(in[3]);
+
+ out[0] = _mm256_mul_ps(out[0], factor);
+ out[1] = _mm256_mul_ps(out[1], factor);
+ out[2] = _mm256_mul_ps(out[2], factor);
+ out[3] = _mm256_mul_ps(out[3], factor);
+
+ _mm256_store_ps(&d0[n], out[0]);
+ _mm256_store_ps(&d1[n], out[1]);
+ _mm256_store_ps(&d2[n], out[2]);
+ _mm256_store_ps(&d3[n], out[3]);
+
+ 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);
+ out[0] = _mm_mul_ss(out[0], factor);
+ out[1] = _mm_mul_ss(out[1], factor);
+ out[2] = _mm_mul_ss(out[2], factor);
+ out[3] = _mm_mul_ss(out[3], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ _mm_store_ss(&d2[n], out[2]);
+ _mm_store_ss(&d3[n], out[3]);
+ s += n_channels;
+ }
+}
+
+void
+conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int32_t *s = src;
+ 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);
+ __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);
+
+ if (SPA_IS_ALIGNED(d0, 32) &&
+ SPA_IS_ALIGNED(d1, 32))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 8) {
+ 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]);
+
+ out[0] = _mm256_mul_ps(out[0], factor);
+ out[1] = _mm256_mul_ps(out[1], factor);
+
+ _mm256_store_ps(&d0[n], out[0]);
+ _mm256_store_ps(&d1[n], out[1]);
+
+ 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);
+ out[0] = _mm_mul_ss(out[0], factor);
+ out[1] = _mm_mul_ss(out[1], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ s += n_channels;
+ }
+}
+
+void
+conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int32_t *s = src;
+ float *d0 = dst[0];
+ uint32_t n, unrolled;
+ __m256i in[2];
+ __m256 out[2], factor = _mm256_set1_ps(1.0f / S24_SCALE);
+ __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);
+
+ if (SPA_IS_ALIGNED(d0, 32))
+ unrolled = n_samples & ~15;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 16) {
+ 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]);
+
+ out[0] = _mm256_mul_ps(out[0], factor);
+ out[1] = _mm256_mul_ps(out[1], factor);
+
+ _mm256_store_ps(&d0[n+0], out[0]);
+ _mm256_store_ps(&d0[n+8], out[1]);
+
+ 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);
+ out = _mm_mul_ss(out, factor);
+ _mm_store_ss(&d0[n], out);
+ s += n_channels;
+ }
+}
+
+void
+conv_s32_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int32_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_s32_to_f32d_4s_avx2(conv, &dst[i], &s[i], n_channels, n_samples);
+ for(; i + 1 < n_channels; i += 2)
+ conv_s32_to_f32d_2s_avx2(conv, &dst[i], &s[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_s32_to_f32d_1s_avx2(conv, &dst[i], &s[i], n_channels, n_samples);
+}
+
+static void
+conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s0 = src[0];
+ int32_t *d = dst;
+ 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);
+
+ if (SPA_IS_ALIGNED(s0, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ 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));
+
+ d[0*n_channels] = _mm_cvtsi128_si32(out[0]);
+ d[1*n_channels] = _mm_cvtsi128_si32(out[1]);
+ d[2*n_channels] = _mm_cvtsi128_si32(out[2]);
+ d[3*n_channels] = _mm_cvtsi128_si32(out[3]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ in[0] = _mm_load_ss(&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 += n_channels;
+ }
+}
+
+static void
+conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s0 = src[0], *s1 = src[1];
+ int32_t *d = dst;
+ 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);
+
+ if (SPA_IS_ALIGNED(s0, 32) &&
+ SPA_IS_ALIGNED(s1, 32))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 8) {
+ in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), scale);
+ in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), scale);
+
+ in[0] = _MM256_CLAMP_PS(in[0], int_min, int_max);
+ in[1] = _MM256_CLAMP_PS(in[1], int_min, int_max);
+
+ out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
+ out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
+ out[0] = _mm256_slli_epi32(out[0], 8);
+ out[1] = _mm256_slli_epi32(out[1], 8);
+
+ t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */
+ t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */
+
+#ifdef __x86_64__
+ *((int64_t*)(d + 0*n_channels)) = _mm256_extract_epi64(t[0], 0);
+ *((int64_t*)(d + 1*n_channels)) = _mm256_extract_epi64(t[0], 1);
+ *((int64_t*)(d + 2*n_channels)) = _mm256_extract_epi64(t[1], 0);
+ *((int64_t*)(d + 3*n_channels)) = _mm256_extract_epi64(t[1], 1);
+ *((int64_t*)(d + 4*n_channels)) = _mm256_extract_epi64(t[0], 2);
+ *((int64_t*)(d + 5*n_channels)) = _mm256_extract_epi64(t[0], 3);
+ *((int64_t*)(d + 6*n_channels)) = _mm256_extract_epi64(t[1], 2);
+ *((int64_t*)(d + 7*n_channels)) = _mm256_extract_epi64(t[1], 3);
+#else
+ _mm_storel_pi((__m64*)(d + 0*n_channels), (__m128)_mm256_extracti128_si256(t[0], 0));
+ _mm_storeh_pi((__m64*)(d + 1*n_channels), (__m128)_mm256_extracti128_si256(t[0], 0));
+ _mm_storel_pi((__m64*)(d + 2*n_channels), (__m128)_mm256_extracti128_si256(t[1], 0));
+ _mm_storeh_pi((__m64*)(d + 3*n_channels), (__m128)_mm256_extracti128_si256(t[1], 0));
+ _mm_storel_pi((__m64*)(d + 4*n_channels), (__m128)_mm256_extracti128_si256(t[0], 1));
+ _mm_storeh_pi((__m64*)(d + 5*n_channels), (__m128)_mm256_extracti128_si256(t[0], 1));
+ _mm_storel_pi((__m64*)(d + 6*n_channels), (__m128)_mm256_extracti128_si256(t[1], 1));
+ _mm_storeh_pi((__m64*)(d + 7*n_channels), (__m128)_mm256_extracti128_si256(t[1], 1));
+#endif
+ d += 8*n_channels;
+ }
+ 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);
+
+ in[0] = _mm_load_ss(&s0[n]);
+ in[1] = _mm_load_ss(&s1[n]);
+
+ in[0] = _mm_unpacklo_ps(in[0], in[1]);
+
+ 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;
+ }
+}
+
+static void
+conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3];
+ int32_t *d = dst;
+ 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);
+
+ if (SPA_IS_ALIGNED(s0, 32) &&
+ SPA_IS_ALIGNED(s1, 32) &&
+ SPA_IS_ALIGNED(s2, 32) &&
+ SPA_IS_ALIGNED(s3, 32))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 8) {
+ in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), scale);
+ in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), scale);
+ in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), scale);
+ in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), scale);
+
+ in[0] = _MM256_CLAMP_PS(in[0], int_min, int_max);
+ in[1] = _MM256_CLAMP_PS(in[1], int_min, int_max);
+ in[2] = _MM256_CLAMP_PS(in[2], int_min, int_max);
+ in[3] = _MM256_CLAMP_PS(in[3], int_min, int_max);
+
+ out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
+ out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
+ out[2] = _mm256_cvtps_epi32(in[2]); /* c0 c1 c2 c3 c4 c5 c6 c7 */
+ out[3] = _mm256_cvtps_epi32(in[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */
+ out[0] = _mm256_slli_epi32(out[0], 8);
+ out[1] = _mm256_slli_epi32(out[1], 8);
+ out[2] = _mm256_slli_epi32(out[2], 8);
+ out[3] = _mm256_slli_epi32(out[3], 8);
+
+ t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */
+ t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */
+ t[2] = _mm256_unpacklo_epi32(out[2], out[3]); /* c0 d0 c1 d1 c4 d4 c5 d5 */
+ t[3] = _mm256_unpackhi_epi32(out[2], out[3]); /* c2 d2 c3 d3 c6 d6 c7 d7 */
+
+ out[0] = _mm256_unpacklo_epi64(t[0], t[2]); /* a0 b0 c0 d0 a4 b4 c4 d4 */
+ out[1] = _mm256_unpackhi_epi64(t[0], t[2]); /* a1 b1 c1 d1 a5 b5 c5 d5 */
+ out[2] = _mm256_unpacklo_epi64(t[1], t[3]); /* a2 b2 c2 d2 a6 b6 c6 d6 */
+ out[3] = _mm256_unpackhi_epi64(t[1], t[3]); /* a3 b3 c3 d3 a7 b7 c7 d7 */
+
+ _mm_storeu_si128((__m128i*)(d + 0*n_channels), _mm256_extracti128_si256(out[0], 0));
+ _mm_storeu_si128((__m128i*)(d + 1*n_channels), _mm256_extracti128_si256(out[1], 0));
+ _mm_storeu_si128((__m128i*)(d + 2*n_channels), _mm256_extracti128_si256(out[2], 0));
+ _mm_storeu_si128((__m128i*)(d + 3*n_channels), _mm256_extracti128_si256(out[3], 0));
+ _mm_storeu_si128((__m128i*)(d + 4*n_channels), _mm256_extracti128_si256(out[0], 1));
+ _mm_storeu_si128((__m128i*)(d + 5*n_channels), _mm256_extracti128_si256(out[1], 1));
+ _mm_storeu_si128((__m128i*)(d + 6*n_channels), _mm256_extracti128_si256(out[2], 1));
+ _mm_storeu_si128((__m128i*)(d + 7*n_channels), _mm256_extracti128_si256(out[3], 1));
+ d += 8*n_channels;
+ }
+ 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);
+
+ in[0] = _mm_load_ss(&s0[n]);
+ in[1] = _mm_load_ss(&s1[n]);
+ in[2] = _mm_load_ss(&s2[n]);
+ in[3] = _mm_load_ss(&s3[n]);
+
+ in[0] = _mm_unpacklo_ps(in[0], in[2]);
+ in[1] = _mm_unpacklo_ps(in[1], in[3]);
+ in[0] = _mm_unpacklo_ps(in[0], in[1]);
+
+ 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;
+ }
+}
+
+void
+conv_f32d_to_s32_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int32_t *d = dst[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_f32d_to_s32_4s_avx2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i + 1 < n_channels; i += 2)
+ conv_f32d_to_s32_2s_avx2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_f32d_to_s32_1s_avx2(conv, &d[i], &src[i], n_channels, n_samples);
+}
+
+static void
+conv_f32d_to_s16_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s0 = src[0];
+ int16_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]);
+
+ 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 = _mm_cvtss_si32(in[0]);
+ d += n_channels;
+ }
+}
+
+static void
+conv_f32d_to_s16_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s0 = src[0], *s1 = src[1];
+ int16_t *d = dst;
+ uint32_t n, unrolled;
+ __m256 in[2];
+ __m256i out[4], t[2];
+ __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 += 8) {
+ in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_scale);
+ in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_scale);
+
+ out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
+ out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
+
+ 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 */
+
+ 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 */
+
+ *((int32_t*)(d + 0*n_channels)) = _mm256_extract_epi32(out[0],0);
+ *((int32_t*)(d + 1*n_channels)) = _mm256_extract_epi32(out[0],1);
+ *((int32_t*)(d + 2*n_channels)) = _mm256_extract_epi32(out[0],2);
+ *((int32_t*)(d + 3*n_channels)) = _mm256_extract_epi32(out[0],3);
+ *((int32_t*)(d + 4*n_channels)) = _mm256_extract_epi32(out[0],4);
+ *((int32_t*)(d + 5*n_channels)) = _mm256_extract_epi32(out[0],5);
+ *((int32_t*)(d + 6*n_channels)) = _mm256_extract_epi32(out[0],6);
+ *((int32_t*)(d + 7*n_channels)) = _mm256_extract_epi32(out[0],7);
+
+ d += 8*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ __m128 in[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);
+
+ in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+ in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale);
+ in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+ in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+ d[0] = _mm_cvtss_si32(in[0]);
+ d[1] = _mm_cvtss_si32(in[1]);
+ d += n_channels;
+ }
+}
+
+static void
+conv_f32d_to_s16_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3];
+ int16_t *d = dst;
+ 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) &&
+ SPA_IS_ALIGNED(s2, 32) &&
+ SPA_IS_ALIGNED(s3, 32))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 8) {
+ in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), int_scale);
+ in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), int_scale);
+ in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), int_scale);
+ in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), int_scale);
+
+ t[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
+ t[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
+ t[2] = _mm256_cvtps_epi32(in[2]); /* c0 c1 c2 c3 c4 c5 c6 c7 */
+ t[3] = _mm256_cvtps_epi32(in[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */
+
+ t[0] = _mm256_packs_epi32(t[0], t[2]); /* a0 a1 a2 a3 c0 c1 c2 c3 a4 a5 a6 a7 c4 c5 c6 c7 */
+ t[1] = _mm256_packs_epi32(t[1], t[3]); /* b0 b1 b2 b3 d0 d1 d2 d3 b4 b5 b6 b7 d4 d5 d6 d7 */
+
+ out[0] = _mm256_unpacklo_epi16(t[0], t[1]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */
+ out[1] = _mm256_unpackhi_epi16(t[0], t[1]); /* c0 d0 c1 d1 c2 d2 c3 d3 c4 d4 c5 d5 c6 d6 c7 d7 */
+
+ out[2] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 c0 d0 a1 b1 c1 d1 a4 b4 c4 d4 a5 b5 c5 d5 */
+ out[3] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 c2 d2 a3 b3 c3 d3 a6 b6 c6 d6 a7 b7 c7 d7 */
+
+#ifdef __x86_64__
+ *(int64_t*)(d + 0*n_channels) = _mm256_extract_epi64(out[2], 0); /* a0 b0 c0 d0 */
+ *(int64_t*)(d + 1*n_channels) = _mm256_extract_epi64(out[2], 1); /* a1 b1 c1 d1 */
+ *(int64_t*)(d + 2*n_channels) = _mm256_extract_epi64(out[3], 0); /* a2 b2 c2 d2 */
+ *(int64_t*)(d + 3*n_channels) = _mm256_extract_epi64(out[3], 1); /* a3 b3 c3 d3 */
+ *(int64_t*)(d + 4*n_channels) = _mm256_extract_epi64(out[2], 2); /* a4 b4 c4 d4 */
+ *(int64_t*)(d + 5*n_channels) = _mm256_extract_epi64(out[2], 3); /* a5 b5 c5 d5 */
+ *(int64_t*)(d + 6*n_channels) = _mm256_extract_epi64(out[3], 2); /* a6 b6 c6 d6 */
+ *(int64_t*)(d + 7*n_channels) = _mm256_extract_epi64(out[3], 3); /* a7 b7 c7 d7 */
+#else
+ _mm_storel_pi((__m64*)(d + 0*n_channels), (__m128)_mm256_extracti128_si256(out[2], 0));
+ _mm_storeh_pi((__m64*)(d + 1*n_channels), (__m128)_mm256_extracti128_si256(out[2], 0));
+ _mm_storel_pi((__m64*)(d + 2*n_channels), (__m128)_mm256_extracti128_si256(out[3], 0));
+ _mm_storeh_pi((__m64*)(d + 3*n_channels), (__m128)_mm256_extracti128_si256(out[3], 0));
+ _mm_storel_pi((__m64*)(d + 4*n_channels), (__m128)_mm256_extracti128_si256(out[2], 1));
+ _mm_storeh_pi((__m64*)(d + 5*n_channels), (__m128)_mm256_extracti128_si256(out[2], 1));
+ _mm_storel_pi((__m64*)(d + 6*n_channels), (__m128)_mm256_extracti128_si256(out[3], 1));
+ _mm_storeh_pi((__m64*)(d + 7*n_channels), (__m128)_mm256_extracti128_si256(out[3], 1));
+#endif
+
+ d += 8*n_channels;
+ }
+ 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[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale);
+ in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale);
+ in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+ in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+ in[2] = _MM_CLAMP_SS(in[2], int_min, int_max);
+ in[3] = _MM_CLAMP_SS(in[3], int_min, int_max);
+ d[0] = _mm_cvtss_si32(in[0]);
+ d[1] = _mm_cvtss_si32(in[1]);
+ d[2] = _mm_cvtss_si32(in[2]);
+ d[3] = _mm_cvtss_si32(in[3]);
+ d += n_channels;
+ }
+}
+
+void
+conv_f32d_to_s16_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int16_t *d = dst[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_f32d_to_s16_4s_avx2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i + 1 < n_channels; i += 2)
+ conv_f32d_to_s16_2s_avx2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_f32d_to_s16_1s_avx2(conv, &d[i], &src[i], n_channels, n_samples);
+}
+
+void
+conv_f32d_to_s16_4_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], *s2 = src[2], *s3 = src[3];
+ int16_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) &&
+ SPA_IS_ALIGNED(s2, 32) &&
+ SPA_IS_ALIGNED(s3, 32))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 8) {
+ in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), int_scale);
+ in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), int_scale);
+ in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), int_scale);
+ in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), int_scale);
+
+ t[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */
+ t[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */
+ t[2] = _mm256_cvtps_epi32(in[2]); /* c0 c1 c2 c3 c4 c5 c6 c7 */
+ t[3] = _mm256_cvtps_epi32(in[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */
+
+ t[0] = _mm256_packs_epi32(t[0], t[2]); /* a0 a1 a2 a3 c0 c1 c2 c3 a4 a5 a6 a7 c4 c5 c6 c7 */
+ t[1] = _mm256_packs_epi32(t[1], t[3]); /* b0 b1 b2 b3 d0 d1 d2 d3 b4 b5 b6 b7 d4 d5 d6 d7 */
+
+ out[0] = _mm256_unpacklo_epi16(t[0], t[1]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */
+ out[1] = _mm256_unpackhi_epi16(t[0], t[1]); /* c0 d0 c1 d1 c2 d2 c3 d3 c4 d4 c5 d5 c6 d6 c7 d7 */
+
+ t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 c0 d0 a1 b1 c1 d1 a4 b4 c4 d4 a5 b5 c5 d5 */
+ t[2] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 c2 d2 a3 b3 c3 d3 a6 b6 c6 d6 a7 b7 c7 d7 */
+
+ out[0] = _mm256_inserti128_si256(t[0], _mm256_extracti128_si256(t[2], 0), 1);
+ out[2] = _mm256_inserti128_si256(t[2], _mm256_extracti128_si256(t[0], 1), 0);
+
+ _mm256_store_si256((__m256i*)(d+0), out[0]);
+ _mm256_store_si256((__m256i*)(d+16), out[2]);
+ 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[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale);
+ in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale);
+ in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+ in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+ in[2] = _MM_CLAMP_SS(in[2], int_min, int_max);
+ in[3] = _MM_CLAMP_SS(in[3], int_min, int_max);
+ d[0] = _mm_cvtss_si32(in[0]);
+ d[1] = _mm_cvtss_si32(in[1]);
+ d[2] = _mm_cvtss_si32(in[2]);
+ d[3] = _mm_cvtss_si32(in[3]);
+ d += 4;
+ }
+}
+void
+conv_f32d_to_s16_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];
+ int16_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 */
+
+ _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] = _mm_cvtss_si32(in[0]);
+ d[1] = _mm_cvtss_si32(in[1]);
+ d += 2;
+ }
+}
diff --git a/spa/plugins/audioconvert/fmt-ops-c.c b/spa/plugins/audioconvert/fmt-ops-c.c
new file mode 100644
index 0000000..92ecb5a
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops-c.c
@@ -0,0 +1,433 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/support/cpu.h>
+#include <spa/utils/defs.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "fmt-ops.h"
+#include "law.h"
+
+#define MAKE_COPY(size) \
+void conv_copy ##size## d_c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ uint32_t i, n_channels = conv->n_channels; \
+ for (i = 0; i < n_channels; i++) \
+ spa_memcpy(dst[i], src[i], n_samples * (size>>3)); \
+} \
+void conv_copy ##size## _c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ spa_memcpy(dst[0], src[0], n_samples * conv->n_channels * (size>>3)); \
+}
+
+MAKE_COPY(8);
+MAKE_COPY(16);
+MAKE_COPY(24);
+MAKE_COPY(32);
+MAKE_COPY(64);
+
+#define MAKE_D_TO_D(sname,stype,dname,dtype,func) \
+void conv_ ##sname## d_to_ ##dname## d_c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ uint32_t i, j, n_channels = conv->n_channels; \
+ for (i = 0; i < n_channels; i++) { \
+ const stype *s = src[i]; \
+ dtype *d = dst[i]; \
+ for (j = 0; j < n_samples; j++) \
+ d[j] = func (s[j]); \
+ } \
+}
+
+#define MAKE_I_TO_I(sname,stype,dname,dtype,func) \
+void conv_ ##sname## _to_ ##dname## _c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ uint32_t j; \
+ const stype *s = src[0]; \
+ dtype *d = dst[0]; \
+ n_samples *= conv->n_channels; \
+ for (j = 0; j < n_samples; j++) \
+ d[j] = func (s[j]); \
+}
+
+#define MAKE_I_TO_D(sname,stype,dname,dtype,func) \
+void conv_ ##sname## _to_ ##dname## d_c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ const stype *s = src[0]; \
+ dtype **d = (dtype**)dst; \
+ uint32_t i, j, n_channels = conv->n_channels; \
+ for (j = 0; j < n_samples; j++) { \
+ for (i = 0; i < n_channels; i++) \
+ d[i][j] = func (*s++); \
+ } \
+}
+
+#define MAKE_D_TO_I(sname,stype,dname,dtype,func) \
+void conv_ ##sname## d_to_ ##dname## _c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ const stype **s = (const stype **)src; \
+ dtype *d = dst[0]; \
+ uint32_t i, j, n_channels = conv->n_channels; \
+ for (j = 0; j < n_samples; j++) { \
+ for (i = 0; i < n_channels; i++) \
+ *d++ = func (s[i][j]); \
+ } \
+}
+
+/* to f32 */
+MAKE_D_TO_D(u8, uint8_t, f32, float, U8_TO_F32);
+MAKE_I_TO_I(u8, uint8_t, f32, float, U8_TO_F32);
+MAKE_I_TO_D(u8, uint8_t, f32, float, U8_TO_F32);
+MAKE_D_TO_I(u8, uint8_t, f32, float, U8_TO_F32);
+
+MAKE_D_TO_D(s8, int8_t, f32, float, S8_TO_F32);
+MAKE_I_TO_I(s8, int8_t, f32, float, S8_TO_F32);
+MAKE_I_TO_D(s8, int8_t, f32, float, S8_TO_F32);
+MAKE_D_TO_I(s8, int8_t, f32, float, S8_TO_F32);
+
+MAKE_I_TO_D(alaw, uint8_t, f32, float, alaw_to_f32);
+MAKE_I_TO_D(ulaw, uint8_t, f32, float, ulaw_to_f32);
+
+MAKE_I_TO_I(u16, uint16_t, f32, float, U16_TO_F32);
+MAKE_I_TO_D(u16, uint16_t, f32, float, U16_TO_F32);
+
+MAKE_D_TO_D(s16, int16_t, f32, float, S16_TO_F32);
+MAKE_I_TO_I(s16, int16_t, f32, float, S16_TO_F32);
+MAKE_I_TO_D(s16, int16_t, f32, float, S16_TO_F32);
+MAKE_D_TO_I(s16, int16_t, f32, float, S16_TO_F32);
+MAKE_I_TO_D(s16s, uint16_t, f32, float, S16S_TO_F32);
+
+MAKE_I_TO_I(u32, uint32_t, f32, float, U32_TO_F32);
+MAKE_I_TO_D(u32, uint32_t, f32, float, U32_TO_F32);
+
+MAKE_D_TO_D(s32, int32_t, f32, float, S32_TO_F32);
+MAKE_I_TO_I(s32, int32_t, f32, float, S32_TO_F32);
+MAKE_I_TO_D(s32, int32_t, f32, float, S32_TO_F32);
+MAKE_D_TO_I(s32, int32_t, f32, float, S32_TO_F32);
+MAKE_I_TO_D(s32s, uint32_t, f32, float, S32S_TO_F32);
+
+MAKE_I_TO_I(u24, uint24_t, f32, float, U24_TO_F32);
+MAKE_I_TO_D(u24, uint24_t, f32, float, U24_TO_F32);
+
+MAKE_D_TO_D(s24, int24_t, f32, float, S24_TO_F32);
+MAKE_I_TO_I(s24, int24_t, f32, float, S24_TO_F32);
+MAKE_I_TO_D(s24, int24_t, f32, float, S24_TO_F32);
+MAKE_D_TO_I(s24, int24_t, f32, float, S24_TO_F32);
+MAKE_I_TO_D(s24s, int24_t, f32, float, S24S_TO_F32);
+
+MAKE_I_TO_I(u24_32, uint32_t, f32, float, U24_32_TO_F32);
+MAKE_I_TO_D(u24_32, uint32_t, f32, float, U24_32_TO_F32);
+
+MAKE_D_TO_D(s24_32, int32_t, f32, float, S24_32_TO_F32);
+MAKE_I_TO_I(s24_32, int32_t, f32, float, S24_32_TO_F32);
+MAKE_I_TO_D(s24_32, int32_t, f32, float, S24_32_TO_F32);
+MAKE_D_TO_I(s24_32, int32_t, f32, float, S24_32_TO_F32);
+MAKE_I_TO_D(s24_32s, uint32_t, f32, float, S24_32S_TO_F32);
+
+MAKE_D_TO_D(f64, double, f32, float, (float));
+MAKE_I_TO_I(f64, double, f32, float, (float));
+MAKE_I_TO_D(f64, double, f32, float, (float));
+MAKE_D_TO_I(f64, double, f32, float, (float));
+MAKE_I_TO_D(f64s, uint64_t, f32, float, (float)F64S_TO_F64);
+
+/* from f32 */
+MAKE_D_TO_D(f32, float, u8, uint8_t, F32_TO_U8);
+MAKE_I_TO_I(f32, float, u8, uint8_t, F32_TO_U8);
+MAKE_I_TO_D(f32, float, u8, uint8_t, F32_TO_U8);
+MAKE_D_TO_I(f32, float, u8, uint8_t, F32_TO_U8);
+
+MAKE_D_TO_D(f32, float, s8, int8_t, F32_TO_S8);
+MAKE_I_TO_I(f32, float, s8, int8_t, F32_TO_S8);
+MAKE_I_TO_D(f32, float, s8, int8_t, F32_TO_S8);
+MAKE_D_TO_I(f32, float, s8, int8_t, F32_TO_S8);
+
+MAKE_D_TO_I(f32, float, alaw, uint8_t, f32_to_alaw);
+MAKE_D_TO_I(f32, float, ulaw, uint8_t, f32_to_ulaw);
+
+MAKE_I_TO_I(f32, float, u16, uint16_t, F32_TO_U16);
+MAKE_D_TO_I(f32, float, u16, uint16_t, F32_TO_U16);
+
+MAKE_D_TO_D(f32, float, s16, int16_t, F32_TO_S16);
+MAKE_I_TO_I(f32, float, s16, int16_t, F32_TO_S16);
+MAKE_I_TO_D(f32, float, s16, int16_t, F32_TO_S16);
+MAKE_D_TO_I(f32, float, s16, int16_t, F32_TO_S16);
+MAKE_D_TO_I(f32, float, s16s, uint16_t, F32_TO_S16S);
+
+MAKE_I_TO_I(f32, float, u32, uint32_t, F32_TO_U32);
+MAKE_D_TO_I(f32, float, u32, uint32_t, F32_TO_U32);
+
+MAKE_D_TO_D(f32, float, s32, int32_t, F32_TO_S32);
+MAKE_I_TO_I(f32, float, s32, int32_t, F32_TO_S32);
+MAKE_I_TO_D(f32, float, s32, int32_t, F32_TO_S32);
+MAKE_D_TO_I(f32, float, s32, int32_t, F32_TO_S32);
+MAKE_D_TO_I(f32, float, s32s, uint32_t, F32_TO_S32S);
+
+MAKE_I_TO_I(f32, float, u24, uint24_t, F32_TO_U24);
+MAKE_D_TO_I(f32, float, u24, uint24_t, F32_TO_U24);
+
+MAKE_D_TO_D(f32, float, s24, int24_t, F32_TO_S24);
+MAKE_I_TO_I(f32, float, s24, int24_t, F32_TO_S24);
+MAKE_I_TO_D(f32, float, s24, int24_t, F32_TO_S24);
+MAKE_D_TO_I(f32, float, s24, int24_t, F32_TO_S24);
+MAKE_D_TO_I(f32, float, s24s, int24_t, F32_TO_S24S);
+
+MAKE_I_TO_I(f32, float, u24_32, uint32_t, F32_TO_U24_32);
+MAKE_D_TO_I(f32, float, u24_32, uint32_t, F32_TO_U24_32);
+
+MAKE_D_TO_D(f32, float, s24_32, int32_t, F32_TO_S24_32);
+MAKE_I_TO_I(f32, float, s24_32, int32_t, F32_TO_S24_32);
+MAKE_I_TO_D(f32, float, s24_32, int32_t, F32_TO_S24_32);
+MAKE_D_TO_I(f32, float, s24_32, int32_t, F32_TO_S24_32);
+MAKE_D_TO_I(f32, float, s24_32s, uint32_t, F32_TO_S24_32S);
+
+MAKE_D_TO_D(f32, float, f64, double, (double));
+MAKE_I_TO_I(f32, float, f64, double, (double));
+MAKE_I_TO_D(f32, float, f64, double, (double));
+MAKE_D_TO_I(f32, float, f64, double, (double));
+MAKE_D_TO_I(f32, float, f64s, uint64_t, F64_TO_F64S);
+
+
+static inline int32_t
+lcnoise(uint32_t *state)
+{
+ *state = (*state * 96314165) + 907633515;
+ return (int32_t)(*state);
+}
+
+void conv_noise_none_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+ memset(noise, 0, n_samples * sizeof(float));
+}
+
+void conv_noise_rect_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+ uint32_t n;
+ uint32_t *state = &conv->random[0];
+ const float scale = conv->scale;
+
+ for (n = 0; n < n_samples; n++)
+ noise[n] = lcnoise(state) * scale;
+}
+
+void conv_noise_tri_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+ uint32_t n;
+ const float scale = conv->scale;
+ uint32_t *state = &conv->random[0];
+
+ for (n = 0; n < n_samples; n++)
+ noise[n] = (lcnoise(state) - lcnoise(state)) * scale;
+}
+
+void conv_noise_tri_hf_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+ uint32_t n;
+ const float scale = conv->scale;
+ uint32_t *state = &conv->random[0];
+ int32_t *prev = &conv->prev[0], old, new;
+
+ old = *prev;
+ for (n = 0; n < n_samples; n++) {
+ new = lcnoise(state);
+ noise[n] = (new - old) * scale;
+ old = new;
+ }
+ *prev = old;
+}
+
+void conv_noise_pattern_c(struct convert *conv, float *noise, uint32_t n_samples)
+{
+ uint32_t n;
+ const float scale = conv->scale;
+ int32_t *prev = &conv->prev[0], old;
+
+ old = *prev;
+ for (n = 0; n < n_samples; n++)
+ noise[n] = scale * (1-((old++>>10)&1));
+ *prev = old;
+}
+
+#define MAKE_D_noise(dname,dtype,func) \
+void conv_f32d_to_ ##dname## d_noise_c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ uint32_t i, j, k, chunk, n_channels = conv->n_channels, noise_size = conv->noise_size; \
+ float *noise = conv->noise; \
+ convert_update_noise(conv, noise, SPA_MIN(n_samples, noise_size)); \
+ for (i = 0; i < n_channels; i++) { \
+ const float *s = src[i]; \
+ dtype *d = dst[i]; \
+ for (j = 0; j < n_samples;) { \
+ chunk = SPA_MIN(n_samples - j, noise_size); \
+ for (k = 0; k < chunk; k++, j++) \
+ d[j] = func (s[j], noise[k]); \
+ } \
+ } \
+}
+
+#define MAKE_I_noise(dname,dtype,func) \
+void conv_f32d_to_ ##dname## _noise_c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ const float **s = (const float **) src; \
+ dtype *d = dst[0]; \
+ uint32_t i, j, k, chunk, n_channels = conv->n_channels, noise_size = conv->noise_size; \
+ float *noise = conv->noise; \
+ convert_update_noise(conv, noise, SPA_MIN(n_samples, noise_size)); \
+ for (j = 0; j < n_samples;) { \
+ chunk = SPA_MIN(n_samples - j, noise_size); \
+ for (k = 0; k < chunk; k++, j++) { \
+ for (i = 0; i < n_channels; i++) \
+ *d++ = func (s[i][j], noise[k]); \
+ } \
+ } \
+}
+
+MAKE_D_noise(u8, uint8_t, F32_TO_U8_D);
+MAKE_I_noise(u8, uint8_t, F32_TO_U8_D);
+MAKE_D_noise(s8, int8_t, F32_TO_S8_D);
+MAKE_I_noise(s8, int8_t, F32_TO_S8_D);
+MAKE_D_noise(s16, int16_t, F32_TO_S16_D);
+MAKE_I_noise(s16, int16_t, F32_TO_S16_D);
+MAKE_I_noise(s16s, uint16_t, F32_TO_S16S_D);
+MAKE_D_noise(s32, int32_t, F32_TO_S32_D);
+MAKE_I_noise(s32, int32_t, F32_TO_S32_D);
+MAKE_I_noise(s32s, uint32_t, F32_TO_S32S_D);
+MAKE_D_noise(s24, int24_t, F32_TO_S24_D);
+MAKE_I_noise(s24, int24_t, F32_TO_S24_D);
+MAKE_I_noise(s24s, int24_t, F32_TO_S24_D);
+MAKE_D_noise(s24_32, int32_t, F32_TO_S24_32_D);
+MAKE_I_noise(s24_32, int32_t, F32_TO_S24_32_D);
+MAKE_I_noise(s24_32s, int32_t, F32_TO_S24_32S_D);
+
+#define SHAPER(type,s,scale,offs,sh,min,max,d) \
+({ \
+ type t; \
+ float v = s * scale + offs; \
+ for (n = 0; n < n_ns; n++) \
+ v += sh->e[idx + n] * ns[n]; \
+ t = FTOI(type, v, 1.0f, 0.0f, d, min, max); \
+ idx = (idx - 1) & NS_MASK; \
+ sh->e[idx] = sh->e[idx + NS_MAX] = v - t; \
+ t; \
+})
+
+#define F32_TO_U8_SH(s,sh,d) SHAPER(uint8_t, s, U8_SCALE, U8_OFFS, sh, U8_MIN, U8_MAX, d)
+#define F32_TO_S8_SH(s,sh,d) SHAPER(int8_t, s, S8_SCALE, 0, sh, S8_MIN, S8_MAX, d)
+#define F32_TO_S16_SH(s,sh,d) SHAPER(int16_t, s, S16_SCALE, 0, sh, S16_MIN, S16_MAX, d)
+#define F32_TO_S16S_SH(s,sh,d) bswap_16(F32_TO_S16_SH(s,sh,d))
+
+#define MAKE_D_shaped(dname,dtype,func) \
+void conv_f32d_to_ ##dname## d_shaped_c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ uint32_t i, j, k, chunk, n_channels = conv->n_channels, noise_size = conv->noise_size; \
+ float *noise = conv->noise; \
+ const float *ns = conv->ns; \
+ uint32_t n, n_ns = conv->n_ns; \
+ convert_update_noise(conv, noise, SPA_MIN(n_samples, noise_size)); \
+ for (i = 0; i < n_channels; i++) { \
+ const float *s = src[i]; \
+ dtype *d = dst[i]; \
+ struct shaper *sh = &conv->shaper[i]; \
+ uint32_t idx = sh->idx; \
+ for (j = 0; j < n_samples;) { \
+ chunk = SPA_MIN(n_samples - j, noise_size); \
+ for (k = 0; k < chunk; k++, j++) \
+ d[j] = func (s[j], sh, noise[k]); \
+ } \
+ sh->idx = idx; \
+ } \
+}
+
+#define MAKE_I_shaped(dname,dtype,func) \
+void conv_f32d_to_ ##dname## _shaped_c(struct convert *conv, \
+ void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \
+ uint32_t n_samples) \
+{ \
+ dtype *d0 = dst[0]; \
+ uint32_t i, j, k, chunk, n_channels = conv->n_channels, noise_size = conv->noise_size; \
+ float *noise = conv->noise; \
+ const float *ns = conv->ns; \
+ uint32_t n, n_ns = conv->n_ns; \
+ convert_update_noise(conv, noise, SPA_MIN(n_samples, noise_size)); \
+ for (i = 0; i < n_channels; i++) { \
+ const float *s = src[i]; \
+ dtype *d = &d0[i]; \
+ struct shaper *sh = &conv->shaper[i]; \
+ uint32_t idx = sh->idx; \
+ for (j = 0; j < n_samples;) { \
+ chunk = SPA_MIN(n_samples - j, noise_size); \
+ for (k = 0; k < chunk; k++, j++) \
+ d[j*n_channels] = func (s[j], sh, noise[k]); \
+ } \
+ sh->idx = idx; \
+ } \
+}
+
+MAKE_D_shaped(u8, uint8_t, F32_TO_U8_SH);
+MAKE_I_shaped(u8, uint8_t, F32_TO_U8_SH);
+MAKE_D_shaped(s8, int8_t, F32_TO_S8_SH);
+MAKE_I_shaped(s8, int8_t, F32_TO_S8_SH);
+MAKE_D_shaped(s16, int16_t, F32_TO_S16_SH);
+MAKE_I_shaped(s16, int16_t, F32_TO_S16_SH);
+MAKE_I_shaped(s16s, uint16_t, F32_TO_S16S_SH);
+
+#define MAKE_DEINTERLEAVE(size1,size2, type,func) \
+ MAKE_I_TO_D(size1,type,size2,type,func)
+
+MAKE_DEINTERLEAVE(8, 8, uint8_t, (uint8_t));
+MAKE_DEINTERLEAVE(16, 16, uint16_t, (uint16_t));
+MAKE_DEINTERLEAVE(24, 24, uint24_t, (uint24_t));
+MAKE_DEINTERLEAVE(32, 32, uint32_t, (uint32_t));
+MAKE_DEINTERLEAVE(32s, 32, uint32_t, bswap_32);
+MAKE_DEINTERLEAVE(64, 64, uint64_t, (uint64_t));
+
+#define MAKE_INTERLEAVE(size1,size2,type,func) \
+ MAKE_D_TO_I(size1,type,size2,type,func)
+
+MAKE_INTERLEAVE(8, 8, uint8_t, (uint8_t));
+MAKE_INTERLEAVE(16, 16, uint16_t, (uint16_t));
+MAKE_INTERLEAVE(24, 24, uint24_t, (uint24_t));
+MAKE_INTERLEAVE(32, 32, uint32_t, (uint32_t));
+MAKE_INTERLEAVE(32, 32s, uint32_t, bswap_32);
+MAKE_INTERLEAVE(64, 64, uint64_t, (uint64_t));
diff --git a/spa/plugins/audioconvert/fmt-ops-neon.c b/spa/plugins/audioconvert/fmt-ops-neon.c
new file mode 100644
index 0000000..e6c8b84
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops-neon.c
@@ -0,0 +1,487 @@
+/* Spa
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <arm_neon.h>
+
+#include "fmt-ops.h"
+
+void
+conv_s16_to_f32d_2_neon(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int16_t *s = src[0];
+ float *d0 = dst[0], *d1 = dst[1];
+ unsigned int remainder = n_samples & 7;
+ n_samples -= remainder;
+
+#ifdef __aarch64__
+ asm volatile(
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " ld2 {v2.8h, v3.8h}, [%[s]], #32\n"
+ " subs %w[n_samples], %w[n_samples], #8\n"
+ " sxtl v0.4s, v2.4h\n"
+ " sxtl2 v1.4s, v2.8h\n"
+ " sxtl v2.4s, v3.4h\n"
+ " sxtl2 v3.4s, v3.8h\n"
+ " scvtf v0.4s, v0.4s, #15\n"
+ " scvtf v1.4s, v1.4s, #15\n"
+ " scvtf v2.4s, v2.4s, #15\n"
+ " scvtf v3.4s, v3.4s, #15\n"
+ " st1 {v0.4s, v1.4s}, [%[d0]], #32\n"
+ " st1 {v2.4s, v3.4s}, [%[d1]], #32\n"
+ " b.ne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " ld2 { v0.h, v1.h }[0], [%[s]], #4\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " sshll v2.4s, v0.4h, #0\n"
+ " sshll v3.4s, v1.4h, #0\n"
+ " scvtf v0.4s, v2.4s, #15\n"
+ " scvtf v1.4s, v3.4s, #15\n"
+ " st1 { v0.s }[0], [%[d0]], #4\n"
+ " st1 { v1.s }[0], [%[d1]], #4\n"
+ " bne 3b\n"
+ "4:"
+ : [d0] "+r" (d0), [d1] "+r" (d1), [s] "+r" (s), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : : "v0", "v1", "v2", "v3", "memory", "cc");
+#else
+ asm volatile(
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " vld2.16 {d0-d3}, [%[s]]!\n"
+ " subs %[n_samples], #8\n"
+ " vmovl.s16 q3, d3\n"
+ " vmovl.s16 q2, d2\n"
+ " vmovl.s16 q1, d1\n"
+ " vmovl.s16 q0, d0\n"
+ " vcvt.f32.s32 q3, q3, #15\n"
+ " vcvt.f32.s32 q2, q2, #15\n"
+ " vcvt.f32.s32 q1, q1, #15\n"
+ " vcvt.f32.s32 q0, q0, #15\n"
+ " vst1.32 {d4-d7}, [%[d1]]!\n"
+ " vst1.32 {d0-d3}, [%[d0]]!\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " vld2.16 { d0[0], d1[0] }, [%[s]]!\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " vmovl.s16 q1, d1\n"
+ " vmovl.s16 q0, d0\n"
+ " vcvt.f32.s32 q1, q1, #15\n"
+ " vcvt.f32.s32 q0, q0, #15\n"
+ " vst1.32 { d2[0] }, [%[d1]]!\n"
+ " vst1.32 { d0[0] }, [%[d0]]!\n"
+ " bne 3b\n"
+ "4:"
+ : [d0] "+r" (d0), [d1] "+r" (d1), [s] "+r" (s), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : : "q0", "q1", "q2", "q3", "memory", "cc");
+#endif
+}
+
+static void
+conv_s16_to_f32d_2s_neon(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int16_t *s = src;
+ float *d0 = dst[0], *d1 = dst[1];
+ uint32_t stride = n_channels << 1;
+ unsigned int remainder = n_samples & 3;
+ n_samples -= remainder;
+
+#ifdef __aarch64__
+ asm volatile(
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " ld2 { v0.h, v1.h }[0], [%[s]], %[stride]\n"
+ " ld2 { v0.h, v1.h }[1], [%[s]], %[stride]\n"
+ " ld2 { v0.h, v1.h }[2], [%[s]], %[stride]\n"
+ " ld2 { v0.h, v1.h }[3], [%[s]], %[stride]\n"
+ " subs %[n_samples], %[n_samples], #4\n"
+ " sshll v2.4s, v0.4h, #0\n"
+ " sshll v3.4s, v1.4h, #0\n"
+ " scvtf v0.4s, v2.4s, #15\n"
+ " scvtf v1.4s, v3.4s, #15\n"
+ " st1 { v0.4s }, [%[d0]], #16\n"
+ " st1 { v1.4s }, [%[d1]], #16\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " ld2 { v0.h, v1.h }[0], [%[s]], %[stride]\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " sshll v2.4s, v0.4h, #0\n"
+ " sshll v3.4s, v1.4h, #0\n"
+ " scvtf v0.4s, v2.4s, #15\n"
+ " scvtf v1.4s, v3.4s, #15\n"
+ " st1 { v0.s }[0], [%[d0]], #4\n"
+ " st1 { v1.s }[0], [%[d1]], #4\n"
+ " bne 3b\n"
+ "4:"
+ : [d0] "+r" (d0), [d1] "+r" (d1), [s] "+r" (s), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : [stride] "r" (stride)
+ : "cc", "v0", "v1", "v2", "v3");
+#else
+ asm volatile(
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " vld2.16 { d0[0], d1[0] }, [%[s]], %[stride]\n"
+ " vld2.16 { d0[1], d1[1] }, [%[s]], %[stride]\n"
+ " vld2.16 { d0[2], d1[2] }, [%[s]], %[stride]\n"
+ " vld2.16 { d0[3], d1[3] }, [%[s]], %[stride]\n"
+ " subs %[n_samples], %[n_samples], #4\n"
+ " vmovl.s16 q1, d1\n"
+ " vmovl.s16 q0, d0\n"
+ " vcvt.f32.s32 q0, q0, #15\n"
+ " vcvt.f32.s32 q1, q1, #15\n"
+ " vst1.32 { q0 }, [%[d0]]!\n"
+ " vst1.32 { q1 }, [%[d1]]!\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " vld2.16 { d0[0], d1[0] }, [%[s]], %[stride]\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " vmovl.s16 q1, d1\n"
+ " vmovl.s16 q0, d0\n"
+ " vcvt.f32.s32 q0, q0, #15\n"
+ " vcvt.f32.s32 q1, q1, #15\n"
+ " vst1.32 { d0[0] }, [%[d0]]!\n"
+ " vst1.32 { d2[0] }, [%[d1]]!\n"
+ " bne 3b\n"
+ "4:"
+ : [d0] "+r" (d0), [d1] "+r" (d1), [s] "+r" (s), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : [stride] "r" (stride)
+ : "cc", "q0", "q1");
+#endif
+}
+
+static void
+conv_s16_to_f32d_1s_neon(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int16_t *s = src;
+ float *d = dst[0];
+ uint32_t stride = n_channels << 1;
+ uint32_t remainder = n_samples & 3;
+ n_samples -= remainder;
+
+#ifdef __aarch64__
+ asm volatile(
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " ld1 { v0.h }[0], [%[s]], %[stride]\n"
+ " ld1 { v0.h }[1], [%[s]], %[stride]\n"
+ " ld1 { v0.h }[2], [%[s]], %[stride]\n"
+ " ld1 { v0.h }[3], [%[s]], %[stride]\n"
+ " subs %[n_samples], %[n_samples], #4\n"
+ " sshll v1.4s, v0.4h, #0\n"
+ " scvtf v0.4s, v1.4s, #15\n"
+ " st1 { v0.4s }, [%[d]], #16\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " ld1 { v0.h }[0], [%[s]], %[stride]\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " sshll v1.4s, v0.4h, #0\n"
+ " scvtf v0.4s, v1.4s, #15\n"
+ " st1 { v0.s }[0], [%[d]], #4\n"
+ " bne 3b\n"
+ "4:"
+ : [d] "+r" (d), [s] "+r" (s), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : [stride] "r" (stride)
+ : "cc", "v0", "v1");
+#else
+ asm volatile(
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " vld1.16 { d0[0] }, [%[s]], %[stride]\n"
+ " vld1.16 { d0[1] }, [%[s]], %[stride]\n"
+ " vld1.16 { d0[2] }, [%[s]], %[stride]\n"
+ " vld1.16 { d0[3] }, [%[s]], %[stride]\n"
+ " subs %[n_samples], %[n_samples], #4\n"
+ " vmovl.s16 q0, d0\n"
+ " vcvt.f32.s32 q0, q0, #15\n"
+ " vst1.32 { q0 }, [%[d]]!\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " vld1.16 { d0[0] }, [%[s]], %[stride]\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " vmovl.s16 q0, d0\n"
+ " vcvt.f32.s32 q0, q0, #15\n"
+ " vst1.32 { d0[0] }, [%[d]]!\n"
+ " bne 3b\n"
+ "4:"
+ : [d] "+r" (d), [s] "+r" (s), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : [stride] "r" (stride)
+ : "cc", "q0");
+#endif
+}
+
+void
+conv_s16_to_f32d_neon(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int16_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 1 < n_channels; i += 2)
+ conv_s16_to_f32d_2s_neon(conv, &dst[i], &s[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_s16_to_f32d_1s_neon(conv, &dst[i], &s[i], n_channels, n_samples);
+}
+
+static void
+conv_f32d_to_s16_2s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s0 = src[0], *s1 = src[1];
+ int16_t *d = dst;
+ uint32_t stride = n_channels << 1;
+ uint32_t remainder = n_samples & 3;
+ n_samples -= remainder;
+
+#ifdef __aarch64__
+ asm volatile(
+ " dup v2.4s, %w[scale]\n"
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " ld1 { v0.4s }, [%[s0]], #16\n"
+ " ld1 { v1.4s }, [%[s1]], #16\n"
+ " subs %[n_samples], %[n_samples], #4\n"
+ " sqadd v0.4s, v0.4s, v2.4s\n"
+ " sqadd v1.4s, v1.4s, v2.4s\n"
+ " fcvtns v0.4s, v0.4s\n"
+ " fcvtns v1.4s, v1.4s\n"
+ " sqxtn v0.4h, v0.4s\n"
+ " sqxtn v1.4h, v1.4s\n"
+ " st2 { v0.h, v1.h }[0], [%[d]], %[stride]\n"
+ " st2 { v0.h, v1.h }[1], [%[d]], %[stride]\n"
+ " st2 { v0.h, v1.h }[2], [%[d]], %[stride]\n"
+ " st2 { v0.h, v1.h }[3], [%[d]], %[stride]\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " ld1 { v0.s }[0], [%[s0]], #4\n"
+ " ld1 { v2.s }[0], [%[s1]], #4\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " sqadd v0.4s, v0.4s, v2.4s\n"
+ " sqadd v1.4s, v1.4s, v2.4s\n"
+ " fcvtns v0.4s, v0.4s\n"
+ " fcvtns v1.4s, v1.4s\n"
+ " sqxtn v0.4h, v0.4s\n"
+ " sqxtn v1.4h, v1.4s\n"
+ " st2 { v0.h, v1.h }[0], [%[d]], %[stride]\n"
+ " bne 3b\n"
+ "4:"
+ : [d] "+r" (d), [s0] "+r" (s0), [s1] "+r" (s1), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : [stride] "r" (stride),
+ [scale] "r" (15 << 23)
+ : "cc", "v0", "v1");
+#else
+ float32x4_t pos = vdupq_n_f32(0.4999999f / S16_SCALE);
+ float32x4_t neg = vdupq_n_f32(-0.4999999f / S16_SCALE);
+
+ asm volatile(
+ " veor q2, q2, q2\n"
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " vld1.32 { q0 }, [%[s0]]!\n"
+ " vld1.32 { q1 }, [%[s1]]!\n"
+ " subs %[n_samples], %[n_samples], #4\n"
+ " vcgt.f32 q3, q0, q2\n"
+ " vcgt.f32 q4, q0, q2\n"
+ " vbsl q3, %q[pos], %q[neg]\n"
+ " vbsl q4, %q[pos], %q[neg]\n"
+ " vadd.f32 q0, q0, q3\n"
+ " vadd.f32 q1, q1, q4\n"
+ " vcvt.s32.f32 q0, q0, #15\n"
+ " vcvt.s32.f32 q1, q1, #15\n"
+ " vqmovn.s32 d0, q0\n"
+ " vqmovn.s32 d1, q1\n"
+ " vst2.16 { d0[0], d1[0] }, [%[d]], %[stride]\n"
+ " vst2.16 { d0[1], d1[1] }, [%[d]], %[stride]\n"
+ " vst2.16 { d0[2], d1[2] }, [%[d]], %[stride]\n"
+ " vst2.16 { d0[3], d1[3] }, [%[d]], %[stride]\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " vld1.32 { d0[0] }, [%[s0]]!\n"
+ " vld1.32 { d2[0] }, [%[s1]]!\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " vcgt.f32 q3, q0, q2\n"
+ " vcgt.f32 q4, q0, q2\n"
+ " vbsl q3, %q[pos], %q[neg]\n"
+ " vbsl q4, %q[pos], %q[neg]\n"
+ " vadd.f32 q0, q0, q3\n"
+ " vadd.f32 q1, q1, q4\n"
+ " vcvt.s32.f32 q0, q0, #15\n"
+ " vcvt.s32.f32 q1, q1, #15\n"
+ " vqmovn.s32 d0, q0\n"
+ " vqmovn.s32 d1, q1\n"
+ " vst2.16 { d0[0], d1[0] }, [%[d]], %[stride]\n"
+ " bne 3b\n"
+ "4:"
+ : [d] "+r" (d), [s0] "+r" (s0), [s1] "+r" (s1), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : [stride] "r" (stride),
+ [pos]"w"(pos),
+ [neg]"w"(neg)
+ : "cc", "q0", "q1", "q2", "q3", "q4");
+#endif
+}
+
+static void
+conv_f32d_to_s16_1s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s = src[0];
+ int16_t *d = dst;
+ uint32_t stride = n_channels << 1;
+ uint32_t remainder = n_samples & 3;
+ n_samples -= remainder;
+
+#ifdef __aarch64__
+ asm volatile(
+ " dup v2.4s, %w[scale]\n"
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " ld1 { v0.4s }, [%[s]], #16\n"
+ " subs %[n_samples], %[n_samples], #4\n"
+ " sqadd v0.4s, v0.4s, v2.4s\n"
+ " fcvtns v0.4s, v0.4s\n"
+ " sqxtn v0.4h, v0.4s\n"
+ " st1 { v0.h }[0], [%[d]], %[stride]\n"
+ " st1 { v0.h }[1], [%[d]], %[stride]\n"
+ " st1 { v0.h }[2], [%[d]], %[stride]\n"
+ " st1 { v0.h }[3], [%[d]], %[stride]\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " ld1 { v0.s }[0], [%[s]], #4\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " sqadd v0.4s, v0.4s, v2.4s\n"
+ " fcvtns v0.4s, v0.4s\n"
+ " sqxtn v0.4h, v0.4s\n"
+ " st1 { v0.h }[0], [%[d]], %[stride]\n"
+ " bne 3b\n"
+ "4:"
+ : [d] "+r" (d), [s] "+r" (s), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : [stride] "r" (stride),
+ [scale] "r" (15 << 23)
+ : "cc", "v0");
+#else
+ float32x4_t pos = vdupq_n_f32(0.4999999f / S16_SCALE);
+ float32x4_t neg = vdupq_n_f32(-0.4999999f / S16_SCALE);
+
+ asm volatile(
+ " veor q1, q1, q1\n"
+ " cmp %[n_samples], #0\n"
+ " beq 2f\n"
+ "1:"
+ " vld1.32 { q0 }, [%[s]]!\n"
+ " subs %[n_samples], %[n_samples], #4\n"
+ " vcgt.f32 q2, q0, q1\n"
+ " vbsl q2, %q[pos], %q[neg]\n"
+ " vadd.f32 q0, q0, q2\n"
+ " vcvt.s32.f32 q0, q0, #15\n"
+ " vqmovn.s32 d0, q0\n"
+ " vst1.16 { d0[0] }, [%[d]], %[stride]\n"
+ " vst1.16 { d0[1] }, [%[d]], %[stride]\n"
+ " vst1.16 { d0[2] }, [%[d]], %[stride]\n"
+ " vst1.16 { d0[3] }, [%[d]], %[stride]\n"
+ " bne 1b\n"
+ "2:"
+ " cmp %[remainder], #0\n"
+ " beq 4f\n"
+ "3:"
+ " vld1.32 { d0[0] }, [%[s]]!\n"
+ " subs %[remainder], %[remainder], #1\n"
+ " vcgt.f32 q2, q0, q1\n"
+ " vbsl q2, %q[pos], %q[neg]\n"
+ " vadd.f32 q0, q0, q2\n"
+ " vcvt.s32.f32 q0, q0, #15\n"
+ " vqmovn.s32 d0, q0\n"
+ " vst1.16 { d0[0] }, [%[d]], %[stride]\n"
+ " bne 3b\n"
+ "4:"
+ : [d] "+r" (d), [s] "+r" (s), [n_samples] "+r" (n_samples),
+ [remainder] "+r" (remainder)
+ : [stride] "r" (stride),
+ [pos]"w"(pos),
+ [neg]"w"(neg)
+ : "cc", "q0", "q1", "q2");
+#endif
+}
+
+void
+conv_f32d_to_s16_neon(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int16_t *d = dst[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 1 < n_channels; i += 2)
+ conv_f32d_to_s16_2s_neon(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_f32d_to_s16_1s_neon(conv, &d[i], &src[i], n_channels, n_samples);
+}
diff --git a/spa/plugins/audioconvert/fmt-ops-sse2.c b/spa/plugins/audioconvert/fmt-ops-sse2.c
new file mode 100644
index 0000000..4e2fce7
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops-sse2.c
@@ -0,0 +1,1438 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "fmt-ops.h"
+
+#include <emmintrin.h>
+
+#define _MM_CLAMP_PS(r,min,max) \
+ _mm_min_ps(_mm_max_ps(r, min), max)
+
+#define _MM_CLAMP_SS(r,min,max) \
+ _mm_min_ss(_mm_max_ss(r, min), max)
+
+static void
+conv_s16_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int16_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_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, s[0]);
+ out = _mm_mul_ss(out, factor);
+ _mm_store_ss(&d0[n], out);
+ s += n_channels;
+ }
+}
+
+void
+conv_s16_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int16_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i < n_channels; i++)
+ conv_s16_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)
+{
+ const int16_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));
+
+ 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, s[0]);
+ out[0] = _mm_mul_ss(out[0], factor);
+ out[1] = _mm_cvtsi32_ss(factor, s[1]);
+ out[1] = _mm_mul_ss(out[1], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ s += 2;
+ }
+}
+
+void
+conv_s24_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int24_t *s = src;
+ float *d0 = dst[0];
+ uint32_t n, unrolled;
+ __m128i in;
+ __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
+
+ if (SPA_IS_ALIGNED(d0, 16) && n_samples > 0) {
+ unrolled = n_samples & ~3;
+ if ((n_samples & 3) == 0)
+ unrolled -= 4;
+ }
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in = _mm_setr_epi32(
+ *((uint32_t*)&s[0 * n_channels]),
+ *((uint32_t*)&s[1 * n_channels]),
+ *((uint32_t*)&s[2 * n_channels]),
+ *((uint32_t*)&s[3 * n_channels]));
+ in = _mm_slli_epi32(in, 8);
+ in = _mm_srai_epi32(in, 8);
+ out = _mm_cvtepi32_ps(in);
+ out = _mm_mul_ps(out, factor);
+ _mm_store_ps(&d0[n], out);
+ s += 4 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+ out = _mm_mul_ss(out, factor);
+ _mm_store_ss(&d0[n], out);
+ s += n_channels;
+ }
+}
+
+static void
+conv_s24_to_f32d_2s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int24_t *s = src;
+ float *d0 = dst[0], *d1 = dst[1];
+ uint32_t n, unrolled;
+ __m128i in[2];
+ __m128 out[2], factor = _mm_set1_ps(1.0f / S24_SCALE);
+
+ if (SPA_IS_ALIGNED(d0, 16) &&
+ SPA_IS_ALIGNED(d1, 16) &&
+ n_samples > 0) {
+ unrolled = n_samples & ~3;
+ if ((n_samples & 3) == 0)
+ unrolled -= 4;
+ }
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_setr_epi32(
+ *((uint32_t*)&s[0 + 0*n_channels]),
+ *((uint32_t*)&s[0 + 1*n_channels]),
+ *((uint32_t*)&s[0 + 2*n_channels]),
+ *((uint32_t*)&s[0 + 3*n_channels]));
+ in[1] = _mm_setr_epi32(
+ *((uint32_t*)&s[1 + 0*n_channels]),
+ *((uint32_t*)&s[1 + 1*n_channels]),
+ *((uint32_t*)&s[1 + 2*n_channels]),
+ *((uint32_t*)&s[1 + 3*n_channels]));
+
+ in[0] = _mm_slli_epi32(in[0], 8);
+ in[1] = _mm_slli_epi32(in[1], 8);
+
+ in[0] = _mm_srai_epi32(in[0], 8);
+ in[1] = _mm_srai_epi32(in[1], 8);
+
+ out[0] = _mm_cvtepi32_ps(in[0]);
+ out[1] = _mm_cvtepi32_ps(in[1]);
+
+ out[0] = _mm_mul_ps(out[0], factor);
+ out[1] = _mm_mul_ps(out[1], factor);
+
+ _mm_store_ps(&d0[n], out[0]);
+ _mm_store_ps(&d1[n], out[1]);
+
+ s += 4 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+ out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
+ out[0] = _mm_mul_ss(out[0], factor);
+ out[1] = _mm_mul_ss(out[1], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ s += n_channels;
+ }
+}
+static void
+conv_s24_to_f32d_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int24_t *s = src;
+ float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
+ uint32_t n, unrolled;
+ __m128i in[4];
+ __m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE);
+
+ if (SPA_IS_ALIGNED(d0, 16) &&
+ SPA_IS_ALIGNED(d1, 16) &&
+ SPA_IS_ALIGNED(d2, 16) &&
+ SPA_IS_ALIGNED(d3, 16) &&
+ n_samples > 0) {
+ unrolled = n_samples & ~3;
+ if ((n_samples & 3) == 0)
+ unrolled -= 4;
+ }
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_setr_epi32(
+ *((uint32_t*)&s[0 + 0*n_channels]),
+ *((uint32_t*)&s[0 + 1*n_channels]),
+ *((uint32_t*)&s[0 + 2*n_channels]),
+ *((uint32_t*)&s[0 + 3*n_channels]));
+ in[1] = _mm_setr_epi32(
+ *((uint32_t*)&s[1 + 0*n_channels]),
+ *((uint32_t*)&s[1 + 1*n_channels]),
+ *((uint32_t*)&s[1 + 2*n_channels]),
+ *((uint32_t*)&s[1 + 3*n_channels]));
+ in[2] = _mm_setr_epi32(
+ *((uint32_t*)&s[2 + 0*n_channels]),
+ *((uint32_t*)&s[2 + 1*n_channels]),
+ *((uint32_t*)&s[2 + 2*n_channels]),
+ *((uint32_t*)&s[2 + 3*n_channels]));
+ in[3] = _mm_setr_epi32(
+ *((uint32_t*)&s[3 + 0*n_channels]),
+ *((uint32_t*)&s[3 + 1*n_channels]),
+ *((uint32_t*)&s[3 + 2*n_channels]),
+ *((uint32_t*)&s[3 + 3*n_channels]));
+
+ in[0] = _mm_slli_epi32(in[0], 8);
+ in[1] = _mm_slli_epi32(in[1], 8);
+ in[2] = _mm_slli_epi32(in[2], 8);
+ in[3] = _mm_slli_epi32(in[3], 8);
+
+ in[0] = _mm_srai_epi32(in[0], 8);
+ in[1] = _mm_srai_epi32(in[1], 8);
+ in[2] = _mm_srai_epi32(in[2], 8);
+ in[3] = _mm_srai_epi32(in[3], 8);
+
+ out[0] = _mm_cvtepi32_ps(in[0]);
+ out[1] = _mm_cvtepi32_ps(in[1]);
+ out[2] = _mm_cvtepi32_ps(in[2]);
+ out[3] = _mm_cvtepi32_ps(in[3]);
+
+ out[0] = _mm_mul_ps(out[0], factor);
+ out[1] = _mm_mul_ps(out[1], factor);
+ out[2] = _mm_mul_ps(out[2], factor);
+ out[3] = _mm_mul_ps(out[3], factor);
+
+ _mm_store_ps(&d0[n], out[0]);
+ _mm_store_ps(&d1[n], out[1]);
+ _mm_store_ps(&d2[n], out[2]);
+ _mm_store_ps(&d3[n], out[3]);
+
+ s += 4 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+ out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
+ out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2)));
+ out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3)));
+ out[0] = _mm_mul_ss(out[0], factor);
+ out[1] = _mm_mul_ss(out[1], factor);
+ out[2] = _mm_mul_ss(out[2], factor);
+ out[3] = _mm_mul_ss(out[3], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ _mm_store_ss(&d2[n], out[2]);
+ _mm_store_ss(&d3[n], out[3]);
+ s += n_channels;
+ }
+}
+
+void
+conv_s24_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int8_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_s24_to_f32d_4s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples);
+ for(; i + 1 < n_channels; i += 2)
+ conv_s24_to_f32d_2s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_s24_to_f32d_1s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples);
+}
+
+
+void
+conv_s32_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int32_t *s = src;
+ float *d0 = dst[0];
+ uint32_t n, unrolled;
+ __m128i in;
+ __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
+
+ if (SPA_IS_ALIGNED(d0, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in = _mm_setr_epi32(s[0*n_channels],
+ 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_mul_ss(out, factor);
+ _mm_store_ss(&d0[n], out);
+ s += n_channels;
+ }
+}
+
+void
+conv_s32_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int32_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i < n_channels; i++)
+ conv_s32_to_f32d_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples);
+}
+
+static void
+conv_f32d_to_s32_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];
+ int32_t *d = dst;
+ uint32_t n, unrolled;
+ __m128 in[1];
+ __m128i out[4];
+ __m128 scale = _mm_set1_ps(S24_SCALE);
+ __m128 int_min = _mm_set1_ps(S24_MIN);
+ __m128 int_max = _mm_set1_ps(S24_MAX);
+
+ if (SPA_IS_ALIGNED(s0, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ 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));
+
+ d[0*n_channels] = _mm_cvtsi128_si32(out[0]);
+ d[1*n_channels] = _mm_cvtsi128_si32(out[1]);
+ d[2*n_channels] = _mm_cvtsi128_si32(out[2]);
+ d[3*n_channels] = _mm_cvtsi128_si32(out[3]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ in[0] = _mm_load_ss(&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 += n_channels;
+ }
+}
+
+static void
+conv_f32d_to_s32_2s_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], *s1 = src[1];
+ int32_t *d = dst;
+ 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);
+
+ if (SPA_IS_ALIGNED(s0, 16) &&
+ SPA_IS_ALIGNED(s1, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale);
+ in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), scale);
+
+ in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
+ in[1] = _MM_CLAMP_PS(in[1], int_min, int_max);
+
+ out[0] = _mm_cvtps_epi32(in[0]);
+ out[1] = _mm_cvtps_epi32(in[1]);
+ out[0] = _mm_slli_epi32(out[0], 8);
+ out[1] = _mm_slli_epi32(out[1], 8);
+
+ t[0] = _mm_unpacklo_epi32(out[0], out[1]);
+ t[1] = _mm_unpackhi_epi32(out[0], out[1]);
+
+ _mm_storel_pd((double*)(d + 0*n_channels), (__m128d)t[0]);
+ _mm_storeh_pd((double*)(d + 1*n_channels), (__m128d)t[0]);
+ _mm_storel_pd((double*)(d + 2*n_channels), (__m128d)t[1]);
+ _mm_storeh_pd((double*)(d + 3*n_channels), (__m128d)t[1]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ in[0] = _mm_load_ss(&s0[n]);
+ in[1] = _mm_load_ss(&s1[n]);
+
+ in[0] = _mm_unpacklo_ps(in[0], in[1]);
+
+ 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;
+ }
+}
+
+static void
+conv_f32d_to_s32_4s_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], *s1 = src[1], *s2 = src[2], *s3 = src[3];
+ int32_t *d = dst;
+ 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);
+
+ if (SPA_IS_ALIGNED(s0, 16) &&
+ SPA_IS_ALIGNED(s1, 16) &&
+ SPA_IS_ALIGNED(s2, 16) &&
+ SPA_IS_ALIGNED(s3, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale);
+ in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), scale);
+ in[2] = _mm_mul_ps(_mm_load_ps(&s2[n]), scale);
+ in[3] = _mm_mul_ps(_mm_load_ps(&s3[n]), scale);
+
+ in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
+ in[1] = _MM_CLAMP_PS(in[1], int_min, int_max);
+ in[2] = _MM_CLAMP_PS(in[2], int_min, int_max);
+ in[3] = _MM_CLAMP_PS(in[3], int_min, int_max);
+
+ _MM_TRANSPOSE4_PS(in[0], in[1], in[2], in[3]);
+
+ 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_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]);
+ _mm_storeu_si128((__m128i*)(d + 2*n_channels), out[2]);
+ _mm_storeu_si128((__m128i*)(d + 3*n_channels), out[3]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ in[0] = _mm_load_ss(&s0[n]);
+ in[1] = _mm_load_ss(&s1[n]);
+ in[2] = _mm_load_ss(&s2[n]);
+ in[3] = _mm_load_ss(&s3[n]);
+
+ in[0] = _mm_unpacklo_ps(in[0], in[2]);
+ in[1] = _mm_unpacklo_ps(in[1], in[3]);
+ in[0] = _mm_unpacklo_ps(in[0], in[1]);
+
+ 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;
+ }
+}
+
+void
+conv_f32d_to_s32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int32_t *d = dst[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_f32d_to_s32_4s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i + 1 < n_channels; i += 2)
+ conv_f32d_to_s32_2s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_f32d_to_s32_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+}
+
+/* 32 bit xorshift PRNG, see https://en.wikipedia.org/wiki/Xorshift */
+#define _MM_XORSHIFT_EPI32(r) \
+({ \
+ __m128i i, t; \
+ i = _mm_load_si128((__m128i*)r); \
+ t = _mm_slli_epi32(i, 13); \
+ i = _mm_xor_si128(i, t); \
+ t = _mm_srli_epi32(i, 17); \
+ i = _mm_xor_si128(i, t); \
+ t = _mm_slli_epi32(i, 5); \
+ i = _mm_xor_si128(i, t); \
+ _mm_store_si128((__m128i*)r, i); \
+ i; \
+})
+
+void conv_noise_rect_sse2(struct convert *conv, float *noise, uint32_t n_samples)
+{
+ uint32_t n;
+ const uint32_t *r = conv->random;
+ __m128 scale = _mm_set1_ps(conv->scale);
+ __m128i in[1];
+ __m128 out[1];
+
+ for (n = 0; n < n_samples; n += 4) {
+ in[0] = _MM_XORSHIFT_EPI32(r);
+ out[0] = _mm_cvtepi32_ps(in[0]);
+ out[0] = _mm_mul_ps(out[0], scale);
+ _mm_store_ps(&noise[n], out[0]);
+ }
+}
+
+void conv_noise_tri_sse2(struct convert *conv, float *noise, uint32_t n_samples)
+{
+ uint32_t n;
+ const uint32_t *r = conv->random;
+ __m128 scale = _mm_set1_ps(conv->scale);
+ __m128i in[1];
+ __m128 out[1];
+
+ for (n = 0; n < n_samples; n += 4) {
+ in[0] = _mm_sub_epi32( _MM_XORSHIFT_EPI32(r), _MM_XORSHIFT_EPI32(r));
+ out[0] = _mm_cvtepi32_ps(in[0]);
+ out[0] = _mm_mul_ps(out[0], scale);
+ _mm_store_ps(&noise[n], out[0]);
+ }
+}
+
+void conv_noise_tri_hf_sse2(struct convert *conv, float *noise, uint32_t n_samples)
+{
+ uint32_t n;
+ int32_t *p = conv->prev;
+ const uint32_t *r = conv->random;
+ __m128 scale = _mm_set1_ps(conv->scale);
+ __m128i in[1], old[1], new[1];
+ __m128 out[1];
+
+ old[0] = _mm_load_si128((__m128i*)p);
+ for (n = 0; n < n_samples; n += 4) {
+ new[0] = _MM_XORSHIFT_EPI32(r);
+ in[0] = _mm_sub_epi32(old[0], new[0]);
+ old[0] = new[0];
+ out[0] = _mm_cvtepi32_ps(in[0]);
+ out[0] = _mm_mul_ps(out[0], scale);
+ _mm_store_ps(&noise[n], out[0]);
+ }
+ _mm_store_si128((__m128i*)p, old[0]);
+}
+
+static void
+conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
+ float *noise, uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s = src;
+ int32_t *d = dst;
+ uint32_t n, unrolled;
+ __m128 in[1];
+ __m128i out[4];
+ __m128 scale = _mm_set1_ps(S24_SCALE);
+ __m128 int_min = _mm_set1_ps(S24_MIN);
+ __m128 int_max = _mm_set1_ps(S24_MAX);
+
+ if (SPA_IS_ALIGNED(s, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), scale);
+ in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n]));
+ in[0] = _MM_CLAMP_PS(in[0], int_min, int_max);
+ out[0] = _mm_cvtps_epi32(in[0]);
+ out[0] = _mm_slli_epi32(out[0], 8);
+ out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
+ out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
+ out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
+
+ d[0*n_channels] = _mm_cvtsi128_si32(out[0]);
+ d[1*n_channels] = _mm_cvtsi128_si32(out[1]);
+ d[2*n_channels] = _mm_cvtsi128_si32(out[2]);
+ d[3*n_channels] = _mm_cvtsi128_si32(out[3]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ in[0] = _mm_load_ss(&s[n]);
+ in[0] = _mm_mul_ss(in[0], scale);
+ in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n]));
+ in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+ *d = _mm_cvtss_si32(in[0]) << 8;
+ d += n_channels;
+ }
+}
+
+void
+conv_f32d_to_s32_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int32_t *d = dst[0];
+ uint32_t i, k, chunk, n_channels = conv->n_channels;
+ float *noise = conv->noise;
+
+ convert_update_noise(conv, noise, SPA_MIN(n_samples, conv->noise_size));
+
+ for(i = 0; i < n_channels; i++) {
+ const float *s = src[i];
+ for(k = 0; k < n_samples; k += chunk) {
+ chunk = SPA_MIN(n_samples - k, conv->noise_size);
+ conv_f32d_to_s32_1s_noise_sse2(conv, &d[i + k*n_channels],
+ &s[k], noise, n_channels, chunk);
+ }
+ }
+}
+
+static void
+conv_interleave_32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int32_t *s0 = src[0];
+ int32_t *d = dst;
+ uint32_t n, unrolled;
+ __m128i out[4];
+
+ if (SPA_IS_ALIGNED(s0, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ out[0] = _mm_load_si128((__m128i*)&s0[n]);
+ out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
+ out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
+ out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
+
+ d[0*n_channels] = _mm_cvtsi128_si32(out[0]);
+ d[1*n_channels] = _mm_cvtsi128_si32(out[1]);
+ d[2*n_channels] = _mm_cvtsi128_si32(out[2]);
+ d[3*n_channels] = _mm_cvtsi128_si32(out[3]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ *d = s0[n];
+ d += n_channels;
+ }
+}
+static void
+conv_interleave_32_4s_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], *s1 = src[1], *s2 = src[2], *s3 = src[3];
+ float *d = dst;
+ uint32_t n, unrolled;
+ __m128 out[4];
+
+ if (SPA_IS_ALIGNED(s0, 16) &&
+ SPA_IS_ALIGNED(s1, 16) &&
+ SPA_IS_ALIGNED(s2, 16) &&
+ SPA_IS_ALIGNED(s3, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ out[0] = _mm_load_ps(&s0[n]);
+ out[1] = _mm_load_ps(&s1[n]);
+ out[2] = _mm_load_ps(&s2[n]);
+ out[3] = _mm_load_ps(&s3[n]);
+
+ _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]);
+
+ _mm_storeu_ps((d + 0*n_channels), out[0]);
+ _mm_storeu_ps((d + 1*n_channels), out[1]);
+ _mm_storeu_ps((d + 2*n_channels), out[2]);
+ _mm_storeu_ps((d + 3*n_channels), out[3]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out[0] = _mm_setr_ps(s0[n], s1[n], s2[n], s3[n]);
+ _mm_storeu_ps(d, out[0]);
+ d += n_channels;
+ }
+}
+
+void
+conv_32d_to_32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int32_t *d = dst[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_interleave_32_4s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ 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)
+{
+ const int32_t *s0 = src[0];
+ int32_t *d = dst;
+ uint32_t n, unrolled;
+ __m128i out[4];
+
+ if (SPA_IS_ALIGNED(s0, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ out[0] = _mm_load_si128((__m128i*)&s0[n]);
+ out[0] = _MM_BSWAP_EPI32(out[0]);
+ out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1));
+ out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2));
+ out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3));
+
+ d[0*n_channels] = _mm_cvtsi128_si32(out[0]);
+ d[1*n_channels] = _mm_cvtsi128_si32(out[1]);
+ d[2*n_channels] = _mm_cvtsi128_si32(out[2]);
+ d[3*n_channels] = _mm_cvtsi128_si32(out[3]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ *d = bswap_32(s0[n]);
+ d += n_channels;
+ }
+}
+static void
+conv_interleave_32s_4s_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], *s1 = src[1], *s2 = src[2], *s3 = src[3];
+ float *d = dst;
+ uint32_t n, unrolled;
+ __m128 out[4];
+
+ if (SPA_IS_ALIGNED(s0, 16) &&
+ SPA_IS_ALIGNED(s1, 16) &&
+ SPA_IS_ALIGNED(s2, 16) &&
+ SPA_IS_ALIGNED(s3, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ out[0] = _mm_load_ps(&s0[n]);
+ out[1] = _mm_load_ps(&s1[n]);
+ out[2] = _mm_load_ps(&s2[n]);
+ out[3] = _mm_load_ps(&s3[n]);
+
+ _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]);
+
+ out[0] = (__m128) _MM_BSWAP_EPI32((__m128i)out[0]);
+ out[1] = (__m128) _MM_BSWAP_EPI32((__m128i)out[1]);
+ out[2] = (__m128) _MM_BSWAP_EPI32((__m128i)out[2]);
+ out[3] = (__m128) _MM_BSWAP_EPI32((__m128i)out[3]);
+
+ _mm_storeu_ps(&d[0*n_channels], out[0]);
+ _mm_storeu_ps(&d[1*n_channels], out[1]);
+ _mm_storeu_ps(&d[2*n_channels], out[2]);
+ _mm_storeu_ps(&d[3*n_channels], out[3]);
+ d += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out[0] = _mm_setr_ps(s0[n], s1[n], s2[n], s3[n]);
+ out[0] = (__m128) _MM_BSWAP_EPI32((__m128i)out[0]);
+ _mm_storeu_ps(d, out[0]);
+ d += n_channels;
+ }
+}
+
+void
+conv_32d_to_32s_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int32_t *d = dst[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_interleave_32s_4s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_interleave_32s_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+}
+
+static void
+conv_deinterleave_32_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s = src;
+ float *d0 = dst[0];
+ uint32_t n, unrolled;
+ __m128 out;
+
+ if (SPA_IS_ALIGNED(d0, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ out = _mm_setr_ps(s[0*n_channels],
+ s[1*n_channels],
+ s[2*n_channels],
+ s[3*n_channels]);
+ _mm_store_ps(&d0[n], out);
+ s += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ d0[n] = *s;
+ s += n_channels;
+ }
+}
+
+static void
+conv_deinterleave_32_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s = src;
+ float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
+ uint32_t n, unrolled;
+ __m128 out[4];
+
+ if (SPA_IS_ALIGNED(d0, 16) &&
+ SPA_IS_ALIGNED(d1, 16) &&
+ SPA_IS_ALIGNED(d2, 16) &&
+ SPA_IS_ALIGNED(d3, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ out[0] = _mm_loadu_ps(&s[0 * n_channels]);
+ out[1] = _mm_loadu_ps(&s[1 * n_channels]);
+ out[2] = _mm_loadu_ps(&s[2 * n_channels]);
+ out[3] = _mm_loadu_ps(&s[3 * n_channels]);
+
+ _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]);
+
+ _mm_store_ps(&d0[n], out[0]);
+ _mm_store_ps(&d1[n], out[1]);
+ _mm_store_ps(&d2[n], out[2]);
+ _mm_store_ps(&d3[n], out[3]);
+ s += 4 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ d0[n] = s[0];
+ d1[n] = s[1];
+ d2[n] = s[2];
+ d3[n] = s[3];
+ s += n_channels;
+ }
+}
+
+void
+conv_32_to_32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const float *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_deinterleave_32_4s_sse2(conv, &dst[i], &s[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_deinterleave_32_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples);
+}
+
+static void
+conv_deinterleave_32s_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s = src;
+ float *d0 = dst[0];
+ uint32_t n, unrolled;
+ __m128 out;
+
+ if (SPA_IS_ALIGNED(d0, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ out = _mm_setr_ps(s[0*n_channels],
+ s[1*n_channels],
+ s[2*n_channels],
+ s[3*n_channels]);
+ out = (__m128) _MM_BSWAP_EPI32((__m128i)out);
+ _mm_store_ps(&d0[n], out);
+ s += 4*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ d0[n] = bswap_32(*s);
+ s += n_channels;
+ }
+}
+
+static void
+conv_deinterleave_32s_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s = src;
+ float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
+ uint32_t n, unrolled;
+ __m128 out[4];
+
+ if (SPA_IS_ALIGNED(d0, 16) &&
+ SPA_IS_ALIGNED(d1, 16) &&
+ SPA_IS_ALIGNED(d2, 16) &&
+ SPA_IS_ALIGNED(d3, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ out[0] = _mm_loadu_ps(&s[0 * n_channels]);
+ out[1] = _mm_loadu_ps(&s[1 * n_channels]);
+ out[2] = _mm_loadu_ps(&s[2 * n_channels]);
+ out[3] = _mm_loadu_ps(&s[3 * n_channels]);
+
+ _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]);
+
+ out[0] = (__m128) _MM_BSWAP_EPI32((__m128i)out[0]);
+ out[1] = (__m128) _MM_BSWAP_EPI32((__m128i)out[1]);
+ out[2] = (__m128) _MM_BSWAP_EPI32((__m128i)out[2]);
+ out[3] = (__m128) _MM_BSWAP_EPI32((__m128i)out[3]);
+
+ _mm_store_ps(&d0[n], out[0]);
+ _mm_store_ps(&d1[n], out[1]);
+ _mm_store_ps(&d2[n], out[2]);
+ _mm_store_ps(&d3[n], out[3]);
+ s += 4 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ d0[n] = bswap_32(s[0]);
+ d1[n] = bswap_32(s[1]);
+ d2[n] = bswap_32(s[2]);
+ d3[n] = bswap_32(s[3]);
+ s += n_channels;
+ }
+}
+
+void
+conv_32s_to_32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const float *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_deinterleave_32s_4s_sse2(conv, &dst[i], &s[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_deinterleave_32s_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples);
+}
+
+static void
+conv_f32_to_s16_1_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
+ uint32_t n_samples)
+{
+ const float *s = src;
+ int16_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(s, 16))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 8) {
+ in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), int_scale);
+ in[1] = _mm_mul_ps(_mm_load_ps(&s[n+4]), int_scale);
+ out[0] = _mm_cvtps_epi32(in[0]);
+ out[1] = _mm_cvtps_epi32(in[1]);
+ out[0] = _mm_packs_epi32(out[0], out[1]);
+ _mm_storeu_si128((__m128i*)(d+0), out[0]);
+ d += 8;
+ }
+ for(; n < n_samples; n++) {
+ in[0] = _mm_mul_ss(_mm_load_ss(&s[n]), int_scale);
+ in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+ *d++ = _mm_cvtss_si32(in[0]);
+ }
+}
+
+void
+conv_f32d_to_s16d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ uint32_t i, n_channels = conv->n_channels;
+ for(i = 0; i < n_channels; i++)
+ conv_f32_to_s16_1_sse2(conv, dst[i], src[i], n_samples);
+}
+
+void
+conv_f32_to_s16_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ conv_f32_to_s16_1_sse2(conv, dst[0], src[0], n_samples * conv->n_channels);
+}
+
+static void
+conv_f32d_to_s16_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];
+ int16_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]);
+
+ 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 = _mm_cvtss_si32(in[0]);
+ d += n_channels;
+ }
+}
+
+static void
+conv_f32d_to_s16_2s_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], *s1 = src[1];
+ int16_t *d = dst;
+ uint32_t n, unrolled;
+ __m128 in[2];
+ __m128i out[4], t[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) &&
+ SPA_IS_ALIGNED(s1, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale);
+ in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), int_scale);
+
+ t[0] = _mm_cvtps_epi32(in[0]);
+ t[1] = _mm_cvtps_epi32(in[1]);
+
+ t[0] = _mm_packs_epi32(t[0], t[0]);
+ t[1] = _mm_packs_epi32(t[1], t[1]);
+
+ out[0] = _mm_unpacklo_epi16(t[0], t[1]);
+ 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));
+
+ *((int32_t*)(d + 0*n_channels)) = _mm_cvtsi128_si32(out[0]);
+ *((int32_t*)(d + 1*n_channels)) = _mm_cvtsi128_si32(out[1]);
+ *((int32_t*)(d + 2*n_channels)) = _mm_cvtsi128_si32(out[2]);
+ *((int32_t*)(d + 3*n_channels)) = _mm_cvtsi128_si32(out[3]);
+ d += 4*n_channels;
+ }
+ 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] = _mm_cvtss_si32(in[0]);
+ d[1] = _mm_cvtss_si32(in[1]);
+ d += n_channels;
+ }
+}
+
+static void
+conv_f32d_to_s16_4s_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], *s1 = src[1], *s2 = src[2], *s3 = src[3];
+ int16_t *d = dst;
+ uint32_t n, unrolled;
+ __m128 in[4];
+ __m128i out[4], t[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) &&
+ SPA_IS_ALIGNED(s2, 16) &&
+ SPA_IS_ALIGNED(s3, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale);
+ in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), int_scale);
+ in[2] = _mm_mul_ps(_mm_load_ps(&s2[n]), int_scale);
+ in[3] = _mm_mul_ps(_mm_load_ps(&s3[n]), int_scale);
+
+ t[0] = _mm_cvtps_epi32(in[0]);
+ t[1] = _mm_cvtps_epi32(in[1]);
+ t[2] = _mm_cvtps_epi32(in[2]);
+ t[3] = _mm_cvtps_epi32(in[3]);
+
+ t[0] = _mm_packs_epi32(t[0], t[2]);
+ t[1] = _mm_packs_epi32(t[1], t[3]);
+
+ out[0] = _mm_unpacklo_epi16(t[0], t[1]);
+ out[1] = _mm_unpackhi_epi16(t[0], t[1]);
+ out[2] = _mm_unpacklo_epi32(out[0], out[1]);
+ out[3] = _mm_unpackhi_epi32(out[0], out[1]);
+
+ _mm_storel_pi((__m64*)(d + 0*n_channels), (__m128)out[2]);
+ _mm_storeh_pi((__m64*)(d + 1*n_channels), (__m128)out[2]);
+ _mm_storel_pi((__m64*)(d + 2*n_channels), (__m128)out[3]);
+ _mm_storeh_pi((__m64*)(d + 3*n_channels), (__m128)out[3]);
+
+ d += 4*n_channels;
+ }
+ 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[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale);
+ in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale);
+ in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+ in[1] = _MM_CLAMP_SS(in[1], int_min, int_max);
+ in[2] = _MM_CLAMP_SS(in[2], int_min, int_max);
+ in[3] = _MM_CLAMP_SS(in[3], int_min, int_max);
+ d[0] = _mm_cvtss_si32(in[0]);
+ d[1] = _mm_cvtss_si32(in[1]);
+ d[2] = _mm_cvtss_si32(in[2]);
+ d[3] = _mm_cvtss_si32(in[3]);
+ d += n_channels;
+ }
+}
+
+void
+conv_f32d_to_s16_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int16_t *d = dst[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_f32d_to_s16_4s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i + 1 < n_channels; i += 2)
+ conv_f32d_to_s16_2s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_f32d_to_s16_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples);
+}
+
+static void
+conv_f32d_to_s16_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
+ const float *noise, uint32_t n_channels, uint32_t n_samples)
+{
+ const float *s0 = src;
+ int16_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);
+ in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n]));
+ in[1] = _mm_add_ps(in[1], _mm_load_ps(&noise[n+4]));
+ out[0] = _mm_cvtps_epi32(in[0]);
+ out[1] = _mm_cvtps_epi32(in[1]);
+ out[0] = _mm_packs_epi32(out[0], out[1]);
+
+ d[0*n_channels] = _mm_extract_epi16(out[0], 0);
+ d[1*n_channels] = _mm_extract_epi16(out[0], 1);
+ d[2*n_channels] = _mm_extract_epi16(out[0], 2);
+ d[3*n_channels] = _mm_extract_epi16(out[0], 3);
+ d[4*n_channels] = _mm_extract_epi16(out[0], 4);
+ d[5*n_channels] = _mm_extract_epi16(out[0], 5);
+ d[6*n_channels] = _mm_extract_epi16(out[0], 6);
+ d[7*n_channels] = _mm_extract_epi16(out[0], 7);
+ d += 8*n_channels;
+ }
+ for(; n < n_samples; n++) {
+ in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale);
+ in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n]));
+ in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+ *d = _mm_cvtss_si32(in[0]);
+ d += n_channels;
+ }
+}
+
+void
+conv_f32d_to_s16_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ int16_t *d = dst[0];
+ uint32_t i, k, chunk, n_channels = conv->n_channels;
+ float *noise = conv->noise;
+
+ convert_update_noise(conv, noise, SPA_MIN(n_samples, conv->noise_size));
+
+ for(i = 0; i < n_channels; i++) {
+ const float *s = src[i];
+ for(k = 0; k < n_samples; k += chunk) {
+ chunk = SPA_MIN(n_samples - k, conv->noise_size);
+ conv_f32d_to_s16_1s_noise_sse2(conv, &d[i + k*n_channels],
+ &s[k], noise, n_channels, chunk);
+ }
+ }
+}
+
+static void
+conv_f32_to_s16_1_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src,
+ const float *noise, uint32_t n_samples)
+{
+ const float *s = src;
+ int16_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(s, 16))
+ unrolled = n_samples & ~7;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 8) {
+ in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), int_scale);
+ in[1] = _mm_mul_ps(_mm_load_ps(&s[n+4]), int_scale);
+ in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n]));
+ in[1] = _mm_add_ps(in[1], _mm_load_ps(&noise[n+4]));
+ out[0] = _mm_cvtps_epi32(in[0]);
+ out[1] = _mm_cvtps_epi32(in[1]);
+ out[0] = _mm_packs_epi32(out[0], out[1]);
+ _mm_storeu_si128((__m128i*)(&d[n]), out[0]);
+ }
+ for(; n < n_samples; n++) {
+ in[0] = _mm_mul_ss(_mm_load_ss(&s[n]), int_scale);
+ in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n]));
+ in[0] = _MM_CLAMP_SS(in[0], int_min, int_max);
+ d[n] = _mm_cvtss_si32(in[0]);
+ }
+}
+
+void
+conv_f32d_to_s16d_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ uint32_t i, k, chunk, n_channels = conv->n_channels;
+ float *noise = conv->noise;
+
+ convert_update_noise(conv, noise, SPA_MIN(n_samples, conv->noise_size));
+
+ for(i = 0; i < n_channels; i++) {
+ const float *s = src[i];
+ int16_t *d = dst[i];
+ for(k = 0; k < n_samples; k += chunk) {
+ chunk = SPA_MIN(n_samples - k, conv->noise_size);
+ conv_f32_to_s16_1_noise_sse2(conv, &d[k], &s[k], noise, chunk);
+ }
+ }
+}
+
+void
+conv_f32d_to_s16_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];
+ int16_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]);
+
+ _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] = _mm_cvtss_si32(in[0]);
+ d[1] = _mm_cvtss_si32(in[1]);
+ d += 2;
+ }
+}
diff --git a/spa/plugins/audioconvert/fmt-ops-sse41.c b/spa/plugins/audioconvert/fmt-ops-sse41.c
new file mode 100644
index 0000000..042294d
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops-sse41.c
@@ -0,0 +1,86 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "fmt-ops.h"
+
+#include <smmintrin.h>
+
+static void
+conv_s24_to_f32d_1s_sse41(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int24_t *s = src;
+ float *d0 = dst[0];
+ uint32_t n, unrolled;
+ __m128i in = _mm_setzero_si128();
+ __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE);
+
+ if (SPA_IS_ALIGNED(d0, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in = _mm_insert_epi32(in, *((uint32_t*)&s[0 * n_channels]), 0);
+ in = _mm_insert_epi32(in, *((uint32_t*)&s[1 * n_channels]), 1);
+ in = _mm_insert_epi32(in, *((uint32_t*)&s[2 * n_channels]), 2);
+ in = _mm_insert_epi32(in, *((uint32_t*)&s[3 * n_channels]), 3);
+ in = _mm_slli_epi32(in, 8);
+ in = _mm_srai_epi32(in, 8);
+ out = _mm_cvtepi32_ps(in);
+ out = _mm_mul_ps(out, factor);
+ _mm_store_ps(&d0[n], out);
+ s += 4 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+ out = _mm_mul_ss(out, factor);
+ _mm_store_ss(&d0[n], out);
+ s += n_channels;
+ }
+}
+
+extern void conv_s24_to_f32d_2s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples);
+extern void conv_s24_to_f32d_4s_ssse3(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples);
+
+void
+conv_s24_to_f32d_sse41(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int8_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+#if defined (HAVE_SSSE3)
+ for(; i + 3 < n_channels; i += 4)
+ conv_s24_to_f32d_4s_ssse3(conv, &dst[i], &s[3*i], n_channels, n_samples);
+#endif
+#if defined (HAVE_SSE2)
+ for(; i + 1 < n_channels; i += 2)
+ conv_s24_to_f32d_2s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples);
+#endif
+ for(; i < n_channels; i++)
+ conv_s24_to_f32d_1s_sse41(conv, &dst[i], &s[3*i], n_channels, n_samples);
+}
diff --git a/spa/plugins/audioconvert/fmt-ops-ssse3.c b/spa/plugins/audioconvert/fmt-ops-ssse3.c
new file mode 100644
index 0000000..c35c7c9
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops-ssse3.c
@@ -0,0 +1,111 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "fmt-ops.h"
+
+#include <tmmintrin.h>
+
+static void
+conv_s24_to_f32d_4s_ssse3(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples)
+{
+ const int24_t *s = src;
+ float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3];
+ uint32_t n, unrolled;
+ __m128i in[4];
+ __m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE);
+ const __m128i mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11);
+ //const __m128i mask = _mm_set_epi8(15, 14, 13, -1, 12, 11, 10, -1, 9, 8, 7, -1, 6, 5, 4, -1);
+
+ if (SPA_IS_ALIGNED(d0, 16) &&
+ SPA_IS_ALIGNED(d1, 16) &&
+ SPA_IS_ALIGNED(d2, 16) &&
+ SPA_IS_ALIGNED(d3, 16))
+ unrolled = n_samples & ~3;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 4) {
+ in[0] = _mm_loadu_si128((__m128i*)(s + 0*n_channels));
+ in[1] = _mm_loadu_si128((__m128i*)(s + 1*n_channels));
+ in[2] = _mm_loadu_si128((__m128i*)(s + 2*n_channels));
+ in[3] = _mm_loadu_si128((__m128i*)(s + 3*n_channels));
+ in[0] = _mm_shuffle_epi8(in[0], mask);
+ in[1] = _mm_shuffle_epi8(in[1], mask);
+ in[2] = _mm_shuffle_epi8(in[2], mask);
+ in[3] = _mm_shuffle_epi8(in[3], mask);
+ in[0] = _mm_srai_epi32(in[0], 8);
+ in[1] = _mm_srai_epi32(in[1], 8);
+ in[2] = _mm_srai_epi32(in[2], 8);
+ in[3] = _mm_srai_epi32(in[3], 8);
+ out[0] = _mm_cvtepi32_ps(in[0]);
+ out[1] = _mm_cvtepi32_ps(in[1]);
+ out[2] = _mm_cvtepi32_ps(in[2]);
+ out[3] = _mm_cvtepi32_ps(in[3]);
+ out[0] = _mm_mul_ps(out[0], factor);
+ out[1] = _mm_mul_ps(out[1], factor);
+ out[2] = _mm_mul_ps(out[2], factor);
+ out[3] = _mm_mul_ps(out[3], factor);
+
+ _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]);
+
+ _mm_store_ps(&d0[n], out[0]);
+ _mm_store_ps(&d1[n], out[1]);
+ _mm_store_ps(&d2[n], out[2]);
+ _mm_store_ps(&d3[n], out[3]);
+ s += 4 * n_channels;
+ }
+ for(; n < n_samples; n++) {
+ out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s));
+ out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1)));
+ out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2)));
+ out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3)));
+ out[0] = _mm_mul_ss(out[0], factor);
+ out[1] = _mm_mul_ss(out[1], factor);
+ out[2] = _mm_mul_ss(out[2], factor);
+ out[3] = _mm_mul_ss(out[3], factor);
+ _mm_store_ss(&d0[n], out[0]);
+ _mm_store_ss(&d1[n], out[1]);
+ _mm_store_ss(&d2[n], out[2]);
+ _mm_store_ss(&d3[n], out[3]);
+ s += n_channels;
+ }
+}
+
+void
+conv_s24_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src,
+ uint32_t n_channels, uint32_t n_samples);
+
+void
+conv_s24_to_f32d_ssse3(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples)
+{
+ const int8_t *s = src[0];
+ uint32_t i = 0, n_channels = conv->n_channels;
+
+ for(; i + 3 < n_channels; i += 4)
+ conv_s24_to_f32d_4s_ssse3(conv, &dst[i], &s[3*i], n_channels, n_samples);
+ for(; i < n_channels; i++)
+ conv_s24_to_f32d_1s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples);
+}
diff --git a/spa/plugins/audioconvert/fmt-ops.c b/spa/plugins/audioconvert/fmt-ops.c
new file mode 100644
index 0000000..4999a20
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops.c
@@ -0,0 +1,564 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/support/cpu.h>
+#include <spa/utils/defs.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "fmt-ops.h"
+
+#define NOISE_SIZE (1<<10)
+#define RANDOM_SIZE (16)
+
+typedef void (*convert_func_t) (struct convert *conv, void * SPA_RESTRICT dst[],
+ const void * SPA_RESTRICT src[], uint32_t n_samples);
+
+struct conv_info {
+ uint32_t src_fmt;
+ uint32_t dst_fmt;
+ uint32_t n_channels;
+
+ convert_func_t process;
+ const char *name;
+
+ uint32_t cpu_flags;
+#define CONV_NOISE (1<<0)
+#define CONV_SHAPE (1<<1)
+ uint32_t conv_flags;
+};
+
+#define MAKE(fmt1,fmt2,chan,func,...) \
+ { SPA_AUDIO_FORMAT_ ##fmt1, SPA_AUDIO_FORMAT_ ##fmt2, chan, func, #func , __VA_ARGS__ }
+
+static struct conv_info conv_table[] =
+{
+ /* to f32 */
+ MAKE(U8, F32, 0, conv_u8_to_f32_c),
+ MAKE(U8, F32, 0, conv_u8_to_f32_c),
+ MAKE(U8P, F32P, 0, conv_u8d_to_f32d_c),
+ MAKE(U8, F32P, 0, conv_u8_to_f32d_c),
+ MAKE(U8P, F32, 0, conv_u8d_to_f32_c),
+
+ MAKE(S8, F32, 0, conv_s8_to_f32_c),
+ MAKE(S8P, F32P, 0, conv_s8d_to_f32d_c),
+ MAKE(S8, F32P, 0, conv_s8_to_f32d_c),
+ MAKE(S8P, F32, 0, conv_s8d_to_f32_c),
+
+ MAKE(ALAW, F32P, 0, conv_alaw_to_f32d_c),
+ MAKE(ULAW, F32P, 0, conv_ulaw_to_f32d_c),
+
+ MAKE(U16, F32, 0, conv_u16_to_f32_c),
+ MAKE(U16, F32P, 0, conv_u16_to_f32d_c),
+
+ MAKE(S16, F32, 0, conv_s16_to_f32_c),
+ MAKE(S16P, F32P, 0, conv_s16d_to_f32d_c),
+#if defined (HAVE_NEON)
+ MAKE(S16, F32P, 2, conv_s16_to_f32d_2_neon, SPA_CPU_FLAG_NEON),
+ MAKE(S16, F32P, 0, conv_s16_to_f32d_neon, SPA_CPU_FLAG_NEON),
+#endif
+#if defined (HAVE_AVX2)
+ MAKE(S16, F32P, 2, conv_s16_to_f32d_2_avx2, SPA_CPU_FLAG_AVX2),
+ MAKE(S16, F32P, 0, conv_s16_to_f32d_avx2, SPA_CPU_FLAG_AVX2),
+#endif
+#if defined (HAVE_SSE2)
+ 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
+ MAKE(S16, F32P, 0, conv_s16_to_f32d_c),
+ MAKE(S16P, F32, 0, conv_s16d_to_f32_c),
+
+ MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_c),
+
+ MAKE(F32, F32, 0, conv_copy32_c),
+ MAKE(F32P, F32P, 0, conv_copy32d_c),
+#if defined (HAVE_SSE2)
+ MAKE(F32, F32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(F32, F32P, 0, conv_32_to_32d_c),
+#if defined (HAVE_SSE2)
+ MAKE(F32P, F32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(F32P, F32, 0, conv_32d_to_32_c),
+
+#if defined (HAVE_SSE2)
+ MAKE(F32_OE, F32P, 0, conv_32s_to_32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(F32_OE, F32P, 0, conv_32s_to_32d_c),
+#if defined (HAVE_SSE2)
+ MAKE(F32P, F32_OE, 0, conv_32d_to_32s_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(F32P, F32_OE, 0, conv_32d_to_32s_c),
+
+ MAKE(U32, F32, 0, conv_u32_to_f32_c),
+ MAKE(U32, F32P, 0, conv_u32_to_f32d_c),
+
+#if defined (HAVE_AVX2)
+ MAKE(S32, F32P, 0, conv_s32_to_f32d_avx2, SPA_CPU_FLAG_AVX2),
+#endif
+#if defined (HAVE_SSE2)
+ MAKE(S32, F32P, 0, conv_s32_to_f32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(S32, F32, 0, conv_s32_to_f32_c),
+ MAKE(S32P, F32P, 0, conv_s32d_to_f32d_c),
+ MAKE(S32, F32P, 0, conv_s32_to_f32d_c),
+ MAKE(S32P, F32, 0, conv_s32d_to_f32_c),
+
+ MAKE(S32_OE, F32P, 0, conv_s32s_to_f32d_c),
+
+ MAKE(U24, F32, 0, conv_u24_to_f32_c),
+ MAKE(U24, F32P, 0, conv_u24_to_f32d_c),
+
+ MAKE(S24, F32, 0, conv_s24_to_f32_c),
+ MAKE(S24P, F32P, 0, conv_s24d_to_f32d_c),
+#if defined (HAVE_AVX2)
+ MAKE(S24, F32P, 0, conv_s24_to_f32d_avx2, SPA_CPU_FLAG_AVX2),
+#endif
+#if defined (HAVE_SSSE3)
+// MAKE(S24, F32P, 0, conv_s24_to_f32d_ssse3, SPA_CPU_FLAG_SSSE3),
+#endif
+#if defined (HAVE_SSE41)
+ MAKE(S24, F32P, 0, conv_s24_to_f32d_sse41, SPA_CPU_FLAG_SSE41),
+#endif
+#if defined (HAVE_SSE2)
+ MAKE(S24, F32P, 0, conv_s24_to_f32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(S24, F32P, 0, conv_s24_to_f32d_c),
+ MAKE(S24P, F32, 0, conv_s24d_to_f32_c),
+
+ MAKE(S24_OE, F32P, 0, conv_s24s_to_f32d_c),
+
+ MAKE(U24_32, F32, 0, conv_u24_32_to_f32_c),
+ MAKE(U24_32, F32P, 0, conv_u24_32_to_f32d_c),
+
+ MAKE(S24_32, F32, 0, conv_s24_32_to_f32_c),
+ MAKE(S24_32P, F32P, 0, conv_s24_32d_to_f32d_c),
+ MAKE(S24_32, F32P, 0, conv_s24_32_to_f32d_c),
+ MAKE(S24_32P, F32, 0, conv_s24_32d_to_f32_c),
+
+ MAKE(S24_32_OE, F32P, 0, conv_s24_32s_to_f32d_c),
+
+ MAKE(F64, F32, 0, conv_f64_to_f32_c),
+ MAKE(F64P, F32P, 0, conv_f64d_to_f32d_c),
+ MAKE(F64, F32P, 0, conv_f64_to_f32d_c),
+ MAKE(F64P, F32, 0, conv_f64d_to_f32_c),
+
+ MAKE(F64_OE, F32P, 0, conv_f64s_to_f32d_c),
+
+ /* from f32 */
+ MAKE(F32, U8, 0, conv_f32_to_u8_c),
+ MAKE(F32P, U8P, 0, conv_f32d_to_u8d_shaped_c, 0, CONV_SHAPE),
+ MAKE(F32P, U8P, 0, conv_f32d_to_u8d_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, U8P, 0, conv_f32d_to_u8d_c),
+ MAKE(F32, U8P, 0, conv_f32_to_u8d_c),
+ MAKE(F32P, U8, 0, conv_f32d_to_u8_shaped_c, 0, CONV_SHAPE),
+ MAKE(F32P, U8, 0, conv_f32d_to_u8_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, U8, 0, conv_f32d_to_u8_c),
+
+ MAKE(F32, S8, 0, conv_f32_to_s8_c),
+ MAKE(F32P, S8P, 0, conv_f32d_to_s8d_shaped_c, 0, CONV_SHAPE),
+ MAKE(F32P, S8P, 0, conv_f32d_to_s8d_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S8P, 0, conv_f32d_to_s8d_c),
+ MAKE(F32, S8P, 0, conv_f32_to_s8d_c),
+ MAKE(F32P, S8, 0, conv_f32d_to_s8_shaped_c, 0, CONV_SHAPE),
+ MAKE(F32P, S8, 0, conv_f32d_to_s8_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S8, 0, conv_f32d_to_s8_c),
+
+ MAKE(F32P, ALAW, 0, conv_f32d_to_alaw_c),
+ MAKE(F32P, ULAW, 0, conv_f32d_to_ulaw_c),
+
+ MAKE(F32, U16, 0, conv_f32_to_u16_c),
+ MAKE(F32P, U16, 0, conv_f32d_to_u16_c),
+
+#if defined (HAVE_SSE2)
+ MAKE(F32, S16, 0, conv_f32_to_s16_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(F32, S16, 0, conv_f32_to_s16_c),
+
+ MAKE(F32P, S16P, 0, conv_f32d_to_s16d_shaped_c, 0, CONV_SHAPE),
+#if defined (HAVE_SSE2)
+ MAKE(F32P, S16P, 0, conv_f32d_to_s16d_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE),
+#endif
+ MAKE(F32P, S16P, 0, conv_f32d_to_s16d_noise_c, 0, CONV_NOISE),
+#if defined (HAVE_SSE2)
+ MAKE(F32P, S16P, 0, conv_f32d_to_s16d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(F32P, S16P, 0, conv_f32d_to_s16d_c),
+
+ MAKE(F32, S16P, 0, conv_f32_to_s16d_c),
+
+ MAKE(F32P, S16, 0, conv_f32d_to_s16_shaped_c, 0, CONV_SHAPE),
+#if defined (HAVE_SSE2)
+ MAKE(F32P, S16, 0, conv_f32d_to_s16_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE),
+#endif
+ MAKE(F32P, S16, 0, conv_f32d_to_s16_noise_c, 0, CONV_NOISE),
+#if defined (HAVE_NEON)
+ MAKE(F32P, S16, 0, conv_f32d_to_s16_neon, SPA_CPU_FLAG_NEON),
+#endif
+#if defined (HAVE_AVX2)
+ MAKE(F32P, S16, 4, conv_f32d_to_s16_4_avx2, SPA_CPU_FLAG_AVX2),
+ MAKE(F32P, S16, 2, conv_f32d_to_s16_2_avx2, SPA_CPU_FLAG_AVX2),
+ MAKE(F32P, S16, 0, conv_f32d_to_s16_avx2, SPA_CPU_FLAG_AVX2),
+#endif
+#if defined (HAVE_SSE2)
+ MAKE(F32P, S16, 2, conv_f32d_to_s16_2_sse2, SPA_CPU_FLAG_SSE2),
+ MAKE(F32P, S16, 0, conv_f32d_to_s16_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(F32P, S16, 0, conv_f32d_to_s16_c),
+
+ MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_shaped_c, 0, CONV_SHAPE),
+ MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_c),
+
+ MAKE(F32, U32, 0, conv_f32_to_u32_c),
+ MAKE(F32P, U32, 0, conv_f32d_to_u32_c),
+
+ MAKE(F32, S32, 0, conv_f32_to_s32_c),
+ MAKE(F32P, S32P, 0, conv_f32d_to_s32d_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S32P, 0, conv_f32d_to_s32d_c),
+ MAKE(F32, S32P, 0, conv_f32_to_s32d_c),
+
+#if defined (HAVE_SSE2)
+ MAKE(F32P, S32, 0, conv_f32d_to_s32_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE),
+#endif
+ MAKE(F32P, S32, 0, conv_f32d_to_s32_noise_c, 0, CONV_NOISE),
+
+#if defined (HAVE_AVX2)
+ MAKE(F32P, S32, 0, conv_f32d_to_s32_avx2, SPA_CPU_FLAG_AVX2),
+#endif
+#if defined (HAVE_SSE2)
+ MAKE(F32P, S32, 0, conv_f32d_to_s32_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(F32P, S32, 0, conv_f32d_to_s32_c),
+
+ MAKE(F32P, S32_OE, 0, conv_f32d_to_s32s_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S32_OE, 0, conv_f32d_to_s32s_c),
+
+ MAKE(F32, U24, 0, conv_f32_to_u24_c),
+ MAKE(F32P, U24, 0, conv_f32d_to_u24_c),
+
+ MAKE(F32, S24, 0, conv_f32_to_s24_c),
+ MAKE(F32P, S24P, 0, conv_f32d_to_s24d_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S24P, 0, conv_f32d_to_s24d_c),
+ MAKE(F32, S24P, 0, conv_f32_to_s24d_c),
+ MAKE(F32P, S24, 0, conv_f32d_to_s24_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S24, 0, conv_f32d_to_s24_c),
+
+ MAKE(F32P, S24_OE, 0, conv_f32d_to_s24s_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S24_OE, 0, conv_f32d_to_s24s_c),
+
+ MAKE(F32, U24_32, 0, conv_f32_to_u24_32_c),
+ MAKE(F32P, U24_32, 0, conv_f32d_to_u24_32_c),
+
+ MAKE(F32, S24_32, 0, conv_f32_to_s24_32_c),
+ MAKE(F32P, S24_32P, 0, conv_f32d_to_s24_32d_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S24_32P, 0, conv_f32d_to_s24_32d_c),
+ MAKE(F32, S24_32P, 0, conv_f32_to_s24_32d_c),
+ MAKE(F32P, S24_32, 0, conv_f32d_to_s24_32_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S24_32, 0, conv_f32d_to_s24_32_c),
+
+ MAKE(F32P, S24_32_OE, 0, conv_f32d_to_s24_32s_noise_c, 0, CONV_NOISE),
+ MAKE(F32P, S24_32_OE, 0, conv_f32d_to_s24_32s_c),
+
+ MAKE(F32, F64, 0, conv_f32_to_f64_c),
+ MAKE(F32P, F64P, 0, conv_f32d_to_f64d_c),
+ MAKE(F32, F64P, 0, conv_f32_to_f64d_c),
+ MAKE(F32P, F64, 0, conv_f32d_to_f64_c),
+
+ MAKE(F32P, F64_OE, 0, conv_f32d_to_f64s_c),
+
+ /* u8 */
+ MAKE(U8, U8, 0, conv_copy8_c),
+ MAKE(U8P, U8P, 0, conv_copy8d_c),
+ MAKE(U8, U8P, 0, conv_8_to_8d_c),
+ MAKE(U8P, U8, 0, conv_8d_to_8_c),
+
+ /* s8 */
+ MAKE(S8, S8, 0, conv_copy8_c),
+ MAKE(S8P, S8P, 0, conv_copy8d_c),
+ MAKE(S8, S8P, 0, conv_8_to_8d_c),
+ MAKE(S8P, S8, 0, conv_8d_to_8_c),
+
+ /* alaw */
+ MAKE(ALAW, ALAW, 0, conv_copy8_c),
+ /* ulaw */
+ MAKE(ULAW, ULAW, 0, conv_copy8_c),
+
+ /* s16 */
+ MAKE(S16, S16, 0, conv_copy16_c),
+ MAKE(S16P, S16P, 0, conv_copy16d_c),
+ MAKE(S16, S16P, 0, conv_16_to_16d_c),
+ MAKE(S16P, S16, 0, conv_16d_to_16_c),
+
+ /* s32 */
+ MAKE(S32, S32, 0, conv_copy32_c),
+ MAKE(S32P, S32P, 0, conv_copy32d_c),
+#if defined (HAVE_SSE2)
+ MAKE(S32, S32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(S32, S32P, 0, conv_32_to_32d_c),
+#if defined (HAVE_SSE2)
+ MAKE(S32P, S32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(S32P, S32, 0, conv_32d_to_32_c),
+
+ /* s24 */
+ MAKE(S24, S24, 0, conv_copy24_c),
+ MAKE(S24P, S24P, 0, conv_copy24d_c),
+ MAKE(S24, S24P, 0, conv_24_to_24d_c),
+ MAKE(S24P, S24, 0, conv_24d_to_24_c),
+
+ /* s24_32 */
+ MAKE(S24_32, S24_32, 0, conv_copy32_c),
+ MAKE(S24_32P, S24_32P, 0, conv_copy32d_c),
+#if defined (HAVE_SSE2)
+ MAKE(S24_32, S24_32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(S24_32, S24_32P, 0, conv_32_to_32d_c),
+#if defined (HAVE_SSE2)
+ MAKE(S24_32P, S24_32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(S24_32P, S24_32, 0, conv_32d_to_32_c),
+
+ /* F64 */
+ MAKE(F64, F64, 0, conv_copy64_c),
+ MAKE(F64P, F64P, 0, conv_copy64d_c),
+ MAKE(F64, F64P, 0, conv_64_to_64d_c),
+ MAKE(F64P, F64, 0, conv_64d_to_64_c),
+};
+#undef MAKE
+
+#define MATCH_CHAN(a,b) ((a) == 0 || (a) == (b))
+#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a)
+#define MATCH_DITHER(a,b) ((a) == 0 || ((a) & (b)) == a)
+
+static const struct conv_info *find_conv_info(uint32_t src_fmt, uint32_t dst_fmt,
+ uint32_t n_channels, uint32_t cpu_flags, uint32_t conv_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(conv_table, c) {
+ if (c->src_fmt == src_fmt &&
+ c->dst_fmt == dst_fmt &&
+ MATCH_CHAN(c->n_channels, n_channels) &&
+ MATCH_CPU_FLAGS(c->cpu_flags, cpu_flags) &&
+ MATCH_DITHER(c->conv_flags, conv_flags))
+ return c;
+ }
+ return NULL;
+}
+
+typedef void (*noise_func_t) (struct convert *conv, float * noise, uint32_t n_samples);
+
+struct noise_info {
+ uint32_t method;
+
+ noise_func_t noise;
+ const char *name;
+
+ uint32_t cpu_flags;
+};
+
+#define MAKE(method,func,...) \
+ { NOISE_METHOD_ ##method, func, #func , __VA_ARGS__ }
+
+static struct noise_info noise_table[] =
+{
+#if defined (HAVE_SSE2)
+ MAKE(RECTANGULAR, conv_noise_rect_sse2, SPA_CPU_FLAG_SSE2),
+ MAKE(TRIANGULAR, conv_noise_tri_sse2, SPA_CPU_FLAG_SSE2),
+ MAKE(TRIANGULAR_HF, conv_noise_tri_hf_sse2, SPA_CPU_FLAG_SSE2),
+#endif
+ MAKE(NONE, conv_noise_none_c),
+ MAKE(RECTANGULAR, conv_noise_rect_c),
+ MAKE(TRIANGULAR, conv_noise_tri_c),
+ MAKE(TRIANGULAR_HF, conv_noise_tri_hf_c),
+ MAKE(PATTERN, conv_noise_pattern_c),
+};
+#undef MAKE
+
+static const struct noise_info *find_noise_info(uint32_t method,
+ uint32_t cpu_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(noise_table, t) {
+ if (t->method == method &&
+ MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+ return t;
+ }
+ return NULL;
+}
+
+static void impl_convert_free(struct convert *conv)
+{
+ conv->process = NULL;
+ free(conv->data);
+ conv->data = NULL;
+}
+
+static bool need_dither(uint32_t format)
+{
+ switch (format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_U8P:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_S8P:
+ case SPA_AUDIO_FORMAT_ULAW:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_S16P:
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ return true;
+ }
+ return false;
+}
+
+/* filters based on F-weighted curves
+ * from 'Psychoacoustically Optimal Noise Shaping' (**)
+ * this filter is the "F-Weighted" noise filter described by Wannamaker
+ * It is designed to produce minimum audibility: */
+static const float wan3[] = { /* Table 3; 3 Coefficients */
+ 1.623f, -0.982f, 0.109f
+};
+/* Noise shaping coefficients from[1], moves most power of the
+ * error noise into inaudible frequency ranges.
+ *
+ * [1]
+ * "Minimally Audible Noise Shaping", Stanley P. Lipshitz,
+ * John Vanderkooy, and Robert A. Wannamaker,
+ * J. Audio Eng. Soc., Vol. 39, No. 11, November 1991. */
+static const float lips44[] = { /* improved E-weighted (appendix: 5) */
+ 2.033f, -2.165f, 1.959f, -1.590f, 0.6149f
+};
+
+static const struct dither_info {
+ uint32_t method;
+ uint32_t noise_method;
+ uint32_t rate;
+ const float *ns;
+ uint32_t n_ns;
+} dither_info[] = {
+ { DITHER_METHOD_NONE, NOISE_METHOD_NONE, },
+ { DITHER_METHOD_RECTANGULAR, NOISE_METHOD_RECTANGULAR, },
+ { DITHER_METHOD_TRIANGULAR, NOISE_METHOD_TRIANGULAR, },
+ { DITHER_METHOD_TRIANGULAR_HF, NOISE_METHOD_TRIANGULAR_HF, },
+ { DITHER_METHOD_WANNAMAKER_3, NOISE_METHOD_TRIANGULAR_HF, 44100, wan3, SPA_N_ELEMENTS(wan3) },
+ { DITHER_METHOD_LIPSHITZ, NOISE_METHOD_TRIANGULAR, 44100, lips44, SPA_N_ELEMENTS(lips44) }
+};
+
+static const struct dither_info *find_dither_info(uint32_t method, uint32_t rate)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(dither_info, di) {
+ if (di->method != method)
+ continue;
+ /* don't use shaped for too low rates, it moves the noise to
+ * audible ranges */
+ if (di->ns != NULL && rate < di->rate * 3 / 4)
+ return find_dither_info(DITHER_METHOD_TRIANGULAR_HF, rate);
+ return di;
+ }
+ return NULL;
+}
+
+int convert_init(struct convert *conv)
+{
+ const struct conv_info *info;
+ const struct dither_info *dinfo;
+ const struct noise_info *ninfo;
+ uint32_t i, conv_flags, data_size[3];
+
+ conv->scale = 1.0f / (float)(INT32_MAX);
+
+ /* disable dither if not needed */
+ if (!need_dither(conv->dst_fmt))
+ conv->method = DITHER_METHOD_NONE;
+
+ dinfo = find_dither_info(conv->method, conv->rate);
+ if (dinfo == NULL)
+ return -EINVAL;
+
+ conv->noise_method = dinfo->noise_method;
+ if (conv->noise_bits > 0) {
+ switch (conv->noise_method) {
+ case NOISE_METHOD_NONE:
+ conv->noise_method = NOISE_METHOD_PATTERN;
+ conv->scale = -1.0f * (1 << (conv->noise_bits-1));
+ break;
+ case NOISE_METHOD_RECTANGULAR:
+ conv->noise_method = NOISE_METHOD_TRIANGULAR;
+ SPA_FALLTHROUGH;
+ case NOISE_METHOD_TRIANGULAR:
+ case NOISE_METHOD_TRIANGULAR_HF:
+ conv->scale *= (1 << (conv->noise_bits-1));
+ break;
+ }
+ }
+ if (conv->noise_method < NOISE_METHOD_TRIANGULAR)
+ conv->scale *= 0.5f;
+
+ conv_flags = 0;
+ if (conv->noise_method != NOISE_METHOD_NONE)
+ conv_flags |= CONV_NOISE;
+ if (dinfo->n_ns > 0) {
+ conv_flags |= CONV_SHAPE;
+ conv->n_ns = dinfo->n_ns;
+ conv->ns = dinfo->ns;
+ }
+
+ info = find_conv_info(conv->src_fmt, conv->dst_fmt, conv->n_channels,
+ conv->cpu_flags, conv_flags);
+ if (info == NULL)
+ return -ENOTSUP;
+
+ ninfo = find_noise_info(conv->noise_method, conv->cpu_flags);
+ if (ninfo == NULL)
+ return -ENOTSUP;
+
+ conv->noise_size = NOISE_SIZE;
+
+ data_size[0] = SPA_ROUND_UP(conv->noise_size * sizeof(float), FMT_OPS_MAX_ALIGN);
+ data_size[1] = SPA_ROUND_UP(RANDOM_SIZE * sizeof(uint32_t), FMT_OPS_MAX_ALIGN);
+ data_size[2] = SPA_ROUND_UP(RANDOM_SIZE * sizeof(int32_t), FMT_OPS_MAX_ALIGN);
+
+ conv->data = calloc(FMT_OPS_MAX_ALIGN +
+ data_size[0] + data_size[1] + data_size[2], 1);
+ if (conv->data == NULL)
+ return -errno;
+
+ conv->noise = SPA_PTR_ALIGN(conv->data, FMT_OPS_MAX_ALIGN, float);
+ conv->random = SPA_PTROFF(conv->noise, data_size[0], uint32_t);
+ conv->prev = SPA_PTROFF(conv->random, data_size[1], int32_t);
+
+ for (i = 0; i < RANDOM_SIZE; i++)
+ conv->random[i] = random();
+
+ conv->is_passthrough = conv->src_fmt == conv->dst_fmt;
+ conv->cpu_flags = info->cpu_flags;
+ conv->update_noise = ninfo->noise;
+ conv->process = info->process;
+ conv->free = impl_convert_free;
+ conv->func_name = info->name;
+
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/fmt-ops.h b/spa/plugins/audioconvert/fmt-ops.h
new file mode 100644
index 0000000..c2afaa2
--- /dev/null
+++ b/spa/plugins/audioconvert/fmt-ops.h
@@ -0,0 +1,488 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <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/string.h>
+
+#define f32_round(a) lrintf(a)
+
+#define ITOF(type,v,scale,offs) \
+ (((type)(v)) * (1.0f / (scale)) - (offs))
+#define FTOI(type,v,scale,offs,noise,min,max) \
+ (type)f32_round(SPA_CLAMPF((v) * (scale) + (offs) + (noise), min, max))
+
+#define FMT_OPS_MAX_ALIGN 32
+
+#define U8_MIN 0u
+#define U8_MAX 255u
+#define U8_SCALE 128.f
+#define U8_OFFS 128.f
+#define U8_TO_F32(v) ITOF(uint8_t, v, U8_SCALE, 1.0f)
+#define F32_TO_U8_D(v,d) FTOI(uint8_t, v, U8_SCALE, U8_OFFS, d, U8_MIN, U8_MAX)
+#define F32_TO_U8(v) F32_TO_U8_D(v, 0.0f)
+
+#define S8_MIN -128
+#define S8_MAX 127
+#define S8_SCALE 128.0f
+#define S8_TO_F32(v) ITOF(int8_t, v, S8_SCALE, 0.0f)
+#define F32_TO_S8_D(v,d) FTOI(int8_t, v, S8_SCALE, 0.0f, d, S8_MIN, S8_MAX)
+#define F32_TO_S8(v) F32_TO_S8_D(v, 0.0f);
+
+#define U16_MIN 0u
+#define U16_MAX 65535u
+#define U16_SCALE 32768.f
+#define U16_OFFS 32768.f
+#define U16_TO_F32(v) ITOF(uint16_t, v, U16_SCALE, 1.0f)
+#define U16S_TO_F32(v) U16_TO_F32(bswap_16(v))
+#define F32_TO_U16_D(v,d) FTOI(uint16_t, v, U16_SCALE, U16_OFFS, d, U16_MIN, U16_MAX)
+#define F32_TO_U16(v) F32_TO_U16_D(v, 0.0f);
+#define F32_TO_U16S_D(v,d) bswap_16(F32_TO_U16_D(v,d))
+#define F32_TO_U16S(v) bswap_16(F32_TO_U16(v))
+
+#define S16_MIN -32768
+#define S16_MAX 32767
+#define S16_SCALE 32768.0f
+#define S16_TO_F32(v) ITOF(int16_t, v, S16_SCALE, 0.0f)
+#define S16S_TO_F32(v) S16_TO_F32(bswap_16(v))
+#define F32_TO_S16_D(v,d) FTOI(int16_t, v, S16_SCALE, 0.0f, d, S16_MIN, S16_MAX)
+#define F32_TO_S16(v) F32_TO_S16_D(v, 0.0f)
+#define F32_TO_S16S_D(v,d) bswap_16(F32_TO_S16_D(v,d))
+#define F32_TO_S16S(v) bswap_16(F32_TO_S16(v))
+
+#define U24_MIN 0u
+#define U24_MAX 16777215u
+#define U24_SCALE 8388608.f
+#define U24_OFFS 8388608.f
+#define U24_TO_F32(v) ITOF(uint32_t, u24_to_u32(v), U24_SCALE, 1.0f)
+#define F32_TO_U24_D(v,d) u32_to_u24(FTOI(uint32_t, v, U24_SCALE, U24_OFFS, d, U24_MIN, U24_MAX))
+#define F32_TO_U24(v) F32_TO_U24_D(v, 0.0f)
+
+#define S24_MIN -8388608
+#define S24_MAX 8388607
+#define S24_SCALE 8388608.0f
+#define S24_TO_F32(v) ITOF(int32_t, s24_to_s32(v), S24_SCALE, 0.0f)
+#define S24S_TO_F32(v) S24_TO_F32(bswap_s24(v))
+#define F32_TO_S24_D(v,d) s32_to_s24(FTOI(int32_t, v, S24_SCALE, 0.0f, d, S24_MIN, S24_MAX))
+#define F32_TO_S24(v) F32_TO_S24_D(v, 0.0f)
+#define F32_TO_S24S(v) bswap_s24(F32_TO_S24(v))
+
+#define U24_32_TO_U32(v) (((uint32_t)(v)) << 8)
+
+#define U24_32_TO_F32(v) U32_TO_F32(U24_32_TO_U32(v))
+#define U24_32S_TO_F32(v) U24_32_TO_F32(bswap_32(v))
+#define F32_TO_U24_32_D(v,d) FTOI(uint32_t, v, U24_SCALE, U24_OFFS, d, U24_MIN, U24_MAX)
+#define F32_TO_U24_32(v) F32_TO_U24_32_D(v, 0.0f)
+#define F32_TO_U24_32S(v) bswap_32(F32_TO_U24_32(v))
+#define F32_TO_U24_32S_D(v,d) bswap_32(F32_TO_U24_32_D(v,d))
+
+#define U32_TO_U24_32(v) (((uint32_t)(v)) >> 8)
+
+#define U32_MIN 0u
+#define U32_MAX 4294967295u
+#define U32_SCALE 2147483648.f
+#define U32_OFFS 2147483648.f
+#define U32_TO_F32(v) ITOF(uint32_t, U32_TO_U24_32(v), U24_SCALE, 1.0f)
+#define F32_TO_U32(v) U24_32_TO_U32(F32_TO_U24_32(v))
+#define F32_TO_U32_D(v,d) U24_32_TO_U32(F32_TO_U24_32_D(v,d))
+
+#define S24_32_TO_S32(v) ((int32_t)(((uint32_t)(v)) << 8))
+
+#define S24_32_TO_F32(v) S32_TO_F32(S24_32_TO_S32(v))
+#define S24_32S_TO_F32(v) S24_32_TO_F32(bswap_32(v))
+#define F32_TO_S24_32_D(v,d) FTOI(int32_t, v, S24_SCALE, 0.0f, d, S24_MIN, S24_MAX)
+#define F32_TO_S24_32(v) F32_TO_S24_32_D(v, 0.0f)
+#define F32_TO_S24_32S(v) bswap_32(F32_TO_S24_32(v))
+#define F32_TO_S24_32S_D(v,d) bswap_32(F32_TO_S24_32_D(v,d))
+
+#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 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 F32_TO_S32S(v) bswap_32(F32_TO_S32(v))
+#define F32_TO_S32S_D(v,d) bswap_32(F32_TO_S32_D(v,d))
+
+typedef struct {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ uint8_t v3;
+ uint8_t v2;
+ uint8_t v1;
+#else
+ uint8_t v1;
+ uint8_t v2;
+ uint8_t v3;
+#endif
+} __attribute__ ((packed)) uint24_t;
+
+typedef struct {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ uint8_t v3;
+ uint8_t v2;
+ int8_t v1;
+#else
+ int8_t v1;
+ uint8_t v2;
+ uint8_t v3;
+#endif
+} __attribute__ ((packed)) int24_t;
+
+static inline uint32_t u24_to_u32(uint24_t src)
+{
+ return ((uint32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3;
+}
+
+#define U32_TO_U24(s) (uint24_t) { .v1 = (uint8_t)(((uint32_t)s) >> 16), \
+ .v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) }
+
+static inline uint24_t u32_to_u24(uint32_t src)
+{
+ return U32_TO_U24(src);
+}
+
+static inline int32_t s24_to_s32(int24_t src)
+{
+ return ((int32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3;
+}
+
+#define S32_TO_S24(s) (int24_t) { .v1 = (int8_t)(((int32_t)s) >> 16), \
+ .v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) }
+
+static inline int24_t s32_to_s24(int32_t src)
+{
+ return S32_TO_S24(src);
+}
+
+static inline uint24_t bswap_u24(uint24_t src)
+{
+ return (uint24_t) { .v1 = src.v3, .v2 = src.v2, .v3 = src.v1 };
+}
+static inline int24_t bswap_s24(int24_t src)
+{
+ return (int24_t) { .v1 = src.v3, .v2 = src.v2, .v3 = src.v1 };
+}
+
+#define F32_TO_F32S(v) \
+ bswap_32((union { uint32_t i; float f; }){ .f = (v) }.i)
+#define F32S_TO_F32(v) \
+ ((union { uint32_t i; float f; }){ .i = bswap_32(v) }.f)
+
+#define F64_TO_F64S(v) \
+ bswap_32((union { uint64_t i; double d; }){ .d = (v) }.i)
+#define F64S_TO_F64(v) \
+ ((union { uint64_t i; double d; }){ .i = bswap_32(v) }.d)
+
+#define NS_MAX 8
+#define NS_MASK (NS_MAX-1)
+
+struct shaper {
+ float e[NS_MAX * 2];
+ uint32_t idx;
+ float r;
+};
+
+struct convert {
+ uint32_t noise_bits;
+#define DITHER_METHOD_NONE 0
+#define DITHER_METHOD_RECTANGULAR 1
+#define DITHER_METHOD_TRIANGULAR 2
+#define DITHER_METHOD_TRIANGULAR_HF 3
+#define DITHER_METHOD_WANNAMAKER_3 4
+#define DITHER_METHOD_LIPSHITZ 5
+ uint32_t method;
+
+ uint32_t src_fmt;
+ uint32_t dst_fmt;
+ uint32_t n_channels;
+ uint32_t rate;
+ uint32_t cpu_flags;
+ const char *func_name;
+
+ unsigned int is_passthrough:1;
+
+ float scale;
+ uint32_t *random;
+ int32_t *prev;
+#define NOISE_METHOD_NONE 0
+#define NOISE_METHOD_RECTANGULAR 1
+#define NOISE_METHOD_TRIANGULAR 2
+#define NOISE_METHOD_TRIANGULAR_HF 3
+#define NOISE_METHOD_PATTERN 4
+ uint32_t noise_method;
+ float *noise;
+ uint32_t noise_size;
+ const float *ns;
+ uint32_t n_ns;
+ struct shaper shaper[64];
+
+ void (*update_noise) (struct convert *conv, float *noise, uint32_t n_samples);
+ void (*process) (struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[],
+ uint32_t n_samples);
+ void (*free) (struct convert *conv);
+
+ void *data;
+};
+
+int convert_init(struct convert *conv);
+
+static const struct dither_method_info {
+ uint32_t method;
+ const char *label;
+ const char *description;
+} dither_method_info[] = {
+ [DITHER_METHOD_NONE] = { DITHER_METHOD_NONE,
+ "none", "Disabled", },
+ [DITHER_METHOD_RECTANGULAR] = { DITHER_METHOD_RECTANGULAR,
+ "rectangular", "Rectangular dithering", },
+ [DITHER_METHOD_TRIANGULAR] = { DITHER_METHOD_TRIANGULAR,
+ "triangular", "Triangular dithering", },
+ [DITHER_METHOD_TRIANGULAR_HF] = { DITHER_METHOD_TRIANGULAR_HF,
+ "triangular-hf", "Sloped Triangular dithering", },
+ [DITHER_METHOD_WANNAMAKER_3] = { DITHER_METHOD_WANNAMAKER_3,
+ "wannamaker3", "Wannamaker 3 dithering", },
+ [DITHER_METHOD_LIPSHITZ] = { DITHER_METHOD_LIPSHITZ,
+ "shaped5", "Lipshitz 5 dithering", },
+};
+
+static inline uint32_t dither_method_from_label(const char *label)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(dither_method_info, i) {
+ if (spa_streq(i->label, label))
+ return i->method;
+ }
+ return DITHER_METHOD_NONE;
+}
+
+#define convert_update_noise(conv,...) (conv)->update_noise(conv, __VA_ARGS__)
+#define convert_process(conv,...) (conv)->process(conv, __VA_ARGS__)
+#define convert_free(conv) (conv)->free(conv)
+
+#define DEFINE_NOISE_FUNCTION(name,arch) \
+void conv_noise_##name##_##arch(struct convert *conv, float *noise, \
+ uint32_t n_samples)
+
+DEFINE_NOISE_FUNCTION(none, c);
+DEFINE_NOISE_FUNCTION(rect, c);
+DEFINE_NOISE_FUNCTION(tri, c);
+DEFINE_NOISE_FUNCTION(tri_hf, c);
+DEFINE_NOISE_FUNCTION(pattern, c);
+#if defined(HAVE_SSE2)
+DEFINE_NOISE_FUNCTION(rect, sse2);
+DEFINE_NOISE_FUNCTION(tri, sse2);
+DEFINE_NOISE_FUNCTION(tri_hf, sse2);
+#endif
+
+#undef DEFINE_NOISE_FUNCTION
+
+#define DEFINE_FUNCTION(name,arch) \
+void conv_##name##_##arch(struct convert *conv, void * SPA_RESTRICT dst[], \
+ const void * SPA_RESTRICT src[], uint32_t n_samples)
+
+DEFINE_FUNCTION(copy8d, c);
+DEFINE_FUNCTION(copy8, c);
+DEFINE_FUNCTION(copy16d, c);
+DEFINE_FUNCTION(copy16, c);
+DEFINE_FUNCTION(copy24d, c);
+DEFINE_FUNCTION(copy24, c);
+DEFINE_FUNCTION(copy32d, c);
+DEFINE_FUNCTION(copy32, c);
+DEFINE_FUNCTION(copy64d, c);
+DEFINE_FUNCTION(copy64, c);
+DEFINE_FUNCTION(u8d_to_f32d, c);
+DEFINE_FUNCTION(u8_to_f32, c);
+DEFINE_FUNCTION(u8_to_f32d, c);
+DEFINE_FUNCTION(u8d_to_f32, c);
+DEFINE_FUNCTION(s8d_to_f32d, c);
+DEFINE_FUNCTION(s8_to_f32, c);
+DEFINE_FUNCTION(s8_to_f32d, c);
+DEFINE_FUNCTION(s8d_to_f32, c);
+DEFINE_FUNCTION(ulaw_to_f32d, c);
+DEFINE_FUNCTION(alaw_to_f32d, c);
+DEFINE_FUNCTION(u16_to_f32, c);
+DEFINE_FUNCTION(u16_to_f32d, c);
+DEFINE_FUNCTION(s16d_to_f32d, c);
+DEFINE_FUNCTION(s16_to_f32, c);
+DEFINE_FUNCTION(s16_to_f32d, c);
+DEFINE_FUNCTION(s16s_to_f32d, c);
+DEFINE_FUNCTION(s16d_to_f32, c);
+DEFINE_FUNCTION(u32_to_f32, c);
+DEFINE_FUNCTION(u32_to_f32d, c);
+DEFINE_FUNCTION(s32d_to_f32d, c);
+DEFINE_FUNCTION(s32_to_f32, c);
+DEFINE_FUNCTION(s32_to_f32d, c);
+DEFINE_FUNCTION(s32s_to_f32d, c);
+DEFINE_FUNCTION(s32d_to_f32, c);
+DEFINE_FUNCTION(u24_to_f32, c);
+DEFINE_FUNCTION(u24_to_f32d, c);
+DEFINE_FUNCTION(s24d_to_f32d, c);
+DEFINE_FUNCTION(s24_to_f32, c);
+DEFINE_FUNCTION(s24_to_f32d, c);
+DEFINE_FUNCTION(s24s_to_f32d, c);
+DEFINE_FUNCTION(s24d_to_f32, c);
+DEFINE_FUNCTION(u24_32_to_f32, c);
+DEFINE_FUNCTION(u24_32_to_f32d, c);
+DEFINE_FUNCTION(s24_32d_to_f32d, c);
+DEFINE_FUNCTION(s24_32_to_f32, c);
+DEFINE_FUNCTION(s24_32_to_f32d, c);
+DEFINE_FUNCTION(s24_32s_to_f32d, c);
+DEFINE_FUNCTION(s24_32d_to_f32, c);
+DEFINE_FUNCTION(f64d_to_f32d, c);
+DEFINE_FUNCTION(f64_to_f32, c);
+DEFINE_FUNCTION(f64_to_f32d, c);
+DEFINE_FUNCTION(f64s_to_f32d, c);
+DEFINE_FUNCTION(f64d_to_f32, c);
+DEFINE_FUNCTION(f32d_to_u8d, c);
+DEFINE_FUNCTION(f32d_to_u8d_noise, c);
+DEFINE_FUNCTION(f32d_to_u8d_shaped, c);
+DEFINE_FUNCTION(f32_to_u8, c);
+DEFINE_FUNCTION(f32_to_u8d, c);
+DEFINE_FUNCTION(f32d_to_u8, c);
+DEFINE_FUNCTION(f32d_to_u8_noise, c);
+DEFINE_FUNCTION(f32d_to_u8_shaped, c);
+DEFINE_FUNCTION(f32d_to_s8d, c);
+DEFINE_FUNCTION(f32d_to_s8d_noise, c);
+DEFINE_FUNCTION(f32d_to_s8d_shaped, c);
+DEFINE_FUNCTION(f32_to_s8, c);
+DEFINE_FUNCTION(f32_to_s8d, c);
+DEFINE_FUNCTION(f32d_to_s8, c);
+DEFINE_FUNCTION(f32d_to_s8_noise, c);
+DEFINE_FUNCTION(f32d_to_s8_shaped, c);
+DEFINE_FUNCTION(f32d_to_alaw, c);
+DEFINE_FUNCTION(f32d_to_ulaw, c);
+DEFINE_FUNCTION(f32_to_u16, c);
+DEFINE_FUNCTION(f32d_to_u16, c);
+DEFINE_FUNCTION(f32d_to_s16d, c);
+DEFINE_FUNCTION(f32d_to_s16d_noise, c);
+DEFINE_FUNCTION(f32d_to_s16d_shaped, c);
+DEFINE_FUNCTION(f32_to_s16, c);
+DEFINE_FUNCTION(f32_to_s16d, c);
+DEFINE_FUNCTION(f32d_to_s16, c);
+DEFINE_FUNCTION(f32d_to_s16_noise, c);
+DEFINE_FUNCTION(f32d_to_s16_shaped, c);
+DEFINE_FUNCTION(f32d_to_s16s, c);
+DEFINE_FUNCTION(f32d_to_s16s_noise, c);
+DEFINE_FUNCTION(f32d_to_s16s_shaped, c);
+DEFINE_FUNCTION(f32_to_u32, c);
+DEFINE_FUNCTION(f32d_to_u32, c);
+DEFINE_FUNCTION(f32d_to_s32d, c);
+DEFINE_FUNCTION(f32d_to_s32d_noise, c);
+DEFINE_FUNCTION(f32_to_s32, c);
+DEFINE_FUNCTION(f32_to_s32d, c);
+DEFINE_FUNCTION(f32d_to_s32, c);
+DEFINE_FUNCTION(f32d_to_s32_noise, c);
+DEFINE_FUNCTION(f32d_to_s32s, c);
+DEFINE_FUNCTION(f32d_to_s32s_noise, c);
+DEFINE_FUNCTION(f32_to_u24, c);
+DEFINE_FUNCTION(f32d_to_u24, c);
+DEFINE_FUNCTION(f32d_to_s24d, c);
+DEFINE_FUNCTION(f32d_to_s24d_noise, c);
+DEFINE_FUNCTION(f32_to_s24, c);
+DEFINE_FUNCTION(f32_to_s24d, c);
+DEFINE_FUNCTION(f32d_to_s24, c);
+DEFINE_FUNCTION(f32d_to_s24_noise, c);
+DEFINE_FUNCTION(f32d_to_s24s, c);
+DEFINE_FUNCTION(f32d_to_s24s_noise, c);
+DEFINE_FUNCTION(f32_to_u24_32, c);
+DEFINE_FUNCTION(f32d_to_u24_32, c);
+DEFINE_FUNCTION(f32d_to_s24_32d, c);
+DEFINE_FUNCTION(f32d_to_s24_32d_noise, c);
+DEFINE_FUNCTION(f32_to_s24_32, c);
+DEFINE_FUNCTION(f32_to_s24_32d, c);
+DEFINE_FUNCTION(f32d_to_s24_32, c);
+DEFINE_FUNCTION(f32d_to_s24_32_noise, c);
+DEFINE_FUNCTION(f32d_to_s24_32s, c);
+DEFINE_FUNCTION(f32d_to_s24_32s_noise, c);
+DEFINE_FUNCTION(f32d_to_f64d, c);
+DEFINE_FUNCTION(f32_to_f64, c);
+DEFINE_FUNCTION(f32_to_f64d, c);
+DEFINE_FUNCTION(f32d_to_f64, c);
+DEFINE_FUNCTION(f32d_to_f64s, c);
+DEFINE_FUNCTION(8_to_8d, c);
+DEFINE_FUNCTION(16_to_16d, c);
+DEFINE_FUNCTION(24_to_24d, c);
+DEFINE_FUNCTION(32_to_32d, c);
+DEFINE_FUNCTION(32s_to_32d, c);
+DEFINE_FUNCTION(64_to_64d, c);
+DEFINE_FUNCTION(64s_to_64sd, c);
+DEFINE_FUNCTION(8d_to_8, c);
+DEFINE_FUNCTION(16d_to_16, c);
+DEFINE_FUNCTION(24d_to_24, c);
+DEFINE_FUNCTION(32d_to_32, c);
+DEFINE_FUNCTION(32d_to_32s, c);
+DEFINE_FUNCTION(64d_to_64, c);
+DEFINE_FUNCTION(64sd_to_64s, c);
+
+#if defined(HAVE_NEON)
+DEFINE_FUNCTION(s16_to_f32d_2, neon);
+DEFINE_FUNCTION(s16_to_f32d, neon);
+DEFINE_FUNCTION(f32d_to_s16, neon);
+#endif
+#if defined(HAVE_SSE2)
+DEFINE_FUNCTION(s16_to_f32d_2, sse2);
+DEFINE_FUNCTION(s16_to_f32d, sse2);
+DEFINE_FUNCTION(s24_to_f32d, sse2);
+DEFINE_FUNCTION(s32_to_f32d, sse2);
+DEFINE_FUNCTION(f32d_to_s32, sse2);
+DEFINE_FUNCTION(f32d_to_s32_noise, sse2);
+DEFINE_FUNCTION(f32_to_s16, sse2);
+DEFINE_FUNCTION(f32d_to_s16_2, sse2);
+DEFINE_FUNCTION(f32d_to_s16, sse2);
+DEFINE_FUNCTION(f32d_to_s16_noise, sse2);
+DEFINE_FUNCTION(f32d_to_s16d, sse2);
+DEFINE_FUNCTION(f32d_to_s16d_noise, sse2);
+DEFINE_FUNCTION(32_to_32d, sse2);
+DEFINE_FUNCTION(32s_to_32d, sse2);
+DEFINE_FUNCTION(32d_to_32, sse2);
+DEFINE_FUNCTION(32d_to_32s, sse2);
+#endif
+#if defined(HAVE_SSSE3)
+DEFINE_FUNCTION(s24_to_f32d, ssse3);
+#endif
+#if defined(HAVE_SSE41)
+DEFINE_FUNCTION(s24_to_f32d, sse41);
+#endif
+#if defined(HAVE_AVX2)
+DEFINE_FUNCTION(s16_to_f32d_2, avx2);
+DEFINE_FUNCTION(s16_to_f32d, avx2);
+DEFINE_FUNCTION(s24_to_f32d, avx2);
+DEFINE_FUNCTION(s32_to_f32d, avx2);
+DEFINE_FUNCTION(f32d_to_s32, avx2);
+DEFINE_FUNCTION(f32d_to_s16_4, avx2);
+DEFINE_FUNCTION(f32d_to_s16_2, avx2);
+DEFINE_FUNCTION(f32d_to_s16, avx2);
+#endif
+
+#undef DEFINE_FUNCTION
diff --git a/spa/plugins/audioconvert/hilbert.h b/spa/plugins/audioconvert/hilbert.h
new file mode 100644
index 0000000..103e6e9
--- /dev/null
+++ b/spa/plugins/audioconvert/hilbert.h
@@ -0,0 +1,69 @@
+/* Hilbert function
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef HILBERT_H
+#define HILBERT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+#include <stddef.h>
+#include <math.h>
+
+static inline void blackman_window(float *taps, int n_taps)
+{
+ int n;
+ for (n = 0; n < n_taps; n++) {
+ float w = 2 * M_PI * n / (n_taps-1);
+ taps[n] = 0.3635819 - 0.4891775 * cos(w)
+ + 0.1365995 * cos(2 * w) - 0.0106411 * cos(3 * w);
+ }
+}
+
+static inline int hilbert_generate(float *taps, int n_taps)
+{
+ int i;
+
+ if ((n_taps & 1) == 0)
+ return -EINVAL;
+
+ for (i = 0; i < n_taps; i++) {
+ int k = -(n_taps / 2) + i;
+ if (k & 1) {
+ float pk = M_PI * k;
+ taps[i] *= (1.0f - cosf(pk)) / pk;
+ } else {
+ taps[i] = 0.0f;
+ }
+ }
+ return 0;
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* HILBERT_H */
diff --git a/spa/plugins/audioconvert/law.h b/spa/plugins/audioconvert/law.h
new file mode 100644
index 0000000..f5273d8
--- /dev/null
+++ b/spa/plugins/audioconvert/law.h
@@ -0,0 +1,2163 @@
+/* Spa
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+static inline float alaw_to_f32(uint8_t alawbyte)
+{
+ static int16_t alaw_decode[256] = {
+ -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736,
+ -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784,
+ -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368,
+ -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392,
+ -22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944,
+ -30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136,
+ -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472,
+ -15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568,
+ -344, -328, -376, -360, -280, -264, -312, -296,
+ -472, -456, -504, -488, -408, -392, -440, -424,
+ -88, -72, -120, -104, -24, -8, -56, -40,
+ -216, -200, -248, -232, -152, -136, -184, -168,
+ -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184,
+ -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696,
+ -688, -656, -752, -720, -560, -528, -624, -592,
+ -944, -912, -1008, -976, -816, -784, -880, -848,
+ 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736,
+ 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784,
+ 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368,
+ 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392,
+ 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944,
+ 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136,
+ 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472,
+ 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568,
+ 344, 328, 376, 360, 280, 264, 312, 296,
+ 472, 456, 504, 488, 408, 392, 440, 424,
+ 88, 72, 120, 104, 24, 8, 56, 40,
+ 216, 200, 248, 232, 152, 136, 184, 168,
+ 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184,
+ 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696,
+ 688, 656, 752, 720, 560, 528, 624, 592,
+ 944, 912, 1008, 976, 816, 784, 880, 848
+ };
+ return S16_TO_F32(alaw_decode[alawbyte]);
+}
+
+static inline float ulaw_to_f32(uint8_t ulawbyte)
+{
+ static int16_t ulaw_decode[256] = {
+ -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956,
+ -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764,
+ -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412,
+ -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316,
+ -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,
+ -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,
+ -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,
+ -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,
+ -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,
+ -1372, -1308, -1244, -1180, -1116, -1052, -988, -924,
+ -876, -844, -812, -780, -748, -716, -684, -652,
+ -620, -588, -556, -524, -492, -460, -428, -396,
+ -372, -356, -340, -324, -308, -292, -276, -260,
+ -244, -228, -212, -196, -180, -164, -148, -132,
+ -120, -112, -104, -96, -88, -80, -72, -64,
+ -56, -48, -40, -32, -24, -16, -8, 0,
+ 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,
+ 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,
+ 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,
+ 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316,
+ 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140,
+ 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092,
+ 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004,
+ 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980,
+ 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436,
+ 1372, 1308, 1244, 1180, 1116, 1052, 988, 924,
+ 876, 844, 812, 780, 748, 716, 684, 652,
+ 620, 588, 556, 524, 492, 460, 428, 396,
+ 372, 356, 340, 324, 308, 292, 276, 260,
+ 244, 228, 212, 196, 180, 164, 148, 132,
+ 120, 112, 104, 96, 88, 80, 72, 64,
+ 56, 48, 40, 32, 24, 16, 8, 0
+ };
+ return S16_TO_F32(ulaw_decode[ulawbyte]);
+}
+
+static inline uint8_t f32_to_alaw(float val)
+{
+ static uint8_t alaw_encode[0x2000] = {
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x3a, 0x3a, 0x3a, 0x3a,
+ 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a,
+ 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a,
+ 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a,
+ 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a,
+ 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a,
+ 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b,
+ 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b,
+ 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b,
+ 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b,
+ 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b,
+ 0x3b, 0x3b, 0x3b, 0x3b, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x39, 0x39, 0x39, 0x39,
+ 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39,
+ 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39,
+ 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39,
+ 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39,
+ 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39,
+ 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e,
+ 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e,
+ 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e,
+ 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e,
+ 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e,
+ 0x3e, 0x3e, 0x3e, 0x3e, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,
+ 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,
+ 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,
+ 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,
+ 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,
+ 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3c, 0x3c, 0x3c, 0x3c,
+ 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c,
+ 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c,
+ 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c,
+ 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c,
+ 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
+ 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
+ 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
+ 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
+ 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
+ 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x33, 0x33, 0x33, 0x33,
+ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
+ 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
+ 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
+ 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
+ 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
+ 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x36, 0x36, 0x36, 0x36,
+ 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
+ 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
+ 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
+ 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
+ 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
+ 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37,
+ 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37,
+ 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37,
+ 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37,
+ 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37,
+ 0x37, 0x37, 0x37, 0x37, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34,
+ 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34,
+ 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34,
+ 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34,
+ 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34,
+ 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x35, 0x35, 0x35, 0x35,
+ 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
+ 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
+ 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
+ 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
+ 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6b, 0x6b, 0x6b, 0x6b,
+ 0x6b, 0x6b, 0x6b, 0x6b, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68,
+ 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x6e, 0x6e, 0x6e, 0x6e,
+ 0x6e, 0x6e, 0x6e, 0x6e, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f,
+ 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6d, 0x6d, 0x6d, 0x6d,
+ 0x6d, 0x6d, 0x6d, 0x6d, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62,
+ 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x60, 0x60, 0x60, 0x60,
+ 0x60, 0x60, 0x60, 0x60, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
+ 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x67, 0x67, 0x67, 0x67,
+ 0x67, 0x67, 0x67, 0x67, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64,
+ 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x7a, 0x7a, 0x7a, 0x7a,
+ 0x7b, 0x7b, 0x7b, 0x7b, 0x78, 0x78, 0x78, 0x78, 0x79, 0x79, 0x79, 0x79,
+ 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f, 0x7f, 0x7c, 0x7c, 0x7c, 0x7c,
+ 0x7d, 0x7d, 0x7d, 0x7d, 0x72, 0x72, 0x72, 0x72, 0x73, 0x73, 0x73, 0x73,
+ 0x70, 0x70, 0x70, 0x70, 0x71, 0x71, 0x71, 0x71, 0x76, 0x76, 0x76, 0x76,
+ 0x77, 0x77, 0x77, 0x77, 0x74, 0x74, 0x74, 0x74, 0x75, 0x75, 0x75, 0x75,
+ 0x4a, 0x4a, 0x4b, 0x4b, 0x48, 0x48, 0x49, 0x49, 0x4e, 0x4e, 0x4f, 0x4f,
+ 0x4c, 0x4c, 0x4d, 0x4d, 0x42, 0x42, 0x43, 0x43, 0x40, 0x40, 0x41, 0x41,
+ 0x46, 0x46, 0x47, 0x47, 0x44, 0x44, 0x45, 0x45, 0x5a, 0x5a, 0x5b, 0x5b,
+ 0x58, 0x58, 0x59, 0x59, 0x5e, 0x5e, 0x5f, 0x5f, 0x5c, 0x5c, 0x5d, 0x5d,
+ 0x52, 0x52, 0x53, 0x53, 0x50, 0x50, 0x51, 0x51, 0x56, 0x56, 0x57, 0x57,
+ 0x54, 0x54, 0x55, 0x55, 0xd5, 0xd5, 0xd4, 0xd4, 0xd7, 0xd7, 0xd6, 0xd6,
+ 0xd1, 0xd1, 0xd0, 0xd0, 0xd3, 0xd3, 0xd2, 0xd2, 0xdd, 0xdd, 0xdc, 0xdc,
+ 0xdf, 0xdf, 0xde, 0xde, 0xd9, 0xd9, 0xd8, 0xd8, 0xdb, 0xdb, 0xda, 0xda,
+ 0xc5, 0xc5, 0xc4, 0xc4, 0xc7, 0xc7, 0xc6, 0xc6, 0xc1, 0xc1, 0xc0, 0xc0,
+ 0xc3, 0xc3, 0xc2, 0xc2, 0xcd, 0xcd, 0xcc, 0xcc, 0xcf, 0xcf, 0xce, 0xce,
+ 0xc9, 0xc9, 0xc8, 0xc8, 0xcb, 0xcb, 0xca, 0xca, 0xf5, 0xf5, 0xf5, 0xf5,
+ 0xf4, 0xf4, 0xf4, 0xf4, 0xf7, 0xf7, 0xf7, 0xf7, 0xf6, 0xf6, 0xf6, 0xf6,
+ 0xf1, 0xf1, 0xf1, 0xf1, 0xf0, 0xf0, 0xf0, 0xf0, 0xf3, 0xf3, 0xf3, 0xf3,
+ 0xf2, 0xf2, 0xf2, 0xf2, 0xfd, 0xfd, 0xfd, 0xfd, 0xfc, 0xfc, 0xfc, 0xfc,
+ 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfe, 0xfe, 0xfe, 0xf9, 0xf9, 0xf9, 0xf9,
+ 0xf8, 0xf8, 0xf8, 0xf8, 0xfb, 0xfb, 0xfb, 0xfb, 0xfa, 0xfa, 0xfa, 0xfa,
+ 0xe5, 0xe5, 0xe5, 0xe5, 0xe5, 0xe5, 0xe5, 0xe5, 0xe4, 0xe4, 0xe4, 0xe4,
+ 0xe4, 0xe4, 0xe4, 0xe4, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7,
+ 0xe6, 0xe6, 0xe6, 0xe6, 0xe6, 0xe6, 0xe6, 0xe6, 0xe1, 0xe1, 0xe1, 0xe1,
+ 0xe1, 0xe1, 0xe1, 0xe1, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0,
+ 0xe3, 0xe3, 0xe3, 0xe3, 0xe3, 0xe3, 0xe3, 0xe3, 0xe2, 0xe2, 0xe2, 0xe2,
+ 0xe2, 0xe2, 0xe2, 0xe2, 0xed, 0xed, 0xed, 0xed, 0xed, 0xed, 0xed, 0xed,
+ 0xec, 0xec, 0xec, 0xec, 0xec, 0xec, 0xec, 0xec, 0xef, 0xef, 0xef, 0xef,
+ 0xef, 0xef, 0xef, 0xef, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee,
+ 0xe9, 0xe9, 0xe9, 0xe9, 0xe9, 0xe9, 0xe9, 0xe9, 0xe8, 0xe8, 0xe8, 0xe8,
+ 0xe8, 0xe8, 0xe8, 0xe8, 0xeb, 0xeb, 0xeb, 0xeb, 0xeb, 0xeb, 0xeb, 0xeb,
+ 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0xb5, 0xb5, 0xb5, 0xb5,
+ 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5,
+ 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5,
+ 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5,
+ 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5,
+ 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7,
+ 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7,
+ 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7,
+ 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7,
+ 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7,
+ 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0,
+ 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0,
+ 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0,
+ 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0,
+ 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0,
+ 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2,
+ 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2,
+ 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2,
+ 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2,
+ 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2,
+ 0xb2, 0xb2, 0xb2, 0xb2, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd,
+ 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd,
+ 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd,
+ 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd,
+ 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd,
+ 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf,
+ 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf,
+ 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf,
+ 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf,
+ 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf,
+ 0xbf, 0xbf, 0xbf, 0xbf, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8,
+ 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8,
+ 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8,
+ 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8,
+ 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8,
+ 0xb8, 0xb8, 0xb8, 0xb8, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xba, 0xba, 0xba, 0xba,
+ 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba,
+ 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba,
+ 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba,
+ 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba,
+ 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa
+ };
+ return alaw_encode[(F32_TO_S16(val)>>3) + 0x1000];
+}
+
+static inline uint8_t f32_to_ulaw(float val)
+{
+ static uint8_t ulaw_encode[0x4000] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
+ 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+ 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+ 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,
+ 0x09, 0x09, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
+ 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
+ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c,
+ 0x0c, 0x0c, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d,
+ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e,
+ 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+ 0x0f, 0x0f, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
+ 0x12, 0x12, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
+ 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15,
+ 0x15, 0x15, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16,
+ 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17,
+ 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19,
+ 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a,
+ 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b,
+ 0x1b, 0x1b, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c,
+ 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d,
+ 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e,
+ 0x1e, 0x1e, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f,
+ 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+ 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
+ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23,
+ 0x23, 0x23, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+ 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25,
+ 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26,
+ 0x26, 0x26, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27,
+ 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29,
+ 0x29, 0x29, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a,
+ 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b,
+ 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c,
+ 0x2c, 0x2c, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
+ 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
+ 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
+ 0x2f, 0x2f, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x31,
+ 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
+ 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
+ 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
+ 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
+ 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
+ 0x32, 0x32, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x34, 0x34,
+ 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34,
+ 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34,
+ 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
+ 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
+ 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
+ 0x35, 0x35, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
+ 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
+ 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x37, 0x37,
+ 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37,
+ 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37,
+ 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x38, 0x38, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39,
+ 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39,
+ 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x3a, 0x3a,
+ 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a,
+ 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a,
+ 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b,
+ 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b,
+ 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b,
+ 0x3b, 0x3b, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c,
+ 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c,
+ 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3d, 0x3d,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d,
+ 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e,
+ 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e,
+ 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e,
+ 0x3e, 0x3e, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,
+ 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,
+ 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x40, 0x40,
+ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40,
+ 0x40, 0x40, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
+ 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
+ 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x43, 0x43,
+ 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43,
+ 0x43, 0x43, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
+ 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45,
+ 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x46, 0x46,
+ 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46,
+ 0x46, 0x46, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47,
+ 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
+ 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x49, 0x49,
+ 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49,
+ 0x49, 0x49, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a,
+ 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b,
+ 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4c, 0x4c,
+ 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c,
+ 0x4c, 0x4c, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d,
+ 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e,
+ 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4f, 0x4f,
+ 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,
+ 0x4f, 0x4f, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x51, 0x51,
+ 0x51, 0x51, 0x51, 0x51, 0x51, 0x51, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52,
+ 0x52, 0x52, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x54, 0x54,
+ 0x54, 0x54, 0x54, 0x54, 0x54, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+ 0x55, 0x55, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x57, 0x57,
+ 0x57, 0x57, 0x57, 0x57, 0x57, 0x57, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58,
+ 0x58, 0x58, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x5a, 0x5a,
+ 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b,
+ 0x5b, 0x5b, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5d, 0x5d,
+ 0x5d, 0x5d, 0x5d, 0x5d, 0x5d, 0x5d, 0x5e, 0x5e, 0x5e, 0x5e, 0x5e, 0x5e,
+ 0x5e, 0x5e, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x60, 0x60,
+ 0x60, 0x60, 0x61, 0x61, 0x61, 0x61, 0x62, 0x62, 0x62, 0x62, 0x63, 0x63,
+ 0x63, 0x63, 0x64, 0x64, 0x64, 0x64, 0x65, 0x65, 0x65, 0x65, 0x66, 0x66,
+ 0x66, 0x66, 0x67, 0x67, 0x67, 0x67, 0x68, 0x68, 0x68, 0x68, 0x69, 0x69,
+ 0x69, 0x69, 0x6a, 0x6a, 0x6a, 0x6a, 0x6b, 0x6b, 0x6b, 0x6b, 0x6c, 0x6c,
+ 0x6c, 0x6c, 0x6d, 0x6d, 0x6d, 0x6d, 0x6e, 0x6e, 0x6e, 0x6e, 0x6f, 0x6f,
+ 0x6f, 0x6f, 0x70, 0x70, 0x71, 0x71, 0x72, 0x72, 0x73, 0x73, 0x74, 0x74,
+ 0x75, 0x75, 0x76, 0x76, 0x77, 0x77, 0x78, 0x78, 0x79, 0x79, 0x7a, 0x7a,
+ 0x7b, 0x7b, 0x7c, 0x7c, 0x7d, 0x7d, 0x7e, 0x7e, 0xff, 0xfe, 0xfe, 0xfd,
+ 0xfd, 0xfc, 0xfc, 0xfb, 0xfb, 0xfa, 0xfa, 0xf9, 0xf9, 0xf8, 0xf8, 0xf7,
+ 0xf7, 0xf6, 0xf6, 0xf5, 0xf5, 0xf4, 0xf4, 0xf3, 0xf3, 0xf2, 0xf2, 0xf1,
+ 0xf1, 0xf0, 0xf0, 0xef, 0xef, 0xef, 0xef, 0xee, 0xee, 0xee, 0xee, 0xed,
+ 0xed, 0xed, 0xed, 0xec, 0xec, 0xec, 0xec, 0xeb, 0xeb, 0xeb, 0xeb, 0xea,
+ 0xea, 0xea, 0xea, 0xe9, 0xe9, 0xe9, 0xe9, 0xe8, 0xe8, 0xe8, 0xe8, 0xe7,
+ 0xe7, 0xe7, 0xe7, 0xe6, 0xe6, 0xe6, 0xe6, 0xe5, 0xe5, 0xe5, 0xe5, 0xe4,
+ 0xe4, 0xe4, 0xe4, 0xe3, 0xe3, 0xe3, 0xe3, 0xe2, 0xe2, 0xe2, 0xe2, 0xe1,
+ 0xe1, 0xe1, 0xe1, 0xe0, 0xe0, 0xe0, 0xe0, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf,
+ 0xdf, 0xdf, 0xdf, 0xde, 0xde, 0xde, 0xde, 0xde, 0xde, 0xde, 0xde, 0xdd,
+ 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdc, 0xdc, 0xdc, 0xdc, 0xdc,
+ 0xdc, 0xdc, 0xdc, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xda,
+ 0xda, 0xda, 0xda, 0xda, 0xda, 0xda, 0xda, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9,
+ 0xd9, 0xd9, 0xd9, 0xd8, 0xd8, 0xd8, 0xd8, 0xd8, 0xd8, 0xd8, 0xd8, 0xd7,
+ 0xd7, 0xd7, 0xd7, 0xd7, 0xd7, 0xd7, 0xd7, 0xd6, 0xd6, 0xd6, 0xd6, 0xd6,
+ 0xd6, 0xd6, 0xd6, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd4,
+ 0xd4, 0xd4, 0xd4, 0xd4, 0xd4, 0xd4, 0xd4, 0xd3, 0xd3, 0xd3, 0xd3, 0xd3,
+ 0xd3, 0xd3, 0xd3, 0xd2, 0xd2, 0xd2, 0xd2, 0xd2, 0xd2, 0xd2, 0xd2, 0xd1,
+ 0xd1, 0xd1, 0xd1, 0xd1, 0xd1, 0xd1, 0xd1, 0xd0, 0xd0, 0xd0, 0xd0, 0xd0,
+ 0xd0, 0xd0, 0xd0, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf,
+ 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xce, 0xce, 0xce, 0xce, 0xce,
+ 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xcd,
+ 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd,
+ 0xcd, 0xcd, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc,
+ 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb,
+ 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xca,
+ 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca,
+ 0xca, 0xca, 0xca, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9,
+ 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8,
+ 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc7,
+ 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7,
+ 0xc7, 0xc7, 0xc7, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6,
+ 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5,
+ 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc4,
+ 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+ 0xc4, 0xc4, 0xc4, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3,
+ 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2,
+ 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc1,
+ 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1,
+ 0xc1, 0xc1, 0xc1, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0,
+ 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf,
+ 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf,
+ 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf,
+ 0xbf, 0xbf, 0xbf, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbd,
+ 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd,
+ 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd,
+ 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc,
+ 0xbc, 0xbc, 0xbc, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+ 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xba,
+ 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba,
+ 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba,
+ 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9,
+ 0xb9, 0xb9, 0xb9, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8,
+ 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8,
+ 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb7,
+ 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7,
+ 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7,
+ 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5,
+ 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5,
+ 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb4,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4,
+ 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3,
+ 0xb3, 0xb3, 0xb3, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2,
+ 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2,
+ 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb1,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1,
+ 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0,
+ 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0,
+ 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0,
+ 0xb0, 0xb0, 0xb0, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf,
+ 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae,
+ 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad,
+ 0xad, 0xad, 0xad, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
+ 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
+ 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+ 0xaa, 0xaa, 0xaa, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8,
+ 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7,
+ 0xa7, 0xa7, 0xa7, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5,
+ 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4,
+ 0xa4, 0xa4, 0xa4, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3,
+ 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2,
+ 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1,
+ 0xa1, 0xa1, 0xa1, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0,
+ 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f,
+ 0x9f, 0x9f, 0x9f, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e,
+ 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d,
+ 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c,
+ 0x9c, 0x9c, 0x9c, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b,
+ 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a,
+ 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99,
+ 0x99, 0x99, 0x99, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98,
+ 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97,
+ 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96,
+ 0x96, 0x96, 0x96, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95,
+ 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94,
+ 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
+ 0x93, 0x93, 0x93, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92,
+ 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91,
+ 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
+ 0x90, 0x90, 0x90, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f,
+ 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e,
+ 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d,
+ 0x8d, 0x8d, 0x8d, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c,
+ 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b,
+ 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a,
+ 0x8a, 0x8a, 0x8a, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89,
+ 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88,
+ 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87,
+ 0x87, 0x87, 0x87, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86,
+ 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85,
+ 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84,
+ 0x84, 0x84, 0x84, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83,
+ 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82,
+ 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81,
+ 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+ 0x80, 0x80, 0x80, 0x80 };
+ return ulaw_encode[(F32_TO_S16(val)>>2) + 0x2000];
+}
diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build
new file mode 100644
index 0000000..f08527a
--- /dev/null
+++ b/spa/plugins/audioconvert/meson.build
@@ -0,0 +1,206 @@
+audioconvert_sources = [
+ 'audioadapter.c',
+ 'audioconvert.c',
+ 'plugin.c'
+]
+
+simd_cargs = []
+simd_dependencies = []
+
+audioconvert_c = static_library('audioconvert_c',
+ [ 'channelmix-ops-c.c',
+ 'biquad.c',
+ 'crossover.c',
+ 'volume-ops-c.c',
+ 'peaks-ops-c.c',
+ 'resample-native-c.c',
+ 'fmt-ops-c.c' ],
+ c_args : ['-Ofast', '-ffast-math'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+simd_dependencies += audioconvert_c
+
+if have_sse
+ audioconvert_sse = static_library('audioconvert_sse',
+ ['resample-native-sse.c',
+ 'volume-ops-sse.c',
+ 'peaks-ops-sse.c',
+ 'channelmix-ops-sse.c' ],
+ c_args : [sse_args, '-Ofast', '-DHAVE_SSE'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_SSE']
+ simd_dependencies += audioconvert_sse
+endif
+if have_sse2
+ audioconvert_sse2 = static_library('audioconvert_sse2',
+ ['fmt-ops-sse2.c' ],
+ c_args : [sse2_args, '-O3', '-DHAVE_SSE2'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_SSE2']
+ simd_dependencies += audioconvert_sse2
+endif
+if have_ssse3
+ audioconvert_ssse3 = static_library('audioconvert_ssse3',
+ ['fmt-ops-ssse3.c',
+ 'resample-native-ssse3.c' ],
+ c_args : [ssse3_args, '-O3', '-DHAVE_SSSE3'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_SSSE3']
+ simd_dependencies += audioconvert_ssse3
+endif
+if have_sse41
+ audioconvert_sse41 = static_library('audioconvert_sse41',
+ ['fmt-ops-sse41.c'],
+ c_args : [sse41_args, '-O3', '-DHAVE_SSE41'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_SSE41']
+ simd_dependencies += audioconvert_sse41
+endif
+if have_avx and have_fma
+ audioconvert_avx = static_library('audioconvert_avx',
+ ['resample-native-avx.c'],
+ c_args : [avx_args, fma_args, '-O3', '-DHAVE_AVX', '-DHAVE_FMA'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_AVX', '-DHAVE_FMA']
+ simd_dependencies += audioconvert_avx
+endif
+if have_avx2
+ audioconvert_avx2 = static_library('audioconvert_avx2',
+ ['fmt-ops-avx2.c'],
+ c_args : [avx2_args, '-O3', '-DHAVE_AVX2'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_AVX2']
+ simd_dependencies += audioconvert_avx2
+endif
+
+if have_neon
+ audioconvert_neon = static_library('audioconvert_neon',
+ ['resample-native-neon.c',
+ 'fmt-ops-neon.c' ],
+ c_args : [neon_args, '-O3', '-DHAVE_NEON'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_NEON']
+ simd_dependencies += audioconvert_neon
+endif
+
+audioconvert_lib = static_library('audioconvert',
+ ['fmt-ops.c',
+ 'channelmix-ops.c',
+ 'peaks-ops.c',
+ 'resample-native.c',
+ 'resample-peaks.c',
+ 'volume-ops.c' ],
+ c_args : [ simd_cargs, '-O3'],
+ link_with : simd_dependencies,
+ include_directories : [configinc],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+audioconvert_dep = declare_dependency(link_with: audioconvert_lib)
+
+spa_audioconvert_lib = shared_library('spa-audioconvert',
+ audioconvert_sources,
+ c_args : simd_cargs,
+ dependencies : [ spa_dep, mathlib, audioconvert_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'audioconvert')
+spa_audioconvert_dep = declare_dependency(link_with: spa_audioconvert_lib)
+
+test_lib = static_library('test_lib',
+ ['test-source.c' ],
+ c_args : ['-O3'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+
+test_apps = [
+ 'test-audioadapter',
+ 'test-audioconvert',
+ 'test-channelmix',
+ 'test-fmt-ops',
+ 'test-peaks',
+ 'test-resample',
+ ]
+
+foreach a : test_apps
+ test(a,
+ executable(a, a + '.c',
+ dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audioconvert_dep, spa_audioconvert_dep ],
+ include_directories : [ configinc ],
+ link_with : [ test_lib ],
+ install_rpath : spa_plugindir / 'audioconvert',
+ c_args : [ simd_cargs ],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'audioconvert'),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ ])
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'audioconvert' / a)
+ configure_file(
+ input: installed_tests_template,
+ output: a + '.test',
+ install_dir: installed_tests_metadir / 'audioconvert',
+ configuration: test_conf
+ )
+ endif
+endforeach
+
+benchmark_apps = [
+ 'benchmark-fmt-ops',
+ 'benchmark-resample',
+ ]
+
+foreach a : benchmark_apps
+ benchmark(a,
+ executable(a, a + '.c',
+ dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audioconvert_dep, spa_audioconvert_dep ],
+ include_directories : [ configinc ],
+ c_args : [ simd_cargs ],
+ install_rpath : spa_plugindir / 'audioconvert',
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'audioconvert'),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ ])
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'audioconvert' / a)
+ configure_file(
+ input: installed_tests_template,
+ output: a + '.test',
+ install_dir: installed_tests_metadir / 'audioconvert',
+ configuration: test_conf
+ )
+ endif
+endforeach
+
+if sndfile_dep.found()
+ sparesample_sources = [
+ 'spa-resample.c',
+ ]
+ executable('spa-resample',
+ sparesample_sources,
+ link_with : [ test_lib ],
+ dependencies : [ spa_dep, sndfile_dep, mathlib, audioconvert_dep ],
+ install : true,
+ )
+endif
diff --git a/spa/plugins/audioconvert/peaks-ops-c.c b/spa/plugins/audioconvert/peaks-ops-c.c
new file mode 100644
index 0000000..45ab1dc
--- /dev/null
+++ b/spa/plugins/audioconvert/peaks-ops-c.c
@@ -0,0 +1,50 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <math.h>
+
+#include "peaks-ops.h"
+
+void peaks_min_max_c(struct peaks *peaks, const float * SPA_RESTRICT src,
+ uint32_t n_samples, float *min, float *max)
+{
+ uint32_t n;
+ float t, mi = *min, ma = *max;
+ for (n = 0; n < n_samples; n++) {
+ t = src[n];
+ mi = fminf(mi, t);
+ ma = fmaxf(ma, t);
+ }
+ *min = mi;
+ *max = ma;
+}
+
+float peaks_abs_max_c(struct peaks *peaks, const float * SPA_RESTRICT src,
+ uint32_t n_samples, float max)
+{
+ uint32_t n;
+ for (n = 0; n < n_samples; n++)
+ max = fmaxf(fabsf(src[n]), max);
+ return max;
+}
diff --git a/spa/plugins/audioconvert/peaks-ops-sse.c b/spa/plugins/audioconvert/peaks-ops-sse.c
new file mode 100644
index 0000000..7ceb2a8
--- /dev/null
+++ b/spa/plugins/audioconvert/peaks-ops-sse.c
@@ -0,0 +1,122 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <math.h>
+
+#include <xmmintrin.h>
+
+#include "peaks-ops.h"
+
+static inline float hmin_ps(__m128 val)
+{
+ __m128 t = _mm_movehl_ps(val, val);
+ t = _mm_min_ps(t, val);
+ val = _mm_shuffle_ps(t, t, 0x55);
+ val = _mm_min_ss(t, val);
+ return _mm_cvtss_f32(val);
+}
+
+static inline float hmax_ps(__m128 val)
+{
+ __m128 t = _mm_movehl_ps(val, val);
+ t = _mm_max_ps(t, val);
+ val = _mm_shuffle_ps(t, t, 0x55);
+ val = _mm_max_ss(t, val);
+ return _mm_cvtss_f32(val);
+}
+
+void peaks_min_max_sse(struct peaks *peaks, const float * SPA_RESTRICT src,
+ uint32_t n_samples, float *min, float *max)
+{
+ uint32_t n;
+ __m128 in;
+ __m128 mi = _mm_set1_ps(*min);
+ __m128 ma = _mm_set1_ps(*max);
+
+ for (n = 0; n < n_samples; n++) {
+ if (SPA_IS_ALIGNED(&src[n], 16))
+ break;
+ in = _mm_set1_ps(src[n]);
+ mi = _mm_min_ps(mi, in);
+ ma = _mm_max_ps(ma, in);
+ }
+ for (; n + 15 < n_samples; n += 16) {
+ in = _mm_load_ps(&src[n + 0]);
+ mi = _mm_min_ps(mi, in);
+ ma = _mm_max_ps(ma, in);
+ in = _mm_load_ps(&src[n + 4]);
+ mi = _mm_min_ps(mi, in);
+ ma = _mm_max_ps(ma, in);
+ in = _mm_load_ps(&src[n + 8]);
+ mi = _mm_min_ps(mi, in);
+ ma = _mm_max_ps(ma, in);
+ in = _mm_load_ps(&src[n + 12]);
+ mi = _mm_min_ps(mi, in);
+ ma = _mm_max_ps(ma, in);
+ }
+ for (; n < n_samples; n++) {
+ in = _mm_set1_ps(src[n]);
+ mi = _mm_min_ps(mi, in);
+ ma = _mm_max_ps(ma, in);
+ }
+ *min = hmin_ps(mi);
+ *max = hmax_ps(ma);
+}
+
+float peaks_abs_max_sse(struct peaks *peaks, const float * SPA_RESTRICT src,
+ uint32_t n_samples, float max)
+{
+ uint32_t n;
+ __m128 in;
+ __m128 ma = _mm_set1_ps(max);
+ const __m128 mask = _mm_set1_ps(-0.0f);
+
+ for (n = 0; n < n_samples; n++) {
+ if (SPA_IS_ALIGNED(&src[n], 16))
+ break;
+ in = _mm_set1_ps(src[n]);
+ in = _mm_andnot_ps(mask, in);
+ ma = _mm_max_ps(ma, in);
+ }
+ for (; n + 15 < n_samples; n += 16) {
+ in = _mm_load_ps(&src[n + 0]);
+ in = _mm_andnot_ps(mask, in);
+ ma = _mm_max_ps(ma, in);
+ in = _mm_load_ps(&src[n + 4]);
+ in = _mm_andnot_ps(mask, in);
+ ma = _mm_max_ps(ma, in);
+ in = _mm_load_ps(&src[n + 8]);
+ in = _mm_andnot_ps(mask, in);
+ ma = _mm_max_ps(ma, in);
+ in = _mm_load_ps(&src[n + 12]);
+ in = _mm_andnot_ps(mask, in);
+ ma = _mm_max_ps(ma, in);
+ }
+ for (; n < n_samples; n++) {
+ in = _mm_set1_ps(src[n]);
+ in = _mm_andnot_ps(mask, in);
+ ma = _mm_max_ps(ma, in);
+ }
+ return hmax_ps(ma);
+}
diff --git a/spa/plugins/audioconvert/peaks-ops.c b/spa/plugins/audioconvert/peaks-ops.c
new file mode 100644
index 0000000..b5cb252
--- /dev/null
+++ b/spa/plugins/audioconvert/peaks-ops.c
@@ -0,0 +1,89 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <errno.h>
+
+#include <spa/support/cpu.h>
+#include <spa/support/log.h>
+#include <spa/utils/defs.h>
+
+#include "peaks-ops.h"
+
+typedef void (*peaks_min_max_func_t) (struct peaks *peaks, const float * SPA_RESTRICT src,
+ uint32_t n_samples, float *min, float *max);
+typedef float (*peaks_abs_max_func_t) (struct peaks *peaks, const float * SPA_RESTRICT src,
+ uint32_t n_samples, float max);
+
+#define MAKE(min_max,abs_max,...) \
+ { min_max, abs_max, #min_max , __VA_ARGS__ }
+
+static const struct peaks_info {
+ peaks_min_max_func_t min_max;
+ peaks_abs_max_func_t abs_max;
+ const char *name;
+ uint32_t cpu_flags;
+} peaks_table[] =
+{
+#if defined (HAVE_SSE)
+ MAKE(peaks_min_max_sse, peaks_abs_max_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(peaks_min_max_c, peaks_abs_max_c),
+};
+#undef MAKE
+
+#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a)
+
+static const struct peaks_info *find_peaks_info(uint32_t cpu_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(peaks_table, t) {
+ if (MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+ return t;
+ }
+ return NULL;
+}
+
+static void impl_peaks_free(struct peaks *peaks)
+{
+ peaks->min_max = NULL;
+ peaks->abs_max = NULL;
+}
+
+int peaks_init(struct peaks *peaks)
+{
+ const struct peaks_info *info;
+
+ info = find_peaks_info(peaks->cpu_flags);
+ if (info == NULL)
+ return -ENOTSUP;
+
+ peaks->cpu_flags = info->cpu_flags;
+ peaks->func_name = info->name;
+ peaks->free = impl_peaks_free;
+ peaks->min_max = info->min_max;
+ peaks->abs_max = info->abs_max;
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/peaks-ops.h b/spa/plugins/audioconvert/peaks-ops.h
new file mode 100644
index 0000000..29da794
--- /dev/null
+++ b/spa/plugins/audioconvert/peaks-ops.h
@@ -0,0 +1,72 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/utils/defs.h>
+
+struct peaks {
+ uint32_t cpu_flags;
+ const char *func_name;
+
+ struct spa_log *log;
+
+ uint32_t flags;
+
+ void (*min_max) (struct peaks *peaks, const float * SPA_RESTRICT src,
+ uint32_t n_samples, float *min, float *max);
+ float (*abs_max) (struct peaks *peaks, const float * SPA_RESTRICT src,
+ uint32_t n_samples, float max);
+
+ void (*free) (struct peaks *peaks);
+};
+
+int peaks_init(struct peaks *peaks);
+
+#define peaks_min_max(peaks,...) (peaks)->min_max(peaks, __VA_ARGS__)
+#define peaks_abs_max(peaks,...) (peaks)->abs_max(peaks, __VA_ARGS__)
+#define peaks_free(peaks) (peaks)->free(peaks)
+
+#define DEFINE_MIN_MAX_FUNCTION(arch) \
+void peaks_min_max_##arch(struct peaks *peaks, \
+ const float * SPA_RESTRICT src, \
+ uint32_t n_samples, float *min, float *max);
+
+#define DEFINE_ABS_MAX_FUNCTION(arch) \
+float peaks_abs_max_##arch(struct peaks *peaks, \
+ const float * SPA_RESTRICT src, \
+ uint32_t n_samples, float max);
+
+#define PEAKS_OPS_MAX_ALIGN 16
+
+DEFINE_MIN_MAX_FUNCTION(c);
+DEFINE_ABS_MAX_FUNCTION(c);
+
+#if defined (HAVE_SSE)
+DEFINE_MIN_MAX_FUNCTION(sse);
+DEFINE_ABS_MAX_FUNCTION(sse);
+#endif
+
+#undef DEFINE_FUNCTION
diff --git a/spa/plugins/audioconvert/plugin.c b/spa/plugins/audioconvert/plugin.c
new file mode 100644
index 0000000..03c206f
--- /dev/null
+++ b/spa/plugins/audioconvert/plugin.c
@@ -0,0 +1,50 @@
+/* Spa Audioconvert plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_audioconvert_factory;
+extern const struct spa_handle_factory spa_audioadapter_factory;
+
+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_audioconvert_factory;
+ break;
+ case 1:
+ *factory = &spa_audioadapter_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/audioconvert/resample-native-avx.c b/spa/plugins/audioconvert/resample-native-avx.c
new file mode 100644
index 0000000..136d6cb
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-native-avx.c
@@ -0,0 +1,94 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "resample-native-impl.h"
+
+#include <assert.h>
+#include <immintrin.h>
+
+static inline void inner_product_avx(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT taps, uint32_t n_taps)
+{
+ __m256 sy[2] = { _mm256_setzero_ps(), _mm256_setzero_ps() }, ty;
+ __m128 sx[2], tx;
+ uint32_t i = 0;
+ uint32_t n_taps4 = n_taps & ~0xf;
+
+ for (; i < n_taps4; i += 16) {
+ ty = (__m256)_mm256_lddqu_si256((__m256i*)(s + i + 0));
+ sy[0] = _mm256_fmadd_ps(ty, _mm256_load_ps(taps + i + 0), sy[0]);
+ ty = (__m256)_mm256_lddqu_si256((__m256i*)(s + i + 8));
+ sy[1] = _mm256_fmadd_ps(ty, _mm256_load_ps(taps + i + 8), sy[1]);
+ }
+ sy[0] = _mm256_add_ps(sy[1], sy[0]);
+ sx[1] = _mm256_extractf128_ps(sy[0], 1);
+ sx[0] = _mm256_extractf128_ps(sy[0], 0);
+ for (; i < n_taps; i += 8) {
+ tx = (__m128)_mm_lddqu_si128((__m128i*)(s + i + 0));
+ sx[0] = _mm_fmadd_ps(tx, _mm_load_ps(taps + i + 0), sx[0]);
+ tx = (__m128)_mm_lddqu_si128((__m128i*)(s + i + 4));
+ sx[1] = _mm_fmadd_ps(tx, _mm_load_ps(taps + i + 4), sx[1]);
+ }
+ sx[0] = _mm_add_ps(sx[0], sx[1]);
+ sx[0] = _mm_hadd_ps(sx[0], sx[0]);
+ sx[0] = _mm_hadd_ps(sx[0], sx[0]);
+ _mm_store_ss(d, sx[0]);
+}
+
+static inline void inner_product_ip_avx(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x,
+ uint32_t n_taps)
+{
+ __m256 sy[2] = { _mm256_setzero_ps(), _mm256_setzero_ps() }, ty;
+ __m128 sx[2], tx;
+ uint32_t i, n_taps4 = n_taps & ~0xf;
+
+ for (i = 0; i < n_taps4; i += 16) {
+ ty = (__m256)_mm256_lddqu_si256((__m256i*)(s + i + 0));
+ sy[0] = _mm256_fmadd_ps(ty, _mm256_load_ps(t0 + i + 0), sy[0]);
+ sy[1] = _mm256_fmadd_ps(ty, _mm256_load_ps(t1 + i + 0), sy[1]);
+ ty = (__m256)_mm256_lddqu_si256((__m256i*)(s + i + 8));
+ sy[0] = _mm256_fmadd_ps(ty, _mm256_load_ps(t0 + i + 8), sy[0]);
+ sy[1] = _mm256_fmadd_ps(ty, _mm256_load_ps(t1 + i + 8), sy[1]);
+ }
+ sx[0] = _mm_add_ps(_mm256_extractf128_ps(sy[0], 0), _mm256_extractf128_ps(sy[0], 1));
+ sx[1] = _mm_add_ps(_mm256_extractf128_ps(sy[1], 0), _mm256_extractf128_ps(sy[1], 1));
+
+ for (; i < n_taps; i += 8) {
+ tx = (__m128)_mm_lddqu_si128((__m128i*)(s + i + 0));
+ sx[0] = _mm_fmadd_ps(tx, _mm_load_ps(t0 + i + 0), sx[0]);
+ sx[1] = _mm_fmadd_ps(tx, _mm_load_ps(t1 + i + 0), sx[1]);
+ tx = (__m128)_mm_lddqu_si128((__m128i*)(s + i + 4));
+ sx[0] = _mm_fmadd_ps(tx, _mm_load_ps(t0 + i + 4), sx[0]);
+ sx[1] = _mm_fmadd_ps(tx, _mm_load_ps(t1 + i + 4), sx[1]);
+ }
+ sx[1] = _mm_mul_ps(_mm_sub_ps(sx[1], sx[0]), _mm_load1_ps(&x));
+ sx[0] = _mm_add_ps(sx[0], sx[1]);
+ sx[0] = _mm_hadd_ps(sx[0], sx[0]);
+ sx[0] = _mm_hadd_ps(sx[0], sx[0]);
+ _mm_store_ss(d, sx[0]);
+}
+
+MAKE_RESAMPLER_FULL(avx);
+MAKE_RESAMPLER_INTER(avx);
diff --git a/spa/plugins/audioconvert/resample-native-c.c b/spa/plugins/audioconvert/resample-native-c.c
new file mode 100644
index 0000000..ce6c57d
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-native-c.c
@@ -0,0 +1,65 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "resample-native-impl.h"
+
+static inline void inner_product_c(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT taps, uint32_t n_taps)
+{
+ float sum = 0.0f;
+#if 1
+ uint32_t i, j, nt2 = n_taps/2;
+ for (i = 0, j = n_taps-1; i < nt2; i++, j--)
+ sum += s[i] * taps[i] + s[j] * taps[j];
+#else
+ uint32_t i;
+ for (i = 0; i < n_taps; i++)
+ sum += s[i] * taps[i];
+#endif
+ *d = sum;
+}
+
+static inline void inner_product_ip_c(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x,
+ uint32_t n_taps)
+{
+ float sum[2] = { 0.0f, 0.0f };
+ uint32_t i;
+#if 1
+ uint32_t j, nt2 = n_taps/2;
+ for (i = 0, j = n_taps-1; i < nt2; i++, j--) {
+ sum[0] += s[i] * t0[i] + s[j] * t0[j];
+ sum[1] += s[i] * t1[i] + s[j] * t1[j];
+ }
+#else
+ for (i = 0; i < n_taps; i++) {
+ sum[0] += s[i] * t0[i];
+ sum[1] += s[i] * t1[i];
+ }
+#endif
+ *d = (sum[1] - sum[0]) * x + sum[0];
+}
+
+MAKE_RESAMPLER_FULL(c);
+MAKE_RESAMPLER_INTER(c);
diff --git a/spa/plugins/audioconvert/resample-native-impl.h b/spa/plugins/audioconvert/resample-native-impl.h
new file mode 100644
index 0000000..5dfc40e
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-native-impl.h
@@ -0,0 +1,191 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "resample.h"
+
+typedef void (*resample_func_t)(struct resample *r,
+ const void * SPA_RESTRICT src[], uint32_t ioffs, uint32_t *in_len,
+ void * SPA_RESTRICT dst[], uint32_t ooffs, uint32_t *out_len);
+
+struct resample_info {
+ uint32_t format;
+ resample_func_t process_copy;
+ const char *copy_name;
+ resample_func_t process_full;
+ const char *full_name;
+ resample_func_t process_inter;
+ const char *inter_name;
+ uint32_t cpu_flags;
+};
+
+struct native_data {
+ double rate;
+ uint32_t n_taps;
+ uint32_t n_phases;
+ uint32_t in_rate;
+ uint32_t out_rate;
+ uint32_t phase;
+ uint32_t inc;
+ uint32_t frac;
+ uint32_t filter_stride;
+ uint32_t filter_stride_os;
+ uint32_t hist;
+ float **history;
+ resample_func_t func;
+ float *filter;
+ float *hist_mem;
+ const struct resample_info *info;
+};
+
+#define DEFINE_RESAMPLER(type,arch) \
+void do_resample_##type##_##arch(struct resample *r, \
+ const void * SPA_RESTRICT src[], uint32_t ioffs, uint32_t *in_len, \
+ void * SPA_RESTRICT dst[], uint32_t ooffs, uint32_t *out_len)
+
+#define MAKE_RESAMPLER_COPY(arch) \
+DEFINE_RESAMPLER(copy,arch) \
+{ \
+ struct native_data *data = r->data; \
+ uint32_t index, n_taps = data->n_taps, n_taps2 = n_taps/2; \
+ uint32_t c, olen = *out_len, ilen = *in_len; \
+ \
+ if (r->channels == 0) \
+ return; \
+ \
+ index = ioffs; \
+ if (ooffs < olen && index + n_taps <= ilen) { \
+ uint32_t to_copy = SPA_MIN(olen - ooffs, \
+ ilen - (index + n_taps) + 1); \
+ for (c = 0; c < r->channels; c++) { \
+ const float *s = src[c]; \
+ float *d = dst[c]; \
+ spa_memcpy(&d[ooffs], &s[index + n_taps2], \
+ to_copy * sizeof(float)); \
+ } \
+ index += to_copy; \
+ ooffs += to_copy; \
+ } \
+ *in_len = index; \
+ *out_len = ooffs; \
+}
+
+#define INC(index,phase,n_phases) \
+ index += inc; \
+ phase += frac; \
+ if (phase >= n_phases) { \
+ phase -= n_phases; \
+ index += 1; \
+ }
+
+#define MAKE_RESAMPLER_FULL(arch) \
+DEFINE_RESAMPLER(full,arch) \
+{ \
+ struct native_data *data = r->data; \
+ uint32_t n_taps = data->n_taps, stride = data->filter_stride_os; \
+ uint32_t index, phase, n_phases = data->out_rate; \
+ uint32_t c, o, olen = *out_len, ilen = *in_len; \
+ uint32_t inc = data->inc, frac = data->frac; \
+ \
+ if (r->channels == 0) \
+ return; \
+ \
+ for (c = 0; c < r->channels; c++) { \
+ const float *s = src[c]; \
+ float *d = dst[c]; \
+ \
+ index = ioffs; \
+ phase = data->phase; \
+ \
+ for (o = ooffs; o < olen && index + n_taps <= ilen; o++) { \
+ inner_product_##arch(&d[o], &s[index], \
+ &data->filter[phase * stride], \
+ n_taps); \
+ INC(index, phase, n_phases); \
+ } \
+ } \
+ *in_len = index; \
+ *out_len = o; \
+ data->phase = phase; \
+}
+
+#define MAKE_RESAMPLER_INTER(arch) \
+DEFINE_RESAMPLER(inter,arch) \
+{ \
+ struct native_data *data = r->data; \
+ uint32_t index, phase, stride = data->filter_stride; \
+ uint32_t n_phases = data->n_phases, out_rate = data->out_rate; \
+ uint32_t n_taps = data->n_taps; \
+ uint32_t c, o, olen = *out_len, ilen = *in_len; \
+ uint32_t inc = data->inc, frac = data->frac; \
+ \
+ if (r->channels == 0) \
+ return; \
+ \
+ for (c = 0; c < r->channels; c++) { \
+ const float *s = src[c]; \
+ float *d = dst[c]; \
+ \
+ index = ioffs; \
+ phase = data->phase; \
+ \
+ for (o = ooffs; o < olen && index + n_taps <= ilen; o++) { \
+ float ph = (float)phase * n_phases / out_rate; \
+ uint32_t offset = floorf(ph); \
+ inner_product_ip_##arch(&d[o], &s[index], \
+ &data->filter[(offset + 0) * stride], \
+ &data->filter[(offset + 1) * stride], \
+ ph - offset, n_taps); \
+ INC(index, phase, out_rate); \
+ } \
+ } \
+ *in_len = index; \
+ *out_len = o; \
+ data->phase = phase; \
+}
+
+
+DEFINE_RESAMPLER(copy,c);
+DEFINE_RESAMPLER(full,c);
+DEFINE_RESAMPLER(inter,c);
+
+#if defined (HAVE_NEON)
+DEFINE_RESAMPLER(full,neon);
+DEFINE_RESAMPLER(inter,neon);
+#endif
+#if defined (HAVE_SSE)
+DEFINE_RESAMPLER(full,sse);
+DEFINE_RESAMPLER(inter,sse);
+#endif
+#if defined (HAVE_SSSE3)
+DEFINE_RESAMPLER(full,ssse3);
+DEFINE_RESAMPLER(inter,ssse3);
+#endif
+#if defined (HAVE_AVX) && defined(HAVE_FMA)
+DEFINE_RESAMPLER(full,avx);
+DEFINE_RESAMPLER(inter,avx);
+#endif
diff --git a/spa/plugins/audioconvert/resample-native-neon.c b/spa/plugins/audioconvert/resample-native-neon.c
new file mode 100644
index 0000000..079152a
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-native-neon.c
@@ -0,0 +1,218 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "resample-native-impl.h"
+
+#include <arm_neon.h>
+
+static inline void inner_product_neon(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT taps, uint32_t n_taps)
+{
+ unsigned int remainder = n_taps % 16;
+ n_taps = n_taps - remainder;
+
+#ifdef __aarch64__
+ asm volatile(
+ " cmp %[n_taps], #0\n"
+ " bne 1f\n"
+ " ld1 {v4.4s}, [%[taps]], #16\n"
+ " ld1 {v8.4s}, [%[s]], #16\n"
+ " subs %[remainder], %[remainder], #4\n"
+ " fmul v0.4s, v4.4s, v8.4s\n"
+ " bne 4f\n"
+ " b 5f\n"
+ "1:"
+ " ld1 {v4.4s, v5.4s, v6.4s, v7.4s}, [%[taps]], #64\n"
+ " ld1 {v8.4s, v9.4s, v10.4s, v11.4s}, [%[s]], #64\n"
+ " subs %[n_taps], %[n_taps], #16\n"
+ " fmul v0.4s, v4.4s, v8.4s\n"
+ " fmul v1.4s, v5.4s, v9.4s\n"
+ " fmul v2.4s, v6.4s, v10.4s\n"
+ " fmul v3.4s, v7.4s, v11.4s\n"
+ " beq 3f\n"
+ "2:"
+ " ld1 {v4.4s, v5.4s, v6.4s, v7.4s}, [%[taps]], #64\n"
+ " ld1 {v8.4s, v9.4s, v10.4s, v11.4s}, [%[s]], #64\n"
+ " subs %[n_taps], %[n_taps], #16\n"
+ " fmla v0.4s, v4.4s, v8.4s\n"
+ " fmla v1.4s, v5.4s, v9.4s\n"
+ " fmla v2.4s, v6.4s, v10.4s\n"
+ " fmla v3.4s, v7.4s, v11.4s\n"
+ " bne 2b\n"
+ "3:"
+ " fadd v4.4s, v0.4s, v1.4s\n"
+ " fadd v5.4s, v2.4s, v3.4s\n"
+ " cmp %[remainder], #0\n"
+ " fadd v0.4s, v4.4s, v5.4s\n"
+ " beq 5f\n"
+ "4:"
+ " ld1 {v6.4s}, [%[taps]], #16\n"
+ " ld1 {v10.4s}, [%[s]], #16\n"
+ " subs %[remainder], %[remainder], #4\n"
+ " fmla v0.4s, v6.4s, v10.4s\n"
+ " bne 4b\n"
+ "5:"
+ " faddp v0.4s, v0.4s, v0.4s\n"
+ " faddp v0.2s, v0.2s, v0.2s\n"
+ " str s0, [%[d]]\n"
+ : [d] "+r" (d), [s] "+r" (s), [taps] "+r" (taps),
+ [n_taps] "+r" (n_taps), [remainder] "+r" (remainder)
+ :
+ : "cc", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8",
+ "v9", "v10", "v11");
+#else
+ asm volatile (
+ " cmp %[n_taps], #0\n"
+ " bne 1f\n"
+ " vld1.32 {q4}, [%[taps] :128]!\n"
+ " vld1.32 {q8}, [%[s]]!\n"
+ " subs %[remainder], %[remainder], #4\n"
+ " vmul.f32 q0, q4, q8\n"
+ " bne 4f\n"
+ " b 5f\n"
+ "1:"
+ " vld1.32 {q4, q5}, [%[taps] :128]!\n"
+ " vld1.32 {q8, q9}, [%[s]]!\n"
+ " vld1.32 {q6, q7}, [%[taps] :128]!\n"
+ " vld1.32 {q10, q11}, [%[s]]!\n"
+ " subs %[n_taps], %[n_taps], #16\n"
+ " vmul.f32 q0, q4, q8\n"
+ " vmul.f32 q1, q5, q9\n"
+ " vmul.f32 q2, q6, q10\n"
+ " vmul.f32 q3, q7, q11\n"
+ " beq 3f\n"
+ "2:"
+ " vld1.32 {q4, q5}, [%[taps] :128]!\n"
+ " vld1.32 {q8, q9}, [%[s]]!\n"
+ " vld1.32 {q6, q7}, [%[taps] :128]!\n"
+ " vld1.32 {q10, q11}, [%[s]]!\n"
+ " subs %[n_taps], %[n_taps], #16\n"
+ " vmla.f32 q0, q4, q8\n"
+ " vmla.f32 q1, q5, q9\n"
+ " vmla.f32 q2, q6, q10\n"
+ " vmla.f32 q3, q7, q11\n"
+ " bne 2b\n"
+ "3:"
+ " vadd.f32 q4, q0, q1\n"
+ " vadd.f32 q5, q2, q3\n"
+ " cmp %[remainder], #0\n"
+ " vadd.f32 q0, q4, q5\n"
+ " beq 5f\n"
+ "4:"
+ " vld1.32 {q6}, [%[taps] :128]!\n"
+ " vld1.32 {q10}, [%[s]]!\n"
+ " subs %[remainder], %[remainder], #4\n"
+ " vmla.f32 q0, q6, q10\n"
+ " bne 4b\n"
+ "5:"
+ " vadd.f32 d0, d0, d1\n"
+ " vpadd.f32 d0, d0, d0\n"
+ " vstr d0, [%[d]]\n"
+ : [d] "+r" (d), [s] "+r" (s), [taps] "+r" (taps),
+ [n_taps] "+l" (n_taps), [remainder] "+l" (remainder)
+ :
+ : "cc", "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8",
+ "q9", "q10", "q11");
+#endif
+}
+
+static inline void inner_product_ip_neon(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x,
+ uint32_t n_taps)
+{
+#ifdef __aarch64__
+ asm volatile(
+ " dup v10.4s, %w[x]\n"
+ " ld1 {v4.4s, v5.4s}, [%[t0]], #32\n"
+ " ld1 {v8.4s, v9.4s}, [%[s]], #32\n"
+ " ld1 {v6.4s, v7.4s}, [%[t1]], #32\n"
+ " subs %[n_taps], %[n_taps], #8\n"
+ " fmul v0.4s, v4.4s, v8.4s\n"
+ " fmul v1.4s, v5.4s, v9.4s\n"
+ " fmul v2.4s, v6.4s, v8.4s\n"
+ " fmul v3.4s, v7.4s, v9.4s\n"
+ " beq 3f\n"
+ "2:"
+ " ld1 {v4.4s, v5.4s}, [%[t0]], #32\n"
+ " ld1 {v8.4s, v9.4s}, [%[s]], #32\n"
+ " ld1 {v6.4s, v7.4s}, [%[t1]], #32\n"
+ " subs %[n_taps], %[n_taps], #8\n"
+ " fmla v0.4s, v4.4s, v8.4s\n"
+ " fmla v1.4s, v5.4s, v9.4s\n"
+ " fmla v2.4s, v6.4s, v8.4s\n"
+ " fmla v3.4s, v7.4s, v9.4s\n"
+ " bne 2b\n"
+ "3:"
+ " fadd v0.4s, v0.4s, v1.4s\n" /* sum[0] */
+ " fadd v2.4s, v2.4s, v3.4s\n" /* sum[1] */
+ " fsub v2.4s, v2.4s, v0.4s\n" /* sum[1] -= sum[0] */
+ " fmla v0.4s, v2.4s, v10.4s\n" /* sum[0] += sum[1] * x */
+ " faddp v0.4s, v0.4s, v0.4s\n"
+ " faddp v0.2s, v0.2s, v0.2s\n"
+ " str s0, [%[d]]\n"
+ : [d] "+r" (d), [s] "+r" (s), [t0] "+r" (t0), [t1] "+r" (t1),
+ [n_taps] "+r" (n_taps), [x] "+r" (x)
+ :
+ : "cc", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8",
+ "v9", "v10");
+#else
+ asm volatile(
+ " vdup.32 q10, %[x]\n"
+ " vld1.32 {q4, q5}, [%[t0] :128]!\n"
+ " vld1.32 {q8, q9}, [%[s]]!\n"
+ " vld1.32 {q6, q7}, [%[t1] :128]!\n"
+ " subs %[n_taps], %[n_taps], #8\n"
+ " vmul.f32 q0, q4, q8\n"
+ " vmul.f32 q1, q5, q9\n"
+ " vmul.f32 q2, q6, q8\n"
+ " vmul.f32 q3, q7, q9\n"
+ " beq 3f\n"
+ "2:"
+ " vld1.32 {q4, q5}, [%[t0] :128]!\n"
+ " vld1.32 {q8, q9}, [%[s]]!\n"
+ " vld1.32 {q6, q7}, [%[t1] :128]!\n"
+ " subs %[n_taps], %[n_taps], #8\n"
+ " vmla.f32 q0, q4, q8\n"
+ " vmla.f32 q1, q5, q9\n"
+ " vmla.f32 q2, q6, q8\n"
+ " vmla.f32 q3, q7, q9\n"
+ " bne 2b\n"
+ "3:"
+ " vadd.f32 q0, q0, q1\n" /* sum[0] */
+ " vadd.f32 q2, q2, q3\n" /* sum[1] */
+ " vsub.f32 q2, q2, q0\n" /* sum[1] -= sum[0] */
+ " vmla.f32 q0, q2, q10\n" /* sum[0] += sum[1] * x */
+ " vadd.f32 d0, d0, d1\n"
+ " vpadd.f32 d0, d0, d0\n"
+ " vstr d0, [%[d]]\n"
+ : [d] "+r" (d), [s] "+r" (s), [t0] "+r" (t0), [t1] "+r" (t1),
+ [n_taps] "+l" (n_taps), [x] "+l" (x)
+ :
+ : "cc", "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8",
+ "q9", "q10");
+#endif
+}
+
+MAKE_RESAMPLER_FULL(neon);
+MAKE_RESAMPLER_INTER(neon);
diff --git a/spa/plugins/audioconvert/resample-native-sse.c b/spa/plugins/audioconvert/resample-native-sse.c
new file mode 100644
index 0000000..fcdb32c
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-native-sse.c
@@ -0,0 +1,94 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "resample-native-impl.h"
+
+#include <xmmintrin.h>
+
+static inline void inner_product_sse(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT taps, uint32_t n_taps)
+{
+ __m128 sum = _mm_setzero_ps();
+ uint32_t i = 0;
+#if 0
+ uint32_t unrolled = n_taps & ~15;
+
+ for (i = 0; i < unrolled; i += 16) {
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(
+ _mm_loadu_ps(s + i + 0),
+ _mm_load_ps(taps + i + 0)));
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(
+ _mm_loadu_ps(s + i + 4),
+ _mm_load_ps(taps + i + 4)));
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(
+ _mm_loadu_ps(s + i + 8),
+ _mm_load_ps(taps + i + 8)));
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(
+ _mm_loadu_ps(s + i + 12),
+ _mm_load_ps(taps + i + 12)));
+ }
+#endif
+ for (; i < n_taps; i += 8) {
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(
+ _mm_loadu_ps(s + i + 0),
+ _mm_load_ps(taps + i + 0)));
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(
+ _mm_loadu_ps(s + i + 4),
+ _mm_load_ps(taps + i + 4)));
+ }
+ sum = _mm_add_ps(sum, _mm_movehl_ps(sum, sum));
+ sum = _mm_add_ss(sum, _mm_shuffle_ps(sum, sum, 0x55));
+ _mm_store_ss(d, sum);
+}
+
+static inline void inner_product_ip_sse(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x,
+ uint32_t n_taps)
+{
+ __m128 sum[2] = { _mm_setzero_ps (), _mm_setzero_ps () }, t;
+ uint32_t i;
+
+ for (i = 0; i < n_taps; i += 8) {
+ t = _mm_loadu_ps(s + i + 0);
+ sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(t, _mm_load_ps(t0 + i + 0)));
+ sum[1] = _mm_add_ps(sum[1], _mm_mul_ps(t, _mm_load_ps(t1 + i + 0)));
+ t = _mm_loadu_ps(s + i + 4);
+ sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(t, _mm_load_ps(t0 + i + 4)));
+ sum[1] = _mm_add_ps(sum[1], _mm_mul_ps(t, _mm_load_ps(t1 + i + 4)));
+ }
+ sum[1] = _mm_mul_ps(_mm_sub_ps(sum[1], sum[0]), _mm_load1_ps(&x));
+ sum[0] = _mm_add_ps(sum[0], sum[1]);
+ 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));
+ _mm_store_ss(d, sum[0]);
+}
+
+MAKE_RESAMPLER_FULL(sse);
+MAKE_RESAMPLER_INTER(sse);
diff --git a/spa/plugins/audioconvert/resample-native-ssse3.c b/spa/plugins/audioconvert/resample-native-ssse3.c
new file mode 100644
index 0000000..ac3675f
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-native-ssse3.c
@@ -0,0 +1,115 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "resample-native-impl.h"
+
+#include <tmmintrin.h>
+
+static inline void inner_product_ssse3(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT taps, uint32_t n_taps)
+{
+ __m128 sum = _mm_setzero_ps();
+ __m128 t0, t1;
+ uint32_t i;
+
+ switch (SPA_PTR_ALIGNMENT(s, 16)) {
+ case 0:
+ for (i = 0; i < n_taps; i += 8) {
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(
+ _mm_load_ps(s + i + 0),
+ _mm_load_ps(taps + i + 0)));
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(
+ _mm_load_ps(s + i + 4),
+ _mm_load_ps(taps + i + 4)));
+ }
+ break;
+ case 4:
+ t0 = _mm_load_ps(s - 1);
+ for (i = 0; i < n_taps; i += 8) {
+ t1 = _mm_load_ps(s + i + 3);
+ t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 4);
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(t0, _mm_load_ps(taps + i + 0)));
+ t0 = t1;
+ t1 = _mm_load_ps(s + i + 7);
+ t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 4);
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(t0, _mm_load_ps(taps + i + 4)));
+ t0 = t1;
+ }
+ break;
+ case 8:
+ t0 = _mm_load_ps(s - 2);
+ for (i = 0; i < n_taps; i += 8) {
+ t1 = _mm_load_ps(s + i + 2);
+ t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 8);
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(t0, _mm_load_ps(taps + i + 0)));
+ t0 = t1;
+ t1 = _mm_load_ps(s + i + 6);
+ t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 8);
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(t0, _mm_load_ps(taps + i + 4)));
+ t0 = t1;
+ }
+ break;
+ case 12:
+ t0 = _mm_load_ps(s - 3);
+ for (i = 0; i < n_taps; i += 8) {
+ t1 = _mm_load_ps(s + i + 1);
+ t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 12);
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(t0, _mm_load_ps(taps + i + 0)));
+ t0 = t1;
+ t1 = _mm_load_ps(s + i + 5);
+ t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 12);
+ sum = _mm_add_ps(sum,
+ _mm_mul_ps(t0, _mm_load_ps(taps + i + 4)));
+ t0 = t1;
+ }
+ break;
+ }
+ sum = _mm_add_ps(sum, _mm_movehdup_ps(sum));
+ sum = _mm_add_ss(sum, _mm_movehl_ps(sum, sum));
+ _mm_store_ss(d, sum);
+}
+
+static inline void inner_product_ip_ssse3(float *d, const float * SPA_RESTRICT s,
+ const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x,
+ uint32_t n_taps)
+{
+ float sum[2] = { 0.0f, 0.0f };
+ uint32_t i;
+
+ for (i = 0; i < n_taps; i++) {
+ sum[0] += s[i] * t0[i];
+ sum[1] += s[i] * t1[i];
+ }
+ *d = (sum[1] - sum[0]) * x + sum[0];
+}
+
+MAKE_RESAMPLER_FULL(ssse3);
+MAKE_RESAMPLER_INTER(ssse3);
diff --git a/spa/plugins/audioconvert/resample-native.c b/spa/plugins/audioconvert/resample-native.c
new file mode 100644
index 0000000..05ce54b
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-native.c
@@ -0,0 +1,400 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/param/audio/format.h>
+
+#include "resample-native-impl.h"
+
+struct quality {
+ uint32_t n_taps;
+ double cutoff;
+};
+
+static const struct quality window_qualities[] = {
+ { 8, 0.53, },
+ { 16, 0.67, },
+ { 24, 0.75, },
+ { 32, 0.80, },
+ { 48, 0.85, }, /* default */
+ { 64, 0.88, },
+ { 80, 0.895, },
+ { 96, 0.910, },
+ { 128, 0.936, },
+ { 144, 0.945, },
+ { 160, 0.950, },
+ { 192, 0.960, },
+ { 256, 0.970, },
+ { 896, 0.990, },
+ { 1024, 0.995, },
+};
+
+static inline double sinc(double x)
+{
+ if (x < 1e-6) return 1.0;
+ x *= M_PI;
+ return sin(x) / x;
+}
+
+static inline double window_blackman(double x, double n_taps)
+{
+ double alpha = 0.232, r;
+ x = 2.0 * M_PI * x / n_taps;
+ r = (1.0 - alpha) / 2.0 + (1.0 / 2.0) * cos(x) +
+ (alpha / 2.0) * cos(2.0 * x);
+ return r;
+}
+
+static inline double window_cosh(double x, double n_taps)
+{
+ double r;
+ double A = 16.97789;
+ double x2;
+ x = 2.0 * x / n_taps;
+ x2 = x * x;
+ if (x2 >= 1.0)
+ return 0.0;
+ /* doi:10.1109/RME.2008.4595727 with tweak */
+ r = (exp(A * sqrt(1 - x2)) - 1) / (exp(A) - 1);
+ return r;
+}
+
+#define window (1 ? window_cosh : window_blackman)
+
+static int build_filter(float *taps, uint32_t stride, uint32_t n_taps, uint32_t n_phases, double cutoff)
+{
+ uint32_t i, j, n_taps12 = n_taps/2;
+
+ for (i = 0; i <= n_phases; i++) {
+ double t = (double) i / (double) n_phases;
+ for (j = 0; j < n_taps12; j++, t += 1.0) {
+ /* exploit symmetry in filter taps */
+ taps[(n_phases - i) * stride + n_taps12 + j] =
+ taps[i * stride + (n_taps12 - j - 1)] =
+ cutoff * sinc(t * cutoff) * window(t, n_taps);
+ }
+ }
+ return 0;
+}
+
+MAKE_RESAMPLER_COPY(c);
+
+#define MAKE(fmt,copy,full,inter,...) \
+ { SPA_AUDIO_FORMAT_ ##fmt, do_resample_ ##copy, #copy, \
+ do_resample_ ##full, #full, do_resample_ ##inter, #inter, __VA_ARGS__ }
+
+static struct resample_info resample_table[] =
+{
+#if defined (HAVE_NEON)
+ MAKE(F32, copy_c, full_neon, inter_neon, SPA_CPU_FLAG_NEON),
+#endif
+#if defined(HAVE_AVX) && defined(HAVE_FMA)
+ MAKE(F32, copy_c, full_avx, inter_avx, SPA_CPU_FLAG_AVX | SPA_CPU_FLAG_FMA3),
+#endif
+#if defined (HAVE_SSSE3)
+ MAKE(F32, copy_c, full_ssse3, inter_ssse3, SPA_CPU_FLAG_SSSE3 | SPA_CPU_FLAG_SLOW_UNALIGNED),
+#endif
+#if defined (HAVE_SSE)
+ MAKE(F32, copy_c, full_sse, inter_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(F32, copy_c, full_c, inter_c),
+};
+#undef MAKE
+
+#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a)
+static const struct resample_info *find_resample_info(uint32_t format, uint32_t cpu_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(resample_table, t) {
+ if (t->format == format &&
+ MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+ return t;
+ }
+ return NULL;
+}
+
+static void impl_native_free(struct resample *r)
+{
+ spa_log_debug(r->log, "native %p: free", r);
+ free(r->data);
+ r->data = NULL;
+}
+
+static inline uint32_t calc_gcd(uint32_t a, uint32_t b)
+{
+ while (b != 0) {
+ uint32_t temp = a;
+ a = b;
+ b = temp % b;
+ }
+ return a;
+}
+
+static void impl_native_update_rate(struct resample *r, double rate)
+{
+ struct native_data *data = r->data;
+ uint32_t in_rate, out_rate, phase, gcd, old_out_rate;
+
+ if (SPA_LIKELY(data->rate == rate))
+ return;
+
+ old_out_rate = data->out_rate;
+ in_rate = r->i_rate / rate;
+ out_rate = r->o_rate;
+ phase = data->phase;
+
+ gcd = calc_gcd(in_rate, out_rate);
+ in_rate /= gcd;
+ out_rate /= gcd;
+
+ data->rate = rate;
+ data->phase = phase * out_rate / old_out_rate;
+ data->in_rate = in_rate;
+ data->out_rate = out_rate;
+
+ data->inc = data->in_rate / data->out_rate;
+ data->frac = data->in_rate % data->out_rate;
+
+ if (data->in_rate == data->out_rate) {
+ data->func = data->info->process_copy;
+ r->func_name = data->info->copy_name;
+ }
+ else if (rate == 1.0) {
+ data->func = data->info->process_full;
+ r->func_name = data->info->full_name;
+ }
+ else {
+ data->func = data->info->process_inter;
+ r->func_name = data->info->inter_name;
+ }
+
+ spa_log_trace_fp(r->log, "native %p: rate:%f in:%d out:%d phase:%d inc:%d frac:%d", r,
+ rate, data->in_rate, data->out_rate, data->phase, data->inc, data->frac);
+
+}
+
+static uint32_t impl_native_in_len(struct resample *r, uint32_t out_len)
+{
+ struct native_data *data = r->data;
+ uint32_t in_len;
+
+ in_len = (data->phase + out_len * data->frac) / data->out_rate;
+ in_len += out_len * data->inc + (data->n_taps - data->hist);
+
+ spa_log_trace_fp(r->log, "native %p: hist:%d %d->%d", r, data->hist, out_len, in_len);
+
+ return in_len;
+}
+
+static void impl_native_process(struct resample *r,
+ const void * SPA_RESTRICT src[], uint32_t *in_len,
+ void * SPA_RESTRICT dst[], uint32_t *out_len)
+{
+ struct native_data *data = r->data;
+ uint32_t n_taps = data->n_taps;
+ float **history = data->history;
+ const float **s = (const float **)src;
+ uint32_t c, refill, hist, in, out, remain;
+
+ hist = data->hist;
+ refill = 0;
+
+ if (SPA_LIKELY(hist)) {
+ /* first work on the history if any. */
+ if (SPA_UNLIKELY(hist <= n_taps)) {
+ /* we need at least n_taps to completely process the
+ * history before we can work on the new input. When
+ * we have less, refill the history. */
+ refill = SPA_MIN(*in_len, n_taps-1);
+ for (c = 0; c < r->channels; c++)
+ spa_memcpy(&history[c][hist], s[c], refill * sizeof(float));
+
+ if (SPA_UNLIKELY(hist + refill < n_taps)) {
+ /* not enough in the history, keep the input in
+ * the history and produce no output */
+ data->hist = hist + refill;
+ *in_len = refill;
+ *out_len = 0;
+ return;
+ }
+ }
+ /* now we have at least n_taps of data in the history
+ * and we try to process it */
+ in = hist + refill;
+ out = *out_len;
+ data->func(r, (const void**)history, 0, &in, dst, 0, &out);
+ spa_log_trace_fp(r->log, "native %p: in:%d/%d out %d/%d hist:%d",
+ r, hist + refill, in, *out_len, out, hist);
+ } else {
+ out = in = 0;
+ }
+
+ if (SPA_LIKELY(in >= hist)) {
+ int skip = in - hist;
+ /* we are past the history and can now work on the new
+ * input data */
+ in = *in_len;
+ data->func(r, src, skip, &in, dst, out, out_len);
+
+ spa_log_trace_fp(r->log, "native %p: in:%d/%d out %d/%d skip:%d",
+ r, *in_len, in, *out_len, out, skip);
+
+ remain = *in_len - in;
+ if (remain > 0 && remain <= n_taps) {
+ /* not enough input data remaining for more output,
+ * copy to history */
+ for (c = 0; c < r->channels; c++)
+ spa_memcpy(history[c], &s[c][in], remain * sizeof(float));
+ } else {
+ /* we have enough input data remaining to produce
+ * more output ask to resubmit. */
+ remain = 0;
+ *in_len = in;
+ }
+ } else {
+ /* we are still working on the history */
+ *out_len = out;
+ remain = hist - in;
+ if (*in_len < n_taps) {
+ /* not enough input data, add it to the history because
+ * resubmitting it is not going to make progress.
+ * We copied this into the history above. */
+ remain += refill;
+ } else {
+ /* input has enough data to possibly produce more output
+ * from the history so ask to resubmit */
+ *in_len = 0;
+ }
+ if (remain) {
+ /* move history */
+ for (c = 0; c < r->channels; c++)
+ spa_memmove(history[c], &history[c][in], remain * sizeof(float));
+ }
+ spa_log_trace_fp(r->log, "native %p: in:%d remain:%d", r, in, remain);
+
+ }
+ data->hist = remain;
+ return;
+}
+
+static void impl_native_reset (struct resample *r)
+{
+ struct native_data *d = r->data;
+ if (d == NULL)
+ return;
+ memset(d->hist_mem, 0, r->channels * sizeof(float) * d->n_taps * 2);
+ if (r->options & RESAMPLE_OPTION_PREFILL)
+ d->hist = d->n_taps - 1;
+ else
+ d->hist = (d->n_taps / 2) - 1;
+ d->phase = 0;
+}
+
+static uint32_t impl_native_delay (struct resample *r)
+{
+ struct native_data *d = r->data;
+ return d->n_taps / 2;
+}
+
+int resample_native_init(struct resample *r)
+{
+ struct native_data *d;
+ const struct quality *q;
+ double scale;
+ uint32_t c, n_taps, n_phases, filter_size, in_rate, out_rate, gcd, filter_stride;
+ uint32_t history_stride, history_size, oversample;
+
+ r->quality = SPA_CLAMP(r->quality, 0, (int) SPA_N_ELEMENTS(window_qualities) - 1);
+ r->free = impl_native_free;
+ r->update_rate = impl_native_update_rate;
+ r->in_len = impl_native_in_len;
+ r->process = impl_native_process;
+ r->reset = impl_native_reset;
+ r->delay = impl_native_delay;
+
+ q = &window_qualities[r->quality];
+
+ gcd = calc_gcd(r->i_rate, r->o_rate);
+
+ in_rate = r->i_rate / gcd;
+ out_rate = r->o_rate / gcd;
+
+ scale = SPA_MIN(q->cutoff * out_rate / in_rate, q->cutoff);
+
+ /* multiple of 8 taps to ease simd optimizations */
+ n_taps = SPA_ROUND_UP_N((uint32_t)ceil(q->n_taps / scale), 8);
+ n_taps = SPA_MIN(n_taps, 1u << 18);
+
+ /* try to get at least 256 phases so that interpolation is
+ * accurate enough when activated */
+ n_phases = out_rate;
+ oversample = (255 + n_phases) / n_phases;
+ n_phases *= oversample;
+
+ filter_stride = SPA_ROUND_UP_N(n_taps * sizeof(float), 64);
+ filter_size = filter_stride * (n_phases + 1);
+ history_stride = SPA_ROUND_UP_N(2 * n_taps * sizeof(float), 64);
+ history_size = r->channels * history_stride;
+
+ d = calloc(1, sizeof(struct native_data) +
+ filter_size +
+ history_size +
+ (r->channels * sizeof(float*)) +
+ 64);
+
+ if (d == NULL)
+ return -errno;
+
+ r->data = d;
+ d->n_taps = n_taps;
+ d->n_phases = n_phases;
+ d->in_rate = in_rate;
+ d->out_rate = out_rate;
+ d->filter = SPA_PTROFF_ALIGN(d, sizeof(struct native_data), 64, float);
+ d->hist_mem = SPA_PTROFF_ALIGN(d->filter, filter_size, 64, float);
+ d->history = SPA_PTROFF(d->hist_mem, history_size, float*);
+ d->filter_stride = filter_stride / sizeof(float);
+ d->filter_stride_os = d->filter_stride * oversample;
+ 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);
+
+ d->info = find_resample_info(SPA_AUDIO_FORMAT_F32, r->cpu_flags);
+ if (SPA_UNLIKELY(d->info == NULL)) {
+ spa_log_error(r->log, "failed to find suitable resample format!");
+ return -ENOTSUP;
+ }
+
+ spa_log_debug(r->log, "native %p: q:%d in:%d out:%d n_taps:%d n_phases:%d features:%08x:%08x",
+ r, r->quality, in_rate, out_rate, n_taps, n_phases,
+ r->cpu_flags, d->info->cpu_flags);
+
+ r->cpu_flags = d->info->cpu_flags;
+
+ impl_native_reset(r);
+ impl_native_update_rate(r, 1.0);
+
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/resample-peaks.c b/spa/plugins/audioconvert/resample-peaks.c
new file mode 100644
index 0000000..c151d60
--- /dev/null
+++ b/spa/plugins/audioconvert/resample-peaks.c
@@ -0,0 +1,148 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <math.h>
+#include <errno.h>
+
+#include <spa/param/audio/format.h>
+
+#include "peaks-ops.h"
+#include "resample.h"
+
+struct peaks_data {
+ uint32_t o_count;
+ uint32_t i_count;
+ struct peaks peaks;
+ float max_f[];
+};
+
+static void resample_peaks_process(struct resample *r,
+ const void * SPA_RESTRICT src[], uint32_t *in_len,
+ void * SPA_RESTRICT dst[], uint32_t *out_len)
+{
+ struct peaks_data *pd = r->data;
+ uint32_t c, i, o, end, chunk, i_count, o_count;
+
+ if (SPA_UNLIKELY(r->channels == 0))
+ return;
+
+ for (c = 0; c < r->channels; c++) {
+ const float *s = src[c];
+ float *d = dst[c], m = pd->max_f[c];
+
+ o_count = pd->o_count;
+ i_count = pd->i_count;
+ o = i = 0;
+
+ while (i < *in_len && o < *out_len) {
+ end = ((uint64_t) (o_count + 1)
+ * r->i_rate) / r->o_rate;
+ end = end > i_count ? end - i_count : 0;
+ chunk = SPA_MIN(end, *in_len);
+
+ m = peaks_abs_max(&pd->peaks, &s[i], chunk - i, m);
+
+ i += chunk;
+
+ if (i == end) {
+ d[o++] = m;
+ m = 0.0f;
+ o_count++;
+ }
+ }
+ pd->max_f[c] = m;
+ }
+ *out_len = o;
+ *in_len = i;
+ pd->o_count = o_count;
+ pd->i_count = i_count + i;
+
+ while (pd->i_count >= r->i_rate) {
+ pd->i_count -= r->i_rate;
+ pd->o_count -= r->o_rate;
+ }
+}
+
+static void impl_peaks_free(struct resample *r)
+{
+ struct peaks_data *d = r->data;
+ if (d != NULL) {
+ peaks_free(&d->peaks);
+ free(d);
+ }
+ r->data = NULL;
+}
+
+static void impl_peaks_update_rate(struct resample *r, double rate)
+{
+}
+
+static uint32_t impl_peaks_delay (struct resample *r)
+{
+ return 0;
+}
+
+static uint32_t impl_peaks_in_len(struct resample *r, uint32_t out_len)
+{
+ return out_len;
+}
+
+static void impl_peaks_reset (struct resample *r)
+{
+ struct peaks_data *d = r->data;
+ d->i_count = d->o_count = 0;
+}
+
+int resample_peaks_init(struct resample *r)
+{
+ struct peaks_data *d;
+ int res;
+
+ r->free = impl_peaks_free;
+ r->update_rate = impl_peaks_update_rate;
+
+ d = calloc(1, sizeof(struct peaks_data) + sizeof(float) * r->channels);
+ if (d == NULL)
+ return -errno;
+
+ d->peaks.log = r->log;
+ d->peaks.cpu_flags = r->cpu_flags;
+ if ((res = peaks_init(&d->peaks)) < 0) {
+ free(d);
+ return res;
+ }
+
+ r->data = d;
+ r->process = resample_peaks_process;
+ r->reset = impl_peaks_reset;
+ r->delay = impl_peaks_delay;
+ r->in_len = impl_peaks_in_len;
+
+ 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);
+
+ r->cpu_flags = d->peaks.cpu_flags;
+ d->i_count = d->o_count = 0;
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/resample.h b/spa/plugins/audioconvert/resample.h
new file mode 100644
index 0000000..b1c89d5
--- /dev/null
+++ b/spa/plugins/audioconvert/resample.h
@@ -0,0 +1,69 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef RESAMPLE_H
+#define RESAMPLE_H
+
+#include <spa/support/cpu.h>
+#include <spa/support/log.h>
+
+#define RESAMPLE_DEFAULT_QUALITY 4
+
+struct resample {
+ struct spa_log *log;
+#define RESAMPLE_OPTION_PREFILL (1<<0)
+ uint32_t options;
+ uint32_t cpu_flags;
+ const char *func_name;
+
+ uint32_t channels;
+ uint32_t i_rate;
+ uint32_t o_rate;
+ double rate;
+ int quality;
+
+ void (*free) (struct resample *r);
+ void (*update_rate) (struct resample *r, double rate);
+ uint32_t (*in_len) (struct resample *r, uint32_t out_len);
+ uint32_t (*out_len) (struct resample *r, uint32_t in_len);
+ void (*process) (struct resample *r,
+ const void * SPA_RESTRICT src[], uint32_t *in_len,
+ void * SPA_RESTRICT dst[], uint32_t *out_len);
+ void (*reset) (struct resample *r);
+ uint32_t (*delay) (struct resample *r);
+ void *data;
+};
+
+#define resample_free(r) (r)->free(r)
+#define resample_update_rate(r,...) (r)->update_rate(r,__VA_ARGS__)
+#define resample_in_len(r,...) (r)->in_len(r,__VA_ARGS__)
+#define resample_out_len(r,...) (r)->out_len(r,__VA_ARGS__)
+#define resample_process(r,...) (r)->process(r,__VA_ARGS__)
+#define resample_reset(r) (r)->reset(r)
+#define resample_delay(r) (r)->delay(r)
+
+int resample_native_init(struct resample *r);
+int resample_peaks_init(struct resample *r);
+
+#endif /* RESAMPLE_H */
diff --git a/spa/plugins/audioconvert/spa-resample.c b/spa/plugins/audioconvert/spa-resample.c
new file mode 100644
index 0000000..4efb718
--- /dev/null
+++ b/spa/plugins/audioconvert/spa-resample.c
@@ -0,0 +1,341 @@
+/* Spa
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+#include <getopt.h>
+
+#include <spa/support/log-impl.h>
+#include <spa/debug/mem.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+
+#include <sndfile.h>
+
+SPA_LOG_IMPL(logger);
+
+#include "resample.h"
+
+#define DEFAULT_QUALITY RESAMPLE_DEFAULT_QUALITY
+
+#define MAX_SAMPLES 4096u
+
+struct data {
+ bool verbose;
+ int rate;
+ int format;
+ int quality;
+ int cpu_flags;
+
+ const char *iname;
+ SF_INFO iinfo;
+ SNDFILE *ifile;
+
+ const char *oname;
+ SF_INFO oinfo;
+ SNDFILE *ofile;
+};
+
+#define STR_FMTS "(s8|s16|s32|f32|f64)"
+
+#define OPTIONS "hvr:f:q:c:"
+static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h'},
+ { "verbose", no_argument, NULL, 'v'},
+
+ { "rate", required_argument, NULL, 'r' },
+ { "format", required_argument, NULL, 'f' },
+ { "quality", required_argument, NULL, 'q' },
+ { "cpuflags", required_argument, NULL, 'c' },
+
+ { 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] <infile> <outfile>\n", name);
+ fprintf(fp,
+ " -h, --help Show this help\n"
+ " -v --verbose Be verbose\n"
+ "\n");
+ fprintf(fp,
+ " -r --rate Output sample rate (default as input)\n"
+ " -f --format Output sample format %s (default as input)\n"
+ " -q --quality Resampler quality (default %u)\n"
+ " -c --cpuflags CPU flags (default 0)\n"
+ "\n",
+ STR_FMTS, DEFAULT_QUALITY);
+}
+
+static inline const char *
+sf_fmt_to_str(int fmt)
+{
+ switch(fmt & SF_FORMAT_SUBMASK) {
+ case SF_FORMAT_PCM_S8:
+ return "s8";
+ case SF_FORMAT_PCM_16:
+ return "s16";
+ case SF_FORMAT_PCM_24:
+ return "s24";
+ case SF_FORMAT_PCM_32:
+ return "s32";
+ case SF_FORMAT_FLOAT:
+ return "f32";
+ case SF_FORMAT_DOUBLE:
+ return "f64";
+ default:
+ return "unknown";
+ }
+}
+
+static inline int
+sf_str_to_fmt(const char *str)
+{
+ if (!str)
+ return -1;
+ if (spa_streq(str, "s8"))
+ return SF_FORMAT_PCM_S8;
+ if (spa_streq(str, "s16"))
+ return SF_FORMAT_PCM_16;
+ if (spa_streq(str, "s24"))
+ return SF_FORMAT_PCM_24;
+ if (spa_streq(str, "s32"))
+ return SF_FORMAT_PCM_32;
+ if (spa_streq(str, "f32"))
+ return SF_FORMAT_FLOAT;
+ if (spa_streq(str, "f64"))
+ return SF_FORMAT_DOUBLE;
+ return -1;
+}
+
+static int open_files(struct data *d)
+{
+ 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",
+ d->iname, sf_strerror(NULL));
+ return -EIO;
+ }
+
+ 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->ofile = sf_open(d->oname, SFM_WRITE, &d->oinfo);
+ if (d->ofile == NULL) {
+ fprintf(stderr, "error: failed to open output file \"%s\": %s\n",
+ d->oname, sf_strerror(NULL));
+ return -EIO;
+ }
+ if (d->verbose) {
+ fprintf(stdout, "input '%s': channels:%d rate:%d format:%s\n",
+ d->iname, d->iinfo.channels, d->iinfo.samplerate,
+ sf_fmt_to_str(d->iinfo.format));
+ fprintf(stdout, "output '%s': channels:%d rate:%d format:%s\n",
+ d->oname, d->oinfo.channels, d->oinfo.samplerate,
+ sf_fmt_to_str(d->oinfo.format));
+ }
+ return 0;
+}
+
+static int close_files(struct data *d)
+{
+ if (d->ifile)
+ sf_close(d->ifile);
+ if (d->ofile)
+ sf_close(d->ofile);
+ return 0;
+}
+
+static int do_conversion(struct data *d)
+{
+ struct resample r;
+ int channels = d->iinfo.channels;
+ float in[MAX_SAMPLES * channels];
+ float out[MAX_SAMPLES * channels];
+ float ibuf[MAX_SAMPLES * channels];
+ float obuf[MAX_SAMPLES * channels];
+ uint32_t in_len, out_len, queued;
+ uint32_t pin_len, pout_len;
+ size_t read, written;
+ const void *src[channels];
+ void *dst[channels];
+ uint32_t i;
+ int res, j, k;
+ uint32_t flushing = UINT32_MAX;
+
+ spa_zero(r);
+ r.cpu_flags = d->cpu_flags;
+ r.log = &logger.log;
+ r.channels = channels;
+ r.i_rate = d->iinfo.samplerate;
+ r.o_rate = d->oinfo.samplerate;
+ r.quality = d->quality < 0 ? DEFAULT_QUALITY : d->quality;
+ if ((res = resample_native_init(&r)) < 0) {
+ fprintf(stderr, "can't init converter: %s\n", spa_strerror(res));
+ return res;
+ }
+
+ for (j = 0; j < channels; j++)
+ src[j] = &in[MAX_SAMPLES * j];
+ for (j = 0; j < channels; j++)
+ dst[j] = &out[MAX_SAMPLES * j];
+
+ read = written = queued = 0;
+ while (true) {
+ pout_len = out_len = MAX_SAMPLES;
+ in_len = SPA_MIN(MAX_SAMPLES, resample_in_len(&r, out_len));
+ in_len -= SPA_MIN(queued, in_len);
+
+ if (in_len > 0) {
+ pin_len = in_len = sf_readf_float(d->ifile, &ibuf[queued * channels], in_len);
+
+ read += pin_len;
+
+ if (pin_len == 0) {
+ if (flushing == 0)
+ break;
+ if (flushing == UINT32_MAX)
+ flushing = resample_delay(&r);
+
+ pin_len = in_len = SPA_MIN(MAX_SAMPLES, flushing);
+ flushing -= in_len;
+
+ for (k = 0, i = 0; i < pin_len; i++) {
+ for (j = 0; j < channels; j++)
+ ibuf[k++] = 0.0;
+ }
+ }
+ }
+ in_len += queued;
+ pin_len = in_len;
+
+ for (k = 0, i = 0; i < pin_len; i++) {
+ for (j = 0; j < channels; j++) {
+ in[MAX_SAMPLES * j + i] = ibuf[k++];
+ }
+ }
+ resample_process(&r, src, &pin_len, dst, &pout_len);
+
+ queued = in_len - pin_len;
+ if (queued)
+ memmove(ibuf, &ibuf[pin_len * channels], queued * channels * sizeof(float));
+
+ if (pout_len > 0) {
+ for (k = 0, i = 0; i < pout_len; i++) {
+ for (j = 0; j < channels; j++) {
+ obuf[k++] = out[MAX_SAMPLES * j + i];
+ }
+ }
+ pout_len = sf_writef_float(d->ofile, obuf, pout_len);
+
+ written += pout_len;
+ }
+ }
+ if (d->verbose)
+ fprintf(stdout, "read %zu samples, wrote %zu samples\n", read, written);
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int longopt_index = 0, ret;
+ struct data data;
+
+ spa_zero(data);
+
+ logger.log.level = SPA_LOG_LEVEL_DEBUG;
+
+ data.quality = -1;
+ 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 'v':
+ data.verbose = true;
+ break;
+ case 'r':
+ ret = atoi(optarg);
+ if (ret <= 0) {
+ fprintf(stderr, "error: bad rate %s\n", optarg);
+ goto error_usage;
+ }
+ data.rate = ret;
+ break;
+ case 'f':
+ ret = sf_str_to_fmt(optarg);
+ if (ret < 0) {
+ fprintf(stderr, "error: bad format %s\n", optarg);
+ goto error_usage;
+ }
+ data.format = ret;
+ break;
+ case 'q':
+ ret = atoi(optarg);
+ if (ret < 0) {
+ fprintf(stderr, "error: bad quality %s\n", optarg);
+ goto error_usage;
+ }
+ data.quality = ret;
+ break;
+ case 'c':
+ data.cpu_flags = strtol(optarg, NULL, 0);
+ break;
+ default:
+ fprintf(stderr, "error: unknown option '%c'\n", c);
+ goto error_usage;
+ }
+ }
+ if (optind + 1 >= argc) {
+ fprintf(stderr, "error: filename arguments missing (%d %d)\n", optind, argc);
+ goto error_usage;
+ }
+ data.iname = argv[optind++];
+ data.oname = argv[optind++];
+
+ if (open_files(&data) < 0)
+ return EXIT_FAILURE;
+
+ do_conversion(&data);
+
+ close_files(&data);
+
+ return 0;
+
+error_usage:
+ show_usage(argv[0], true);
+ return EXIT_FAILURE;
+}
diff --git a/spa/plugins/audioconvert/test-audioadapter.c b/spa/plugins/audioconvert/test-audioadapter.c
new file mode 100644
index 0000000..a51feee
--- /dev/null
+++ b/spa/plugins/audioconvert/test-audioadapter.c
@@ -0,0 +1,302 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/support/plugin.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/node/node.h>
+#include <spa/debug/mem.h>
+#include <spa/support/log-impl.h>
+
+SPA_LOG_IMPL(logger);
+
+extern const struct spa_handle_factory test_source_factory;
+
+struct context {
+ struct spa_handle *follower_handle;
+ struct spa_node *follower_node;
+
+ struct spa_handle *adapter_handle;
+ struct spa_node *adapter_node;
+};
+
+static const struct spa_handle_factory *find_factory(const char *name)
+{
+ uint32_t index = 0;
+ const struct spa_handle_factory *factory;
+
+ while (spa_handle_factory_enum(&factory, &index) == 1) {
+ if (spa_streq(factory->name, name))
+ return factory;
+ }
+ return NULL;
+}
+
+static int setup_context(struct context *ctx)
+{
+ size_t size;
+ int res;
+ struct spa_support support[1];
+ struct spa_dict_item items[1];
+ const struct spa_handle_factory *factory;
+ char value[32];
+ void *iface;
+
+ logger.log.level = SPA_LOG_LEVEL_TRACE;
+ support[0] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, &logger.log);
+
+ /* make follower */
+ factory = &test_source_factory;
+ size = spa_handle_factory_get_size(factory, NULL);
+ ctx->follower_handle = calloc(1, size);
+ spa_assert_se(ctx->follower_handle != NULL);
+
+ res = spa_handle_factory_init(factory,
+ ctx->follower_handle,
+ NULL, support, 1);
+ spa_assert_se(res >= 0);
+
+ res = spa_handle_get_interface(ctx->follower_handle,
+ SPA_TYPE_INTERFACE_Node, &iface);
+ spa_assert_se(res >= 0);
+
+ ctx->follower_node = iface;
+
+ /* make adapter */
+ factory = find_factory(SPA_NAME_AUDIO_ADAPT);
+ spa_assert_se(factory != NULL);
+
+ size = spa_handle_factory_get_size(factory, NULL);
+
+ ctx->adapter_handle = calloc(1, size);
+ spa_assert_se(ctx->adapter_handle != NULL);
+
+ snprintf(value, sizeof(value), "pointer:%p", ctx->follower_node);
+ items[0] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value);
+
+ res = spa_handle_factory_init(factory,
+ ctx->adapter_handle,
+ &SPA_DICT_INIT(items, 1),
+ support, 1);
+ spa_assert_se(res >= 0);
+
+ res = spa_handle_get_interface(ctx->adapter_handle,
+ SPA_TYPE_INTERFACE_Node, &iface);
+ spa_assert_se(res >= 0);
+ ctx->adapter_node = iface;
+
+ return 0;
+}
+
+static int clean_context(struct context *ctx)
+{
+ spa_handle_clear(ctx->adapter_handle);
+ spa_handle_clear(ctx->follower_handle);
+ free(ctx->adapter_handle);
+ free(ctx->follower_handle);
+ return 0;
+}
+
+static void node_info(void *data, const struct spa_node_info *info)
+{
+ fprintf(stderr, "input %d, output %d\n",
+ info->max_input_ports,
+ info->max_output_ports);
+
+ spa_assert_se(info->max_input_ports == 0);
+ spa_assert_se(info->max_output_ports > 0);
+}
+
+static void port_info_none(void *data,
+ enum spa_direction direction, uint32_t port,
+ const struct spa_port_info *info)
+{
+ spa_assert_not_reached();
+}
+
+
+static int test_init_state(struct context *ctx)
+{
+ struct spa_hook listener;
+ static const struct spa_node_events init_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .info = node_info,
+ .port_info = port_info_none,
+ };
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->adapter_node,
+ &listener, &init_events, ctx);
+ spa_hook_remove(&listener);
+
+ return 0;
+}
+
+static void port_info_5_1(void *data,
+ enum spa_direction direction, uint32_t port,
+ const struct spa_port_info *info)
+{
+ spa_assert_se(direction == SPA_DIRECTION_OUTPUT);
+ spa_assert_se(port < 6);
+}
+
+static void port_info_1_1(void *data,
+ enum spa_direction direction, uint32_t port,
+ const struct spa_port_info *info)
+{
+ spa_assert_se(direction == SPA_DIRECTION_OUTPUT);
+ spa_assert_se(port < 2);
+}
+
+static int test_split_setup(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .port_info = port_info_5_1,
+ };
+
+ /* external format */
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_F32P;
+ info.channels = 6;
+ info.rate = 48000;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ info.position[2] = SPA_AUDIO_CHANNEL_FC;
+ info.position[3] = SPA_AUDIO_CHANNEL_LFE;
+ info.position[4] = SPA_AUDIO_CHANNEL_SL;
+ info.position[5] = SPA_AUDIO_CHANNEL_SR;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ spa_log_debug(&logger.log, "set profile %d@%d", info.channels, info.rate);
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+
+ res = spa_node_set_param(ctx->adapter_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->adapter_node,
+ &listener, &node_events, ctx);
+ spa_hook_remove(&listener);
+
+ /* internal format */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_S16;
+ info.rate = 44100;
+ info.channels = 2;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ spa_log_debug(&logger.log, "set format %d@%d", info.channels, info.rate);
+ res = spa_node_set_param(ctx->adapter_node, SPA_PARAM_Format, 0, param);
+ spa_log_debug(&logger.log, "result %d", res);
+ spa_assert_se(res >= 0);
+
+ return 0;
+}
+
+static int test_passthrough_setup(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .port_info = port_info_1_1,
+ };
+
+ /* internal format */
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_S16;
+ info.channels = 2;
+ info.rate = 44100;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ spa_log_debug(&logger.log, "set profile %d@%d", info.channels, info.rate);
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_passthrough),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+
+ res = spa_node_set_param(ctx->adapter_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->adapter_node,
+ &listener, &node_events, ctx);
+ spa_hook_remove(&listener);
+
+ return 0;
+}
+
+
+int main(int argc, char *argv[])
+{
+ struct context ctx;
+
+ spa_zero(ctx);
+
+ setup_context(&ctx);
+
+ test_init_state(&ctx);
+ test_split_setup(&ctx);
+ test_passthrough_setup(&ctx);
+
+ clean_context(&ctx);
+
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/test-audioconvert.c b/spa/plugins/audioconvert/test-audioconvert.c
new file mode 100644
index 0000000..99ba866
--- /dev/null
+++ b/spa/plugins/audioconvert/test-audioconvert.c
@@ -0,0 +1,1158 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/support/plugin.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/debug/mem.h>
+#include <spa/support/log-impl.h>
+
+SPA_LOG_IMPL(logger);
+
+extern const struct spa_handle_factory test_source_factory;
+
+#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1)
+
+struct context {
+ struct spa_handle *convert_handle;
+ struct spa_node *convert_node;
+
+ bool got_node_info;
+ uint32_t n_port_info[2];
+ bool got_port_info[2][MAX_PORTS];
+};
+
+static const struct spa_handle_factory *find_factory(const char *name)
+{
+ uint32_t index = 0;
+ const struct spa_handle_factory *factory;
+
+ while (spa_handle_factory_enum(&factory, &index) == 1) {
+ if (spa_streq(factory->name, name))
+ return factory;
+ }
+ return NULL;
+}
+
+static int setup_context(struct context *ctx)
+{
+ size_t size;
+ int res;
+ struct spa_support support[1];
+ struct spa_dict_item items[2];
+ const struct spa_handle_factory *factory;
+ void *iface;
+
+ logger.log.level = SPA_LOG_LEVEL_TRACE;
+ support[0] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, &logger);
+
+ /* make convert */
+ factory = find_factory(SPA_NAME_AUDIO_CONVERT);
+ spa_assert_se(factory != NULL);
+
+ size = spa_handle_factory_get_size(factory, NULL);
+
+ ctx->convert_handle = calloc(1, size);
+ spa_assert_se(ctx->convert_handle != NULL);
+
+ items[0] = SPA_DICT_ITEM_INIT("clock.quantum-limit", "8192");
+
+ res = spa_handle_factory_init(factory,
+ ctx->convert_handle,
+ &SPA_DICT_INIT(items, 1),
+ support, 1);
+ spa_assert_se(res >= 0);
+
+ res = spa_handle_get_interface(ctx->convert_handle,
+ SPA_TYPE_INTERFACE_Node, &iface);
+ spa_assert_se(res >= 0);
+ ctx->convert_node = iface;
+
+ return 0;
+}
+
+static int clean_context(struct context *ctx)
+{
+ spa_handle_clear(ctx->convert_handle);
+ free(ctx->convert_handle);
+ return 0;
+}
+
+static void node_info_check(void *data, const struct spa_node_info *info)
+{
+ struct context *ctx = data;
+
+ fprintf(stderr, "input %d, output %d\n",
+ info->max_input_ports,
+ info->max_output_ports);
+
+ spa_assert_se(info->max_input_ports == MAX_PORTS);
+ spa_assert_se(info->max_output_ports == MAX_PORTS);
+
+ ctx->got_node_info = true;
+}
+
+static void port_info_check(void *data,
+ enum spa_direction direction, uint32_t port,
+ const struct spa_port_info *info)
+{
+ struct context *ctx = data;
+
+ fprintf(stderr, "port %d %d %p\n", direction, port, info);
+
+ ctx->got_port_info[direction][port] = true;
+ ctx->n_port_info[direction]++;
+}
+
+static int test_init_state(struct context *ctx)
+{
+ struct spa_hook listener;
+ static const struct spa_node_events init_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .info = node_info_check,
+ .port_info = port_info_check,
+ };
+
+ spa_zero(ctx->got_node_info);
+ spa_zero(ctx->n_port_info);
+ spa_zero(ctx->got_port_info);
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->convert_node,
+ &listener, &init_events, ctx);
+ spa_hook_remove(&listener);
+
+ spa_assert_se(ctx->got_node_info);
+ spa_assert_se(ctx->n_port_info[0] == 1);
+ spa_assert_se(ctx->n_port_info[1] == 1);
+ spa_assert_se(ctx->got_port_info[0][0] == true);
+ spa_assert_se(ctx->got_port_info[1][0] == true);
+
+ return 0;
+}
+
+static int test_set_in_format(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+
+ /* other format */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ info = (struct spa_audio_info_raw) {
+ .format = SPA_AUDIO_FORMAT_S16,
+ .rate = 44100,
+ .channels = 2,
+ .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, }
+ };
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ res = spa_node_port_set_param(ctx->convert_node, SPA_DIRECTION_INPUT, 0,
+ SPA_PARAM_Format, 0, param);
+ spa_assert_se(res == 0);
+
+ return 0;
+}
+
+static int test_split_setup1(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .port_info = port_info_check,
+ };
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->convert_node,
+ &listener, &node_events, ctx);
+
+ /* port config, output as DSP */
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_F32P;
+ info.rate = 48000;
+ info.channels = 6;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ info.position[2] = SPA_AUDIO_CHANNEL_FC;
+ info.position[3] = SPA_AUDIO_CHANNEL_LFE;
+ info.position[4] = SPA_AUDIO_CHANNEL_SL;
+ info.position[5] = SPA_AUDIO_CHANNEL_SR;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+
+ res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ spa_hook_remove(&listener);
+
+ return 0;
+}
+
+static int test_split_setup2(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .port_info = port_info_check,
+ };
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->convert_node,
+ &listener, &node_events, ctx);
+
+ /* port config, output as DSP */
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_F32P;
+ info.rate = 48000;
+ info.channels = 4;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ info.position[2] = SPA_AUDIO_CHANNEL_RL;
+ info.position[3] = SPA_AUDIO_CHANNEL_RR;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+
+ res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ spa_hook_remove(&listener);
+
+ return 0;
+}
+
+static int test_convert_setup1(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ int res;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .port_info = port_info_check,
+ };
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->convert_node,
+ &listener, &node_events, ctx);
+
+ /* port config, output convert */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_convert));
+
+ res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ spa_hook_remove(&listener);
+
+ return 0;
+}
+
+static int test_set_out_format(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+
+ /* out format */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ info = (struct spa_audio_info_raw) {
+ .format = SPA_AUDIO_FORMAT_S32P,
+ .rate = 96000,
+ .channels = 8,
+ .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR,
+ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, }
+ };
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ res = spa_node_port_set_param(ctx->convert_node, SPA_DIRECTION_OUTPUT, 0,
+ SPA_PARAM_Format, 0, param);
+ spa_assert_se(res == 0);
+
+ return 0;
+}
+
+static int test_merge_setup1(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .port_info = port_info_check,
+ };
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->convert_node,
+ &listener, &node_events, ctx);
+
+ /* port config, output as DSP */
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_F32P;
+ info.rate = 48000;
+ info.channels = 6;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ info.position[2] = SPA_AUDIO_CHANNEL_FC;
+ info.position[3] = SPA_AUDIO_CHANNEL_LFE;
+ info.position[4] = SPA_AUDIO_CHANNEL_RL;
+ info.position[5] = SPA_AUDIO_CHANNEL_RR;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+
+ res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ spa_hook_remove(&listener);
+
+ return 0;
+}
+
+static int test_set_out_format2(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+
+ /* out format */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ info = (struct spa_audio_info_raw) {
+ .format = SPA_AUDIO_FORMAT_S16,
+ .rate = 32000,
+ .channels = 2,
+ .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, }
+ };
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ res = spa_node_port_set_param(ctx->convert_node, SPA_DIRECTION_OUTPUT, 0,
+ SPA_PARAM_Format, 0, param);
+ spa_assert_se(res == 0);
+
+ return 0;
+}
+
+static int test_merge_setup2(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .port_info = port_info_check,
+ };
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->convert_node,
+ &listener, &node_events, ctx);
+
+ /* port config, output as DSP */
+ spa_zero(info);
+ info.format = SPA_AUDIO_FORMAT_F32P;
+ info.rate = 96000;
+ info.channels = 4;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ info.position[2] = SPA_AUDIO_CHANNEL_FC;
+ info.position[3] = SPA_AUDIO_CHANNEL_LFE;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+
+ res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ spa_hook_remove(&listener);
+
+ return 0;
+}
+
+static int test_convert_setup2(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ int res;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .port_info = port_info_check,
+ };
+
+ spa_zero(listener);
+ spa_node_add_listener(ctx->convert_node,
+ &listener, &node_events, ctx);
+
+ /* port config, input convert */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_convert));
+
+ res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ spa_hook_remove(&listener);
+
+ return 0;
+}
+
+static int test_set_in_format2(struct context *ctx)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_audio_info_raw info;
+ int res;
+
+ /* other format */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ info = (struct spa_audio_info_raw) {
+ .format = SPA_AUDIO_FORMAT_S24,
+ .rate = 48000,
+ .channels = 3,
+ .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_LFE, }
+ };
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info);
+
+ res = spa_node_port_set_param(ctx->convert_node, SPA_DIRECTION_INPUT, 0,
+ SPA_PARAM_Format, 0, param);
+ spa_assert_se(res == 0);
+
+ return 0;
+}
+
+static int setup_direction(struct context *ctx, enum spa_direction direction, uint32_t mode,
+ struct spa_audio_info_raw *info)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param, *format;
+ int res;
+ uint32_t i;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ format = spa_format_audio_raw_build(&b, SPA_PARAM_Format, info);
+
+ switch (mode) {
+ case SPA_PARAM_PORT_CONFIG_MODE_dsp:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(direction),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(format));
+ break;
+
+ case SPA_PARAM_PORT_CONFIG_MODE_convert:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(direction),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode));
+ break;
+ default:
+ return -EINVAL;
+ }
+ res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param);
+ spa_assert_se(res == 0);
+
+ switch (mode) {
+ case SPA_PARAM_PORT_CONFIG_MODE_convert:
+ res = spa_node_port_set_param(ctx->convert_node, direction, 0,
+ SPA_PARAM_Format, 0, format);
+ spa_assert_se(res == 0);
+ break;
+ case SPA_PARAM_PORT_CONFIG_MODE_dsp:
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ format = spa_format_audio_dsp_build(&b, SPA_PARAM_Format,
+ &SPA_AUDIO_INFO_DSP_INIT(
+ .format = SPA_AUDIO_FORMAT_F32P));
+ for (i = 0; i < info->channels; i++) {
+ res = spa_node_port_set_param(ctx->convert_node, direction, i,
+ SPA_PARAM_Format, 0, format);
+ spa_assert_se(res == 0);
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+struct buffer {
+ struct spa_buffer buffer;
+ struct spa_data datas[MAX_PORTS];
+ struct spa_chunk chunks[MAX_PORTS];
+};
+
+struct data {
+ uint32_t mode;
+ struct spa_audio_info_raw info;
+ uint32_t ports;
+ uint32_t planes;
+ const void *data[MAX_PORTS];
+ uint32_t size;
+};
+
+static int run_convert(struct context *ctx, struct data *in_data,
+ struct data *out_data)
+{
+ struct spa_command cmd;
+ int res;
+ uint32_t i, j, k;
+ struct buffer in_buffers[in_data->ports];
+ struct buffer out_buffers[out_data->ports];
+ struct spa_io_buffers in_io[in_data->ports];
+ struct spa_io_buffers out_io[out_data->ports];
+
+ setup_direction(ctx, SPA_DIRECTION_INPUT, in_data->mode, &in_data->info);
+ setup_direction(ctx, SPA_DIRECTION_OUTPUT, out_data->mode, &out_data->info);
+
+ cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start);
+ res = spa_node_send_command(ctx->convert_node, &cmd);
+ spa_assert_se(res == 0);
+
+ for (i = 0, k = 0; i < in_data->ports; i++) {
+ struct buffer *b = &in_buffers[i];
+ struct spa_buffer *buffers[1];
+ spa_zero(*b);
+ b->buffer.datas = b->datas;
+ b->buffer.n_datas = in_data->planes;
+
+ for (j = 0; j < in_data->planes; j++, k++) {
+ b->datas[j].type = SPA_DATA_MemPtr;
+ b->datas[j].flags = 0;
+ b->datas[j].fd = -1;
+ b->datas[j].mapoffset = 0;
+ b->datas[j].maxsize = in_data->size;
+ b->datas[j].data = (void *)in_data->data[k];
+ b->datas[j].chunk = &b->chunks[j];
+ b->datas[j].chunk->offset = 0;
+ b->datas[j].chunk->size = in_data->size;
+ b->datas[j].chunk->stride = 0;
+ }
+ buffers[0] = &b->buffer;
+ res = spa_node_port_use_buffers(ctx->convert_node, SPA_DIRECTION_INPUT, i,
+ 0, buffers, 1);
+ spa_assert_se(res == 0);
+
+ in_io[i].status = SPA_STATUS_HAVE_DATA;
+ in_io[i].buffer_id = 0;
+
+ res = spa_node_port_set_io(ctx->convert_node, SPA_DIRECTION_INPUT, i,
+ SPA_IO_Buffers, &in_io[i], sizeof(in_io[i]));
+ spa_assert_se(res == 0);
+ }
+ for (i = 0; i < out_data->ports; i++) {
+ struct buffer *b = &out_buffers[i];
+ struct spa_buffer *buffers[1];
+ spa_zero(*b);
+ b->buffer.datas = b->datas;
+ b->buffer.n_datas = out_data->planes;
+
+ for (j = 0; j < out_data->planes; j++) {
+ b->datas[j].type = SPA_DATA_MemPtr;
+ b->datas[j].flags = 0;
+ b->datas[j].fd = -1;
+ b->datas[j].mapoffset = 0;
+ b->datas[j].maxsize = out_data->size;
+ b->datas[j].data = calloc(1, out_data->size);
+ b->datas[j].chunk = &b->chunks[j];
+ b->datas[j].chunk->offset = 0;
+ b->datas[j].chunk->size = 0;
+ b->datas[j].chunk->stride = 0;
+ }
+ buffers[0] = &b->buffer;
+ res = spa_node_port_use_buffers(ctx->convert_node,
+ SPA_DIRECTION_OUTPUT, i, 0, buffers, 1);
+ spa_assert_se(res == 0);
+
+ out_io[i].status = SPA_STATUS_NEED_DATA;
+ out_io[i].buffer_id = -1;
+
+ res = spa_node_port_set_io(ctx->convert_node, SPA_DIRECTION_OUTPUT, i,
+ SPA_IO_Buffers, &out_io[i], sizeof(out_io[i]));
+ spa_assert_se(res == 0);
+ }
+
+ res = spa_node_process(ctx->convert_node);
+ spa_assert_se(res == (SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA));
+
+ for (i = 0, k = 0; i < out_data->ports; i++) {
+ struct buffer *b = &out_buffers[i];
+
+ spa_assert_se(out_io[i].status == SPA_STATUS_HAVE_DATA);
+ spa_assert_se(out_io[i].buffer_id == 0);
+
+ for (j = 0; j < out_data->planes; j++, k++) {
+ spa_assert_se(b->datas[j].chunk->offset == 0);
+ spa_assert_se(b->datas[j].chunk->size == out_data->size);
+
+ res = memcmp(b->datas[j].data, out_data->data[k], out_data->size);
+ if (res != 0) {
+ fprintf(stderr, "error port %d plane %d\n", i, j);
+ spa_debug_mem(0, b->datas[j].data, out_data->size);
+ spa_debug_mem(0, out_data->data[k], out_data->size);
+ }
+ spa_assert_se(res == 0);
+
+ free(b->datas[j].data);
+ }
+ }
+ cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Suspend);
+ res = spa_node_send_command(ctx->convert_node, &cmd);
+ spa_assert_se(res == 0);
+
+ return 0;
+}
+
+static const float data_f32p_1[] = { 0.1f, 0.1f, 0.1f, 0.1f };
+static const float data_f32p_2[] = { 0.2f, 0.2f, 0.2f, 0.2f };
+static const float data_f32p_3[] = { 0.3f, 0.3f, 0.3f, 0.3f };
+static const float data_f32p_4[] = { 0.4f, 0.4f, 0.4f, 0.4f };
+static const float data_f32p_5[] = { 0.5f, 0.5f, 0.5f, 0.5f };
+static const float data_f32p_5_6p1[] = { 0.953553438f, 0.953553438f, 0.953553438f, 0.953553438f };
+static const float data_f32p_6[] = { 0.6f, 0.6f, 0.6f, 0.6f };
+static const float data_f32p_6_6p1[] = { 1.053553343f, 1.053553343f, 1.053553343f, 1.053553343f };
+static const float data_f32p_7[] = { 0.7f, 0.7f, 0.7f, 0.7f };
+static const float data_f32p_8[] = { 0.8f, 0.8f, 0.8f, 0.8f };
+
+static const float data_f32_5p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f };
+static const float data_f32_6p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f };
+static const float data_f32_6p1_from_5p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f,
+ 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f };
+
+static const float data_f32_7p1_remapped[] = { 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f,
+ 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f,
+ 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f,
+ 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f };
+static const float data_f32_5p1_remapped[] = { 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f,
+ 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f,
+ 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f,
+ 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f };
+
+struct data dsp_5p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ }),
+ .ports = 6,
+ .planes = 1,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, },
+ .size = sizeof(float) * 4
+};
+
+struct data dsp_5p1_from_6p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ }),
+ .ports = 6,
+ .planes = 1,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5_6p1, data_f32p_6_6p1, },
+ .size = sizeof(float) * 4
+};
+
+
+struct data dsp_5p1_remapped = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ }),
+ .ports = 6,
+ .planes = 1,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_5, data_f32p_6, data_f32p_3, data_f32p_4, },
+ .size = sizeof(float) * 4
+};
+
+struct data dsp_5p1_remapped_from_6p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ }),
+ .ports = 6,
+ .planes = 1,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_5_6p1, data_f32p_6_6p1, data_f32p_3, data_f32p_4, },
+ .size = sizeof(float) * 4
+};
+
+struct data dsp_6p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 7,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RC,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ }),
+ .ports = 7,
+ .planes = 1,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 },
+ .size = sizeof(float) * 4
+};
+
+struct data dsp_6p1_side = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 7,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RC,
+ SPA_AUDIO_CHANNEL_SL,
+ SPA_AUDIO_CHANNEL_SR,
+ }),
+ .ports = 7,
+ .planes = 1,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 },
+ .size = sizeof(float) * 4
+};
+
+struct data dsp_7p1_remapped = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 8,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ SPA_AUDIO_CHANNEL_SL,
+ SPA_AUDIO_CHANNEL_SR,
+ }),
+ .ports = 8,
+ .planes = 1,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_7, data_f32p_8, data_f32p_5, data_f32p_6 },
+ .size = sizeof(data_f32p_1)
+};
+
+struct data dsp_5p1_remapped_2 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FL,
+ }),
+ .ports = 6,
+ .planes = 1,
+ .data = { data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_2, data_f32p_1, },
+ .size = sizeof(float) * 4
+};
+
+struct data conv_f32_48000_5p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ }),
+ .ports = 1,
+ .planes = 1,
+ .data = { data_f32_5p1 },
+ .size = sizeof(data_f32_5p1)
+};
+
+struct data conv_f32_48000_5p1_remapped = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ }),
+ .ports = 1,
+ .planes = 1,
+ .data = { data_f32_5p1_remapped },
+ .size = sizeof(data_f32_5p1_remapped)
+};
+
+struct data conv_f32p_48000_5p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32P,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ }),
+ .ports = 1,
+ .planes = 6,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, },
+ .size = sizeof(float) * 4
+};
+
+struct data conv_f32_48000_6p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 7,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RC,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ }),
+ .ports = 1,
+ .planes = 1,
+ .data = { data_f32_6p1 },
+ .size = sizeof(data_f32_6p1)
+};
+
+struct data conv_f32_48000_6p1_from_5p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 7,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RC,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ }),
+ .ports = 1,
+ .planes = 1,
+ .data = { data_f32_6p1_from_5p1 },
+ .size = sizeof(data_f32_6p1_from_5p1)
+};
+
+struct data conv_f32_48000_6p1_side = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 7,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RC,
+ SPA_AUDIO_CHANNEL_SL,
+ SPA_AUDIO_CHANNEL_SR,
+ }),
+ .ports = 1,
+ .planes = 1,
+ .data = { data_f32_6p1 },
+ .size = sizeof(data_f32_6p1)
+};
+
+struct data conv_f32p_48000_6p1 = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32P,
+ .rate = 48000,
+ .channels = 7,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ SPA_AUDIO_CHANNEL_RC,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ }),
+ .ports = 1,
+ .planes = 7,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 },
+ .size = sizeof(float) * 4
+};
+
+struct data conv_f32p_48000_5p1_remapped = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32P,
+ .rate = 48000,
+ .channels = 6,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ }),
+ .ports = 1,
+ .planes = 6,
+ .data = { data_f32p_1, data_f32p_2, data_f32p_5, data_f32p_6, data_f32p_3, data_f32p_4, },
+ .size = sizeof(float) * 4
+};
+
+struct data conv_f32_48000_7p1_remapped = {
+ .mode = SPA_PARAM_PORT_CONFIG_MODE_convert,
+ .info = SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32,
+ .rate = 48000,
+ .channels = 8,
+ .position = {
+ SPA_AUDIO_CHANNEL_FL,
+ SPA_AUDIO_CHANNEL_FR,
+ SPA_AUDIO_CHANNEL_SL,
+ SPA_AUDIO_CHANNEL_SR,
+ SPA_AUDIO_CHANNEL_RL,
+ SPA_AUDIO_CHANNEL_RR,
+ SPA_AUDIO_CHANNEL_FC,
+ SPA_AUDIO_CHANNEL_LFE,
+ }),
+ .ports = 1,
+ .planes = 1,
+ .data = { data_f32_7p1_remapped, },
+ .size = sizeof(data_f32_7p1_remapped)
+};
+
+static int test_convert_remap_dsp(struct context *ctx)
+{
+ run_convert(ctx, &dsp_5p1, &conv_f32_48000_5p1);
+ run_convert(ctx, &dsp_5p1, &conv_f32p_48000_5p1);
+ run_convert(ctx, &dsp_5p1, &conv_f32_48000_5p1_remapped);
+ run_convert(ctx, &dsp_5p1, &conv_f32p_48000_5p1_remapped);
+ run_convert(ctx, &dsp_5p1_remapped, &conv_f32_48000_5p1);
+ run_convert(ctx, &dsp_5p1_remapped, &conv_f32p_48000_5p1);
+ run_convert(ctx, &dsp_5p1_remapped, &conv_f32_48000_5p1_remapped);
+ run_convert(ctx, &dsp_5p1_remapped, &conv_f32p_48000_5p1_remapped);
+ run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32_48000_5p1);
+ run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32p_48000_5p1);
+ run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32_48000_5p1_remapped);
+ run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32p_48000_5p1_remapped);
+ run_convert(ctx, &dsp_6p1, &conv_f32p_48000_6p1);
+ run_convert(ctx, &dsp_6p1, &conv_f32_48000_6p1);
+ run_convert(ctx, &dsp_6p1_side, &conv_f32_48000_6p1_side);
+
+ run_convert(ctx, &dsp_5p1, &conv_f32_48000_6p1_from_5p1);
+ return 0;
+}
+
+static int test_convert_remap_conv(struct context *ctx)
+{
+ run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1);
+ run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1_remapped);
+ run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1_remapped_2);
+ run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1);
+ run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1_remapped);
+ run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1_remapped_2);
+ run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1);
+ run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1_remapped);
+ run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1_remapped_2);
+ run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1);
+ run_convert(ctx, &conv_f32p_48000_6p1, &dsp_6p1);
+ run_convert(ctx, &conv_f32_48000_6p1, &dsp_6p1);
+ run_convert(ctx, &conv_f32_48000_6p1_side, &dsp_6p1_side);
+ run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1_remapped);
+ run_convert(ctx, &conv_f32_48000_7p1_remapped, &dsp_7p1_remapped);
+ run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1_remapped_2);
+
+ run_convert(ctx, &conv_f32_48000_6p1, &dsp_5p1_from_6p1);
+ run_convert(ctx, &conv_f32_48000_6p1_side, &dsp_5p1_from_6p1);
+ run_convert(ctx, &conv_f32_48000_6p1, &dsp_5p1_remapped_from_6p1);
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ struct context ctx;
+
+ spa_zero(ctx);
+
+ setup_context(&ctx);
+
+ test_init_state(&ctx);
+ test_set_in_format(&ctx);
+ test_split_setup1(&ctx);
+ test_split_setup2(&ctx);
+ test_convert_setup1(&ctx);
+ test_set_out_format(&ctx);
+ test_merge_setup1(&ctx);
+ test_set_out_format2(&ctx);
+ test_merge_setup2(&ctx);
+ test_convert_setup2(&ctx);
+ test_set_in_format2(&ctx);
+ test_set_out_format(&ctx);
+
+ test_convert_remap_dsp(&ctx);
+ test_convert_remap_conv(&ctx);
+
+ clean_context(&ctx);
+
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/test-channelmix.c b/spa/plugins/audioconvert/test-channelmix.c
new file mode 100644
index 0000000..9e052e2
--- /dev/null
+++ b/spa/plugins/audioconvert/test-channelmix.c
@@ -0,0 +1,374 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/support/log-impl.h>
+#include <spa/debug/mem.h>
+
+static uint32_t cpu_flags;
+
+SPA_LOG_IMPL(logger);
+
+#define MATRIX(...) (float[]) { __VA_ARGS__ }
+
+#include "test-helper.h"
+#include "channelmix-ops.c"
+
+#define CLOSE_ENOUGH(a,b) (fabs((a)-(b)) < 0.000001f)
+
+static void dump_matrix(struct channelmix *mix, float *coeff)
+{
+ uint32_t i, j;
+
+ for (i = 0; i < mix->dst_chan; i++) {
+ for (j = 0; j < mix->src_chan; j++) {
+ float v = mix->matrix[i][j];
+ spa_log_debug(mix->log, "%d %d: %f <-> %f", i, j, v, *coeff);
+ spa_assert_se(CLOSE_ENOUGH(v, *coeff));
+ coeff++;
+ }
+ }
+}
+
+static void test_mix(uint32_t src_chan, uint32_t src_mask, uint32_t dst_chan, uint32_t dst_mask, uint32_t options, float *coeff)
+{
+ struct channelmix mix;
+
+ spa_log_debug(&logger.log, "start %d->%d (%08x -> %08x)", src_chan, dst_chan, src_mask, dst_mask);
+
+ spa_zero(mix);
+ mix.options = options;
+ mix.src_chan = src_chan;
+ mix.dst_chan = dst_chan;
+ mix.src_mask = src_mask;
+ mix.dst_mask = dst_mask;
+ mix.log = &logger.log;
+
+ spa_assert_se(channelmix_init(&mix) == 0);
+ channelmix_set_volume(&mix, 1.0f, false, 0, NULL);
+ dump_matrix(&mix, coeff);
+}
+
+static void test_1_N_MONO(void)
+{
+ test_mix(1, _M(MONO), 2, _M(FL)|_M(FR), 0,
+ MATRIX(1.0, 1.0));
+ test_mix(1, _M(MONO), 3, _M(FL)|_M(FR)|_M(LFE), 0,
+ MATRIX(1.0, 1.0, 1.0));
+ test_mix(1, _M(MONO), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0,
+ MATRIX(1.0, 1.0, 1.0, 1.0));
+ test_mix(1, _M(MONO), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0,
+ MATRIX(1.0, 1.0, 1.0, 1.0));
+ test_mix(1, _M(MONO), 12, 0, 0,
+ MATRIX(1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
+ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0));
+}
+
+static void test_1_N_FC(void)
+{
+ test_mix(1, _M(FC), 2, _M(FL)|_M(FR), 0,
+ MATRIX(0.707107, 0.707107));
+ test_mix(1, _M(FC), 3, _M(FL)|_M(FR)|_M(LFE), 0,
+ MATRIX(0.707107, 0.707107, 0.0));
+ test_mix(1, _M(FC), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0,
+ MATRIX(0.0, 0.0, 1.0, 0.0));
+ test_mix(1, _M(FC), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0,
+ MATRIX(0.707107, 0.707107, 0.0, 0.0));
+ test_mix(1, _M(FC), 12, 0, 0,
+ MATRIX(1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
+ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0));
+}
+
+static void test_N_1(void)
+{
+ test_mix(1, _M(MONO), 1, _M(MONO), 0,
+ MATRIX(1.0));
+ test_mix(1, _M(MONO), 1, _M(FC), 0,
+ MATRIX(1.0));
+ test_mix(1, _M(FC), 1, _M(MONO), 0,
+ MATRIX(1.0));
+ test_mix(1, _M(FC), 1, _M(FC), 0,
+ MATRIX(1.0));
+ test_mix(2, _M(FL)|_M(FR), 1, _M(MONO), 0,
+ MATRIX(0.707107, 0.707107));
+ test_mix(12, 0, 1, _M(MONO), 0,
+ MATRIX(0.083333, 0.083333, 0.083333, 0.083333, 0.083333, 0.083333,
+ 0.083333, 0.083333, 0.083333, 0.083333, 0.083333, 0.0833333));
+}
+
+static void test_3p1_N(void)
+{
+ test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 1, _M(MONO), 0,
+ MATRIX(0.707107, 0.707107, 1.0, 0.0));
+ test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 2, _M(FL)|_M(FR), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.707107, 0.0 ));
+ test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 3, _M(FL)|_M(FR)|_M(LFE), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.707107, 0.0,
+ 0.0, 0.0, 0.0, 1.0 ));
+ test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0,));
+ test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.707107, 0.0,
+ 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0,));
+}
+
+static void test_4_N(void)
+{
+ test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 1, _M(MONO), 0,
+ MATRIX(0.707107, 0.707107, 0.5, 0.5));
+ test_mix(4, _M(FL)|_M(FR)|_M(SL)|_M(SR), 1, _M(MONO), 0,
+ MATRIX(0.707107, 0.707107, 0.5, 0.5));
+ test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 2, _M(FL)|_M(FR), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.0, 0.707107));
+ test_mix(4, _M(FL)|_M(FR)|_M(SL)|_M(SR), 2, _M(FL)|_M(FR), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.0, 0.707107));
+ test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 3, _M(FL)|_M(FR)|_M(LFE), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.0, 0.707107,
+ 0.0, 0.0, 0.0, 0.0));
+ test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0));
+ test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.0, 0.707107,
+ 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0));
+ test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), CHANNELMIX_OPTION_UPMIX,
+ MATRIX(1.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.0, 0.707107,
+ 0.707107, 0.707107, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0));
+}
+
+static void test_5p1_N(void)
+{
+ test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 1, _M(MONO), 0,
+ MATRIX(0.707107, 0.707107, 1.0, 0.0, 0.5, 0.5));
+ test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 2, _M(FL)|_M(FR), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107));
+ test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RL)|_M(RR), 2, _M(FL)|_M(FR), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107));
+ test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 3, _M(FL)|_M(FR)|_M(LFE), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0));
+ test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.707107,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0));
+ test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.707107, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0));
+ test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 5, _M(FL)|_M(FR)|_M(FC)|_M(SL)|_M(SR), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0));
+ test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0));
+}
+
+static void test_6p1_N(void)
+{
+ test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RC)|_M(SL)|_M(SR), 1, _M(MONO), 0,
+ MATRIX(0.707107, 0.707107, 1.0, 0.0, 0.5, 0.5, 0.5));
+ test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC),
+ 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.707107,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.707107));
+ test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC),
+ 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RL)|_M(RR), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.707107,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.707107));
+ test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RC)|_M(RL)|_M(RR),
+ 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RL)|_M(RR), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.707107, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.707107, 0.0, 1.0));
+ test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC),
+ 8, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RL)|_M(RR), 0,
+ MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.707107,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.707107));
+}
+
+static void test_7p1_N(void)
+{
+ test_mix(8, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RL)|_M(RR), 1, _M(MONO), 0,
+ MATRIX(0.707107, 0.707107, 1.0, 0.0, 0.5, 0.5, 0.5, 0.5));
+ test_mix(8, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RL)|_M(RR), 2, _M(FL)|_M(FR), 0,
+ MATRIX(1.0, 0.0, 0.707107, 0.0, 0.707107, 0.0, 0.707107, 0.0,
+ 0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107, 0.0, 0.707107));
+}
+
+static void check_samples(float **s1, float **s2, uint32_t n_s, uint32_t n_samples)
+{
+ uint32_t i, j;
+ for (i = 0; i < n_s; i++) {
+ for (j = 0; j < n_samples; j++) {
+ spa_assert_se(CLOSE_ENOUGH(s1[i][j], s2[i][j]));
+ }
+ }
+}
+
+static void run_n_m_impl(struct channelmix *mix, const void **src, uint32_t n_samples)
+{
+ uint32_t dst_chan = mix->dst_chan, i;
+ float dst_c_data[dst_chan][n_samples];
+ float dst_x_data[dst_chan][n_samples];
+ void *dst_c[dst_chan], *dst_x[dst_chan];
+
+ for (i = 0; i < dst_chan; i++) {
+ dst_c[i] = dst_c_data[i];
+ dst_x[i] = dst_x_data[i];
+ }
+
+ channelmix_f32_n_m_c(mix, dst_c, src, n_samples);
+
+ channelmix_f32_n_m_c(mix, dst_x, src, n_samples);
+ check_samples((float**)dst_c, (float**)dst_x, dst_chan, n_samples);
+
+#if defined(HAVE_SSE)
+ if (cpu_flags & SPA_CPU_FLAG_SSE) {
+ channelmix_f32_n_m_sse(mix, dst_x, src, n_samples);
+ check_samples((float**)dst_c, (float**)dst_x, dst_chan, n_samples);
+ }
+#endif
+}
+
+static void test_n_m_impl(void)
+{
+ struct channelmix mix;
+ unsigned int i, j;
+#define N_SAMPLES 251
+ float src_data[16][N_SAMPLES], *src[16];
+
+ spa_log_debug(&logger.log, "start");
+
+ for (i = 0; i < 16; i++) {
+ for (j = 0; j < N_SAMPLES; j++)
+ src_data[i][j] = (drand48() - 0.5f) * 2.5f;
+ src[i] = src_data[i];
+ }
+
+ spa_zero(mix);
+ mix.src_chan = 16;
+ mix.dst_chan = 12;
+ mix.log = &logger.log;
+ mix.cpu_flags = cpu_flags;
+ spa_assert_se(channelmix_init(&mix) == 0);
+ channelmix_set_volume(&mix, 1.0f, false, 0, NULL);
+
+ /* identity matrix */
+ run_n_m_impl(&mix, (const void**)src, N_SAMPLES);
+
+ /* some zero destination */
+ mix.matrix_orig[2][2] = 0.0f;
+ mix.matrix_orig[7][7] = 0.0f;
+ channelmix_set_volume(&mix, 1.0f, false, 0, NULL);
+ run_n_m_impl(&mix, (const void**)src, N_SAMPLES);
+
+ /* random matrix */
+ for (i = 0; i < mix.dst_chan; i++) {
+ for (j = 0; j < mix.src_chan; j++) {
+ mix.matrix_orig[i][j] = drand48() - 0.5f;
+ }
+ }
+ channelmix_set_volume(&mix, 1.0f, false, 0, NULL);
+
+ run_n_m_impl(&mix, (const void**)src, N_SAMPLES);
+}
+
+int main(int argc, char *argv[])
+{
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ srand48(SPA_TIMESPEC_TO_NSEC(&ts));
+
+ logger.log.level = SPA_LOG_LEVEL_TRACE;
+
+ cpu_flags = get_cpu_flags();
+ printf("got CPU flags %d\n", cpu_flags);
+
+ test_1_N_MONO();
+ test_1_N_FC();
+ test_N_1();
+ test_3p1_N();
+ test_4_N();
+ test_5p1_N();
+ test_6p1_N();
+ test_7p1_N();
+
+ test_n_m_impl();
+
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/test-fmt-ops.c b/spa/plugins/audioconvert/test-fmt-ops.c
new file mode 100644
index 0000000..8c8d4cd
--- /dev/null
+++ b/spa/plugins/audioconvert/test-fmt-ops.c
@@ -0,0 +1,798 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/debug/mem.h>
+
+#include "test-helper.h"
+#include "fmt-ops.c"
+
+#define N_SAMPLES 253
+#define N_CHANNELS 11
+
+static uint32_t cpu_flags;
+
+static uint8_t samp_in[N_SAMPLES * 8];
+static uint8_t samp_out[N_SAMPLES * 8];
+static uint8_t temp_in[N_SAMPLES * N_CHANNELS * 8];
+static uint8_t temp_out[N_SAMPLES * N_CHANNELS * 8];
+
+static void compare_mem(int i, int j, const void *m1, const void *m2, size_t size)
+{
+ int res = memcmp(m1, m2, size);
+ if (res != 0) {
+ fprintf(stderr, "%d %d %zd:\n", i, j, size);
+ spa_debug_mem(0, m1, size);
+ spa_debug_mem(0, m2, size);
+ }
+ spa_assert_se(res == 0);
+}
+
+static void run_test(const char *name,
+ const void *in, size_t in_size, const void *out, size_t out_size, size_t n_samples,
+ bool in_packed, bool out_packed, convert_func_t func)
+{
+ const void *ip[N_CHANNELS];
+ void *tp[N_CHANNELS];
+ int i, j;
+ const uint8_t *in8 = in, *out8 = out;
+ struct convert conv;
+
+ conv.n_channels = N_CHANNELS;
+
+ for (j = 0; j < N_SAMPLES; j++) {
+ memcpy(&samp_in[j * in_size], &in8[(j % n_samples) * in_size], in_size);
+ memcpy(&samp_out[j * out_size], &out8[(j % n_samples) * out_size], out_size);
+ }
+
+ for (j = 0; j < N_CHANNELS; j++)
+ ip[j] = samp_in;
+
+ if (in_packed) {
+ tp[0] = temp_in;
+ switch(in_size) {
+ case 1:
+ conv_8d_to_8_c(&conv, tp, ip, N_SAMPLES);
+ break;
+ case 2:
+ conv_16d_to_16_c(&conv, tp, ip, N_SAMPLES);
+ break;
+ case 3:
+ conv_24d_to_24_c(&conv, tp, ip, N_SAMPLES);
+ break;
+ case 4:
+ conv_32d_to_32_c(&conv, tp, ip, N_SAMPLES);
+ break;
+ case 8:
+ conv_64d_to_64_c(&conv, tp, ip, N_SAMPLES);
+ break;
+ default:
+ fprintf(stderr, "unknown size %zd\n", in_size);
+ return;
+ }
+ ip[0] = temp_in;
+ }
+
+ spa_zero(temp_out);
+ for (j = 0; j < N_CHANNELS; j++)
+ tp[j] = &temp_out[j * N_SAMPLES * out_size];
+
+ fprintf(stderr, "test %s:\n", name);
+ func(&conv, tp, ip, N_SAMPLES);
+
+ if (out_packed) {
+ const uint8_t *d = tp[0], *s = samp_out;
+ for (i = 0; i < N_SAMPLES; i++) {
+ for (j = 0; j < N_CHANNELS; j++) {
+ compare_mem(i, j, d, s, out_size);
+ d += out_size;
+ }
+ s += out_size;
+ }
+ } else {
+ for (j = 0; j < N_CHANNELS; j++) {
+ compare_mem(0, j, tp[j], samp_out, N_SAMPLES * out_size);
+ }
+ }
+}
+
+static void test_f32_s8(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+ 1.0f/160.f, 1.0f/256.f, -1.0f/160.f, -1.0f/256.f };
+ static const int8_t out[] = { 0, 127, -128, 64, 192, 127, -128, 1, 0, -1, 0 };
+
+ run_test("test_f32_s8", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_s8_c);
+ run_test("test_f32d_s8", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s8_c);
+ run_test("test_f32_s8d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_f32_to_s8d_c);
+ run_test("test_f32d_s8d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_f32d_to_s8d_c);
+}
+
+static void test_s8_f32(void)
+{
+ static const int8_t in[] = { 0, 127, -128, 64, 192, };
+ static const float out[] = { 0.0f, 0.9921875f, -1.0f, 0.5f, -0.5f, };
+
+ run_test("test_s8_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_s8_to_f32_c);
+ run_test("test_s8d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_s8d_to_f32_c);
+ run_test("test_s8_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s8_to_f32d_c);
+ run_test("test_s8d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_s8d_to_f32d_c);
+}
+
+static void test_f32_u8(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+ 1.0f/160.f, 1.0f/256.f, -1.0f/160.f, -1.0f/256.f };
+ static const uint8_t out[] = { 128, 255, 0, 192, 64, 255, 0, 129, 128, 127, 128 };
+
+ run_test("test_f32_u8", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_u8_c);
+ run_test("test_f32d_u8", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_u8_c);
+ run_test("test_f32_u8d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_f32_to_u8d_c);
+ run_test("test_f32d_u8d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_f32d_to_u8d_c);
+}
+
+static void test_u8_f32(void)
+{
+ static const uint8_t in[] = { 128, 255, 0, 192, 64, };
+ static const float out[] = { 0.0f, 0.9921875f, -1.0f, 0.5f, -0.5f, };
+
+ run_test("test_u8_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_u8_to_f32_c);
+ run_test("test_u8d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_u8d_to_f32_c);
+ run_test("test_u8_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_u8_to_f32d_c);
+ run_test("test_u8d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_u8d_to_f32d_c);
+}
+
+static void test_f32_u16(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+ 1.0f/49152.f, 1.0f/65536.f, -1.0f/49152.f, -1.0f/65536.f };
+ static const uint16_t out[] = { 32768, 65535, 0, 49152, 16384, 65535, 0,
+ 32769, 32768, 32767, 32768 };
+
+ run_test("test_f32_u16", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_u16_c);
+ run_test("test_f32d_u16", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_u16_c);
+}
+
+static void test_u16_f32(void)
+{
+ static const uint16_t in[] = { 32768, 65535, 0, 49152, 16384, };
+ static const float out[] = { 0.0f, 0.999969482422f, -1.0f, 0.5f, -0.5f };
+
+ run_test("test_u16_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_u16_to_f32d_c);
+ run_test("test_u16_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_u16_to_f32_c);
+}
+
+static void test_f32_s16(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+ 1.0f/49152.f, 1.0f/65536.f, -1.0f/49152.f, -1.0f/65536.f };
+ static const int16_t out[] = { 0, 32767, -32768, 16384, -16384, 32767, -32768,
+ 1, 0, -1, 0 };
+
+ run_test("test_f32_s16", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_s16_c);
+ run_test("test_f32d_s16", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s16_c);
+ run_test("test_f32_s16d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_f32_to_s16d_c);
+ run_test("test_f32d_s16d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_f32d_to_s16d_c);
+#if defined(HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_f32_s16_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_s16_sse2);
+ run_test("test_f32d_s16_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s16_sse2);
+ run_test("test_f32d_s16d_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_f32d_to_s16d_sse2);
+ }
+#endif
+#if defined(HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_f32d_s16_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s16_avx2);
+ }
+#endif
+#if defined(HAVE_NEON)
+ if (cpu_flags & SPA_CPU_FLAG_NEON) {
+ run_test("test_f32d_s16_neon", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s16_neon);
+ }
+#endif
+}
+
+static void test_s16_f32(void)
+{
+ static const int16_t in[] = { 0, 32767, -32768, 16384, -16384, };
+ static const float out[] = { 0.0f, 0.999969482422f, -1.0f, 0.5f, -0.5f };
+
+ run_test("test_s16_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s16_to_f32d_c);
+ run_test("test_s16d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_s16d_to_f32_c);
+ run_test("test_s16_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_s16_to_f32_c);
+ run_test("test_s16d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_s16d_to_f32d_c);
+#if defined(HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_s16_f32d_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s16_to_f32d_sse2);
+ }
+#endif
+#if defined(HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_s16_f32d_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s16_to_f32d_avx2);
+ }
+#endif
+#if defined(HAVE_NEON)
+ if (cpu_flags & SPA_CPU_FLAG_NEON) {
+ run_test("test_s16_f32d_neon", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s16_to_f32d_neon);
+ }
+#endif
+}
+
+static void test_f32_u32(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+ 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+ static const uint32_t out[] = { 0x80000000, 0xffffff00, 0x0, 0xc0000000, 0x40000000,
+ 0xffffff00, 0x0,
+ 0x80000100, 0x80000000, 0x7fffff00, 0x80000000 };
+
+ run_test("test_f32_u32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_u32_c);
+ run_test("test_f32d_u32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_u32_c);
+}
+
+static void test_u32_f32(void)
+{
+ static const uint32_t in[] = { 0x80000000, 0xffffff00, 0x0, 0xc0000000, 0x40000000 };
+ static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, };
+
+ run_test("test_u32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_u32_to_f32d_c);
+ run_test("test_u32_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_u32_to_f32_c);
+}
+
+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 };
+
+ run_test("test_f32_s32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_s32_c);
+ run_test("test_f32d_s32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s32_c);
+ run_test("test_f32_s32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_f32_to_s32d_c);
+ run_test("test_f32d_s32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_f32d_to_s32d_c);
+#if defined(HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_f32d_s32_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s32_sse2);
+ }
+#endif
+#if defined(HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_f32d_s32_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s32_avx2);
+ }
+#endif
+}
+
+static void test_s32_f32(void)
+{
+ static const int32_t in[] = { 0, 0x7fffff00, 0x80000000, 0x40000000, 0xc0000000 };
+ static const float out[] = { 0.0f, 0.999999880791, -1.0f, 0.5, -0.5, };
+
+ run_test("test_s32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s32_to_f32d_c);
+ run_test("test_s32d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_s32d_to_f32_c);
+ run_test("test_s32_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_s32_to_f32_c);
+ run_test("test_s32d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_s32d_to_f32d_c);
+#if defined(HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_s32_f32d_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s32_to_f32d_sse2);
+ }
+#endif
+#if defined(HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_s32_f32d_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s32_to_f32d_avx2);
+ }
+#endif
+}
+
+static void test_f32_u24(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+ 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+ static const uint24_t out[] = { U32_TO_U24(0x00800000), U32_TO_U24(0xffffff),
+ U32_TO_U24(0x000000), U32_TO_U24(0xc00000), U32_TO_U24(0x400000),
+ U32_TO_U24(0xffffff), U32_TO_U24(0x000000),
+ U32_TO_U24(0x800001), U32_TO_U24(0x800000), U32_TO_U24(0x7fffff),
+ U32_TO_U24(0x800000) };
+
+ run_test("test_f32_u24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in),
+ true, true, conv_f32_to_u24_c);
+ run_test("test_f32d_u24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in),
+ false, true, conv_f32d_to_u24_c);
+}
+
+static void test_u24_f32(void)
+{
+ static const uint24_t in[] = { U32_TO_U24(0x00800000), U32_TO_U24(0xffffff),
+ U32_TO_U24(0x000000), U32_TO_U24(0xc00000), U32_TO_U24(0x400000) };
+ static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5, -0.5, };
+
+ run_test("test_u24_f32d", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_u24_to_f32d_c);
+ run_test("test_u24_f32", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_u24_to_f32_c);
+}
+
+static void test_f32_s24(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 int24_t out[] = { S32_TO_S24(0), S32_TO_S24(0x7fffff),
+ S32_TO_S24(0xff800000), S32_TO_S24(0x400000), S32_TO_S24(0xc00000),
+ S32_TO_S24(0x7fffff), S32_TO_S24(0xff800000),
+ S32_TO_S24(0x000001), S32_TO_S24(0x000000), S32_TO_S24(0xffffffff),
+ S32_TO_S24(0x000000) };
+
+ run_test("test_f32_s24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in),
+ true, true, conv_f32_to_s24_c);
+ run_test("test_f32d_s24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in),
+ false, true, conv_f32d_to_s24_c);
+ run_test("test_f32_s24d", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in),
+ true, false, conv_f32_to_s24d_c);
+ run_test("test_f32d_s24d", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in),
+ false, false, conv_f32d_to_s24d_c);
+}
+
+static void test_s24_f32(void)
+{
+ static const int24_t in[] = { S32_TO_S24(0), S32_TO_S24(0x7fffff),
+ S32_TO_S24(0xff800000), S32_TO_S24(0x400000), S32_TO_S24(0xc00000) };
+ static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, };
+
+ run_test("test_s24_f32d", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s24_to_f32d_c);
+ run_test("test_s24d_f32", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_s24d_to_f32_c);
+ run_test("test_s24_f32", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_s24_to_f32_c);
+ run_test("test_s24d_f32d", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_s24d_to_f32d_c);
+#if defined(HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_s24_f32d_sse2", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s24_to_f32d_sse2);
+ }
+#endif
+#if defined(HAVE_SSSE3)
+ if (cpu_flags & SPA_CPU_FLAG_SSSE3) {
+ run_test("test_s24_f32d_ssse3", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s24_to_f32d_ssse3);
+ }
+#endif
+#if defined(HAVE_SSE41)
+ if (cpu_flags & SPA_CPU_FLAG_SSE41) {
+ run_test("test_s24_f32d_sse41", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s24_to_f32d_sse41);
+ }
+#endif
+#if defined(HAVE_AVX2)
+ if (cpu_flags & SPA_CPU_FLAG_AVX2) {
+ run_test("test_s24_f32d_avx2", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s24_to_f32d_avx2);
+ }
+#endif
+}
+
+static void test_f32_u24_32(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+ 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+ static const uint32_t out[] = { 0x800000, 0xffffff, 0x0, 0xc00000, 0x400000,
+ 0xffffff, 0x000000,
+ 0x800001, 0x800000, 0x7fffff, 0x800000 };
+
+ run_test("test_f32_u24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_u24_32_c);
+ run_test("test_f32d_u24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_u24_32_c);
+}
+
+static void test_u24_32_f32(void)
+{
+ static const uint32_t in[] = { 0x800000, 0xffffff, 0x0, 0xc00000, 0x400000, 0x11000000 };
+ static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, -1.0f };
+
+ run_test("test_u24_32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_u24_32_to_f32d_c);
+ run_test("test_u24_32_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_u24_32_to_f32_c);
+}
+
+static void test_f32_s24_32(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f,
+ 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 };
+ static const int32_t out[] = { 0, 0x7fffff, 0xff800000, 0x400000, 0xffc00000,
+ 0x7fffff, 0xff800000,
+ 0x000001, 0x000000, 0xffffffff, 0x000000 };
+
+ run_test("test_f32_s24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_s24_32_c);
+ run_test("test_f32d_s24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_s24_32_c);
+ run_test("test_f32_s24_32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_f32_to_s24_32d_c);
+ run_test("test_f32d_s24_32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_f32d_to_s24_32d_c);
+}
+
+static void test_s24_32_f32(void)
+{
+ static const int32_t in[] = { 0, 0x7fffff, 0xff800000, 0x400000, 0xffc00000, 0x66800000 };
+ static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, -1.0f };
+
+ run_test("test_s24_32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_s24_32_to_f32d_c);
+ run_test("test_s24_32d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_s24_32d_to_f32_c);
+ run_test("test_s24_32_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_s24_32_to_f32_c);
+ run_test("test_s24_32d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_s24_32d_to_f32d_c);
+}
+
+static void test_f64_f32(void)
+{
+ static const double in[] = { 0.0, 1.0, -1.0, 0.5, -0.5, };
+ static const float out[] = { 0.0, 1.0, -1.0, 0.5, -0.5, };
+
+ run_test("test_f64_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_f64_to_f32d_c);
+ run_test("test_f64d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f64d_to_f32_c);
+ run_test("test_f64_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f64_to_f32_c);
+ run_test("test_f64d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_f64d_to_f32d_c);
+}
+
+static void test_f32_f64(void)
+{
+ static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
+ static const double out[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f };
+
+ run_test("test_f32_f64", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, true, conv_f32_to_f64_c);
+ run_test("test_f32d_f64", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, true, conv_f32d_to_f64_c);
+ run_test("test_f32_f64d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ true, false, conv_f32_to_f64d_c);
+ run_test("test_f32d_f64d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out),
+ false, false, conv_f32d_to_f64d_c);
+}
+
+static void test_lossless_s8(void)
+{
+ int8_t i;
+
+ fprintf(stderr, "test %s:\n", __func__);
+ for (i = S8_MIN; i < S8_MAX; i+=1) {
+ float v = S8_TO_F32(i);
+ int8_t t = F32_TO_S8(v);
+ spa_assert_se(i == t);
+ }
+}
+
+static void test_lossless_u8(void)
+{
+ uint8_t i;
+
+ fprintf(stderr, "test %s:\n", __func__);
+ for (i = U8_MIN; i < U8_MAX; i+=1) {
+ float v = U8_TO_F32(i);
+ uint8_t t = F32_TO_U8(v);
+ spa_assert_se(i == t);
+ }
+}
+static void test_lossless_s16(void)
+{
+ int16_t i;
+
+ fprintf(stderr, "test %s:\n", __func__);
+ for (i = S16_MIN; i < S16_MAX; i+=3) {
+ float v = S16_TO_F32(i);
+ int16_t t = F32_TO_S16(v);
+ spa_assert_se(i == t);
+ }
+}
+
+static void test_lossless_u16(void)
+{
+ uint32_t i;
+
+ fprintf(stderr, "test %s:\n", __func__);
+ for (i = U16_MIN; i < U16_MAX; i+=3) {
+ float v = U16_TO_F32(i);
+ uint16_t t = F32_TO_U16(v);
+ spa_assert_se(i == t);
+ }
+}
+
+static void test_lossless_s24(void)
+{
+ int32_t i;
+
+ fprintf(stderr, "test %s:\n", __func__);
+ for (i = S24_MIN; i < S24_MAX; i+=13) {
+ float v = S24_TO_F32(s32_to_s24(i));
+ int32_t t = s24_to_s32(F32_TO_S24(v));
+ spa_assert_se(i == t);
+ }
+}
+
+static void test_lossless_u24(void)
+{
+ uint32_t i;
+
+ fprintf(stderr, "test %s:\n", __func__);
+ for (i = U24_MIN; i < U24_MAX; i+=11) {
+ float v = U24_TO_F32(u32_to_u24(i));
+ uint32_t t = u24_to_u32(F32_TO_U24(v));
+ spa_assert_se(i == t);
+ }
+}
+
+static void test_lossless_s32(void)
+{
+ int32_t i;
+
+ fprintf(stderr, "test %s:\n", __func__);
+ for (i = S32_MIN; i < S32_MAX; i+=255) {
+ float v = S32_TO_F32(i);
+ int32_t t = F32_TO_S32(v);
+ spa_assert_se(SPA_ABS(i - t) <= 256);
+ }
+}
+
+static void test_lossless_u32(void)
+{
+ uint32_t i;
+
+ fprintf(stderr, "test %s:\n", __func__);
+ for (i = U32_MIN; i < U32_MAX; i+=255) {
+ float v = U32_TO_F32(i);
+ uint32_t t = F32_TO_U32(v);
+ spa_assert_se(i > t ? (i - t) <= 256 : (t - i) <= 256);
+ }
+}
+
+static void test_swaps(void)
+{
+ {
+ uint24_t v = U32_TO_U24(0x123456);
+ uint24_t t = U32_TO_U24(0x563412);
+ uint24_t s = bswap_u24(v);
+ spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0);
+ }
+ {
+ int24_t v = S32_TO_S24(0xfffe1dc0);
+ int24_t t = S32_TO_S24(0xffc01dfe);
+ int24_t s = bswap_s24(v);
+ spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0);
+ }
+ {
+ int24_t v = S32_TO_S24(0x123456);
+ int24_t t = S32_TO_S24(0x563412);
+ int24_t s = bswap_s24(v);
+ spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0);
+ }
+}
+
+static void run_test_noise(uint32_t fmt, uint32_t noise, uint32_t flags)
+{
+ struct convert conv;
+ const void *ip[N_CHANNELS];
+ void *op[N_CHANNELS];
+ uint32_t i, range;
+ bool all_zero;
+
+ spa_zero(conv);
+
+ conv.noise_bits = noise;
+ conv.src_fmt = SPA_AUDIO_FORMAT_F32P;
+ conv.dst_fmt = fmt;
+ conv.n_channels = 2;
+ conv.rate = 44100;
+ conv.cpu_flags = flags;
+ spa_assert_se(convert_init(&conv) == 0);
+ fprintf(stderr, "test noise %s:\n", conv.func_name);
+
+ memset(samp_in, 0, sizeof(samp_in));
+ for (i = 0; i < conv.n_channels; i++) {
+ ip[i] = samp_in;
+ op[i] = samp_out;
+ }
+ convert_process(&conv, op, ip, N_SAMPLES);
+
+ range = 1 << conv.noise_bits;
+
+ all_zero = true;
+ for (i = 0; i < conv.n_channels * N_SAMPLES; i++) {
+ switch (fmt) {
+ case SPA_AUDIO_FORMAT_S8:
+ {
+ int8_t *d = (int8_t *)samp_out;
+ if (d[i] != 0)
+ all_zero = false;
+ spa_assert_se(SPA_ABS(d[i] - 0) <= (int8_t)range);
+ break;
+ }
+ case SPA_AUDIO_FORMAT_U8:
+ {
+ uint8_t *d = (uint8_t *)samp_out;
+ if (d[i] != 0x80)
+ all_zero = false;
+ spa_assert_se((int8_t)SPA_ABS(d[i] - 0x80) <= (int8_t)(range<<1));
+ break;
+ }
+ case SPA_AUDIO_FORMAT_S16:
+ {
+ int16_t *d = (int16_t *)samp_out;
+ if (d[i] != 0)
+ all_zero = false;
+ spa_assert_se(SPA_ABS(d[i] - 0) <= (int16_t)range);
+ break;
+ }
+ case SPA_AUDIO_FORMAT_S24:
+ {
+ int24_t *d = (int24_t *)samp_out;
+ int32_t t = s24_to_s32(d[i]);
+ if (t != 0)
+ all_zero = false;
+ spa_assert_se(SPA_ABS(t - 0) <= (int32_t)range);
+ break;
+ }
+ case SPA_AUDIO_FORMAT_S32:
+ {
+ int32_t *d = (int32_t *)samp_out;
+ if (d[i] != 0)
+ all_zero = false;
+ spa_assert_se(SPA_ABS(d[i] - 0) <= (int32_t)(range << 8));
+ break;
+ }
+ default:
+ spa_assert_not_reached();
+ break;
+ }
+ }
+ spa_assert_se(all_zero == false);
+ convert_free(&conv);
+}
+
+static void test_noise(void)
+{
+ run_test_noise(SPA_AUDIO_FORMAT_S8, 1, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_S8, 2, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_U8, 1, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_U8, 2, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_S16, 1, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_S16, 2, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_S24, 1, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_S24, 2, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_S32, 1, 0);
+ run_test_noise(SPA_AUDIO_FORMAT_S32, 2, 0);
+}
+
+int main(int argc, char *argv[])
+{
+ cpu_flags = get_cpu_flags();
+ printf("got CPU flags %d\n", cpu_flags);
+
+ test_f32_s8();
+ test_s8_f32();
+ test_f32_u8();
+ test_u8_f32();
+ test_f32_u16();
+ test_u16_f32();
+ test_f32_s16();
+ test_s16_f32();
+ test_f32_u32();
+ test_u32_f32();
+ test_f32_s32();
+ test_s32_f32();
+ test_f32_u24();
+ test_u24_f32();
+ test_f32_s24();
+ test_s24_f32();
+ test_f32_u24_32();
+ test_u24_32_f32();
+ test_f32_s24_32();
+ test_s24_32_f32();
+ test_f32_f64();
+ test_f64_f32();
+
+ test_lossless_s8();
+ test_lossless_u8();
+ test_lossless_s16();
+ test_lossless_u16();
+ test_lossless_s24();
+ test_lossless_u24();
+ test_lossless_s32();
+ test_lossless_u32();
+
+ test_swaps();
+
+ test_noise();
+
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/test-helper.h b/spa/plugins/audioconvert/test-helper.h
new file mode 100644
index 0000000..8c789bd
--- /dev/null
+++ b/spa/plugins/audioconvert/test-helper.h
@@ -0,0 +1,97 @@
+#include <dlfcn.h>
+
+#include <spa/support/plugin.h>
+#include <spa/utils/type.h>
+#include <spa/utils/result.h>
+#include <spa/support/cpu.h>
+#include <spa/utils/names.h>
+
+static inline const struct spa_handle_factory *get_factory(spa_handle_factory_enum_func_t enum_func,
+ const char *name, uint32_t version)
+{
+ uint32_t i;
+ int res;
+ const struct spa_handle_factory *factory;
+
+ for (i = 0;;) {
+ if ((res = enum_func(&factory, &i)) <= 0) {
+ if (res < 0)
+ errno = -res;
+ break;
+ }
+ if (factory->version >= version &&
+ !strcmp(factory->name, name))
+ return factory;
+ }
+ return NULL;
+}
+
+static inline struct spa_handle *load_handle(const struct spa_support *support,
+ uint32_t n_support, const char *lib, const char *name)
+{
+ int res, len;
+ void *hnd;
+ spa_handle_factory_enum_func_t enum_func;
+ const struct spa_handle_factory *factory;
+ struct spa_handle *handle;
+ const char *str;
+ char *path;
+
+ if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
+ str = PLUGINDIR;
+
+ len = strlen(str) + strlen(lib) + 2;
+ path = alloca(len);
+ snprintf(path, len, "%s/%s", str, lib);
+
+ if ((hnd = dlopen(path, RTLD_NOW)) == NULL) {
+ fprintf(stderr, "can't load %s: %s\n", lib, dlerror());
+ res = -ENOENT;
+ goto error;
+ }
+ if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ fprintf(stderr, "can't find enum function\n");
+ res = -ENXIO;
+ goto error_close;
+ }
+
+ if ((factory = get_factory(enum_func, name, SPA_VERSION_HANDLE_FACTORY)) == NULL) {
+ fprintf(stderr, "can't find factory\n");
+ res = -ENOENT;
+ goto error_close;
+ }
+ handle = calloc(1, spa_handle_factory_get_size(factory, NULL));
+ if ((res = spa_handle_factory_init(factory, handle,
+ NULL, support, n_support)) < 0) {
+ fprintf(stderr, "can't make factory instance: %d\n", res);
+ goto error_close;
+ }
+ return handle;
+
+error_close:
+ dlclose(hnd);
+error:
+ errno = -res;
+ return NULL;
+}
+
+static inline uint32_t get_cpu_flags(void)
+{
+ struct spa_handle *handle;
+ uint32_t flags;
+ void *iface;
+ int res;
+
+ handle = load_handle(NULL, 0, "support/libspa-support.so", SPA_NAME_SUPPORT_CPU);
+ if (handle == NULL)
+ return 0;
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_CPU, &iface)) < 0) {
+ fprintf(stderr, "can't get CPU interface %s\n", spa_strerror(res));
+ return 0;
+ }
+ flags = spa_cpu_get_flags((struct spa_cpu*)iface);
+
+ free(handle);
+
+ return flags;
+}
diff --git a/spa/plugins/audioconvert/test-peaks.c b/spa/plugins/audioconvert/test-peaks.c
new file mode 100644
index 0000000..3f7d093
--- /dev/null
+++ b/spa/plugins/audioconvert/test-peaks.c
@@ -0,0 +1,128 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/support/log-impl.h>
+#include <spa/debug/mem.h>
+
+SPA_LOG_IMPL(logger);
+
+static uint32_t cpu_flags;
+
+#include "test-helper.h"
+
+#include "peaks-ops.c"
+
+static void test_impl(void)
+{
+ struct peaks peaks;
+ unsigned int i;
+ float vals[1038];
+ float min[2] = { 0.0f, 0.0f }, max[2] = { 0.0f, 0.0f }, absmax[2] = { 0.0f, 0.0f };
+
+ for (i = 0; i < SPA_N_ELEMENTS(vals); i++)
+ vals[i] = (drand48() - 0.5f) * 2.5f;
+
+ peaks_min_max_c(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, &min[0], &max[0]);
+ printf("c peaks min:%f max:%f\n", min[0], max[0]);
+
+ absmax[0] = peaks_abs_max_c(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, 0.0f);
+ printf("c peaks abs-max:%f\n", absmax[0]);
+
+#if defined(HAVE_SSE)
+ if (cpu_flags & SPA_CPU_FLAG_SSE) {
+ peaks_min_max_sse(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, &min[1], &max[1]);
+ printf("sse peaks min:%f max:%f\n", min[1], max[1]);
+
+ absmax[1] = peaks_abs_max_sse(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, 0.0f);
+ printf("sse peaks abs-max:%f\n", absmax[1]);
+
+ spa_assert(min[0] == min[1]);
+ spa_assert(max[0] == max[1]);
+ spa_assert(absmax[0] == absmax[1]);
+ }
+#endif
+
+}
+
+static void test_min_max(void)
+{
+ struct peaks peaks;
+ const float vals[] = { 0.0f, 0.5f, -0.5f, 0.0f, 0.6f, -0.8f, -0.5f, 0.0f };
+ float min = 0.0f, max = 0.0f;
+
+ spa_zero(peaks);
+ peaks.log = &logger.log;
+ peaks.cpu_flags = cpu_flags;
+ peaks_init(&peaks);
+
+ peaks_min_max(&peaks, vals, SPA_N_ELEMENTS(vals), &min, &max);
+
+ spa_assert(min == -0.8f);
+ spa_assert(max == 0.6f);
+}
+
+static void test_abs_max(void)
+{
+ struct peaks peaks;
+ const float vals[] = { 0.0f, 0.5f, -0.5f, 0.0f, 0.6f, -0.8f, -0.5f, 0.0f };
+ float max = 0.0f;
+
+ spa_zero(peaks);
+ peaks.log = &logger.log;
+ peaks.cpu_flags = cpu_flags;
+ peaks_init(&peaks);
+
+ max = peaks_abs_max(&peaks, vals, SPA_N_ELEMENTS(vals), max);
+
+ spa_assert(max == 0.8f);
+}
+
+int main(int argc, char *argv[])
+{
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ srand48(SPA_TIMESPEC_TO_NSEC(&ts));
+
+ logger.log.level = SPA_LOG_LEVEL_TRACE;
+
+ cpu_flags = get_cpu_flags();
+ printf("got CPU flags %d\n", cpu_flags);
+
+ test_impl();
+
+ test_min_max();
+ test_abs_max();
+
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/test-resample.c b/spa/plugins/audioconvert/test-resample.c
new file mode 100644
index 0000000..4f11f53
--- /dev/null
+++ b/spa/plugins/audioconvert/test-resample.c
@@ -0,0 +1,177 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/support/log-impl.h>
+#include <spa/debug/mem.h>
+
+SPA_LOG_IMPL(logger);
+
+#include "resample.h"
+
+#define N_SAMPLES 253
+#define N_CHANNELS 11
+
+static float samp_in[N_SAMPLES * 4];
+static float samp_out[N_SAMPLES * 4];
+
+static void feed_1(struct resample *r)
+{
+ uint32_t i;
+ const void *src[1];
+ void *dst[1];
+
+ spa_zero(samp_out);
+ src[0] = samp_in;
+ dst[0] = samp_out;
+
+ for (i = 0; i < 500; i++) {
+ uint32_t in, out;
+
+ in = out = 1;
+ samp_in[0] = i;
+ resample_process(r, src, &in, dst, &out);
+ fprintf(stderr, "%d %d %f %d\n", i, in, samp_out[0], out);
+ }
+}
+
+static void test_native(void)
+{
+ struct resample r;
+
+ spa_zero(r);
+ r.log = &logger.log;
+ r.channels = 1;
+ r.i_rate = 44100;
+ r.o_rate = 44100;
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+
+ feed_1(&r);
+ resample_free(&r);
+
+ spa_zero(r);
+ r.log = &logger.log;
+ r.channels = 1;
+ r.i_rate = 44100;
+ r.o_rate = 48000;
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+
+ feed_1(&r);
+ resample_free(&r);
+}
+
+static void pull_blocks(struct resample *r, uint32_t first, uint32_t size)
+{
+ uint32_t i;
+ float in[SPA_MAX(size, first) * 2];
+ float out[SPA_MAX(size, first) * 2];
+ const void *src[1];
+ void *dst[1];
+ uint32_t in_len, out_len;
+ uint32_t pin_len, pout_len;
+
+ src[0] = in;
+ dst[0] = out;
+
+ for (i = 0; i < 500; i++) {
+ pout_len = out_len = i == 0 ? first : size;
+ pin_len = in_len = resample_in_len(r, out_len);
+
+ resample_process(r, src, &pin_len, dst, &pout_len);
+
+ fprintf(stderr, "%d: %d %d %d %d %d\n", i,
+ in_len, pin_len, out_len, pout_len,
+ resample_in_len(r, size));
+
+ spa_assert_se(in_len == pin_len);
+ spa_assert_se(out_len == pout_len);
+ }
+}
+
+static void test_in_len(void)
+{
+ struct resample r;
+
+ spa_zero(r);
+ r.log = &logger.log;
+ r.channels = 1;
+ r.i_rate = 32000;
+ r.o_rate = 48000;
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+
+ pull_blocks(&r, 1024, 1024);
+ resample_free(&r);
+
+ spa_zero(r);
+ r.log = &logger.log;
+ r.channels = 1;
+ r.i_rate = 44100;
+ r.o_rate = 48000;
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+
+ pull_blocks(&r, 1024, 1024);
+ resample_free(&r);
+
+ spa_zero(r);
+ r.log = &logger.log;
+ r.channels = 1;
+ r.i_rate = 48000;
+ r.o_rate = 44100;
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+
+ pull_blocks(&r, 1024, 1024);
+ resample_free(&r);
+
+ spa_zero(r);
+ r.log = &logger.log;
+ r.channels = 1;
+ r.i_rate = 44100;
+ r.o_rate = 48000;
+ r.quality = RESAMPLE_DEFAULT_QUALITY;
+ resample_native_init(&r);
+
+ pull_blocks(&r, 513, 64);
+ resample_free(&r);
+}
+
+int main(int argc, char *argv[])
+{
+ logger.log.level = SPA_LOG_LEVEL_TRACE;
+
+ test_native();
+ test_in_len();
+
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/test-source.c b/spa/plugins/audioconvert/test-source.c
new file mode 100644
index 0000000..b352323
--- /dev/null
+++ b/spa/plugins/audioconvert/test-source.c
@@ -0,0 +1,931 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/cpu.h>
+#include <spa/utils/list.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/types.h>
+
+#define NAME "test-source"
+
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+
+#define MAX_BUFFERS 32
+
+struct impl;
+
+#define DEFAULT_MUTE false
+#define DEFAULT_VOLUME 1.0f
+
+struct props {
+ float volume;
+ bool mute;
+};
+
+static void props_reset(struct props *props)
+{
+ props->mute = DEFAULT_MUTE;
+ props->volume = DEFAULT_VOLUME;
+}
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1 << 0)
+ uint32_t flags;
+ struct spa_list link;
+ struct spa_buffer *outbuf;
+ struct spa_meta_header *h;
+};
+
+struct port {
+ uint32_t direction;
+ uint32_t id;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[8];
+
+ struct spa_io_buffers *io;
+
+ struct spa_audio_info format;
+ uint32_t stride;
+ uint32_t blocks;
+ uint32_t size;
+ unsigned int have_format:1;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list queue;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+
+ uint32_t quantum_limit;
+
+ struct spa_hook_list hooks;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct props props;
+ struct spa_param_info params[8];
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ struct port out_port;
+
+ unsigned int started:1;
+};
+
+#define CHECK_PORT(this,d,id) (d == SPA_DIRECTION_OUTPUT && id == 0)
+#define GET_OUT_PORT(this,id) (&this->out_port)
+#define GET_PORT(this,d,id) (d == SPA_DIRECTION_OUTPUT ? GET_OUT_PORT(this,id) : NULL)
+
+static void emit_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_trace(this->log, NAME" %p: add listener %p", this, listener);
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_info(this, true);
+ emit_port_info(this, GET_OUT_PORT(this, 0), true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *user_data)
+{
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_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_PropInfo:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume),
+ SPA_PROP_INFO_description, SPA_POD_String("Volume"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute),
+ SPA_PROP_INFO_description, SPA_POD_String("Mute"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->mute));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_volume, SPA_POD_Float(p->volume),
+ SPA_PROP_mute, SPA_POD_Bool(p->mute));
+ 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 apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ struct props *p = &this->props;
+ int changed = 0;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &p->volume) == 0)
+ changed++;
+ break;
+ case SPA_PROP_mute:
+ if (spa_pod_get_bool(&prop->value, &p->mute) == 0)
+ changed++;
+ break;
+ default:
+ break;
+ }
+ }
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ if (param == NULL) {
+ props_reset(&this->props);
+ return 0;
+ }
+ if (apply_props(this, param) > 0) {
+ this->info.change_mask = SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[1].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+ 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);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = GET_OUT_PORT(this, 0);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ this->started = true;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ 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)
+{
+ switch (index) {
+ case 0:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(DEFAULT_RATE, 1, INT32_MAX),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(DEFAULT_CHANNELS, 1, INT32_MAX));
+ break;
+ case 1:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S32),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(DEFAULT_RATE, 1, INT32_MAX),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(DEFAULT_CHANNELS, 1, INT32_MAX));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(this, direction, port_id,
+ result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ {
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * 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;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers %p", this, port);
+ port->n_buffers = 0;
+ spa_list_init(&port->queue);
+ }
+ return 0;
+}
+
+static int calc_width(struct spa_audio_info *info)
+{
+ switch (info->info.raw.format) {
+ case SPA_AUDIO_FORMAT_U8P:
+ case SPA_AUDIO_FORMAT_U8:
+ return 1;
+ case SPA_AUDIO_FORMAT_S16P:
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ return 2;
+ case SPA_AUDIO_FORMAT_S24P:
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ return 3;
+ case SPA_AUDIO_FORMAT_S32P:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ return 4;
+ default:
+ 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);
+
+ if (format == NULL) {
+ if (port->have_format) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ }
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if ((res = spa_format_audio_raw_parse(format, &info.info.raw)) < 0)
+ return res;
+
+ port->stride = calc_width(&info);
+ if (port->stride == 0)
+ return -EINVAL;
+ if (info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0)
+ return -EINVAL;
+
+ if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) {
+ port->blocks = info.info.raw.channels;
+ }
+ else {
+ port->stride *= info.info.raw.channels;
+ port->blocks = 1;
+ }
+ port->have_format = true;
+ port->format = info;
+
+ spa_log_debug(this->log, NAME " %p: set format on port %d %d", this, port_id, res);
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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;
+ int res;
+
+ spa_return_val_if_fail(object != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, NAME" %p: set param %d", this, id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(object, direction, port_id, flags, param);
+ break;
+ default:
+ res = -ENOENT;
+ }
+ return res;
+}
+
+static void recycle_buffer(struct impl *this, struct port *port, struct buffer *b)
+{
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_list_append(&port->queue, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ spa_log_trace_fp(this->log, NAME " %p: recycle buffer %d", this, b->id);
+ }
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i, j, size = SPA_ID_INVALID;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_return_val_if_fail(port->have_format, -EIO);
+
+ spa_log_debug(this->log, NAME " %p: use buffers %d on port %d", this, n_buffers, port_id);
+
+ clear_buffers(this, port);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ uint32_t n_datas = buffers[i]->n_datas;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->flags = BUFFER_FLAG_OUT;
+ b->outbuf = buffers[i];
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ for (j = 0; j < n_datas; j++) {
+ if (size == SPA_ID_INVALID)
+ size = d[j].maxsize;
+ else if (size != d[j].maxsize)
+ return -EINVAL;
+
+ if (d[j].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ return -EINVAL;
+ }
+ if (!SPA_IS_ALIGNED(d[j].data, 16)) {
+ spa_log_warn(this->log, NAME " %p: memory %d on buffer %d not aligned",
+ this, j, i);
+ }
+ }
+ recycle_buffer(this, port, b);
+ }
+ port->n_buffers = n_buffers;
+ port->size = size;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static struct buffer *dequeue_buffer(struct impl *this, struct port *port)
+{
+ struct buffer *b;
+
+ if (spa_list_is_empty(&port->queue))
+ return NULL;
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ return b;
+}
+
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+ port = GET_PORT(this, SPA_DIRECTION_OUTPUT, port_id);
+ if (buffer_id < port->n_buffers)
+ recycle_buffer(this, port, &port->buffers[buffer_id]);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+ struct buffer *buf;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = GET_OUT_PORT(this, 0);
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, NAME " %p: status %d", this, io->status);
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ goto done;
+
+ /* recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, &port->buffers[io->buffer_id]);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if ((buf = dequeue_buffer(this, port)) == NULL)
+ return io->status = -EPIPE;
+
+ io->status = SPA_STATUS_HAVE_DATA;
+ io->buffer_id = buf->id;
+
+ done:
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "clock.quantum-limit"))
+ spa_atou32(s, &this->quantum_limit, 0);
+ }
+
+ spa_log_debug(this->log, NAME " %p: init", this);
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 2;
+ props_reset(&this->props);
+
+ port = GET_OUT_PORT(this, 0);
+ port->direction = SPA_DIRECTION_OUTPUT;
+ port->id = 0;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+ spa_list_init(&port->queue);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+const struct spa_handle_factory test_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_AUDIO_PROCESS_CHANNELMIX,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/audioconvert/volume-ops-c.c b/spa/plugins/audioconvert/volume-ops-c.c
new file mode 100644
index 0000000..0cc6f5f
--- /dev/null
+++ b/spa/plugins/audioconvert/volume-ops-c.c
@@ -0,0 +1,45 @@
+/* Spa
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "volume-ops.h"
+
+void
+volume_f32_c(struct volume *vol, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, float volume, uint32_t n_samples)
+{
+ uint32_t n;
+ float *d = (float*)dst;
+ const float *s = (const float*)src;
+
+ if (volume == VOLUME_MIN) {
+ memset(d, 0, n_samples * sizeof(float));
+ }
+ else if (volume == VOLUME_NORM) {
+ spa_memcpy(d, s, n_samples * sizeof(float));
+ }
+ else {
+ for (n = 0; n < n_samples; n++)
+ d[n] = s[n] * volume;
+ }
+}
diff --git a/spa/plugins/audioconvert/volume-ops-sse.c b/spa/plugins/audioconvert/volume-ops-sse.c
new file mode 100644
index 0000000..cd1f3cc
--- /dev/null
+++ b/spa/plugins/audioconvert/volume-ops-sse.c
@@ -0,0 +1,66 @@
+/* Spa
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "volume-ops.h"
+
+#include <xmmintrin.h>
+
+void
+volume_f32_sse(struct volume *vol, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, float volume, uint32_t n_samples)
+{
+ uint32_t n, unrolled;
+ float *d = (float*)dst;
+ const float *s = (const float*)src;
+
+ if (volume == VOLUME_MIN) {
+ memset(d, 0, n_samples * sizeof(float));
+ }
+ else if (volume == VOLUME_NORM) {
+ spa_memcpy(d, s, n_samples * sizeof(float));
+ }
+ else {
+ __m128 t[4];
+ const __m128 vol = _mm_set1_ps(volume);
+
+ if (SPA_IS_ALIGNED(d, 16) &&
+ SPA_IS_ALIGNED(s, 16))
+ unrolled = n_samples & ~15;
+ else
+ unrolled = 0;
+
+ for(n = 0; n < unrolled; n += 16) {
+ t[0] = _mm_load_ps(&s[n]);
+ t[1] = _mm_load_ps(&s[n+4]);
+ t[2] = _mm_load_ps(&s[n+8]);
+ t[3] = _mm_load_ps(&s[n+12]);
+ _mm_store_ps(&d[n], _mm_mul_ps(t[0], vol));
+ _mm_store_ps(&d[n+4], _mm_mul_ps(t[1], vol));
+ _mm_store_ps(&d[n+8], _mm_mul_ps(t[2], vol));
+ _mm_store_ps(&d[n+12], _mm_mul_ps(t[3], vol));
+ }
+ for(; n < n_samples; n++)
+ _mm_store_ss(&d[n], _mm_mul_ss(_mm_load_ss(&s[n]), vol));
+ }
+}
diff --git a/spa/plugins/audioconvert/volume-ops.c b/spa/plugins/audioconvert/volume-ops.c
new file mode 100644
index 0000000..6890cfa
--- /dev/null
+++ b/spa/plugins/audioconvert/volume-ops.c
@@ -0,0 +1,84 @@
+/* Spa
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/support/cpu.h>
+#include <spa/support/log.h>
+#include <spa/utils/defs.h>
+
+#include "volume-ops.h"
+
+typedef void (*volume_func_t) (struct volume *vol, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, float volume, uint32_t n_samples);
+
+#define MAKE(func,...) \
+ { func, #func , __VA_ARGS__ }
+
+static const struct volume_info {
+ volume_func_t process;
+ const char *name;
+ uint32_t cpu_flags;
+} volume_table[] =
+{
+#if defined (HAVE_SSE)
+ MAKE(volume_f32_sse, SPA_CPU_FLAG_SSE),
+#endif
+ MAKE(volume_f32_c),
+};
+#undef MAKE
+
+#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a)
+
+static const struct volume_info *find_volume_info(uint32_t cpu_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(volume_table, t) {
+ if (MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+ return t;
+ }
+ return NULL;
+}
+
+static void impl_volume_free(struct volume *vol)
+{
+ vol->process = NULL;
+}
+
+int volume_init(struct volume *vol)
+{
+ const struct volume_info *info;
+
+ info = find_volume_info(vol->cpu_flags);
+ if (info == NULL)
+ return -ENOTSUP;
+
+ vol->cpu_flags = info->cpu_flags;
+ vol->func_name = info->name;
+ vol->free = impl_volume_free;
+ vol->process = info->process;
+ return 0;
+}
diff --git a/spa/plugins/audioconvert/volume-ops.h b/spa/plugins/audioconvert/volume-ops.h
new file mode 100644
index 0000000..0825712
--- /dev/null
+++ b/spa/plugins/audioconvert/volume-ops.h
@@ -0,0 +1,68 @@
+/* Spa
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/utils/defs.h>
+#include <spa/param/audio/raw.h>
+
+#define VOLUME_MIN 0.0f
+#define VOLUME_NORM 1.0f
+
+struct volume {
+ uint32_t cpu_flags;
+ const char *func_name;
+
+ struct spa_log *log;
+
+ uint32_t flags;
+
+ void (*process) (struct volume *vol, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, float volume, uint32_t n_samples);
+ void (*free) (struct volume *vol);
+
+ void *data;
+};
+
+int volume_init(struct volume *vol);
+
+#define volume_process(vol,...) (vol)->process(vol, __VA_ARGS__)
+#define volume_free(vol) (vol)->free(vol)
+
+#define DEFINE_FUNCTION(name,arch) \
+void volume_##name##_##arch(struct volume *vol, \
+ void * SPA_RESTRICT dst, \
+ const void * SPA_RESTRICT src, \
+ float volume, uint32_t n_samples);
+
+#define VOLUME_OPS_MAX_ALIGN 16
+
+DEFINE_FUNCTION(f32, c);
+
+#if defined (HAVE_SSE)
+DEFINE_FUNCTION(f32, sse);
+#endif
+
+#undef DEFINE_FUNCTION
diff --git a/spa/plugins/audiomixer/audiomixer.c b/spa/plugins/audiomixer/audiomixer.c
new file mode 100644
index 0000000..25da978
--- /dev/null
+++ b/spa/plugins/audiomixer/audiomixer.c
@@ -0,0 +1,990 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/cpu.h>
+#include <spa/utils/list.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+
+#include "mix-ops.h"
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT log_topic
+static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.audiomixer");
+
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+
+#define MAX_BUFFERS 64
+#define MAX_PORTS 128
+#define MAX_CHANNELS 64
+#define MAX_ALIGN MIX_OPS_MAX_ALIGN
+
+#define PORT_DEFAULT_VOLUME 1.0
+#define PORT_DEFAULT_MUTE false
+
+struct port_props {
+ double volume;
+ int32_t mute;
+};
+
+static void port_props_reset(struct port_props *props)
+{
+ props->volume = PORT_DEFAULT_VOLUME;
+ props->mute = PORT_DEFAULT_MUTE;
+}
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_QUEUED (1 << 0)
+ uint32_t flags;
+
+ struct spa_list link;
+ struct spa_buffer *buffer;
+ struct spa_meta_header *h;
+ struct spa_buffer buf;
+};
+
+struct port {
+ uint32_t direction;
+ uint32_t id;
+
+ struct port_props props;
+
+ struct spa_io_buffers *io;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[8];
+
+ unsigned int valid:1;
+ unsigned int have_format:1;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list queue;
+ size_t queued_bytes;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_cpu *cpu;
+ uint32_t cpu_flags;
+ uint32_t max_align;
+ uint32_t quantum_limit;
+
+ struct mix_ops ops;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[8];
+
+ struct spa_hook_list hooks;
+
+ uint32_t port_count;
+ uint32_t last_port;
+ struct port *in_ports[MAX_PORTS];
+ struct port out_ports[1];
+
+ int n_formats;
+ struct spa_audio_info format;
+
+ unsigned int have_format:1;
+ unsigned int started:1;
+ uint32_t stride;
+ uint32_t blocks;
+};
+
+#define PORT_VALID(p) ((p) != NULL && (p)->valid)
+#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)]))
+#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)]))
+#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+#define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p))
+#define GET_IN_PORT(this,p) (this->in_ports[p])
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ this->started = true;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, GET_OUT_PORT(this, 0), true);
+ for (i = 0; i < this->last_port; i++) {
+ if (PORT_VALID(this->in_ports[i]))
+ emit_port_info(this, GET_IN_PORT(this, i), true);
+ }
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *user_data)
+{
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_IN_PORT(this, port_id);
+ if (port == NULL) {
+ port = calloc(1, sizeof(struct port));
+ if (port == NULL)
+ return -errno;
+ this->in_ports[port_id] = port;
+ }
+ port->direction = SPA_DIRECTION_INPUT;
+ port->id = port_id;
+
+ port_props_reset(&port->props);
+
+ spa_list_init(&port->queue);
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF |
+ SPA_PORT_FLAG_DYNAMIC_DATA |
+ SPA_PORT_FLAG_REMOVABLE |
+ SPA_PORT_FLAG_OPTIONAL;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+
+ this->port_count++;
+ if (this->last_port <= port_id)
+ this->last_port = port_id + 1;
+ port->valid = true;
+
+ spa_log_debug(this->log, "%p: add port %d:%d %d", this,
+ direction, port_id, this->last_port);
+ emit_port_info(this, port, true);
+
+ return 0;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_IN_PORT(this, port_id);
+
+ port->valid = false;
+ this->port_count--;
+ if (port->have_format && this->have_format) {
+ if (--this->n_formats == 0)
+ this->have_format = false;
+ }
+ spa_memzero(port, sizeof(struct port));
+
+ if (port_id + 1 == this->last_port) {
+ int i;
+
+ for (i = this->last_port - 1; i >= 0; i--)
+ if (PORT_VALID(GET_IN_PORT(this, i)))
+ break;
+
+ this->last_port = i + 1;
+ }
+ spa_log_debug(this->log, "%p: remove port %d:%d %d", this,
+ direction, port_id, this->last_port);
+
+ spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
+
+ return 0;
+}
+
+static int port_enum_formats(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct impl *this = object;
+
+ switch (index) {
+ case 0:
+ if (this->have_format) {
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(this->format.info.raw.format),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(this->format.info.raw.rate),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(this->format.info.raw.channels));
+ } else {
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Int(12,
+ SPA_AUDIO_FORMAT_S8,
+ SPA_AUDIO_FORMAT_U8,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_U16,
+ SPA_AUDIO_FORMAT_S24,
+ SPA_AUDIO_FORMAT_U24,
+ SPA_AUDIO_FORMAT_S32,
+ SPA_AUDIO_FORMAT_U32,
+ SPA_AUDIO_FORMAT_S24_32,
+ SPA_AUDIO_FORMAT_U24_32,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F64),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(
+ DEFAULT_RATE, 1, INT32_MAX),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
+ DEFAULT_CHANNELS, 1, INT32_MAX));
+ }
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(this, direction, port_id, result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &this->format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->stride,
+ 16 * this->stride,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->stride));
+ break;
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, "%p: clear buffers %p", this, port);
+ port->n_buffers = 0;
+ spa_list_init(&port->queue);
+ }
+ return 0;
+}
+
+static int queue_buffer(struct impl *this, struct port *port, struct buffer *b)
+{
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
+ return -EINVAL;
+
+ spa_list_append(&port->queue, &b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED);
+ spa_log_trace_fp(this->log, "%p: queue buffer %d", this, b->id);
+ return 0;
+}
+
+static struct buffer *dequeue_buffer(struct impl *this, struct port *port)
+{
+ struct buffer *b;
+
+ if (spa_list_is_empty(&port->queue))
+ return NULL;
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED);
+ spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id);
+ return b;
+}
+
+static int calc_width(struct spa_audio_info *info)
+{
+ switch (info->info.raw.format) {
+ case SPA_AUDIO_FORMAT_U8P:
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8P:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return 1;
+ case SPA_AUDIO_FORMAT_S16P:
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return 2;
+ case SPA_AUDIO_FORMAT_S24P:
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return 3;
+ case SPA_AUDIO_FORMAT_F64P:
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return 8;
+ default:
+ return 4;
+ }
+}
+
+static int 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);
+
+ if (format == NULL) {
+ if (port->have_format) {
+ port->have_format = false;
+ if (--this->n_formats == 0)
+ this->have_format = false;
+ clear_buffers(this, port);
+ }
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (this->have_format) {
+ if (memcmp(&info, &this->format, sizeof(struct spa_audio_info)))
+ return -EINVAL;
+ } else {
+ if (info.info.raw.format == 0 ||
+ info.info.raw.channels == 0)
+ return -EINVAL;
+
+ this->ops.fmt = info.info.raw.format;
+ this->ops.n_channels = info.info.raw.channels;
+ this->ops.cpu_flags = this->cpu_flags;
+
+ if ((res = mix_ops_init(&this->ops)) < 0)
+ return res;
+
+ this->stride = calc_width(&info);
+
+ if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) {
+ this->blocks = info.info.raw.channels;
+ } else {
+ this->stride *= info.info.raw.channels;
+ this->blocks = 1;
+ }
+
+ this->have_format = true;
+ this->format = info;
+ }
+ if (!port->have_format) {
+ this->n_formats++;
+ port->have_format = true;
+ spa_log_debug(this->log, "%p: set format on port %d",
+ this, port_id);
+ }
+ }
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(this, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_debug(this->log, "%p: use %d buffers on port %d:%d",
+ this, n_buffers, direction, port_id);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->buffer = buffers[i];
+ b->flags = 0;
+ b->id = i;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+ b->buf = *buffers[i];
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: invalid memory on buffer %p", this,
+ buffers[i]);
+ return -EINVAL;
+ }
+ if (!SPA_IS_ALIGNED(d[0].data, this->max_align)) {
+ spa_log_warn(this->log, "%p: memory on buffer %d not aligned", this, i);
+ }
+ if (direction == SPA_DIRECTION_OUTPUT)
+ queue_buffer(this, port, b);
+
+ spa_log_debug(this->log, "%p: port %d:%d buffer:%d n_data:%d data:%p maxsize:%d",
+ this, direction, port_id, i,
+ buffers[i]->n_datas, d[0].data, d[0].maxsize);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_debug(this->log, "%p: port %d:%d io %d %p/%zd", this,
+ direction, port_id, id, data, size);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+ port = GET_OUT_PORT(this, 0);
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ return queue_buffer(this, port, &port->buffers[buffer_id]);
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *outport;
+ struct spa_io_buffers *outio;
+ uint32_t n_buffers, i, maxsize;
+ struct buffer **buffers;
+ struct buffer *outb;
+ const void **datas;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ outport = GET_OUT_PORT(this, 0);
+ if ((outio = outport->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, "%p: status %p %d %d",
+ this, outio, outio->status, outio->buffer_id);
+
+ if (SPA_UNLIKELY(outio->status == SPA_STATUS_HAVE_DATA))
+ return outio->status;
+
+ /* recycle */
+ if (SPA_LIKELY(outio->buffer_id < outport->n_buffers)) {
+ queue_buffer(this, outport, &outport->buffers[outio->buffer_id]);
+ outio->buffer_id = SPA_ID_INVALID;
+ }
+
+ buffers = alloca(MAX_PORTS * sizeof(struct buffer *));
+ datas = alloca(MAX_PORTS * sizeof(void *));
+ n_buffers = 0;
+
+ maxsize = UINT32_MAX;
+
+ for (i = 0; i < this->last_port; i++) {
+ struct port *inport = GET_IN_PORT(this, i);
+ struct spa_io_buffers *inio = NULL;
+ struct buffer *inb;
+ struct spa_data *bd;
+ uint32_t size, offs;
+
+ if (SPA_UNLIKELY(!PORT_VALID(inport) ||
+ (inio = inport->io) == NULL ||
+ inio->buffer_id >= inport->n_buffers ||
+ inio->status != SPA_STATUS_HAVE_DATA)) {
+ spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d "
+ "io:%p status:%d buf_id:%d n_buffers:%d", this,
+ i, PORT_VALID(inport), inio,
+ inio ? inio->status : -1,
+ inio ? inio->buffer_id : SPA_ID_INVALID,
+ inport->n_buffers);
+ continue;
+ }
+
+ inb = &inport->buffers[inio->buffer_id];
+ bd = &inb->buffer->datas[0];
+
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->maxsize - offs, bd->chunk->size);
+ maxsize = SPA_MIN(size, maxsize);
+
+ spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d %d:%d", this,
+ i, inio, outio, inio->status, inio->buffer_id,
+ offs, size);
+
+ if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) {
+ datas[n_buffers] = SPA_PTROFF(bd->data, offs, void);
+ buffers[n_buffers++] = inb;
+ }
+ inio->status = SPA_STATUS_NEED_DATA;
+ }
+
+ outb = dequeue_buffer(this, outport);
+ if (SPA_UNLIKELY(outb == NULL)) {
+ spa_log_trace(this->log, "%p: out of buffers", this);
+ return -EPIPE;
+ }
+
+ if (n_buffers == 1) {
+ *outb->buffer = *buffers[0]->buffer;
+ } else {
+ struct spa_data *d = outb->buf.datas;
+
+ *outb->buffer = outb->buf;
+
+ maxsize = SPA_MIN(maxsize, d[0].maxsize);
+
+ d[0].chunk->offset = 0;
+ d[0].chunk->size = maxsize;
+ d[0].chunk->stride = this->stride;
+ SPA_FLAG_UPDATE(d[0].chunk->flags, SPA_CHUNK_FLAG_EMPTY, n_buffers == 0);
+
+ mix_ops_process(&this->ops, d[0].data,
+ datas, n_buffers, maxsize / this->stride);
+ }
+
+ outio->buffer_id = outb->id;
+ outio->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+ uint32_t i;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ for (i = 0; i < MAX_PORTS; i++)
+ free(this->in_ports[i]);
+ mix_ops_free(&this->ops);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(this->log, log_topic);
+
+ this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+ if (this->cpu) {
+ this->cpu_flags = spa_cpu_get_flags(this->cpu);
+ this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
+ }
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "clock.quantum-limit"))
+ spa_atou32(s, &this->quantum_limit, 0);
+ }
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = MAX_PORTS;
+ this->info.max_output_ports = 1;
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS;
+ this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS;
+
+ port = GET_OUT_PORT(this, 0);
+ port->valid = true;
+ port->direction = SPA_DIRECTION_OUTPUT;
+ port->id = 0;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+
+ spa_list_init(&port->queue);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+const struct spa_handle_factory spa_audiomixer_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_AUDIO_MIXER,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/audiomixer/benchmark-mix-ops.c b/spa/plugins/audiomixer/benchmark-mix-ops.c
new file mode 100644
index 0000000..e698417
--- /dev/null
+++ b/spa/plugins/audiomixer/benchmark-mix-ops.c
@@ -0,0 +1,222 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include "test-helper.h"
+#include "mix-ops.h"
+
+static uint32_t cpu_flags;
+
+typedef void (*mix_func_t) (struct mix_ops *ops, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples);
+struct stats {
+ uint32_t n_samples;
+ uint32_t n_src;
+ uint64_t perf;
+ const char *name;
+ const char *impl;
+};
+
+#define MAX_SAMPLES 4096
+#define MAX_SRC 11
+
+#define MAX_COUNT 100
+
+static uint8_t samp_in[MAX_SAMPLES * MAX_SRC * 8];
+static uint8_t samp_out[MAX_SAMPLES * 8];
+
+static const int sample_sizes[] = { 0, 1, 128, 513, 4096 };
+static const int src_counts[] = { 1, 2, 4, 6, 8, 11 };
+
+#define MAX_RESULTS SPA_N_ELEMENTS(sample_sizes) * SPA_N_ELEMENTS(src_counts) * 70
+
+static uint32_t n_results = 0;
+static struct stats results[MAX_RESULTS];
+
+static void run_test1(const char *name, const char *impl, mix_func_t func, int n_src, int n_samples)
+{
+ int i, j;
+ const void *ip[n_src];
+ void *op;
+ struct timespec ts;
+ uint64_t count, t1, t2;
+ struct mix_ops mix;
+
+ mix.n_channels = 1;
+
+ for (j = 0; j < n_src; j++)
+ ip[j] = SPA_PTR_ALIGN(&samp_in[j * n_samples * 4], 32, void);
+ op = SPA_PTR_ALIGN(samp_out, 32, void);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ count = 0;
+ for (i = 0; i < MAX_COUNT; i++) {
+ func(&mix, op, ip, n_src, n_samples);
+ count++;
+ }
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ spa_assert(n_results < MAX_RESULTS);
+
+ results[n_results++] = (struct stats) {
+ .n_samples = n_samples,
+ .n_src = n_src,
+ .perf = count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1),
+ .name = name,
+ .impl = impl
+ };
+}
+
+static void run_test(const char *name, const char *impl, mix_func_t func)
+{
+ size_t i, j;
+
+ for (i = 0; i < SPA_N_ELEMENTS(sample_sizes); i++) {
+ for (j = 0; j < SPA_N_ELEMENTS(src_counts); j++) {
+ run_test1(name, impl, func, src_counts[j],
+ (sample_sizes[i] + (src_counts[j] -1)) / src_counts[j]);
+ }
+ }
+}
+
+static void test_s8(void)
+{
+ run_test("test_s8", "c", mix_s8_c);
+}
+static void test_u8(void)
+{
+ run_test("test_u8", "c", mix_u8_c);
+}
+
+static void test_s16(void)
+{
+ run_test("test_s16", "c", mix_s16_c);
+}
+static void test_u16(void)
+{
+ run_test("test_u8", "c", mix_u16_c);
+}
+
+static void test_s24(void)
+{
+ run_test("test_s24", "c", mix_s24_c);
+}
+static void test_u24(void)
+{
+ run_test("test_u24", "c", mix_u24_c);
+}
+static void test_s24_32(void)
+{
+ run_test("test_s24_32", "c", mix_s24_32_c);
+}
+static void test_u24_32(void)
+{
+ run_test("test_u24_32", "c", mix_u24_32_c);
+}
+
+static void test_s32(void)
+{
+ run_test("test_s32", "c", mix_s32_c);
+}
+static void test_u32(void)
+{
+ run_test("test_u32", "c", mix_u32_c);
+}
+
+static void test_f32(void)
+{
+ run_test("test_f32", "c", mix_f32_c);
+#if defined (HAVE_SSE)
+ if (cpu_flags & SPA_CPU_FLAG_SSE) {
+ run_test("test_f32", "sse", mix_f32_sse);
+ }
+#endif
+#if defined (HAVE_AVX)
+ if (cpu_flags & SPA_CPU_FLAG_AVX) {
+ run_test("test_f32", "avx", mix_f32_avx);
+ }
+#endif
+}
+
+static void test_f64(void)
+{
+ run_test("test_f64", "c", mix_f64_c);
+#if defined (HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_f64", "sse2", mix_f64_sse2);
+ }
+#endif
+}
+
+static int compare_func(const void *_a, const void *_b)
+{
+ const struct stats *a = _a, *b = _b;
+ int diff;
+ if ((diff = strcmp(a->name, b->name)) != 0) return diff;
+ if ((diff = a->n_samples - b->n_samples) != 0) return diff;
+ if ((diff = a->n_src - b->n_src) != 0) return diff;
+ if ((diff = b->perf - a->perf) != 0) return diff;
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ uint32_t i;
+
+ cpu_flags = get_cpu_flags();
+ printf("got get CPU flags %d\n", cpu_flags);
+
+ test_s8();
+ test_u8();
+ test_s16();
+ test_u16();
+ test_s24();
+ test_u24();
+ test_s32();
+ test_u32();
+ test_s24_32();
+ test_u24_32();
+ test_f32();
+ test_f64();
+
+ qsort(results, n_results, sizeof(struct stats), compare_func);
+
+ for (i = 0; i < n_results; i++) {
+ struct stats *s = &results[i];
+ fprintf(stderr, "%-12."PRIu64" \t%-32.32s %s \t samples %d, src %d\n",
+ s->perf, s->name, s->impl, s->n_samples, s->n_src);
+ }
+ return 0;
+}
diff --git a/spa/plugins/audiomixer/meson.build b/spa/plugins/audiomixer/meson.build
new file mode 100644
index 0000000..5a4a1fb
--- /dev/null
+++ b/spa/plugins/audiomixer/meson.build
@@ -0,0 +1,126 @@
+audiomixer_sources = [
+ 'audiomixer.c',
+ 'mixer-dsp.c',
+ 'plugin.c'
+]
+
+simd_cargs = []
+simd_dependencies = []
+
+audiomixer_c = static_library('audiomixer_c',
+ ['mix-ops-c.c' ],
+ c_args : ['-O3'],
+ dependencies : [ spa_dep ],
+ install : false
+)
+simd_dependencies += audiomixer_c
+
+if have_sse
+ audiomixer_sse = static_library('audiomixer_sse',
+ ['mix-ops-sse.c' ],
+ c_args : [sse_args, '-O3', '-DHAVE_SSE'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_SSE']
+ simd_dependencies += audiomixer_sse
+endif
+if have_sse2
+ audiomixer_sse2 = static_library('audiomixer_sse2',
+ ['mix-ops-sse2.c' ],
+ c_args : [sse2_args, '-O3', '-DHAVE_SSE2'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_SSE2']
+ simd_dependencies += audiomixer_sse2
+endif
+if have_avx and have_fma
+ audiomixer_avx = static_library('audiomixer_avx',
+ ['mix-ops-avx.c'],
+ c_args : [avx_args, fma_args, '-O3', '-DHAVE_AVX', '-DHAVE_FMA'],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+ simd_cargs += ['-DHAVE_AVX', '-DHAVE_FMA']
+ simd_dependencies += audiomixer_avx
+endif
+
+audiomixer_lib = static_library('audiomixer',
+ ['mix-ops.c' ],
+ c_args : [ simd_cargs, '-O3'],
+ link_with : simd_dependencies,
+ include_directories : [configinc],
+ dependencies : [ spa_dep ],
+ install : false
+ )
+audiomixer_dep = declare_dependency(link_with: audiomixer_lib)
+
+spa_audiomixer_lib = shared_library('spa-audiomixer',
+ audiomixer_sources,
+ c_args : simd_cargs,
+ link_with : simd_dependencies,
+ dependencies : [ spa_dep, mathlib, audiomixer_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'audiomixer'
+)
+spa_audiomixer_dep = declare_dependency(link_with: spa_audiomixer_lib)
+
+test_apps = [
+ 'test-mix-ops',
+ ]
+
+foreach a : test_apps
+ test(a,
+ executable(a, a + '.c',
+ dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audiomixer_dep ],
+ include_directories : [ configinc ],
+ link_with : [ test_lib ],
+ install_rpath : spa_plugindir / 'audiomixer',
+ c_args : [ simd_cargs ],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'audiomixer'),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ ])
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'audiomixer' / a)
+ configure_file(
+ input: installed_tests_template,
+ output: a + '.test',
+ install_dir: installed_tests_metadir / 'audiomixer',
+ configuration: test_conf
+ )
+ endif
+endforeach
+
+benchmark_apps = [
+ 'benchmark-mix-ops',
+ ]
+
+foreach a : benchmark_apps
+ benchmark(a,
+ executable(a, a + '.c',
+ dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audiomixer_dep ],
+ include_directories : [ configinc ],
+ c_args : [ simd_cargs ],
+ install_rpath : spa_plugindir / 'audiomixer',
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'audiomixer'),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ ])
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'audiomixer' / a)
+ configure_file(
+ input: installed_tests_template,
+ output: a + '.test',
+ install_dir: installed_tests_metadir / 'audiomixer',
+ configuration: test_conf
+ )
+ endif
+endforeach
diff --git a/spa/plugins/audiomixer/mix-ops-avx.c b/spa/plugins/audiomixer/mix-ops-avx.c
new file mode 100644
index 0000000..3c5aa6b
--- /dev/null
+++ b/spa/plugins/audiomixer/mix-ops-avx.c
@@ -0,0 +1,88 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "mix-ops.h"
+
+#include <immintrin.h>
+
+void
+mix_f32_avx(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_src, uint32_t n_samples)
+{
+ n_samples *= ops->n_channels;
+
+ if (n_src == 0)
+ memset(dst, 0, n_samples * ops->n_channels * sizeof(float));
+ else if (n_src == 1) {
+ if (dst != src[0])
+ spa_memcpy(dst, src[0], n_samples * sizeof(float));
+ } else {
+ uint32_t i, n, unrolled;
+ const float **s = (const float **)src;
+ float *d = dst;
+
+ if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 32))) {
+ unrolled = n_samples & ~31;
+ for (i = 0; i < n_src; i++) {
+ if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 32))) {
+ unrolled = 0;
+ break;
+ }
+ }
+ } else
+ unrolled = 0;
+
+ for (n = 0; n < unrolled; n += 32) {
+ __m256 in[4];
+
+ in[0] = _mm256_load_ps(&s[0][n + 0]);
+ in[1] = _mm256_load_ps(&s[0][n + 8]);
+ in[2] = _mm256_load_ps(&s[0][n + 16]);
+ in[3] = _mm256_load_ps(&s[0][n + 24]);
+ for (i = 1; i < n_src; i++) {
+ in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&s[i][n + 0]));
+ in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&s[i][n + 8]));
+ in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&s[i][n + 16]));
+ in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&s[i][n + 24]));
+ }
+ _mm256_store_ps(&d[n + 0], in[0]);
+ _mm256_store_ps(&d[n + 8], in[1]);
+ _mm256_store_ps(&d[n + 16], in[2]);
+ _mm256_store_ps(&d[n + 24], in[3]);
+ }
+ for (; n < n_samples; n++) {
+ __m128 in[1];
+ in[0] = _mm_load_ss(&s[0][n]);
+ for (i = 1; i < n_src; i++)
+ in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n]));
+ _mm_store_ss(&d[n], in[0]);
+ }
+ }
+}
diff --git a/spa/plugins/audiomixer/mix-ops-c.c b/spa/plugins/audiomixer/mix-ops-c.c
new file mode 100644
index 0000000..2f79cd8
--- /dev/null
+++ b/spa/plugins/audiomixer/mix-ops-c.c
@@ -0,0 +1,68 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "mix-ops.h"
+
+#define MAKE_FUNC(name,type,atype,accum,clamp,zero) \
+void mix_ ##name## _c(struct mix_ops *ops, \
+ void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], \
+ uint32_t n_src, uint32_t n_samples) \
+{ \
+ uint32_t i, n; \
+ type *d = dst; \
+ const type **s = (const type **)src; \
+ n_samples *= ops->n_channels; \
+ if (n_src == 0 && zero) \
+ memset(dst, 0, n_samples * sizeof(type)); \
+ else if (n_src == 1) { \
+ if (dst != src[0]) \
+ spa_memcpy(dst, src[0], n_samples * sizeof(type)); \
+ } else { \
+ for (n = 0; n < n_samples; n++) { \
+ atype ac = 0; \
+ for (i = 0; i < n_src; i++) \
+ ac = accum (ac, s[i][n]); \
+ d[n] = clamp (ac); \
+ } \
+ } \
+}
+
+MAKE_FUNC(s8, int8_t, int16_t, S8_ACCUM, S8_CLAMP, true);
+MAKE_FUNC(u8, uint8_t, int16_t, U8_ACCUM, U8_CLAMP, false);
+MAKE_FUNC(s16, int16_t, int32_t, S16_ACCUM, S16_CLAMP, true);
+MAKE_FUNC(u16, uint16_t, int16_t, U16_ACCUM, U16_CLAMP, false);
+MAKE_FUNC(s24, int24_t, int32_t, S24_ACCUM, S24_CLAMP, false);
+MAKE_FUNC(u24, uint24_t, int32_t, U24_ACCUM, U24_CLAMP, false);
+MAKE_FUNC(s32, int32_t, int64_t, S32_ACCUM, S32_CLAMP, true);
+MAKE_FUNC(u32, uint32_t, int64_t, U32_ACCUM, U32_CLAMP, false);
+MAKE_FUNC(s24_32, int32_t, int32_t, S24_32_ACCUM, S24_32_CLAMP, true);
+MAKE_FUNC(u24_32, uint32_t, int32_t, U24_32_ACCUM, U24_32_CLAMP, false);
+MAKE_FUNC(f32, float, float, F32_ACCUM, F32_CLAMP, true);
+MAKE_FUNC(f64, double, double, F64_ACCUM, F64_CLAMP, true);
diff --git a/spa/plugins/audiomixer/mix-ops-sse.c b/spa/plugins/audiomixer/mix-ops-sse.c
new file mode 100644
index 0000000..bae619b
--- /dev/null
+++ b/spa/plugins/audiomixer/mix-ops-sse.c
@@ -0,0 +1,87 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "mix-ops.h"
+
+#include <xmmintrin.h>
+
+void
+mix_f32_sse(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_src, uint32_t n_samples)
+{
+ n_samples *= ops->n_channels;
+
+ if (n_src == 0) {
+ memset(dst, 0, n_samples * sizeof(float));
+ } else if (n_src == 1) {
+ if (dst != src[0])
+ spa_memcpy(dst, src[0], n_samples * sizeof(float));
+ } else {
+ uint32_t n, i, unrolled;
+ __m128 in[4];
+ const float **s = (const float **)src;
+ float *d = dst;
+
+ if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) {
+ unrolled = n_samples & ~15;
+ for (i = 0; i < n_src; i++) {
+ if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) {
+ unrolled = 0;
+ break;
+ }
+ }
+ } else
+ unrolled = 0;
+
+ 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]);
+ }
+ }
+}
diff --git a/spa/plugins/audiomixer/mix-ops-sse2.c b/spa/plugins/audiomixer/mix-ops-sse2.c
new file mode 100644
index 0000000..e2f632d
--- /dev/null
+++ b/spa/plugins/audiomixer/mix-ops-sse2.c
@@ -0,0 +1,87 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "mix-ops.h"
+
+#include <emmintrin.h>
+
+void
+mix_f64_sse2(struct mix_ops *ops, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[],
+ uint32_t n_src, uint32_t n_samples)
+{
+ n_samples *= ops->n_channels;
+
+ if (n_src == 0) {
+ memset(dst, 0, n_samples * sizeof(double));
+ } else if (n_src == 1) {
+ if (dst != src[0])
+ spa_memcpy(dst, src[0], n_samples * sizeof(double));
+ } else {
+ uint32_t n, i, unrolled;
+ __m128d in[4];
+ const double **s = (const double **)src;
+ double *d = dst;
+
+ if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) {
+ unrolled = n_samples & ~15;
+ for (i = 0; i < n_src; i++) {
+ if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) {
+ unrolled = 0;
+ break;
+ }
+ }
+ } else
+ unrolled = 0;
+
+ for (n = 0; n < unrolled; n += 8) {
+ in[0] = _mm_load_pd(&s[0][n+0]);
+ in[1] = _mm_load_pd(&s[0][n+2]);
+ in[2] = _mm_load_pd(&s[0][n+4]);
+ in[3] = _mm_load_pd(&s[0][n+6]);
+
+ for (i = 1; i < n_src; i++) {
+ in[0] = _mm_add_pd(in[0], _mm_load_pd(&s[i][n+0]));
+ in[1] = _mm_add_pd(in[1], _mm_load_pd(&s[i][n+2]));
+ in[2] = _mm_add_pd(in[2], _mm_load_pd(&s[i][n+4]));
+ in[3] = _mm_add_pd(in[3], _mm_load_pd(&s[i][n+6]));
+ }
+ _mm_store_pd(&d[n+0], in[0]);
+ _mm_store_pd(&d[n+2], in[1]);
+ _mm_store_pd(&d[n+4], in[2]);
+ _mm_store_pd(&d[n+6], in[3]);
+ }
+ for (; n < n_samples; n++) {
+ in[0] = _mm_load_sd(&s[0][n]);
+ for (i = 1; i < n_src; i++)
+ in[0] = _mm_add_sd(in[0], _mm_load_sd(&s[i][n]));
+ _mm_store_sd(&d[n], in[0]);
+ }
+ }
+}
diff --git a/spa/plugins/audiomixer/mix-ops.c b/spa/plugins/audiomixer/mix-ops.c
new file mode 100644
index 0000000..b459926
--- /dev/null
+++ b/spa/plugins/audiomixer/mix-ops.c
@@ -0,0 +1,136 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/support/cpu.h>
+#include <spa/utils/defs.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "mix-ops.h"
+
+typedef void (*mix_func_t) (struct mix_ops *ops, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples);
+
+struct mix_info {
+ uint32_t fmt;
+ uint32_t n_channels;
+ uint32_t cpu_flags;
+ uint32_t stride;
+ mix_func_t process;
+};
+
+static struct mix_info mix_table[] =
+{
+ /* f32 */
+#if defined(HAVE_AVX)
+ { SPA_AUDIO_FORMAT_F32, 0, SPA_CPU_FLAG_AVX, 4, mix_f32_avx },
+ { SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_AVX, 4, mix_f32_avx },
+#endif
+#if defined (HAVE_SSE)
+ { SPA_AUDIO_FORMAT_F32, 0, SPA_CPU_FLAG_SSE, 4, mix_f32_sse },
+ { SPA_AUDIO_FORMAT_F32P, 0, SPA_CPU_FLAG_SSE, 4, mix_f32_sse },
+#endif
+ { SPA_AUDIO_FORMAT_F32, 0, 0, 4, mix_f32_c },
+ { SPA_AUDIO_FORMAT_F32P, 0, 0, 4, mix_f32_c },
+
+ /* f64 */
+#if defined (HAVE_SSE2)
+ { SPA_AUDIO_FORMAT_F64, 0, SPA_CPU_FLAG_SSE2, 8, mix_f64_sse2 },
+ { SPA_AUDIO_FORMAT_F64P, 0, SPA_CPU_FLAG_SSE2, 8, mix_f64_sse2 },
+#endif
+ { SPA_AUDIO_FORMAT_F64, 0, 0, 8, mix_f64_c },
+ { SPA_AUDIO_FORMAT_F64P, 0, 0, 8, mix_f64_c },
+
+ /* s8 */
+ { SPA_AUDIO_FORMAT_S8, 0, 0, 1, mix_s8_c },
+ { SPA_AUDIO_FORMAT_S8P, 0, 0, 1, mix_s8_c },
+ { SPA_AUDIO_FORMAT_U8, 0, 0, 1, mix_u8_c },
+ { SPA_AUDIO_FORMAT_U8P, 0, 0, 1, mix_u8_c },
+
+ /* s16 */
+ { SPA_AUDIO_FORMAT_S16, 0, 0, 2, mix_s16_c },
+ { SPA_AUDIO_FORMAT_S16P, 0, 0, 2, mix_s16_c },
+ { SPA_AUDIO_FORMAT_U16, 0, 0, 2, mix_u16_c },
+
+ /* s24 */
+ { SPA_AUDIO_FORMAT_S24, 0, 0, 3, mix_s24_c },
+ { SPA_AUDIO_FORMAT_S24P, 0, 0, 3, mix_s24_c },
+ { SPA_AUDIO_FORMAT_U24, 0, 0, 3, mix_u24_c },
+
+ /* s32 */
+ { SPA_AUDIO_FORMAT_S32, 0, 0, 4, mix_s32_c },
+ { SPA_AUDIO_FORMAT_S32P, 0, 0, 4, mix_s32_c },
+ { SPA_AUDIO_FORMAT_U32, 0, 0, 4, mix_u32_c },
+
+ /* s24_32 */
+ { SPA_AUDIO_FORMAT_S24_32, 0, 0, 4, mix_s24_32_c },
+ { SPA_AUDIO_FORMAT_S24_32P, 0, 0, 4, mix_s24_32_c },
+ { SPA_AUDIO_FORMAT_U24_32, 0, 0, 4, mix_u24_32_c },
+};
+
+#define MATCH_CHAN(a,b) ((a) == 0 || (a) == (b))
+#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a)
+
+static const struct mix_info *find_mix_info(uint32_t fmt,
+ uint32_t n_channels, uint32_t cpu_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(mix_table, t) {
+ if (t->fmt == fmt &&
+ MATCH_CHAN(t->n_channels, n_channels) &&
+ MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+ return t;
+ }
+ return NULL;
+}
+
+static void impl_mix_ops_clear(struct mix_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples)
+{
+ const struct mix_info *info = ops->priv;
+ memset(dst, 0, n_samples * info->stride);
+}
+
+static void impl_mix_ops_free(struct mix_ops *ops)
+{
+ spa_zero(*ops);
+}
+
+int mix_ops_init(struct mix_ops *ops)
+{
+ const struct mix_info *info;
+
+ info = find_mix_info(ops->fmt, ops->n_channels, ops->cpu_flags);
+ if (info == NULL)
+ return -ENOTSUP;
+
+ ops->priv = info;
+ ops->cpu_flags = info->cpu_flags;
+ ops->clear = impl_mix_ops_clear;
+ ops->process = info->process;
+ ops->free = impl_mix_ops_free;
+
+ return 0;
+}
diff --git a/spa/plugins/audiomixer/mix-ops.h b/spa/plugins/audiomixer/mix-ops.h
new file mode 100644
index 0000000..11e88dc
--- /dev/null
+++ b/spa/plugins/audiomixer/mix-ops.h
@@ -0,0 +1,169 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/defs.h>
+
+typedef struct {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ uint8_t v3;
+ uint8_t v2;
+ uint8_t v1;
+#else
+ uint8_t v1;
+ uint8_t v2;
+ uint8_t v3;
+#endif
+} __attribute__ ((packed)) uint24_t;
+
+typedef struct {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ uint8_t v3;
+ uint8_t v2;
+ int8_t v1;
+#else
+ int8_t v1;
+ uint8_t v2;
+ uint8_t v3;
+#endif
+} __attribute__ ((packed)) int24_t;
+
+static inline uint32_t u24_to_u32(uint24_t src)
+{
+ return ((uint32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3;
+}
+
+#define U32_TO_U24(s) (uint24_t) { .v1 = (uint8_t)(((uint32_t)s) >> 16), \
+ .v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) }
+
+static inline uint24_t u32_to_u24(uint32_t src)
+{
+ return U32_TO_U24(src);
+}
+
+static inline int32_t s24_to_s32(int24_t src)
+{
+ return ((int32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3;
+}
+
+#define S32_TO_S24(s) (int24_t) { .v1 = (int8_t)(((int32_t)s) >> 16), \
+ .v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) }
+
+static inline int24_t s32_to_s24(int32_t src)
+{
+ return S32_TO_S24(src);
+}
+
+
+#define S8_MIN -128
+#define S8_MAX 127
+#define S8_ACCUM(a,b) ((a) + (int16_t)(b))
+#define S8_CLAMP(a) (int8_t)(SPA_CLAMP((a), S8_MIN, S8_MAX))
+#define U8_OFFS 128
+#define U8_ACCUM(a,b) ((a) + ((int16_t)(b) - U8_OFFS))
+#define U8_CLAMP(a) (uint8_t)(SPA_CLAMP((a), S8_MIN, S8_MAX) + U8_OFFS)
+
+#define S16_MIN -32768
+#define S16_MAX 32767
+#define S16_ACCUM(a,b) ((a) + (int32_t)(b))
+#define S16_CLAMP(a) (int16_t)(SPA_CLAMP((a), S16_MIN, S16_MAX))
+#define U16_OFFS 32768
+#define U16_ACCUM(a,b) ((a) + ((int32_t)(b) - U16_OFFS))
+#define U16_CLAMP(a) (uint16_t)(SPA_CLAMP((a), S16_MIN, S16_MAX) + U16_OFFS)
+
+#define S24_32_MIN -8388608
+#define S24_32_MAX 8388607
+#define S24_32_ACCUM(a,b) ((a) + (int32_t)(b))
+#define S24_32_CLAMP(a) (int32_t)(SPA_CLAMP((a), S24_32_MIN, S24_32_MAX))
+#define U24_32_OFFS 8388608
+#define U24_32_ACCUM(a,b) ((a) + ((int32_t)(b) - U24_32_OFFS))
+#define U24_32_CLAMP(a) (uint32_t)(SPA_CLAMP((a), S24_32_MIN, S24_32_MAX) + U24_32_OFFS)
+
+#define S24_ACCUM(a,b) S24_32_ACCUM(a, s24_to_s32(b))
+#define S24_CLAMP(a) s32_to_s24(S24_32_CLAMP(a))
+#define U24_ACCUM(a,b) U24_32_ACCUM(a, u24_to_u32(b))
+#define U24_CLAMP(a) u32_to_u24(U24_32_CLAMP(a))
+
+#define S32_MIN -2147483648
+#define S32_MAX 2147483647
+#define S32_ACCUM(a,b) ((a) + (int64_t)(b))
+#define S32_CLAMP(a) (int32_t)(SPA_CLAMP((a), S32_MIN, S32_MAX))
+#define U32_OFFS 2147483648
+#define U32_ACCUM(a,b) ((a) + ((int64_t)(b) - U32_OFFS))
+#define U32_CLAMP(a) (uint32_t)(SPA_CLAMP((a), S32_MIN, S32_MAX) + U32_OFFS)
+
+#define F32_ACCUM(a,b) ((a) + (b))
+#define F32_CLAMP(a) (a)
+#define F64_ACCUM(a,b) ((a) + (b))
+#define F64_CLAMP(a) (a)
+
+struct mix_ops {
+ uint32_t fmt;
+ uint32_t n_channels;
+ uint32_t cpu_flags;
+
+ void (*clear) (struct mix_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples);
+ void (*process) (struct mix_ops *ops,
+ void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src[], uint32_t n_src,
+ uint32_t n_samples);
+ void (*free) (struct mix_ops *ops);
+
+ const void *priv;
+};
+
+int mix_ops_init(struct mix_ops *ops);
+
+#define mix_ops_clear(ops,...) (ops)->clear(ops, __VA_ARGS__)
+#define mix_ops_process(ops,...) (ops)->process(ops, __VA_ARGS__)
+#define mix_ops_free(ops) (ops)->free(ops)
+
+#define DEFINE_FUNCTION(name,arch) \
+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_FUNCTION(s8, c);
+DEFINE_FUNCTION(u8, c);
+DEFINE_FUNCTION(s16, c);
+DEFINE_FUNCTION(u16, c);
+DEFINE_FUNCTION(s24, c);
+DEFINE_FUNCTION(u24, c);
+DEFINE_FUNCTION(s32, c);
+DEFINE_FUNCTION(u32, c);
+DEFINE_FUNCTION(s24_32, c);
+DEFINE_FUNCTION(u24_32, c);
+DEFINE_FUNCTION(f32, c);
+DEFINE_FUNCTION(f64, c);
+
+#if defined(HAVE_SSE)
+DEFINE_FUNCTION(f32, sse);
+#endif
+#if defined(HAVE_SSE2)
+DEFINE_FUNCTION(f64, sse2);
+#endif
+#if defined(HAVE_AVX)
+DEFINE_FUNCTION(f32, avx);
+#endif
diff --git a/spa/plugins/audiomixer/mixer-dsp.c b/spa/plugins/audiomixer/mixer-dsp.c
new file mode 100644
index 0000000..534cfab
--- /dev/null
+++ b/spa/plugins/audiomixer/mixer-dsp.c
@@ -0,0 +1,927 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/cpu.h>
+#include <spa/utils/list.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+
+#include "mix-ops.h"
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT log_topic
+static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.mixer-dsp");
+
+#define MAX_BUFFERS 64
+#define MAX_PORTS 128
+#define MAX_ALIGN MIX_OPS_MAX_ALIGN
+
+#define PORT_DEFAULT_VOLUME 1.0
+#define PORT_DEFAULT_MUTE false
+
+struct port_props {
+ double volume;
+ int32_t mute;
+};
+
+static void port_props_reset(struct port_props *props)
+{
+ props->volume = PORT_DEFAULT_VOLUME;
+ props->mute = PORT_DEFAULT_MUTE;
+}
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_QUEUED (1 << 0)
+ uint32_t flags;
+
+ struct spa_list link;
+ struct spa_buffer *buffer;
+ struct spa_meta_header *h;
+ struct spa_buffer buf;
+};
+
+struct port {
+ uint32_t direction;
+ uint32_t id;
+
+ struct port_props props;
+
+ struct spa_io_buffers *io;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[8];
+
+ unsigned int valid:1;
+ unsigned int have_format:1;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list queue;
+ size_t queued_bytes;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_cpu *cpu;
+ uint32_t cpu_flags;
+ uint32_t max_align;
+
+ uint32_t quantum_limit;
+
+ struct mix_ops ops;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[8];
+
+ struct spa_hook_list hooks;
+
+ uint32_t port_count;
+ uint32_t last_port;
+ struct port *in_ports[MAX_PORTS];
+ struct port out_ports[1];
+
+ int n_formats;
+ struct spa_audio_info format;
+ uint32_t stride;
+
+ unsigned int have_format:1;
+ unsigned int started:1;
+};
+
+#define PORT_VALID(p) ((p) != NULL && (p)->valid)
+#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)]))
+#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)]))
+#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+#define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p))
+#define GET_IN_PORT(this,p) (this->in_ports[p])
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ this->started = true;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, GET_OUT_PORT(this, 0), true);
+ for (i = 0; i < this->last_port; i++) {
+ if (PORT_VALID(this->in_ports[i]))
+ emit_port_info(this, GET_IN_PORT(this, i), true);
+ }
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *user_data)
+{
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_IN_PORT (this, port_id);
+ if (port == NULL) {
+ port = calloc(1, sizeof(struct port));
+ if (port == NULL)
+ return -errno;
+ this->in_ports[port_id] = port;
+ }
+
+ port->direction = direction;
+ port->id = port_id;
+
+ port_props_reset(&port->props);
+
+ spa_list_init(&port->queue);
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF |
+ SPA_PORT_FLAG_DYNAMIC_DATA |
+ SPA_PORT_FLAG_REMOVABLE |
+ SPA_PORT_FLAG_OPTIONAL;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+
+ this->port_count++;
+ if (this->last_port <= port_id)
+ this->last_port = port_id + 1;
+ port->valid = true;
+
+ spa_log_debug(this->log, "%p: add port %d:%d %d", this,
+ direction, port_id, this->last_port);
+ emit_port_info(this, port, true);
+
+ return 0;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_IN_PORT (this, port_id);
+
+ port->valid = false;
+ this->port_count--;
+ if (port->have_format && this->have_format) {
+ if (--this->n_formats == 0)
+ this->have_format = false;
+ }
+ spa_memzero(port, sizeof(struct port));
+
+ if (port_id + 1 == this->last_port) {
+ int i;
+
+ for (i = this->last_port - 1; i >= 0; i--)
+ if (PORT_VALID(GET_IN_PORT(this, i)))
+ break;
+
+ this->last_port = i + 1;
+ }
+ spa_log_debug(this->log, "%p: remove port %d:%d %d", this,
+ direction, port_id, this->last_port);
+
+ spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
+
+ return 0;
+}
+
+static int port_enum_formats(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct impl *this = object;
+
+ switch (index) {
+ case 0:
+ if (this->have_format) {
+ *param = spa_format_audio_dsp_build(builder, SPA_PARAM_EnumFormat,
+ &this->format.info.dsp);
+ } else {
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
+ }
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(this, direction, port_id, result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_dsp_build(&b, id, &this->format.info.dsp);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->stride,
+ 16 * this->stride,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->stride));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, "%p: clear buffers %p", this, port);
+ port->n_buffers = 0;
+ spa_list_init(&port->queue);
+ }
+ return 0;
+}
+
+static int queue_buffer(struct impl *this, struct port *port, struct buffer *b)
+{
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
+ return -EINVAL;
+
+ spa_list_append(&port->queue, &b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED);
+ spa_log_trace_fp(this->log, "%p: queue buffer %d", this, b->id);
+ return 0;
+}
+
+static struct buffer *dequeue_buffer(struct impl *this, struct port *port)
+{
+ struct buffer *b;
+
+ if (spa_list_is_empty(&port->queue))
+ return NULL;
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED);
+ spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id);
+ return b;
+}
+
+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);
+
+ if (format == NULL) {
+ if (port->have_format) {
+ port->have_format = false;
+ if (--this->n_formats == 0)
+ this->have_format = false;
+ clear_buffers(this, port);
+ }
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_dsp)
+ return -EINVAL;
+
+ if (spa_format_audio_dsp_parse(format, &info.info.dsp) < 0)
+ return -EINVAL;
+
+ if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32)
+ return -EINVAL;
+
+ if (!this->have_format) {
+ this->ops.fmt = info.info.dsp.format;
+ this->ops.n_channels = 1;
+ this->ops.cpu_flags = this->cpu_flags;
+
+ if ((res = mix_ops_init(&this->ops)) < 0)
+ return res;
+
+ this->stride = sizeof(float);
+ this->have_format = true;
+ this->format = info;
+ }
+ if (!port->have_format) {
+ this->n_formats++;
+ port->have_format = true;
+ spa_log_debug(this->log, "%p: set format on port %d:%d",
+ this, direction, port_id);
+ }
+ }
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(this, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_debug(this->log, "%p: use %d buffers on port %d:%d",
+ this, n_buffers, direction, port_id);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->buffer = buffers[i];
+ b->flags = 0;
+ b->id = i;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+ b->buf = *buffers[i];
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: invalid memory on buffer %d", this, i);
+ return -EINVAL;
+ }
+ if (!SPA_IS_ALIGNED(d[0].data, this->max_align)) {
+ spa_log_warn(this->log, "%p: memory on buffer %d not aligned", this, i);
+ }
+ if (direction == SPA_DIRECTION_OUTPUT)
+ queue_buffer(this, port, b);
+
+ spa_log_debug(this->log, "%p: port %d:%d buffer:%d n_data:%d data:%p maxsize:%d",
+ this, direction, port_id, i,
+ buffers[i]->n_datas, d[0].data, d[0].maxsize);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_debug(this->log, "%p: port %d:%d io %d %p/%zd", this,
+ direction, port_id, id, data, size);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+ port = GET_OUT_PORT(this, 0);
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ return queue_buffer(this, port, &port->buffers[buffer_id]);
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *outport;
+ struct spa_io_buffers *outio;
+ uint32_t n_buffers, i, maxsize;
+ struct buffer **buffers;
+ struct buffer *outb;
+ const void **datas;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ outport = GET_OUT_PORT(this, 0);
+ if ((outio = outport->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, "%p: status %p %d %d",
+ this, outio, outio->status, outio->buffer_id);
+
+ if (SPA_UNLIKELY(outio->status == SPA_STATUS_HAVE_DATA))
+ return outio->status;
+
+ /* recycle */
+ if (SPA_LIKELY(outio->buffer_id < outport->n_buffers)) {
+ queue_buffer(this, outport, &outport->buffers[outio->buffer_id]);
+ outio->buffer_id = SPA_ID_INVALID;
+ }
+
+ buffers = alloca(MAX_PORTS * sizeof(struct buffer *));
+ datas = alloca(MAX_PORTS * sizeof(void *));
+ n_buffers = 0;
+
+ maxsize = UINT32_MAX;
+
+ for (i = 0; i < this->last_port; i++) {
+ struct port *inport = GET_IN_PORT(this, i);
+ struct spa_io_buffers *inio = NULL;
+ struct buffer *inb;
+ struct spa_data *bd;
+ uint32_t size, offs;
+
+ if (SPA_UNLIKELY(!PORT_VALID(inport) ||
+ (inio = inport->io) == NULL ||
+ inio->buffer_id >= inport->n_buffers ||
+ inio->status != SPA_STATUS_HAVE_DATA)) {
+ spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d "
+ "io:%p status:%d buf_id:%d n_buffers:%d", this,
+ i, PORT_VALID(inport), inio,
+ inio ? inio->status : -1,
+ inio ? inio->buffer_id : SPA_ID_INVALID,
+ inport->n_buffers);
+ continue;
+ }
+
+ inb = &inport->buffers[inio->buffer_id];
+ bd = &inb->buffer->datas[0];
+
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->maxsize - offs, bd->chunk->size);
+ maxsize = SPA_MIN(maxsize, size);
+
+ spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d %d:%d", this,
+ i, inio, outio, inio->status, inio->buffer_id,
+ offs, size);
+
+ if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) {
+ datas[n_buffers] = SPA_PTROFF(bd->data, offs, void);
+ buffers[n_buffers++] = inb;
+ }
+ inio->status = SPA_STATUS_NEED_DATA;
+ }
+
+ outb = dequeue_buffer(this, outport);
+ if (SPA_UNLIKELY(outb == NULL)) {
+ spa_log_trace(this->log, "%p: out of buffers", this);
+ return -EPIPE;
+ }
+
+ if (n_buffers == 1) {
+ *outb->buffer = *buffers[0]->buffer;
+ } else {
+ struct spa_data *d = outb->buf.datas;
+
+ *outb->buffer = outb->buf;
+
+ maxsize = SPA_MIN(maxsize, d[0].maxsize);
+
+ d[0].chunk->offset = 0;
+ d[0].chunk->size = maxsize;
+ d[0].chunk->stride = sizeof(float);
+ SPA_FLAG_UPDATE(d[0].chunk->flags, SPA_CHUNK_FLAG_EMPTY, n_buffers == 0);
+
+ spa_log_trace_fp(this->log, "%p: %d mix %d", this, n_buffers, maxsize);
+
+ mix_ops_process(&this->ops, d[0].data,
+ datas, n_buffers, maxsize / sizeof(float));
+ }
+
+ outio->buffer_id = outb->id;
+ outio->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+ uint32_t i;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ for (i = 0; i < MAX_PORTS; i++)
+ free(this->in_ports[i]);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(this->log, log_topic);
+
+ this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+ if (this->cpu) {
+ this->cpu_flags = spa_cpu_get_flags(this->cpu);
+ this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu));
+ }
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "clock.quantum-limit"))
+ spa_atou32(s, &this->quantum_limit, 0);
+ }
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = MAX_PORTS;
+ this->info.max_output_ports = 1;
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS;
+ this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS;
+
+ port = GET_OUT_PORT(this, 0);
+ port->valid = true;
+ port->direction = SPA_DIRECTION_OUTPUT;
+ port->id = 0;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+
+ spa_list_init(&port->queue);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+const struct spa_handle_factory spa_mixer_dsp_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_AUDIO_MIXER_DSP,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/audiomixer/plugin.c b/spa/plugins/audiomixer/plugin.c
new file mode 100644
index 0000000..3915425
--- /dev/null
+++ b/spa/plugins/audiomixer/plugin.c
@@ -0,0 +1,50 @@
+/* Spa Audiomixer plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_audiomixer_factory;
+extern const struct spa_handle_factory spa_mixer_dsp_factory;
+
+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_audiomixer_factory;
+ break;
+ case 1:
+ *factory = &spa_mixer_dsp_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/audiomixer/test-helper.h b/spa/plugins/audiomixer/test-helper.h
new file mode 100644
index 0000000..8c789bd
--- /dev/null
+++ b/spa/plugins/audiomixer/test-helper.h
@@ -0,0 +1,97 @@
+#include <dlfcn.h>
+
+#include <spa/support/plugin.h>
+#include <spa/utils/type.h>
+#include <spa/utils/result.h>
+#include <spa/support/cpu.h>
+#include <spa/utils/names.h>
+
+static inline const struct spa_handle_factory *get_factory(spa_handle_factory_enum_func_t enum_func,
+ const char *name, uint32_t version)
+{
+ uint32_t i;
+ int res;
+ const struct spa_handle_factory *factory;
+
+ for (i = 0;;) {
+ if ((res = enum_func(&factory, &i)) <= 0) {
+ if (res < 0)
+ errno = -res;
+ break;
+ }
+ if (factory->version >= version &&
+ !strcmp(factory->name, name))
+ return factory;
+ }
+ return NULL;
+}
+
+static inline struct spa_handle *load_handle(const struct spa_support *support,
+ uint32_t n_support, const char *lib, const char *name)
+{
+ int res, len;
+ void *hnd;
+ spa_handle_factory_enum_func_t enum_func;
+ const struct spa_handle_factory *factory;
+ struct spa_handle *handle;
+ const char *str;
+ char *path;
+
+ if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
+ str = PLUGINDIR;
+
+ len = strlen(str) + strlen(lib) + 2;
+ path = alloca(len);
+ snprintf(path, len, "%s/%s", str, lib);
+
+ if ((hnd = dlopen(path, RTLD_NOW)) == NULL) {
+ fprintf(stderr, "can't load %s: %s\n", lib, dlerror());
+ res = -ENOENT;
+ goto error;
+ }
+ if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ fprintf(stderr, "can't find enum function\n");
+ res = -ENXIO;
+ goto error_close;
+ }
+
+ if ((factory = get_factory(enum_func, name, SPA_VERSION_HANDLE_FACTORY)) == NULL) {
+ fprintf(stderr, "can't find factory\n");
+ res = -ENOENT;
+ goto error_close;
+ }
+ handle = calloc(1, spa_handle_factory_get_size(factory, NULL));
+ if ((res = spa_handle_factory_init(factory, handle,
+ NULL, support, n_support)) < 0) {
+ fprintf(stderr, "can't make factory instance: %d\n", res);
+ goto error_close;
+ }
+ return handle;
+
+error_close:
+ dlclose(hnd);
+error:
+ errno = -res;
+ return NULL;
+}
+
+static inline uint32_t get_cpu_flags(void)
+{
+ struct spa_handle *handle;
+ uint32_t flags;
+ void *iface;
+ int res;
+
+ handle = load_handle(NULL, 0, "support/libspa-support.so", SPA_NAME_SUPPORT_CPU);
+ if (handle == NULL)
+ return 0;
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_CPU, &iface)) < 0) {
+ fprintf(stderr, "can't get CPU interface %s\n", spa_strerror(res));
+ return 0;
+ }
+ flags = spa_cpu_get_flags((struct spa_cpu*)iface);
+
+ free(handle);
+
+ return flags;
+}
diff --git a/spa/plugins/audiomixer/test-mix-ops.c b/spa/plugins/audiomixer/test-mix-ops.c
new file mode 100644
index 0000000..a20f7a4
--- /dev/null
+++ b/spa/plugins/audiomixer/test-mix-ops.c
@@ -0,0 +1,293 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/debug/mem.h>
+
+#include "test-helper.h"
+#include "mix-ops.c"
+
+static uint32_t cpu_flags;
+
+#define N_SAMPLES 1024
+
+static uint8_t samp_out[N_SAMPLES * 8];
+
+static void compare_mem(int i, int j, const void *m1, const void *m2, size_t size)
+{
+ int res = memcmp(m1, m2, size);
+ if (res != 0) {
+ fprintf(stderr, "%d %d %zd:\n", i, j, size);
+ spa_debug_mem(0, m1, size);
+ spa_debug_mem(0, m2, size);
+ }
+ spa_assert_se(res == 0);
+}
+
+static int run_test(const char *name, const void *src[], uint32_t n_src, const void *dst,
+ size_t dst_size, uint32_t n_samples, mix_func_t mix)
+{
+ struct mix_ops ops;
+
+ ops.fmt = SPA_AUDIO_FORMAT_F32;
+ ops.n_channels = 1;
+ ops.cpu_flags = cpu_flags;
+ mix_ops_init(&ops);
+
+ fprintf(stderr, "%s\n", name);
+
+ mix(&ops, (void *)samp_out, src, n_src, n_samples);
+ compare_mem(0, 0, samp_out, dst, dst_size);
+ return 0;
+}
+
+static void test_s8(void)
+{
+ int8_t out[] = { 0x00, 0x00, 0x00, 0x00 };
+ int8_t in_1[] = { 0x00, 0x00, 0x00, 0x00 };
+ int8_t in_2[] = { 0x7f, 0x80, 0x40, 0xc0 };
+ int8_t in_3[] = { 0x40, 0xc0, 0xc0, 0x40 };
+ int8_t in_4[] = { 0xc0, 0x40, 0x40, 0xc0 };
+ int8_t out_4[] = { 0x7f, 0x80, 0x40, 0xc0 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_s8_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s8_c);
+ run_test("test_s8_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s8_c);
+ run_test("test_s8_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s8_c);
+}
+
+static void test_u8(void)
+{
+ uint8_t out[] = { 0x80, 0x80, 0x80, 0x80 };
+ uint8_t in_1[] = { 0x80, 0x80, 0x80, 0x80 };
+ uint8_t in_2[] = { 0xff, 0x00, 0xc0, 0x40 };
+ uint8_t in_3[] = { 0xc0, 0x40, 0x40, 0xc0 };
+ uint8_t in_4[] = { 0x40, 0xc0, 0xc0, 0x40 };
+ uint8_t out_4[] = { 0xff, 0x00, 0xc0, 0x40 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_u8_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u8_c);
+ run_test("test_u8_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u8_c);
+ run_test("test_u8_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u8_c);
+}
+
+static void test_s16(void)
+{
+ int16_t out[] = { 0x0000, 0x0000, 0x0000, 0x0000 };
+ int16_t in_1[] = { 0x0000, 0x0000, 0x0000, 0x0000 };
+ int16_t in_2[] = { 0x7fff, 0x8000, 0x4000, 0xc000 };
+ int16_t in_3[] = { 0x4000, 0xc000, 0xc000, 0x4000 };
+ int16_t in_4[] = { 0xc000, 0x4000, 0x4000, 0xc000 };
+ int16_t out_4[] = { 0x7fff, 0x8000, 0x4000, 0xc000 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_s16_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s16_c);
+ run_test("test_s16_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s16_c);
+ run_test("test_s16_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s16_c);
+}
+
+static void test_u16(void)
+{
+ uint16_t out[] = { 0x8000, 0x8000, 0x8000, 0x8000 };
+ uint16_t in_1[] = { 0x8000, 0x8000, 0x8000 , 0x8000};
+ uint16_t in_2[] = { 0xffff, 0x0000, 0xc000, 0x4000 };
+ uint16_t in_3[] = { 0xc000, 0x4000, 0x4000, 0xc000 };
+ uint16_t in_4[] = { 0x4000, 0xc000, 0xc000, 0x4000 };
+ uint16_t out_4[] = { 0xffff, 0x0000, 0xc000, 0x4000 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_u16_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u16_c);
+ run_test("test_u16_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u16_c);
+ run_test("test_u16_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u16_c);
+}
+
+static void test_s24(void)
+{
+ int24_t out[] = { S32_TO_S24(0x000000), S32_TO_S24(0x000000), S32_TO_S24(0x000000) };
+ int24_t in_1[] = { S32_TO_S24(0x000000), S32_TO_S24(0x000000), S32_TO_S24(0x000000) };
+ int24_t in_2[] = { S32_TO_S24(0x7fffff), S32_TO_S24(0xff800000), S32_TO_S24(0x400000) };
+ int24_t in_3[] = { S32_TO_S24(0x400000), S32_TO_S24(0xffc00000), S32_TO_S24(0xffc00000) };
+ int24_t in_4[] = { S32_TO_S24(0xffc00000), S32_TO_S24(0x400000), S32_TO_S24(0x400000) };
+ int24_t out_4[] = { S32_TO_S24(0x7fffff), S32_TO_S24(0xff800000), S32_TO_S24(0x400000) };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_s24_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s24_c);
+ run_test("test_s24_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s24_c);
+ run_test("test_s24_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s24_c);
+}
+
+static void test_u24(void)
+{
+ uint24_t out[] = { U32_TO_U24(0x800000), U32_TO_U24(0x800000), U32_TO_U24(0x800000) };
+ uint24_t in_1[] = { U32_TO_U24(0x800000), U32_TO_U24(0x800000), U32_TO_U24(0x800000) };
+ uint24_t in_2[] = { U32_TO_U24(0xffffffff), U32_TO_U24(0x000000), U32_TO_U24(0xffc00000) };
+ uint24_t in_3[] = { U32_TO_U24(0xffc00000), U32_TO_U24(0x400000), U32_TO_U24(0x400000) };
+ uint24_t in_4[] = { U32_TO_U24(0x400000), U32_TO_U24(0xffc00000), U32_TO_U24(0xffc00000) };
+ uint24_t out_4[] = { U32_TO_U24(0xffffffff), U32_TO_U24(0x000000), U32_TO_U24(0xffc00000) };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_u24_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u24_c);
+ run_test("test_u24_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u24_c);
+ run_test("test_u24_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u24_c);
+}
+
+static void test_s32(void)
+{
+ int32_t out[] = { 0x00000000, 0x00000000, 0x00000000, 0x00000000 };
+ int32_t in_1[] = { 0x00000000, 0x00000000, 0x00000000, 0x00000000 };
+ int32_t in_2[] = { 0x7fffffff, 0x80000000, 0x40000000, 0xc0000000 };
+ int32_t in_3[] = { 0x40000000, 0xc0000000, 0xc0000000, 0x40000000 };
+ int32_t in_4[] = { 0xc0000000, 0x40000000, 0x40000000, 0xc0000000 };
+ int32_t out_4[] = { 0x7fffffff, 0x80000000, 0x40000000, 0xc0000000 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_s32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s32_c);
+ run_test("test_s32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s32_c);
+ run_test("test_s32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s32_c);
+}
+
+static void test_u32(void)
+{
+ uint32_t out[] = { 0x80000000, 0x80000000, 0x80000000, 0x80000000 };
+ uint32_t in_1[] = { 0x80000000, 0x80000000, 0x80000000, 0x80000000 };
+ uint32_t in_2[] = { 0xffffffff, 0x00000000, 0xc0000000, 0x40000000 };
+ uint32_t in_3[] = { 0xc0000000, 0x40000000, 0x40000000, 0xc0000000 };
+ uint32_t in_4[] = { 0x40000000, 0xc0000000, 0xc0000000, 0x40000000 };
+ uint32_t out_4[] = { 0xffffffff, 0x00000000, 0xc0000000, 0x40000000 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_u32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u32_c);
+ run_test("test_u32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u32_c);
+ run_test("test_u32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u32_c);
+}
+
+static void test_s24_32(void)
+{
+ int32_t out[] = { 0x000000, 0x000000, 0x000000, 0x000000 };
+ int32_t in_1[] = { 0x000000, 0x000000, 0x000000, 0x000000 };
+ int32_t in_2[] = { 0x7fffff, 0xff800000, 0x400000, 0xffc00000 };
+ int32_t in_3[] = { 0x400000, 0xffc00000, 0xffc00000, 0x400000 };
+ int32_t in_4[] = { 0xffc00000, 0x400000, 0x400000, 0xffc00000 };
+ int32_t out_4[] = { 0x7fffff, 0xff800000, 0x400000, 0xffc00000 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_s24_32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_s24_32_c);
+ run_test("test_s24_32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_s24_32_c);
+ run_test("test_s24_32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_s24_32_c);
+}
+
+static void test_u24_32(void)
+{
+ uint32_t out[] = { 0x800000, 0x800000, 0x800000, 0x800000 };
+ uint32_t in_1[] = { 0x800000, 0x800000, 0x800000, 0x800000 };
+ uint32_t in_2[] = { 0xffffff, 0x000000, 0xc00000, 0x400000 };
+ uint32_t in_3[] = { 0xc00000, 0x400000, 0x400000, 0xc00000 };
+ uint32_t in_4[] = { 0x400000, 0xc00000, 0xc00000, 0x400000 };
+ uint32_t out_4[] = { 0xffffff, 0x000000, 0xc00000, 0x400000 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_u24_32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_u24_32_c);
+ run_test("test_u24_32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_u24_32_c);
+ run_test("test_u24_32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_u24_32_c);
+}
+
+static void test_f32(void)
+{
+ float out[] = { 0.0f, 0.0f, 0.0f, 0.0f };
+ float in_1[] = { 0.0f, 0.0f, 0.0f, 0.0f };
+ float in_2[] = { 1.0f, -1.0f, 0.5f, -0.5f };
+ float in_3[] = { 0.5f, -0.5f, -0.5f, 0.5f };
+ float in_4[] = { -0.5f, 1.0f, 0.5f, -0.5f };
+ float out_4[] = { 1.0f, -0.5f, 0.5f, -0.5f };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_f32_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f32_c);
+ run_test("test_f32_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f32_c);
+ run_test("test_f32_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f32_c);
+#if defined(HAVE_SSE)
+ if (cpu_flags & SPA_CPU_FLAG_SSE) {
+ run_test("test_f32_0_sse", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f32_sse);
+ run_test("test_f32_1_sse", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f32_sse);
+ run_test("test_f32_4_sse", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f32_sse);
+ }
+#endif
+#if defined(HAVE_AVX)
+ if (cpu_flags & SPA_CPU_FLAG_AVX) {
+ run_test("test_f32_0_avx", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f32_avx);
+ run_test("test_f32_1_avx", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f32_avx);
+ run_test("test_f32_4_avx", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f32_avx);
+ }
+#endif
+}
+
+static void test_f64(void)
+{
+ double out[] = { 0.0, 0.0, 0.0, 0.0 };
+ double in_1[] = { 0.0, 0.0, 0.0, 0.0 };
+ double in_2[] = { 1.0, -1.0, 0.5, -0.5 };
+ double in_3[] = { 0.5, -0.5, -0.5, 0.5 };
+ double in_4[] = { -0.5, 1.0, 0.5, -0.5 };
+ double out_4[] = { 1.0, -0.5, 0.5, -0.5 };
+ const void *src[6] = { in_1, in_2, in_3, in_4 };
+
+ run_test("test_f64_0", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f64_c);
+ run_test("test_f64_1", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f64_c);
+ run_test("test_f64_4", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f64_c);
+#if defined(HAVE_SSE2)
+ if (cpu_flags & SPA_CPU_FLAG_SSE2) {
+ run_test("test_f64_0_sse2", NULL, 0, out, sizeof(out), SPA_N_ELEMENTS(out), mix_f64_sse2);
+ run_test("test_f64_1_sse2", src, 1, in_1, sizeof(in_1), SPA_N_ELEMENTS(in_1), mix_f64_sse2);
+ run_test("test_f64_4_sse2", src, 4, out_4, sizeof(out_4), SPA_N_ELEMENTS(out_4), mix_f64_sse2);
+ }
+#endif
+}
+
+int main(int argc, char *argv[])
+{
+ cpu_flags = get_cpu_flags();
+ printf("got get CPU flags %d\n", cpu_flags);
+
+ test_s8();
+ test_u8();
+ test_s16();
+ test_u16();
+ test_s24();
+ test_u24();
+ test_s32();
+ test_u32();
+ test_s24_32();
+ test_u24_32();
+ test_f32();
+ test_f64();
+
+ return 0;
+}
diff --git a/spa/plugins/audiotestsrc/audiotestsrc.c b/spa/plugins/audiotestsrc/audiotestsrc.c
new file mode 100644
index 0000000..1501e1d
--- /dev/null
+++ b/spa/plugins/audiotestsrc/audiotestsrc.c
@@ -0,0 +1,1159 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/control/control.h>
+
+#define NAME "audiotestsrc"
+
+#define SAMPLES_TO_TIME(this,s) ((s) * SPA_NSEC_PER_SEC / (port)->current_format.info.raw.rate)
+#define BYTES_TO_SAMPLES(this,b) ((b)/(port)->bpf)
+#define BYTES_TO_TIME(this,b) SAMPLES_TO_TIME(this, BYTES_TO_SAMPLES (this, b))
+
+enum wave_type {
+ WAVE_SINE,
+ WAVE_SQUARE,
+};
+
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+
+#define DEFAULT_LIVE true
+#define DEFAULT_WAVE WAVE_SINE
+#define DEFAULT_FREQ 440.0
+#define DEFAULT_VOLUME 1.0
+
+struct props {
+ bool live;
+ uint32_t wave;
+ float freq;
+ float volume;
+};
+
+static void reset_props(struct props *props)
+{
+ props->live = DEFAULT_LIVE;
+ props->wave = DEFAULT_WAVE;
+ props->freq = DEFAULT_FREQ;
+ props->volume = DEFAULT_VOLUME;
+}
+
+#define MAX_BUFFERS 16
+#define MAX_PORTS 1
+
+struct buffer {
+ uint32_t id;
+ struct spa_buffer *outbuf;
+ bool outstanding;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct impl;
+
+typedef void (*render_func_t) (struct impl *this, void *samples, size_t n_samples);
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+ struct spa_io_sequence *io_control;
+
+ bool have_format;
+ struct spa_audio_info current_format;
+ size_t bpf;
+ render_func_t render_func;
+ float accumulator;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list empty;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ uint32_t quantum_limit;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[2];
+ struct props props;
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ bool async;
+ struct spa_source timer_source;
+ struct itimerspec timerspec;
+
+ bool started;
+ uint64_t start_time;
+ uint64_t elapsed_time;
+
+ uint64_t sample_count;
+
+ struct port port;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_PORTS)
+
+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_PropInfo:
+ {
+ struct props *p = &this->props;
+ struct spa_pod_frame f[2];
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_live),
+ SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"),
+ SPA_PROP_INFO_type, SPA_POD_Bool(p->live));
+ break;
+ case 1:
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_waveType),
+ SPA_PROP_INFO_description, SPA_POD_String("Select the waveform"),
+ SPA_PROP_INFO_type, SPA_POD_Int(p->wave),
+ 0);
+ spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+ spa_pod_builder_push_struct(&b, &f[1]);
+ spa_pod_builder_int(&b, WAVE_SINE);
+ spa_pod_builder_string(&b, "Sine wave");
+ spa_pod_builder_int(&b, WAVE_SQUARE);
+ spa_pod_builder_string(&b, "Square wave");
+ spa_pod_builder_pop(&b, &f[1]);
+ param = spa_pod_builder_pop(&b, &f[0]);
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_frequency),
+ SPA_PROP_INFO_description, SPA_POD_String("Select the frequency"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->freq, 0.0, 50000000.0));
+ break;
+ case 3:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume),
+ SPA_PROP_INFO_description, SPA_POD_String("Select the volume"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_live, SPA_POD_Bool(p->live),
+ SPA_PROP_waveType, SPA_POD_Int(p->wave),
+ SPA_PROP_frequency, SPA_POD_Float(p->freq),
+ SPA_PROP_volume, SPA_POD_Float(p->volume));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_IO:
+ {
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_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 (id == SPA_PARAM_Props) {
+ struct props *p = &this->props;
+ struct port *port = &this->port;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_live, SPA_POD_OPT_Bool(&p->live),
+ SPA_PROP_waveType, SPA_POD_OPT_Int(&p->wave),
+ SPA_PROP_frequency, SPA_POD_OPT_Float(&p->freq),
+ SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume));
+
+ if (p->live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ else
+ port->info.flags &= ~SPA_PORT_FLAG_LIVE;
+ }
+ else
+ return -ENOENT;
+
+ 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);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ if (size > 0 && size < sizeof(struct spa_io_clock))
+ return -EINVAL;
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+#include "render.c"
+
+static void set_timer(struct impl *this, bool enabled)
+{
+ if (this->async || this->props.live) {
+ if (enabled) {
+ if (this->props.live) {
+ uint64_t next_time = this->start_time + this->elapsed_time;
+ this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC;
+ this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC;
+ } else {
+ this->timerspec.it_value.tv_sec = 0;
+ this->timerspec.it_value.tv_nsec = 1;
+ }
+ } else {
+ 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);
+ }
+}
+
+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, NAME " %p: timerfd error: %s",
+ this, spa_strerror(res));
+ }
+ }
+ return 0;
+}
+
+static int make_buffer(struct impl *this)
+{
+ struct buffer *b;
+ struct port *port = &this->port;
+ struct spa_io_buffers *io = port->io;
+ uint32_t n_bytes, n_samples, maxsize;
+ void *data;
+ struct spa_data *d;
+ uint32_t filled, avail;
+ uint32_t index, offset, l0, l1;
+
+ if (read_timer(this) < 0)
+ return 0;
+
+ if (spa_list_is_empty(&port->empty)) {
+ set_timer(this, false);
+ spa_log_error(this->log, NAME " %p: out of buffers", this);
+ return -EPIPE;
+ }
+ b = spa_list_first(&port->empty, struct buffer, link);
+ spa_list_remove(&b->link);
+ b->outstanding = true;
+
+ d = b->outbuf->datas;
+ maxsize = d[0].maxsize;
+ data = d[0].data;
+
+ n_bytes = maxsize;
+
+ spa_log_trace(this->log, NAME " %p: dequeue buffer %d %d %d", this, b->id,
+ maxsize, n_bytes);
+
+ filled = 0;
+ index = 0;
+ avail = maxsize - filled;
+
+ offset = index % maxsize;
+
+ if (this->position && this->position->clock.duration) {
+ n_bytes = SPA_MIN(avail, n_bytes);
+ n_samples = this->position->clock.duration;
+ if (n_samples * port->bpf < n_bytes)
+ n_bytes = n_samples * port->bpf;
+ } else {
+ n_bytes = SPA_MIN(avail, n_bytes);
+ n_samples = n_bytes / port->bpf;
+ }
+ l0 = SPA_MIN(n_bytes, maxsize - offset) / port->bpf;
+ l1 = n_samples - l0;
+
+ port->render_func(this, SPA_PTROFF(data, offset, void), l0);
+ if (l1 > 0)
+ port->render_func(this, data, l1);
+
+ d[0].chunk->offset = index;
+ d[0].chunk->size = n_bytes;
+ d[0].chunk->stride = port->bpf;
+
+ if (b->h) {
+ b->h->seq = this->sample_count;
+ b->h->pts = this->start_time + this->elapsed_time;
+ b->h->dts_offset = 0;
+ }
+
+ this->sample_count += n_samples;
+ this->elapsed_time = SAMPLES_TO_TIME(this, this->sample_count);
+ set_timer(this, true);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ return io->status;
+}
+
+static void on_output(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ int res;
+
+ res = make_buffer(this);
+
+ if (res == SPA_STATUS_HAVE_DATA)
+ spa_node_call_ready(&this->callbacks, res);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ struct timespec now;
+
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (this->started)
+ return 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (this->props.live)
+ this->start_time = SPA_TIMESPEC_TO_NSEC(&now);
+ else
+ this->start_time = 0;
+ this->sample_count = 0;
+ this->elapsed_time = 0;
+
+ this->started = true;
+ set_timer(this, true);
+ break;
+ }
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if (!this->started)
+ return 0;
+ this->started = false;
+ set_timer(this, false);
+ break;
+
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_MEDIA_CLASS, "Audio/Source" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+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);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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(struct impl *this,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ switch (index) {
+ case 0:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(5,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_S32,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F64),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(
+ DEFAULT_RATE, 1, INT32_MAX),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
+ DEFAULT_CHANNELS, 1, INT32_MAX));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(this, direction, port_id,
+ result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->bpf,
+ 16 * port->bpf,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->bpf));
+ break;
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Control),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_sequence)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_info(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ spa_list_init(&port->empty);
+ this->started = false;
+ set_timer(this, false);
+ }
+ 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)
+{
+ int res;
+ struct port *port = &this->port;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+ int idx;
+ const size_t sizes[4] = { 2, 4, 4, 8 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0)
+ return -EINVAL;
+
+ switch (info.info.raw.format) {
+ case SPA_AUDIO_FORMAT_S16:
+ idx = 0;
+ break;
+ case SPA_AUDIO_FORMAT_S32:
+ idx = 1;
+ break;
+ case SPA_AUDIO_FORMAT_F32:
+ idx = 2;
+ break;
+ case SPA_AUDIO_FORMAT_F64:
+ idx = 3;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ port->bpf = sizes[idx] * info.info.raw.channels;
+ port->current_format = info;
+ port->have_format = true;
+ port->render_func = sine_funcs[idx];
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ if (id == SPA_PARAM_Format)
+ return port_set_format(this, direction, port_id, flags, param);
+
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->outstanding = false;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ return -EINVAL;
+ }
+ spa_list_append(&port->empty, &b->link);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_Control:
+ port->io_control = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+ struct buffer *b = &port->buffers[id];
+ spa_return_if_fail(b->outstanding);
+
+ spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id);
+
+ b->outstanding = false;
+ spa_list_append(&port->empty, &b->link);
+
+ if (!this->props.live)
+ set_timer(this, true);
+}
+
+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(port_id == 0, -EINVAL);
+ port = &this->port;
+ spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL);
+
+ reuse_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int process_control(struct impl *this, struct spa_pod_sequence *sequence)
+{
+ struct spa_pod_control *c;
+
+ SPA_POD_SEQUENCE_FOREACH(sequence, c) {
+ switch (c->type) {
+ case SPA_CONTROL_Properties:
+ {
+ struct props *p = &this->props;
+ spa_pod_parse_object(&c->value,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_frequency, SPA_POD_OPT_Float(&p->freq),
+ SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume));
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+
+ io = port->io;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (port->io_control)
+ process_control(this, &port->io_control->sequence);
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (io->buffer_id < port->n_buffers) {
+ reuse_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (!this->props.live)
+ return make_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .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 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);
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (this->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);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ 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);
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "clock.quantum-limit"))
+ spa_atou32(s, &this->quantum_limit, 0);
+ }
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ 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->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;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF;
+ if (this->props.live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+ spa_list_init(&port->empty);
+
+ spa_log_info(this->log, NAME " %p: initialized", this);
+
+ 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, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Generate an audio test pattern" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_audiotestsrc_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ NAME,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/audiotestsrc/meson.build b/spa/plugins/audiotestsrc/meson.build
new file mode 100644
index 0000000..d1b2242
--- /dev/null
+++ b/spa/plugins/audiotestsrc/meson.build
@@ -0,0 +1,7 @@
+audiotestsrc_sources = ['audiotestsrc.c', 'plugin.c']
+
+audiotestsrclib = shared_library('spa-audiotestsrc',
+ audiotestsrc_sources,
+ dependencies : [ spa_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'audiotestsrc')
diff --git a/spa/plugins/audiotestsrc/plugin.c b/spa/plugins/audiotestsrc/plugin.c
new file mode 100644
index 0000000..4c51895
--- /dev/null
+++ b/spa/plugins/audiotestsrc/plugin.c
@@ -0,0 +1,46 @@
+/* Spa Volume plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_audiotestsrc_factory;
+
+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_audiotestsrc_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/audiotestsrc/render.c b/spa/plugins/audiotestsrc/render.c
new file mode 100644
index 0000000..4133fd0
--- /dev/null
+++ b/spa/plugins/audiotestsrc/render.c
@@ -0,0 +1,64 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <math.h>
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+#define DEFINE_SINE(type,scale) \
+static void \
+audio_test_src_create_sine_##type (struct impl *this, type *samples, size_t n_samples) \
+{ \
+ size_t i; \
+ uint32_t c, channels; \
+ float step, amp; \
+ float freq = this->props.freq; \
+ float volume = this->props.volume; \
+ \
+ channels = this->port.current_format.info.raw.channels; \
+ step = M_PI_M2 * freq / this->port.current_format.info.raw.rate; \
+ amp = volume * scale; \
+ \
+ for (i = 0; i < n_samples; i++) { \
+ type val; \
+ this->port.accumulator += step; \
+ if (this->port.accumulator >= M_PI_M2) \
+ this->port.accumulator -= M_PI_M2; \
+ val = (type) (sin (this->port.accumulator) * amp); \
+ for (c = 0; c < channels; ++c) \
+ *samples++ = val; \
+ } \
+}
+
+DEFINE_SINE(int16_t, 32767.0);
+DEFINE_SINE(int32_t, 2147483647.0);
+DEFINE_SINE(float, 1.0);
+DEFINE_SINE(double, 1.0);
+
+static const render_func_t sine_funcs[] = {
+ (render_func_t) audio_test_src_create_sine_int16_t,
+ (render_func_t) audio_test_src_create_sine_int32_t,
+ (render_func_t) audio_test_src_create_sine_float,
+ (render_func_t) audio_test_src_create_sine_double
+};
diff --git a/spa/plugins/avb/avb-pcm-sink.c b/spa/plugins/avb/avb-pcm-sink.c
new file mode 100644
index 0000000..f453494
--- /dev/null
+++ b/spa/plugins/avb/avb-pcm-sink.c
@@ -0,0 +1,910 @@
+/* Spa AVB PCM Sink
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/monitor/device.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/param/audio/format.h>
+#include <spa/pod/filter.h>
+
+#include "avb-pcm.h"
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+#define GET_PORT(this,d,p) (&this->ports[p])
+
+static void reset_props(struct props *props)
+{
+ snprintf(props->ifname, sizeof(props->ifname), "%s", DEFAULT_IFNAME);
+ parse_addr(props->addr, DEFAULT_ADDR);
+ props->prio = DEFAULT_PRIO;
+ parse_streamid(&props->streamid, DEFAULT_STREAMID);
+ props->mtt = DEFAULT_MTT;
+ props->t_uncertainty = DEFAULT_TU;
+ props->frames_per_pdu = DEFAULT_FRAMES_PER_PDU;
+}
+
+static void emit_node_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ struct spa_dict_item items[4];
+ uint32_t i, n_items = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "avb");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Sink");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+ this->info.props = &SPA_DICT_INIT(items, n_items);
+
+ if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->info.n_params; i++) {
+ if (this->params[i].user > 0) {
+ this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_info(&this->hooks, &this->info);
+
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct state *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ uint32_t i;
+
+ if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < port->info.n_params; i++) {
+ if (port->params[i].user > 0) {
+ port->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ port->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ default:
+ param = spa_avb_enum_propinfo(this, result.index, &b);
+ if (param == NULL)
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct spa_pod_frame f;
+
+ switch (result.index) {
+ case 0:
+ spa_pod_builder_push_object(&b, &f,
+ SPA_TYPE_OBJECT_Props, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(this->process_latency.ns),
+ 0);
+ spa_avb_add_prop_params(this, &b);
+ param = spa_pod_builder_pop(&b, &f);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_ProcessLatency:
+ switch (result.index) {
+ case 0:
+ param = spa_process_latency_build(&b, id, &this->process_latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ spa_avb_reassign_follower(this);
+
+ return 0;
+}
+
+static void handle_process_latency(struct state *this,
+ const struct spa_process_latency_info *info)
+{
+ bool ns_changed = this->process_latency.ns != info->ns;
+ struct port *port = &this->ports[0];
+
+ if (this->process_latency.quantum == info->quantum &&
+ this->process_latency.rate == info->rate &&
+ !ns_changed)
+ return;
+
+ this->process_latency = *info;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ if (ns_changed)
+ this->params[NODE_Props].user++;
+ this->params[NODE_ProcessLatency].user++;
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[PORT_Latency].user++;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod *params = NULL;
+ int64_t lat_ns = -1;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&lat_ns),
+ SPA_PROP_params, SPA_POD_OPT_Pod(&params));
+
+ spa_avb_parse_prop_params(this, params);
+ if (lat_ns != -1) {
+ struct spa_process_latency_info info;
+ info = this->process_latency;
+ info.ns = lat_ns;
+ handle_process_latency(this, &info);
+ }
+ emit_node_info(this, false);
+ emit_port_info(this, &this->ports[0], false);
+ break;
+ }
+ case SPA_PARAM_ProcessLatency:
+ {
+ struct spa_process_latency_info info;
+ if ((res = spa_process_latency_parse(param, &info)) < 0)
+ return res;
+
+ handle_process_latency(this, &info);
+
+ emit_node_info(this, false);
+ emit_port_info(this, &this->ports[0], false);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_ParamBegin:
+ break;
+ case SPA_NODE_COMMAND_ParamEnd:
+ break;
+ case SPA_NODE_COMMAND_Start:
+ if (!this->ports[0].have_format)
+ return -EIO;
+ if (this->ports[0].n_buffers == 0)
+ return -EIO;
+ if ((res = spa_avb_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = spa_avb_pause(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct state *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->ports[0], true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ return spa_avb_enum_format(this, seq, start, num, filter);
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id,
+ &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->stride,
+ 16 * this->stride,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->stride));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ {
+ struct spa_latency_info latency = this->latency[result.index];
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ spa_process_latency_info_add(&this->process_latency, &latency);
+ param = spa_latency_build(&b, id, &latency);
+ break;
+ }
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct state *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(void *object, struct port *port,
+ uint32_t flags, const struct spa_pod *format)
+{
+ struct state *this = object;
+ int err;
+
+ if (format == NULL) {
+ if (!port->have_format)
+ return 0;
+
+ spa_log_debug(this->log, "clear format");
+ port->have_format = false;
+ spa_avb_clear_format(this);
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if ((err = spa_avb_set_format(this, &info, flags)) < 0)
+ return err;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ emit_node_info(this, false);
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, this->rate);
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ port->params[PORT_Latency].user++;
+ } else {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ this->latency[info.direction] = info;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[PORT_Latency].user++;
+ emit_port_info(this, port, false);
+ break;
+ }
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct state *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers);
+
+ if (port->n_buffers > 0) {
+ spa_avb_pause(this);
+ clear_buffers(this, port);
+ }
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->flags = BUFFER_FLAG_OUT;
+
+ b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct state *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ port->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct state *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = GET_PORT(this, SPA_DIRECTION_INPUT, 0);
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, io->status,
+ io->buffer_id, port->n_buffers);
+
+ if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
+ io->status = SPA_STATUS_NEED_DATA;
+ return SPA_STATUS_HAVE_DATA;
+ }
+ if (io->status == SPA_STATUS_HAVE_DATA &&
+ io->buffer_id < port->n_buffers) {
+ struct buffer *b = &port->buffers[io->buffer_id];
+
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_warn(this->log, "%p: buffer %u in use",
+ this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+ spa_log_trace_fp(this->log, "%p: queue buffer %u", this, io->buffer_id);
+ spa_list_append(&port->ready, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ io->buffer_id = SPA_ID_INVALID;
+
+ spa_avb_write(this);
+
+ io->status = SPA_STATUS_OK;
+ }
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct state *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct state *this;
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ this = (struct state *) handle;
+ spa_avb_clear(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support)
+{
+ struct state *this;
+ struct port *port;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct state *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ avb_log_topic_init(this->log);
+
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ spa_hook_list_init(&this->hooks);
+
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ reset_props(&this->props);
+
+ port = GET_PORT(this, SPA_DIRECTION_INPUT, 0);
+ port->direction = SPA_DIRECTION_INPUT;
+
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ spa_list_init(&port->ready);
+
+ this->latency[port->direction] = SPA_LATENCY_INFO(
+ port->direction,
+ .min_quantum = 1.0f,
+ .max_quantum = 1.0f);
+ this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ return spa_avb_init(this, info);
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with AVB" },
+ { SPA_KEY_FACTORY_USAGE, "[]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_avb_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ "avb.pcm.sink",
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/avb/avb-pcm-source.c b/spa/plugins/avb/avb-pcm-source.c
new file mode 100644
index 0000000..9ff9b21
--- /dev/null
+++ b/spa/plugins/avb/avb-pcm-source.c
@@ -0,0 +1,910 @@
+/* Spa AVB PCM Source
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/monitor/device.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/param/audio/format.h>
+#include <spa/pod/filter.h>
+
+#include "avb-pcm.h"
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+#define GET_PORT(this,d,p) (&this->ports[p])
+
+static void reset_props(struct props *props)
+{
+ snprintf(props->ifname, sizeof(props->ifname), "%s", DEFAULT_IFNAME);
+ parse_addr(props->addr, DEFAULT_ADDR);
+ props->prio = DEFAULT_PRIO;
+ parse_streamid(&props->streamid, DEFAULT_STREAMID);
+ props->mtt = DEFAULT_MTT;
+ props->t_uncertainty = DEFAULT_TU;
+ props->frames_per_pdu = DEFAULT_FRAMES_PER_PDU;
+}
+
+static void emit_node_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ struct spa_dict_item items[4];
+ uint32_t i, n_items = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "avb");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Source");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+ this->info.props = &SPA_DICT_INIT(items, n_items);
+
+ if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->info.n_params; i++) {
+ if (this->params[i].user > 0) {
+ this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_info(&this->hooks, &this->info);
+
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct state *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ uint32_t i;
+
+ if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < port->info.n_params; i++) {
+ if (port->params[i].user > 0) {
+ port->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ port->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ default:
+ param = spa_avb_enum_propinfo(this, result.index, &b);
+ if (param == NULL)
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct spa_pod_frame f;
+
+ switch (result.index) {
+ case 0:
+ spa_pod_builder_push_object(&b, &f,
+ SPA_TYPE_OBJECT_Props, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(this->process_latency.ns),
+ 0);
+ spa_avb_add_prop_params(this, &b);
+ param = spa_pod_builder_pop(&b, &f);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_ProcessLatency:
+ switch (result.index) {
+ case 0:
+ param = spa_process_latency_build(&b, id, &this->process_latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ spa_avb_reassign_follower(this);
+
+ return 0;
+}
+
+static void handle_process_latency(struct state *this,
+ const struct spa_process_latency_info *info)
+{
+ bool ns_changed = this->process_latency.ns != info->ns;
+ struct port *port = &this->ports[0];
+
+ if (this->process_latency.quantum == info->quantum &&
+ this->process_latency.rate == info->rate &&
+ !ns_changed)
+ return;
+
+ this->process_latency = *info;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ if (ns_changed)
+ this->params[NODE_Props].user++;
+ this->params[NODE_ProcessLatency].user++;
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[PORT_Latency].user++;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod *params = NULL;
+ int64_t lat_ns = -1;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&lat_ns),
+ SPA_PROP_params, SPA_POD_OPT_Pod(&params));
+
+ spa_avb_parse_prop_params(this, params);
+ if (lat_ns != -1) {
+ struct spa_process_latency_info info;
+ info = this->process_latency;
+ info.ns = lat_ns;
+ handle_process_latency(this, &info);
+ }
+ emit_node_info(this, false);
+ emit_port_info(this, &this->ports[0], false);
+ break;
+ }
+ case SPA_PARAM_ProcessLatency:
+ {
+ struct spa_process_latency_info info;
+ if ((res = spa_process_latency_parse(param, &info)) < 0)
+ return res;
+
+ handle_process_latency(this, &info);
+
+ emit_node_info(this, false);
+ emit_port_info(this, &this->ports[0], false);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_ParamBegin:
+ break;
+ case SPA_NODE_COMMAND_ParamEnd:
+ break;
+ case SPA_NODE_COMMAND_Start:
+ if (!this->ports[0].have_format)
+ return -EIO;
+ if (this->ports[0].n_buffers == 0)
+ return -EIO;
+ if ((res = spa_avb_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = spa_avb_pause(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct state *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->ports[0], true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ return spa_avb_enum_format(this, seq, start, num, filter);
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id,
+ &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->stride,
+ 16 * this->stride,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->stride));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ {
+ struct spa_latency_info latency = this->latency[result.index];
+ if (latency.direction == SPA_DIRECTION_OUTPUT)
+ spa_process_latency_info_add(&this->process_latency, &latency);
+ param = spa_latency_build(&b, id, &latency);
+ break;
+ }
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct state *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(void *object, struct port *port,
+ uint32_t flags, const struct spa_pod *format)
+{
+ struct state *this = object;
+ int err;
+
+ if (format == NULL) {
+ if (!port->have_format)
+ return 0;
+
+ spa_log_debug(this->log, "clear format");
+ port->have_format = false;
+ spa_avb_clear_format(this);
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if ((err = spa_avb_set_format(this, &info, flags)) < 0)
+ return err;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ emit_node_info(this, false);
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, this->rate);
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ port->params[PORT_Latency].user++;
+ } else {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ this->latency[info.direction] = info;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[PORT_Latency].user++;
+ emit_port_info(this, port, false);
+ break;
+ }
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct state *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers);
+
+ if (port->n_buffers > 0) {
+ spa_avb_pause(this);
+ clear_buffers(this, port);
+ }
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->flags = BUFFER_FLAG_OUT;
+
+ b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct state *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ port->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct state *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+ struct buffer *b;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = GET_PORT(this, SPA_DIRECTION_OUTPUT, 0);
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, "%p: process %d %d/%d %d", this, io->status,
+ io->buffer_id, port->n_buffers, this->following);
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (io->buffer_id < port->n_buffers) {
+ spa_avb_recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (spa_list_is_empty(&port->ready) && this->following) {
+ spa_avb_read(this);
+ }
+ if (spa_list_is_empty(&port->ready) || !this->following)
+ return SPA_STATUS_OK;
+
+ b = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct state *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct state *this;
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ this = (struct state *) handle;
+ spa_avb_clear(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support)
+{
+ struct state *this;
+ struct port *port;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct state *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ avb_log_topic_init(this->log);
+
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ reset_props(&this->props);
+
+ port = GET_PORT(this, SPA_DIRECTION_OUTPUT, 0);
+ port->direction = SPA_DIRECTION_OUTPUT;
+
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ spa_list_init(&port->ready);
+
+ this->latency[port->direction] = SPA_LATENCY_INFO(
+ port->direction,
+ .min_quantum = 1.0f,
+ .max_quantum = 1.0f);
+ this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+
+ return spa_avb_init(this, info);
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with AVB" },
+ { SPA_KEY_FACTORY_USAGE, "[]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_avb_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ "avb.pcm.source",
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/avb/avb-pcm.c b/spa/plugins/avb/avb-pcm.c
new file mode 100644
index 0000000..484adc6
--- /dev/null
+++ b/spa/plugins/avb/avb-pcm.c
@@ -0,0 +1,1227 @@
+/* Spa AVB PCM
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sched.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/time.h>
+#include <math.h>
+#include <limits.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+
+#include <spa/pod/filter.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/support/system.h>
+#include <spa/utils/keys.h>
+
+#include "avb-pcm.h"
+
+#define TAI_OFFSET (37ULL * SPA_NSEC_PER_SEC)
+#define TAI_TO_UTC(t) (t - TAI_OFFSET)
+
+static int avb_set_param(struct state *state, const char *k, const char *s)
+{
+ struct props *p = &state->props;
+ int fmt_change = 0;
+ if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) {
+ state->default_channels = atoi(s);
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) {
+ state->default_rate = atoi(s);
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_FORMAT)) {
+ state->default_format = spa_avb_format_from_name(s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
+ spa_avb_parse_position(&state->default_pos, s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) {
+ state->n_allowed_rates = spa_avb_parse_rates(state->allowed_rates,
+ MAX_RATES, s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, "avb.ifname")) {
+ snprintf(p->ifname, sizeof(p->ifname), "%s", s);
+ } else if (spa_streq(k, "avb.macaddr")) {
+ parse_addr(p->addr, s);
+ } else if (spa_streq(k, "avb.prio")) {
+ p->prio = atoi(s);
+ } else if (spa_streq(k, "avb.streamid")) {
+ parse_streamid(&p->streamid, s);
+ } else if (spa_streq(k, "avb.mtt")) {
+ p->mtt = atoi(s);
+ } else if (spa_streq(k, "avb.time-uncertainty")) {
+ p->t_uncertainty = atoi(s);
+ } else if (spa_streq(k, "avb.frames-per-pdu")) {
+ p->frames_per_pdu = atoi(s);
+ } else if (spa_streq(k, "avb.ptime-tolerance")) {
+ p->ptime_tolerance = atoi(s);
+ } else if (spa_streq(k, "latency.internal.rate")) {
+ state->process_latency.rate = atoi(s);
+ } else if (spa_streq(k, "latency.internal.ns")) {
+ state->process_latency.ns = atoi(s);
+ } else if (spa_streq(k, "clock.name")) {
+ spa_scnprintf(state->clock_name,
+ sizeof(state->clock_name), "%s", s);
+ } else
+ return 0;
+
+ if (fmt_change > 0) {
+ struct port *port = &state->ports[0];
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[PORT_EnumFormat].user++;
+ }
+ return 1;
+}
+
+static int position_to_string(struct channel_map *map, char *val, size_t len)
+{
+ uint32_t i, o = 0;
+ int r;
+ o += snprintf(val, len, "[ ");
+ for (i = 0; i < map->channels; i++) {
+ r = snprintf(val+o, len-o, "%s%s", i == 0 ? "" : ", ",
+ spa_debug_type_find_short_name(spa_type_audio_channel,
+ map->pos[i]));
+ if (r < 0 || o + r >= len)
+ return -ENOSPC;
+ o += r;
+ }
+ if (len > o)
+ o += snprintf(val+o, len-o, " ]");
+ return 0;
+}
+
+static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, size_t len)
+{
+ uint32_t i, o = 0;
+ int r;
+ o += snprintf(val, len, "[ ");
+ for (i = 0; i < n_vals; i++) {
+ r = snprintf(val+o, len-o, "%s%d", i == 0 ? "" : ", ", vals[i]);
+ if (r < 0 || o + r >= len)
+ return -ENOSPC;
+ o += r;
+ }
+ if (len > o)
+ o += snprintf(val+o, len-o, " ]");
+ return 0;
+}
+
+struct spa_pod *spa_avb_enum_propinfo(struct state *state,
+ uint32_t idx, struct spa_pod_builder *b)
+{
+ struct spa_pod *param;
+ struct props *p = &state->props;
+ char tmp[128];
+
+ switch (idx) {
+ case 0:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_CHANNELS),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Channels"),
+ SPA_PROP_INFO_type, SPA_POD_Int(state->default_channels),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_RATE),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Rate"),
+ SPA_PROP_INFO_type, SPA_POD_Int(state->default_rate),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_FORMAT),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Format"),
+ SPA_PROP_INFO_type, SPA_POD_String(
+ spa_debug_type_find_short_name(spa_type_audio_format,
+ state->default_format)),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 3:
+ {
+ char buf[1024];
+ position_to_string(&state->default_pos, buf, sizeof(buf));
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_POSITION),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Position"),
+ SPA_PROP_INFO_type, SPA_POD_String(buf),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ }
+ case 4:
+ {
+ char buf[1024];
+ uint32_array_to_string(state->allowed_rates, state->n_allowed_rates, buf, sizeof(buf));
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_ALLOWED_RATES),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Allowed Rates"),
+ SPA_PROP_INFO_type, SPA_POD_String(buf),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ }
+ case 5:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("avb.ifname"),
+ SPA_PROP_INFO_description, SPA_POD_String("The AVB interface name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->ifname, sizeof(p->ifname)),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 6:
+ format_addr(tmp, sizeof(tmp), p->addr);
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("avb.macaddr"),
+ SPA_PROP_INFO_description, SPA_POD_String("The AVB MAC address"),
+ SPA_PROP_INFO_type, SPA_POD_String(tmp),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 7:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("avb.prio"),
+ SPA_PROP_INFO_description, SPA_POD_String("The AVB stream priority"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->prio, 0, INT32_MAX),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 8:
+ format_streamid(tmp, sizeof(tmp), p->streamid);
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("avb.streamid"),
+ SPA_PROP_INFO_description, SPA_POD_String("The AVB stream id"),
+ SPA_PROP_INFO_type, SPA_POD_String(tmp),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 9:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("avb.mtt"),
+ SPA_PROP_INFO_description, SPA_POD_String("The AVB mtt"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->mtt, 0, INT32_MAX),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 10:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("avb.time-uncertainty"),
+ SPA_PROP_INFO_description, SPA_POD_String("The AVB time uncertainty"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->t_uncertainty, 0, INT32_MAX),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 11:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("avb.frames-per-pdu"),
+ SPA_PROP_INFO_description, SPA_POD_String("The AVB frames per packet"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->frames_per_pdu, 0, INT32_MAX),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 12:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("avb.ptime-tolerance"),
+ SPA_PROP_INFO_description, SPA_POD_String("The AVB packet tolerance"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->ptime_tolerance, 0, INT32_MAX),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 13:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("latency.internal.rate"),
+ SPA_PROP_INFO_description, SPA_POD_String("Internal latency in samples"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->process_latency.rate,
+ 0, 65536),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 14:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("latency.internal.ns"),
+ SPA_PROP_INFO_description, SPA_POD_String("Internal latency in nanoseconds"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(state->process_latency.ns,
+ 0, 2 * SPA_NSEC_PER_SEC),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 15:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("clock.name"),
+ SPA_PROP_INFO_description, SPA_POD_String("The name of the clock"),
+ SPA_PROP_INFO_type, SPA_POD_String(state->clock_name),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ default:
+ return NULL;
+ }
+ return param;
+}
+
+int spa_avb_add_prop_params(struct state *state, struct spa_pod_builder *b)
+{
+ struct props *p = &state->props;
+ struct spa_pod_frame f[1];
+ char buf[1024];
+
+ spa_pod_builder_prop(b, SPA_PROP_params, 0);
+ spa_pod_builder_push_struct(b, &f[0]);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_CHANNELS);
+ spa_pod_builder_int(b, state->default_channels);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_RATE);
+ spa_pod_builder_int(b, state->default_rate);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_FORMAT);
+ spa_pod_builder_string(b,
+ spa_debug_type_find_short_name(spa_type_audio_format,
+ state->default_format));
+
+ position_to_string(&state->default_pos, buf, sizeof(buf));
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_POSITION);
+ spa_pod_builder_string(b, buf);
+
+ uint32_array_to_string(state->allowed_rates, state->n_allowed_rates,
+ buf, sizeof(buf));
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_ALLOWED_RATES);
+ spa_pod_builder_string(b, buf);
+
+ spa_pod_builder_string(b, "avb.ifname");
+ spa_pod_builder_string(b, p->ifname);
+
+ format_addr(buf, sizeof(buf), p->addr);
+ spa_pod_builder_string(b, "avb.macadr");
+ spa_pod_builder_string(b, buf);
+
+ spa_pod_builder_string(b, "avb.prio");
+ spa_pod_builder_int(b, p->prio);
+
+ format_streamid(buf, sizeof(buf), p->streamid);
+ spa_pod_builder_string(b, "avb.streamid");
+ spa_pod_builder_string(b, buf);
+ spa_pod_builder_string(b, "avb.mtt");
+ spa_pod_builder_int(b, p->mtt);
+ spa_pod_builder_string(b, "avb.time-uncertainty");
+ spa_pod_builder_int(b, p->t_uncertainty);
+ spa_pod_builder_string(b, "avb.frames-per-pdu");
+ spa_pod_builder_int(b, p->frames_per_pdu);
+ spa_pod_builder_string(b, "avb.ptime-tolerance");
+ spa_pod_builder_int(b, p->ptime_tolerance);
+
+ spa_pod_builder_string(b, "latency.internal.rate");
+ spa_pod_builder_int(b, state->process_latency.rate);
+
+ spa_pod_builder_string(b, "latency.internal.ns");
+ spa_pod_builder_long(b, state->process_latency.ns);
+
+ spa_pod_builder_string(b, "clock.name");
+ spa_pod_builder_string(b, state->clock_name);
+
+ spa_pod_builder_pop(b, &f[0]);
+ return 0;
+}
+
+int spa_avb_parse_prop_params(struct state *state, struct spa_pod *params)
+{
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ int changed = 0;
+
+ if (params == NULL)
+ return 0;
+
+ spa_pod_parser_pod(&prs, params);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0)
+ return 0;
+
+ while (true) {
+ const char *name;
+ struct spa_pod *pod;
+ char value[512];
+
+ if (spa_pod_parser_get_string(&prs, &name) < 0)
+ break;
+
+ if (spa_pod_parser_get_pod(&prs, &pod) < 0)
+ break;
+ if (spa_pod_is_string(pod)) {
+ spa_pod_copy_string(pod, sizeof(value), value);
+ } else if (spa_pod_is_int(pod)) {
+ snprintf(value, sizeof(value), "%d",
+ SPA_POD_VALUE(struct spa_pod_int, pod));
+ } else if (spa_pod_is_long(pod)) {
+ snprintf(value, sizeof(value), "%"PRIi64,
+ SPA_POD_VALUE(struct spa_pod_long, pod));
+ } else if (spa_pod_is_bool(pod)) {
+ snprintf(value, sizeof(value), "%s",
+ SPA_POD_VALUE(struct spa_pod_bool, pod) ?
+ "true" : "false");
+ } else
+ continue;
+
+ spa_log_info(state->log, "key:'%s' val:'%s'", name, value);
+ avb_set_param(state, name, value);
+ changed++;
+ }
+ if (changed > 0) {
+ state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ state->params[NODE_Props].user++;
+ }
+ return changed;
+}
+
+int spa_avb_init(struct state *state, const struct spa_dict *info)
+{
+ uint32_t i;
+
+ state->quantum_limit = 8192;
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "clock.quantum-limit")) {
+ spa_atou32(s, &state->quantum_limit, 0);
+ } else {
+ avb_set_param(state, k, s);
+ }
+ }
+
+ state->ringbuffer_size = state->quantum_limit * 64;
+ state->ringbuffer_data = calloc(1, state->ringbuffer_size * 4);
+ spa_ringbuffer_init(&state->ring);
+ return 0;
+}
+
+int spa_avb_clear(struct state *state)
+{
+ return 0;
+}
+
+static int spa_format_to_aaf(uint32_t format)
+{
+ switch(format) {
+ case SPA_AUDIO_FORMAT_F32_BE: return SPA_AVBTP_AAF_FORMAT_FLOAT_32BIT;
+ case SPA_AUDIO_FORMAT_S32_BE: return SPA_AVBTP_AAF_FORMAT_INT_32BIT;
+ case SPA_AUDIO_FORMAT_S24_BE: return SPA_AVBTP_AAF_FORMAT_INT_24BIT;
+ case SPA_AUDIO_FORMAT_S16_BE: return SPA_AVBTP_AAF_FORMAT_INT_16BIT;
+ default: return SPA_AVBTP_AAF_FORMAT_USER;
+ }
+}
+
+static int calc_frame_size(uint32_t format)
+{
+ switch(format) {
+ case SPA_AUDIO_FORMAT_F32_BE:
+ case SPA_AUDIO_FORMAT_S32_BE: return 4;
+ case SPA_AUDIO_FORMAT_S24_BE: return 3;
+ case SPA_AUDIO_FORMAT_S16_BE: return 2;
+ default: return 0;
+ }
+}
+
+static int spa_rate_to_aaf(uint32_t rate)
+{
+ switch(rate) {
+ case 8000: return SPA_AVBTP_AAF_PCM_NSR_8KHZ;
+ case 16000: return SPA_AVBTP_AAF_PCM_NSR_16KHZ;
+ case 24000: return SPA_AVBTP_AAF_PCM_NSR_24KHZ;
+ case 32000: return SPA_AVBTP_AAF_PCM_NSR_32KHZ;
+ case 44100: return SPA_AVBTP_AAF_PCM_NSR_44_1KHZ;
+ case 48000: return SPA_AVBTP_AAF_PCM_NSR_48KHZ;
+ case 88200: return SPA_AVBTP_AAF_PCM_NSR_88_2KHZ;
+ case 96000: return SPA_AVBTP_AAF_PCM_NSR_96KHZ;
+ case 176400: return SPA_AVBTP_AAF_PCM_NSR_176_4KHZ;
+ case 192000: return SPA_AVBTP_AAF_PCM_NSR_192KHZ;
+ default: return SPA_AVBTP_AAF_PCM_NSR_USER;
+ }
+}
+
+int
+spa_avb_enum_format(struct state *state, int seq, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[2];
+ struct spa_pod *fmt;
+ int res = 0;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ result.id = SPA_PARAM_EnumFormat;
+ result.next = start;
+
+next:
+ result.index = result.next++;
+
+ if (result.index > 0)
+ return 0;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+ spa_pod_builder_add(&b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ 0);
+
+ spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_format, 0);
+ if (state->default_format != 0) {
+ spa_pod_builder_id(&b, state->default_format);
+ } else {
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Enum, 0);
+ spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_F32_BE);
+ spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_F32_BE);
+ spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S32_BE);
+ spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S24_BE);
+ spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S16_BE);
+ spa_pod_builder_pop(&b, &f[1]);
+ }
+ spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_rate, 0);
+ if (state->default_rate != 0) {
+ spa_pod_builder_int(&b, state->default_rate);
+ } else {
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Enum, 0);
+ spa_pod_builder_int(&b, 48000);
+ spa_pod_builder_int(&b, 8000);
+ spa_pod_builder_int(&b, 16000);
+ spa_pod_builder_int(&b, 24000);
+ spa_pod_builder_int(&b, 32000);
+ spa_pod_builder_int(&b, 44100);
+ spa_pod_builder_int(&b, 48000);
+ spa_pod_builder_int(&b, 88200);
+ spa_pod_builder_int(&b, 96000);
+ spa_pod_builder_int(&b, 176400);
+ spa_pod_builder_int(&b, 192000);
+ spa_pod_builder_pop(&b, &f[1]);
+ }
+ spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_channels, 0);
+ if (state->default_channels != 0) {
+ spa_pod_builder_int(&b, state->default_channels);
+ } else {
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Range, 0);
+ spa_pod_builder_int(&b, 8);
+ spa_pod_builder_int(&b, 2);
+ spa_pod_builder_int(&b, 32);
+ spa_pod_builder_pop(&b, &f[1]);
+ }
+ fmt = spa_pod_builder_pop(&b, &f[0]);
+
+ if (spa_pod_filter(&b, &result.param, fmt, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&state->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return res;
+}
+
+static int setup_socket(struct state *state)
+{
+ int fd, res;
+ struct ifreq req;
+ struct props *p = &state->props;
+
+ fd = socket(AF_PACKET, SOCK_DGRAM|SOCK_NONBLOCK, htons(ETH_P_TSN));
+ if (fd < 0) {
+ spa_log_error(state->log, "socket() failed: %m");
+ return -errno;
+ }
+
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", p->ifname);
+ res = ioctl(fd, SIOCGIFINDEX, &req);
+ if (res < 0) {
+ spa_log_error(state->log, "SIOCGIFINDEX %s failed: %m", p->ifname);
+ res = -errno;
+ goto error_close;
+ }
+
+ state->sock_addr.sll_family = AF_PACKET;
+ state->sock_addr.sll_protocol = htons(ETH_P_TSN);
+ state->sock_addr.sll_halen = ETH_ALEN;
+ state->sock_addr.sll_ifindex = req.ifr_ifindex;
+ memcpy(&state->sock_addr.sll_addr, p->addr, ETH_ALEN);
+
+ if (state->ports[0].direction == SPA_DIRECTION_INPUT) {
+ struct sock_txtime txtime_cfg;
+
+ res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &p->prio,
+ sizeof(p->prio));
+ if (res < 0) {
+ spa_log_error(state->log, "setsockopt(SO_PRIORITY %d) failed: %m", p->prio);
+ res = -errno;
+ goto error_close;
+ }
+
+ txtime_cfg.clockid = CLOCK_TAI;
+ txtime_cfg.flags = 0;
+ res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
+ sizeof(txtime_cfg));
+ if (res < 0) {
+ spa_log_error(state->log, "setsockopt(SO_TXTIME) failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+ } else {
+ struct packet_mreq mreq = { 0 };
+
+ res = bind(fd, (struct sockaddr *) &state->sock_addr,
+ sizeof(state->sock_addr));
+ if (res < 0) {
+ spa_log_error(state->log, "bind() failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+
+ mreq.mr_ifindex = req.ifr_ifindex;
+ mreq.mr_type = PACKET_MR_MULTICAST;
+ mreq.mr_alen = ETH_ALEN;
+ memcpy(&mreq.mr_address, p->addr, ETH_ALEN);
+ res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
+ &mreq, sizeof(struct packet_mreq));
+ if (res < 0) {
+ spa_log_error(state->log, "setsockopt(ADD_MEMBERSHIP) failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+ }
+ state->sockfd = fd;
+ return 0;
+
+error_close:
+ close(fd);
+ return res;
+}
+
+static int setup_packet(struct state *state, struct spa_audio_info *fmt)
+{
+ struct spa_avbtp_packet_aaf *pdu;
+ struct props *p = &state->props;
+ ssize_t payload_size, hdr_size, pdu_size;
+
+ hdr_size = sizeof(*pdu);
+ payload_size = state->stride * p->frames_per_pdu;
+ pdu_size = hdr_size + payload_size;
+ if ((pdu = calloc(1, pdu_size)) == NULL)
+ return -errno;
+
+ SPA_AVBTP_PACKET_AAF_SET_SUBTYPE(pdu, SPA_AVBTP_SUBTYPE_AAF);
+
+ if (state->ports[0].direction == SPA_DIRECTION_INPUT) {
+ SPA_AVBTP_PACKET_AAF_SET_SV(pdu, 1);
+ SPA_AVBTP_PACKET_AAF_SET_STREAM_ID(pdu, p->streamid);
+ SPA_AVBTP_PACKET_AAF_SET_TV(pdu, 1);
+ SPA_AVBTP_PACKET_AAF_SET_FORMAT(pdu, spa_format_to_aaf(state->format));
+ SPA_AVBTP_PACKET_AAF_SET_NSR(pdu, spa_rate_to_aaf(state->rate));
+ SPA_AVBTP_PACKET_AAF_SET_CHAN_PER_FRAME(pdu, state->channels);
+ SPA_AVBTP_PACKET_AAF_SET_BIT_DEPTH(pdu, calc_frame_size(state->format)*8);
+ SPA_AVBTP_PACKET_AAF_SET_DATA_LEN(pdu, payload_size);
+ SPA_AVBTP_PACKET_AAF_SET_SP(pdu, SPA_AVBTP_AAF_PCM_SP_NORMAL);
+ }
+ state->pdu = pdu;
+ state->hdr_size = hdr_size;
+ state->payload_size = payload_size;
+ state->pdu_size = pdu_size;
+ return 0;
+}
+
+static int setup_msg(struct state *state)
+{
+ state->iov[0].iov_base = state->pdu;
+ state->iov[0].iov_len = state->hdr_size;
+ state->iov[1].iov_base = state->pdu->payload;
+ state->iov[1].iov_len = state->payload_size;
+ state->iov[2].iov_base = state->pdu->payload;
+ state->iov[2].iov_len = 0;
+ state->msg.msg_name = &state->sock_addr;
+ state->msg.msg_namelen = sizeof(state->sock_addr);
+ state->msg.msg_iov = state->iov;
+ state->msg.msg_iovlen = 3;
+ state->msg.msg_control = state->control;
+ state->msg.msg_controllen = sizeof(state->control);
+ state->cmsg = CMSG_FIRSTHDR(&state->msg);
+ state->cmsg->cmsg_level = SOL_SOCKET;
+ state->cmsg->cmsg_type = SCM_TXTIME;
+ state->cmsg->cmsg_len = CMSG_LEN(sizeof(__u64));
+ return 0;
+}
+
+int spa_avb_clear_format(struct state *state)
+{
+ close(state->sockfd);
+ close(state->timerfd);
+ free(state->pdu);
+
+ return 0;
+}
+
+int spa_avb_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags)
+{
+ int res, frame_size;
+ struct props *p = &state->props;
+
+ frame_size = calc_frame_size(fmt->info.raw.format);
+ if (frame_size == 0)
+ return -EINVAL;
+
+ if (fmt->info.raw.rate == 0 ||
+ fmt->info.raw.channels == 0)
+ return -EINVAL;
+
+ state->format = fmt->info.raw.format;
+ state->rate = fmt->info.raw.rate;
+ state->channels = fmt->info.raw.channels;
+ state->blocks = 1;
+ state->stride = state->channels * frame_size;
+
+ if ((res = setup_socket(state)) < 0)
+ return res;
+
+ if ((res = spa_system_timerfd_create(state->data_system,
+ CLOCK_REALTIME, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_close_sockfd;
+
+ state->timerfd = res;
+
+ if ((res = setup_packet(state, fmt)) < 0)
+ return res;
+
+ if ((res = setup_msg(state)) < 0)
+ return res;
+
+ state->pdu_period = SPA_NSEC_PER_SEC * p->frames_per_pdu /
+ state->rate;
+
+ return 0;
+
+error_close_sockfd:
+ close(state->sockfd);
+ return res;
+}
+
+void spa_avb_recycle_buffer(struct state *this, struct port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_trace_fp(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+}
+
+static void reset_buffers(struct state *this, struct port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ if (port->direction == SPA_DIRECTION_INPUT) {
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ } else {
+ spa_list_append(&port->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+ }
+}
+
+static inline bool is_pdu_valid(struct state *state)
+{
+ uint8_t seq_num;
+
+ seq_num = SPA_AVBTP_PACKET_AAF_GET_SEQ_NUM(state->pdu);
+
+ if (state->prev_seq != 0 && (uint8_t)(state->prev_seq + 1) != seq_num) {
+ spa_log_warn(state->log, "dropped packets %d != %d", state->prev_seq + 1, seq_num);
+ }
+ state->prev_seq = seq_num;
+ return true;
+}
+
+static inline void
+set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
+ uint32_t offset, struct iovec *iov, uint32_t len)
+{
+ iov[0].iov_len = SPA_MIN(len, size - offset);
+ iov[0].iov_base = SPA_PTROFF(buffer, offset, void);
+ iov[1].iov_len = len - iov[0].iov_len;
+ iov[1].iov_base = buffer;
+}
+
+static void avb_on_socket_event(struct spa_source *source)
+{
+ struct state *state = source->data;
+ ssize_t n;
+ int32_t filled;
+ uint32_t subtype, index;
+ struct spa_avbtp_packet_aaf *pdu = state->pdu;
+ bool overrun = false;
+
+ filled = spa_ringbuffer_get_write_index(&state->ring, &index);
+ overrun = filled > (int32_t) state->ringbuffer_size;
+ if (overrun) {
+ state->iov[1].iov_base = state->pdu->payload;
+ state->iov[1].iov_len = state->payload_size;
+ state->iov[2].iov_len = 0;
+ } else {
+ set_iovec(&state->ring,
+ state->ringbuffer_data,
+ state->ringbuffer_size,
+ index % state->ringbuffer_size,
+ &state->iov[1], state->payload_size);
+ }
+
+ n = recvmsg(state->sockfd, &state->msg, 0);
+ if (n < 0) {
+ spa_log_error(state->log, "recv() failed: %m");
+ return;
+ }
+ if (n != (ssize_t)state->pdu_size) {
+ spa_log_error(state->log, "AVB packet dropped: Invalid size");
+ return;
+ }
+
+ subtype = SPA_AVBTP_PACKET_AAF_GET_SUBTYPE(pdu);
+ if (subtype != SPA_AVBTP_SUBTYPE_AAF) {
+ spa_log_error(state->log, "non supported subtype %d", subtype);
+ return;
+ }
+ if (!is_pdu_valid(state)) {
+ spa_log_error(state->log, "AAF PDU invalid");
+ return;
+ }
+ if (overrun) {
+ spa_log_warn(state->log, "overrun %d", filled);
+ return;
+ }
+ index += state->payload_size;
+ spa_ringbuffer_write_update(&state->ring, index);
+}
+
+static void set_timeout(struct state *state, uint64_t next_time)
+{
+ struct itimerspec ts;
+ uint64_t time_utc;
+
+ spa_log_trace(state->log, "set timeout %"PRIu64, next_time);
+
+ time_utc = next_time > TAI_OFFSET ? TAI_TO_UTC(next_time) : 0;
+ ts.it_value.tv_sec = time_utc / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time_utc % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(state->data_system,
+ state->timer_source.fd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int flush_write(struct state *state, uint64_t current_time)
+{
+ int32_t avail, wanted;
+ uint32_t index;
+ uint64_t ptime, txtime;
+ int pdu_count;
+ struct props *p = &state->props;
+ struct spa_avbtp_packet_aaf *pdu = state->pdu;
+ ssize_t n;
+
+ avail = spa_ringbuffer_get_read_index(&state->ring, &index);
+ wanted = state->duration * state->stride;
+ if (avail < wanted) {
+ spa_log_warn(state->log, "underrun %d < %d", avail, wanted);
+ return -EPIPE;
+ }
+
+ pdu_count = state->duration / p->frames_per_pdu;
+
+ txtime = current_time + p->t_uncertainty;
+ ptime = txtime + p->mtt;
+
+ while (pdu_count--) {
+ *(__u64 *)CMSG_DATA(state->cmsg) = txtime;
+
+ set_iovec(&state->ring,
+ state->ringbuffer_data,
+ state->ringbuffer_size,
+ index % state->ringbuffer_size,
+ &state->iov[1], state->payload_size);
+
+ SPA_AVBTP_PACKET_AAF_SET_SEQ_NUM(pdu, state->pdu_seq++);
+ SPA_AVBTP_PACKET_AAF_SET_TIMESTAMP(pdu, ptime);
+
+ n = sendmsg(state->sockfd, &state->msg, MSG_NOSIGNAL);
+ if (n < 0 || n != (ssize_t)state->pdu_size) {
+ spa_log_error(state->log, "sendmdg() failed: %m");
+ }
+ txtime += state->pdu_period;
+ ptime += state->pdu_period;
+ index += state->payload_size;
+ }
+ spa_ringbuffer_read_update(&state->ring, index);
+ return 0;
+}
+
+int spa_avb_write(struct state *state)
+{
+ int32_t filled;
+ uint32_t index, to_write;
+ struct port *port = &state->ports[0];
+
+ filled = spa_ringbuffer_get_write_index(&state->ring, &index);
+ if (filled < 0) {
+ spa_log_warn(state->log, "underrun %d", filled);
+ } else if (filled > (int32_t)state->ringbuffer_size) {
+ spa_log_warn(state->log, "overrun %d", filled);
+ }
+ to_write = state->ringbuffer_size - filled;
+
+ while (!spa_list_is_empty(&port->ready) && to_write > 0) {
+ size_t n_bytes;
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t offs, avail, size;
+
+ b = spa_list_first(&port->ready, struct buffer, link);
+ d = b->buf->datas;
+
+ offs = SPA_MIN(d[0].chunk->offset + port->ready_offset, d[0].maxsize);
+ size = SPA_MIN(d[0].chunk->size, d[0].maxsize - offs);
+ avail = size - offs;
+
+ n_bytes = SPA_MIN(avail, to_write);
+ if (n_bytes == 0)
+ break;
+
+ spa_ringbuffer_write_data(&state->ring,
+ state->ringbuffer_data,
+ state->ringbuffer_size,
+ index % state->ringbuffer_size,
+ SPA_PTROFF(d[0].data, offs, void),
+ n_bytes);
+
+ port->ready_offset += n_bytes;
+
+ if (port->ready_offset >= size || avail == 0) {
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ port->io->buffer_id = b->id;
+ spa_log_trace_fp(state->log, "%p: reuse buffer %u", state, b->id);
+
+ spa_node_call_reuse_buffer(&state->callbacks, 0, b->id);
+
+ port->ready_offset = 0;
+ }
+ to_write -= n_bytes;
+ index += n_bytes;
+ }
+ spa_ringbuffer_write_update(&state->ring, index);
+
+ if (state->following) {
+ flush_write(state, state->position->clock.nsec);
+ }
+ return 0;
+}
+
+static int handle_play(struct state *state, uint64_t current_time)
+{
+ flush_write(state, current_time);
+ spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA);
+ return 0;
+}
+
+int spa_avb_read(struct state *state)
+{
+ int32_t avail, wanted;
+ uint32_t index;
+ struct port *port = &state->ports[0];
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t n_bytes;
+
+ if (state->position)
+ state->duration = state->position->clock.duration;
+
+ avail = spa_ringbuffer_get_read_index(&state->ring, &index);
+ wanted = state->duration * state->stride;
+
+ if (spa_list_is_empty(&port->free)) {
+ spa_log_warn(state->log, "out of buffers");
+ return -EPIPE;
+ }
+
+ b = spa_list_first(&port->free, struct buffer, link);
+ d = b->buf->datas;
+
+ n_bytes = SPA_MIN(d[0].maxsize, (uint32_t)wanted);
+
+ if (avail < wanted) {
+ spa_log_warn(state->log, "capture underrun %d < %d", avail, wanted);
+ memset(d[0].data, 0, n_bytes);
+ } else {
+ spa_ringbuffer_read_data(&state->ring,
+ state->ringbuffer_data,
+ state->ringbuffer_size,
+ index % state->ringbuffer_size,
+ d[0].data, n_bytes);
+ index += n_bytes;
+ spa_ringbuffer_read_update(&state->ring, index);
+ }
+
+ d[0].chunk->offset = 0;
+ d[0].chunk->size = n_bytes;
+ d[0].chunk->stride = state->stride;
+ d[0].chunk->flags = 0;
+
+ spa_list_remove(&b->link);
+ spa_list_append(&port->ready, &b->link);
+
+ return 0;
+}
+
+static int handle_capture(struct state *state, uint64_t current_time)
+{
+ struct port *port = &state->ports[0];
+ struct spa_io_buffers *io;
+ struct buffer *b;
+
+ spa_avb_read(state);
+
+ if (spa_list_is_empty(&port->ready))
+ return 0;
+
+ io = port->io;
+ if (io != NULL &&
+ (io->status != SPA_STATUS_HAVE_DATA || port->rate_match != NULL)) {
+ if (io->buffer_id < port->n_buffers)
+ spa_avb_recycle_buffer(state, port, io->buffer_id);
+
+ b = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ spa_log_trace_fp(state->log, "%p: output buffer:%d", state, b->id);
+ }
+ spa_node_call_ready(&state->callbacks, SPA_STATUS_HAVE_DATA);
+ return 0;
+}
+
+static void avb_on_timeout_event(struct spa_source *source)
+{
+ struct state *state = source->data;
+ uint64_t expirations, current_time, duration;
+ uint32_t rate;
+ int res;
+
+ spa_log_trace(state->log, "timeout");
+
+ if ((res = spa_system_timerfd_read(state->data_system,
+ state->timer_source.fd, &expirations)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_error(state->log, "read timerfd: %s", spa_strerror(res));
+ return;
+ }
+
+ current_time = state->next_time;
+ if (SPA_LIKELY(state->position)) {
+ duration = state->position->clock.duration;
+ rate = state->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+ state->duration = duration;
+
+ if (state->ports[0].direction == SPA_DIRECTION_INPUT)
+ handle_play(state, current_time);
+ else
+ handle_capture(state, current_time);
+
+ state->next_time = current_time + duration * SPA_NSEC_PER_SEC / rate;
+
+ if (SPA_LIKELY(state->clock)) {
+ state->clock->nsec = current_time;
+ state->clock->position += duration;
+ state->clock->duration = duration;
+ state->clock->delay = 0;
+ state->clock->rate_diff = 1.0;
+ state->clock->next_nsec = state->next_time;
+ }
+
+ set_timeout(state, state->next_time);
+}
+
+static int set_timers(struct state *state)
+{
+ struct timespec now;
+ int res;
+
+ if ((res = spa_system_clock_gettime(state->data_system, CLOCK_TAI, &now)) < 0)
+ return res;
+
+ state->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ if (state->following) {
+ set_timeout(state, 0);
+ } else {
+ set_timeout(state, state->next_time);
+ }
+ return 0;
+}
+
+static inline bool is_following(struct state *state)
+{
+ return state->position && state->clock && state->position->clock.id != state->clock->id;
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct state *state = user_data;
+ spa_dll_init(&state->dll);
+ set_timers(state);
+ return 0;
+}
+
+int spa_avb_reassign_follower(struct state *state)
+{
+ bool following, freewheel;
+
+ if (!state->started)
+ return 0;
+
+ following = is_following(state);
+ if (following != state->following) {
+ spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following);
+ state->following = following;
+ spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state);
+ }
+
+ freewheel = state->position &&
+ SPA_FLAG_IS_SET(state->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL);
+
+ if (state->freewheel != freewheel) {
+ spa_log_debug(state->log, "%p: freewheel %d->%d", state, state->freewheel, freewheel);
+ state->freewheel = freewheel;
+ }
+ return 0;
+}
+
+int spa_avb_start(struct state *state)
+{
+ if (state->started)
+ return 0;
+
+ if (state->position) {
+ state->duration = state->position->clock.duration;
+ state->rate_denom = state->position->clock.rate.denom;
+ } else {
+ state->duration = 1024;
+ state->rate_denom = state->rate;
+ }
+
+ spa_dll_init(&state->dll);
+ state->max_error = (256.0 * state->rate) / state->rate_denom;
+
+ state->following = is_following(state);
+
+ state->timer_source.func = avb_on_timeout_event;
+ state->timer_source.data = state;
+ state->timer_source.fd = state->timerfd;
+ state->timer_source.mask = SPA_IO_IN;
+ state->timer_source.rmask = 0;
+ spa_loop_add_source(state->data_loop, &state->timer_source);
+
+ state->pdu_seq = 0;
+
+ if (state->ports[0].direction == SPA_DIRECTION_OUTPUT) {
+ state->sock_source.func = avb_on_socket_event;
+ state->sock_source.data = state;
+ state->sock_source.fd = state->sockfd;
+ state->sock_source.mask = SPA_IO_IN;
+ state->sock_source.rmask = 0;
+ spa_loop_add_source(state->data_loop, &state->sock_source);
+ }
+
+ reset_buffers(state, &state->ports[0]);
+
+ set_timers(state);
+
+ state->started = true;
+
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct state *state = user_data;
+
+ spa_loop_remove_source(state->data_loop, &state->timer_source);
+
+ if (state->ports[0].direction == SPA_DIRECTION_OUTPUT) {
+ spa_loop_remove_source(state->data_loop, &state->sock_source);
+ }
+ return 0;
+}
+
+int spa_avb_pause(struct state *state)
+{
+ if (!state->started)
+ return 0;
+
+ spa_log_debug(state->log, "%p: pause", state);
+
+ spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state);
+
+ state->started = false;
+ set_timeout(state, 0);
+
+ return 0;
+}
diff --git a/spa/plugins/avb/avb-pcm.h b/spa/plugins/avb/avb-pcm.h
new file mode 100644
index 0000000..bb3bce6
--- /dev/null
+++ b/spa/plugins/avb/avb-pcm.h
@@ -0,0 +1,343 @@
+/* Spa AVB PCM
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AVB_PCM_H
+#define SPA_AVB_PCM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <math.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/net_tstamp.h>
+#include <limits.h>
+#include <net/if.h>
+
+#include <avbtp/packets.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/json.h>
+#include <spa/utils/dll.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/debug/types.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "avb.h"
+
+#define MAX_RATES 16
+
+#define DEFAULT_IFNAME "eth0"
+#define DEFAULT_ADDR "01:AA:AA:AA:AA:AA"
+#define DEFAULT_PRIO 0
+#define DEFAULT_STREAMID "AA:BB:CC:DD:EE:FF:0000"
+#define DEFAULT_MTT 5000000
+#define DEFAULT_TU 1000000
+#define DEFAULT_FRAMES_PER_PDU 8
+
+#define DEFAULT_PERIOD 1024u
+#define DEFAULT_RATE 48000u
+#define DEFAULT_CHANNELS 8u
+
+struct props {
+ char ifname[IFNAMSIZ];
+ unsigned char addr[ETH_ALEN];
+ int prio;
+ uint64_t streamid;
+ int mtt;
+ int t_uncertainty;
+ uint32_t frames_per_pdu;
+ int ptime_tolerance;
+};
+
+static inline int parse_addr(unsigned char addr[ETH_ALEN], const char *str)
+{
+ unsigned char ad[ETH_ALEN];
+ if (sscanf(str, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
+ &ad[0], &ad[1], &ad[2], &ad[3], &ad[4], &ad[5]) != 6)
+ return -EINVAL;
+ memcpy(addr, ad, sizeof(ad));
+ return 0;
+}
+static inline char *format_addr(char *str, size_t size, const unsigned char addr[ETH_ALEN])
+{
+ snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x",
+ addr[0], addr[1], addr[2],
+ addr[3], addr[4], addr[5]);
+ return str;
+}
+
+static inline int parse_streamid(uint64_t *streamid, const char *str)
+{
+ unsigned char addr[6];
+ unsigned short unique_id;
+ if (sscanf(str, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx:%hx",
+ &addr[0], &addr[1], &addr[2], &addr[3],
+ &addr[4], &addr[5], &unique_id) != 7)
+ return -EINVAL;
+ *streamid = (uint64_t) addr[0] << 56 |
+ (uint64_t) addr[1] << 48 |
+ (uint64_t) addr[2] << 40 |
+ (uint64_t) addr[3] << 32 |
+ (uint64_t) addr[4] << 24 |
+ (uint64_t) addr[5] << 16 |
+ unique_id;
+ return 0;
+}
+static inline char *format_streamid(char *str, size_t size, const uint64_t streamid)
+{
+ snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x:%04x",
+ (uint8_t)(streamid >> 56),
+ (uint8_t)(streamid >> 48),
+ (uint8_t)(streamid >> 40),
+ (uint8_t)(streamid >> 32),
+ (uint8_t)(streamid >> 24),
+ (uint8_t)(streamid >> 16),
+ (uint16_t)(streamid));
+ return str;
+}
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+#define BW_MAX 0.128
+#define BW_MED 0.064
+#define BW_MIN 0.016
+#define BW_PERIOD (3 * SPA_NSEC_PER_SEC)
+
+struct channel_map {
+ uint32_t channels;
+ uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct port {
+ enum spa_direction direction;
+ uint32_t id;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+#define PORT_EnumFormat 0
+#define PORT_Meta 1
+#define PORT_IO 2
+#define PORT_Format 3
+#define PORT_Buffers 4
+#define PORT_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ bool have_format;
+ struct spa_audio_info current_format;
+
+ struct spa_io_buffers *io;
+ struct spa_io_rate_match *rate_match;
+ struct buffer buffers[MAX_BUFFERS];
+ unsigned int n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+ uint32_t ready_offset;
+};
+
+struct state {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_system *data_system;
+ struct spa_loop *data_loop;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define NODE_PropInfo 0
+#define NODE_Props 1
+#define NODE_IO 2
+#define NODE_ProcessLatency 3
+#define N_NODE_PARAMS 4
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ uint32_t default_period_size;
+ uint32_t default_format;
+ unsigned int default_channels;
+ unsigned int default_rate;
+ uint32_t allowed_rates[MAX_RATES];
+ uint32_t n_allowed_rates;
+ struct channel_map default_pos;
+ char clock_name[64];
+ uint32_t quantum_limit;
+
+ uint32_t format;
+ uint32_t rate;
+ uint32_t channels;
+ uint32_t stride;
+ uint32_t blocks;
+ uint32_t rate_denom;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ struct port ports[1];
+
+ uint32_t duration;
+ unsigned int following:1;
+ unsigned int matching:1;
+ unsigned int resample:1;
+ unsigned int started:1;
+ unsigned int freewheel:1;
+
+ int timerfd;
+ struct spa_source timer_source;
+ uint64_t next_time;
+
+ int sockfd;
+ struct spa_source sock_source;
+ struct sockaddr_ll sock_addr;
+
+ struct spa_avbtp_packet_aaf *pdu;
+ size_t hdr_size;
+ size_t payload_size;
+ size_t pdu_size;
+ int64_t pdu_period;
+ uint8_t pdu_seq;
+ uint8_t prev_seq;
+
+ struct iovec iov[3];
+ struct msghdr msg;
+ char control[CMSG_SPACE(sizeof(__u64))];
+ struct cmsghdr *cmsg;
+
+ uint8_t *ringbuffer_data;
+ uint32_t ringbuffer_size;
+ struct spa_ringbuffer ring;
+
+ struct spa_dll dll;
+ double max_error;
+
+ struct spa_latency_info latency[2];
+ struct spa_process_latency_info process_latency;
+};
+
+struct spa_pod *spa_avb_enum_propinfo(struct state *state,
+ uint32_t idx, struct spa_pod_builder *b);
+int spa_avb_add_prop_params(struct state *state, struct spa_pod_builder *b);
+int spa_avb_parse_prop_params(struct state *state, struct spa_pod *params);
+
+int spa_avb_enum_format(struct state *state, int seq,
+ uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+
+int spa_avb_clear_format(struct state *state);
+int spa_avb_set_format(struct state *state, struct spa_audio_info *info, uint32_t flags);
+
+int spa_avb_init(struct state *state, const struct spa_dict *info);
+int spa_avb_clear(struct state *state);
+
+int spa_avb_start(struct state *state);
+int spa_avb_reassign_follower(struct state *state);
+int spa_avb_pause(struct state *state);
+
+int spa_avb_write(struct state *state);
+int spa_avb_read(struct state *state);
+int spa_avb_skip(struct state *state);
+
+void spa_avb_recycle_buffer(struct state *state, struct port *port, uint32_t buffer_id);
+
+static inline uint32_t spa_avb_format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static inline uint32_t spa_avb_channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0)
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static inline void spa_avb_parse_position(struct channel_map *map, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ map->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ map->channels < SPA_AUDIO_MAX_CHANNELS) {
+ map->pos[map->channels++] = spa_avb_channel_from_name(v);
+ }
+}
+
+static inline uint32_t spa_avb_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+ uint32_t count;
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ count = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && count < max)
+ rates[count++] = atoi(v);
+ return count;
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_AVB_PCM_H */
diff --git a/spa/plugins/avb/avb.c b/spa/plugins/avb/avb.c
new file mode 100644
index 0000000..8f67310
--- /dev/null
+++ b/spa/plugins/avb/avb.c
@@ -0,0 +1,54 @@
+/* Spa AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+
+extern const struct spa_handle_factory spa_avb_sink_factory;
+extern const struct spa_handle_factory spa_avb_source_factory;
+
+struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.avb");
+struct spa_log_topic *avb_log_topic = &log_topic;
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *factory = &spa_avb_sink_factory;
+ break;
+ case 1:
+ *factory = &spa_avb_source_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/avb/avb.h b/spa/plugins/avb/avb.h
new file mode 100644
index 0000000..a99a0fe
--- /dev/null
+++ b/spa/plugins/avb/avb.h
@@ -0,0 +1,39 @@
+/* Spa AVB
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AVB_H
+#define SPA_AVB_H
+
+#include <spa/support/log.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT avb_log_topic
+extern struct spa_log_topic *avb_log_topic;
+
+static inline void avb_log_topic_init(struct spa_log *log)
+{
+ spa_log_topic_init(log, avb_log_topic);
+}
+
+#endif /* SPA_AVB_H */
diff --git a/spa/plugins/avb/avbtp/packets.h b/spa/plugins/avb/avbtp/packets.h
new file mode 100644
index 0000000..3d4a652
--- /dev/null
+++ b/spa/plugins/avb/avbtp/packets.h
@@ -0,0 +1,220 @@
+/* Spa AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_AVB_PACKETS_H
+#define SPA_AVB_PACKETS_H
+
+#define SPA_AVBTP_SUBTYPE_61883_IIDC 0x00
+#define SPA_AVBTP_SUBTYPE_MMA_STREAM 0x01
+#define SPA_AVBTP_SUBTYPE_AAF 0x02
+#define SPA_AVBTP_SUBTYPE_CVF 0x03
+#define SPA_AVBTP_SUBTYPE_CRF 0x04
+#define SPA_AVBTP_SUBTYPE_TSCF 0x05
+#define SPA_AVBTP_SUBTYPE_SVF 0x06
+#define SPA_AVBTP_SUBTYPE_RVF 0x07
+#define SPA_AVBTP_SUBTYPE_AEF_CONTINUOUS 0x6E
+#define SPA_AVBTP_SUBTYPE_VSF_STREAM 0x6F
+#define SPA_AVBTP_SUBTYPE_EF_STREAM 0x7F
+#define SPA_AVBTP_SUBTYPE_NTSCF 0x82
+#define SPA_AVBTP_SUBTYPE_ESCF 0xEC
+#define SPA_AVBTP_SUBTYPE_EECF 0xED
+#define SPA_AVBTP_SUBTYPE_AEF_DISCRETE 0xEE
+#define SPA_AVBTP_SUBTYPE_ADP 0xFA
+#define SPA_AVBTP_SUBTYPE_AECP 0xFB
+#define SPA_AVBTP_SUBTYPE_ACMP 0xFC
+#define SPA_AVBTP_SUBTYPE_MAAP 0xFE
+#define SPA_AVBTP_SUBTYPE_EF_CONTROL 0xFF
+
+struct spa_avbtp_packet_common {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1; /* stream_id valid */
+ unsigned version:3;
+ unsigned subtype_data1:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned subtype_data1:4;
+ unsigned version:3;
+ unsigned sv:1;
+#elif
+#error "Unknown byte order"
+#endif
+ uint16_t subtype_data2;
+ uint64_t stream_id;
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define SPA_AVBTP_PACKET_SET_SUBTYPE(p,v) ((p)->subtype = (v))
+#define SPA_AVBTP_PACKET_SET_SV(p,v) ((p)->sv = (v))
+#define SPA_AVBTP_PACKET_SET_VERSION(p,v) ((p)->version = (v))
+#define SPA_AVBTP_PACKET_SET_STREAM_ID(p,v) ((p)->stream_id = htobe64(v))
+
+#define SPA_AVBTP_PACKET_GET_SUBTYPE(p) ((p)->subtype)
+#define SPA_AVBTP_PACKET_GET_SV(p) ((p)->sv)
+#define SPA_AVBTP_PACKET_GET_VERSION(p) ((p)->version)
+#define SPA_AVBTP_PACKET_GET_STREAM_ID(p) be64toh((p)->stream_id)
+
+struct spa_avbtp_packet_cc {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1;
+ unsigned version:3;
+ unsigned control_data1:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned control_data1:4;
+ unsigned version:3;
+ unsigned sv:1;
+#endif
+ uint8_t status;
+ uint16_t control_frame_length;
+ uint64_t stream_id;
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define SPA_AVBTP_PACKET_CC_SET_SUBTYPE(p,v) ((p)->subtype = (v))
+#define SPA_AVBTP_PACKET_CC_SET_SV(p,v) ((p)->sv = (v))
+#define SPA_AVBTP_PACKET_CC_SET_VERSION(p,v) ((p)->version = (v))
+#define SPA_AVBTP_PACKET_CC_SET_STREAM_ID(p,v) ((p)->stream_id = htobe64(v))
+#define SPA_AVBTP_PACKET_CC_SET_STATUS(p,v) ((p)->status = (v))
+#define SPA_AVBTP_PACKET_CC_SET_LENGTH(p,v) ((p)->control_frame_length = htons(v))
+
+#define SPA_AVBTP_PACKET_CC_GET_SUBTYPE(p) ((p)->subtype)
+#define SPA_AVBTP_PACKET_CC_GET_SV(p) ((p)->sv)
+#define SPA_AVBTP_PACKET_CC_GET_VERSION(p) ((p)->version)
+#define SPA_AVBTP_PACKET_CC_GET_STREAM_ID(p) be64toh((p)->stream_id)
+#define SPA_AVBTP_PACKET_CC_GET_STATUS(p) ((p)->status)
+#define SPA_AVBTP_PACKET_CC_GET_LENGTH(p) ntohs((p)->control_frame_length)
+
+/* AAF */
+struct spa_avbtp_packet_aaf {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1;
+ unsigned version:3;
+ unsigned mr:1;
+ unsigned _r1:1;
+ unsigned gv:1;
+ unsigned tv:1;
+
+ uint8_t seq_num;
+
+ unsigned _r2:7;
+ unsigned tu:1;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned tv:1;
+ unsigned gv:1;
+ unsigned _r1:1;
+ unsigned mr:1;
+ unsigned version:3;
+ unsigned sv:1;
+
+ uint8_t seq_num;
+
+ unsigned tu:1;
+ unsigned _r2:7;
+#endif
+ uint64_t stream_id;
+ uint32_t timestamp;
+#define SPA_AVBTP_AAF_FORMAT_USER 0x00
+#define SPA_AVBTP_AAF_FORMAT_FLOAT_32BIT 0x01
+#define SPA_AVBTP_AAF_FORMAT_INT_32BIT 0x02
+#define SPA_AVBTP_AAF_FORMAT_INT_24BIT 0x03
+#define SPA_AVBTP_AAF_FORMAT_INT_16BIT 0x04
+#define SPA_AVBTP_AAF_FORMAT_AES3_32BIT 0x05
+ uint8_t format;
+
+#define SPA_AVBTP_AAF_PCM_NSR_USER 0x00
+#define SPA_AVBTP_AAF_PCM_NSR_8KHZ 0x01
+#define SPA_AVBTP_AAF_PCM_NSR_16KHZ 0x02
+#define SPA_AVBTP_AAF_PCM_NSR_32KHZ 0x03
+#define SPA_AVBTP_AAF_PCM_NSR_44_1KHZ 0x04
+#define SPA_AVBTP_AAF_PCM_NSR_48KHZ 0x05
+#define SPA_AVBTP_AAF_PCM_NSR_88_2KHZ 0x06
+#define SPA_AVBTP_AAF_PCM_NSR_96KHZ 0x07
+#define SPA_AVBTP_AAF_PCM_NSR_176_4KHZ 0x08
+#define SPA_AVBTP_AAF_PCM_NSR_192KHZ 0x09
+#define SPA_AVBTP_AAF_PCM_NSR_24KHZ 0x0A
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned nsr:4;
+ unsigned _r3:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned _r3:4;
+ unsigned nsr:4;
+#endif
+ uint8_t chan_per_frame;
+ uint8_t bit_depth;
+ uint16_t data_len;
+
+#define SPA_AVBTP_AAF_PCM_SP_NORMAL 0x00
+#define SPA_AVBTP_AAF_PCM_SP_SPARSE 0x01
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned _r4:3;
+ unsigned sp:1;
+ unsigned event:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned event:4;
+ unsigned sp:1;
+ unsigned _r4:3;
+#endif
+ uint8_t _r5;
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define SPA_AVBTP_PACKET_AAF_SET_SUBTYPE(p,v) ((p)->subtype = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_SV(p,v) ((p)->sv = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_VERSION(p,v) ((p)->version = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_MR(p,v) ((p)->mr = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_GV(p,v) ((p)->gv = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_TV(p,v) ((p)->tv = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_SEQ_NUM(p,v) ((p)->seq_num = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_TU(p,v) ((p)->tu = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_STREAM_ID(p,v) ((p)->stream_id = htobe64(v))
+#define SPA_AVBTP_PACKET_AAF_SET_TIMESTAMP(p,v) ((p)->timestamp = htonl(v))
+#define SPA_AVBTP_PACKET_AAF_SET_DATA_LEN(p,v) ((p)->data_len = htons(v))
+#define SPA_AVBTP_PACKET_AAF_SET_FORMAT(p,v) ((p)->format = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_NSR(p,v) ((p)->nsr = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_CHAN_PER_FRAME(p,v) ((p)->chan_per_frame = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_BIT_DEPTH(p,v) ((p)->bit_depth = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_SP(p,v) ((p)->sp = (v))
+#define SPA_AVBTP_PACKET_AAF_SET_EVENT(p,v) ((p)->event = (v))
+
+#define SPA_AVBTP_PACKET_AAF_GET_SUBTYPE(p) ((p)->subtype)
+#define SPA_AVBTP_PACKET_AAF_GET_SV(p) ((p)->sv)
+#define SPA_AVBTP_PACKET_AAF_GET_VERSION(p) ((p)->version)
+#define SPA_AVBTP_PACKET_AAF_GET_MR(p) ((p)->mr)
+#define SPA_AVBTP_PACKET_AAF_GET_GV(p) ((p)->gv)
+#define SPA_AVBTP_PACKET_AAF_GET_TV(p) ((p)->tv)
+#define SPA_AVBTP_PACKET_AAF_GET_SEQ_NUM(p) ((p)->seq_num)
+#define SPA_AVBTP_PACKET_AAF_GET_TU(p) ((p)->tu)
+#define SPA_AVBTP_PACKET_AAF_GET_STREAM_ID(p) be64toh((p)->stream_id)
+#define SPA_AVBTP_PACKET_AAF_GET_TIMESTAMP(p) ntohl((p)->timestamp)
+#define SPA_AVBTP_PACKET_AAF_GET_DATA_LEN(p) ntohs((p)->data_len)
+#define SPA_AVBTP_PACKET_AAF_GET_FORMAT(p) ((p)->format)
+#define SPA_AVBTP_PACKET_AAF_GET_NSR(p) ((p)->nsr)
+#define SPA_AVBTP_PACKET_AAF_GET_CHAN_PER_FRAME(p) ((p)->chan_per_frame)
+#define SPA_AVBTP_PACKET_AAF_GET_BIT_DEPTH(p) ((p)->bit_depth)
+#define SPA_AVBTP_PACKET_AAF_GET_SP(p) ((p)->sp)
+#define SPA_AVBTP_PACKET_AAF_GET_EVENT(p) ((p)->event)
+
+
+#endif /* SPA_AVB_PACKETS_H */
diff --git a/spa/plugins/avb/meson.build b/spa/plugins/avb/meson.build
new file mode 100644
index 0000000..2d9759e
--- /dev/null
+++ b/spa/plugins/avb/meson.build
@@ -0,0 +1,14 @@
+spa_avb_sources = ['avb.c',
+ 'avb.h',
+ 'avb-pcm-sink.c',
+ 'avb-pcm-source.c',
+ 'avb-pcm.c' ]
+
+spa_avb = shared_library(
+ 'spa-avb',
+ [ spa_avb_sources ],
+ include_directories : [configinc],
+ dependencies : [ spa_dep, mathlib, epoll_shim_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'avb'
+)
diff --git a/spa/plugins/bluez5/README-MIDI.md b/spa/plugins/bluez5/README-MIDI.md
new file mode 100644
index 0000000..45d092c
--- /dev/null
+++ b/spa/plugins/bluez5/README-MIDI.md
@@ -0,0 +1,24 @@
+## BLE MIDI & SELinux
+
+The SELinux configuration on Fedora 37 (as of 2022-11-10) does not
+permit access to the bluetoothd APIs needed for BLE MIDI.
+
+As a workaround, hopefully to be not necessary in future, you can
+permit such access by creating a file `blemidi.te` with contents:
+
+ policy_module(blemidi, 1.0);
+
+ require {
+ type system_dbusd_t;
+ type unconfined_t;
+ type bluetooth_t;
+ }
+
+ allow bluetooth_t unconfined_t:unix_stream_socket { read write };
+ allow system_dbusd_t bluetooth_t:unix_stream_socket { read write };
+
+Then having package `selinux-policy-devel` installed, running
+`make -f /usr/share/selinux/devel/Makefile blemidi.pp`, and finally
+to insert the rules via `sudo semodule -i blemidi.pp`.
+
+The policy change can be removed by `sudo semodule -r blemidi`.
diff --git a/spa/plugins/bluez5/README-OPUS-A2DP.md b/spa/plugins/bluez5/README-OPUS-A2DP.md
new file mode 100644
index 0000000..a7aefc1
--- /dev/null
+++ b/spa/plugins/bluez5/README-OPUS-A2DP.md
@@ -0,0 +1,335 @@
+---
+title: OPUS-A2DP-0.5 specification
+author: Pauli Virtanen <pav@iki.fi>
+date: Jun 4, 2022
+---
+
+# OPUS-A2DP-0.5 specification
+
+In this file, a way to use Opus as an A2DP vendor codec is specified.
+
+We will call this "OPUS-A2DP-0.5". There is no previous public
+specification for using Opus as an A2DP vendor codec (to my
+knowledge), which is why we need this one.
+
+[[_TOC_]]
+
+# Media Codec Capabilities
+
+The Media Codec Specific Information Elements ([AVDTP v1.3], §8.21.5)
+capability and configuration structure is as follows:
+
+| Octet | Bits | Meaning |
+|-------|------|-----------------------------------------------|
+| 0-5 | 0-7 | Vendor ID Part |
+| 6-7 | 0-7 | Channel Configuration |
+| 8-11 | 0-7 | Audio Location Configuration |
+| 12-14 | 0-7 | Limits Configuration |
+| 15-16 | 0-7 | Return Direction Channel Configuration |
+| 17-20 | 0-7 | Return Direction Audio Location Configuration |
+| 21-23 | 0-7 | Return Direction Limits Configuration |
+
+All integer fields and multi-byte bitfields are laid out in **little
+endian** order. All integer fields are unsigned.
+
+Each entry may have different meaning when present as a capability.
+Below, we indicate this by abbreviations CAP for capability and SEL
+for the value selected by SRC.
+
+Bits in fields marked RFA (Reserved For Additions) shall be set to
+zero.
+
+> **Note**
+>
+> See `a2dp-codec-caps.h` for definition as C structs.
+
+## Vendor ID Part
+
+The fixed value
+
+| Octet | Bits | Meaning |
+|-------|------|-------------------------------|
+| 0-3 | 0-7 | A2DP Vendor ID (0x05F1) |
+| 4-5 | 0-7 | A2DP Vendor Codec ID (0x1005) |
+
+> **Note**
+>
+> The Vendor ID is that of the Linux Foundation, and we are using it
+> here unofficially.
+
+## Channel Configuration
+
+The channel configuration consists of the channel count, and the count
+of coupled streams. The latter indicates which channels are encoded as
+left/right pairs, as defined in Sec. 5.1.1 of Opus Ogg Encapsulation [RFC7845].
+
+| Octet | Bits | Meaning |
+|-------|------|------------------------------------------------------------|
+| 6 | 0-7 | Channel Count. CAP: maximum number supported. SEL: actual. |
+| 7 | 0-7 | Coupled Stream Count. CAP: 0. SEL: actual. |
+
+The Channel Count indicates the number of logical channels encoded in
+the data stream.
+
+The Coupled Stream Count indicates the number of streams that encode a
+coupled (left & right) channel pair. The count shall satisfy
+`(Channel Count) >= 2*(Coupled Stream Count)`.
+The Stream Count is `(Channel Count) - (Coupled Stream Count)`.
+
+The logical Channels are identified by a Channel Index *j* such that `0 <= j
+< (Channel Count)`. The channels `0 <= j < 2*(Coupled Stream Count)`
+are encoded in the *k*-th stream of the payload, where `k = floor(j/2)` and
+`j mod 2` determines which of the two channels of the stream the logical
+channel is. The channels `2*(Coupled Stream Count) <= j < (Channel Count)`
+are encoded in the *k*-th stream of the payload, where `k = j - (Coupled Stream Count)`.
+
+> **Note**
+>
+> The prescription here is identical to [RFC7845] with channel mapping
+> `mapping[j] = j`. We do not want to include the mapping table in the
+> A2DP capabilities, so it is assumed to be trivial.
+
+## Audio Location Configuration
+
+The semantic meaning for each channel is determined by their Audio
+Location bitfield.
+
+| Octet | Bits | Meaning |
+|-------|------|------------------------------------------------------|
+| 8-11 | 0-7 | Audio Location bitfield. CAP: available. SEL: actual |
+
+The values specified in CAP are informative, and SEL may contain bits
+that were not set in CAP. SNK shall handle unsupported audio
+locations. It may do this for example by ignoring unsupported channels
+or via suitable up/downmixing. Hence, SRC may transmit channels with
+audio locations that are not marked supported by SNK.
+
+The audio location bit values are:
+
+| Channel Order | Bitmask | Audio Location |
+|---------------|------------|-------------------------|
+| 0 | 0x00000001 | Front Left |
+| 1 | 0x00000002 | Front Right |
+| 2 | 0x00000400 | Side Left |
+| 3 | 0x00000800 | Side Right |
+| 4 | 0x00000010 | Back Left |
+| 5 | 0x00000020 | Back Right |
+| 6 | 0x00000040 | Front Left of Center |
+| 7 | 0x00000080 | Front Right of Center |
+| 8 | 0x00001000 | Top Front Left |
+| 9 | 0x00002000 | Top Front Right |
+| 10 | 0x00040000 | Top Side Left |
+| 11 | 0x00080000 | Top Side Right |
+| 12 | 0x00010000 | Top Back Left |
+| 13 | 0x00020000 | Top Back Right |
+| 14 | 0x00400000 | Bottom Front Left |
+| 15 | 0x00800000 | Bottom Front Right |
+| 16 | 0x01000000 | Front Left Wide |
+| 17 | 0x02000000 | Front Right Wide |
+| 18 | 0x04000000 | Left Surround |
+| 19 | 0x08000000 | Right Surround |
+| 20 | 0x00000004 | Front Center |
+| 21 | 0x00000100 | Back Center |
+| 22 | 0x00004000 | Top Front Center |
+| 23 | 0x00008000 | Top Center |
+| 24 | 0x00100000 | Top Back Center |
+| 25 | 0x00200000 | Bottom Front Center |
+| 26 | 0x00000008 | Low Frequency Effects 1 |
+| 27 | 0x00000200 | Low Frequency Effects 2 |
+| 28 | 0x10000000 | RFA |
+| 29 | 0x20000000 | RFA |
+| 30 | 0x40000000 | RFA |
+| 31 | 0x80000000 | RFA |
+
+Each bit value is associated with a Channel Order. The bits set in
+the bitfield define audio locations for the streams present in the
+payload. The set bit with the smallest Channel Order value defines the
+audio location for the Channel Index *j=0*, the bit with the next
+lowest Channel Order value defines the audio location for the Channel
+Index *j=1*, and so forth.
+
+When the Channel Count is larger than the number of bits set in the
+Audio Location bitfield, the audio locations of the remaining channels
+are unspecified. Implementations may handle them as appropriate for
+their use case, considering them as AUX0–AUXN, or in the case of
+Channel Count = 1, as the single mono audio channel.
+
+When the Channel Count is smaller than the number of bits set in the
+Audio Location bitfield, the audio locations for the channels are
+assigned as above, and remaining excess bits shall be ignored.
+
+> **Note**
+>
+> The channel audio location specification is similar to the location
+> bitfield of the `Audio_Channel_Allocation` LTV structure in Bluetooth
+> SIG [Assigned Numbers, Generic Audio] used in the LE Audio, and the
+> bitmasks defined above are the same.
+>
+> The channel ordering differs from LE Audio, and is defined here to be
+> compatible with the internal stream ordering in the reference Opus
+> Multistream surround encoder Mapping Family 0 and 1 output. This
+> allows making use of its surround masking and LFE handling
+> capabilities. The stream ordering of the reference Opus surround
+> encoder, although being unchanged since its addition in 2013, is an
+> internal detail of the encoder. Implementations using the surround
+> encoder need to check that the mapping table used by the encoder
+> corresponds to the above channel ordering.
+>
+> For reference, we list the Audio Location bitfield values
+> corresponding to the different channel counts in Opus Mapping Family 0
+> and 1 surround encoder output, and the expected mapping table:
+>
+> | Mapping Family | Channel Count | Audio Location Value | Stream Ordering | Mapping Table |
+> |----------------|---------------|----------------------|---------------------------------|--------------------------|
+> | 0 | 1 | 0x00000000 | mono | {0} |
+> | 0 | 2 | 0x00000003 | FL, FR | {0, 1} |
+> | 1 | 1 | 0x00000000 | mono | {0} |
+> | 1 | 2 | 0x00000003 | FL, FR | {0, 1} |
+> | 1 | 3 | 0x00000007 | FL, FR, FC | {0, 2, 1} |
+> | 1 | 4 | 0x00000033 | FL, FR, BL, BR | {0, 1, 2, 3} |
+> | 1 | 5 | 0x00000037 | FL, FR, BL, BR, FC | {0, 4, 1, 2, 3} |
+> | 1 | 6 | 0x0000003f | FL, FR, BL, BR, FC, LFE | {0, 4, 1, 2, 3, 5} |
+> | 1 | 7 | 0x00000d0f | FL, FR, SL, SR, FC, BC, LFE | {0, 4, 1, 2, 3, 5, 6} |
+> | 1 | 8 | 0x00000c3f | FL, FR, SL, SR, BL, BR, FC, LFE | {0, 6, 1, 2, 3, 4, 5, 7} |
+>
+> The Mapping Table in the table indicates the mapping table selected by
+> `opus_multistream_surround_encoder_create` (Opus 1.3.1). If the
+> encoder outputs a different mapping table in a future Opus encoder
+> release, the channel ordering will be incorrect, and the surround
+> encoder can not be used. We expect that the probability of the Opus
+> encoder authors making such changes is negligible.
+
+## Limits Configuration
+
+The limits for allowed frame durations and maximum bitrate can also be
+configured.
+
+| Octet | Bits | Meaning |
+|-------|------|-----------------------------------------------------|
+| 16 | 0 | Frame duration 2.5ms. CAP: supported, SEL: selected |
+| 16 | 1 | Frame duration 5ms. CAP: supported, SEL: selected |
+| 16 | 2 | Frame duration 10ms. CAP: supported, SEL: selected |
+| 16 | 3 | Frame duration 20ms. CAP: supported, SEL: selected |
+| 16 | 4 | Frame duration 40ms. CAP: supported, SEL: selected |
+| 16 | 5-7 | RFA |
+
+| Octet | Bits | Meaning |
+|-------|------|------------------------------------------------|
+| 17-18 | 0-7 | Maximum bitrate. CAP: supported, SEL: selected |
+
+The maximum bitrate is given in units of 1024 bits per second.
+
+The maximum bitrate field in CAP may contain value 0 to indicate
+everything is supported.
+
+## Bidirectional Audio Configuration
+
+Bidirectional audio may be supported. Its Channel Configuration, Audio
+Location Configuration, and Limits Configuration have identical form
+to the forward direction, and represented by exactly similar
+structures.
+
+Namely:
+
+| Octet | Bits | Meaning |
+|-------|------|----------------------------------------------------|
+| 19-20 | 0-7 | Channel Configuration fields, for return direction |
+| 21-28 | 0-7 | Audio Location fields, for return direction |
+| 29-31 | 0-7 | Limits Configuration fields, for return direction |
+
+If no return channel is supported or selected, the number of channels
+is set to 0 in CAP or SEL.
+
+> **Note**
+>
+> This is a nonstandard extension to A2DP. The return direction audio
+> data is simply sent back via the underlying L2CAP connection, which
+> is bidirectional, in the same format as the forward direction audio.
+> This is similar to what aptX-LL and FastStream do.
+
+# Packet Structure
+
+Each packet consists of an RTP header, an RTP payload header, and a
+payload containing Opus Multistream data.
+
+| Octet | Bits | Meaning |
+|-------|------|--------------------------|
+| 0-11 | 0-7 | RTP header |
+| 12 | 0-7 | RTP payload header |
+| 13-N | 0-7 | Opus Multistream payload |
+
+For each Bluetooth packet, the payload shall contain exactly one Opus
+Multistream packet, or a fragment of one. The Opus Multistream packet
+may be fragmented to several consecutive Bluetooth packets.
+
+The format of the Multistream data is the same as in the audio packets
+of [RFC7845], or, as produced/consumed by the Opus Multistream API.
+
+> **Note**
+>
+> We DO NOT follow [RFC7587], as we want fragmentation and multichannel support.
+
+## RTP Header
+
+See [RFC3550].
+
+The RTP payload type is pt=96 (dynamic).
+
+## RTP Payload Header
+
+The RTP payload header is used to indicate if and how the Opus
+Multistream packet is fragmented across several consecutive Bluetooth
+packets.
+
+| Octet | Bits | Meaning
+|--------|------|--------------------------------------------------------
+| 0 | 0-3 | Frame Count
+| 4 | 4 | RFA
+| 4 | 5 | Is Last Fragment
+| 4 | 6 | Is First Fragment
+| 4 | 7 | Is Fragmented
+
+In each packet, Frame Count indicates how many Bluetooth packets are
+still to be received (including the present packet) before the Opus
+Multistream packet is complete.
+
+The Is Fragment flag indicates whether the present packet contains
+fragmented payload.
+
+The Is Last Fragment flag indicates whether the present packet is the
+last part of fragmented payload.
+
+The Is First Fragment flag indicates whether the present packet is the
+first part of fragmented payload.
+
+In non-fragmented packets, Frame Count shall be (1), and the other bits
+in the header zero.
+
+## Opus Payload
+
+The Opus payload is a single Opus Multistream packet, or its fragment.
+
+In case of fragmentation, as indicated by the RTP payload header,
+concatenating the payloads of the fragment Bluetooth packets shall
+yield the total Opus Multistream packet.
+
+The SRC should choose encoder parameters such that Bluetooth bandwidth
+limitations are not exceeded.
+
+The SRC may include FEC data. The SNK may enable forward error
+correction instead of PLC.
+
+
+# References
+
+1. Bluetooth [AVDTP v1.3]
+2. IETF [RFC3550]
+3. IETF [RFC7587]
+4. IETF [RFC7845]
+5. Bluetooth [Assigned Numbers, Generic Audio]
+
+[AVDTP v1.3]: https://www.bluetooth.com/specifications/specs/a-v-distribution-transport-protocol-1-3/
+[RFC3550]: https://datatracker.ietf.org/doc/html/rfc3550
+[RFC7587]: https://datatracker.ietf.org/doc/html/rfc7587
+[RFC7845]: https://datatracker.ietf.org/doc/html/rfc7845
+[Assigned Numbers, Generic Audio]: https://www.bluetooth.com/specifications/assigned-numbers/
diff --git a/spa/plugins/bluez5/README-SBC-XQ.md b/spa/plugins/bluez5/README-SBC-XQ.md
new file mode 100644
index 0000000..3b393aa
--- /dev/null
+++ b/spa/plugins/bluez5/README-SBC-XQ.md
@@ -0,0 +1,54 @@
+## SBC XQ
+
+SBC XQ is standard SBC codec operating at high bitrates and thus reaching the
+transparent audio transport quality of AptX (HD) or other proprietary codecs.
+
+A2DP specification (A2DP SPEC) defines SBC parameters. These parameters are
+negotiated between the source (SRC) and the receiver (SNK) at connection time :
+
+- Audio channel mode : Joint Stereo, Stereo, Dual Channel, Mono : all modes
+ are MANDATORY for the SNK according to A2DP specification
+- Number of subbands: 4 or 8 - both MANDATORY for the SNK implementation
+- Blocks Length: 4, 8, 12, 16 - all MANDATORY for the SNK implementation
+- Allocation Method: Loudness, SNR - both MANDATORY for the SNK implementation
+- Maximum and minimum bit pool : between 2 to 250, expressed in 8 bit uint
+ (Unsigned integer, Most significant bit first) :
+ - A2DP spec v1.2 states that requires all SNK implementation shall handle
+ bitrates of up to 512 kbps (which correspond to bitpool = 76).
+ - A2DP spec v1.3 doesn't specify any bitrate limit, and some high-end SNK
+ devices announce bitpool between 62 and 94 (bitpool 94 = 551kbps bitrate).
+
+Bluetooth standard radio capabilities are as follow :
+
+| Bluetooth speed EDR | EDR 2Mbps | | EDR 3Mbps |
+|-------------------------|-----------|-------|-----------|
+| Speed (b/s) | 2097152 | | 3145728 |
+| Radio slot length (s) | 0.000625 | | 0.000625 |
+| Radio slots / s | 1600 | | 1600 |
+| Slot size (B) | 163.84 | | 245.76 |
+| Max payload/5 slots (B) | 676.2 | | 1085.8 |
+| max bitrate (Kb/s) | 1408.75 | | 2262.08 |
+
+The A2DP specification V1.3 provides RECOMMENDATIONS for bitpool implementation
+for the encoder of the SRC : it is required to support AT LEAST the following
+settings :
+
+- STEREO MODE : 53
+- MONO MODE : 31
+- DUAL CHANNEL : unspecified, so let's assume that the MONO value can be used : 3
+
+According to http://soundexpert.org/articles/-/blogs/audio-quality-of-sbc-xq-bluetooth-audio-codec ,
+AptX quality can be reached either :
+
+- in STEREO MODE, with bitpool ~ 76
+- in DUAL CHANNEL MODE, with bitpool ~ 38 per channel
+
+| sampling Freq (Hz) | 44100 | 48000 |
+|-------------------------|-----------|-------|
+| bitpool / channel | 38 | 35 |
+| Frame length DUAL (B) | 164 | 152 |
+| Frame length JST (B) | 165 | 153 |
+| Frame length ST (B) | 164 | 152 |
+| bitrate DUAL CH (kb/s) | 452 | 456 |
+| bitrate JOINT ST (kb/s) | 454 | 459 |
+| bitrate STEREO (kb/s) | 452 | 456 |
diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c
new file mode 100644
index 0000000..46a8740
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-aac.c
@@ -0,0 +1,661 @@
+/* Spa A2DP AAC codec
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/utils/dict.h>
+
+#include <fdk-aac/aacenc_lib.h>
+#include <fdk-aac/aacdecoder_lib.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+static struct spa_log *log;
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.aac");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define DEFAULT_AAC_BITRATE 320000
+#define MIN_AAC_BITRATE 64000
+
+struct props {
+ int bitratemode;
+};
+
+struct impl {
+ HANDLE_AACENCODER aacenc;
+ HANDLE_AACDECODER aacdec;
+
+ struct rtp_header *header;
+
+ size_t mtu;
+ int codesize;
+
+ int max_bitrate;
+ int cur_bitrate;
+
+ uint32_t rate;
+ uint32_t channels;
+ int samplesize;
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ static const a2dp_aac_t a2dp_aac = {
+ .object_type =
+ /* NOTE: AAC Long Term Prediction and AAC Scalable are
+ * not supported by the FDK-AAC library. */
+ AAC_OBJECT_TYPE_MPEG2_AAC_LC |
+ AAC_OBJECT_TYPE_MPEG4_AAC_LC,
+ AAC_INIT_FREQUENCY(
+ AAC_SAMPLING_FREQ_8000 |
+ AAC_SAMPLING_FREQ_11025 |
+ AAC_SAMPLING_FREQ_12000 |
+ AAC_SAMPLING_FREQ_16000 |
+ AAC_SAMPLING_FREQ_22050 |
+ AAC_SAMPLING_FREQ_24000 |
+ AAC_SAMPLING_FREQ_32000 |
+ AAC_SAMPLING_FREQ_44100 |
+ AAC_SAMPLING_FREQ_48000 |
+ AAC_SAMPLING_FREQ_64000 |
+ AAC_SAMPLING_FREQ_88200 |
+ AAC_SAMPLING_FREQ_96000)
+ .channels =
+ AAC_CHANNELS_1 |
+ AAC_CHANNELS_2,
+ .vbr = 1,
+ AAC_INIT_BITRATE(DEFAULT_AAC_BITRATE)
+ };
+
+ memcpy(caps, &a2dp_aac, sizeof(a2dp_aac));
+ return sizeof(a2dp_aac);
+}
+
+static const struct media_codec_config
+aac_frequencies[] = {
+ { AAC_SAMPLING_FREQ_48000, 48000, 11 },
+ { AAC_SAMPLING_FREQ_44100, 44100, 10 },
+ { AAC_SAMPLING_FREQ_96000, 96000, 9 },
+ { AAC_SAMPLING_FREQ_88200, 88200, 8 },
+ { AAC_SAMPLING_FREQ_64000, 64000, 7 },
+ { AAC_SAMPLING_FREQ_32000, 32000, 6 },
+ { AAC_SAMPLING_FREQ_24000, 24000, 5 },
+ { AAC_SAMPLING_FREQ_22050, 22050, 4 },
+ { AAC_SAMPLING_FREQ_16000, 16000, 3 },
+ { AAC_SAMPLING_FREQ_12000, 12000, 2 },
+ { AAC_SAMPLING_FREQ_11025, 11025, 1 },
+ { AAC_SAMPLING_FREQ_8000, 8000, 0 },
+};
+
+static const struct media_codec_config
+aac_channel_modes[] = {
+ { AAC_CHANNELS_2, 2, 1 },
+ { AAC_CHANNELS_1, 1, 0 },
+};
+
+static int get_valid_aac_bitrate(a2dp_aac_t *conf)
+{
+ if (AAC_GET_BITRATE(*conf) < MIN_AAC_BITRATE) {
+ /* Unknown (0) or bogus bitrate */
+ return DEFAULT_AAC_BITRATE;
+ } else {
+ return SPA_MIN(AAC_GET_BITRATE(*conf), DEFAULT_AAC_BITRATE);
+ }
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_aac_t conf;
+ int i;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ conf = *(a2dp_aac_t*)caps;
+
+ if (conf.object_type & AAC_OBJECT_TYPE_MPEG2_AAC_LC)
+ conf.object_type = AAC_OBJECT_TYPE_MPEG2_AAC_LC;
+ else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC)
+ conf.object_type = AAC_OBJECT_TYPE_MPEG4_AAC_LC;
+ else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LTP)
+ return -ENOTSUP; /* Not supported by FDK-AAC */
+ else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_SCA)
+ return -ENOTSUP; /* Not supported by FDK-AAC */
+ else
+ return -ENOTSUP;
+
+ if ((i = media_codec_select_config(aac_frequencies,
+ SPA_N_ELEMENTS(aac_frequencies),
+ AAC_GET_FREQUENCY(conf),
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ AAC_SET_FREQUENCY(conf, aac_frequencies[i].config);
+
+ if ((i = media_codec_select_config(aac_channel_modes,
+ SPA_N_ELEMENTS(aac_channel_modes),
+ conf.channels,
+ info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
+ )) < 0)
+ return -ENOTSUP;
+ conf.channels = aac_channel_modes[i].config;
+
+ AAC_SET_BITRATE(conf, get_valid_aac_bitrate(&conf));
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+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)
+{
+ a2dp_aac_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ 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_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) {
+ if (AAC_GET_FREQUENCY(conf) & f->config) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, f->value);
+ spa_pod_builder_int(b, f->value);
+ }
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+
+ if (SPA_FLAG_IS_SET(conf.channels, AAC_CHANNELS_1 | AAC_CHANNELS_2)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if (conf.channels & AAC_CHANNELS_1) {
+ 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);
+ } else if (conf.channels & AAC_CHANNELS_2) {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ } else
+ return -EINVAL;
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ a2dp_aac_t conf;
+ size_t j;
+
+ if (caps == NULL || caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S16;
+
+ /*
+ * A2DP v1.3.2, 4.5.2: only one bit shall be set in bitfields.
+ * However, there is a report (#1342) of device setting multiple
+ * bits for AAC object type. It's not clear if this was due to
+ * a BlueZ bug, but we can be lax here and below in codec_init.
+ */
+ if (!(conf.object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC |
+ AAC_OBJECT_TYPE_MPEG4_AAC_LC)))
+ return -EINVAL;
+ j = 0;
+ SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) {
+ if (AAC_GET_FREQUENCY(conf) & f->config) {
+ info->info.raw.rate = f->value;
+ j++;
+ break;
+ }
+ }
+ if (j == 0)
+ return -EINVAL;
+
+ if (conf.channels & AAC_CHANNELS_2) {
+ info->info.raw.channels = 2;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ } else if (conf.channels & AAC_CHANNELS_1) {
+ info->info.raw.channels = 1;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ } else {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
+{
+ struct props *p = calloc(1, sizeof(struct props));
+ const char *str;
+
+ if (p == NULL)
+ return NULL;
+
+ if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.aac.bitratemode")) == NULL)
+ str = "0";
+
+ p->bitratemode = SPA_CLAMP(atoi(str), 0, 5);
+ return p;
+}
+
+static void codec_clear_props(void *props)
+{
+ free(props);
+}
+
+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;
+ a2dp_aac_t *conf = config;
+ struct props *p = props;
+ UINT bitratemode;
+ int res;
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL) {
+ res = -errno;
+ goto error;
+ }
+ this->mtu = mtu;
+ this->rate = info->info.raw.rate;
+ this->channels = info->info.raw.channels;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16) {
+ res = -EINVAL;
+ goto error;
+ }
+ this->samplesize = 2;
+
+ bitratemode = p ? p->bitratemode : 0;
+
+ res = aacEncOpen(&this->aacenc, 0, this->channels);
+ if (res != AACENC_OK)
+ goto error;
+
+ if (!(conf->object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC |
+ AAC_OBJECT_TYPE_MPEG4_AAC_LC))) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_SAMPLERATE, this->rate);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_CHANNELMODE, this->channels);
+ if (res != AACENC_OK)
+ goto error;
+
+ if (conf->vbr) {
+ res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATEMODE,
+ bitratemode);
+ if (res != AACENC_OK)
+ goto error;
+ }
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_AUDIOMUXVER, 2);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_SIGNALING_MODE, 1);
+ if (res != AACENC_OK)
+ goto error;
+
+ // Fragmentation is not implemented yet,
+ // so make sure every encoded AAC frame fits in (mtu - header)
+ this->max_bitrate = ((this->mtu - sizeof(struct rtp_header)) * 8 * this->rate) / 1024;
+ this->max_bitrate = SPA_MIN(this->max_bitrate, get_valid_aac_bitrate(conf));
+ this->cur_bitrate = this->max_bitrate;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATE, this->cur_bitrate);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_PEAK_BITRATE, this->max_bitrate);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_TRANSMUX, TT_MP4_LATM_MCP1);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_HEADER_PERIOD, 1);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_AFTERBURNER, 1);
+ if (res != AACENC_OK)
+ goto error;
+
+ res = aacEncEncode(this->aacenc, NULL, NULL, NULL, NULL);
+ if (res != AACENC_OK)
+ goto error;
+
+ AACENC_InfoStruct enc_info = {};
+ res = aacEncInfo(this->aacenc, &enc_info);
+ if (res != AACENC_OK)
+ goto error;
+
+ this->codesize = enc_info.frameLength * this->channels * this->samplesize;
+
+ this->aacdec = aacDecoder_Open(TT_MP4_LATM_MCP1, 1);
+ if (!this->aacdec) {
+ res = -EINVAL;
+ goto error;
+ }
+
+#ifdef AACDECODER_LIB_VL0
+ res = aacDecoder_SetParam(this->aacdec, AAC_PCM_MIN_OUTPUT_CHANNELS, this->channels);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "Couldn't set min output channels: 0x%04X", res);
+ goto error;
+ }
+
+ res = aacDecoder_SetParam(this->aacdec, AAC_PCM_MAX_OUTPUT_CHANNELS, this->channels);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "Couldn't set max output channels: 0x%04X", res);
+ goto error;
+ }
+#else
+ res = aacDecoder_SetParam(this->aacdec, AAC_PCM_OUTPUT_CHANNELS, this->channels);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "Couldn't set output channels: 0x%04X", res);
+ goto error;
+ }
+#endif
+
+ return this;
+
+error:
+ if (this && this->aacenc)
+ aacEncClose(&this->aacenc);
+ if (this && this->aacdec)
+ aacDecoder_Close(this->aacdec);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ if (this->aacenc)
+ aacEncClose(&this->aacenc);
+ if (this->aacdec)
+ aacDecoder_Close(this->aacdec);
+ free(this);
+}
+
+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)
+{
+ struct impl *this = data;
+
+ this->header = (struct rtp_header *)dst;
+ memset(this->header, 0, sizeof(struct rtp_header));
+
+ this->header->v = 2;
+ this->header->pt = 96;
+ this->header->sequence_number = htons(seqnum);
+ this->header->timestamp = htonl(timestamp);
+ this->header->ssrc = htonl(1);
+ return sizeof(struct rtp_header);
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int res;
+
+ void *in_bufs[] = {(void *) src};
+ int in_buf_ids[] = {IN_AUDIO_DATA};
+ int in_buf_sizes[] = {src_size};
+ int in_buf_el_sizes[] = {this->samplesize};
+ AACENC_BufDesc in_buf_desc = {
+ .numBufs = 1,
+ .bufs = in_bufs,
+ .bufferIdentifiers = in_buf_ids,
+ .bufSizes = in_buf_sizes,
+ .bufElSizes = in_buf_el_sizes,
+ };
+ AACENC_InArgs in_args = {
+ .numInSamples = src_size / this->samplesize,
+ };
+
+ void *out_bufs[] = {dst};
+ int out_buf_ids[] = {OUT_BITSTREAM_DATA};
+ int out_buf_sizes[] = {dst_size};
+ int out_buf_el_sizes[] = {this->samplesize};
+ AACENC_BufDesc out_buf_desc = {
+ .numBufs = 1,
+ .bufs = out_bufs,
+ .bufferIdentifiers = out_buf_ids,
+ .bufSizes = out_buf_sizes,
+ .bufElSizes = out_buf_el_sizes,
+ };
+ AACENC_OutArgs out_args = {};
+
+ res = aacEncEncode(this->aacenc, &in_buf_desc, &out_buf_desc, &in_args, &out_args);
+ if (res != AACENC_OK)
+ return -EINVAL;
+
+ *dst_out = out_args.numOutBytes;
+ *need_flush = NEED_FLUSH_ALL;
+
+ /* RFC6416: It is set to 1 to indicate that the RTP packet contains a complete
+ * audioMuxElement or the last fragment of an audioMuxElement */
+ this->header->m = 1;
+
+ return out_args.numInSamples * this->samplesize;
+}
+
+static int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ const struct rtp_header *header = src;
+ size_t header_size = sizeof(struct rtp_header);
+
+ spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+
+ return header_size;
+}
+
+static int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ uint data_size = (uint)src_size;
+ uint bytes_valid = data_size;
+ CStreamInfo *aacinf;
+ int res;
+
+ res = aacDecoder_Fill(this->aacdec, (UCHAR **)&src, &data_size, &bytes_valid);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "AAC buffer fill error: 0x%04X", res);
+ return -EINVAL;
+ }
+
+ res = aacDecoder_DecodeFrame(this->aacdec, dst, dst_size, 0);
+ if (res != AAC_DEC_OK) {
+ spa_log_debug(log, "AAC decode frame error: 0x%04X", res);
+ return -EINVAL;
+ }
+
+ aacinf = aacDecoder_GetStreamInfo(this->aacdec);
+ if (!aacinf) {
+ spa_log_debug(log, "AAC get stream info failed");
+ return -EINVAL;
+ }
+ *dst_out = aacinf->frameSize * aacinf->numChannels * this->samplesize;
+
+ return src_size - bytes_valid;
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_change_bitrate(struct impl *this, int new_bitrate)
+{
+ int res;
+
+ new_bitrate = SPA_MIN(new_bitrate, this->max_bitrate);
+ new_bitrate = SPA_MAX(new_bitrate, 64000);
+
+ if (new_bitrate == this->cur_bitrate)
+ return 0;
+
+ this->cur_bitrate = new_bitrate;
+
+ res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATE, this->cur_bitrate);
+ if (res != AACENC_OK)
+ return -EINVAL;
+
+ return this->cur_bitrate;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ struct impl *this = data;
+ return codec_change_bitrate(this, (this->cur_bitrate * 2) / 3);
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ struct impl *this = data;
+ return codec_change_bitrate(this, (this->cur_bitrate * 4) / 3);
+}
+
+static void codec_set_log(struct spa_log *global_log)
+{
+ log = global_log;
+ spa_log_topic_init(log, &log_topic);
+}
+
+const struct media_codec a2dp_codec_aac = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_AAC,
+ .codec_id = A2DP_CODEC_MPEG24,
+ .name = "aac",
+ .description = "AAC",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .init_props = codec_init_props,
+ .clear_props = codec_clear_props,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .abr_process = codec_abr_process,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+ .set_log = codec_set_log,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "aac",
+ &a2dp_codec_aac
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-aptx.c b/spa/plugins/bluez5/a2dp-codec-aptx.c
new file mode 100644
index 0000000..6938e47
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-aptx.c
@@ -0,0 +1,748 @@
+/* Spa A2DP aptX codec
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <sbc/sbc.h>
+
+#include <freeaptx.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+#define APTX_LL_LEVEL1(level) (((level) >> 8) & 0xFF)
+#define APTX_LL_LEVEL2(level) (((level) >> 0) & 0xFF)
+#define APTX_LL_LEVEL(level1, level2) ((((level1) & 0xFF) << 8) | (((level2) & 0xFF) << 0))
+
+#define MSBC_DECODED_SIZE 240
+#define MSBC_ENCODED_SIZE 60
+#define MSBC_PAYLOAD_SIZE 57
+
+/*
+ * XXX: Bump requested device buffer levels up by 50% from defaults,
+ * XXX: increasing latency similarly. This seems to be necessary for
+ * XXX: stable output when moving headphones. It might be possible to
+ * XXX: reduce this by changing the scheduling of the socket writes.
+ */
+#define LL_LEVEL_ADJUSTMENT 3/2
+
+struct impl {
+ struct aptx_context *aptx;
+
+ struct rtp_header *header;
+
+ size_t mtu;
+ int codesize;
+ int frame_length;
+ int frame_count;
+ int max_frames;
+
+ bool hd;
+};
+
+struct msbc_impl {
+ sbc_t msbc;
+};
+
+static inline bool codec_is_hd(const struct media_codec *codec)
+{
+ return codec->vendor.codec_id == APTX_HD_CODEC_ID
+ && codec->vendor.vendor_id == APTX_HD_VENDOR_ID;
+}
+
+static inline bool codec_is_ll(const struct media_codec *codec)
+{
+ return (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL) ||
+ (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX);
+}
+
+static inline size_t codec_get_caps_size(const struct media_codec *codec)
+{
+ if (codec_is_hd(codec))
+ return sizeof(a2dp_aptx_hd_t);
+ else if (codec_is_ll(codec))
+ return sizeof(a2dp_aptx_ll_t);
+ else
+ return sizeof(a2dp_aptx_t);
+}
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ size_t actual_conf_size = codec_get_caps_size(codec);
+ const a2dp_aptx_t a2dp_aptx = {
+ .info = codec->vendor,
+ .frequency =
+ APTX_SAMPLING_FREQ_16000 |
+ APTX_SAMPLING_FREQ_32000 |
+ APTX_SAMPLING_FREQ_44100 |
+ APTX_SAMPLING_FREQ_48000,
+ .channel_mode =
+ APTX_CHANNEL_MODE_STEREO,
+ };
+ const a2dp_aptx_ll_t a2dp_aptx_ll = {
+ .aptx = a2dp_aptx,
+ .bidirect_link = codec->duplex_codec ? true : false,
+ .has_new_caps = false,
+ };
+ if (codec_is_ll(codec))
+ memcpy(caps, &a2dp_aptx_ll, sizeof(a2dp_aptx_ll));
+ else
+ memcpy(caps, &a2dp_aptx, sizeof(a2dp_aptx));
+ return actual_conf_size;
+}
+
+static const struct media_codec_config
+aptx_frequencies[] = {
+ { APTX_SAMPLING_FREQ_48000, 48000, 3 },
+ { APTX_SAMPLING_FREQ_44100, 44100, 2 },
+ { APTX_SAMPLING_FREQ_32000, 32000, 1 },
+ { APTX_SAMPLING_FREQ_16000, 16000, 0 },
+};
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_aptx_t conf;
+ int i;
+ size_t actual_conf_size = codec_get_caps_size(codec);
+
+ if (caps_size < sizeof(conf) || actual_conf_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ if ((i = media_codec_select_config(aptx_frequencies,
+ SPA_N_ELEMENTS(aptx_frequencies),
+ conf.frequency,
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ conf.frequency = aptx_frequencies[i].config;
+
+ if (conf.channel_mode & APTX_CHANNEL_MODE_STEREO)
+ conf.channel_mode = APTX_CHANNEL_MODE_STEREO;
+ else
+ return -ENOTSUP;
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return actual_conf_size;
+}
+
+static int codec_select_config_ll(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_aptx_ll_ext_t conf = { 0 };
+ size_t actual_conf_size;
+ int res;
+
+ /* caps may contain only conf.base, or also the extended attributes */
+
+ if (caps_size < sizeof(conf.base))
+ return -EINVAL;
+
+ memcpy(&conf, caps, SPA_MIN(caps_size, sizeof(conf)));
+
+ actual_conf_size = conf.base.has_new_caps ? sizeof(conf) : sizeof(conf.base);
+ if (caps_size < actual_conf_size)
+ return -EINVAL;
+
+ if (codec->duplex_codec && !conf.base.bidirect_link)
+ return -ENOTSUP;
+
+ if ((res = codec_select_config(codec, flags, caps, caps_size, info, settings, config)) < 0)
+ return res;
+
+ memcpy(&conf.base.aptx, config, sizeof(conf.base.aptx));
+
+ if (conf.base.has_new_caps) {
+ int target_level = APTX_LL_LEVEL(conf.target_level1, conf.target_level2);
+ int initial_level = APTX_LL_LEVEL(conf.initial_level1, conf.initial_level2);
+ int good_working_level = APTX_LL_LEVEL(conf.good_working_level1, conf.good_working_level2);
+
+ target_level = SPA_MAX(target_level, APTX_LL_TARGET_CODEC_LEVEL * LL_LEVEL_ADJUSTMENT);
+ initial_level = SPA_MAX(initial_level, APTX_LL_INITIAL_CODEC_LEVEL * LL_LEVEL_ADJUSTMENT);
+ good_working_level = SPA_MAX(good_working_level, APTX_LL_GOOD_WORKING_LEVEL * LL_LEVEL_ADJUSTMENT);
+
+ conf.target_level1 = APTX_LL_LEVEL1(target_level);
+ conf.target_level2 = APTX_LL_LEVEL2(target_level);
+ conf.initial_level1 = APTX_LL_LEVEL1(initial_level);
+ conf.initial_level2 = APTX_LL_LEVEL2(initial_level);
+ conf.good_working_level1 = APTX_LL_LEVEL1(good_working_level);
+ conf.good_working_level2 = APTX_LL_LEVEL2(good_working_level);
+
+ if (conf.sra_max_rate == 0)
+ conf.sra_max_rate = APTX_LL_SRA_MAX_RATE;
+ if (conf.sra_avg_time == 0)
+ conf.sra_avg_time = APTX_LL_SRA_AVG_TIME;
+ }
+
+ memcpy(config, &conf, actual_conf_size);
+
+ return actual_conf_size;
+}
+
+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)
+{
+ a2dp_aptx_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.frequency & APTX_SAMPLING_FREQ_48000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.frequency & APTX_SAMPLING_FREQ_44100) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 44100);
+ spa_pod_builder_int(b, 44100);
+ }
+ if (conf.frequency & APTX_SAMPLING_FREQ_32000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 32000);
+ spa_pod_builder_int(b, 32000);
+ }
+ if (conf.frequency & APTX_SAMPLING_FREQ_16000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 16000);
+ spa_pod_builder_int(b, 16000);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (SPA_FLAG_IS_SET(conf.channel_mode, APTX_CHANNEL_MODE_MONO | APTX_CHANNEL_MODE_STEREO)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if (conf.channel_mode & APTX_CHANNEL_MODE_MONO) {
+ 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);
+ } else if (conf.channel_mode & APTX_CHANNEL_MODE_STEREO) {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ } else
+ return -EINVAL;
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+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;
+ int res;
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ this->hd = codec_is_hd(codec);
+
+ if ((this->aptx = aptx_init(this->hd)) == NULL)
+ goto error_errno;
+
+ this->mtu = mtu;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S24) {
+ res = -EINVAL;
+ goto error;
+ }
+ this->frame_length = this->hd ? 6 : 4;
+ this->codesize = 4 * 3 * 2;
+
+ if (this->hd)
+ this->max_frames = (this->mtu - sizeof(struct rtp_header)) / this->frame_length;
+ else if (codec_is_ll(codec))
+ this->max_frames = SPA_MIN(256u, this->mtu) / this->frame_length;
+ else
+ this->max_frames = this->mtu / this->frame_length;
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+error:
+ if (this->aptx)
+ aptx_finish(this->aptx);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ aptx_finish(this->aptx);
+ free(this);
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+
+ this->frame_count = 0;
+
+ if (!this->hd)
+ return 0;
+
+ this->header = (struct rtp_header *)dst;
+ memset(this->header, 0, sizeof(struct rtp_header));
+
+ this->header->v = 2;
+ this->header->pt = 96;
+ this->header->sequence_number = htons(seqnum);
+ this->header->timestamp = htonl(timestamp);
+ return sizeof(struct rtp_header);
+}
+
+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 avail_dst_size;
+ int res;
+
+ avail_dst_size = (this->max_frames - this->frame_count) * this->frame_length;
+ if (SPA_UNLIKELY(dst_size < avail_dst_size)) {
+ *need_flush = NEED_FLUSH_ALL;
+ return 0;
+ }
+
+ res = aptx_encode(this->aptx, src, src_size,
+ dst, avail_dst_size, dst_out);
+ if(SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+
+ this->frame_count += *dst_out / this->frame_length;
+ *need_flush = (this->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
+ return res;
+}
+
+static int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ struct impl *this = data;
+
+ if (!this->hd)
+ return 0;
+
+ const struct rtp_header *header = src;
+ size_t header_size = sizeof(struct rtp_header);
+
+ spa_return_val_if_fail(src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+ return header_size;
+}
+
+static int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ int res;
+
+ res = aptx_decode(this->aptx, src, src_size,
+ dst, dst_size, dst_out);
+
+ return res;
+}
+
+/*
+ * mSBC duplex codec
+ *
+ * When connected as SRC to SNK, aptX-LL sink may send back mSBC data.
+ */
+
+static int msbc_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ struct spa_audio_info_raw info = { 0, };
+
+ if (caps_size < sizeof(a2dp_aptx_ll_t))
+ return -EINVAL;
+
+ if (idx > 0)
+ return 0;
+
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ info.channels = 1;
+ info.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ info.rate = 16000;
+
+ *param = spa_format_audio_raw_build(b, id, &info);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int msbc_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE;
+ info->info.raw.channels = 1;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ info->info.raw.rate = 16000;
+ return 0;
+}
+
+static int msbc_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_get_block_size(void *data)
+{
+ return MSBC_DECODED_SIZE;
+}
+
+static void *msbc_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ struct msbc_impl *this = NULL;
+ int res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16_LE) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct msbc_impl))) == NULL)
+ goto error_errno;
+
+ if ((res = sbc_init_msbc(&this->msbc, 0)) < 0)
+ goto error;
+
+ this->msbc.endian = SBC_LE;
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+error:
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void msbc_deinit(void *data)
+{
+ struct msbc_impl *this = data;
+ sbc_finish(&this->msbc);
+ free(this);
+}
+
+static int msbc_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ return -ENOTSUP;
+}
+
+static int msbc_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ return 0;
+}
+
+static int msbc_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct msbc_impl *this = data;
+ const uint8_t sync[3] = { 0xAD, 0x00, 0x00 };
+ size_t processed = 0;
+ int res;
+
+ spa_assert(sizeof(sync) <= MSBC_PAYLOAD_SIZE);
+
+ *dst_out = 0;
+
+ /* Scan for msbc sync sequence.
+ * We could probably assume fixed (<57-byte payload><1-byte pad>)+ format
+ * which devices seem to be sending. Don't know if there are variations,
+ * so we make weaker assumption here.
+ */
+ while (src_size >= MSBC_PAYLOAD_SIZE) {
+ if (memcmp(src, sync, sizeof(sync)) == 0)
+ break;
+ src = (uint8_t*)src + 1;
+ --src_size;
+ ++processed;
+ }
+
+ res = sbc_decode(&this->msbc, src, src_size,
+ dst, dst_size, dst_out);
+ if (res <= 0)
+ res = SPA_MIN((size_t)MSBC_PAYLOAD_SIZE, src_size); /* skip bad payload */
+
+ processed += res;
+ return processed;
+}
+
+
+const struct media_codec a2dp_codec_aptx = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX,
+ .codec_id = A2DP_CODEC_VENDOR,
+ .vendor = { .vendor_id = APTX_VENDOR_ID,
+ .codec_id = APTX_CODEC_ID },
+ .name = "aptx",
+ .description = "aptX",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+
+const struct media_codec a2dp_codec_aptx_hd = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
+ .codec_id = A2DP_CODEC_VENDOR,
+ .vendor = { .vendor_id = APTX_HD_VENDOR_ID,
+ .codec_id = APTX_HD_CODEC_ID },
+ .name = "aptx_hd",
+ .description = "aptX HD",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+#define APTX_LL_COMMON_DEFS \
+ .codec_id = A2DP_CODEC_VENDOR, \
+ .description = "aptX-LL", \
+ .fill_caps = codec_fill_caps, \
+ .select_config = codec_select_config_ll, \
+ .enum_config = codec_enum_config, \
+ .init = codec_init, \
+ .deinit = codec_deinit, \
+ .get_block_size = codec_get_block_size, \
+ .abr_process = codec_abr_process, \
+ .start_encode = codec_start_encode, \
+ .encode = codec_encode, \
+ .reduce_bitpool = codec_reduce_bitpool, \
+ .increase_bitpool = codec_increase_bitpool
+
+
+const struct media_codec a2dp_codec_aptx_ll_0 = {
+ APTX_LL_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
+ .vendor = { .vendor_id = APTX_LL_VENDOR_ID,
+ .codec_id = APTX_LL_CODEC_ID },
+ .name = "aptx_ll",
+ .endpoint_name = "aptx_ll_0",
+};
+
+const struct media_codec a2dp_codec_aptx_ll_1 = {
+ APTX_LL_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
+ .vendor = { .vendor_id = APTX_LL_VENDOR_ID2,
+ .codec_id = APTX_LL_CODEC_ID },
+ .name = "aptx_ll",
+ .endpoint_name = "aptx_ll_1",
+};
+
+/* Voice channel mSBC, not a real A2DP codec */
+static const struct media_codec aptx_ll_msbc = {
+ .codec_id = A2DP_CODEC_VENDOR,
+ .name = "aptx_ll_msbc",
+ .description = "aptX-LL mSBC",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config_ll,
+ .enum_config = msbc_enum_config,
+ .validate_config = msbc_validate_config,
+ .init = msbc_init,
+ .deinit = msbc_deinit,
+ .get_block_size = msbc_get_block_size,
+ .abr_process = msbc_abr_process,
+ .start_encode = msbc_start_encode,
+ .encode = msbc_encode,
+ .start_decode = msbc_start_decode,
+ .decode = msbc_decode,
+ .reduce_bitpool = msbc_reduce_bitpool,
+ .increase_bitpool = msbc_increase_bitpool,
+};
+
+static const struct spa_dict_item duplex_info_items[] = {
+ { "duplex.boost", "true" },
+};
+static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items);
+
+const struct media_codec a2dp_codec_aptx_ll_duplex_0 = {
+ APTX_LL_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
+ .vendor = { .vendor_id = APTX_LL_VENDOR_ID,
+ .codec_id = APTX_LL_CODEC_ID },
+ .name = "aptx_ll_duplex",
+ .endpoint_name = "aptx_ll_duplex_0",
+ .duplex_codec = &aptx_ll_msbc,
+ .info = &duplex_info,
+};
+
+const struct media_codec a2dp_codec_aptx_ll_duplex_1 = {
+ APTX_LL_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
+ .vendor = { .vendor_id = APTX_LL_VENDOR_ID2,
+ .codec_id = APTX_LL_CODEC_ID },
+ .name = "aptx_ll_duplex",
+ .endpoint_name = "aptx_ll_duplex_1",
+ .duplex_codec = &aptx_ll_msbc,
+ .info = &duplex_info,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "aptx",
+ &a2dp_codec_aptx_hd,
+ &a2dp_codec_aptx,
+ &a2dp_codec_aptx_ll_0,
+ &a2dp_codec_aptx_ll_1,
+ &a2dp_codec_aptx_ll_duplex_0,
+ &a2dp_codec_aptx_ll_duplex_1
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-caps.h b/spa/plugins/bluez5/a2dp-codec-caps.h
new file mode 100644
index 0000000..9f72592
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-caps.h
@@ -0,0 +1,460 @@
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2006-2010 Nokia Corporation
+ * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org>
+ * Copyright (C) 2018 Pali Rohár <pali.rohar@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef SPA_BLUEZ5_A2DP_CODEC_CAPS_H_
+#define SPA_BLUEZ5_A2DP_CODEC_CAPS_H_
+
+#include <stdint.h>
+#include <stddef.h>
+
+#define A2DP_CODEC_SBC 0x00
+#define A2DP_CODEC_MPEG12 0x01
+#define A2DP_CODEC_MPEG24 0x02
+#define A2DP_CODEC_ATRAC 0x03
+#define A2DP_CODEC_VENDOR 0xFF
+
+#define A2DP_MAX_CAPS_SIZE 254
+
+/* customized 16-bit vendor extension */
+#define A2DP_CODEC_VENDOR_APTX 0x4FFF
+#define A2DP_CODEC_VENDOR_LDAC 0x2DFF
+
+#define SBC_SAMPLING_FREQ_48000 (1 << 0)
+#define SBC_SAMPLING_FREQ_44100 (1 << 1)
+#define SBC_SAMPLING_FREQ_32000 (1 << 2)
+#define SBC_SAMPLING_FREQ_16000 (1 << 3)
+
+#define SBC_CHANNEL_MODE_JOINT_STEREO (1 << 0)
+#define SBC_CHANNEL_MODE_STEREO (1 << 1)
+#define SBC_CHANNEL_MODE_DUAL_CHANNEL (1 << 2)
+#define SBC_CHANNEL_MODE_MONO (1 << 3)
+
+#define SBC_BLOCK_LENGTH_16 (1 << 0)
+#define SBC_BLOCK_LENGTH_12 (1 << 1)
+#define SBC_BLOCK_LENGTH_8 (1 << 2)
+#define SBC_BLOCK_LENGTH_4 (1 << 3)
+
+#define SBC_SUBBANDS_8 (1 << 0)
+#define SBC_SUBBANDS_4 (1 << 1)
+
+#define SBC_ALLOCATION_LOUDNESS (1 << 0)
+#define SBC_ALLOCATION_SNR (1 << 1)
+
+#define SBC_MIN_BITPOOL 2
+#define SBC_MAX_BITPOOL 64
+
+#define MPEG_CHANNEL_MODE_JOINT_STEREO (1 << 0)
+#define MPEG_CHANNEL_MODE_STEREO (1 << 1)
+#define MPEG_CHANNEL_MODE_DUAL_CHANNEL (1 << 2)
+#define MPEG_CHANNEL_MODE_MONO (1 << 3)
+
+#define MPEG_LAYER_MP3 (1 << 0)
+#define MPEG_LAYER_MP2 (1 << 1)
+#define MPEG_LAYER_MP1 (1 << 2)
+
+#define MPEG_SAMPLING_FREQ_48000 (1 << 0)
+#define MPEG_SAMPLING_FREQ_44100 (1 << 1)
+#define MPEG_SAMPLING_FREQ_32000 (1 << 2)
+#define MPEG_SAMPLING_FREQ_24000 (1 << 3)
+#define MPEG_SAMPLING_FREQ_22050 (1 << 4)
+#define MPEG_SAMPLING_FREQ_16000 (1 << 5)
+
+#define MPEG_BIT_RATE_VBR 0x8000
+#define MPEG_BIT_RATE_320000 0x4000
+#define MPEG_BIT_RATE_256000 0x2000
+#define MPEG_BIT_RATE_224000 0x1000
+#define MPEG_BIT_RATE_192000 0x0800
+#define MPEG_BIT_RATE_160000 0x0400
+#define MPEG_BIT_RATE_128000 0x0200
+#define MPEG_BIT_RATE_112000 0x0100
+#define MPEG_BIT_RATE_96000 0x0080
+#define MPEG_BIT_RATE_80000 0x0040
+#define MPEG_BIT_RATE_64000 0x0020
+#define MPEG_BIT_RATE_56000 0x0010
+#define MPEG_BIT_RATE_48000 0x0008
+#define MPEG_BIT_RATE_40000 0x0004
+#define MPEG_BIT_RATE_32000 0x0002
+#define MPEG_BIT_RATE_FREE 0x0001
+
+#define AAC_OBJECT_TYPE_MPEG2_AAC_LC 0x80
+#define AAC_OBJECT_TYPE_MPEG4_AAC_LC 0x40
+#define AAC_OBJECT_TYPE_MPEG4_AAC_LTP 0x20
+#define AAC_OBJECT_TYPE_MPEG4_AAC_SCA 0x10
+
+#define AAC_SAMPLING_FREQ_8000 0x0800
+#define AAC_SAMPLING_FREQ_11025 0x0400
+#define AAC_SAMPLING_FREQ_12000 0x0200
+#define AAC_SAMPLING_FREQ_16000 0x0100
+#define AAC_SAMPLING_FREQ_22050 0x0080
+#define AAC_SAMPLING_FREQ_24000 0x0040
+#define AAC_SAMPLING_FREQ_32000 0x0020
+#define AAC_SAMPLING_FREQ_44100 0x0010
+#define AAC_SAMPLING_FREQ_48000 0x0008
+#define AAC_SAMPLING_FREQ_64000 0x0004
+#define AAC_SAMPLING_FREQ_88200 0x0002
+#define AAC_SAMPLING_FREQ_96000 0x0001
+
+#define AAC_CHANNELS_1 0x02
+#define AAC_CHANNELS_2 0x01
+
+#define AAC_GET_BITRATE(a) ((a).bitrate1 << 16 | \
+ (a).bitrate2 << 8 | (a).bitrate3)
+#define AAC_GET_FREQUENCY(a) ((a).frequency1 << 4 | (a).frequency2)
+
+#define AAC_SET_BITRATE(a, b) \
+ do { \
+ (a).bitrate1 = ((b) >> 16) & 0x7f; \
+ (a).bitrate2 = ((b) >> 8) & 0xff; \
+ (a).bitrate3 = (b) & 0xff; \
+ } while (0)
+#define AAC_SET_FREQUENCY(a, f) \
+ do { \
+ (a).frequency1 = ((f) >> 4) & 0xff; \
+ (a).frequency2 = (f) & 0x0f; \
+ } while (0)
+
+#define AAC_INIT_BITRATE(b) \
+ .bitrate1 = ((b) >> 16) & 0x7f, \
+ .bitrate2 = ((b) >> 8) & 0xff, \
+ .bitrate3 = (b) & 0xff,
+#define AAC_INIT_FREQUENCY(f) \
+ .frequency1 = ((f) >> 4) & 0xff, \
+ .frequency2 = (f) & 0x0f,
+
+#define APTX_VENDOR_ID 0x0000004f
+#define APTX_CODEC_ID 0x0001
+
+#define APTX_CHANNEL_MODE_MONO 0x01
+#define APTX_CHANNEL_MODE_STEREO 0x02
+
+#define APTX_SAMPLING_FREQ_16000 0x08
+#define APTX_SAMPLING_FREQ_32000 0x04
+#define APTX_SAMPLING_FREQ_44100 0x02
+#define APTX_SAMPLING_FREQ_48000 0x01
+
+#define APTX_HD_VENDOR_ID 0x000000D7
+#define APTX_HD_CODEC_ID 0x0024
+
+#define APTX_HD_CHANNEL_MODE_MONO 0x1
+#define APTX_HD_CHANNEL_MODE_STEREO 0x2
+
+#define APTX_HD_SAMPLING_FREQ_16000 0x8
+#define APTX_HD_SAMPLING_FREQ_32000 0x4
+#define APTX_HD_SAMPLING_FREQ_44100 0x2
+#define APTX_HD_SAMPLING_FREQ_48000 0x1
+
+#define APTX_LL_VENDOR_ID 0x0000000a
+#define APTX_LL_VENDOR_ID2 0x000000d7
+#define APTX_LL_CODEC_ID 0x0002
+
+/**
+ * Default parameters for aptX LL (Sprint) encoder
+ */
+#define APTX_LL_TARGET_CODEC_LEVEL 180 /* target codec buffer level */
+#define APTX_LL_INITIAL_CODEC_LEVEL 360 /* initial codec buffer level */
+#define APTX_LL_SRA_MAX_RATE 50 /* x/10000 = 0.005 SRA rate */
+#define APTX_LL_SRA_AVG_TIME 1 /* SRA averaging time = 1s */
+#define APTX_LL_GOOD_WORKING_LEVEL 180 /* good working buffer level */
+
+#define LDAC_VENDOR_ID 0x0000012d
+#define LDAC_CODEC_ID 0x00aa
+
+#define LDAC_CHANNEL_MODE_MONO 0x04
+#define LDAC_CHANNEL_MODE_DUAL_CHANNEL 0x02
+#define LDAC_CHANNEL_MODE_STEREO 0x01
+
+#define LDAC_SAMPLING_FREQ_44100 0x20
+#define LDAC_SAMPLING_FREQ_48000 0x10
+#define LDAC_SAMPLING_FREQ_88200 0x08
+#define LDAC_SAMPLING_FREQ_96000 0x04
+#define LDAC_SAMPLING_FREQ_176400 0x02
+#define LDAC_SAMPLING_FREQ_192000 0x01
+
+#define FASTSTREAM_VENDOR_ID 0x0000000a
+#define FASTSTREAM_CODEC_ID 0x0001
+
+#define FASTSTREAM_DIRECTION_SINK 0x1
+#define FASTSTREAM_DIRECTION_SOURCE 0x2
+
+#define FASTSTREAM_SINK_SAMPLING_FREQ_44100 0x2
+#define FASTSTREAM_SINK_SAMPLING_FREQ_48000 0x1
+
+#define FASTSTREAM_SOURCE_SAMPLING_FREQ_16000 0x2
+
+#define LC3PLUS_HR_GET_FRAME_DURATION(a) ((a).frame_duration & 0xf0)
+#define LC3PLUS_HR_INIT_FRAME_DURATION(v) \
+ .frame_duration = ((v) & 0xf0),
+#define LC3PLUS_HR_SET_FRAME_DURATION(a, v) \
+ do { \
+ (a).frame_duration = ((v) & 0xf0); \
+ } while (0)
+
+#define LC3PLUS_HR_GET_FREQUENCY(a) (((a).frequency1 << 8) | (a).frequency2)
+#define LC3PLUS_HR_INIT_FREQUENCY(v) \
+ .frequency1 = (((v) >> 8) & 0xff), \
+ .frequency2 = ((v) & 0xff),
+#define LC3PLUS_HR_SET_FREQUENCY(a, v) \
+ do { \
+ (a).frequency1 = ((v) >> 8) & 0xff; \
+ (a).frequency2 = (v) & 0xff; \
+ } while (0)
+
+#define LC3PLUS_HR_VENDOR_ID 0x000008a9
+#define LC3PLUS_HR_CODEC_ID 0x0001
+
+#define LC3PLUS_HR_FRAME_DURATION_10MS (1 << 6)
+#define LC3PLUS_HR_FRAME_DURATION_5MS (1 << 5)
+#define LC3PLUS_HR_FRAME_DURATION_2_5MS (1 << 4)
+
+#define LC3PLUS_HR_CHANNELS_1 (1 << 7)
+#define LC3PLUS_HR_CHANNELS_2 (1 << 6)
+
+#define LC3PLUS_HR_SAMPLING_FREQ_48000 (1 << 8)
+#define LC3PLUS_HR_SAMPLING_FREQ_96000 (1 << 7)
+
+#define OPUS_05_VENDOR_ID 0x000005f1
+#define OPUS_05_CODEC_ID 0x1005
+
+#define OPUS_05_MAPPING_FAMILY_0 (1 << 0)
+#define OPUS_05_MAPPING_FAMILY_1 (1 << 1)
+#define OPUS_05_MAPPING_FAMILY_255 (1 << 2)
+
+#define OPUS_05_FRAME_DURATION_25 (1 << 0)
+#define OPUS_05_FRAME_DURATION_50 (1 << 1)
+#define OPUS_05_FRAME_DURATION_100 (1 << 2)
+#define OPUS_05_FRAME_DURATION_200 (1 << 3)
+#define OPUS_05_FRAME_DURATION_400 (1 << 4)
+
+#define OPUS_05_GET_UINT16(a, field) \
+ (((a).field ## 2 << 8) | (a).field ## 1)
+#define OPUS_05_INIT_UINT16(field, v) \
+ .field ## 1 = ((v) & 0xff), \
+ .field ## 2 = (((v) >> 8) & 0xff),
+#define OPUS_05_SET_UINT16(a, field, v) \
+ do { \
+ (a).field ## 1 = ((v) & 0xff); \
+ (a).field ## 2 = (((v) >> 8) & 0xff); \
+ } while (0)
+#define OPUS_05_GET_UINT32(a, field) \
+ (((a).field ## 4 << 24) | ((a).field ## 3 << 16) | \
+ ((a).field ## 2 << 8) | (a).field ## 1)
+#define OPUS_05_INIT_UINT32(field, v) \
+ .field ## 1 = ((v) & 0xff), \
+ .field ## 2 = (((v) >> 8) & 0xff), \
+ .field ## 3 = (((v) >> 16) & 0xff), \
+ .field ## 4 = (((v) >> 24) & 0xff),
+#define OPUS_05_SET_UINT32(a, field, v) \
+ do { \
+ (a).field ## 1 = ((v) & 0xff); \
+ (a).field ## 2 = (((v) >> 8) & 0xff); \
+ (a).field ## 3 = (((v) >> 16) & 0xff); \
+ (a).field ## 4 = (((v) >> 24) & 0xff); \
+ } while (0)
+
+#define OPUS_05_GET_LOCATION(a) OPUS_05_GET_UINT32(a, location)
+#define OPUS_05_INIT_LOCATION(v) OPUS_05_INIT_UINT32(location, v)
+#define OPUS_05_SET_LOCATION(a, v) OPUS_05_SET_UINT32(a, location, v)
+
+#define OPUS_05_GET_BITRATE(a) OPUS_05_GET_UINT16(a, bitrate)
+#define OPUS_05_INIT_BITRATE(v) OPUS_05_INIT_UINT16(bitrate, v)
+#define OPUS_05_SET_BITRATE(a, v) OPUS_05_SET_UINT16(a, bitrate, v)
+
+
+typedef struct {
+ uint32_t vendor_id;
+ uint16_t codec_id;
+} __attribute__ ((packed)) a2dp_vendor_codec_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frequency;
+ uint8_t channel_mode;
+} __attribute__ ((packed)) a2dp_ldac_t;
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+
+typedef struct {
+ uint8_t channel_mode:4;
+ uint8_t frequency:4;
+ uint8_t allocation_method:2;
+ uint8_t subbands:2;
+ uint8_t block_length:4;
+ uint8_t min_bitpool;
+ uint8_t max_bitpool;
+} __attribute__ ((packed)) a2dp_sbc_t;
+
+typedef struct {
+ uint8_t channel_mode:4;
+ uint8_t crc:1;
+ uint8_t layer:3;
+ uint8_t frequency:6;
+ uint8_t mpf:1;
+ uint8_t rfa:1;
+ uint16_t bitrate;
+} __attribute__ ((packed)) a2dp_mpeg_t;
+
+typedef struct {
+ uint8_t object_type;
+ uint8_t frequency1;
+ uint8_t rfa:2;
+ uint8_t channels:2;
+ uint8_t frequency2:4;
+ uint8_t bitrate1:7;
+ uint8_t vbr:1;
+ uint8_t bitrate2;
+ uint8_t bitrate3;
+} __attribute__ ((packed)) a2dp_aac_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t channel_mode:4;
+ uint8_t frequency:4;
+} __attribute__ ((packed)) a2dp_aptx_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t channel_mode:4;
+ uint8_t frequency:4;
+ uint32_t rfa;
+} __attribute__ ((packed)) a2dp_aptx_hd_t;
+
+typedef struct {
+ a2dp_aptx_t aptx;
+ uint8_t bidirect_link:1;
+ uint8_t has_new_caps:1;
+ uint8_t reserved:6;
+} __attribute__ ((packed)) a2dp_aptx_ll_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t direction;
+ uint8_t sink_frequency:4;
+ uint8_t source_frequency:4;
+} __attribute__ ((packed)) a2dp_faststream_t;
+
+#elif __BYTE_ORDER == __BIG_ENDIAN
+
+typedef struct {
+ uint8_t frequency:4;
+ uint8_t channel_mode:4;
+ uint8_t block_length:4;
+ uint8_t subbands:2;
+ uint8_t allocation_method:2;
+ uint8_t min_bitpool;
+ uint8_t max_bitpool;
+} __attribute__ ((packed)) a2dp_sbc_t;
+
+typedef struct {
+ uint8_t layer:3;
+ uint8_t crc:1;
+ uint8_t channel_mode:4;
+ uint8_t rfa:1;
+ uint8_t mpf:1;
+ uint8_t frequency:6;
+ uint16_t bitrate;
+} __attribute__ ((packed)) a2dp_mpeg_t;
+
+typedef struct {
+ uint8_t object_type;
+ uint8_t frequency1;
+ uint8_t frequency2:4;
+ uint8_t channels:2;
+ uint8_t rfa:2;
+ uint8_t vbr:1;
+ uint8_t bitrate1:7;
+ uint8_t bitrate2;
+ uint8_t bitrate3;
+} __attribute__ ((packed)) a2dp_aac_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frequency:4;
+ uint8_t channel_mode:4;
+} __attribute__ ((packed)) a2dp_aptx_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frequency:4;
+ uint8_t channel_mode:4;
+ uint32_t rfa;
+} __attribute__ ((packed)) a2dp_aptx_hd_t;
+
+typedef struct {
+ a2dp_aptx_t aptx;
+ uint8_t reserved:6;
+ uint8_t has_new_caps:1;
+ uint8_t bidirect_link:1;
+} __attribute__ ((packed)) a2dp_aptx_ll_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t direction;
+ uint8_t source_frequency:4;
+ uint8_t sink_frequency:4;
+} __attribute__ ((packed)) a2dp_faststream_t;
+
+#else
+#error "Unknown byte order"
+#endif
+
+typedef struct {
+ a2dp_aptx_ll_t base;
+ uint8_t reserved;
+ uint8_t target_level2;
+ uint8_t target_level1;
+ uint8_t initial_level2;
+ uint8_t initial_level1;
+ uint8_t sra_max_rate;
+ uint8_t sra_avg_time;
+ uint8_t good_working_level2;
+ uint8_t good_working_level1;
+} __attribute__ ((packed)) a2dp_aptx_ll_ext_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ uint8_t frame_duration;
+ uint8_t channels;
+ uint8_t frequency1;
+ uint8_t frequency2;
+} __attribute__ ((packed)) a2dp_lc3plus_hr_t;
+
+typedef struct {
+ uint8_t channels;
+ uint8_t coupled_streams;
+ uint8_t location1;
+ uint8_t location2;
+ uint8_t location3;
+ uint8_t location4;
+ uint8_t frame_duration;
+ uint8_t bitrate1;
+ uint8_t bitrate2;
+} __attribute__ ((packed)) a2dp_opus_05_direction_t;
+
+typedef struct {
+ a2dp_vendor_codec_t info;
+ a2dp_opus_05_direction_t main;
+ a2dp_opus_05_direction_t bidi;
+} __attribute__ ((packed)) a2dp_opus_05_t;
+
+#endif
diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c
new file mode 100644
index 0000000..a579ead
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-faststream.c
@@ -0,0 +1,640 @@
+/* Spa A2DP FastStream codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2021 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <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 <sbc/sbc.h>
+
+#include "media-codecs.h"
+
+struct impl {
+ sbc_t sbc;
+
+ size_t mtu;
+ int codesize;
+ int frame_count;
+ int max_frames;
+};
+
+struct duplex_impl {
+ sbc_t sbc;
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ const a2dp_faststream_t a2dp_faststream = {
+ .info = codec->vendor,
+ .direction = FASTSTREAM_DIRECTION_SINK |
+ (codec->duplex_codec ? FASTSTREAM_DIRECTION_SOURCE : 0),
+ .sink_frequency =
+ FASTSTREAM_SINK_SAMPLING_FREQ_44100 |
+ FASTSTREAM_SINK_SAMPLING_FREQ_48000,
+ .source_frequency =
+ FASTSTREAM_SOURCE_SAMPLING_FREQ_16000,
+ };
+
+ memcpy(caps, &a2dp_faststream, sizeof(a2dp_faststream));
+ return sizeof(a2dp_faststream);
+}
+
+static const struct media_codec_config
+frequencies[] = {
+ { FASTSTREAM_SINK_SAMPLING_FREQ_48000, 48000, 1 },
+ { FASTSTREAM_SINK_SAMPLING_FREQ_44100, 44100, 0 },
+};
+
+static const struct media_codec_config
+duplex_frequencies[] = {
+ { FASTSTREAM_SOURCE_SAMPLING_FREQ_16000, 16000, 0 },
+};
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_faststream_t conf;
+ int i;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ if (codec->duplex_codec && !(conf.direction & FASTSTREAM_DIRECTION_SOURCE))
+ return -ENOTSUP;
+
+ if (!(conf.direction & FASTSTREAM_DIRECTION_SINK))
+ return -ENOTSUP;
+
+ conf.direction = FASTSTREAM_DIRECTION_SINK;
+
+ if (codec->duplex_codec)
+ conf.direction |= FASTSTREAM_DIRECTION_SOURCE;
+
+ if ((i = media_codec_select_config(frequencies,
+ SPA_N_ELEMENTS(frequencies),
+ conf.sink_frequency,
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ conf.sink_frequency = frequencies[i].config;
+
+ if ((i = media_codec_select_config(duplex_frequencies,
+ SPA_N_ELEMENTS(duplex_frequencies),
+ conf.source_frequency,
+ 16000
+ )) < 0)
+ return -ENOTSUP;
+ conf.source_frequency = duplex_frequencies[i].config;
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+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)
+{
+ a2dp_faststream_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ 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_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_48000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_44100) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 44100);
+ spa_pod_builder_int(b, 44100);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static size_t ceil2(size_t v)
+{
+ if (v % 2 != 0 && v < SIZE_MAX)
+ v += 1;
+ return v;
+}
+
+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)
+{
+ a2dp_faststream_t *conf = config;
+ struct impl *this;
+ bool sbc_initialized = false;
+ int res;
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ if ((res = sbc_init(&this->sbc, 0)) < 0)
+ goto error;
+
+ sbc_initialized = true;
+ this->sbc.endian = SBC_LE;
+ this->mtu = mtu;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ switch (conf->sink_frequency) {
+ case FASTSTREAM_SINK_SAMPLING_FREQ_44100:
+ this->sbc.frequency = SBC_FREQ_44100;
+ break;
+ case FASTSTREAM_SINK_SAMPLING_FREQ_48000:
+ this->sbc.frequency = SBC_FREQ_48000;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->sbc.mode = SBC_MODE_JOINT_STEREO;
+ this->sbc.subbands = SBC_SB_8;
+ this->sbc.allocation = SBC_AM_LOUDNESS;
+ this->sbc.blocks = SBC_BLK_16;
+ this->sbc.bitpool = 29;
+
+ this->codesize = sbc_get_codesize(&this->sbc);
+
+ this->max_frames = 3;
+ if (this->mtu < this->max_frames * ceil2(sbc_get_frame_length(&this->sbc))) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+
+error:
+ if (sbc_initialized)
+ sbc_finish(&this->sbc);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ sbc_finish(&this->sbc);
+ free(this);
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+ this->frame_count = 0;
+ return 0;
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int res;
+
+ res = sbc_encode(&this->sbc, src, src_size,
+ dst, dst_size, (ssize_t*)dst_out);
+ if (SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+ spa_assert(res == this->codesize);
+
+ if (*dst_out % 2 != 0 && *dst_out < dst_size) {
+ /* Pad similarly as in input stream */
+ *((uint8_t *)dst + *dst_out) = 0;
+ ++*dst_out;
+ }
+
+ this->frame_count += res / this->codesize;
+ *need_flush = (this->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
+ return res;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ return 0;
+}
+
+static int do_decode(sbc_t *sbc,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ size_t processed = 0;
+ int res;
+
+ *dst_out = 0;
+
+ /* Scan for SBC syncword.
+ * We could probably assume 1-byte paddings instead,
+ * which devices seem to be sending.
+ */
+ while (src_size >= 1) {
+ if (*(uint8_t*)src == 0x9C)
+ break;
+ src = (uint8_t*)src + 1;
+ --src_size;
+ ++processed;
+ }
+
+ res = sbc_decode(sbc, src, src_size,
+ dst, dst_size, dst_out);
+ if (res <= 0)
+ res = SPA_MIN((size_t)1, src_size); /* skip bad payload */
+
+ processed += res;
+ return processed;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ return do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out);
+}
+
+/*
+ * Duplex codec
+ *
+ * When connected as SRC to SNK, FastStream sink may send back SBC data.
+ */
+
+static int duplex_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ a2dp_faststream_t conf;
+ struct spa_audio_info_raw info = { 0, };
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ switch (conf.source_frequency) {
+ case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000:
+ info.rate = 16000;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /*
+ * Some headsets send mono stream, others stereo. This information
+ * is contained in the SBC headers, and becomes known only when
+ * stream arrives. To be able to work in both cases, we will
+ * produce 2-channel output, and will double the channels
+ * in the decoding step if mono stream was received.
+ */
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ info.channels = 2;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+
+ *param = spa_format_audio_raw_build(b, id, &info);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int duplex_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE;
+ info->info.raw.channels = 2;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ info->info.raw.rate = 16000;
+ return 0;
+}
+
+static int duplex_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_get_block_size(void *data)
+{
+ return 0;
+}
+
+static void *duplex_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ a2dp_faststream_t *conf = config;
+ struct duplex_impl *this = NULL;
+ int res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16_LE) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct duplex_impl))) == NULL)
+ goto error_errno;
+
+ if ((res = sbc_init(&this->sbc, 0)) < 0)
+ goto error;
+
+ switch (conf->source_frequency) {
+ case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000:
+ this->sbc.frequency = SBC_FREQ_16000;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->sbc.endian = SBC_LE;
+ this->sbc.mode = SBC_MODE_MONO;
+ this->sbc.subbands = SBC_SB_8;
+ this->sbc.allocation = SBC_AM_LOUDNESS;
+ this->sbc.blocks = SBC_BLK_16;
+ this->sbc.bitpool = 32;
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+error:
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void duplex_deinit(void *data)
+{
+ struct duplex_impl *this = data;
+ sbc_finish(&this->sbc);
+ free(this);
+}
+
+static int duplex_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ return -ENOTSUP;
+}
+
+static int duplex_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ return 0;
+}
+
+/** Convert S16LE stereo -> S16LE mono, in-place (only for testing purposes) */
+static SPA_UNUSED size_t convert_s16le_c2_to_c1(int16_t *data, size_t size, size_t max_size)
+{
+ size_t i;
+ for (i = 0; i < size / 2; ++i)
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ data[i] = data[2*i]/2 + data[2*i+1]/2;
+#else
+ data[i] = bswap_16(bswap_16(data[2*i])/2 + bswap_16(data[2*i+1])/2);
+#endif
+ return size / 2;
+}
+
+/** Convert S16LE mono -> S16LE stereo, in-place */
+static size_t convert_s16le_c1_to_c2(uint8_t *data, size_t size, size_t max_size)
+{
+ size_t pos;
+
+ pos = 2 * SPA_MIN(size / 2, max_size / 4);
+ size = 2 * pos;
+
+ /* We'll trust the compiler to optimize this */
+ while (pos >= 2) {
+ pos -= 2;
+ data[2*pos+3] = data[pos+1];
+ data[2*pos+2] = data[pos];
+ data[2*pos+1] = data[pos+1];
+ data[2*pos] = data[pos];
+ }
+
+ return size;
+}
+
+static int duplex_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct duplex_impl *this = data;
+ int res;
+
+ *dst_out = 0;
+ res = do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out);
+
+ /*
+ * Depending on headers of first frame, libsbc may output either
+ * 1 or 2 channels. This function should always produce 2 channels,
+ * so we'll just double the channels here.
+ */
+ if (this->sbc.mode == SBC_MODE_MONO)
+ *dst_out = convert_s16le_c1_to_c2(dst, *dst_out, dst_size);
+
+ return res;
+}
+
+/* Voice channel SBC, not a real A2DP codec */
+static const struct media_codec duplex_codec = {
+ .codec_id = A2DP_CODEC_VENDOR,
+ .name = "faststream_sbc",
+ .description = "FastStream duplex SBC",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = duplex_enum_config,
+ .validate_config = duplex_validate_config,
+ .init = duplex_init,
+ .deinit = duplex_deinit,
+ .get_block_size = duplex_get_block_size,
+ .abr_process = duplex_abr_process,
+ .start_encode = duplex_start_encode,
+ .encode = duplex_encode,
+ .start_decode = duplex_start_decode,
+ .decode = duplex_decode,
+ .reduce_bitpool = duplex_reduce_bitpool,
+ .increase_bitpool = duplex_increase_bitpool,
+};
+
+#define FASTSTREAM_COMMON_DEFS \
+ .codec_id = A2DP_CODEC_VENDOR, \
+ .vendor = { .vendor_id = FASTSTREAM_VENDOR_ID, \
+ .codec_id = FASTSTREAM_CODEC_ID }, \
+ .description = "FastStream", \
+ .fill_caps = codec_fill_caps, \
+ .select_config = codec_select_config, \
+ .enum_config = codec_enum_config, \
+ .init = codec_init, \
+ .deinit = codec_deinit, \
+ .get_block_size = codec_get_block_size, \
+ .abr_process = codec_abr_process, \
+ .start_encode = codec_start_encode, \
+ .encode = codec_encode, \
+ .reduce_bitpool = codec_reduce_bitpool, \
+ .increase_bitpool = codec_increase_bitpool
+
+const struct media_codec a2dp_codec_faststream = {
+ FASTSTREAM_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
+ .name = "faststream",
+};
+
+static const struct spa_dict_item duplex_info_items[] = {
+ { "duplex.boost", "true" },
+};
+static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items);
+
+const struct media_codec a2dp_codec_faststream_duplex = {
+ FASTSTREAM_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
+ .name = "faststream_duplex",
+ .duplex_codec = &duplex_codec,
+ .info = &duplex_info,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "faststream",
+ &a2dp_codec_faststream,
+ &a2dp_codec_faststream_duplex
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-lc3plus.c b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
new file mode 100644
index 0000000..2896624
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c
@@ -0,0 +1,790 @@
+/* Spa A2DP LC3plus HR codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <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>
+
+#ifdef HAVE_LC3PLUS_H
+#include <lc3plus.h>
+#else
+#include <lc3.h>
+#endif
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+#define BITRATE_MIN 96000
+#define BITRATE_MAX 512000
+#define BITRATE_DEFAULT 160000
+
+struct dec_data {
+ int frame_size;
+ int fragment_size;
+ int fragment_count;
+ uint8_t fragment[LC3PLUS_MAX_BYTES];
+};
+
+struct enc_data {
+ struct rtp_header *header;
+ struct rtp_payload *payload;
+
+ int samples;
+ int codesize;
+
+ int packet_size;
+ int fragment_size;
+ int fragment_count;
+ void *fragment;
+
+ int bitrate;
+ int next_bitrate;
+};
+
+struct impl {
+ LC3PLUS_Enc *enc;
+ LC3PLUS_Dec *dec;
+
+ int mtu;
+ int samplerate;
+ int channels;
+ int frame_dms;
+ int bitrate;
+
+ struct dec_data d;
+ struct enc_data e;
+
+ int32_t buf[2][LC3PLUS_MAX_SAMPLES];
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ const a2dp_lc3plus_hr_t a2dp_lc3plus_hr = {
+ .info = codec->vendor,
+ LC3PLUS_HR_INIT_FRAME_DURATION(LC3PLUS_HR_FRAME_DURATION_10MS
+ | LC3PLUS_HR_FRAME_DURATION_5MS
+ | LC3PLUS_HR_FRAME_DURATION_2_5MS)
+ .channels = LC3PLUS_HR_CHANNELS_1 | LC3PLUS_HR_CHANNELS_2,
+ LC3PLUS_HR_INIT_FREQUENCY(LC3PLUS_HR_SAMPLING_FREQ_48000
+ | (lc3plus_samplerate_supported(96000) ? LC3PLUS_HR_SAMPLING_FREQ_96000 : 0))
+ };
+ memcpy(caps, &a2dp_lc3plus_hr, sizeof(a2dp_lc3plus_hr));
+ return sizeof(a2dp_lc3plus_hr);
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_lc3plus_hr_t conf;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_48000)
+ && lc3plus_samplerate_supported(48000))
+ LC3PLUS_HR_SET_FREQUENCY(conf, LC3PLUS_HR_SAMPLING_FREQ_48000);
+ else if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_96000)
+ && lc3plus_samplerate_supported(96000))
+ LC3PLUS_HR_SET_FREQUENCY(conf, LC3PLUS_HR_SAMPLING_FREQ_96000);
+ else
+ return -ENOTSUP;
+
+ if ((conf.channels & LC3PLUS_HR_CHANNELS_2) &&
+ lc3plus_channels_supported(2))
+ conf.channels = LC3PLUS_HR_CHANNELS_2;
+ else if ((conf.channels & LC3PLUS_HR_CHANNELS_1) &&
+ lc3plus_channels_supported(1))
+ conf.channels = LC3PLUS_HR_CHANNELS_1;
+ else
+ return -ENOTSUP;
+
+ if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_10MS)
+ LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_10MS);
+ else if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_5MS)
+ LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_5MS);
+ else if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_2_5MS)
+ LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_2_5MS);
+ else
+ return -ENOTSUP;
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
+{
+ a2dp_lc3plus_hr_t conf1, conf2;
+ a2dp_lc3plus_hr_t *conf;
+ int res1, res2;
+ int a, b;
+
+ /* Order selected configurations by preference */
+ res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1);
+ res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2);
+
+#define PREFER_EXPR(expr) \
+ do { \
+ conf = &conf1; \
+ a = (expr); \
+ conf = &conf2; \
+ b = (expr); \
+ if (a != b) \
+ return b - a; \
+ } while (0)
+
+#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0)
+
+ /* Prefer valid */
+ a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_lc3plus_hr_t)) ? 1 : 0;
+ b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_lc3plus_hr_t)) ? 1 : 0;
+ if (!a || !b)
+ return b - a;
+
+ PREFER_BOOL(conf->channels & LC3PLUS_HR_CHANNELS_2);
+ PREFER_BOOL(LC3PLUS_HR_GET_FREQUENCY(*conf) & (LC3PLUS_HR_SAMPLING_FREQ_48000 | LC3PLUS_HR_SAMPLING_FREQ_96000));
+ PREFER_BOOL(LC3PLUS_HR_GET_FREQUENCY(*conf) & LC3PLUS_HR_SAMPLING_FREQ_48000);
+
+ return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+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)
+{
+ a2dp_lc3plus_hr_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24_32),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_96000) &&
+ lc3plus_samplerate_supported(96000)) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 96000);
+ spa_pod_builder_int(b, 96000);
+ }
+ if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_48000) &&
+ lc3plus_samplerate_supported(48000)) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if ((conf.channels & (LC3PLUS_HR_CHANNELS_2 | LC3PLUS_HR_CHANNELS_1)) &&
+ lc3plus_channels_supported(2) && lc3plus_channels_supported(1)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if ((conf.channels & LC3PLUS_HR_CHANNELS_2) && lc3plus_channels_supported(2)) {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ } else if ((conf.channels & LC3PLUS_HR_CHANNELS_1) && lc3plus_channels_supported(1)) {
+ 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 int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ const a2dp_lc3plus_hr_t *conf;
+
+ if (caps == NULL || caps_size < sizeof(*conf))
+ return -EINVAL;
+
+ conf = caps;
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S24_32;
+
+ switch (LC3PLUS_HR_GET_FREQUENCY(*conf)) {
+ case LC3PLUS_HR_SAMPLING_FREQ_96000:
+ if (!lc3plus_samplerate_supported(96000))
+ return -EINVAL;
+ info->info.raw.rate = 96000;
+ break;
+ case LC3PLUS_HR_SAMPLING_FREQ_48000:
+ if (!lc3plus_samplerate_supported(48000))
+ return -EINVAL;
+ info->info.raw.rate = 48000;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (conf->channels) {
+ case LC3PLUS_HR_CHANNELS_2:
+ if (!lc3plus_channels_supported(2))
+ return -EINVAL;
+ info->info.raw.channels = 2;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ break;
+ case LC3PLUS_HR_CHANNELS_1:
+ if (!lc3plus_channels_supported(1))
+ return -EINVAL;
+ info->info.raw.channels = 1;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (LC3PLUS_HR_GET_FRAME_DURATION(*conf)) {
+ case LC3PLUS_HR_FRAME_DURATION_10MS:
+ case LC3PLUS_HR_FRAME_DURATION_5MS:
+ case LC3PLUS_HR_FRAME_DURATION_2_5MS:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static size_t ceildiv(size_t v, size_t divisor)
+{
+ if (v % divisor == 0)
+ return v / divisor;
+ else
+ return v / divisor + 1;
+}
+
+static bool check_mtu_vs_frame_dms(struct impl *this)
+{
+ /* Only 10ms frames can be fragmented (max 0xf fragments);
+ * others must fit in single MTU */
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+ size_t max_fragments = (this->frame_dms == 100) ? 0xf : 1;
+ size_t payload_size = lc3plus_enc_get_num_bytes(this->enc);
+ return (size_t)this->mtu >= header_size + ceildiv(payload_size, max_fragments);
+}
+
+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)
+{
+ a2dp_lc3plus_hr_t *conf = config;
+ struct impl *this = NULL;
+ struct spa_audio_info config_info;
+ int size;
+ int res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S24_32) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0)
+ goto error;
+
+ this->mtu = mtu;
+ this->samplerate = config_info.info.raw.rate;
+ this->channels = config_info.info.raw.channels;
+ this->bitrate = BITRATE_DEFAULT * this->channels;
+
+ switch (LC3PLUS_HR_GET_FRAME_DURATION(*conf)) {
+ case LC3PLUS_HR_FRAME_DURATION_10MS:
+ this->frame_dms = 100;
+ break;
+ case LC3PLUS_HR_FRAME_DURATION_5MS:
+ this->frame_dms = 50;
+ break;
+ case LC3PLUS_HR_FRAME_DURATION_2_5MS:
+ this->frame_dms = 25;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((size = lc3plus_enc_get_size(this->samplerate, this->channels)) == 0) {
+ res = -EIO;
+ goto error;
+ }
+ if ((this->enc = calloc(1, size)) == NULL)
+ goto error_errno;
+ if (lc3plus_enc_init(this->enc, this->samplerate, this->channels) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (lc3plus_enc_set_frame_ms(this->enc, this->frame_dms/10.0f) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (lc3plus_enc_set_hrmode(this->enc, 1) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ while (1) {
+ /* Find a valid bitrate */
+ if (lc3plus_enc_set_bitrate(this->enc, this->bitrate) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (check_mtu_vs_frame_dms(this))
+ break;
+ this->bitrate = this->bitrate * 3/4;
+ }
+
+ if ((size = lc3plus_dec_get_size(this->samplerate, this->channels)) == 0) {
+ res = -EINVAL;
+ goto error;
+ }
+ if ((this->dec = calloc(1, size)) == NULL)
+ goto error_errno;
+ if (lc3plus_dec_init(this->dec, this->samplerate, this->channels, LC3PLUS_PLC_ADVANCED) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (lc3plus_dec_set_frame_ms(this->dec, this->frame_dms/10.0f) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+ if (lc3plus_dec_set_hrmode(this->dec, 1) != LC3PLUS_OK) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->e.samples = lc3plus_enc_get_input_samples(this->enc);
+ this->e.codesize = this->e.samples * this->channels * sizeof(int32_t);
+
+ spa_assert(this->e.samples <= LC3PLUS_MAX_SAMPLES);
+
+ this->e.bitrate = this->bitrate;
+ this->e.next_bitrate = this->bitrate;
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+
+error:
+ if (this && this->enc)
+ lc3plus_enc_free_memory(this->enc);
+ if (this && this->dec)
+ lc3plus_dec_free_memory(this->dec);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ lc3plus_enc_free_memory(this->enc);
+ lc3plus_dec_free_memory(this->dec);
+ free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->e.codesize;
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_update_bitrate(struct impl *this)
+{
+ this->e.next_bitrate = SPA_CLAMP(this->e.next_bitrate,
+ BITRATE_MIN * this->channels, BITRATE_MAX * this->channels);
+
+ if (this->e.next_bitrate == this->e.bitrate)
+ return 0;
+
+ this->e.bitrate = this->e.next_bitrate;
+
+ if (lc3plus_enc_set_bitrate(this->enc, this->e.bitrate) != LC3PLUS_OK ||
+ !check_mtu_vs_frame_dms(this)) {
+ lc3plus_enc_set_bitrate(this->enc, this->bitrate);
+ return -EINVAL;
+ }
+
+ this->bitrate = this->e.bitrate;
+
+ return 0;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ if (dst_size <= header_size)
+ return -EINVAL;
+
+ codec_update_bitrate(this);
+
+ this->e.header = (struct rtp_header *)dst;
+ this->e.payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+ memset(dst, 0, header_size);
+
+ this->e.payload->frame_count = 0;
+ this->e.header->v = 2;
+ this->e.header->pt = 96;
+ this->e.header->sequence_number = htons(seqnum);
+ this->e.header->timestamp = htonl(timestamp);
+ this->e.header->ssrc = htonl(1);
+
+ this->e.packet_size = header_size;
+ return this->e.packet_size;
+}
+
+static void deinterleave_32_c2(int32_t * SPA_RESTRICT * SPA_RESTRICT dst, const int32_t * SPA_RESTRICT src, size_t n_samples)
+{
+ /* We'll trust the compiler to optimize this */
+ const size_t n_channels = 2;
+ size_t i, j;
+ for (j = 0; j < n_samples; ++j)
+ for (i = 0; i < n_channels; ++i)
+ dst[i][j] = *src++;
+}
+
+static void interleave_32_c2(int32_t * SPA_RESTRICT dst, const int32_t * SPA_RESTRICT * SPA_RESTRICT src, size_t n_samples)
+{
+ const size_t n_channels = 2;
+ size_t i, j;
+ for (j = 0; j < n_samples; ++j)
+ for (i = 0; i < n_channels; ++i)
+ *dst++ = src[i][j];
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int frame_bytes;
+ LC3PLUS_Error res;
+ int size, processed;
+ int header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+ int32_t *inputs[2];
+
+ if (src == NULL) {
+ /* Produce fragment packets.
+ *
+ * We assume the caller gives the same buffer here as in the previous
+ * calls to encode(), without changes in the buffer content.
+ */
+ if (this->e.fragment == NULL ||
+ this->e.fragment_count <= 1 ||
+ this->e.fragment < dst ||
+ SPA_PTROFF(this->e.fragment, this->e.fragment_size, void) > SPA_PTROFF(dst, dst_size, void)) {
+ this->e.fragment = NULL;
+ return -EINVAL;
+ }
+
+ size = SPA_MIN(this->mtu - header_size, this->e.fragment_size);
+ memmove(dst, this->e.fragment, size);
+ *dst_out = size;
+
+ this->e.payload->is_fragmented = 1;
+ this->e.payload->frame_count = --this->e.fragment_count;
+ this->e.payload->is_last_fragment = (this->e.fragment_count == 1);
+
+ if (this->e.fragment_size > size && this->e.fragment_count > 1) {
+ this->e.fragment = SPA_PTROFF(this->e.fragment, size, void);
+ this->e.fragment_size -= size;
+ *need_flush = NEED_FLUSH_FRAGMENT;
+ } else {
+ this->e.fragment = NULL;
+ *need_flush = NEED_FLUSH_ALL;
+ }
+ return 0;
+ }
+
+ frame_bytes = lc3plus_enc_get_num_bytes(this->enc);
+ processed = 0;
+
+ if (src_size < (size_t)this->e.codesize)
+ goto done;
+ if (dst_size < (size_t)frame_bytes)
+ goto done;
+ if (this->e.payload->frame_count > 0 &&
+ this->e.packet_size + frame_bytes > this->mtu)
+ goto done;
+
+ if (this->channels == 1) {
+ inputs[0] = (int32_t *)src;
+ res = lc3plus_enc24(this->enc, inputs, dst, &size);
+ } else {
+ inputs[0] = this->buf[0];
+ inputs[1] = this->buf[1];
+ deinterleave_32_c2(inputs, src, this->e.samples);
+ res = lc3plus_enc24(this->enc, inputs, dst, &size);
+ }
+ if (SPA_UNLIKELY(res != LC3PLUS_OK))
+ return -EINVAL;
+ *dst_out = size;
+
+ processed += this->e.codesize;
+ this->e.packet_size += size;
+ this->e.payload->frame_count++;
+
+done:
+ if (this->e.payload->frame_count == 0)
+ return processed;
+ if (this->e.payload->frame_count < 0xf &&
+ this->frame_dms * (this->e.payload->frame_count + 1) < 200 &&
+ this->e.packet_size + frame_bytes <= this->mtu)
+ return processed; /* add another frame */
+
+ if (this->e.packet_size > this->mtu) {
+ /* Fragment packet */
+ spa_assert(this->e.payload->frame_count == 1);
+ spa_assert(this->frame_dms == 100);
+
+ this->e.fragment_count = ceildiv(this->e.packet_size - header_size,
+ this->mtu - header_size);
+
+ this->e.payload->is_fragmented = 1;
+ this->e.payload->is_first_fragment = 1;
+ this->e.payload->frame_count = this->e.fragment_count;
+
+ this->e.fragment_size = this->e.packet_size - this->mtu;
+ this->e.fragment = SPA_PTROFF(dst, *dst_out - this->e.fragment_size, void);
+ *need_flush = NEED_FLUSH_FRAGMENT;
+
+ /*
+ * We keep the rest of the encoded frame in the same buffer, and rely
+ * that the caller won't overwrite it before the next call to encode()
+ */
+ *dst_out = SPA_PTRDIFF(this->e.fragment, dst);
+ } else {
+ *need_flush = NEED_FLUSH_ALL;
+ }
+
+ return processed;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ struct impl *this = data;
+ const struct rtp_header *header = src;
+ const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void);
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+
+ if (payload->is_fragmented) {
+ if (payload->is_first_fragment) {
+ this->d.fragment_size = 0;
+ } else if (payload->frame_count + 1 != this->d.fragment_count ||
+ (payload->frame_count == 1 && !payload->is_last_fragment)){
+ /* Fragments not in right order: drop packet */
+ return -EINVAL;
+ }
+ this->d.fragment_count = payload->frame_count;
+ this->d.frame_size = src_size - header_size;
+ } else {
+ if (payload->frame_count <= 0)
+ return -EINVAL;
+ this->d.fragment_count = 0;
+ this->d.frame_size = (src_size - header_size) / payload->frame_count;
+ if (this->d.frame_size <= 0)
+ return -EINVAL;
+ }
+
+ return header_size;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ LC3PLUS_Error res;
+ int32_t *outputs[2];
+ int consumed;
+ int samples;
+
+ if (this->d.fragment_count > 0) {
+ /* Fragmented frame */
+ size_t avail;
+ avail = SPA_MIN(sizeof(this->d.fragment) - this->d.fragment_size, src_size);
+ memcpy(SPA_PTROFF(this->d.fragment, this->d.fragment_size, void), src, avail);
+
+ this->d.fragment_size += avail;
+ consumed = src_size;
+
+ if (this->d.fragment_count > 1) {
+ /* More fragments to come */
+ *dst_out = 0;
+ return consumed;
+ }
+
+ src = this->d.fragment;
+ src_size = this->d.fragment_size;
+
+ this->d.fragment_count = 0;
+ this->d.fragment_size = 0;
+ } else {
+ src_size = SPA_MIN((size_t)this->d.frame_size, src_size);
+ consumed = src_size;
+ }
+
+ samples = lc3plus_dec_get_output_samples(this->dec);
+ *dst_out = samples * this->channels * sizeof(int32_t);
+ if (dst_size < *dst_out)
+ return -EINVAL;
+
+ if (this->channels == 1) {
+ outputs[0] = (int32_t *)dst;
+ res = lc3plus_dec24(this->dec, (void *)src, src_size, outputs, 0);
+ } else {
+ outputs[0] = this->buf[0];
+ outputs[1] = this->buf[1];
+ res = lc3plus_dec24(this->dec, (void *)src, src_size, outputs, 0);
+ interleave_32_c2(dst, (const int32_t**)outputs, samples);
+ }
+ if (SPA_UNLIKELY(res != LC3PLUS_OK && res != LC3PLUS_DECODE_ERROR))
+ return -EINVAL;
+
+ return consumed;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ struct impl *this = data;
+ this->e.next_bitrate = SPA_CLAMP(this->bitrate * 3 / 4,
+ BITRATE_MIN * this->channels, BITRATE_MAX * this->channels);
+ return this->e.next_bitrate;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ struct impl *this = data;
+ this->e.next_bitrate = SPA_CLAMP(this->bitrate * 5 / 4,
+ BITRATE_MIN * this->channels, BITRATE_MAX * this->channels);
+ return this->e.next_bitrate;
+}
+
+const struct media_codec a2dp_codec_lc3plus_hr = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR,
+ .name = "lc3plus_hr",
+ .codec_id = A2DP_CODEC_VENDOR,
+ .vendor = { .vendor_id = LC3PLUS_HR_VENDOR_ID,
+ .codec_id = LC3PLUS_HR_CODEC_ID },
+ .description = "LC3plus HR",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .caps_preference_cmp = codec_caps_preference_cmp,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "lc3plus",
+ &a2dp_codec_lc3plus_hr
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-ldac.c b/spa/plugins/bluez5/a2dp-codec-ldac.c
new file mode 100644
index 0000000..624dd4a
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-ldac.c
@@ -0,0 +1,604 @@
+/* Spa A2DP LDAC codec
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <spa/utils/string.h>
+#include <spa/utils/dict.h>
+#include <spa/pod/parser.h>
+#include <spa/param/props.h>
+#include <spa/param/audio/format.h>
+
+#include <ldacBT.h>
+
+#ifdef ENABLE_LDAC_ABR
+#include <ldacBT_abr.h>
+#endif
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+#define LDACBT_EQMID_AUTO -1
+
+#define LDAC_ABR_MAX_PACKET_NBYTES 1280
+
+#define LDAC_ABR_INTERVAL_MS 5 /* 2 frames * 128 lsu / 48000 */
+
+/* decrease ABR thresholds to increase stability */
+#define LDAC_ABR_THRESHOLD_CRITICAL 6
+#define LDAC_ABR_THRESHOLD_DANGEROUSTREND 4
+#define LDAC_ABR_THRESHOLD_SAFETY_FOR_HQSQ 3
+
+#define LDAC_ABR_SOCK_BUFFER_SIZE (LDAC_ABR_THRESHOLD_CRITICAL * LDAC_ABR_MAX_PACKET_NBYTES)
+
+
+struct props {
+ int eqmid;
+};
+
+struct impl {
+ HANDLE_LDAC_BT ldac;
+#ifdef ENABLE_LDAC_ABR
+ HANDLE_LDAC_ABR ldac_abr;
+#endif
+ bool enable_abr;
+
+ struct rtp_header *header;
+ struct rtp_payload *payload;
+
+ int mtu;
+ int eqmid;
+ int frequency;
+ int fmt;
+ int codesize;
+ int frame_length;
+ int frame_count;
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ static const a2dp_ldac_t a2dp_ldac = {
+ .info.vendor_id = LDAC_VENDOR_ID,
+ .info.codec_id = LDAC_CODEC_ID,
+ .frequency = LDACBT_SAMPLING_FREQ_044100 |
+ LDACBT_SAMPLING_FREQ_048000 |
+ LDACBT_SAMPLING_FREQ_088200 |
+ LDACBT_SAMPLING_FREQ_096000,
+ .channel_mode = LDACBT_CHANNEL_MODE_MONO |
+ LDACBT_CHANNEL_MODE_DUAL_CHANNEL |
+ LDACBT_CHANNEL_MODE_STEREO,
+ };
+
+ memcpy(caps, &a2dp_ldac, sizeof(a2dp_ldac));
+ return sizeof(a2dp_ldac);
+}
+
+static const struct media_codec_config
+ldac_frequencies[] = {
+ { LDACBT_SAMPLING_FREQ_044100, 44100, 3 },
+ { LDACBT_SAMPLING_FREQ_048000, 48000, 2 },
+ { LDACBT_SAMPLING_FREQ_088200, 88200, 1 },
+ { LDACBT_SAMPLING_FREQ_096000, 96000, 0 },
+};
+
+static const struct media_codec_config
+ldac_channel_modes[] = {
+ { LDACBT_CHANNEL_MODE_STEREO, 2, 2 },
+ { LDACBT_CHANNEL_MODE_DUAL_CHANNEL, 2, 1 },
+ { LDACBT_CHANNEL_MODE_MONO, 1, 0 },
+};
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_ldac_t conf;
+ int i;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ if ((i = media_codec_select_config(ldac_frequencies,
+ SPA_N_ELEMENTS(ldac_frequencies),
+ conf.frequency,
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ conf.frequency = ldac_frequencies[i].config;
+
+ if ((i = media_codec_select_config(ldac_channel_modes,
+ SPA_N_ELEMENTS(ldac_channel_modes),
+ conf.channel_mode,
+ info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
+ )) < 0)
+ return -ENOTSUP;
+ conf.channel_mode = ldac_channel_modes[i].config;
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+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)
+{
+ a2dp_ldac_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t i = 0;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ 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_CHOICE_ENUM_Id(5,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_S32,
+ SPA_AUDIO_FORMAT_S24,
+ SPA_AUDIO_FORMAT_S16),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.frequency & LDACBT_SAMPLING_FREQ_048000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.frequency & LDACBT_SAMPLING_FREQ_044100) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 44100);
+ spa_pod_builder_int(b, 44100);
+ }
+ if (conf.frequency & LDACBT_SAMPLING_FREQ_088200) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 88200);
+ spa_pod_builder_int(b, 88200);
+ }
+ if (conf.frequency & LDACBT_SAMPLING_FREQ_096000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 96000);
+ spa_pod_builder_int(b, 96000);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (conf.channel_mode & LDACBT_CHANNEL_MODE_MONO &&
+ conf.channel_mode & (LDACBT_CHANNEL_MODE_STEREO |
+ LDACBT_CHANNEL_MODE_DUAL_CHANNEL)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if (conf.channel_mode & LDACBT_CHANNEL_MODE_MONO) {
+ 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);
+ } else {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ }
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+#ifdef ENABLE_LDAC_ABR
+ return -ENOTSUP;
+#else
+ struct impl *this = data;
+ int res;
+ if (this->eqmid == LDACBT_EQMID_MQ || !this->enable_abr)
+ return this->eqmid;
+ res = ldacBT_alter_eqmid_priority(this->ldac, LDACBT_EQMID_INC_CONNECTION);
+ return res;
+#endif
+}
+
+static int codec_increase_bitpool(void *data)
+{
+#ifdef ENABLE_LDAC_ABR
+ return -ENOTSUP;
+#else
+ struct impl *this = data;
+ int res;
+ if (!this->enable_abr)
+ return this->eqmid;
+ res = ldacBT_alter_eqmid_priority(this->ldac, LDACBT_EQMID_INC_QUALITY);
+ return res;
+#endif
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static int string_to_eqmid(const char * eqmid)
+{
+ if (spa_streq("auto", eqmid))
+ return LDACBT_EQMID_AUTO;
+ else if (spa_streq("hq", eqmid))
+ return LDACBT_EQMID_HQ;
+ else if (spa_streq("sq", eqmid))
+ return LDACBT_EQMID_SQ;
+ else if (spa_streq("mq", eqmid))
+ return LDACBT_EQMID_MQ;
+ else
+ return LDACBT_EQMID_AUTO;
+}
+
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
+{
+ struct props *p = calloc(1, sizeof(struct props));
+ const char *str;
+
+ if (p == NULL)
+ return NULL;
+
+ if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.ldac.quality")) == NULL)
+ str = "auto";
+
+ p->eqmid = string_to_eqmid(str);
+ return p;
+}
+
+static void codec_clear_props(void *props)
+{
+ free(props);
+}
+
+static int codec_enum_props(void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ struct props *p = props;
+ struct spa_pod_frame f[2];
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (idx) {
+ case 0:
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+ spa_pod_builder_prop(b, SPA_PROP_INFO_id, 0);
+ spa_pod_builder_id(b, SPA_PROP_quality);
+ spa_pod_builder_prop(b, SPA_PROP_INFO_description, 0);
+ spa_pod_builder_string(b, "LDAC quality");
+
+ spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+ spa_pod_builder_int(b, p->eqmid);
+ spa_pod_builder_int(b, LDACBT_EQMID_AUTO);
+ spa_pod_builder_int(b, LDACBT_EQMID_HQ);
+ spa_pod_builder_int(b, LDACBT_EQMID_SQ);
+ spa_pod_builder_int(b, LDACBT_EQMID_MQ);
+ 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]);
+ spa_pod_builder_int(b, LDACBT_EQMID_AUTO);
+ spa_pod_builder_string(b, "auto");
+ spa_pod_builder_int(b, LDACBT_EQMID_HQ);
+ spa_pod_builder_string(b, "hq");
+ spa_pod_builder_int(b, LDACBT_EQMID_SQ);
+ spa_pod_builder_string(b, "sq");
+ spa_pod_builder_int(b, LDACBT_EQMID_MQ);
+ spa_pod_builder_string(b, "mq");
+ spa_pod_builder_pop(b, &f[1]);
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (idx) {
+ case 0:
+ *param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_quality, SPA_POD_Int(p->eqmid));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 1;
+}
+
+static int codec_set_props(void *props, const struct spa_pod *param)
+{
+ struct props *p = props;
+ const int prev_eqmid = p->eqmid;
+ if (param == NULL) {
+ p->eqmid = LDACBT_EQMID_AUTO;
+ } else {
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_quality, SPA_POD_OPT_Int(&p->eqmid));
+ if (p->eqmid != LDACBT_EQMID_AUTO &&
+ (p->eqmid < LDACBT_EQMID_HQ || p->eqmid > LDACBT_EQMID_MQ))
+ p->eqmid = prev_eqmid;
+ }
+
+ return prev_eqmid != p->eqmid;
+}
+
+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;
+ a2dp_ldac_t *conf = config;
+ int res;
+ struct props *p = props;
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL)
+ goto error_errno;
+
+ this->ldac = ldacBT_get_handle();
+ if (this->ldac == NULL)
+ goto error_errno;
+
+#ifdef ENABLE_LDAC_ABR
+ this->ldac_abr = ldac_ABR_get_handle();
+ if (this->ldac_abr == NULL)
+ goto error_errno;
+#endif
+
+ if (p == NULL || p->eqmid == LDACBT_EQMID_AUTO) {
+ this->eqmid = LDACBT_EQMID_SQ;
+ this->enable_abr = true;
+ } else {
+ this->eqmid = p->eqmid;
+ this->enable_abr = false;
+ }
+
+ this->mtu = mtu;
+ this->frequency = info->info.raw.rate;
+ this->codesize = info->info.raw.channels * LDACBT_ENC_LSU;
+
+ switch (info->info.raw.format) {
+ case SPA_AUDIO_FORMAT_F32:
+ this->fmt = LDACBT_SMPL_FMT_F32;
+ this->codesize *= 4;
+ break;
+ case SPA_AUDIO_FORMAT_S32:
+ this->fmt = LDACBT_SMPL_FMT_S32;
+ this->codesize *= 4;
+ break;
+ case SPA_AUDIO_FORMAT_S24:
+ this->fmt = LDACBT_SMPL_FMT_S24;
+ this->codesize *= 3;
+ break;
+ case SPA_AUDIO_FORMAT_S16:
+ this->fmt = LDACBT_SMPL_FMT_S16;
+ this->codesize *= 2;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ res = ldacBT_init_handle_encode(this->ldac,
+ this->mtu,
+ this->eqmid,
+ conf->channel_mode,
+ this->fmt,
+ this->frequency);
+ if (res < 0)
+ goto error;
+
+#ifdef ENABLE_LDAC_ABR
+ res = ldac_ABR_Init(this->ldac_abr, LDAC_ABR_INTERVAL_MS);
+ if (res < 0)
+ goto error;
+
+ res = ldac_ABR_set_thresholds(this->ldac_abr,
+ LDAC_ABR_THRESHOLD_CRITICAL,
+ LDAC_ABR_THRESHOLD_DANGEROUSTREND,
+ LDAC_ABR_THRESHOLD_SAFETY_FOR_HQSQ);
+ if (res < 0)
+ goto error;
+#endif
+
+ return this;
+
+error_errno:
+ res = -errno;
+error:
+ if (this && this->ldac)
+ ldacBT_free_handle(this->ldac);
+#ifdef ENABLE_LDAC_ABR
+ if (this && this->ldac_abr)
+ ldac_ABR_free_handle(this->ldac_abr);
+#endif
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ if (this->ldac)
+ ldacBT_free_handle(this->ldac);
+#ifdef ENABLE_LDAC_ABR
+ if (this->ldac_abr)
+ ldac_ABR_free_handle(this->ldac_abr);
+#endif
+ free(this);
+}
+
+static int codec_update_props(void *data, void *props)
+{
+ struct impl *this = data;
+ struct props *p = props;
+ int res;
+
+ if (p == NULL)
+ return 0;
+
+ if (p->eqmid == LDACBT_EQMID_AUTO) {
+ this->eqmid = LDACBT_EQMID_SQ;
+ this->enable_abr = true;
+ } else {
+ this->eqmid = p->eqmid;
+ this->enable_abr = false;
+ }
+
+ if ((res = ldacBT_set_eqmid(this->ldac, this->eqmid)) < 0)
+ goto error;
+ return 0;
+error:
+ return res;
+}
+
+static int codec_abr_process(void *data, size_t unsent)
+{
+#ifdef ENABLE_LDAC_ABR
+ struct impl *this = data;
+ int res;
+ res = ldac_ABR_Proc(this->ldac, this->ldac_abr,
+ unsent / LDAC_ABR_MAX_PACKET_NBYTES, this->enable_abr);
+ return res;
+#else
+ return -ENOTSUP;
+#endif
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+
+ this->header = (struct rtp_header *)dst;
+ this->payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+ memset(this->header, 0, sizeof(struct rtp_header)+sizeof(struct rtp_payload));
+
+ this->payload->frame_count = 0;
+ this->header->v = 2;
+ this->header->pt = 96;
+ this->header->sequence_number = htons(seqnum);
+ this->header->timestamp = htonl(timestamp);
+ this->header->ssrc = htonl(1);
+ return sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int res, src_used, dst_used, frame_num = 0;
+
+ src_used = src_size;
+ dst_used = dst_size;
+
+ res = ldacBT_encode(this->ldac, (void*)src, &src_used, dst, &dst_used, &frame_num);
+ if (SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+
+ *dst_out = dst_used;
+
+ this->payload->frame_count += frame_num;
+ *need_flush = (this->payload->frame_count > 0) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
+
+ return src_used;
+}
+
+const struct media_codec a2dp_codec_ldac = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
+ .codec_id = A2DP_CODEC_VENDOR,
+ .vendor = { .vendor_id = LDAC_VENDOR_ID,
+ .codec_id = LDAC_CODEC_ID },
+ .name = "ldac",
+ .description = "LDAC",
+#ifdef ENABLE_LDAC_ABR
+ .send_buf_size = LDAC_ABR_SOCK_BUFFER_SIZE,
+#endif
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .init_props = codec_init_props,
+ .enum_props = codec_enum_props,
+ .set_props = codec_set_props,
+ .clear_props = codec_clear_props,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .update_props = codec_update_props,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "ldac",
+ &a2dp_codec_ldac
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c
new file mode 100644
index 0000000..32ae290
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-opus.c
@@ -0,0 +1,1444 @@
+/* Spa A2DP Opus Codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <string.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#if __BYTE_ORDER != __LITTLE_ENDIAN
+#include <byteswap.h>
+#endif
+
+#include <spa/debug/types.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/audio/raw.h>
+#include <spa/utils/string.h>
+#include <spa/utils/dict.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <opus.h>
+#include <opus_multistream.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+static struct spa_log *log;
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.opus");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define BUFSIZE_FROM_BITRATE(frame_dms,bitrate) ((bitrate)/8 * (frame_dms) / 10000 * 5/4) /* estimate */
+
+/*
+ * Opus CVBR target bitrate. When connecting, it is set to the INITIAL
+ * value, and after that adjusted according to link quality between the MIN and
+ * MAX values. The bitrate adjusts up to either MAX or the value at
+ * which the socket buffer starts filling up, whichever is lower.
+ *
+ * With perfect connection quality, the target bitrate converges to the MAX
+ * value. Under realistic conditions, the upper limit may often be as low as
+ * 300-500kbit/s, so the INITIAL values are not higher than this.
+ *
+ * The MAX is here set to 2-2.5x and INITIAL to 1.5x the upper Opus recommended
+ * values [1], to be safer quality-wise for CVBR, and MIN to the lower
+ * recommended value.
+ *
+ * [1] https://wiki.xiph.org/Opus_Recommended_Settings
+ */
+#define BITRATE_INITIAL 192000
+#define BITRATE_MAX 320000
+#define BITRATE_MIN 96000
+
+#define BITRATE_INITIAL_51 384000
+#define BITRATE_MAX_51 600000
+#define BITRATE_MIN_51 128000
+
+#define BITRATE_INITIAL_71 450000
+#define BITRATE_MAX_71 900000
+#define BITRATE_MIN_71 256000
+
+#define BITRATE_DUPLEX_BIDI 160000
+
+#define OPUS_05_MAX_BYTES (15 * 1024)
+
+struct props {
+ uint32_t channels;
+ uint32_t coupled_streams;
+ uint32_t location;
+ uint32_t max_bitrate;
+ uint8_t frame_duration;
+ int application;
+
+ uint32_t bidi_channels;
+ uint32_t bidi_coupled_streams;
+ uint32_t bidi_location;
+ uint32_t bidi_max_bitrate;
+ uint32_t bidi_frame_duration;
+ int bidi_application;
+};
+
+struct dec_data {
+ int fragment_size;
+ int fragment_count;
+ uint8_t fragment[OPUS_05_MAX_BYTES];
+};
+
+struct abr {
+ uint64_t now;
+ uint64_t last_update;
+
+ uint32_t buffer_level;
+ uint32_t packet_size;
+ uint32_t total_size;
+ bool bad;
+
+ uint64_t last_change;
+ uint64_t retry_interval;
+
+ bool prev_bad;
+};
+
+struct enc_data {
+ struct rtp_header *header;
+ struct rtp_payload *payload;
+
+ struct abr abr;
+
+ int samples;
+ int codesize;
+
+ int packet_size;
+ int fragment_size;
+ int fragment_count;
+ void *fragment;
+
+ int bitrate_min;
+ int bitrate_max;
+
+ int bitrate;
+ int next_bitrate;
+
+ int frame_dms;
+ int application;
+};
+
+struct impl {
+ OpusMSEncoder *enc;
+ OpusMSDecoder *dec;
+
+ int mtu;
+ int samplerate;
+ int application;
+
+ uint8_t channels;
+ uint8_t streams;
+ uint8_t coupled_streams;
+
+ bool is_bidi;
+
+ struct dec_data d;
+ struct enc_data e;
+};
+
+struct audio_location {
+ uint32_t mask;
+ enum spa_audio_channel position;
+};
+
+struct surround_encoder_mapping {
+ uint8_t channels;
+ uint8_t coupled_streams;
+ uint32_t location;
+ uint8_t mapping[8]; /**< permutation streams -> vorbis order */
+ uint8_t inv_mapping[8]; /**< permutation vorbis order -> streams */
+};
+
+/* Bluetooth SIG, Assigned Numbers, Generic Audio, Audio Location Definitions */
+#define BT_AUDIO_LOCATION_FL 0x00000001 /* Front Left */
+#define BT_AUDIO_LOCATION_FR 0x00000002 /* Front Right */
+#define BT_AUDIO_LOCATION_FC 0x00000004 /* Front Center */
+#define BT_AUDIO_LOCATION_LFE 0x00000008 /* Low Frequency Effects 1 */
+#define BT_AUDIO_LOCATION_RL 0x00000010 /* Back Left */
+#define BT_AUDIO_LOCATION_RR 0x00000020 /* Back Right */
+#define BT_AUDIO_LOCATION_FLC 0x00000040 /* Front Left of Center */
+#define BT_AUDIO_LOCATION_FRC 0x00000080 /* Front Right of Center */
+#define BT_AUDIO_LOCATION_RC 0x00000100 /* Back Center */
+#define BT_AUDIO_LOCATION_LFE2 0x00000200 /* Low Frequency Effects 2 */
+#define BT_AUDIO_LOCATION_SL 0x00000400 /* Side Left */
+#define BT_AUDIO_LOCATION_SR 0x00000800 /* Side Right */
+#define BT_AUDIO_LOCATION_TFL 0x00001000 /* Top Front Left */
+#define BT_AUDIO_LOCATION_TFR 0x00002000 /* Top Front Right */
+#define BT_AUDIO_LOCATION_TFC 0x00004000 /* Top Front Center */
+#define BT_AUDIO_LOCATION_TC 0x00008000 /* Top Center */
+#define BT_AUDIO_LOCATION_TRL 0x00010000 /* Top Back Left */
+#define BT_AUDIO_LOCATION_TRR 0x00020000 /* Top Back Right */
+#define BT_AUDIO_LOCATION_TSL 0x00040000 /* Top Side Left */
+#define BT_AUDIO_LOCATION_TSR 0x00080000 /* Top Side Right */
+#define BT_AUDIO_LOCATION_TRC 0x00100000 /* Top Back Center */
+#define BT_AUDIO_LOCATION_BC 0x00200000 /* Bottom Front Center */
+#define BT_AUDIO_LOCATION_BLC 0x00400000 /* Bottom Front Left */
+#define BT_AUDIO_LOCATION_BRC 0x00800000 /* Bottom Front Right */
+#define BT_AUDIO_LOCATION_FLW 0x01000000 /* Fron Left Wide */
+#define BT_AUDIO_LOCATION_FRW 0x02000000 /* Front Right Wide */
+#define BT_AUDIO_LOCATION_SSL 0x04000000 /* Left Surround */
+#define BT_AUDIO_LOCATION_SSR 0x08000000 /* Right Surround */
+
+#define BT_AUDIO_LOCATION_ANY 0x0fffffff
+
+static const struct audio_location audio_locations[] = {
+ { BT_AUDIO_LOCATION_FL, SPA_AUDIO_CHANNEL_FL },
+ { BT_AUDIO_LOCATION_FR, SPA_AUDIO_CHANNEL_FR },
+ { BT_AUDIO_LOCATION_SL, SPA_AUDIO_CHANNEL_SL },
+ { BT_AUDIO_LOCATION_SR, SPA_AUDIO_CHANNEL_SR },
+ { BT_AUDIO_LOCATION_RL, SPA_AUDIO_CHANNEL_RL },
+ { BT_AUDIO_LOCATION_RR, SPA_AUDIO_CHANNEL_RR },
+ { BT_AUDIO_LOCATION_FLC, SPA_AUDIO_CHANNEL_FLC },
+ { BT_AUDIO_LOCATION_FRC, SPA_AUDIO_CHANNEL_FRC },
+ { BT_AUDIO_LOCATION_TFL, SPA_AUDIO_CHANNEL_TFL },
+ { BT_AUDIO_LOCATION_TFR, SPA_AUDIO_CHANNEL_TFR },
+ { BT_AUDIO_LOCATION_TSL, SPA_AUDIO_CHANNEL_TSL },
+ { BT_AUDIO_LOCATION_TSR, SPA_AUDIO_CHANNEL_TSR },
+ { BT_AUDIO_LOCATION_TRL, SPA_AUDIO_CHANNEL_TRL },
+ { BT_AUDIO_LOCATION_TRR, SPA_AUDIO_CHANNEL_TRR },
+ { BT_AUDIO_LOCATION_BLC, SPA_AUDIO_CHANNEL_BLC },
+ { BT_AUDIO_LOCATION_BRC, SPA_AUDIO_CHANNEL_BRC },
+ { BT_AUDIO_LOCATION_FLW, SPA_AUDIO_CHANNEL_FLW },
+ { BT_AUDIO_LOCATION_FRW, SPA_AUDIO_CHANNEL_FRW },
+ { BT_AUDIO_LOCATION_SSL, SPA_AUDIO_CHANNEL_SL }, /* ~ Side Left */
+ { BT_AUDIO_LOCATION_SSR, SPA_AUDIO_CHANNEL_SR }, /* ~ Side Right */
+ { BT_AUDIO_LOCATION_FC, SPA_AUDIO_CHANNEL_FC },
+ { BT_AUDIO_LOCATION_RC, SPA_AUDIO_CHANNEL_RC },
+ { BT_AUDIO_LOCATION_TFC, SPA_AUDIO_CHANNEL_TFC },
+ { BT_AUDIO_LOCATION_TC, SPA_AUDIO_CHANNEL_TC },
+ { BT_AUDIO_LOCATION_TRC, SPA_AUDIO_CHANNEL_TRC },
+ { BT_AUDIO_LOCATION_BC, SPA_AUDIO_CHANNEL_BC },
+ { BT_AUDIO_LOCATION_LFE, SPA_AUDIO_CHANNEL_LFE },
+ { BT_AUDIO_LOCATION_LFE2, SPA_AUDIO_CHANNEL_LFE2 },
+};
+
+/* Opus surround encoder mapping tables for the supported channel configurations */
+static const struct surround_encoder_mapping surround_encoders[] = {
+ { 1, 0, (0x0),
+ { 0 }, { 0 } },
+ { 2, 1, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR),
+ { 0, 1 }, { 0, 1 } },
+ { 3, 1, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_FC),
+ { 0, 2, 1 }, { 0, 2, 1 } },
+ { 4, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+ BT_AUDIO_LOCATION_RR),
+ { 0, 1, 2, 3 }, { 0, 1, 2, 3 } },
+ { 5, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+ BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC),
+ { 0, 4, 1, 2, 3 }, { 0, 2, 3, 4, 1 } },
+ { 6, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL |
+ BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC |
+ BT_AUDIO_LOCATION_LFE),
+ { 0, 4, 1, 2, 3, 5 }, { 0, 2, 3, 4, 1, 5 } },
+ { 7, 3, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_SL |
+ BT_AUDIO_LOCATION_SR | BT_AUDIO_LOCATION_FC |
+ BT_AUDIO_LOCATION_RC | BT_AUDIO_LOCATION_LFE),
+ { 0, 4, 1, 2, 3, 5, 6 }, { 0, 2, 3, 4, 1, 5, 6 } },
+ { 8, 3, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_SL |
+ BT_AUDIO_LOCATION_SR | BT_AUDIO_LOCATION_RL |
+ BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC |
+ BT_AUDIO_LOCATION_LFE),
+ { 0, 6, 1, 2, 3, 4, 5, 7 }, { 0, 2, 3, 4, 5, 6, 1, 7 } },
+};
+
+static uint32_t bt_channel_from_name(const char *name)
+{
+ size_t i;
+ enum spa_audio_channel position = SPA_AUDIO_CHANNEL_UNKNOWN;
+
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) {
+ position = spa_type_audio_channel[i].type;
+ break;
+ }
+ }
+ for (i = 0; i < SPA_N_ELEMENTS(audio_locations); i++) {
+ if (position == audio_locations[i].position)
+ return audio_locations[i].mask;
+ }
+ return 0;
+}
+
+static uint32_t parse_locations(const char *str)
+{
+ char *s, *p, *save = NULL;
+ uint32_t location = 0;
+
+ if (!str)
+ return 0;
+
+ s = strdup(str);
+ if (s == NULL)
+ return 0;
+
+ for (p = s; (p = strtok_r(p, ", ", &save)) != NULL; p = NULL) {
+ if (*p == '\0')
+ continue;
+ location |= bt_channel_from_name(p);
+ }
+ free(s);
+
+ return location;
+}
+
+static void parse_settings(struct props *props, const struct spa_dict *settings)
+{
+ const char *str;
+ uint32_t v;
+
+ /* Pro Audio settings */
+ spa_zero(*props);
+ props->channels = 8;
+ props->coupled_streams = 0;
+ props->location = 0;
+ props->max_bitrate = BITRATE_MAX;
+ props->frame_duration = OPUS_05_FRAME_DURATION_100;
+ props->application = OPUS_APPLICATION_AUDIO;
+
+ props->bidi_channels = 1;
+ props->bidi_coupled_streams = 0;
+ props->bidi_location = 0;
+ props->bidi_max_bitrate = BITRATE_DUPLEX_BIDI;
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_400;
+ props->bidi_application = OPUS_APPLICATION_AUDIO;
+
+ if (settings == NULL)
+ return;
+
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.channels"), &v, 0))
+ props->channels = SPA_CLAMP(v, 1u, SPA_AUDIO_MAX_CHANNELS);
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.max-bitrate"), &v, 0))
+ props->max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN);
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.coupled-streams"), &v, 0))
+ props->coupled_streams = SPA_CLAMP(v, 0u, props->channels / 2);
+
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.channels"), &v, 0))
+ props->bidi_channels = SPA_CLAMP(v, 0u, SPA_AUDIO_MAX_CHANNELS);
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.max-bitrate"), &v, 0))
+ props->bidi_max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN);
+ if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.coupled-streams"), &v, 0))
+ props->bidi_coupled_streams = SPA_CLAMP(v, 0u, props->bidi_channels / 2);
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.locations");
+ props->location = parse_locations(str);
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.locations");
+ props->bidi_location = parse_locations(str);
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.frame-dms");
+ if (spa_streq(str, "25"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_25;
+ else if (spa_streq(str, "50"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_50;
+ else if (spa_streq(str, "100"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_100;
+ else if (spa_streq(str, "200"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_200;
+ else if (spa_streq(str, "400"))
+ props->frame_duration = OPUS_05_FRAME_DURATION_400;
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.frame-dms");
+ if (spa_streq(str, "25"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_25;
+ else if (spa_streq(str, "50"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_50;
+ else if (spa_streq(str, "100"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_100;
+ else if (spa_streq(str, "200"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_200;
+ else if (spa_streq(str, "400"))
+ props->bidi_frame_duration = OPUS_05_FRAME_DURATION_400;
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.application");
+ if (spa_streq(str, "audio"))
+ props->application = OPUS_APPLICATION_AUDIO;
+ else if (spa_streq(str, "voip"))
+ props->application = OPUS_APPLICATION_VOIP;
+ else if (spa_streq(str, "lowdelay"))
+ props->application = OPUS_APPLICATION_RESTRICTED_LOWDELAY;
+
+
+ str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.application");
+ if (spa_streq(str, "audio"))
+ props->bidi_application = OPUS_APPLICATION_AUDIO;
+ else if (spa_streq(str, "voip"))
+ props->bidi_application = OPUS_APPLICATION_VOIP;
+ else if (spa_streq(str, "lowdelay"))
+ props->bidi_application = OPUS_APPLICATION_RESTRICTED_LOWDELAY;
+}
+
+static int set_channel_conf(const struct media_codec *codec, a2dp_opus_05_t *caps, const struct props *props)
+{
+ /*
+ * Predefined codec profiles
+ */
+ if (caps->main.channels < 1)
+ return -EINVAL;
+
+ caps->main.coupled_streams = 0;
+ OPUS_05_SET_LOCATION(caps->main, 0);
+
+ caps->bidi.coupled_streams = 0;
+ OPUS_05_SET_LOCATION(caps->bidi, 0);
+
+ switch (codec->id) {
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05:
+ caps->main.channels = SPA_MIN(2, caps->main.channels);
+ if (caps->main.channels == 2) {
+ caps->main.coupled_streams = surround_encoders[1].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, surround_encoders[1].location);
+ }
+ caps->bidi.channels = 0;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51:
+ if (caps->main.channels < 6)
+ return -EINVAL;
+ caps->main.channels = surround_encoders[5].channels;
+ caps->main.coupled_streams = surround_encoders[5].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, surround_encoders[5].location);
+ caps->bidi.channels = 0;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71:
+ if (caps->main.channels < 8)
+ return -EINVAL;
+ caps->main.channels = surround_encoders[7].channels;
+ caps->main.coupled_streams = surround_encoders[7].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, surround_encoders[7].location);
+ caps->bidi.channels = 0;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX:
+ if (caps->bidi.channels < 1)
+ return -EINVAL;
+ caps->main.channels = SPA_MIN(2, caps->main.channels);
+ if (caps->main.channels == 2) {
+ caps->main.coupled_streams = surround_encoders[1].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, surround_encoders[1].location);
+ }
+ caps->bidi.channels = SPA_MIN(2, caps->bidi.channels);
+ if (caps->bidi.channels == 2) {
+ caps->bidi.coupled_streams = surround_encoders[1].coupled_streams;
+ OPUS_05_SET_LOCATION(caps->bidi, surround_encoders[1].location);
+ }
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO:
+ if (caps->main.channels < props->channels)
+ return -EINVAL;
+ if (props->bidi_channels == 0 && caps->bidi.channels != 0)
+ return -EINVAL;
+ if (caps->bidi.channels < props->bidi_channels)
+ return -EINVAL;
+ caps->main.channels = props->channels;
+ caps->main.coupled_streams = props->coupled_streams;
+ OPUS_05_SET_LOCATION(caps->main, props->location);
+ caps->bidi.channels = props->bidi_channels;
+ caps->bidi.coupled_streams = props->bidi_coupled_streams;
+ OPUS_05_SET_LOCATION(caps->bidi, props->bidi_location);
+ break;
+ default:
+ spa_assert(false);
+ };
+
+ return 0;
+}
+
+static void get_default_bitrates(const struct media_codec *codec, bool bidi, int *min, int *max, int *init)
+{
+ int tmp;
+
+ if (min == NULL)
+ min = &tmp;
+ if (max == NULL)
+ max = &tmp;
+ if (init == NULL)
+ init = &tmp;
+
+ if (bidi) {
+ *min = SPA_MIN(BITRATE_MIN, BITRATE_DUPLEX_BIDI);
+ *max = BITRATE_DUPLEX_BIDI;
+ *init = BITRATE_DUPLEX_BIDI;
+ return;
+ }
+
+ switch (codec->id) {
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05:
+ *min = BITRATE_MIN;
+ *max = BITRATE_MAX;
+ *init = BITRATE_INITIAL;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51:
+ *min = BITRATE_MIN_51;
+ *max = BITRATE_MAX_51;
+ *init = BITRATE_INITIAL_51;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71:
+ *min = BITRATE_MIN_71;
+ *max = BITRATE_MAX_71;
+ *init = BITRATE_INITIAL_71;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX:
+ *min = BITRATE_MIN;
+ *max = BITRATE_MAX;
+ *init = BITRATE_INITIAL;
+ break;
+ case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO:
+ default:
+ spa_assert_not_reached();
+ };
+}
+
+static int get_mapping(const struct media_codec *codec, const a2dp_opus_05_direction_t *conf,
+ bool use_surround_encoder, uint8_t *streams_ret, uint8_t *coupled_streams_ret,
+ const uint8_t **surround_mapping, uint32_t *positions)
+{
+ const uint8_t channels = conf->channels;
+ const uint32_t location = OPUS_05_GET_LOCATION(*conf);
+ const uint8_t coupled_streams = conf->coupled_streams;
+ const uint8_t *permutation = NULL;
+ size_t i, j;
+
+ if (channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+ if (2 * coupled_streams > channels)
+ return -EINVAL;
+
+ if (streams_ret)
+ *streams_ret = channels - coupled_streams;
+ if (coupled_streams_ret)
+ *coupled_streams_ret = coupled_streams;
+
+ if (channels == 0)
+ return 0;
+
+ if (use_surround_encoder) {
+ /* Opus surround encoder supports only some channel configurations, and
+ * needs a specific input channel ordering */
+ for (i = 0; i < SPA_N_ELEMENTS(surround_encoders); ++i) {
+ const struct surround_encoder_mapping *m = &surround_encoders[i];
+
+ if (m->channels == channels &&
+ m->coupled_streams == coupled_streams &&
+ m->location == location)
+ {
+ spa_assert(channels <= SPA_N_ELEMENTS(m->inv_mapping));
+ permutation = m->inv_mapping;
+ if (surround_mapping)
+ *surround_mapping = m->mapping;
+ break;
+ }
+ }
+ if (permutation == NULL && surround_mapping)
+ *surround_mapping = NULL;
+ }
+
+ if (positions) {
+ for (i = 0, j = 0; i < SPA_N_ELEMENTS(audio_locations) && j < channels; ++i) {
+ const struct audio_location loc = audio_locations[i];
+
+ if (location & loc.mask) {
+ if (permutation)
+ positions[permutation[j++]] = loc.position;
+ else
+ positions[j++] = loc.position;
+ }
+ }
+ for (i = SPA_AUDIO_CHANNEL_START_Aux; j < channels; ++i, ++j)
+ positions[j] = i;
+ }
+
+ return 0;
+}
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_opus_05_t a2dp_opus_05 = {
+ .info = codec->vendor,
+ .main = {
+ .channels = SPA_AUDIO_MAX_CHANNELS,
+ .frame_duration = (OPUS_05_FRAME_DURATION_25 |
+ OPUS_05_FRAME_DURATION_50 |
+ OPUS_05_FRAME_DURATION_100 |
+ OPUS_05_FRAME_DURATION_200 |
+ OPUS_05_FRAME_DURATION_400),
+ OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY)
+ OPUS_05_INIT_BITRATE(0)
+ },
+ .bidi = {
+ .channels = SPA_AUDIO_MAX_CHANNELS,
+ .frame_duration = (OPUS_05_FRAME_DURATION_25 |
+ OPUS_05_FRAME_DURATION_50 |
+ OPUS_05_FRAME_DURATION_100 |
+ OPUS_05_FRAME_DURATION_200 |
+ OPUS_05_FRAME_DURATION_400),
+ OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY)
+ OPUS_05_INIT_BITRATE(0)
+ }
+ };
+
+ /* Only duplex/pro codec has bidi, since bluez5-device has to know early
+ * whether to show nodes or not. */
+ if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX &&
+ codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+ spa_zero(a2dp_opus_05.bidi);
+
+ memcpy(caps, &a2dp_opus_05, sizeof(a2dp_opus_05));
+ return sizeof(a2dp_opus_05);
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ struct props props;
+ a2dp_opus_05_t conf;
+ int res;
+ int max;
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (codec->vendor.vendor_id != conf.info.vendor_id ||
+ codec->vendor.codec_id != conf.info.codec_id)
+ return -ENOTSUP;
+
+ parse_settings(&props, global_settings);
+
+ /* Channel Configuration & Audio Location */
+ if ((res = set_channel_conf(codec, &conf, &props)) < 0)
+ return res;
+
+ /* Limits */
+ if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) {
+ max = props.max_bitrate;
+ if (OPUS_05_GET_BITRATE(conf.main) != 0)
+ OPUS_05_SET_BITRATE(conf.main, SPA_MIN(OPUS_05_GET_BITRATE(conf.main), max / 1024));
+ else
+ OPUS_05_SET_BITRATE(conf.main, max / 1024);
+
+ max = props.bidi_max_bitrate;
+ if (OPUS_05_GET_BITRATE(conf.bidi) != 0)
+ OPUS_05_SET_BITRATE(conf.bidi, SPA_MIN(OPUS_05_GET_BITRATE(conf.bidi), max / 1024));
+ else
+ OPUS_05_SET_BITRATE(conf.bidi, max / 1024);
+
+ if (conf.main.frame_duration & props.frame_duration)
+ conf.main.frame_duration = props.frame_duration;
+ else
+ return -EINVAL;
+
+ if (conf.bidi.channels == 0)
+ true;
+ else if (conf.bidi.frame_duration & props.bidi_frame_duration)
+ conf.bidi.frame_duration = props.bidi_frame_duration;
+ else
+ return -EINVAL;
+ } else {
+ if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_100)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_100;
+ else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_200)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_200;
+ else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_400)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_400;
+ else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_50)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_50;
+ else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_25)
+ conf.main.frame_duration = OPUS_05_FRAME_DURATION_25;
+ else
+ return -EINVAL;
+
+ get_default_bitrates(codec, false, NULL, &max, NULL);
+
+ if (OPUS_05_GET_BITRATE(conf.main) != 0)
+ OPUS_05_SET_BITRATE(conf.main, SPA_MIN(OPUS_05_GET_BITRATE(conf.main), max / 1024));
+ else
+ OPUS_05_SET_BITRATE(conf.main, max / 1024);
+
+ /* longer bidi frames appear to work better */
+ if (conf.bidi.channels == 0)
+ true;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_200)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_200;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_100)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_100;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_400)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_400;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_50)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_50;
+ else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_25)
+ conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_25;
+ else
+ return -EINVAL;
+
+ get_default_bitrates(codec, true, NULL, &max, NULL);
+
+ if (conf.bidi.channels == 0)
+ true;
+ else if (OPUS_05_GET_BITRATE(conf.bidi) != 0)
+ OPUS_05_SET_BITRATE(conf.bidi, SPA_MIN(OPUS_05_GET_BITRATE(conf.bidi), max / 1024));
+ else
+ OPUS_05_SET_BITRATE(conf.bidi, max / 1024);
+ }
+
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings)
+{
+ a2dp_opus_05_t conf1, conf2, cap1, cap2;
+ a2dp_opus_05_t *conf;
+ int res1, res2;
+ int a, b;
+
+ /* Order selected configurations by preference */
+ res1 = codec->select_config(codec, flags, caps1, caps1_size, info, global_settings, (uint8_t *)&conf1);
+ res2 = codec->select_config(codec, flags, caps2, caps2_size, info, global_settings, (uint8_t *)&conf2);
+
+#define PREFER_EXPR(expr) \
+ do { \
+ conf = &conf1; \
+ a = (expr); \
+ conf = &conf2; \
+ b = (expr); \
+ if (a != b) \
+ return b - a; \
+ } while (0)
+
+#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0)
+
+ /* Prefer valid */
+ a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_opus_05_t)) ? 1 : 0;
+ b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_opus_05_t)) ? 1 : 0;
+ if (!a || !b)
+ return b - a;
+
+ memcpy(&cap1, caps1, sizeof(cap1));
+ memcpy(&cap2, caps2, sizeof(cap2));
+
+ if (conf1.bidi.channels == 0 && conf2.bidi.channels == 0) {
+ /* If no bidi, prefer the SEP that has none */
+ a = (cap1.bidi.channels == 0);
+ b = (cap2.bidi.channels == 0);
+ if (a != b)
+ return b - a;
+ }
+
+ PREFER_EXPR(conf->main.channels);
+ PREFER_EXPR(conf->bidi.channels);
+ PREFER_EXPR(OPUS_05_GET_BITRATE(conf->main));
+ PREFER_EXPR(OPUS_05_GET_BITRATE(conf->bidi));
+
+ return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static bool is_duplex_codec(const struct media_codec *codec)
+{
+ return codec->id == 0;
+}
+
+static bool use_surround_encoder(const struct media_codec *codec, bool is_sink)
+{
+ if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+ return false;
+
+ if (is_duplex_codec(codec))
+ return is_sink;
+ else
+ return !is_sink;
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
+ a2dp_opus_05_t conf;
+ a2dp_opus_05_direction_t *dir;
+ struct spa_pod_frame f[1];
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (idx > 0)
+ return 0;
+
+ dir = !is_duplex_codec(codec) ? &conf.main : &conf.bidi;
+
+ if (get_mapping(codec, dir, surround_encoder, NULL, NULL, NULL, position) < 0)
+ return -EINVAL;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_F32),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_ENUM_Int(6,
+ 48000, 48000, 24000, 16000, 12000, 8000),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(dir->channels),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, dir->channels, position),
+ 0);
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
+ const a2dp_opus_05_direction_t *dir1, *dir2;
+ const a2dp_opus_05_t *conf;
+
+ if (caps == NULL || caps_size < sizeof(*conf))
+ return -EINVAL;
+
+ conf = caps;
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_F32;
+ info->info.raw.rate = 0; /* not specified by config */
+
+ if (2 * conf->main.coupled_streams > conf->main.channels)
+ return -EINVAL;
+ if (2 * conf->bidi.coupled_streams > conf->bidi.channels)
+ return -EINVAL;
+
+ if (!is_duplex_codec(codec)) {
+ dir1 = &conf->main;
+ dir2 = &conf->bidi;
+ } else {
+ dir1 = &conf->bidi;
+ dir2 = &conf->main;
+ }
+
+ info->info.raw.channels = dir1->channels;
+ if (get_mapping(codec, dir1, surround_encoder, NULL, NULL, NULL, info->info.raw.position) < 0)
+ return -EINVAL;
+ if (get_mapping(codec, dir2, surround_encoder, NULL, NULL, NULL, NULL) < 0)
+ return -EINVAL;
+
+ return 0;
+}
+
+static size_t ceildiv(size_t v, size_t divisor)
+{
+ if (v % divisor == 0)
+ return v / divisor;
+ else
+ return v / divisor + 1;
+}
+
+static bool check_bitrate_vs_frame_dms(struct impl *this, size_t bitrate)
+{
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+ size_t max_fragments = 0xf;
+ size_t payload_size = BUFSIZE_FROM_BITRATE(bitrate, this->e.frame_dms);
+ return (size_t)this->mtu >= header_size + ceildiv(payload_size, max_fragments);
+}
+
+static int parse_frame_dms(int bitfield)
+{
+ switch (bitfield) {
+ case OPUS_05_FRAME_DURATION_25:
+ return 25;
+ case OPUS_05_FRAME_DURATION_50:
+ return 50;
+ case OPUS_05_FRAME_DURATION_100:
+ return 100;
+ case OPUS_05_FRAME_DURATION_200:
+ return 200;
+ case OPUS_05_FRAME_DURATION_400:
+ return 400;
+ default:
+ return -EINVAL;
+ }
+}
+
+static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings)
+{
+ struct props *p;
+
+ if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO)
+ return NULL;
+
+ p = calloc(1, sizeof(struct props));
+ if (p == NULL)
+ return NULL;
+
+ parse_settings(p, settings);
+
+ return p;
+}
+
+static void codec_clear_props(void *props)
+{
+ free(props);
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK);
+ a2dp_opus_05_t *conf = config;
+ a2dp_opus_05_direction_t *dir;
+ struct impl *this = NULL;
+ struct spa_audio_info config_info;
+ const uint8_t *enc_mapping = NULL;
+ unsigned char mapping[256];
+ size_t i;
+ int res;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_F32) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ this->is_bidi = is_duplex_codec(codec);
+ dir = !this->is_bidi ? &conf->main : &conf->bidi;
+
+ if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0)
+ goto error;
+ if ((res = get_mapping(codec, dir, surround_encoder, &this->streams, &this->coupled_streams,
+ &enc_mapping, NULL)) < 0)
+ goto error;
+ if (config_info.info.raw.channels != info->info.raw.channels) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->mtu = mtu;
+ this->samplerate = info->info.raw.rate;
+ this->channels = config_info.info.raw.channels;
+ this->application = OPUS_APPLICATION_AUDIO;
+
+ if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO && props) {
+ struct props *p = props;
+ this->application = !this->is_bidi ? p->application :
+ p->bidi_application;
+ }
+
+ /*
+ * Setup encoder
+ */
+ if (enc_mapping) {
+ int streams, coupled_streams;
+ bool incompatible_opus_surround_encoder = false;
+
+ this->enc = opus_multistream_surround_encoder_create(
+ this->samplerate, this->channels, 1, &streams, &coupled_streams,
+ mapping, this->application, &res);
+
+ if (this->enc) {
+ /* Check surround encoder channel mapping is what we want */
+ if (streams != this->streams || coupled_streams != this->coupled_streams)
+ incompatible_opus_surround_encoder = true;
+ for (i = 0; i < this->channels; ++i)
+ if (enc_mapping[i] != mapping[i])
+ incompatible_opus_surround_encoder = true;
+ }
+
+ /* Assert: this should never happen */
+ spa_assert(!incompatible_opus_surround_encoder);
+ if (incompatible_opus_surround_encoder) {
+ res = -EINVAL;
+ goto error;
+ }
+ } else {
+ for (i = 0; i < this->channels; ++i)
+ mapping[i] = i;
+ this->enc = opus_multistream_encoder_create(
+ this->samplerate, this->channels, this->streams, this->coupled_streams,
+ mapping, this->application, &res);
+ }
+ if (this->enc == NULL) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this->e.frame_dms = parse_frame_dms(dir->frame_duration)) < 0) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) {
+ get_default_bitrates(codec, this->is_bidi, &this->e.bitrate_min,
+ &this->e.bitrate_max, &this->e.bitrate);
+ this->e.bitrate_max = SPA_MIN(this->e.bitrate_max,
+ OPUS_05_GET_BITRATE(*dir) * 1024);
+ } else {
+ this->e.bitrate_max = OPUS_05_GET_BITRATE(*dir) * 1024;
+ this->e.bitrate_min = BITRATE_MIN;
+ this->e.bitrate = BITRATE_INITIAL;
+ }
+
+ this->e.bitrate_min = SPA_MIN(this->e.bitrate_min, this->e.bitrate_max);
+ this->e.bitrate = SPA_CLAMP(this->e.bitrate, this->e.bitrate_min, this->e.bitrate_max);
+
+ this->e.next_bitrate = this->e.bitrate;
+ opus_multistream_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate));
+
+ this->e.samples = this->e.frame_dms * this->samplerate / 10000;
+ this->e.codesize = this->e.samples * (int)this->channels * sizeof(float);
+
+
+ /*
+ * Setup decoder
+ */
+ for (i = 0; i < this->channels; ++i)
+ mapping[i] = i;
+ this->dec = opus_multistream_decoder_create(
+ this->samplerate, this->channels,
+ this->streams, this->coupled_streams,
+ mapping, &res);
+ if (this->dec == NULL) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+
+error:
+ if (this && this->enc)
+ opus_multistream_encoder_destroy(this->enc);
+ if (this && this->dec)
+ opus_multistream_decoder_destroy(this->dec);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ opus_multistream_encoder_destroy(this->enc);
+ opus_multistream_decoder_destroy(this->dec);
+ free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->e.codesize;
+}
+
+static int codec_update_bitrate(struct impl *this)
+{
+ this->e.next_bitrate = SPA_CLAMP(this->e.next_bitrate,
+ this->e.bitrate_min, this->e.bitrate_max);
+
+ if (!check_bitrate_vs_frame_dms(this, this->e.next_bitrate)) {
+ this->e.next_bitrate = this->e.bitrate;
+ return 0;
+ }
+
+ this->e.bitrate = this->e.next_bitrate;
+ opus_multistream_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate));
+ return 0;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ if (dst_size <= header_size)
+ return -EINVAL;
+
+ codec_update_bitrate(this);
+
+ this->e.header = (struct rtp_header *)dst;
+ this->e.payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+ memset(dst, 0, header_size);
+
+ this->e.payload->frame_count = 0;
+ this->e.header->v = 2;
+ this->e.header->pt = 96;
+ this->e.header->sequence_number = htons(seqnum);
+ this->e.header->timestamp = htonl(timestamp);
+ this->e.header->ssrc = htonl(1);
+
+ this->e.packet_size = header_size;
+ return this->e.packet_size;
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ const int header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+ int size;
+ int res;
+
+ if (src == NULL) {
+ /* Produce fragment packets.
+ *
+ * We assume the caller gives the same buffer here as in the previous
+ * calls to encode(), without changes in the buffer content.
+ */
+ if (this->e.fragment == NULL ||
+ this->e.fragment_count <= 1 ||
+ this->e.fragment < dst ||
+ SPA_PTROFF(this->e.fragment, this->e.fragment_size, void) > SPA_PTROFF(dst, dst_size, void)) {
+ this->e.fragment = NULL;
+ return -EINVAL;
+ }
+
+ size = SPA_MIN(this->mtu - header_size, this->e.fragment_size);
+ memmove(dst, this->e.fragment, size);
+ *dst_out = size;
+
+ this->e.payload->is_fragmented = 1;
+ this->e.payload->frame_count = --this->e.fragment_count;
+ this->e.payload->is_last_fragment = (this->e.fragment_count == 1);
+
+ if (this->e.fragment_size > size && this->e.fragment_count > 1) {
+ this->e.fragment = SPA_PTROFF(this->e.fragment, size, void);
+ this->e.fragment_size -= size;
+ *need_flush = NEED_FLUSH_FRAGMENT;
+ } else {
+ this->e.fragment = NULL;
+ *need_flush = NEED_FLUSH_ALL;
+ }
+ return 0;
+ }
+
+ if (src_size < (size_t)this->e.codesize) {
+ *dst_out = 0;
+ return 0;
+ }
+
+ res = opus_multistream_encode_float(
+ this->enc, src, this->e.samples, dst, dst_size);
+ if (res < 0)
+ return -EINVAL;
+ *dst_out = res;
+
+ this->e.packet_size += res;
+ this->e.payload->frame_count++;
+
+ if (this->e.packet_size > this->mtu) {
+ /* Fragment packet */
+ this->e.fragment_count = ceildiv(this->e.packet_size - header_size,
+ this->mtu - header_size);
+
+ this->e.payload->is_fragmented = 1;
+ this->e.payload->is_first_fragment = 1;
+ this->e.payload->frame_count = this->e.fragment_count;
+
+ this->e.fragment_size = this->e.packet_size - this->mtu;
+ this->e.fragment = SPA_PTROFF(dst, *dst_out - this->e.fragment_size, void);
+ *need_flush = NEED_FLUSH_FRAGMENT;
+
+ /*
+ * We keep the rest of the encoded frame in the same buffer, and rely
+ * that the caller won't overwrite it before the next call to encode()
+ */
+ *dst_out = SPA_PTRDIFF(this->e.fragment, dst);
+ } else {
+ *need_flush = NEED_FLUSH_ALL;
+ }
+
+ return this->e.codesize;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ struct impl *this = data;
+ const struct rtp_header *header = src;
+ const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void);
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+
+ if (payload->is_fragmented) {
+ if (payload->is_first_fragment) {
+ this->d.fragment_size = 0;
+ } else if (payload->frame_count + 1 != this->d.fragment_count ||
+ (payload->frame_count == 1 && !payload->is_last_fragment)){
+ /* Fragments not in right order: drop packet */
+ return -EINVAL;
+ }
+ this->d.fragment_count = payload->frame_count;
+ } else {
+ if (payload->frame_count != 1)
+ return -EINVAL;
+ this->d.fragment_count = 0;
+ }
+
+ return header_size;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ int consumed = src_size;
+ int res;
+ int dst_samples;
+
+ if (this->d.fragment_count > 0) {
+ /* Fragmented frame */
+ size_t avail;
+ avail = SPA_MIN(sizeof(this->d.fragment) - this->d.fragment_size, src_size);
+ memcpy(SPA_PTROFF(this->d.fragment, this->d.fragment_size, void), src, avail);
+
+ this->d.fragment_size += avail;
+
+ if (this->d.fragment_count > 1) {
+ /* More fragments to come */
+ *dst_out = 0;
+ return consumed;
+ }
+
+ src = this->d.fragment;
+ src_size = this->d.fragment_size;
+
+ this->d.fragment_count = 0;
+ this->d.fragment_size = 0;
+ }
+
+ dst_samples = dst_size / (sizeof(float) * this->channels);
+ res = opus_multistream_decode_float(this->dec, src, src_size, dst, dst_samples, 0);
+ if (res < 0)
+ return -EINVAL;
+ *dst_out = (size_t)res * this->channels * sizeof(float);
+
+ return consumed;
+}
+
+static int codec_abr_process(void *data, size_t unsent)
+{
+ const uint64_t interval = SPA_NSEC_PER_SEC;
+ struct impl *this = data;
+ struct abr *abr = &this->e.abr;
+ bool level_bad, level_good;
+ uint32_t actual_bitrate;
+
+ abr->total_size += this->e.packet_size;
+
+ if (this->e.payload->is_fragmented && !this->e.payload->is_first_fragment)
+ return 0;
+
+ abr->now += this->e.frame_dms * SPA_NSEC_PER_MSEC / 10;
+
+ abr->buffer_level = SPA_MAX(abr->buffer_level, unsent);
+ abr->packet_size = SPA_MAX(abr->packet_size, (uint32_t)this->e.packet_size);
+ abr->packet_size = SPA_MAX(abr->packet_size, 128u);
+
+ level_bad = abr->buffer_level > 2 * (uint32_t)this->mtu || abr->bad;
+ level_good = abr->buffer_level == 0;
+
+ if (!(abr->last_update + interval <= abr->now ||
+ (level_bad && abr->last_change + interval <= abr->now)))
+ return 0;
+
+ actual_bitrate = (uint64_t)abr->total_size*8*SPA_NSEC_PER_SEC
+ / SPA_MAX(1u, abr->now - abr->last_update);
+
+ spa_log_debug(log, "opus ABR bitrate:%d actual:%d level:%d (%s) bad:%d retry:%ds size:%d",
+ (int)this->e.bitrate,
+ (int)actual_bitrate,
+ (int)abr->buffer_level,
+ level_bad ? "bad" : (level_good ? "good" : "-"),
+ (int)abr->bad,
+ (int)(abr->retry_interval / SPA_NSEC_PER_SEC),
+ (int)abr->packet_size);
+
+ if (level_bad) {
+ this->e.next_bitrate = this->e.bitrate * 11 / 12;
+ abr->last_change = abr->now;
+ abr->retry_interval = SPA_MIN(abr->retry_interval + 10*interval,
+ 30 * interval);
+ } else if (!level_good) {
+ abr->last_change = abr->now;
+ } else if (abr->now < abr->last_change + abr->retry_interval) {
+ /* noop */
+ } else if (actual_bitrate*3/2 < (uint32_t)this->e.bitrate) {
+ /* actual bitrate is small compared to target; probably silence */
+ } else {
+ this->e.next_bitrate = this->e.bitrate
+ + SPA_MAX(1, this->e.bitrate_max / 40);
+ abr->last_change = abr->now;
+ abr->retry_interval = SPA_MAX(abr->retry_interval, (5+4)*interval)
+ - 4*interval;
+ }
+
+ abr->last_update = abr->now;
+ abr->buffer_level = 0;
+ abr->bad = false;
+ abr->packet_size = 0;
+ abr->total_size = 0;
+
+ return 0;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ struct impl *this = data;
+ struct abr *abr = &this->e.abr;
+ abr->bad = true;
+ return 0;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ return 0;
+}
+
+static void codec_set_log(struct spa_log *global_log)
+{
+ log = global_log;
+ spa_log_topic_init(log, &log_topic);
+}
+
+#define OPUS_05_COMMON_DEFS \
+ .codec_id = A2DP_CODEC_VENDOR, \
+ .vendor = { .vendor_id = OPUS_05_VENDOR_ID, \
+ .codec_id = OPUS_05_CODEC_ID }, \
+ .select_config = codec_select_config, \
+ .enum_config = codec_enum_config, \
+ .validate_config = codec_validate_config, \
+ .caps_preference_cmp = codec_caps_preference_cmp, \
+ .init = codec_init, \
+ .deinit = codec_deinit, \
+ .get_block_size = codec_get_block_size, \
+ .abr_process = codec_abr_process, \
+ .start_encode = codec_start_encode, \
+ .encode = codec_encode, \
+ .reduce_bitpool = codec_reduce_bitpool, \
+ .increase_bitpool = codec_increase_bitpool, \
+ .set_log = codec_set_log
+
+#define OPUS_05_COMMON_FULL_DEFS \
+ OPUS_05_COMMON_DEFS, \
+ .start_decode = codec_start_decode, \
+ .decode = codec_decode
+
+const struct media_codec a2dp_codec_opus_05 = {
+ OPUS_05_COMMON_FULL_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
+ .name = "opus_05",
+ .description = "Opus",
+ .fill_caps = codec_fill_caps,
+};
+
+const struct media_codec a2dp_codec_opus_05_51 = {
+ OPUS_05_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51,
+ .name = "opus_05_51",
+ .description = "Opus 5.1 Surround",
+ .endpoint_name = "opus_05",
+ .fill_caps = NULL,
+};
+
+const struct media_codec a2dp_codec_opus_05_71 = {
+ OPUS_05_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71,
+ .name = "opus_05_71",
+ .description = "Opus 7.1 Surround",
+ .endpoint_name = "opus_05",
+ .fill_caps = NULL,
+};
+
+/* Bidi return channel codec: doesn't have endpoints */
+const struct media_codec a2dp_codec_opus_05_return = {
+ OPUS_05_COMMON_FULL_DEFS,
+ .id = 0,
+ .name = "opus_05_duplex_bidi",
+ .description = "Opus Duplex Bidi channel",
+};
+
+const struct media_codec a2dp_codec_opus_05_duplex = {
+ OPUS_05_COMMON_FULL_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
+ .name = "opus_05_duplex",
+ .description = "Opus Duplex",
+ .duplex_codec = &a2dp_codec_opus_05_return,
+ .fill_caps = codec_fill_caps,
+};
+
+const struct media_codec a2dp_codec_opus_05_pro = {
+ OPUS_05_COMMON_DEFS,
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
+ .name = "opus_05_pro",
+ .description = "Opus Pro Audio",
+ .init_props = codec_init_props,
+ .clear_props = codec_clear_props,
+ .duplex_codec = &a2dp_codec_opus_05_return,
+ .endpoint_name = "opus_05_duplex",
+ .fill_caps = NULL,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "opus",
+ &a2dp_codec_opus_05,
+ &a2dp_codec_opus_05_51,
+ &a2dp_codec_opus_05_71,
+ &a2dp_codec_opus_05_duplex,
+ &a2dp_codec_opus_05_pro
+);
diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c
new file mode 100644
index 0000000..27a57bd
--- /dev/null
+++ b/spa/plugins/bluez5/a2dp-codec-sbc.c
@@ -0,0 +1,689 @@
+/* Spa A2DP SBC codec
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/utils/string.h>
+
+#include <sbc/sbc.h>
+
+#include "rtp.h"
+#include "media-codecs.h"
+
+#define MAX_FRAME_COUNT 16
+
+struct impl {
+ sbc_t sbc;
+
+ struct rtp_header *header;
+ struct rtp_payload *payload;
+
+ size_t mtu;
+ int codesize;
+ int max_frames;
+
+ int min_bitpool;
+ int max_bitpool;
+};
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ static const a2dp_sbc_t a2dp_sbc = {
+ .frequency =
+ SBC_SAMPLING_FREQ_16000 |
+ SBC_SAMPLING_FREQ_32000 |
+ SBC_SAMPLING_FREQ_44100 |
+ SBC_SAMPLING_FREQ_48000,
+ .channel_mode =
+ SBC_CHANNEL_MODE_MONO |
+ SBC_CHANNEL_MODE_DUAL_CHANNEL |
+ SBC_CHANNEL_MODE_STEREO |
+ SBC_CHANNEL_MODE_JOINT_STEREO,
+ .block_length =
+ SBC_BLOCK_LENGTH_4 |
+ SBC_BLOCK_LENGTH_8 |
+ SBC_BLOCK_LENGTH_12 |
+ SBC_BLOCK_LENGTH_16,
+ .subbands =
+ SBC_SUBBANDS_4 |
+ SBC_SUBBANDS_8,
+ .allocation_method =
+ SBC_ALLOCATION_SNR |
+ SBC_ALLOCATION_LOUDNESS,
+ .min_bitpool = SBC_MIN_BITPOOL,
+ .max_bitpool = SBC_MAX_BITPOOL,
+ };
+
+ memcpy(caps, &a2dp_sbc, sizeof(a2dp_sbc));
+ return sizeof(a2dp_sbc);
+}
+
+static uint8_t default_bitpool(uint8_t freq, uint8_t mode, bool xq)
+{
+ /* A2DP spec v1.2 states that all SNK implementation shall handle bitrates
+ * of up to 512 kbps (~ bitpool = 86 stereo, or 2x43 dual channel at 44.1KHz
+ * or ~ bitpool = 78 stereo, or 2x39 dual channel at 48KHz). */
+ switch (freq) {
+ case SBC_SAMPLING_FREQ_16000:
+ case SBC_SAMPLING_FREQ_32000:
+ return 64;
+
+ case SBC_SAMPLING_FREQ_44100:
+ switch (mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ return xq ? 43 : 32;
+
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ return xq ? 86 : 64;
+ }
+ return xq ? 86 : 64;
+ case SBC_SAMPLING_FREQ_48000:
+ switch (mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ return xq ? 39 : 29;
+
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ return xq ? 78 : 58;
+ }
+ return xq ? 78 : 58;
+ }
+ return xq ? 86 : 64;
+}
+
+
+static const struct media_codec_config
+sbc_frequencies[] = {
+ { SBC_SAMPLING_FREQ_48000, 48000, 3 },
+ { SBC_SAMPLING_FREQ_44100, 44100, 2 },
+ { SBC_SAMPLING_FREQ_32000, 32000, 1 },
+ { SBC_SAMPLING_FREQ_16000, 16000, 0 },
+};
+
+static const struct media_codec_config
+sbc_xq_frequencies[] = {
+ { SBC_SAMPLING_FREQ_44100, 44100, 1 },
+ { SBC_SAMPLING_FREQ_48000, 48000, 0 },
+};
+
+static const struct media_codec_config
+sbc_channel_modes[] = {
+ { SBC_CHANNEL_MODE_JOINT_STEREO, 2, 3 },
+ { SBC_CHANNEL_MODE_STEREO, 2, 2 },
+ { SBC_CHANNEL_MODE_DUAL_CHANNEL, 2, 1 },
+ { SBC_CHANNEL_MODE_MONO, 1, 0 },
+};
+
+static const struct media_codec_config
+sbc_xq_channel_modes[] = {
+ { SBC_CHANNEL_MODE_DUAL_CHANNEL, 2, 2 },
+ { SBC_CHANNEL_MODE_JOINT_STEREO, 2, 1 },
+ { SBC_CHANNEL_MODE_STEREO, 2, 0 },
+};
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ a2dp_sbc_t conf;
+ int bitpool, i;
+ size_t n;
+ const struct media_codec_config *configs;
+ bool xq = false;
+
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ xq = (spa_streq(codec->name, "sbc_xq"));
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ if (xq) {
+ configs = sbc_xq_frequencies;
+ n = SPA_N_ELEMENTS(sbc_xq_frequencies);
+ } else {
+ configs = sbc_frequencies;
+ n = SPA_N_ELEMENTS(sbc_frequencies);
+ }
+ if ((i = media_codec_select_config(configs, n, conf.frequency,
+ info ? info->rate : A2DP_CODEC_DEFAULT_RATE
+ )) < 0)
+ return -ENOTSUP;
+ conf.frequency = configs[i].config;
+
+ if (xq) {
+ configs = sbc_xq_channel_modes;
+ n = SPA_N_ELEMENTS(sbc_xq_channel_modes);
+ } else {
+ configs = sbc_channel_modes;
+ n = SPA_N_ELEMENTS(sbc_channel_modes);
+ }
+ if ((i = media_codec_select_config(configs, n, conf.channel_mode,
+ info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS
+ )) < 0)
+ return -ENOTSUP;
+ conf.channel_mode = configs[i].config;
+
+ if (conf.block_length & SBC_BLOCK_LENGTH_16)
+ conf.block_length = SBC_BLOCK_LENGTH_16;
+ else if (conf.block_length & SBC_BLOCK_LENGTH_12)
+ conf.block_length = SBC_BLOCK_LENGTH_12;
+ else if (conf.block_length & SBC_BLOCK_LENGTH_8)
+ conf.block_length = SBC_BLOCK_LENGTH_8;
+ else if (conf.block_length & SBC_BLOCK_LENGTH_4)
+ conf.block_length = SBC_BLOCK_LENGTH_4;
+ else
+ return -ENOTSUP;
+
+ if (conf.subbands & SBC_SUBBANDS_8)
+ conf.subbands = SBC_SUBBANDS_8;
+ else if (conf.subbands & SBC_SUBBANDS_4)
+ conf.subbands = SBC_SUBBANDS_4;
+ else
+ return -ENOTSUP;
+
+ if (conf.allocation_method & SBC_ALLOCATION_LOUDNESS)
+ conf.allocation_method = SBC_ALLOCATION_LOUDNESS;
+ else if (conf.allocation_method & SBC_ALLOCATION_SNR)
+ conf.allocation_method = SBC_ALLOCATION_SNR;
+ else
+ return -ENOTSUP;
+
+ bitpool = default_bitpool(conf.frequency, conf.channel_mode, xq);
+
+ conf.min_bitpool = SPA_MAX(SBC_MIN_BITPOOL, conf.min_bitpool);
+ conf.max_bitpool = SPA_MIN(bitpool, conf.max_bitpool);
+ memcpy(config, &conf, sizeof(conf));
+
+ return sizeof(conf);
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
+{
+ a2dp_sbc_t conf1, conf2;
+ a2dp_sbc_t *conf;
+ int res1, res2;
+ int a, b;
+ bool xq = (spa_streq(codec->name, "sbc_xq"));
+
+ /* Order selected configurations by preference */
+ res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1);
+ res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2);
+
+#define PREFER_EXPR(expr) \
+ do { \
+ conf = &conf1; \
+ a = (expr); \
+ conf = &conf2; \
+ b = (expr); \
+ if (a != b) \
+ return b - a; \
+ } while (0)
+
+#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0)
+
+ /* Prefer valid */
+ a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_sbc_t)) ? 1 : 0;
+ b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_sbc_t)) ? 1 : 0;
+ if (!a || !b)
+ return b - a;
+
+ PREFER_BOOL(conf->frequency & (SBC_SAMPLING_FREQ_48000 | SBC_SAMPLING_FREQ_44100));
+
+ if (xq)
+ PREFER_BOOL(conf->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL);
+ else
+ PREFER_BOOL(conf->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO);
+
+ PREFER_EXPR(conf->max_bitpool);
+
+ return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ const a2dp_sbc_t *conf;
+
+ if (caps == NULL || caps_size < sizeof(*conf))
+ return -EINVAL;
+
+ conf = caps;
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S16;
+
+ switch (conf->frequency) {
+ case SBC_SAMPLING_FREQ_16000:
+ info->info.raw.rate = 16000;
+ break;
+ case SBC_SAMPLING_FREQ_32000:
+ info->info.raw.rate = 32000;
+ break;
+ case SBC_SAMPLING_FREQ_44100:
+ info->info.raw.rate = 44100;
+ break;
+ case SBC_SAMPLING_FREQ_48000:
+ info->info.raw.rate = 48000;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (conf->channel_mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ info->info.raw.channels = 1;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO;
+ break;
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ case SBC_CHANNEL_MODE_STEREO:
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ info->info.raw.channels = 2;
+ info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (conf->subbands) {
+ case SBC_SUBBANDS_4:
+ case SBC_SUBBANDS_8:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (conf->block_length) {
+ case SBC_BLOCK_LENGTH_4:
+ case SBC_BLOCK_LENGTH_8:
+ case SBC_BLOCK_LENGTH_12:
+ case SBC_BLOCK_LENGTH_16:
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int codec_set_bitpool(struct impl *this, int bitpool)
+{
+ size_t rtp_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ this->sbc.bitpool = SPA_CLAMP(bitpool, this->min_bitpool, this->max_bitpool);
+ this->codesize = sbc_get_codesize(&this->sbc);
+ this->max_frames = (this->mtu - rtp_size) / sbc_get_frame_length(&this->sbc);
+ if (this->max_frames > 15)
+ this->max_frames = 15;
+ return this->sbc.bitpool;
+}
+
+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)
+{
+ a2dp_sbc_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t i = 0;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+
+ if (caps_size < sizeof(conf))
+ return -EINVAL;
+
+ memcpy(&conf, caps, sizeof(conf));
+
+ 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_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.frequency & SBC_SAMPLING_FREQ_48000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.frequency & SBC_SAMPLING_FREQ_44100) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 44100);
+ spa_pod_builder_int(b, 44100);
+ }
+ if (conf.frequency & SBC_SAMPLING_FREQ_32000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 32000);
+ spa_pod_builder_int(b, 32000);
+ }
+ if (conf.frequency & SBC_SAMPLING_FREQ_16000) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 16000);
+ spa_pod_builder_int(b, 16000);
+ }
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (conf.channel_mode & SBC_CHANNEL_MODE_MONO &&
+ conf.channel_mode & (SBC_CHANNEL_MODE_JOINT_STEREO |
+ SBC_CHANNEL_MODE_STEREO | SBC_CHANNEL_MODE_DUAL_CHANNEL)) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2),
+ 0);
+ } else if (conf.channel_mode & SBC_CHANNEL_MODE_MONO) {
+ 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);
+ } else {
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, 2, position),
+ 0);
+ }
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ struct impl *this = data;
+ return codec_set_bitpool(this, this->sbc.bitpool - 2);
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ struct impl *this = data;
+ return codec_set_bitpool(this, this->sbc.bitpool + 1);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+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;
+ a2dp_sbc_t *conf = config;
+ int res;
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ sbc_init(&this->sbc, 0);
+ this->sbc.endian = SBC_LE;
+ this->mtu = mtu;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S16) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ switch (conf->frequency) {
+ case SBC_SAMPLING_FREQ_16000:
+ this->sbc.frequency = SBC_FREQ_16000;
+ break;
+ case SBC_SAMPLING_FREQ_32000:
+ this->sbc.frequency = SBC_FREQ_32000;
+ break;
+ case SBC_SAMPLING_FREQ_44100:
+ this->sbc.frequency = SBC_FREQ_44100;
+ break;
+ case SBC_SAMPLING_FREQ_48000:
+ this->sbc.frequency = SBC_FREQ_48000;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ switch (conf->channel_mode) {
+ case SBC_CHANNEL_MODE_MONO:
+ this->sbc.mode = SBC_MODE_MONO;
+ break;
+ case SBC_CHANNEL_MODE_DUAL_CHANNEL:
+ this->sbc.mode = SBC_MODE_DUAL_CHANNEL;
+ break;
+ case SBC_CHANNEL_MODE_STEREO:
+ this->sbc.mode = SBC_MODE_STEREO;
+ break;
+ case SBC_CHANNEL_MODE_JOINT_STEREO:
+ this->sbc.mode = SBC_MODE_JOINT_STEREO;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ switch (conf->subbands) {
+ case SBC_SUBBANDS_4:
+ this->sbc.subbands = SBC_SB_4;
+ break;
+ case SBC_SUBBANDS_8:
+ this->sbc.subbands = SBC_SB_8;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (conf->allocation_method & SBC_ALLOCATION_LOUDNESS)
+ this->sbc.allocation = SBC_AM_LOUDNESS;
+ else
+ this->sbc.allocation = SBC_AM_SNR;
+
+ switch (conf->block_length) {
+ case SBC_BLOCK_LENGTH_4:
+ this->sbc.blocks = SBC_BLK_4;
+ break;
+ case SBC_BLOCK_LENGTH_8:
+ this->sbc.blocks = SBC_BLK_8;
+ break;
+ case SBC_BLOCK_LENGTH_12:
+ this->sbc.blocks = SBC_BLK_12;
+ break;
+ case SBC_BLOCK_LENGTH_16:
+ this->sbc.blocks = SBC_BLK_16;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->min_bitpool = SPA_MAX(conf->min_bitpool, 12);
+ this->max_bitpool = conf->max_bitpool;
+
+ codec_set_bitpool(this, conf->max_bitpool);
+
+ return this;
+error:
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ sbc_finish(&this->sbc);
+ free(this);
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ struct impl *this = data;
+
+ this->header = (struct rtp_header *)dst;
+ this->payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload);
+ memset(this->header, 0, sizeof(struct rtp_header)+sizeof(struct rtp_payload));
+
+ this->payload->frame_count = 0;
+ this->header->v = 2;
+ this->header->pt = 96;
+ this->header->sequence_number = htons(seqnum);
+ this->header->timestamp = htonl(timestamp);
+ this->header->ssrc = htonl(1);
+ return sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int res;
+
+ res = sbc_encode(&this->sbc, src, src_size,
+ dst, dst_size, (ssize_t*)dst_out);
+ if (SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+ spa_assert(res == this->codesize);
+
+ this->payload->frame_count += res / this->codesize;
+ *need_flush = (this->payload->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO;
+ return res;
+}
+
+static int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ const struct rtp_header *header = src;
+ size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload);
+
+ spa_return_val_if_fail (src_size > header_size, -EINVAL);
+
+ if (seqnum)
+ *seqnum = ntohs(header->sequence_number);
+ if (timestamp)
+ *timestamp = ntohl(header->timestamp);
+ return header_size;
+}
+
+static int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ int res;
+
+ res = sbc_decode(&this->sbc, src, src_size,
+ dst, dst_size, dst_out);
+
+ return res;
+}
+
+const struct media_codec a2dp_codec_sbc = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC,
+ .codec_id = A2DP_CODEC_SBC,
+ .name = "sbc",
+ .description = "SBC",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .caps_preference_cmp = codec_caps_preference_cmp,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+const struct media_codec a2dp_codec_sbc_xq = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ,
+ .codec_id = A2DP_CODEC_SBC,
+ .name = "sbc_xq",
+ .description = "SBC-XQ",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .caps_preference_cmp = codec_caps_preference_cmp,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool,
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "sbc",
+ &a2dp_codec_sbc,
+ &a2dp_codec_sbc_xq
+);
diff --git a/spa/plugins/bluez5/backend-hsphfpd.c b/spa/plugins/bluez5/backend-hsphfpd.c
new file mode 100644
index 0000000..93f512d
--- /dev/null
+++ b/spa/plugins/bluez5/backend-hsphfpd.c
@@ -0,0 +1,1588 @@
+/* Spa hsphfpd backend
+ *
+ * Based on previous work for pulseaudio by: Pali Rohár <pali.rohar@gmail.com>
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <sys/socket.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/param/audio/raw.h>
+
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.hsphfpd");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl {
+ struct spa_bt_backend this;
+
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_dbus *dbus;
+ DBusConnection *conn;
+
+ const struct spa_bt_quirks *quirks;
+
+ struct spa_list endpoint_list;
+ bool endpoints_listed;
+
+ char *hsphfpd_service_id;
+
+ bool acquire_in_progress;
+
+ unsigned int filters_added:1;
+ unsigned int msbc_supported:1;
+};
+
+enum hsphfpd_volume_control {
+ HSPHFPD_VOLUME_CONTROL_NONE = 1,
+ HSPHFPD_VOLUME_CONTROL_LOCAL,
+ HSPHFPD_VOLUME_CONTROL_REMOTE,
+};
+
+enum hsphfpd_profile {
+ HSPHFPD_PROFILE_HEADSET = 1,
+ HSPHFPD_PROFILE_HANDSFREE,
+};
+
+enum hsphfpd_role {
+ HSPHFPD_ROLE_CLIENT = 1,
+ HSPHFPD_ROLE_GATEWAY,
+};
+
+struct hsphfpd_transport_data {
+ char *transport_path;
+ bool rx_soft_volume;
+ bool tx_soft_volume;
+ int rx_volume_gain;
+ int tx_volume_gain;
+ int max_rx_volume_gain;
+ int max_tx_volume_gain;
+ enum hsphfpd_volume_control rx_volume_control;
+ enum hsphfpd_volume_control tx_volume_control;
+};
+
+struct hsphfpd_endpoint {
+ struct spa_list link;
+ char *path;
+ bool valid;
+ bool connected;
+ char *remote_address;
+ char *local_address;
+ enum hsphfpd_profile profile;
+ enum hsphfpd_role role;
+ int air_codecs;
+};
+
+#define DBUS_INTERFACE_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager"
+
+#define HSPHFPD_APPLICATION_MANAGER_INTERFACE HSPHFPD_SERVICE ".ApplicationManager"
+#define HSPHFPD_ENDPOINT_INTERFACE HSPHFPD_SERVICE ".Endpoint"
+#define HSPHFPD_AUDIO_AGENT_INTERFACE HSPHFPD_SERVICE ".AudioAgent"
+#define HSPHFPD_AUDIO_TRANSPORT_INTERFACE HSPHFPD_SERVICE ".AudioTransport"
+
+#define APPLICATION_OBJECT_MANAGER_PATH "/Profile/hsphfpd/manager"
+#define HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ "/Profile/hsphfpd/pcm_s16le_8khz_agent"
+#define HSPHFP_AUDIO_CLIENT_MSBC "/Profile/hsphfpd/msbc_agent"
+
+#define HSPHFP_AIR_CODEC_CVSD "CVSD"
+#define HSPHFP_AIR_CODEC_MSBC "mSBC"
+#define HSPHFP_AGENT_CODEC_PCM "PCM_s16le_8kHz"
+#define HSPHFP_AGENT_CODEC_MSBC "mSBC"
+
+#define APPLICATION_OBJECT_MANAGER_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>\n" \
+ " <interface name=\"" DBUS_INTERFACE_OBJECTMANAGER "\">\n" \
+ " <method name=\"GetManagedObjects\">\n" \
+ " <arg name=\"objects\" direction=\"out\" type=\"a{oa{sa{sv}}}\"/>\n" \
+ " </method>\n" \
+ " <signal name=\"InterfacesAdded\">\n" \
+ " <arg name=\"object\" type=\"o\"/>\n" \
+ " <arg name=\"interfaces\" type=\"a{sa{sv}}\"/>\n" \
+ " </signal>\n" \
+ " <signal name=\"InterfacesRemoved\">\n" \
+ " <arg name=\"object\" type=\"o\"/>\n" \
+ " <arg name=\"interfaces\" type=\"as\"/>\n" \
+ " </signal>\n" \
+ " </interface>\n" \
+ " <interface name=\"" DBUS_INTERFACE_INTROSPECTABLE "\">\n" \
+ " <method name=\"Introspect\">\n" \
+ " <arg name=\"data\" direction=\"out\" type=\"s\"/>\n" \
+ " </method>\n" \
+ " </interface>\n" \
+ "</node>\n"
+
+#define AUDIO_AGENT_ENDPOINT_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>\n" \
+ " <interface name=\"" HSPHFPD_AUDIO_AGENT_INTERFACE "\">\n" \
+ " <method name=\"NewConnection\">\n" \
+ " <arg name=\"audio_transport\" direction=\"in\" type=\"o\"/>\n" \
+ " <arg name=\"fd\" direction=\"in\" type=\"h\"/>\n" \
+ " <arg name=\"properties\" direction=\"in\" type=\"a{sv}\"/>\n" \
+ " </method>\n" \
+ " <property name=\"AgentCodec\" type=\"s\" access=\"read\"/>\n" \
+ " </interface>\n" \
+ " <interface name=\"" DBUS_INTERFACE_INTROSPECTABLE "\">\n" \
+ " <method name=\"Introspect\">\n" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>\n" \
+ " </method>\n" \
+ " </interface>\n" \
+ "</node>\n"
+
+#define HSPHFPD_ERROR_INVALID_ARGUMENTS HSPHFPD_SERVICE ".Error.InvalidArguments"
+#define HSPHFPD_ERROR_ALREADY_EXISTS HSPHFPD_SERVICE ".Error.AlreadyExists"
+#define HSPHFPD_ERROR_DOES_NOT_EXISTS HSPHFPD_SERVICE ".Error.DoesNotExist"
+#define HSPHFPD_ERROR_NOT_CONNECTED HSPHFPD_SERVICE ".Error.NotConnected"
+#define HSPHFPD_ERROR_ALREADY_CONNECTED HSPHFPD_SERVICE ".Error.AlreadyConnected"
+#define HSPHFPD_ERROR_IN_PROGRESS HSPHFPD_SERVICE ".Error.InProgress"
+#define HSPHFPD_ERROR_IN_USE HSPHFPD_SERVICE ".Error.InUse"
+#define HSPHFPD_ERROR_NOT_SUPPORTED HSPHFPD_SERVICE ".Error.NotSupported"
+#define HSPHFPD_ERROR_NOT_AVAILABLE HSPHFPD_SERVICE ".Error.NotAvailable"
+#define HSPHFPD_ERROR_FAILED HSPHFPD_SERVICE ".Error.Failed"
+#define HSPHFPD_ERROR_REJECTED HSPHFPD_SERVICE ".Error.Rejected"
+#define HSPHFPD_ERROR_CANCELED HSPHFPD_SERVICE ".Error.Canceled"
+
+static struct hsphfpd_endpoint *endpoint_find(struct impl *backend, const char *path)
+{
+ struct hsphfpd_endpoint *d;
+ spa_list_for_each(d, &backend->endpoint_list, link)
+ if (spa_streq(d->path, path))
+ return d;
+ return NULL;
+}
+
+static void endpoint_free(struct hsphfpd_endpoint *endpoint)
+{
+ spa_list_remove(&endpoint->link);
+ free(endpoint->path);
+ if (endpoint->local_address)
+ free(endpoint->local_address);
+ if (endpoint->remote_address)
+ free(endpoint->remote_address);
+}
+
+static bool hsphfpd_cmp_transport_path(struct spa_bt_transport *t, const void *data)
+{
+ struct hsphfpd_transport_data *td = t->user_data;
+ if (spa_streq(td->transport_path, data))
+ return true;
+
+ return false;
+}
+
+static inline bool check_signature(DBusMessage *m, const char sig[])
+{
+ return spa_streq(dbus_message_get_signature(m), sig);
+}
+
+static int set_dbus_property(struct impl *backend,
+ const char *service,
+ const char *path,
+ const char *interface,
+ const char *property,
+ int type,
+ void *value)
+{
+ DBusMessage *m, *r;
+ DBusMessageIter iter;
+ DBusError err;
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "Set");
+ if (m == NULL)
+ return -ENOMEM;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &interface, DBUS_TYPE_STRING, &property, DBUS_TYPE_INVALID);
+ dbus_message_iter_init_append(m, &iter);
+ dbus_message_iter_append_basic(&iter, type, value);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_error(backend->log, "Transport Set() failed for transport %s (%s)", path, err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Set() returned error: %s", dbus_message_get_error_name(r));
+ return -EIO;
+ }
+
+ dbus_message_unref(r);
+ return 0;
+}
+
+static inline void set_rx_volume_gain_property(const struct spa_bt_transport *transport, uint16_t gain)
+{
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+
+ if (transport->fd < 0 || transport_data->rx_volume_control <= HSPHFPD_VOLUME_CONTROL_NONE)
+ return;
+ if (set_dbus_property(backend, HSPHFPD_SERVICE, transport_data->transport_path,
+ HSPHFPD_AUDIO_TRANSPORT_INTERFACE, "RxVolumeGain",
+ DBUS_TYPE_UINT16, &gain))
+ spa_log_error(backend->log, "Changing rx volume gain to %u for transport %s failed",
+ (unsigned)gain, transport_data->transport_path);
+}
+
+static inline void set_tx_volume_gain_property(const struct spa_bt_transport *transport, uint16_t gain)
+{
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+
+ if (transport->fd < 0 || transport_data->tx_volume_control <= HSPHFPD_VOLUME_CONTROL_NONE)
+ return;
+ if (set_dbus_property(backend, HSPHFPD_SERVICE, transport_data->transport_path,
+ HSPHFPD_AUDIO_TRANSPORT_INTERFACE, "TxVolumeGain",
+ DBUS_TYPE_UINT16, &gain))
+ spa_log_error(backend->log, "Changing tx volume gain to %u for transport %s failed",
+ (unsigned)gain, transport_data->transport_path);
+}
+
+static void parse_transport_properties_values(struct impl *backend,
+ const char *transport_path,
+ DBusMessageIter *i,
+ const char **endpoint_path,
+ const char **air_codec,
+ enum hsphfpd_volume_control *rx_volume_control,
+ enum hsphfpd_volume_control *tx_volume_control,
+ uint16_t *rx_volume_gain,
+ uint16_t *tx_volume_gain,
+ uint16_t *mtu)
+{
+ DBusMessageIter element_i;
+
+ spa_assert(i);
+
+ dbus_message_iter_recurse(i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter dict_i, variant_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&element_i, &dict_i);
+
+ if (dbus_message_iter_get_arg_type(&dict_i) != DBUS_TYPE_STRING) {
+ spa_log_error(backend->log, "Received invalid property for transport %s", transport_path);
+ return;
+ }
+
+ dbus_message_iter_get_basic(&dict_i, &key);
+
+ if (!dbus_message_iter_next(&dict_i)) {
+ spa_log_error(backend->log, "Received invalid property for transport %s", transport_path);
+ return;
+ }
+
+ if (dbus_message_iter_get_arg_type(&dict_i) != DBUS_TYPE_VARIANT) {
+ spa_log_error(backend->log, "Received invalid property for transport %s", transport_path);
+ return;
+ }
+
+ dbus_message_iter_recurse(&dict_i, &variant_i);
+
+ switch (dbus_message_iter_get_arg_type(&variant_i)) {
+ case DBUS_TYPE_STRING:
+ if (spa_streq(key, "RxVolumeControl") || spa_streq(key, "TxVolumeControl")) {
+ const char *value;
+ enum hsphfpd_volume_control volume_control;
+
+ dbus_message_iter_get_basic(&variant_i, &value);
+ if (spa_streq(value, "none"))
+ volume_control = HSPHFPD_VOLUME_CONTROL_NONE;
+ else if (spa_streq(value, "local"))
+ volume_control = HSPHFPD_VOLUME_CONTROL_LOCAL;
+ else if (spa_streq(value, "remote"))
+ volume_control = HSPHFPD_VOLUME_CONTROL_REMOTE;
+ else
+ volume_control = 0;
+
+ if (!volume_control)
+ spa_log_warn(backend->log, "Transport %s received invalid '%s' property value '%s', ignoring", transport_path, key, value);
+ else if (spa_streq(key, "RxVolumeControl"))
+ *rx_volume_control = volume_control;
+ else if (spa_streq(key, "TxVolumeControl"))
+ *tx_volume_control = volume_control;
+ } else if (spa_streq(key, "AirCodec"))
+ dbus_message_iter_get_basic(&variant_i, air_codec);
+ break;
+
+ case DBUS_TYPE_UINT16:
+ if (spa_streq(key, "MTU"))
+ dbus_message_iter_get_basic(&variant_i, mtu);
+ else if (spa_streq(key, "RxVolumeGain"))
+ dbus_message_iter_get_basic(&variant_i, rx_volume_gain);
+ else if (spa_streq(key, "TxVolumeGain"))
+ dbus_message_iter_get_basic(&variant_i, tx_volume_gain);
+ break;
+
+ case DBUS_TYPE_OBJECT_PATH:
+ if (spa_streq(key, "Endpoint"))
+ dbus_message_iter_get_basic(&variant_i, endpoint_path);
+ break;
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+}
+
+static void hsphfpd_parse_transport_properties(struct impl *backend, struct spa_bt_transport *transport, DBusMessageIter *i)
+{
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+ const char *endpoint_path = NULL;
+ const char *air_codec = NULL;
+ enum hsphfpd_volume_control rx_volume_control = 0;
+ enum hsphfpd_volume_control tx_volume_control = 0;
+ uint16_t rx_volume_gain = -1;
+ uint16_t tx_volume_gain = -1;
+ uint16_t mtu = 0;
+ bool rx_volume_gain_changed = false;
+ bool tx_volume_gain_changed = false;
+ bool rx_volume_control_changed = false;
+ bool tx_volume_control_changed = false;
+ bool rx_soft_volume_changed = false;
+ bool tx_soft_volume_changed = false;
+
+ parse_transport_properties_values(backend, transport_data->transport_path, i, &endpoint_path,
+ &air_codec, &rx_volume_control, &tx_volume_control,
+ &rx_volume_gain, &tx_volume_gain, &mtu);
+
+ if (endpoint_path)
+ spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring",
+ transport_data->transport_path, "Endpoint");
+
+ if (air_codec)
+ spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring",
+ transport_data->transport_path, "AirCodec");
+
+ if (mtu)
+ spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring",
+ transport_data->transport_path, "MTU");
+
+ if (rx_volume_control) {
+ if (!!transport_data->rx_soft_volume != !!(rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE)) {
+ spa_log_info(backend->log, "Transport %s changed rx soft volume from %d to %d",
+ transport_data->transport_path, transport_data->rx_soft_volume,
+ (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE));
+ transport_data->rx_soft_volume = (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE);
+ rx_soft_volume_changed = true;
+ }
+ if (transport_data->rx_volume_control != rx_volume_control) {
+ transport_data->rx_volume_control = rx_volume_control;
+ rx_volume_control_changed = true;
+ }
+ }
+
+ if (tx_volume_control) {
+ if (!!transport_data->tx_soft_volume != !!(tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE)) {
+ spa_log_info(backend->log, "Transport %s changed tx soft volume from %d to %d",
+ transport_data->transport_path, transport_data->rx_soft_volume,
+ (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE));
+ transport_data->tx_soft_volume = (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE);
+ tx_soft_volume_changed = true;
+ }
+ if (transport_data->tx_volume_control != tx_volume_control) {
+ transport_data->tx_volume_control = tx_volume_control;
+ tx_volume_control_changed = true;
+ }
+ }
+
+ if (rx_volume_gain != (uint16_t)-1) {
+ if (transport_data->rx_volume_gain != rx_volume_gain) {
+ spa_log_info(backend->log, "Transport %s changed rx volume gain from %u to %u",
+ transport_data->transport_path, (unsigned)transport_data->rx_volume_gain, (unsigned)rx_volume_gain);
+ transport_data->rx_volume_gain = rx_volume_gain;
+ rx_volume_gain_changed = true;
+ }
+ }
+
+ if (tx_volume_gain != (uint16_t)-1) {
+ if (transport_data->tx_volume_gain != tx_volume_gain) {
+ spa_log_info(backend->log, "Transport %s changed tx volume gain from %u to %u",
+ transport_data->transport_path, (unsigned)transport_data->tx_volume_gain, (unsigned)tx_volume_gain);
+ transport_data->tx_volume_gain = tx_volume_gain;
+ tx_volume_gain_changed = true;
+ }
+ }
+
+#if 0
+ if (rx_volume_gain_changed || rx_soft_volume_changed)
+ pa_hook_fire(pa_bluetooth_discovery_hook(transport_data->hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_RX_VOLUME_GAIN_CHANGED), transport);
+
+ if (tx_volume_gain_changed || tx_soft_volume_changed)
+ pa_hook_fire(pa_bluetooth_discovery_hook(transport_data->hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_TX_VOLUME_GAIN_CHANGED), transport);
+#else
+ spa_log_debug(backend->log, "RX volume gain changed: %d, soft volume changed: %d", rx_volume_gain_changed, rx_soft_volume_changed);
+ spa_log_debug(backend->log, "TX volume gain changed: %d, soft volume changed: %d", tx_volume_gain_changed, tx_soft_volume_changed);
+#endif
+
+ if (rx_volume_control_changed)
+ set_rx_volume_gain_property(transport, transport_data->rx_volume_gain);
+
+ if (tx_volume_control_changed)
+ set_tx_volume_gain_property(transport, transport_data->tx_volume_gain);
+}
+
+static DBusHandlerResult audio_agent_get_property(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata)
+{
+ const char *interface;
+ const char *property;
+ const char *agent_codec;
+ DBusMessage *r = NULL;
+
+ if (!check_signature(m, "ss")) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call");
+ goto fail;
+ }
+
+ if (dbus_message_get_args(m, NULL,
+ DBUS_TYPE_STRING, &interface,
+ DBUS_TYPE_STRING, &property,
+ DBUS_TYPE_INVALID) == FALSE) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid arguments in method call");
+ goto fail;
+ }
+
+ if (!spa_streq(interface, HSPHFPD_AUDIO_AGENT_INTERFACE))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (!spa_streq(property, "AgentCodec")) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid property in method call");
+ goto fail;
+ }
+
+ if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ))
+ agent_codec = HSPHFP_AGENT_CODEC_PCM;
+ else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC))
+ agent_codec = HSPHFP_AGENT_CODEC_MSBC;
+ else {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid path in method call");
+ goto fail;
+ }
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &agent_codec, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+fail:
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult audio_agent_getall_properties(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata)
+{
+ const char *interface;
+ DBusMessageIter iter, array, dict, data;
+ const char *agent_codec_key = "AgentCodec";
+ const char *agent_codec;
+ DBusMessage *r = NULL;
+
+ if (!check_signature(m, "s")) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call");
+ goto fail;
+ }
+
+ if (dbus_message_get_args(m, NULL,
+ DBUS_TYPE_STRING, &interface,
+ DBUS_TYPE_INVALID) == FALSE) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid arguments in method call");
+ goto fail;
+ }
+
+ if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ))
+ agent_codec = HSPHFP_AGENT_CODEC_PCM;
+ else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC))
+ agent_codec = HSPHFP_AGENT_CODEC_MSBC;
+ else {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid path in method call");
+ goto fail;
+ }
+
+ if (!spa_streq(interface, HSPHFPD_AUDIO_AGENT_INTERFACE))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &array);
+ dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &dict);
+ dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &agent_codec_key);
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, "s", &data);
+ dbus_message_iter_append_basic(&data, DBUS_TYPE_BOOLEAN, &agent_codec);
+ dbus_message_iter_close_container(&dict, &data);
+ dbus_message_iter_close_container(&array, &dict);
+ dbus_message_iter_close_container(&iter, &array);
+
+fail:
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult hsphfpd_new_audio_connection(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata)
+{
+ struct impl *backend = userdata;
+ DBusMessageIter arg_i;
+ const char *transport_path;
+ int fd;
+ const char *sender;
+ const char *endpoint_path = NULL;
+ const char *air_codec = NULL;
+ enum hsphfpd_volume_control rx_volume_control = 0;
+ enum hsphfpd_volume_control tx_volume_control = 0;
+ uint16_t rx_volume_gain = -1;
+ uint16_t tx_volume_gain = -1;
+ uint16_t mtu = 0;
+ unsigned int codec;
+ struct hsphfpd_endpoint *endpoint;
+ struct spa_bt_transport *transport;
+ struct hsphfpd_transport_data *transport_data;
+ DBusMessage *r = NULL;
+
+ if (!check_signature(m, "oha{sv}")) {
+ r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call");
+ goto fail;
+ }
+
+ if (!dbus_message_iter_init(m, &arg_i))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_iter_get_basic(&arg_i, &transport_path);
+ dbus_message_iter_next(&arg_i);
+ dbus_message_iter_get_basic(&arg_i, &fd);
+
+ spa_log_debug(backend->log, "NewConnection %s, fd %d", transport_path, fd);
+
+ sender = dbus_message_get_sender(m);
+ if (!spa_streq(sender, backend->hsphfpd_service_id)) {
+ close(fd);
+ spa_log_error(backend->log, "Sender '%s' is not authorized", sender);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Sender '%s' is not authorized", sender);
+ goto fail;
+ }
+
+ if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ))
+ codec = HFP_AUDIO_CODEC_CVSD;
+ else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC))
+ codec = HFP_AUDIO_CODEC_MSBC;
+ else {
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "Invalid path");
+ goto fail;
+ }
+
+ dbus_message_iter_next(&arg_i);
+ parse_transport_properties_values(backend, transport_path, &arg_i,
+ &endpoint_path, &air_codec,
+ &rx_volume_control, &tx_volume_control,
+ &rx_volume_gain, &tx_volume_gain,
+ &mtu);
+
+ if (!endpoint_path) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "Endpoint property was not specified");
+ goto fail;
+ }
+
+ if (!air_codec) {
+ close(fd);
+ spa_log_error(backend->log, "AirCodec property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "AirCodec property was not specified");
+ goto fail;
+ }
+
+ if (!rx_volume_control) {
+ close(fd);
+ spa_log_error(backend->log, "RxVolumeControl property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "RxVolumeControl property was not specified");
+ goto fail;
+ }
+
+ if (!tx_volume_control) {
+ close(fd);
+ spa_log_error(backend->log, "TxVolumeControl property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "TxVolumeControl property was not specified");
+ goto fail;
+ }
+
+ if (rx_volume_control != HSPHFPD_VOLUME_CONTROL_NONE) {
+ if (rx_volume_gain == (uint16_t)-1) {
+ close(fd);
+ spa_log_error(backend->log, "RxVolumeGain property was not specified, but VolumeControl is not none");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "RxVolumeGain property was not specified, but VolumeControl is not none");
+ goto fail;
+ }
+ } else {
+ rx_volume_gain = 15; /* No volume control, so set maximal value */
+ }
+
+ if (tx_volume_control != HSPHFPD_VOLUME_CONTROL_NONE) {
+ if (tx_volume_gain == (uint16_t)-1) {
+ close(fd);
+ spa_log_error(backend->log, "TxVolumeGain property was not specified, but VolumeControl is not none");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "TxVolumeGain property was not specified, but VolumeControl is not none");
+ goto fail;
+ }
+ } else {
+ tx_volume_gain = 15; /* No volume control, so set maximal value */
+ }
+
+ if (!mtu) {
+ close(fd);
+ spa_log_error(backend->log, "MTU property was not specified");
+ r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "MTU property was not specified");
+ goto fail;
+ }
+
+ endpoint = endpoint_find(backend, endpoint_path);
+ if (!endpoint) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint %s does not exist", endpoint_path);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s does not exist", endpoint_path);
+ goto fail;
+ }
+
+ if (!endpoint->valid) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint %s is not valid", endpoint_path);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s is not valid", endpoint_path);
+ goto fail;
+ }
+
+ transport = spa_bt_transport_find(backend->monitor, endpoint_path);
+ if (!transport) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint %s is not connected", endpoint_path);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s is not connected", endpoint_path);
+ goto fail;
+ }
+
+ if (transport->codec != codec)
+ spa_log_warn(backend->log, "Expecting codec to be %d, got %d", transport->codec, codec);
+
+ if (transport->fd >= 0) {
+ close(fd);
+ spa_log_error(backend->log, "Endpoint %s has already active transport", endpoint_path);
+ r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s has already active transport", endpoint_path);
+ goto fail;
+ }
+
+ transport_data = transport->user_data;
+ transport_data->transport_path = strdup(transport_path);
+ transport_data->rx_soft_volume = (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE);
+ transport_data->tx_soft_volume = (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE);
+ transport_data->rx_volume_gain = rx_volume_gain;
+ transport_data->tx_volume_gain = tx_volume_gain;
+ transport_data->rx_volume_control = rx_volume_control;
+ transport_data->tx_volume_control = tx_volume_control;
+
+#if 0
+ pa_hook_fire(pa_bluetooth_discovery_hook(hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_RX_VOLUME_GAIN_CHANGED), transport);
+ pa_hook_fire(pa_bluetooth_discovery_hook(hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_TX_VOLUME_GAIN_CHANGED), transport);
+#endif
+
+ transport->read_mtu = mtu;
+ transport->write_mtu = mtu;
+
+ transport->fd = fd;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+fail:
+ if (r) {
+ DBusHandlerResult res = DBUS_HANDLER_RESULT_HANDLED;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ res = DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_unref(r);
+ return res;
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult audio_agent_endpoint_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+ DBusHandlerResult res;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = AUDIO_AGENT_ENDPOINT_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get"))
+ res = audio_agent_get_property(c, m, path, userdata);
+ else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll"))
+ res = audio_agent_getall_properties(c, m, path, userdata);
+ else if (dbus_message_is_method_call(m, HSPHFPD_AUDIO_AGENT_INTERFACE, "NewConnection"))
+ res = hsphfpd_new_audio_connection(c, m, path, userdata);
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void append_audio_agent_object(DBusMessageIter *iter, const char *endpoint, const char *agent_codec)
+{
+ const char *interface_name = HSPHFPD_AUDIO_AGENT_INTERFACE;
+ DBusMessageIter object, array, entry, dict, codec, data;
+ char *str = "AgentCodec";
+
+ dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY, NULL, &object);
+ dbus_message_iter_append_basic(&object, DBUS_TYPE_OBJECT_PATH, &endpoint);
+
+ dbus_message_iter_open_container(&object, DBUS_TYPE_ARRAY, "{sa{sv}}", &array);
+
+ dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface_name);
+
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &codec);
+ dbus_message_iter_append_basic(&codec, DBUS_TYPE_STRING, &str);
+ dbus_message_iter_open_container(&codec, DBUS_TYPE_VARIANT, "s", &data);
+ dbus_message_iter_append_basic(&data, DBUS_TYPE_STRING, &agent_codec);
+ dbus_message_iter_close_container(&codec, &data);
+ dbus_message_iter_close_container(&dict, &codec);
+
+ dbus_message_iter_close_container(&entry, &dict);
+ dbus_message_iter_close_container(&array, &entry);
+ dbus_message_iter_close_container(&object, &array);
+ dbus_message_iter_close_container(iter, &object);
+}
+
+static DBusHandlerResult application_object_manager_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = APPLICATION_OBJECT_MANAGER_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects")) {
+ DBusMessageIter iter, array;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_iter_init_append(r, &iter);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{oa{sa{sv}}}", &array);
+
+ append_audio_agent_object(&array, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ, HSPHFP_AGENT_CODEC_PCM);
+ if (backend->msbc_supported)
+ append_audio_agent_object(&array, HSPHFP_AUDIO_CLIENT_MSBC, HSPHFP_AGENT_CODEC_MSBC);
+
+ dbus_message_iter_close_container(&iter, &array);
+ } else
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void hsphfpd_audio_acquire_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+ const char *transport_path;
+ const char *service_id;
+ const char *agent_path;
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ backend->acquire_in_progress = false;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "RegisterApplication() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!spa_streq(dbus_message_get_sender(r), backend->hsphfpd_service_id)) {
+ spa_log_error(backend->log, "Reply for " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() from invalid sender");
+ goto finish;
+ }
+
+ if (!check_signature(r, "oso")) {
+ spa_log_error(backend->log, "Invalid reply signature for " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio()");
+ goto finish;
+ }
+
+ if (dbus_message_get_args(r, &error,
+ DBUS_TYPE_OBJECT_PATH, &transport_path,
+ DBUS_TYPE_STRING, &service_id,
+ DBUS_TYPE_OBJECT_PATH, &agent_path,
+ DBUS_TYPE_INVALID) == FALSE) {
+ spa_log_error(backend->log, "Failed to parse " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() reply: %s", error.message);
+ goto finish;
+ }
+
+ if (!spa_streq(service_id, dbus_bus_get_unique_name(backend->conn))) {
+ spa_log_warn(backend->log, HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() failed: Other audio application took audio socket");
+ goto finish;
+ }
+
+ spa_log_debug(backend->log, "hsphfpd audio acquired");
+
+finish:
+ dbus_message_unref(r);
+ dbus_pending_call_unref(pending);
+}
+
+static int hsphfpd_audio_acquire(void *data, bool optional)
+{
+ struct spa_bt_transport *transport = data;
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ DBusMessage *m;
+ const char *air_codec = HSPHFP_AIR_CODEC_CVSD;
+ const char *agent_codec = HSPHFP_AGENT_CODEC_PCM;
+ DBusPendingCall *call;
+ DBusError err;
+
+ spa_log_debug(backend->log, "transport %p: Acquire %s",
+ transport, transport->path);
+
+ if (backend->acquire_in_progress)
+ return -EINPROGRESS;
+
+ if (transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ air_codec = HSPHFP_AIR_CODEC_MSBC;
+ agent_codec = HSPHFP_AGENT_CODEC_MSBC;
+ }
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE,
+ transport->path,
+ HSPHFPD_ENDPOINT_INTERFACE,
+ "ConnectAudio");
+ if (m == NULL)
+ return -ENOMEM;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &air_codec, DBUS_TYPE_STRING, &agent_codec, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+
+ dbus_connection_send_with_reply(backend->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, hsphfpd_audio_acquire_reply, backend, NULL);
+ dbus_message_unref(m);
+
+ /* The ConnectAudio method triggers Introspect and NewConnection calls,
+ which will set the fd to use for the SCO data.
+ We need to run the DBus loop to be able to reply to those method calls */
+ backend->acquire_in_progress = true;
+ while (backend->acquire_in_progress && dbus_connection_read_write_dispatch(backend->conn, -1))
+ ; // empty loop body
+
+ return 0;
+}
+
+static int hsphfpd_audio_release(void *data)
+{
+ struct spa_bt_transport *transport = data;
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+
+ spa_log_debug(backend->log, "transport %p: Release %s",
+ transport, transport->path);
+
+ if (transport->sco_io) {
+ spa_bt_sco_io_destroy(transport->sco_io);
+ transport->sco_io = NULL;
+ }
+
+ /* shutdown to make sure connection is dropped immediately */
+ shutdown(transport->fd, SHUT_RDWR);
+ close(transport->fd);
+ if (transport_data->transport_path) {
+ free(transport_data->transport_path);
+ transport_data->transport_path = NULL;
+ }
+ transport->fd = -1;
+
+ return 0;
+}
+
+static int hsphfpd_audio_destroy(void *data)
+{
+ struct spa_bt_transport *transport = data;
+ struct hsphfpd_transport_data *transport_data = transport->user_data;
+
+ if (transport_data->transport_path) {
+ free(transport_data->transport_path);
+ transport_data->transport_path = NULL;
+ }
+
+ return 0;
+}
+
+static const struct spa_bt_transport_implementation hsphfpd_transport_impl = {
+ SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
+ .acquire = hsphfpd_audio_acquire,
+ .release = hsphfpd_audio_release,
+ .destroy = hsphfpd_audio_destroy,
+};
+
+static DBusHandlerResult hsphfpd_parse_endpoint_properties(struct impl *backend, struct hsphfpd_endpoint *endpoint, DBusMessageIter *i)
+{
+ DBusMessageIter element_i;
+ struct spa_bt_device *d;
+ struct spa_bt_transport *t;
+
+ dbus_message_iter_recurse(i, &element_i);
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter dict_i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&element_i, &dict_i);
+ dbus_message_iter_get_basic(&dict_i, &key);
+ dbus_message_iter_next(&dict_i);
+ dbus_message_iter_recurse(&dict_i, &value_i);
+ switch (dbus_message_iter_get_arg_type(&value_i)) {
+ case DBUS_TYPE_STRING:
+ {
+ const char *value;
+ dbus_message_iter_get_basic(&value_i, &value);
+ if (spa_streq(key, "RemoteAddress"))
+ endpoint->remote_address = strdup(value);
+ else if (spa_streq(key, "LocalAddress"))
+ endpoint->local_address = strdup(value);
+ else if (spa_streq(key, "Profile")) {
+ if (endpoint->profile)
+ spa_log_warn(backend->log, "Endpoint %s received a duplicate '%s' property, ignoring", endpoint->path, key);
+ else if (spa_streq(value, "headset"))
+ endpoint->profile = HSPHFPD_PROFILE_HEADSET;
+ else if (spa_streq(value, "handsfree"))
+ endpoint->profile = HSPHFPD_PROFILE_HANDSFREE;
+ else
+ spa_log_warn(backend->log, "Endpoint %s received invalid '%s' property value '%s', ignoring", endpoint->path, key, value);
+ } else if (spa_streq(key, "Role")) {
+ if (endpoint->role)
+ spa_log_warn(backend->log, "Endpoint %s received a duplicate '%s' property, ignoring", endpoint->path, key);
+ else if (spa_streq(value, "client"))
+ endpoint->role = HSPHFPD_ROLE_CLIENT;
+ else if (spa_streq(value, "gateway"))
+ endpoint->role = HSPHFPD_ROLE_GATEWAY;
+ else
+ spa_log_warn(backend->log, "Endpoint %s received invalid '%s' property value '%s', ignoring", endpoint->path, key, value);
+ }
+ spa_log_trace(backend->log, " %s: %s (%p)", key, value, endpoint);
+ }
+ break;
+
+ case DBUS_TYPE_BOOLEAN:
+ {
+ bool value;
+ dbus_message_iter_get_basic(&value_i, &value);
+ if (spa_streq(key, "Connected"))
+ endpoint->connected = value;
+ spa_log_trace(backend->log, " %s: %d", key, value);
+ }
+ break;
+
+ case DBUS_TYPE_ARRAY:
+ {
+ if (spa_streq(key, "AudioCodecs")) {
+ DBusMessageIter array_i;
+ const char *value;
+
+ endpoint->air_codecs = 0;
+ dbus_message_iter_recurse(&value_i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ dbus_message_iter_get_basic(&array_i, &value);
+ if (spa_streq(value, HSPHFP_AIR_CODEC_CVSD))
+ endpoint->air_codecs |= HFP_AUDIO_CODEC_CVSD;
+ if (spa_streq(value, HSPHFP_AIR_CODEC_MSBC))
+ endpoint->air_codecs |= HFP_AUDIO_CODEC_MSBC;
+ dbus_message_iter_next(&array_i);
+ }
+ }
+ }
+ break;
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+
+ if (!endpoint->valid && endpoint->local_address && endpoint->remote_address && endpoint->profile && endpoint->role)
+ endpoint->valid = true;
+
+ if (!endpoint->remote_address || !endpoint->local_address) {
+ spa_log_debug(backend->log, "Missing addresses for %s", endpoint->path);
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+
+ d = spa_bt_device_find_by_address(backend->monitor, endpoint->remote_address, endpoint->local_address);
+ if (!d || !d->adapter) {
+ spa_log_debug(backend->log, "No device for %s", endpoint->path);
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+
+ if ((t = spa_bt_transport_find(backend->monitor, endpoint->path)) != NULL) {
+ /* Release transport on disconnection, or when mSBC is supported if there
+ is an update of the remote codecs */
+ if (!endpoint->connected || (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC) && t->codec == HFP_AUDIO_CODEC_CVSD)) {
+ spa_bt_transport_free(t);
+ spa_bt_device_check_profiles(d, false);
+ spa_log_debug(backend->log, "Transport released for %s", endpoint->path);
+ } else {
+ spa_log_debug(backend->log, "Transport already configured for %s", endpoint->path);
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+ }
+
+ if (!endpoint->valid || !endpoint->connected)
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+ char *t_path = strdup(endpoint->path);
+ t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct hsphfpd_transport_data));
+ if (t == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ free(t_path);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+ spa_bt_transport_set_implementation(t, &hsphfpd_transport_impl, t);
+
+ t->device = d;
+ spa_list_append(&t->device->transport_list, &t->device_link);
+ t->backend = &backend->this;
+ t->profile = SPA_BT_PROFILE_NULL;
+ if (endpoint->profile == HSPHFPD_PROFILE_HEADSET) {
+ if (endpoint->role == HSPHFPD_ROLE_CLIENT)
+ t->profile = SPA_BT_PROFILE_HSP_HS;
+ else if (endpoint->role == HSPHFPD_ROLE_GATEWAY)
+ t->profile = SPA_BT_PROFILE_HSP_AG;
+ } else if (endpoint->profile == HSPHFPD_PROFILE_HANDSFREE) {
+ if (endpoint->role == HSPHFPD_ROLE_CLIENT)
+ t->profile = SPA_BT_PROFILE_HFP_HF;
+ else if (endpoint->role == HSPHFPD_ROLE_GATEWAY)
+ t->profile = SPA_BT_PROFILE_HFP_AG;
+ }
+ if (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC))
+ t->codec = HFP_AUDIO_CODEC_MSBC;
+ else
+ t->codec = HFP_AUDIO_CODEC_CVSD;
+
+ t->n_channels = 1;
+ t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
+
+ spa_bt_device_add_profile(d, t->profile);
+ spa_bt_device_connect_profile(t->device, t->profile);
+
+ spa_log_debug(backend->log, "Transport %s available for hsphfpd", endpoint->path);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult hsphfpd_parse_interfaces(struct impl *backend, DBusMessageIter *dict_i)
+{
+ DBusMessageIter element_i;
+ const char *path;
+
+ spa_assert(backend);
+ spa_assert(dict_i);
+
+ dbus_message_iter_get_basic(dict_i, &path);
+ dbus_message_iter_next(dict_i);
+ dbus_message_iter_recurse(dict_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter iface_i;
+ const char *interface;
+
+ dbus_message_iter_recurse(&element_i, &iface_i);
+ dbus_message_iter_get_basic(&iface_i, &interface);
+ dbus_message_iter_next(&iface_i);
+
+ if (spa_streq(interface, HSPHFPD_ENDPOINT_INTERFACE)) {
+ struct hsphfpd_endpoint *endpoint;
+
+ endpoint = endpoint_find(backend, path);
+ if (!endpoint) {
+ endpoint = calloc(1, sizeof(struct hsphfpd_endpoint));
+ endpoint->path = strdup(path);
+ spa_list_append(&backend->endpoint_list, &endpoint->link);
+ spa_log_debug(backend->log, "Found endpoint %s", path);
+ }
+ hsphfpd_parse_endpoint_properties(backend, endpoint, &iface_i);
+ } else
+ spa_log_debug(backend->log, "Unknown interface %s found, skipping", interface);
+
+ dbus_message_iter_next(&element_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void hsphfpd_get_endpoints_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+ DBusMessageIter i, array_i;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Failed to get a list of endpoints from hsphfpd: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!spa_streq(dbus_message_get_sender(r), backend->hsphfpd_service_id)) {
+ spa_log_error(backend->log, "Reply for GetManagedObjects() from invalid sender");
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !check_signature(r, "a{oa{sa{sv}}}")) {
+ spa_log_error(backend->log, "Invalid arguments in GetManagedObjects() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter dict_i;
+
+ dbus_message_iter_recurse(&array_i, &dict_i);
+ hsphfpd_parse_interfaces(backend, &dict_i);
+ dbus_message_iter_next(&array_i);
+ }
+
+ backend->endpoints_listed = true;
+
+finish:
+ dbus_message_unref(r);
+ dbus_pending_call_unref(pending);
+}
+
+static int backend_hsphfpd_register(void *data)
+{
+ struct impl *backend = data;
+ DBusMessage *m, *r;
+ const char *path = APPLICATION_OBJECT_MANAGER_PATH;
+ DBusPendingCall *call;
+ DBusError err;
+ int res;
+
+ spa_log_debug(backend->log, "Registering to hsphfpd");
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/",
+ HSPHFPD_APPLICATION_MANAGER_INTERFACE, "RegisterApplication");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ if (dbus_error_has_name(&err, "org.freedesktop.DBus.Error.ServiceUnknown")) {
+ spa_log_info(backend->log, "hsphfpd not available: %s",
+ err.message);
+ res = -ENOTSUP;
+ } else {
+ spa_log_warn(backend->log, "Registering application %s failed: %s (%s)",
+ path, err.message, err.name);
+ res = -EIO;
+ }
+ dbus_error_free(&err);
+ return res;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "RegisterApplication() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+ dbus_message_unref(r);
+
+ backend->hsphfpd_service_id = strdup(dbus_message_get_sender(r));
+
+ spa_log_debug(backend->log, "Registered to hsphfpd");
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/",
+ DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects");
+ if (m == NULL)
+ goto finish;
+
+ dbus_connection_send_with_reply(backend->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, hsphfpd_get_endpoints_reply, backend, NULL);
+ dbus_message_unref(m);
+
+ return 0;
+
+finish:
+ dbus_message_unref(r);
+ return -EIO;
+}
+
+static int backend_hsphfpd_unregistered(void *data)
+{
+ struct impl *backend = data;
+ struct hsphfpd_endpoint *endpoint;
+
+ if (backend->hsphfpd_service_id) {
+ free(backend->hsphfpd_service_id);
+ backend->hsphfpd_service_id = NULL;
+ }
+ backend->endpoints_listed = false;
+ spa_list_consume(endpoint, &backend->endpoint_list, link)
+ endpoint_free(endpoint);
+
+ return 0;
+}
+
+static DBusHandlerResult hsphfpd_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ const char *sender;
+ struct impl *backend = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ sender = dbus_message_get_sender(m);
+
+ if (backend->hsphfpd_service_id && spa_streq(sender, backend->hsphfpd_service_id)) {
+ if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, "InterfacesAdded")) {
+ DBusMessageIter arg_i;
+
+ spa_log_warn(backend->log, "sender: %s", dbus_message_get_sender(m));
+
+ if (!backend->endpoints_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "oa{sa{sv}}")) {
+ spa_log_error(backend->log, "Invalid signature found in InterfacesAdded");
+ goto finish;
+ }
+
+ hsphfpd_parse_interfaces(backend, &arg_i);
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, "InterfacesRemoved")) {
+ const char *path;
+ DBusMessageIter arg_i, element_i;
+
+ if (!backend->endpoints_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "oas")) {
+ spa_log_error(backend->log, "Invalid signature found in InterfacesRemoved");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&arg_i, &path);
+ dbus_message_iter_next(&arg_i);
+ dbus_message_iter_recurse(&arg_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) {
+ const char *iface;
+
+ dbus_message_iter_get_basic(&element_i, &iface);
+
+ if (spa_streq(iface, HSPHFPD_ENDPOINT_INTERFACE)) {
+ struct hsphfpd_endpoint *endpoint;
+ struct spa_bt_transport *transport = spa_bt_transport_find(backend->monitor, path);
+
+ if (transport)
+ spa_bt_transport_free(transport);
+
+ spa_log_debug(backend->log, "Remove endpoint %s", path);
+ endpoint = endpoint_find(backend, path);
+ if (endpoint)
+ endpoint_free(endpoint);
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged")) {
+ DBusMessageIter arg_i;
+ const char *iface;
+ const char *path;
+
+ if (!backend->endpoints_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "sa{sv}as")) {
+ spa_log_error(backend->log, "Invalid signature found in PropertiesChanged");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&arg_i, &iface);
+ dbus_message_iter_next(&arg_i);
+
+ path = dbus_message_get_path(m);
+
+ if (spa_streq(iface, HSPHFPD_ENDPOINT_INTERFACE)) {
+ struct hsphfpd_endpoint *endpoint = endpoint_find(backend, path);
+ if (!endpoint) {
+ spa_log_warn(backend->log, "Properties changed on unknown endpoint %s", path);
+ goto finish;
+ }
+ spa_log_debug(backend->log, "Properties changed on endpoint %s", path);
+ hsphfpd_parse_endpoint_properties(backend, endpoint, &arg_i);
+ } else if (spa_streq(iface, HSPHFPD_AUDIO_TRANSPORT_INTERFACE)) {
+ struct spa_bt_transport *transport = spa_bt_transport_find_full(backend->monitor,
+ hsphfpd_cmp_transport_path,
+ (const void *)path);
+ if (!transport) {
+ spa_log_warn(backend->log, "Properties changed on unknown transport %s", path);
+ goto finish;
+ }
+ spa_log_debug(backend->log, "Properties changed on transport %s", path);
+ hsphfpd_parse_transport_properties(backend, transport, &arg_i);
+ }
+ }
+ }
+
+finish:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(void *data)
+{
+ struct impl *backend = data;
+ DBusError err;
+
+ if (backend->filters_added)
+ return 0;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(backend->conn, hsphfpd_filter_cb, backend, NULL)) {
+ spa_log_error(backend->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" HSPHFPD_SERVICE "',"
+ "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='InterfacesAdded'", &err);
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" HSPHFPD_SERVICE "',"
+ "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='InterfacesRemoved'", &err);
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" HSPHFPD_SERVICE "',"
+ "interface='" DBUS_INTERFACE_PROPERTIES "',member='PropertiesChanged',"
+ "arg0='" HSPHFPD_ENDPOINT_INTERFACE "'", &err);
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" HSPHFPD_SERVICE "',"
+ "interface='" DBUS_INTERFACE_PROPERTIES "',member='PropertiesChanged',"
+ "arg0='" HSPHFPD_AUDIO_TRANSPORT_INTERFACE "'", &err);
+
+ backend->filters_added = true;
+
+ return 0;
+
+fail:
+ dbus_error_free(&err);
+ return -EIO;
+}
+
+static int backend_hsphfpd_free(void *data)
+{
+ struct impl *backend = data;
+ struct hsphfpd_endpoint *endpoint;
+
+ if (backend->filters_added) {
+ dbus_connection_remove_filter(backend->conn, hsphfpd_filter_cb, backend);
+ backend->filters_added = false;
+ }
+
+ if (backend->msbc_supported)
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_MSBC);
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ);
+ dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH);
+
+ spa_list_consume(endpoint, &backend->endpoint_list, link)
+ endpoint_free(endpoint);
+
+ free(backend);
+
+ return 0;
+}
+
+static const struct spa_bt_backend_implementation backend_impl = {
+ SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
+ .free = backend_hsphfpd_free,
+ .register_profiles = backend_hsphfpd_register,
+ .unregister_profiles = backend_hsphfpd_unregistered,
+};
+
+static bool is_available(struct impl *backend)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ bool success = false;
+
+ m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/",
+ DBUS_INTERFACE_INTROSPECTABLE, "Introspect");
+ if (m == NULL)
+ return false;
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN)
+ success = true;
+
+ if (r)
+ dbus_message_unref(r);
+ else
+ dbus_error_free(&err);
+
+ return success;
+}
+
+struct spa_bt_backend *backend_hsphfpd_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *backend;
+ const char *str;
+ static const DBusObjectPathVTable vtable_application_object_manager = {
+ .message_function = application_object_manager_handler,
+ };
+ static const DBusObjectPathVTable vtable_audio_agent_endpoint = {
+ .message_function = audio_agent_endpoint_handler,
+ };
+
+ backend = calloc(1, sizeof(struct impl));
+ if (backend == NULL)
+ return NULL;
+
+ spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend);
+
+ backend->this.name = "hsphfpd";
+ backend->this.exclusive = true;
+ backend->monitor = monitor;
+ backend->quirks = quirks;
+ backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ backend->conn = dbus_connection;
+ if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc")))
+ backend->msbc_supported = spa_atob(str);
+ else
+ backend->msbc_supported = false;
+
+ spa_log_topic_init(backend->log, &log_topic);
+
+ spa_list_init(&backend->endpoint_list);
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ APPLICATION_OBJECT_MANAGER_PATH,
+ &vtable_application_object_manager, backend)) {
+ free(backend);
+ return NULL;
+ }
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ,
+ &vtable_audio_agent_endpoint, backend)) {
+ dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH);
+ free(backend);
+ return NULL;
+ }
+
+ if (backend->msbc_supported && !dbus_connection_register_object_path(backend->conn,
+ HSPHFP_AUDIO_CLIENT_MSBC,
+ &vtable_audio_agent_endpoint, backend)) {
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ);
+ dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH);
+ free(backend);
+ return NULL;
+ }
+
+ if (add_filters(backend) < 0) {
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_MSBC);
+ dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ);
+ dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH);
+ free(backend);
+ return NULL;
+ }
+
+ backend->this.available = is_available(backend);
+
+ return &backend->this;
+}
diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c
new file mode 100644
index 0000000..5eec82c
--- /dev/null
+++ b/spa/plugins/bluez5/backend-native.c
@@ -0,0 +1,2838 @@
+/* Spa HSP/HFP native backend
+ *
+ * Copyright © 2018 Wim Taymans
+ * Copyright © 2021 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/sco.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/debug/mem.h>
+#include <spa/debug/log.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/raw.h>
+
+#include "defs.h"
+
+#ifdef HAVE_LIBUSB
+#include <libusb.h>
+#endif
+
+#include "modemmanager.h"
+#include "upower.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.native");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define PROP_KEY_HEADSET_ROLES "bluez5.headset-roles"
+
+#define HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC 5000
+#define HFP_CODEC_SWITCH_TIMEOUT_MSEC 20000
+
+#define INTERNATIONAL_NUMBER 145
+#define NATIONAL_NUMBER 129
+
+#define MAX_HF_INDICATORS 16
+
+enum {
+ HFP_AG_INITIAL_CODEC_SETUP_NONE = 0,
+ HFP_AG_INITIAL_CODEC_SETUP_SEND,
+ HFP_AG_INITIAL_CODEC_SETUP_WAIT
+};
+
+#define CIND_INDICATORS "(\"service\",(0-1)),(\"call\",(0-1)),(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5)),(\"roam\",(0-1)),(\"battchg\",(0-5))"
+enum {
+ CIND_SERVICE = 1,
+ CIND_CALL,
+ CIND_CALLSETUP,
+ CIND_CALLHELD,
+ CIND_SIGNAL,
+ CIND_ROAM,
+ CIND_BATTERY_LEVEL,
+ CIND_MAX
+};
+
+struct modem {
+ bool network_has_service;
+ unsigned int signal_strength;
+ bool network_is_roaming;
+ char *operator_name;
+ char *own_number;
+ bool active_call;
+ unsigned int call_setup;
+};
+
+struct impl {
+ struct spa_bt_backend this;
+
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_system *main_system;
+ struct spa_loop_utils *loop_utils;
+ struct spa_dbus *dbus;
+ DBusConnection *conn;
+
+#define DEFAULT_ENABLED_PROFILES (SPA_BT_PROFILE_HFP_HF | SPA_BT_PROFILE_HFP_AG)
+ enum spa_bt_profile enabled_profiles;
+
+ struct spa_source sco;
+
+ const struct spa_bt_quirks *quirks;
+
+ struct spa_list rfcomm_list;
+ unsigned int defer_setup_enabled:1;
+
+ struct modem modem;
+ unsigned int battery_level;
+
+ void *modemmanager;
+ struct spa_source *ring_timer;
+ void *upower;
+};
+
+struct transport_data {
+ struct rfcomm *rfcomm;
+ struct spa_source sco;
+};
+
+enum hfp_hf_state {
+ hfp_hf_brsf,
+ hfp_hf_bac,
+ hfp_hf_cind1,
+ hfp_hf_cind2,
+ hfp_hf_cmer,
+ hfp_hf_slc1,
+ hfp_hf_slc2,
+ hfp_hf_vgs,
+ hfp_hf_vgm,
+ hfp_hf_bcs
+};
+
+enum hsp_hs_state {
+ hsp_hs_init1,
+ hsp_hs_init2,
+ hsp_hs_vgs,
+ hsp_hs_vgm,
+};
+
+struct rfcomm_volume {
+ bool active;
+ int hw_volume;
+};
+
+struct rfcomm {
+ struct spa_list link;
+ struct spa_source source;
+ struct impl *backend;
+ struct spa_bt_device *device;
+ struct spa_hook device_listener;
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+ enum spa_bt_profile profile;
+ struct spa_source timer;
+ struct spa_source *volume_sync_timer;
+ char* path;
+ bool has_volume;
+ struct rfcomm_volume volumes[SPA_BT_VOLUME_ID_TERM];
+ unsigned int broken_mic_hw_volume:1;
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ unsigned int slc_configured:1;
+ unsigned int codec_negotiation_supported:1;
+ unsigned int msbc_supported_by_hfp:1;
+ unsigned int hfp_ag_switching_codec:1;
+ unsigned int hfp_ag_initial_codec_setup:2;
+ unsigned int cind_call_active:1;
+ unsigned int cind_call_notify:1;
+ unsigned int extended_error_reporting:1;
+ unsigned int clip_notify:1;
+ enum hfp_hf_state hf_state;
+ enum hsp_hs_state hs_state;
+ unsigned int codec;
+ uint32_t cind_enabled_indicators;
+ char *hf_indicators[MAX_HF_INDICATORS];
+#endif
+};
+
+static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ DBusMessage *r;
+
+ r = dbus_message_new_error(m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented",
+ "Method not implemented");
+ if (r == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void transport_destroy(void *data)
+{
+ struct rfcomm *rfcomm = data;
+ struct impl *backend = rfcomm->backend;
+
+ spa_log_debug(backend->log, "transport %p destroy", rfcomm->transport);
+ rfcomm->transport = NULL;
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = transport_destroy,
+};
+
+static const struct spa_bt_transport_implementation sco_transport_impl;
+
+static struct spa_bt_transport *_transport_create(struct rfcomm *rfcomm)
+{
+ struct impl *backend = rfcomm->backend;
+ struct spa_bt_transport *t = NULL;
+ struct transport_data *td;
+ char* pathfd;
+
+ if ((pathfd = spa_aprintf("%s/fd%d", rfcomm->path, rfcomm->source.fd)) == NULL)
+ return NULL;
+
+ t = spa_bt_transport_create(backend->monitor, pathfd, sizeof(struct transport_data));
+ if (t == NULL)
+ goto finish;
+ spa_bt_transport_set_implementation(t, &sco_transport_impl, t);
+
+ t->device = rfcomm->device;
+ spa_list_append(&t->device->transport_list, &t->device_link);
+ t->profile = rfcomm->profile;
+ t->backend = &backend->this;
+ t->n_channels = 1;
+ t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
+
+ td = t->user_data;
+ td->rfcomm = rfcomm;
+
+ if (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) {
+ t->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_AG_VOLUME;
+ t->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_AG_VOLUME;
+ } else {
+ t->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_RX_VOLUME;
+ t->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME;
+ }
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) {
+ t->volumes[i].active = rfcomm->volumes[i].active;
+ t->volumes[i].hw_volume_max = SPA_BT_VOLUME_HS_MAX;
+ if (rfcomm->volumes[i].active && rfcomm->volumes[i].hw_volume != SPA_BT_VOLUME_INVALID)
+ t->volumes[i].volume =
+ spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t->volumes[i].hw_volume_max);
+ }
+
+ spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm);
+
+finish:
+ return t;
+}
+
+static int codec_switch_stop_timer(struct rfcomm *rfcomm);
+
+static void volume_sync_stop_timer(struct rfcomm *rfcomm);
+
+static void rfcomm_free(struct rfcomm *rfcomm)
+{
+ codec_switch_stop_timer(rfcomm);
+ for (int i = 0; i < MAX_HF_INDICATORS; i++) {
+ if (rfcomm->hf_indicators[i]) {
+ free(rfcomm->hf_indicators[i]);
+ }
+ }
+ spa_list_remove(&rfcomm->link);
+ if (rfcomm->path)
+ free(rfcomm->path);
+ if (rfcomm->transport) {
+ spa_hook_remove(&rfcomm->transport_listener);
+ spa_bt_transport_free(rfcomm->transport);
+ }
+ if (rfcomm->device) {
+ spa_bt_device_report_battery_level(rfcomm->device, SPA_BT_NO_BATTERY);
+ spa_hook_remove(&rfcomm->device_listener);
+ rfcomm->device = NULL;
+ }
+ if (rfcomm->source.fd >= 0) {
+ if (rfcomm->source.loop)
+ spa_loop_remove_source(rfcomm->source.loop, &rfcomm->source);
+ shutdown(rfcomm->source.fd, SHUT_RDWR);
+ close (rfcomm->source.fd);
+ rfcomm->source.fd = -1;
+ }
+ if (rfcomm->volume_sync_timer)
+ spa_loop_utils_destroy_source(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer);
+ free(rfcomm);
+}
+
+#define RFCOMM_MESSAGE_MAX_LENGTH 256
+
+/* from HF/HS to AG */
+SPA_PRINTF_FUNC(2, 3)
+static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format, ...)
+{
+ struct impl *backend = rfcomm->backend;
+ char message[RFCOMM_MESSAGE_MAX_LENGTH + 1];
+ ssize_t len;
+ va_list args;
+
+ va_start(args, format);
+ len = vsnprintf(message, RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args);
+ va_end(args);
+
+ if (len < 0)
+ return -EINVAL;
+
+ if (len > RFCOMM_MESSAGE_MAX_LENGTH)
+ return -E2BIG;
+
+ spa_log_debug(backend->log, "RFCOMM >> %s", message);
+
+ /*
+ * The format of an AT command from the HF to the AG shall be: <AT command><cr>
+ * - HFP 1.8, 4.34.1
+ *
+ * The format for a command from the HS to the AG is thus: AT<cmd>=<value><cr>
+ * - HSP 1.2, 4.8.1
+ */
+ message[len] = '\r';
+ /* `message` is no longer null-terminated */
+
+ len = write(rfcomm->source.fd, message, len + 1);
+ /* we ignore any errors, it's not critical and real errors should
+ * be caught with the HANGUP and ERROR events handled above */
+ if (len < 0) {
+ len = -errno;
+ spa_log_error(backend->log, "RFCOMM write error: %s", strerror(errno));
+ }
+
+ return len;
+}
+
+/* from AG to HF/HS */
+SPA_PRINTF_FUNC(2, 3)
+static ssize_t rfcomm_send_reply(const struct rfcomm *rfcomm, const char *format, ...)
+{
+ struct impl *backend = rfcomm->backend;
+ char message[RFCOMM_MESSAGE_MAX_LENGTH + 4];
+ ssize_t len;
+ va_list args;
+
+ va_start(args, format);
+ len = vsnprintf(&message[2], RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args);
+ va_end(args);
+
+ if (len < 0)
+ return -EINVAL;
+
+ if (len > RFCOMM_MESSAGE_MAX_LENGTH)
+ return -E2BIG;
+
+ spa_log_debug(backend->log, "RFCOMM >> %s", &message[2]);
+
+ /*
+ * The format of the OK code from the AG to the HF shall be: <cr><lf>OK<cr><lf>
+ * The format of the generic ERROR code from the AG to the HF shall be: <cr><lf>ERROR<cr><lf>
+ * The format of an unsolicited result code from the AG to the HF shall be: <cr><lf><result code><cr><lf>
+ * - HFP 1.8, 4.34.1
+ *
+ * If the command is processed successfully, the resulting response from the AG to the HS is: <cr><lf>OK<cr><lf>
+ * If the command is not processed successfully, or is not recognized,
+ * the resulting response from the AG to the HS is: <cr><lf>ERROR<cr><lf>
+ * The format for an unsolicited result code (such as RING) from the AG to the HS is: <cr><lf><result code><cr><lf>
+ * - HSP 1.2, 4.8.1
+ */
+ message[0] = '\r';
+ message[1] = '\n';
+ message[len + 2] = '\r';
+ message[len + 3] = '\n';
+ /* `message` is no longer null-terminated */
+
+ len = write(rfcomm->source.fd, message, len + 4);
+ /* we ignore any errors, it's not critical and real errors should
+ * be caught with the HANGUP and ERROR events handled above */
+ if (len < 0) {
+ len = -errno;
+ spa_log_error(backend->log, "RFCOMM write error: %s", strerror(errno));
+ }
+
+ return len;
+}
+
+static void rfcomm_send_error(const struct rfcomm *rfcomm, enum cmee_error error)
+{
+ if (rfcomm->extended_error_reporting)
+ rfcomm_send_reply(rfcomm, "+CME ERROR: %d", error);
+ else
+ rfcomm_send_reply(rfcomm, "ERROR");
+}
+
+static bool rfcomm_volume_enabled(struct rfcomm *rfcomm)
+{
+ return rfcomm->device != NULL
+ && (rfcomm->device->hw_volume_profiles & rfcomm->profile);
+}
+
+static void rfcomm_emit_volume_changed(struct rfcomm *rfcomm, int id, int hw_volume)
+{
+ struct spa_bt_transport_volume *t_volume;
+
+ if (!rfcomm_volume_enabled(rfcomm))
+ return;
+
+ if ((id == SPA_BT_VOLUME_ID_RX || id == SPA_BT_VOLUME_ID_TX) && hw_volume >= 0) {
+ rfcomm->volumes[id].active = true;
+ rfcomm->volumes[id].hw_volume = hw_volume;
+ }
+
+ spa_log_debug(rfcomm->backend->log, "volume changed %d", hw_volume);
+
+ if (rfcomm->transport == NULL || !rfcomm->has_volume)
+ return;
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) {
+ t_volume = &rfcomm->transport->volumes[i];
+ t_volume->active = rfcomm->volumes[i].active;
+ t_volume->volume =
+ spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t_volume->hw_volume_max);
+ }
+
+ spa_bt_transport_emit_volume_changed(rfcomm->transport);
+}
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+static bool rfcomm_hsp_ag(struct rfcomm *rfcomm, char* buf)
+{
+ struct impl *backend = rfcomm->backend;
+ unsigned int gain, dummy;
+
+ /* There are only three HSP AT commands:
+ * AT+VGS=value: value between 0 and 15, sent by the HS to AG to set the speaker gain.
+ * AT+VGM=value: value between 0 and 15, sent by the HS to AG to set the microphone gain.
+ * AT+CKPD=200: Sent by HS when headset button is pressed. */
+ if (sscanf(buf, "AT+VGS=%d", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf);
+ rfcomm_send_reply(rfcomm, "ERROR");
+ }
+ } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ if (!rfcomm->broken_mic_hw_volume)
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ rfcomm_send_reply(rfcomm, "ERROR");
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf);
+ }
+ } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) {
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+static bool rfcomm_send_volume_cmd(struct rfcomm *rfcomm, int id)
+{
+ struct spa_bt_transport_volume *t_volume;
+ const char *format;
+ int hw_volume;
+
+ if (!rfcomm_volume_enabled(rfcomm))
+ return false;
+
+ t_volume = rfcomm->transport ? &rfcomm->transport->volumes[id] : NULL;
+
+ if (!(t_volume && t_volume->active))
+ return false;
+
+ hw_volume = spa_bt_volume_linear_to_hw(t_volume->volume, t_volume->hw_volume_max);
+ rfcomm->volumes[id].hw_volume = hw_volume;
+
+ if (id == SPA_BT_VOLUME_ID_TX)
+ format = "AT+VGM";
+ else if (id == SPA_BT_VOLUME_ID_RX)
+ format = "AT+VGS";
+ else
+ spa_assert_not_reached();
+
+ rfcomm_send_cmd(rfcomm, "%s=%d", format, hw_volume);
+
+ return true;
+}
+
+static bool rfcomm_hsp_hs(struct rfcomm *rfcomm, char* buf)
+{
+ struct impl *backend = rfcomm->backend;
+ unsigned int gain;
+
+ /* There are only three HSP AT result codes:
+ * +VGS=value: value between 0 and 15, sent by AG to HS as a response to an AT+VGS command
+ * or when the gain is changed on the AG side.
+ * +VGM=value: value between 0 and 15, sent by AG to HS as a response to an AT+VGM command
+ * or when the gain is changed on the AG side.
+ * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because
+ * it does not expect a reply. */
+ if (sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf);
+ }
+ } else if (sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf);
+ }
+ } else if (spa_strstartswith(buf, "\r\nOK\r\n")) {
+ if (rfcomm->hs_state == hsp_hs_init2) {
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+ rfcomm->hs_state = hsp_hs_vgs;
+ else
+ rfcomm->hs_state = hsp_hs_init1;
+ } else if (rfcomm->hs_state == hsp_hs_vgs) {
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX))
+ rfcomm->hs_state = hsp_hs_vgm;
+ else
+ rfcomm->hs_state = hsp_hs_init1;
+ }
+ }
+
+ return true;
+}
+#endif
+
+#ifdef HAVE_LIBUSB
+static bool check_usb_altsetting_6(struct impl *backend, uint16_t vendor_id, uint16_t product_id)
+{
+ libusb_context *ctx = NULL;
+ struct libusb_config_descriptor *cfg = NULL;
+ libusb_device **devices = NULL;
+
+ ssize_t ndev, idev;
+ int res;
+ bool ok = false;
+
+ if ((res = libusb_init(&ctx)) < 0) {
+ ctx = NULL;
+ goto fail;
+ }
+
+ if ((ndev = libusb_get_device_list(ctx, &devices)) < 0) {
+ res = ndev;
+ devices = NULL;
+ goto fail;
+ }
+
+ for (idev = 0; idev < ndev; ++idev) {
+ libusb_device *dev = devices[idev];
+ struct libusb_device_descriptor desc;
+ int icfg;
+
+ libusb_get_device_descriptor(dev, &desc);
+ if (vendor_id != desc.idVendor || product_id != desc.idProduct)
+ continue;
+
+ /* Check the device has Bluetooth isoch. altsetting 6 interface */
+
+ for (icfg = 0; icfg < desc.bNumConfigurations; ++icfg) {
+ int iiface;
+
+ if ((res = libusb_get_config_descriptor(dev, icfg, &cfg)) != 0) {
+ cfg = NULL;
+ goto fail;
+ }
+
+ for (iiface = 0; iiface < cfg->bNumInterfaces; ++iiface) {
+ const struct libusb_interface *iface = &cfg->interface[iiface];
+ int ialt;
+
+ for (ialt = 0; ialt < iface->num_altsetting; ++ialt) {
+ const struct libusb_interface_descriptor *idesc = &iface->altsetting[ialt];
+ int iep;
+
+ if (idesc->bInterfaceClass != LIBUSB_CLASS_WIRELESS ||
+ idesc->bInterfaceSubClass != 1 /* RF */ ||
+ idesc->bInterfaceProtocol != 1 /* Bluetooth */ ||
+ idesc->bAlternateSetting != 6)
+ continue;
+
+ for (iep = 0; iep < idesc->bNumEndpoints; ++iep) {
+ const struct libusb_endpoint_descriptor *ep = &idesc->endpoint[iep];
+ if ((ep->bmAttributes & 0x3) == 0x1 /* isochronous */) {
+ ok = true;
+ goto done;
+ }
+ }
+ }
+ }
+
+ libusb_free_config_descriptor(cfg);
+ cfg = NULL;
+ }
+ }
+
+done:
+ if (cfg)
+ libusb_free_config_descriptor(cfg);
+ if (devices)
+ libusb_free_device_list(devices, 0);
+ if (ctx)
+ libusb_exit(ctx);
+ return ok;
+
+fail:
+ spa_log_info(backend->log, "failed to acquire USB device info: %d (%s)",
+ res, libusb_strerror(res));
+ ok = false;
+ goto done;
+}
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+
+static bool device_supports_required_mSBC_transport_modes(
+ struct impl *backend, struct spa_bt_device *device)
+{
+ int res;
+ bool msbc_ok, msbc_alt1_ok;
+ uint32_t bt_features;
+
+ if (device->adapter == NULL)
+ return false;
+
+ if (backend->quirks && spa_bt_quirks_get_features(backend->quirks, device->adapter, device, &bt_features) == 0) {
+ msbc_ok = bt_features & SPA_BT_FEATURE_MSBC;
+ msbc_alt1_ok = bt_features & (SPA_BT_FEATURE_MSBC_ALT1 | SPA_BT_FEATURE_MSBC_ALT1_RTL);
+ } else {
+ msbc_ok = true;
+ msbc_alt1_ok = true;
+ }
+
+ spa_log_info(backend->log,
+ "bluez-monitor/hardware.conf: msbc:%d msbc-alt1:%d", (int)msbc_ok, (int)msbc_alt1_ok);
+
+ if (!msbc_ok && !msbc_alt1_ok)
+ return false;
+
+ res = spa_bt_adapter_has_msbc(device->adapter);
+ if (res < 0) {
+ spa_log_warn(backend->log,
+ "adapter %s: failed to determine msbc/esco capability (%d)",
+ device->adapter->path, res);
+ } else if (res == 0) {
+ spa_log_info(backend->log,
+ "adapter %s: no msbc/esco transport",
+ device->adapter->path);
+ return false;
+ } else {
+ spa_log_debug(backend->log,
+ "adapter %s: has msbc/esco transport",
+ device->adapter->path);
+ }
+
+ /* Check if USB ALT6 is really available on the device */
+ if (device->adapter->bus_type == BUS_TYPE_USB && !msbc_alt1_ok && msbc_ok) {
+#ifdef HAVE_LIBUSB
+ if (device->adapter->source_id == SOURCE_ID_USB) {
+ msbc_ok = check_usb_altsetting_6(backend, device->adapter->vendor_id,
+ device->adapter->product_id);
+ } else {
+ msbc_ok = false;
+ }
+ if (!msbc_ok)
+ spa_log_info(backend->log, "bluetooth host adapter does not support USB ALT6");
+#else
+ spa_log_info(backend->log,
+ "compiled without libusb; can't check if bluetooth adapter has USB ALT6");
+ msbc_ok = false;
+#endif
+ }
+ if (device->adapter->bus_type != BUS_TYPE_USB)
+ msbc_alt1_ok = false;
+
+ return msbc_ok || msbc_alt1_ok;
+}
+
+static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec);
+
+static void process_iphoneaccev_indicator(struct rfcomm *rfcomm, unsigned int key, unsigned int value)
+{
+ struct impl *backend = rfcomm->backend;
+
+ spa_log_debug(backend->log, "key:%u value:%u", key, value);
+
+ switch (key) {
+ case SPA_BT_HFP_HF_IPHONEACCEV_KEY_BATTERY_LEVEL: {
+ // Battery level is reported in range of 0-9, convert to 10-100%
+ uint8_t level = (SPA_CLAMP(value, 0u, 9u) + 1) * 10;
+ spa_log_debug(backend->log, "battery level: %d%%", (int) level);
+
+ // TODO: report without Battery Provider (using props)
+ spa_bt_device_report_battery_level(rfcomm->device, level);
+ break;
+ }
+ case SPA_BT_HFP_HF_IPHONEACCEV_KEY_DOCK_STATE:
+ break;
+ default:
+ spa_log_warn(backend->log, "unknown AT+IPHONEACCEV key:%u value:%u", key, value);
+ break;
+ }
+}
+
+static void process_hfp_hf_indicator(struct rfcomm *rfcomm, unsigned int indicator, unsigned int value)
+{
+ struct impl *backend = rfcomm->backend;
+
+ spa_log_debug(backend->log, "indicator:%u value:%u", indicator, value);
+
+ switch (indicator) {
+ case SPA_BT_HFP_HF_INDICATOR_ENHANCED_SAFETY:
+ break;
+ case SPA_BT_HFP_HF_INDICATOR_BATTERY_LEVEL:
+ // Battery level is reported in range 0-100
+ spa_log_debug(backend->log, "battery level: %u%%", value);
+
+ if (value <= 100) {
+ // TODO: report without Battery Provider (using props)
+ spa_bt_device_report_battery_level(rfcomm->device, value);
+ } else {
+ spa_log_warn(backend->log, "battery HF indicator %u outside of range [0, 100]: %u", indicator, value);
+ }
+ break;
+ default:
+ spa_log_warn(backend->log, "unknown HF indicator:%u value:%u", indicator, value);
+ break;
+ }
+}
+
+static void rfcomm_hfp_ag_set_cind(struct rfcomm *rfcomm, bool call_active)
+{
+ if (rfcomm->profile != SPA_BT_PROFILE_HFP_HF)
+ return;
+
+ if (call_active == rfcomm->cind_call_active)
+ return;
+
+ rfcomm->cind_call_active = call_active;
+
+ if (!rfcomm->cind_call_notify)
+ return;
+
+ rfcomm_send_reply(rfcomm, "+CIEV: 2,%d", rfcomm->cind_call_active);
+}
+
+static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf)
+{
+ struct impl *backend = rfcomm->backend;
+ unsigned int features;
+ unsigned int gain;
+ unsigned int count, r;
+ unsigned int selected_codec;
+ unsigned int indicator;
+ unsigned int indicator_value;
+ unsigned int value;
+ int xapl_vendor;
+ int xapl_product;
+ int xapl_features;
+
+ spa_debug_log_mem(backend->log, SPA_LOG_LEVEL_DEBUG, 2, buf, strlen(buf));
+
+ /* Some devices send initial \n: be permissive */
+ while (*buf == '\n')
+ ++buf;
+
+ if (sscanf(buf, "AT+BRSF=%u", &features) == 1) {
+ unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE;
+
+ /*
+ * Determine device volume control. Some headsets only support control of
+ * TX volume, but not RX, even if they have a microphone. Determine this
+ * separately based on whether we also get AT+VGS/AT+VGM, and quirks.
+ */
+ rfcomm->has_volume = (features & SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL);
+
+ /* Decide if we want to signal that the computer supports mSBC negotiation
+ This should be done when the computers bluetooth adapter supports the necessary transport mode */
+ if (device_supports_required_mSBC_transport_modes(backend, rfcomm->device)) {
+
+ /* set the feature bit that indicates AG (=computer) supports codec negotiation */
+ ag_features |= SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION;
+
+ /* let's see if the headset supports codec negotiation */
+ if ((features & (SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION)) != 0) {
+ spa_log_debug(backend->log,
+ "RFCOMM features = %i, codec negotiation supported by headset",
+ features);
+ /* Prepare reply: Audio Gateway (=computer) supports codec negotiation */
+ rfcomm->codec_negotiation_supported = true;
+ rfcomm->msbc_supported_by_hfp = false;
+ } else {
+ /* Codec negotiation not supported */
+ spa_log_debug(backend->log,
+ "RFCOMM features = %i, codec negotiation NOT supported by headset",
+ features);
+
+ rfcomm->codec_negotiation_supported = false;
+ rfcomm->msbc_supported_by_hfp = false;
+ }
+ }
+
+ /* send reply to HF with the features supported by Audio Gateway (=computer) */
+ ag_features |= mm_supported_features();
+ ag_features |= SPA_BT_HFP_AG_FEATURE_HF_INDICATORS;
+ rfcomm_send_reply(rfcomm, "+BRSF: %u", ag_features);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+BAC=")) {
+ /* retrieve supported codecs */
+ /* response has the form AT+BAC=<codecID1>,<codecID2>,<codecIDx>
+ strategy: split the string into tokens */
+
+ char* token;
+ int cntr = 0;
+
+ while ((token = strsep(&buf, "=,"))) {
+ unsigned int codec_id;
+
+ /* skip token 0 i.e. the "AT+BAC=" part */
+ if (cntr > 0 && sscanf(token, "%u", &codec_id) == 1) {
+ spa_log_debug(backend->log, "RFCOMM AT+BAC found codec %u", codec_id);
+ if (codec_id == HFP_AUDIO_CODEC_MSBC) {
+ rfcomm->msbc_supported_by_hfp = true;
+ spa_log_debug(backend->log, "RFCOMM headset supports mSBC codec");
+ }
+ }
+ cntr++;
+ }
+
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CIND=?")) {
+ rfcomm_send_reply(rfcomm, "+CIND:%s", CIND_INDICATORS);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CIND?")) {
+ rfcomm_send_reply(rfcomm, "+CIND: %d,%d,%d,0,%d,%d,%d", backend->modem.network_has_service,
+ backend->modem.active_call, backend->modem.call_setup, backend->modem.signal_strength,
+ backend->modem.network_is_roaming, backend->battery_level);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CMER")) {
+ int mode, keyp, disp, ind;
+
+ rfcomm->slc_configured = true;
+ rfcomm_send_reply(rfcomm, "OK");
+
+ rfcomm->cind_call_active = false;
+ if (sscanf(buf, "AT+CMER= %d , %d , %d , %d", &mode, &keyp, &disp, &ind) == 4)
+ rfcomm->cind_call_notify = ind ? true : false;
+ else
+ rfcomm->cind_call_notify = false;
+
+ /* switch codec to mSBC by sending unsolicited +BCS message */
+ if (rfcomm->codec_negotiation_supported && rfcomm->msbc_supported_by_hfp) {
+ spa_log_debug(backend->log, "RFCOMM initial codec setup");
+ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_SEND;
+ rfcomm_send_reply(rfcomm, "+BCS: 2");
+ codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC);
+ } else {
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ // 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);
+ }
+ }
+ } else if (spa_streq(buf, "\r")) {
+ /* No commands, reply OK (ITU-T Rec. V.250 Sec. 5.2.1 & 5.6) */
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (!rfcomm->slc_configured) {
+ spa_log_warn(backend->log, "RFCOMM receive command before SLC completed: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+
+ /* *****
+ * Following commands requires a Service Level Connection
+ * ***** */
+
+ } else if (sscanf(buf, "AT+BCS=%u", &selected_codec) == 1) {
+ /* parse BCS(=Bluetooth Codec Selection) reply */
+ bool was_switching_codec = rfcomm->hfp_ag_switching_codec && (rfcomm->device != NULL);
+ rfcomm->hfp_ag_switching_codec = false;
+ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE;
+ codec_switch_stop_timer(rfcomm);
+ volume_sync_stop_timer(rfcomm);
+
+ if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) {
+ spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ if (was_switching_codec)
+ spa_bt_device_emit_codec_switched(rfcomm->device, -EIO);
+ return true;
+ }
+
+ rfcomm->codec = selected_codec;
+
+ spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec);
+
+ /* Recreate transport, since previous connection may now be invalid */
+ if (rfcomm->transport)
+ spa_bt_transport_free(rfcomm->transport);
+
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ // 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);
+
+ rfcomm_send_reply(rfcomm, "OK");
+ if (was_switching_codec)
+ spa_bt_device_emit_codec_switched(rfcomm->device, 0);
+ } else if (spa_strstartswith(buf, "AT+BIA=")) {
+ /* retrieve indicators activation
+ * form: AT+BIA=[indrep1],[indrep2],[indrepx] */
+ char *str = buf + 7;
+ unsigned int ind = 1;
+
+ while (*str && ind < CIND_MAX && *str != '\r' && *str != '\n') {
+ if (*str == ',') {
+ ind++;
+ goto next_indicator;
+ }
+
+ /* Ignore updates to mandantory indicators which are always ON */
+ if (ind == CIND_CALL || ind == CIND_CALLSETUP || ind == CIND_CALLHELD)
+ goto next_indicator;
+
+ switch (*str) {
+ case '0':
+ rfcomm->cind_enabled_indicators &= ~(1 << ind);
+ break;
+ case '1':
+ rfcomm->cind_enabled_indicators |= (1 << ind);
+ break;
+ default:
+ spa_log_warn(backend->log, "Unsupported entry in %s: %c", buf, *str);
+ }
+next_indicator:
+ str++;
+ }
+
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CLCC")) {
+ struct spa_list *calls;
+ struct call *call;
+ unsigned int type;
+
+ if (backend->modemmanager) {
+ calls = mm_get_calls(backend->modemmanager);
+ spa_list_for_each(call, calls, link) {
+ if (!call->number) {
+ rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u", call->index, call->direction, call->state, call->multiparty);
+ } else {
+ if (spa_strstartswith(call->number, "+"))
+ type = INTERNATIONAL_NUMBER;
+ else
+ type = NATIONAL_NUMBER;
+ rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u,\"%s\",%d", call->index, call->direction, call->state,
+ call->multiparty, call->number, type);
+ }
+ }
+ }
+
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (sscanf(buf, "AT+CLIP=%u", &value) == 1) {
+ if (value > 1) {
+ spa_log_debug(backend->log, "Unsupported AT+CLIP value: %u", value);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+ }
+
+ rfcomm->clip_notify = value;
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (sscanf(buf, "AT+CMEE=%u", &value) == 1) {
+ if (value > 1) {
+ spa_log_debug(backend->log, "Unsupported AT+CMEE value: %u", value);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+ }
+
+ rfcomm->extended_error_reporting = value;
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+CNUM")) {
+ if (backend->modem.own_number) {
+ unsigned int type;
+ if (spa_strstartswith(backend->modem.own_number, "+"))
+ type = INTERNATIONAL_NUMBER;
+ else
+ type = NATIONAL_NUMBER;
+ rfcomm_send_reply(rfcomm, "+CNUM: ,\"%s\",%u", backend->modem.own_number, type);
+ }
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+COPS=")) {
+ unsigned int mode, val;
+
+ if (sscanf(buf, "AT+COPS=%u,%u", &mode, &val) != 2 ||
+ mode != 3 || val != 0) {
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ } else {
+ rfcomm_send_reply(rfcomm, "OK");
+ }
+ } else if (spa_strstartswith(buf, "AT+COPS?")) {
+ if (!backend->modem.network_has_service) {
+ rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE);
+ } else {
+ if (backend->modem.operator_name)
+ rfcomm_send_reply(rfcomm, "+COPS: 0,0,\"%s\"", backend->modem.operator_name);
+ else
+ rfcomm_send_reply(rfcomm, "+COPS: 0,,");
+ rfcomm_send_reply(rfcomm, "OK");
+ }
+ } else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ if (!rfcomm->broken_mic_hw_volume)
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED);
+ }
+ } else if (sscanf(buf, "AT+VGS=%u", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
+ rfcomm_send_reply(rfcomm, "OK");
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED);
+ }
+ } else if (spa_strstartswith(buf, "AT+BIND=?")) {
+ rfcomm_send_reply(rfcomm, "+BIND: (2)");
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+BIND?")) {
+ rfcomm_send_reply(rfcomm, "+BIND: 2,1");
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (spa_strstartswith(buf, "AT+BIND=")) {
+ // BIND=... should return a comma separated list of indicators and
+ // 2 should be among the other numbers telling that battery charge
+ // is supported
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (sscanf(buf, "AT+BIEV=%u,%u", &indicator, &indicator_value) == 2) {
+ process_hfp_hf_indicator(rfcomm, indicator, indicator_value);
+ } else if (sscanf(buf, "AT+XAPL=%04x-%04x-%*[^,],%u", &xapl_vendor, &xapl_product, &xapl_features) == 3) {
+ if (xapl_features & SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING) {
+ /* claim, that we support battery status reports */
+ rfcomm_send_reply(rfcomm, "+XAPL=iPhone,%u", SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING);
+ }
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (sscanf(buf, "AT+IPHONEACCEV=%u%n", &count, &r) == 1) {
+ if (count < 1 || count > 100)
+ return false;
+
+ buf += r;
+
+ for (unsigned int i = 0; i < count; i++) {
+ unsigned int key, value;
+
+ if (sscanf(buf, " , %u , %u%n", &key, &value, &r) != 2)
+ return false;
+
+ process_iphoneaccev_indicator(rfcomm, key, value);
+ buf += r;
+ }
+ } else if (spa_strstartswith(buf, "AT+APLSIRI?")) {
+ // This command is sent when we activate Apple extensions
+ rfcomm_send_reply(rfcomm, "OK");
+ } else if (!mm_is_available(backend->modemmanager)) {
+ spa_log_warn(backend->log, "RFCOMM receive command but modem not available: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_NO_CONNECTION_TO_PHONE);
+ return true;
+
+ /* *****
+ * Following commands requires a Service Level Connection
+ * and acces to a modem
+ * ***** */
+
+ } else if (!backend->modem.network_has_service) {
+ spa_log_warn(backend->log, "RFCOMM receive command but network not available: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE);
+ return true;
+
+ /* *****
+ * Following commands requires a Service Level Connection,
+ * acces to a modem and to the network
+ * ***** */
+
+ } else if (spa_strstartswith(buf, "ATA")) {
+ enum cmee_error error;
+
+ if (!mm_answer_call(backend->modemmanager, rfcomm, &error)) {
+ rfcomm_send_error(rfcomm, error);
+ return true;
+ }
+ } else if (spa_strstartswith(buf, "ATD")) {
+ char number[31], sep;
+ enum cmee_error error;
+
+ if (sscanf(buf, "ATD%30[^;]%c", number, &sep) != 2 || sep != ';') {
+ spa_log_debug(backend->log, "Failed to parse ATD: \"%s\"", buf);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+ }
+
+ if (!mm_do_call(backend->modemmanager, number, rfcomm, &error)) {
+ rfcomm_send_error(rfcomm, error);
+ return true;
+ }
+ } else if (spa_strstartswith(buf, "AT+CHUP")) {
+ enum cmee_error error;
+
+ if (!mm_hangup_call(backend->modemmanager, rfcomm, &error)) {
+ rfcomm_send_error(rfcomm, error);
+ return true;
+ }
+ } else if (spa_strstartswith(buf, "AT+VTS=")) {
+ char *dtmf;
+ enum cmee_error error;
+
+ dtmf = calloc(1, 2);
+ if (sscanf(buf, "AT+VTS=%1s", dtmf) != 1) {
+ spa_log_debug(backend->log, "Failed to parse AT+VTS: \"%s\"", buf);
+ rfcomm_send_error(rfcomm, CMEE_AG_FAILURE);
+ return true;
+ }
+
+ if (!mm_send_dtmf(backend->modemmanager, dtmf, rfcomm, &error)) {
+ rfcomm_send_error(rfcomm, error);
+ return true;
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* buf)
+{
+ struct impl *backend = rfcomm->backend;
+ unsigned int features, gain, selected_codec, indicator, value;
+ char* token;
+
+ while ((token = strsep(&buf, "\r\n"))) {
+ if (sscanf(token, "+BRSF:%u", &features) == 1) {
+ if (((features & (SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION)) != 0) &&
+ rfcomm->msbc_supported_by_hfp)
+ rfcomm->codec_negotiation_supported = true;
+ } else if (sscanf(token, "+BCS:%u", &selected_codec) == 1 && rfcomm->codec_negotiation_supported) {
+ if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) {
+ spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec);
+
+ /* send codec selection to AG */
+ rfcomm_send_cmd(rfcomm, "AT+BCS=%u", selected_codec);
+
+ rfcomm->hf_state = hfp_hf_bcs;
+
+ if (!rfcomm->transport || (rfcomm->transport->codec != selected_codec) ) {
+ if (rfcomm->transport)
+ spa_bt_transport_free(rfcomm->transport);
+
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ // TODO: We should manage the missing transport
+ } else {
+ rfcomm->transport->codec = selected_codec;
+ spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+ }
+ }
+ }
+ } else if (sscanf(token, "+VGM%*1[:=]%u", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", token);
+ }
+ } else if (sscanf(token, "+VGS%*1[:=]%u", &gain) == 1) {
+ if (gain <= SPA_BT_VOLUME_HS_MAX) {
+ rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain);
+ } else {
+ spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", token);
+ }
+ } else if (spa_strstartswith(token, "+CIND: (")) {
+ uint8_t i = 1;
+ while (strstr(token, "\"")) {
+ token += strcspn(token, "\"") + 1;
+ token[strcspn(token, "\"")] = 0;
+ rfcomm->hf_indicators[i] = strdup(token);
+ token += strcspn(token, "\"") + 1;
+ i++;
+ if (i == MAX_HF_INDICATORS) {
+ break;
+ }
+ }
+ } else if (spa_strstartswith(token, "+CIND: ")) {
+ token[strcspn(token, "\r")] = 0;
+ token[strcspn(token, "\n")] = 0;
+ token += strlen("+CIND: ");
+ uint8_t i = 1;
+ while (strlen(token)) {
+ if (i >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[i]) {
+ break;
+ }
+ token[strcspn(token, ",")] = 0;
+ spa_log_info(backend->log, "AG indicator state: %s = %i", rfcomm->hf_indicators[i], atoi(token));
+
+ if (spa_streq(rfcomm->hf_indicators[i], "battchg")) {
+ spa_bt_device_report_battery_level(rfcomm->device, atoi(token) * 100 / 5);
+ }
+
+ token += strcspn(token, "\0") + 1;
+ i++;
+ }
+ } else if (sscanf(token, "+CIEV: %u,%u", &indicator, &value) == 2) {
+ if (indicator >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[indicator]) {
+ spa_log_warn(backend->log, "indicator %u has not been registered, ignoring", indicator);
+ } else {
+ spa_log_info(backend->log, "AG indicator update: %s = %u", rfcomm->hf_indicators[indicator], value);
+
+ if (spa_streq(rfcomm->hf_indicators[indicator], "battchg")) {
+ spa_bt_device_report_battery_level(rfcomm->device, value * 100 / 5);
+ }
+ }
+ } else if (spa_strstartswith(token, "OK")) {
+ switch(rfcomm->hf_state) {
+ case hfp_hf_brsf:
+ if (rfcomm->codec_negotiation_supported) {
+ rfcomm_send_cmd(rfcomm, "AT+BAC=1,2");
+ 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_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) {
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ // TODO: We should manage the missing transport
+ } else {
+ rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
+ spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+ }
+ }
+ /* 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;
+ }
+ }
+ }
+
+ return true;
+}
+
+#endif
+
+static void rfcomm_event(struct spa_source *source)
+{
+ struct rfcomm *rfcomm = source->data;
+ struct impl *backend = rfcomm->backend;
+
+ if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
+ spa_log_info(backend->log, "lost RFCOMM connection.");
+ rfcomm_free(rfcomm);
+ return;
+ }
+
+ if (source->rmask & SPA_IO_IN) {
+ char buf[512];
+ ssize_t len;
+ bool res = false;
+
+ len = read(source->fd, buf, 511);
+ if (len < 0) {
+ spa_log_error(backend->log, "RFCOMM read error: %s", strerror(errno));
+ return;
+ }
+ buf[len] = 0;
+ spa_log_debug(backend->log, "RFCOMM << %s", buf);
+
+ switch (rfcomm->profile) {
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ case SPA_BT_PROFILE_HSP_HS:
+ res = rfcomm_hsp_ag(rfcomm, buf);
+ break;
+ case SPA_BT_PROFILE_HSP_AG:
+ res = rfcomm_hsp_hs(rfcomm, buf);
+ break;
+#endif
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ case SPA_BT_PROFILE_HFP_HF:
+ res = rfcomm_hfp_ag(rfcomm, buf);
+ break;
+ case SPA_BT_PROFILE_HFP_AG:
+ res = rfcomm_hfp_hf(rfcomm, buf);
+ break;
+#endif
+ default:
+ break;
+ }
+
+ if (!res) {
+ spa_log_debug(backend->log, "RFCOMM received unsupported command: %s", buf);
+ rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_SUPPORTED);
+ }
+ }
+}
+
+static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapter, bool msbc)
+{
+ struct sockaddr_sco addr;
+ socklen_t len;
+ bdaddr_t src;
+ int sock = -1;
+
+ sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO);
+ if (sock < 0) {
+ spa_log_error(backend->log, "socket(SEQPACKET, SCO) %s", strerror(errno));
+ goto fail;
+ }
+
+ str2ba(adapter->address, &src);
+
+ len = sizeof(addr);
+ memset(&addr, 0, len);
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, &src);
+
+ if (bind(sock, (struct sockaddr *) &addr, len) < 0) {
+ spa_log_error(backend->log, "bind(): %s", strerror(errno));
+ goto fail;
+ }
+
+ spa_log_debug(backend->log, "msbc=%d", (int)msbc);
+ if (msbc) {
+ /* set correct socket options for mSBC */
+ struct bt_voice voice_config;
+ memset(&voice_config, 0, sizeof(voice_config));
+ voice_config.setting = BT_VOICE_TRANSPARENT;
+ if (setsockopt(sock, SOL_BLUETOOTH, BT_VOICE, &voice_config, sizeof(voice_config)) < 0) {
+ spa_log_error(backend->log, "setsockopt(): %s", strerror(errno));
+ goto fail;
+ }
+ }
+
+ return sock;
+
+fail:
+ if (sock >= 0)
+ close(sock);
+ return -1;
+}
+
+static int sco_do_connect(struct spa_bt_transport *t)
+{
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+ struct spa_bt_device *d = t->device;
+ struct transport_data *td = t->user_data;
+ struct sockaddr_sco addr;
+ socklen_t len;
+ int err;
+ int sock;
+ bdaddr_t dst;
+ int retry = 2;
+
+ spa_log_debug(backend->log, "transport %p: enter sco_do_connect, codec=%u",
+ t, t->codec);
+
+ if (d->adapter == NULL)
+ return -EIO;
+
+ str2ba(d->address, &dst);
+
+again:
+ sock = sco_create_socket(backend, d->adapter, (t->codec == HFP_AUDIO_CODEC_MSBC));
+ if (sock < 0)
+ return -1;
+
+ len = sizeof(addr);
+ memset(&addr, 0, len);
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, &dst);
+
+ spa_log_debug(backend->log, "transport %p: doing connect", t);
+ err = connect(sock, (struct sockaddr *) &addr, len);
+ if (err < 0 && errno == ECONNABORTED && retry-- > 0) {
+ spa_log_warn(backend->log, "connect(): %s. Remaining retry:%d",
+ strerror(errno), retry);
+ close(sock);
+ goto again;
+ } else if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) {
+ spa_log_error(backend->log, "connect(): %s", strerror(errno));
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (errno == EOPNOTSUPP && t->codec == HFP_AUDIO_CODEC_MSBC &&
+ td->rfcomm->msbc_supported_by_hfp) {
+ /* Adapter doesn't support msbc. Renegotiate. */
+ d->adapter->msbc_probed = true;
+ d->adapter->has_msbc = false;
+ td->rfcomm->msbc_supported_by_hfp = false;
+ if (t->profile == SPA_BT_PROFILE_HFP_HF) {
+ td->rfcomm->hfp_ag_switching_codec = true;
+ rfcomm_send_reply(td->rfcomm, "+BCS: 1");
+ } else if (t->profile == SPA_BT_PROFILE_HFP_AG) {
+ rfcomm_send_cmd(td->rfcomm, "AT+BAC=1");
+ }
+ }
+#endif
+ goto fail_close;
+ }
+
+ return sock;
+
+fail_close:
+ close(sock);
+ return -1;
+}
+
+static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later);
+
+static void wait_for_socket(int fd)
+{
+ struct pollfd fds[1];
+ const int timeout_ms = 500;
+
+ fds[0].fd = fd;
+ fds[0].events = POLLIN | POLLERR | POLLHUP;
+ poll(fds, 1, timeout_ms);
+}
+
+static int sco_acquire_cb(void *data, bool optional)
+{
+ struct spa_bt_transport *t = data;
+ struct transport_data *td = t->user_data;
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+ int sock;
+ socklen_t len;
+
+ spa_log_debug(backend->log, "transport %p: enter sco_acquire_cb", t);
+
+ if (optional || t->fd > 0)
+ sock = t->fd;
+ else
+ sock = sco_do_connect(t);
+
+ if (sock < 0)
+ goto fail;
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ rfcomm_hfp_ag_set_cind(td->rfcomm, true);
+#endif
+
+ /*
+ * Send RFCOMM volume after connection is ready, and also after
+ * a timeout.
+ *
+ * Some headsets adjust their HFP volume when in A2DP mode
+ * without reporting via RFCOMM to us, so the volume level can
+ * be out of sync, and we can't know what it is. Moreover, they may
+ * take the first +VGS command after connection only partially
+ * into account, and need a long enough timeout.
+ *
+ * E.g. with Sennheiser HD-250BT, the first +VGS changes the
+ * actual volume, but does not update the level in the hardware
+ * volume buttons, which is updated by an +VGS event only after
+ * sufficient time is elapsed from the connection.
+ */
+ wait_for_socket(sock);
+ rfcomm_ag_sync_volume(td->rfcomm, false);
+ rfcomm_ag_sync_volume(td->rfcomm, true);
+
+ t->fd = sock;
+
+ /* Fallback value */
+ t->read_mtu = 48;
+ t->write_mtu = 48;
+
+ if (true) {
+ struct sco_options sco_opt;
+
+ len = sizeof(sco_opt);
+ memset(&sco_opt, 0, len);
+
+ if (getsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0)
+ spa_log_warn(backend->log, "getsockopt(SCO_OPTIONS) failed, loading defaults");
+ else {
+ spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu);
+ t->read_mtu = sco_opt.mtu;
+ t->write_mtu = sco_opt.mtu;
+ }
+ }
+ spa_log_debug(backend->log, "transport %p: read_mtu=%u, write_mtu=%u", t, t->read_mtu, t->write_mtu);
+
+ return 0;
+
+fail:
+ return -1;
+}
+
+static int sco_release_cb(void *data)
+{
+ struct spa_bt_transport *t = data;
+ struct transport_data *td = t->user_data;
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+
+ spa_log_info(backend->log, "Transport %s released", t->path);
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ rfcomm_hfp_ag_set_cind(td->rfcomm, false);
+#endif
+
+ if (t->sco_io) {
+ spa_bt_sco_io_destroy(t->sco_io);
+ t->sco_io = NULL;
+ }
+
+ if (t->fd > 0) {
+ /* Shutdown and close the socket */
+ shutdown(t->fd, SHUT_RDWR);
+ close(t->fd);
+ t->fd = -1;
+ }
+
+ return 0;
+}
+
+static void sco_event(struct spa_source *source)
+{
+ struct spa_bt_transport *t = source->data;
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+
+ if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
+ spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno));
+ if (t->fd >= 0) {
+ if (source->loop)
+ spa_loop_remove_source(source->loop, source);
+ shutdown(t->fd, SHUT_RDWR);
+ close (t->fd);
+ t->fd = -1;
+ spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE);
+ }
+ }
+}
+
+static void sco_listen_event(struct spa_source *source)
+{
+ struct impl *backend = source->data;
+ struct sockaddr_sco addr;
+ socklen_t addrlen;
+ int sock = -1;
+ char local_address[18], remote_address[18];
+ struct rfcomm *rfcomm;
+ struct spa_bt_transport *t = NULL;
+ struct transport_data *td;
+
+ if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
+ spa_log_error(backend->log, "error listening SCO connection: %s", strerror(errno));
+ goto fail;
+ }
+
+ memset(&addr, 0, sizeof(addr));
+ addrlen = sizeof(addr);
+
+ spa_log_debug(backend->log, "doing accept");
+ sock = accept(source->fd, (struct sockaddr *) &addr, &addrlen);
+ if (sock < 0) {
+ if (errno != EAGAIN)
+ spa_log_error(backend->log, "SCO accept(): %s", strerror(errno));
+ goto fail;
+ }
+
+ ba2str(&addr.sco_bdaddr, remote_address);
+
+ memset(&addr, 0, sizeof(addr));
+ addrlen = sizeof(addr);
+
+ if (getsockname(sock, (struct sockaddr *) &addr, &addrlen) < 0) {
+ spa_log_error(backend->log, "SCO getsockname(): %s", strerror(errno));
+ goto fail;
+ }
+
+ ba2str(&addr.sco_bdaddr, local_address);
+
+ /* Find transport for local and remote address */
+ spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+ if (rfcomm->transport && spa_streq(rfcomm->transport->device->address, remote_address) &&
+ spa_streq(rfcomm->transport->device->adapter->address, local_address)) {
+ t = rfcomm->transport;
+ break;
+ }
+ }
+ if (!t) {
+ spa_log_debug(backend->log, "No transport for adapter %s and remote %s",
+ local_address, remote_address);
+ goto fail;
+ }
+
+ /* The Synchronous Connection shall always be established by the AG, i.e. the remote profile
+ should be a HSP AG or HFP AG profile */
+ if ((t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) == 0) {
+ spa_log_debug(backend->log, "transport %p: Rejecting incoming audio connection to an AG profile", t);
+ goto fail;
+ }
+
+ if (t->fd >= 0) {
+ spa_log_debug(backend->log, "transport %p: Rejecting, audio already connected", t);
+ goto fail;
+ }
+
+ spa_log_debug(backend->log, "transport %p: codec=%u", t, t->codec);
+ if (backend->defer_setup_enabled) {
+ /* In BT_DEFER_SETUP mode, when a connection is accepted, the listening socket is unblocked but
+ * the effective connection setup happens only on first receive, allowing to configure the
+ * accepted socket. */
+ char buff;
+
+ if (t->codec == HFP_AUDIO_CODEC_MSBC) {
+ /* set correct socket options for mSBC */
+ struct bt_voice voice_config;
+ memset(&voice_config, 0, sizeof(voice_config));
+ voice_config.setting = BT_VOICE_TRANSPARENT;
+ if (setsockopt(sock, SOL_BLUETOOTH, BT_VOICE, &voice_config, sizeof(voice_config)) < 0) {
+ spa_log_error(backend->log, "transport %p: setsockopt(): %s", t, strerror(errno));
+ goto fail;
+ }
+ }
+
+ /* First read from the accepted socket is non-blocking and returns a zero length buffer. */
+ if (read(sock, &buff, 1) == -1) {
+ spa_log_error(backend->log, "transport %p: Couldn't authorize SCO connection: %s", t, strerror(errno));
+ goto fail;
+ }
+ }
+
+ t->fd = sock;
+
+ td = t->user_data;
+ td->sco.func = sco_event;
+ td->sco.data = t;
+ td->sco.fd = sock;
+ td->sco.mask = SPA_IO_HUP | SPA_IO_ERR;
+ td->sco.rmask = 0;
+ spa_loop_add_source(backend->main_loop, &td->sco);
+
+ spa_log_debug(backend->log, "transport %p: audio connected", t);
+
+ /* Report initial volume to remote */
+ if (t->profile == SPA_BT_PROFILE_HSP_AG) {
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+ rfcomm->hs_state = hsp_hs_vgs;
+ else
+ rfcomm->hs_state = hsp_hs_init1;
+ } else if (t->profile == SPA_BT_PROFILE_HFP_AG) {
+ if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX))
+ rfcomm->hf_state = hfp_hf_vgs;
+ else
+ rfcomm->hf_state = hfp_hf_slc1;
+ }
+
+ spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_PENDING);
+ return;
+
+fail:
+ if (sock >= 0)
+ close(sock);
+ return;
+}
+
+static int sco_listen(struct impl *backend)
+{
+ struct sockaddr_sco addr;
+ int sock;
+ uint32_t defer = 1;
+
+ sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, BTPROTO_SCO);
+ if (sock < 0) {
+ spa_log_error(backend->log, "socket(SEQPACKET, SCO) %m");
+ return -errno;
+ }
+
+ /* Bind to local address */
+ memset(&addr, 0, sizeof(addr));
+ addr.sco_family = AF_BLUETOOTH;
+ bacpy(&addr.sco_bdaddr, BDADDR_ANY);
+
+ if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
+ spa_log_error(backend->log, "bind(): %m");
+ goto fail_close;
+ }
+
+ if (setsockopt(sock, SOL_BLUETOOTH, BT_DEFER_SETUP, &defer, sizeof(defer)) < 0) {
+ spa_log_warn(backend->log, "Can't enable deferred setup: %s", strerror(errno));
+ backend->defer_setup_enabled = 0;
+ } else {
+ backend->defer_setup_enabled = 1;
+ }
+
+ spa_log_debug(backend->log, "doing listen");
+ if (listen(sock, 1) < 0) {
+ spa_log_error(backend->log, "listen(): %m");
+ goto fail_close;
+ }
+
+ backend->sco.func = sco_listen_event;
+ backend->sco.data = backend;
+ backend->sco.fd = sock;
+ backend->sco.mask = SPA_IO_IN;
+ backend->sco.rmask = 0;
+ spa_loop_add_source(backend->main_loop, &backend->sco);
+
+ return sock;
+
+fail_close:
+ close(sock);
+ return -1;
+}
+
+static int rfcomm_ag_set_volume(struct spa_bt_transport *t, int id)
+{
+ struct transport_data *td = t->user_data;
+ struct rfcomm *rfcomm = td->rfcomm;
+ const char *format;
+ int value;
+
+ if (!rfcomm_volume_enabled(rfcomm)
+ || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ || !(rfcomm->has_volume && rfcomm->volumes[id].active))
+ return -ENOTSUP;
+
+ value = rfcomm->volumes[id].hw_volume;
+
+ if (id == SPA_BT_VOLUME_ID_RX)
+ if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF)
+ format = "+VGM: %d";
+ else
+ format = "+VGM=%d";
+ else if (id == SPA_BT_VOLUME_ID_TX)
+ if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF)
+ format = "+VGS: %d";
+ else
+ format = "+VGS=%d";
+ else
+ spa_assert_not_reached();
+
+ if (rfcomm->transport)
+ rfcomm_send_reply(rfcomm, format, value);
+
+ return 0;
+}
+
+static int sco_set_volume_cb(void *data, int id, float volume)
+{
+ struct spa_bt_transport *t = data;
+ struct spa_bt_transport_volume *t_volume = &t->volumes[id];
+ struct transport_data *td = t->user_data;
+ struct rfcomm *rfcomm = td->rfcomm;
+ int value;
+
+ if (!rfcomm_volume_enabled(rfcomm)
+ || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ || !(rfcomm->has_volume && rfcomm->volumes[id].active))
+ return -ENOTSUP;
+
+ value = spa_bt_volume_linear_to_hw(volume, t_volume->hw_volume_max);
+ t_volume->volume = volume;
+
+ if (rfcomm->volumes[id].hw_volume == value)
+ return 0;
+ rfcomm->volumes[id].hw_volume = value;
+
+ return rfcomm_ag_set_volume(t, id);
+}
+
+static const struct spa_bt_transport_implementation sco_transport_impl = {
+ SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
+ .acquire = sco_acquire_cb,
+ .release = sco_release_cb,
+ .set_volume = sco_set_volume_cb,
+};
+
+static struct rfcomm *device_find_rfcomm(struct impl *backend, struct spa_bt_device *device)
+{
+ struct rfcomm *rfcomm;
+ spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+ if (rfcomm->device == device)
+ return rfcomm;
+ }
+ return NULL;
+}
+
+static int backend_native_supports_codec(void *data, struct spa_bt_device *device, unsigned int codec)
+{
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ struct impl *backend = data;
+ struct rfcomm *rfcomm;
+
+ rfcomm = device_find_rfcomm(backend, device);
+ if (rfcomm == NULL || rfcomm->profile != SPA_BT_PROFILE_HFP_HF)
+ return -ENOTSUP;
+
+ if (codec == HFP_AUDIO_CODEC_CVSD)
+ return 1;
+
+ return (codec == HFP_AUDIO_CODEC_MSBC &&
+ (rfcomm->profile == SPA_BT_PROFILE_HFP_AG ||
+ rfcomm->profile == SPA_BT_PROFILE_HFP_HF) &&
+ rfcomm->msbc_supported_by_hfp &&
+ rfcomm->codec_negotiation_supported) ? 1 : 0;
+#else
+ return -ENOTSUP;
+#endif
+}
+
+static int codec_switch_stop_timer(struct rfcomm *rfcomm)
+{
+ struct impl *backend = rfcomm->backend;
+ struct itimerspec ts;
+
+ if (rfcomm->timer.data == NULL)
+ return 0;
+
+ spa_loop_remove_source(backend->main_loop, &rfcomm->timer);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL);
+ spa_system_close(backend->main_system, rfcomm->timer.fd);
+ rfcomm->timer.data = NULL;
+ return 0;
+}
+
+static void volume_sync_stop_timer(struct rfcomm *rfcomm)
+{
+ if (rfcomm->volume_sync_timer)
+ spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer,
+ NULL, NULL, false);
+}
+
+static void volume_sync_timer_event(void *data, uint64_t expirations)
+{
+ struct rfcomm *rfcomm = data;
+
+ volume_sync_stop_timer(rfcomm);
+
+ if (rfcomm->transport) {
+ rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX);
+ rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX);
+ }
+}
+
+static int volume_sync_start_timer(struct rfcomm *rfcomm)
+{
+ struct timespec ts;
+ const uint64_t timeout = 1500 * SPA_NSEC_PER_MSEC;
+
+ if (rfcomm->volume_sync_timer == NULL)
+ rfcomm->volume_sync_timer = spa_loop_utils_add_timer(rfcomm->backend->loop_utils,
+ volume_sync_timer_event, rfcomm);
+
+ if (rfcomm->volume_sync_timer == NULL)
+ return -EIO;
+
+ ts.tv_sec = timeout / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer,
+ &ts, NULL, false);
+
+ return 0;
+}
+
+static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later)
+{
+ if (rfcomm->transport == NULL)
+ return -ENOENT;
+
+ if (!later) {
+ rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX);
+ rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX);
+ } else {
+ volume_sync_start_timer(rfcomm);
+ }
+
+ return 0;
+}
+
+static void codec_switch_timer_event(struct spa_source *source)
+{
+ struct rfcomm *rfcomm = source->data;
+ struct impl *backend = rfcomm->backend;
+ uint64_t exp;
+
+ if (spa_system_timerfd_read(backend->main_system, source->fd, &exp) < 0)
+ spa_log_warn(backend->log, "error reading timerfd: %s", strerror(errno));
+
+ codec_switch_stop_timer(rfcomm);
+
+ spa_log_debug(backend->log, "rfcomm %p: codec switch timeout", rfcomm);
+
+ switch (rfcomm->hfp_ag_initial_codec_setup) {
+ case HFP_AG_INITIAL_CODEC_SETUP_SEND:
+ /* Retry codec selection */
+ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_WAIT;
+ rfcomm_send_reply(rfcomm, "+BCS: 2");
+ codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC);
+ return;
+ case HFP_AG_INITIAL_CODEC_SETUP_WAIT:
+ /* Failure, try falling back to CVSD. */
+ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE;
+ if (rfcomm->transport == NULL) {
+ rfcomm->transport = _transport_create(rfcomm);
+ if (rfcomm->transport == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ } else {
+ rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD;
+ spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile);
+ }
+ }
+ rfcomm_send_reply(rfcomm, "+BCS: 1");
+ return;
+ default:
+ break;
+ }
+
+ if (rfcomm->hfp_ag_switching_codec) {
+ rfcomm->hfp_ag_switching_codec = false;
+ if (rfcomm->device)
+ spa_bt_device_emit_codec_switched(rfcomm->device, -EIO);
+ }
+}
+
+static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec)
+{
+ struct impl *backend = rfcomm->backend;
+ struct itimerspec ts;
+
+ spa_log_debug(backend->log, "rfcomm %p: start timer", rfcomm);
+ if (rfcomm->timer.data == NULL) {
+ rfcomm->timer.data = rfcomm;
+ rfcomm->timer.func = codec_switch_timer_event;
+ rfcomm->timer.fd = spa_system_timerfd_create(backend->main_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ rfcomm->timer.mask = SPA_IO_IN;
+ rfcomm->timer.rmask = 0;
+ spa_loop_add_source(backend->main_loop, &rfcomm->timer);
+ }
+ ts.it_value.tv_sec = timeout_msec / SPA_MSEC_PER_SEC;
+ ts.it_value.tv_nsec = (timeout_msec % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL);
+ return 0;
+}
+
+static int backend_native_ensure_codec(void *data, struct spa_bt_device *device, unsigned int codec)
+{
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ struct impl *backend = data;
+ struct rfcomm *rfcomm;
+ int res;
+
+ res = backend_native_supports_codec(data, device, codec);
+ if (res <= 0)
+ return -EINVAL;
+
+ rfcomm = device_find_rfcomm(backend, device);
+ if (rfcomm == NULL)
+ return -ENOTSUP;
+
+ if (!rfcomm->codec_negotiation_supported)
+ return -ENOTSUP;
+
+ if (rfcomm->codec == codec) {
+ spa_bt_device_emit_codec_switched(device, 0);
+ return 0;
+ }
+
+ if ((res = rfcomm_send_reply(rfcomm, "+BCS: %u", codec)) < 0)
+ return res;
+
+ rfcomm->hfp_ag_switching_codec = true;
+ codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC);
+
+ return 0;
+#else
+ return -ENOTSUP;
+#endif
+}
+
+static void device_destroy(void *data)
+{
+ struct rfcomm *rfcomm = data;
+ rfcomm_free(rfcomm);
+}
+
+static const struct spa_bt_device_events device_events = {
+ SPA_VERSION_BT_DEVICE_EVENTS,
+ .destroy = device_destroy,
+};
+
+static enum spa_bt_profile path_to_profile(const char *path)
+{
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ if (spa_streq(path, PROFILE_HSP_AG))
+ return SPA_BT_PROFILE_HSP_HS;
+
+ if (spa_streq(path, PROFILE_HSP_HS))
+ return SPA_BT_PROFILE_HSP_AG;
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (spa_streq(path, PROFILE_HFP_AG))
+ return SPA_BT_PROFILE_HFP_HF;
+
+ if (spa_streq(path, PROFILE_HFP_HF))
+ return SPA_BT_PROFILE_HFP_AG;
+#endif
+
+ return SPA_BT_PROFILE_NULL;
+}
+
+static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ DBusMessage *r;
+ DBusMessageIter it[5];
+ const char *handler, *path;
+ enum spa_bt_profile profile;
+ struct rfcomm *rfcomm;
+ struct spa_bt_device *d;
+ struct spa_bt_transport *t = NULL;
+ int fd;
+
+ if (!dbus_message_has_signature(m, "oha{sv}")) {
+ spa_log_warn(backend->log, "invalid NewConnection() signature");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ handler = dbus_message_get_path(m);
+ profile = path_to_profile(handler);
+ if (profile == SPA_BT_PROFILE_NULL) {
+ spa_log_warn(backend->log, "invalid handler %s", handler);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_init(m, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &path);
+
+ d = spa_bt_device_find(backend->monitor, path);
+ if (d == NULL || d->adapter == NULL) {
+ spa_log_warn(backend->log, "unknown device for path %s", path);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ spa_bt_device_add_profile(d, profile);
+
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_get_basic(&it[0], &fd);
+
+ spa_log_debug(backend->log, "NewConnection path=%s, fd=%d, profile %s", path, fd, handler);
+
+ rfcomm = calloc(1, sizeof(struct rfcomm));
+ if (rfcomm == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ rfcomm->backend = backend;
+ rfcomm->profile = profile;
+ rfcomm->device = d;
+ rfcomm->path = strdup(path);
+ rfcomm->source.func = rfcomm_event;
+ rfcomm->source.data = rfcomm;
+ rfcomm->source.fd = fd;
+ rfcomm->source.mask = SPA_IO_IN;
+ rfcomm->source.rmask = 0;
+ /* By default all indicators are enabled */
+ rfcomm->cind_enabled_indicators = 0xFFFFFFFF;
+ memset(rfcomm->hf_indicators, 0, sizeof rfcomm->hf_indicators);
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) {
+ if (rfcomm->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)
+ rfcomm->volumes[i].active = true;
+ rfcomm->volumes[i].hw_volume = SPA_BT_VOLUME_INVALID;
+ }
+
+ spa_bt_device_add_listener(d, &rfcomm->device_listener, &device_events, rfcomm);
+ spa_loop_add_source(backend->main_loop, &rfcomm->source);
+ spa_list_append(&backend->rfcomm_list, &rfcomm->link);
+
+ if (profile == SPA_BT_PROFILE_HSP_HS || profile == SPA_BT_PROFILE_HSP_AG) {
+ t = _transport_create(rfcomm);
+ if (t == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ goto fail_need_memory;
+ }
+ rfcomm->transport = t;
+ rfcomm->has_volume = rfcomm_volume_enabled(rfcomm);
+
+ if (profile == SPA_BT_PROFILE_HSP_AG) {
+ rfcomm->hs_state = hsp_hs_init1;
+ }
+
+ spa_bt_device_connect_profile(t->device, profile);
+
+ spa_log_debug(backend->log, "Transport %s available for profile %s", t->path, handler);
+ } else if (profile == SPA_BT_PROFILE_HFP_AG) {
+ /* Start SLC connection */
+ unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_NONE;
+
+ /* Decide if we want to signal that the HF supports mSBC negotiation
+ This should be done when the bluetooth adapter supports the necessary transport mode */
+ if (device_supports_required_mSBC_transport_modes(backend, rfcomm->device)) {
+ /* set the feature bit that indicates HF supports codec negotiation */
+ hf_features |= SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION;
+ rfcomm->msbc_supported_by_hfp = true;
+ rfcomm->codec_negotiation_supported = false;
+ } else {
+ rfcomm->msbc_supported_by_hfp = false;
+ rfcomm->codec_negotiation_supported = false;
+ }
+
+ if (rfcomm_volume_enabled(rfcomm)) {
+ rfcomm->has_volume = true;
+ hf_features |= SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL;
+ }
+
+ /* send command to AG with the features supported by Hands-Free */
+ rfcomm_send_cmd(rfcomm, "AT+BRSF=%u", hf_features);
+
+ rfcomm->hf_state = hfp_hf_brsf;
+ }
+
+ if (rfcomm_volume_enabled(rfcomm) && (profile == SPA_BT_PROFILE_HFP_HF || profile == SPA_BT_PROFILE_HSP_HS)) {
+ uint32_t device_features;
+ if (spa_bt_quirks_get_features(backend->quirks, d->adapter, d, &device_features) == 0) {
+ rfcomm->broken_mic_hw_volume = !(device_features & SPA_BT_FEATURE_HW_VOLUME_MIC);
+ if (rfcomm->broken_mic_hw_volume)
+ spa_log_debug(backend->log, "microphone HW volume disabled by quirk");
+ }
+ }
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ goto fail_need_memory;
+ if (!dbus_connection_send(conn, r, NULL))
+ goto fail_need_memory;
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+fail_need_memory:
+ if (rfcomm)
+ rfcomm_free(rfcomm);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+}
+
+static DBusHandlerResult profile_request_disconnection(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ DBusMessage *r;
+ const char *handler, *path;
+ struct spa_bt_device *d;
+ enum spa_bt_profile profile = SPA_BT_PROFILE_NULL;
+ DBusMessageIter it[5];
+ struct rfcomm *rfcomm, *rfcomm_tmp;
+
+ if (!dbus_message_has_signature(m, "o")) {
+ spa_log_warn(backend->log, "invalid RequestDisconnection() signature");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ handler = dbus_message_get_path(m);
+ profile = path_to_profile(handler);
+ if (profile == SPA_BT_PROFILE_NULL) {
+ spa_log_warn(backend->log, "invalid handler %s", handler);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_init(m, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &path);
+
+ d = spa_bt_device_find(backend->monitor, path);
+ if (d == NULL || d->adapter == NULL) {
+ spa_log_warn(backend->log, "unknown device for path %s", path);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ spa_list_for_each_safe(rfcomm, rfcomm_tmp, &backend->rfcomm_list, link) {
+ if (rfcomm->device == d && rfcomm->profile == profile) {
+ rfcomm_free(rfcomm);
+ }
+ }
+ spa_bt_device_check_profiles(d, false);
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult profile_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+ DBusHandlerResult res;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = PROFILE_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "Release"))
+ res = profile_release(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "RequestDisconnection"))
+ res = profile_request_disconnection(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "NewConnection"))
+ res = profile_new_connection(c, m, userdata);
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void register_profile_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) {
+ spa_log_warn(backend->log, "Register profile not supported");
+ goto finish;
+ }
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(backend->log, "Error registering profile");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "RegisterProfile() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ finish:
+ dbus_message_unref(r);
+ dbus_pending_call_unref(pending);
+}
+
+static int register_profile(struct impl *backend, const char *profile, const char *uuid)
+{
+ DBusMessage *m;
+ DBusMessageIter it[4];
+ dbus_bool_t autoconnect;
+ dbus_uint16_t version, chan, features;
+ char *str;
+ DBusPendingCall *call;
+
+ if (!(backend->enabled_profiles & spa_bt_profile_from_uuid(uuid)))
+ return -ECANCELED;
+
+ spa_log_debug(backend->log, "Registering Profile %s %s", profile, uuid);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez",
+ BLUEZ_PROFILE_MANAGER_INTERFACE, "RegisterProfile");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_message_iter_init_append(m, &it[0]);
+ dbus_message_iter_append_basic(&it[0], DBUS_TYPE_OBJECT_PATH, &profile);
+ dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &uuid);
+ dbus_message_iter_open_container(&it[0], DBUS_TYPE_ARRAY, "{sv}", &it[1]);
+
+ if (spa_streq(uuid, SPA_BT_UUID_HSP_HS) ||
+ spa_streq(uuid, SPA_BT_UUID_HSP_HS_ALT)) {
+
+ /* In the headset role, the connection will only be initiated from the remote side */
+ str = "AutoConnect";
+ autoconnect = 0;
+ 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, "b", &it[3]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_BOOLEAN, &autoconnect);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+
+ str = "Channel";
+ chan = HSP_HS_DEFAULT_CHANNEL;
+ 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]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &chan);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+
+ /* HSP version 1.2 */
+ str = "Version";
+ version = 0x0102;
+ 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]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+ } else if (spa_streq(uuid, SPA_BT_UUID_HFP_AG)) {
+ str = "Features";
+
+ /* We announce wideband speech support anyway */
+ features = SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH;
+ 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]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &features);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+
+ /* HFP version 1.7 */
+ str = "Version";
+ version = 0x0107;
+ 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]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+ } else if (spa_streq(uuid, SPA_BT_UUID_HFP_HF)) {
+ str = "Features";
+
+ /* We announce wideband speech support anyway */
+ features = SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH;
+ 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]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &features);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+
+ /* HFP version 1.7 */
+ str = "Version";
+ version = 0x0107;
+ 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]);
+ dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version);
+ dbus_message_iter_close_container(&it[2], &it[3]);
+ dbus_message_iter_close_container(&it[1], &it[2]);
+ }
+ dbus_message_iter_close_container(&it[0], &it[1]);
+
+ dbus_connection_send_with_reply(backend->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, register_profile_reply, backend, NULL);
+ dbus_message_unref(m);
+ return 0;
+}
+
+static void unregister_profile(struct impl *backend, const char *profile)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+
+ spa_log_debug(backend->log, "Unregistering Profile %s", profile);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez",
+ BLUEZ_PROFILE_MANAGER_INTERFACE, "UnregisterProfile");
+ if (m == NULL)
+ return;
+
+ dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &profile, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_info(backend->log, "Unregistering Profile %s failed", profile);
+ dbus_error_free(&err);
+ return;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "UnregisterProfile() returned error: %s", dbus_message_get_error_name(r));
+ return;
+ }
+
+ dbus_message_unref(r);
+}
+
+static int backend_native_register_profiles(void *data)
+{
+ struct impl *backend = data;
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ register_profile(backend, PROFILE_HSP_AG, SPA_BT_UUID_HSP_AG);
+ register_profile(backend, PROFILE_HSP_HS, SPA_BT_UUID_HSP_HS);
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ register_profile(backend, PROFILE_HFP_AG, SPA_BT_UUID_HFP_AG);
+ register_profile(backend, PROFILE_HFP_HF, SPA_BT_UUID_HFP_HF);
+#endif
+
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ sco_listen(backend);
+
+ return 0;
+}
+
+static void sco_close(struct impl *backend)
+{
+ if (backend->sco.fd >= 0) {
+ if (backend->sco.loop)
+ spa_loop_remove_source(backend->sco.loop, &backend->sco);
+ shutdown(backend->sco.fd, SHUT_RDWR);
+ close (backend->sco.fd);
+ backend->sco.fd = -1;
+ }
+}
+
+static int backend_native_unregister_profiles(void *data)
+{
+ struct impl *backend = data;
+
+ sco_close(backend);
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HSP_AG)
+ unregister_profile(backend, PROFILE_HSP_AG);
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HSP_HS)
+ unregister_profile(backend, PROFILE_HSP_HS);
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HFP_AG)
+ unregister_profile(backend, PROFILE_HFP_AG);
+ if (backend->enabled_profiles & SPA_BT_PROFILE_HFP_HF)
+ unregister_profile(backend, PROFILE_HFP_HF);
+#endif
+
+ return 0;
+}
+
+static void send_ciev_for_each_rfcomm(struct impl *backend, int indicator, int value)
+{
+ struct rfcomm *rfcomm;
+
+ spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+ if (rfcomm->slc_configured &&
+ ((indicator == CIND_CALL || indicator == CIND_CALLSETUP || indicator == CIND_CALLHELD) ||
+ (rfcomm->cind_call_notify && (rfcomm->cind_enabled_indicators & (1 << indicator)))))
+ rfcomm_send_reply(rfcomm, "+CIEV: %d,%d", indicator, value);
+ }
+}
+
+static void ring_timer_event(void *data, uint64_t expirations)
+{
+ struct impl *backend = data;
+ const char *number;
+ unsigned int type;
+ struct timespec ts;
+ const uint64_t timeout = 1 * SPA_NSEC_PER_SEC;
+ struct rfcomm *rfcomm;
+
+ number = mm_get_incoming_call_number(backend->modemmanager);
+ if (number) {
+ if (spa_strstartswith(number, "+"))
+ type = INTERNATIONAL_NUMBER;
+ else
+ type = NATIONAL_NUMBER;
+ }
+
+ ts.tv_sec = timeout / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, &ts, NULL, false);
+
+ spa_list_for_each(rfcomm, &backend->rfcomm_list, link) {
+ if (rfcomm->slc_configured) {
+ rfcomm_send_reply(rfcomm, "RING");
+ if (rfcomm->clip_notify && number)
+ rfcomm_send_reply(rfcomm, "+CLIP: \"%s\",%u", number, type);
+ }
+ }
+}
+
+static void set_call_active(bool active, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.active_call != active) {
+ backend->modem.active_call = active;
+ send_ciev_for_each_rfcomm(backend, CIND_CALL, active);
+ }
+}
+
+static void set_call_setup(enum call_setup value, void *user_data)
+{
+ struct impl *backend = user_data;
+ enum call_setup old = backend->modem.call_setup;
+
+ if (backend->modem.call_setup != value) {
+ backend->modem.call_setup = value;
+ send_ciev_for_each_rfcomm(backend, CIND_CALLSETUP, value);
+ }
+
+ if (value == CIND_CALLSETUP_INCOMING) {
+ if (backend->ring_timer == NULL)
+ backend->ring_timer = spa_loop_utils_add_timer(backend->loop_utils, ring_timer_event, backend);
+
+ if (backend->ring_timer == NULL) {
+ spa_log_warn(backend->log, "Failed to create ring timer");
+ return;
+ }
+
+ ring_timer_event(backend, 0);
+ } else if (old == CIND_CALLSETUP_INCOMING) {
+ spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, NULL, NULL, false);
+ }
+}
+
+void set_battery_level(unsigned int level, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->battery_level != level) {
+ backend->battery_level = level;
+ send_ciev_for_each_rfcomm(backend, CIND_BATTERY_LEVEL, level);
+ }
+}
+
+static void set_modem_operator_name(const char *name, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.operator_name) {
+ free(backend->modem.operator_name);
+ backend->modem.operator_name = NULL;
+ }
+
+ if (name)
+ backend->modem.operator_name = strdup(name);
+}
+
+static void set_modem_roaming(bool is_roaming, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.network_is_roaming != is_roaming) {
+ backend->modem.network_is_roaming = is_roaming;
+ send_ciev_for_each_rfcomm(backend, CIND_ROAM, is_roaming);
+ }
+}
+
+static void set_modem_own_number(const char *number, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.own_number) {
+ free(backend->modem.own_number);
+ backend->modem.own_number = NULL;
+ }
+
+ if (number)
+ backend->modem.own_number = strdup(number);
+}
+
+static void set_modem_service(bool available, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.network_has_service != available) {
+ backend->modem.network_has_service = available;
+ send_ciev_for_each_rfcomm(backend, CIND_SERVICE, available);
+ }
+}
+
+static void set_modem_signal_strength(unsigned int strength, void *user_data)
+{
+ struct impl *backend = user_data;
+
+ if (backend->modem.signal_strength != strength) {
+ backend->modem.signal_strength = strength;
+ send_ciev_for_each_rfcomm(backend, CIND_SIGNAL, strength);
+ }
+}
+
+static void send_cmd_result(bool success, enum cmee_error error, void *user_data)
+{
+ struct rfcomm *rfcomm = user_data;
+
+ if (success) {
+ rfcomm_send_reply(rfcomm, "OK");
+ return;
+ }
+
+ rfcomm_send_error(rfcomm, error);
+}
+
+static int backend_native_free(void *data)
+{
+ struct impl *backend = data;
+
+ struct rfcomm *rfcomm;
+
+ sco_close(backend);
+
+ if (backend->modemmanager) {
+ mm_unregister(backend);
+ backend->modemmanager = NULL;
+ }
+
+ if (backend->upower) {
+ upower_unregister(backend->upower);
+ backend->upower = NULL;
+ }
+
+ if (backend->ring_timer)
+ spa_loop_utils_destroy_source(backend->loop_utils, backend->ring_timer);
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG);
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS);
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_AG);
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_HF);
+#endif
+
+ spa_list_consume(rfcomm, &backend->rfcomm_list, link)
+ rfcomm_free(rfcomm);
+
+ if (backend->modem.operator_name)
+ free(backend->modem.operator_name);
+ free(backend);
+
+ return 0;
+}
+
+static int parse_headset_roles(struct impl *backend, const struct spa_dict *info)
+{
+ const char *str;
+ int profiles = SPA_BT_PROFILE_NULL;
+
+ if (info == NULL ||
+ (str = spa_dict_lookup(info, PROP_KEY_HEADSET_ROLES)) == NULL)
+ goto fallback;
+
+ profiles = spa_bt_profiles_from_json_array(str);
+ if (profiles < 0)
+ goto fallback;
+
+ backend->enabled_profiles = profiles & SPA_BT_PROFILE_HEADSET_AUDIO;
+ return 0;
+fallback:
+ backend->enabled_profiles = DEFAULT_ENABLED_PROFILES;
+ return 0;
+}
+
+static const struct spa_bt_backend_implementation backend_impl = {
+ SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
+ .free = backend_native_free,
+ .register_profiles = backend_native_register_profiles,
+ .unregister_profiles = backend_native_unregister_profiles,
+ .ensure_codec = backend_native_ensure_codec,
+ .supports_codec = backend_native_supports_codec,
+};
+
+static const struct mm_ops mm_ops = {
+ .send_cmd_result = send_cmd_result,
+ .set_modem_service = set_modem_service,
+ .set_modem_signal_strength = set_modem_signal_strength,
+ .set_modem_operator_name = set_modem_operator_name,
+ .set_modem_own_number = set_modem_own_number,
+ .set_modem_roaming = set_modem_roaming,
+ .set_call_active = set_call_active,
+ .set_call_setup = set_call_setup,
+};
+
+struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *backend;
+
+ static const DBusObjectPathVTable vtable_profile = {
+ .message_function = profile_handler,
+ };
+
+ backend = calloc(1, sizeof(struct impl));
+ if (backend == NULL)
+ return NULL;
+
+ spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend);
+
+ backend->this.name = "native";
+ backend->monitor = monitor;
+ backend->quirks = quirks;
+ backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+ backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
+ backend->conn = dbus_connection;
+ backend->sco.fd = -1;
+
+ spa_log_topic_init(backend->log, &log_topic);
+
+ spa_list_init(&backend->rfcomm_list);
+
+ if (parse_headset_roles(backend, info) < 0)
+ goto fail;
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ if (!dbus_connection_register_object_path(backend->conn,
+ PROFILE_HSP_AG,
+ &vtable_profile, backend)) {
+ goto fail;
+ }
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ PROFILE_HSP_HS,
+ &vtable_profile, backend)) {
+ goto fail1;
+ }
+#endif
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+ if (!dbus_connection_register_object_path(backend->conn,
+ PROFILE_HFP_AG,
+ &vtable_profile, backend)) {
+ goto fail2;
+ }
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ PROFILE_HFP_HF,
+ &vtable_profile, backend)) {
+ goto fail3;
+ }
+#endif
+
+ backend->modemmanager = mm_register(backend->log, backend->conn, info, &mm_ops, backend);
+ backend->upower = upower_register(backend->log, backend->conn, set_battery_level, backend);
+
+ return &backend->this;
+
+#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE
+fail3:
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_AG);
+fail2:
+#endif
+#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS);
+fail1:
+ dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG);
+#endif
+fail:
+ free(backend);
+ return NULL;
+}
diff --git a/spa/plugins/bluez5/backend-ofono.c b/spa/plugins/bluez5/backend-ofono.c
new file mode 100644
index 0000000..3ba2b03
--- /dev/null
+++ b/spa/plugins/bluez5/backend-ofono.c
@@ -0,0 +1,947 @@
+/* Spa oFono backend
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <poll.h>
+#include <sys/socket.h>
+
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/sco.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/utils/result.h>
+#include <spa/param/audio/raw.h>
+
+#include "defs.h"
+
+#define INITIAL_INTERVAL_NSEC (500 * SPA_NSEC_PER_MSEC)
+#define ACTION_INTERVAL_NSEC (3000 * SPA_NSEC_PER_MSEC)
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.ofono");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl {
+ struct spa_bt_backend this;
+
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_system *main_system;
+ struct spa_dbus *dbus;
+ struct spa_loop_utils *loop_utils;
+ DBusConnection *conn;
+
+ const struct spa_bt_quirks *quirks;
+
+ struct spa_source *timer;
+
+ unsigned int filters_added:1;
+ unsigned int msbc_supported:1;
+};
+
+struct transport_data {
+ struct spa_source sco;
+ unsigned int broken:1;
+ unsigned int activated:1;
+};
+
+#define OFONO_HF_AUDIO_MANAGER_INTERFACE OFONO_SERVICE ".HandsfreeAudioManager"
+#define OFONO_HF_AUDIO_CARD_INTERFACE OFONO_SERVICE ".HandsfreeAudioCard"
+#define OFONO_HF_AUDIO_AGENT_INTERFACE OFONO_SERVICE ".HandsfreeAudioAgent"
+
+#define OFONO_AUDIO_CLIENT "/Profile/ofono"
+
+#define OFONO_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name=\"" OFONO_HF_AUDIO_AGENT_INTERFACE "\">" \
+ " <method name=\"Release\">" \
+ " </method>" \
+ " <method name=\"NewConnection\">" \
+ " <arg name=\"card\" direction=\"in\" type=\"o\"/>" \
+ " <arg name=\"fd\" direction=\"in\" type=\"h\"/>" \
+ " <arg name=\"codec\" direction=\"in\" type=\"b\"/>" \
+ " </method>" \
+ " </interface>" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
+ " <method name=\"Introspect\">" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+#define OFONO_ERROR_INVALID_ARGUMENTS "org.ofono.Error.InvalidArguments"
+#define OFONO_ERROR_NOT_IMPLEMENTED "org.ofono.Error.NotImplemented"
+#define OFONO_ERROR_IN_USE "org.ofono.Error.InUse"
+#define OFONO_ERROR_FAILED "org.ofono.Error.Failed"
+
+static void ofono_transport_get_mtu(struct impl *backend, struct spa_bt_transport *t)
+{
+ struct sco_options sco_opt;
+ socklen_t len;
+
+ /* Fallback values */
+ t->read_mtu = 48;
+ t->write_mtu = 48;
+
+ len = sizeof(sco_opt);
+ memset(&sco_opt, 0, len);
+
+ if (getsockopt(t->fd, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0)
+ spa_log_warn(backend->log, "getsockopt(SCO_OPTIONS) failed, loading defaults");
+ else {
+ spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu);
+ t->read_mtu = sco_opt.mtu;
+ t->write_mtu = sco_opt.mtu;
+ }
+}
+
+static struct spa_bt_transport *_transport_create(struct impl *backend,
+ const char *path,
+ struct spa_bt_device *device,
+ enum spa_bt_profile profile,
+ int codec,
+ struct spa_callbacks *impl)
+{
+ struct spa_bt_transport *t = NULL;
+ char *t_path = strdup(path);
+
+ t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct transport_data));
+ if (t == NULL) {
+ spa_log_warn(backend->log, "can't create transport: %m");
+ free(t_path);
+ goto finish;
+ }
+ spa_bt_transport_set_implementation(t, impl, t);
+
+ t->device = device;
+ spa_list_append(&t->device->transport_list, &t->device_link);
+ t->backend = &backend->this;
+ t->profile = profile;
+ t->codec = codec;
+ t->n_channels = 1;
+ t->channels[0] = SPA_AUDIO_CHANNEL_MONO;
+
+finish:
+ return t;
+}
+
+static int _audio_acquire(struct impl *backend, const char *path, uint8_t *codec)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ int ret = 0;
+
+ m = dbus_message_new_method_call(OFONO_SERVICE, path,
+ OFONO_HF_AUDIO_CARD_INTERFACE,
+ "Acquire");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_error_init(&err);
+
+ /*
+ * XXX: We assume here oFono replies. It however can happen that the headset does
+ * XXX: not properly respond to the codec negotiation RFCOMM commands.
+ * XXX: oFono (1.34) fails to handle this condition, and will not send DBus reply
+ * XXX: in this case. The transport acquire API is synchronous, so we can't
+ * XXX: do better here right now.
+ */
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_error(backend->log, "Transport Acquire() failed for transport %s (%s)",
+ path, err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Acquire returned error: %s", dbus_message_get_error_name(r));
+ ret = -EIO;
+ goto finish;
+ }
+
+ if (!dbus_message_get_args(r, &err,
+ DBUS_TYPE_UNIX_FD, &ret,
+ DBUS_TYPE_BYTE, codec,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(backend->log, "Failed to parse Acquire() reply: %s", err.message);
+ dbus_error_free(&err);
+ ret = -EIO;
+ goto finish;
+ }
+
+finish:
+ dbus_message_unref(r);
+ return ret;
+}
+
+static int ofono_audio_acquire(void *data, bool optional)
+{
+ struct spa_bt_transport *transport = data;
+ struct transport_data *td = transport->user_data;
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+ uint8_t codec;
+ int ret = 0;
+
+ if (transport->fd >= 0)
+ goto finish;
+ if (td->broken) {
+ ret = -EIO;
+ goto finish;
+ }
+
+ spa_bt_device_update_last_bluez_action_time(transport->device);
+
+ ret = _audio_acquire(backend, transport->path, &codec);
+ if (ret < 0)
+ goto finish;
+
+ transport->fd = ret;
+
+ if (transport->codec != codec) {
+ struct timespec ts;
+
+ spa_log_info(backend->log, "transport %p: acquired codec (%d) differs from transport one (%d)",
+ transport, codec, transport->codec);
+
+ /* shutdown to make sure connection is dropped immediately */
+ shutdown(transport->fd, SHUT_RDWR);
+ close(transport->fd);
+ transport->fd = -1;
+
+ /* schedule immediate profile update, from main loop */
+ transport->codec = codec;
+ td->broken = true;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 1;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
+ &ts, NULL, false);
+ return -EIO;
+ }
+
+ td->broken = false;
+
+ spa_log_debug(backend->log, "transport %p: Acquire %s, fd %d codec %d", transport,
+ transport->path, transport->fd, transport->codec);
+
+ ofono_transport_get_mtu(backend, transport);
+ ret = 0;
+
+finish:
+ return ret;
+}
+
+static int ofono_audio_release(void *data)
+{
+ struct spa_bt_transport *transport = data;
+ struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this);
+
+ spa_log_debug(backend->log, "transport %p: Release %s",
+ transport, transport->path);
+
+ if (transport->sco_io) {
+ spa_bt_sco_io_destroy(transport->sco_io);
+ transport->sco_io = NULL;
+ }
+
+ /* shutdown to make sure connection is dropped immediately */
+ shutdown(transport->fd, SHUT_RDWR);
+ close(transport->fd);
+ transport->fd = -1;
+
+ return 0;
+}
+
+static DBusHandlerResult ofono_audio_card_removed(struct impl *backend, const char *path)
+{
+ struct spa_bt_transport *transport;
+
+ spa_assert(backend);
+ spa_assert(path);
+
+ spa_log_debug(backend->log, "card removed: %s", path);
+
+ transport = spa_bt_transport_find(backend->monitor, path);
+
+ if (transport != NULL) {
+ struct spa_bt_device *device = transport->device;
+
+ spa_log_debug(backend->log, "transport %p: free %s",
+ transport, transport->path);
+
+ spa_bt_transport_free(transport);
+ if (device != NULL)
+ spa_bt_device_check_profiles(device, false);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static const struct spa_bt_transport_implementation ofono_transport_impl = {
+ SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION,
+ .acquire = ofono_audio_acquire,
+ .release = ofono_audio_release,
+};
+
+bool activate_transport(struct spa_bt_transport *t, const void *data)
+{
+ struct impl *backend = (void *)data;
+ struct transport_data *td = t->user_data;
+ struct timespec ts;
+ uint64_t now, threshold;
+
+ if (t->backend != &backend->this)
+ return false;
+
+ /* Check device-specific rate limit */
+ spa_system_clock_gettime(backend->main_system, CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ threshold = t->device->last_bluez_action_time + ACTION_INTERVAL_NSEC;
+ if (now < threshold) {
+ ts.tv_sec = (threshold - now) / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = (threshold - now) % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
+ &ts, NULL, false);
+ return false;
+ }
+
+ if (!td->activated) {
+ /* Connect profile */
+ spa_log_debug(backend->log, "Transport %s activated", t->path);
+ td->activated = true;
+ spa_bt_device_connect_profile(t->device, t->profile);
+ }
+
+ if (td->broken) {
+ /* Recreate the transport */
+ struct spa_bt_transport *t_copy;
+
+ t_copy = _transport_create(backend, t->path, t->device,
+ t->profile, t->codec, (struct spa_callbacks *)&ofono_transport_impl);
+ spa_bt_transport_free(t);
+
+ if (t_copy)
+ spa_bt_device_connect_profile(t_copy->device, t_copy->profile);
+
+ return true;
+ }
+
+ return false;
+}
+
+static void activate_transports(struct impl *backend)
+{
+ while (spa_bt_transport_find_full(backend->monitor, activate_transport, backend));
+}
+
+static void activate_timer_event(void *userdata, uint64_t expirations)
+{
+ struct impl *backend = userdata;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->timer, NULL, NULL, false);
+ activate_transports(backend);
+}
+
+static DBusHandlerResult ofono_audio_card_found(struct impl *backend, char *path, DBusMessageIter *props_i)
+{
+ const char *remote_address = NULL;
+ const char *local_address = NULL;
+ struct spa_bt_device *d;
+ struct spa_bt_transport *t;
+ struct transport_data *td;
+ enum spa_bt_profile profile = SPA_BT_PROFILE_HFP_AG;
+ uint8_t codec = backend->msbc_supported ?
+ HFP_AUDIO_CODEC_MSBC : HFP_AUDIO_CODEC_CVSD;
+
+ spa_assert(backend);
+ spa_assert(path);
+ spa_assert(props_i);
+
+ spa_log_debug(backend->log, "new card: %s", path);
+
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key, *value;
+ char c;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if ((c = dbus_message_iter_get_arg_type(&value_i)) != DBUS_TYPE_STRING) {
+ spa_log_error(backend->log, "Invalid properties for %s: expected 's', received '%c'", path, c);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_get_basic(&value_i, &value);
+
+ if (spa_streq(key, "RemoteAddress")) {
+ remote_address = value;
+ } else if (spa_streq(key, "LocalAddress")) {
+ local_address = value;
+ } else if (spa_streq(key, "Type")) {
+ if (spa_streq(value, "gateway"))
+ profile = SPA_BT_PROFILE_HFP_HF;
+ }
+
+ spa_log_debug(backend->log, "%s: %s", key, value);
+
+ dbus_message_iter_next(props_i);
+ }
+
+ if (!remote_address || !local_address) {
+ spa_log_error(backend->log, "Missing addresses for %s", path);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ d = spa_bt_device_find_by_address(backend->monitor, remote_address, local_address);
+ if (!d || !d->adapter) {
+ spa_log_error(backend->log, "Device doesn’t exist for %s", path);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ spa_bt_device_add_profile(d, profile);
+
+ t = _transport_create(backend, path, d, profile, codec, (struct spa_callbacks *)&ofono_transport_impl);
+ if (t == NULL) {
+ spa_log_error(backend->log, "failed to create transport: %s", spa_strerror(-errno));
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ td = t->user_data;
+
+ /*
+ * For HF profile, delay profile connect, so that we likely don't do it at the
+ * same time as the device is busy with A2DP connect. This avoids some oFono
+ * misbehavior (see comment in _audio_acquire above).
+ *
+ * For AG mode, we delay the emission of the nodes, so it is not necessary
+ * to know the codec in advance.
+ */
+ if (profile == SPA_BT_PROFILE_HFP_HF) {
+ struct timespec ts;
+ ts.tv_sec = INITIAL_INTERVAL_NSEC / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = INITIAL_INTERVAL_NSEC % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(backend->loop_utils, backend->timer,
+ &ts, NULL, false);
+ } else {
+ td->activated = true;
+ spa_bt_device_connect_profile(t->device, t->profile);
+ }
+
+ spa_log_debug(backend->log, "Transport %s available, codec %d", t->path, t->codec);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult ofono_release(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ DBusMessage *r;
+
+ spa_log_warn(backend->log, "release");
+
+ r = dbus_message_new_error(m, OFONO_HF_AUDIO_AGENT_INTERFACE ".Error.NotImplemented",
+ "Method not implemented");
+ if (r == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void sco_event(struct spa_source *source)
+{
+ struct spa_bt_transport *t = source->data;
+ struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this);
+
+ if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) {
+ spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno));
+ if (t->fd >= 0) {
+ if (source->loop)
+ spa_loop_remove_source(source->loop, source);
+ shutdown(t->fd, SHUT_RDWR);
+ close (t->fd);
+ t->fd = -1;
+ spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE);
+ }
+ }
+}
+
+static int enable_sco_socket(int sock)
+{
+ char c;
+ struct pollfd pfd;
+
+ if (sock < 0)
+ return ENOTCONN;
+
+ memset(&pfd, 0, sizeof(pfd));
+ pfd.fd = sock;
+ pfd.events = POLLOUT;
+
+ if (poll(&pfd, 1, 0) < 0)
+ return errno;
+
+ /*
+ * If socket already writable then it is not in defer setup state,
+ * otherwise it needs to be read to authorize the connection.
+ */
+ if ((pfd.revents & POLLOUT))
+ return 0;
+
+ /* Enable socket by reading 1 byte */
+ if (read(sock, &c, 1) < 0)
+ return errno;
+
+ return 0;
+}
+
+static DBusHandlerResult ofono_new_audio_connection(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path;
+ int fd;
+ uint8_t codec;
+ struct spa_bt_transport *t;
+ struct transport_data *td;
+ DBusMessage *r = NULL;
+
+ if (dbus_message_get_args(m, NULL,
+ DBUS_TYPE_OBJECT_PATH, &path,
+ DBUS_TYPE_UNIX_FD, &fd,
+ DBUS_TYPE_BYTE, &codec,
+ DBUS_TYPE_INVALID) == FALSE) {
+ r = dbus_message_new_error(m, OFONO_ERROR_INVALID_ARGUMENTS, "Invalid arguments in method call");
+ goto fail;
+ }
+
+ t = spa_bt_transport_find(backend->monitor, path);
+ if (t && (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) {
+ int err;
+
+ err = enable_sco_socket(fd);
+ if (err) {
+ spa_log_error(backend->log, "transport %p: Couldn't authorize SCO connection: %s", t, strerror(err));
+ r = dbus_message_new_error(m, OFONO_ERROR_FAILED, "SCO authorization failed");
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ goto fail;
+ }
+
+ t->fd = fd;
+ t->codec = codec;
+
+ spa_log_debug(backend->log, "transport %p: NewConnection %s, fd %d codec %d",
+ t, t->path, t->fd, t->codec);
+
+ td = t->user_data;
+ td->sco.func = sco_event;
+ td->sco.data = t;
+ td->sco.fd = fd;
+ td->sco.mask = SPA_IO_HUP | SPA_IO_ERR;
+ td->sco.rmask = 0;
+ spa_loop_add_source(backend->main_loop, &td->sco);
+
+ ofono_transport_get_mtu(backend, t);
+ spa_bt_transport_set_state (t, SPA_BT_TRANSPORT_STATE_PENDING);
+ }
+ else if (fd) {
+ spa_log_debug(backend->log, "ignoring NewConnection");
+ r = dbus_message_new_error(m, OFONO_ERROR_NOT_IMPLEMENTED, "Method not implemented");
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ }
+
+fail:
+ if (r) {
+ DBusHandlerResult res = DBUS_HANDLER_RESULT_HANDLED;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ res = DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_unref(r);
+ return res;
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult ofono_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *backend = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+ DBusHandlerResult res;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(backend->log, "path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = OFONO_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(backend->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else if (dbus_message_is_method_call(m, OFONO_HF_AUDIO_AGENT_INTERFACE, "Release"))
+ res = ofono_release(c, m, userdata);
+ else if (dbus_message_is_method_call(m, OFONO_HF_AUDIO_AGENT_INTERFACE, "NewConnection"))
+ res = ofono_new_audio_connection(c, m, userdata);
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void ofono_getcards_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+ DBusMessageIter i, array_i, struct_i, props_i;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Failed to get a list of handsfree audio cards: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a(oa{sv})")) {
+ spa_log_error(backend->log, "Invalid arguments in GetCards() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ char *path;
+
+ dbus_message_iter_recurse(&array_i, &struct_i);
+ dbus_message_iter_get_basic(&struct_i, &path);
+ dbus_message_iter_next(&struct_i);
+
+ dbus_message_iter_recurse(&struct_i, &props_i);
+
+ ofono_audio_card_found(backend, path, &props_i);
+
+ dbus_message_iter_next(&array_i);
+ }
+
+finish:
+ dbus_message_unref(r);
+ dbus_pending_call_unref(pending);
+}
+
+static int backend_ofono_register(void *data)
+{
+ struct impl *backend = data;
+
+ DBusMessage *m, *r;
+ const char *path = OFONO_AUDIO_CLIENT;
+ uint8_t codecs[2];
+ const uint8_t *pcodecs = codecs;
+ int ncodecs = 0, res;
+ DBusPendingCall *call;
+ DBusError err;
+
+ spa_log_debug(backend->log, "Registering");
+
+ m = dbus_message_new_method_call(OFONO_SERVICE, "/",
+ OFONO_HF_AUDIO_MANAGER_INTERFACE, "Register");
+ if (m == NULL)
+ return -ENOMEM;
+
+ codecs[ncodecs++] = HFP_AUDIO_CODEC_CVSD;
+ if (backend->msbc_supported)
+ codecs[ncodecs++] = HFP_AUDIO_CODEC_MSBC;
+
+ dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path,
+ DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &pcodecs, ncodecs,
+ DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ if (dbus_error_has_name(&err, "org.freedesktop.DBus.Error.ServiceUnknown")) {
+ spa_log_info(backend->log, "oFono not available: %s",
+ err.message);
+ res = -ENOTSUP;
+ } else {
+ spa_log_warn(backend->log, "Registering Profile %s failed: %s (%s)",
+ path, err.message, err.name);
+ res = -EIO;
+ }
+ dbus_error_free(&err);
+ return res;
+ }
+
+ if (dbus_message_is_error(r, OFONO_ERROR_INVALID_ARGUMENTS)) {
+ spa_log_warn(backend->log, "invalid arguments");
+ goto finish;
+ }
+ if (dbus_message_is_error(r, OFONO_ERROR_IN_USE)) {
+ spa_log_warn(backend->log, "already in use");
+ goto finish;
+ }
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(backend->log, "Error registering profile");
+ goto finish;
+ }
+ if (dbus_message_is_error(r, DBUS_ERROR_SERVICE_UNKNOWN)) {
+ spa_log_info(backend->log, "oFono not available, disabling");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Register() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+ dbus_message_unref(r);
+
+ spa_log_debug(backend->log, "registered");
+
+ m = dbus_message_new_method_call(OFONO_SERVICE, "/",
+ OFONO_HF_AUDIO_MANAGER_INTERFACE, "GetCards");
+ if (m == NULL)
+ goto finish;
+
+ dbus_connection_send_with_reply(backend->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, ofono_getcards_reply, backend, NULL);
+ dbus_message_unref(m);
+
+ return 0;
+
+finish:
+ dbus_message_unref(r);
+ return -EIO;
+}
+
+static DBusHandlerResult ofono_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ if (dbus_message_is_signal(m, OFONO_HF_AUDIO_MANAGER_INTERFACE, "CardAdded")) {
+ char *p;
+ DBusMessageIter arg_i, props_i;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sv}")) {
+ spa_log_error(backend->log, "Failed to parse org.ofono.HandsfreeAudioManager.CardAdded");
+ goto fail;
+ }
+
+ dbus_message_iter_get_basic(&arg_i, &p);
+
+ dbus_message_iter_next(&arg_i);
+ spa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY);
+
+ dbus_message_iter_recurse(&arg_i, &props_i);
+
+ return ofono_audio_card_found(backend, p, &props_i);
+ } else if (dbus_message_is_signal(m, OFONO_HF_AUDIO_MANAGER_INTERFACE, "CardRemoved")) {
+ const char *p;
+
+ if (!dbus_message_get_args(m, &err, DBUS_TYPE_OBJECT_PATH, &p, DBUS_TYPE_INVALID)) {
+ spa_log_error(backend->log, "Failed to parse org.ofono.HandsfreeAudioManager.CardRemoved: %s", err.message);
+ goto fail;
+ }
+
+ return ofono_audio_card_removed(backend, p);
+ }
+
+fail:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(struct impl *backend)
+{
+ DBusError err;
+
+ if (backend->filters_added)
+ return 0;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(backend->conn, ofono_filter_cb, backend, NULL)) {
+ spa_log_error(backend->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" OFONO_SERVICE "',"
+ "interface='" OFONO_HF_AUDIO_MANAGER_INTERFACE "',member='CardAdded'", &err);
+ dbus_bus_add_match(backend->conn,
+ "type='signal',sender='" OFONO_SERVICE "',"
+ "interface='" OFONO_HF_AUDIO_MANAGER_INTERFACE "',member='CardRemoved'", &err);
+
+ backend->filters_added = true;
+
+ return 0;
+
+fail:
+ dbus_error_free(&err);
+ return -EIO;
+}
+
+static int backend_ofono_free(void *data)
+{
+ struct impl *backend = data;
+
+ if (backend->filters_added) {
+ dbus_connection_remove_filter(backend->conn, ofono_filter_cb, backend);
+ backend->filters_added = false;
+ }
+
+ if (backend->timer)
+ spa_loop_utils_destroy_source(backend->loop_utils, backend->timer);
+
+ dbus_connection_unregister_object_path(backend->conn, OFONO_AUDIO_CLIENT);
+
+ free(backend);
+
+ return 0;
+}
+
+static const struct spa_bt_backend_implementation backend_impl = {
+ SPA_VERSION_BT_BACKEND_IMPLEMENTATION,
+ .free = backend_ofono_free,
+ .register_profiles = backend_ofono_register,
+};
+
+static bool is_available(struct impl *backend)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ bool success = false;
+
+ m = dbus_message_new_method_call(OFONO_SERVICE, "/",
+ DBUS_INTERFACE_INTROSPECTABLE, "Introspect");
+ if (m == NULL)
+ return false;
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN)
+ success = true;
+
+ if (r)
+ dbus_message_unref(r);
+ else
+ dbus_error_free(&err);
+
+ return success;
+}
+
+struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *backend;
+ const char *str;
+ static const DBusObjectPathVTable vtable_profile = {
+ .message_function = ofono_handler,
+ };
+
+ backend = calloc(1, sizeof(struct impl));
+ if (backend == NULL)
+ return NULL;
+
+ spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend);
+
+ backend->this.name = "ofono";
+ backend->this.exclusive = true;
+ backend->monitor = monitor;
+ backend->quirks = quirks;
+ backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+ backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
+ backend->conn = dbus_connection;
+ if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc")))
+ backend->msbc_supported = spa_atob(str);
+ else
+ backend->msbc_supported = false;
+
+ spa_log_topic_init(backend->log, &log_topic);
+
+ backend->timer = spa_loop_utils_add_timer(backend->loop_utils, activate_timer_event, backend);
+ if (backend->timer == NULL) {
+ free(backend);
+ return NULL;
+ }
+
+ if (!dbus_connection_register_object_path(backend->conn,
+ OFONO_AUDIO_CLIENT,
+ &vtable_profile, backend)) {
+ free(backend);
+ return NULL;
+ }
+
+ if (add_filters(backend) < 0) {
+ dbus_connection_unregister_object_path(backend->conn, OFONO_AUDIO_CLIENT);
+ free(backend);
+ return NULL;
+ }
+
+ backend->this.available = is_available(backend);
+
+ return &backend->this;
+}
diff --git a/spa/plugins/bluez5/bap-codec-caps.h b/spa/plugins/bluez5/bap-codec-caps.h
new file mode 100644
index 0000000..7bfac35
--- /dev/null
+++ b/spa/plugins/bluez5/bap-codec-caps.h
@@ -0,0 +1,142 @@
+/* Spa BAP codec API
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef SPA_BLUEZ5_BAP_CODEC_CAPS_H_
+#define SPA_BLUEZ5_BAP_CODEC_CAPS_H_
+
+#define BAP_CODEC_LC3 0x06
+
+#define LC3_TYPE_FREQ 0x01
+#define LC3_FREQ_8KHZ (1 << 0)
+#define LC3_FREQ_11KHZ (1 << 1)
+#define LC3_FREQ_16KHZ (1 << 2)
+#define LC3_FREQ_22KHZ (1 << 3)
+#define LC3_FREQ_24KHZ (1 << 4)
+#define LC3_FREQ_32KHZ (1 << 5)
+#define LC3_FREQ_44KHZ (1 << 6)
+#define LC3_FREQ_48KHZ (1 << 7)
+#define LC3_FREQ_ANY (LC3_FREQ_8KHZ | \
+ LC3_FREQ_11KHZ | \
+ LC3_FREQ_16KHZ | \
+ LC3_FREQ_22KHZ | \
+ LC3_FREQ_24KHZ | \
+ LC3_FREQ_32KHZ | \
+ LC3_FREQ_44KHZ | \
+ LC3_FREQ_48KHZ)
+
+#define LC3_TYPE_DUR 0x02
+#define LC3_DUR_7_5 (1 << 0)
+#define LC3_DUR_10 (1 << 1)
+#define LC3_DUR_ANY (LC3_DUR_7_5 | \
+ LC3_DUR_10)
+
+#define LC3_TYPE_CHAN 0x03
+#define LC3_CHAN_1 (1 << 0)
+#define LC3_CHAN_2 (1 << 1)
+
+#define LC3_TYPE_FRAMELEN 0x04
+#define LC3_TYPE_BLKS 0x05
+
+/* LC3 config parameters */
+#define LC3_CONFIG_FREQ_8KHZ 0x01
+#define LC3_CONFIG_FREQ_11KHZ 0x02
+#define LC3_CONFIG_FREQ_16KHZ 0x03
+#define LC3_CONFIG_FREQ_22KHZ 0x04
+#define LC3_CONFIG_FREQ_24KHZ 0x05
+#define LC3_CONFIG_FREQ_32KHZ 0x06
+#define LC3_CONFIG_FREQ_44KHZ 0x07
+#define LC3_CONFIG_FREQ_48KHZ 0x08
+
+#define LC3_CONFIG_DURATION_7_5 0x00
+#define LC3_CONFIG_DURATION_10 0x01
+
+#define LC3_CONFIG_CHNL_NOT_ALLOWED 0x00000000
+#define LC3_CONFIG_CHNL_FL 0x00000001 /* front left */
+#define LC3_CONFIG_CHNL_FR 0x00000002 /* front right */
+#define LC3_CONFIG_CHNL_FC 0x00000004 /* front center */
+#define LC3_CONFIG_CHNL_LFE 0x00000008 /* LFE */
+#define LC3_CONFIG_CHNL_BL 0x00000010 /* back left */
+#define LC3_CONFIG_CHNL_BR 0x00000020 /* back right */
+#define LC3_CONFIG_CHNL_FLC 0x00000040 /* front left center */
+#define LC3_CONFIG_CHNL_FRC 0x00000080 /* front right center */
+#define LC3_CONFIG_CHNL_BC 0x00000100 /* back center */
+#define LC3_CONFIG_CHNL_LFE2 0x00000200 /* LFE 2 */
+#define LC3_CONFIG_CHNL_SL 0x00000400 /* side left */
+#define LC3_CONFIG_CHNL_SR 0x00000800 /* side right */
+#define LC3_CONFIG_CHNL_TFL 0x00001000 /* top front left */
+#define LC3_CONFIG_CHNL_TFR 0x00002000 /* top front right */
+#define LC3_CONFIG_CHNL_TFC 0x00004000 /* top front center */
+#define LC3_CONFIG_CHNL_TC 0x00008000 /* top center */
+#define LC3_CONFIG_CHNL_TBL 0x00010000 /* top back left */
+#define LC3_CONFIG_CHNL_TBR 0x00020000 /* top back right */
+#define LC3_CONFIG_CHNL_TSL 0x00040000 /* top side left */
+#define LC3_CONFIG_CHNL_TSR 0x00080000 /* top side right */
+#define LC3_CONFIG_CHNL_TBC 0x00100000 /* top back center */
+#define LC3_CONFIG_CHNL_BFC 0x00200000 /* bottom front center */
+#define LC3_CONFIG_CHNL_BFL 0x00400000 /* bottom front left */
+#define LC3_CONFIG_CHNL_BFR 0x00800000 /* bottom front right */
+#define LC3_CONFIG_CHNL_FLW 0x01000000 /* front left wide */
+#define LC3_CONFIG_CHNL_FRW 0x02000000 /* front right wide */
+#define LC3_CONFIG_CHNL_LS 0x04000000 /* left surround */
+#define LC3_CONFIG_CHNL_RS 0x08000000 /* right surround */
+
+#define LC3_MAX_CHANNELS 28
+
+typedef struct {
+ uint8_t rate;
+ uint8_t frame_duration;
+ uint32_t channels;
+ uint16_t framelen;
+ uint8_t n_blks;
+} __attribute__ ((packed)) bap_lc3_t;
+
+#define BT_ISO_QOS_CIG_UNSET 0xff
+#define BT_ISO_QOS_CIS_UNSET 0xff
+
+#define BT_ISO_QOS_TARGET_LATENCY_LOW 0x01
+#define BT_ISO_QOS_TARGET_LATENCY_BALANCED 0x02
+#define BT_ISO_QOS_TARGET_LATENCY_RELIABILITY 0x03
+
+struct bap_endpoint_qos {
+ uint8_t framing;
+ uint8_t phy;
+ uint8_t retransmission;
+ uint16_t latency;
+ uint32_t delay_min;
+ uint32_t delay_max;
+ uint32_t preferred_delay_min;
+ uint32_t preferred_delay_max;
+};
+
+struct bap_codec_qos {
+ uint32_t interval;
+ uint8_t framing;
+ uint8_t phy;
+ uint16_t sdu;
+ uint8_t retransmission;
+ uint16_t latency;
+ uint32_t delay;
+ uint8_t target_latency;
+};
+
+#endif
diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c
new file mode 100644
index 0000000..fe81168
--- /dev/null
+++ b/spa/plugins/bluez5/bap-codec-lc3.c
@@ -0,0 +1,859 @@
+/* Spa BAP LC3 codec
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2022 Pauli Virtanen
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <unistd.h>
+#include <stddef.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#include <bluetooth/bluetooth.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <lc3.h>
+
+#include "media-codecs.h"
+#include "bap-codec-caps.h"
+
+#define MAX_PACS 64
+
+struct impl {
+ lc3_encoder_t enc[LC3_MAX_CHANNELS];
+ lc3_decoder_t dec[LC3_MAX_CHANNELS];
+
+ int mtu;
+ int samplerate;
+ int channels;
+ int frame_dus;
+ int framelen;
+ int samples;
+ unsigned int codesize;
+};
+
+struct __attribute__((packed)) ltv {
+ uint8_t len;
+ uint8_t type;
+ uint8_t value[];
+};
+
+struct pac_data {
+ const uint8_t *data;
+ size_t size;
+};
+
+static int write_ltv(uint8_t *dest, uint8_t type, void* value, size_t len)
+{
+ struct ltv *ltv = (struct ltv *)dest;
+
+ ltv->len = len + 1;
+ ltv->type = type;
+ memcpy(ltv->value, value, len);
+
+ return len + 2;
+}
+
+static int write_ltv_uint8(uint8_t *dest, uint8_t type, uint8_t value)
+{
+ return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int write_ltv_uint16(uint8_t *dest, uint8_t type, uint16_t value)
+{
+ return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int write_ltv_uint32(uint8_t *dest, uint8_t type, uint32_t value)
+{
+ return write_ltv(dest, type, &value, sizeof(value));
+}
+
+static int codec_fill_caps(const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE])
+{
+ uint8_t *data = caps;
+ uint16_t framelen[2] = {htobs(LC3_MIN_FRAME_BYTES), htobs(LC3_MAX_FRAME_BYTES)};
+
+ data += write_ltv_uint16(data, LC3_TYPE_FREQ,
+ htobs(LC3_FREQ_48KHZ | LC3_FREQ_24KHZ | LC3_FREQ_16KHZ | LC3_FREQ_8KHZ));
+ data += write_ltv_uint8(data, LC3_TYPE_DUR, LC3_DUR_ANY);
+ data += write_ltv_uint8(data, LC3_TYPE_CHAN, LC3_CHAN_1 | LC3_CHAN_2);
+ data += write_ltv(data, LC3_TYPE_FRAMELEN, framelen, sizeof(framelen));
+ data += write_ltv_uint8(data, LC3_TYPE_BLKS, 2);
+
+ return data - caps;
+}
+
+static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_data pacs[MAX_PACS])
+{
+ /*
+ * BlueZ capabilites for the same codec may contain multiple
+ * PACs separated by zero-length LTV (see BlueZ b907befc2d80)
+ */
+ int pac = 0;
+
+ pacs[pac] = (struct pac_data){ data, 0 };
+
+ while (data_size > 0) {
+ struct ltv *ltv = (struct ltv *)data;
+
+ if (ltv->len == 0) {
+ /* delimiter */
+ if (pac + 1 >= MAX_PACS)
+ break;
+
+ ++pac;
+ pacs[pac] = (struct pac_data){ data + 1, 0 };
+ } else if (ltv->len >= data_size) {
+ return -EINVAL;
+ } else {
+ pacs[pac].size += ltv->len + 1;
+ }
+ data_size -= ltv->len + 1;
+ data += ltv->len + 1;
+ }
+
+ return pac + 1;
+}
+
+static bool parse_capabilities(bap_lc3_t *conf, const uint8_t *data, size_t data_size)
+{
+ uint16_t framelen_min = 0, framelen_max = 0;
+
+ if (!data_size)
+ return false;
+ memset(conf, 0, sizeof(*conf));
+
+ conf->frame_duration = 0xFF;
+
+ while (data_size > 0) {
+ struct ltv *ltv = (struct ltv *)data;
+
+ if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size)
+ return false;
+
+ switch (ltv->type) {
+ case LC3_TYPE_FREQ:
+ spa_return_val_if_fail(ltv->len == 3, false);
+ {
+ uint16_t rate = ltv->value[0] + (ltv->value[1] << 8);
+ if (rate & LC3_FREQ_48KHZ)
+ conf->rate = LC3_CONFIG_FREQ_48KHZ;
+ else if (rate & LC3_FREQ_24KHZ)
+ conf->rate = LC3_CONFIG_FREQ_24KHZ;
+ else if (rate & LC3_FREQ_16KHZ)
+ conf->rate = LC3_CONFIG_FREQ_16KHZ;
+ else if (rate & LC3_FREQ_8KHZ)
+ conf->rate = LC3_CONFIG_FREQ_8KHZ;
+ else
+ return false;
+ }
+ break;
+ case LC3_TYPE_DUR:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ {
+ uint8_t duration = ltv->value[0];
+ if (duration & LC3_DUR_10)
+ conf->frame_duration = LC3_CONFIG_DURATION_10;
+ else if (duration & LC3_DUR_7_5)
+ conf->frame_duration = LC3_CONFIG_DURATION_7_5;
+ else
+ return false;
+ }
+ break;
+ case LC3_TYPE_CHAN:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ {
+ uint8_t channels = ltv->value[0];
+ /* Only mono or stereo streams are currently supported,
+ * in both case Audio location is defined as both Front Left
+ * and Front Right, difference is done by the n_blks parameter.
+ */
+ if ((channels & LC3_CHAN_2) || (channels & LC3_CHAN_1))
+ conf->channels = LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL;
+ else
+ return false;
+ }
+ break;
+ case LC3_TYPE_FRAMELEN:
+ spa_return_val_if_fail(ltv->len == 5, false);
+ framelen_min = ltv->value[0] + (ltv->value[1] << 8);
+ framelen_max = ltv->value[2] + (ltv->value[3] << 8);
+ break;
+ case LC3_TYPE_BLKS:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ conf->n_blks = ltv->value[0];
+ if (!conf->n_blks)
+ return false;
+ break;
+ default:
+ return false;
+ }
+ data_size -= ltv->len + 1;
+ data += ltv->len + 1;
+ }
+
+ if (framelen_min < LC3_MIN_FRAME_BYTES || framelen_max > LC3_MAX_FRAME_BYTES)
+ return false;
+ if (conf->frame_duration == 0xFF || !conf->rate)
+ return false;
+ if (!conf->channels)
+ conf->channels = LC3_CONFIG_CHNL_FL;
+
+ switch (conf->rate) {
+ case LC3_CONFIG_FREQ_48KHZ:
+ if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+ conf->framelen = 117;
+ else
+ conf->framelen = 120;
+ break;
+ case LC3_CONFIG_FREQ_24KHZ:
+ if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+ conf->framelen = 45;
+ else
+ conf->framelen = 60;
+ break;
+ case LC3_CONFIG_FREQ_16KHZ:
+ if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+ conf->framelen = 30;
+ else
+ conf->framelen = 40;
+ break;
+ case LC3_CONFIG_FREQ_8KHZ:
+ if (conf->frame_duration == LC3_CONFIG_DURATION_7_5)
+ conf->framelen = 26;
+ else
+ conf->framelen = 30;
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+static bool parse_conf(bap_lc3_t *conf, const uint8_t *data, size_t data_size)
+{
+ if (!data_size)
+ return false;
+ memset(conf, 0, sizeof(*conf));
+
+ conf->frame_duration = 0xFF;
+
+ while (data_size > 0) {
+ struct ltv *ltv = (struct ltv *)data;
+
+ if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size)
+ return false;
+
+ switch (ltv->type) {
+ case LC3_TYPE_FREQ:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ conf->rate = ltv->value[0];
+ break;
+ case LC3_TYPE_DUR:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ conf->frame_duration = ltv->value[0];
+ break;
+ case LC3_TYPE_CHAN:
+ spa_return_val_if_fail(ltv->len == 5, false);
+ conf->channels = ltv->value[0] + (ltv->value[1] << 8) + (ltv->value[2] << 16) + (ltv->value[3] << 24);
+ break;
+ case LC3_TYPE_FRAMELEN:
+ spa_return_val_if_fail(ltv->len == 3, false);
+ conf->framelen = ltv->value[0] + (ltv->value[1] << 8);
+ break;
+ case LC3_TYPE_BLKS:
+ spa_return_val_if_fail(ltv->len == 2, false);
+ conf->n_blks = ltv->value[0];
+ if (!conf->n_blks)
+ return false;
+ break;
+ default:
+ return false;
+ }
+ data_size -= ltv->len + 1;
+ data += ltv->len + 1;
+ }
+
+ if (conf->frame_duration == 0xFF || !conf->rate)
+ return false;
+
+ return true;
+}
+
+static int conf_cmp(const bap_lc3_t *conf1, int res1, const bap_lc3_t *conf2, int res2)
+{
+ const bap_lc3_t *conf;
+ int a, b;
+
+#define PREFER_EXPR(expr) \
+ do { \
+ conf = conf1; \
+ a = (expr); \
+ conf = conf2; \
+ b = (expr); \
+ if (a != b) \
+ return b - a; \
+ } while (0)
+
+#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0)
+
+ /* Prefer valid */
+ a = (res1 > 0 && (size_t)res1 == sizeof(bap_lc3_t)) ? 1 : 0;
+ b = (res2 > 0 && (size_t)res2 == sizeof(bap_lc3_t)) ? 1 : 0;
+ if (!a || !b)
+ return b - a;
+
+ PREFER_BOOL(conf->channels & LC3_CHAN_2);
+ PREFER_BOOL(conf->rate & (LC3_CONFIG_FREQ_48KHZ | LC3_CONFIG_FREQ_24KHZ | LC3_CONFIG_FREQ_16KHZ | LC3_CONFIG_FREQ_8KHZ));
+ PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_48KHZ);
+
+ return 0;
+
+#undef PREFER_EXPR
+#undef PREFER_BOOL
+}
+
+static int pac_cmp(const void *p1, const void *p2)
+{
+ const struct pac_data *pac1 = p1;
+ const struct pac_data *pac2 = p2;
+ bap_lc3_t conf1, conf2;
+ int res1, res2;
+
+ res1 = parse_capabilities(&conf1, pac1->data, pac1->size) ? (int)sizeof(bap_lc3_t) : -EINVAL;
+ res2 = parse_capabilities(&conf2, pac2->data, pac2->size) ? (int)sizeof(bap_lc3_t) : -EINVAL;
+
+ return conf_cmp(&conf1, res1, &conf2, res2);
+}
+
+static int codec_select_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE])
+{
+ struct pac_data pacs[MAX_PACS];
+ int npacs;
+ bap_lc3_t conf;
+ uint8_t *data = config;
+
+ if (caps == NULL)
+ return -EINVAL;
+
+ /* Select best conf from those possible */
+ npacs = parse_bluez_pacs(caps, caps_size, pacs);
+ if (npacs < 0)
+ return npacs;
+ else if (npacs == 0)
+ return -EINVAL;
+
+ qsort(pacs, npacs, sizeof(struct pac_data), pac_cmp);
+
+ if (!parse_capabilities(&conf, pacs[0].data, pacs[0].size))
+ return -ENOTSUP;
+
+ data += write_ltv_uint8(data, LC3_TYPE_FREQ, conf.rate);
+ data += write_ltv_uint8(data, LC3_TYPE_DUR, conf.frame_duration);
+ data += write_ltv_uint32(data, LC3_TYPE_CHAN, htobl(conf.channels));
+ data += write_ltv_uint16(data, LC3_TYPE_FRAMELEN, htobs(conf.framelen));
+ data += write_ltv_uint8(data, LC3_TYPE_BLKS, conf.n_blks);
+
+ return data - config;
+}
+
+static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings)
+{
+ bap_lc3_t conf1, conf2;
+ int res1, res2;
+
+ /* Order selected configurations by preference */
+ res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1);
+ res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2);
+
+ return conf_cmp(&conf1, res1, &conf2, res2);
+}
+
+static uint8_t channels_to_positions(uint32_t channels, uint8_t n_channels, uint32_t *position)
+{
+ uint8_t n_positions = 0;
+
+ spa_assert(n_channels <= SPA_AUDIO_MAX_CHANNELS);
+
+ /* First check if stream is configure for Mono, i.e. 1 block for both Front
+ * Left anf Front Right,
+ * else map LE Audio locations to PipeWire locations in the ascending order
+ * which will be used as block order in stream.
+ */
+ if ((channels & (LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL)) == (LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL) &&
+ n_channels == 1) {
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
+ n_positions = 1;
+ } else {
+#define CHANNEL_2_SPACHANNEL(channel,spa_channel) if (channels & channel) position[n_positions++] = spa_channel;
+
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FL, SPA_AUDIO_CHANNEL_FL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FR, SPA_AUDIO_CHANNEL_FR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FC, SPA_AUDIO_CHANNEL_FC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LFE, SPA_AUDIO_CHANNEL_LFE);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BL, SPA_AUDIO_CHANNEL_RL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BR, SPA_AUDIO_CHANNEL_RR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FLC, SPA_AUDIO_CHANNEL_FLC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FRC, SPA_AUDIO_CHANNEL_FRC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BC, SPA_AUDIO_CHANNEL_BC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LFE2, SPA_AUDIO_CHANNEL_LFE2);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_SL, SPA_AUDIO_CHANNEL_SL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_SR, SPA_AUDIO_CHANNEL_SR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFL, SPA_AUDIO_CHANNEL_TFL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFR, SPA_AUDIO_CHANNEL_TFR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFC, SPA_AUDIO_CHANNEL_TFC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TC, SPA_AUDIO_CHANNEL_TC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBL, SPA_AUDIO_CHANNEL_TRL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBR, SPA_AUDIO_CHANNEL_TRR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TSL, SPA_AUDIO_CHANNEL_TSL);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TSR, SPA_AUDIO_CHANNEL_TSR);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBC, SPA_AUDIO_CHANNEL_TRC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFC, SPA_AUDIO_CHANNEL_BC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFL, SPA_AUDIO_CHANNEL_BLC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFR, SPA_AUDIO_CHANNEL_BRC);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FLW, SPA_AUDIO_CHANNEL_FLW);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FRW, SPA_AUDIO_CHANNEL_FRW);
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LS, SPA_AUDIO_CHANNEL_LLFE); /* is it the right mapping? */
+ CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_RS, SPA_AUDIO_CHANNEL_RLFE); /* is it the right mapping? */
+
+#undef CHANNEL_2_SPACHANNEL
+ }
+
+ return n_positions;
+}
+
+static int codec_enum_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *b, struct spa_pod **param)
+{
+ bap_lc3_t conf;
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t i = 0;
+ uint8_t res;
+
+ if (!parse_conf(&conf, caps, caps_size))
+ return -EINVAL;
+
+ if (idx > 0)
+ return 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24_32),
+ 0);
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+ i = 0;
+ if (conf.rate & LC3_CONFIG_FREQ_48KHZ) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 48000);
+ spa_pod_builder_int(b, 48000);
+ }
+ if (conf.rate & LC3_CONFIG_FREQ_24KHZ) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 24000);
+ spa_pod_builder_int(b, 24000);
+ }
+ if (conf.rate & LC3_CONFIG_FREQ_16KHZ) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 16000);
+ spa_pod_builder_int(b, 16000);
+ }
+ if (conf.rate & LC3_CONFIG_FREQ_8KHZ) {
+ if (i++ == 0)
+ spa_pod_builder_int(b, 8000);
+ spa_pod_builder_int(b, 8000);
+ }
+ if (i == 0)
+ return -EINVAL;
+ if (i > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ res = channels_to_positions(conf.channels, conf.n_blks, position);
+ if (res == 0)
+ return -EINVAL;
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(res),
+ SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, res, position),
+ 0);
+
+ *param = spa_pod_builder_pop(b, &f[0]);
+ return *param == NULL ? -EIO : 1;
+}
+
+static int codec_validate_config(const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info)
+{
+ bap_lc3_t conf;
+ uint8_t res;
+
+ if (caps == NULL)
+ return -EINVAL;
+
+ if (!parse_conf(&conf, caps, caps_size))
+ return -ENOTSUP;
+
+ spa_zero(*info);
+ info->media_type = SPA_MEDIA_TYPE_audio;
+ info->media_subtype = SPA_MEDIA_SUBTYPE_raw;
+ info->info.raw.format = SPA_AUDIO_FORMAT_S24_32;
+
+ switch (conf.rate) {
+ case LC3_CONFIG_FREQ_48KHZ:
+ info->info.raw.rate = 48000U;
+ break;
+ case LC3_CONFIG_FREQ_24KHZ:
+ info->info.raw.rate = 24000U;
+ break;
+ case LC3_CONFIG_FREQ_16KHZ:
+ info->info.raw.rate = 16000U;
+ break;
+ case LC3_CONFIG_FREQ_8KHZ:
+ info->info.raw.rate = 8000U;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ res = channels_to_positions(conf.channels, conf.n_blks, info->info.raw.position);
+ if (res == 0)
+ return -EINVAL;
+ info->info.raw.channels = res;
+
+ switch (conf.frame_duration) {
+ case LC3_CONFIG_DURATION_10:
+ case LC3_CONFIG_DURATION_7_5:
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int codec_get_qos(const struct media_codec *codec,
+ const void *config, size_t config_size,
+ const struct bap_endpoint_qos *endpoint_qos,
+ struct bap_codec_qos *qos)
+{
+ bap_lc3_t conf;
+
+ spa_zero(*qos);
+
+ if (!parse_conf(&conf, config, config_size))
+ return -EINVAL;
+
+ qos->framing = false;
+ if (endpoint_qos->phy & 0x2)
+ qos->phy = 0x2;
+ else if (endpoint_qos->phy & 0x1)
+ qos->phy = 0x1;
+ else
+ qos->phy = 0x2;
+ qos->retransmission = 2; /* default */
+ qos->sdu = conf.framelen * conf.n_blks;
+ qos->latency = 20; /* default */
+ qos->delay = 40000U;
+ qos->interval = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000);
+ qos->target_latency = BT_ISO_QOS_TARGET_LATENCY_BALANCED;
+
+ switch (conf.rate) {
+ case LC3_CONFIG_FREQ_8KHZ:
+ case LC3_CONFIG_FREQ_16KHZ:
+ case LC3_CONFIG_FREQ_24KHZ:
+ case LC3_CONFIG_FREQ_32KHZ:
+ qos->retransmission = 2;
+ qos->latency = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 8 : 10);
+ break;
+ case LC3_CONFIG_FREQ_48KHZ:
+ qos->retransmission = 5;
+ qos->latency = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 15 : 20);
+ break;
+ }
+
+ /* Clamp to ASE values */
+ if (endpoint_qos->latency >= 0x0005 && endpoint_qos->latency <= 0x0FA0)
+ /* Values outside the range are RFU */
+ qos->latency = SPA_MAX(qos->latency, endpoint_qos->latency);
+
+ if (endpoint_qos->delay_min)
+ qos->delay = SPA_MAX(qos->delay, endpoint_qos->delay_min);
+ if (endpoint_qos->delay_max)
+ qos->delay = SPA_MIN(qos->delay, endpoint_qos->delay_max);
+
+ return 0;
+}
+
+static void *codec_init(const struct media_codec *codec, uint32_t flags,
+ void *config, size_t config_len, const struct spa_audio_info *info,
+ void *props, size_t mtu)
+{
+ bap_lc3_t conf;
+ struct impl *this = NULL;
+ struct spa_audio_info config_info;
+ int res, ich;
+
+ if (info->media_type != SPA_MEDIA_TYPE_audio ||
+ info->media_subtype != SPA_MEDIA_SUBTYPE_raw ||
+ info->info.raw.format != SPA_AUDIO_FORMAT_S24_32) {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((this = calloc(1, sizeof(struct impl))) == NULL)
+ goto error_errno;
+
+ if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0)
+ goto error;
+
+ if (!parse_conf(&conf, config, config_len)) {
+ res = -ENOTSUP;
+ goto error;
+ }
+
+ this->mtu = mtu;
+ this->samplerate = config_info.info.raw.rate;
+ this->channels = config_info.info.raw.channels;
+ this->framelen = conf.framelen;
+
+ switch (conf.frame_duration) {
+ case LC3_CONFIG_DURATION_10:
+ this->frame_dus = 10000;
+ break;
+ case LC3_CONFIG_DURATION_7_5:
+ this->frame_dus = 7500;
+ break;
+ default:
+ res = -EINVAL;
+ goto error;
+ }
+
+ this->samples = lc3_frame_samples(this->frame_dus, this->samplerate);
+ if (this->samples < 0) {
+ res = -EINVAL;
+ goto error;
+ }
+ this->codesize = this->samples * this->channels * sizeof(int32_t);
+
+ if (!(flags & MEDIA_CODEC_FLAG_SINK)) {
+ for (ich = 0; ich < this->channels; ich++) {
+ this->enc[ich] = lc3_setup_encoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_encoder_size(this->frame_dus, this->samplerate)));
+ if (this->enc[ich] == NULL) {
+ res = -EINVAL;
+ goto error;
+ }
+ }
+ } else {
+ for (ich = 0; ich < this->channels; ich++) {
+ this->dec[ich] = lc3_setup_decoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_decoder_size(this->frame_dus, this->samplerate)));
+ if (this->dec[ich] == NULL) {
+ res = -EINVAL;
+ goto error;
+ }
+ }
+ }
+
+ return this;
+
+error_errno:
+ res = -errno;
+ goto error;
+
+error:
+ if (this) {
+ for (ich = 0; ich < this->channels; ich++) {
+ if (this->enc[ich])
+ free(this->enc[ich]);
+ if (this->dec[ich])
+ free(this->dec[ich]);
+ }
+ }
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+static void codec_deinit(void *data)
+{
+ struct impl *this = data;
+ int ich;
+
+ for (ich = 0; ich < this->channels; ich++) {
+ if (this->enc[ich])
+ free(this->enc[ich]);
+ if (this->dec[ich])
+ free(this->dec[ich]);
+ }
+ free(this);
+}
+
+static int codec_get_block_size(void *data)
+{
+ struct impl *this = data;
+ return this->codesize;
+}
+
+static int codec_abr_process (void *data, size_t unsent)
+{
+ return -ENOTSUP;
+}
+
+static int codec_start_encode (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp)
+{
+ return 0;
+}
+
+static int codec_encode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush)
+{
+ struct impl *this = data;
+ int frame_bytes;
+ int ich, res;
+ int size, processed;
+
+ frame_bytes = lc3_frame_bytes(this->frame_dus, this->samplerate);
+ processed = 0;
+ size = 0;
+
+ if (src_size < (size_t)this->codesize)
+ goto done;
+ if (dst_size < (size_t)frame_bytes)
+ goto done;
+
+ for (ich = 0; ich < this->channels; ich++) {
+ uint8_t *in = (uint8_t *)src + (ich * 4);
+ uint8_t *out = (uint8_t *)dst + ich * this->framelen;
+ res = lc3_encode(this->enc[ich], LC3_PCM_FORMAT_S24, in, this->channels, this->framelen, out);
+ size += this->framelen;
+ if (SPA_UNLIKELY(res != 0))
+ return -EINVAL;
+ }
+ *dst_out = size;
+
+ processed += this->codesize;
+
+done:
+ spa_assert(size <= this->mtu);
+ *need_flush = NEED_FLUSH_ALL;
+
+ return processed;
+}
+
+static SPA_UNUSED int codec_start_decode (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp)
+{
+ return 0;
+}
+
+static SPA_UNUSED int codec_decode(void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out)
+{
+ struct impl *this = data;
+ int ich, res;
+ int consumed;
+ int samples;
+
+ spa_return_val_if_fail((size_t)(this->framelen * this->channels) == src_size, -EINVAL);
+ consumed = 0;
+
+ samples = lc3_frame_samples(this->frame_dus, this->samplerate);
+ if (samples == -1)
+ return -EINVAL;
+ if (dst_size < this->codesize)
+ return -EINVAL;
+
+ for (ich = 0; ich < this->channels; ich++) {
+ uint8_t *in = (uint8_t *)src + ich * this->framelen;
+ uint8_t *out = (uint8_t *)dst + (ich * 4);
+ res = lc3_decode(this->dec[ich], in, this->framelen, LC3_PCM_FORMAT_S24, out, this->channels);
+ if (SPA_UNLIKELY(res < 0))
+ return -EINVAL;
+ consumed += this->framelen;
+ }
+
+ *dst_out = this->codesize;
+
+ return consumed;
+}
+
+static int codec_reduce_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+static int codec_increase_bitpool(void *data)
+{
+ return -ENOTSUP;
+}
+
+const struct media_codec bap_codec_lc3 = {
+ .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3,
+ .name = "lc3",
+ .codec_id = BAP_CODEC_LC3,
+ .bap = true,
+ .description = "LC3",
+ .fill_caps = codec_fill_caps,
+ .select_config = codec_select_config,
+ .enum_config = codec_enum_config,
+ .validate_config = codec_validate_config,
+ .get_qos = codec_get_qos,
+ .caps_preference_cmp = codec_caps_preference_cmp,
+ .init = codec_init,
+ .deinit = codec_deinit,
+ .get_block_size = codec_get_block_size,
+ .abr_process = codec_abr_process,
+ .start_encode = codec_start_encode,
+ .encode = codec_encode,
+ .start_decode = codec_start_decode,
+ .decode = codec_decode,
+ .reduce_bitpool = codec_reduce_bitpool,
+ .increase_bitpool = codec_increase_bitpool
+};
+
+MEDIA_CODEC_EXPORT_DEF(
+ "lc3",
+ &bap_codec_lc3
+);
diff --git a/spa/plugins/bluez5/bluez-hardware.conf b/spa/plugins/bluez5/bluez-hardware.conf
new file mode 100644
index 0000000..0247f75
--- /dev/null
+++ b/spa/plugins/bluez5/bluez-hardware.conf
@@ -0,0 +1,103 @@
+# List of hardware/kernel features, which cannot be detected generically.
+#
+# The `feature` is enabled only if all three of adapter, device, and
+# kernel have it.
+#
+# For each of the adapter/device/kernel, the match rules are processed
+# one at a time, and the first one that matches is used.
+#
+# Features and tags:
+# msbc "standard" mSBC (60 byte tx packet)
+# msbc-alt1 USB adapters with mSBC in ALT1 setting (24 byte tx packet)
+# msbc-alt1-rtl Realtek USB adapters with mSBC in ALT1 setting (24 byte tx packet)
+# hw-volume AVRCP and HSP/HFP hardware volume support
+# hw-volume-mic Functional HSP/HFP microphone volume support
+# sbc-xq "nonstandard" SBC codec setting with better sound quality
+# faststream FastStream codec support
+# a2dp-duplex A2DP duplex codec support
+#
+# Features are disabled with the key "no-features" whose value is an
+# array of strings in the match rule.
+
+bluez5.features.device = [
+ # properties:
+ # - name
+ # - address ("ff:ff:ff:ff:ff:ff")
+ # - vendor-id ("bluetooth:ffff", "usb:ffff")
+ # - product-id
+ # - version-id
+
+ { name = "Air 1 Plus", no-features = [ hw-volume-mic ] },
+ { name = "AirPods", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "AirPods Pro", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "AXLOIE Goin", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "BAA 100", no-features = [ hw-volume ] }, # Buxton BAA 100, doesn't remember volume, #pipewire-1449
+ { name = "D50s", address = "~^00:13:ef:", no-features = [ hw-volume ] }, # volume has no effect, #pipewire-1562
+ { name = "FiiO BTR3", address = "~^40:ed:98:", no-features = [ faststream ] }, # #pipewire-1658
+ { name = "JBL Endurance RUN BT", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "JBL LIVE650BTNC" },
+ { name = "Motorola DC800", no-features = [ sbc-xq ] }, # #pipewire-1590
+ { name = "Motorola S305", no-features = [ sbc-xq ] }, # #pipewire-1590
+ { name = "Soundcore Life P2-L", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { name = "Soundcore Motion B", no-features = [ hw-volume ] },
+ { name = "SoundCore mini", no-features = [ hw-volume ] }, # #pipewire-1686
+ { name = "SoundCore 2", no-features = [ sbc-xq ] }, # #pipewire-2291
+ { name = "Tribit MAXSound Plus", no-features = [ hw-volume ] }, # #pipewire-1592
+ { name = "Urbanista Stockholm Plus", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+
+ { address = "~^44:5e:cd:", no-features = [ faststream, a2dp-duplex ]}, # #pipewire-1756
+
+ { address = "~^94:16:25:", no-features = [ hw-volume ]}, # AirPods 2
+ { address = "~^9c:64:8b:", no-features = [ hw-volume ]}, # AirPods 2
+ { address = "~^a0:e9:db:", no-features = [ hw-volume ]}, # Ausdom M05
+ { address = "~^0c:a6:94:", no-features = [ hw-volume ]}, # deepblue2
+ { address = "~^00:14:02:", no-features = [ hw-volume ]}, # iKross IKBT83B HS
+ { address = "~^44:5e:f3:", no-features = [ hw-volume ]}, # JayBird BlueBuds X
+ { address = "~^d4:9c:28:", no-features = [ hw-volume ]}, # JayBird BlueBuds X
+ { address = "~^00:18:6b:", no-features = [ hw-volume ]}, # LG Tone HBS-730
+ { address = "~^b8:ad:3e:", no-features = [ hw-volume ]}, # LG Tone HBS-730
+ { address = "~^a0:e9:db:", no-features = [ hw-volume ]}, # LG Tone HV-800
+ { address = "~^00:24:1c:", no-features = [ hw-volume ]}, # Motorola Roadster
+ { address = "~^00:11:b1:", no-features = [ hw-volume ]}, # Mpow Cheetah
+ { address = "~^a4:15:66:", no-features = [ hw-volume ]}, # SOL REPUBLIC Tracks Air
+ { address = "~^00:14:f1:", no-features = [ hw-volume ]}, # Swage Rokitboost HS
+ { address = "~^00:26:7e:", no-features = [ hw-volume ]}, # VW Car Kit
+ { address = "~^90:03:b7:", no-features = [ hw-volume ]}, # VW Car Kit
+
+ # All features are enabled by default; it's simpler to block non-working devices one by one.
+]
+
+bluez5.features.adapter = [
+ # properties:
+ # - address ("ff:ff:ff:ff:ff:ff")
+ # - bus-type ("usb", "other")
+ # - vendor-id ("usb:ffff")
+ # - product-id ("ffff")
+
+ # Realtek Semiconductor Corp.
+ { bus-type = "usb", vendor-id = "usb:0bda" },
+
+ # Generic USB adapters
+ { bus-type = "usb", no-features = [ msbc-alt1-rtl ] },
+
+ # Other adapters
+ { no-features = [ msbc-alt1-rtl ] },
+]
+
+bluez5.features.kernel = [
+ # properties (as in uname):
+ # - sysname
+ # - release
+ # - version
+
+ # See https://lore.kernel.org/linux-bluetooth/20201210012003.133000-1-tpiepho@gmail.com/
+ # https://lore.kernel.org/linux-bluetooth/b86543908684cc6cd9afaf4de10fac7af1a49665.camel@iki.fi/
+ { sysname = "Linux", release = "~^[0-4]\\.", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { sysname = "Linux", release = "~^5\\.[1-7]\\.", no-features = [ msbc-alt1, msbc-alt1-rtl ] },
+ { sysname = "Linux", release = "~^5\\.(8|9)\\.", no-features = [ msbc-alt1 ] },
+ { sysname = "Linux", release = "~^5\\.10\\.(1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|51|52|53|54|55|56|57|58|59|60|61)($|[^0-9])", no-features = [ msbc-alt1 ] },
+ { sysname = "Linux", release = "~^5\\.12\\.(18|19)($|[^0-9])", no-features = [ msbc-alt1 ] },
+ { sysname = "Linux", release = "~^5\\.13\\.(3|4|5|6|7|8|9|10|11|12|13)($|[^0-9])", no-features = [ msbc-alt1 ] },
+
+ { no-features = [] },
+]
diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c
new file mode 100644
index 0000000..4034f99
--- /dev/null
+++ b/spa/plugins/bluez5/bluez5-dbus.c
@@ -0,0 +1,5182 @@
+/* Spa V4l2 dbus
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <fcntl.h>
+
+#include <bluetooth/bluetooth.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/debug/mem.h>
+#include <spa/debug/log.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/support/plugin-loader.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include "config.h"
+#include "codec-loader.h"
+#include "player.h"
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+enum backend_selection {
+ BACKEND_NONE = -2,
+ BACKEND_ANY = -1,
+ BACKEND_HSPHFPD = 0,
+ BACKEND_OFONO = 1,
+ BACKEND_NATIVE = 2,
+ BACKEND_NUM,
+};
+
+/*
+ * Rate limit for BlueZ SetConfiguration calls.
+ *
+ * Too rapid calls to BlueZ API may cause A2DP profile to disappear, as the
+ * internal BlueZ/connection state gets confused. Use some reasonable minimum
+ * interval.
+ *
+ * AVDTP v1.3 Sec. 6.13 mentions 3 seconds as a reasonable timeout in one case
+ * (ACP connection reset timeout, if no INT response). The case here is
+ * different, but we assume a similar value is fine here.
+ */
+#define BLUEZ_ACTION_RATE_MSEC 3000
+
+#define CODEC_SWITCH_RETRIES 1
+
+
+struct spa_bt_monitor {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_system *main_system;
+ struct spa_plugin_loader *plugin_loader;
+ struct spa_dbus *dbus;
+ struct spa_dbus_connection *dbus_connection;
+ DBusConnection *conn;
+
+ struct spa_hook_list hooks;
+
+ uint32_t id;
+
+ const struct media_codec * const * media_codecs;
+
+ /*
+ * Lists of BlueZ objects, kept up-to-date by following DBus events
+ * initiated by BlueZ. Object lifetime is also determined by that.
+ */
+ struct spa_list adapter_list;
+ struct spa_list device_list;
+ struct spa_list remote_endpoint_list;
+ struct spa_list transport_list;
+
+ unsigned int filters_added:1;
+ unsigned int objects_listed:1;
+ DBusPendingCall *get_managed_objects_call;
+
+ struct spa_bt_backend *backend;
+ struct spa_bt_backend *backends[BACKEND_NUM];
+ enum backend_selection backend_selection;
+
+ struct spa_dict enabled_codecs;
+
+ unsigned int connection_info_supported:1;
+ unsigned int dummy_avrcp_player:1;
+
+ struct spa_bt_quirks *quirks;
+
+#define MAX_SETTINGS 128
+ struct spa_dict_item global_setting_items[MAX_SETTINGS];
+ struct spa_dict global_settings;
+
+ /* A reference audio info for A2DP codec configuration. */
+ struct media_codec_audio_info default_audio_info;
+
+ bool le_audio_supported;
+};
+
+/* Stream endpoints owned by BlueZ for each device */
+struct spa_bt_remote_endpoint {
+ struct spa_list link;
+ struct spa_list device_link;
+ struct spa_bt_monitor *monitor;
+ char *path;
+
+ char *uuid;
+ unsigned int codec;
+ struct spa_bt_device *device;
+ uint8_t *capabilities;
+ int capabilities_len;
+ bool delay_reporting;
+ bool acceptor;
+};
+
+/*
+ * Codec switching tries various codec/remote endpoint combinations
+ * in order, until an acceptable one is found. This triggers BlueZ
+ * to initiate DBus calls that result to the creation of a transport
+ * with the desired capabilities.
+ * The codec switch struct tracks candidates still to be tried.
+ */
+struct spa_bt_media_codec_switch {
+ struct spa_bt_device *device;
+ struct spa_list device_link;
+
+ /*
+ * Codec switch may be waiting for either DBus reply from BlueZ
+ * or a timeout (but not both).
+ */
+ struct spa_source timer;
+ DBusPendingCall *pending;
+
+ uint32_t profile;
+
+ /*
+ * Called asynchronously, so endpoint paths instead of pointers (which may be
+ * invalidated in the meantime).
+ */
+ const struct media_codec **codecs;
+ char **paths;
+
+ const struct media_codec **codec_iter; /**< outer iterator over codecs */
+ char **path_iter; /**< inner iterator over endpoint paths */
+
+ uint16_t retries;
+ size_t num_paths;
+};
+
+#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)
+
+#define BT_DEVICE_DISCONNECTED 0
+#define BT_DEVICE_CONNECTED 1
+#define BT_DEVICE_INIT -1
+
+/*
+ * SCO socket connect may fail with ECONNABORTED if it is done too soon after
+ * previous close. To avoid this in cases where nodes are toggled between
+ * stopped/started rapidly, postpone release until the transport has remained
+ * unused for a time. Since this appears common to multiple SCO backends, we do
+ * it for all SCO backends here.
+ */
+#define SCO_TRANSPORT_RELEASE_TIMEOUT_MSEC 1000
+#define SPA_BT_TRANSPORT_IS_SCO(transport) (transport->backend != NULL)
+
+#define TRANSPORT_VOLUME_TIMEOUT_MSEC 200
+
+static int spa_bt_transport_stop_volume_timer(struct spa_bt_transport *transport);
+static int spa_bt_transport_start_volume_timer(struct spa_bt_transport *transport);
+static int spa_bt_transport_stop_release_timer(struct spa_bt_transport *transport);
+static int spa_bt_transport_start_release_timer(struct spa_bt_transport *transport);
+
+static int device_start_timer(struct spa_bt_device *device);
+static int device_stop_timer(struct spa_bt_device *device);
+
+// Working with BlueZ Battery Provider.
+// Developed using https://github.com/dgreid/adhd/commit/655b58f as an example of DBus calls.
+
+// Name of battery, formatted as /org/freedesktop/pipewire/battery/org/bluez/hciX/dev_XX_XX_XX_XX_XX_XX
+static char *battery_get_name(const char *device_path)
+{
+ char *path = malloc(strlen(PIPEWIRE_BATTERY_PROVIDER) + strlen(device_path) + 1);
+ sprintf(path, PIPEWIRE_BATTERY_PROVIDER "%s", device_path);
+ return path;
+}
+
+// Unregister virtual battery of device
+static void battery_remove(struct spa_bt_device *device) {
+ DBusMessageIter i, entry;
+ DBusMessage *m;
+ const char *interface;
+
+ if (device->battery_pending_call) {
+ spa_log_debug(device->monitor->log, "Cancelling and freeing pending battery provider register call");
+ dbus_pending_call_cancel(device->battery_pending_call);
+ dbus_pending_call_unref(device->battery_pending_call);
+ device->battery_pending_call = NULL;
+ }
+
+ if (!device->adapter || !device->adapter->has_battery_provider || !device->has_battery)
+ return;
+
+ spa_log_debug(device->monitor->log, "Removing virtual battery: %s", device->battery_path);
+
+ m = dbus_message_new_signal(PIPEWIRE_BATTERY_PROVIDER,
+ DBUS_INTERFACE_OBJECT_MANAGER,
+ DBUS_SIGNAL_INTERFACES_REMOVED);
+
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH,
+ &device->battery_path);
+ dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY,
+ DBUS_TYPE_STRING_AS_STRING, &entry);
+ interface = BLUEZ_INTERFACE_BATTERY_PROVIDER;
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING,
+ &interface);
+ dbus_message_iter_close_container(&i, &entry);
+
+ if (!dbus_connection_send(device->monitor->conn, m, NULL)) {
+ spa_log_error(device->monitor->log, "sending " DBUS_SIGNAL_INTERFACES_REMOVED " failed");
+ }
+
+ dbus_message_unref(m);
+
+ device->has_battery = false;
+}
+
+// Create properties for Battery Provider request
+static void battery_write_properties(DBusMessageIter *iter, struct spa_bt_device *device)
+{
+ DBusMessageIter dict, entry, variant;
+
+ dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL,
+ &entry);
+ const char *prop_percentage = "Percentage";
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &prop_percentage);
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+ DBUS_TYPE_BYTE_AS_STRING, &variant);
+ dbus_message_iter_append_basic(&variant, DBUS_TYPE_BYTE, &device->battery);
+ dbus_message_iter_close_container(&entry, &variant);
+ dbus_message_iter_close_container(&dict, &entry);
+
+ dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+ const char *prop_device = "Device";
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &prop_device);
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT,
+ DBUS_TYPE_OBJECT_PATH_AS_STRING,
+ &variant);
+ dbus_message_iter_append_basic(&variant, DBUS_TYPE_OBJECT_PATH, &device->path);
+ dbus_message_iter_close_container(&entry, &variant);
+ dbus_message_iter_close_container(&dict, &entry);
+
+ dbus_message_iter_close_container(iter, &dict);
+}
+
+// Send current percentage to BlueZ
+static void battery_update(struct spa_bt_device *device)
+{
+ spa_log_debug(device->monitor->log, "updating battery: %s", device->battery_path);
+
+ DBusMessage *msg;
+ DBusMessageIter iter;
+
+ msg = dbus_message_new_signal(device->battery_path,
+ DBUS_INTERFACE_PROPERTIES,
+ DBUS_SIGNAL_PROPERTIES_CHANGED);
+
+ dbus_message_iter_init_append(msg, &iter);
+ const char *interface = BLUEZ_INTERFACE_BATTERY_PROVIDER;
+ dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING,
+ &interface);
+
+ battery_write_properties(&iter, device);
+
+ if (!dbus_connection_send(device->monitor->conn, msg, NULL))
+ spa_log_error(device->monitor->log, "Error updating battery");
+
+ dbus_message_unref(msg);
+}
+
+// Create new virtual battery with value stored in current device object
+static void battery_create(struct spa_bt_device *device) {
+ DBusMessage *msg;
+ DBusMessageIter iter, entry, dict;
+ msg = dbus_message_new_signal(PIPEWIRE_BATTERY_PROVIDER,
+ DBUS_INTERFACE_OBJECT_MANAGER,
+ DBUS_SIGNAL_INTERFACES_ADDED);
+
+ dbus_message_iter_init_append(msg, &iter);
+ dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
+ &device->battery_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);
+ const char *interface = BLUEZ_INTERFACE_BATTERY_PROVIDER;
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING,
+ &interface);
+
+ battery_write_properties(&entry, device);
+
+ dbus_message_iter_close_container(&dict, &entry);
+ dbus_message_iter_close_container(&iter, &dict);
+
+ if (!dbus_connection_send(device->monitor->conn, msg, NULL)) {
+ spa_log_error(device->monitor->log, "Failed to create virtual battery for %s", device->address);
+ return;
+ }
+
+ dbus_message_unref(msg);
+
+ spa_log_debug(device->monitor->log, "Created virtual battery for %s", device->address);
+ device->has_battery = true;
+}
+
+static void on_battery_provider_registered(DBusPendingCall *pending_call,
+ void *data)
+{
+ DBusMessage *reply;
+ struct spa_bt_device *device = data;
+
+ reply = dbus_pending_call_steal_reply(pending_call);
+ dbus_pending_call_unref(pending_call);
+
+ device->battery_pending_call = NULL;
+
+ if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(device->monitor->log, "Failed to register battery provider. Error: %s", dbus_message_get_error_name(reply));
+ spa_log_error(device->monitor->log, "BlueZ Battery Provider is not available, won't retry to register it. Make sure you are running BlueZ 5.56+ with experimental features to use Battery Provider.");
+ device->adapter->battery_provider_unavailable = true;
+ dbus_message_unref(reply);
+ return;
+ }
+
+ spa_log_debug(device->monitor->log, "Registered Battery Provider");
+
+ device->adapter->has_battery_provider = true;
+
+ if (!device->has_battery)
+ battery_create(device);
+
+ dbus_message_unref(reply);
+}
+
+// Register Battery Provider for adapter and then create virtual battery for device
+static void register_battery_provider(struct spa_bt_device *device)
+{
+ DBusMessage *method_call;
+ DBusMessageIter message_iter;
+
+ if (device->battery_pending_call) {
+ spa_log_debug(device->monitor->log, "Already registering battery provider");
+ return;
+ }
+
+ method_call = dbus_message_new_method_call(
+ BLUEZ_SERVICE, device->adapter_path,
+ BLUEZ_INTERFACE_BATTERY_PROVIDER_MANAGER,
+ "RegisterBatteryProvider");
+
+ if (!method_call) {
+ spa_log_error(device->monitor->log, "Failed to register battery provider");
+ return;
+ }
+
+ dbus_message_iter_init_append(method_call, &message_iter);
+ const char *object_path = PIPEWIRE_BATTERY_PROVIDER;
+ dbus_message_iter_append_basic(&message_iter, DBUS_TYPE_OBJECT_PATH,
+ &object_path);
+
+ if (!dbus_connection_send_with_reply(device->monitor->conn, method_call, &device->battery_pending_call,
+ DBUS_TIMEOUT_USE_DEFAULT)) {
+ dbus_message_unref(method_call);
+ spa_log_error(device->monitor->log, "Failed to register battery provider");
+ return;
+ }
+
+ dbus_message_unref(method_call);
+
+ if (!device->battery_pending_call) {
+ spa_log_error(device->monitor->log, "Failed to register battery provider");
+ return;
+ }
+
+ if (!dbus_pending_call_set_notify(
+ device->battery_pending_call, on_battery_provider_registered,
+ device, NULL)) {
+ spa_log_error(device->monitor->log, "Failed to register battery provider");
+ dbus_pending_call_cancel(device->battery_pending_call);
+ dbus_pending_call_unref(device->battery_pending_call);
+ device->battery_pending_call = NULL;
+ }
+}
+
+static int media_codec_to_endpoint(const struct media_codec *codec,
+ enum spa_bt_media_direction direction,
+ char** object_path)
+{
+ const char * endpoint;
+
+ if (direction == SPA_BT_MEDIA_SOURCE)
+ endpoint = codec->bap ? BAP_SOURCE_ENDPOINT : A2DP_SOURCE_ENDPOINT;
+ else
+ endpoint = codec->bap ? BAP_SINK_ENDPOINT : A2DP_SINK_ENDPOINT;
+
+ *object_path = spa_aprintf("%s/%s", endpoint,
+ codec->endpoint_name ? codec->endpoint_name : codec->name);
+ if (*object_path == NULL)
+ return -errno;
+ return 0;
+}
+
+static const struct media_codec *media_endpoint_to_codec(struct spa_bt_monitor *monitor, const char *endpoint, bool *sink, const struct media_codec *preferred)
+{
+ const char *ep_name;
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ const struct media_codec *found = NULL;
+ int i;
+
+ if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/")) {
+ ep_name = endpoint + strlen(A2DP_SINK_ENDPOINT "/");
+ *sink = true;
+ } else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) {
+ ep_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/");
+ *sink = false;
+ } else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/")) {
+ ep_name = endpoint + strlen(BAP_SOURCE_ENDPOINT "/");
+ *sink = false;
+ } else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/")) {
+ ep_name = endpoint + strlen(BAP_SINK_ENDPOINT "/");
+ *sink = true;
+ } else {
+ *sink = true;
+ return NULL;
+ }
+
+ for (i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+ const char *codec_ep_name =
+ codec->endpoint_name ? codec->endpoint_name : codec->name;
+
+ if (!spa_streq(ep_name, codec_ep_name))
+ continue;
+ if ((*sink && !codec->decode) || (!*sink && !codec->encode))
+ continue;
+
+ /* Same endpoint may be shared with multiple codec objects,
+ * which may e.g. correspond to different encoder settings.
+ * Look up which one we selected.
+ */
+ if ((preferred && codec == preferred) || found == NULL)
+ found = codec;
+ }
+ return found;
+}
+
+static int media_endpoint_to_profile(const char *endpoint)
+{
+
+ if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/"))
+ return SPA_BT_PROFILE_A2DP_SOURCE;
+ else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/"))
+ return SPA_BT_PROFILE_A2DP_SINK;
+ else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/"))
+ return SPA_BT_PROFILE_BAP_SOURCE;
+ else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/"))
+ return SPA_BT_PROFILE_BAP_SINK;
+ else
+ return SPA_BT_PROFILE_NULL;
+}
+
+static bool is_media_codec_enabled(struct spa_bt_monitor *monitor, const struct media_codec *codec)
+{
+ return spa_dict_lookup(&monitor->enabled_codecs, codec->name) != NULL;
+}
+
+static bool codec_has_direction(const struct media_codec *codec, enum spa_bt_media_direction direction)
+{
+ switch (direction) {
+ case SPA_BT_MEDIA_SOURCE:
+ return codec->encode;
+ case SPA_BT_MEDIA_SINK:
+ return codec->decode;
+ default:
+ spa_assert_not_reached();
+ }
+}
+
+static bool endpoint_should_be_registered(struct spa_bt_monitor *monitor,
+ const struct media_codec *codec,
+ enum spa_bt_media_direction direction)
+{
+ /* Codecs with fill_caps == NULL share endpoint with another codec,
+ * and don't have their own endpoint
+ */
+ return is_media_codec_enabled(monitor, codec) &&
+ codec_has_direction(codec, direction) &&
+ codec->fill_caps;
+}
+
+static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ const char *path;
+ uint8_t *cap, config[A2DP_MAX_CAPS_SIZE];
+ uint8_t *pconf = (uint8_t *) config;
+ DBusMessage *r;
+ DBusError err;
+ int size, res;
+ const struct media_codec *codec;
+ bool sink;
+
+ dbus_error_init(&err);
+
+ path = dbus_message_get_path(m);
+
+ if (!dbus_message_get_args(m, &err, DBUS_TYPE_ARRAY,
+ DBUS_TYPE_BYTE, &cap, &size, DBUS_TYPE_INVALID)) {
+ spa_log_error(monitor->log, "Endpoint SelectConfiguration(): %s", err.message);
+ dbus_error_free(&err);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ spa_log_info(monitor->log, "%p: %s select conf %d", monitor, path, size);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, cap, (size_t)size);
+
+ /* For codecs sharing the same endpoint, BlueZ-initiated connections
+ * always pick the default one. The session manager will
+ * switch the codec to a saved value after connection, so this generally
+ * does not matter.
+ */
+ codec = media_endpoint_to_codec(monitor, path, &sink, NULL);
+ spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+
+ if (codec != NULL)
+ /* FIXME: We can't determine which device the SelectConfiguration()
+ * call is associated with, therefore device settings are not passed.
+ * This causes inconsistency with SelectConfiguration() triggered
+ * by codec switching.
+ */
+ res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, cap, size, &monitor->default_audio_info,
+ &monitor->global_settings, config);
+ else
+ res = -ENOTSUP;
+
+ if (res < 0 || res != size) {
+ spa_log_error(monitor->log, "can't select config: %d (%s)",
+ res, spa_strerror(res));
+ if ((r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments",
+ "Unable to select configuration")) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ goto exit_send;
+ }
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, pconf, (size_t)size);
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_ARRAY,
+ DBUS_TYPE_BYTE, &pconf, size, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+exit_send:
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant);
+static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const char* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size);
+static struct spa_bt_remote_endpoint *remote_endpoint_find(struct spa_bt_monitor *monitor, const char *path);
+
+static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ const char *path;
+ DBusMessageIter args, props, iter;
+ DBusMessage *r = NULL;
+ int res;
+ const struct media_codec *codec;
+ bool sink;
+ const char *err_msg = "Unknown error";
+
+ const char *endpoint_path = NULL;
+ uint8_t caps[A2DP_MAX_CAPS_SIZE];
+ uint8_t config[A2DP_MAX_CAPS_SIZE];
+ int caps_size = 0;
+ int conf_size;
+ DBusMessageIter dict;
+ struct bap_endpoint_qos endpoint_qos;
+
+ spa_zero(endpoint_qos);
+
+ if (!dbus_message_iter_init(m, &args) || !spa_streq(dbus_message_get_signature(m), "a{sv}")) {
+ spa_log_error(monitor->log, "Invalid signature for method SelectProperties()");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_recurse(&args, &props);
+ if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY)
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ path = dbus_message_get_path(m);
+
+ /* TODO: for codecs with shared endpoint, this currently always picks the default
+ * one. However, currently we don't have BAP codecs with shared endpoint, so
+ * this does not matter, but in case they are needed later we should pick the
+ * right one here.
+ */
+ codec = media_endpoint_to_codec(monitor, path, &sink, NULL);
+ spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+ if (!codec) {
+ spa_log_error(monitor->log, "Unsupported codec");
+ err_msg = "Unsupported codec";
+ goto error;
+ }
+
+ /* Parse transport properties */
+ while (dbus_message_iter_get_arg_type(&props) == DBUS_TYPE_DICT_ENTRY) {
+ const char *key;
+ DBusMessageIter value, entry;
+ int type;
+
+ dbus_message_iter_recurse(&props, &entry);
+ dbus_message_iter_get_basic(&entry, &key);
+
+ dbus_message_iter_next(&entry);
+ dbus_message_iter_recurse(&entry, &value);
+
+ type = dbus_message_iter_get_arg_type(&value);
+
+ if (spa_streq(key, "Capabilities")) {
+ DBusMessageIter array;
+ uint8_t *buf;
+
+ if (type != DBUS_TYPE_ARRAY) {
+ spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type);
+ goto error_invalid;
+ }
+
+ dbus_message_iter_recurse(&value, &array);
+ type = dbus_message_iter_get_arg_type(&array);
+ if (type != DBUS_TYPE_BYTE) {
+ spa_log_error(monitor->log, "%s is an array of wrong type %c", key, (char)type);
+ goto error_invalid;
+ }
+
+ dbus_message_iter_get_fixed_array(&array, &buf, &caps_size);
+ if (caps_size > (int)sizeof(caps)) {
+ spa_log_error(monitor->log, "%s size:%d too large", key, (int)caps_size);
+ goto error_invalid;
+ }
+ memcpy(caps, buf, caps_size);
+
+ spa_log_info(monitor->log, "%p: %s %s size:%d", monitor, path, key, caps_size);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', caps, (size_t)caps_size);
+ } else if (spa_streq(key, "Endpoint")) {
+ if (type != DBUS_TYPE_OBJECT_PATH) {
+ spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type);
+ goto error_invalid;
+ }
+
+ dbus_message_iter_get_basic(&value, &endpoint_path);
+
+ spa_log_info(monitor->log, "%p: %s %s %s", monitor, path, key, endpoint_path);
+ } else if (type == DBUS_TYPE_BYTE) {
+ uint8_t v;
+ dbus_message_iter_get_basic(&value, &v);
+
+ spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+ if (spa_streq(key, "Framing"))
+ endpoint_qos.framing = v;
+ else if (spa_streq(key, "PHY"))
+ endpoint_qos.phy = v;
+ else
+ spa_log_info(monitor->log, "Unknown property %s", key);
+ } else if (type == DBUS_TYPE_UINT16) {
+ dbus_uint16_t v;
+ dbus_message_iter_get_basic(&value, &v);
+
+ spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+ if (spa_streq(key, "Latency"))
+ endpoint_qos.latency = v;
+ else
+ spa_log_info(monitor->log, "Unknown property %s", key);
+ } else if (type == DBUS_TYPE_UINT32) {
+ dbus_uint32_t v;
+ dbus_message_iter_get_basic(&value, &v);
+
+ spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v);
+
+ if (spa_streq(key, "MinimumDelay"))
+ endpoint_qos.delay_min = v;
+ else if (spa_streq(key, "MaximumDelay"))
+ endpoint_qos.delay_max = v;
+ else if (spa_streq(key, "PreferredMinimumDelay"))
+ endpoint_qos.preferred_delay_min = v;
+ else if (spa_streq(key, "PreferredMaximumDelay"))
+ endpoint_qos.preferred_delay_max = v;
+ else
+ spa_log_info(monitor->log, "Unknown property %s", key);
+ } else {
+ spa_log_info(monitor->log, "Unknown property %s", key);
+ }
+
+ dbus_message_iter_next(&props);
+ }
+
+ if (codec->bap) {
+ struct spa_bt_remote_endpoint *ep;
+
+ ep = remote_endpoint_find(monitor, endpoint_path);
+ if (!ep) {
+ spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", endpoint_path);
+ goto error_invalid;
+ }
+
+ /* Call of SelectProperties means that local device acts as an initiator
+ * and therefor remote endpoint is an acceptor
+ */
+ ep->acceptor = true;
+ }
+
+ /* TODO: determine which device the SelectConfiguration() call is associated
+ * with; it's known here based on the remote endpoint.
+ */
+ conf_size = codec->select_config(codec, 0, caps, caps_size, &monitor->default_audio_info, NULL, config);
+ if (conf_size < 0) {
+ spa_log_error(monitor->log, "can't select config: %d (%s)",
+ conf_size, spa_strerror(conf_size));
+ goto error_invalid;
+ }
+ spa_log_info(monitor->log, "%p: selected conf %d", monitor, conf_size);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', (uint8_t *)config, (size_t)conf_size);
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ dbus_message_iter_init_append(r, &iter);
+
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
+ DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
+ DBUS_TYPE_STRING_AS_STRING
+ DBUS_TYPE_VARIANT_AS_STRING
+ DBUS_DICT_ENTRY_END_CHAR_AS_STRING,
+ &dict);
+ append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, &config, conf_size);
+
+ if (codec->get_qos) {
+ struct bap_codec_qos qos;
+ dbus_bool_t framing;
+ const char *phy_str;
+
+ spa_zero(qos);
+
+ res = codec->get_qos(codec, config, conf_size, &endpoint_qos, &qos);
+ if (res < 0) {
+ spa_log_error(monitor->log, "can't select QOS config: %d (%s)",
+ res, spa_strerror(res));
+ goto error_invalid;
+ }
+
+ append_basic_variant_dict_entry(&dict, "Interval", DBUS_TYPE_UINT32, "u", &qos.interval);
+ framing = (qos.framing ? TRUE : FALSE);
+ append_basic_variant_dict_entry(&dict, "Framing", DBUS_TYPE_BOOLEAN, "b", &framing);
+ if (qos.phy == 0x1)
+ phy_str = "1M";
+ else if (qos.phy == 0x2)
+ phy_str = "2M";
+ else
+ spa_assert_not_reached();
+ append_basic_variant_dict_entry(&dict, "PHY", DBUS_TYPE_STRING, "s", &phy_str);
+ append_basic_variant_dict_entry(&dict, "SDU", DBUS_TYPE_UINT16, "q", &qos.sdu);
+ append_basic_variant_dict_entry(&dict, "Retransmissions", DBUS_TYPE_BYTE, "y", &qos.retransmission);
+ append_basic_variant_dict_entry(&dict, "Latency", DBUS_TYPE_UINT16, "q", &qos.latency);
+ append_basic_variant_dict_entry(&dict, "Delay", DBUS_TYPE_UINT32, "u", &qos.delay);
+ append_basic_variant_dict_entry(&dict, "TargetLatency", DBUS_TYPE_BYTE, "y", &qos.target_latency);
+ }
+
+ dbus_message_iter_close_container(&iter, &dict);
+
+ if (r) {
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+error_invalid:
+ err_msg = "Invalid property";
+ goto error;
+
+error:
+ if (r)
+ dbus_message_unref(r);
+ if ((r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", err_msg)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL)) {
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static struct spa_bt_adapter *adapter_find(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_adapter *d;
+ spa_list_for_each(d, &monitor->adapter_list, link)
+ if (spa_streq(d->path, path))
+ return d;
+ return NULL;
+}
+
+static bool check_iter_signature(DBusMessageIter *it, const char *sig)
+{
+ char *v;
+ bool res;
+ v = dbus_message_iter_get_signature(it);
+ res = spa_streq(v, sig);
+ dbus_free(v);
+ return res;
+}
+
+static int parse_modalias(const char *modalias, uint16_t *source, uint16_t *vendor,
+ uint16_t *product, uint16_t *version)
+{
+ char *pos;
+ unsigned int src, i, j, k;
+
+ if (spa_strstartswith(modalias, "bluetooth:"))
+ src = SOURCE_ID_BLUETOOTH;
+ else if (spa_strstartswith(modalias, "usb:"))
+ src = SOURCE_ID_USB;
+ else
+ return -EINVAL;
+
+ pos = strchr(modalias, ':');
+ if (pos == NULL)
+ return -EINVAL;
+
+ if (sscanf(pos + 1, "v%04Xp%04Xd%04X", &i, &j, &k) != 3)
+ return -EINVAL;
+
+ /* Ignore BlueZ placeholder value */
+ if (src == SOURCE_ID_USB && i == 0x1d6b && j == 0x0246)
+ return -ENXIO;
+
+ *source = src;
+ *vendor = i;
+ *product = j;
+ *version = k;
+
+ return 0;
+}
+
+static int adapter_update_props(struct spa_bt_adapter *adapter,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ struct spa_bt_monitor *monitor = adapter->monitor;
+
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+ int type;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ type = dbus_message_iter_get_arg_type(&it[1]);
+
+ if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) {
+ const char *value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "adapter %p: %s=%s", adapter, key, value);
+
+ if (spa_streq(key, "Alias")) {
+ free(adapter->alias);
+ adapter->alias = strdup(value);
+ }
+ else if (spa_streq(key, "Name")) {
+ free(adapter->name);
+ adapter->name = strdup(value);
+ }
+ else if (spa_streq(key, "Address")) {
+ free(adapter->address);
+ adapter->address = strdup(value);
+ }
+ else if (spa_streq(key, "Modalias")) {
+ int ret;
+ ret = parse_modalias(value, &adapter->source_id, &adapter->vendor_id,
+ &adapter->product_id, &adapter->version_id);
+ if (ret < 0)
+ spa_log_debug(monitor->log, "adapter %p: %s=%s ignored: %s",
+ adapter, key, value, spa_strerror(ret));
+ }
+ }
+ else if (type == DBUS_TYPE_UINT32) {
+ uint32_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "adapter %p: %s=%d", adapter, key, value);
+
+ if (spa_streq(key, "Class"))
+ adapter->bluetooth_class = value;
+
+ }
+ else if (type == DBUS_TYPE_BOOLEAN) {
+ int value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "adapter %p: %s=%d", adapter, key, value);
+
+ if (spa_streq(key, "Powered")) {
+ adapter->powered = value;
+ }
+ }
+ else if (spa_streq(key, "UUIDs")) {
+ DBusMessageIter iter;
+
+ if (!check_iter_signature(&it[1], "as"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+
+ while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+ const char *uuid;
+ enum spa_bt_profile profile;
+
+ dbus_message_iter_get_basic(&iter, &uuid);
+
+ profile = spa_bt_profile_from_uuid(uuid);
+
+ if (profile && (adapter->profiles & profile) == 0) {
+ spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, uuid);
+ adapter->profiles |= profile;
+ } else if (strcasecmp(uuid, SPA_BT_UUID_PACS) == 0 &&
+ (adapter->profiles & SPA_BT_PROFILE_BAP_SINK) == 0) {
+ spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_SINK);
+ adapter->profiles |= SPA_BT_PROFILE_BAP_SINK;
+ spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_SOURCE);
+ adapter->profiles |= SPA_BT_PROFILE_BAP_SOURCE;
+ }
+ dbus_message_iter_next(&iter);
+ }
+ }
+ else
+ spa_log_debug(monitor->log, "adapter %p: unhandled key %s", adapter, key);
+
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static void adapter_update_devices(struct spa_bt_adapter *adapter)
+{
+ struct spa_bt_monitor *monitor = adapter->monitor;
+ struct spa_bt_device *device;
+
+ /*
+ * Update devices when new adapter appears.
+ * Devices may appear on DBus before or after the adapter does.
+ */
+
+ spa_list_for_each(device, &monitor->device_list, link) {
+ if (device->adapter == NULL && spa_streq(device->adapter_path, adapter->path))
+ device->adapter = adapter;
+ }
+}
+
+static void adapter_register_player(struct spa_bt_adapter *adapter)
+{
+ if (adapter->player_registered || !adapter->monitor->dummy_avrcp_player)
+ return;
+
+ if (spa_bt_player_register(adapter->dummy_player, adapter->path) == 0)
+ adapter->player_registered = true;
+}
+
+static int adapter_init_bus_type(struct spa_bt_monitor *monitor, struct spa_bt_adapter *d)
+{
+ char path[1024], buf[1024];
+ const char *str;
+ ssize_t res = -EINVAL;
+
+ d->bus_type = BUS_TYPE_OTHER;
+
+ str = strrchr(d->path, '/'); /* hciXX */
+ if (str == NULL)
+ return -ENOENT;
+
+ snprintf(path, sizeof(path), "/sys/class/bluetooth/%s/device/subsystem", str);
+ if ((res = readlink(path, buf, sizeof(buf)-1)) < 0)
+ return -errno;
+ buf[res] = '\0';
+
+ str = strrchr(buf, '/');
+ if (str && spa_streq(str, "/usb"))
+ d->bus_type = BUS_TYPE_USB;
+ return 0;
+}
+
+static int adapter_init_modalias(struct spa_bt_monitor *monitor, struct spa_bt_adapter *d)
+{
+ char path[1024];
+ FILE *f = NULL;
+ int vendor_id, product_id;
+ const char *str;
+ int res = -EINVAL;
+
+ /* Lookup vendor/product id for the device, if present */
+ str = strrchr(d->path, '/'); /* hciXX */
+ if (str == NULL)
+ goto fail;
+ snprintf(path, sizeof(path), "/sys/class/bluetooth/%s/device/modalias", str);
+ if ((f = fopen(path, "rbe")) == NULL) {
+ res = -errno;
+ goto fail;
+ }
+ if (fscanf(f, "usb:v%04Xp%04X", &vendor_id, &product_id) != 2)
+ goto fail;
+ d->source_id = SOURCE_ID_USB;
+ d->vendor_id = vendor_id;
+ d->product_id = product_id;
+ fclose(f);
+
+ spa_log_debug(monitor->log, "adapter %p: usb vendor:%04x product:%04x",
+ d, vendor_id, product_id);
+ return 0;
+
+fail:
+ if (f)
+ fclose(f);
+ return res;
+}
+
+static struct spa_bt_adapter *adapter_create(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_adapter *d;
+
+ d = calloc(1, sizeof(struct spa_bt_adapter));
+ if (d == NULL)
+ return NULL;
+
+ d->dummy_player = spa_bt_player_new(monitor->conn, monitor->log);
+ if (d->dummy_player == NULL) {
+ free(d);
+ return NULL;
+ }
+
+ d->monitor = monitor;
+ d->path = strdup(path);
+
+ spa_list_prepend(&monitor->adapter_list, &d->link);
+
+ adapter_init_bus_type(monitor, d);
+ adapter_init_modalias(monitor, d);
+
+ return d;
+}
+
+static void device_free(struct spa_bt_device *device);
+
+static void adapter_free(struct spa_bt_adapter *adapter)
+{
+ struct spa_bt_monitor *monitor = adapter->monitor;
+ struct spa_bt_device *d, *td;
+
+ spa_log_debug(monitor->log, "%p", adapter);
+
+ /* Devices should be destroyed before their assigned adapter */
+ spa_list_for_each_safe(d, td, &monitor->device_list, link)
+ if (d->adapter == adapter)
+ device_free(d);
+
+ spa_bt_player_destroy(adapter->dummy_player);
+
+ spa_list_remove(&adapter->link);
+ free(adapter->alias);
+ free(adapter->name);
+ free(adapter->address);
+ free(adapter->path);
+ free(adapter);
+}
+
+static uint32_t adapter_connectable_profiles(struct spa_bt_adapter *adapter)
+{
+ const uint32_t profiles = adapter->profiles;
+ uint32_t mask = 0;
+
+ if (profiles & SPA_BT_PROFILE_A2DP_SINK)
+ mask |= SPA_BT_PROFILE_A2DP_SOURCE;
+ if (profiles & SPA_BT_PROFILE_A2DP_SOURCE)
+ mask |= SPA_BT_PROFILE_A2DP_SINK;
+
+ if (profiles & SPA_BT_PROFILE_BAP_SINK)
+ mask |= SPA_BT_PROFILE_BAP_SOURCE;
+ if (profiles & SPA_BT_PROFILE_BAP_SOURCE)
+ mask |= SPA_BT_PROFILE_BAP_SINK;
+
+ if (profiles & SPA_BT_PROFILE_HSP_AG)
+ mask |= SPA_BT_PROFILE_HSP_HS;
+ if (profiles & SPA_BT_PROFILE_HSP_HS)
+ mask |= SPA_BT_PROFILE_HSP_AG;
+
+ if (profiles & SPA_BT_PROFILE_HFP_AG)
+ mask |= SPA_BT_PROFILE_HFP_HF;
+ if (profiles & SPA_BT_PROFILE_HFP_HF)
+ mask |= SPA_BT_PROFILE_HFP_AG;
+
+ return mask;
+}
+
+struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_device *d;
+ spa_list_for_each(d, &monitor->device_list, link)
+ if (spa_streq(d->path, path))
+ return d;
+ return NULL;
+}
+
+struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monitor, const char *remote_address, const char *local_address)
+{
+ struct spa_bt_device *d;
+ spa_list_for_each(d, &monitor->device_list, link)
+ if (spa_streq(d->address, remote_address) && spa_streq(d->adapter->address, local_address))
+ return d;
+ return NULL;
+}
+
+void spa_bt_device_update_last_bluez_action_time(struct spa_bt_device *device)
+{
+ struct timespec ts;
+ spa_system_clock_gettime(device->monitor->main_system, CLOCK_MONOTONIC, &ts);
+ device->last_bluez_action_time = SPA_TIMESPEC_TO_NSEC(&ts);
+}
+
+static struct spa_bt_device *device_create(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_device *d;
+
+ d = calloc(1, sizeof(struct spa_bt_device));
+ if (d == NULL)
+ return NULL;
+
+ d->id = monitor->id++;
+ d->monitor = monitor;
+ d->path = strdup(path);
+ d->battery_path = battery_get_name(d->path);
+ d->reconnect_profiles = DEFAULT_RECONNECT_PROFILES;
+ d->hw_volume_profiles = DEFAULT_HW_VOLUME_PROFILES;
+
+ spa_list_init(&d->remote_endpoint_list);
+ spa_list_init(&d->transport_list);
+ spa_list_init(&d->codec_switch_list);
+
+ spa_hook_list_init(&d->listener_list);
+
+ spa_list_prepend(&monitor->device_list, &d->link);
+
+ spa_bt_device_update_last_bluez_action_time(d);
+
+ return d;
+}
+
+static int device_stop_timer(struct spa_bt_device *device);
+
+static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw);
+
+static void device_clear_sub(struct spa_bt_device *device)
+{
+ battery_remove(device);
+ spa_bt_device_release_transports(device);
+}
+
+static void device_free(struct spa_bt_device *device)
+{
+ struct spa_bt_remote_endpoint *ep, *tep;
+ struct spa_bt_media_codec_switch *sw;
+ struct spa_bt_transport *t, *tt;
+ struct spa_bt_monitor *monitor = device->monitor;
+
+ spa_log_debug(monitor->log, "%p", device);
+
+ spa_bt_device_emit_destroy(device);
+
+ device_clear_sub(device);
+ device_stop_timer(device);
+
+ if (device->added) {
+ spa_device_emit_object_info(&monitor->hooks, device->id, NULL);
+ }
+
+ spa_list_for_each_safe(ep, tep, &device->remote_endpoint_list, device_link) {
+ if (ep->device == device) {
+ spa_list_remove(&ep->device_link);
+ ep->device = NULL;
+ }
+ }
+
+ spa_list_for_each_safe(t, tt, &device->transport_list, device_link) {
+ if (t->device == device) {
+ spa_list_remove(&t->device_link);
+ t->device = NULL;
+ }
+ }
+
+ spa_list_consume(sw, &device->codec_switch_list, device_link)
+ media_codec_switch_free(sw);
+
+ spa_list_remove(&device->link);
+ free(device->path);
+ free(device->alias);
+ free(device->address);
+ free(device->adapter_path);
+ free(device->battery_path);
+ free(device->name);
+ free(device->icon);
+ free(device);
+}
+
+int spa_bt_format_vendor_product_id(uint16_t source_id, uint16_t vendor_id, uint16_t product_id,
+ char *vendor_str, int vendor_str_size, char *product_str, int product_str_size)
+{
+ char *source_str;
+
+ switch (source_id) {
+ case SOURCE_ID_USB:
+ source_str = "usb";
+ break;
+ case SOURCE_ID_BLUETOOTH:
+ source_str = "bluetooth";
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ spa_scnprintf(vendor_str, vendor_str_size, "%s:%04x", source_str, (unsigned int)vendor_id);
+ spa_scnprintf(product_str, product_str_size, "%04x", (unsigned int)product_id);
+ return 0;
+}
+
+static void emit_device_info(struct spa_bt_monitor *monitor,
+ struct spa_bt_device *device, bool with_connection)
+{
+ struct spa_device_object_info info;
+ char dev[32], name[128], class[16], vendor_id[64], product_id[64], product_id_tot[67];
+ struct spa_dict_item items[23];
+ uint32_t n_items = 0;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Device;
+ info.factory_name = SPA_NAME_API_BLUEZ5_DEVICE;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.flags = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ snprintf(name, sizeof(name), "bluez_card.%s", device->address);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, name);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_DESCRIPTION, device->alias);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ALIAS, device->name);
+ if (spa_bt_format_vendor_product_id(
+ device->source_id, device->vendor_id, device->product_id,
+ vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) {
+ snprintf(product_id_tot, sizeof(product_id_tot), "0x%s", product_id);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, vendor_id);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, product_id_tot);
+ }
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_FORM_FACTOR,
+ spa_bt_form_factor_name(
+ spa_bt_form_factor_from_class(device->bluetooth_class)));
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_STRING, device->address);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, device->icon);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, device->path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address);
+ snprintf(dev, sizeof(dev), "pointer:%p", device);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_DEVICE, dev);
+ snprintf(class, sizeof(class), "0x%06x", device->bluetooth_class);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class);
+
+ if (with_connection) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CONNECTION,
+ device->connected ? "connected": "disconnected");
+ }
+
+ info.props = &SPA_DICT_INIT(items, n_items);
+ spa_device_emit_object_info(&monitor->hooks, device->id, &info);
+}
+
+static int device_connected_old(struct spa_bt_monitor *monitor,
+ struct spa_bt_device *device, int connected)
+{
+
+ if (connected == BT_DEVICE_INIT)
+ return 0;
+
+ device->connected = connected;
+
+ if (device->connected) {
+ emit_device_info(monitor, device, false);
+ device->added = true;
+ } else {
+ if (!device->added)
+ return 0;
+
+ device_clear_sub(device);
+ spa_device_emit_object_info(&monitor->hooks, device->id, NULL);
+ device->added = false;
+ }
+
+ return 0;
+}
+
+enum {
+ BT_DEVICE_RECONNECT_INIT = 0,
+ BT_DEVICE_RECONNECT_PROFILE,
+ BT_DEVICE_RECONNECT_STOP
+};
+
+static int device_connected(struct spa_bt_monitor *monitor,
+ struct spa_bt_device *device, int status)
+{
+ bool connected, init = (status == BT_DEVICE_INIT);
+
+ connected = init ? 0 : status;
+
+ if (!init) {
+ device->reconnect_state =
+ connected ? BT_DEVICE_RECONNECT_STOP
+ : BT_DEVICE_RECONNECT_PROFILE;
+ }
+
+ if ((device->connected_profiles != 0) ^ connected) {
+ spa_log_error(monitor->log,
+ "device %p: unexpected call, connected_profiles:%08x connected:%d",
+ device, device->connected_profiles, device->connected);
+ return -EINVAL;
+ }
+
+ if (!monitor->connection_info_supported)
+ return device_connected_old(monitor, device, status);
+
+ if (init) {
+ device->connected = connected;
+ } else {
+ if (!device->added || !(connected ^ device->connected))
+ return 0;
+
+ device->connected = connected;
+ spa_bt_device_emit_connected(device, device->connected);
+
+ if (!device->connected)
+ device_clear_sub(device);
+ }
+
+ emit_device_info(monitor, device, true);
+ device->added = true;
+
+ return 0;
+}
+
+/*
+ * Add profile to device based on bluez actions
+ * (update property UUIDs, trigger profile handlers),
+ * in case UUIDs is empty on signal InterfaceAdded for
+ * org.bluez.Device1. And emit device info if there is
+ * at least 1 profile on device. This should be called
+ * before any device setting accessing.
+ */
+int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile profile)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+
+ if (profile && (device->profiles & profile) == 0) {
+ spa_log_info(monitor->log, "device %p: add new profile %08x", device, profile);
+ device->profiles |= profile;
+ }
+
+ if (!device->added && device->profiles) {
+ device_connected(monitor, device, BT_DEVICE_INIT);
+ if (device->reconnect_state == BT_DEVICE_RECONNECT_INIT)
+ device_start_timer(device);
+ }
+
+ return 0;
+}
+
+
+static int device_try_connect_profile(struct spa_bt_device *device,
+ const char *profile_uuid)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ DBusMessage *m;
+
+ spa_log_info(monitor->log, "device %p %s: profile %s not connected; try ConnectProfile()",
+ device, device->path, profile_uuid);
+
+ /* Call org.bluez.Device1.ConnectProfile() on device, ignoring result */
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ device->path,
+ BLUEZ_DEVICE_INTERFACE,
+ "ConnectProfile");
+ if (m == NULL)
+ return -ENOMEM;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &profile_uuid, DBUS_TYPE_INVALID);
+ if (!dbus_connection_send(monitor->conn, m, NULL)) {
+ dbus_message_unref(m);
+ return -EIO;
+ }
+ dbus_message_unref(m);
+
+ return 0;
+}
+
+static int reconnect_device_profiles(struct spa_bt_device *device)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ struct spa_bt_device *d;
+ uint32_t reconnect = device->profiles
+ & device->reconnect_profiles
+ & (device->connected_profiles ^ device->profiles);
+
+ /* Don't try to connect to same device via multiple adapters */
+ spa_list_for_each(d, &monitor->device_list, link) {
+ if (d != device && spa_streq(d->address, device->address)) {
+ if (d->paired && d->trusted && !d->blocked &&
+ d->reconnect_state == BT_DEVICE_RECONNECT_STOP)
+ reconnect &= ~d->reconnect_profiles;
+ if (d->connected_profiles)
+ reconnect = 0;
+ }
+ }
+
+ /* Connect only profiles the adapter has a counterpart for */
+ if (device->adapter)
+ reconnect &= adapter_connectable_profiles(device->adapter);
+
+ if (!(device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)) {
+ if (reconnect & SPA_BT_PROFILE_HFP_HF) {
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HSP_HS);
+ } else if (reconnect & SPA_BT_PROFILE_HSP_HS) {
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HFP_HF);
+ }
+ } else
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HEADSET_HEAD_UNIT);
+
+ if (!(device->connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) {
+ if (reconnect & SPA_BT_PROFILE_HFP_AG)
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HSP_AG);
+ else if (reconnect & SPA_BT_PROFILE_HSP_AG)
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HFP_AG);
+ } else
+ SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+
+ if (reconnect & SPA_BT_PROFILE_HFP_HF)
+ device_try_connect_profile(device, SPA_BT_UUID_HFP_HF);
+ if (reconnect & SPA_BT_PROFILE_HSP_HS)
+ device_try_connect_profile(device, SPA_BT_UUID_HSP_HS);
+ if (reconnect & SPA_BT_PROFILE_HFP_AG)
+ device_try_connect_profile(device, SPA_BT_UUID_HFP_AG);
+ if (reconnect & SPA_BT_PROFILE_HSP_AG)
+ device_try_connect_profile(device, SPA_BT_UUID_HSP_AG);
+ if (reconnect & SPA_BT_PROFILE_A2DP_SINK)
+ device_try_connect_profile(device, SPA_BT_UUID_A2DP_SINK);
+ if (reconnect & SPA_BT_PROFILE_A2DP_SOURCE)
+ device_try_connect_profile(device, SPA_BT_UUID_A2DP_SOURCE);
+ if (reconnect & SPA_BT_PROFILE_BAP_SINK)
+ device_try_connect_profile(device, SPA_BT_UUID_BAP_SINK);
+ if (reconnect & SPA_BT_PROFILE_BAP_SOURCE)
+ device_try_connect_profile(device, SPA_BT_UUID_BAP_SOURCE);
+
+ return reconnect;
+}
+
+#define DEVICE_RECONNECT_TIMEOUT_SEC 2
+#define DEVICE_PROFILE_TIMEOUT_SEC 6
+
+static void device_timer_event(struct spa_source *source)
+{
+ struct spa_bt_device *device = source->data;
+ struct spa_bt_monitor *monitor = device->monitor;
+ uint64_t exp;
+
+ if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
+ spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+
+ spa_log_debug(monitor->log, "device %p: timeout %08x %08x",
+ device, device->profiles, device->connected_profiles);
+ device_stop_timer(device);
+ if (BT_DEVICE_RECONNECT_STOP != device->reconnect_state) {
+ device->reconnect_state = BT_DEVICE_RECONNECT_STOP;
+ if (device->paired
+ && device->trusted
+ && !device->blocked
+ && device->reconnect_profiles != 0
+ && reconnect_device_profiles(device))
+ {
+ device_start_timer(device);
+ return;
+ }
+ }
+ if (device->connected_profiles)
+ device_connected(device->monitor, device, BT_DEVICE_CONNECTED);
+}
+
+static int device_start_timer(struct spa_bt_device *device)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ struct itimerspec ts;
+
+ spa_log_debug(monitor->log, "device %p: start timer", device);
+ if (device->timer.data == NULL) {
+ device->timer.data = device;
+ device->timer.func = device_timer_event;
+ device->timer.fd = spa_system_timerfd_create(monitor->main_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ device->timer.mask = SPA_IO_IN;
+ device->timer.rmask = 0;
+ spa_loop_add_source(monitor->main_loop, &device->timer);
+ }
+ ts.it_value.tv_sec = device->reconnect_state == BT_DEVICE_RECONNECT_STOP
+ ? DEVICE_PROFILE_TIMEOUT_SEC
+ : DEVICE_RECONNECT_TIMEOUT_SEC;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, device->timer.fd, 0, &ts, NULL);
+ return 0;
+}
+
+static int device_stop_timer(struct spa_bt_device *device)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ struct itimerspec ts;
+
+ if (device->timer.data == NULL)
+ return 0;
+
+ spa_log_debug(monitor->log, "device %p: stop timer", device);
+ spa_loop_remove_source(monitor->main_loop, &device->timer);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, device->timer.fd, 0, &ts, NULL);
+ spa_system_close(monitor->main_system, device->timer.fd);
+ device->timer.data = NULL;
+ return 0;
+}
+
+int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ uint32_t connected_profiles = device->connected_profiles;
+ uint32_t connectable_profiles =
+ device->adapter ? adapter_connectable_profiles(device->adapter) : 0;
+ uint32_t direction_masks[3] = {
+ SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_HEADSET_HEAD_UNIT,
+ SPA_BT_PROFILE_MEDIA_SOURCE,
+ SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY,
+ };
+ bool direction_connected = false;
+ bool all_connected;
+ size_t i;
+
+ if (connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ connected_profiles |= SPA_BT_PROFILE_HEADSET_HEAD_UNIT;
+ if (connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)
+ connected_profiles |= SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY;
+
+ for (i = 0; i < SPA_N_ELEMENTS(direction_masks); ++i) {
+ uint32_t mask = direction_masks[i] & device->profiles & connectable_profiles;
+ if (mask && (connected_profiles & mask) == mask)
+ direction_connected = true;
+ }
+
+ all_connected = (device->profiles & connected_profiles) == device->profiles;
+
+ spa_log_debug(monitor->log, "device %p: profiles %08x %08x connectable:%08x added:%d all:%d dir:%d",
+ device, device->profiles, connected_profiles, connectable_profiles,
+ device->added, all_connected, direction_connected);
+
+ if (connected_profiles == 0 && spa_list_is_empty(&device->codec_switch_list)) {
+ device_stop_timer(device);
+ device_connected(monitor, device, BT_DEVICE_DISCONNECTED);
+ } else if (force || direction_connected || all_connected) {
+ device_stop_timer(device);
+ device_connected(monitor, device, BT_DEVICE_CONNECTED);
+ } else {
+ /* The initial reconnect event has not been triggered,
+ * the connecting is triggered by bluez. */
+ if (device->reconnect_state == BT_DEVICE_RECONNECT_INIT)
+ device->reconnect_state = BT_DEVICE_RECONNECT_PROFILE;
+ device_start_timer(device);
+ }
+ return 0;
+}
+
+static void device_set_connected(struct spa_bt_device *device, int connected)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+
+ if (device->connected && !connected)
+ device->connected_profiles = 0;
+
+ if (connected)
+ spa_bt_device_check_profiles(device, false);
+ else {
+ /* Stop codec switch on disconnect */
+ struct spa_bt_media_codec_switch *sw;
+ spa_list_consume(sw, &device->codec_switch_list, device_link)
+ media_codec_switch_free(sw);
+
+ if (device->reconnect_state != BT_DEVICE_RECONNECT_INIT)
+ device_stop_timer(device);
+ device_connected(monitor, device, BT_DEVICE_DISCONNECTED);
+ }
+}
+
+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;
+ spa_bt_device_check_profiles(device, false);
+ if (device->connected_profiles != prev_connected)
+ spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected);
+ return 0;
+}
+
+static void device_update_hw_volume_profiles(struct spa_bt_device *device)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ uint32_t bt_features = 0;
+
+ if (!monitor->quirks)
+ return;
+
+ if (spa_bt_quirks_get_features(monitor->quirks, device->adapter, device, &bt_features) != 0)
+ return;
+
+ if (!(bt_features & SPA_BT_FEATURE_HW_VOLUME))
+ device->hw_volume_profiles = 0;
+
+ spa_log_debug(monitor->log, "hw-volume-profiles:%08x", (int)device->hw_volume_profiles);
+}
+
+static int device_update_props(struct spa_bt_device *device,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+ int type;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ type = dbus_message_iter_get_arg_type(&it[1]);
+
+ if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) {
+ const char *value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%s", device, key, value);
+
+ if (spa_streq(key, "Alias")) {
+ free(device->alias);
+ device->alias = strdup(value);
+ }
+ else if (spa_streq(key, "Name")) {
+ free(device->name);
+ device->name = strdup(value);
+ }
+ else if (spa_streq(key, "Address")) {
+ free(device->address);
+ device->address = strdup(value);
+ }
+ else if (spa_streq(key, "Adapter")) {
+ free(device->adapter_path);
+ device->adapter_path = strdup(value);
+
+ device->adapter = adapter_find(monitor, value);
+ if (device->adapter == NULL) {
+ spa_log_info(monitor->log, "unknown adapter %s", value);
+ }
+ }
+ else if (spa_streq(key, "Icon")) {
+ free(device->icon);
+ device->icon = strdup(value);
+ }
+ else if (spa_streq(key, "Modalias")) {
+ int ret;
+ ret = parse_modalias(value, &device->source_id, &device->vendor_id,
+ &device->product_id, &device->version_id);
+ if (ret < 0)
+ spa_log_debug(monitor->log, "device %p: %s=%s ignored: %s",
+ device, key, value, spa_strerror(ret));
+ }
+ }
+ else if (type == DBUS_TYPE_UINT32) {
+ uint32_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%08x", device, key, value);
+
+ if (spa_streq(key, "Class"))
+ device->bluetooth_class = value;
+ }
+ else if (type == DBUS_TYPE_UINT16) {
+ uint16_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value);
+
+ if (spa_streq(key, "Appearance"))
+ device->appearance = value;
+ }
+ else if (type == DBUS_TYPE_INT16) {
+ int16_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value);
+
+ if (spa_streq(key, "RSSI"))
+ device->RSSI = value;
+ }
+ else if (type == DBUS_TYPE_BOOLEAN) {
+ int value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value);
+
+ if (spa_streq(key, "Paired")) {
+ device->paired = value;
+ }
+ else if (spa_streq(key, "Trusted")) {
+ device->trusted = value;
+ }
+ else if (spa_streq(key, "Connected")) {
+ device_set_connected(device, value);
+ }
+ else if (spa_streq(key, "Blocked")) {
+ device->blocked = value;
+ }
+ else if (spa_streq(key, "ServicesResolved")) {
+ if (value)
+ spa_bt_device_check_profiles(device, false);
+ }
+ }
+ else if (spa_streq(key, "UUIDs")) {
+ DBusMessageIter iter;
+ uint32_t prev_profiles = device->profiles;
+
+ if (!check_iter_signature(&it[1], "as"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+
+ while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+ const char *uuid;
+ enum spa_bt_profile profile;
+
+ dbus_message_iter_get_basic(&iter, &uuid);
+
+ profile = spa_bt_profile_from_uuid(uuid);
+
+ /* Only add A2DP/BAP profiles if HSP/HFP backed is none.
+ * This allows BT device to connect instantly instead of waiting for
+ * profile timeout, because all available profiles are connected.
+ */
+ if (monitor->backend_selection != BACKEND_NONE || (monitor->backend_selection == BACKEND_NONE &&
+ profile & (SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_MEDIA_SOURCE))) {
+ if (profile && (device->profiles & profile) == 0) {
+ spa_log_debug(monitor->log, "device %p: add UUID=%s", device, uuid);
+ device->profiles |= profile;
+ }
+ }
+ dbus_message_iter_next(&iter);
+ }
+
+ if (device->profiles != prev_profiles)
+ spa_bt_device_emit_profiles_changed(
+ device, prev_profiles, device->connected_profiles);
+ }
+ else
+ spa_log_debug(monitor->log, "device %p: unhandled key %s type %d", device, key, type);
+
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static bool device_props_ready(struct spa_bt_device *device)
+{
+ /*
+ * In some cases, BlueZ device props may be missing part of
+ * the information required when the interface first appears.
+ */
+ return device->adapter && device->address;
+}
+
+bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, bool sink)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ struct spa_bt_remote_endpoint *ep;
+ const struct { enum spa_bluetooth_audio_codec codec; uint32_t mask; } quirks[] = {
+ { SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, SPA_BT_FEATURE_SBC_XQ },
+ { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, SPA_BT_FEATURE_FASTSTREAM },
+ { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_FASTSTREAM },
+ { SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX },
+ { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX },
+ };
+ size_t i;
+
+ if (!is_media_codec_enabled(device->monitor, codec))
+ return false;
+
+ if (!device->adapter->application_registered) {
+ /* Codec switching not supported: only plain SBC allowed */
+ return (codec->codec_id == A2DP_CODEC_SBC && spa_streq(codec->name, "sbc"));
+ }
+
+ /* Check codec quirks */
+ for (i = 0; i < SPA_N_ELEMENTS(quirks); ++i) {
+ uint32_t bt_features;
+
+ if (codec->id != quirks[i].codec)
+ continue;
+ if (monitor->quirks == NULL)
+ break;
+ if (spa_bt_quirks_get_features(monitor->quirks, device->adapter, device, &bt_features) < 0)
+ break;
+ if (!(bt_features & quirks[i].mask))
+ return false;
+ }
+
+ spa_list_for_each(ep, &device->remote_endpoint_list, device_link) {
+ const enum spa_bt_profile profile = spa_bt_profile_from_uuid(ep->uuid);
+ enum spa_bt_profile expected;
+
+ if (codec->bap)
+ expected = sink ? SPA_BT_PROFILE_BAP_SINK : SPA_BT_PROFILE_BAP_SOURCE;
+ else
+ expected = sink ? SPA_BT_PROFILE_A2DP_SINK : SPA_BT_PROFILE_A2DP_SOURCE;
+
+ if (profile != expected)
+ continue;
+
+ if (media_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len,
+ &ep->monitor->default_audio_info, &monitor->global_settings))
+ return true;
+ }
+
+ return false;
+}
+
+const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count, bool sink)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ const struct media_codec **supported_codecs;
+ size_t i, j, size;
+
+ *count = 0;
+
+ size = 8;
+ supported_codecs = malloc(size * sizeof(const struct media_codec *));
+ if (supported_codecs == NULL)
+ return NULL;
+
+ j = 0;
+ for (i = 0; media_codecs[i] != NULL; ++i) {
+ if (spa_bt_device_supports_media_codec(device, media_codecs[i], sink)) {
+ supported_codecs[j] = media_codecs[i];
+ ++j;
+ }
+
+ if (j >= size) {
+ const struct media_codec **p;
+ size = size * 2;
+#ifdef HAVE_REALLOCARRRAY
+ p = reallocarray(supported_codecs, size, sizeof(const struct media_codec *));
+#else
+ p = realloc(supported_codecs, size * sizeof(const struct media_codec *));
+#endif
+ if (p == NULL) {
+ free(supported_codecs);
+ return NULL;
+ }
+ supported_codecs = p;
+ }
+ }
+
+ supported_codecs[j] = NULL;
+ *count = j;
+
+ return supported_codecs;
+}
+
+static struct spa_bt_remote_endpoint *device_remote_endpoint_find(struct spa_bt_device *device, const char *path)
+{
+ struct spa_bt_remote_endpoint *ep;
+ spa_list_for_each(ep, &device->remote_endpoint_list, device_link)
+ if (spa_streq(ep->path, path))
+ return ep;
+ return NULL;
+}
+
+static struct spa_bt_remote_endpoint *remote_endpoint_find(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_remote_endpoint *ep;
+ spa_list_for_each(ep, &monitor->remote_endpoint_list, link)
+ if (spa_streq(ep->path, path))
+ return ep;
+ return NULL;
+}
+
+static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_endpoint,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ struct spa_bt_monitor *monitor = remote_endpoint->monitor;
+
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+ int type;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ type = dbus_message_iter_get_arg_type(&it[1]);
+
+ if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) {
+ const char *value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "remote_endpoint %p: %s=%s", remote_endpoint, key, value);
+
+ if (spa_streq(key, "UUID")) {
+ free(remote_endpoint->uuid);
+ remote_endpoint->uuid = strdup(value);
+ }
+ else if (spa_streq(key, "Device")) {
+ struct spa_bt_device *device;
+ device = spa_bt_device_find(monitor, value);
+ if (device == NULL)
+ goto next;
+ spa_log_debug(monitor->log, "remote_endpoint %p: device -> %p", remote_endpoint, device);
+
+ if (remote_endpoint->device != device) {
+ if (remote_endpoint->device != NULL)
+ spa_list_remove(&remote_endpoint->device_link);
+ remote_endpoint->device = device;
+ if (device != NULL)
+ spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link);
+ }
+ }
+ }
+ else if (type == DBUS_TYPE_BOOLEAN) {
+ int value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, value);
+
+ if (spa_streq(key, "DelayReporting")) {
+ remote_endpoint->delay_reporting = value;
+ }
+ }
+ else if (type == DBUS_TYPE_BYTE) {
+ uint8_t value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "remote_endpoint %p: %s=%02x", remote_endpoint, key, value);
+
+ if (spa_streq(key, "Codec")) {
+ remote_endpoint->codec = value;
+ }
+ }
+ else if (spa_streq(key, "Capabilities")) {
+ 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);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, value, (size_t)len);
+
+ free(remote_endpoint->capabilities);
+ remote_endpoint->capabilities_len = 0;
+
+ remote_endpoint->capabilities = malloc(len);
+ if (remote_endpoint->capabilities) {
+ memcpy(remote_endpoint->capabilities, value, len);
+ remote_endpoint->capabilities_len = len;
+ }
+ }
+ else
+ spa_log_debug(monitor->log, "remote_endpoint %p: unhandled key %s", remote_endpoint, key);
+
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static struct spa_bt_remote_endpoint *remote_endpoint_create(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_remote_endpoint *ep;
+
+ ep = calloc(1, sizeof(struct spa_bt_remote_endpoint));
+ if (ep == NULL)
+ return NULL;
+
+ ep->monitor = monitor;
+ ep->path = strdup(path);
+
+ spa_list_prepend(&monitor->remote_endpoint_list, &ep->link);
+
+ return ep;
+}
+
+static void remote_endpoint_free(struct spa_bt_remote_endpoint *remote_endpoint)
+{
+ struct spa_bt_monitor *monitor = remote_endpoint->monitor;
+
+ spa_log_debug(monitor->log, "remote endpoint %p: free %s",
+ remote_endpoint, remote_endpoint->path);
+
+ if (remote_endpoint->device)
+ spa_list_remove(&remote_endpoint->device_link);
+
+ spa_list_remove(&remote_endpoint->link);
+ free(remote_endpoint->path);
+ free(remote_endpoint->uuid);
+ free(remote_endpoint->capabilities);
+ free(remote_endpoint);
+}
+
+struct spa_bt_transport *spa_bt_transport_find(struct spa_bt_monitor *monitor, const char *path)
+{
+ struct spa_bt_transport *t;
+ spa_list_for_each(t, &monitor->transport_list, link)
+ if (spa_streq(t->path, path))
+ return t;
+ return NULL;
+}
+
+struct spa_bt_transport *spa_bt_transport_find_full(struct spa_bt_monitor *monitor,
+ bool (*callback) (struct spa_bt_transport *t, const void *data),
+ const void *data)
+{
+ struct spa_bt_transport *t;
+
+ spa_list_for_each(t, &monitor->transport_list, link)
+ if (callback(t, data) == true)
+ return t;
+ return NULL;
+}
+
+
+struct spa_bt_transport *spa_bt_transport_create(struct spa_bt_monitor *monitor, char *path, size_t extra)
+{
+ struct spa_bt_transport *t;
+
+ t = calloc(1, sizeof(struct spa_bt_transport) + extra);
+ if (t == NULL)
+ return NULL;
+
+ t->acquire_refcount = 0;
+ t->monitor = monitor;
+ t->path = path;
+ t->fd = -1;
+ t->sco_io = NULL;
+ t->delay = SPA_BT_UNKNOWN_DELAY;
+ t->user_data = SPA_PTROFF(t, sizeof(struct spa_bt_transport), void);
+ spa_hook_list_init(&t->listener_list);
+ spa_list_init(&t->bap_transport_linked);
+
+ spa_list_append(&monitor->transport_list, &t->link);
+
+ return t;
+}
+
+bool spa_bt_transport_volume_enabled(struct spa_bt_transport *transport)
+{
+ return transport->device != NULL
+ && (transport->device->hw_volume_profiles & transport->profile);
+}
+
+static void transport_sync_volume(struct spa_bt_transport *transport)
+{
+ if (!spa_bt_transport_volume_enabled(transport))
+ return;
+
+ for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i)
+ spa_bt_transport_set_volume(transport, i, transport->volumes[i].volume);
+ spa_bt_transport_emit_volume_changed(transport);
+}
+
+void spa_bt_transport_set_state(struct spa_bt_transport *transport, enum spa_bt_transport_state state)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ enum spa_bt_transport_state old = transport->state;
+
+ if (old != state) {
+ transport->state = state;
+ spa_log_debug(monitor->log, "transport %p: %s state changed %d -> %d",
+ transport, transport->path, old, state);
+ spa_bt_transport_emit_state_changed(transport, old, state);
+ if (state >= SPA_BT_TRANSPORT_STATE_PENDING && old < SPA_BT_TRANSPORT_STATE_PENDING)
+ transport_sync_volume(transport);
+ }
+}
+
+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);
+
+ spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_IDLE);
+
+ spa_bt_transport_keepalive(transport, false);
+
+ spa_bt_transport_emit_destroy(transport);
+
+ spa_bt_transport_stop_volume_timer(transport);
+ spa_bt_transport_stop_release_timer(transport);
+
+ if (transport->sco_io) {
+ spa_bt_sco_io_destroy(transport->sco_io);
+ transport->sco_io = NULL;
+ }
+
+ spa_bt_transport_destroy(transport);
+
+ if (transport->fd >= 0) {
+ spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
+
+ shutdown(transport->fd, SHUT_RDWR);
+ close(transport->fd);
+ transport->fd = -1;
+ }
+
+ spa_list_remove(&transport->link);
+ if (transport->device) {
+ prev_connected = transport->device->connected_profiles;
+ transport->device->connected_profiles &= ~transport->profile;
+ spa_list_remove(&transport->device_link);
+ }
+
+ if (device && device->connected_profiles != prev_connected)
+ spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected);
+
+ spa_list_remove(&transport->bap_transport_linked);
+
+ free(transport->endpoint_path);
+ free(transport->path);
+ free(transport);
+}
+
+int spa_bt_transport_keepalive(struct spa_bt_transport *t, bool keepalive)
+{
+ if (keepalive) {
+ t->keepalive = true;
+ return 0;
+ }
+
+ t->keepalive = false;
+
+ if (t->acquire_refcount == 0 && t->acquired) {
+ t->acquire_refcount = 1;
+ return spa_bt_transport_release(t);
+ }
+
+ return 0;
+}
+
+int spa_bt_transport_acquire(struct spa_bt_transport *transport, bool optional)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ int res;
+
+ if (transport->acquire_refcount > 0) {
+ spa_log_debug(monitor->log, "transport %p: incref %s", transport, transport->path);
+ transport->acquire_refcount += 1;
+ return 0;
+ }
+ spa_assert(transport->acquire_refcount == 0);
+
+ if (!transport->acquired)
+ res = spa_bt_transport_impl(transport, acquire, 0, optional);
+ else
+ res = 0;
+
+ if (res >= 0) {
+ transport->acquire_refcount = 1;
+ transport->acquired = true;
+ }
+
+ return res;
+}
+
+int spa_bt_transport_release(struct spa_bt_transport *transport)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ int res;
+
+ if (transport->acquire_refcount > 1) {
+ spa_log_debug(monitor->log, "transport %p: decref %s", transport, transport->path);
+ transport->acquire_refcount -= 1;
+ return 0;
+ }
+ else if (transport->acquire_refcount == 0) {
+ spa_log_info(monitor->log, "transport %s already released", transport->path);
+ return 0;
+ }
+ spa_assert(transport->acquire_refcount == 1);
+ spa_assert(transport->acquired);
+
+ if (SPA_BT_TRANSPORT_IS_SCO(transport)) {
+ /* Postpone SCO transport releases, since we might need it again soon */
+ res = spa_bt_transport_start_release_timer(transport);
+ } else if (transport->keepalive) {
+ res = 0;
+ transport->acquire_refcount = 0;
+ spa_log_debug(monitor->log, "transport %p: keepalive %s on release",
+ transport, transport->path);
+ } else {
+ res = spa_bt_transport_impl(transport, release, 0);
+ if (res >= 0) {
+ transport->acquire_refcount = 0;
+ transport->acquired = false;
+ }
+ }
+
+ return res;
+}
+
+static int spa_bt_transport_release_now(struct spa_bt_transport *transport)
+{
+ int res;
+
+ if (!transport->acquired)
+ return 0;
+
+ spa_bt_transport_stop_release_timer(transport);
+ res = spa_bt_transport_impl(transport, release, 0);
+ if (res >= 0) {
+ transport->acquire_refcount = 0;
+ transport->acquired = false;
+ }
+
+ return res;
+}
+
+int spa_bt_device_release_transports(struct spa_bt_device *device)
+{
+ struct spa_bt_transport *t;
+ spa_list_for_each(t, &device->transport_list, device_link)
+ spa_bt_transport_release_now(t);
+ return 0;
+}
+
+static int start_timeout_timer(struct spa_bt_monitor *monitor,
+ struct spa_source *timer, spa_source_func_t timer_event,
+ time_t timeout_msec, void *data)
+{
+ struct itimerspec ts;
+ if (timer->data == NULL) {
+ timer->data = data;
+ timer->func = timer_event;
+ timer->fd = spa_system_timerfd_create(
+ monitor->main_system, CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ timer->mask = SPA_IO_IN;
+ timer->rmask = 0;
+ spa_loop_add_source(monitor->main_loop, timer);
+ }
+ ts.it_value.tv_sec = timeout_msec / SPA_MSEC_PER_SEC;
+ ts.it_value.tv_nsec = (timeout_msec % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, timer->fd, 0, &ts, NULL);
+ return 0;
+}
+
+static int stop_timeout_timer(struct spa_bt_monitor *monitor, struct spa_source *timer)
+{
+ struct itimerspec ts;
+
+ if (timer->data == NULL)
+ return 0;
+
+ spa_loop_remove_source(monitor->main_loop, timer);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, timer->fd, 0, &ts, NULL);
+ spa_system_close(monitor->main_system, timer->fd);
+ timer->data = NULL;
+ return 0;
+}
+
+static void spa_bt_transport_release_timer_event(struct spa_source *source)
+{
+ struct spa_bt_transport *transport = source->data;
+ struct spa_bt_monitor *monitor = transport->monitor;
+
+ spa_assert(transport->acquire_refcount >= 1);
+ spa_assert(transport->acquired);
+
+ spa_bt_transport_stop_release_timer(transport);
+
+ if (transport->acquire_refcount == 1) {
+ if (!transport->keepalive) {
+ spa_bt_transport_impl(transport, release, 0);
+ transport->acquired = false;
+ } else {
+ spa_log_debug(monitor->log, "transport %p: keepalive %s on release",
+ transport, transport->path);
+ }
+ } else {
+ spa_log_debug(monitor->log, "transport %p: delayed decref %s", transport, transport->path);
+ }
+ transport->acquire_refcount -= 1;
+}
+
+static int spa_bt_transport_start_release_timer(struct spa_bt_transport *transport)
+{
+ return start_timeout_timer(transport->monitor,
+ &transport->release_timer,
+ spa_bt_transport_release_timer_event,
+ SCO_TRANSPORT_RELEASE_TIMEOUT_MSEC, transport);
+}
+
+static int spa_bt_transport_stop_release_timer(struct spa_bt_transport *transport)
+{
+ return stop_timeout_timer(transport->monitor, &transport->release_timer);
+}
+
+static void spa_bt_transport_volume_changed(struct spa_bt_transport *transport)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ struct spa_bt_transport_volume * t_volume;
+ int volume_id;
+
+ if (transport->profile & SPA_BT_PROFILE_A2DP_SINK)
+ volume_id = SPA_BT_VOLUME_ID_TX;
+ else if (transport->profile & SPA_BT_PROFILE_A2DP_SOURCE)
+ volume_id = SPA_BT_VOLUME_ID_RX;
+ else
+ return;
+
+ t_volume = &transport->volumes[volume_id];
+
+ if (t_volume->hw_volume != t_volume->new_hw_volume) {
+ t_volume->hw_volume = t_volume->new_hw_volume;
+ t_volume->volume = spa_bt_volume_hw_to_linear(t_volume->hw_volume,
+ t_volume->hw_volume_max);
+ spa_log_debug(monitor->log, "transport %p: volume changed %d(%f) ",
+ transport, t_volume->new_hw_volume, t_volume->volume);
+ if (spa_bt_transport_volume_enabled(transport)) {
+ transport->device->a2dp_volume_active[volume_id] = true;
+ spa_bt_transport_emit_volume_changed(transport);
+ }
+ }
+}
+
+static void spa_bt_transport_volume_timer_event(struct spa_source *source)
+{
+ struct spa_bt_transport *transport = source->data;
+ struct spa_bt_monitor *monitor = transport->monitor;
+ uint64_t exp;
+
+ if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
+ spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+
+ spa_bt_transport_volume_changed(transport);
+}
+
+static int spa_bt_transport_start_volume_timer(struct spa_bt_transport *transport)
+{
+ return start_timeout_timer(transport->monitor,
+ &transport->volume_timer,
+ spa_bt_transport_volume_timer_event,
+ TRANSPORT_VOLUME_TIMEOUT_MSEC, transport);
+}
+
+static int spa_bt_transport_stop_volume_timer(struct spa_bt_transport *transport)
+{
+ return stop_timeout_timer(transport->monitor, &transport->volume_timer);
+}
+
+
+int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop)
+{
+ if (t->sco_io == NULL) {
+ t->sco_io = spa_bt_sco_io_create(data_loop,
+ t->fd,
+ t->read_mtu,
+ t->write_mtu);
+ if (t->sco_io == NULL)
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *t)
+{
+ if (t->delay != SPA_BT_UNKNOWN_DELAY)
+ return (int64_t)t->delay * 100 * SPA_NSEC_PER_USEC;
+
+ /* Fallback values when device does not provide information */
+
+ if (t->media_codec == NULL)
+ return 30 * SPA_NSEC_PER_MSEC;
+
+ switch (t->media_codec->id) {
+ case SPA_BLUETOOTH_AUDIO_CODEC_SBC:
+ case SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ:
+ return 200 * SPA_NSEC_PER_MSEC;
+ case SPA_BLUETOOTH_AUDIO_CODEC_MPEG:
+ case SPA_BLUETOOTH_AUDIO_CODEC_AAC:
+ return 200 * SPA_NSEC_PER_MSEC;
+ case SPA_BLUETOOTH_AUDIO_CODEC_APTX:
+ case SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD:
+ return 150 * SPA_NSEC_PER_MSEC;
+ case SPA_BLUETOOTH_AUDIO_CODEC_LDAC:
+ return 175 * SPA_NSEC_PER_MSEC;
+ case SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL:
+ case SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX:
+ case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM:
+ case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX:
+ case SPA_BLUETOOTH_AUDIO_CODEC_LC3:
+ return 40 * SPA_NSEC_PER_MSEC;
+ default:
+ break;
+ };
+ return 150 * SPA_NSEC_PER_MSEC;
+}
+
+static int transport_update_props(struct spa_bt_transport *transport,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+ int type;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ type = dbus_message_iter_get_arg_type(&it[1]);
+
+ if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) {
+ const char *value;
+
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%s", transport, key, value);
+
+ if (spa_streq(key, "UUID")) {
+ switch (spa_bt_profile_from_uuid(value)) {
+ case SPA_BT_PROFILE_A2DP_SOURCE:
+ transport->profile = SPA_BT_PROFILE_A2DP_SINK;
+ break;
+ case SPA_BT_PROFILE_A2DP_SINK:
+ transport->profile = SPA_BT_PROFILE_A2DP_SOURCE;
+ break;
+ case SPA_BT_PROFILE_BAP_SOURCE:
+ transport->profile = SPA_BT_PROFILE_BAP_SINK;
+ break;
+ case SPA_BT_PROFILE_BAP_SINK:
+ transport->profile = SPA_BT_PROFILE_BAP_SOURCE;
+ break;
+ default:
+ spa_log_warn(monitor->log, "unknown profile %s", value);
+ break;
+ }
+ }
+ else if (spa_streq(key, "State")) {
+ spa_bt_transport_set_state(transport, spa_bt_transport_state_from_string(value));
+ }
+ else if (spa_streq(key, "Device")) {
+ struct spa_bt_device *device = spa_bt_device_find(monitor, value);
+ if (transport->device != device) {
+ if (transport->device != NULL)
+ spa_list_remove(&transport->device_link);
+ transport->device = device;
+ if (device != NULL)
+ spa_list_append(&device->transport_list, &transport->device_link);
+ else
+ spa_log_warn(monitor->log, "could not find device %s", value);
+ }
+ }
+ else if (spa_streq(key, "Endpoint")) {
+ struct spa_bt_remote_endpoint *ep = remote_endpoint_find(monitor, value);
+ if (!ep) {
+ spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", value);
+ goto next;
+ }
+
+ // If the remote endpoint is an acceptor this transport is an initiator
+ transport->bap_initiator = ep->acceptor;
+ }
+ }
+ else if (spa_streq(key, "Codec")) {
+ uint8_t value;
+
+ if (type != DBUS_TYPE_BYTE)
+ goto next;
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value);
+
+ transport->codec = value;
+ }
+ else if (spa_streq(key, "Configuration")) {
+ 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, "transport %p: %s=%d", transport, key, len);
+ spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, value, (size_t)len);
+
+ free(transport->configuration);
+ transport->configuration_len = 0;
+
+ transport->configuration = malloc(len);
+ if (transport->configuration) {
+ memcpy(transport->configuration, value, len);
+ transport->configuration_len = len;
+ }
+ }
+ else if (spa_streq(key, "Volume")) {
+ uint16_t value;
+ struct spa_bt_transport_volume * t_volume;
+
+ if (type != DBUS_TYPE_UINT16)
+ goto next;
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%d", transport, key, value);
+
+ if (transport->profile & SPA_BT_PROFILE_A2DP_SINK)
+ 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
+ goto next;
+
+ t_volume->active = true;
+ t_volume->new_hw_volume = value;
+
+ if (transport->profile & SPA_BT_PROFILE_A2DP_SINK)
+ spa_bt_transport_start_volume_timer(transport);
+ else
+ spa_bt_transport_volume_changed(transport);
+ }
+ else if (spa_streq(key, "Delay")) {
+ uint16_t value;
+
+ if (type != DBUS_TYPE_UINT16)
+ goto next;
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value);
+
+ transport->delay = value;
+ spa_bt_transport_emit_delay_changed(transport);
+ }
+ else if (spa_streq(key, "PresentationDelay")) {
+ uint32_t value;
+
+ if (type != DBUS_TYPE_UINT32)
+ goto next;
+ dbus_message_iter_get_basic(&it[1], &value);
+
+ spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value);
+
+ transport->delay = value / 100;
+ spa_bt_transport_emit_delay_changed(transport);
+ }
+ else if (spa_streq(key, "Links")) {
+ DBusMessageIter iter;
+
+ if (!check_iter_signature(&it[1], "ao"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+ while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+ const char *transport_path;
+ struct spa_bt_transport *t;
+
+ dbus_message_iter_get_basic(&iter, &transport_path);
+
+ spa_log_debug(monitor->log, "transport %p: Linked with=%s", transport, transport_path);
+ t = spa_bt_transport_find(monitor, transport_path);
+ if (!t) {
+ spa_log_warn(monitor->log, "Unable to find linked transport");
+ dbus_message_iter_next(&iter);
+ continue;
+ }
+
+ if (spa_list_is_empty(&t->bap_transport_linked))
+ spa_list_append(&transport->bap_transport_linked, &t->bap_transport_linked);
+ else if (spa_list_is_empty(&transport->bap_transport_linked))
+ spa_list_append(&t->bap_transport_linked, &transport->bap_transport_linked);
+
+ dbus_message_iter_next(&iter);
+ }
+ }
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static int transport_set_property_volume(struct spa_bt_transport *transport, uint16_t value)
+{
+ struct spa_bt_monitor *monitor = transport->monitor;
+ DBusMessage *m, *r;
+ DBusMessageIter it[2];
+ DBusError err;
+ const char *interface = BLUEZ_MEDIA_TRANSPORT_INTERFACE;
+ const char *name = "Volume";
+ int res = 0;
+
+ 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, &name);
+ 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]);
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err);
+
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ spa_log_error(monitor->log, "set volume %u failed for transport %s (%s)",
+ value, transport->path, err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR)
+ res = -EIO;
+
+ dbus_message_unref(r);
+
+ spa_log_debug(monitor->log, "transport %p: set volume to %d", transport, value);
+
+ return res;
+}
+
+static int transport_set_volume(void *data, int id, float volume)
+{
+ struct spa_bt_transport *transport = data;
+ struct spa_bt_transport_volume *t_volume = &transport->volumes[id];
+ uint16_t value;
+
+ if (!t_volume->active || !spa_bt_transport_volume_enabled(transport))
+ return -ENOTSUP;
+
+ value = spa_bt_volume_linear_to_hw(volume, 127);
+ t_volume->volume = volume;
+
+ /* AVRCP volume would not applied on remote sink device
+ * if transport is not acquired (idle). */
+ if (transport->fd < 0 && (transport->profile & SPA_BT_PROFILE_A2DP_SINK)) {
+ t_volume->hw_volume = SPA_BT_VOLUME_INVALID;
+ return 0;
+ } else if (t_volume->hw_volume != value) {
+ t_volume->hw_volume = value;
+ spa_bt_transport_stop_volume_timer(transport);
+ transport_set_property_volume(transport, value);
+ }
+ return 0;
+}
+
+static int transport_acquire(void *data, bool optional)
+{
+ struct spa_bt_transport *transport = data;
+ struct spa_bt_monitor *monitor = transport->monitor;
+ DBusMessage *m, *r = NULL;
+ DBusError err;
+ int ret = 0;
+ const char *method = optional ? "TryAcquire" : "Acquire";
+ struct spa_bt_transport *t_linked;
+
+ /* For LE Audio, multiple transport from the same device may share the same
+ * stream (CIS) and group (CIG) but for different direction, e.g. a speaker and
+ * a microphone. In this case they are linked.
+ * If one of them has already been acquired this function should not call Acquire
+ * or TryAcquire but re-use values from the previously acquired transport.
+ */
+ spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) {
+ if (t_linked->acquired && t_linked->device == transport->device) {
+ transport->fd = t_linked->fd;
+ transport->read_mtu = t_linked->read_mtu;
+ transport->write_mtu = t_linked->write_mtu;
+ spa_log_debug(monitor->log, "transport %p: linked transport %s", transport, t_linked->path);
+ goto done;
+ }
+ }
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ transport->path,
+ BLUEZ_MEDIA_TRANSPORT_INTERFACE,
+ method);
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ if (optional && spa_streq(err.name, "org.bluez.Error.NotAvailable")) {
+ spa_log_info(monitor->log, "Failed optional acquire of unavailable transport %s",
+ transport->path);
+ }
+ else {
+ spa_log_error(monitor->log, "Transport %s() failed for transport %s (%s)",
+ method, transport->path, err.message);
+ }
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(monitor->log, "%s returned error: %s", method, dbus_message_get_error_name(r));
+ ret = -EIO;
+ goto finish;
+ }
+
+ if (!dbus_message_get_args(r, &err,
+ DBUS_TYPE_UNIX_FD, &transport->fd,
+ DBUS_TYPE_UINT16, &transport->read_mtu,
+ DBUS_TYPE_UINT16, &transport->write_mtu,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(monitor->log, "Failed to parse %s() reply: %s", method, err.message);
+ dbus_error_free(&err);
+ ret = -EIO;
+ goto finish;
+ }
+done:
+ spa_log_debug(monitor->log, "transport %p: %s %s, fd %d MTU %d:%d", transport, method,
+ transport->path, transport->fd, transport->read_mtu, transport->write_mtu);
+
+ spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_PLAYING);
+
+ transport_sync_volume(transport);
+
+finish:
+ if (r)
+ dbus_message_unref(r);
+ return ret;
+}
+
+static int transport_release(void *data)
+{
+ struct spa_bt_transport *transport = data;
+ struct spa_bt_monitor *monitor = transport->monitor;
+ DBusMessage *m, *r;
+ DBusError err;
+ bool is_idle = (transport->state == SPA_BT_TRANSPORT_STATE_IDLE);
+ struct spa_bt_transport *t_linked;
+ bool linked = false;
+
+ spa_log_debug(monitor->log, "transport %p: Release %s",
+ transport, transport->path);
+
+ spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED);
+
+ /* For LE Audio, multiple transport stream (CIS) can be linked together (CIG).
+ * If they are part of the same device they re-use the same fd, and call to
+ * release should be done for the last one only.
+ */
+ spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) {
+ if (t_linked->acquired && t_linked->device == transport->device) {
+ linked = true;
+ break;
+ }
+ }
+ if (linked) {
+ spa_log_info(monitor->log, "Linked transport %s released", transport->path);
+ transport->fd = -1;
+ return 0;
+ }
+
+ close(transport->fd);
+ transport->fd = -1;
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ transport->path,
+ BLUEZ_MEDIA_TRANSPORT_INTERFACE,
+ "Release");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_error_init(&err);
+
+ r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r != NULL)
+ dbus_message_unref(r);
+
+ if (dbus_error_is_set(&err)) {
+ if (is_idle) {
+ /* XXX: The fd always needs to be closed. However, Release()
+ * XXX: apparently doesn't need to be called on idle transports
+ * XXX: and fails. We call it just to be sure (e.g. in case
+ * XXX: there's a race with updating the property), but tone down the error.
+ */
+ spa_log_debug(monitor->log, "Failed to release idle transport %s: %s",
+ transport->path, err.message);
+ } else {
+ spa_log_error(monitor->log, "Failed to release transport %s: %s",
+ transport->path, err.message);
+ }
+ dbus_error_free(&err);
+ }
+ else
+ spa_log_info(monitor->log, "Transport %s released", transport->path);
+
+ 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,
+};
+
+static void media_codec_switch_reply(DBusPendingCall *pending, void *userdata);
+
+static int media_codec_switch_cmp(const void *a, const void *b);
+
+static struct spa_bt_media_codec_switch *media_codec_switch_cmp_sw; /* global for qsort */
+
+static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout);
+
+static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw);
+
+static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw)
+{
+ char **p;
+
+ media_codec_switch_stop_timer(sw);
+
+ if (sw->pending != NULL) {
+ dbus_pending_call_cancel(sw->pending);
+ dbus_pending_call_unref(sw->pending);
+ }
+
+ if (sw->device != NULL)
+ spa_list_remove(&sw->device_link);
+
+ if (sw->paths != NULL)
+ for (p = sw->paths; *p != NULL; ++p)
+ free(*p);
+
+ free(sw->paths);
+ free(sw->codecs);
+ free(sw);
+}
+
+static void media_codec_switch_next(struct spa_bt_media_codec_switch *sw)
+{
+ spa_assert(*sw->codec_iter != NULL && *sw->path_iter != NULL);
+
+ ++sw->path_iter;
+ if (*sw->path_iter == NULL) {
+ ++sw->codec_iter;
+ sw->path_iter = sw->paths;
+ }
+
+ sw->retries = CODEC_SWITCH_RETRIES;
+}
+
+static bool media_codec_switch_process_current(struct spa_bt_media_codec_switch *sw)
+{
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_transport *t;
+ const struct media_codec *codec;
+ uint8_t config[A2DP_MAX_CAPS_SIZE];
+ enum spa_bt_media_direction direction;
+ char *local_endpoint = NULL;
+ int res, config_size;
+ dbus_bool_t dbus_ret;
+ DBusMessage *m;
+ DBusMessageIter iter, d;
+ int i;
+ bool sink;
+
+ /* Try setting configuration for current codec on current endpoint in list */
+
+ codec = *sw->codec_iter;
+
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: consider codec %s for remote endpoint %s",
+ sw, (*sw->codec_iter)->name, *sw->path_iter);
+
+ ep = device_remote_endpoint_find(sw->device, *sw->path_iter);
+
+ if (ep == NULL || ep->capabilities == NULL || ep->uuid == NULL) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s not valid, try next",
+ sw, *sw->path_iter);
+ goto next;
+ }
+
+ /* Setup and check compatible configuration */
+ if (ep->codec != codec->codec_id) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: different codec, try next", sw);
+ goto next;
+ }
+
+ if (!(sw->profile & spa_bt_profile_from_uuid(ep->uuid))) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: wrong uuid (%s) for profile, try next",
+ sw, ep->uuid);
+ goto next;
+ }
+
+ if ((sw->profile & SPA_BT_PROFILE_A2DP_SINK) || (sw->profile & SPA_BT_PROFILE_BAP_SINK) ) {
+ direction = SPA_BT_MEDIA_SOURCE;
+ sink = false;
+ } else if ((sw->profile & SPA_BT_PROFILE_A2DP_SOURCE) || (sw->profile & SPA_BT_PROFILE_BAP_SOURCE) ) {
+ direction = SPA_BT_MEDIA_SINK;
+ sink = true;
+ } else {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: bad profile (%d), try next",
+ sw, sw->profile);
+ goto next;
+ }
+
+ if (media_codec_to_endpoint(codec, direction, &local_endpoint) < 0) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: no endpoint for codec %s, try next",
+ sw, codec->name);
+ goto next;
+ }
+
+ /* Each endpoint can be used by only one device at a time (on each adapter) */
+ spa_list_for_each(t, &sw->device->monitor->transport_list, link) {
+ if (t->device == sw->device)
+ continue;
+ if (t->device->adapter != sw->device->adapter)
+ continue;
+ if (spa_streq(t->endpoint_path, local_endpoint)) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s in use, try next",
+ sw, local_endpoint);
+ goto next;
+ }
+ }
+
+ res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, ep->capabilities, ep->capabilities_len,
+ &sw->device->monitor->default_audio_info,
+ &sw->device->monitor->global_settings, config);
+ if (res < 0) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: incompatible capabilities (%d), try next",
+ sw, res);
+ goto next;
+ }
+ config_size = res;
+
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: configuration %d", sw, config_size);
+ for (i = 0; i < config_size; i++)
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: %d: %02x", sw, i, config[i]);
+
+ /* Codecs may share the same endpoint, so indicate which one we are using */
+ sw->device->preferred_codec = codec;
+
+ /* org.bluez.MediaEndpoint1.SetConfiguration on remote endpoint */
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, ep->path, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration");
+ if (m == NULL) {
+ spa_log_debug(sw->device->monitor->log, "media codec switch %p: dbus allocation failure, try next", sw);
+ goto next;
+ }
+
+ spa_bt_device_update_last_bluez_action_time(sw->device);
+
+ spa_log_info(sw->device->monitor->log, "media codec switch %p: trying codec %s for endpoint %s, local endpoint %s",
+ sw, codec->name, ep->path, local_endpoint);
+
+ dbus_message_iter_init_append(m, &iter);
+ dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &local_endpoint);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &d);
+ append_basic_array_variant_dict_entry(&d, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, config, config_size);
+ dbus_message_iter_close_container(&iter, &d);
+
+ spa_assert(sw->pending == NULL);
+ dbus_ret = dbus_connection_send_with_reply(sw->device->monitor->conn, m, &sw->pending, -1);
+
+ if (!dbus_ret || sw->pending == NULL) {
+ spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus call failure, try next", sw);
+ dbus_message_unref(m);
+ goto next;
+ }
+
+ dbus_ret = dbus_pending_call_set_notify(sw->pending, media_codec_switch_reply, sw, NULL);
+ dbus_message_unref(m);
+
+ if (!dbus_ret) {
+ spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus set notify failure", sw);
+ goto next;
+ }
+
+ free(local_endpoint);
+ return true;
+
+next:
+ free(local_endpoint);
+ return false;
+}
+
+static void media_codec_switch_process(struct spa_bt_media_codec_switch *sw)
+{
+ while (*sw->codec_iter != NULL && *sw->path_iter != NULL) {
+ struct timespec ts;
+ uint64_t now, threshold;
+
+ /* Rate limit BlueZ calls */
+ spa_system_clock_gettime(sw->device->monitor->main_system, CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ threshold = sw->device->last_bluez_action_time + BLUEZ_ACTION_RATE_MSEC * SPA_NSEC_PER_MSEC;
+ if (now < threshold) {
+ /* Wait for timeout */
+ media_codec_switch_start_timer(sw, threshold - now);
+ return;
+ }
+
+ if (sw->path_iter == sw->paths && (*sw->codec_iter)->caps_preference_cmp) {
+ /* Sort endpoints according to codec preference, when at a new codec. */
+ media_codec_switch_cmp_sw = sw;
+ qsort(sw->paths, sw->num_paths, sizeof(char *), media_codec_switch_cmp);
+ }
+
+ if (media_codec_switch_process_current(sw)) {
+ /* Wait for dbus reply */
+ return;
+ }
+
+ media_codec_switch_next(sw);
+ };
+
+ /* Didn't find any suitable endpoint. Report failure. */
+ spa_log_info(sw->device->monitor->log, "media codec switch %p: failed to get an endpoint", sw);
+ spa_bt_device_emit_codec_switched(sw->device, -ENODEV);
+ spa_bt_device_check_profiles(sw->device, false);
+ media_codec_switch_free(sw);
+}
+
+static bool media_codec_switch_goto_active(struct spa_bt_media_codec_switch *sw)
+{
+ struct spa_bt_device *device = sw->device;
+ struct spa_bt_media_codec_switch *active_sw;
+
+ active_sw = spa_list_first(&device->codec_switch_list, struct spa_bt_media_codec_switch, device_link);
+
+ if (active_sw != sw) {
+ struct spa_bt_media_codec_switch *t;
+
+ /* This codec switch has been canceled. Switch to the newest one. */
+ spa_log_debug(sw->device->monitor->log,
+ "media codec switch %p: canceled, go to new switch", sw);
+
+ spa_list_for_each_safe(sw, t, &device->codec_switch_list, device_link) {
+ if (sw != active_sw)
+ media_codec_switch_free(sw);
+ }
+
+ media_codec_switch_process(active_sw);
+ return false;
+ }
+
+ return true;
+}
+
+static void media_codec_switch_timer_event(struct spa_source *source)
+{
+ struct spa_bt_media_codec_switch *sw = source->data;
+ struct spa_bt_device *device = sw->device;
+ struct spa_bt_monitor *monitor = device->monitor;
+ uint64_t exp;
+
+ if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0)
+ spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno));
+
+ spa_log_debug(monitor->log, "media codec switch %p: rate limit timer event", sw);
+
+ media_codec_switch_stop_timer(sw);
+
+ if (!media_codec_switch_goto_active(sw))
+ return;
+
+ media_codec_switch_process(sw);
+}
+
+static void media_codec_switch_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct spa_bt_media_codec_switch *sw = user_data;
+ struct spa_bt_device *device = sw->device;
+ DBusMessage *r;
+
+ r = dbus_pending_call_steal_reply(pending);
+
+ spa_assert(sw->pending == pending);
+ dbus_pending_call_unref(pending);
+ sw->pending = NULL;
+
+ spa_bt_device_update_last_bluez_action_time(device);
+
+ if (!media_codec_switch_goto_active(sw)) {
+ if (r != NULL)
+ dbus_message_unref(r);
+ return;
+ }
+
+ if (r == NULL) {
+ spa_log_error(sw->device->monitor->log,
+ "media codec switch %p: empty reply from dbus, trying next",
+ sw);
+ goto next;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_debug(sw->device->monitor->log,
+ "media codec switch %p: failed (%s), trying next",
+ sw, dbus_message_get_error_name(r));
+ dbus_message_unref(r);
+ goto next;
+ }
+
+ dbus_message_unref(r);
+
+ /* Success */
+ spa_log_info(sw->device->monitor->log, "media codec switch %p: success", sw);
+ spa_bt_device_emit_codec_switched(sw->device, 0);
+ spa_bt_device_check_profiles(sw->device, false);
+ media_codec_switch_free(sw);
+ return;
+
+next:
+ if (sw->retries > 0)
+ --sw->retries;
+ else
+ media_codec_switch_next(sw);
+
+ media_codec_switch_process(sw);
+ return;
+}
+
+static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout)
+{
+ struct spa_bt_monitor *monitor = sw->device->monitor;
+ struct itimerspec ts;
+
+ spa_assert(sw->timer.data == NULL);
+
+ spa_log_debug(monitor->log, "media codec switch %p: starting rate limit timer", sw);
+
+ if (sw->timer.data == NULL) {
+ sw->timer.data = sw;
+ sw->timer.func = media_codec_switch_timer_event;
+ sw->timer.fd = spa_system_timerfd_create(monitor->main_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ sw->timer.mask = SPA_IO_IN;
+ sw->timer.rmask = 0;
+ spa_loop_add_source(monitor->main_loop, &sw->timer);
+ }
+ ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, sw->timer.fd, 0, &ts, NULL);
+ return 0;
+}
+
+static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw)
+{
+ struct spa_bt_monitor *monitor = sw->device->monitor;
+ struct itimerspec ts;
+
+ if (sw->timer.data == NULL)
+ return 0;
+
+ spa_log_debug(monitor->log, "media codec switch %p: stopping rate limit timer", sw);
+
+ spa_loop_remove_source(monitor->main_loop, &sw->timer);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(monitor->main_system, sw->timer.fd, 0, &ts, NULL);
+ spa_system_close(monitor->main_system, sw->timer.fd);
+ sw->timer.data = NULL;
+ return 0;
+}
+
+static int media_codec_switch_cmp(const void *a, const void *b)
+{
+ struct spa_bt_media_codec_switch *sw = media_codec_switch_cmp_sw;
+ const struct media_codec *codec = *sw->codec_iter;
+ const char *path1 = *(char **)a, *path2 = *(char **)b;
+ struct spa_bt_remote_endpoint *ep1, *ep2;
+ uint32_t flags;
+
+ ep1 = device_remote_endpoint_find(sw->device, path1);
+ ep2 = device_remote_endpoint_find(sw->device, path2);
+
+ if (ep1 != NULL && (ep1->uuid == NULL || ep1->codec != codec->codec_id || ep1->capabilities == NULL))
+ ep1 = NULL;
+ if (ep2 != NULL && (ep2->uuid == NULL || ep2->codec != codec->codec_id || ep2->capabilities == NULL))
+ ep2 = NULL;
+ if (ep1 && ep2 && !spa_streq(ep1->uuid, ep2->uuid)) {
+ ep1 = NULL;
+ ep2 = NULL;
+ }
+
+ if (ep1 == NULL && ep2 == NULL)
+ return 0;
+ else if (ep1 == NULL)
+ return 1;
+ else if (ep2 == NULL)
+ return -1;
+
+ if (codec->bap)
+ flags = spa_streq(ep1->uuid, SPA_BT_UUID_BAP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0;
+ else
+ flags = spa_streq(ep1->uuid, SPA_BT_UUID_A2DP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0;
+
+ return codec->caps_preference_cmp(codec, flags, ep1->capabilities, ep1->capabilities_len,
+ ep2->capabilities, ep2->capabilities_len, &sw->device->monitor->default_audio_info,
+ &sw->device->monitor->global_settings);
+}
+
+/* Ensure there's a transport for at least one of the listed codecs */
+int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs)
+{
+ struct spa_bt_media_codec_switch *sw;
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_transport *t;
+ const struct media_codec *preferred_codec = NULL;
+ size_t i, j, num_codecs, num_eps;
+
+ if (!device->adapter->application_registered) {
+ /* Codec switching not supported */
+ return -ENOTSUP;
+ }
+
+ for (i = 0; codecs[i] != NULL; ++i) {
+ if (spa_bt_device_supports_media_codec(device, codecs[i], true)) {
+ preferred_codec = codecs[i];
+ break;
+ }
+ }
+
+ /* Check if we already have an enabled transport for the most preferred codec.
+ * However, if there already was a codec switch running, these transports may
+ * disappear soon. In that case, we have to do the full thing.
+ */
+ if (spa_list_is_empty(&device->codec_switch_list) && preferred_codec != NULL) {
+ spa_list_for_each(t, &device->transport_list, device_link) {
+ if (t->media_codec != preferred_codec)
+ continue;
+
+ if ((device->connected_profiles & t->profile) != t->profile)
+ continue;
+
+ spa_bt_device_emit_codec_switched(device, 0);
+ return 0;
+ }
+ }
+
+ /* Setup and start iteration */
+
+ sw = calloc(1, sizeof(struct spa_bt_media_codec_switch));
+ if (sw == NULL)
+ return -ENOMEM;
+
+ num_eps = 0;
+ spa_list_for_each(ep, &device->remote_endpoint_list, device_link)
+ ++num_eps;
+
+ num_codecs = 0;
+ while (codecs[num_codecs] != NULL)
+ ++num_codecs;
+
+ sw->codecs = calloc(num_codecs + 1, sizeof(const struct media_codec *));
+ sw->paths = calloc(num_eps + 1, sizeof(char *));
+ sw->num_paths = num_eps;
+
+ if (sw->codecs == NULL || sw->paths == NULL) {
+ media_codec_switch_free(sw);
+ return -ENOMEM;
+ }
+
+ for (i = 0, j = 0; i < num_codecs; ++i) {
+ if (is_media_codec_enabled(device->monitor, codecs[i])) {
+ sw->codecs[j] = codecs[i];
+ ++j;
+ }
+ }
+ sw->codecs[j] = NULL;
+
+ i = 0;
+ spa_list_for_each(ep, &device->remote_endpoint_list, device_link) {
+ sw->paths[i] = strdup(ep->path);
+ if (sw->paths[i] == NULL) {
+ media_codec_switch_free(sw);
+ return -ENOMEM;
+ }
+ ++i;
+ }
+ sw->paths[i] = NULL;
+
+ sw->codec_iter = sw->codecs;
+ sw->path_iter = sw->paths;
+ sw->retries = CODEC_SWITCH_RETRIES;
+
+ sw->profile = device->connected_profiles;
+
+ sw->device = device;
+
+ if (!spa_list_is_empty(&device->codec_switch_list)) {
+ /*
+ * There's a codec switch already running, either waiting for timeout or
+ * BlueZ reply.
+ *
+ * BlueZ does not appear to allow calling dbus_pending_call_cancel on an
+ * active request, so we have to wait for the reply to arrive first, and
+ * only then start processing this request. The timeout we would also have
+ * to wait to pass in any case, so we don't cancel it either.
+ */
+ spa_log_debug(sw->device->monitor->log,
+ "media codec switch %p: already in progress, canceling previous",
+ sw);
+
+ spa_list_prepend(&device->codec_switch_list, &sw->device_link);
+ } else {
+ spa_list_prepend(&device->codec_switch_list, &sw->device_link);
+ media_codec_switch_process(sw);
+ }
+
+ return 0;
+}
+
+int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ return spa_bt_backend_ensure_codec(monitor->backend, device, codec);
+}
+
+int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec)
+{
+ struct spa_bt_monitor *monitor = device->monitor;
+ return spa_bt_backend_supports_codec(monitor->backend, device, codec);
+}
+
+static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn,
+ const char *path, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ const char *transport_path, *endpoint;
+ DBusMessageIter it[2];
+ DBusMessage *r;
+ struct spa_bt_transport *transport;
+ const struct media_codec *codec;
+ int profile;
+ bool sink;
+
+ if (!dbus_message_has_signature(m, "oa{sv}")) {
+ spa_log_warn(monitor->log, "invalid SetConfiguration() signature");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ endpoint = dbus_message_get_path(m);
+
+ profile = media_endpoint_to_profile(endpoint);
+ codec = media_endpoint_to_codec(monitor, endpoint, &sink, NULL);
+ if (codec == NULL) {
+ spa_log_warn(monitor->log, "unknown SetConfiguration() codec");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ dbus_message_iter_init(m, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &transport_path);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ transport = spa_bt_transport_find(monitor, transport_path);
+
+ if (transport == NULL) {
+ char *tpath = strdup(transport_path);
+
+ transport = spa_bt_transport_create(monitor, tpath, 0);
+ if (transport == NULL) {
+ free(tpath);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+
+ spa_bt_transport_set_implementation(transport, &transport_impl, transport);
+
+ if (profile & SPA_BT_PROFILE_A2DP_SOURCE) {
+ transport->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_AG_VOLUME;
+ transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_AG_VOLUME;
+ } else {
+ transport->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_RX_VOLUME;
+ transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME;
+ }
+ }
+
+ 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;
+ }
+
+ free(transport->endpoint_path);
+ transport->endpoint_path = strdup(endpoint);
+ transport->profile = profile;
+ transport->media_codec = codec;
+ transport_update_props(transport, &it[1], NULL);
+
+ if (transport->device == NULL || transport->device->adapter == NULL) {
+ spa_log_warn(monitor->log, "no device found for transport");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ /* If multiple codecs share the endpoint, pick the one we wanted */
+ transport->media_codec = codec = media_endpoint_to_codec(monitor, endpoint, &sink,
+ transport->device->preferred_codec);
+ spa_assert(codec != NULL);
+ spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : "<null>");
+
+ spa_bt_device_update_last_bluez_action_time(transport->device);
+
+ if (profile & SPA_BT_PROFILE_A2DP_SOURCE) {
+ /* PW is the rendering device so it's responsible for reporting hardware volume. */
+ transport->volumes[SPA_BT_VOLUME_ID_RX].active = true;
+ } else if (profile & SPA_BT_PROFILE_A2DP_SINK) {
+ transport->volumes[SPA_BT_VOLUME_ID_TX].active
+ |= transport->device->a2dp_volume_active[SPA_BT_VOLUME_ID_TX];
+ }
+
+ if (codec->validate_config) {
+ struct spa_audio_info info;
+ if (codec->validate_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0,
+ transport->configuration, transport->configuration_len,
+ &info) < 0) {
+ spa_log_error(monitor->log, "invalid transport configuration");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ transport->n_channels = info.info.raw.channels;
+ memcpy(transport->channels, info.info.raw.position,
+ transport->n_channels * sizeof(uint32_t));
+ } else {
+ transport->n_channels = 2;
+ transport->channels[0] = SPA_AUDIO_CHANNEL_FL;
+ transport->channels[1] = SPA_AUDIO_CHANNEL_FR;
+ }
+ spa_log_info(monitor->log, "%p: %s validate conf channels:%d",
+ monitor, path, transport->n_channels);
+
+ spa_bt_device_add_profile(transport->device, transport->profile);
+
+ spa_bt_device_connect_profile(transport->device, transport->profile);
+
+ /* Sync initial volumes */
+ transport_sync_volume(transport);
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult endpoint_clear_configuration(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ DBusError err;
+ DBusMessage *r;
+ const char *transport_path;
+ struct spa_bt_transport *transport;
+
+ dbus_error_init(&err);
+
+ if (!dbus_message_get_args(m, &err,
+ DBUS_TYPE_OBJECT_PATH, &transport_path,
+ DBUS_TYPE_INVALID)) {
+ spa_log_warn(monitor->log, "Bad ClearConfiguration method call: %s",
+ err.message);
+ dbus_error_free(&err);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ transport = spa_bt_transport_find(monitor, transport_path);
+
+ if (transport != NULL) {
+ struct spa_bt_device *device = transport->device;
+
+ spa_log_debug(monitor->log, "transport %p: free %s",
+ transport, transport->path);
+
+ spa_bt_transport_free(transport);
+ if (device != NULL)
+ spa_bt_device_check_profiles(device, false);
+ }
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult endpoint_release(DBusConnection *conn, DBusMessage *m, void *userdata)
+{
+ DBusMessage *r;
+
+ r = dbus_message_new_error(m,
+ BLUEZ_MEDIA_ENDPOINT_INTERFACE ".Error.NotImplemented",
+ "Method not implemented");
+ if (r == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult endpoint_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct spa_bt_monitor *monitor = userdata;
+ const char *path, *interface, *member;
+ DBusMessage *r;
+ DBusHandlerResult res;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(monitor->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = ENDPOINT_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(monitor->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration"))
+ res = endpoint_set_configuration(c, path, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectConfiguration"))
+ res = endpoint_select_configuration(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectProperties"))
+ res = endpoint_select_properties(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "ClearConfiguration"))
+ res = endpoint_clear_configuration(c, m, userdata);
+ else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "Release"))
+ res = endpoint_release(c, m, userdata);
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void bluez_register_endpoint_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct spa_bt_monitor *monitor = user_data;
+ DBusMessage *r;
+
+ r = dbus_pending_call_steal_reply(pending);
+ dbus_pending_call_unref(pending);
+
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(monitor->log, "BlueZ D-Bus ObjectManager not available");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(monitor->log, "RegisterEndpoint() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+finish:
+ dbus_message_unref(r);
+}
+
+static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) {
+ DBusMessageIter dict_entry_it, variant_it;
+ dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it);
+ dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key);
+
+ dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it);
+ dbus_message_iter_append_basic(&variant_it, variant_type_int, variant);
+ dbus_message_iter_close_container(&dict_entry_it, &variant_it);
+ dbus_message_iter_close_container(dict, &dict_entry_it);
+}
+
+static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const char* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size) {
+ DBusMessageIter dict_entry_it, variant_it, array_it;
+ dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it);
+ dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key);
+
+ dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it);
+ dbus_message_iter_open_container(&variant_it, DBUS_TYPE_ARRAY, array_type_str, &array_it);
+ dbus_message_iter_append_fixed_array (&array_it, array_type_int, &data, data_size);
+ dbus_message_iter_close_container(&variant_it, &array_it);
+ dbus_message_iter_close_container(&dict_entry_it, &variant_it);
+ dbus_message_iter_close_container(dict, &dict_entry_it);
+}
+
+static int bluez_register_endpoint(struct spa_bt_monitor *monitor,
+ const char *path, enum spa_bt_media_direction direction,
+ const char *uuid, const struct media_codec *codec)
+{
+ char *object_path = NULL;
+ DBusMessage *m;
+ DBusMessageIter object_it, dict_it;
+ DBusPendingCall *call;
+ uint8_t caps[A2DP_MAX_CAPS_SIZE];
+ int ret, caps_size;
+ uint16_t codec_id = codec->codec_id;
+ bool sink = (direction == SPA_BT_MEDIA_SINK);
+
+ spa_assert(codec->fill_caps);
+
+ ret = media_codec_to_endpoint(codec, direction, &object_path);
+ if (ret < 0)
+ goto error;
+
+ ret = caps_size = codec->fill_caps(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, caps);
+ if (ret < 0)
+ goto error;
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ path,
+ BLUEZ_MEDIA_INTERFACE,
+ "RegisterEndpoint");
+ if (m == NULL) {
+ ret = -EIO;
+ goto error;
+ }
+
+ dbus_message_iter_init_append(m, &object_it);
+ dbus_message_iter_append_basic(&object_it, DBUS_TYPE_OBJECT_PATH, &object_path);
+
+ dbus_message_iter_open_container(&object_it, DBUS_TYPE_ARRAY, "{sv}", &dict_it);
+
+ append_basic_variant_dict_entry(&dict_it,"UUID", DBUS_TYPE_STRING, "s", &uuid);
+ append_basic_variant_dict_entry(&dict_it, "Codec", DBUS_TYPE_BYTE, "y", &codec_id);
+ append_basic_array_variant_dict_entry(&dict_it, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
+
+ dbus_message_iter_close_container(&object_it, &dict_it);
+
+ dbus_connection_send_with_reply(monitor->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, bluez_register_endpoint_reply, monitor, NULL);
+ dbus_message_unref(m);
+
+ free(object_path);
+
+ return 0;
+
+error:
+ free(object_path);
+ return ret;
+}
+
+static int adapter_register_endpoints(struct spa_bt_adapter *a)
+{
+ struct spa_bt_monitor *monitor = a->monitor;
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ int i;
+ int err = 0;
+
+ if (a->endpoints_registered)
+ return err;
+
+ /* The legacy bluez5 api doesn't support codec switching
+ * It doesn't make sense to register codecs other than SBC
+ * as bluez5 will probably use SBC anyway and we have no control over it
+ * let's incentivize users to upgrade their bluez5 daemon
+ * if they want proper media codec support
+ * */
+ spa_log_warn(monitor->log,
+ "Using legacy bluez5 API for A2DP - only SBC will be supported. "
+ "No LE Audio. Please upgrade bluez5.");
+
+ monitor->le_audio_supported = false;
+
+ for (i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+
+ if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_SBC)
+ continue;
+
+ if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) {
+ if ((err = bluez_register_endpoint(monitor, a->path,
+ SPA_BT_MEDIA_SOURCE,
+ SPA_BT_UUID_A2DP_SOURCE,
+ codec)))
+ goto out;
+ }
+
+ if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) {
+ if ((err = bluez_register_endpoint(monitor, a->path,
+ SPA_BT_MEDIA_SINK,
+ SPA_BT_UUID_A2DP_SINK,
+ codec)))
+ goto out;
+ }
+
+ a->endpoints_registered = true;
+ break;
+ }
+
+ if (!a->endpoints_registered) {
+ /* Should never happen as SBC support is always enabled */
+ spa_log_error(monitor->log, "Broken PipeWire build - unable to locate SBC codec");
+ err = -ENOSYS;
+ }
+
+out:
+ if (err) {
+ spa_log_error(monitor->log, "Failed to register bluez5 endpoints");
+ }
+ return err;
+}
+
+static void append_media_object(DBusMessageIter *iter, const char *endpoint,
+ const char *uuid, uint8_t codec_id, uint8_t *caps, size_t caps_size)
+{
+ const char *interface_name = BLUEZ_MEDIA_ENDPOINT_INTERFACE;
+ DBusMessageIter object, array, entry, dict;
+ dbus_bool_t delay_reporting;
+
+ dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY, NULL, &object);
+ dbus_message_iter_append_basic(&object, DBUS_TYPE_OBJECT_PATH, &endpoint);
+
+ dbus_message_iter_open_container(&object, DBUS_TYPE_ARRAY, "{sa{sv}}", &array);
+
+ dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &entry);
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface_name);
+
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_ARRAY, "{sv}", &dict);
+
+ append_basic_variant_dict_entry(&dict, "UUID", DBUS_TYPE_STRING, "s", &uuid);
+ append_basic_variant_dict_entry(&dict, "Codec", DBUS_TYPE_BYTE, "y", &codec_id);
+ append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size);
+ if (spa_bt_profile_from_uuid(uuid) & SPA_BT_PROFILE_A2DP_SOURCE) {
+ delay_reporting = TRUE;
+ append_basic_variant_dict_entry(&dict, "DelayReporting", DBUS_TYPE_BOOLEAN, "b", &delay_reporting);
+ }
+
+ dbus_message_iter_close_container(&entry, &dict);
+ dbus_message_iter_close_container(&array, &entry);
+ dbus_message_iter_close_container(&object, &array);
+ dbus_message_iter_close_container(iter, &object);
+}
+
+static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *m, void *user_data)
+{
+ struct spa_bt_monitor *monitor = user_data;
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ const char *path, *interface, *member;
+ char *endpoint;
+ DBusMessage *r;
+ DBusMessageIter iter, array;
+ DBusHandlerResult res;
+ int i;
+
+ path = dbus_message_get_path(m);
+ interface = dbus_message_get_interface(m);
+ member = dbus_message_get_member(m);
+
+ spa_log_debug(monitor->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) {
+ const char *xml = OBJECT_MANAGER_INTROSPECT_XML;
+
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ if (!dbus_connection_send(monitor->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_unref(r);
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else if (dbus_message_is_method_call(m, "org.freedesktop.DBus.ObjectManager", "GetManagedObjects")) {
+ if ((r = dbus_message_new_method_return(m)) == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ dbus_message_iter_init_append(r, &iter);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{oa{sa{sv}}}", &array);
+
+ for (i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+ uint8_t caps[A2DP_MAX_CAPS_SIZE];
+ int caps_size, ret;
+ uint16_t codec_id = codec->codec_id;
+
+ if (!is_media_codec_enabled(monitor, codec))
+ continue;
+
+ if (codec->bap && !monitor->le_audio_supported) {
+ /* The legacy bluez5 api doesn't support LE Audio
+ * It doesn't make sense to register unsupported codecs as it prevents
+ * registration of A2DP codecs
+ * let's incentivize users to upgrade their bluez5 daemon
+ * if they want proper media codec support
+ * */
+ spa_log_warn(monitor->log, "Trying to use legacy bluez5 API for LE Audio - only A2DP will be supported. "
+ "Please upgrade bluez5.");
+ continue;
+ }
+
+ if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) {
+ caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, caps);
+ if (caps_size < 0)
+ continue;
+
+ ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SINK, &endpoint);
+ if (ret == 0) {
+ spa_log_info(monitor->log, "register media sink codec %s: %s", media_codecs[i]->name, endpoint);
+ append_media_object(&array, endpoint,
+ codec->bap ? SPA_BT_UUID_BAP_SINK : SPA_BT_UUID_A2DP_SINK,
+ codec_id, caps, caps_size);
+ free(endpoint);
+ }
+ }
+
+ if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) {
+ caps_size = codec->fill_caps(codec, 0, caps);
+ if (caps_size < 0)
+ continue;
+
+ ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SOURCE, &endpoint);
+ if (ret == 0) {
+ spa_log_info(monitor->log, "register media source codec %s: %s", media_codecs[i]->name, endpoint);
+ append_media_object(&array, endpoint,
+ codec->bap ? SPA_BT_UUID_BAP_SOURCE : SPA_BT_UUID_A2DP_SOURCE,
+ codec_id, caps, caps_size);
+ free(endpoint);
+ }
+ }
+ }
+
+ dbus_message_iter_close_container(&iter, &array);
+ if (!dbus_connection_send(monitor->conn, r, NULL))
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ res = DBUS_HANDLER_RESULT_HANDLED;
+ }
+ else
+ res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ return res;
+}
+
+static void bluez_register_application_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct spa_bt_adapter *adapter = user_data;
+ struct spa_bt_monitor *monitor = adapter->monitor;
+ DBusMessage *r;
+ bool fallback = true;
+
+ r = dbus_pending_call_steal_reply(pending);
+ dbus_pending_call_unref(pending);
+
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) {
+ spa_log_warn(monitor->log, "Registering media applications for adapter %s is disabled in bluez5", adapter->path);
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(monitor->log, "RegisterApplication() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ fallback = false;
+ adapter->application_registered = true;
+
+finish:
+ dbus_message_unref(r);
+
+ if (fallback)
+ adapter_register_endpoints(adapter);
+}
+
+static int register_media_endpoint(struct spa_bt_monitor *monitor,
+ const struct media_codec *codec,
+ enum spa_bt_media_direction direction)
+{
+ static const DBusObjectPathVTable vtable_endpoint = {
+ .message_function = endpoint_handler,
+ };
+
+ if (!endpoint_should_be_registered(monitor, codec, direction))
+ return 0;
+
+ char *object_path = NULL;
+ int ret = media_codec_to_endpoint(codec, direction, &object_path);
+ if (ret < 0)
+ return ret;
+
+ spa_log_info(monitor->log, "registering endpoint: %s", object_path);
+
+ if (!dbus_connection_register_object_path(monitor->conn,
+ object_path,
+ &vtable_endpoint, monitor))
+ {
+ ret = -EIO;
+ }
+
+ free(object_path);
+ return ret;
+}
+
+static int register_media_application(struct spa_bt_monitor * monitor)
+{
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+ const DBusObjectPathVTable vtable_object_manager = {
+ .message_function = object_manager_handler,
+ };
+
+ spa_log_info(monitor->log, "Registering media application object: " MEDIA_OBJECT_MANAGER_PATH);
+
+ if (!dbus_connection_register_object_path(monitor->conn,
+ MEDIA_OBJECT_MANAGER_PATH,
+ &vtable_object_manager, monitor))
+ return -EIO;
+
+ for (int i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+
+ register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE);
+ register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK);
+ }
+
+ return 0;
+}
+
+static void unregister_media_endpoint(struct spa_bt_monitor *monitor,
+ const struct media_codec *codec,
+ enum spa_bt_media_direction direction)
+{
+ if (!endpoint_should_be_registered(monitor, codec, direction))
+ return;
+
+ char *object_path = NULL;
+ int ret = media_codec_to_endpoint(codec, direction, &object_path);
+ if (ret < 0)
+ return;
+
+ spa_log_info(monitor->log, "unregistering endpoint: %s", object_path);
+
+ if (!dbus_connection_unregister_object_path(monitor->conn, object_path))
+ spa_log_warn(monitor->log, "failed to unregister %s\n", object_path);
+
+ free(object_path);
+}
+
+static void unregister_media_application(struct spa_bt_monitor * monitor)
+{
+ const struct media_codec * const * const media_codecs = monitor->media_codecs;
+
+ for (int i = 0; media_codecs[i]; i++) {
+ const struct media_codec *codec = media_codecs[i];
+
+ unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE);
+ unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK);
+ }
+
+ dbus_connection_unregister_object_path(monitor->conn, MEDIA_OBJECT_MANAGER_PATH);
+}
+
+static int adapter_register_application(struct spa_bt_adapter *a) {
+ const char *object_manager_path = MEDIA_OBJECT_MANAGER_PATH;
+ struct spa_bt_monitor *monitor = a->monitor;
+ DBusMessage *m;
+ DBusMessageIter i, d;
+ DBusPendingCall *call;
+
+ if (a->application_registered)
+ return 0;
+
+ spa_log_debug(monitor->log, "Registering bluez5 media application on adapter %s", a->path);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ a->path,
+ BLUEZ_MEDIA_INTERFACE,
+ "RegisterApplication");
+ if (m == NULL)
+ return -EIO;
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &object_manager_path);
+ dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, "{sv}", &d);
+ dbus_message_iter_close_container(&i, &d);
+
+ dbus_connection_send_with_reply(monitor->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, bluez_register_application_reply, a, NULL);
+ dbus_message_unref(m);
+
+ return 0;
+}
+
+static int switch_backend(struct spa_bt_monitor *monitor, struct spa_bt_backend *backend)
+{
+ int res;
+ size_t i;
+
+ spa_return_val_if_fail(backend != NULL, -EINVAL);
+
+ if (!backend->available)
+ return -ENODEV;
+
+ for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) {
+ struct spa_bt_backend *b = monitor->backends[i];
+ if (backend != b && b && b->available && b->exclusive)
+ spa_log_warn(monitor->log,
+ "%s running, but not configured as HFP/HSP backend: "
+ "it may interfere with HFP/HSP functionality.",
+ b->name);
+ }
+
+ if (monitor->backend == backend)
+ return 0;
+
+ spa_log_info(monitor->log, "Switching to HFP/HSP backend %s", backend->name);
+
+ spa_bt_backend_unregister_profiles(monitor->backend);
+
+ if ((res = spa_bt_backend_register_profiles(backend)) < 0) {
+ monitor->backend = NULL;
+ return res;
+ }
+
+ monitor->backend = backend;
+ return 0;
+}
+
+static void reselect_backend(struct spa_bt_monitor *monitor, bool silent)
+{
+ struct spa_bt_backend *backend;
+ size_t i;
+
+ spa_log_debug(monitor->log, "re-selecting HFP/HSP backend");
+
+ if (monitor->backend_selection == BACKEND_NONE) {
+ spa_bt_backend_unregister_profiles(monitor->backend);
+ monitor->backend = NULL;
+ return;
+ } else if (monitor->backend_selection == BACKEND_ANY) {
+ for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) {
+ backend = monitor->backends[i];
+ if (backend && switch_backend(monitor, backend) == 0)
+ return;
+ }
+ } else {
+ backend = monitor->backends[monitor->backend_selection];
+ if (backend && switch_backend(monitor, backend) == 0)
+ return;
+ }
+
+ spa_bt_backend_unregister_profiles(monitor->backend);
+ monitor->backend = NULL;
+
+ if (!silent)
+ spa_log_error(monitor->log, "Failed to start HFP/HSP backend %s",
+ backend ? backend->name : "none");
+}
+
+static int media_update_props(struct spa_bt_monitor *monitor,
+ DBusMessageIter *props_iter,
+ DBusMessageIter *invalidated_iter)
+{
+ while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) {
+ DBusMessageIter it[2];
+ const char *key;
+
+ dbus_message_iter_recurse(props_iter, &it[0]);
+ dbus_message_iter_get_basic(&it[0], &key);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ if (spa_streq(key, "SupportedUUIDs")) {
+ DBusMessageIter iter;
+
+ if (!check_iter_signature(&it[1], "as"))
+ goto next;
+
+ dbus_message_iter_recurse(&it[1], &iter);
+
+ while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) {
+ const char *uuid;
+
+ dbus_message_iter_get_basic(&iter, &uuid);
+
+ if (spa_streq(uuid, SPA_BT_UUID_BAP_SINK)) {
+ monitor->le_audio_supported = true;
+ spa_log_info(monitor->log, "LE Audio supported");
+ }
+ dbus_message_iter_next(&iter);
+ }
+ }
+ else
+ spa_log_debug(monitor->log, "media: unhandled key %s", key);
+
+next:
+ dbus_message_iter_next(props_iter);
+ }
+ return 0;
+}
+
+static void interface_added(struct spa_bt_monitor *monitor,
+ DBusConnection *conn,
+ const char *object_path,
+ const char *interface_name,
+ DBusMessageIter *props_iter)
+{
+ spa_log_debug(monitor->log, "Found object %s, interface %s", object_path, interface_name);
+
+ if (spa_streq(interface_name, BLUEZ_ADAPTER_INTERFACE)) {
+ struct spa_bt_adapter *a;
+
+ a = adapter_find(monitor, object_path);
+ if (a == NULL) {
+ a = adapter_create(monitor, object_path);
+ if (a == NULL) {
+ spa_log_warn(monitor->log, "can't create adapter: %m");
+ return;
+ }
+ }
+ adapter_update_props(a, props_iter, NULL);
+ adapter_register_application(a);
+ adapter_register_player(a);
+ adapter_update_devices(a);
+ }
+ else if (spa_streq(interface_name, BLUEZ_PROFILE_MANAGER_INTERFACE)) {
+ if (monitor->backends[BACKEND_NATIVE])
+ monitor->backends[BACKEND_NATIVE]->available = true;
+ reselect_backend(monitor, false);
+ }
+ else if (spa_streq(interface_name, BLUEZ_DEVICE_INTERFACE)) {
+ struct spa_bt_device *d;
+
+ d = spa_bt_device_find(monitor, object_path);
+ if (d == NULL) {
+ d = device_create(monitor, object_path);
+ if (d == NULL) {
+ spa_log_warn(monitor->log, "can't create Bluetooth device %s: %m",
+ object_path);
+ return;
+ }
+ }
+
+ device_update_props(d, props_iter, NULL);
+ d->reconnect_state = BT_DEVICE_RECONNECT_INIT;
+
+ if (!device_props_ready(d))
+ return;
+
+ device_update_hw_volume_profiles(d);
+
+ /* Trigger bluez device creation before bluez profile negotiation started so that
+ * profile connection handlers can receive per-device settings during profile negotiation. */
+ spa_bt_device_add_profile(d, SPA_BT_PROFILE_NULL);
+ }
+ else if (spa_streq(interface_name, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) {
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_device *d;
+
+ ep = remote_endpoint_find(monitor, object_path);
+ if (ep == NULL) {
+ ep = remote_endpoint_create(monitor, object_path);
+ if (ep == NULL) {
+ spa_log_warn(monitor->log, "can't create Bluetooth remote endpoint %s: %m",
+ object_path);
+ return;
+ }
+ }
+ remote_endpoint_update_props(ep, props_iter, NULL);
+
+ d = ep->device;
+ if (d)
+ spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
+ }
+ else if (spa_streq(interface_name, BLUEZ_MEDIA_INTERFACE)) {
+ media_update_props(monitor, props_iter, NULL);
+ }
+}
+
+static void interfaces_added(struct spa_bt_monitor *monitor, DBusMessageIter *arg_iter)
+{
+ DBusMessageIter it[3];
+ const char *object_path;
+
+ dbus_message_iter_get_basic(arg_iter, &object_path);
+ dbus_message_iter_next(arg_iter);
+ dbus_message_iter_recurse(arg_iter, &it[0]);
+
+ while (dbus_message_iter_get_arg_type(&it[0]) != DBUS_TYPE_INVALID) {
+ const char *interface_name;
+
+ dbus_message_iter_recurse(&it[0], &it[1]);
+ dbus_message_iter_get_basic(&it[1], &interface_name);
+ dbus_message_iter_next(&it[1]);
+ dbus_message_iter_recurse(&it[1], &it[2]);
+
+ interface_added(monitor, monitor->conn,
+ object_path, interface_name,
+ &it[2]);
+
+ dbus_message_iter_next(&it[0]);
+ }
+}
+
+static void interfaces_removed(struct spa_bt_monitor *monitor, DBusMessageIter *arg_iter)
+{
+ const char *object_path;
+ DBusMessageIter it;
+
+ dbus_message_iter_get_basic(arg_iter, &object_path);
+ dbus_message_iter_next(arg_iter);
+ dbus_message_iter_recurse(arg_iter, &it);
+
+ while (dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_INVALID) {
+ const char *interface_name;
+
+ dbus_message_iter_get_basic(&it, &interface_name);
+
+ spa_log_debug(monitor->log, "Found object %s, interface %s", object_path, interface_name);
+
+ if (spa_streq(interface_name, BLUEZ_DEVICE_INTERFACE)) {
+ struct spa_bt_device *d;
+ d = spa_bt_device_find(monitor, object_path);
+ if (d != NULL)
+ device_free(d);
+ } else if (spa_streq(interface_name, BLUEZ_ADAPTER_INTERFACE)) {
+ struct spa_bt_adapter *a;
+ a = adapter_find(monitor, object_path);
+ if (a != NULL)
+ adapter_free(a);
+ } else if (spa_streq(interface_name, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) {
+ struct spa_bt_remote_endpoint *ep;
+ ep = remote_endpoint_find(monitor, object_path);
+ if (ep != NULL) {
+ 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);
+ }
+ }
+
+ dbus_message_iter_next(&it);
+ }
+}
+
+static void get_managed_objects_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct spa_bt_monitor *monitor = user_data;
+ DBusMessage *r;
+ DBusMessageIter it[6];
+
+ spa_assert(pending == monitor->get_managed_objects_call);
+ monitor->get_managed_objects_call = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ dbus_pending_call_unref(pending);
+
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(monitor->log, "BlueZ D-Bus ObjectManager not available");
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(monitor->log, "GetManagedObjects() failed: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &it[0]) ||
+ !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) {
+ spa_log_error(monitor->log, "Invalid reply signature for GetManagedObjects()");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ while (dbus_message_iter_get_arg_type(&it[1]) != DBUS_TYPE_INVALID) {
+ dbus_message_iter_recurse(&it[1], &it[2]);
+
+ interfaces_added(monitor, &it[2]);
+
+ dbus_message_iter_next(&it[1]);
+ }
+
+ reselect_backend(monitor, false);
+
+ monitor->objects_listed = true;
+
+finish:
+ dbus_message_unref(r);
+ return;
+}
+
+static void get_managed_objects(struct spa_bt_monitor *monitor)
+{
+ if (monitor->objects_listed || monitor->get_managed_objects_call)
+ return;
+
+ DBusMessage *m;
+ DBusPendingCall *call;
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE,
+ "/",
+ "org.freedesktop.DBus.ObjectManager",
+ "GetManagedObjects");
+
+ dbus_message_set_auto_start(m, false);
+
+ dbus_connection_send_with_reply(monitor->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, get_managed_objects_reply, monitor, NULL);
+ dbus_message_unref(m);
+
+ monitor->get_managed_objects_call = call;
+}
+
+static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ struct spa_bt_monitor *monitor = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+ const char *name, *old_owner, *new_owner;
+
+ spa_log_debug(monitor->log, "Name owner changed %s", dbus_message_get_path(m));
+
+ if (!dbus_message_get_args(m, &err,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old_owner,
+ DBUS_TYPE_STRING, &new_owner,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(monitor->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message);
+ goto fail;
+ }
+
+ if (spa_streq(name, BLUEZ_SERVICE)) {
+ bool has_old_owner = old_owner && *old_owner;
+ bool has_new_owner = new_owner && *new_owner;
+
+ if (has_old_owner) {
+ spa_log_debug(monitor->log, "Bluetooth daemon disappeared");
+
+ if (monitor->backends[BACKEND_NATIVE])
+ monitor->backends[BACKEND_NATIVE]->available = false;
+
+ reselect_backend(monitor, true);
+ }
+
+ if (has_old_owner || has_new_owner) {
+ struct spa_bt_adapter *a;
+ struct spa_bt_device *d;
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_transport *t;
+
+ monitor->objects_listed = false;
+
+ spa_list_consume(t, &monitor->transport_list, link)
+ spa_bt_transport_free(t);
+ spa_list_consume(ep, &monitor->remote_endpoint_list, link)
+ remote_endpoint_free(ep);
+ spa_list_consume(d, &monitor->device_list, link)
+ device_free(d);
+ spa_list_consume(a, &monitor->adapter_list, link)
+ adapter_free(a);
+ }
+
+ if (has_new_owner) {
+ spa_log_debug(monitor->log, "Bluetooth daemon appeared");
+ get_managed_objects(monitor);
+ }
+ } else if (spa_streq(name, OFONO_SERVICE)) {
+ if (monitor->backends[BACKEND_OFONO])
+ monitor->backends[BACKEND_OFONO]->available = (new_owner && *new_owner);
+ reselect_backend(monitor, false);
+ } else if (spa_streq(name, HSPHFPD_SERVICE)) {
+ if (monitor->backends[BACKEND_HSPHFPD])
+ monitor->backends[BACKEND_HSPHFPD]->available = (new_owner && *new_owner);
+ reselect_backend(monitor, false);
+ }
+ } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesAdded")) {
+ DBusMessageIter it;
+
+ spa_log_debug(monitor->log, "interfaces added %s", dbus_message_get_path(m));
+
+ if (!monitor->objects_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &it) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) {
+ spa_log_error(monitor->log, "Invalid signature found in InterfacesAdded");
+ goto finish;
+ }
+
+ interfaces_added(monitor, &it);
+ } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesRemoved")) {
+ DBusMessageIter it;
+
+ spa_log_debug(monitor->log, "interfaces removed %s", dbus_message_get_path(m));
+
+ if (!monitor->objects_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &it) || !spa_streq(dbus_message_get_signature(m), "oas")) {
+ spa_log_error(monitor->log, "Invalid signature found in InterfacesRemoved");
+ goto finish;
+ }
+
+ interfaces_removed(monitor, &it);
+ } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.Properties", "PropertiesChanged")) {
+ DBusMessageIter it[2];
+ const char *iface, *path;
+
+ if (!monitor->objects_listed)
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &it[0]) ||
+ !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) {
+ spa_log_error(monitor->log, "Invalid signature found in PropertiesChanged");
+ goto finish;
+ }
+ path = dbus_message_get_path(m);
+
+ dbus_message_iter_get_basic(&it[0], &iface);
+ dbus_message_iter_next(&it[0]);
+ dbus_message_iter_recurse(&it[0], &it[1]);
+
+ if (spa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) {
+ struct spa_bt_adapter *a;
+
+ a = adapter_find(monitor, path);
+ if (a == NULL) {
+ spa_log_warn(monitor->log,
+ "Properties changed in unknown adapter %s", path);
+ goto finish;
+ }
+ spa_log_debug(monitor->log, "Properties changed in adapter %s", path);
+
+ adapter_update_props(a, &it[1], NULL);
+ }
+ else if (spa_streq(iface, BLUEZ_DEVICE_INTERFACE)) {
+ struct spa_bt_device *d;
+
+ d = spa_bt_device_find(monitor, path);
+ if (d == NULL) {
+ spa_log_debug(monitor->log,
+ "Properties changed in unknown device %s", path);
+ goto finish;
+ }
+ spa_log_debug(monitor->log, "Properties changed in device %s", path);
+
+ device_update_props(d, &it[1], NULL);
+
+ if (!device_props_ready(d))
+ goto finish;
+
+ device_update_hw_volume_profiles(d);
+
+ spa_bt_device_add_profile(d, SPA_BT_PROFILE_NULL);
+ }
+ else if (spa_streq(iface, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) {
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_device *d;
+
+ ep = remote_endpoint_find(monitor, path);
+ if (ep == NULL) {
+ spa_log_debug(monitor->log,
+ "Properties changed in unknown remote endpoint %s", path);
+ goto finish;
+ }
+ spa_log_debug(monitor->log, "Properties changed in remote endpoint %s", path);
+
+ remote_endpoint_update_props(ep, &it[1], NULL);
+
+ d = ep->device;
+ if (d)
+ spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles);
+ }
+ else if (spa_streq(iface, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) {
+ struct spa_bt_transport *transport;
+
+ transport = spa_bt_transport_find(monitor, path);
+ if (transport == NULL) {
+ spa_log_warn(monitor->log,
+ "Properties changed in unknown transport %s", path);
+ goto finish;
+ }
+
+ spa_log_debug(monitor->log, "Properties changed in transport %s", path);
+
+ transport_update_props(transport, &it[1], NULL);
+ }
+ }
+
+fail:
+ dbus_error_free(&err);
+finish:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static void add_filters(struct spa_bt_monitor *this)
+{
+ DBusError err;
+
+ if (this->filters_added)
+ return;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(this->conn, filter_cb, this, NULL)) {
+ spa_log_error(this->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged',"
+ "arg0='" BLUEZ_SERVICE "'", &err);
+#ifdef HAVE_BLUEZ_5_BACKEND_OFONO
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged',"
+ "arg0='" OFONO_SERVICE "'", &err);
+#endif
+#ifdef HAVE_BLUEZ_5_BACKEND_HSPHFPD
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged',"
+ "arg0='" HSPHFPD_SERVICE "'", &err);
+#endif
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.ObjectManager',member='InterfacesRemoved'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',"
+ "arg0='" BLUEZ_ADAPTER_INTERFACE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',"
+ "arg0='" BLUEZ_DEVICE_INTERFACE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',"
+ "arg0='" BLUEZ_MEDIA_ENDPOINT_INTERFACE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" BLUEZ_SERVICE "',"
+ "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',"
+ "arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", &err);
+
+ this->filters_added = true;
+
+ return;
+
+fail:
+ dbus_error_free(&err);
+}
+
+static int
+impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ struct spa_bt_monitor *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ add_filters(this);
+ get_managed_objects(this);
+
+ struct spa_bt_device *device;
+ spa_list_for_each(device, &this->device_list, link) {
+ if (device->added)
+ emit_device_info(this, device, this->connection_info_supported);
+ }
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct spa_bt_monitor *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct spa_bt_monitor *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct spa_bt_monitor *monitor;
+ struct spa_bt_adapter *a;
+ struct spa_bt_device *d;
+ struct spa_bt_remote_endpoint *ep;
+ struct spa_bt_transport *t;
+ const struct spa_dict_item *it;
+ size_t i;
+
+ monitor = (struct spa_bt_monitor *) handle;
+
+ /*
+ * We don't call BlueZ API unregister methods here, since BlueZ generally does the
+ * unregistration when the DBus connection is closed below. We'll unregister DBus
+ * object managers and filter callbacks though.
+ */
+
+ unregister_media_application(monitor);
+
+ if (monitor->filters_added) {
+ dbus_connection_remove_filter(monitor->conn, filter_cb, monitor);
+ monitor->filters_added = false;
+ }
+
+ if (monitor->get_managed_objects_call) {
+ dbus_pending_call_cancel(monitor->get_managed_objects_call);
+ dbus_pending_call_unref(monitor->get_managed_objects_call);
+ }
+
+ spa_list_consume(t, &monitor->transport_list, link)
+ spa_bt_transport_free(t);
+ spa_list_consume(ep, &monitor->remote_endpoint_list, link)
+ remote_endpoint_free(ep);
+ spa_list_consume(d, &monitor->device_list, link)
+ device_free(d);
+ spa_list_consume(a, &monitor->adapter_list, link)
+ adapter_free(a);
+
+ for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) {
+ spa_bt_backend_free(monitor->backends[i]);
+ monitor->backends[i] = NULL;
+ }
+
+ spa_dict_for_each(it, &monitor->global_settings) {
+ free((void *)it->key);
+ free((void *)it->value);
+ }
+
+ free((void*)monitor->enabled_codecs.items);
+ spa_zero(monitor->enabled_codecs);
+
+ dbus_connection_unref(monitor->conn);
+ spa_dbus_connection_destroy(monitor->dbus_connection);
+ monitor->dbus_connection = NULL;
+ monitor->conn = NULL;
+
+ monitor->objects_listed = false;
+
+ monitor->connection_info_supported = false;
+
+ monitor->backend = NULL;
+ monitor->backend_selection = BACKEND_NATIVE;
+
+ spa_bt_quirks_destroy(monitor->quirks);
+
+ free_media_codecs(monitor->media_codecs);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct spa_bt_monitor);
+}
+
+int spa_bt_profiles_from_json_array(const char *str)
+{
+ struct spa_json it, 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)
+ return -EINVAL;
+
+ while (spa_json_get_string(&it_array, role_name, sizeof(role_name)) > 0) {
+ if (spa_streq(role_name, "hsp_hs")) {
+ profiles |= SPA_BT_PROFILE_HSP_HS;
+ } else if (spa_streq(role_name, "hsp_ag")) {
+ profiles |= SPA_BT_PROFILE_HSP_AG;
+ } else if (spa_streq(role_name, "hfp_hf")) {
+ profiles |= SPA_BT_PROFILE_HFP_HF;
+ } else if (spa_streq(role_name, "hfp_ag")) {
+ profiles |= SPA_BT_PROFILE_HFP_AG;
+ } else if (spa_streq(role_name, "a2dp_sink")) {
+ profiles |= SPA_BT_PROFILE_A2DP_SINK;
+ } else if (spa_streq(role_name, "a2dp_source")) {
+ profiles |= SPA_BT_PROFILE_A2DP_SOURCE;
+ } else if (spa_streq(role_name, "bap_sink")) {
+ profiles |= SPA_BT_PROFILE_BAP_SINK;
+ } else if (spa_streq(role_name, "bap_source")) {
+ profiles |= SPA_BT_PROFILE_BAP_SOURCE;
+ }
+ }
+
+ return profiles;
+}
+
+static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict *info)
+{
+ 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;
+ char codec_name[256];
+ size_t num_codecs;
+ int i;
+
+ /* Parse bluez5.codecs property to a dict of enabled codecs */
+
+ num_codecs = 0;
+ while (media_codecs[num_codecs])
+ ++num_codecs;
+
+ codecs = calloc(num_codecs, sizeof(struct spa_dict_item));
+ if (codecs == NULL)
+ return -ENOMEM;
+
+ 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) {
+ spa_log_error(this->log, "property bluez5.codecs '%s' is not an array", str);
+ goto fallback;
+ }
+
+ this->enabled_codecs = SPA_DICT_INIT(codecs, 0);
+
+ while (spa_json_get_string(&it_array, codec_name, sizeof(codec_name)) > 0) {
+ int i;
+
+ for (i = 0; media_codecs[i]; ++i) {
+ const struct media_codec *codec = media_codecs[i];
+
+ if (!spa_streq(codec->name, codec_name))
+ continue;
+
+ if (spa_dict_lookup_item(&this->enabled_codecs, codec->name) != NULL)
+ continue;
+
+ spa_log_debug(this->log, "enabling codec %s", codec->name);
+
+ spa_assert(this->enabled_codecs.n_items < num_codecs);
+
+ codecs[this->enabled_codecs.n_items].key = codec->name;
+ codecs[this->enabled_codecs.n_items].value = "true";
+ ++this->enabled_codecs.n_items;
+
+ break;
+ }
+ }
+
+ spa_dict_qsort(&this->enabled_codecs);
+
+ for (i = 0; media_codecs[i]; ++i) {
+ const struct media_codec *codec = media_codecs[i];
+ if (!is_media_codec_enabled(this, codec))
+ spa_log_debug(this->log, "disabling codec %s", codec->name);
+ }
+ return 0;
+
+fallback:
+ for (i = 0; media_codecs[i]; ++i) {
+ const struct media_codec *codec = media_codecs[i];
+ spa_log_debug(this->log, "enabling codec %s", codec->name);
+ codecs[i].key = codec->name;
+ codecs[i].value = "true";
+ }
+ this->enabled_codecs = SPA_DICT_INIT(codecs, i);
+ spa_dict_qsort(&this->enabled_codecs);
+ return 0;
+}
+
+static void get_global_settings(struct spa_bt_monitor *this, const struct spa_dict *dict)
+{
+ uint32_t n_items = 0;
+ uint32_t i;
+
+ if (dict == NULL) {
+ this->global_settings = SPA_DICT_INIT(this->global_setting_items, 0);
+ return;
+ }
+
+ for (i = 0; i < dict->n_items && n_items < SPA_N_ELEMENTS(this->global_setting_items); i++) {
+ const struct spa_dict_item *it = &dict->items[i];
+ if (spa_strstartswith(it->key, "bluez5.") && it->value != NULL)
+ this->global_setting_items[n_items++] =
+ SPA_DICT_ITEM_INIT(strdup(it->key), strdup(it->value));
+ }
+
+ this->global_settings = SPA_DICT_INIT(this->global_setting_items, n_items);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct spa_bt_monitor *this;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct spa_bt_monitor *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+ this->plugin_loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->dbus == NULL) {
+ spa_log_error(this->log, "a dbus is needed");
+ return -EINVAL;
+ }
+
+ if (this->plugin_loader == NULL) {
+ spa_log_error(this->log, "a plugin loader is needed");
+ return -EINVAL;
+ }
+
+ this->media_codecs = NULL;
+ this->quirks = NULL;
+ this->conn = NULL;
+ this->dbus_connection = NULL;
+
+ this->media_codecs = load_media_codecs(this->plugin_loader, this->log);
+ if (this->media_codecs == NULL) {
+ spa_log_error(this->log, "failed to load required media codec plugins");
+ res = -EIO;
+ goto fail;
+ }
+
+ this->quirks = spa_bt_quirks_create(info, this->log);
+ if (this->quirks == NULL) {
+ spa_log_error(this->log, "failed to parse quirk table");
+ res = -EINVAL;
+ goto fail;
+ }
+
+ this->dbus_connection = spa_dbus_get_connection(this->dbus, SPA_DBUS_TYPE_SYSTEM);
+ if (this->dbus_connection == NULL) {
+ spa_log_error(this->log, "no dbus connection");
+ res = -EIO;
+ goto fail;
+ }
+ this->conn = spa_dbus_connection_get(this->dbus_connection);
+ if (this->conn == NULL) {
+ spa_log_error(this->log, "failed to get dbus connection");
+ res = -EIO;
+ goto fail;
+ }
+
+ /* 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(this->conn);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ spa_list_init(&this->adapter_list);
+ spa_list_init(&this->device_list);
+ spa_list_init(&this->remote_endpoint_list);
+ spa_list_init(&this->transport_list);
+
+ if ((res = parse_codec_array(this, info)) < 0)
+ goto fail;
+
+ this->default_audio_info.rate = A2DP_CODEC_DEFAULT_RATE;
+ this->default_audio_info.channels = A2DP_CODEC_DEFAULT_CHANNELS;
+
+ this->backend_selection = BACKEND_NATIVE;
+
+ get_global_settings(this, info);
+
+ if (info) {
+ const char *str;
+ uint32_t tmp;
+
+ if ((str = spa_dict_lookup(info, "api.bluez5.connection-info")) != NULL &&
+ spa_atob(str))
+ this->connection_info_supported = true;
+
+ if ((str = spa_dict_lookup(info, "bluez5.default.rate")) != NULL &&
+ (tmp = atoi(str)) > 0)
+ this->default_audio_info.rate = tmp;
+
+ if ((str = spa_dict_lookup(info, "bluez5.default.channels")) != NULL &&
+ ((tmp = atoi(str)) > 0))
+ this->default_audio_info.channels = tmp;
+
+ if ((str = spa_dict_lookup(info, "bluez5.hfphsp-backend")) != NULL) {
+ if (spa_streq(str, "none"))
+ this->backend_selection = BACKEND_NONE;
+ else if (spa_streq(str, "any"))
+ this->backend_selection = BACKEND_ANY;
+ else if (spa_streq(str, "ofono"))
+ this->backend_selection = BACKEND_OFONO;
+ else if (spa_streq(str, "hsphfpd"))
+ this->backend_selection = BACKEND_HSPHFPD;
+ else if (spa_streq(str, "native"))
+ this->backend_selection = BACKEND_NATIVE;
+ }
+
+ if ((str = spa_dict_lookup(info, "bluez5.dummy-avrcp-player")) != NULL)
+ this->dummy_avrcp_player = spa_atob(str);
+ else
+ this->dummy_avrcp_player = false;
+ }
+
+ register_media_application(this);
+
+ /* Create backends. They're started after we get a reply from Bluez. */
+ this->backends[BACKEND_NATIVE] = backend_native_new(this, this->conn, info, this->quirks, support, n_support);
+ this->backends[BACKEND_OFONO] = backend_ofono_new(this, this->conn, info, this->quirks, support, n_support);
+ this->backends[BACKEND_HSPHFPD] = backend_hsphfpd_new(this, this->conn, info, this->quirks, support, n_support);
+
+ return 0;
+
+fail:
+ if (this->media_codecs)
+ free_media_codecs(this->media_codecs);
+ if (this->quirks)
+ spa_bt_quirks_destroy(this->quirks);
+ if (this->conn)
+ dbus_connection_unref(this->conn);
+ if (this->dbus_connection)
+ spa_dbus_connection_destroy(this->dbus_connection);
+ this->media_codecs = NULL;
+ this->quirks = NULL;
+ this->conn = NULL;
+ this->dbus_connection = NULL;
+ return res;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+const struct spa_handle_factory spa_bluez5_dbus_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_ENUM_DBUS,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+
+// Report battery percentage to BlueZ using experimental (BlueZ 5.56) Battery Provider API. No-op if no changes occurred.
+int spa_bt_device_report_battery_level(struct spa_bt_device *device, uint8_t percentage)
+{
+ if (percentage == SPA_BT_NO_BATTERY) {
+ battery_remove(device);
+ return 0;
+ }
+
+ // BlueZ likely is running without battery provider support, don't try to report battery
+ if (device->adapter->battery_provider_unavailable) return 0;
+
+ // If everything is initialized and battery level has not changed we don't need to send anything to BlueZ
+ if (device->adapter->has_battery_provider && device->has_battery && device->battery == percentage) return 1;
+
+ device->battery = percentage;
+
+ if (!device->adapter->has_battery_provider) {
+ // No provider: register it, create battery when registered
+ register_battery_provider(device);
+ } else if (!device->has_battery) {
+ // Have provider but no battery: create battery with correct percentage
+ battery_create(device);
+ } else {
+ // Just update existing battery percentage
+ battery_update(device);
+ }
+
+ return 1;
+}
diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c
new file mode 100644
index 0000000..dcdfeaf
--- /dev/null
+++ b/spa/plugins/bluez5/bluez5-device.c
@@ -0,0 +1,2398 @@
+/* Spa Bluez5 Device
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <errno.h>
+
+#include <spa/support/log.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/support/i18n.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/monitor/event.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/bluetooth/audio.h>
+#include <spa/param/bluetooth/type-info.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#include "defs.h"
+#include "media-codecs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.device");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define MAX_DEVICES 64
+
+#define DEVICE_ID_SOURCE 0
+#define DEVICE_ID_SINK 1
+#define DYNAMIC_NODE_ID_FLAG 0x1000
+
+static struct spa_i18n *_i18n;
+
+#define _(_str) spa_i18n_text(_i18n,(_str))
+#define N_(_str) (_str)
+
+enum {
+ DEVICE_PROFILE_OFF = 0,
+ DEVICE_PROFILE_AG = 1,
+ DEVICE_PROFILE_A2DP = 2,
+ DEVICE_PROFILE_HSP_HFP = 3,
+ DEVICE_PROFILE_BAP = 4,
+ DEVICE_PROFILE_LAST = DEVICE_PROFILE_BAP,
+};
+
+struct props {
+ enum spa_bluetooth_audio_codec codec;
+ bool offload_active;
+};
+
+static void reset_props(struct props *props)
+{
+ props->codec = 0;
+ props->offload_active = false;
+}
+
+struct impl;
+
+struct node {
+ struct impl *impl;
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+ uint32_t id;
+ unsigned int active:1;
+ unsigned int mute:1;
+ unsigned int save:1;
+ unsigned int a2dp_duplex:1;
+ unsigned int offload_acquired:1;
+ uint32_t n_channels;
+ int64_t latency_offset;
+ uint32_t channels[SPA_AUDIO_MAX_CHANNELS];
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+ float soft_volumes[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct dynamic_node
+{
+ struct impl *impl;
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+ uint32_t id;
+ const char *factory_name;
+ bool a2dp_duplex;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+
+ uint32_t info_all;
+ struct spa_device_info info;
+#define IDX_EnumProfile 0
+#define IDX_Profile 1
+#define IDX_EnumRoute 2
+#define IDX_Route 3
+#define IDX_PropInfo 4
+#define IDX_Props 5
+ struct spa_param_info params[6];
+
+ struct spa_hook_list hooks;
+
+ struct props props;
+
+ struct spa_bt_device *bt_dev;
+ struct spa_hook bt_dev_listener;
+
+ uint32_t profile;
+ unsigned int switching_codec:1;
+ unsigned int save_profile:1;
+ uint32_t prev_bt_connected_profiles;
+
+ const struct media_codec **supported_codecs;
+ size_t supported_codec_count;
+
+ struct dynamic_node dyn_media_source;
+ struct dynamic_node dyn_media_sink;
+ struct dynamic_node dyn_sco_source;
+ struct dynamic_node dyn_sco_sink;
+
+#define MAX_SETTINGS 32
+ struct spa_dict_item setting_items[MAX_SETTINGS];
+ struct spa_dict setting_dict;
+
+ struct node nodes[2];
+};
+
+static void init_node(struct impl *this, struct node *node, uint32_t id)
+{
+ uint32_t i;
+
+ spa_zero(*node);
+ node->id = id;
+ for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) {
+ node->volumes[i] = 1.0f;
+ node->soft_volumes[i] = 1.0f;
+ }
+}
+
+static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec id, const struct media_codec **codecs, size_t size)
+{
+ const struct media_codec * const *c;
+
+ spa_assert(size > 0);
+ spa_assert(this->supported_codecs);
+
+ for (c = this->supported_codecs; *c && size > 1; ++c) {
+ if ((*c)->id == id || id == 0) {
+ *codecs++ = *c;
+ --size;
+ }
+ }
+
+ *codecs = NULL;
+}
+
+static const struct media_codec *get_supported_media_codec(struct impl *this, enum spa_bluetooth_audio_codec id, size_t *idx)
+{
+ const struct media_codec *media_codec = NULL;
+ size_t i;
+ for (i = 0; i < this->supported_codec_count; ++i) {
+ if (this->supported_codecs[i]->id == id) {
+ media_codec = this->supported_codecs[i];
+ if (idx)
+ *idx = i;
+ }
+ }
+ return media_codec;
+}
+
+static unsigned int get_hfp_codec(enum spa_bluetooth_audio_codec id)
+{
+ switch (id) {
+ case SPA_BLUETOOTH_AUDIO_CODEC_CVSD:
+ return HFP_AUDIO_CODEC_CVSD;
+ case SPA_BLUETOOTH_AUDIO_CODEC_MSBC:
+ return HFP_AUDIO_CODEC_MSBC;
+ default:
+ return 0;
+ }
+}
+
+static enum spa_bluetooth_audio_codec get_hfp_codec_id(unsigned int codec)
+{
+ switch (codec) {
+ case HFP_AUDIO_CODEC_MSBC:
+ return SPA_BLUETOOTH_AUDIO_CODEC_MSBC;
+ case HFP_AUDIO_CODEC_CVSD:
+ return SPA_BLUETOOTH_AUDIO_CODEC_CVSD;
+ }
+ return SPA_ID_INVALID;
+}
+
+static const char *get_hfp_codec_description(unsigned int codec)
+{
+ switch (codec) {
+ case HFP_AUDIO_CODEC_MSBC:
+ return "mSBC";
+ case HFP_AUDIO_CODEC_CVSD:
+ return "CVSD";
+ }
+ return "unknown";
+}
+
+static const char *get_hfp_codec_name(unsigned int codec)
+{
+ switch (codec) {
+ case HFP_AUDIO_CODEC_MSBC:
+ return "msbc";
+ case HFP_AUDIO_CODEC_CVSD:
+ return "cvsd";
+ }
+ return "unknown";
+}
+
+static const char *get_codec_name(struct spa_bt_transport *t, bool a2dp_duplex)
+{
+ if (t->media_codec != NULL) {
+ if (a2dp_duplex && t->media_codec->duplex_codec)
+ return t->media_codec->duplex_codec->name;
+ return t->media_codec->name;
+ }
+ return get_hfp_codec_name(t->codec);
+}
+
+static void transport_destroy(void *userdata)
+{
+ struct node *node = userdata;
+ node->transport = NULL;
+}
+
+static void emit_node_props(struct impl *this, struct node *node, bool full)
+{
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, node->id);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float, node->n_channels, node->volumes),
+ SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float, node->n_channels, node->soft_volumes),
+ SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, node->n_channels, node->channels));
+ if (full) {
+ spa_pod_builder_add(&b,
+ SPA_PROP_mute, SPA_POD_Bool(node->mute),
+ SPA_PROP_softMute, SPA_POD_Bool(node->mute),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(node->latency_offset),
+ 0);
+ }
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static void emit_volume(struct impl *this, struct node *node)
+{
+ emit_node_props(this, node, false);
+}
+
+static void emit_info(struct impl *this, bool full);
+
+static float get_soft_volume_boost(struct node *node)
+{
+ const struct media_codec *codec = node->transport ? node->transport->media_codec : NULL;
+
+ /*
+ * For A2DP duplex, the duplex microphone channel sometimes does not appear
+ * to have hardware gain, and input volume is very low.
+ *
+ * Work around this by boosting the software volume level, i.e. adjust
+ * the scale on the user-visible volume control to something more sensible.
+ * If this causes clipping, the user can just reduce the mic volume to
+ * bring SW gain below 1.
+ */
+ if (node->a2dp_duplex && node->transport && codec && codec->info &&
+ spa_atob(spa_dict_lookup(codec->info, "duplex.boost")) &&
+ node->id == DEVICE_ID_SOURCE &&
+ !node->transport->volumes[SPA_BT_VOLUME_ID_RX].active)
+ return 10.0f; /* 20 dB boost */
+
+ /* In all other cases, no boost */
+ return 1.0f;
+}
+
+static float node_get_hw_volume(struct node *node)
+{
+ uint32_t i;
+ float hw_volume = 0.0f;
+ for (i = 0; i < node->n_channels; i++)
+ hw_volume = SPA_MAX(node->volumes[i], hw_volume);
+ return SPA_MIN(hw_volume, 1.0f);
+}
+
+static void node_update_soft_volumes(struct node *node, float hw_volume)
+{
+ for (uint32_t i = 0; i < node->n_channels; ++i) {
+ node->soft_volumes[i] = hw_volume > 0.0f
+ ? node->volumes[i] / hw_volume
+ : 0.0f;
+ }
+}
+
+static bool node_update_volume_from_transport(struct node *node, bool reset)
+{
+ struct impl *impl = node->impl;
+ struct spa_bt_transport_volume *t_volume;
+ float prev_hw_volume;
+
+ if (!node->transport || !spa_bt_transport_volume_enabled(node->transport))
+ return false;
+
+ /* PW is the controller for remote device. */
+ if (impl->profile != DEVICE_PROFILE_A2DP
+ && impl->profile != DEVICE_PROFILE_BAP
+ && impl->profile != DEVICE_PROFILE_HSP_HFP)
+ return false;
+
+ t_volume = &node->transport->volumes[node->id];
+
+ if (!t_volume->active)
+ return false;
+
+ prev_hw_volume = node_get_hw_volume(node);
+
+ if (!reset) {
+ for (uint32_t i = 0; i < node->n_channels; ++i) {
+ node->volumes[i] = prev_hw_volume > 0.0f
+ ? node->volumes[i] * t_volume->volume / prev_hw_volume
+ : t_volume->volume;
+ }
+ } else {
+ for (uint32_t i = 0; i < node->n_channels; ++i)
+ node->volumes[i] = t_volume->volume;
+ }
+
+ node_update_soft_volumes(node, t_volume->volume);
+
+ /*
+ * Consider volume changes from the headset as requested
+ * by the user, and to be saved by the SM.
+ */
+ node->save = true;
+
+ return true;
+}
+
+static void volume_changed(void *userdata)
+{
+ struct node *node = userdata;
+ struct impl *impl = node->impl;
+
+ if (!node_update_volume_from_transport(node, false))
+ return;
+
+ emit_volume(impl, node);
+
+ impl->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ impl->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(impl, false);
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_DEVICE_EVENTS,
+ .destroy = transport_destroy,
+ .volume_changed = volume_changed,
+};
+
+static int node_offload_set_active(struct node *node, bool active)
+{
+ int res = 0;
+
+ if (node->transport == NULL || !node->active)
+ return -ENOTSUP;
+
+ if (active && !node->offload_acquired)
+ res = spa_bt_transport_acquire(node->transport, false);
+ else if (!active && node->offload_acquired)
+ res = spa_bt_transport_release(node->transport);
+
+ if (res >= 0)
+ node->offload_acquired = active;
+
+ return res;
+}
+
+static void get_channels(struct spa_bt_transport *t, bool a2dp_duplex, uint32_t *n_channels, uint32_t *channels)
+{
+ const struct media_codec *codec;
+ struct spa_audio_info info = { 0 };
+
+ if (!a2dp_duplex || !t->media_codec || !t->media_codec->duplex_codec) {
+ *n_channels = t->n_channels;
+ memcpy(channels, t->channels, t->n_channels * sizeof(uint32_t));
+ return;
+ }
+
+ codec = t->media_codec->duplex_codec;
+
+ if (!codec->validate_config ||
+ codec->validate_config(codec, 0,
+ t->configuration, t->configuration_len,
+ &info) < 0) {
+ *n_channels = 1;
+ channels[0] = SPA_AUDIO_CHANNEL_MONO;
+ return;
+ }
+
+ *n_channels = info.info.raw.channels;
+ memcpy(channels, info.info.raw.position,
+ info.info.raw.channels * sizeof(uint32_t));
+}
+
+static void emit_node(struct impl *this, struct spa_bt_transport *t,
+ uint32_t id, const char *factory_name, bool a2dp_duplex)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ struct spa_device_object_info info;
+ struct spa_dict_item items[8];
+ uint32_t n_items = 0;
+ char transport[32], str_id[32];
+ bool is_dyn_node = SPA_FLAG_IS_SET(id, DYNAMIC_NODE_ID_FLAG);
+
+ snprintf(transport, sizeof(transport), "pointer:%p", t);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_TRANSPORT, transport);
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PROFILE, spa_bt_profile_name(t->profile));
+ items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CODEC, get_codec_name(t, a2dp_duplex));
+ items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address);
+ items[4] = SPA_DICT_ITEM_INIT("device.routes", "1");
+ n_items = 5;
+ if (!is_dyn_node) {
+ snprintf(str_id, sizeof(str_id), "%d", id);
+ items[5] = SPA_DICT_ITEM_INIT("card.profile.device", str_id);
+ n_items++;
+ }
+ if (spa_streq(spa_bt_profile_name(t->profile), "headset-head-unit")) {
+ items[n_items] = SPA_DICT_ITEM_INIT("device.intended-roles", "Communication");
+ n_items++;
+ }
+ if (a2dp_duplex) {
+ items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.a2dp-duplex", "true");
+ n_items++;
+ }
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+ info.factory_name = factory_name;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.props = &SPA_DICT_INIT(items, n_items);
+
+ SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG);
+ spa_device_emit_object_info(&this->hooks, id, &info);
+
+ if (!is_dyn_node) {
+ uint32_t prev_channels = this->nodes[id].n_channels;
+ float boost;
+
+ this->nodes[id].impl = this;
+ this->nodes[id].active = true;
+ this->nodes[id].offload_acquired = false;
+ this->nodes[id].a2dp_duplex = a2dp_duplex;
+ get_channels(t, a2dp_duplex, &this->nodes[id].n_channels, this->nodes[id].channels);
+ if (this->nodes[id].transport)
+ spa_hook_remove(&this->nodes[id].transport_listener);
+ this->nodes[id].transport = t;
+ spa_bt_transport_add_listener(t, &this->nodes[id].transport_listener, &transport_events, &this->nodes[id]);
+
+ if (prev_channels > 0) {
+ size_t i;
+ /*
+ * Spread mono volume to all channels, if we had switched HFP -> A2DP.
+ * XXX: we should also use different route for hfp and a2dp
+ */
+ for (i = prev_channels; i < this->nodes[id].n_channels; ++i)
+ this->nodes[id].volumes[i] = this->nodes[id].volumes[i % prev_channels];
+ }
+
+ node_update_volume_from_transport(&this->nodes[id], true);
+
+ boost = get_soft_volume_boost(&this->nodes[id]);
+ if (boost != 1.0f) {
+ size_t i;
+ for (i = 0; i < this->nodes[id].n_channels; ++i)
+ this->nodes[id].soft_volumes[i] = this->nodes[id].volumes[i] * boost;
+ }
+
+ emit_node_props(this, &this->nodes[id], true);
+ }
+}
+
+static struct spa_bt_transport *find_transport(struct impl *this, int profile, enum spa_bluetooth_audio_codec codec)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ struct spa_bt_transport *t;
+
+ spa_list_for_each(t, &device->transport_list, device_link) {
+ bool codec_ok = codec == 0 ||
+ (t->media_codec != NULL && t->media_codec->id == codec) ||
+ get_hfp_codec_id(t->codec) == codec;
+
+ if ((t->profile & device->connected_profiles) &&
+ (t->profile & profile) == t->profile &&
+ codec_ok)
+ return t;
+ }
+
+ return NULL;
+}
+
+static void dynamic_node_transport_destroy(void *data)
+{
+ struct dynamic_node *this = data;
+ spa_log_debug(this->impl->log, "transport %p destroy", this->transport);
+ this->transport = NULL;
+}
+
+static void dynamic_node_transport_state_changed(void *data,
+ enum spa_bt_transport_state old,
+ enum spa_bt_transport_state state)
+{
+ struct dynamic_node *this = data;
+ struct impl *impl = this->impl;
+ struct spa_bt_transport *t = this->transport;
+
+ spa_log_debug(impl->log, "transport %p state %d->%d", t, old, state);
+
+ if (state >= SPA_BT_TRANSPORT_STATE_PENDING && old < SPA_BT_TRANSPORT_STATE_PENDING) {
+ if (!SPA_FLAG_IS_SET(this->id, DYNAMIC_NODE_ID_FLAG)) {
+ SPA_FLAG_SET(this->id, DYNAMIC_NODE_ID_FLAG);
+ spa_bt_transport_keepalive(t, true);
+ emit_node(impl, t, this->id, this->factory_name, this->a2dp_duplex);
+ }
+ } else if (state < SPA_BT_TRANSPORT_STATE_PENDING && old >= SPA_BT_TRANSPORT_STATE_PENDING) {
+ if (SPA_FLAG_IS_SET(this->id, DYNAMIC_NODE_ID_FLAG)) {
+ SPA_FLAG_CLEAR(this->id, DYNAMIC_NODE_ID_FLAG);
+ spa_bt_transport_keepalive(t, false);
+ spa_device_emit_object_info(&impl->hooks, this->id, NULL);
+ }
+ }
+}
+
+static void dynamic_node_volume_changed(void *data)
+{
+ struct dynamic_node *node = data;
+ struct impl *impl = node->impl;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ struct spa_bt_transport_volume *t_volume;
+ int id = node->id, volume_id;
+
+ SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG);
+
+ /* Remote device is the controller */
+ if (!node->transport || impl->profile != DEVICE_PROFILE_AG
+ || !spa_bt_transport_volume_enabled(node->transport))
+ return;
+
+ if (id == 0 || id == 2)
+ volume_id = SPA_BT_VOLUME_ID_RX;
+ else if (id == 1)
+ volume_id = SPA_BT_VOLUME_ID_TX;
+ else
+ return;
+
+ t_volume = &node->transport->volumes[volume_id];
+ if (!t_volume->active)
+ return;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, id);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_volume, SPA_POD_Float(t_volume->volume));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_log_debug(impl->log, "dynamic node %p: volume %d changed %f, profile %d",
+ node, volume_id, t_volume->volume, node->transport->profile);
+
+ /* Dynamic node doesn't has route, we can only set volume on adaptar node. */
+ spa_device_emit_event(&impl->hooks, event);
+}
+
+static const struct spa_bt_transport_events dynamic_node_transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = dynamic_node_transport_destroy,
+ .state_changed = dynamic_node_transport_state_changed,
+ .volume_changed = dynamic_node_volume_changed,
+};
+
+static void emit_dynamic_node(struct dynamic_node *this, struct impl *impl,
+ struct spa_bt_transport *t, uint32_t id, const char *factory_name, bool a2dp_duplex)
+{
+ spa_log_debug(impl->log, "dynamic node, transport: %p->%p id: %08x->%08x",
+ this->transport, t, this->id, id);
+
+ if (this->transport) {
+ /* Session manager don't really handles transport ptr changing. */
+ spa_assert(this->transport == t);
+ spa_hook_remove(&this->transport_listener);
+ }
+
+ this->impl = impl;
+ this->transport = t;
+ this->id = id;
+ this->factory_name = factory_name;
+ this->a2dp_duplex = a2dp_duplex;
+
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &dynamic_node_transport_events, this);
+
+ /* emits the node if the state is already pending */
+ dynamic_node_transport_state_changed (this, SPA_BT_TRANSPORT_STATE_IDLE, t->state);
+}
+
+static void remove_dynamic_node(struct dynamic_node *this)
+{
+ if (this->transport == NULL)
+ return;
+
+ /* destroy the node, if it exists */
+ dynamic_node_transport_state_changed (this, this->transport->state,
+ SPA_BT_TRANSPORT_STATE_IDLE);
+
+ spa_hook_remove(&this->transport_listener);
+ this->impl = NULL;
+ this->transport = NULL;
+ this->id = 0;
+ this->factory_name = NULL;
+}
+
+static int emit_nodes(struct impl *this)
+{
+ struct spa_bt_transport *t;
+
+ switch (this->profile) {
+ case DEVICE_PROFILE_OFF:
+ break;
+ case DEVICE_PROFILE_AG:
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) {
+ t = find_transport(this, SPA_BT_PROFILE_HFP_AG, 0);
+ if (!t)
+ t = find_transport(this, SPA_BT_PROFILE_HSP_AG, 0);
+ if (t) {
+ if (t->profile == SPA_BT_PROFILE_HSP_AG)
+ this->props.codec = 0;
+ else
+ this->props.codec = get_hfp_codec_id(t->codec);
+ emit_dynamic_node(&this->dyn_sco_source, this, t,
+ 0, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false);
+ emit_dynamic_node(&this->dyn_sco_sink, this, t,
+ 1, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
+ }
+ }
+ if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_A2DP_SOURCE)) {
+ t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ emit_dynamic_node(&this->dyn_media_source, this, t,
+ 2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
+
+ if (t->media_codec->duplex_codec) {
+ emit_dynamic_node(&this->dyn_media_sink, this, t,
+ 3, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
+ }
+ }
+ }
+ 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, 0);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ emit_dynamic_node(&this->dyn_media_source, this, t,
+ DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false);
+
+ if (t->media_codec->duplex_codec) {
+ emit_node(this, t,
+ DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, true);
+ }
+ }
+ }
+
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) {
+ t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK, this->props.codec);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, false);
+
+ if (t->media_codec->duplex_codec) {
+ emit_node(this, t,
+ DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, true);
+ }
+ }
+ }
+
+ if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
+ this->props.codec = 0;
+ break;
+ case DEVICE_PROFILE_BAP:
+ if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SOURCE)) {
+ t = find_transport(this, SPA_BT_PROFILE_BAP_SOURCE, 0);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ if (t->bap_initiator)
+ emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
+ else
+ emit_dynamic_node(&this->dyn_media_source, this, t,
+ DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false);
+ }
+ }
+
+ if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SINK)) {
+ t = find_transport(this, SPA_BT_PROFILE_BAP_SINK, this->props.codec);
+ if (t) {
+ this->props.codec = t->media_codec->id;
+ if (t->bap_initiator)
+ emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
+ else
+ emit_dynamic_node(&this->dyn_media_sink, this, t,
+ DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false);
+ }
+ }
+
+ if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
+ this->props.codec = 0;
+ break;
+ case DEVICE_PROFILE_HSP_HFP:
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) {
+ t = find_transport(this, SPA_BT_PROFILE_HFP_HF, this->props.codec);
+ if (!t)
+ t = find_transport(this, SPA_BT_PROFILE_HSP_HS, 0);
+ if (t) {
+ if (t->profile == SPA_BT_PROFILE_HSP_HS)
+ this->props.codec = 0;
+ else
+ this->props.codec = get_hfp_codec_id(t->codec);
+ emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false);
+ emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_SCO_SINK, false);
+ }
+ }
+
+ if (spa_bt_device_supports_hfp_codec(this->bt_dev, get_hfp_codec(this->props.codec)) != 1)
+ this->props.codec = 0;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_DEVICE_BUS, "bluetooth" },
+ { SPA_KEY_MEDIA_CLASS, "Audio/Device" },
+};
+
+static void emit_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(info_items);
+
+ spa_device_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_remove_nodes(struct impl *this)
+{
+ remove_dynamic_node (&this->dyn_media_source);
+ remove_dynamic_node (&this->dyn_media_sink);
+ remove_dynamic_node (&this->dyn_sco_source);
+ remove_dynamic_node (&this->dyn_sco_sink);
+
+ for (uint32_t i = 0; i < 2; i++) {
+ struct node * node = &this->nodes[i];
+ node_offload_set_active(node, false);
+ if (node->transport) {
+ spa_hook_remove(&node->transport_listener);
+ node->transport = NULL;
+ }
+ if (node->active) {
+ spa_device_emit_object_info(&this->hooks, i, NULL);
+ node->active = false;
+ }
+ }
+
+ this->props.offload_active = false;
+}
+
+static bool validate_profile(struct impl *this, uint32_t profile,
+ enum spa_bluetooth_audio_codec codec);
+
+static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec, bool save)
+{
+ if (!validate_profile(this, profile, codec)) {
+ spa_log_warn(this->log, "trying to set invalid profile %d, codec %d, %08x %08x",
+ profile, codec,
+ this->bt_dev->profiles, this->bt_dev->connected_profiles);
+ return -EINVAL;
+ }
+
+ this->save_profile = save;
+
+ if (this->profile == profile &&
+ (this->profile != DEVICE_PROFILE_A2DP || codec == this->props.codec) &&
+ (this->profile != DEVICE_PROFILE_BAP || codec == this->props.codec) &&
+ (this->profile != DEVICE_PROFILE_HSP_HFP || codec == this->props.codec))
+ return 0;
+
+ emit_remove_nodes(this);
+
+ spa_bt_device_release_transports(this->bt_dev);
+
+ this->profile = profile;
+ this->prev_bt_connected_profiles = this->bt_dev->connected_profiles;
+ this->props.codec = codec;
+
+ /*
+ * A2DP/BAP: ensure there's a transport with the selected codec (0 means any).
+ * Don't try to switch codecs when the device is in the A2DP source role, since
+ * devices do not appear to like that.
+ */
+ if ((profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP)
+ && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)) {
+ int ret;
+ const struct media_codec *codecs[64];
+
+ get_media_codecs(this, codec, codecs, SPA_N_ELEMENTS(codecs));
+
+ this->switching_codec = true;
+
+ ret = spa_bt_device_ensure_media_codec(this->bt_dev, codecs);
+ if (ret < 0) {
+ if (ret != -ENOTSUP)
+ spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret);
+ } else {
+ return 0;
+ }
+ } else if (profile == DEVICE_PROFILE_HSP_HFP && get_hfp_codec(codec) && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG)) {
+ int ret;
+
+ this->switching_codec = true;
+
+ ret = spa_bt_device_ensure_hfp_codec(this->bt_dev, get_hfp_codec(codec));
+ if (ret < 0) {
+ if (ret != -ENOTSUP)
+ spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret);
+ } else {
+ return 0;
+ }
+ }
+
+ this->switching_codec = false;
+ this->props.codec = 0;
+ emit_nodes(this);
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+
+ return 0;
+}
+
+static void codec_switched(void *userdata, int status)
+{
+ struct impl *this = userdata;
+
+ spa_log_debug(this->log, "codec switched (status %d)", status);
+
+ this->switching_codec = false;
+
+ if (status < 0) {
+ /* Failed to switch: return to a fallback profile */
+ spa_log_error(this->log, "failed to switch codec (%d), setting fallback profile", status);
+ if (this->profile == DEVICE_PROFILE_A2DP && this->props.codec != 0) {
+ this->props.codec = 0;
+ } else if (this->profile == DEVICE_PROFILE_BAP && this->props.codec != 0) {
+ this->props.codec = 0;
+ } else if (this->profile == DEVICE_PROFILE_HSP_HFP && this->props.codec != 0) {
+ this->props.codec = 0;
+ } else {
+ this->profile = DEVICE_PROFILE_OFF;
+ }
+ }
+
+ emit_remove_nodes(this);
+ emit_nodes(this);
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ if (this->prev_bt_connected_profiles != this->bt_dev->connected_profiles)
+ this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+}
+
+static void profiles_changed(void *userdata, uint32_t prev_profiles, uint32_t prev_connected_profiles)
+{
+ 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);
+
+ if (this->switching_codec)
+ return;
+
+ if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_MEDIA_SINK) {
+ free(this->supported_codecs);
+ this->supported_codecs = spa_bt_device_get_supported_media_codecs(
+ this->bt_dev, &this->supported_codec_count, true);
+ }
+
+ switch (this->profile) {
+ case DEVICE_PROFILE_OFF:
+ /* Noop */
+ nodes_changed = false;
+ break;
+ case DEVICE_PROFILE_AG:
+ nodes_changed = (connected_change & (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY |
+ SPA_BT_PROFILE_MEDIA_SOURCE));
+ spa_log_debug(this->log, "profiles changed: AG nodes changed: %d",
+ nodes_changed);
+ break;
+ case DEVICE_PROFILE_A2DP:
+ case DEVICE_PROFILE_BAP:
+ if (get_supported_media_codec(this, this->props.codec, NULL) == NULL)
+ this->props.codec = 0;
+ nodes_changed = (connected_change & (SPA_BT_PROFILE_MEDIA_SINK |
+ SPA_BT_PROFILE_MEDIA_SOURCE));
+ spa_log_debug(this->log, "profiles changed: media nodes changed: %d",
+ nodes_changed);
+ break;
+ case DEVICE_PROFILE_HSP_HFP:
+ if (spa_bt_device_supports_hfp_codec(this->bt_dev, get_hfp_codec(this->props.codec)) != 1)
+ this->props.codec = 0;
+ nodes_changed = (connected_change & SPA_BT_PROFILE_HEADSET_HEAD_UNIT);
+ spa_log_debug(this->log, "profiles changed: HSP/HFP nodes changed: %d",
+ nodes_changed);
+ break;
+ }
+
+ if (nodes_changed) {
+ emit_remove_nodes(this);
+ emit_nodes(this);
+ }
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; /* Profile changes may affect routes */
+ this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+}
+
+static void set_initial_profile(struct impl *this);
+
+static void device_connected(void *userdata, bool connected) {
+ struct impl *this = userdata;
+
+ spa_log_debug(this->log, "connected: %d", connected);
+
+ if (connected ^ (this->profile != DEVICE_PROFILE_OFF))
+ set_initial_profile(this);
+}
+
+static const struct spa_bt_device_events bt_dev_events = {
+ SPA_VERSION_BT_DEVICE_EVENTS,
+ .connected = device_connected,
+ .codec_switched = codec_switched,
+ .profiles_changed = profiles_changed,
+};
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ if (events->info)
+ emit_info(this, true);
+
+ if (events->object_info)
+ emit_nodes(this);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum spa_bluetooth_audio_codec codec)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ uint32_t mask;
+ bool have_output = false, have_input = false;
+ const struct media_codec *media_codec;
+
+ switch (index) {
+ case DEVICE_PROFILE_A2DP:
+ case DEVICE_PROFILE_BAP:
+ if (device->connected_profiles & SPA_BT_PROFILE_MEDIA_SINK)
+ have_output = true;
+
+ media_codec = get_supported_media_codec(this, codec, NULL);
+ if (media_codec && media_codec->duplex_codec)
+ have_input = true;
+ break;
+ case DEVICE_PROFILE_HSP_HFP:
+ if (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)
+ have_output = have_input = true;
+ break;
+ default:
+ break;
+ }
+
+ mask = 0;
+ if (have_output)
+ mask |= 1 << SPA_DIRECTION_OUTPUT;
+ if (have_input)
+ mask |= 1 << SPA_DIRECTION_INPUT;
+ return mask;
+}
+
+static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32_t *next, enum spa_bluetooth_audio_codec *codec)
+{
+ /*
+ * XXX: The codecs should probably become a separate param, and not have
+ * XXX: separate profiles for each one.
+ */
+
+ *codec = 0;
+ *next = index + 1;
+
+ if (index <= DEVICE_PROFILE_LAST) {
+ return index;
+ } else if (index != SPA_ID_INVALID) {
+ const struct spa_type_info *info;
+ uint32_t profile;
+
+ *codec = index - DEVICE_PROFILE_LAST;
+ *next = SPA_ID_INVALID;
+
+ for (info = spa_type_bluetooth_audio_codec; info->type; ++info)
+ if (info->type > *codec)
+ *next = SPA_MIN(info->type + 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;
+
+ return profile;
+ }
+
+ *next = SPA_ID_INVALID;
+ return SPA_ID_INVALID;
+}
+
+static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec)
+{
+ if (profile == DEVICE_PROFILE_OFF || profile == DEVICE_PROFILE_AG)
+ return profile;
+
+ if (profile == DEVICE_PROFILE_A2DP) {
+ if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_MEDIA_SOURCE))
+ return profile;
+
+ return codec + DEVICE_PROFILE_LAST;
+ }
+
+ if (profile == DEVICE_PROFILE_BAP) {
+ if (codec == 0)
+ return profile;
+
+ return codec + DEVICE_PROFILE_LAST;
+ }
+
+ if (profile == DEVICE_PROFILE_HSP_HFP) {
+ if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG))
+ return profile;
+
+ return codec + DEVICE_PROFILE_LAST;
+ }
+
+ return SPA_ID_INVALID;
+}
+
+static bool set_initial_hsp_hfp_profile(struct impl *this)
+{
+ struct spa_bt_transport *t;
+ int i;
+
+ for (i = SPA_BT_PROFILE_HSP_HS; i <= SPA_BT_PROFILE_HFP_AG; i <<= 1) {
+ if (!(this->bt_dev->connected_profiles & i))
+ continue;
+
+ t = find_transport(this, i, 0);
+ if (t) {
+ this->profile = (i & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) ?
+ DEVICE_PROFILE_AG : DEVICE_PROFILE_HSP_HFP;
+ this->props.codec = get_hfp_codec_id(t->codec);
+
+ spa_log_debug(this->log, "initial profile HSP/HFP profile:%d codec:%d",
+ this->profile, this->props.codec);
+ return true;
+ }
+ }
+ return false;
+}
+
+static void set_initial_profile(struct impl *this)
+{
+ struct spa_bt_transport *t;
+ int i;
+
+ this->switching_codec = false;
+
+ if (this->supported_codecs)
+ free(this->supported_codecs);
+ this->supported_codecs = spa_bt_device_get_supported_media_codecs(
+ this->bt_dev, &this->supported_codec_count, true);
+
+ /* Prefer BAP, then A2DP, then HFP, then null, but select AG if the device
+ appears not to have BAP_SINK, A2DP_SINK or any HEAD_UNIT profile */
+
+ /* If default profile is set to HSP/HFP, first try those and exit if found. */
+ if (this->bt_dev->settings != NULL) {
+ const char *str = spa_dict_lookup(this->bt_dev->settings, "bluez5.profile");
+ 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) {
+ if (!(this->bt_dev->connected_profiles & i))
+ continue;
+
+ t = find_transport(this, i, 0);
+ if (t) {
+ if (i == SPA_BT_PROFILE_A2DP_SOURCE || i == SPA_BT_PROFILE_BAP_SOURCE)
+ this->profile = DEVICE_PROFILE_AG;
+ else if (i == SPA_BT_PROFILE_BAP_SINK)
+ this->profile = DEVICE_PROFILE_BAP;
+ else
+ this->profile = DEVICE_PROFILE_A2DP;
+ this->props.codec = t->media_codec->id;
+ spa_log_debug(this->log, "initial profile media profile:%d codec:%d",
+ this->profile, this->props.codec);
+ return;
+ }
+ }
+
+ if (set_initial_hsp_hfp_profile(this))
+ return;
+
+off:
+ spa_log_debug(this->log, "initial profile off");
+
+ this->profile = DEVICE_PROFILE_OFF;
+ this->props.codec = 0;
+}
+
+static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b,
+ uint32_t id, uint32_t index, uint32_t profile_index, enum spa_bluetooth_audio_codec codec,
+ bool current)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ struct spa_pod_frame f[2];
+ const char *name, *desc;
+ char *name_and_codec = NULL;
+ char *desc_and_codec = NULL;
+ uint32_t n_source = 0, n_sink = 0;
+ uint32_t capture[1] = { DEVICE_ID_SOURCE }, playback[1] = { DEVICE_ID_SINK };
+ int priority;
+
+ switch (profile_index) {
+ case DEVICE_PROFILE_OFF:
+ name = "off";
+ desc = _("Off");
+ priority = 0;
+ break;
+ case DEVICE_PROFILE_AG:
+ {
+ uint32_t profile = device->connected_profiles &
+ (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+ if (profile == 0) {
+ return NULL;
+ } else {
+ name = "audio-gateway";
+ desc = _("Audio Gateway (A2DP Source & HSP/HFP AG)");
+ }
+ priority = 256;
+ break;
+ }
+ case DEVICE_PROFILE_A2DP:
+ {
+ /* make this device profile visible only if there is an A2DP sink */
+ uint32_t profile = device->connected_profiles &
+ (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE);
+ if (!(profile & SPA_BT_PROFILE_A2DP_SINK)) {
+ return NULL;
+ }
+ name = spa_bt_profile_name(profile);
+ n_sink++;
+ if (codec) {
+ size_t idx;
+ const struct media_codec *media_codec = get_supported_media_codec(this, codec, &idx);
+ if (media_codec == NULL) {
+ errno = EINVAL;
+ return NULL;
+ }
+ name_and_codec = spa_aprintf("%s-%s", name, media_codec->name);
+ name = name_and_codec;
+ if (profile == SPA_BT_PROFILE_A2DP_SINK && !media_codec->duplex_codec) {
+ desc_and_codec = spa_aprintf(_("High Fidelity Playback (A2DP Sink, codec %s)"),
+ media_codec->description);
+ } else {
+ desc_and_codec = spa_aprintf(_("High Fidelity Duplex (A2DP Source/Sink, codec %s)"),
+ media_codec->description);
+
+ }
+ desc = desc_and_codec;
+ priority = 16 + this->supported_codec_count - idx; /* order as in codec list */
+ } else {
+ if (profile == SPA_BT_PROFILE_A2DP_SINK) {
+ desc = _("High Fidelity Playback (A2DP Sink)");
+ } else {
+ desc = _("High Fidelity Duplex (A2DP Source/Sink)");
+ }
+ priority = 16;
+ }
+ break;
+ }
+ case DEVICE_PROFILE_BAP:
+ {
+ uint32_t profile = device->connected_profiles &
+ (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE);
+ size_t idx;
+ const struct media_codec *media_codec;
+
+ if (profile == 0)
+ return NULL;
+
+ if (!codec) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ if (profile & (SPA_BT_PROFILE_BAP_SINK))
+ n_sink++;
+ if (profile & (SPA_BT_PROFILE_BAP_SOURCE))
+ n_source++;
+
+ name = spa_bt_profile_name(profile);
+
+ media_codec = get_supported_media_codec(this, codec, &idx);
+ if (media_codec == NULL) {
+ errno = EINVAL;
+ return NULL;
+ }
+ name_and_codec = spa_aprintf("%s-%s", name, media_codec->name);
+ name = name_and_codec;
+ switch (profile) {
+ case SPA_BT_PROFILE_BAP_SINK:
+ desc_and_codec = spa_aprintf(_("High Fidelity Playback (BAP Sink, codec %s)"),
+ media_codec->description);
+ break;
+ case SPA_BT_PROFILE_BAP_SOURCE:
+ desc_and_codec = spa_aprintf(_("High Fidelity Input (BAP Source, codec %s)"),
+ media_codec->description);
+ break;
+ default:
+ desc_and_codec = spa_aprintf(_("High Fidelity Duplex (BAP Source/Sink, codec %s)"),
+ media_codec->description);
+ }
+ desc = desc_and_codec;
+ priority = 128 + this->supported_codec_count - idx; /* order as in codec list */
+ break;
+ }
+ case DEVICE_PROFILE_HSP_HFP:
+ {
+ /* make this device profile visible only if there is a head unit */
+ uint32_t profile = device->connected_profiles &
+ SPA_BT_PROFILE_HEADSET_HEAD_UNIT;
+ if (profile == 0) {
+ return NULL;
+ }
+ name = spa_bt_profile_name(profile);
+ n_source++;
+ n_sink++;
+ if (codec) {
+ bool codec_ok = !(profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+ unsigned int hfp_codec = get_hfp_codec(codec);
+ if (spa_bt_device_supports_hfp_codec(this->bt_dev, hfp_codec) != 1)
+ codec_ok = false;
+ if (!codec_ok) {
+ errno = EINVAL;
+ return NULL;
+ }
+ name_and_codec = spa_aprintf("%s-%s", name, get_hfp_codec_name(hfp_codec));
+ name = name_and_codec;
+ desc_and_codec = spa_aprintf(_("Headset Head Unit (HSP/HFP, codec %s)"),
+ get_hfp_codec_description(hfp_codec));
+ desc = desc_and_codec;
+ priority = 1 + hfp_codec; /* prefer msbc over cvsd */
+ } else {
+ desc = _("Headset Head Unit (HSP/HFP)");
+ priority = 1;
+ }
+ break;
+ }
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(name),
+ SPA_PARAM_PROFILE_description, SPA_POD_String(desc),
+ SPA_PARAM_PROFILE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes),
+ SPA_PARAM_PROFILE_priority, SPA_POD_Int(priority),
+ 0);
+ if (n_source > 0 || n_sink > 0) {
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ if (n_source > 0) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Source"),
+ SPA_POD_Int(n_source),
+ SPA_POD_String("card.profile.devices"),
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, 1, capture));
+ }
+ if (n_sink > 0) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Sink"),
+ SPA_POD_Int(n_sink),
+ SPA_POD_String("card.profile.devices"),
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, 1, playback));
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ if (current) {
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_save, 0);
+ spa_pod_builder_bool(b, this->save_profile);
+ }
+
+ if (name_and_codec)
+ free(name_and_codec);
+ if (desc_and_codec)
+ free(desc_and_codec);
+
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static bool validate_profile(struct impl *this, uint32_t profile,
+ enum spa_bluetooth_audio_codec codec)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ return (build_profile(this, &b, 0, 0, profile, codec, false) != NULL);
+}
+
+static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b,
+ uint32_t id, uint32_t port, uint32_t profile)
+{
+ struct spa_bt_device *device = this->bt_dev;
+ struct spa_pod_frame f[2];
+ enum spa_direction direction;
+ const char *name_prefix, *description, *hfp_description, *port_type;
+ enum spa_bt_form_factor ff;
+ enum spa_bluetooth_audio_codec codec;
+ char name[128];
+ uint32_t i, j, mask, next;
+ uint32_t dev = SPA_ID_INVALID, enum_dev;
+
+ ff = spa_bt_form_factor_from_class(device->bluetooth_class);
+
+ switch (ff) {
+ case SPA_BT_FORM_FACTOR_HEADSET:
+ name_prefix = "headset";
+ description = _("Headset");
+ hfp_description = _("Handsfree");
+ port_type = "headset";
+ break;
+ case SPA_BT_FORM_FACTOR_HANDSFREE:
+ name_prefix = "handsfree";
+ description = _("Handsfree");
+ hfp_description = _("Handsfree (HFP)");
+ port_type = "handsfree";
+ break;
+ case SPA_BT_FORM_FACTOR_MICROPHONE:
+ name_prefix = "microphone";
+ description = _("Microphone");
+ hfp_description = _("Handsfree");
+ port_type = "mic";
+ break;
+ case SPA_BT_FORM_FACTOR_SPEAKER:
+ name_prefix = "speaker";
+ description = _("Speaker");
+ hfp_description = _("Handsfree");
+ port_type = "speaker";
+ break;
+ case SPA_BT_FORM_FACTOR_HEADPHONE:
+ name_prefix = "headphone";
+ description = _("Headphone");
+ hfp_description = _("Handsfree");
+ port_type = "headphones";
+ break;
+ case SPA_BT_FORM_FACTOR_PORTABLE:
+ name_prefix = "portable";
+ description = _("Portable");
+ hfp_description = _("Handsfree");
+ port_type = "portable";
+ break;
+ case SPA_BT_FORM_FACTOR_CAR:
+ name_prefix = "car";
+ description = _("Car");
+ hfp_description = _("Handsfree");
+ port_type = "car";
+ break;
+ case SPA_BT_FORM_FACTOR_HIFI:
+ name_prefix = "hifi";
+ description = _("HiFi");
+ hfp_description = _("Handsfree");
+ port_type = "hifi";
+ break;
+ case SPA_BT_FORM_FACTOR_PHONE:
+ name_prefix = "phone";
+ description = _("Phone");
+ hfp_description = _("Handsfree");
+ port_type = "phone";
+ break;
+ case SPA_BT_FORM_FACTOR_UNKNOWN:
+ default:
+ name_prefix = "bluetooth";
+ description = _("Bluetooth");
+ hfp_description = _("Bluetooth (HFP)");
+ port_type = "bluetooth";
+ break;
+ }
+
+ switch (port) {
+ case 0:
+ direction = SPA_DIRECTION_INPUT;
+ snprintf(name, sizeof(name), "%s-input", name_prefix);
+ enum_dev = DEVICE_ID_SOURCE;
+ if (profile == DEVICE_PROFILE_A2DP)
+ dev = enum_dev;
+ else if (profile != SPA_ID_INVALID)
+ enum_dev = SPA_ID_INVALID;
+ break;
+ case 1:
+ direction = SPA_DIRECTION_OUTPUT;
+ snprintf(name, sizeof(name), "%s-output", name_prefix);
+ enum_dev = DEVICE_ID_SINK;
+ if (profile == DEVICE_PROFILE_A2DP)
+ dev = enum_dev;
+ else if (profile != SPA_ID_INVALID)
+ enum_dev = SPA_ID_INVALID;
+ break;
+ case 2:
+ direction = SPA_DIRECTION_INPUT;
+ snprintf(name, sizeof(name), "%s-hf-input", name_prefix);
+ description = hfp_description;
+ enum_dev = DEVICE_ID_SOURCE;
+ if (profile == DEVICE_PROFILE_HSP_HFP)
+ dev = enum_dev;
+ else if (profile != SPA_ID_INVALID)
+ enum_dev = SPA_ID_INVALID;
+ break;
+ case 3:
+ direction = SPA_DIRECTION_OUTPUT;
+ snprintf(name, sizeof(name), "%s-hf-output", name_prefix);
+ description = hfp_description;
+ enum_dev = DEVICE_ID_SINK;
+ if (profile == DEVICE_PROFILE_HSP_HFP)
+ dev = enum_dev;
+ else if (profile != SPA_ID_INVALID)
+ enum_dev = SPA_ID_INVALID;
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ if (enum_dev == SPA_ID_INVALID) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(port),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(direction),
+ SPA_PARAM_ROUTE_name, SPA_POD_String(name),
+ SPA_PARAM_ROUTE_description, SPA_POD_String(description),
+ SPA_PARAM_ROUTE_priority, SPA_POD_Int(0),
+ SPA_PARAM_ROUTE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes),
+ 0);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_info, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, 1);
+ spa_pod_builder_add(b,
+ SPA_POD_String("port.type"),
+ SPA_POD_String(port_type),
+ NULL);
+ spa_pod_builder_pop(b, &f[1]);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0);
+ 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) {
+ uint32_t profile_mask;
+
+ if (j == DEVICE_PROFILE_A2DP && !(port == 0 || port == 1))
+ continue;
+ if (j == DEVICE_PROFILE_HSP_HFP && !(port == 2 || port == 3))
+ continue;
+
+ profile_mask = profile_direction_mask(this, j, codec);
+ if (!(profile_mask & (1 << direction)))
+ continue;
+
+ /* Check the profile actually exists */
+ if (!validate_profile(this, j, codec))
+ continue;
+
+ mask |= profile_mask;
+ spa_pod_builder_int(b, i);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (!(mask & (1 << direction))) {
+ /* No profile has route direction */
+ return NULL;
+ }
+
+ if (dev != SPA_ID_INVALID) {
+ struct node *node = &this->nodes[dev];
+ struct spa_bt_transport_volume *t_volume;
+
+ mask = profile_direction_mask(this, this->profile, this->props.codec);
+ if (!(mask & (1 << direction)))
+ return NULL;
+
+ t_volume = node->transport
+ ? &node->transport->volumes[node->id]
+ : NULL;
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_device, 0);
+ spa_pod_builder_int(b, dev);
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_props, 0);
+ spa_pod_builder_push_object(b, &f[1], SPA_TYPE_OBJECT_Props, id);
+
+ spa_pod_builder_prop(b, SPA_PROP_mute, 0);
+ spa_pod_builder_bool(b, node->mute);
+
+ spa_pod_builder_prop(b, SPA_PROP_channelVolumes,
+ (t_volume && t_volume->active) ? SPA_POD_PROP_FLAG_HARDWARE : 0);
+ spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float,
+ node->n_channels, node->volumes);
+
+ if (t_volume && t_volume->active) {
+ spa_pod_builder_prop(b, SPA_PROP_volumeStep, SPA_POD_PROP_FLAG_READONLY);
+ spa_pod_builder_float(b, 1.0f / (t_volume->hw_volume_max + 1));
+ }
+
+ spa_pod_builder_prop(b, SPA_PROP_channelMap, 0);
+ spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id,
+ node->n_channels, node->channels);
+
+ if ((this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) &&
+ dev == DEVICE_ID_SINK) {
+ spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0);
+ spa_pod_builder_long(b, node->latency_offset);
+ }
+
+ spa_pod_builder_pop(b, &f[1]);
+
+ 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_devices, 0);
+ spa_pod_builder_push_array(b, &f[1]);
+ spa_pod_builder_int(b, enum_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]);
+}
+
+static bool iterate_supported_media_codecs(struct impl *this, int *j, const struct media_codec **codec)
+{
+ int i;
+
+next:
+ *j = *j + 1;
+ spa_assert(*j >= 0);
+ if ((size_t)*j >= this->supported_codec_count)
+ return false;
+
+ for (i = 0; i < *j; ++i)
+ if (this->supported_codecs[i]->id == this->supported_codecs[*j]->id)
+ goto next;
+
+ *codec = this->supported_codecs[*j];
+ return true;
+}
+
+static struct spa_pod *build_prop_info_codec(struct impl *this, struct spa_pod_builder *b, uint32_t id)
+{
+ struct spa_pod_frame f[2];
+ struct spa_pod_choice *choice;
+ const struct media_codec *codec;
+ size_t n;
+ int j;
+
+#define FOR_EACH_MEDIA_CODEC(j, codec) \
+ for (j = -1; iterate_supported_media_codecs(this, &j, &codec);)
+#define FOR_EACH_HFP_CODEC(j) \
+ for (j = HFP_AUDIO_CODEC_MSBC; j >= HFP_AUDIO_CODEC_CVSD; --j) \
+ if (spa_bt_device_supports_hfp_codec(this->bt_dev, j) == 1)
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+
+ /*
+ * XXX: the ids in principle should use builder_id, not builder_int,
+ * XXX: but the type info for _type and _labels doesn't work quite right now.
+ */
+
+ /* Transport codec */
+ spa_pod_builder_prop(b, SPA_PROP_INFO_id, 0);
+ spa_pod_builder_id(b, SPA_PROP_bluetoothAudioCodec);
+ spa_pod_builder_prop(b, SPA_PROP_INFO_description, 0);
+ spa_pod_builder_string(b, "Air codec");
+ spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+ choice = (struct spa_pod_choice *)spa_pod_builder_frame(b, &f[1]);
+ n = 0;
+ if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
+ FOR_EACH_MEDIA_CODEC(j, codec) {
+ if (n == 0)
+ spa_pod_builder_int(b, codec->id);
+ spa_pod_builder_int(b, codec->id);
+ ++n;
+ }
+ } else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
+ FOR_EACH_HFP_CODEC(j) {
+ if (n == 0)
+ spa_pod_builder_int(b, get_hfp_codec_id(j));
+ spa_pod_builder_int(b, get_hfp_codec_id(j));
+ ++n;
+ }
+ }
+ if (n == 0)
+ choice->body.type = SPA_CHOICE_None;
+ 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) {
+ FOR_EACH_MEDIA_CODEC(j, codec) {
+ spa_pod_builder_int(b, codec->id);
+ spa_pod_builder_string(b, codec->description);
+ }
+ } else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
+ FOR_EACH_HFP_CODEC(j) {
+ spa_pod_builder_int(b, get_hfp_codec_id(j));
+ spa_pod_builder_string(b, get_hfp_codec_description(j));
+ }
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ return spa_pod_builder_pop(b, &f[0]);
+
+#undef FOR_EACH_MEDIA_CODEC
+#undef FOR_EACH_HFP_CODEC
+}
+
+static struct spa_pod *build_props(struct impl *this, struct spa_pod_builder *b, uint32_t id)
+{
+ struct props *p = &this->props;
+
+ return spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(p->codec),
+ SPA_PROP_bluetoothOffloadActive, SPA_POD_Bool(p->offload_active));
+}
+
+static int impl_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[2048];
+ struct spa_result_device_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_EnumProfile:
+ {
+ uint32_t profile;
+ 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:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Profile:
+ {
+ uint32_t index;
+
+ switch (result.index) {
+ case 0:
+ index = get_index_from_profile(this, this->profile, this->props.codec);
+ param = build_profile(this, &b, id, index, this->profile, this->props.codec, true);
+ if (param == NULL)
+ return 0;
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_EnumRoute:
+ {
+ switch (result.index) {
+ case 0: case 1: case 2: case 3:
+ param = build_route(this, &b, id, result.index, SPA_ID_INVALID);
+ if (param == NULL)
+ goto next;
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Route:
+ {
+ switch (result.index) {
+ case 0: case 1: case 2: case 3:
+ param = build_route(this, &b, id, result.index, this->profile);
+ if (param == NULL)
+ goto next;
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ case 0:
+ param = build_prop_info_codec(this, &b, id);
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_bluetoothOffloadActive),
+ SPA_PROP_INFO_description, SPA_POD_String("Bluetooth audio offload active"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(false));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ case 0:
+ param = build_props(this, &b, id);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_device_emit_result(&this->hooks, seq, 0,
+ SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int node_set_volume(struct impl *this, struct node *node, float volumes[], uint32_t n_volumes)
+{
+ uint32_t i;
+ int changed = 0;
+ struct spa_bt_transport_volume *t_volume;
+
+ if (n_volumes == 0)
+ return -EINVAL;
+
+ spa_log_info(this->log, "node %p volume %f", node, volumes[0]);
+
+ for (i = 0; i < node->n_channels; i++) {
+ if (node->volumes[i] == volumes[i % n_volumes])
+ continue;
+ ++changed;
+ node->volumes[i] = volumes[i % n_volumes];
+ }
+
+ t_volume = node->transport ? &node->transport->volumes[node->id]: NULL;
+
+ if (t_volume && t_volume->active
+ && spa_bt_transport_volume_enabled(node->transport)) {
+ float hw_volume = node_get_hw_volume(node);
+ spa_log_debug(this->log, "node %p hardware volume %f", node, hw_volume);
+
+ node_update_soft_volumes(node, hw_volume);
+ spa_bt_transport_set_volume(node->transport, node->id, hw_volume);
+ } else {
+ float boost = get_soft_volume_boost(node);
+ for (uint32_t i = 0; i < node->n_channels; ++i)
+ node->soft_volumes[i] = node->volumes[i] * boost;
+ }
+
+ emit_volume(this, node);
+
+ return changed;
+}
+
+static int node_set_mute(struct impl *this, struct node *node, bool mute)
+{
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ int changed = 0;
+
+ spa_log_info(this->log, "node %p mute %d", node, mute);
+
+ changed = (node->mute != mute);
+ node->mute = mute;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, node->id);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_mute, SPA_POD_Bool(mute),
+ SPA_PROP_softMute, SPA_POD_Bool(mute));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+
+ return changed;
+}
+
+static int node_set_latency_offset(struct impl *this, struct node *node, int64_t latency_offset)
+{
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ int changed = 0;
+
+ spa_log_info(this->log, "node %p latency offset %"PRIi64" nsec", node, latency_offset);
+
+ changed = (node->latency_offset != latency_offset);
+ node->latency_offset = latency_offset;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, node->id);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(latency_offset));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+
+ return changed;
+}
+
+static int apply_device_props(struct impl *this, struct node *node, struct spa_pod *props)
+{
+ float volume = 0;
+ bool mute = 0;
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) props;
+ int changed = 0;
+ float volumes[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t channels[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t n_volumes = 0, SPA_UNUSED n_channels = 0;
+ int64_t latency_offset = 0;
+
+ if (!spa_pod_is_object_type(props, SPA_TYPE_OBJECT_Props))
+ return -EINVAL;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &volume) == 0) {
+ int res = node_set_volume(this, node, &volume, 1);
+ if (res > 0)
+ ++changed;
+ }
+ break;
+ case SPA_PROP_mute:
+ if (spa_pod_get_bool(&prop->value, &mute) == 0) {
+ int res = node_set_mute(this, node, mute);
+ if (res > 0)
+ ++changed;
+ }
+ break;
+ case SPA_PROP_channelVolumes:
+ n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ volumes, SPA_AUDIO_MAX_CHANNELS);
+ break;
+ case SPA_PROP_channelMap:
+ n_channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ channels, SPA_AUDIO_MAX_CHANNELS);
+ break;
+ case SPA_PROP_latencyOffsetNsec:
+ if (spa_pod_get_long(&prop->value, &latency_offset) == 0) {
+ int res = node_set_latency_offset(this, node, latency_offset);
+ if (res > 0)
+ ++changed;
+ }
+ }
+ }
+ if (n_volumes > 0) {
+ int res = node_set_volume(this, node, volumes, n_volumes);
+ if (res > 0)
+ ++changed;
+ }
+
+ return changed;
+}
+
+static void apply_prop_offload_active(struct impl *this, bool active)
+{
+ bool old_value = this->props.offload_active;
+
+ this->props.offload_active = active;
+
+ for (int i = 0; i < 2; i++) {
+ node_offload_set_active(&this->nodes[i], active);
+ if (!this->nodes[i].offload_acquired)
+ this->props.offload_active = false;
+ }
+
+ if (this->props.offload_active != old_value) {
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_info(this, false);
+ }
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Profile:
+ {
+ uint32_t idx, next;
+ uint32_t profile;
+ enum spa_bluetooth_audio_codec codec;
+ bool save = false;
+
+ if (param == NULL)
+ return -EINVAL;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_PROFILE_save, SPA_POD_OPT_Bool(&save))) < 0) {
+ spa_log_warn(this->log, "can't parse profile");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+
+ profile = get_profile_from_index(this, idx, &next, &codec);
+ if (profile == SPA_ID_INVALID)
+ return -EINVAL;
+
+ spa_log_debug(this->log, "setting profile %d codec:%d save:%d", profile, codec, (int)save);
+ return set_profile(this, profile, codec, save);
+ }
+ case SPA_PARAM_Route:
+ {
+ uint32_t idx, device;
+ struct spa_pod *props = NULL;
+ struct node *node;
+ bool save = false;
+
+ if (param == NULL)
+ return -EINVAL;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&device),
+ SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props),
+ SPA_PARAM_ROUTE_save, SPA_POD_OPT_Bool(&save))) < 0) {
+ spa_log_warn(this->log, "can't parse route");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+ if (device > 1 || !this->nodes[device].active)
+ return -EINVAL;
+
+ node = &this->nodes[device];
+ node->save = save;
+ if (props) {
+ int changed = apply_device_props(this, node, props);
+ if (changed > 0) {
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL;
+ }
+ emit_info(this, false);
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ uint32_t codec_id = SPA_ID_INVALID;
+ bool offload_active = this->props.offload_active;
+
+ if (param == NULL)
+ return 0;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_bluetoothAudioCodec, SPA_POD_OPT_Id(&codec_id),
+ SPA_PROP_bluetoothOffloadActive, SPA_POD_OPT_Bool(&offload_active))) < 0) {
+ spa_log_warn(this->log, "can't parse props");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+
+ spa_log_debug(this->log, "setting props codec:%d offload:%d", (int)codec_id, (int)offload_active);
+
+ apply_prop_offload_active(this, offload_active);
+
+ if (codec_id == SPA_ID_INVALID)
+ return 0;
+
+ if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) {
+ size_t j;
+ for (j = 0; j < this->supported_codec_count; ++j) {
+ if (this->supported_codecs[j]->id == codec_id) {
+ return set_profile(this, this->profile, codec_id, true);
+ }
+ }
+ } else if (this->profile == DEVICE_PROFILE_HSP_HFP) {
+ if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_CVSD &&
+ spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_CVSD) == 1) {
+ return set_profile(this, this->profile, codec_id, true);
+ } else if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_MSBC &&
+ spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_MSBC) == 1) {
+ return set_profile(this, this->profile, codec_id, true);
+ }
+ }
+ return -EINVAL;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ const struct spa_dict_item *it;
+
+ emit_remove_nodes(this);
+
+ free(this->supported_codecs);
+ if (this->bt_dev) {
+ this->bt_dev->settings = NULL;
+ spa_hook_remove(&this->bt_dev_listener);
+ }
+
+ spa_dict_for_each(it, &this->setting_dict) {
+ if(it->key)
+ free((void *)it->key);
+ if(it->value)
+ free((void *)it->value);
+ }
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static const struct spa_dict*
+filter_bluez_device_setting(struct impl *this, const struct spa_dict *dict)
+{
+ uint32_t n_items = 0;
+ for (uint32_t i = 0
+ ; i < dict->n_items && n_items < SPA_N_ELEMENTS(this->setting_items)
+ ; i++)
+ {
+ const struct spa_dict_item *it = &dict->items[i];
+ if (it->key != NULL && strncmp(it->key, "bluez", 5) == 0 && it->value != NULL) {
+ this->setting_items[n_items++] =
+ SPA_DICT_ITEM_INIT(strdup(it->key), strdup(it->value));
+ }
+ }
+ this->setting_dict = SPA_DICT_INIT(this->setting_items, n_items);
+ return &this->setting_dict;
+}
+
+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;
+ const char *str;
+
+ 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);
+ _i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_DEVICE)))
+ sscanf(str, "pointer:%p", &this->bt_dev);
+
+ if (this->bt_dev == NULL) {
+ spa_log_error(this->log, "a device is needed");
+ return -EINVAL;
+ }
+
+ if (info) {
+ int profiles;
+ this->bt_dev->settings = filter_bluez_device_setting(this, info);
+
+ if ((str = spa_dict_lookup(info, "bluez5.auto-connect")) != NULL) {
+ if ((profiles = spa_bt_profiles_from_json_array(str)) >= 0)
+ this->bt_dev->reconnect_profiles = profiles;
+ }
+
+ if ((str = spa_dict_lookup(info, "bluez5.hw-volume")) != NULL) {
+ if ((profiles = spa_bt_profiles_from_json_array(str)) >= 0)
+ this->bt_dev->hw_volume_profiles = profiles;
+ }
+ }
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ init_node(this, &this->nodes[0], 0);
+ init_node(this, &this->nodes[1], 1);
+
+ this->info = SPA_DEVICE_INFO_INIT();
+ this->info_all = SPA_DEVICE_CHANGE_MASK_PROPS |
+ SPA_DEVICE_CHANGE_MASK_PARAMS;
+
+ this->params[IDX_EnumProfile] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ this->params[IDX_Profile] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_EnumRoute] = SPA_PARAM_INFO(SPA_PARAM_EnumRoute, SPA_PARAM_INFO_READ);
+ this->params[IDX_Route] = SPA_PARAM_INFO(SPA_PARAM_Route, 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 = 6;
+
+ spa_bt_device_add_listener(this->bt_dev, &this->bt_dev_listener, &bt_dev_events, this);
+
+ set_initial_profile(this);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+static const struct spa_dict_item handle_info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "A bluetooth device" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_DEVICE"=<device>" },
+};
+
+static const struct spa_dict handle_info = SPA_DICT_INIT_ARRAY(handle_info_items);
+
+const struct spa_handle_factory spa_bluez5_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_DEVICE,
+ &handle_info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c
new file mode 100644
index 0000000..f8363f8
--- /dev/null
+++ b/spa/plugins/bluez5/codec-loader.c
@@ -0,0 +1,234 @@
+/* Spa A2DP codec API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <spa/utils/string.h>
+
+#include "defs.h"
+#include "codec-loader.h"
+
+#define MEDIA_CODEC_LIB_BASE "bluez5/libspa-codec-bluez5-"
+
+/* AVDTP allows 0x3E endpoints, can't have more codecs than that */
+#define MAX_CODECS 0x3E
+#define MAX_HANDLES MAX_CODECS
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl {
+ const struct media_codec *codecs[MAX_CODECS + 1];
+ struct spa_handle *handles[MAX_HANDLES];
+ size_t n_codecs;
+ size_t n_handles;
+ struct spa_plugin_loader *loader;
+ struct spa_log *log;
+};
+
+static int codec_order(const struct media_codec *c)
+{
+ static const enum spa_bluetooth_audio_codec order[] = {
+ SPA_BLUETOOTH_AUDIO_CODEC_LC3,
+ SPA_BLUETOOTH_AUDIO_CODEC_LDAC,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX,
+ SPA_BLUETOOTH_AUDIO_CODEC_AAC,
+ SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR,
+ SPA_BLUETOOTH_AUDIO_CODEC_MPEG,
+ SPA_BLUETOOTH_AUDIO_CODEC_SBC,
+ SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL,
+ SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM,
+ SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX,
+ SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO,
+ };
+ size_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(order); ++i)
+ if (c->id == order[i])
+ return i;
+ return SPA_N_ELEMENTS(order);
+}
+
+static int codec_order_cmp(const void *a, const void *b)
+{
+ const struct media_codec * const *ca = a;
+ const struct media_codec * const *cb = b;
+ int ia = codec_order(*ca);
+ int ib = codec_order(*cb);
+ if (*ca == *cb)
+ return 0;
+ return (ia == ib) ? (*ca < *cb ? -1 : 1) : ia - ib;
+}
+
+static int load_media_codecs_from(struct impl *impl, const char *factory_name, const char *libname)
+{
+ struct spa_handle *handle = NULL;
+ void *iface;
+ const struct spa_bluez5_codec_a2dp *bluez5_codec_a2dp;
+ int n_codecs = 0;
+ int res;
+ size_t i;
+ struct spa_dict_item info_items[] = {
+ { SPA_KEY_LIBRARY_NAME, libname },
+ };
+ struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+ handle = spa_plugin_loader_load(impl->loader, factory_name, &info);
+ if (handle == NULL) {
+ spa_log_info(impl->log, "Bluetooth codec plugin %s not available", factory_name);
+ return -ENOENT;
+ }
+
+ spa_log_debug(impl->log, "loading codecs from %s", factory_name);
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Bluez5CodecMedia, &iface)) < 0) {
+ spa_log_warn(impl->log, "Bluetooth codec plugin %s has no codec interface",
+ factory_name);
+ goto fail;
+ }
+
+ bluez5_codec_a2dp = iface;
+
+ if (bluez5_codec_a2dp->iface.version != SPA_VERSION_BLUEZ5_CODEC_MEDIA) {
+ spa_log_warn(impl->log, "codec plugin %s has incompatible ABI version (%d != %d)",
+ factory_name, bluez5_codec_a2dp->iface.version, SPA_VERSION_BLUEZ5_CODEC_MEDIA);
+ res = -ENOENT;
+ goto fail;
+ }
+
+ for (i = 0; bluez5_codec_a2dp->codecs[i]; ++i) {
+ const struct media_codec *c = bluez5_codec_a2dp->codecs[i];
+ const char *ep = c->endpoint_name ? c->endpoint_name : c->name;
+ size_t j;
+
+ if (!ep)
+ goto next_codec;
+
+ if (impl->n_codecs >= MAX_CODECS) {
+ spa_log_error(impl->log, "too many A2DP codecs");
+ break;
+ }
+
+ /* Don't load duplicate endpoints */
+ for (j = 0; j < impl->n_codecs; ++j) {
+ const struct media_codec *c2 = impl->codecs[j];
+ const char *ep2 = c2->endpoint_name ? c2->endpoint_name : c2->name;
+ if (spa_streq(ep, ep2) && c->fill_caps && c2->fill_caps) {
+ spa_log_debug(impl->log, "media codec %s from %s duplicate endpoint %s",
+ c->name, factory_name, ep);
+ goto next_codec;
+ }
+ }
+
+ spa_log_debug(impl->log, "loaded media codec %s from %s, endpoint:%s",
+ c->name, factory_name, ep);
+
+ if (c->set_log)
+ c->set_log(impl->log);
+
+ impl->codecs[impl->n_codecs++] = c;
+ ++n_codecs;
+
+ next_codec:
+ continue;
+ }
+
+ if (n_codecs > 0)
+ impl->handles[impl->n_handles++] = handle;
+ else
+ spa_plugin_loader_unload(impl->loader, handle);
+
+ return 0;
+
+fail:
+ if (handle)
+ spa_plugin_loader_unload(impl->loader, handle);
+ return res;
+}
+
+const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *loader, struct spa_log *log)
+{
+ struct impl *impl;
+ bool has_sbc;
+ size_t i;
+ const struct { const char *factory; const char *lib; } plugins[] = {
+#define MEDIA_CODEC_FACTORY_LIB(basename) \
+ { MEDIA_CODEC_FACTORY_NAME(basename), MEDIA_CODEC_LIB_BASE basename }
+ MEDIA_CODEC_FACTORY_LIB("aac"),
+ MEDIA_CODEC_FACTORY_LIB("aptx"),
+ MEDIA_CODEC_FACTORY_LIB("faststream"),
+ MEDIA_CODEC_FACTORY_LIB("ldac"),
+ MEDIA_CODEC_FACTORY_LIB("sbc"),
+ MEDIA_CODEC_FACTORY_LIB("lc3plus"),
+ MEDIA_CODEC_FACTORY_LIB("opus"),
+ MEDIA_CODEC_FACTORY_LIB("lc3")
+#undef MEDIA_CODEC_FACTORY_LIB
+ };
+
+ impl = calloc(sizeof(struct impl), 1);
+ if (impl == NULL)
+ return NULL;
+
+ impl->loader = loader;
+ impl->log = log;
+
+ spa_log_topic_init(impl->log, &log_topic);
+
+ for (i = 0; i < SPA_N_ELEMENTS(plugins); ++i)
+ load_media_codecs_from(impl, plugins[i].factory, plugins[i].lib);
+
+ has_sbc = false;
+ for (i = 0; i < impl->n_codecs; ++i)
+ if (impl->codecs[i]->id == SPA_BLUETOOTH_AUDIO_CODEC_SBC)
+ has_sbc = true;
+
+ if (!has_sbc) {
+ spa_log_error(impl->log, "failed to load A2DP SBC codec from plugins");
+ free_media_codecs(impl->codecs);
+ errno = ENOENT;
+ return NULL;
+ }
+
+ qsort(impl->codecs, impl->n_codecs, sizeof(const struct media_codec *), codec_order_cmp);
+
+ return impl->codecs;
+}
+
+void free_media_codecs(const struct media_codec * const *media_codecs)
+{
+ struct impl *impl = SPA_CONTAINER_OF(media_codecs, struct impl, codecs);
+ size_t i;
+
+ for (i = 0; i < impl->n_handles; ++i)
+ spa_plugin_loader_unload(impl->loader, impl->handles[i]);
+
+ free(impl);
+}
diff --git a/spa/plugins/bluez5/codec-loader.h b/spa/plugins/bluez5/codec-loader.h
new file mode 100644
index 0000000..b77d9c4
--- /dev/null
+++ b/spa/plugins/bluez5/codec-loader.h
@@ -0,0 +1,39 @@
+/* Spa A2DP codec API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_CODEC_LOADER_H_
+#define SPA_BLUEZ5_CODEC_LOADER_H_
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include <spa/support/plugin-loader.h>
+
+#include "a2dp-codec-caps.h"
+#include "media-codecs.h"
+
+const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *loader, struct spa_log *log);
+void free_media_codecs(const struct media_codec * const *media_codecs);
+
+#endif
diff --git a/spa/plugins/bluez5/dbus-monitor.c b/spa/plugins/bluez5/dbus-monitor.c
new file mode 100644
index 0000000..6fbfb2a
--- /dev/null
+++ b/spa/plugins/bluez5/dbus-monitor.c
@@ -0,0 +1,265 @@
+/* Spa midi dbus
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include <gio/gio.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+
+#include "dbus-monitor.h"
+
+
+static void on_g_properties_changed(GDBusProxy *proxy,
+ GVariant *changed_properties, char **invalidated_properties,
+ gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GDBusInterfaceInfo *info = g_dbus_interface_get_info(G_DBUS_INTERFACE(proxy));
+ const char *name = info ? info->name : NULL;
+ const struct dbus_monitor_proxy_type *p;
+
+ spa_log_trace(monitor->log, "%p: dbus object updated path=%s, name=%s",
+ monitor, g_dbus_proxy_get_object_path(proxy), name ? name : "<null>");
+
+ for (p = monitor->proxy_types; p && p->proxy_type != G_TYPE_INVALID ; ++p) {
+ if (G_TYPE_CHECK_INSTANCE_TYPE(proxy, p->proxy_type)) {
+ if (p->on_update)
+ p->on_update(monitor, G_DBUS_INTERFACE(proxy));
+ }
+ }
+}
+
+static void on_remove(struct dbus_monitor *monitor, GDBusProxy *proxy)
+{
+ const struct dbus_monitor_proxy_type *p;
+
+ for (p = monitor->proxy_types; p && p->proxy_type != G_TYPE_INVALID ; ++p) {
+ if (G_TYPE_CHECK_INSTANCE_TYPE(proxy, p->proxy_type)) {
+ if (p->on_remove)
+ p->on_remove(monitor, G_DBUS_INTERFACE(proxy));
+ }
+ }
+}
+
+static void on_interface_added(GDBusObjectManager *self, GDBusObject *object,
+ GDBusInterface *iface, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GDBusInterfaceInfo *info = g_dbus_interface_get_info(iface);
+ const char *name = info ? info->name : NULL;
+
+ spa_log_trace(monitor->log, "%p: dbus interface added path=%s, name=%s",
+ monitor, g_dbus_object_get_object_path(object), name ? name : "<null>");
+
+ if (!g_object_get_data(G_OBJECT(iface), "dbus-monitor-signals-connected")) {
+ g_object_set_data(G_OBJECT(iface), "dbus-monitor-signals-connected", GUINT_TO_POINTER(1));
+ g_signal_connect(iface, "g-properties-changed",
+ G_CALLBACK(on_g_properties_changed),
+ monitor);
+ }
+
+ on_g_properties_changed(G_DBUS_PROXY(iface),
+ NULL, NULL, monitor);
+}
+
+static void on_interface_removed(GDBusObjectManager *manager, GDBusObject *object,
+ GDBusInterface *iface, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GDBusInterfaceInfo *info = g_dbus_interface_get_info(iface);
+ const char *name = info ? info->name : NULL;
+
+ spa_log_trace(monitor->log, "%p: dbus interface removed path=%s, name=%s",
+ monitor, g_dbus_object_get_object_path(object), name ? name : "<null>");
+
+ if (g_object_get_data(G_OBJECT(iface), "dbus-monitor-signals-connected")) {
+ g_object_disconnect(G_OBJECT(iface), "g-properties-changed",
+ G_CALLBACK(on_g_properties_changed),
+ monitor, NULL);
+ g_object_set_data(G_OBJECT(iface), "dbus-monitor-signals-connected", NULL);
+ }
+
+ on_remove(monitor, G_DBUS_PROXY(iface));
+}
+
+static void on_object_added(GDBusObjectManager *self, GDBusObject *object,
+ gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GList *interfaces = g_dbus_object_get_interfaces(object);
+
+ /*
+ * on_interface_added won't necessarily be called on objects on
+ * name owner changes, so we have to call it here for all interfaces.
+ */
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ on_interface_added(dbus_monitor_manager(monitor),
+ object, G_DBUS_INTERFACE(lli->data), monitor);
+ }
+
+ g_list_free_full(interfaces, g_object_unref);
+}
+
+static void on_object_removed(GDBusObjectManager *manager, GDBusObject *object,
+ gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GList *interfaces = g_dbus_object_get_interfaces(object);
+
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ on_interface_removed(dbus_monitor_manager(monitor),
+ object, G_DBUS_INTERFACE(lli->data), monitor);
+ }
+
+ g_list_free_full(interfaces, g_object_unref);
+}
+
+static void on_notify(GObject *gobject, GParamSpec *pspec, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+
+ if (spa_streq(pspec->name, "name-owner") && monitor->on_name_owner_change)
+ monitor->on_name_owner_change(monitor);
+}
+
+static GType get_proxy_type(GDBusObjectManagerClient *manager, const gchar *object_path,
+ const gchar *interface_name, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ const struct dbus_monitor_proxy_type *p;
+
+ for (p = monitor->proxy_types; p && p->proxy_type != G_TYPE_INVALID; ++p) {
+ if (spa_streq(p->interface_name, interface_name))
+ return p->proxy_type;
+ }
+
+ return G_TYPE_DBUS_PROXY;
+}
+
+static void init_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ struct dbus_monitor *monitor = user_data;
+ GError *error = NULL;
+ GList *objects;
+ GObject *ret;
+
+ g_clear_object(&monitor->call);
+
+ ret = g_async_initable_new_finish(G_ASYNC_INITABLE(source_object), res, &error);
+ if (!ret) {
+ spa_log_error(monitor->log, "%p: creating DBus object monitor failed: %s",
+ monitor, error->message);
+ g_error_free(error);
+ return;
+ }
+ monitor->manager = G_DBUS_OBJECT_MANAGER_CLIENT(ret);
+
+ spa_log_debug(monitor->log, "%p: DBus monitor started", monitor);
+
+ g_signal_connect(monitor->manager, "interface-added",
+ G_CALLBACK(on_interface_added), monitor);
+ g_signal_connect(monitor->manager, "interface-removed",
+ G_CALLBACK(on_interface_removed), monitor);
+ g_signal_connect(monitor->manager, "object-added",
+ G_CALLBACK(on_object_added), monitor);
+ g_signal_connect(monitor->manager, "object-removed",
+ G_CALLBACK(on_object_removed), monitor);
+ g_signal_connect(monitor->manager, "notify",
+ G_CALLBACK(on_notify), monitor);
+
+ /* List all objects now */
+ objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(monitor));
+ for (GList *llo = g_list_first(objects); llo; llo = llo->next) {
+ GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data));
+
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ on_interface_added(dbus_monitor_manager(monitor),
+ G_DBUS_OBJECT(llo->data), G_DBUS_INTERFACE(lli->data),
+ monitor);
+ }
+ g_list_free_full(interfaces, g_object_unref);
+ }
+ g_list_free_full(objects, g_object_unref);
+}
+
+void dbus_monitor_init(struct dbus_monitor *monitor,
+ GType client_type,
+ struct spa_log *log, GDBusConnection *conn,
+ const char *name, const char *object_path,
+ const struct dbus_monitor_proxy_type *proxy_types,
+ void (*on_name_owner_change)(struct dbus_monitor *monitor))
+{
+ GDBusObjectManagerClientFlags flags = G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START;
+ size_t i;
+
+ spa_zero(*monitor);
+
+ monitor->log = log;
+ monitor->call = g_cancellable_new();
+ monitor->on_name_owner_change = on_name_owner_change;
+
+ spa_zero(monitor->proxy_types);
+
+ for (i = 0; proxy_types && proxy_types[i].proxy_type != G_TYPE_INVALID; ++i) {
+ spa_assert(i < DBUS_MONITOR_MAX_TYPES);
+ monitor->proxy_types[i] = proxy_types[i];
+ }
+
+ g_async_initable_new_async(client_type, G_PRIORITY_DEFAULT,
+ monitor->call, init_done, monitor,
+ "flags", flags, "name", name, "connection", conn,
+ "object-path", object_path,
+ "get-proxy-type-func", get_proxy_type,
+ "get-proxy-type-user-data", monitor,
+ NULL);
+}
+
+void dbus_monitor_clear(struct dbus_monitor *monitor)
+{
+ g_cancellable_cancel(monitor->call);
+ g_clear_object(&monitor->call);
+
+ if (monitor->manager) {
+ /*
+ * Indicate all objects should stop now.
+ *
+ * This has to be a separate hook, because the proxy finalizers
+ * may be called later asynchronously via e.g. DBus callbacks.
+ */
+ GList *objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(monitor));
+ for (GList *llo = g_list_first(objects); llo; llo = llo->next) {
+ GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data));
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ on_interface_removed(dbus_monitor_manager(monitor),
+ G_DBUS_OBJECT(llo->data), G_DBUS_INTERFACE(lli->data),
+ monitor);
+ }
+ g_list_free_full(interfaces, g_object_unref);
+ }
+ g_list_free_full(objects, g_object_unref);
+ }
+
+ g_clear_object(&monitor->manager);
+ spa_zero(*monitor);
+}
diff --git a/spa/plugins/bluez5/dbus-monitor.h b/spa/plugins/bluez5/dbus-monitor.h
new file mode 100644
index 0000000..f9fa12b
--- /dev/null
+++ b/spa/plugins/bluez5/dbus-monitor.h
@@ -0,0 +1,83 @@
+/* Spa midi dbus
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef DBUS_MONITOR_H_
+#define DBUS_MONITOR_H_
+
+#include <gio/gio.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+
+#define DBUS_MONITOR_MAX_TYPES 16
+
+struct dbus_monitor;
+
+struct dbus_monitor_proxy_type
+{
+ /** Interface name to monitor, or NULL for object type */
+ const char *interface_name;
+
+ /** GObject type for the proxy */
+ GType proxy_type;
+
+ /** Hook called when object added or properties changed */
+ void (*on_update)(struct dbus_monitor *monitor, GDBusInterface *iface);
+
+ /** Hook called when object is removed (or on monitor shutdown) */
+ void (*on_remove)(struct dbus_monitor *monitor, GDBusInterface *iface);
+};
+
+struct dbus_monitor
+{
+ GDBusObjectManagerClient *manager;
+ struct spa_log *log;
+ GCancellable *call;
+ struct dbus_monitor_proxy_type proxy_types[DBUS_MONITOR_MAX_TYPES+1];
+ void (*on_name_owner_change)(struct dbus_monitor *monitor);
+ void *user_data;
+};
+
+static inline GDBusObjectManager *dbus_monitor_manager(struct dbus_monitor *monitor)
+{
+ return G_DBUS_OBJECT_MANAGER(monitor->manager);
+}
+
+/**
+ * Create a DBus object monitor, with a given interface to proxy type map.
+ *
+ * \param proxy_types Mapping between interface names and watched proxy
+ * types, terminated by G_TYPE_INVALID.
+ * \param on_object_update Called for all objects and interfaces on
+ * startup, and when object properties are modified.
+ */
+void dbus_monitor_init(struct dbus_monitor *monitor,
+ GType client_type, struct spa_log *log, GDBusConnection *conn,
+ const char *name, const char *object_path,
+ const struct dbus_monitor_proxy_type *proxy_types,
+ void (*on_name_owner_change)(struct dbus_monitor *monitor));
+
+void dbus_monitor_clear(struct dbus_monitor *monitor);
+
+#endif DBUS_MONITOR_H_
diff --git a/spa/plugins/bluez5/decode-buffer.h b/spa/plugins/bluez5/decode-buffer.h
new file mode 100644
index 0000000..434f735
--- /dev/null
+++ b/spa/plugins/bluez5/decode-buffer.h
@@ -0,0 +1,486 @@
+/* Spa Bluez5 decode buffer
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * \file decode-buffer.h Buffering for Bluetooth sources
+ *
+ * A linear buffer, which is compacted when it gets half full.
+ *
+ * Also contains buffering logic, which calculates a rate correction
+ * factor to maintain the buffer level at the target value.
+ *
+ * Consider typical packet intervals with nominal frame duration
+ * of 10ms:
+ *
+ * ... 5ms | 5ms | 20ms | 5ms | 5ms | 20ms ...
+ *
+ * ... 3ms | 3ms | 4ms | 30ms | 3ms | 3ms | 4ms | 30ms ...
+ *
+ * plus random jitter; 10ms nominal may occasionally have 20+ms interval.
+ * The regular timer cycle cannot be aligned with this, so process()
+ * may occur at any time.
+ *
+ * The buffer level is the difference between the number of samples in
+ * buffer immediately after receiving a packet, and the samples consumed
+ * before receiving the next packet.
+ *
+ * The buffer level indicates how much any packet can be delayed without
+ * underrun. If it is positive, there are no underruns.
+ *
+ * The rate correction aims to maintain the average level at a safety margin.
+ */
+
+#ifndef SPA_BLUEZ5_DECODE_BUFFER_H
+#define SPA_BLUEZ5_DECODE_BUFFER_H
+
+#include <stdlib.h>
+#include <spa/utils/defs.h>
+#include <spa/support/log.h>
+
+#define BUFFERING_LONG_MSEC (2*60000)
+#define BUFFERING_SHORT_MSEC 1000
+#define BUFFERING_RATE_DIFF_MAX 0.005
+
+/**
+ * Safety margin.
+ *
+ * The spike is the long-window maximum difference
+ * between minimum and average buffer level.
+ */
+#define BUFFERING_TARGET(spike,packet_size) \
+ SPA_CLAMP((spike)*3/2, (packet_size), 6*(packet_size))
+
+/**
+ * Rate controller.
+ *
+ * It's here in a form, where it operates on the running average
+ * so it's compatible with the level spike determination, and
+ * clamping the rate to a range is easy. The impulse response
+ * is similar to spa_dll, and step response does not have sign changes.
+ *
+ * The controller iterates as
+ *
+ * avg(j+1) = (1 - beta) avg(j) + beta level(j)
+ * corr(j+1) = corr(j) + a [avg(j+1) - avg(j)] / duration
+ * + b [avg(j) - target] / duration
+ *
+ * with beta = duration/avg_period < 0.5 is the moving average parameter,
+ * and a = beta/3 + ..., b = beta^2/27 + ....
+ *
+ * This choice results to c(j) being low-pass filtered, and buffer level(j)
+ * converging towards target with stable damped evolution with eigenvalues
+ * real and close to each other around (1 - beta)^(1/3).
+ *
+ * Derivation:
+ *
+ * The deviation from the buffer level target evolves as
+ *
+ * delta(j) = level(j) - target
+ * delta(j+1) = delta(j) + r(j) - c(j+1)
+ *
+ * where r is samples received in one duration, and c corrected rate
+ * (samples per duration).
+ *
+ * The rate correction is in general determined by linear filter f
+ *
+ * c(j+1) = c(j) + \sum_{k=0}^\infty delta(j - k) f(k)
+ *
+ * If \sum_k f(k) is not zero, the only fixed point is c=r, delta=0,
+ * so this structure (if the filter is stable) rate matches and
+ * drives buffer level to target.
+ *
+ * The z-transform then is
+ *
+ * delta(z) = G(z) r(z)
+ * c(z) = F(z) delta(z)
+ * G(z) = (z - 1) / [(z - 1)^2 + z f(z)]
+ * F(z) = f(z) / (z - 1)
+ *
+ * We now want: poles of G(z) must be in |z|<1 for stability, F(z)
+ * should damp high frequencies, and f(z) is causal.
+ *
+ * To satisfy the conditions, take
+ *
+ * (z - 1)^2 + z f(z) = p(z) / q(z)
+ *
+ * where p(z) is polynomial with leading term z^n with wanted root
+ * structure, and q(z) is any polynomial with leading term z^{n-2}.
+ * This guarantees f(z) is causal, and G(z) = (z-1) q(z) / p(z).
+ * We can choose p(z) and q(z) to improve low-pass properties of F(z).
+ *
+ * Simplest choice is p(z)=(z-x)^2 and q(z)=1, but that gives flat
+ * high frequency response in F(z). Better choice is p(z) = (z-u)*(z-v)*(z-w)
+ * and q(z) = z - r. To make F(z) better lowpass, one can cancel
+ * a resulting 1/z pole in F(z) by setting r=u*v*w. Then,
+ *
+ * G(z) = (z - u*v*w)*(z - 1) / [(z - u)*(z - v)*(z - w)]
+ * F(z) = (a z + b - a) / (z - 1) * H(z)
+ * H(z) = beta / (z - 1 + beta)
+ * beta = 1 - u*v*w
+ * a = [(1-u) + (1-v) + (1-w) - beta] / beta
+ * b = (1-u)*(1-v)*(1-w) / beta
+ *
+ * which corresponds to iteration for c(j):
+ *
+ * avg(j+1) = (1 - beta) avg(j) + beta delta(j)
+ * c(j+1) = c(j) + a [avg(j+1) - avg(j)] + b avg(j)
+ *
+ * So the controller operates on the running average,
+ * which gives the low-pass property for c(j).
+ *
+ * The simplest filter is obtained by putting the poles at
+ * u=v=w=(1-beta)**(1/3). Since beta << 1, computing the root
+ * can be avoided by expanding in series.
+ *
+ * Overshoot in impulse response could be reduced by moving one of the
+ * poles closer to z=1, but this increases the step response time.
+ */
+struct spa_bt_rate_control
+{
+ double avg;
+ double corr;
+};
+
+static void spa_bt_rate_control_init(struct spa_bt_rate_control *this, double level)
+{
+ this->avg = level;
+ this->corr = 1.0;
+}
+
+static double spa_bt_rate_control_update(struct spa_bt_rate_control *this, double level,
+ double target, double duration, double period)
+{
+ /*
+ * u = (1 - beta)^(1/3)
+ * x = a / beta
+ * y = b / beta
+ * a = (2 + u) * (1 - u)^2 / beta
+ * b = (1 - u)^3 / beta
+ * beta -> 0
+ */
+ const double beta = SPA_CLAMP(duration / period, 0, 0.5);
+ const double x = 1.0/3;
+ const double y = beta/27;
+ double avg;
+
+ avg = beta * level + (1 - beta) * this->avg;
+ this->corr += x * (avg - this->avg) / period
+ + y * (this->avg - target) / period;
+ this->avg = avg;
+
+ this->corr = SPA_CLAMP(this->corr,
+ 1 - BUFFERING_RATE_DIFF_MAX,
+ 1 + BUFFERING_RATE_DIFF_MAX);
+
+ return this->corr;
+}
+
+
+/** Windowed min/max */
+struct spa_bt_ptp
+{
+ union {
+ int32_t min;
+ int32_t mins[4];
+ };
+ union {
+ int32_t max;
+ int32_t maxs[4];
+ };
+ uint32_t pos;
+ uint32_t period;
+};
+
+struct spa_bt_decode_buffer
+{
+ struct spa_log *log;
+
+ uint32_t frame_size;
+ uint32_t rate;
+
+ uint8_t *buffer_decoded;
+ uint32_t buffer_size;
+ uint32_t buffer_reserve;
+ uint32_t write_index;
+ uint32_t read_index;
+
+ struct spa_bt_ptp spike; /**< spikes (long window) */
+ struct spa_bt_ptp packet_size; /**< packet size (short window) */
+
+ struct spa_bt_rate_control ctl;
+ double corr;
+
+ uint32_t prev_consumed;
+ uint32_t prev_avail;
+ uint32_t prev_duration;
+ uint32_t underrun;
+ uint32_t pos;
+
+ uint8_t received:1;
+ uint8_t buffering:1;
+};
+
+static void spa_bt_ptp_init(struct spa_bt_ptp *p, int32_t period)
+{
+ size_t i;
+
+ spa_zero(*p);
+ for (i = 0; i < SPA_N_ELEMENTS(p->mins); ++i) {
+ p->mins[i] = INT32_MAX;
+ p->maxs[i] = INT32_MIN;
+ }
+ p->period = period;
+}
+
+static void spa_bt_ptp_update(struct spa_bt_ptp *p, int32_t value, uint32_t duration)
+{
+ const size_t n = SPA_N_ELEMENTS(p->mins);
+ size_t i;
+
+ for (i = 0; i < n; ++i) {
+ p->mins[i] = SPA_MIN(p->mins[i], value);
+ p->maxs[i] = SPA_MAX(p->maxs[i], value);
+ }
+
+ p->pos += duration;
+ if (p->pos >= p->period / (n - 1)) {
+ p->pos = 0;
+ for (i = 1; i < SPA_N_ELEMENTS(p->mins); ++i) {
+ p->mins[i-1] = p->mins[i];
+ p->maxs[i-1] = p->maxs[i];
+ }
+ p->mins[n-1] = INT32_MAX;
+ p->maxs[n-1] = INT32_MIN;
+ }
+}
+
+static int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct spa_log *log,
+ uint32_t frame_size, uint32_t rate, uint32_t quantum_limit, uint32_t reserve)
+{
+ spa_zero(*this);
+ this->frame_size = frame_size;
+ this->rate = rate;
+ this->log = log;
+ this->buffer_reserve = this->frame_size * reserve;
+ this->buffer_size = this->frame_size * quantum_limit * 2;
+ this->buffer_size += this->buffer_reserve;
+ this->corr = 1.0;
+ this->buffering = true;
+
+ spa_bt_rate_control_init(&this->ctl, 0);
+
+ spa_bt_ptp_init(&this->spike, (uint64_t)this->rate * BUFFERING_LONG_MSEC / 1000);
+ spa_bt_ptp_init(&this->packet_size, (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000);
+
+ if ((this->buffer_decoded = malloc(this->buffer_size)) == NULL) {
+ this->buffer_size = 0;
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static void spa_bt_decode_buffer_clear(struct spa_bt_decode_buffer *this)
+{
+ free(this->buffer_decoded);
+ spa_zero(*this);
+}
+
+static void spa_bt_decode_buffer_compact(struct spa_bt_decode_buffer *this)
+{
+ uint32_t avail;
+
+ spa_assert(this->read_index <= this->write_index);
+
+ if (this->read_index == this->write_index) {
+ this->read_index = 0;
+ this->write_index = 0;
+ goto done;
+ }
+
+ if (this->write_index > this->read_index + this->buffer_size - this->buffer_reserve) {
+ /* Drop data to keep buffer reserve free */
+ spa_log_info(this->log, "%p buffer overrun: dropping data", this);
+ this->read_index = this->write_index + this->buffer_reserve - this->buffer_size;
+ }
+
+ if (this->write_index < (this->buffer_size - this->buffer_reserve) / 2
+ || this->read_index == 0)
+ goto done;
+
+ avail = this->write_index - this->read_index;
+ spa_memmove(this->buffer_decoded,
+ SPA_PTROFF(this->buffer_decoded, this->read_index, void),
+ avail);
+ this->read_index = 0;
+ this->write_index = avail;
+
+done:
+ spa_assert(this->buffer_size - this->write_index >= this->buffer_reserve);
+}
+
+static void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer *this, uint32_t *avail)
+{
+ spa_bt_decode_buffer_compact(this);
+ spa_assert(this->buffer_size >= this->write_index);
+ *avail = this->buffer_size - this->write_index;
+ return SPA_PTROFF(this->buffer_decoded, this->write_index, void);
+}
+
+static void spa_bt_decode_buffer_write_packet(struct spa_bt_decode_buffer *this, uint32_t size)
+{
+ spa_assert(size % this->frame_size == 0);
+ this->write_index += size;
+ this->received = true;
+ spa_bt_ptp_update(&this->packet_size, size / this->frame_size, size / this->frame_size);
+}
+
+static void *spa_bt_decode_buffer_get_read(struct spa_bt_decode_buffer *this, uint32_t *avail)
+{
+ spa_assert(this->write_index >= this->read_index);
+ if (!this->buffering)
+ *avail = this->write_index - this->read_index;
+ else
+ *avail = 0;
+ return SPA_PTROFF(this->buffer_decoded, this->read_index, void);
+}
+
+static void spa_bt_decode_buffer_read(struct spa_bt_decode_buffer *this, uint32_t size)
+{
+ spa_assert(size % this->frame_size == 0);
+ this->read_index += size;
+}
+
+static void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this)
+{
+ int32_t size = (this->write_index - this->read_index) / this->frame_size;
+ int32_t level;
+
+ this->prev_avail = size * this->frame_size;
+ this->prev_consumed = this->prev_duration;
+
+ level = (int32_t)this->prev_avail/this->frame_size
+ - (int32_t)this->prev_duration;
+ this->corr = 1.0;
+
+ spa_bt_rate_control_init(&this->ctl, level);
+}
+
+static void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, uint32_t duration)
+{
+ const uint32_t data_size = samples * this->frame_size;
+ const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8);
+ const int32_t max_level = SPA_MAX(8 * packet_size, (int32_t)duration);
+ uint32_t avail;
+
+ if (SPA_UNLIKELY(duration != this->prev_duration)) {
+ this->prev_duration = duration;
+ spa_bt_decode_buffer_recover(this);
+ }
+
+ if (SPA_UNLIKELY(this->buffering)) {
+ int32_t size = (this->write_index - this->read_index) / this->frame_size;
+
+ this->corr = 1.0;
+
+ spa_log_trace(this->log, "%p buffering size:%d", this, (int)size);
+
+ if (this->received &&
+ packet_size > 0 &&
+ size >= SPA_MAX(3*packet_size, (int32_t)duration))
+ this->buffering = false;
+ else
+ return;
+
+ spa_bt_decode_buffer_recover(this);
+ }
+
+ spa_bt_decode_buffer_get_read(this, &avail);
+
+ if (this->received) {
+ const uint32_t avg_period = (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000;
+ int32_t level, target;
+
+ /* Track buffer level */
+ level = (int32_t)(this->prev_avail/this->frame_size) - (int32_t)this->prev_consumed;
+ level = SPA_MAX(level, -max_level);
+ this->prev_consumed = SPA_MIN(this->prev_consumed, avg_period);
+
+ spa_bt_ptp_update(&this->spike, this->ctl.avg - level, this->prev_consumed);
+
+ /* Update target level */
+ target = BUFFERING_TARGET(this->spike.max, packet_size);
+
+ if (level > SPA_MAX(4 * target, 2*(int32_t)duration) &&
+ avail > data_size) {
+ /* Lagging too much: drop data */
+ uint32_t size = SPA_MIN(avail - data_size,
+ (level - target*5/2) * this->frame_size);
+
+ spa_bt_decode_buffer_read(this, size);
+ spa_log_trace(this->log, "%p overrun samples:%d level:%d target:%d",
+ this, (int)size/this->frame_size,
+ (int)level, (int)target);
+
+ spa_bt_decode_buffer_recover(this);
+ }
+
+ this->pos += this->prev_consumed;
+ if (this->pos > this->rate) {
+ spa_log_debug(this->log,
+ "%p avg:%d target:%d level:%d buffer:%d spike:%d corr:%f",
+ this,
+ (int)this->ctl.avg,
+ (int)target,
+ (int)level,
+ (int)(avail / this->frame_size),
+ (int)this->spike.max,
+ (double)this->corr);
+ this->pos = 0;
+ }
+
+ this->corr = spa_bt_rate_control_update(&this->ctl,
+ level, target, this->prev_consumed, avg_period);
+
+ spa_bt_decode_buffer_get_read(this, &avail);
+
+ this->prev_consumed = 0;
+ this->prev_avail = avail;
+ this->underrun = 0;
+ this->received = false;
+ }
+
+ if (avail < data_size) {
+ spa_log_trace(this->log, "%p underrun samples:%d", this,
+ (data_size - avail) / this->frame_size);
+ this->underrun += samples;
+ if (this->underrun >= SPA_MIN((uint32_t)max_level, this->buffer_size / this->frame_size)) {
+ this->buffering = true;
+ spa_log_debug(this->log, "%p underrun too much: start buffering", this);
+ }
+ }
+
+ this->prev_consumed += samples;
+}
+
+#endif
diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h
new file mode 100644
index 0000000..5d194b3
--- /dev/null
+++ b/spa/plugins/bluez5/defs.h
@@ -0,0 +1,822 @@
+/* Spa Bluez5 Monitor
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_DEFS_H
+#define SPA_BLUEZ5_DEFS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <math.h>
+
+#include <spa/support/dbus.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/utils/hook.h>
+
+#include <dbus/dbus.h>
+
+#include "config.h"
+
+#define BLUEZ_SERVICE "org.bluez"
+#define BLUEZ_PROFILE_MANAGER_INTERFACE BLUEZ_SERVICE ".ProfileManager1"
+#define BLUEZ_PROFILE_INTERFACE BLUEZ_SERVICE ".Profile1"
+#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1"
+#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1"
+#define BLUEZ_MEDIA_INTERFACE BLUEZ_SERVICE ".Media1"
+#define BLUEZ_MEDIA_ENDPOINT_INTERFACE BLUEZ_SERVICE ".MediaEndpoint1"
+#define BLUEZ_MEDIA_TRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1"
+#define BLUEZ_INTERFACE_BATTERY_PROVIDER BLUEZ_SERVICE ".BatteryProvider1"
+#define BLUEZ_INTERFACE_BATTERY_PROVIDER_MANAGER BLUEZ_SERVICE ".BatteryProviderManager1"
+
+#define DBUS_INTERFACE_OBJECT_MANAGER "org.freedesktop.DBus.ObjectManager"
+#define DBUS_SIGNAL_INTERFACES_ADDED "InterfacesAdded"
+#define DBUS_SIGNAL_INTERFACES_REMOVED "InterfacesRemoved"
+#define DBUS_SIGNAL_PROPERTIES_CHANGED "PropertiesChanged"
+
+#define PIPEWIRE_BATTERY_PROVIDER "/org/freedesktop/pipewire/battery"
+
+#define OBJECT_MANAGER_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>\n" \
+ " <interface name=\"org.freedesktop.DBus.ObjectManager\">\n" \
+ " <method name=\"GetManagedObjects\">\n" \
+ " <arg name=\"objects\" direction=\"out\" type=\"a{oa{sa{sv}}}\"/>\n" \
+ " </method>\n" \
+ " <signal name=\"InterfacesAdded\">\n" \
+ " <arg name=\"object\" type=\"o\"/>\n" \
+ " <arg name=\"interfaces\" type=\"a{sa{sv}}\"/>\n" \
+ " </signal>\n" \
+ " <signal name=\"InterfacesRemoved\">\n" \
+ " <arg name=\"object\" type=\"o\"/>\n" \
+ " <arg name=\"interfaces\" type=\"as\"/>\n" \
+ " </signal>\n" \
+ " </interface>\n" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">\n" \
+ " <method name=\"Introspect\">\n" \
+ " <arg name=\"data\" direction=\"out\" type=\"s\"/>\n" \
+ " </method>\n" \
+ " </interface>\n" \
+ " <node name=\"A2DPSink\"/>\n" \
+ " <node name=\"A2DPSource\"/>\n" \
+ "</node>\n"
+
+#define ENDPOINT_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name=\"" BLUEZ_MEDIA_ENDPOINT_INTERFACE "\">" \
+ " <method name=\"SetConfiguration\">" \
+ " <arg name=\"transport\" direction=\"in\" type=\"o\"/>" \
+ " <arg name=\"properties\" direction=\"in\" type=\"ay\"/>" \
+ " </method>" \
+ " <method name=\"SelectConfiguration\">" \
+ " <arg name=\"capabilities\" direction=\"in\" type=\"ay\"/>" \
+ " <arg name=\"configuration\" direction=\"out\" type=\"ay\"/>" \
+ " </method>" \
+ " <method name=\"ClearConfiguration\">" \
+ " <arg name=\"transport\" direction=\"in\" type=\"o\"/>" \
+ " </method>" \
+ " <method name=\"Release\">" \
+ " </method>" \
+ " </interface>" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
+ " <method name=\"Introspect\">" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+#define PROFILE_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name=\"" BLUEZ_PROFILE_INTERFACE "\">" \
+ " <method name=\"Release\">" \
+ " </method>" \
+ " <method name=\"RequestDisconnection\">" \
+ " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \
+ " </method>" \
+ " <method name=\"NewConnection\">" \
+ " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \
+ " <arg name=\"fd\" direction=\"in\" type=\"h\"/>" \
+ " <arg name=\"opts\" direction=\"in\" type=\"a{sv}\"/>" \
+ " </method>" \
+ " </interface>" \
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">" \
+ " <method name=\"Introspect\">" \
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+#define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported"
+
+#define SPA_BT_UUID_A2DP_SOURCE "0000110a-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_A2DP_SINK "0000110b-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_HS "00001108-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_HS_ALT "00001131-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HSP_AG "00001112-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HFP_HF "0000111e-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_HFP_AG "0000111f-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_PACS "00001850-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_BAP_SINK "00002bc9-0000-1000-8000-00805f9b34fb"
+#define SPA_BT_UUID_BAP_SOURCE "00002bcb-0000-1000-8000-00805f9b34fb"
+
+#define PROFILE_HSP_AG "/Profile/HSPAG"
+#define PROFILE_HSP_HS "/Profile/HSPHS"
+#define PROFILE_HFP_AG "/Profile/HFPAG"
+#define PROFILE_HFP_HF "/Profile/HFPHF"
+
+#define HSP_HS_DEFAULT_CHANNEL 3
+
+#define SOURCE_ID_BLUETOOTH 0x1 /* Bluetooth SIG */
+#define SOURCE_ID_USB 0x2 /* USB Implementer's Forum */
+
+#define BUS_TYPE_USB 1
+#define BUS_TYPE_OTHER 255
+
+#define HFP_AUDIO_CODEC_CVSD 0x01
+#define HFP_AUDIO_CODEC_MSBC 0x02
+
+#define MEDIA_OBJECT_MANAGER_PATH "/MediaEndpoint"
+#define A2DP_SINK_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/A2DPSink"
+#define A2DP_SOURCE_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/A2DPSource"
+
+#define BAP_SINK_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/BAPSink"
+#define BAP_SOURCE_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/BAPSource"
+
+#define SPA_BT_UNKNOWN_DELAY 0
+
+#define SPA_BT_NO_BATTERY ((uint8_t)255)
+
+/* HFP uses SBC encoding with precisely defined parameters. Hence, the size
+ * of the input (number of PCM samples) and output is known up front. */
+#define MSBC_DECODED_SIZE 240
+#define MSBC_ENCODED_SIZE 60 /* 2 bytes header + 57 mSBC payload + 1 byte padding */
+#define MSBC_PAYLOAD_SIZE 57
+
+enum spa_bt_media_direction {
+ SPA_BT_MEDIA_SOURCE,
+ SPA_BT_MEDIA_SINK,
+};
+
+enum spa_bt_profile {
+ SPA_BT_PROFILE_NULL = 0,
+ SPA_BT_PROFILE_BAP_SINK = (1 << 0),
+ SPA_BT_PROFILE_BAP_SOURCE = (1 << 1),
+ SPA_BT_PROFILE_A2DP_SINK = (1 << 2),
+ SPA_BT_PROFILE_A2DP_SOURCE = (1 << 3),
+ SPA_BT_PROFILE_HSP_HS = (1 << 4),
+ SPA_BT_PROFILE_HSP_AG = (1 << 5),
+ SPA_BT_PROFILE_HFP_HF = (1 << 6),
+ SPA_BT_PROFILE_HFP_AG = (1 << 7),
+
+ SPA_BT_PROFILE_A2DP_DUPLEX = (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE),
+ SPA_BT_PROFILE_BAP_DUPLEX = (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE),
+ SPA_BT_PROFILE_HEADSET_HEAD_UNIT = (SPA_BT_PROFILE_HSP_HS | SPA_BT_PROFILE_HFP_HF),
+ SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY = (SPA_BT_PROFILE_HSP_AG | SPA_BT_PROFILE_HFP_AG),
+ SPA_BT_PROFILE_HEADSET_AUDIO = (SPA_BT_PROFILE_HEADSET_HEAD_UNIT | SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY),
+
+ SPA_BT_PROFILE_MEDIA_SINK = (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_BAP_SINK),
+ SPA_BT_PROFILE_MEDIA_SOURCE = (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_BAP_SOURCE),
+};
+
+static inline enum spa_bt_profile spa_bt_profile_from_uuid(const char *uuid)
+{
+ if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SOURCE) == 0)
+ return SPA_BT_PROFILE_A2DP_SOURCE;
+ else if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SINK) == 0)
+ return SPA_BT_PROFILE_A2DP_SINK;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HSP_HS) == 0)
+ return SPA_BT_PROFILE_HSP_HS;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HSP_HS_ALT) == 0)
+ return SPA_BT_PROFILE_HSP_HS;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HSP_AG) == 0)
+ return SPA_BT_PROFILE_HSP_AG;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HFP_HF) == 0)
+ return SPA_BT_PROFILE_HFP_HF;
+ else if (strcasecmp(uuid, SPA_BT_UUID_HFP_AG) == 0)
+ return SPA_BT_PROFILE_HFP_AG;
+ else if (strcasecmp(uuid, SPA_BT_UUID_BAP_SINK) == 0)
+ return SPA_BT_PROFILE_BAP_SINK;
+ else if (strcasecmp(uuid, SPA_BT_UUID_BAP_SOURCE) == 0)
+ return SPA_BT_PROFILE_BAP_SOURCE;
+ else
+ return 0;
+}
+int spa_bt_profiles_from_json_array(const char *str);
+
+int spa_bt_format_vendor_product_id(uint16_t source_id, uint16_t vendor_id,
+ uint16_t product_id, char *vendor_str, int vendor_str_size,
+ char *product_str, int product_str_size);
+
+enum spa_bt_hfp_ag_feature {
+ SPA_BT_HFP_AG_FEATURE_NONE = (0),
+ SPA_BT_HFP_AG_FEATURE_3WAY = (1 << 0),
+ SPA_BT_HFP_AG_FEATURE_ECNR = (1 << 1),
+ SPA_BT_HFP_AG_FEATURE_VOICE_RECOG = (1 << 2),
+ SPA_BT_HFP_AG_FEATURE_IN_BAND_RING_TONE = (1 << 3),
+ SPA_BT_HFP_AG_FEATURE_ATTACH_VOICE_TAG = (1 << 4),
+ SPA_BT_HFP_AG_FEATURE_REJECT_CALL = (1 << 5),
+ SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS = (1 << 6),
+ SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_CONTROL = (1 << 7),
+ SPA_BT_HFP_AG_FEATURE_EXTENDED_RES_CODE = (1 << 8),
+ SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION = (1 << 9),
+ SPA_BT_HFP_AG_FEATURE_HF_INDICATORS = (1 << 10),
+ SPA_BT_HFP_AG_FEATURE_ESCO_S4 = (1 << 11),
+};
+
+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),
+};
+
+enum spa_bt_hfp_hf_feature {
+ SPA_BT_HFP_HF_FEATURE_NONE = (0),
+ SPA_BT_HFP_HF_FEATURE_ECNR = (1 << 0),
+ SPA_BT_HFP_HF_FEATURE_3WAY = (1 << 1),
+ SPA_BT_HFP_HF_FEATURE_CLIP = (1 << 2),
+ SPA_BT_HFP_HF_FEATURE_VOICE_RECOGNITION = (1 << 3),
+ SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL = (1 << 4),
+ SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_STATUS = (1 << 5),
+ SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_CONTROL = (1 << 6),
+ SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION = (1 << 7),
+ SPA_BT_HFP_HF_FEATURE_HF_INDICATORS = (1 << 8),
+ SPA_BT_HFP_HF_FEATURE_ESCO_S4 = (1 << 9),
+};
+
+/* https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Hands-Free%20Profile.pdf */
+enum spa_bt_hfp_hf_indicator {
+ SPA_BT_HFP_HF_INDICATOR_ENHANCED_SAFETY = 1,
+ SPA_BT_HFP_HF_INDICATOR_BATTERY_LEVEL = 2,
+};
+
+/* HFP Command AT+IPHONEACCEV
+ * https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf */
+enum spa_bt_hfp_hf_iphoneaccev_keys {
+ SPA_BT_HFP_HF_IPHONEACCEV_KEY_BATTERY_LEVEL = 1,
+ SPA_BT_HFP_HF_IPHONEACCEV_KEY_DOCK_STATE = 2,
+};
+
+/* HFP Command AT+XAPL
+ * https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
+ * Bits 0, 5 and above are reserved. */
+enum spa_bt_hfp_hf_xapl_features {
+ SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING = (1 << 1),
+ SPA_BT_HFP_HF_XAPL_FEATURE_DOCKED_OR_POWERED = (1 << 2),
+ SPA_BT_HFP_HF_XAPL_FEATURE_SIRI_STATUS_REPORTING = (1 << 3),
+ SPA_BT_HFP_HF_XAPL_FEATURE_NOISE_REDUCTION_REPORTING = (1 << 4),
+};
+
+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_REMOTE_VOLUME_CONTROL = (1 << 4),
+ SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH = (1 << 5),
+};
+
+static inline const char *spa_bt_profile_name (enum spa_bt_profile profile) {
+ switch (profile) {
+ case SPA_BT_PROFILE_A2DP_SOURCE:
+ return "a2dp-source";
+ case SPA_BT_PROFILE_A2DP_SINK:
+ return "a2dp-sink";
+ case SPA_BT_PROFILE_A2DP_DUPLEX:
+ return "a2dp-duplex";
+ case SPA_BT_PROFILE_HSP_HS:
+ case SPA_BT_PROFILE_HFP_HF:
+ case SPA_BT_PROFILE_HEADSET_HEAD_UNIT:
+ return "headset-head-unit";
+ case SPA_BT_PROFILE_HSP_AG:
+ case SPA_BT_PROFILE_HFP_AG:
+ case SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY:
+ return "headset-audio-gateway";
+ case SPA_BT_PROFILE_HEADSET_AUDIO:
+ return "headset-audio";
+ case SPA_BT_PROFILE_BAP_SOURCE:
+ return "bap-source";
+ case SPA_BT_PROFILE_BAP_SINK:
+ return "bap-sink";
+ case SPA_BT_PROFILE_BAP_DUPLEX:
+ return "bap-duplex";
+ default:
+ break;
+ }
+ return "unknown";
+}
+
+struct spa_bt_monitor;
+struct spa_bt_backend;
+struct spa_bt_player;
+
+struct spa_bt_adapter {
+ struct spa_list link;
+ struct spa_bt_monitor *monitor;
+ struct spa_bt_player *dummy_player;
+ char *path;
+ char *alias;
+ char *address;
+ char *name;
+ int bus_type;
+ uint16_t source_id;
+ uint16_t vendor_id;
+ uint16_t product_id;
+ uint16_t version_id;
+ uint32_t bluetooth_class;
+ uint32_t profiles;
+ int powered;
+ unsigned int has_msbc:1;
+ unsigned int msbc_probed:1;
+ unsigned int endpoints_registered:1;
+ unsigned int application_registered:1;
+ unsigned int player_registered:1;
+ unsigned int has_battery_provider:1;
+ unsigned int battery_provider_unavailable:1;
+};
+
+enum spa_bt_form_factor {
+ SPA_BT_FORM_FACTOR_UNKNOWN,
+ SPA_BT_FORM_FACTOR_HEADSET,
+ SPA_BT_FORM_FACTOR_HANDSFREE,
+ SPA_BT_FORM_FACTOR_MICROPHONE,
+ SPA_BT_FORM_FACTOR_SPEAKER,
+ SPA_BT_FORM_FACTOR_HEADPHONE,
+ SPA_BT_FORM_FACTOR_PORTABLE,
+ SPA_BT_FORM_FACTOR_CAR,
+ SPA_BT_FORM_FACTOR_HIFI,
+ SPA_BT_FORM_FACTOR_PHONE,
+};
+
+static inline const char *spa_bt_form_factor_name(enum spa_bt_form_factor ff)
+{
+ switch (ff) {
+ case SPA_BT_FORM_FACTOR_HEADSET:
+ return "headset";
+ case SPA_BT_FORM_FACTOR_HANDSFREE:
+ return "hands-free";
+ case SPA_BT_FORM_FACTOR_MICROPHONE:
+ return "microphone";
+ case SPA_BT_FORM_FACTOR_SPEAKER:
+ return "speaker";
+ case SPA_BT_FORM_FACTOR_HEADPHONE:
+ return "headphone";
+ case SPA_BT_FORM_FACTOR_PORTABLE:
+ return "portable";
+ case SPA_BT_FORM_FACTOR_CAR:
+ return "car";
+ case SPA_BT_FORM_FACTOR_HIFI:
+ return "hifi";
+ case SPA_BT_FORM_FACTOR_PHONE:
+ return "phone";
+ case SPA_BT_FORM_FACTOR_UNKNOWN:
+ default:
+ return "unknown";
+ }
+}
+
+static inline enum spa_bt_form_factor spa_bt_form_factor_from_class(uint32_t bluetooth_class)
+{
+ uint32_t major, minor;
+ /* See Bluetooth Assigned Numbers:
+ * https://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm */
+ major = (bluetooth_class >> 8) & 0x1F;
+ minor = (bluetooth_class >> 2) & 0x3F;
+
+ switch (major) {
+ case 2:
+ return SPA_BT_FORM_FACTOR_PHONE;
+ case 4:
+ switch (minor) {
+ case 1:
+ return SPA_BT_FORM_FACTOR_HEADSET;
+ case 2:
+ return SPA_BT_FORM_FACTOR_HANDSFREE;
+ case 4:
+ return SPA_BT_FORM_FACTOR_MICROPHONE;
+ case 5:
+ return SPA_BT_FORM_FACTOR_SPEAKER;
+ case 6:
+ return SPA_BT_FORM_FACTOR_HEADPHONE;
+ case 7:
+ return SPA_BT_FORM_FACTOR_PORTABLE;
+ case 8:
+ return SPA_BT_FORM_FACTOR_CAR;
+ case 10:
+ return SPA_BT_FORM_FACTOR_HIFI;
+ }
+ }
+ return SPA_BT_FORM_FACTOR_UNKNOWN;
+}
+
+struct spa_bt_media_codec_switch;
+struct spa_bt_transport;
+
+struct spa_bt_device_events {
+#define SPA_VERSION_BT_DEVICE_EVENTS 0
+ uint32_t version;
+
+ /** Device connection status */
+ void (*connected) (void *data, bool connected);
+
+ /** Codec switching completed */
+ void (*codec_switched) (void *data, int status);
+
+ /** Profile configuration changed */
+ void (*profiles_changed) (void *data, uint32_t prev_profiles, uint32_t prev_connected);
+
+ /** Device freed */
+ void (*destroy) (void *data);
+};
+
+struct media_codec;
+
+struct spa_bt_device {
+ struct spa_list link;
+ struct spa_bt_monitor *monitor;
+ struct spa_bt_adapter *adapter;
+ uint32_t id;
+ char *path;
+ char *alias;
+ char *address;
+ char *adapter_path;
+ char *battery_path;
+ char *name;
+ char *icon;
+ uint16_t source_id;
+ uint16_t vendor_id;
+ uint16_t product_id;
+ uint16_t version_id;
+ uint32_t bluetooth_class;
+ uint16_t appearance;
+ uint16_t RSSI;
+ int paired;
+ int trusted;
+ int connected;
+ int blocked;
+ uint32_t profiles;
+ uint32_t connected_profiles;
+ uint32_t reconnect_profiles;
+ int reconnect_state;
+ struct spa_source timer;
+ struct spa_list remote_endpoint_list;
+ struct spa_list transport_list;
+ struct spa_list codec_switch_list;
+ uint8_t battery;
+ int has_battery;
+
+ uint32_t hw_volume_profiles;
+ /* Even though A2DP volume is exposed on transport interface, the
+ * volume activation info would not be variate between transports
+ * under same device. So it's safe to cache activation info here. */
+ bool a2dp_volume_active[2];
+
+ uint64_t last_bluez_action_time;
+
+ struct spa_hook_list listener_list;
+ bool added;
+
+ const struct spa_dict *settings;
+
+ DBusPendingCall *battery_pending_call;
+
+ const struct media_codec *preferred_codec;
+};
+
+struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path);
+struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monitor, const char *remote_address, const char *local_address);
+int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile profile);
+int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile);
+int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force);
+int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs);
+bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, bool sink);
+const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count, bool sink);
+int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec);
+int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec);
+int spa_bt_device_release_transports(struct spa_bt_device *device);
+int spa_bt_device_report_battery_level(struct spa_bt_device *device, uint8_t percentage);
+void spa_bt_device_update_last_bluez_action_time(struct spa_bt_device *device);
+
+#define spa_bt_device_emit(d,m,v,...) spa_hook_list_call(&(d)->listener_list, \
+ struct spa_bt_device_events, \
+ m, v, ##__VA_ARGS__)
+#define spa_bt_device_emit_connected(d,...) spa_bt_device_emit(d, connected, 0, __VA_ARGS__)
+#define spa_bt_device_emit_codec_switched(d,...) spa_bt_device_emit(d, codec_switched, 0, __VA_ARGS__)
+#define spa_bt_device_emit_profiles_changed(d,...) spa_bt_device_emit(d, profiles_changed, 0, __VA_ARGS__)
+#define spa_bt_device_emit_destroy(d) spa_bt_device_emit(d, destroy, 0)
+#define spa_bt_device_add_listener(d,listener,events,data) \
+ spa_hook_list_append(&(d)->listener_list, listener, events, data)
+
+struct spa_bt_sco_io;
+
+struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_loop *data_loop, int fd, uint16_t read_mtu, uint16_t write_mtu);
+void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io);
+void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *userdata, uint8_t *data, int size), void *userdata);
+void spa_bt_sco_io_set_sink_cb(struct spa_bt_sco_io *io, int (*sink_cb)(void *userdata), void *userdata);
+int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *data, int size);
+
+#define SPA_BT_VOLUME_ID_RX 0
+#define SPA_BT_VOLUME_ID_TX 1
+#define SPA_BT_VOLUME_ID_TERM 2
+
+#define SPA_BT_VOLUME_INVALID -1
+#define SPA_BT_VOLUME_HS_MAX 15
+#define SPA_BT_VOLUME_A2DP_MAX 127
+
+enum spa_bt_transport_state {
+ SPA_BT_TRANSPORT_STATE_IDLE,
+ SPA_BT_TRANSPORT_STATE_PENDING,
+ SPA_BT_TRANSPORT_STATE_ACTIVE,
+};
+
+struct spa_bt_transport_events {
+#define SPA_VERSION_BT_TRANSPORT_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+ void (*delay_changed) (void *data);
+ void (*state_changed) (void *data, enum spa_bt_transport_state old,
+ enum spa_bt_transport_state state);
+ void (*volume_changed) (void *data);
+};
+
+struct spa_bt_transport_implementation {
+#define SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION 0
+ uint32_t version;
+
+ int (*acquire) (void *data, bool optional);
+ int (*release) (void *data);
+ int (*set_volume) (void *data, int id, float volume);
+ int (*destroy) (void *data);
+};
+
+struct spa_bt_transport_volume {
+ bool active;
+ float volume;
+ int hw_volume_max;
+
+ /* XXX: items below should be put to user_data */
+ int hw_volume;
+ int new_hw_volume;
+};
+
+struct spa_bt_transport {
+ struct spa_list link;
+ struct spa_bt_monitor *monitor;
+ struct spa_bt_backend *backend;
+ char *path;
+ struct spa_bt_device *device;
+ struct spa_list device_link;
+ enum spa_bt_profile profile;
+ enum spa_bt_transport_state state;
+ const struct media_codec *media_codec;
+ unsigned int codec;
+ void *configuration;
+ int configuration_len;
+ char *endpoint_path;
+ bool bap_initiator;
+ struct spa_list bap_transport_linked;
+
+ uint32_t n_channels;
+ uint32_t channels[64];
+
+ struct spa_bt_transport_volume volumes[SPA_BT_VOLUME_ID_TERM];
+
+ int acquire_refcount;
+ bool acquired;
+ bool keepalive;
+ int fd;
+ uint16_t read_mtu;
+ uint16_t write_mtu;
+ uint16_t delay;
+
+ struct spa_bt_sco_io *sco_io;
+
+ struct spa_source volume_timer;
+ struct spa_source release_timer;
+
+ struct spa_hook_list listener_list;
+ struct spa_callbacks impl;
+
+ /* user_data must be the last item in the struct */
+ void *user_data;
+};
+
+struct spa_bt_transport *spa_bt_transport_create(struct spa_bt_monitor *monitor, char *path, size_t extra);
+void spa_bt_transport_free(struct spa_bt_transport *transport);
+void spa_bt_transport_set_state(struct spa_bt_transport *transport, enum spa_bt_transport_state state);
+struct spa_bt_transport *spa_bt_transport_find(struct spa_bt_monitor *monitor, const char *path);
+struct spa_bt_transport *spa_bt_transport_find_full(struct spa_bt_monitor *monitor,
+ bool (*callback) (struct spa_bt_transport *t, const void *data),
+ const void *data);
+int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *transport);
+bool spa_bt_transport_volume_enabled(struct spa_bt_transport *transport);
+
+int spa_bt_transport_acquire(struct spa_bt_transport *t, bool optional);
+int spa_bt_transport_release(struct spa_bt_transport *t);
+int spa_bt_transport_keepalive(struct spa_bt_transport *t, bool keepalive);
+int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop);
+
+#define spa_bt_transport_emit(t,m,v,...) spa_hook_list_call(&(t)->listener_list, \
+ struct spa_bt_transport_events, \
+ m, v, ##__VA_ARGS__)
+#define spa_bt_transport_emit_destroy(t) spa_bt_transport_emit(t, destroy, 0)
+#define spa_bt_transport_emit_delay_changed(t) spa_bt_transport_emit(t, delay_changed, 0)
+#define spa_bt_transport_emit_state_changed(t,...) spa_bt_transport_emit(t, state_changed, 0, __VA_ARGS__)
+#define spa_bt_transport_emit_volume_changed(t) spa_bt_transport_emit(t, volume_changed, 0)
+
+#define spa_bt_transport_add_listener(t,listener,events,data) \
+ spa_hook_list_append(&(t)->listener_list, listener, events, data)
+
+#define spa_bt_transport_set_implementation(t,_impl,_data) \
+ (t)->impl = SPA_CALLBACKS_INIT(_impl, _data)
+
+#define spa_bt_transport_impl(t,m,v,...) \
+({ \
+ int res = 0; \
+ spa_callbacks_call_res(&(t)->impl, \
+ struct spa_bt_transport_implementation, \
+ res, m, v, ##__VA_ARGS__); \
+ res; \
+})
+
+#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__)
+
+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)
+ return SPA_BT_TRANSPORT_STATE_PENDING;
+ else if (strcasecmp("active", value) == 0)
+ return SPA_BT_TRANSPORT_STATE_ACTIVE;
+ else
+ return SPA_BT_TRANSPORT_STATE_IDLE;
+}
+
+#define DEFAULT_AG_VOLUME 1.0f
+#define DEFAULT_RX_VOLUME 1.0f
+#define DEFAULT_TX_VOLUME 0.064f /* spa_bt_volume_hw_to_linear(40, 100) */
+
+/* AVRCP/HSP volume is considered as percentage, so map it to pulseaudio (cubic) volume. */
+static inline uint32_t spa_bt_volume_linear_to_hw(double v, uint32_t hw_volume_max)
+{
+ if (v <= 0.0)
+ return 0;
+ if (v >= 1.0)
+ return hw_volume_max;
+ return SPA_CLAMP((uint64_t) lround(cbrt(v) * hw_volume_max),
+ 0u, hw_volume_max);
+}
+
+static inline double spa_bt_volume_hw_to_linear(uint32_t v, uint32_t hw_volume_max)
+{
+ double f;
+ if (v <= 0)
+ return 0.0;
+ if (v >= hw_volume_max)
+ return 1.0;
+ f = ((double) v / hw_volume_max);
+ return f * f * f;
+}
+
+enum spa_bt_feature {
+ SPA_BT_FEATURE_MSBC = (1 << 0),
+ SPA_BT_FEATURE_MSBC_ALT1 = (1 << 1),
+ SPA_BT_FEATURE_MSBC_ALT1_RTL = (1 << 2),
+ SPA_BT_FEATURE_HW_VOLUME = (1 << 3),
+ SPA_BT_FEATURE_HW_VOLUME_MIC = (1 << 4),
+ SPA_BT_FEATURE_SBC_XQ = (1 << 5),
+ SPA_BT_FEATURE_FASTSTREAM = (1 << 6),
+ SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7),
+};
+
+struct spa_bt_quirks;
+
+struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct spa_log *log);
+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_destroy(struct spa_bt_quirks *quirks);
+
+int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter);
+
+struct spa_bt_backend_implementation {
+#define SPA_VERSION_BT_BACKEND_IMPLEMENTATION 0
+ uint32_t version;
+
+ int (*free) (void *data);
+ int (*register_profiles) (void *data);
+ int (*unregister_profiles) (void *data);
+ int (*ensure_codec) (void *data, struct spa_bt_device *device, unsigned int codec);
+ int (*supports_codec) (void *data, struct spa_bt_device *device, unsigned int codec);
+};
+
+struct spa_bt_backend {
+ struct spa_callbacks impl;
+ const char *name;
+ bool available;
+ bool exclusive;
+};
+
+#define spa_bt_backend_set_implementation(b,_impl,_data) \
+ (b)->impl = SPA_CALLBACKS_INIT(_impl, _data)
+
+#define spa_bt_backend_impl(b,m,v,...) \
+({ \
+ int res = -ENOTSUP; \
+ if (b) \
+ spa_callbacks_call_res(&(b)->impl, \
+ struct spa_bt_backend_implementation, \
+ res, m, v, ##__VA_ARGS__); \
+ res; \
+})
+
+#define spa_bt_backend_free(b) spa_bt_backend_impl(b, free, 0)
+#define spa_bt_backend_register_profiles(b) spa_bt_backend_impl(b, register_profiles, 0)
+#define spa_bt_backend_unregister_profiles(b) spa_bt_backend_impl(b, unregister_profiles, 0)
+#define spa_bt_backend_ensure_codec(b,...) spa_bt_backend_impl(b, ensure_codec, 0, __VA_ARGS__)
+#define spa_bt_backend_supports_codec(b,...) spa_bt_backend_impl(b, supports_codec, 0, __VA_ARGS__)
+
+static inline struct spa_bt_backend *dummy_backend_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ return NULL;
+}
+
+#ifdef HAVE_BLUEZ_5_BACKEND_NATIVE
+struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support);
+#else
+#define backend_native_new dummy_backend_new
+#endif
+
+#define OFONO_SERVICE "org.ofono"
+#ifdef HAVE_BLUEZ_5_BACKEND_OFONO
+struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support);
+#else
+#define backend_ofono_new dummy_backend_new
+#endif
+
+#define HSPHFPD_SERVICE "org.hsphfpd"
+#ifdef HAVE_BLUEZ_5_BACKEND_HSPHFPD
+struct spa_bt_backend *backend_hsphfpd_new(struct spa_bt_monitor *monitor,
+ void *dbus_connection,
+ const struct spa_dict *info,
+ const struct spa_bt_quirks *quirks,
+ const struct spa_support *support,
+ uint32_t n_support);
+#else
+#define backend_hsphfpd_new dummy_backend_new
+#endif
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_BLUEZ5_DEFS_H */
diff --git a/spa/plugins/bluez5/hci.c b/spa/plugins/bluez5/hci.c
new file mode 100644
index 0000000..88dc1fc
--- /dev/null
+++ b/spa/plugins/bluez5/hci.c
@@ -0,0 +1,93 @@
+/* Spa HSP/HFP native backend HCI support
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <sys/uio.h>
+#include <sys/socket.h>
+
+#include "defs.h"
+
+#ifndef HAVE_BLUEZ_5_HCI
+
+int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter)
+{
+ if (adapter->msbc_probed)
+ return adapter->has_msbc;
+ return -EOPNOTSUPP;
+}
+
+#else
+
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/hci.h>
+#include <bluetooth/hci_lib.h>
+
+int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter)
+{
+ int hci_id, res;
+ int sock = -1;
+ uint8_t features[8], max_page = 0;
+ struct sockaddr_hci a;
+ const char *str;
+
+ if (adapter->msbc_probed)
+ return adapter->has_msbc;
+
+ str = strrchr(adapter->path, '/'); /* hciXX */
+ if (str == NULL || sscanf(str, "/hci%d", &hci_id) != 1 || hci_id < 0)
+ return -ENOENT;
+
+ sock = socket(AF_BLUETOOTH, SOCK_RAW | SOCK_CLOEXEC, BTPROTO_HCI);
+ if (sock < 0)
+ goto error;
+
+ memset(&a, 0, sizeof(a));
+ a.hci_family = AF_BLUETOOTH;
+ a.hci_dev = hci_id;
+ if (bind(sock, (struct sockaddr *) &a, sizeof(a)) < 0)
+ goto error;
+
+ if (hci_read_local_ext_features(sock, 0, &max_page, features, 1000) < 0)
+ goto error;
+
+ close(sock);
+
+ adapter->msbc_probed = true;
+ adapter->has_msbc = ((features[2] & LMP_TRSP_SCO) && (features[3] & LMP_ESCO)) ? 1 : 0;
+ return adapter->has_msbc;
+
+error:
+ res = -errno;
+ if (sock >= 0)
+ close(sock);
+ return res;
+}
+
+#endif
diff --git a/spa/plugins/bluez5/media-codecs.c b/spa/plugins/bluez5/media-codecs.c
new file mode 100644
index 0000000..28445fc
--- /dev/null
+++ b/spa/plugins/bluez5/media-codecs.c
@@ -0,0 +1,212 @@
+/*
+ * BlueALSA - bluez-a2dp.c
+ * Copyright (c) 2016-2017 Arkadiusz Bokowy
+ *
+ * This file is a part of bluez-alsa.
+ *
+ * This project is licensed under the terms of the MIT license.
+ *
+ */
+
+#include <spa/utils/string.h>
+
+#include "media-codecs.h"
+
+int media_codec_select_config(const struct media_codec_config configs[], size_t n,
+ uint32_t cap, int preferred_value)
+{
+ size_t i;
+ int *scores, res;
+ unsigned int max_priority;
+
+ if (n == 0)
+ return -EINVAL;
+
+ scores = calloc(n, sizeof(int));
+ if (scores == NULL)
+ return -errno;
+
+ max_priority = configs[0].priority;
+ for (i = 1; i < n; ++i) {
+ if (configs[i].priority > max_priority)
+ max_priority = configs[i].priority;
+ }
+
+ for (i = 0; i < n; ++i) {
+ if (!(configs[i].config & cap)) {
+ scores[i] = -1;
+ continue;
+ }
+ if (configs[i].value == preferred_value)
+ scores[i] = 100 * (max_priority + 1);
+ else if (configs[i].value > preferred_value)
+ scores[i] = 10 * (max_priority + 1);
+ else
+ scores[i] = 1;
+
+ scores[i] *= configs[i].priority + 1;
+ }
+
+ res = 0;
+ for (i = 1; i < n; ++i) {
+ if (scores[i] > scores[res])
+ res = i;
+ }
+
+ if (scores[res] < 0)
+ res = -EINVAL;
+
+ free(scores);
+ return res;
+}
+
+bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_id,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings)
+{
+ uint8_t config[A2DP_MAX_CAPS_SIZE];
+ int res;
+
+ if (codec_id != codec->codec_id)
+ return false;
+
+ if (caps == NULL)
+ return false;
+
+ res = codec->select_config(codec, 0, caps, caps_size, info, global_settings, config);
+ if (res < 0)
+ return false;
+
+ if (codec->bap)
+ return true;
+ else
+ return ((size_t)res == caps_size);
+}
+
+#ifdef CODEC_PLUGIN
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_bluez5_codec_a2dp bluez5_codec_a2dp;
+};
+
+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_Bluez5CodecMedia))
+ *interface = &this->bluez5_codec_a2dp;
+ 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;
+
+ 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->bluez5_codec_a2dp.codecs = codec_plugin_media_codecs;
+ this->bluez5_codec_a2dp.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Bluez5CodecMedia,
+ SPA_VERSION_BLUEZ5_CODEC_MEDIA,
+ NULL,
+ this);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Bluez5CodecMedia,},
+};
+
+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 handle_info_items[] = {
+ { SPA_KEY_FACTORY_DESCRIPTION, "Bluetooth codec plugin" },
+};
+
+static const struct spa_dict handle_info = SPA_DICT_INIT_ARRAY(handle_info_items);
+
+static struct spa_handle_factory handle_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ NULL,
+ &handle_info,
+ 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);
+
+ if (handle_factory.name == NULL)
+ handle_factory.name = codec_plugin_factory_name;
+
+ switch (*index) {
+ case 0:
+ *factory = &handle_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+#endif
diff --git a/spa/plugins/bluez5/media-codecs.h b/spa/plugins/bluez5/media-codecs.h
new file mode 100644
index 0000000..d3447db
--- /dev/null
+++ b/spa/plugins/bluez5/media-codecs.h
@@ -0,0 +1,178 @@
+/* Spa A2DP codec API
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef SPA_BLUEZ5_A2DP_CODECS_H_
+#define SPA_BLUEZ5_A2DP_CODECS_H_
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include <spa/param/audio/format.h>
+#include <spa/param/bluetooth/audio.h>
+#include <spa/utils/names.h>
+#include <spa/support/plugin.h>
+#include <spa/pod/pod.h>
+#include <spa/pod/builder.h>
+#include <spa/support/log.h>
+
+#include "a2dp-codec-caps.h"
+#include "bap-codec-caps.h"
+
+/*
+ * The codec plugin SPA interface is private. The version should be incremented
+ * when any of the structs or semantics change.
+ */
+
+#define SPA_TYPE_INTERFACE_Bluez5CodecMedia SPA_TYPE_INFO_INTERFACE_BASE "Bluez5:Codec:Media:Private"
+
+#define SPA_VERSION_BLUEZ5_CODEC_MEDIA 7
+
+struct spa_bluez5_codec_a2dp {
+ struct spa_interface iface;
+ const struct media_codec * const *codecs; /**< NULL terminated array */
+};
+
+#define MEDIA_CODEC_FACTORY_NAME(basename) (SPA_NAME_API_CODEC_BLUEZ5_MEDIA "." basename)
+
+#ifdef CODEC_PLUGIN
+#define MEDIA_CODEC_EXPORT_DEF(basename,...) \
+ const char *codec_plugin_factory_name = MEDIA_CODEC_FACTORY_NAME(basename); \
+ static const struct media_codec * const codec_plugin_media_codec_list[] = { __VA_ARGS__, NULL }; \
+ const struct media_codec * const * const codec_plugin_media_codecs = codec_plugin_media_codec_list;
+
+extern const struct media_codec * const * const codec_plugin_media_codecs;
+extern const char *codec_plugin_factory_name;
+#endif
+
+#define MEDIA_CODEC_FLAG_SINK (1 << 0)
+
+#define A2DP_CODEC_DEFAULT_RATE 48000
+#define A2DP_CODEC_DEFAULT_CHANNELS 2
+
+enum {
+ NEED_FLUSH_NO = 0,
+ NEED_FLUSH_ALL = 1,
+ NEED_FLUSH_FRAGMENT = 2,
+};
+
+struct media_codec_audio_info {
+ uint32_t rate;
+ uint32_t channels;
+};
+
+struct media_codec {
+ enum spa_bluetooth_audio_codec id;
+ uint8_t codec_id;
+ a2dp_vendor_codec_t vendor;
+
+ bool bap;
+
+ const char *name;
+ const char *description;
+ const char *endpoint_name; /**< Endpoint name. If NULL, same as name */
+ const struct spa_dict *info;
+
+ const size_t send_buf_size;
+
+ const struct media_codec *duplex_codec; /**< Codec for non-standard A2DP duplex channel */
+
+ struct spa_log *log;
+
+ /** If fill_caps is NULL, no endpoint is registered (for sharing with another codec). */
+ int (*fill_caps) (const struct media_codec *codec, uint32_t flags,
+ uint8_t caps[A2DP_MAX_CAPS_SIZE]);
+
+ int (*select_config) (const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE]);
+ int (*enum_config) (const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *builder, struct spa_pod **param);
+ int (*validate_config) (const struct media_codec *codec, uint32_t flags,
+ const void *caps, size_t caps_size,
+ struct spa_audio_info *info);
+ int (*get_qos)(const struct media_codec *codec,
+ const void *config, size_t config_size,
+ const struct bap_endpoint_qos *endpoint_qos,
+ struct bap_codec_qos *qos);
+
+ /** qsort comparison sorting caps in order of preference for the codec.
+ * Used in codec switching to select best remote endpoints.
+ * The caps handed in correspond to this codec_id, but are
+ * otherwise not checked beforehand.
+ */
+ int (*caps_preference_cmp) (const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size,
+ const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings);
+
+ void *(*init_props) (const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings);
+ void (*clear_props) (void *);
+ int (*enum_props) (void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx,
+ struct spa_pod_builder *builder, struct spa_pod **param);
+ int (*set_props) (void *props, const struct spa_pod *param);
+
+ void *(*init) (const struct media_codec *codec, uint32_t flags, void *config, size_t config_size,
+ const struct spa_audio_info *info, void *props, size_t mtu);
+ void (*deinit) (void *data);
+
+ int (*update_props) (void *data, void *props);
+
+ int (*get_block_size) (void *data);
+
+ int (*abr_process) (void *data, size_t unsent);
+
+ int (*start_encode) (void *data,
+ void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp);
+ int (*encode) (void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out, int *need_flush);
+
+ int (*start_decode) (void *data,
+ const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp);
+ int (*decode) (void *data,
+ const void *src, size_t src_size,
+ void *dst, size_t dst_size,
+ size_t *dst_out);
+
+ int (*reduce_bitpool) (void *data);
+ int (*increase_bitpool) (void *data);
+
+ void (*set_log) (struct spa_log *global_log);
+};
+
+struct media_codec_config {
+ uint32_t config;
+ int value;
+ unsigned int priority;
+};
+
+int media_codec_select_config(const struct media_codec_config configs[], size_t n,
+ uint32_t cap, int preferred_value);
+
+bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_id,
+ const void *caps, size_t caps_size, const struct media_codec_audio_info *info,
+ const struct spa_dict *global_settings);
+
+#endif
diff --git a/spa/plugins/bluez5/media-sink.c b/spa/plugins/bluez5/media-sink.c
new file mode 100644
index 0000000..29eec1f
--- /dev/null
+++ b/spa/plugins/bluez5/media-sink.c
@@ -0,0 +1,1868 @@
+/* Spa Media Sink
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/mem.h>
+#include <spa/debug/log.h>
+
+#include <sbc/sbc.h>
+
+#include "defs.h"
+#include "rtp.h"
+#include "media-codecs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.media");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ int64_t latency_offset;
+ char clock_name[64];
+};
+
+#define FILL_FRAMES 4
+#define MIN_BUFFERS 2
+#define MAX_BUFFERS 32
+#define BUFFER_SIZE (8192*8)
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ struct spa_audio_info current_format;
+ uint32_t frame_size;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ size_t ready_offset;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint32_t quantum_limit;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define N_NODE_PARAMS 2
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+
+ struct port port;
+
+ unsigned int started:1;
+ unsigned int following:1;
+ unsigned int is_output:1;
+ unsigned int flush_pending:1;
+
+ unsigned int is_duplex:1;
+
+ struct spa_source source;
+ int timerfd;
+ struct spa_source flush_source;
+ struct spa_source flush_timer_source;
+ int flush_timerfd;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t current_time;
+ uint64_t next_time;
+ uint64_t last_error;
+ uint64_t process_time;
+
+ uint64_t prev_flush_time;
+ uint64_t next_flush_time;
+
+ const struct media_codec *codec;
+ bool codec_props_changed;
+ void *codec_props;
+ void *codec_data;
+ struct spa_audio_info codec_format;
+
+ int need_flush;
+ bool fragment;
+ uint32_t block_size;
+ uint8_t buffer[BUFFER_SIZE];
+ uint32_t buffer_used;
+ uint32_t header_size;
+ uint32_t block_count;
+ uint16_t seqnum;
+ uint32_t timestamp;
+ uint64_t sample_count;
+ uint8_t tmp_buffer[BUFFER_SIZE];
+ uint32_t tmp_buffer_used;
+ uint32_t fd_buffer_size;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+
+static void reset_props(struct impl *this, struct props *props)
+{
+ props->latency_offset = 0;
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+}
+
+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, index_offset = 0;
+ bool enum_codec = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
+ SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, INT64_MIN, INT64_MAX));
+ break;
+ default:
+ enum_codec = true;
+ index_offset = 1;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset));
+ break;
+ default:
+ enum_codec = true;
+ index_offset = 1;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (enum_codec) {
+ int res;
+ if (this->codec->enum_props == NULL || this->codec_props == NULL ||
+ this->transport == NULL)
+ return 0;
+ else if ((res = this->codec->enum_props(this->codec_props,
+ this->transport->device->settings,
+ id, result.index - index_offset, &b, &param)) != 1)
+ return res;
+ }
+
+ 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 set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ set_timers(this);
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static void emit_port_info(struct impl *this, struct port *port, bool full);
+
+static void set_latency(struct impl *this, bool emit_latency)
+{
+ struct port *port = &this->port;
+ int64_t delay;
+
+ if (this->transport == NULL)
+ return;
+
+ delay = spa_bt_transport_get_delay_nsec(this->transport);
+ delay += SPA_CLAMP(this->props.latency_offset, -delay, INT64_MAX / 2);
+ port->latency.min_ns = port->latency.max_ns = delay;
+
+ if (emit_latency) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_port_info(this, port, false);
+ }
+}
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(this, &new_props);
+ } else {
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset));
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+
+ if (changed)
+ set_latency(this, true);
+
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ int res, codec_res = 0;
+ res = apply_props(this, param);
+ if (this->codec_props && this->codec->set_props) {
+ codec_res = this->codec->set_props(this->codec_props, param);
+ if (codec_res > 0)
+ this->codec_props_changed = true;
+ }
+ if (res > 0 || codec_res > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int reset_buffer(struct impl *this)
+{
+ if (this->codec_props_changed && this->codec_props
+ && this->codec->update_props) {
+ this->codec->update_props(this->codec_data, this->codec_props);
+ this->codec_props_changed = false;
+ }
+ this->need_flush = 0;
+ this->block_count = 0;
+ this->fragment = false;
+ this->buffer_used = this->codec->start_encode(this->codec_data,
+ this->buffer, sizeof(this->buffer),
+ this->seqnum++, this->timestamp);
+ this->header_size = this->buffer_used;
+ this->timestamp = this->sample_count;
+ return 0;
+}
+
+static int get_transport_unused_size(struct impl *this)
+{
+ int res, value;
+ res = ioctl(this->flush_source.fd, TIOCOUTQ, &value);
+ if (res < 0) {
+ spa_log_error(this->log, "%p: ioctl fail: %m", this);
+ return -errno;
+ }
+ spa_log_trace(this->log, "%p: fd unused buffer size:%d/%d", this, value, this->fd_buffer_size);
+ return value;
+}
+
+static int send_buffer(struct impl *this)
+{
+ int written, unsent;
+
+ unsent = get_transport_unused_size(this);
+ if (unsent >= 0) {
+ unsent = this->fd_buffer_size - unsent;
+ this->codec->abr_process(this->codec_data, unsent);
+ }
+
+ written = send(this->flush_source.fd, this->buffer,
+ this->buffer_used, MSG_DONTWAIT | MSG_NOSIGNAL);
+
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
+ struct timespec ts;
+ uint64_t now;
+ uint64_t dt;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ dt = now - this->prev_flush_time;
+ this->prev_flush_time = now;
+
+ spa_log_trace(this->log,
+ "%p: send blocks:%d block:%u seq:%u ts:%u size:%u "
+ "wrote:%d dt:%"PRIu64,
+ this, this->block_count, this->block_size, this->seqnum,
+ this->timestamp, this->buffer_used, written, dt);
+ }
+
+ if (written < 0) {
+ spa_log_debug(this->log, "%p: %m", this);
+ return -errno;
+ }
+
+ return written;
+}
+
+static int encode_buffer(struct impl *this, const void *data, uint32_t size)
+{
+ int processed;
+ size_t out_encoded;
+ struct port *port = &this->port;
+ const void *from_data = data;
+ int from_size = size;
+
+ spa_log_trace(this->log, "%p: encode %d used %d, %d %d %d",
+ this, size, this->buffer_used, port->frame_size, this->block_size,
+ this->block_count);
+
+ if (this->need_flush)
+ return 0;
+
+ if (this->buffer_used >= sizeof(this->buffer))
+ return -ENOSPC;
+
+ if (size < this->block_size - this->tmp_buffer_used) {
+ memcpy(this->tmp_buffer + this->tmp_buffer_used, data, size);
+ this->tmp_buffer_used += size;
+ return size;
+ } else if (this->tmp_buffer_used > 0) {
+ memcpy(this->tmp_buffer + this->tmp_buffer_used, data, this->block_size - this->tmp_buffer_used);
+ from_data = this->tmp_buffer;
+ from_size = this->block_size;
+ this->tmp_buffer_used = this->block_size - this->tmp_buffer_used;
+ }
+
+ processed = this->codec->encode(this->codec_data,
+ from_data, from_size,
+ this->buffer + this->buffer_used,
+ sizeof(this->buffer) - this->buffer_used,
+ &out_encoded, &this->need_flush);
+ if (processed < 0)
+ return processed;
+
+ this->sample_count += processed / port->frame_size;
+ this->block_count += processed / this->block_size;
+ this->buffer_used += out_encoded;
+
+ spa_log_trace(this->log, "%p: processed %d %zd used %d",
+ this, processed, out_encoded, this->buffer_used);
+
+ if (this->tmp_buffer_used) {
+ processed = this->tmp_buffer_used;
+ this->tmp_buffer_used = 0;
+ }
+ return processed;
+}
+
+static int encode_fragment(struct impl *this)
+{
+ int res;
+ size_t out_encoded;
+ struct port *port = &this->port;
+
+ spa_log_trace(this->log, "%p: encode fragment used %d, %d %d %d",
+ this, this->buffer_used, port->frame_size, this->block_size,
+ this->block_count);
+
+ if (this->need_flush)
+ return 0;
+
+ res = this->codec->encode(this->codec_data,
+ NULL, 0,
+ this->buffer + this->buffer_used,
+ sizeof(this->buffer) - this->buffer_used,
+ &out_encoded, &this->need_flush);
+ if (res < 0)
+ return res;
+ if (res != 0)
+ return -EINVAL;
+
+ this->buffer_used += out_encoded;
+
+ spa_log_trace(this->log, "%p: processed fragment %zd used %d",
+ this, out_encoded, this->buffer_used);
+
+ return 0;
+}
+
+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);
+
+ if (this->need_flush)
+ return send_buffer(this);
+
+ return 0;
+}
+
+static int add_data(struct impl *this, const void *data, uint32_t size)
+{
+ int processed, total = 0;
+
+ while (size > 0) {
+ processed = encode_buffer(this, data, size);
+
+ if (processed <= 0)
+ return total > 0 ? total : processed;
+
+ data = SPA_PTROFF(data, processed, void);
+ size -= processed;
+ total += processed;
+ }
+ return total;
+}
+
+static void enable_flush_timer(struct impl *this, bool enabled)
+{
+ struct itimerspec ts;
+
+ if (!enabled)
+ this->next_flush_time = 0;
+
+ ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system,
+ this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+
+ this->flush_pending = enabled;
+}
+
+static uint32_t get_queued_frames(struct impl *this)
+{
+ struct port *port = &this->port;
+ uint32_t bytes = 0;
+ struct buffer *b;
+
+ spa_list_for_each(b, &port->ready, link) {
+ struct spa_data *d = b->buf->datas;
+
+ bytes += d[0].chunk->size;
+ }
+
+ if (bytes > port->ready_offset)
+ bytes -= port->ready_offset;
+ else
+ bytes = 0;
+
+ return bytes / port->frame_size;
+}
+
+static int flush_data(struct impl *this, uint64_t now_time)
+{
+ int written;
+ uint32_t total_frames;
+ struct port *port = &this->port;
+ int unused_buffer;
+
+ if (!this->flush_source.loop) {
+ /* I/O in error state */
+ return -EIO;
+ }
+
+ total_frames = 0;
+again:
+ written = 0;
+ if (this->fragment && !this->need_flush) {
+ int res;
+ this->fragment = false;
+ if ((res = encode_fragment(this)) < 0) {
+ /* Error */
+ reset_buffer(this);
+ return res;
+ }
+ }
+ while (!spa_list_is_empty(&port->ready) && !this->need_flush) {
+ uint8_t *src;
+ uint32_t n_bytes, n_frames;
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t index, offs, avail, l0, l1;
+
+ b = spa_list_first(&port->ready, struct buffer, link);
+ d = b->buf->datas;
+
+ src = d[0].data;
+
+ index = d[0].chunk->offset + port->ready_offset;
+ avail = d[0].chunk->size - port->ready_offset;
+ avail /= port->frame_size;
+
+ offs = index % d[0].maxsize;
+ n_frames = avail;
+ n_bytes = n_frames * port->frame_size;
+
+ l0 = SPA_MIN(n_bytes, d[0].maxsize - offs);
+ l1 = n_bytes - l0;
+
+ written = add_data(this, src + offs, l0);
+ if (written > 0 && l1 > 0)
+ written += add_data(this, src, l1);
+ if (written <= 0) {
+ if (written < 0 && written != -ENOSPC) {
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ this->port.io->buffer_id = b->id;
+ spa_log_warn(this->log, "%p: error %s, reuse buffer %u",
+ this, spa_strerror(written), b->id);
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ port->ready_offset = 0;
+ }
+ break;
+ }
+
+ n_frames = written / port->frame_size;
+
+ port->ready_offset += written;
+
+ if (port->ready_offset >= d[0].chunk->size) {
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ spa_log_trace(this->log, "%p: reuse buffer %u", this, b->id);
+ this->port.io->buffer_id = b->id;
+
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ port->ready_offset = 0;
+ }
+ total_frames += n_frames;
+
+ spa_log_trace(this->log, "%p: written %u frames", this, total_frames);
+ }
+
+ if (written > 0 && this->buffer_used == this->header_size) {
+ enable_flush_timer(this, false);
+ return 0;
+ }
+
+ if (this->flush_pending) {
+ spa_log_trace(this->log, "%p: wait for flush timer", this);
+ return 0;
+ }
+
+ /*
+ * Get socket queue size before writing to it.
+ * This should be the same as buffer size to increase bitpool
+ * Bitpool shouldn't be increased when data is left over in the buffer
+ */
+ unused_buffer = get_transport_unused_size(this);
+
+ written = flush_buffer(this);
+
+ if (written == -EAGAIN) {
+ spa_log_trace(this->log, "%p: fail flush", this);
+ if (now_time - this->last_error > SPA_NSEC_PER_SEC / 2) {
+ int res = this->codec->reduce_bitpool(this->codec_data);
+
+ spa_log_debug(this->log, "%p: reduce bitpool: %i", this, res);
+ this->last_error = now_time;
+ }
+
+ /*
+ * The socket buffer is full, and the device is not processing data
+ * fast enough, so should just skip this packet. There will be a sound
+ * glitch in any case.
+ */
+ written = this->buffer_used;
+ }
+
+ if (written < 0) {
+ spa_log_trace(this->log, "%p: error flushing %s", this,
+ spa_strerror(written));
+ reset_buffer(this);
+ enable_flush_timer(this, false);
+ return written;
+ }
+ else if (written > 0) {
+ /*
+ * We cannot write all data we have at once, since this can exceed device
+ * buffers (esp. for the A2DP low-latency codecs) and socket buffers, so
+ * flush needs to be delayed.
+ */
+ uint32_t packet_samples = this->block_count * this->block_size
+ / port->frame_size;
+ uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC
+ / port->current_format.info.raw.rate;
+
+ if (SPA_LIKELY(this->position)) {
+ uint32_t frames = get_queued_frames(this);
+ uint64_t duration_ns;
+
+ /*
+ * Flush at the time position of the next buffered sample.
+ */
+ duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC
+ / this->position->clock.rate.denom);
+ this->next_flush_time = this->process_time + duration_ns
+ - ((uint64_t)frames * SPA_NSEC_PER_SEC
+ / port->current_format.info.raw.rate);
+
+ /*
+ * We could delay the output by one packet to avoid waiting
+ * for the next buffer and so make send intervals exactly regular.
+ * However, this is not needed for A2DP or BAP. The controller
+ * will do the scheduling for us, and there's also the socket buffer
+ * in between.
+ */
+#if 0
+ this->next_flush_time += SPA_MIN(packet_time,
+ duration_ns * (port->n_buffers - 1));
+#endif
+ } else {
+ if (this->next_flush_time == 0)
+ this->next_flush_time = this->process_time;
+ this->next_flush_time += packet_time;
+ }
+
+ if (this->need_flush == NEED_FLUSH_FRAGMENT) {
+ reset_buffer(this);
+ this->fragment = true;
+ goto again;
+ }
+
+ if (now_time - this->last_error > SPA_NSEC_PER_SEC) {
+ if (unused_buffer == (int)this->fd_buffer_size) {
+ int res = this->codec->increase_bitpool(this->codec_data);
+
+ spa_log_debug(this->log, "%p: increase bitpool: %i", this, res);
+ }
+ this->last_error = now_time;
+ }
+
+ spa_log_trace(this->log, "%p: flush at:%"PRIu64" process:%"PRIu64, this,
+ this->next_flush_time, this->process_time);
+ reset_buffer(this);
+ enable_flush_timer(this, true);
+ }
+ else {
+ /* Don't want to flush yet, or failed to write anything */
+ spa_log_trace(this->log, "%p: skip flush", this);
+ enable_flush_timer(this, false);
+ }
+ return 0;
+}
+
+static void media_on_flush_error(struct spa_source *source)
+{
+ struct impl *this = source->data;
+
+ spa_log_trace(this->log, "%p: flush event", this);
+
+ if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ spa_log_warn(this->log, "%p: error %d", this, source->rmask);
+ if (this->flush_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->flush_source);
+ return;
+ }
+}
+
+static void media_on_flush_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t exp;
+ int res;
+
+ spa_log_trace(this->log, "%p: flush on timeout", this);
+
+ if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+
+ if (this->transport == NULL) {
+ enable_flush_timer(this, false);
+ return;
+ }
+
+ while (exp-- > 0) {
+ this->flush_pending = false;
+ flush_data(this, this->current_time);
+ }
+}
+
+static void media_on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ uint64_t exp, duration;
+ uint32_t rate;
+ struct spa_io_buffers *io = port->io;
+ uint64_t prev_time, now_time;
+ int res;
+
+ if (this->transport == NULL)
+ return;
+
+ if (this->started) {
+ if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s",
+ spa_strerror(res));
+ return;
+ }
+ }
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_debug(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ this->next_time = now_time + duration * SPA_NSEC_PER_SEC / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ int64_t delay_nsec;
+
+ this->clock->nsec = now_time;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->rate_diff = 1.0f;
+ this->clock->next_nsec = this->next_time;
+
+ delay_nsec = spa_bt_transport_get_delay_nsec(this->transport);
+
+ /* Negative delay doesn't work properly, so disallow it */
+ delay_nsec += SPA_CLAMP(this->props.latency_offset, -delay_nsec, INT64_MAX / 2);
+
+ this->clock->delay = (delay_nsec * this->clock->rate.denom) / SPA_NSEC_PER_SEC;
+ }
+
+
+ spa_log_trace(this->log, "%p: %d", this, io->status);
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int do_start(struct impl *this)
+{
+ int res, val, size;
+ struct port *port;
+ socklen_t len;
+ uint8_t *conf;
+ uint32_t flags;
+
+ if (this->started)
+ return 0;
+
+ spa_return_val_if_fail(this->transport, -EIO);
+
+ this->following = is_following(this);
+
+ spa_log_debug(this->log, "%p: start following:%d", this, this->following);
+
+ if ((res = spa_bt_transport_acquire(this->transport, false)) < 0)
+ return res;
+
+ port = &this->port;
+
+ conf = this->transport->configuration;
+ size = this->transport->configuration_len;
+
+ spa_log_debug(this->log, "Transport configuration:");
+ spa_debug_log_mem(this->log, SPA_LOG_LEVEL_DEBUG, 2, conf, (size_t)size);
+
+ flags = this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0;
+
+ this->codec_data = this->codec->init(this->codec,
+ flags,
+ this->transport->configuration,
+ this->transport->configuration_len,
+ &port->current_format,
+ this->codec_props,
+ this->transport->write_mtu);
+ if (this->codec_data == NULL)
+ return -EIO;
+
+ spa_log_info(this->log, "%p: using %s codec %s, delay:%"PRIi64" ms", this,
+ this->codec->bap ? "BAP" : "A2DP", this->codec->description,
+ (int64_t)(spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC));
+
+ this->seqnum = 0;
+
+ this->block_size = this->codec->get_block_size(this->codec_data);
+ if (this->block_size > sizeof(this->tmp_buffer)) {
+ spa_log_error(this->log, "block-size %d > %zu",
+ this->block_size, sizeof(this->tmp_buffer));
+ return -EIO;
+ }
+
+ spa_log_debug(this->log, "%p: block_size %d", this, this->block_size);
+
+ val = this->codec->send_buf_size > 0
+ /* The kernel doubles the SO_SNDBUF option value set by setsockopt(). */
+ ? this->codec->send_buf_size / 2 + this->codec->send_buf_size % 2
+ : FILL_FRAMES * this->transport->write_mtu;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "%p: SO_SNDBUF %m", this);
+
+ len = sizeof(val);
+ if (getsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, &len) < 0) {
+ spa_log_warn(this->log, "%p: SO_SNDBUF %m", this);
+ }
+ else {
+ spa_log_debug(this->log, "%p: SO_SNDBUF: %d", this, val);
+ }
+ this->fd_buffer_size = val;
+
+ val = FILL_FRAMES * this->transport->read_mtu;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_RCVBUF, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "%p: SO_RCVBUF %m", this);
+
+ val = 6;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "SO_PRIORITY failed: %m");
+
+ reset_buffer(this);
+
+ this->source.data = this;
+ this->source.fd = this->timerfd;
+ this->source.func = media_on_timeout;
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->source);
+
+ this->flush_timer_source.data = this;
+ this->flush_timer_source.fd = this->flush_timerfd;
+ this->flush_timer_source.func = media_on_flush_timeout;
+ this->flush_timer_source.mask = SPA_IO_IN;
+ this->flush_timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->flush_timer_source);
+
+ this->flush_source.data = this;
+ this->flush_source.fd = this->transport->fd;
+ this->flush_source.func = media_on_flush_error;
+ this->flush_source.mask = SPA_IO_ERR | SPA_IO_HUP;
+ this->flush_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->flush_source);
+
+ this->flush_pending = false;
+
+ set_timers(this);
+ this->started = true;
+
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
+ if (this->flush_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->flush_source);
+
+ if (this->flush_timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->flush_timer_source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->flush_timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ int res = 0;
+
+ if (!this->started)
+ return 0;
+
+ spa_log_trace(this->log, "%p: stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ this->started = false;
+
+ if (this->transport)
+ res = spa_bt_transport_release(this->transport);
+
+ if (this->codec_data)
+ this->codec->deinit(this->codec_data);
+ this->codec_data = NULL;
+
+ return res;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, this->is_output ? "Audio/Sink" : "Stream/Input/Audio" },
+ { "media.name", ((this->transport && this->transport->device->name) ?
+ this->transport->device->name : this->codec->bap ? "BAP" : "A2DP" ) },
+ { SPA_KEY_NODE_DRIVER, this->is_output ? "true" : "false" },
+ };
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ 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 (this->codec == NULL)
+ return -EIO;
+ if (this->transport == NULL)
+ return -EIO;
+
+ if ((res = this->codec->enum_config(this->codec,
+ this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0,
+ this->transport->configuration,
+ this->transport->configuration_len,
+ id, result.index, &b, &param)) != 1)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(
+ MIN_BUFFERS,
+ MIN_BUFFERS,
+ MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->frame_size,
+ 16 * port->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size));
+ break;
+
+ 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:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ do_stop(this);
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ spa_log_debug(this->log, "clear format");
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+
+ port->frame_size = info.info.raw.channels;
+ switch (info.info.raw.format) {
+ case SPA_AUDIO_FORMAT_S16:
+ port->frame_size *= 2;
+ break;
+ case SPA_AUDIO_FORMAT_S24:
+ port->frame_size *= 3;
+ break;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_F32:
+ port->frame_size *= 4;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ 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);
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ } 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ spa_log_debug(this->log, "use buffers %d", n_buffers);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+
+ b->buf = buffers[i];
+ b->id = i;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (buffers[i]->datas[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
+ io->status = SPA_STATUS_NEED_DATA;
+ return SPA_STATUS_HAVE_DATA;
+ }
+
+ if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) {
+ struct buffer *b = &port->buffers[io->buffer_id];
+
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+
+ spa_log_trace(this->log, "%p: queue buffer %u", this, io->buffer_id);
+
+ spa_list_append(&port->ready, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+
+ io->buffer_id = SPA_ID_INVALID;
+ io->status = SPA_STATUS_OK;
+ }
+
+ if (this->following) {
+ if (this->position) {
+ this->current_time = this->position->clock.nsec;
+ } else {
+ struct timespec now;
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
+ }
+ }
+
+ this->process_time = this->current_time;
+
+ if (!spa_list_is_empty(&port->ready)) {
+ spa_log_trace(this->log, "%p: flush on process", this);
+ flush_data(this, this->current_time);
+ }
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static void transport_delay_changed(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p delay changed", this->transport);
+ set_latency(this, true);
+}
+
+static int do_transport_destroy(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ this->transport = NULL;
+ return 0;
+}
+
+static void transport_destroy(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p destroy", this->transport);
+ spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this);
+}
+
+static void transport_state_changed(void *data,
+ enum spa_bt_transport_state old,
+ enum spa_bt_transport_state state)
+{
+ struct impl *this = data;
+
+ spa_log_debug(this->log, "%p: transport %p state %d->%d", this, this->transport, old, state);
+
+ if (state < SPA_BT_TRANSPORT_STATE_ACTIVE && old == SPA_BT_TRANSPORT_STATE_ACTIVE &&
+ this->started) {
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+
+ spa_log_debug(this->log, "%p: transport %p becomes inactive: stop and indicate error",
+ this, this->transport);
+
+ /*
+ * If establishing connection fails due to remote end not activating
+ * the transport, we won't get a write error, but instead see a transport
+ * state change.
+ *
+ * Stop and emit a node error, to let upper levels handle it.
+ */
+
+ do_stop(this);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_node_emit_event(&this->hooks,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_EVENT_Node, SPA_NODE_EVENT_Error));
+ }
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .delay_changed = transport_delay_changed,
+ .state_changed = transport_state_changed,
+ .destroy = transport_destroy,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ do_stop(this);
+ if (this->codec_props && this->codec->clear_props)
+ this->codec->clear_props(this->codec_props);
+ if (this->transport)
+ spa_hook_remove(&this->transport_listener);
+ spa_system_close(this->data_system, this->timerfd);
+ spa_system_close(this->data_system, this->flush_timerfd);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+
+ 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);
+ 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);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS |
+ SPA_NODE_CHANGE_MASK_PROPS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 0;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ 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;
+
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = 0;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ spa_list_init(&port->ready);
+
+ this->quantum_limit = 8192;
+
+ if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+ spa_atou32(str, &this->quantum_limit, 0);
+
+ if (info && (str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL)
+ this->is_duplex = spa_atob(str);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)))
+ sscanf(str, "pointer:%p", &this->transport);
+
+ if (this->transport == NULL) {
+ spa_log_error(this->log, "a transport is needed");
+ return -EINVAL;
+ }
+ if (this->transport->media_codec == NULL) {
+ spa_log_error(this->log, "a transport codec is needed");
+ return -EINVAL;
+ }
+
+ this->codec = this->transport->media_codec;
+
+ if (this->is_duplex) {
+ if (!this->codec->duplex_codec) {
+ spa_log_error(this->log, "transport codec doesn't support duplex");
+ return -EINVAL;
+ }
+ this->codec = this->codec->duplex_codec;
+ }
+
+ if (this->codec->init_props != NULL)
+ this->codec_props = this->codec->init_props(this->codec,
+ this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0,
+ this->transport->device->settings);
+
+ if (this->codec->bap)
+ this->is_output = this->transport->bap_initiator;
+ else
+ this->is_output = true;
+
+ reset_props(this, &this->props);
+
+ set_latency(this, false);
+
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &transport_events, this);
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ this->flush_timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ 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, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the media" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_media_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_MEDIA_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+
+/* Retained for backward compatibility: */
+const struct spa_handle_factory spa_a2dp_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_A2DP_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/media-source.c b/spa/plugins/bluez5/media-source.c
new file mode 100644
index 0000000..360f812
--- /dev/null
+++ b/spa/plugins/bluez5/media-source.c
@@ -0,0 +1,1707 @@
+/* Spa Media Source
+ *
+ * Copyright © 2018 Wim Taymans
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include "defs.h"
+#include "rtp.h"
+#include "media-codecs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.media");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#include "decode-buffer.h"
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ char clock_name[64];
+};
+
+#define FILL_FRAMES 2
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ unsigned int outstanding:1;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ struct spa_audio_info current_format;
+ uint32_t frame_size;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_io_rate_match *rate_match;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ struct spa_bt_decode_buffer buffer;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint32_t quantum_limit;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define IDX_NODE_IO 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+
+ struct port port;
+
+ unsigned int started:1;
+ unsigned int transport_acquired:1;
+ unsigned int following:1;
+ unsigned int matching:1;
+ unsigned int resampling:1;
+
+ unsigned int is_input:1;
+ unsigned int is_duplex:1;
+ unsigned int use_duplex_source:1;
+
+ int fd;
+ struct spa_source source;
+
+ struct spa_source timer_source;
+ int timerfd;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t current_time;
+ uint64_t next_time;
+
+ const struct media_codec *codec;
+ bool codec_props_changed;
+ void *codec_props;
+ void *codec_data;
+ struct spa_audio_info codec_format;
+
+ uint8_t buffer_read[4096];
+ struct timespec now;
+ uint64_t sample_count;
+
+ int duplex_timerfd;
+ uint64_t duplex_timeout;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+}
+
+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, index_offset = 0;
+ bool enum_codec = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ default:
+ enum_codec = true;
+ index_offset = 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ default:
+ enum_codec = true;
+ index_offset = 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (enum_codec) {
+ int res;
+ if (this->codec->enum_props == NULL || this->codec_props == NULL ||
+ this->transport == NULL)
+ return 0;
+ else if ((res = this->codec->enum_props(this->codec_props,
+ this->transport->device->settings,
+ id, result.index - index_offset,
+ &b, &param)) != 1)
+ return res;
+ }
+
+ 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 set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct port *port = &this->port;
+
+ set_timers(this);
+ spa_bt_decode_buffer_recover(&port->buffer);
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(&new_props);
+ } else {
+ /* noop */
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ int res, codec_res = 0;
+ res = apply_props(this, param);
+ if (this->codec_props && this->codec->set_props) {
+ codec_res = this->codec->set_props(this->codec_props, param);
+ if (codec_res > 0)
+ this->codec_props_changed = true;
+ }
+ if (res > 0 || codec_res > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static void reset_buffers(struct port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+}
+
+static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (b->outstanding) {
+ spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+}
+
+static int32_t read_data(struct impl *this) {
+ const ssize_t b_size = sizeof(this->buffer_read);
+ int32_t size_read = 0;
+
+again:
+ /* read data from socket */
+ size_read = recv(this->fd, this->buffer_read, b_size, MSG_DONTWAIT);
+
+ if (size_read == 0)
+ return 0;
+ else if (size_read < 0) {
+ /* retry if interrupted */
+ if (errno == EINTR)
+ goto again;
+
+ /* return socket has no data */
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return 0;
+
+ /* go to 'stop' if socket has an error */
+ spa_log_error(this->log, "read error: %s", strerror(errno));
+ return -errno;
+ }
+
+ return size_read;
+}
+
+static int32_t decode_data(struct impl *this, uint8_t *src, uint32_t src_size,
+ uint8_t *dst, uint32_t dst_size)
+{
+ ssize_t processed;
+ size_t written, avail;
+
+ if ((processed = this->codec->start_decode(this->codec_data,
+ src, src_size, NULL, NULL)) < 0)
+ return processed;
+
+ src += processed;
+ src_size -= processed;
+
+ /* decode */
+ avail = dst_size;
+ while (src_size > 0) {
+ if ((processed = this->codec->decode(this->codec_data,
+ src, src_size, dst, avail, &written)) <= 0)
+ return processed;
+
+ /* update source and dest pointers */
+ spa_return_val_if_fail (avail > written, -ENOSPC);
+ src_size -= processed;
+ src += processed;
+ avail -= written;
+ dst += written;
+ }
+ return dst_size - avail;
+}
+
+static void media_on_ready_read(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ struct timespec now;
+ void *buf;
+ int32_t size_read, decoded;
+ uint32_t avail;
+ uint64_t dt;
+
+ /* make sure the source is an input */
+ if ((source->rmask & SPA_IO_IN) == 0) {
+ spa_log_error(this->log, "source is not an input, rmask=%d", source->rmask);
+ goto stop;
+ }
+ if (this->transport == NULL) {
+ spa_log_debug(this->log, "no transport, stop reading");
+ goto stop;
+ }
+
+ spa_log_trace(this->log, "socket poll");
+
+ /* read */
+ size_read = read_data (this);
+ if (size_read == 0)
+ return;
+ if (size_read < 0) {
+ spa_log_error(this->log, "failed to read data: %s", spa_strerror(size_read));
+ 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);
+ this->codec_props_changed = false;
+ }
+
+ /* decode to buffer */
+ buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+ spa_log_trace(this->log, "read socket data size:%d, avail:%d", size_read, avail);
+ decoded = decode_data(this, this->buffer_read, size_read, buf, avail);
+ if (decoded < 0) {
+ spa_log_debug(this->log, "failed to decode data: %d", decoded);
+ return;
+ }
+ if (decoded == 0) {
+ spa_log_trace(this->log, "no decoded socket data");
+ return;
+ }
+
+ /* discard when not started */
+ if (!this->started)
+ return;
+
+ spa_bt_decode_buffer_write_packet(&port->buffer, decoded);
+
+ dt = SPA_TIMESPEC_TO_NSEC(&this->now);
+ this->now = now;
+ dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt;
+
+ spa_log_trace(this->log, "decoded socket data size:%d frames:%d dt:%d dms",
+ (int)decoded, (int)decoded/port->frame_size,
+ (int)(dt / 100000));
+
+ return;
+
+stop:
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+}
+
+static int set_duplex_timeout(struct impl *this, uint64_t timeout)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->duplex_timerfd, 0, &ts, NULL);
+}
+
+static void media_on_duplex_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t exp;
+ int res;
+
+ if ((res = spa_system_timerfd_read(this->data_system, this->duplex_timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+
+ set_duplex_timeout(this, this->duplex_timeout);
+
+ media_on_ready_read(source);
+}
+
+static int setup_matching(struct impl *this)
+{
+ struct port *port = &this->port;
+
+ if (this->position && port->rate_match) {
+ port->rate_match->rate = 1 / port->buffer.corr;
+
+ this->matching = this->following;
+ this->resampling = this->matching ||
+ (port->current_format.info.raw.rate != this->position->clock.rate.denom);
+ } else {
+ this->matching = false;
+ this->resampling = false;
+ }
+
+ if (port->rate_match)
+ SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching);
+
+ return 0;
+}
+
+static int produce_buffer(struct impl *this);
+
+static void media_on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ uint64_t exp, duration;
+ uint32_t rate;
+ uint64_t prev_time, now_time;
+ int res;
+
+ if (this->transport == NULL)
+ return;
+
+ if (this->started) {
+ if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+ }
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ setup_matching(this);
+
+ this->next_time = now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = now_time;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->rate_diff = port->buffer.corr;
+ this->clock->next_nsec = this->next_time;
+ }
+
+ if (port->io) {
+ int status = produce_buffer(this);
+ spa_log_trace(this->log, "%p: io:%d status:%d", this, port->io->status, status);
+ }
+
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int transport_start(struct impl *this)
+{
+ int res, val;
+ struct port *port = &this->port;
+ uint32_t flags;
+
+ if (this->transport_acquired)
+ return 0;
+
+ spa_log_debug(this->log, "%p: transport %p acquire", this,
+ this->transport);
+ if ((res = spa_bt_transport_acquire(this->transport, false)) < 0)
+ return res;
+
+ this->transport_acquired = true;
+
+ flags = this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK;
+
+ this->codec_data = this->codec->init(this->codec,
+ flags,
+ this->transport->configuration,
+ this->transport->configuration_len,
+ &port->current_format,
+ this->codec_props,
+ this->transport->read_mtu);
+ if (this->codec_data == NULL)
+ return -EIO;
+
+ spa_log_info(this->log, "%p: using %s codec %s", this,
+ this->codec->bap ? "BAP" : "A2DP", this->codec->description);
+
+ val = fcntl(this->transport->fd, F_GETFL);
+ if (fcntl(this->transport->fd, F_SETFL, val | O_NONBLOCK) < 0)
+ spa_log_warn(this->log, "%p: fcntl %u %m", this, val | O_NONBLOCK);
+
+ val = FILL_FRAMES * this->transport->write_mtu;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "%p: SO_SNDBUF %m", this);
+
+ val = FILL_FRAMES * this->transport->read_mtu;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_RCVBUF, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "%p: SO_RCVBUF %m", this);
+
+ val = 6;
+ if (setsockopt(this->transport->fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+ spa_log_warn(this->log, "SO_PRIORITY failed: %m");
+
+ reset_buffers(port);
+
+ spa_bt_decode_buffer_clear(&port->buffer);
+ if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log,
+ port->frame_size, port->current_format.info.raw.rate,
+ this->quantum_limit, this->quantum_limit)) < 0)
+ return res;
+
+ this->fd = this->transport->fd;
+
+ this->source.data = this;
+
+ if (!this->use_duplex_source) {
+ this->source.fd = this->transport->fd;
+ this->source.func = media_on_ready_read;
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->source);
+ } else {
+ /*
+ * XXX: For an unknown reason (on Linux 5.13.10), the socket when working with
+ * XXX: "duplex" stream sometimes stops waking up from the poll, even though
+ * XXX: you can recv() from the socket with no problem.
+ * XXX:
+ * XXX: The reason for this should be found and fixed.
+ * XXX: To work around this, for now we just do the stupid thing and poll
+ * XXX: on a timer, chosen so that it's fast enough for the aptX-LL codec
+ * XXX: we currently support (which sends mSBC data), and also for Opus
+ * XXX: forward stream.
+ */
+ this->source.fd = this->duplex_timerfd;
+ this->source.func = media_on_duplex_timeout;
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->source);
+
+ this->duplex_timeout = SPA_NSEC_PER_MSEC * 25/10;
+ set_duplex_timeout(this, this->duplex_timeout);
+ }
+
+ this->timer_source.data = this;
+ this->timer_source.fd = this->timerfd;
+ this->timer_source.func = media_on_timeout;
+ this->timer_source.mask = SPA_IO_IN;
+ this->timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->timer_source);
+
+ this->sample_count = 0;
+
+ setup_matching(this);
+
+ set_timers(this);
+
+ return 0;
+}
+
+static int do_start(struct impl *this)
+{
+ int res = 0;
+
+ if (this->started)
+ return 0;
+
+ spa_return_val_if_fail(this->transport != NULL, -EIO);
+
+ this->following = is_following(this);
+
+ spa_log_debug(this->log, "%p: start state:%d following:%d",
+ this, this->transport->state, this->following);
+
+ if (this->transport->state >= SPA_BT_TRANSPORT_STATE_PENDING ||
+ this->is_duplex || this->codec->bap)
+ res = transport_start(this);
+
+ this->started = true;
+
+ return res;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ spa_log_debug(this->log, "%p: remove source", this);
+
+ set_duplex_timeout(this, 0);
+
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+
+ if (this->timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->timer_source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+static int transport_stop(struct impl *this)
+{
+ struct port *port = &this->port;
+ int res;
+
+ spa_log_debug(this->log, "%p: transport stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ if (this->transport && this->transport_acquired)
+ res = spa_bt_transport_release(this->transport);
+ else
+ res = 0;
+
+ this->transport_acquired = false;
+
+ if (this->codec_data)
+ this->codec->deinit(this->codec_data);
+ this->codec_data = NULL;
+
+ spa_bt_decode_buffer_clear(&port->buffer);
+
+ return res;
+}
+
+static int do_stop(struct impl *this)
+{
+ int res;
+
+ if (!this->started)
+ return 0;
+
+ spa_log_debug(this->log, "%p: stop", this);
+
+ res = transport_stop(this);
+
+ this->started = false;
+
+ return res;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, this->is_input ? "Audio/Source" : "Stream/Output/Audio" },
+ { SPA_KEY_NODE_LATENCY, this->is_input ? "" : "512/48000" },
+ { "media.name", ((this->transport && this->transport->device->name) ?
+ this->transport->device->name : this->codec->bap ? "BAP" : "A2DP") },
+ { SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" },
+ };
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ 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 (result.index > 0)
+ return 0;
+ if (this->codec == NULL)
+ return -EIO;
+ if (this->transport == NULL)
+ return -EIO;
+
+ if ((res = this->codec->enum_config(this->codec,
+ this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK,
+ this->transport->configuration,
+ this->transport->configuration_len,
+ id, result.index, &b, &param)) != 1)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->frame_size,
+ 16 * port->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ do_stop(this);
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ spa_log_debug(this->log, "clear format");
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+
+ port->frame_size = info.info.raw.channels;
+
+ switch (info.info.raw.format) {
+ case SPA_AUDIO_FORMAT_S16:
+ port->frame_size *= 2;
+ break;
+ case SPA_AUDIO_FORMAT_S24:
+ port->frame_size *= 3;
+ break;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_F32:
+ port->frame_size *= 4;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ 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);
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ } 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ spa_log_debug(this->log, "use buffers %d", n_buffers);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ port->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+ port = &this->port;
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ recycle_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static uint32_t get_samples(struct impl *this, uint32_t *duration)
+{
+ struct port *port = &this->port;
+ uint32_t samples;
+
+ if (SPA_LIKELY(port->rate_match) && this->resampling) {
+ samples = port->rate_match->size;
+ } else {
+ if (SPA_LIKELY(this->position))
+ samples = this->position->clock.duration * port->current_format.info.raw.rate
+ / this->position->clock.rate.denom;
+ else
+ samples = 1024;
+ }
+
+ if (SPA_LIKELY(this->position))
+ *duration = this->position->clock.duration * port->current_format.info.raw.rate
+ / this->position->clock.rate.denom;
+ else if (SPA_LIKELY(this->clock))
+ *duration = this->clock->duration * port->current_format.info.raw.rate
+ / this->clock->rate.denom;
+ else
+ *duration = 1024 * port->current_format.info.raw.rate / 48000;
+
+ return samples;
+}
+
+static void process_buffering(struct impl *this)
+{
+ struct port *port = &this->port;
+ uint32_t duration;
+ const uint32_t samples = get_samples(this, &duration);
+ uint32_t avail;
+ void *buf;
+
+ spa_bt_decode_buffer_process(&port->buffer, samples, duration);
+
+ setup_matching(this);
+
+ buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail);
+
+ /* copy data to buffers */
+ if (!spa_list_is_empty(&port->free) && avail > 0) {
+ struct buffer *buffer;
+ struct spa_data *datas;
+ uint32_t data_size;
+
+ data_size = samples * port->frame_size;
+
+ avail = SPA_MIN(avail, data_size);
+
+ spa_bt_decode_buffer_read(&port->buffer, avail);
+
+ buffer = spa_list_first(&port->free, struct buffer, link);
+ spa_list_remove(&buffer->link);
+
+ spa_log_trace(this->log, "dequeue %d", buffer->id);
+
+ if (buffer->h) {
+ buffer->h->seq = this->sample_count;
+ buffer->h->pts = SPA_TIMESPEC_TO_NSEC(&this->now);
+ buffer->h->dts_offset = 0;
+ }
+
+ datas = buffer->buf->datas;
+
+ spa_assert(datas[0].maxsize >= data_size);
+
+ datas[0].chunk->offset = 0;
+ datas[0].chunk->size = avail;
+ datas[0].chunk->stride = port->frame_size;
+
+ memcpy(datas[0].data, buf, avail);
+
+ this->sample_count += avail / port->frame_size;
+
+ /* ready buffer if full */
+ spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)avail / port->frame_size);
+ spa_list_append(&port->ready, &buffer->link);
+ }
+}
+
+static int produce_buffer(struct impl *this)
+{
+ struct buffer *buffer;
+ struct port *port = &this->port;
+ struct spa_io_buffers *io = port->io;
+
+ if (io == NULL)
+ return -EIO;
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Handle buffering */
+ process_buffering(this);
+
+ /* Return if there are no buffers ready to be processed */
+ if (spa_list_is_empty(&port->ready))
+ return SPA_STATUS_OK;
+
+ /* Get the new buffer from the ready list */
+ buffer = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&buffer->link);
+ buffer->outstanding = true;
+
+ /* Set the new buffer in IO */
+ io->buffer_id = buffer->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ /* Notify we have a buffer ready to be processed */
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ spa_log_trace(this->log, "%p status:%d", this, io->status);
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Follower produces buffers here, driver in timeout */
+ if (this->following)
+ return produce_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int do_transport_destroy(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ this->transport = NULL;
+ this->transport_acquired = false;
+ return 0;
+}
+
+static void transport_destroy(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p destroy", this->transport);
+ spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this);
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = transport_destroy,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ struct port *port = &this->port;
+
+ do_stop(this);
+ if (this->codec_props && this->codec->clear_props)
+ this->codec->clear_props(this->codec_props);
+ if (this->transport)
+ spa_hook_remove(&this->transport_listener);
+ spa_system_close(this->data_system, this->timerfd);
+ if (this->duplex_timerfd >= 0) {
+ spa_system_close(this->data_system, this->duplex_timerfd);
+ this->duplex_timerfd = -1;
+ }
+ spa_bt_decode_buffer_clear(&port->buffer);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+
+ 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);
+ 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);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ /* set the node info */
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 0;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ 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->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ /* set the port info */
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ /* Init the buffer lists */
+ spa_list_init(&port->ready);
+ spa_list_init(&port->free);
+
+ this->quantum_limit = 8192;
+
+ if (info != NULL) {
+ if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+ spa_atou32(str, &this->quantum_limit, 0);
+ if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)) != NULL)
+ sscanf(str, "pointer:%p", &this->transport);
+ if ((str = spa_dict_lookup(info, "bluez5.media-source-role")) != NULL)
+ this->is_input = spa_streq(str, "input");
+ if ((str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL)
+ this->is_duplex = spa_atob(str);
+ }
+
+ if (this->transport == NULL) {
+ spa_log_error(this->log, "a transport is needed");
+ return -EINVAL;
+ }
+ if (this->transport->media_codec == NULL) {
+ spa_log_error(this->log, "a transport codec is needed");
+ return -EINVAL;
+ }
+ this->codec = this->transport->media_codec;
+
+ if (this->is_duplex) {
+ if (!this->codec->duplex_codec) {
+ spa_log_error(this->log, "transport codec doesn't support duplex");
+ return -EINVAL;
+ }
+ this->codec = this->codec->duplex_codec;
+ this->is_input = true;
+ }
+ this->use_duplex_source = this->is_duplex || (this->codec->duplex_codec != NULL);
+
+ if (this->codec->bap)
+ this->is_input = this->transport->bap_initiator;
+
+ if (this->codec->init_props != NULL)
+ this->codec_props = this->codec->init_props(this->codec,
+ this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK,
+ this->transport->device->settings);
+
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &transport_events, this);
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ if (this->use_duplex_source) {
+ this->duplex_timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ } else {
+ this->duplex_timerfd = -1;
+ }
+
+ 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, "Collabora Ltd. <contact@collabora.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with media" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_media_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_MEDIA_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+
+/* Retained for backward compatibility */
+const struct spa_handle_factory spa_a2dp_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_A2DP_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build
new file mode 100644
index 0000000..f189570
--- /dev/null
+++ b/spa/plugins/bluez5/meson.build
@@ -0,0 +1,206 @@
+gnome = import('gnome')
+
+bluez5_deps = [ mathlib, dbus_dep, glib2_dep, sbc_dep, bluez_dep, gio_dep, gio_unix_dep ]
+foreach dep: bluez5_deps
+ if not dep.found()
+ subdir_done()
+ endif
+endforeach
+
+cdata.set('HAVE_BLUEZ_5_BACKEND_NATIVE',
+ get_option('bluez5-backend-hsp-native').allowed() or
+ get_option('bluez5-backend-hfp-native').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_HSP_NATIVE', get_option('bluez5-backend-hsp-native').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_HFP_NATIVE', get_option('bluez5-backend-hfp-native').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_NATIVE_MM', get_option('bluez5-backend-native-mm').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_OFONO', get_option('bluez5-backend-ofono').allowed())
+cdata.set('HAVE_BLUEZ_5_BACKEND_HSPHFPD', get_option('bluez5-backend-hsphfpd').allowed())
+cdata.set('HAVE_BLUEZ_5_HCI', dependency('bluez', version: '< 6', required: false).found())
+
+bluez5_sources = [
+ 'plugin.c',
+ 'codec-loader.c',
+ 'media-codecs.c',
+ 'media-sink.c',
+ 'media-source.c',
+ 'sco-sink.c',
+ 'sco-source.c',
+ 'sco-io.c',
+ 'quirks.c',
+ 'player.c',
+ 'bluez5-device.c',
+ 'bluez5-dbus.c',
+ 'hci.c',
+ 'dbus-monitor.c',
+ 'midi-enum.c',
+ 'midi-parser.c',
+ 'midi-node.c',
+ 'midi-server.c',
+]
+
+bluez5_interface_src = gnome.gdbus_codegen('bluez5-interface-gen',
+ sources: 'org.bluez.xml',
+ interface_prefix : 'org.bluez.',
+ object_manager: true,
+ namespace : 'Bluez5',
+ annotations : [
+ ['org.bluez.GattCharacteristic1.AcquireNotify()', 'org.gtk.GDBus.C.UnixFD', 'true'],
+ ['org.bluez.GattCharacteristic1.AcquireWrite()', 'org.gtk.GDBus.C.UnixFD', 'true'],
+ ]
+)
+bluez5_sources += [ bluez5_interface_src ]
+
+bluez5_data = ['bluez-hardware.conf']
+
+install_data(bluez5_data, install_dir : spa_datadir / 'bluez5')
+
+if get_option('bluez5-backend-hsp-native').allowed() or get_option('bluez5-backend-hfp-native').allowed()
+ if libusb_dep.found()
+ bluez5_deps += libusb_dep
+ endif
+ if mm_dep.found()
+ bluez5_deps += mm_dep
+ bluez5_sources += ['modemmanager.c']
+ endif
+ bluez5_sources += ['backend-native.c', 'upower.c']
+endif
+
+if get_option('bluez5-backend-ofono').allowed()
+ bluez5_sources += ['backend-ofono.c']
+endif
+
+if get_option('bluez5-backend-hsphfpd').allowed()
+ bluez5_sources += ['backend-hsphfpd.c']
+endif
+
+# The library uses GObject, and cannot be unloaded
+bluez5_link_args = [ '-Wl,-z', '-Wl,nodelete' ]
+
+bluez5lib = shared_library('spa-bluez5',
+ bluez5_sources,
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, bluez5_deps ],
+ link_args : bluez5_link_args,
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+
+codec_args = [ '-DCODEC_PLUGIN' ]
+
+bluez_codec_sbc = shared_library('spa-codec-bluez5-sbc',
+ [ 'a2dp-codec-sbc.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, sbc_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+
+bluez_codec_faststream = shared_library('spa-codec-bluez5-faststream',
+ [ 'a2dp-codec-faststream.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, sbc_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+
+if fdk_aac_dep.found()
+ bluez_codec_aac = shared_library('spa-codec-bluez5-aac',
+ [ 'a2dp-codec-aac.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, fdk_aac_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if aptx_dep.found()
+ bluez_codec_aptx = shared_library('spa-codec-bluez5-aptx',
+ [ 'a2dp-codec-aptx.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, aptx_dep, sbc_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if ldac_dep.found()
+ ldac_args = codec_args
+ ldac_dep = [ ldac_dep ]
+ if ldac_abr_dep.found()
+ ldac_args += [ '-DENABLE_LDAC_ABR' ]
+ ldac_dep += ldac_abr_dep
+ endif
+ bluez_codec_ldac = shared_library('spa-codec-bluez5-ldac',
+ [ 'a2dp-codec-ldac.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : ldac_args,
+ dependencies : [ spa_dep, ldac_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if get_option('bluez5-codec-lc3plus').allowed() and lc3plus_dep.found()
+ bluez_codec_lc3plus = shared_library('spa-codec-bluez5-lc3plus',
+ [ 'a2dp-codec-lc3plus.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, lc3plus_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if get_option('bluez5-codec-opus').allowed() and opus_dep.found()
+ opus_args = codec_args
+ opus_dep = [ opus_dep ]
+ bluez_codec_opus = shared_library('spa-codec-bluez5-opus',
+ [ 'a2dp-codec-opus.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : opus_args,
+ dependencies : [ spa_dep, opus_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+if get_option('bluez5-codec-lc3').allowed() and lc3_dep.found()
+ bluez_codec_lc3 = shared_library('spa-codec-bluez5-lc3',
+ [ 'bap-codec-lc3.c', 'media-codecs.c' ],
+ include_directories : [ configinc ],
+ c_args : codec_args,
+ dependencies : [ spa_dep, lc3_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'bluez5')
+endif
+
+test_apps = [
+ 'test-midi',
+]
+bluez5_test_lib = static_library('bluez5_test_lib',
+ [ 'midi-parser.c' ],
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, bluez5_deps ],
+ install : false
+)
+
+foreach a : test_apps
+ test(a,
+ executable(a, a + '.c',
+ dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, bluez5_deps ],
+ include_directories : [ configinc ],
+ link_with : [ bluez5_test_lib ],
+ install_rpath : spa_plugindir / 'bluez5',
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'bluez5'),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ ])
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'bluez5' / a)
+ configure_file(
+ input: installed_tests_template,
+ output: a + '.test',
+ install_dir: installed_tests_metadir / 'bluez5',
+ configuration: test_conf
+ )
+ endif
+endforeach
diff --git a/spa/plugins/bluez5/midi-enum.c b/spa/plugins/bluez5/midi-enum.c
new file mode 100644
index 0000000..a966591
--- /dev/null
+++ b/spa/plugins/bluez5/midi-enum.c
@@ -0,0 +1,887 @@
+/* Spa midi dbus
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/node/node.h>
+#include <spa/node/keys.h>
+
+#include "midi.h"
+#include "config.h"
+
+#include "bluez5-interface-gen.h"
+#include "dbus-monitor.h"
+
+#define MIDI_OBJECT_PATH "/midi"
+#define MIDI_PROFILE_PATH MIDI_OBJECT_PATH "/profile"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl
+{
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+
+ GDBusConnection *conn;
+ struct dbus_monitor monitor;
+ GDBusObjectManagerServer *manager;
+
+ struct spa_hook_list hooks;
+
+ uint32_t id;
+};
+
+struct _MidiEnumCharacteristicProxy
+{
+ Bluez5GattCharacteristic1Proxy parent_instance;
+
+ struct impl *impl;
+
+ gchar *description;
+ uint32_t id;
+ GCancellable *read_call;
+ GCancellable *dsc_call;
+ unsigned int node_emitted:1;
+ unsigned int read_probed:1;
+ unsigned int read_done:1;
+ unsigned int dsc_probed:1;
+ unsigned int dsc_done:1;
+};
+
+G_DECLARE_FINAL_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, MIDI_ENUM,
+ CHARACTERISTIC_PROXY, Bluez5GattCharacteristic1Proxy)
+G_DEFINE_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, BLUEZ5_TYPE_GATT_CHARACTERISTIC1_PROXY)
+#define MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY (midi_enum_characteristic_proxy_get_type())
+
+struct _MidiEnumManagerProxy
+{
+ Bluez5GattManager1Proxy parent_instance;
+
+ GCancellable *register_call;
+ unsigned int registered:1;
+};
+
+G_DECLARE_FINAL_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, MIDI_ENUM,
+ MANAGER_PROXY, Bluez5GattManager1Proxy)
+G_DEFINE_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, BLUEZ5_TYPE_GATT_MANAGER1_PROXY)
+#define MIDI_ENUM_TYPE_MANAGER_PROXY (midi_enum_manager_proxy_get_type())
+
+
+static void emit_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr, Bluez5Device1 *device)
+{
+ struct spa_device_object_info info;
+ char nick[512], class[16];
+ struct spa_dict_item items[23];
+ uint32_t n_items = 0;
+ const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr));
+ const char *alias = bluez5_device1_get_alias(device);
+
+ spa_log_debug(impl->log, "emit node for path=%s", path);
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+ info.factory_name = SPA_NAME_API_BLUEZ5_MIDI_NODE;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.flags = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Midi/Bridge");
+ items[n_items++] = SPA_DICT_ITEM_INIT("node.description",
+ alias ? alias : bluez5_device1_get_name(device));
+ if (chr->description && chr->description[0] != '\0') {
+ spa_scnprintf(nick, sizeof(nick), "%s (%s)", alias, chr->description);
+ items[n_items++] = SPA_DICT_ITEM_INIT("node.nick", nick);
+ }
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, bluez5_device1_get_icon(device));
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, bluez5_device1_get_address(device));
+ snprintf(class, sizeof(class), "0x%06x", bluez5_device1_get_class(device));
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ROLE, "client");
+
+ info.props = &SPA_DICT_INIT(items, n_items);
+ spa_device_emit_object_info(&impl->hooks, chr->id, &info);
+}
+
+static void remove_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ spa_log_debug(impl->log, "remove node for path=%s", g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
+
+ spa_device_emit_object_info(&impl->hooks, chr->id, NULL);
+}
+
+static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr);
+
+static void read_probe_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(source_object);
+ struct impl *impl = user_data;
+ gchar *value = NULL;
+ GError *err = NULL;
+
+ bluez5_gatt_characteristic1_call_read_value_finish(
+ BLUEZ5_GATT_CHARACTERISTIC1(source_object), &value, res, &err);
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now */
+ g_error_free(err);
+ goto done;
+ }
+ if (err) {
+ spa_log_error(impl->log, "%s.ReadValue() failed: %s",
+ BLUEZ_GATT_CHR_INTERFACE,
+ err->message);
+ g_error_free(err);
+ goto done;
+ }
+
+ g_free(value);
+
+ spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
+
+ chr->read_done = true;
+
+ check_chr_node(impl, chr);
+
+done:
+ g_clear_object(&chr->read_call);
+}
+
+static int read_probe(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ GVariantBuilder builder;
+ GVariant *options;
+
+ /*
+ * BLE MIDI-1.0 §5: The Central shall read the MIDI I/O characteristic
+ * of the Peripheral after establishing a connection with the accessory.
+ */
+
+ if (chr->read_probed)
+ return 0;
+ if (chr->read_call)
+ return -EBUSY;
+
+ chr->read_probed = true;
+
+ spa_log_debug(impl->log, "MIDI GATT read probe for path=%s",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
+
+ chr->read_call = g_cancellable_new();
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+
+ bluez5_gatt_characteristic1_call_read_value(BLUEZ5_GATT_CHARACTERISTIC1(chr),
+ options,
+ chr->read_call,
+ read_probe_reply,
+ impl);
+
+ return 0;
+}
+
+Bluez5GattDescriptor1 *find_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr));
+ Bluez5GattDescriptor1 *found = NULL;;
+ GList *objects;
+
+ objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor));
+
+ for (GList *llo = g_list_first(objects); llo; llo = llo->next) {
+ GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data));
+
+ for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) {
+ Bluez5GattDescriptor1 *dsc;
+
+ if (!BLUEZ5_IS_GATT_DESCRIPTOR1(lli->data))
+ continue;
+
+ dsc = BLUEZ5_GATT_DESCRIPTOR1(lli->data);
+
+ if (!spa_streq(bluez5_gatt_descriptor1_get_uuid(dsc),
+ BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID))
+ continue;
+
+ if (spa_streq(bluez5_gatt_descriptor1_get_characteristic(dsc), path)) {
+ found = dsc;
+ break;
+ }
+ }
+ g_list_free_full(interfaces, g_object_unref);
+
+ if (found)
+ break;
+ }
+ g_list_free_full(objects, g_object_unref);
+
+ return found;
+}
+
+static void read_dsc_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(user_data);
+ struct impl *impl = chr->impl;
+ gchar *value = NULL;
+ GError *err = NULL;
+
+ chr->dsc_done = true;
+
+ bluez5_gatt_descriptor1_call_read_value_finish(
+ BLUEZ5_GATT_DESCRIPTOR1(source_object), &value, res, &err);
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now */
+ g_error_free(err);
+ goto done;
+ }
+ if (err) {
+ spa_log_error(impl->log, "%s.ReadValue() failed: %s",
+ BLUEZ_GATT_DSC_INTERFACE,
+ err->message);
+ g_error_free(err);
+ goto done;
+ }
+
+ spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)));
+
+ g_free(chr->description);
+ chr->description = value;
+
+ spa_log_debug(impl->log, "MIDI GATT user descriptor value: '%s'",
+ chr->description);
+
+ check_chr_node(impl, chr);
+
+done:
+ g_clear_object(&chr->dsc_call);
+}
+
+static int read_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ Bluez5GattDescriptor1 *dsc;
+ GVariant *options;
+ GVariantBuilder builder;
+
+ if (chr->dsc_probed)
+ return 0;
+ if (chr->dsc_call)
+ return -EBUSY;
+
+ chr->dsc_probed = true;
+
+ dsc = find_dsc(impl, chr);
+ if (dsc == NULL) {
+ chr->dsc_done = true;
+ return -ENOENT;
+ }
+
+ spa_log_debug(impl->log, "MIDI GATT user descriptor read, path=%s",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(dsc)));
+
+ chr->dsc_call = g_cancellable_new();
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+
+ bluez5_gatt_descriptor1_call_read_value(BLUEZ5_GATT_DESCRIPTOR1(dsc),
+ options,
+ chr->dsc_call,
+ read_dsc_reply,
+ chr);
+
+ return 0;
+}
+
+static int read_probe_reset(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ g_cancellable_cancel(chr->read_call);
+ g_clear_object(&chr->read_call);
+
+ g_cancellable_cancel(chr->dsc_call);
+ g_clear_object(&chr->dsc_call);
+
+ chr->read_probed = false;
+ chr->read_done = false;
+ chr->dsc_probed = false;
+ chr->dsc_done = false;
+ return 0;
+}
+
+static void lookup_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr,
+ Bluez5GattService1 **service, Bluez5Device1 **device)
+{
+ GDBusObject *object;
+ const char *service_path;
+ const char *device_path;
+
+ *service = NULL;
+ *device = NULL;
+
+ service_path = bluez5_gatt_characteristic1_get_service(BLUEZ5_GATT_CHARACTERISTIC1(chr));
+ if (!service_path)
+ return;
+
+ object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), service_path);
+ if (object) {
+ GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_GATT_SERVICE_INTERFACE);
+ *service = BLUEZ5_GATT_SERVICE1(iface);
+ }
+
+ if (!*service)
+ return;
+
+ device_path = bluez5_gatt_service1_get_device(*service);
+ if (!device_path)
+ return;
+
+ object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), device_path);
+ if (object) {
+ GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_DEVICE_INTERFACE);
+ *device = BLUEZ5_DEVICE1(iface);
+ }
+}
+
+static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr)
+{
+ Bluez5GattService1 *service;
+ Bluez5Device1 *device;
+ bool available;
+
+ lookup_chr_node(impl, chr, &service, &device);
+
+ if (!device || !bluez5_device1_get_connected(device)) {
+ /* Retry read probe on each connection */
+ read_probe_reset(impl, chr);
+ }
+
+ spa_log_debug(impl->log,
+ "At %s, connected:%d resolved:%d",
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)),
+ bluez5_device1_get_connected(device),
+ bluez5_device1_get_services_resolved(device));
+
+ available = service && device &&
+ bluez5_device1_get_connected(device) &&
+ bluez5_device1_get_services_resolved(device) &&
+ spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID) &&
+ spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)),
+ BT_MIDI_CHR_UUID);
+
+ if (available && !chr->read_done) {
+ read_probe(impl, chr);
+ available = false;
+ }
+
+ if (available && !chr->dsc_done) {
+ read_dsc(impl, chr);
+ available = chr->dsc_done;
+ }
+
+ if (chr->node_emitted && !available) {
+ remove_chr_node(impl, chr);
+ chr->node_emitted = false;
+ } else if (!chr->node_emitted && available) {
+ emit_chr_node(impl, chr, device);
+ chr->node_emitted = true;
+ }
+}
+
+static GList *get_all_valid_chr(struct impl *impl)
+{
+ GList *lst = NULL;
+ GList *objects;
+
+ if (!dbus_monitor_manager(&impl->monitor)) {
+ /* Still initializing (or it failed) */
+ return NULL;
+ }
+
+ objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor));
+ for (GList *p = g_list_first(objects); p; p = p->next) {
+ GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(p->data));
+
+ for (GList *p2 = g_list_first(interfaces); p2; p2 = p2->next) {
+ MidiEnumCharacteristicProxy *chr;
+
+ if (!MIDI_ENUM_IS_CHARACTERISTIC_PROXY(p2->data))
+ continue;
+
+ chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p2->data);
+ if (chr->impl == NULL)
+ continue;
+
+ lst = g_list_append(lst, g_object_ref(chr));
+ }
+ g_list_free_full(interfaces, g_object_unref);
+ }
+ g_list_free_full(objects, g_object_unref);
+
+ return lst;
+}
+
+static void check_all_nodes(struct impl *impl)
+{
+ /*
+ * Check if the nodes we have emitted are in sync with connected devices.
+ */
+
+ GList *chrs = get_all_valid_chr(impl);
+
+ for (GList *p = chrs; p; p = p->next)
+ check_chr_node(impl, MIDI_ENUM_CHARACTERISTIC_PROXY(p->data));
+
+ g_list_free_full(chrs, g_object_unref);
+}
+
+static void manager_register_application_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(source_object);
+ struct impl *impl = user_data;
+ GError *err = NULL;
+
+ bluez5_gatt_manager1_call_register_application_finish(
+ BLUEZ5_GATT_MANAGER1(source_object), res, &err);
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now */
+ g_error_free(err);
+ goto done;
+ }
+ if (err) {
+ spa_log_error(impl->log, "%s.RegisterApplication() failed: %s",
+ BLUEZ_GATT_MANAGER_INTERFACE,
+ err->message);
+ g_error_free(err);
+ goto done;
+ }
+
+ manager->registered = true;
+
+done:
+ g_clear_object(&manager->register_call);
+}
+
+static int manager_register_application(struct impl *impl, MidiEnumManagerProxy *manager)
+{
+ GVariantBuilder builder;
+ GVariant *options;
+
+ if (manager->registered)
+ return 0;
+ if (manager->register_call)
+ return -EBUSY;
+
+ spa_log_debug(impl->log, "%s.RegisterApplication(%s) on %s",
+ BLUEZ_GATT_MANAGER_INTERFACE,
+ g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(manager)));
+
+ manager->register_call = g_cancellable_new();
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+
+ bluez5_gatt_manager1_call_register_application(BLUEZ5_GATT_MANAGER1(manager),
+ g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
+ options,
+ manager->register_call,
+ manager_register_application_reply,
+ impl);
+
+ return 0;
+}
+
+/*
+ * DBus monitoring (Glib)
+ */
+
+static void midi_enum_characteristic_proxy_init(MidiEnumCharacteristicProxy *chr)
+{
+}
+
+static void midi_enum_characteristic_proxy_finalize(GObject *object)
+{
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(object);
+
+ g_cancellable_cancel(chr->read_call);
+ g_clear_object(&chr->read_call);
+
+ g_cancellable_cancel(chr->dsc_call);
+ g_clear_object(&chr->dsc_call);
+
+ if (chr->impl && chr->node_emitted)
+ remove_chr_node(chr->impl, chr);
+
+ chr->impl = NULL;
+
+ g_free(chr->description);
+ chr->description = NULL;
+}
+
+static void midi_enum_characteristic_proxy_class_init(MidiEnumCharacteristicProxyClass *klass)
+{
+ GObjectClass *object_class = (GObjectClass *) klass;
+
+ object_class->finalize = midi_enum_characteristic_proxy_finalize;
+}
+
+static void midi_enum_manager_proxy_init(MidiEnumManagerProxy *manager)
+{
+}
+
+static void midi_enum_manager_proxy_finalize(GObject *object)
+{
+ MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(object);
+
+ g_cancellable_cancel(manager->register_call);
+ g_clear_object(&manager->register_call);
+}
+
+static void midi_enum_manager_proxy_class_init(MidiEnumManagerProxyClass *klass)
+{
+ GObjectClass *object_class = (GObjectClass *) klass;
+
+ object_class->finalize = midi_enum_manager_proxy_finalize;
+}
+
+static void manager_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+
+ manager_register_application(impl, MIDI_ENUM_MANAGER_PROXY(iface));
+}
+
+static void manager_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ midi_enum_manager_proxy_finalize(G_OBJECT(iface));
+}
+
+static void device_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+
+ check_all_nodes(impl);
+}
+
+static void service_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+ Bluez5GattService1 *service = BLUEZ5_GATT_SERVICE1(iface);
+
+ if (!spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID))
+ return;
+
+ check_all_nodes(impl);
+}
+
+static void chr_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(iface);
+
+ if (!spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)),
+ BT_MIDI_CHR_UUID))
+ return;
+
+ if (chr->impl == NULL) {
+ chr->impl = impl;
+ chr->id = ++impl->id;
+ }
+
+ check_chr_node(impl, chr);
+}
+
+static void chr_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ midi_enum_characteristic_proxy_finalize(G_OBJECT(iface));
+}
+
+static void monitor_start(struct impl *impl)
+{
+ struct dbus_monitor_proxy_type proxy_types[] = {
+ { BLUEZ_DEVICE_INTERFACE, BLUEZ5_TYPE_DEVICE1_PROXY, device_update, NULL },
+ { BLUEZ_GATT_MANAGER_INTERFACE, MIDI_ENUM_TYPE_MANAGER_PROXY, manager_update, manager_clear },
+ { BLUEZ_GATT_SERVICE_INTERFACE, BLUEZ5_TYPE_GATT_SERVICE1_PROXY, service_update, NULL },
+ { BLUEZ_GATT_CHR_INTERFACE, MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY, chr_update, chr_clear },
+ { BLUEZ_GATT_DSC_INTERFACE, BLUEZ5_TYPE_GATT_DESCRIPTOR1_PROXY, NULL, NULL },
+ { NULL, BLUEZ5_TYPE_OBJECT_PROXY, NULL, NULL },
+ { NULL, G_TYPE_INVALID, NULL, NULL }
+ };
+
+ SPA_STATIC_ASSERT(SPA_N_ELEMENTS(proxy_types) <= DBUS_MONITOR_MAX_TYPES);
+
+ dbus_monitor_init(&impl->monitor, BLUEZ5_TYPE_OBJECT_MANAGER_CLIENT,
+ impl->log, impl->conn, BLUEZ_SERVICE, "/", proxy_types, NULL);
+}
+
+/*
+ * DBus GATT profile, to enable BlueZ autoconnect
+ */
+
+static gboolean profile_handle_release(Bluez5GattProfile1 *iface, GDBusMethodInvocation *invocation)
+{
+ bluez5_gatt_profile1_complete_release(iface, invocation);
+ return TRUE;
+}
+
+static int export_profile(struct impl *impl)
+{
+ static const char *uuids[] = { BT_MIDI_SERVICE_UUID, NULL };
+ GDBusObjectSkeleton *skeleton = NULL;
+ Bluez5GattProfile1 *iface = NULL;
+ int res = -ENOMEM;
+
+ iface = bluez5_gatt_profile1_skeleton_new();
+ if (!iface)
+ goto done;
+
+ skeleton = g_dbus_object_skeleton_new(MIDI_PROFILE_PATH);
+ if (!skeleton)
+ goto done;
+ g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
+
+ bluez5_gatt_profile1_set_uuids(iface, uuids);
+ g_signal_connect(iface, "handle-release", G_CALLBACK(profile_handle_release), NULL);
+
+ g_dbus_object_manager_server_export(impl->manager, skeleton);
+
+ spa_log_debug(impl->log, "MIDI GATT Profile exported, path=%s",
+ g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
+
+ res = 0;
+
+done:
+ g_clear_object(&iface);
+ g_clear_object(&skeleton);
+ return res;
+}
+
+/*
+ * Monitor impl
+ */
+
+static int impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ GList *chrs;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ chrs = get_all_valid_chr(this);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ for (GList *p = g_list_first(chrs); p; p = p->next) {
+ MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p->data);
+ Bluez5Device1 *device;
+ Bluez5GattService1 *service;
+
+ if (!chr->node_emitted)
+ continue;
+
+ lookup_chr_node(this, chr, &service, &device);
+ if (device)
+ emit_chr_node(this, chr, device);
+ }
+ g_list_free_full(chrs, g_object_unref);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ this = (struct impl *) handle;
+
+ dbus_monitor_clear(&this->monitor);
+ g_clear_object(&this->manager);
+ g_clear_object(&this->conn);
+
+ spa_zero(*this);
+
+ 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;
+ GError *error = NULL;
+ int res = 0;
+
+ 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);
+
+ if (this->log == NULL)
+ return -EINVAL;
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (!(info && spa_atob(spa_dict_lookup(info, SPA_KEY_API_GLIB_MAINLOOP)))) {
+ spa_log_error(this->log, "Glib mainloop is not usable: %s not set",
+ SPA_KEY_API_GLIB_MAINLOOP);
+ return -EINVAL;
+ }
+
+ spa_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ this->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
+ if (!this->conn) {
+ spa_log_error(this->log, "Creating GDBus connection failed: %s",
+ error->message);
+ g_error_free(error);
+ goto fail;
+ }
+
+ this->manager = g_dbus_object_manager_server_new(MIDI_OBJECT_PATH);
+ if (!this->manager){
+ spa_log_error(this->log, "Creating GDBus object manager failed");
+ goto fail;
+ }
+
+ if ((res = export_profile(this)) < 0)
+ goto fail;
+
+ g_dbus_object_manager_server_set_connection(this->manager, this->conn);
+
+ monitor_start(this);
+
+ return 0;
+
+fail:
+ res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
+ impl_clear(handle);
+ return res;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Bluez5 MIDI connection" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_bluez5_midi_enum_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_MIDI_ENUM,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/midi-node.c b/spa/plugins/bluez5/midi-node.c
new file mode 100644
index 0000000..c6a7e46
--- /dev/null
+++ b/spa/plugins/bluez5/midi-node.c
@@ -0,0 +1,2151 @@
+/* Spa MIDI node
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+#include <spa/utils/dll.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/monitor/device.h>
+#include <spa/control/control.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include <spa/debug/mem.h>
+#include <spa/debug/log.h>
+
+#include "midi.h"
+
+#include "bluez5-interface-gen.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi.node");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+#define DLL_BW 0.05
+
+#define DEFAULT_LATENCY_OFFSET (0 * SPA_NSEC_PER_MSEC)
+
+#define MAX_BUFFERS 32
+
+#define MIDI_RINGBUF_SIZE (8192*4)
+
+enum node_role {
+ NODE_SERVER,
+ NODE_CLIENT,
+};
+
+struct props {
+ char clock_name[64];
+ char device_name[512];
+ int64_t latency_offset;
+};
+
+struct midi_event_ringbuffer_entry {
+ uint64_t time;
+ unsigned int size;
+};
+
+struct midi_event_ringbuffer {
+ struct spa_ringbuffer rbuf;
+ uint8_t buf[MIDI_RINGBUF_SIZE];
+};
+
+struct buffer {
+ uint32_t id;
+ unsigned int outgoing:1;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct time_sync {
+ uint64_t prev_recv_time;
+ uint64_t recv_time;
+
+ uint16_t prev_device_timestamp;
+ uint16_t device_timestamp;
+
+ uint64_t device_time;
+
+ struct spa_dll dll;
+};
+
+struct port {
+ uint32_t id;
+ enum spa_direction direction;
+
+ struct spa_audio_info current_format;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ int fd;
+ uint16_t mtu;
+
+ struct buffer *buffer;
+ struct spa_pod_builder builder;
+ struct spa_pod_frame frame;
+
+ struct time_sync sync;
+
+ unsigned int acquired:1;
+ GCancellable *acquire_call;
+
+ struct spa_source source;
+
+ struct impl *impl;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ GDBusConnection *conn;
+ Bluez5GattCharacteristic1 *proxy;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define IDX_NODE_IO 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+#define PORT_IN 0
+#define PORT_OUT 1
+#define N_PORTS 2
+ struct port ports[N_PORTS];
+
+ char *chr_path;
+
+ unsigned int started:1;
+ unsigned int following:1;
+
+ struct spa_source timer_source;
+
+ int timerfd;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint32_t duration;
+ uint32_t rate;
+
+ uint64_t current_time;
+ uint64_t next_time;
+
+ struct midi_event_ringbuffer event_rbuf;
+
+ struct spa_bt_midi_parser parser;
+ struct spa_bt_midi_parser tmp_parser;
+ uint8_t read_buffer[MIDI_MAX_MTU];
+
+ struct spa_bt_midi_writer writer;
+
+ enum node_role role;
+
+ struct spa_bt_midi_server *server;
+};
+
+#define CHECK_PORT(this,d,p) ((p) == 0 && ((d) == SPA_DIRECTION_INPUT || (d) == SPA_DIRECTION_OUTPUT))
+#define GET_PORT(this,d,p) (&(this)->ports[(d) == SPA_DIRECTION_OUTPUT ? PORT_OUT : PORT_IN])
+
+static void midi_event_ringbuffer_init(struct midi_event_ringbuffer *mbuf)
+{
+ spa_ringbuffer_init(&mbuf->rbuf);
+}
+
+static int midi_event_ringbuffer_push(struct midi_event_ringbuffer *mbuf,
+ uint64_t time, uint8_t *event, unsigned int size)
+{
+ const unsigned int bufsize = sizeof(mbuf->buf);
+ int32_t avail;
+ uint32_t index;
+ struct midi_event_ringbuffer_entry evt = {
+ .time = time,
+ .size = size
+ };
+
+ avail = spa_ringbuffer_get_write_index(&mbuf->rbuf, &index);
+ if (avail < 0 || avail + sizeof(evt) + size > bufsize)
+ return -ENOSPC;
+
+ spa_ringbuffer_write_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ &evt, sizeof(evt));
+ index += sizeof(evt);
+ spa_ringbuffer_write_update(&mbuf->rbuf, index);
+ spa_ringbuffer_write_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ event, size);
+ index += size;
+ spa_ringbuffer_write_update(&mbuf->rbuf, index);
+
+ return 0;
+}
+
+static int midi_event_ringbuffer_peek(struct midi_event_ringbuffer *mbuf, uint64_t *time, unsigned int *size)
+{
+ const unsigned bufsize = sizeof(mbuf->buf);
+ int32_t avail;
+ uint32_t index;
+ struct midi_event_ringbuffer_entry evt;
+
+ avail = spa_ringbuffer_get_read_index(&mbuf->rbuf, &index);
+ if (avail < (int)sizeof(evt))
+ return -ENOENT;
+
+ spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ &evt, sizeof(evt));
+
+ *time = evt.time;
+ *size = evt.size;
+ return 0;
+}
+
+static int midi_event_ringbuffer_pop(struct midi_event_ringbuffer *mbuf, uint8_t *data, size_t max_size)
+{
+ const unsigned bufsize = sizeof(mbuf->buf);
+ int32_t avail;
+ uint32_t index;
+ struct midi_event_ringbuffer_entry evt;
+
+ avail = spa_ringbuffer_get_read_index(&mbuf->rbuf, &index);
+ if (avail < (int)sizeof(evt))
+ return -ENOENT;
+
+ spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ &evt, sizeof(evt));
+ index += sizeof(evt);
+ avail -= sizeof(evt);
+ spa_ringbuffer_read_update(&mbuf->rbuf, index);
+
+ if ((uint32_t)avail < evt.size) {
+ /* corrupted ringbuffer: should never happen */
+ spa_assert_not_reached();
+ return -EINVAL;
+ }
+
+ if (evt.size <= max_size)
+ spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize,
+ data, SPA_MIN(max_size, evt.size));
+ index += evt.size;
+ spa_ringbuffer_read_update(&mbuf->rbuf, index);
+
+ if (evt.size > max_size)
+ return -ENOSPC;
+
+ return 0;
+}
+
+static void reset_props(struct props *props)
+{
+ props->latency_offset = DEFAULT_LATENCY_OFFSET;
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+ props->device_name[0] = '\0';
+}
+
+static bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (b->outgoing) {
+ spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ b->outgoing = false;
+ }
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static void reset_buffers(struct port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ spa_list_append(&port->free, &b->link);
+ b->outgoing = false;
+ } else {
+ b->outgoing = true;
+ }
+ }
+}
+
+static struct buffer *peek_buffer(struct impl *this, struct port *port)
+{
+ if (spa_list_is_empty(&port->free))
+ return NULL;
+ return spa_list_first(&port->free, struct buffer, link);
+}
+
+static int prepare_buffer(struct impl *this, struct port *port)
+{
+ if (port->buffer != NULL)
+ return 0;
+ if ((port->buffer = peek_buffer(this, port)) == NULL)
+ return -EPIPE;
+
+ spa_pod_builder_init(&port->builder,
+ port->buffer->buf->datas[0].data,
+ port->buffer->buf->datas[0].maxsize);
+ spa_pod_builder_push_sequence(&port->builder, &port->frame, 0);
+
+ return 0;
+}
+
+static int finish_buffer(struct impl *this, struct port *port)
+{
+ if (port->buffer == NULL)
+ return 0;
+
+ spa_pod_builder_pop(&port->builder, &port->frame);
+
+ port->buffer->buf->datas[0].chunk->offset = 0;
+ port->buffer->buf->datas[0].chunk->size = port->builder.state.offset;
+
+ /* move buffer to ready queue */
+ spa_list_remove(&port->buffer->link);
+ spa_list_append(&port->ready, &port->buffer->link);
+ port->buffer = NULL;
+
+ return 0;
+}
+
+/* Replace value -> value + n*period, to minimize |value - target| */
+static int64_t unwrap_to_closest(int64_t value, int64_t target, int64_t period)
+{
+ if (value > target)
+ value -= SPA_ROUND_DOWN(value - target + period/2, period);
+ if (value < target)
+ value += SPA_ROUND_DOWN(target - value + period/2, period);
+ return value;
+}
+
+static int64_t time_diff(uint64_t a, uint64_t b)
+{
+ if (a >= b)
+ return a - b;
+ else
+ return -(int64_t)(b - a);
+}
+
+static void midi_event_get_last_timestamp(void *user_data, uint16_t timestamp, uint8_t *data, size_t size)
+{
+ int *last_timestamp = user_data;
+ *last_timestamp = timestamp;
+}
+
+static uint64_t midi_convert_time(struct time_sync *sync, uint16_t timestamp)
+{
+ int offset;
+
+ /*
+ * sync->device_timestamp is a device timestamp that corresponds to system
+ * clock time sync->device_time.
+ *
+ * It is the timestamp of the last MIDI event in the current packet, so we can
+ * assume here no event here has timestamp after it.
+ */
+ if (timestamp > sync->device_timestamp)
+ offset = sync->device_timestamp + MIDI_CLOCK_PERIOD_MSEC - timestamp;
+ else
+ offset = sync->device_timestamp - timestamp;
+
+ return sync->device_time - offset * SPA_NSEC_PER_MSEC;
+}
+
+static void midi_event_recv(void *user_data, uint16_t timestamp, uint8_t *data, size_t size)
+{
+ struct impl *this = user_data;
+ struct port *port = &this->ports[PORT_OUT];
+ struct time_sync *sync = &port->sync;
+ uint64_t time;
+ int res;
+
+ spa_assert(size > 0);
+
+ time = midi_convert_time(sync, timestamp);
+
+ 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));
+ }
+}
+
+static int unacquire_port(struct port *port)
+{
+ struct impl *this = port->impl;
+
+ if (!port->acquired)
+ return 0;
+
+ spa_log_debug(this->log, "%p: unacquire port:%d", this, port->direction);
+
+ shutdown(port->fd, SHUT_RDWR);
+ close(port->fd);
+ port->fd = -1;
+ port->acquired = false;
+
+ if (this->server)
+ spa_bt_midi_server_released(this->server,
+ (port->direction == SPA_DIRECTION_OUTPUT));
+
+ return 0;
+}
+
+static int do_unacquire_port(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *data, size_t size, void *user_data)
+{
+ struct port *port = user_data;
+
+ /* in main thread */
+ unacquire_port(port);
+ return 0;
+}
+
+static void on_ready_read(struct spa_source *source)
+{
+ struct port *port = source->data;
+ struct impl *this = port->impl;
+ struct timespec now;
+ int res, size, last_timestamp;
+
+ if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_ERR) ||
+ SPA_FLAG_IS_SET(source->rmask, SPA_IO_HUP)) {
+ spa_log_debug(this->log, "%p: port:%d ERR/HUP", this, port->direction);
+ goto stop;
+ }
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+
+ /* read data from socket */
+again:
+ size = recv(port->fd, this->read_buffer, sizeof(this->read_buffer), MSG_DONTWAIT | MSG_NOSIGNAL);
+ if (size == 0) {
+ return;
+ } else if (size < 0) {
+ if (errno == EINTR)
+ goto again;
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ return;
+ goto stop;
+ }
+
+ spa_log_trace(this->log, "%p: port:%d recv data size:%d", this, port->direction, size);
+ spa_debug_log_mem(this->log, SPA_LOG_LEVEL_TRACE, 4, this->read_buffer, size);
+
+ if (port->direction != SPA_DIRECTION_OUTPUT) {
+ /* Just monitor errors for the input port */
+ spa_log_debug(this->log, "%p: port:%d is not RX port; ignoring data",
+ this, port->direction);
+ return;
+ }
+
+ /* prepare for producing events */
+ if (port->io == NULL || port->n_buffers == 0 || !this->started)
+ return;
+
+ /*
+ * Remote clock synchronization:
+ *
+ * Assume: Last timestamp in packet on average corresponds to packet send time.
+ * There is some unknown latency in between, but on average it is constant.
+ *
+ * The `device_time` computed below is the estimated wall-clock time
+ * corresponding to the timestamp `device_timestamp` of the last event
+ * in the packet. This timestamp is late by the average transmission latency,
+ * which is unknown.
+ *
+ * Packet reception jitter and any clock drift is smoothed over with DLL.
+ * The estimated timestamps are stable and preserve event intervals.
+ *
+ * To allow latency_offset to work better, we don't write the events
+ * to the output buffer here, but instead put them to a ringbuffer.
+ * This is because if the offset shifts events to later buffers,
+ * this is simpler to handle with the rbuf.
+ */
+ last_timestamp = -1;
+ spa_bt_midi_parser_dup(&this->parser, &this->tmp_parser, true);
+ res = spa_bt_midi_parser_parse(&this->tmp_parser, this->read_buffer, size, true,
+ midi_event_get_last_timestamp, &last_timestamp);
+ if (res >= 0 && last_timestamp >= 0) {
+ struct time_sync *sync = &port->sync;
+ int64_t clock_elapsed;
+ int64_t device_elapsed;
+ int64_t err_nsec;
+ double corr, tcorr;
+
+ sync->prev_recv_time = sync->recv_time;
+ sync->recv_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ sync->prev_device_timestamp = sync->device_timestamp;
+ sync->device_timestamp = last_timestamp;
+
+ if (port->sync.prev_recv_time == 0) {
+ sync->prev_recv_time = sync->recv_time;
+ sync->prev_device_timestamp = sync->device_timestamp;
+ spa_dll_init(&sync->dll);
+ }
+ if (SPA_UNLIKELY(sync->dll.bw == 0))
+ spa_dll_set_bw(&sync->dll, DLL_BW, 1024, 48000);
+
+ /* move device clock forward */
+ clock_elapsed = sync->recv_time - sync->prev_recv_time;
+
+ device_elapsed = (int)sync->device_timestamp - (int)sync->prev_device_timestamp;
+ device_elapsed *= SPA_NSEC_PER_MSEC;
+ device_elapsed = unwrap_to_closest(device_elapsed, clock_elapsed, MIDI_CLOCK_PERIOD_NSEC);
+ sync->device_time += device_elapsed;
+
+ /* smooth clock sync */
+ err_nsec = time_diff(sync->recv_time, sync->device_time);
+ corr = spa_dll_update(&sync->dll,
+ -SPA_CLAMP(err_nsec, -20*SPA_NSEC_PER_MSEC, 20*SPA_NSEC_PER_MSEC)
+ * this->rate / SPA_NSEC_PER_SEC);
+ tcorr = SPA_MIN(device_elapsed, SPA_NSEC_PER_SEC) * (corr - 1);
+ sync->device_time += tcorr;
+
+ /* reset if too much off */
+ if (err_nsec < -50 * SPA_NSEC_PER_MSEC ||
+ err_nsec > 200 * SPA_NSEC_PER_MSEC ||
+ SPA_ABS(tcorr) > 20*SPA_NSEC_PER_MSEC ||
+ device_elapsed < 0) {
+ spa_log_debug(this->log, "%p: device clock sync off too much: resync", this);
+ spa_dll_init(&sync->dll);
+ sync->device_time = sync->recv_time;
+ }
+
+ spa_log_debug(this->log,
+ "timestamp:%d dt:%d dt2:%d err:%.1f tcorr:%.2f (ms) corr:%f",
+ (int)sync->device_timestamp,
+ (int)(clock_elapsed/SPA_NSEC_PER_MSEC),
+ (int)(device_elapsed/SPA_NSEC_PER_MSEC),
+ (double)err_nsec / SPA_NSEC_PER_MSEC,
+ tcorr/SPA_NSEC_PER_MSEC,
+ corr);
+ }
+
+ /* put midi event data to the buffer */
+ res = spa_bt_midi_parser_parse(&this->parser, this->read_buffer, size, false,
+ midi_event_recv, this);
+ if (res < 0) {
+ /* bad data */
+ spa_bt_midi_parser_init(&this->parser);
+
+ spa_log_info(this->log, "BLE MIDI data packet parsing failed: %d", res);
+ spa_debug_log_mem(this->log, SPA_LOG_LEVEL_DEBUG, 4, this->read_buffer, size);
+ }
+
+ return;
+
+stop:
+ spa_log_debug(this->log, "%p: port:%d stopping port", this, port->direction);
+
+ if (port->source.loop)
+ spa_loop_remove_source(this->data_loop, &port->source);
+
+ /* port->acquired is updated only from the main thread */
+ spa_loop_invoke(this->main_loop, do_unacquire_port, 0, NULL, 0, false, port);
+}
+
+static int process_output(struct impl *this)
+{
+ struct port *port = &this->ports[PORT_OUT];
+ struct buffer *buffer;
+ struct spa_io_buffers *io = port->io;
+
+ /* Check if we are able to process */
+ if (io == NULL || !port->acquired)
+ return SPA_STATUS_OK;
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Produce buffer */
+ if (prepare_buffer(this, port) >= 0) {
+ /*
+ * this->current_time is at the end time of the buffer, and offsets
+ * are recorded vs. the start of the buffer.
+ */
+ const uint64_t start_time = this->current_time
+ - this->duration * SPA_NSEC_PER_SEC / this->rate;
+ const uint64_t end_time = this->current_time;
+ uint64_t time;
+ uint32_t offset;
+ void *buf;
+ unsigned int size;
+ int res;
+
+ while (true) {
+ res = midi_event_ringbuffer_peek(&this->event_rbuf, &time, &size);
+ if (res < 0)
+ break;
+
+ time -= this->props.latency_offset;
+
+ if (time > end_time) {
+ break;
+ } else if (time + SPA_NSEC_PER_MSEC < start_time) {
+ /* Log events in the past by more than 1 ms, but don't
+ * do anything about them. The user can change the latency
+ * offset to choose whether to tradeoff latency for more
+ * accurate timestamps.
+ *
+ * TODO: maybe this information should be available in
+ * a more visible place, some latency property?
+ */
+ spa_log_debug(this->log, "%p: event in the past by %d ms",
+ this, (int)((start_time - time) / SPA_NSEC_PER_MSEC));
+ }
+
+ time = SPA_MAX(time, start_time) - start_time;
+ 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);
+ buf = spa_pod_builder_reserve_bytes(&port->builder, size);
+ if (buf) {
+ midi_event_ringbuffer_pop(&this->event_rbuf, buf, size);
+
+ spa_log_trace(this->log, "%p: produce event:0x%x offset:%d time:%"PRIu64"",
+ this, (int)*(uint8_t*)buf, (int)offset,
+ (uint64_t)(start_time + offset * SPA_NSEC_PER_SEC / this->rate));
+ }
+ }
+
+ finish_buffer(this, port);
+ }
+
+ /* Return if there are no buffers ready to be processed */
+ if (spa_list_is_empty(&port->ready))
+ return SPA_STATUS_OK;
+
+ /* Get the new buffer from the ready list */
+ buffer = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&buffer->link);
+ buffer->outgoing = true;
+
+ /* Set the new buffer in IO */
+ io->buffer_id = buffer->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ /* Notify we have a buffer ready to be processed */
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static int flush_packet(struct impl *this)
+{
+ struct port *port = &this->ports[PORT_IN];
+ int res;
+
+ if (this->writer.size == 0)
+ return 0;
+
+ res = send(port->fd, this->writer.buf, this->writer.size,
+ MSG_DONTWAIT | MSG_NOSIGNAL);
+ if (res < 0)
+ return -errno;
+
+ spa_log_trace(this->log, "%p: send packet size:%d", this, this->writer.size);
+ spa_debug_log_mem(this->log, SPA_LOG_LEVEL_TRACE, 4, this->writer.buf, this->writer.size);
+
+ return 0;
+}
+
+static int write_data(struct impl *this, struct spa_data *d)
+{
+ struct port *port = &this->ports[PORT_IN];
+ struct spa_pod_sequence *pod;
+ struct spa_pod_control *c;
+ uint64_t time;
+ int res;
+
+ pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size);
+ if (pod == NULL) {
+ spa_log_warn(this->log, "%p: invalid sequence in buffer max:%u offset:%u size:%u",
+ this, d->maxsize, d->chunk->offset, d->chunk->size);
+ return -EINVAL;
+ }
+
+ spa_bt_midi_writer_init(&this->writer, port->mtu);
+ time = 0;
+
+ SPA_POD_SEQUENCE_FOREACH(pod, c) {
+ uint8_t *event;
+ size_t size;
+
+ if (c->type != SPA_CONTROL_Midi)
+ 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);
+
+ spa_log_trace(this->log, "%p: output event:0x%x time:%"PRIu64, this,
+ (size > 0) ? event[0] : 0, time);
+
+ do {
+ res = spa_bt_midi_writer_write(&this->writer,
+ time, event, size);
+ if (res < 0) {
+ return res;
+ } else if (res) {
+ int res2;
+ if ((res2 = flush_packet(this)) < 0)
+ return res2;
+ }
+ } while (res);
+ }
+
+ if ((res = flush_packet(this)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int process_input(struct impl *this)
+{
+ struct port *port = &this->ports[PORT_IN];
+ struct buffer *b;
+ struct spa_io_buffers *io = port->io;
+ int res;
+
+ /* Check if we are able to process */
+ if (io == NULL || !port->acquired)
+ return SPA_STATUS_OK;
+
+ if (io->status != SPA_STATUS_HAVE_DATA || io->buffer_id >= port->n_buffers)
+ return SPA_STATUS_OK;
+
+ b = &port->buffers[io->buffer_id];
+ if (!b->outgoing) {
+ spa_log_warn(this->log, "%p: buffer %u not outgoing", this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+
+ if ((res = write_data(this, &b->buf->datas[0])) < 0) {
+ spa_log_info(this->log, "%p: writing data failed: %s",
+ this, spa_strerror(res));
+ }
+
+ port->io->buffer_id = b->id;
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_reuse_buffer(&this->callbacks, 0, io->buffer_id);
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static void update_position(struct impl *this)
+{
+ if (SPA_LIKELY(this->position)) {
+ this->duration = this->position->clock.duration;
+ this->rate = this->position->clock.rate.denom;
+ } else {
+ this->duration = 1024;
+ this->rate = 48000;
+ }
+}
+
+static void on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t exp;
+ uint64_t prev_time, now_time;
+ int status;
+
+ if (!this->started)
+ return;
+
+ if (spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0)
+ spa_log_warn(this->log, "%p: error reading timerfd: %s", this, strerror(errno));
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ update_position(this);
+
+ this->next_time = now_time + this->duration * SPA_NSEC_PER_SEC / this->rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = now_time;
+ this->clock->position += this->duration;
+ this->clock->duration = this->duration;
+ this->clock->rate_diff = 1.0f;
+ this->clock->next_nsec = this->next_time;
+ }
+
+ status = process_output(this);
+ spa_log_trace(this->log, "%p: status:%d", this, status);
+
+ spa_node_call_ready(&this->callbacks, status | SPA_STATUS_NEED_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int do_start(struct impl *this);
+
+static int do_release(struct impl *this);
+
+static int do_stop(struct impl *this);
+
+static void acquire_reply(GObject *source_object, GAsyncResult *res, gpointer user_data, bool notify)
+{
+ struct port *port;
+ struct impl *this;
+ const char *method;
+ GError *err = NULL;
+ GUnixFDList *fd_list = NULL;
+ GVariant *fd_handle = NULL;
+ int fd;
+ guint16 mtu;
+
+ if (notify) {
+ bluez5_gatt_characteristic1_call_acquire_notify_finish(
+ BLUEZ5_GATT_CHARACTERISTIC1(source_object), &fd_handle, &mtu, &fd_list, res, &err);
+ } else {
+ bluez5_gatt_characteristic1_call_acquire_write_finish(
+ BLUEZ5_GATT_CHARACTERISTIC1(source_object), &fd_handle, &mtu, &fd_list, res, &err);
+ }
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now. */
+ g_error_free(err);
+ return;
+ }
+
+ port = user_data;
+ this = port->impl;
+ method = notify ? "AcquireNotify" : "AcquireWrite";
+ if (err) {
+ spa_log_error(this->log, "%s.%s() for %s failed: %s",
+ BLUEZ_GATT_CHR_INTERFACE, method,
+ this->chr_path, err->message);
+ goto fail;
+ }
+
+ fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(fd_handle), &err);
+ if (fd < 0) {
+ spa_log_error(this->log, "%s.%s() for %s failed to get fd: %s",
+ BLUEZ_GATT_CHR_INTERFACE, method,
+ this->chr_path, err->message);
+ goto fail;
+ }
+
+ spa_log_info(this->log, "%p: BLE MIDI %s %s success mtu:%d",
+ this, this->chr_path, method, mtu);
+ port->fd = fd;
+ port->mtu = mtu;
+ port->acquired = true;
+
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ spa_bt_midi_parser_init(&this->parser);
+
+ /* Start source */
+ port->source.data = port;
+ port->source.fd = port->fd;
+ port->source.func = on_ready_read;
+ port->source.mask = SPA_IO_IN | SPA_IO_HUP | SPA_IO_ERR;
+ port->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &port->source);
+ }
+ return;
+
+fail:
+ g_error_free(err);
+ g_clear_object(&fd_list);
+ g_clear_object(&fd_handle);
+ do_stop(this);
+ do_release(this);
+}
+
+static void acquire_notify_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ acquire_reply(source_object, res, user_data, true);
+}
+
+static void acquire_write_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ acquire_reply(source_object, res, user_data, false);
+}
+
+static int do_acquire(struct port *port)
+{
+ struct impl *this = port->impl;
+ const char *method = (port->direction == SPA_DIRECTION_OUTPUT) ?
+ "AcquireNotify" : "AcquireWrite";
+ GVariant *options;
+ GVariantBuilder builder;
+
+ if (port->acquired)
+ return 0;
+ if (port->acquire_call)
+ return 0;
+
+ spa_log_info(this->log,
+ "%p: port %d: client %s for BLE MIDI device characteristic %s",
+ this, port->direction, method, this->chr_path);
+
+ port->acquire_call = g_cancellable_new();
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ bluez5_gatt_characteristic1_call_acquire_notify(
+ BLUEZ5_GATT_CHARACTERISTIC1(this->proxy),
+ options,
+ NULL,
+ port->acquire_call,
+ acquire_notify_reply,
+ port);
+ } else {
+ bluez5_gatt_characteristic1_call_acquire_write(
+ BLUEZ5_GATT_CHARACTERISTIC1(this->proxy),
+ options,
+ NULL,
+ port->acquire_call,
+ acquire_write_reply,
+ port);
+ }
+
+ return 0;
+}
+
+static int server_do_acquire(struct port *port, int fd, uint16_t mtu)
+{
+ struct impl *this = port->impl;
+ const char *method = (port->direction == SPA_DIRECTION_OUTPUT) ?
+ "AcquireWrite" : "AcquireNotify";
+
+ spa_log_info(this->log,
+ "%p: port %d: server %s for BLE MIDI device characteristic %s",
+ this, port->direction, method, this->server->chr_path);
+
+ if (port->acquired) {
+ spa_log_info(this->log,
+ "%p: port %d: %s failed: already acquired",
+ this, port->direction, method);
+ return -EBUSY;
+ }
+
+ port->fd = fd;
+ port->mtu = mtu;
+
+ if (port->direction == SPA_DIRECTION_OUTPUT)
+ spa_bt_midi_parser_init(&this->parser);
+
+ /* Start source */
+ port->source.data = port;
+ port->source.fd = port->fd;
+ port->source.func = on_ready_read;
+ port->source.mask = SPA_IO_HUP | SPA_IO_ERR;
+ if (port->direction == SPA_DIRECTION_OUTPUT)
+ port->source.mask |= SPA_IO_IN;
+ port->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &port->source);
+
+ port->acquired = true;
+ return 0;
+}
+
+static int server_acquire_write(void *user_data, int fd, uint16_t mtu)
+{
+ struct impl *this = user_data;
+ return server_do_acquire(&this->ports[PORT_OUT], fd, mtu);
+}
+
+static int server_acquire_notify(void *user_data, int fd, uint16_t mtu)
+{
+ struct impl *this = user_data;
+ return server_do_acquire(&this->ports[PORT_IN], fd, mtu);
+}
+
+static int server_release(void *user_data)
+{
+ struct impl *this = user_data;
+ do_release(this);
+ return 0;
+}
+
+static const char *server_description(void *user_data)
+{
+ struct impl *this = user_data;
+ return this->props.device_name;
+}
+
+static int do_remove_port_source(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct impl *this = user_data;
+ int i;
+
+ for (i = 0; i < N_PORTS; ++i) {
+ struct port *port = &this->ports[i];
+
+ if (port->source.loop)
+ spa_loop_remove_source(this->data_loop, &port->source);
+ }
+
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ if (this->timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->timer_source);
+
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ int res = 0;
+
+ spa_log_debug(this->log, "%p: stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ this->started = false;
+
+ return res;
+}
+
+static int do_release(struct impl *this)
+{
+ int res = 0;
+ size_t i;
+
+ spa_log_debug(this->log, "%p: release", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_port_source, 0, NULL, 0, true, this);
+
+ for (i = 0; i < N_PORTS; ++i) {
+ struct port *port = &this->ports[i];
+
+ g_cancellable_cancel(port->acquire_call);
+ g_clear_object(&port->acquire_call);
+
+ unacquire_port(port);
+ }
+
+ return res;
+}
+
+static int do_start(struct impl *this)
+{
+ int res;
+ size_t i;
+
+ if (this->started)
+ return 0;
+
+ this->following = is_following(this);
+
+ update_position(this);
+
+ spa_log_debug(this->log, "%p: start following:%d",
+ this, this->following);
+
+ for (i = 0; i < N_PORTS; ++i) {
+ struct port *port = &this->ports[i];
+
+ switch (this->role) {
+ case NODE_CLIENT:
+ /* Acquire Bluetooth I/O */
+ if ((res = do_acquire(port)) < 0) {
+ do_stop(this);
+ do_release(this);
+ return res;
+ }
+ break;
+ case NODE_SERVER:
+ /*
+ * In MIDI server role, the device/BlueZ invokes
+ * the acquire asynchronously as available/needed.
+ */
+ break;
+ default:
+ spa_assert_not_reached();
+ }
+
+ reset_buffers(port);
+ }
+
+ midi_event_ringbuffer_init(&this->event_rbuf);
+
+ this->started = true;
+
+ /* Start timer */
+ this->timer_source.data = this;
+ this->timer_source.fd = this->timerfd;
+ this->timer_source.func = on_timeout;
+ this->timer_source.mask = SPA_IO_IN;
+ this->timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->timer_source);
+
+ set_timers(this);
+
+ return 0;
+}
+
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+
+ set_timers(this);
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+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_PropInfo:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
+ SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, INT64_MIN, INT64_MAX));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName),
+ SPA_PROP_INFO_description, SPA_POD_String("Device name"),
+ SPA_PROP_INFO_type, SPA_POD_String(p->device_name));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset),
+ SPA_PROP_deviceName, SPA_POD_String(p->device_name));
+ 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 void emit_port_info(struct impl *this, struct port *port, bool full);
+
+static void set_latency(struct impl *this, bool emit_latency)
+{
+ struct port *port = &this->ports[PORT_OUT];
+
+ port->latency.min_ns = port->latency.max_ns = this->props.latency_offset;
+
+ if (emit_latency) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_port_info(this, port, false);
+ }
+}
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(&new_props);
+ } else {
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset),
+ SPA_PROP_deviceName, SPA_POD_OPT_Stringn(new_props.device_name,
+ sizeof(new_props.device_name)));
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+
+ if (changed)
+ set_latency(this, true);
+
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ if (apply_props(this, param) > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ int res, res2;
+
+ 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 ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ res = do_stop(this);
+ if (this->role == NODE_CLIENT)
+ res2 = do_release(this);
+ else
+ res2 = 0;
+ if (res < 0)
+ return res;
+ if (res2 < 0)
+ return res2;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Midi/Bridge" },
+ };
+ 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);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ size_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+
+ for (i = 0; i < N_PORTS; ++i)
+ emit_port_info(this, &this->ports[i], true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ 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);
+
+ 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 (result.index > 0)
+ return 0;
+
+ 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));
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ 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));
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ 4096, 4096, INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1));
+ 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:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ if (!port->have_format)
+ return 0;
+
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_application ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_control)
+ return -EINVAL;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, 1);
+ 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: use buffers %d", this, n_buffers);
+
+ if (!port->have_format)
+ return -EIO;
+
+ clear_buffers(this, port);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ reset_buffers(port);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+ port = GET_PORT(this, SPA_DIRECTION_OUTPUT, port_id);
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ recycle_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ int status = SPA_STATUS_OK;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (!this->started)
+ return SPA_STATUS_OK;
+
+ if (this->following) {
+ if (this->position) {
+ this->current_time = this->position->clock.nsec;
+ } else {
+ struct timespec now = { 0 };
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
+ }
+ }
+
+ update_position(this);
+
+ if (this->following)
+ status |= process_output(this);
+
+ status |= process_input(this);
+
+ return status;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static const struct spa_bt_midi_server_cb impl_server = {
+ .acquire_write = server_acquire_write,
+ .acquire_notify = server_acquire_notify,
+ .release = server_release,
+ .get_description = server_description,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ do_stop(this);
+ do_release(this);
+
+ free(this->chr_path);
+ if (this->timerfd > 0)
+ spa_system_close(this->data_system, this->timerfd);
+ if (this->server)
+ spa_bt_midi_server_destroy(this->server);
+ g_clear_object(&this->proxy);
+ g_clear_object(&this->conn);
+
+ spa_zero(*this);
+
+ 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;
+ const char *device_name = "";
+ int res = 0;
+ GError *err = NULL;
+ size_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ 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);
+
+ if (this->log == NULL)
+ return -EINVAL;
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (!(info && spa_atob(spa_dict_lookup(info, SPA_KEY_API_GLIB_MAINLOOP)))) {
+ spa_log_error(this->log, "Glib mainloop is not usable: %s not set",
+ SPA_KEY_API_GLIB_MAINLOOP);
+ return -EINVAL;
+ }
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->role = NODE_CLIENT;
+
+ if (info) {
+ const char *str;
+
+ if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_PATH)) != NULL)
+ this->chr_path = strdup(str);
+
+ if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_ROLE)) != NULL) {
+ if (spa_streq(str, "server"))
+ this->role = NODE_SERVER;
+ }
+
+ if ((str = spa_dict_lookup(info, "node.nick")) != NULL)
+ device_name = str;
+ else if ((str = spa_dict_lookup(info, "node.description")) != NULL)
+ device_name = str;
+ }
+
+ if (this->role == NODE_CLIENT && this->chr_path == NULL) {
+ spa_log_error(this->log, "missing MIDI service characteristic path");
+ res = -EINVAL;
+ goto fail;
+ }
+
+ this->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &err);
+ if (this->conn == NULL) {
+ spa_log_error(this->log, "failed to get dbus connection: %s",
+ err->message);
+ g_error_free(err);
+ res = -EIO;
+ goto fail;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ spa_scnprintf(this->props.device_name, sizeof(this->props.device_name),
+ "%s", device_name);
+
+ /* set the node info */
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ 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->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ /* set the port info */
+ 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_PORT_NAME, "in"),
+ SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "in"),
+ };
+ 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_PORT_NAME, "out"),
+ SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "out"),
+ };
+ static const struct spa_dict in_port_props = SPA_DICT_INIT_ARRAY(in_port_items);
+ static const struct spa_dict out_port_props = SPA_DICT_INIT_ARRAY(out_port_items);
+
+ spa_zero(*port);
+
+ port->impl = this;
+
+ port->id = 0;
+ port->direction = (i == PORT_OUT) ? SPA_DIRECTION_OUTPUT :
+ SPA_DIRECTION_INPUT;
+
+ 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.change_mask = SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+ port->info.props = (i == PORT_OUT) ? &out_port_props : &in_port_props;
+
+ port->latency = SPA_LATENCY_INFO(port->direction);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ /* Init the buffer lists */
+ spa_list_init(&port->ready);
+ spa_list_init(&port->free);
+ }
+
+ this->duration = 1024;
+ this->rate = 48000;
+
+ set_latency(this, false);
+
+ if (this->role == NODE_SERVER) {
+ this->server = spa_bt_midi_server_new(&impl_server, this->conn, this->log, this);
+ if (this->server == NULL)
+ goto fail;
+ } else {
+ this->proxy = bluez5_gatt_characteristic1_proxy_new_sync(this->conn,
+ G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+ BLUEZ_SERVICE,
+ this->chr_path,
+ NULL,
+ &err);
+ if (this->proxy == NULL) {
+ spa_log_error(this->log,
+ "Failed to create BLE MIDI GATT proxy %s: %s",
+ this->chr_path, err->message);
+ g_error_free(err);
+ res = -EIO;
+ goto fail;
+ }
+ }
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ return 0;
+
+fail:
+ res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
+ impl_clear(handle);
+ return res;
+}
+
+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, "Pauli Virtanen <pav@iki.fi>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Bluez5 MIDI connection" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_bluez5_midi_node_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_MIDI_NODE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/midi-parser.c b/spa/plugins/bluez5/midi-parser.c
new file mode 100644
index 0000000..ba3cd32
--- /dev/null
+++ b/spa/plugins/bluez5/midi-parser.c
@@ -0,0 +1,295 @@
+/* BLE MIDI parser
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+
+#include "midi.h"
+
+enum midi_event_class {
+ MIDI_BASIC,
+ MIDI_SYSEX,
+ MIDI_SYSCOMMON,
+ MIDI_REALTIME,
+ MIDI_ERROR
+};
+
+static enum midi_event_class midi_event_info(uint8_t status, unsigned int *size)
+{
+ switch (status) {
+ case 0x80 ... 0x8f:
+ case 0x90 ... 0x9f:
+ case 0xa0 ... 0xaf:
+ case 0xb0 ... 0xbf:
+ case 0xe0 ... 0xef:
+ *size = 3;
+ return MIDI_BASIC;
+ case 0xc0 ... 0xcf:
+ case 0xd0 ... 0xdf:
+ *size = 2;
+ return MIDI_BASIC;
+ case 0xf0:
+ /* variable; count only status byte here */
+ *size = 1;
+ return MIDI_SYSEX;
+ case 0xf1:
+ case 0xf3:
+ *size = 2;
+ return MIDI_SYSCOMMON;
+ case 0xf2:
+ *size = 3;
+ return MIDI_SYSCOMMON;
+ case 0xf6:
+ case 0xf7:
+ *size = 1;
+ return MIDI_SYSCOMMON;
+ case 0xf8 ... 0xff:
+ *size = 1;
+ return MIDI_REALTIME;
+ case 0xf4:
+ case 0xf5:
+ default:
+ /* undefined MIDI status */
+ *size = 0;
+ return MIDI_ERROR;
+ }
+}
+
+static void timestamp_set_high(uint16_t *time, uint8_t byte)
+{
+ *time = (byte & 0x3f) << 7;
+}
+
+static void timestamp_set_low(uint16_t *time, uint8_t byte)
+{
+ if ((*time & 0x7f) > (byte & 0x7f))
+ *time += 0x80;
+
+ *time &= ~0x7f;
+ *time |= byte & 0x7f;
+}
+
+int spa_bt_midi_parser_parse(struct spa_bt_midi_parser *parser,
+ const uint8_t *src, size_t src_size, bool only_time,
+ void (*event)(void *user_data, uint16_t time, uint8_t *event, size_t event_size),
+ void *user_data)
+{
+ const uint8_t *src_end = src + src_size;
+ uint8_t running_status = 0;
+ uint16_t time;
+ uint8_t byte;
+
+#define NEXT() do { if (src == src_end) return -EINVAL; byte = *src++; } while (0)
+#define PUT(byte) do { if (only_time) { parser->size++; break; } \
+ if (parser->size == sizeof(parser->buf)) return -ENOSPC; \
+ parser->buf[parser->size++] = (byte); } while (0)
+
+ /* Header */
+ NEXT();
+ if (!(byte & 0x80))
+ return -EINVAL;
+ timestamp_set_high(&time, byte);
+
+ while (src < src_end) {
+ NEXT();
+
+ if (!parser->sysex) {
+ uint8_t status = 0;
+ unsigned int event_size;
+
+ if (byte & 0x80) {
+ /* Timestamp */
+ timestamp_set_low(&time, byte);
+ NEXT();
+
+ /* Status? */
+ if (byte & 0x80) {
+ parser->size = 0;
+ PUT(byte);
+ status = byte;
+ }
+ }
+
+ if (status == 0) {
+ /* Running status */
+ parser->size = 0;
+ PUT(running_status);
+ PUT(byte);
+ status = running_status;
+ }
+
+ switch (midi_event_info(status, &event_size)) {
+ case MIDI_BASIC:
+ running_status = (event_size > 1) ? status : 0;
+ break;
+ case MIDI_REALTIME:
+ case MIDI_SYSCOMMON:
+ /* keep previous running status */
+ break;
+ case MIDI_SYSEX:
+ parser->sysex = true;
+ /* XXX: not fully clear if SYSEX can be running status, assume no */
+ running_status = 0;
+ continue;
+ default:
+ goto malformed;
+ }
+
+ /* Event data */
+ while (parser->size < event_size) {
+ NEXT();
+ if (byte & 0x80) {
+ /* BLE MIDI allows no interleaved events */
+ goto malformed;
+ }
+ PUT(byte);
+ }
+
+ event(user_data, time, parser->buf, parser->size);
+ } else {
+ if (byte & 0x80) {
+ /* Timestamp */
+ timestamp_set_low(&time, byte);
+ NEXT();
+
+ if (byte == 0xf7) {
+ /* Sysex end */
+ PUT(byte);
+ event(user_data, time, parser->buf, parser->size);
+ parser->sysex = false;
+ } else {
+ /* Interleaved realtime event */
+ unsigned int event_size;
+
+ if (midi_event_info(byte, &event_size) != MIDI_REALTIME)
+ goto malformed;
+ spa_assert(event_size == 1);
+ event(user_data, time, &byte, 1);
+ }
+ } else {
+ PUT(byte);
+ }
+ }
+ }
+
+#undef NEXT
+#undef PUT
+
+ return 0;
+
+malformed:
+ /* Error (potentially recoverable) */
+ return -EINVAL;
+}
+
+
+int spa_bt_midi_writer_write(struct spa_bt_midi_writer *writer,
+ uint64_t time, const uint8_t *event, size_t event_size)
+{
+ /* BLE MIDI-1.0: maximum payload size is MTU - 3 */
+ const unsigned int max_size = writer->mtu - 3;
+ const uint64_t time_msec = (time / SPA_NSEC_PER_MSEC);
+ const uint16_t timestamp = time_msec & 0x1fff;
+
+#define PUT(byte) do { if (writer->size >= max_size) return -ENOSPC; \
+ writer->buf[writer->size++] = (byte); } while (0)
+
+ if (writer->mtu < 5+3)
+ return -ENOSPC; /* all events must fit */
+
+ spa_assert(max_size <= sizeof(writer->buf));
+ spa_assert(writer->size <= max_size);
+
+ if (event_size == 0)
+ return 0;
+
+ if (writer->flush) {
+ writer->flush = false;
+ writer->size = 0;
+ }
+
+ if (writer->size == max_size)
+ goto flush;
+
+ /* Packet header */
+ if (writer->size == 0) {
+ PUT(0x80 | (timestamp >> 7));
+ writer->running_status = 0;
+ writer->running_time_msec = time_msec;
+ }
+
+ /* Timestamp low bits can wrap around, but not multiple times */
+ if (time_msec > writer->running_time_msec + 0x7f)
+ goto flush;
+
+ spa_assert(writer->pos < event_size);
+
+ for (; writer->pos < event_size; ++writer->pos) {
+ const unsigned int unused = max_size - writer->size;
+ const uint8_t byte = event[writer->pos];
+
+ if (byte & 0x80) {
+ enum midi_event_class class;
+ unsigned int expected_size;
+
+ class = midi_event_info(event[0], &expected_size);
+
+ if (class == MIDI_BASIC && expected_size > 1 &&
+ writer->running_status == byte &&
+ writer->running_time_msec == time_msec) {
+ /* Running status: continue with data */
+ continue;
+ }
+
+ if (unused < expected_size + 1)
+ goto flush;
+
+ /* Timestamp before status */
+ PUT(0x80 | (timestamp & 0x7f));
+ writer->running_time_msec = time_msec;
+
+ if (class == MIDI_BASIC && expected_size > 1)
+ writer->running_status = byte;
+ else
+ writer->running_status = 0;
+ } else if (unused == 0) {
+ break;
+ }
+
+ PUT(byte);
+ }
+
+ if (writer->pos < event_size)
+ goto flush;
+
+ writer->pos = 0;
+ return 0;
+
+flush:
+ writer->flush = true;
+ return 1;
+
+#undef PUT
+}
diff --git a/spa/plugins/bluez5/midi-server.c b/spa/plugins/bluez5/midi-server.c
new file mode 100644
index 0000000..a1b2682
--- /dev/null
+++ b/spa/plugins/bluez5/midi-server.c
@@ -0,0 +1,581 @@
+/* Spa Bluez5 midi
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+
+#include "midi.h"
+
+#include "bluez5-interface-gen.h"
+#include "dbus-monitor.h"
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT (&impl->log_topic)
+
+#define MIDI_SERVER_PATH "/midiserver%u"
+#define MIDI_SERVICE_PATH "/midiserver%u/service"
+#define MIDI_CHR_PATH "/midiserver%u/service/chr"
+#define MIDI_DSC_PATH "/midiserver%u/service/chr/dsc"
+
+#define BLE_DEFAULT_MTU 23
+
+struct impl
+{
+ struct spa_bt_midi_server this;
+
+ struct spa_log_topic log_topic;
+ struct spa_log *log;
+
+ const struct spa_bt_midi_server_cb *cb;
+
+ GDBusConnection *conn;
+ struct dbus_monitor monitor;
+ GDBusObjectManagerServer *manager;
+
+ Bluez5GattCharacteristic1 *chr;
+
+ void *user_data;
+
+ uint32_t server_id;
+
+ unsigned int write_acquired:1;
+ unsigned int notify_acquired:1;
+};
+
+struct _MidiServerManagerProxy
+{
+ Bluez5GattManager1Proxy parent_instance;
+
+ GCancellable *register_call;
+ unsigned int registered:1;
+};
+
+G_DECLARE_FINAL_TYPE(MidiServerManagerProxy, midi_server_manager_proxy, MIDI_SERVER,
+ MANAGER_PROXY, Bluez5GattManager1Proxy)
+G_DEFINE_TYPE(MidiServerManagerProxy, midi_server_manager_proxy, BLUEZ5_TYPE_GATT_MANAGER1_PROXY)
+#define MIDI_SERVER_TYPE_MANAGER_PROXY (midi_server_manager_proxy_get_type())
+
+
+/*
+ * Characteristic user descriptor: not in BLE MIDI standard, but we
+ * put a device name here in case we have multiple MIDI endpoints.
+ */
+
+static gboolean dsc_handle_read_value(Bluez5GattDescriptor1 *iface, GDBusMethodInvocation *invocation,
+ GVariant *arg_options, gpointer user_data)
+{
+ struct impl *impl = user_data;
+ const char *description = NULL;
+ uint16_t offset = 0;
+ int len;
+
+ g_variant_lookup(arg_options, "offset", "q", &offset);
+
+ if (impl->cb->get_description)
+ description = impl->cb->get_description(impl->user_data);
+ if (!description)
+ description = "";
+
+ len = strlen(description);
+ if (offset > len) {
+ g_dbus_method_invocation_return_dbus_error(invocation,
+ "org.freedesktop.DBus.Error.InvalidArgs",
+ "Invalid arguments");
+ return TRUE;
+ }
+
+ bluez5_gatt_descriptor1_complete_read_value(iface,
+ invocation, description + offset);
+ return TRUE;
+}
+
+static int export_dsc(struct impl *impl)
+{
+ static const char * const flags[] = { "encrypt-read", NULL };
+ GDBusObjectSkeleton *skeleton = NULL;
+ Bluez5GattDescriptor1 *iface = NULL;
+ int res = -ENOMEM;
+ char path[128];
+
+ iface = bluez5_gatt_descriptor1_skeleton_new();
+ if (!iface)
+ goto done;
+
+ spa_scnprintf(path, sizeof(path), MIDI_DSC_PATH, impl->server_id);
+ skeleton = g_dbus_object_skeleton_new(path);
+ if (!skeleton)
+ goto done;
+ g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
+
+ bluez5_gatt_descriptor1_set_uuid(iface, BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID);
+ spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id);
+ bluez5_gatt_descriptor1_set_characteristic(iface, path);
+ bluez5_gatt_descriptor1_set_flags(iface, flags);
+
+ g_signal_connect(iface, "handle-read-value", G_CALLBACK(dsc_handle_read_value), impl);
+
+ g_dbus_object_manager_server_export(impl->manager, skeleton);
+
+ spa_log_debug(impl->log, "MIDI GATT Descriptor exported, path=%s",
+ g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
+
+ res = 0;
+
+done:
+ g_clear_object(&iface);
+ g_clear_object(&skeleton);
+ return res;
+}
+
+
+/*
+ * MIDI characteristic
+ */
+
+static gboolean chr_handle_read_value(Bluez5GattCharacteristic1 *iface,
+ GDBusMethodInvocation *invocation, GVariant *arg_options,
+ gpointer user_data)
+{
+ /* BLE MIDI-1.0: returns empty value */
+ bluez5_gatt_characteristic1_complete_read_value(iface, invocation, "");
+ return TRUE;
+}
+
+static void chr_change_acquired(struct impl *impl, bool write, bool enabled)
+{
+ if (write) {
+ impl->write_acquired = enabled;
+ bluez5_gatt_characteristic1_set_write_acquired(impl->chr, enabled);
+ } else {
+ impl->notify_acquired = enabled;
+ bluez5_gatt_characteristic1_set_notify_acquired(impl->chr, enabled);
+ }
+}
+
+static int create_socketpair(int fds[2])
+{
+ if (socketpair(AF_LOCAL, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, fds) < 0)
+ return -errno;
+ return 0;
+}
+
+static gboolean chr_handle_acquire(Bluez5GattCharacteristic1 *object,
+ GDBusMethodInvocation *invocation,
+ GUnixFDList *dummy, GVariant *arg_options,
+ bool write, gpointer user_data)
+{
+ struct impl *impl = user_data;
+ const char *err_msg = "Failed";
+ uint16_t mtu = BLE_DEFAULT_MTU;
+ gint fds[2] = {-1, -1};
+ int res;
+ GUnixFDList *fd_list = NULL;
+ GVariant *fd_handle = NULL;
+ GError *err = NULL;
+
+ if ((write && (impl->cb->acquire_write == NULL)) ||
+ (!write && (impl->cb->acquire_notify == NULL))) {
+ err_msg = "Not supported";
+ goto fail;
+ }
+ if ((write && impl->write_acquired) ||
+ (!write && impl->notify_acquired)) {
+ err_msg = "Already acquired";
+ goto fail;
+ }
+
+ g_variant_lookup(arg_options, "mtu", "q", &mtu);
+
+ if (create_socketpair(fds) < 0) {
+ err_msg = "Socketpair creation failed";
+ goto fail;
+ }
+
+ if (write)
+ res = impl->cb->acquire_write(impl->user_data, fds[0], mtu);
+ else
+ res = impl->cb->acquire_notify(impl->user_data, fds[0], mtu);
+ if (res < 0) {
+ err_msg = "Acquiring failed";
+ goto fail;
+ }
+ fds[0] = -1;
+
+ fd_handle = g_variant_new_handle(0);
+ fd_list = g_unix_fd_list_new_from_array(&fds[1], 1);
+ fds[1] = -1;
+
+ chr_change_acquired(impl, write, true);
+
+ if (write) {
+ bluez5_gatt_characteristic1_complete_acquire_write(
+ object, invocation, fd_list, fd_handle, mtu);
+ } else {
+ bluez5_gatt_characteristic1_complete_acquire_notify(
+ object, invocation, fd_list, fd_handle, mtu);
+ }
+
+ g_clear_object(&fd_list);
+ return TRUE;
+
+fail:
+ if (fds[0] >= 0)
+ close(fds[0]);
+ if (fds[1] >= 0)
+ close(fds[1]);
+
+ if (err)
+ g_error_free(err);
+ g_clear_pointer(&fd_handle, g_variant_unref);
+ g_clear_object(&fd_list);
+ g_dbus_method_invocation_return_dbus_error(invocation,
+ "org.freedesktop.DBus.Error.Failed", err_msg);
+ return TRUE;
+
+}
+
+static gboolean chr_handle_acquire_write(Bluez5GattCharacteristic1 *object,
+ GDBusMethodInvocation *invocation,
+ GUnixFDList *fd_list, GVariant *arg_options,
+ gpointer user_data)
+{
+ return chr_handle_acquire(object, invocation, fd_list, arg_options, true, user_data);
+}
+
+static gboolean chr_handle_acquire_notify(Bluez5GattCharacteristic1 *object,
+ GDBusMethodInvocation *invocation,
+ GUnixFDList *fd_list, GVariant *arg_options,
+ gpointer user_data)
+{
+ return chr_handle_acquire(object, invocation, fd_list, arg_options, false, user_data);
+}
+
+static int export_chr(struct impl *impl)
+{
+ static const char * const flags[] = { "encrypt-read", "write-without-response",
+ "encrypt-write", "encrypt-notify", NULL };
+ GDBusObjectSkeleton *skeleton = NULL;
+ Bluez5GattCharacteristic1 *iface = NULL;
+ int res = -ENOMEM;
+ char path[128];
+
+ iface = bluez5_gatt_characteristic1_skeleton_new();
+ if (!iface)
+ goto done;
+
+ spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id);
+ skeleton = g_dbus_object_skeleton_new(path);
+ if (!skeleton)
+ goto done;
+ g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
+
+ bluez5_gatt_characteristic1_set_uuid(iface, BT_MIDI_CHR_UUID);
+ spa_scnprintf(path, sizeof(path), MIDI_SERVICE_PATH, impl->server_id);
+ bluez5_gatt_characteristic1_set_service(iface, path);
+ bluez5_gatt_characteristic1_set_write_acquired(iface, FALSE);
+ bluez5_gatt_characteristic1_set_notify_acquired(iface, FALSE);
+ bluez5_gatt_characteristic1_set_flags(iface, flags);
+
+ g_signal_connect(iface, "handle-read-value", G_CALLBACK(chr_handle_read_value), impl);
+ g_signal_connect(iface, "handle-acquire-write", G_CALLBACK(chr_handle_acquire_write), impl);
+ g_signal_connect(iface, "handle-acquire-notify", G_CALLBACK(chr_handle_acquire_notify), impl);
+
+ g_dbus_object_manager_server_export(impl->manager, skeleton);
+
+ impl->chr = g_object_ref(iface);
+
+ spa_log_debug(impl->log, "MIDI GATT Characteristic exported, path=%s",
+ g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
+
+ res = 0;
+
+done:
+ g_clear_object(&iface);
+ g_clear_object(&skeleton);
+ return res;
+}
+
+
+/*
+ * MIDI service
+ */
+
+static int export_service(struct impl *impl)
+{
+ GDBusObjectSkeleton *skeleton = NULL;
+ Bluez5GattService1 *iface = NULL;
+ int res = -ENOMEM;
+ char path[128];
+
+ iface = bluez5_gatt_service1_skeleton_new();
+ if (!iface)
+ goto done;
+
+ spa_scnprintf(path, sizeof(path), MIDI_SERVICE_PATH, impl->server_id);
+ skeleton = g_dbus_object_skeleton_new(path);
+ if (!skeleton)
+ goto done;
+ g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface));
+
+ bluez5_gatt_service1_set_uuid(iface, BT_MIDI_SERVICE_UUID);
+ bluez5_gatt_service1_set_primary(iface, TRUE);
+
+ g_dbus_object_manager_server_export(impl->manager, skeleton);
+
+ spa_log_debug(impl->log, "MIDI GATT Service exported, path=%s",
+ g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton)));
+
+ res = 0;
+
+done:
+ g_clear_object(&iface);
+ g_clear_object(&skeleton);
+ return res;
+}
+
+
+/*
+ * Registration on all GattManagers
+ */
+
+static void manager_register_application_reply(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+ MidiServerManagerProxy *manager = MIDI_SERVER_MANAGER_PROXY(source_object);
+ struct impl *impl = user_data;
+ GError *err = NULL;
+
+ bluez5_gatt_manager1_call_register_application_finish(
+ BLUEZ5_GATT_MANAGER1(source_object), res, &err);
+
+ if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Operation canceled: user_data may be invalid by now */
+ g_error_free(err);
+ goto done;
+ }
+ if (err) {
+ spa_log_error(impl->log, "%s.RegisterApplication() failed: %s",
+ BLUEZ_GATT_MANAGER_INTERFACE,
+ err->message);
+ g_error_free(err);
+ goto done;
+ }
+
+ manager->registered = true;
+
+done:
+ g_clear_object(&manager->register_call);
+}
+
+static int manager_register_application(struct impl *impl, MidiServerManagerProxy *manager)
+{
+ GVariantBuilder builder;
+ GVariant *options;
+
+ if (manager->registered)
+ return 0;
+ if (manager->register_call)
+ return -EBUSY;
+
+ spa_log_debug(impl->log, "%s.RegisterApplication(%s) on %s",
+ BLUEZ_GATT_MANAGER_INTERFACE,
+ g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
+ g_dbus_proxy_get_object_path(G_DBUS_PROXY(manager)));
+
+ manager->register_call = g_cancellable_new();
+ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+ options = g_variant_builder_end(&builder);
+ bluez5_gatt_manager1_call_register_application(BLUEZ5_GATT_MANAGER1(manager),
+ g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)),
+ options,
+ manager->register_call,
+ manager_register_application_reply,
+ impl);
+
+ return 0;
+}
+
+static void midi_server_manager_proxy_init(MidiServerManagerProxy *manager)
+{
+}
+
+static void midi_server_manager_proxy_finalize(GObject *object)
+{
+ MidiServerManagerProxy *manager = MIDI_SERVER_MANAGER_PROXY(object);
+
+ g_cancellable_cancel(manager->register_call);
+ g_clear_object(&manager->register_call);
+}
+
+static void midi_server_manager_proxy_class_init(MidiServerManagerProxyClass *klass)
+{
+ GObjectClass *object_class = (GObjectClass *) klass;
+
+ object_class->finalize = midi_server_manager_proxy_finalize;
+}
+
+static void manager_update(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+
+ manager_register_application(impl, MIDI_SERVER_MANAGER_PROXY(iface));
+}
+
+static void manager_clear(struct dbus_monitor *monitor, GDBusInterface *iface)
+{
+ midi_server_manager_proxy_finalize(G_OBJECT(iface));
+}
+
+static void on_name_owner_change(struct dbus_monitor *monitor)
+{
+ struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor);
+
+ /*
+ * BlueZ disappeared/appeared. It does not appear to close the sockets
+ * it quits, so we should force the chr release now.
+ */
+ if (impl->cb->release)
+ impl->cb->release(impl->user_data);
+ chr_change_acquired(impl, true, false);
+ chr_change_acquired(impl, false, false);
+}
+
+static void monitor_start(struct impl *impl)
+{
+ struct dbus_monitor_proxy_type proxy_types[] = {
+ { BLUEZ_GATT_MANAGER_INTERFACE, MIDI_SERVER_TYPE_MANAGER_PROXY, manager_update, manager_clear },
+ { NULL, BLUEZ5_TYPE_OBJECT_PROXY, NULL, NULL },
+ { NULL, G_TYPE_INVALID, NULL, NULL },
+ };
+
+ SPA_STATIC_ASSERT(SPA_N_ELEMENTS(proxy_types) <= DBUS_MONITOR_MAX_TYPES);
+
+ dbus_monitor_init(&impl->monitor, BLUEZ5_TYPE_OBJECT_MANAGER_CLIENT,
+ impl->log, impl->conn, BLUEZ_SERVICE, "/", proxy_types,
+ on_name_owner_change);
+}
+
+
+/*
+ * Object registration
+ */
+
+static int export_objects(struct impl *impl)
+{
+ int res = 0;
+ char path[128];
+
+ spa_scnprintf(path, sizeof(path), MIDI_SERVER_PATH, impl->server_id);
+ impl->manager = g_dbus_object_manager_server_new(path);
+ if (!impl->manager){
+ spa_log_error(impl->log, "Creating GDBus object manager failed");
+ goto fail;
+ }
+
+ if ((res = export_service(impl)) < 0)
+ goto fail;
+
+ if ((res = export_chr(impl)) < 0)
+ goto fail;
+
+ if ((res = export_dsc(impl)) < 0)
+ goto fail;
+
+ g_dbus_object_manager_server_set_connection(impl->manager, impl->conn);
+
+ return 0;
+
+fail:
+ res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
+
+ spa_log_error(impl->log, "Failed to register BLE MIDI services in DBus: %s",
+ spa_strerror(res));
+
+ g_clear_object(&impl->manager);
+
+ return res;
+}
+
+struct spa_bt_midi_server *spa_bt_midi_server_new(const struct spa_bt_midi_server_cb *cb,
+ GDBusConnection *conn, struct spa_log *log, void *user_data)
+{
+ static unsigned int server_id = 0;
+ struct impl *impl;
+ char path[128];
+ int res = 0;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto fail;
+
+ impl->server_id = server_id++;
+ impl->user_data = user_data;
+ impl->cb = cb;
+ impl->log = log;
+ impl->log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi.server");
+ impl->conn = conn;
+ spa_log_topic_init(impl->log, &impl->log_topic);
+
+ if ((res = export_objects(impl)) < 0)
+ goto fail;
+
+ monitor_start(impl);
+
+ g_object_ref(impl->conn);
+
+ spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id);
+ impl->this.chr_path = strdup(path);
+
+ return &impl->this;
+
+fail:
+ res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO);
+ free(impl);
+ errno = res;
+ return NULL;
+}
+
+void spa_bt_midi_server_destroy(struct spa_bt_midi_server *server)
+{
+ struct impl *impl = SPA_CONTAINER_OF(server, struct impl, this);
+
+ free((void *)impl->this.chr_path);
+ g_clear_object(&impl->chr);
+ dbus_monitor_clear(&impl->monitor);
+ g_clear_object(&impl->manager);
+ g_clear_object(&impl->conn);
+
+ free(impl);
+}
+
+void spa_bt_midi_server_released(struct spa_bt_midi_server *server, bool write)
+{
+ struct impl *impl = SPA_CONTAINER_OF(server, struct impl, this);
+
+ chr_change_acquired(impl, write, false);
+}
diff --git a/spa/plugins/bluez5/midi.h b/spa/plugins/bluez5/midi.h
new file mode 100644
index 0000000..fbf2702
--- /dev/null
+++ b/spa/plugins/bluez5/midi.h
@@ -0,0 +1,142 @@
+/* Spa V4l2 dbus
+ *
+ * Copyright © 2022 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef SPA_BT_MIDI_H_
+#define SPA_BT_MIDI_H_
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <gio/gio.h>
+
+#include <spa/utils/defs.h>
+#include <spa/support/log.h>
+
+#define BLUEZ_SERVICE "org.bluez"
+#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1"
+#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1"
+#define BLUEZ_GATT_MANAGER_INTERFACE BLUEZ_SERVICE ".GattManager1"
+#define BLUEZ_GATT_PROFILE_INTERFACE BLUEZ_SERVICE ".GattProfile1"
+#define BLUEZ_GATT_SERVICE_INTERFACE BLUEZ_SERVICE ".GattService1"
+#define BLUEZ_GATT_CHR_INTERFACE BLUEZ_SERVICE ".GattCharacteristic1"
+#define BLUEZ_GATT_DSC_INTERFACE BLUEZ_SERVICE ".GattDescriptor1"
+
+#define BT_MIDI_SERVICE_UUID "03b80e5a-ede8-4b33-a751-6ce34ec4c700"
+#define BT_MIDI_CHR_UUID "7772e5db-3868-4112-a1a9-f2669d106bf3"
+#define BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID "00002901-0000-1000-8000-00805f9b34fb"
+
+#define MIDI_BUF_SIZE 8192
+#define MIDI_MAX_MTU 8192
+
+#define MIDI_CLOCK_PERIOD_MSEC 0x2000
+#define MIDI_CLOCK_PERIOD_NSEC (MIDI_CLOCK_PERIOD_MSEC * SPA_NSEC_PER_MSEC)
+
+struct spa_bt_midi_server
+{
+ const char *chr_path;
+};
+
+struct spa_bt_midi_parser {
+ unsigned int size;
+ unsigned int sysex:1;
+ uint8_t buf[MIDI_BUF_SIZE];
+};
+
+struct spa_bt_midi_writer {
+ unsigned int size;
+ unsigned int mtu;
+ unsigned int pos;
+ uint8_t running_status;
+ uint64_t running_time_msec;
+ unsigned int flush:1;
+ uint8_t buf[MIDI_MAX_MTU];
+};
+
+struct spa_bt_midi_server_cb
+{
+ int (*acquire_notify)(void *user_data, int fd, uint16_t mtu);
+ int (*acquire_write)(void *user_data, int fd, uint16_t mtu);
+ int (*release)(void *user_data);
+ const char *(*get_description)(void *user_data);
+};
+
+static inline void spa_bt_midi_parser_init(struct spa_bt_midi_parser *parser)
+{
+ parser->size = 0;
+ parser->sysex = 0;
+}
+
+static inline void spa_bt_midi_parser_dup(struct spa_bt_midi_parser *src, struct spa_bt_midi_parser *dst, bool only_time)
+{
+ dst->size = src->size;
+ dst->sysex = src->sysex;
+ if (!only_time)
+ memcpy(dst->buf, src->buf, src->size);
+}
+
+/**
+ * Parse a single BLE MIDI data packet to normalized MIDI events.
+ */
+int spa_bt_midi_parser_parse(struct spa_bt_midi_parser *parser,
+ const uint8_t *src, size_t src_size,
+ bool only_time,
+ void (*event)(void *user_data, uint16_t time, uint8_t *event, size_t event_size),
+ void *user_data);
+
+static inline void spa_bt_midi_writer_init(struct spa_bt_midi_writer *writer, unsigned int mtu)
+{
+ writer->size = 0;
+ writer->mtu = SPA_MIN(mtu, (unsigned int)MIDI_MAX_MTU);
+ writer->pos = 0;
+ writer->running_status = 0;
+ writer->running_time_msec = 0;
+ writer->flush = 0;
+}
+
+/**
+ * Add a new event to midi writer buffer.
+ *
+ * spa_bt_midi_writer_init(&writer, mtu);
+ * for (time, event, size) in midi events {
+ * do {
+ * res = spa_bt_midi_writer_write(&writer, time, event, size);
+ * if (res < 0) {
+ * fail with error
+ * } else if (res) {
+ * send_packet(writer->buf, writer->size);
+ * }
+ * } while (res);
+ * }
+ * if (writer.size > 0)
+ * send_packet(writer->buf, writer->size);
+ */
+int spa_bt_midi_writer_write(struct spa_bt_midi_writer *writer,
+ uint64_t time, const uint8_t *event, size_t event_size);
+
+struct spa_bt_midi_server *spa_bt_midi_server_new(const struct spa_bt_midi_server_cb *cb,
+ GDBusConnection *conn, struct spa_log *log, void *user_data);
+void spa_bt_midi_server_released(struct spa_bt_midi_server *server, bool write);
+void spa_bt_midi_server_destroy(struct spa_bt_midi_server *server);
+
+#endif
diff --git a/spa/plugins/bluez5/modemmanager.c b/spa/plugins/bluez5/modemmanager.c
new file mode 100644
index 0000000..d9df95a
--- /dev/null
+++ b/spa/plugins/bluez5/modemmanager.c
@@ -0,0 +1,1249 @@
+/* Spa Bluez5 ModemManager proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <spa/utils/string.h>
+
+#include <ModemManager.h>
+
+#include "modemmanager.h"
+
+#define DBUS_INTERFACE_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager"
+
+struct modem {
+ char *path;
+ bool network_has_service;
+ unsigned int signal_strength;
+};
+
+struct impl {
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ DBusConnection *conn;
+
+ char *allowed_modem_device;
+ bool filters_added;
+ DBusPendingCall *pending;
+ DBusPendingCall *voice_pending;
+
+ const struct mm_ops *ops;
+ void *user_data;
+
+ struct modem modem;
+ struct spa_list call_list;
+};
+
+struct dbus_cmd_data {
+ struct impl *this;
+ struct call *call;
+ void *user_data;
+};
+
+static bool mm_dbus_connection_send_with_reply(struct impl *this, DBusMessage *m, DBusPendingCall **pending_return,
+ DBusPendingCallNotifyFunction function, void *user_data)
+{
+ dbus_bool_t dbus_ret;
+
+ spa_assert(*pending_return == NULL);
+
+ dbus_ret = dbus_connection_send_with_reply(this->conn, m, pending_return, -1);
+ if (!dbus_ret || *pending_return == NULL) {
+ spa_log_debug(this->log, "dbus call failure");
+ return false;
+ }
+
+ dbus_ret = dbus_pending_call_set_notify(*pending_return, function, user_data, NULL);
+ if (!dbus_ret) {
+ spa_log_debug(this->log, "dbus set notify failure");
+ dbus_pending_call_cancel(*pending_return);
+ dbus_pending_call_unref(*pending_return);
+ *pending_return = NULL;
+ return false;
+ }
+
+ return true;
+}
+
+static int mm_state_to_clcc(struct impl *this, MMCallState state)
+{
+ switch (state) {
+ case MM_CALL_STATE_DIALING:
+ return CLCC_DIALING;
+ case MM_CALL_STATE_RINGING_OUT:
+ return CLCC_ALERTING;
+ case MM_CALL_STATE_RINGING_IN:
+ return CLCC_INCOMING;
+ case MM_CALL_STATE_ACTIVE:
+ return CLCC_ACTIVE;
+ case MM_CALL_STATE_HELD:
+ return CLCC_HELD;
+ case MM_CALL_STATE_WAITING:
+ return CLCC_WAITING;
+ case MM_CALL_STATE_TERMINATED:
+ case MM_CALL_STATE_UNKNOWN:
+ default:
+ return -1;
+ }
+}
+
+static void mm_call_state_changed(struct impl *this)
+{
+ struct call *call;
+ bool call_indicator = false;
+ enum call_setup call_setup_indicator = CIND_CALLSETUP_NONE;
+
+ spa_list_for_each(call, &this->call_list, link) {
+ call_indicator |= (call->state == CLCC_ACTIVE);
+
+ if (call->state == CLCC_INCOMING && call_setup_indicator < CIND_CALLSETUP_INCOMING)
+ call_setup_indicator = CIND_CALLSETUP_INCOMING;
+ else if (call->state == CLCC_DIALING && call_setup_indicator < CIND_CALLSETUP_DIALING)
+ call_setup_indicator = CIND_CALLSETUP_DIALING;
+ else if (call->state == CLCC_ALERTING && call_setup_indicator < CIND_CALLSETUP_ALERTING)
+ call_setup_indicator = CIND_CALLSETUP_ALERTING;
+ }
+
+ if (this->ops->set_call_active)
+ this->ops->set_call_active(call_indicator, this->user_data);
+
+ if (this->ops->set_call_setup)
+ this->ops->set_call_setup(call_setup_indicator, this->user_data);
+}
+
+static void mm_get_call_properties_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct call *call = user_data;
+ struct impl *this = call->this;
+ DBusMessage *r;
+ DBusMessageIter arg_i, element_i;
+ MMCallDirection direction;
+ MMCallState state;
+
+ spa_assert(call->pending == pending);
+ dbus_pending_call_unref(pending);
+ call->pending = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(this->log, "ModemManager D-Bus Call not available");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "GetAll() failed: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &arg_i) || !spa_streq(dbus_message_get_signature(r), "a{sv}")) {
+ spa_log_error(this->log, "Invalid arguments in GetAll() reply");
+ goto finish;
+ }
+
+ spa_log_debug(this->log, "Call path: %s", call->path);
+
+ dbus_message_iter_recurse(&arg_i, &element_i);
+ while (dbus_message_iter_get_arg_type(&element_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&element_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if (spa_streq(key, MM_CALL_PROPERTY_DIRECTION)) {
+ dbus_message_iter_get_basic(&value_i, &direction);
+ spa_log_debug(this->log, "Call direction: %u", direction);
+ call->direction = (direction == MM_CALL_DIRECTION_INCOMING) ? CALL_INCOMING : CALL_OUTGOING;
+ } else if (spa_streq(key, MM_CALL_PROPERTY_NUMBER)) {
+ char *number;
+
+ dbus_message_iter_get_basic(&value_i, &number);
+ spa_log_debug(this->log, "Call number: %s", number);
+ if (call->number)
+ free(call->number);
+ call->number = strdup(number);
+ } else if (spa_streq(key, MM_CALL_PROPERTY_STATE)) {
+ int clcc_state;
+
+ dbus_message_iter_get_basic(&value_i, &state);
+ spa_log_debug(this->log, "Call state: %u", state);
+ clcc_state = mm_state_to_clcc(this, state);
+ if (clcc_state < 0) {
+ spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state);
+ } else {
+ call->state = clcc_state;
+ mm_call_state_changed(this);
+ }
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+
+finish:
+ dbus_message_unref(r);
+}
+
+static DBusHandlerResult mm_parse_voice_properties(struct impl *this, DBusMessageIter *props_i)
+{
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i, element_i;
+ const char *key;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if (spa_streq(key, MM_MODEM_VOICE_PROPERTY_CALLS)) {
+ spa_log_debug(this->log, "Voice properties");
+ dbus_message_iter_recurse(&value_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_OBJECT_PATH) {
+ const char *call_object;
+
+ dbus_message_iter_get_basic(&element_i, &call_object);
+ spa_log_debug(this->log, " Call: %s", call_object);
+
+ dbus_message_iter_next(&element_i);
+ }
+ }
+
+ dbus_message_iter_next(props_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_modem3gpp_properties(struct impl *this, DBusMessageIter *props_i)
+{
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORNAME)) {
+ char *operator_name;
+
+ dbus_message_iter_get_basic(&value_i, &operator_name);
+ spa_log_debug(this->log, "Network operator code: %s", operator_name);
+ if (this->ops->set_modem_operator_name)
+ this->ops->set_modem_operator_name(operator_name, this->user_data);
+ } else if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_REGISTRATIONSTATE)) {
+ MMModem3gppRegistrationState state;
+ bool is_roaming;
+
+ dbus_message_iter_get_basic(&value_i, &state);
+ spa_log_debug(this->log, "Registration state: %d", state);
+
+ if (state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING ||
+ state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED ||
+ state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY)
+ is_roaming = true;
+ else
+ is_roaming = false;
+
+ if (this->ops->set_modem_roaming)
+ this->ops->set_modem_roaming(is_roaming, this->user_data);
+ }
+
+ dbus_message_iter_next(props_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_modem_properties(struct impl *this, DBusMessageIter *props_i)
+{
+ while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if(spa_streq(key, MM_MODEM_PROPERTY_EQUIPMENTIDENTIFIER)) {
+ char *imei;
+
+ dbus_message_iter_get_basic(&value_i, &imei);
+ spa_log_debug(this->log, "Modem IMEI: %s", imei);
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_MANUFACTURER)) {
+ char *manufacturer;
+
+ dbus_message_iter_get_basic(&value_i, &manufacturer);
+ spa_log_debug(this->log, "Modem manufacturer: %s", manufacturer);
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_MODEL)) {
+ char *model;
+
+ dbus_message_iter_get_basic(&value_i, &model);
+ spa_log_debug(this->log, "Modem model: %s", model);
+ } else if (spa_streq(key, MM_MODEM_PROPERTY_OWNNUMBERS)) {
+ char *number;
+ DBusMessageIter array_i;
+
+ dbus_message_iter_recurse(&value_i, &array_i);
+ if (dbus_message_iter_get_arg_type(&array_i) == DBUS_TYPE_STRING) {
+ dbus_message_iter_get_basic(&array_i, &number);
+ spa_log_debug(this->log, "Modem own number: %s", number);
+ if (this->ops->set_modem_own_number)
+ this->ops->set_modem_own_number(number, this->user_data);
+ }
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_REVISION)) {
+ char *revision;
+
+ dbus_message_iter_get_basic(&value_i, &revision);
+ spa_log_debug(this->log, "Modem revision: %s", revision);
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_SIGNALQUALITY)) {
+ unsigned int percentage, signal_strength;
+ DBusMessageIter struct_i;
+
+ dbus_message_iter_recurse(&value_i, &struct_i);
+ if (dbus_message_iter_get_arg_type(&struct_i) == DBUS_TYPE_UINT32) {
+ dbus_message_iter_get_basic(&struct_i, &percentage);
+ signal_strength = (unsigned int) round(percentage / 20.0);
+ spa_log_debug(this->log, "Network signal strength: %d (%d)", percentage, signal_strength);
+ if(this->ops->set_modem_signal_strength)
+ this->ops->set_modem_signal_strength(signal_strength, this->user_data);
+ }
+ } else if(spa_streq(key, MM_MODEM_PROPERTY_STATE)) {
+ MMModemState state;
+ bool has_service;
+
+ dbus_message_iter_get_basic(&value_i, &state);
+ spa_log_debug(this->log, "Network state: %d", state);
+
+ has_service = (state >= MM_MODEM_STATE_REGISTERED);
+ if (this->ops->set_modem_service)
+ this->ops->set_modem_service(has_service, this->user_data);
+ }
+
+ dbus_message_iter_next(props_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult mm_parse_interfaces(struct impl *this, DBusMessageIter *dict_i)
+{
+ DBusMessageIter element_i, props_i;
+ const char *path;
+
+ spa_assert(this);
+ spa_assert(dict_i);
+
+ dbus_message_iter_get_basic(dict_i, &path);
+ dbus_message_iter_next(dict_i);
+ dbus_message_iter_recurse(dict_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) {
+ DBusMessageIter iface_i;
+ const char *interface;
+
+ dbus_message_iter_recurse(&element_i, &iface_i);
+ dbus_message_iter_get_basic(&iface_i, &interface);
+ dbus_message_iter_next(&iface_i);
+ spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+ dbus_message_iter_recurse(&iface_i, &props_i);
+
+ if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) {
+ spa_log_debug(this->log, "Found Modem interface %s, path %s", interface, path);
+ if (this->modem.path == NULL) {
+ if (this->allowed_modem_device) {
+ DBusMessageIter i;
+
+ dbus_message_iter_recurse(&iface_i, &i);
+ while (dbus_message_iter_get_arg_type(&i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter key_i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&i, &key_i);
+
+ dbus_message_iter_get_basic(&key_i, &key);
+ dbus_message_iter_next(&key_i);
+ dbus_message_iter_recurse(&key_i, &value_i);
+
+ if (spa_streq(key, MM_MODEM_PROPERTY_DEVICE)) {
+ char *device;
+
+ dbus_message_iter_get_basic(&value_i, &device);
+ if (!spa_streq(this->allowed_modem_device, device)) {
+ spa_log_debug(this->log, "Modem not allowed: %s", device);
+ goto next;
+ }
+ }
+ dbus_message_iter_next(&i);
+ }
+ }
+ this->modem.path = strdup(path);
+ } else if (!spa_streq(this->modem.path, path)) {
+ spa_log_debug(this->log, "A modem is already registered");
+ goto next;
+ }
+ mm_parse_modem_properties(this, &props_i);
+ } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+ if (spa_streq(this->modem.path, path)) {
+ spa_log_debug(this->log, "Found Modem3GPP interface %s, path %s", interface, path);
+ mm_parse_modem3gpp_properties(this, &props_i);
+ }
+ } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+ if (spa_streq(this->modem.path, path)) {
+ spa_log_debug(this->log, "Found Voice interface %s, path %s", interface, path);
+ mm_parse_voice_properties(this, &props_i);
+ }
+ }
+
+next:
+ dbus_message_iter_next(&element_i);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void mm_get_managed_objects_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *this = user_data;
+ DBusMessage *r;
+ DBusMessageIter i, array_i;
+
+ spa_assert(this->pending == pending);
+ dbus_pending_call_unref(pending);
+ this->pending = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "Failed to get a list of endpoints from ModemManager: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) {
+ spa_log_error(this->log, "Invalid arguments in GetManagedObjects() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &array_i);
+ while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter dict_i;
+
+ dbus_message_iter_recurse(&array_i, &dict_i);
+ mm_parse_interfaces(this, &dict_i);
+ dbus_message_iter_next(&array_i);
+ }
+
+finish:
+ dbus_message_unref(r);
+}
+
+static void call_free(struct call *call) {
+ spa_list_remove(&call->link);
+
+ if (call->pending != NULL) {
+ dbus_pending_call_cancel(call->pending);
+ dbus_pending_call_unref(call->pending);
+ }
+
+ if (call->number)
+ free(call->number);
+ if (call->path)
+ free(call->path);
+ free(call);
+}
+
+static void mm_clean_voice(struct impl *this)
+{
+ struct call *call;
+
+ spa_list_consume(call, &this->call_list, link)
+ call_free(call);
+
+ if (this->voice_pending != NULL) {
+ dbus_pending_call_cancel(this->voice_pending);
+ dbus_pending_call_unref(this->voice_pending);
+ }
+
+ if (this->ops->set_call_setup)
+ this->ops->set_call_setup(CIND_CALLSETUP_NONE, this->user_data);
+ if (this->ops->set_call_active)
+ this->ops->set_call_active(false, this->user_data);
+}
+
+static void mm_clean_modem3gpp(struct impl *this)
+{
+ if (this->ops->set_modem_operator_name)
+ this->ops->set_modem_operator_name(NULL, this->user_data);
+ if (this->ops->set_modem_roaming)
+ this->ops->set_modem_roaming(false, this->user_data);
+}
+
+static void mm_clean_modem(struct impl *this)
+{
+ if (this->modem.path) {
+ free(this->modem.path);
+ this->modem.path = NULL;
+ }
+ if(this->ops->set_modem_signal_strength)
+ this->ops->set_modem_signal_strength(0, this->user_data);
+ if (this->ops->set_modem_service)
+ this->ops->set_modem_service(false, this->user_data);
+ this->modem.network_has_service = false;
+}
+
+static DBusHandlerResult mm_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ struct impl *this = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+ const char *name, *old_owner, *new_owner;
+
+ spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m));
+
+ if (!dbus_message_get_args(m, &err,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old_owner,
+ DBUS_TYPE_STRING, &new_owner,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message);
+ goto finish;
+ }
+
+ if (spa_streq(name, MM_DBUS_SERVICE)) {
+ if (old_owner && *old_owner) {
+ spa_log_debug(this->log, "ModemManager daemon disappeared (%s)", old_owner);
+ mm_clean_voice(this);
+ mm_clean_modem3gpp(this);
+ mm_clean_modem(this);
+ }
+
+ if (new_owner && *new_owner)
+ spa_log_debug(this->log, "ModemManager daemon appeared (%s)", new_owner);
+ }
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_ADDED)) {
+ DBusMessageIter arg_i;
+
+ spa_log_warn(this->log, "sender: %s", dbus_message_get_sender(m));
+
+ if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) {
+ spa_log_error(this->log, "Invalid signature found in InterfacesAdded");
+ goto finish;
+ }
+
+ mm_parse_interfaces(this, &arg_i);
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_REMOVED)) {
+ const char *path;
+ DBusMessageIter arg_i, element_i;
+
+ if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oas")) {
+ spa_log_error(this->log, "Invalid signature found in InterfacesRemoved");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&arg_i, &path);
+ if (!spa_streq(this->modem.path, path))
+ goto finish;
+
+ dbus_message_iter_next(&arg_i);
+ dbus_message_iter_recurse(&arg_i, &element_i);
+
+ while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) {
+ const char *iface;
+
+ dbus_message_iter_get_basic(&element_i, &iface);
+
+ spa_log_debug(this->log, "Interface removed %s", path);
+ if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM)) {
+ spa_log_debug(this->log, "Modem interface %s removed, path %s", iface, path);
+ mm_clean_modem(this);
+ } else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+ spa_log_debug(this->log, "Modem3GPP interface %s removed, path %s", iface, path);
+ mm_clean_modem3gpp(this);
+ } else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+ spa_log_debug(this->log, "Voice interface %s removed, path %s", iface, path);
+ mm_clean_voice(this);
+ }
+
+ dbus_message_iter_next(&element_i);
+ }
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) {
+ const char *path;
+ DBusMessageIter iface_i, props_i;
+ const char *interface;
+
+ path = dbus_message_get_path(m);
+ if (!spa_streq(this->modem.path, path))
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) {
+ spa_log_error(this->log, "Invalid signature found in PropertiesChanged");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&iface_i, &interface);
+ dbus_message_iter_next(&iface_i);
+ spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+ dbus_message_iter_recurse(&iface_i, &props_i);
+
+ if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) {
+ spa_log_debug(this->log, "Properties changed on %s", path);
+ mm_parse_modem_properties(this, &props_i);
+ } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) {
+ spa_log_debug(this->log, "Properties changed on %s", path);
+ mm_parse_modem3gpp_properties(this, &props_i);
+ } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) {
+ spa_log_debug(this->log, "Properties changed on %s", path);
+ mm_parse_voice_properties(this, &props_i);
+ }
+ } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLADDED)) {
+ DBusMessageIter iface_i;
+ const char *path;
+ struct call *call_object;
+ const char *mm_call_interface = MM_DBUS_INTERFACE_CALL;
+
+ if (!spa_streq(this->modem.path, dbus_message_get_path(m)))
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) {
+ spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLADDED);
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&iface_i, &path);
+ spa_log_debug(this->log, "New call: %s", path);
+
+ call_object = calloc(1, sizeof(struct call));
+ if (call_object == NULL)
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ call_object->this = this;
+ call_object->path = strdup(path);
+ spa_list_append(&this->call_list, &call_object->link);
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "GetAll");
+ if (m == NULL)
+ goto finish;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &mm_call_interface, DBUS_TYPE_INVALID);
+ if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_properties_reply, call_object)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ goto finish;
+ }
+ } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLDELETED)) {
+ const char *path;
+ DBusMessageIter iface_i;
+ struct call *call, *call_tmp;
+
+ if (!spa_streq(this->modem.path, dbus_message_get_path(m)))
+ goto finish;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) {
+ spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLDELETED);
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&iface_i, &path);
+ spa_log_debug(this->log, "Call ended: %s", path);
+
+ spa_list_for_each_safe(call, call_tmp, &this->call_list, link) {
+ if (spa_streq(call->path, path))
+ call_free(call);
+ }
+ mm_call_state_changed(this);
+ } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_CALL, MM_CALL_SIGNAL_STATECHANGED)) {
+ const char *path;
+ DBusMessageIter iface_i;
+ MMCallState old, new;
+ MMCallStateReason reason;
+ struct call *call = NULL, *call_tmp;
+ int clcc_state;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "iiu")) {
+ spa_log_error(this->log, "Invalid signature found in %s", MM_CALL_SIGNAL_STATECHANGED);
+ goto finish;
+ }
+
+ path = dbus_message_get_path(m);
+
+ dbus_message_iter_get_basic(&iface_i, &old);
+ dbus_message_iter_next(&iface_i);
+ dbus_message_iter_get_basic(&iface_i, &new);
+ dbus_message_iter_next(&iface_i);
+ dbus_message_iter_get_basic(&iface_i, &reason);
+
+ spa_log_debug(this->log, "Call state %s changed to %d (old = %d, reason = %u)", path, new, old, reason);
+
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (spa_streq(call_tmp->path, path)) {
+ call = call_tmp;
+ break;
+ }
+ }
+
+ if (call == NULL) {
+ spa_log_warn(this->log, "No call reference for %s", path);
+ goto finish;
+ }
+
+ clcc_state = mm_state_to_clcc(this, new);
+ if (clcc_state < 0) {
+ spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state);
+ } else {
+ call->state = clcc_state;
+ mm_call_state_changed(this);
+ }
+ }
+
+finish:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(struct impl *this)
+{
+ DBusError err;
+
+ if (this->filters_added)
+ return 0;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(this->conn, mm_filter_cb, this, NULL)) {
+ spa_log_error(this->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" MM_DBUS_SERVICE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_ADDED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_REMOVED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLADDED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLDELETED "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" MM_DBUS_SERVICE "',"
+ "interface='" MM_DBUS_INTERFACE_CALL "',member='" MM_CALL_SIGNAL_STATECHANGED "'", &err);
+
+ this->filters_added = true;
+
+ return 0;
+
+fail:
+ dbus_error_free(&err);
+ return -EIO;
+}
+
+static bool is_dbus_service_available(struct impl *this, const char *service)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ bool success = false;
+
+ m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus",
+ "org.freedesktop.DBus", "NameHasOwner");
+ if (m == NULL)
+ return false;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_info(this->log, "NameHasOwner failed for %s", service);
+ dbus_error_free(&err);
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_get_args(r, &err,
+ DBUS_TYPE_BOOLEAN, &success,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message);
+ dbus_error_free(&err);
+ goto finish;
+ }
+
+finish:
+ if (r)
+ dbus_message_unref(r);
+
+ return success;
+}
+
+bool mm_is_available(void *modemmanager)
+{
+ struct impl *this = modemmanager;
+
+ if (this == NULL)
+ return false;
+
+ return this->modem.path != NULL;
+}
+
+unsigned int mm_supported_features()
+{
+ return SPA_BT_HFP_AG_FEATURE_REJECT_CALL | SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS;
+}
+
+static void mm_get_call_simple_reply(DBusPendingCall *pending, void *data)
+{
+ struct dbus_cmd_data *dbus_cmd_data = data;
+ struct impl *this = dbus_cmd_data->this;
+ struct call *call = dbus_cmd_data->call;
+ void *user_data = dbus_cmd_data->user_data;
+ DBusMessage *r;
+
+ free(data);
+
+ spa_assert(call->pending == pending);
+ dbus_pending_call_unref(pending);
+ call->pending = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(this->log, "ModemManager D-Bus method not available");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ this->ops->send_cmd_result(true, 0, user_data);
+ return;
+
+finish:
+ this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data);
+}
+
+static void mm_get_call_create_reply(DBusPendingCall *pending, void *data)
+{
+ struct dbus_cmd_data *dbus_cmd_data = data;
+ struct impl *this = dbus_cmd_data->this;
+ void *user_data = dbus_cmd_data->user_data;
+ DBusMessage *r;
+
+ free(data);
+
+ spa_assert(this->voice_pending == pending);
+ dbus_pending_call_unref(pending);
+ this->voice_pending = NULL;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) {
+ spa_log_warn(this->log, "ModemManager D-Bus method not available");
+ goto finish;
+ }
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ this->ops->send_cmd_result(true, 0, user_data);
+ return;
+
+finish:
+ this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data);
+}
+
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+ struct impl *this = modemmanager;
+ struct call *call_object, *call_tmp;
+ struct dbus_cmd_data *data;
+ DBusMessage *m;
+
+ call_object = NULL;
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_INCOMING) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ if (!call_object) {
+ spa_log_debug(this->log, "No ringing in call");
+ if (error)
+ *error = CMEE_OPERATION_NOT_ALLOWED;
+ return false;
+ }
+
+ data = malloc(sizeof(struct dbus_cmd_data));
+ if (!data) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ data->this = this;
+ data->call = call_object;
+ data->user_data = user_data;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_ACCEPT);
+ if (m == NULL) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+
+ return true;
+}
+
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+ struct impl *this = modemmanager;
+ struct call *call_object, *call_tmp;
+ struct dbus_cmd_data *data;
+ DBusMessage *m;
+
+ call_object = NULL;
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_ACTIVE) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ if (!call_object) {
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_DIALING ||
+ call_tmp->state == CLCC_ALERTING ||
+ call_tmp->state == CLCC_INCOMING) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ }
+ if (!call_object) {
+ spa_log_debug(this->log, "No call to reject or hang up");
+ if (error)
+ *error = CMEE_OPERATION_NOT_ALLOWED;
+ return false;
+ }
+
+ data = malloc(sizeof(struct dbus_cmd_data));
+ if (!data) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ data->this = this;
+ data->call = call_object;
+ data->user_data = user_data;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_HANGUP);
+ if (m == NULL) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+
+ return true;
+}
+
+static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) {
+ DBusMessageIter dict_entry_it, variant_it;
+ dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it);
+ dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key);
+
+ dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it);
+ dbus_message_iter_append_basic(&variant_it, variant_type_int, variant);
+ dbus_message_iter_close_container(&dict_entry_it, &variant_it);
+ dbus_message_iter_close_container(dict, &dict_entry_it);
+}
+
+static inline bool is_valid_dial_string_char(char c)
+{
+ return ('0' <= c && c <= '9')
+ || ('A' <= c && c <= 'C')
+ || c == '*'
+ || c == '#'
+ || c == '+';
+}
+
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error)
+{
+ struct impl *this = modemmanager;
+ struct dbus_cmd_data *data;
+ DBusMessage *m;
+ DBusMessageIter iter, dict;
+
+ for (size_t i = 0; number[i]; i++) {
+ if (!is_valid_dial_string_char(number[i])) {
+ spa_log_warn(this->log, "Call creation canceled, invalid character found in dial string: %c", number[i]);
+ if (error)
+ *error = CMEE_INVALID_CHARACTERS_DIAL_STRING;
+ return false;
+ }
+ }
+
+ data = malloc(sizeof(struct dbus_cmd_data));
+ if (!data) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ data->this = this;
+ data->user_data = user_data;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, this->modem.path, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_METHOD_CREATECALL);
+ if (m == NULL) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ dbus_message_iter_init_append(m, &iter);
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict);
+ append_basic_variant_dict_entry(&dict, "number", DBUS_TYPE_STRING, "s", &number);
+ dbus_message_iter_close_container(&iter, &dict);
+ if (!mm_dbus_connection_send_with_reply(this, m, &this->voice_pending, mm_get_call_create_reply, data)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+
+ return true;
+}
+
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error)
+{
+ struct impl *this = modemmanager;
+ struct call *call_object, *call_tmp;
+ struct dbus_cmd_data *data;
+ DBusMessage *m;
+
+ call_object = NULL;
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_ACTIVE) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ if (!call_object) {
+ spa_log_debug(this->log, "No active call");
+ if (error)
+ *error = CMEE_OPERATION_NOT_ALLOWED;
+ return false;
+ }
+
+ /* Allowed dtmf characters: 0-9, *, #, A-D */
+ if (!((dtmf[0] >= '0' && dtmf[0] <= '9')
+ || (dtmf[0] == '*')
+ || (dtmf[0] == '#')
+ || (dtmf[0] >= 'A' && dtmf[0] <= 'D'))) {
+ spa_log_debug(this->log, "Invalid DTMF character: %s", dtmf);
+ if (error)
+ *error = CMEE_INVALID_CHARACTERS_TEXT_STRING;
+ return false;
+ }
+
+ data = malloc(sizeof(struct dbus_cmd_data));
+ if (!data) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ data->this = this;
+ data->call = call_object;
+ data->user_data = user_data;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_SENDDTMF);
+ if (m == NULL) {
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &dtmf, DBUS_TYPE_INVALID);
+ if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ if (error)
+ *error = CMEE_AG_FAILURE;
+ return false;
+ }
+
+ return true;
+}
+
+const char *mm_get_incoming_call_number(void *modemmanager)
+{
+ struct impl *this = modemmanager;
+ struct call *call_object, *call_tmp;
+
+ call_object = NULL;
+ spa_list_for_each(call_tmp, &this->call_list, link) {
+ if (call_tmp->state == CLCC_INCOMING) {
+ call_object = call_tmp;
+ break;
+ }
+ }
+ if (!call_object) {
+ spa_log_debug(this->log, "No ringing in call");
+ return NULL;
+ }
+
+ return call_object->number;
+}
+
+struct spa_list *mm_get_calls(void *modemmanager)
+{
+ struct impl *this = modemmanager;
+
+ return &this->call_list;
+}
+
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+ const struct mm_ops *ops, void *user_data)
+{
+ struct impl *this;
+ const char *modem_device_str = NULL;
+ bool modem_device_found = false;
+
+ spa_assert(log);
+ spa_assert(dbus_connection);
+
+ if (info) {
+ if ((modem_device_str = spa_dict_lookup(info, "bluez5.hfphsp-backend-native-modem")) != NULL) {
+ if (!spa_streq(modem_device_str, "none"))
+ modem_device_found = true;
+ }
+ }
+ if (!modem_device_found) {
+ spa_log_info(log, "No modem allowed, doesn't link to ModemManager");
+ return NULL;
+ }
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL)
+ return NULL;
+
+ this->log = log;
+ this->conn = dbus_connection;
+ this->ops = ops;
+ this->user_data = user_data;
+ if (modem_device_str && !spa_streq(modem_device_str, "any"))
+ this->allowed_modem_device = strdup(modem_device_str);
+ spa_list_init(&this->call_list);
+
+ if (add_filters(this) < 0) {
+ goto fail;
+ }
+
+ if (is_dbus_service_available(this, MM_DBUS_SERVICE)) {
+ DBusMessage *m;
+
+ m = dbus_message_new_method_call(MM_DBUS_SERVICE, "/org/freedesktop/ModemManager1",
+ DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects");
+ if (m == NULL)
+ goto fail;
+
+ if (!mm_dbus_connection_send_with_reply(this, m, &this->pending, mm_get_managed_objects_reply, this)) {
+ spa_log_error(this->log, "dbus call failure");
+ dbus_message_unref(m);
+ goto fail;
+ }
+ }
+
+ return this;
+
+fail:
+ free(this);
+ return NULL;
+}
+
+void mm_unregister(void *data)
+{
+ struct impl *this = data;
+
+ if (this->pending != NULL) {
+ dbus_pending_call_cancel(this->pending);
+ dbus_pending_call_unref(this->pending);
+ }
+
+ mm_clean_voice(this);
+ mm_clean_modem3gpp(this);
+ mm_clean_modem(this);
+
+ if (this->filters_added) {
+ dbus_connection_remove_filter(this->conn, mm_filter_cb, this);
+ this->filters_added = false;
+ }
+
+ if (this->allowed_modem_device)
+ free(this->allowed_modem_device);
+
+ free(this);
+}
diff --git a/spa/plugins/bluez5/modemmanager.h b/spa/plugins/bluez5/modemmanager.h
new file mode 100644
index 0000000..a239b2a
--- /dev/null
+++ b/spa/plugins/bluez5/modemmanager.h
@@ -0,0 +1,161 @@
+/* Spa Bluez5 ModemManager proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_MODEMMANAGER_H_
+#define SPA_BLUEZ5_MODEMMANAGER_H_
+
+#include <spa/utils/list.h>
+
+#include "defs.h"
+
+enum cmee_error {
+ CMEE_AG_FAILURE = 0,
+ CMEE_NO_CONNECTION_TO_PHONE = 1,
+ CMEE_OPERATION_NOT_ALLOWED = 3,
+ CMEE_OPERATION_NOT_SUPPORTED = 4,
+ CMEE_INVALID_CHARACTERS_TEXT_STRING = 25,
+ CMEE_INVALID_CHARACTERS_DIAL_STRING = 27,
+ CMEE_NO_NETWORK_SERVICE = 30
+};
+
+enum call_setup {
+ CIND_CALLSETUP_NONE = 0,
+ CIND_CALLSETUP_INCOMING,
+ CIND_CALLSETUP_DIALING,
+ CIND_CALLSETUP_ALERTING
+};
+
+enum call_direction {
+ CALL_OUTGOING,
+ CALL_INCOMING
+};
+
+enum call_state {
+ CLCC_ACTIVE,
+ CLCC_HELD,
+ CLCC_DIALING,
+ CLCC_ALERTING,
+ CLCC_INCOMING,
+ CLCC_WAITING,
+ CLCC_RESPONSE_AND_HOLD
+};
+
+struct call {
+ struct spa_list link;
+ unsigned int index;
+ struct impl *this;
+ DBusPendingCall *pending;
+
+ char *path;
+ char *number;
+ bool call_indicator;
+ enum call_direction direction;
+ enum call_state state;
+ bool multiparty;
+};
+
+struct mm_ops {
+ void (*send_cmd_result)(bool success, enum cmee_error error, void *user_data);
+ void (*set_modem_service)(bool available, void *user_data);
+ void (*set_modem_signal_strength)(unsigned int strength, void *user_data);
+ void (*set_modem_operator_name)(const char *name, void *user_data);
+ void (*set_modem_own_number)(const char *number, void *user_data);
+ void (*set_modem_roaming)(bool is_roaming, void *user_data);
+ void (*set_call_active)(bool active, void *user_data);
+ void (*set_call_setup)(enum call_setup value, void *user_data);
+};
+
+#ifdef HAVE_BLUEZ_5_BACKEND_NATIVE_MM
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+ const struct mm_ops *ops, void *user_data);
+void mm_unregister(void *data);
+bool mm_is_available(void *modemmanager);
+unsigned int mm_supported_features();
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error);
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error);
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error);
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error);
+const char *mm_get_incoming_call_number(void *modemmanager);
+struct spa_list *mm_get_calls(void *modemmanager);
+#else
+void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info,
+ const struct mm_ops *ops, void *user_data)
+{
+ return NULL;
+}
+
+void mm_unregister(void *data)
+{
+}
+
+bool mm_is_available(void *modemmanager)
+{
+ return false;
+}
+
+unsigned int mm_supported_features(void)
+{
+ return 0;
+}
+
+bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+ if (error)
+ *error = CMEE_OPERATION_NOT_SUPPORTED;
+ return false;
+}
+
+bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error)
+{
+ if (error)
+ *error = CMEE_OPERATION_NOT_SUPPORTED;
+ return false;
+}
+
+bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error)
+{
+ if (error)
+ *error = CMEE_OPERATION_NOT_SUPPORTED;
+ return false;
+}
+
+bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error)
+{
+ if (error)
+ *error = CMEE_OPERATION_NOT_SUPPORTED;
+ return false;
+}
+
+const char *mm_get_incoming_call_number(void *modemmanager)
+{
+ return NULL;
+}
+
+struct spa_list *mm_get_calls(void *modemmanager)
+{
+ return NULL;
+}
+#endif
+
+#endif
diff --git a/spa/plugins/bluez5/org.bluez.xml b/spa/plugins/bluez5/org.bluez.xml
new file mode 100644
index 0000000..dee131e
--- /dev/null
+++ b/spa/plugins/bluez5/org.bluez.xml
@@ -0,0 +1,71 @@
+<node>
+ <interface name="org.bluez.Adapter1">
+ <method name="RegisterApplication">
+ <arg direction="in" type="o" name="path"/>
+ <arg direction="in" type="a{sv}" name="options"/>
+ </method>
+ </interface>
+
+ <interface name="org.bluez.Device1">
+ <property name="Adapter" type="o" access="read"/>
+ <property name="Connected" type="b" access="read"/>
+ <property name="ServicesResolved" type="b" access="read"/>
+ <property name="Name" type="s" access="read"/>
+ <property name="Alias" type="s" access="read"/>
+ <property name="Address" type="s" access="read"/>
+ <property name="Icon" type="s" access="read"/>
+ <property name="Class" type="u" access="read"/>
+ <property name="Appearance" type="q" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.GattManager1">
+ <method name="RegisterApplication">
+ <arg direction="in" type="o" name="path"/>
+ <arg direction="in" type="a{sv}" name="options"/>
+ </method>
+ </interface>
+
+ <interface name="org.bluez.GattProfile1">
+ <method name="Release">
+ </method>
+ <property name="UUIDs" type="as" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.GattService1">
+ <property name="UUID" type="s" access="read"/>
+ <property name="Primary" type="b" access="read"/>
+ <property name="Device" type="o" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.GattCharacteristic1">
+ <method name="ReadValue">
+ <arg direction="in" type="a{sv}" name="options"/>
+ <arg direction="out" type="ay" name="value"/>
+ </method>
+ <method name="AcquireNotify">
+ <arg direction="in" type="a{sv}" name="options"/>
+ <arg direction="out" type="h" name="fd"/>
+ <arg direction="out" type="q" name="mtu"/>
+ </method>
+ <method name="AcquireWrite">
+ <arg direction="in" type="a{sv}" name="options"/>
+ <arg direction="out" type="h" name="fd"/>
+ <arg direction="out" type="q" name="mtu"/>
+ </method>
+ <property name="UUID" type="s" access="read"/>
+ <property name="Service" type="o" access="read"/>
+ <property name="WriteAcquired" type="b" access="read"/>
+ <property name="NotifyAcquired" type="b" access="read"/>
+ <property name="Flags" type="as" access="read"/>
+ </interface>
+
+ <interface name="org.bluez.GattDescriptor1">
+ <method name="ReadValue">
+ <arg direction="in" type="a{sv}" name="options"/>
+ <arg direction="out" type="ay" name="value"/>
+ </method>
+ <property name="UUID" type="s" access="read"/>
+ <property name="Characteristic" type="o" access="read"/>
+ <property name="Flags" type="as" access="read"/>
+ </interface>
+</node>
diff --git a/spa/plugins/bluez5/player.c b/spa/plugins/bluez5/player.c
new file mode 100644
index 0000000..a77ca25
--- /dev/null
+++ b/spa/plugins/bluez5/player.c
@@ -0,0 +1,428 @@
+/* Spa Bluez5 AVRCP Player
+ *
+ * Copyright © 2021 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdbool.h>
+#include <dbus/dbus.h>
+
+#include <spa/utils/string.h>
+
+#include "defs.h"
+#include "player.h"
+
+#define PLAYER_OBJECT_PATH_BASE "/media_player"
+
+#define PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player"
+
+#define PLAYER_INTROSPECT_XML \
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
+ "<node>" \
+ " <interface name='" PLAYER_INTERFACE "'>" \
+ " <property name='PlaybackStatus' type='s' access='read'/>" \
+ " </interface>" \
+ " <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>" \
+ " <interface name='" DBUS_INTERFACE_INTROSPECTABLE "'>" \
+ " <method name='Introspect'>" \
+ " <arg name='xml' type='s' direction='out'/>" \
+ " </method>" \
+ " </interface>" \
+ "</node>"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.player");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define MAX_PROPERTIES 1
+
+struct impl {
+ struct spa_bt_player this;
+ DBusConnection *conn;
+ char *path;
+ struct spa_log *log;
+ struct spa_dict_item properties_items[MAX_PROPERTIES];
+ struct spa_dict properties;
+ unsigned int playing_count;
+};
+
+static size_t instance_counter = 0;
+
+static DBusMessage *properties_get(struct impl *impl, DBusMessage *m)
+{
+ const char *iface, *name;
+ size_t j;
+
+ if (!dbus_message_get_args(m, NULL,
+ DBUS_TYPE_STRING, &iface,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID))
+ return NULL;
+
+ if (!spa_streq(iface, PLAYER_INTERFACE))
+ return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
+ "No such interface");
+
+ for (j = 0; j < impl->properties.n_items; ++j) {
+ const struct spa_dict_item *item = &impl->properties.items[j];
+ if (spa_streq(item->key, name)) {
+ DBusMessage *r;
+ DBusMessageIter i, v;
+
+ 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,
+ "s", &v);
+ dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING,
+ &item->value);
+ dbus_message_iter_close_container(&i, &v);
+ return r;
+ }
+ }
+
+ return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
+ "No such property");
+}
+
+static void append_properties(struct impl *impl, DBusMessageIter *i)
+{
+ DBusMessageIter d, e, v;
+ size_t j;
+
+ dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY,
+ DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
+ DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
+ DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d);
+
+ for (j = 0; j < impl->properties.n_items; ++j) {
+ const struct spa_dict_item *item = &impl->properties.items[j];
+
+ spa_log_debug(impl->log, "player %s: %s=%s", impl->path,
+ item->key, item->value);
+
+ dbus_message_iter_open_container(&d, DBUS_TYPE_DICT_ENTRY, NULL, &e);
+ dbus_message_iter_append_basic(&e, DBUS_TYPE_STRING, &item->key);
+ dbus_message_iter_open_container(&e, DBUS_TYPE_VARIANT, "s", &v);
+ dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, &item->value);
+ dbus_message_iter_close_container(&e, &v);
+ dbus_message_iter_close_container(&d, &e);
+ }
+
+ dbus_message_iter_close_container(i, &d);
+}
+
+static DBusMessage *properties_get_all(struct impl *impl, DBusMessage *m)
+{
+ const char *iface, *name;
+ DBusMessage *r;
+ DBusMessageIter i;
+
+ if (!dbus_message_get_args(m, NULL,
+ DBUS_TYPE_STRING, &iface,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID))
+ return NULL;
+
+ if (!spa_streq(iface, PLAYER_INTERFACE))
+ return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
+ "No such interface");
+
+ r = dbus_message_new_method_return(m);
+ if (r == NULL)
+ return NULL;
+
+ dbus_message_iter_init_append(r, &i);
+ append_properties(impl, &i);
+ return r;
+}
+
+static DBusMessage *properties_set(struct impl *impl, DBusMessage *m)
+{
+ return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY,
+ "Property not writable");
+}
+
+static DBusMessage *introspect(struct impl *impl, DBusMessage *m)
+{
+ const char *xml = PLAYER_INTROSPECT_XML;
+ DBusMessage *r;
+ 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 r;
+}
+
+static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct impl *impl = userdata;
+ DBusMessage *r;
+
+ if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) {
+ r = introspect(impl, m);
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) {
+ r = properties_get(impl, m);
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) {
+ r = properties_get_all(impl, m);
+ } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) {
+ r = properties_set(impl, 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)) {
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+ dbus_message_unref(r);
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static int send_update_signal(struct impl *impl)
+{
+ DBusMessage *m;
+ const char *iface = PLAYER_INTERFACE;
+ DBusMessageIter i, a;
+ int res = 0;
+
+ m = dbus_message_new_signal(impl->path, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged");
+ if (m == NULL)
+ return -ENOMEM;
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &iface);
+
+ append_properties(impl, &i);
+
+ 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, m, NULL))
+ res = -EIO;
+
+ dbus_message_unref(m);
+
+ return res;
+}
+
+static void update_properties(struct impl *impl, bool send_signal)
+{
+ int nitems = 0;
+
+ switch (impl->this.state) {
+ case SPA_BT_PLAYER_PLAYING:
+ impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Playing");
+ break;
+ case SPA_BT_PLAYER_STOPPED:
+ impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Stopped");
+ break;
+ }
+ impl->properties = SPA_DICT_INIT(impl->properties_items, nitems);
+
+ if (!send_signal)
+ return;
+
+ send_update_signal(impl);
+}
+
+struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log)
+{
+ struct impl *impl;
+ const DBusObjectPathVTable vtable = {
+ .message_function = player_handler,
+ };
+
+ spa_log_topic_init(log, &log_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return NULL;
+
+ impl->this.state = SPA_BT_PLAYER_STOPPED;
+ impl->conn = dbus_connection;
+ impl->log = log;
+ impl->path = spa_aprintf("%s%zu", PLAYER_OBJECT_PATH_BASE, instance_counter++);
+ if (impl->path == NULL) {
+ free(impl);
+ return NULL;
+ }
+
+ dbus_connection_ref(impl->conn);
+
+ update_properties(impl, false);
+
+ if (!dbus_connection_register_object_path(impl->conn, impl->path, &vtable, impl)) {
+ spa_bt_player_destroy(&impl->this);
+ errno = EIO;
+ return NULL;
+ }
+
+ return &impl->this;
+}
+
+void spa_bt_player_destroy(struct spa_bt_player *player)
+{
+ struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
+
+ /*
+ * We unregister only the object path, but don't unregister it from
+ * BlueZ, to avoid hanging on BlueZ DBus activation. The assumption is
+ * that the DBus connection is terminated immediately after.
+ */
+ dbus_connection_unregister_object_path(impl->conn, impl->path);
+
+ dbus_connection_unref(impl->conn);
+ free(impl->path);
+ free(impl);
+}
+
+int spa_bt_player_set_state(struct spa_bt_player *player, enum spa_bt_player_state state)
+{
+ struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
+
+ switch (state) {
+ case SPA_BT_PLAYER_PLAYING:
+ if (impl->playing_count++ > 0)
+ return 0;
+ break;
+ case SPA_BT_PLAYER_STOPPED:
+ if (impl->playing_count == 0)
+ return -EINVAL;
+ if (--impl->playing_count > 0)
+ return 0;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ impl->this.state = state;
+ update_properties(impl, true);
+
+ return 0;
+}
+
+int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path)
+{
+ struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
+
+ DBusError err;
+ DBusMessageIter i;
+ DBusMessage *m, *r;
+ int res = 0;
+
+ spa_log_debug(impl->log, "RegisterPlayer() for dummy AVRCP player %s for %s",
+ impl->path, adapter_path);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
+ BLUEZ_MEDIA_INTERFACE, "RegisterPlayer");
+ if (m == NULL)
+ return -EIO;
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
+ append_properties(impl, &i);
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ spa_log_error(impl->log, "RegisterPlayer() failed (%s)", err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(impl->log, "RegisterPlayer() failed");
+ res = -EIO;
+ }
+
+ dbus_message_unref(r);
+
+ return res;
+}
+
+int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path)
+{
+ struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
+
+ DBusError err;
+ DBusMessageIter i;
+ DBusMessage *m, *r;
+ int res = 0;
+
+ spa_log_debug(impl->log, "UnregisterPlayer() for dummy AVRCP player %s for %s",
+ impl->path, adapter_path);
+
+ m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
+ BLUEZ_MEDIA_INTERFACE, "UnregisterPlayer");
+ if (m == NULL)
+ return -EIO;
+
+ dbus_message_iter_init_append(m, &i);
+ dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
+ dbus_message_unref(m);
+
+ if (r == NULL) {
+ spa_log_error(impl->log, "UnregisterPlayer() failed (%s)", err.message);
+ dbus_error_free(&err);
+ return -EIO;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(impl->log, "UnregisterPlayer() failed");
+ res = -EIO;
+ }
+
+ dbus_message_unref(r);
+
+ return res;
+}
diff --git a/spa/plugins/bluez5/player.h b/spa/plugins/bluez5/player.h
new file mode 100644
index 0000000..b50eb6b
--- /dev/null
+++ b/spa/plugins/bluez5/player.h
@@ -0,0 +1,51 @@
+/* Spa Bluez5 AVRCP Player
+ *
+ * Copyright © 2021 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_PLAYER_H_
+#define SPA_BLUEZ5_PLAYER_H_
+
+enum spa_bt_player_state {
+ SPA_BT_PLAYER_STOPPED,
+ SPA_BT_PLAYER_PLAYING,
+};
+
+/**
+ * Dummy AVRCP player.
+ *
+ * Some headsets require an AVRCP player to be present, before their
+ * AVRCP volume synchronization works. To work around this, we
+ * register a dummy player that does nothing.
+ */
+struct spa_bt_player {
+ enum spa_bt_player_state state;
+};
+
+struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log);
+void spa_bt_player_destroy(struct spa_bt_player *player);
+int spa_bt_player_set_state(struct spa_bt_player *player,
+ enum spa_bt_player_state state);
+int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path);
+int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path);
+
+#endif
diff --git a/spa/plugins/bluez5/plugin.c b/spa/plugins/bluez5/plugin.c
new file mode 100644
index 0000000..7b7f862
--- /dev/null
+++ b/spa/plugins/bluez5/plugin.c
@@ -0,0 +1,83 @@
+/* Spa Volume plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_bluez5_dbus_factory;
+extern const struct spa_handle_factory spa_bluez5_device_factory;
+extern const struct spa_handle_factory spa_media_sink_factory;
+extern const struct spa_handle_factory spa_media_source_factory;
+extern const struct spa_handle_factory spa_sco_sink_factory;
+extern const struct spa_handle_factory spa_sco_source_factory;
+extern const struct spa_handle_factory spa_a2dp_sink_factory;
+extern const struct spa_handle_factory spa_a2dp_source_factory;
+extern const struct spa_handle_factory spa_bluez5_midi_enum_factory;
+extern const struct spa_handle_factory spa_bluez5_midi_node_factory;
+
+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_bluez5_dbus_factory;
+ break;
+ case 1:
+ *factory = &spa_bluez5_device_factory;
+ break;
+ case 2:
+ *factory = &spa_media_sink_factory;
+ break;
+ case 3:
+ *factory = &spa_media_source_factory;
+ break;
+ case 4:
+ *factory = &spa_sco_sink_factory;
+ break;
+ case 5:
+ *factory = &spa_sco_source_factory;
+ break;
+ case 6:
+ *factory = &spa_a2dp_sink_factory;
+ break;
+ case 7:
+ *factory = &spa_a2dp_source_factory;
+ break;
+ case 8:
+ *factory = &spa_bluez5_midi_enum_factory;
+ break;
+ case 9:
+ *factory = &spa_bluez5_midi_node_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c
new file mode 100644
index 0000000..8a7f926
--- /dev/null
+++ b/spa/plugins/bluez5/quirks.c
@@ -0,0 +1,406 @@
+/* Device/adapter/kernel quirk table
+ *
+ * Copyright © 2021 Pauli Virtanen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <fcntl.h>
+#include <regex.h>
+#include <limits.h>
+#include <sys/utsname.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <bluetooth/bluetooth.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.quirks");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct spa_bt_quirks {
+ struct spa_log *log;
+
+ int force_msbc;
+ int force_hw_volume;
+ int force_sbc_xq;
+ int force_faststream;
+ int force_a2dp_duplex;
+
+ char *device_rules;
+ char *adapter_rules;
+ char *kernel_rules;
+};
+
+static enum spa_bt_feature parse_feature(const char *str)
+{
+ static const struct { const char *key; enum spa_bt_feature value; } feature_keys[] = {
+ { "msbc", SPA_BT_FEATURE_MSBC },
+ { "msbc-alt1", SPA_BT_FEATURE_MSBC_ALT1 },
+ { "msbc-alt1-rtl", SPA_BT_FEATURE_MSBC_ALT1_RTL },
+ { "hw-volume", SPA_BT_FEATURE_HW_VOLUME },
+ { "hw-volume-mic", SPA_BT_FEATURE_HW_VOLUME_MIC },
+ { "sbc-xq", SPA_BT_FEATURE_SBC_XQ },
+ { "faststream", SPA_BT_FEATURE_FASTSTREAM },
+ { "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX },
+ };
+ SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) {
+ if (spa_streq(str, f->key))
+ return f->value;
+ }
+ return 0;
+}
+
+static int do_match(const char *rules, struct spa_dict *dict, uint32_t *no_features)
+{
+ struct spa_json rules_json = SPA_JSON_INIT(rules, strlen(rules));
+ struct spa_json rules_arr, it[2];
+
+ if (spa_json_enter_array(&rules_json, &rules_arr) <= 0)
+ return 1;
+
+ while (spa_json_enter_object(&rules_arr, &it[0]) > 0) {
+ char key[256];
+ int match = true;
+ uint32_t no_features_cur = 0;
+
+ while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) {
+ char val[4096];
+ const char *str, *value;
+ int len;
+ bool success = false;
+
+ if (spa_streq(key, "no-features")) {
+ if (spa_json_enter_array(&it[0], &it[1]) > 0) {
+ 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 {
+ if (spa_json_parse_stringn(value, len, val, sizeof(val)) < 0)
+ continue;
+ value = val;
+ }
+
+ str = spa_dict_lookup(dict, key);
+ if (value == NULL) {
+ success = str == NULL;
+ } else if (str != NULL) {
+ if (value[0] == '~') {
+ regex_t r;
+ if (regcomp(&r, value+1, REG_EXTENDED | REG_NOSUB) == 0) {
+ if (regexec(&r, str, 0, NULL, 0) == 0)
+ success = true;
+ regfree(&r);
+ }
+ } else if (spa_streq(str, value)) {
+ success = true;
+ }
+ }
+
+ if (!success) {
+ match = false;
+ break;
+ }
+ }
+
+ if (match) {
+ *no_features = no_features_cur;
+ return 0;
+ }
+ }
+ return 0;
+}
+
+static int parse_force_flag(const struct spa_dict *info, const char *key)
+{
+ const char *str;
+ str = spa_dict_lookup(info, key);
+ if (str == NULL)
+ return -1;
+ else
+ return (strcmp(str, "true") == 0 || atoi(str)) ? 1 : 0;
+}
+
+static void load_quirks(struct spa_bt_quirks *this, const char *str, size_t len)
+{
+ struct spa_json data = SPA_JSON_INIT(str, len);
+ struct spa_json rules;
+ char key[1024];
+
+ 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;
+
+ if (!spa_json_is_container(value, sz))
+ continue;
+
+ sz = spa_json_container_len(&rules, value, sz);
+
+ if (spa_streq(key, "bluez5.features.kernel") && !this->kernel_rules)
+ this->kernel_rules = strndup(value, sz);
+ else if (spa_streq(key, "bluez5.features.adapter") && !this->adapter_rules)
+ this->adapter_rules = strndup(value, sz);
+ else if (spa_streq(key, "bluez5.features.device") && !this->device_rules)
+ this->device_rules = strndup(value, sz);
+ }
+}
+
+static int load_conf(struct spa_bt_quirks *this, const char *path)
+{
+ char *data;
+ struct stat sbuf;
+ int fd = -1;
+
+ spa_log_debug(this->log, "loading %s", path);
+
+ if ((fd = open(path, O_CLOEXEC | O_RDONLY)) < 0)
+ goto fail;
+ if (fstat(fd, &sbuf) < 0)
+ goto fail;
+ if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
+ goto fail;
+ close(fd);
+
+ load_quirks(this, data, sbuf.st_size);
+ munmap(data, sbuf.st_size);
+
+ return 0;
+
+fail:
+ if (fd >= 0)
+ close(fd);
+ return -errno;
+}
+
+struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct spa_log *log)
+{
+ struct spa_bt_quirks *this;
+ const char *str;
+
+ if (!info) {
+ errno = -EINVAL;
+ return NULL;
+ }
+
+ this = calloc(1, sizeof(struct spa_bt_quirks));
+ if (this == NULL)
+ return NULL;
+
+ this->log = log;
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ this->force_sbc_xq = parse_force_flag(info, "bluez5.enable-sbc-xq");
+ this->force_msbc = parse_force_flag(info, "bluez5.enable-msbc");
+ this->force_hw_volume = parse_force_flag(info, "bluez5.enable-hw-volume");
+ this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream");
+ this->force_a2dp_duplex = parse_force_flag(info, "bluez5.enable-a2dp-duplex");
+
+ if ((str = spa_dict_lookup(info, "bluez5.hardware-database")) != NULL) {
+ spa_log_debug(this->log, "loading session manager provided data");
+ load_quirks(this, str, strlen(str));
+ } else {
+ char path[PATH_MAX];
+ const char *dir = getenv("SPA_DATA_DIR");
+ int res;
+
+ if (dir == NULL)
+ dir = SPADATADIR;
+
+ if (spa_scnprintf(path, sizeof(path), "%s/bluez5/bluez-hardware.conf", dir) >= 0)
+ if ((res = load_conf(this, path)) < 0)
+ spa_log_warn(this->log, "failed to load '%s': %s", path,
+ spa_strerror(res));
+ }
+ if (!(this->kernel_rules && this->adapter_rules && this->device_rules))
+ spa_log_warn(this->log, "failed to load bluez-hardware.conf");
+
+ return this;
+}
+
+void spa_bt_quirks_destroy(struct spa_bt_quirks *this)
+{
+ free(this->kernel_rules);
+ free(this->adapter_rules);
+ free(this->device_rules);
+ free(this);
+}
+
+static void log_props(struct spa_log *log, const struct spa_dict *dict)
+{
+ const struct spa_dict_item *item;
+ spa_dict_for_each(item, dict)
+ spa_log_debug(log, "quirk property %s=%s", item->key, item->value);
+}
+
+static void strtolower(char *src, char *dst, int maxsize)
+{
+ while (maxsize > 1 && *src != '\0') {
+ *dst = (*src >= 'A' && *src <= 'Z') ? ('a' + (*src - 'A')) : *src;
+ ++src;
+ ++dst;
+ --maxsize;
+ }
+ if (maxsize > 0)
+ *dst = '\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)
+{
+ struct spa_dict props;
+ struct spa_dict_item items[5];
+ int res;
+
+ *features = ~(uint32_t)0;
+
+ /* Kernel */
+ if (this->kernel_rules) {
+ 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);
+ do_match(this->kernel_rules, &props, &no_features);
+ spa_log_debug(this->log, "kernel quirks:%08x", no_features);
+ *features &= ~no_features;
+ }
+
+ /* Adapter */
+ if (this->adapter_rules && adapter) {
+ uint32_t no_features = 0;
+ int nitems = 0;
+ char vendor_id[64], product_id[64], address[64];
+
+ if (spa_bt_format_vendor_product_id(
+ adapter->source_id, adapter->vendor_id, adapter->product_id,
+ vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) {
+ items[nitems++] = SPA_DICT_ITEM_INIT("vendor-id", vendor_id);
+ items[nitems++] = SPA_DICT_ITEM_INIT("product-id", product_id);
+ }
+ items[nitems++] = SPA_DICT_ITEM_INIT("bus-type",
+ (adapter->bus_type == BUS_TYPE_USB) ? "usb" : "other");
+ if (adapter->address) {
+ strtolower(adapter->address, address, sizeof(address));
+ items[nitems++] = SPA_DICT_ITEM_INIT("address", address);
+ }
+ props = SPA_DICT_INIT(items, nitems);
+ log_props(this->log, &props);
+ do_match(this->adapter_rules, &props, &no_features);
+ spa_log_debug(this->log, "adapter quirks:%08x", no_features);
+ *features &= ~no_features;
+ }
+
+ /* Device */
+ if (this->device_rules && device) {
+ uint32_t no_features = 0;
+ int nitems = 0;
+ char vendor_id[64], product_id[64], version_id[64], address[64];
+ if (spa_bt_format_vendor_product_id(
+ device->source_id, device->vendor_id, device->product_id,
+ vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) {
+ snprintf(version_id, sizeof(version_id), "%04x",
+ (unsigned int)device->version_id);
+ items[nitems++] = SPA_DICT_ITEM_INIT("vendor-id", vendor_id);
+ items[nitems++] = SPA_DICT_ITEM_INIT("product-id", product_id);
+ items[nitems++] = SPA_DICT_ITEM_INIT("version-id", version_id);
+ }
+ if (device->name)
+ items[nitems++] = SPA_DICT_ITEM_INIT("name", device->name);
+ if (device->address) {
+ strtolower(device->address, address, sizeof(address));
+ items[nitems++] = SPA_DICT_ITEM_INIT("address", address);
+ }
+ props = SPA_DICT_INIT(items, nitems);
+ log_props(this->log, &props);
+ do_match(this->device_rules, &props, &no_features);
+ spa_log_debug(this->log, "device quirks:%08x", no_features);
+ *features &= ~no_features;
+ }
+
+ /* Force flags */
+ if (this->force_msbc != -1) {
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC, this->force_msbc);
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC_ALT1, this->force_msbc);
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC_ALT1_RTL, this->force_msbc);
+ }
+
+ if (this->force_hw_volume != -1)
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_HW_VOLUME, this->force_hw_volume);
+
+ if (this->force_sbc_xq != -1)
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_SBC_XQ, this->force_sbc_xq);
+
+ if (this->force_faststream != -1)
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_FASTSTREAM, this->force_faststream);
+
+ if (this->force_a2dp_duplex != -1)
+ SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_A2DP_DUPLEX, this->force_a2dp_duplex);
+
+ return 0;
+}
diff --git a/spa/plugins/bluez5/rtp.h b/spa/plugins/bluez5/rtp.h
new file mode 100644
index 0000000..20694c1
--- /dev/null
+++ b/spa/plugins/bluez5/rtp.h
@@ -0,0 +1,74 @@
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org>
+ *
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+
+struct rtp_header {
+ unsigned cc:4;
+ unsigned x:1;
+ unsigned p:1;
+ unsigned v:2;
+
+ unsigned pt:7;
+ unsigned m:1;
+
+ uint16_t sequence_number;
+ uint32_t timestamp;
+ uint32_t ssrc;
+ uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_payload {
+ unsigned frame_count:4;
+ unsigned rfa0:1;
+ unsigned is_last_fragment:1;
+ unsigned is_first_fragment:1;
+ unsigned is_fragmented:1;
+} __attribute__ ((packed));
+
+#elif __BYTE_ORDER == __BIG_ENDIAN
+
+struct rtp_header {
+ unsigned v:2;
+ unsigned p:1;
+ unsigned x:1;
+ unsigned cc:4;
+
+ unsigned m:1;
+ unsigned pt:7;
+
+ uint16_t sequence_number;
+ uint32_t timestamp;
+ uint32_t ssrc;
+ uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_payload {
+ unsigned is_fragmented:1;
+ unsigned is_first_fragment:1;
+ unsigned is_last_fragment:1;
+ unsigned rfa0:1;
+ unsigned frame_count:4;
+} __attribute__ ((packed));
+
+#else
+#error "Unknown byte order"
+#endif
diff --git a/spa/plugins/bluez5/sco-io.c b/spa/plugins/bluez5/sco-io.c
new file mode 100644
index 0000000..0657750
--- /dev/null
+++ b/spa/plugins/bluez5/sco-io.c
@@ -0,0 +1,289 @@
+/* Spa SCO I/O
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include <sbc/sbc.h>
+
+#include "defs.h"
+
+
+/* We'll use the read rx data size to find the correct packet size for writing,
+ * since kernel might not report it as the socket MTU, see
+ * https://lore.kernel.org/linux-bluetooth/20201210003528.3pmaxvubiwegxmhl@pali/T/
+ *
+ * We continue reading also when there's no source connected, to keep socket
+ * flushed.
+ *
+ * XXX: when the kernel/backends start giving the right values, the heuristic
+ * XXX: can be removed
+ */
+#define MAX_MTU 1024
+
+
+struct spa_bt_sco_io {
+ bool started;
+
+ uint8_t read_buffer[MAX_MTU];
+ uint32_t read_size;
+
+ int fd;
+ uint16_t read_mtu;
+ uint16_t write_mtu;
+
+ struct spa_loop *data_loop;
+ struct spa_source source;
+
+ int (*source_cb)(void *userdata, uint8_t *data, int size);
+ void *source_userdata;
+
+ int (*sink_cb)(void *userdata);
+ void *sink_userdata;
+};
+
+
+static void update_source(struct spa_bt_sco_io *io)
+{
+ int enabled;
+ int changed = 0;
+
+ enabled = io->sink_cb != NULL;
+ if (SPA_FLAG_IS_SET(io->source.mask, SPA_IO_OUT) != enabled) {
+ SPA_FLAG_UPDATE(io->source.mask, SPA_IO_OUT, enabled);
+ changed = 1;
+ }
+
+ if (changed) {
+ spa_loop_update_source(io->data_loop, &io->source);
+ }
+}
+
+static void sco_io_on_ready(struct spa_source *source)
+{
+ struct spa_bt_sco_io *io = source->data;
+
+ if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_IN)) {
+ int res;
+
+ read_again:
+ res = read(io->fd, io->read_buffer, SPA_MIN(io->read_mtu, MAX_MTU));
+ if (res <= 0) {
+ if (errno == EINTR) {
+ /* retry if interrupted */
+ goto read_again;
+ } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ /* no data: try it next time */
+ goto read_done;
+ }
+
+ /* error */
+ goto stop;
+ }
+
+ io->read_size = res;
+
+ if (io->source_cb) {
+ int res;
+ res = io->source_cb(io->source_userdata, io->read_buffer, io->read_size);
+ if (res) {
+ io->source_cb = NULL;
+ }
+ }
+ }
+
+read_done:
+ if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_OUT)) {
+ if (io->sink_cb) {
+ int res;
+ res = io->sink_cb(io->sink_userdata);
+ if (res) {
+ io->sink_cb = NULL;
+ }
+ }
+ }
+
+ if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_ERR) || SPA_FLAG_IS_SET(source->rmask, SPA_IO_HUP)) {
+ goto stop;
+ }
+
+ /* Poll socket in/out only if necessary */
+ update_source(io);
+
+ return;
+
+stop:
+ if (io->source.loop) {
+ spa_loop_remove_source(io->data_loop, &io->source);
+ io->started = false;
+ }
+}
+
+/*
+ * Write data to socket in correctly sized blocks.
+ * Returns the number of bytes written, 0 when data cannot be written now or
+ * there is too little of it to write, and <0 on write error.
+ */
+int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *buf, int size)
+{
+ uint16_t packet_size;
+ uint8_t *buf_start = buf;
+
+ if (io->read_size == 0) {
+ /* The proper write packet size is not known yet */
+ return 0;
+ }
+
+ packet_size = SPA_MIN(io->write_mtu, io->read_size);
+
+ if (size < packet_size) {
+ return 0;
+ }
+
+ do {
+ int written;
+
+ written = write(io->fd, buf, packet_size);
+ if (written < 0) {
+ if (errno == EINTR) {
+ /* retry if interrupted */
+ continue;
+ } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ /* Don't continue writing */
+ break;
+ }
+ return -errno;
+ }
+
+ buf += written;
+ size -= written;
+ } while (size >= packet_size);
+
+ return buf - buf_start;
+}
+
+
+struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_loop *data_loop,
+ int fd,
+ uint16_t read_mtu,
+ uint16_t write_mtu)
+{
+ struct spa_bt_sco_io *io;
+
+ io = calloc(1, sizeof(struct spa_bt_sco_io));
+ if (io == NULL)
+ return io;
+
+ io->fd = fd;
+ io->read_mtu = read_mtu;
+ io->write_mtu = write_mtu;
+ io->data_loop = data_loop;
+
+ io->read_size = 0;
+
+ /* Add the ready callback */
+ io->source.data = io;
+ io->source.fd = io->fd;
+ io->source.func = sco_io_on_ready;
+ io->source.mask = SPA_IO_IN | SPA_IO_OUT | SPA_IO_ERR | SPA_IO_HUP;
+ io->source.rmask = 0;
+ spa_loop_add_source(io->data_loop, &io->source);
+
+ io->started = true;
+
+ return io;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct spa_bt_sco_io *io = user_data;
+
+ if (io->source.loop)
+ spa_loop_remove_source(io->data_loop, &io->source);
+
+ return 0;
+}
+
+void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io)
+{
+ if (io->started)
+ spa_loop_invoke(io->data_loop, do_remove_source, 0, NULL, 0, true, io);
+
+ io->started = false;
+ free(io);
+}
+
+/* Set source callback.
+ * This function should only be called from the data thread.
+ * Callback is called (in data loop) with data just read from the socket.
+ */
+void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *, uint8_t *, int), void *userdata)
+{
+ io->source_cb = source_cb;
+ io->source_userdata = userdata;
+
+ if (io->started) {
+ update_source(io);
+ }
+}
+
+/* Set sink callback.
+ * This function should only be called from the data thread.
+ * Callback is called (in data loop) when socket can be written to.
+ */
+void spa_bt_sco_io_set_sink_cb(struct spa_bt_sco_io *io, int (*sink_cb)(void *), void *userdata)
+{
+ io->sink_cb = sink_cb;
+ io->sink_userdata = userdata;
+
+ if (io->started) {
+ update_source(io);
+ }
+}
diff --git a/spa/plugins/bluez5/sco-sink.c b/spa/plugins/bluez5/sco-sink.c
new file mode 100644
index 0000000..18a34f6
--- /dev/null
+++ b/spa/plugins/bluez5/sco-sink.c
@@ -0,0 +1,1517 @@
+/* Spa SCO Sink
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.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/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include <sbc/sbc.h>
+
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.sco");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ char clock_name[64];
+};
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ unsigned int outstanding:1;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ struct spa_audio_info current_format;
+ int frame_size;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_io_rate_match *rate_match;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list ready;
+
+ struct buffer *current_buffer;
+ uint32_t ready_offset;
+ uint8_t write_buffer[4096];
+ uint32_t write_buffer_size;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ /* Support */
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ /* Hooks and callbacks */
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ /* Info */
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define N_NODE_PARAMS 2
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ uint32_t quantum_limit;
+
+ /* Transport */
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+
+ /* Port */
+ struct port port;
+
+ /* Flags */
+ unsigned int started:1;
+ unsigned int following:1;
+ unsigned int flush_pending:1;
+
+ /* Sources */
+ struct spa_source source;
+ struct spa_source flush_timer_source;
+
+ /* Timer */
+ int timerfd;
+ int flush_timerfd;
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t current_time;
+ uint64_t next_time;
+ uint64_t process_time;
+ uint64_t prev_flush_time;
+ uint64_t next_flush_time;
+
+ /* mSBC */
+ sbc_t msbc;
+ uint8_t *buffer;
+ uint8_t *buffer_head;
+ uint8_t *buffer_next;
+ int buffer_size;
+ int msbc_seq;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+
+static const char sntable[4] = { 0x08, 0x38, 0xC8, 0xF8 };
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+}
+
+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_PropInfo:
+ {
+ switch (result.index) {
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id);
+ 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 set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ set_timers(this);
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(&new_props);
+ } else {
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL);
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ if (apply_props(this, param) > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static void enable_flush_timer(struct impl *this, bool enabled)
+{
+ struct itimerspec ts;
+
+ if (!enabled)
+ this->next_flush_time = 0;
+
+ ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system,
+ this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+
+ this->flush_pending = enabled;
+}
+
+static uint32_t get_queued_frames(struct impl *this)
+{
+ struct port *port = &this->port;
+ uint32_t bytes = 0;
+ struct buffer *b;
+
+ spa_list_for_each(b, &port->ready, link) {
+ struct spa_data *d = b->buf->datas;
+
+ bytes += d[0].chunk->size;
+ }
+
+ if (bytes > port->ready_offset)
+ bytes -= port->ready_offset;
+ else
+ bytes = 0;
+
+ return bytes / port->frame_size;
+}
+
+static void flush_data(struct impl *this)
+{
+ struct port *port = &this->port;
+ const uint32_t min_in_size =
+ (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ?
+ MSBC_DECODED_SIZE : this->transport->write_mtu;
+ uint8_t * const packet =
+ (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ?
+ this->buffer_head : port->write_buffer;
+ const uint32_t packet_samples = min_in_size / port->frame_size;
+ const uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC
+ / port->current_format.info.raw.rate;
+ int processed = 0;
+ int written;
+
+ if (this->transport == NULL || this->transport->sco_io == NULL)
+ return;
+
+ while (!spa_list_is_empty(&port->ready) && port->write_buffer_size < min_in_size) {
+ struct spa_data *datas;
+
+ /* get buffer */
+ if (!port->current_buffer) {
+ spa_return_if_fail(!spa_list_is_empty(&port->ready));
+ port->current_buffer = spa_list_first(&port->ready, struct buffer, link);
+ port->ready_offset = 0;
+ }
+ datas = port->current_buffer->buf->datas;
+
+ /* if buffer has data, copy it into the write buffer */
+ if (datas[0].chunk->size - port->ready_offset > 0) {
+ const uint32_t avail =
+ SPA_MIN(min_in_size, datas[0].chunk->size - port->ready_offset);
+ const uint32_t size =
+ (avail + port->write_buffer_size) > min_in_size ?
+ min_in_size - port->write_buffer_size : avail;
+ memcpy(port->write_buffer + port->write_buffer_size,
+ (uint8_t *)datas[0].data + port->ready_offset,
+ size);
+ port->write_buffer_size += size;
+ port->ready_offset += size;
+ } else {
+ struct buffer *b;
+
+ b = port->current_buffer;
+ port->current_buffer = NULL;
+
+ /* reuse buffer */
+ spa_list_remove(&b->link);
+ b->outstanding = true;
+ spa_log_trace(this->log, "sco-sink %p: reuse buffer %u", this, b->id);
+ port->io->buffer_id = b->id;
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ }
+ }
+
+ if (this->flush_pending) {
+ spa_log_trace(this->log, "%p: wait for flush timer", this);
+ return;
+ }
+
+ if (port->write_buffer_size < min_in_size) {
+ /* wait for more data */
+ spa_log_trace(this->log, "%p: skip flush", this);
+ enable_flush_timer(this, false);
+ return;
+ }
+
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ ssize_t out_encoded;
+
+ /* Encode */
+ if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) {
+ /* Buffer overrun; shouldn't usually happen. Drop data and reset. */
+ this->buffer_head = this->buffer_next = this->buffer;
+ spa_log_warn(this->log, "sco-sink: mSBC buffer overrun, dropping data");
+ }
+ this->buffer_next[0] = 0x01;
+ this->buffer_next[1] = sntable[this->msbc_seq % 4];
+ this->buffer_next[59] = 0x00;
+ this->msbc_seq = (this->msbc_seq + 1) % 4;
+ processed = sbc_encode(&this->msbc, port->write_buffer, port->write_buffer_size,
+ this->buffer_next + 2, MSBC_ENCODED_SIZE - 3, &out_encoded);
+ if (processed < 0) {
+ spa_log_warn(this->log, "sbc_encode failed: %d", processed);
+ return;
+ }
+ this->buffer_next += out_encoded + 3;
+ port->write_buffer_size = 0;
+
+ /* Write */
+ written = spa_bt_sco_io_write(this->transport->sco_io, packet,
+ this->buffer_next - this->buffer_head);
+ if (written < 0) {
+ spa_log_warn(this->log, "failed to write data: %d (%s)",
+ written, spa_strerror(written));
+ goto stop;
+ }
+
+ this->buffer_head += written;
+
+ if (this->buffer_head == this->buffer_next)
+ this->buffer_head = this->buffer_next = this->buffer;
+ else if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) {
+ /* Written bytes is not necessarily commensurate
+ * with MSBC_ENCODED_SIZE. If this occurs, copy data.
+ */
+ int size = this->buffer_next - this->buffer_head;
+ spa_memmove(this->buffer, this->buffer_head, size);
+ this->buffer_next = this->buffer + size;
+ this->buffer_head = this->buffer;
+ }
+ } else {
+ written = spa_bt_sco_io_write(this->transport->sco_io, packet,
+ port->write_buffer_size);
+ if (written < 0) {
+ spa_log_warn(this->log, "sco-sink: write failure: %d (%s)",
+ written, spa_strerror(written));
+ goto stop;
+ } else if (written == 0) {
+ /* EAGAIN or similar, just skip ahead */
+ written = SPA_MIN(port->write_buffer_size, (uint32_t)48);
+ }
+
+ processed = written;
+ port->write_buffer_size -= written;
+
+ if (port->write_buffer_size > 0 && written > 0) {
+ spa_memmove(port->write_buffer, port->write_buffer + written, port->write_buffer_size);
+ }
+ }
+
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
+ struct timespec ts;
+ uint64_t now;
+ uint64_t dt;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ dt = now - this->prev_flush_time;
+ this->prev_flush_time = now;
+
+ spa_log_trace(this->log,
+ "%p: send wrote:%d dt:%"PRIu64,
+ this, written, dt);
+ }
+
+ spa_log_trace(this->log, "write socket data %d", written);
+
+ if (SPA_LIKELY(this->position)) {
+ uint32_t frames = get_queued_frames(this);
+ uint64_t duration_ns;
+
+ /*
+ * Flush at the time position of the next buffered sample.
+ */
+ duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC
+ / this->position->clock.rate.denom);
+ this->next_flush_time = this->process_time + duration_ns
+ - ((uint64_t)frames * SPA_NSEC_PER_SEC
+ / port->current_format.info.raw.rate);
+
+ /*
+ * We could delay the output by one packet to avoid waiting
+ * for the next buffer and so make send intervals more regular.
+ * However, this appears not needed in practice, and it's better
+ * to not add latency if not needed.
+ */
+#if 0
+ this->next_flush_time += SPA_MIN(packet_time,
+ duration_ns * (port->n_buffers - 1));
+#endif
+ } else {
+ if (this->next_flush_time == 0)
+ this->next_flush_time = this->process_time;
+ this->next_flush_time += packet_time;
+ }
+
+ enable_flush_timer(this, true);
+ return;
+
+stop:
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+ enable_flush_timer(this, false);
+}
+
+static void sco_on_flush_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t exp;
+ int res;
+
+ spa_log_trace(this->log, "%p: flush on timeout", this);
+
+ if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+
+ if (this->transport == NULL) {
+ enable_flush_timer(this, false);
+ return;
+ }
+
+ while (exp-- > 0) {
+ this->flush_pending = false;
+ flush_data(this);
+ }
+}
+
+static void sco_on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ uint64_t exp, duration;
+ uint32_t rate;
+ struct spa_io_buffers *io = port->io;
+ uint64_t prev_time, now_time;
+ int res;
+
+ if (this->transport == NULL)
+ return;
+
+ if (this->started) {
+ if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res));
+ return;
+ }
+ }
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_debug(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ this->next_time = now_time + duration * SPA_NSEC_PER_SEC / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = now_time;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->rate_diff = 1.0f;
+ this->clock->next_nsec = this->next_time;
+ this->clock->delay = 0;
+ }
+
+ spa_log_trace(this->log, "%p: %d", this, io->status);
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+/* greater common divider */
+static int gcd(int a, int b) {
+ while(b) {
+ int c = b;
+ b = a % b;
+ a = c;
+ }
+ return a;
+}
+/* least common multiple */
+static int lcm(int a, int b) {
+ return (a*b)/gcd(a,b);
+}
+
+static int do_start(struct impl *this)
+{
+ bool do_accept;
+ int res;
+
+ /* Don't do anything if the node has already started */
+ if (this->started)
+ return 0;
+
+ /* Make sure the transport is valid */
+ spa_return_val_if_fail(this->transport != NULL, -EIO);
+
+ this->following = is_following(this);
+
+ spa_log_debug(this->log, "%p: start following:%d", this, this->following);
+
+ /* Do accept if Gateway; otherwise do connect for Head Unit */
+ do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY;
+
+ /* acquire the socket fd (false -> connect | true -> accept) */
+ if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0)
+ return res;
+
+ /* Init mSBC if needed */
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ sbc_init_msbc(&this->msbc, 0);
+ /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */
+ this->msbc.endian = SBC_LE;
+
+ /* write_mtu might not be correct at this point, so we'll throw
+ * in some common ones, at the cost of a potentially larger
+ * allocation (size <= 120 * write_mtu). If it still fails to be
+ * commensurate, we may end up doing memmoves, but nothing worse
+ * is going to happen.
+ */
+ this->buffer_size = lcm(24, lcm(60, lcm(this->transport->write_mtu, 2 * MSBC_ENCODED_SIZE)));
+ this->buffer = calloc(this->buffer_size, sizeof(uint8_t));
+ this->buffer_head = this->buffer_next = this->buffer;
+ if (this->buffer == NULL) {
+ res = -errno;
+ goto fail;
+ }
+ }
+
+ spa_return_val_if_fail(this->transport->write_mtu <= sizeof(this->port.write_buffer), -EINVAL);
+
+ /* start socket i/o */
+ if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0)
+ goto fail;
+
+ /* Add the timeout callback */
+ this->source.data = this;
+ this->source.fd = this->timerfd;
+ this->source.func = sco_on_timeout;
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->source);
+
+ this->flush_timer_source.data = this;
+ this->flush_timer_source.fd = this->flush_timerfd;
+ this->flush_timer_source.func = sco_on_flush_timeout;
+ this->flush_timer_source.mask = SPA_IO_IN;
+ this->flush_timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->flush_timer_source);
+
+ /* start processing */
+ this->flush_pending = false;
+ set_timers(this);
+
+ /* Set the started flag */
+ this->started = true;
+
+ return 0;
+
+fail:
+ free(this->buffer);
+ this->buffer = NULL;
+ spa_bt_transport_release(this->transport);
+ return res;
+}
+
+/* Drop any buffered data remaining in the port */
+static void drop_port_output(struct impl *this)
+{
+ struct port *port = &this->port;
+
+ port->write_buffer_size = 0;
+ port->current_buffer = NULL;
+ port->ready_offset = 0;
+
+ while (!spa_list_is_empty(&port->ready)) {
+ struct buffer *b;
+ b = spa_list_first(&port->ready, struct buffer, link);
+
+ spa_list_remove(&b->link);
+ b->outstanding = true;
+ port->io->buffer_id = b->id;
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ }
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ set_timeout(this, 0);
+ if (this->source.loop)
+ spa_loop_remove_source(this->data_loop, &this->source);
+
+ if (this->flush_timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->flush_timer_source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->flush_timerfd, 0, &ts, NULL);
+
+ /* Drop buffered data in the ready queue. Ideally there shouldn't be any. */
+ drop_port_output(this);
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ int res = 0;
+
+ if (!this->started)
+ return 0;
+
+ spa_log_trace(this->log, "sco-sink %p: stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ this->started = false;
+
+ if (this->buffer) {
+ free(this->buffer);
+ this->buffer = NULL;
+ this->buffer_head = this->buffer_next = this->buffer;
+ }
+
+ if (this->transport) {
+ /* Release the transport; it is responsible for closing the fd */
+ res = spa_bt_transport_release(this->transport);
+ }
+
+ return res;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ static const struct spa_dict_item hu_node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Audio/Sink" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+ };
+
+ const struct spa_dict_item ag_node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Stream/Input/Audio" },
+ { "media.name", ((this->transport && this->transport->device->name) ?
+ this->transport->device->name : "HSP/HFP") },
+ { SPA_KEY_MEDIA_ROLE, "Communication" },
+ };
+ bool is_ag = this->transport &&
+ (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = is_ag ?
+ &SPA_DICT_INIT_ARRAY(ag_node_info_items) :
+ &SPA_DICT_INIT_ARRAY(hu_node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ 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);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ 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 (result.index > 0)
+ return 0;
+ if (this->transport == NULL)
+ return -EIO;
+
+ /* set the info structure */
+ struct spa_audio_info_raw info = { 0, };
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ info.channels = 1;
+ info.position[0] = SPA_AUDIO_CHANNEL_MONO;
+
+ /* CVSD format has a rate of 8kHz
+ * MSBC format has a rate of 16kHz */
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC)
+ info.rate = 16000;
+ else
+ info.rate = 8000;
+
+ /* build the param */
+ param = spa_format_audio_raw_build(&b, id, &info);
+
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->frame_size,
+ 16 * port->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ do_stop(this);
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ spa_log_debug(this->log, "clear format");
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.format != SPA_AUDIO_FORMAT_S16_LE ||
+ info.info.raw.rate == 0 ||
+ info.info.raw.channels != 1)
+ return -EINVAL;
+
+ port->frame_size = info.info.raw.channels * 2;
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ 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);
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ } 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ spa_log_debug(this->log, "use buffers %d", n_buffers);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->outstanding = true;
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (buffers[i]->datas[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ port->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
+ io->status = SPA_STATUS_NEED_DATA;
+ return SPA_STATUS_HAVE_DATA;
+ }
+
+ if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) {
+ struct buffer *b = &port->buffers[io->buffer_id];
+
+ if (!b->outstanding) {
+ spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+
+ spa_log_trace(this->log, "%p: queue buffer %u", this, io->buffer_id);
+
+ spa_list_append(&port->ready, &b->link);
+ b->outstanding = false;
+ io->buffer_id = SPA_ID_INVALID;
+ io->status = SPA_STATUS_OK;
+ }
+
+ if (this->following) {
+ if (this->position) {
+ this->current_time = this->position->clock.nsec;
+ } else {
+ struct timespec now;
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->current_time = SPA_TIMESPEC_TO_NSEC(&now);
+ }
+ }
+
+ this->process_time = this->current_time;
+
+ if (!spa_list_is_empty(&port->ready)) {
+ spa_log_trace(this->log, "%p: flush on process", this);
+ flush_data(this);
+ }
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int do_transport_destroy(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ this->transport = NULL;
+ return 0;
+}
+
+static void transport_destroy(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p destroy", this->transport);
+ spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this);
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = transport_destroy,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ do_stop(this);
+ if (this->transport)
+ spa_hook_remove(&this->transport_listener);
+ spa_system_close(this->data_system, this->timerfd);
+ spa_system_close(this->data_system, this->flush_timerfd);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+
+ 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);
+ 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);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS |
+ SPA_NODE_CHANGE_MASK_PROPS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 0;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ 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;
+
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = 0;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ spa_list_init(&port->ready);
+
+ this->quantum_limit = 8192;
+
+ if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+ spa_atou32(str, &this->quantum_limit, 0);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)))
+ sscanf(str, "pointer:%p", &this->transport);
+
+ if (this->transport == NULL) {
+ spa_log_error(this->log, "a transport is needed");
+ return -EINVAL;
+ }
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &transport_events, this);
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ this->flush_timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ 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, "Collabora Ltd. <contact@collabora.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play bluetooth audio with hsp/hfp" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_sco_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_SCO_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/sco-source.c b/spa/plugins/bluez5/sco-source.c
new file mode 100644
index 0000000..6f5fe08
--- /dev/null
+++ b/spa/plugins/bluez5/sco-source.c
@@ -0,0 +1,1592 @@
+/* Spa SCO Source
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <time.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/support/log.h>
+#include <spa/support/system.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/monitor/device.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include <sbc/sbc.h>
+
+#include "defs.h"
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.sco");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#include "decode-buffer.h"
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ char clock_name[64];
+};
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ unsigned int outstanding:1;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ struct spa_audio_info current_format;
+ int frame_size;
+ unsigned int have_format:1;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_io_rate_match *rate_match;
+ struct spa_latency_info latency;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ struct spa_bt_decode_buffer buffer;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint32_t quantum_limit;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define IDX_NODE_IO 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_bt_transport *transport;
+ struct spa_hook transport_listener;
+
+ struct port port;
+
+ unsigned int started:1;
+ unsigned int following:1;
+ unsigned int matching:1;
+ unsigned int resampling:1;
+
+ struct spa_source timer_source;
+ int timerfd;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t current_time;
+ uint64_t next_time;
+
+ /* mSBC */
+ sbc_t msbc;
+ bool msbc_seq_initialized;
+ uint8_t msbc_seq;
+
+ /* mSBC frame parsing */
+ uint8_t msbc_buffer[MSBC_ENCODED_SIZE];
+ uint8_t msbc_buffer_pos;
+
+ struct timespec now;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+}
+
+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_PropInfo:
+ {
+ switch (result.index) {
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ 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 set_timeout(struct impl *this, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return spa_system_timerfd_settime(this->data_system,
+ this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ return set_timeout(this, this->following ? 0 : this->next_time);
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct port *port = &this->port;
+
+ set_timers(this);
+ spa_bt_decode_buffer_recover(&port->buffer);
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ bool following;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ following = is_following(this);
+ if (this->started && following != this->following) {
+ spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full);
+
+static int apply_props(struct impl *this, const struct spa_pod *param)
+{
+ struct props new_props = this->props;
+ int changed = 0;
+
+ if (param == NULL) {
+ reset_props(&new_props);
+ } else {
+ /* noop */
+ }
+
+ changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0);
+ this->props = new_props;
+ return changed;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ if (apply_props(this, param) > 0) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_node_info(this, false);
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static void reset_buffers(struct port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+}
+
+static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (b->outstanding) {
+ spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+}
+
+/* Append data to msbc buffer, syncing buffer start to frame headers */
+static void msbc_buffer_append_byte(struct impl *this, uint8_t byte)
+{
+ /* Parse mSBC frame header */
+ if (this->msbc_buffer_pos == 0) {
+ if (byte != 0x01) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos == 1) {
+ if (!((byte & 0x0F) == 0x08 &&
+ ((byte >> 4) & 1) == ((byte >> 5) & 1) &&
+ ((byte >> 6) & 1) == ((byte >> 7) & 1))) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos == 2) {
+ /* .. and beginning of MSBC frame: SYNCWORD + 2 nul bytes */
+ if (byte != 0xAD) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos == 3) {
+ if (byte != 0x00) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos == 4) {
+ if (byte != 0x00) {
+ this->msbc_buffer_pos = 0;
+ return;
+ }
+ }
+ else if (this->msbc_buffer_pos >= MSBC_ENCODED_SIZE) {
+ /* Packet completed. Reset. */
+ this->msbc_buffer_pos = 0;
+ msbc_buffer_append_byte(this, byte);
+ return;
+ }
+ this->msbc_buffer[this->msbc_buffer_pos] = byte;
+ ++this->msbc_buffer_pos;
+}
+
+/* Helper function for debugging */
+static SPA_UNUSED void hexdump_to_log(struct impl *this, uint8_t *data, size_t size)
+{
+ char buf[2048];
+ size_t i, col = 0, pos = 0;
+ buf[0] = '\0';
+ for (i = 0; i < size; ++i) {
+ int res;
+ res = spa_scnprintf(buf + pos, sizeof(buf) - pos, "%s%02x",
+ (col == 0) ? "\n\t" : " ", data[i]);
+ if (res < 0)
+ break;
+ pos += res;
+ col = (col + 1) % 16;
+ }
+ spa_log_trace(this->log, "hexdump (%d bytes):%s", (int)size, buf);
+}
+
+/* helper function to detect if a packet consists only of zeros */
+static bool is_zero_packet(uint8_t *data, int size)
+{
+ for (int i = 0; i < size; ++i) {
+ if (data[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static uint32_t preprocess_and_decode_msbc_data(void *userdata, uint8_t *read_data, int size_read)
+{
+ struct impl *this = userdata;
+ struct port *port = &this->port;
+ uint32_t decoded = 0;
+ int i;
+
+ spa_log_trace(this->log, "handling mSBC data");
+
+ /*
+ * Check if the packet contains only zeros - if so ignore the packet.
+ * This is necessary, because some kernels insert bogus "all-zero" packets
+ * into the datastream.
+ * See https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/549
+ */
+ if (is_zero_packet(read_data, size_read))
+ return 0;
+
+ for (i = 0; i < size_read; ++i) {
+ void *buf;
+ uint32_t avail;
+ int seq, processed;
+ size_t written;
+
+ msbc_buffer_append_byte(this, read_data[i]);
+
+ if (this->msbc_buffer_pos != MSBC_ENCODED_SIZE)
+ continue;
+
+ /*
+ * Handle found mSBC packet
+ */
+
+ buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+
+ /* Check sequence number */
+ seq = ((this->msbc_buffer[1] >> 4) & 1) |
+ ((this->msbc_buffer[1] >> 6) & 2);
+
+ spa_log_trace(this->log, "mSBC packet seq=%u", seq);
+ if (!this->msbc_seq_initialized) {
+ this->msbc_seq_initialized = true;
+ this->msbc_seq = seq;
+ } else if (seq != this->msbc_seq) {
+ /* TODO: PLC (too late to insert data now) */
+ spa_log_info(this->log,
+ "missing mSBC packet: %u != %u", seq, this->msbc_seq);
+ this->msbc_seq = seq;
+ }
+
+ this->msbc_seq = (this->msbc_seq + 1) % 4;
+
+ if (avail < MSBC_DECODED_SIZE)
+ spa_log_warn(this->log, "Output buffer full, dropping msbc data");
+
+ /* decode frame */
+ processed = sbc_decode(
+ &this->msbc, this->msbc_buffer + 2, MSBC_ENCODED_SIZE - 3,
+ buf, avail, &written);
+
+ if (processed < 0) {
+ spa_log_warn(this->log, "sbc_decode failed: %d", processed);
+ /* TODO: manage errors */
+ continue;
+ }
+
+ spa_bt_decode_buffer_write_packet(&port->buffer, written);
+ decoded += written;
+ }
+
+ return decoded;
+}
+
+static int sco_source_cb(void *userdata, uint8_t *read_data, int size_read)
+{
+ struct impl *this = userdata;
+ struct port *port = &this->port;
+ uint32_t decoded;
+ uint64_t dt;
+
+ if (this->transport == NULL) {
+ spa_log_debug(this->log, "no transport, stop reading");
+ goto stop;
+ }
+
+ /* update the current pts */
+ dt = SPA_TIMESPEC_TO_NSEC(&this->now);
+ spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &this->now);
+ dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt;
+
+ /* handle data read from socket */
+#if 0
+ hexdump_to_log(this, read_data, size_read);
+#endif
+
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ decoded = preprocess_and_decode_msbc_data(userdata, read_data, size_read);
+ } else {
+ uint32_t avail;
+ uint8_t *packet;
+
+ if (size_read != 48 && is_zero_packet(read_data, size_read)) {
+ /* Adapter is returning non-standard CVSD stream. For example
+ * Intel 8087:0029 at Firmware revision 0.0 build 191 week 21 2021
+ * on kernel 5.13.19 produces such data.
+ */
+ return 0;
+ }
+
+ if (size_read % port->frame_size != 0) {
+ /* Unaligned data: reception or adapter problem.
+ * Consider the whole packet lost and report.
+ */
+ spa_log_debug(this->log,
+ "received bad Bluetooth SCO CVSD packet");
+ return 0;
+ }
+
+ packet = spa_bt_decode_buffer_get_write(&port->buffer, &avail);
+ avail = SPA_MIN(avail, (uint32_t)size_read);
+ spa_memmove(packet, read_data, avail);
+ spa_bt_decode_buffer_write_packet(&port->buffer, avail);
+
+ decoded = avail;
+ }
+
+ spa_log_trace(this->log, "read socket data size:%d decoded frames:%d dt:%d dms",
+ size_read, decoded / port->frame_size,
+ (int)(dt / 100000));
+
+ return 0;
+
+stop:
+ return 1;
+}
+
+static int setup_matching(struct impl *this)
+{
+ struct port *port = &this->port;
+
+ if (this->position && port->rate_match) {
+ port->rate_match->rate = 1 / port->buffer.corr;
+
+ this->matching = this->following;
+ this->resampling = this->matching ||
+ (port->current_format.info.raw.rate != this->position->clock.rate.denom);
+ } else {
+ this->matching = false;
+ this->resampling = false;
+ }
+
+ if (port->rate_match)
+ SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching);
+
+ return 0;
+}
+
+static int produce_buffer(struct impl *this);
+
+static void sco_on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ uint64_t exp, duration;
+ uint32_t rate;
+ uint64_t prev_time, now_time;
+ int res;
+
+ if (this->transport == NULL)
+ return;
+
+ if (this->started) {
+ if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(this->log, "error reading timerfd: %s",
+ spa_strerror(res));
+ return;
+ }
+ }
+
+ prev_time = this->current_time;
+ now_time = this->current_time = this->next_time;
+
+ spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this,
+ now_time, now_time - prev_time);
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ setup_matching(this);
+
+ this->next_time = now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = now_time;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->rate_diff = port->buffer.corr;
+ this->clock->next_nsec = this->next_time;
+ }
+
+ if (port->io) {
+ int status = produce_buffer(this);
+ spa_log_trace(this->log, "%p: io:%d status:%d", this, port->io->status, status);
+ }
+
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int do_add_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+
+ spa_bt_sco_io_set_source_cb(this->transport->sco_io, sco_source_cb, this);
+
+ return 0;
+}
+
+static int do_start(struct impl *this)
+{
+ struct port *port = &this->port;
+ bool do_accept;
+ int res;
+
+ /* Don't do anything if the node has already started */
+ if (this->started)
+ return 0;
+
+ this->following = is_following(this);
+
+ spa_log_debug(this->log, "%p: start following:%d",
+ this, this->following);
+
+ /* Make sure the transport is valid */
+ spa_return_val_if_fail (this->transport != NULL, -EIO);
+
+ /* Do accept if Gateway; otherwise do connect for Head Unit */
+ do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY;
+
+ /* acquire the socket fd (false -> connect | true -> accept) */
+ if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0)
+ return res;
+
+ /* Reset the buffers and sample count */
+ reset_buffers(port);
+
+ spa_bt_decode_buffer_clear(&port->buffer);
+ if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log,
+ port->frame_size, port->current_format.info.raw.rate,
+ this->quantum_limit, this->quantum_limit)) < 0)
+ return res;
+
+ /* Init mSBC if needed */
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) {
+ sbc_init_msbc(&this->msbc, 0);
+ /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */
+ this->msbc.endian = SBC_LE;
+ this->msbc_seq_initialized = false;
+
+ this->msbc_buffer_pos = 0;
+ }
+
+ /* Start socket i/o */
+ if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0)
+ goto fail;
+ spa_loop_invoke(this->data_loop, do_add_source, 0, NULL, 0, true, this);
+
+ /* Start timer */
+ this->timer_source.data = this;
+ this->timer_source.fd = this->timerfd;
+ this->timer_source.func = sco_on_timeout;
+ this->timer_source.mask = SPA_IO_IN;
+ this->timer_source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &this->timer_source);
+
+ setup_matching(this);
+ set_timers(this);
+
+ /* Set the started flag */
+ this->started = true;
+
+ return 0;
+
+fail:
+ spa_bt_transport_release(this->transport);
+ return res;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ struct itimerspec ts;
+
+ if (this->transport && this->transport->sco_io)
+ spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL);
+
+ if (this->timer_source.loop)
+ spa_loop_remove_source(this->data_loop, &this->timer_source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ struct port *port = &this->port;
+ int res = 0;
+
+ if (!this->started)
+ return 0;
+
+ spa_log_debug(this->log, "sco-source %p: stop", this);
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this);
+
+ this->started = false;
+
+ if (this->transport) {
+ /* Release the transport; it is responsible for closing the fd */
+ res = spa_bt_transport_release(this->transport);
+ }
+
+ spa_bt_decode_buffer_clear(&port->buffer);
+
+ return res;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if ((res = do_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = do_stop(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ static const struct spa_dict_item hu_node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Audio/Source" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+ };
+ const struct spa_dict_item ag_node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "bluez5" },
+ { SPA_KEY_MEDIA_CLASS, "Stream/Output/Audio" },
+ { "media.name", ((this->transport && this->transport->device->name) ?
+ this->transport->device->name : "HSP/HFP") },
+ { SPA_KEY_MEDIA_ROLE, "Communication" },
+ };
+ bool is_ag = this->transport && (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY);
+ 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 = is_ag ?
+ &SPA_DICT_INIT_ARRAY(ag_node_info_items) :
+ &SPA_DICT_INIT_ARRAY(hu_node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ 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);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ 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 (result.index > 0)
+ return 0;
+ if (this->transport == NULL)
+ return -EIO;
+
+ /* set the info structure */
+ struct spa_audio_info_raw info = { 0, };
+ info.format = SPA_AUDIO_FORMAT_S16_LE;
+ info.channels = 1;
+ info.position[0] = SPA_AUDIO_CHANNEL_MONO;
+
+ /* CVSD format has a rate of 8kHz
+ * MSBC format has a rate of 16kHz */
+ if (this->transport->codec == HFP_AUDIO_CODEC_MSBC)
+ info.rate = 16000;
+ else
+ info.rate = 8000;
+
+ /* build the param */
+ param = spa_format_audio_raw_build(&b, id, &info);
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->frame_size,
+ 16 * port->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0:
+ param = spa_latency_build(&b, id, &port->latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ do_stop(this);
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int err;
+
+ if (format == NULL) {
+ spa_log_debug(this->log, "clear format");
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.format != SPA_AUDIO_FORMAT_S16_LE ||
+ info.info.raw.rate == 0 ||
+ info.info.raw.channels != 1)
+ return -EINVAL;
+
+ port->frame_size = info.info.raw.channels * 2;
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ 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);
+ port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ } 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ res = 0;
+ break;
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ spa_log_debug(this->log, "use buffers %d", n_buffers);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_list_append(&port->free, &b->link);
+ b->outstanding = false;
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ port->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+ port = &this->port;
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ recycle_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static uint32_t get_samples(struct impl *this, uint32_t *duration)
+{
+ struct port *port = &this->port;
+ uint32_t samples;
+
+ if (SPA_LIKELY(port->rate_match) && this->resampling) {
+ samples = port->rate_match->size;
+ } else {
+ if (SPA_LIKELY(this->position))
+ samples = this->position->clock.duration * port->current_format.info.raw.rate
+ / this->position->clock.rate.denom;
+ else
+ samples = 1024;
+ }
+
+ if (SPA_LIKELY(this->position))
+ *duration = this->position->clock.duration * port->current_format.info.raw.rate
+ / this->position->clock.rate.denom;
+ else if (SPA_LIKELY(this->clock))
+ *duration = this->clock->duration * port->current_format.info.raw.rate
+ / this->clock->rate.denom;
+ else
+ *duration = 1024 * port->current_format.info.raw.rate / 48000;
+
+ return samples;
+}
+
+static void process_buffering(struct impl *this)
+{
+ struct port *port = &this->port;
+ uint32_t duration;
+ const uint32_t samples = get_samples(this, &duration);
+ void *buf;
+ uint32_t avail;
+
+ spa_bt_decode_buffer_process(&port->buffer, samples, duration);
+
+ setup_matching(this);
+
+ buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail);
+
+ /* copy data to buffers */
+ if (!spa_list_is_empty(&port->free) && avail > 0) {
+ struct buffer *buffer;
+ struct spa_data *datas;
+ uint32_t data_size;
+
+ data_size = samples * port->frame_size;
+
+ avail = SPA_MIN(avail, data_size);
+
+ spa_bt_decode_buffer_read(&port->buffer, avail);
+
+ buffer = spa_list_first(&port->free, struct buffer, link);
+ spa_list_remove(&buffer->link);
+
+ spa_log_trace(this->log, "dequeue %d", buffer->id);
+
+ datas = buffer->buf->datas;
+
+ spa_assert(datas[0].maxsize >= data_size);
+
+ datas[0].chunk->offset = 0;
+ datas[0].chunk->size = avail;
+ datas[0].chunk->stride = port->frame_size;
+ memcpy(datas[0].data, buf, avail);
+
+ /* ready buffer if full */
+ spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)avail / port->frame_size);
+ spa_list_append(&port->ready, &buffer->link);
+ }
+}
+
+static int produce_buffer(struct impl *this)
+{
+ struct buffer *buffer;
+ struct port *port = &this->port;
+ struct spa_io_buffers *io = port->io;
+
+ if (io == NULL)
+ return -EIO;
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Handle buffering */
+ process_buffering(this);
+
+ /* Return if there are no buffers ready to be processed */
+ if (spa_list_is_empty(&port->ready))
+ return SPA_STATUS_OK;
+
+ /* Get the new buffer from the ready list */
+ buffer = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&buffer->link);
+ buffer->outstanding = true;
+
+ /* Set the new buffer in IO */
+ io->buffer_id = buffer->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ /* Notify we have a buffer ready to be processed */
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ /* Return if we already have a buffer */
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ /* Recycle */
+ if (io->buffer_id < port->n_buffers) {
+ recycle_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* Follower produces buffers here, driver in timeout */
+ if (this->following)
+ return produce_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int do_transport_destroy(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ this->transport = NULL;
+ return 0;
+}
+
+static void transport_destroy(void *data)
+{
+ struct impl *this = data;
+ spa_log_debug(this->log, "transport %p destroy", this->transport);
+ spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this);
+}
+
+static const struct spa_bt_transport_events transport_events = {
+ SPA_VERSION_BT_TRANSPORT_EVENTS,
+ .destroy = transport_destroy,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ do_stop(this);
+ if (this->transport)
+ spa_hook_remove(&this->transport_listener);
+ spa_system_close(this->data_system, this->timerfd);
+ spa_bt_decode_buffer_clear(&this->port.buffer);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+
+ 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);
+ 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);
+
+ spa_log_topic_init(this->log, &log_topic);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ /* set the node info */
+ 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;
+ 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->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ /* set the port info */
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+ port->latency.min_quantum = 1.0f;
+ port->latency.max_quantum = 1.0f;
+
+ /* Init the buffer lists */
+ spa_list_init(&port->ready);
+ spa_list_init(&port->free);
+
+ this->quantum_limit = 8192;
+ if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")))
+ spa_atou32(str, &this->quantum_limit, 0);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)))
+ sscanf(str, "pointer:%p", &this->transport);
+
+ if (this->transport == NULL) {
+ spa_log_error(this->log, "a transport is needed");
+ return -EINVAL;
+ }
+ spa_bt_transport_add_listener(this->transport,
+ &this->transport_listener, &transport_events, this);
+
+ this->timerfd = spa_system_timerfd_create(this->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+
+ return 0;
+}
+
+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, "Collabora Ltd. <contact@collabora.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with hsp/hfp" },
+ { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=<transport>" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_sco_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_BLUEZ5_SCO_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/bluez5/test-midi.c b/spa/plugins/bluez5/test-midi.c
new file mode 100644
index 0000000..8e517aa
--- /dev/null
+++ b/spa/plugins/bluez5/test-midi.c
@@ -0,0 +1,299 @@
+#include <spa/utils/defs.h>
+
+#include "midi.h"
+
+#define TIME_HI(v) (0x80 | ((v >> 7) & 0x3f))
+#define TIME_LO(v) (0x80 | (v & 0x7f))
+
+struct event {
+ uint16_t time_msec;
+ size_t size;
+ const uint8_t *data;
+};
+
+struct packet {
+ size_t size;
+ const uint8_t *data;
+};
+
+struct test_info {
+ const struct packet *packets;
+ const struct event *events;
+ unsigned int i;
+};
+
+static const struct packet midi_1_packets[] = {
+ {
+ .size = 27,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1234),
+ /* event 1 */
+ TIME_LO(0x1234), 0xa0, 0x01, 0x02,
+ /* event 2: running status */
+ 0x03, 0x04,
+ /* event 3: running status with timestamp */
+ TIME_LO(0x1235), 0x05, 0x06,
+ /* event 4 */
+ TIME_LO(0x1236), 0xf8,
+ /* event 5: sysex */
+ TIME_LO(0x1237), 0xf0, 0x0a, 0x0b, 0x0c,
+ /* event 6: realtime event inside sysex */
+ TIME_LO(0x1238), 0xff,
+ /* event 5 continues */
+ 0x0d, 0x0e, TIME_LO(0x1239), 0xf7,
+ /* event 6: sysex */
+ TIME_LO(0x1240), 0xf0, 0x10, 0x11,
+ /* packet end in middle of sysex */
+ },
+ },
+ {
+ .size = 7,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1241),
+ /* event 6: continued from previous packet */
+ 0x12, TIME_LO(0x1241), 0xf7,
+ /* event 7 */
+ TIME_LO(0x1242), 0xf1, 0x13,
+ }
+ },
+ {0}
+};
+
+static const struct event midi_1_events[] = {
+ { 0x1234, 3, (uint8_t[]) { 0xa0, 0x01, 0x02 } },
+ { 0x1234, 3, (uint8_t[]) { 0xa0, 0x03, 0x04 } },
+ { 0x1235, 3, (uint8_t[]) { 0xa0, 0x05, 0x06 } },
+ { 0x1236, 1, (uint8_t[]) { 0xf8 } },
+ /* realtime event inside sysex come before it */
+ { 0x1238, 1, (uint8_t[]) { 0xff } },
+ /* sysex timestamp indicates the end time; sysex contains the end marker */
+ { 0x1239, 7, (uint8_t[]) { 0xf0, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf7 } },
+ { 0x1241, 5, (uint8_t[]) { 0xf0, 0x10, 0x11, 0x12, 0xf7 } },
+ { 0x1242, 2, (uint8_t[]) { 0xf1, 0x13 } },
+ {0}
+};
+
+static const struct packet midi_1_packets_mtu14[] = {
+ {
+ .size = 11,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1234),
+ TIME_LO(0x1234), 0xa0, 0x01, 0x02,
+ 0x03, 0x04,
+ /* output Apple-style BLE; running status only for coincident time */
+ TIME_LO(0x1235), 0xa0, 0x05, 0x06,
+ },
+ },
+ {
+ .size = 11,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1236),
+ TIME_LO(0x1236), 0xf8,
+ TIME_LO(0x1238), 0xff,
+ TIME_LO(0x1239), 0xf0, 0x0a, 0x0b, 0x0c, 0x0d,
+ },
+ },
+ {
+ .size = 11,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1239),
+ 0x0e, TIME_LO(0x1239), 0xf7,
+ TIME_LO(0x1241), 0xf0, 0x10, 0x11, 0x12, TIME_LO(0x1241), 0xf7
+ },
+ },
+ {
+ .size = 4,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1242),
+ TIME_LO(0x1242), 0xf1, 0x13
+ },
+ },
+ {0}
+};
+
+static const struct packet midi_2_packets[] = {
+ {
+ .size = 9,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1234),
+ /* event 1 */
+ TIME_LO(0x1234), 0xa0, 0x01, 0x02,
+ /* event 2: timestamp low bits rollover */
+ TIME_LO(0x12b3), 0xa0, 0x03, 0x04,
+ },
+ },
+ {
+ .size = 5,
+ .data = (uint8_t[]) {
+ TIME_HI(0x18b3),
+ /* event 3: timestamp high bits jump */
+ TIME_LO(0x18b3), 0xa0, 0x05, 0x06,
+ },
+ },
+ {0}
+};
+
+static const struct event midi_2_events[] = {
+ { 0x1234, 3, (uint8_t[]) { 0xa0, 0x01, 0x02 } },
+ { 0x12b3, 3, (uint8_t[]) { 0xa0, 0x03, 0x04 } },
+ { 0x18b3, 3, (uint8_t[]) { 0xa0, 0x05, 0x06 } },
+ {0}
+};
+
+static const struct packet midi_2_packets_mtu11[] = {
+ /* Small MTU: only room for one event per packet */
+ {
+ .size = 5,
+ .data = (uint8_t[]) {
+ TIME_HI(0x1234), TIME_LO(0x1234), 0xa0, 0x01, 0x02,
+ },
+ },
+ {
+ .size = 5,
+ .data = (uint8_t[]) {
+ TIME_HI(0x12b3), TIME_LO(0x12b3), 0xa0, 0x03, 0x04,
+ },
+ },
+ {
+ .size = 5,
+ .data = (uint8_t[]) {
+ TIME_HI(0x18b3), TIME_LO(0x18b3), 0xa0, 0x05, 0x06,
+ },
+ },
+ {0}
+};
+
+
+static void check_event(void *user_data, uint16_t time, uint8_t *event, size_t event_size)
+{
+ struct test_info *info = user_data;
+ const struct event *ev = &info->events[info->i];
+
+ spa_assert_se(ev->size > 0);
+ spa_assert_se(ev->time_msec == time);
+ spa_assert_se(ev->size == event_size);
+ spa_assert_se(memcmp(event, ev->data, ev->size) == 0);
+
+ ++info->i;
+}
+
+static void check_parser(struct test_info *info)
+{
+ struct spa_bt_midi_parser parser;
+ int res;
+ int i;
+
+ info->i = 0;
+
+ spa_bt_midi_parser_init(&parser);
+ for (i = 0; info->packets[i].size > 0; ++i) {
+ res = spa_bt_midi_parser_parse(&parser,
+ info->packets[i].data, info->packets[i].size,
+ false, check_event, info);
+ spa_assert_se(res == 0);
+ }
+ spa_assert_se(info->events[info->i].size == 0);
+}
+
+static void check_writer(struct test_info *info, unsigned int mtu)
+{
+ struct spa_bt_midi_writer writer;
+ struct spa_bt_midi_parser parser;
+ unsigned int i, packet;
+ void SPA_UNUSED *buf = writer.buf;
+
+ spa_bt_midi_parser_init(&parser);
+ spa_bt_midi_writer_init(&writer, mtu);
+
+ packet = 0;
+ info->i = 0;
+
+ for (i = 0; info->events[i].size > 0; ++i) {
+ const struct event *ev = &info->events[i];
+ bool last = (info->events[i+1].size == 0);
+ int res;
+
+ do {
+ res = spa_bt_midi_writer_write(&writer,
+ ev->time_msec * SPA_NSEC_PER_MSEC, ev->data, ev->size);
+ spa_assert_se(res >= 0);
+ if (res || last) {
+ int r;
+
+ spa_assert_se(info->packets[packet].size > 0);
+ spa_assert_se(writer.size == info->packets[packet].size);
+ spa_assert_se(memcmp(writer.buf, info->packets[packet].data, writer.size) == 0);
+ ++packet;
+
+ /* Test roundtrip */
+ r = spa_bt_midi_parser_parse(&parser, writer.buf, writer.size,
+ false, check_event, info);
+ spa_assert_se(r == 0);
+ }
+ } while (res);
+ }
+
+ spa_assert_se(info->packets[packet].size == 0);
+ spa_assert_se(info->events[info->i].size == 0);
+}
+
+static void test_midi_parser_1(void)
+{
+ struct test_info info = {
+ .packets = midi_1_packets,
+ .events = midi_1_events,
+ };
+
+ check_parser(&info);
+}
+
+static void test_midi_parser_2(void)
+{
+ struct test_info info = {
+ .packets = midi_2_packets,
+ .events = midi_2_events,
+ };
+
+ check_parser(&info);
+}
+
+static void test_midi_writer_1(void)
+{
+ struct test_info info = {
+ .packets = midi_1_packets_mtu14,
+ .events = midi_1_events,
+ };
+
+ check_writer(&info, 14);
+}
+
+static void test_midi_writer_2(void)
+{
+ struct test_info info = {
+ .packets = midi_2_packets,
+ .events = midi_2_events,
+ };
+
+ check_writer(&info, 23);
+ check_writer(&info, 12);
+}
+
+static void test_midi_writer_3(void)
+{
+ struct test_info info = {
+ .packets = midi_2_packets_mtu11,
+ .events = midi_2_events,
+ };
+
+ check_writer(&info, 11);
+}
+
+int main(void)
+{
+ test_midi_parser_1();
+ test_midi_parser_2();
+ test_midi_writer_1();
+ test_midi_writer_2();
+ test_midi_writer_3();
+ return 0;
+}
diff --git a/spa/plugins/bluez5/upower.c b/spa/plugins/bluez5/upower.c
new file mode 100644
index 0000000..23a637a
--- /dev/null
+++ b/spa/plugins/bluez5/upower.c
@@ -0,0 +1,311 @@
+/* Spa Bluez5 UPower proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <spa/utils/string.h>
+
+#include "upower.h"
+
+#define UPOWER_SERVICE "org.freedesktop.UPower"
+#define UPOWER_DEVICE_INTERFACE UPOWER_SERVICE ".Device"
+#define UPOWER_DISPLAY_DEVICE_OBJECT "/org/freedesktop/UPower/devices/DisplayDevice"
+
+struct impl {
+ struct spa_bt_monitor *monitor;
+
+ struct spa_log *log;
+ DBusConnection *conn;
+
+ bool filters_added;
+
+ void *user_data;
+ void (*set_battery_level)(unsigned int level, void *user_data);
+};
+
+static DBusHandlerResult upower_parse_percentage(struct impl *this, DBusMessageIter *variant_i)
+{
+ double percentage;
+ unsigned int battery_level;
+
+ dbus_message_iter_get_basic(variant_i, &percentage);
+ spa_log_debug(this->log, "Battery level: %f %%", percentage);
+
+ battery_level = (unsigned int) round(percentage / 20.0);
+ this->set_battery_level(battery_level, this->user_data);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void upower_get_percentage_properties_reply(DBusPendingCall *pending, void *user_data)
+{
+ struct impl *backend = user_data;
+ DBusMessage *r;
+ DBusMessageIter i, variant_i;
+
+ r = dbus_pending_call_steal_reply(pending);
+ if (r == NULL)
+ return;
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(backend->log, "Failed to get percentage from UPower: %s",
+ dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "v")) {
+ spa_log_error(backend->log, "Invalid arguments in Get() reply");
+ goto finish;
+ }
+
+ dbus_message_iter_recurse(&i, &variant_i);
+ upower_parse_percentage(backend, &variant_i);
+
+finish:
+ dbus_message_unref(r);
+}
+
+static void upower_clean(struct impl *this)
+{
+ this->set_battery_level(0, this->user_data);
+}
+
+static DBusHandlerResult upower_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data)
+{
+ struct impl *this = user_data;
+ DBusError err;
+
+ dbus_error_init(&err);
+
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+ const char *name, *old_owner, *new_owner;
+
+ spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m));
+
+ if (!dbus_message_get_args(m, &err,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old_owner,
+ DBUS_TYPE_STRING, &new_owner,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message);
+ goto finish;
+ }
+
+ if (spa_streq(name, UPOWER_SERVICE)) {
+ if (old_owner && *old_owner) {
+ spa_log_debug(this->log, "UPower daemon disappeared (%s)", old_owner);
+ upower_clean(this);
+ }
+
+ if (new_owner && *new_owner) {
+ DBusPendingCall *call;
+ static const char* upower_device_interface = UPOWER_DEVICE_INTERFACE;
+ static const char* percentage_property = "Percentage";
+
+ spa_log_debug(this->log, "UPower daemon appeared (%s)", new_owner);
+
+ m = dbus_message_new_method_call(UPOWER_SERVICE, UPOWER_DISPLAY_DEVICE_OBJECT, DBUS_INTERFACE_PROPERTIES, "Get");
+ if (m == NULL)
+ goto finish;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &upower_device_interface,
+ DBUS_TYPE_STRING, &percentage_property, DBUS_TYPE_INVALID);
+ dbus_connection_send_with_reply(this->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, upower_get_percentage_properties_reply, this, NULL);
+ dbus_message_unref(m);
+ }
+ }
+ } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) {
+ const char *path;
+ DBusMessageIter iface_i, props_i;
+ const char *interface;
+
+ if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) {
+ spa_log_error(this->log, "Invalid signature found in PropertiesChanged");
+ goto finish;
+ }
+
+ dbus_message_iter_get_basic(&iface_i, &interface);
+ dbus_message_iter_next(&iface_i);
+ spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY);
+
+ dbus_message_iter_recurse(&iface_i, &props_i);
+
+ path = dbus_message_get_path(m);
+
+ if (spa_streq(interface, UPOWER_DEVICE_INTERFACE)) {
+ spa_log_debug(this->log, "Properties changed on %s", path);
+
+ while (dbus_message_iter_get_arg_type(&props_i) != DBUS_TYPE_INVALID) {
+ DBusMessageIter i, value_i;
+ const char *key;
+
+ dbus_message_iter_recurse(&props_i, &i);
+
+ dbus_message_iter_get_basic(&i, &key);
+ dbus_message_iter_next(&i);
+ dbus_message_iter_recurse(&i, &value_i);
+
+ if(spa_streq(key, "Percentage"))
+ upower_parse_percentage(this, &value_i);
+
+ dbus_message_iter_next(&props_i);
+ }
+ }
+ }
+
+finish:
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static int add_filters(struct impl *this)
+{
+ DBusError err;
+
+ if (this->filters_added)
+ return 0;
+
+ dbus_error_init(&err);
+
+ if (!dbus_connection_add_filter(this->conn, upower_filter_cb, this, NULL)) {
+ spa_log_error(this->log, "failed to add filter function");
+ goto fail;
+ }
+
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" UPOWER_SERVICE "'", &err);
+ dbus_bus_add_match(this->conn,
+ "type='signal',sender='" UPOWER_SERVICE "',"
+ "interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "',"
+ "path='" UPOWER_DISPLAY_DEVICE_OBJECT "',arg0='" UPOWER_DEVICE_INTERFACE "'", &err);
+
+ this->filters_added = true;
+
+ return 0;
+
+fail:
+ dbus_error_free(&err);
+ return -EIO;
+}
+
+static bool is_dbus_service_available(struct impl *this, const char *service)
+{
+ DBusMessage *m, *r;
+ DBusError err;
+ bool success = false;
+
+ m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus",
+ "org.freedesktop.DBus", "NameHasOwner");
+ if (m == NULL)
+ return false;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID);
+
+ dbus_error_init(&err);
+ r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err);
+ dbus_message_unref(m);
+ m = NULL;
+
+ if (r == NULL) {
+ spa_log_info(this->log, "NameHasOwner failed for %s", service);
+ dbus_error_free(&err);
+ goto finish;
+ }
+
+ if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
+ spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r));
+ goto finish;
+ }
+
+ if (!dbus_message_get_args(r, &err,
+ DBUS_TYPE_BOOLEAN, &success,
+ DBUS_TYPE_INVALID)) {
+ spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message);
+ dbus_error_free(&err);
+ goto finish;
+ }
+
+finish:
+ if (r)
+ dbus_message_unref(r);
+
+ return success;
+}
+
+void *upower_register(struct spa_log *log,
+ void *dbus_connection,
+ void (*set_battery_level)(unsigned int level, void *user_data),
+ void *user_data)
+{
+ struct impl *this;
+
+ spa_assert(log);
+ spa_assert(dbus_connection);
+ spa_assert(set_battery_level);
+ spa_assert(user_data);
+
+ this = calloc(1, sizeof(struct impl));
+ if (this == NULL)
+ return NULL;
+
+ this->log = log;
+ this->conn = dbus_connection;
+ this->set_battery_level = set_battery_level;
+ this->user_data = user_data;
+
+ if (add_filters(this) < 0) {
+ goto fail4;
+ }
+
+ if (is_dbus_service_available(this, UPOWER_SERVICE)) {
+ DBusMessage *m;
+ DBusPendingCall *call;
+ static const char* upower_device_interface = UPOWER_DEVICE_INTERFACE;
+ static const char* percentage_property = "Percentage";
+
+ m = dbus_message_new_method_call(UPOWER_SERVICE, UPOWER_DISPLAY_DEVICE_OBJECT, DBUS_INTERFACE_PROPERTIES, "Get");
+ if (m == NULL)
+ goto fail4;
+ dbus_message_append_args(m, DBUS_TYPE_STRING, &upower_device_interface,
+ DBUS_TYPE_STRING, &percentage_property, DBUS_TYPE_INVALID);
+ dbus_connection_send_with_reply(this->conn, m, &call, -1);
+ dbus_pending_call_set_notify(call, upower_get_percentage_properties_reply, this, NULL);
+ dbus_message_unref(m);
+ }
+
+ return this;
+
+fail4:
+ free(this);
+ return NULL;
+}
+
+void upower_unregister(void *data)
+{
+ struct impl *this = data;
+
+ if (this->filters_added) {
+ dbus_connection_remove_filter(this->conn, upower_filter_cb, this);
+ this->filters_added = false;
+ }
+ free(this);
+}
diff --git a/spa/plugins/bluez5/upower.h b/spa/plugins/bluez5/upower.h
new file mode 100644
index 0000000..9ebd751
--- /dev/null
+++ b/spa/plugins/bluez5/upower.h
@@ -0,0 +1,36 @@
+/* Spa Bluez5 UPower proxy
+ *
+ * Copyright © 2022 Collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_BLUEZ5_UPOWER_H_
+#define SPA_BLUEZ5_UPOWER_H_
+
+#include "defs.h"
+
+void *upower_register(struct spa_log *log,
+ void *dbus_connection,
+ void (*set_battery_level)(unsigned int level, void *user_data),
+ void *user_data);
+void upower_unregister(void *data);
+
+#endif \ No newline at end of file
diff --git a/spa/plugins/control/meson.build b/spa/plugins/control/meson.build
new file mode 100644
index 0000000..adabdfa
--- /dev/null
+++ b/spa/plugins/control/meson.build
@@ -0,0 +1,10 @@
+control_sources = [
+ 'mixer.c',
+ 'plugin.c'
+]
+
+controllib = shared_library('spa-control',
+ control_sources,
+ dependencies : [ spa_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'control')
diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c
new file mode 100644
index 0000000..9d28bef
--- /dev/null
+++ b/spa/plugins/control/mixer.c
@@ -0,0 +1,869 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/cpu.h>
+#include <spa/utils/list.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/control/control.h>
+#include <spa/pod/filter.h>
+
+#define NAME "control-mixer"
+
+#define MAX_BUFFERS 64
+#define MAX_PORTS 128
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_QUEUED (1 << 0)
+ uint32_t flags;
+
+ struct spa_list link;
+ struct spa_buffer *buffer;
+};
+
+struct port {
+ uint32_t direction;
+ uint32_t id;
+
+ struct spa_io_buffers *io;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[8];
+
+ unsigned int valid:1;
+ unsigned int have_format:1;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list queue;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[8];
+
+ struct spa_hook_list hooks;
+
+ uint32_t port_count;
+ uint32_t last_port;
+ struct port *in_ports[MAX_PORTS];
+ struct port out_ports[1];
+
+ int n_formats;
+
+ unsigned int have_format:1;
+ unsigned int started:1;
+};
+
+#define PORT_VALID(p) ((p) != NULL && (p)->valid)
+#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)]))
+#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)]))
+#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+#define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p))
+#define GET_IN_PORT(this,p) (this->in_ports[p])
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ this->started = true;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, GET_OUT_PORT(this, 0), true);
+ for (i = 0; i < this->last_port; i++) {
+ if (PORT_VALID(this->in_ports[i]))
+ emit_port_info(this, GET_IN_PORT(this, i), true);
+ }
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *user_data)
+{
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_IN_PORT (this, port_id);
+ if (port == NULL) {
+ port = calloc(1, sizeof(struct port));
+ if (port == NULL)
+ return -errno;
+ this->in_ports[port_id] = port;
+ }
+
+ port->direction = direction;
+ port->id = port_id;
+
+ spa_list_init(&port->queue);
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF |
+ SPA_PORT_FLAG_DYNAMIC_DATA |
+ SPA_PORT_FLAG_REMOVABLE |
+ SPA_PORT_FLAG_OPTIONAL;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+
+ this->port_count++;
+ if (this->last_port <= port_id)
+ this->last_port = port_id + 1;
+ port->valid = true;
+
+ spa_log_debug(this->log, NAME " %p: add port %d %d", this, port_id, this->last_port);
+ emit_port_info(this, port, true);
+
+ return 0;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_IN_PORT (this, port_id);
+
+ port->valid = false;
+ this->port_count--;
+ if (port->have_format && this->have_format) {
+ if (--this->n_formats == 0)
+ this->have_format = false;
+ }
+ spa_memzero(port, sizeof(struct port));
+
+ if (port_id + 1 == this->last_port) {
+ int i;
+
+ for (i = this->last_port - 1; i >= 0; i--)
+ if (GET_IN_PORT (this, i)->valid)
+ break;
+
+ this->last_port = i + 1;
+ }
+ spa_log_debug(this->log, NAME " %p: remove port %d %d", this, port_id, this->last_port);
+
+ spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
+
+ return 0;
+}
+
+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)
+{
+ switch (index) {
+ case 0:
+ *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));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(this, direction, port_id, result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if ((res = port_enum_formats(this, direction, port_id, result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(4096, 512, INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1));
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers %p", this, port);
+ port->n_buffers = 0;
+ spa_list_init(&port->queue);
+ }
+ return 0;
+}
+
+static int queue_buffer(struct impl *this, struct port *port, struct buffer *b)
+{
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED))
+ return -EINVAL;
+
+ spa_list_append(&port->queue, &b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED);
+ spa_log_trace_fp(this->log, NAME " %p: queue buffer %d", this, b->id);
+ return 0;
+}
+
+static struct buffer *dequeue_buffer(struct impl *this, struct port *port)
+{
+ struct buffer *b;
+
+ if (spa_list_is_empty(&port->queue))
+ return NULL;
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED);
+ spa_log_trace_fp(this->log, NAME " %p: dequeue buffer %d", this, b->id);
+ return b;
+}
+
+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);
+
+ if (format == NULL) {
+ if (port->have_format) {
+ port->have_format = false;
+ if (--this->n_formats == 0)
+ this->have_format = false;
+ clear_buffers(this, port);
+ }
+ } else {
+ uint32_t media_type, media_subtype;
+ if ((res = spa_format_parse(format, &media_type, &media_subtype)) < 0)
+ return res;
+
+ if (media_type != SPA_MEDIA_TYPE_application ||
+ media_subtype != SPA_MEDIA_SUBTYPE_control)
+ return -EINVAL;
+
+ this->have_format = true;
+
+ if (!port->have_format) {
+ this->n_formats++;
+ port->have_format = true;
+ spa_log_debug(this->log, NAME " %p: set format on port %d:%d",
+ this, direction, port_id);
+ }
+ }
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(this, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ 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, NAME " %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;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->buffer = buffers[i];
+ b->flags = 0;
+ b->id = i;
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %d", this, i);
+ return -EINVAL;
+ }
+ if (direction == SPA_DIRECTION_OUTPUT)
+ queue_buffer(this, port, b);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, NAME " %p: port %d:%d io %d %p/%zd", this,
+ direction, port_id, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+ port = GET_OUT_PORT(this, 0);
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ return queue_buffer(this, port, &port->buffers[buffer_id]);
+}
+
+static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b)
+{
+ if (a->offset < b->offset)
+ return -1;
+ if (a->offset > b->offset)
+ return 1;
+ if (a->type != b->type)
+ return 0;
+ 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)
+ return 0;
+
+ da = SPA_POD_BODY(&a->value);
+ db = SPA_POD_BODY(&b->value);
+ if ((da[0] & 0xf) != (db[0] & 0xf))
+ return 0;
+ return priotab[(db[0]>>4) & 7] - priotab[(da[0]>>4) & 7];
+ }
+ default:
+ return 0;
+ }
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *outport;
+ struct spa_io_buffers *outio;
+ uint32_t n_seq, i;
+ struct spa_pod_sequence **seq;
+ struct spa_pod_control **ctrl;
+ struct spa_pod_builder builder;
+ struct spa_pod_frame f;
+ struct buffer *outb;
+ struct spa_data *d;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ outport = GET_OUT_PORT(this, 0);
+ if ((outio = outport->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, NAME " %p: status %p %d %d",
+ this, outio, outio->status, outio->buffer_id);
+
+ if (outio->status == SPA_STATUS_HAVE_DATA)
+ return outio->status;
+
+ /* recycle */
+ if (outio->buffer_id < outport->n_buffers) {
+ queue_buffer(this, outport, &outport->buffers[outio->buffer_id]);
+ outio->buffer_id = SPA_ID_INVALID;
+ }
+
+ /* get output buffer */
+ if ((outb = dequeue_buffer(this, outport)) == NULL) {
+ spa_log_trace(this->log, NAME " %p: out of buffers", this);
+ return -EPIPE;
+ }
+
+ ctrl = alloca(MAX_PORTS * sizeof(struct spa_pod_control *));
+ seq = alloca(MAX_PORTS * sizeof(struct spa_pod_sequence *));
+ n_seq = 0;
+
+ /* collect all sequence pod on input ports */
+ for (i = 0; i < this->last_port; i++) {
+ struct port *inport = GET_IN_PORT(this, i);
+ struct spa_io_buffers *inio = NULL;
+ void *pod;
+
+ if (!inport->valid ||
+ (inio = inport->io) == NULL ||
+ inio->buffer_id >= inport->n_buffers ||
+ inio->status != SPA_STATUS_HAVE_DATA) {
+ spa_log_trace_fp(this->log, NAME " %p: skip input idx:%d valid:%d "
+ "io:%p status:%d buf_id:%d n_buffers:%d", this,
+ i, inport->valid, inio,
+ inio ? inio->status : -1,
+ inio ? inio->buffer_id : SPA_ID_INVALID,
+ inport->n_buffers);
+ continue;
+ }
+
+ spa_log_trace_fp(this->log, NAME " %p: mix input %d %p->%p %d %d", this,
+ i, inio, outio, inio->status, inio->buffer_id);
+
+ d = inport->buffers[inio->buffer_id].buffer->datas;
+
+ if ((pod = spa_pod_from_data(d->data, d->maxsize,
+ d->chunk->offset, d->chunk->size)) == NULL)
+ continue;
+ if (!spa_pod_is_sequence(pod))
+ continue;
+
+ seq[n_seq] = pod;
+ ctrl[n_seq] = spa_pod_control_first(&seq[n_seq]->body);
+ inio->status = SPA_STATUS_NEED_DATA;
+ n_seq++;
+ }
+
+ d = outb->buffer->datas;
+
+ /* prepare to write into output */
+ spa_pod_builder_init(&builder, d->data, d->maxsize);
+ spa_pod_builder_push_sequence(&builder, &f, 0);
+
+ /* merge sort all sequences into output buffer */
+ while (true) {
+ struct spa_pod_control *next = NULL;
+ uint32_t next_index = 0;
+
+ for (i = 0; i < n_seq; i++) {
+ if (!spa_pod_control_is_inside(&seq[i]->body,
+ SPA_POD_BODY_SIZE(seq[i]), ctrl[i]))
+ continue;
+
+ if (next == NULL || event_sort(ctrl[i], next) <= 0) {
+ next = ctrl[i];
+ next_index = i;
+ }
+ }
+ if (next == NULL)
+ break;
+
+ 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]);
+ }
+ spa_pod_builder_pop(&builder, &f);
+
+ d->chunk->offset = 0;
+ d->chunk->size = builder.state.offset;
+ d->chunk->stride = 1;
+ d->chunk->flags = 0;
+
+ outio->buffer_id = outb->id;
+ outio->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+ uint32_t i;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ for (i = 0; i < MAX_PORTS; i++)
+ free(this->in_ports[i]);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+
+ 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_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = MAX_PORTS;
+ this->info.max_output_ports = 1;
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS;
+ this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS;
+
+ port = GET_OUT_PORT(this, 0);
+ port->valid = true;
+ port->direction = SPA_DIRECTION_OUTPUT;
+ port->id = 0;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS;
+ port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+
+ spa_list_init(&port->queue);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+const struct spa_handle_factory spa_control_mixer_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_CONTROL_MIXER,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/control/plugin.c b/spa/plugins/control/plugin.c
new file mode 100644
index 0000000..ed53382
--- /dev/null
+++ b/spa/plugins/control/plugin.c
@@ -0,0 +1,46 @@
+/* Spa Control plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_control_mixer_factory;
+
+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_control_mixer_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/ffmpeg/ffmpeg-dec.c b/spa/plugins/ffmpeg/ffmpeg-dec.c
new file mode 100644
index 0000000..e9c4c1e
--- /dev/null
+++ b/spa/plugins/ffmpeg/ffmpeg-dec.c
@@ -0,0 +1,529 @@
+/* Spa FFmpeg decoder
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <spa/utils/string.h>
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/video/format.h>
+#include <spa/pod/filter.h>
+
+#include "ffmpeg.h"
+
+#define IS_VALID_PORT(this,d,id) ((id) == 0)
+#define GET_IN_PORT(this,p) (&this->in_ports[p])
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+ struct spa_list link;
+};
+
+struct port {
+ enum spa_direction direction;
+ uint32_t id;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[8];
+
+ struct spa_video_info current_format;
+ unsigned int have_format:1;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_io_buffers *io;
+
+ struct spa_list free;
+ struct spa_list ready;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[2];
+
+ struct spa_hook_list hooks;
+
+ struct port in_ports[1];
+ struct port out_ports[1];
+
+ bool started;
+};
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ if (this == NULL || command == NULL)
+ return -EINVAL;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ this->started = true;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, GET_IN_PORT(this, 0), true);
+ emit_port_info(this, GET_OUT_PORT(this, 0), true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *user_data)
+{
+ return 0;
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_remove_port(void *object,
+ enum spa_direction direction,
+ uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int port_enum_formats(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ if (!IS_VALID_PORT(object, direction, port_id))
+ return -EINVAL;
+
+ switch (index) {
+ case 0:
+ *param = NULL;
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int port_get_format(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (!port->have_format)
+ return -EIO;
+
+ if (index > 0)
+ return 0;
+
+ *param = NULL;
+
+ 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 spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ 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_Format:
+ if ((res = port_get_format(this, direction, port_id,
+ result.index, filter, &param, &b)) <= 0)
+ return res;
+ 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(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;
+
+ if (this == NULL || format == NULL)
+ return -EINVAL;
+
+ if (!IS_VALID_PORT(this, direction, port_id))
+ return -EINVAL;
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (format == NULL) {
+ port->have_format = false;
+ } else {
+ struct spa_video_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_video &&
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_video_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (!(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
+ port->current_format = info;
+ port->have_format = true;
+ }
+ }
+ 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)
+{
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(object, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ if (object == NULL)
+ return -EINVAL;
+
+ if (!IS_VALID_PORT(object, direction, port_id))
+ return -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;
+ struct port *port;
+
+ if (this == NULL)
+ return -EINVAL;
+
+ if (!IS_VALID_PORT(this, direction, port_id))
+ return -EINVAL;
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (id == SPA_IO_Buffers)
+ port->io = data;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *output;
+
+ if (this == NULL)
+ return -EINVAL;
+
+ port = &this->out_ports[0];
+
+ if ((output = port->io) == NULL)
+ return -EIO;
+
+ if (!port->have_format) {
+ output->status = -EIO;
+ return -EIO;
+ }
+ output->status = SPA_STATUS_OK;
+
+ return SPA_STATUS_OK;
+}
+
+static int
+impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ if (object == NULL)
+ return -EINVAL;
+
+ if (port_id != 0)
+ return -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;
+
+ if (handle == NULL || interface == NULL)
+ return -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;
+}
+
+size_t
+spa_ffmpeg_dec_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+int
+spa_ffmpeg_dec_init(struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+
+ 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_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->info.params = this->params;
+
+ port = GET_IN_PORT(this, 0);
+ port->direction = SPA_DIRECTION_INPUT;
+ port->id = 0;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = 0;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->info.params = port->params;
+ port->info.n_params = 2;
+
+ port = GET_OUT_PORT(this, 0);
+ port->direction = SPA_DIRECTION_OUTPUT;
+ port->id = 0;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = 0;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->info.params = port->params;
+ port->info.n_params = 2;
+
+ return 0;
+}
diff --git a/spa/plugins/ffmpeg/ffmpeg-enc.c b/spa/plugins/ffmpeg/ffmpeg-enc.c
new file mode 100644
index 0000000..6218183
--- /dev/null
+++ b/spa/plugins/ffmpeg/ffmpeg-enc.c
@@ -0,0 +1,507 @@
+/* Spa FFmpeg encoder
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <spa/utils/string.h>
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/pod/filter.h>
+
+#include "ffmpeg.h"
+
+#define IS_VALID_PORT(this,d,id) ((id) == 0)
+#define GET_IN_PORT(this,p) (&this->in_ports[p])
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+ struct spa_list link;
+};
+
+struct port {
+ enum spa_direction direction;
+ uint32_t id;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[8];
+
+ struct spa_video_info current_format;
+ unsigned int have_format:1;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_io_buffers *io;
+
+ struct spa_list free;
+ struct spa_list ready;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[2];
+
+ struct spa_hook_list hooks;
+
+ struct port in_ports[1];
+ struct port out_ports[1];
+
+ bool started;
+};
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ if (this == NULL || command == NULL)
+ return -EINVAL;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ this->started = true;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, GET_IN_PORT(this, 0), true);
+ emit_port_info(this, GET_OUT_PORT(this, 0), true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *user_data)
+{
+ return 0;
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_remove_port(void *object,
+ enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int port_enum_formats(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ return -ENOTSUP;
+}
+
+static int port_get_format(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (!port->have_format)
+ return -EIO;
+
+ if (index > 0)
+ return 0;
+
+ *param = NULL;
+
+ 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 spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ 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_Format:
+ if ((res = port_get_format(this, direction, port_id,
+ result.index, filter, &param, &b)) <= 0)
+ return res;
+ 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(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);
+
+ if (format == NULL) {
+ port->have_format = false;
+ } else {
+ struct spa_video_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_video &&
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_video_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (!(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
+ port->current_format = info;
+ port->have_format = true;
+ }
+ }
+ 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)
+{
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(object, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ if (object == NULL)
+ return -EINVAL;
+
+ if (!IS_VALID_PORT(object, direction, port_id))
+ return -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;
+ struct port *port;
+
+ if (this == NULL)
+ return -EINVAL;
+
+ if (!IS_VALID_PORT(this, direction, port_id))
+ return -EINVAL;
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (id == SPA_IO_Buffers)
+ port->io = data;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int
+impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ if (object == NULL)
+ return -EINVAL;
+
+ if (port_id != 0)
+ return -EINVAL;
+
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *output;
+
+ if (this == NULL)
+ return -EINVAL;
+
+ if ((output = this->out_ports[0].io) == NULL)
+ return -EIO;
+
+ port = &this->out_ports[0];
+
+ if (!port->have_format) {
+ output->status = -EIO;
+ return -EIO;
+ }
+ output->status = SPA_STATUS_OK;
+
+ return SPA_STATUS_OK;
+}
+
+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;
+
+ if (handle == NULL || interface == NULL)
+ return -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;
+}
+
+size_t
+spa_ffmpeg_enc_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+int
+spa_ffmpeg_enc_init(struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support, uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+
+ 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_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->info.params = this->params;
+
+ port = GET_IN_PORT(this, 0);
+ port->direction = SPA_DIRECTION_INPUT;
+ port->id = 0;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = 0;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->info.params = port->params;
+ port->info.n_params = 2;
+
+ port = GET_OUT_PORT(this, 0);
+ port->direction = SPA_DIRECTION_OUTPUT;
+ port->id = 0;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = 0;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->info.params = port->params;
+ port->info.n_params = 2;
+
+ return 0;
+}
diff --git a/spa/plugins/ffmpeg/ffmpeg.c b/spa/plugins/ffmpeg/ffmpeg.c
new file mode 100644
index 0000000..9546dae
--- /dev/null
+++ b/spa/plugins/ffmpeg/ffmpeg.c
@@ -0,0 +1,160 @@
+/* Spa FFmpeg support
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/node/node.h>
+
+#include <libavcodec/avcodec.h>
+
+#include "ffmpeg.h"
+
+static int
+ffmpeg_dec_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)
+{
+ if (factory == NULL || handle == NULL)
+ return -EINVAL;
+
+ return spa_ffmpeg_dec_init(handle, info, support, n_support);
+}
+
+static int
+ffmpeg_enc_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)
+{
+ if (factory == NULL || handle == NULL)
+ return -EINVAL;
+
+ return spa_ffmpeg_enc_init(handle, info, support, n_support);
+}
+
+static const struct spa_interface_info ffmpeg_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node, },
+};
+
+static int
+ffmpeg_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ if (factory == NULL || info == NULL || index == NULL)
+ return -EINVAL;
+
+ if (*index < SPA_N_ELEMENTS(ffmpeg_interfaces))
+ *info = &ffmpeg_interfaces[(*index)++];
+ else
+ return 0;
+
+ return 1;
+}
+
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58, 10, 100)
+static const AVCodec *find_codec_by_index(uint32_t index)
+{
+ static void *av_iter_data;
+ static uint32_t next_index;
+
+ const AVCodec *c = NULL;
+
+ if (index == 0) {
+ av_iter_data = NULL;
+ next_index = 0;
+ }
+
+ while (next_index <= index) {
+ c = av_codec_iterate(&av_iter_data);
+ next_index += 1;
+
+ if (!c)
+ break;
+ }
+
+ return c;
+}
+#else
+static const AVCodec *find_codec_by_index(uint32_t index)
+{
+ static const AVCodec *last_codec;
+ static uint32_t next_index;
+
+ if (index == 0) {
+ last_codec = NULL;
+ next_index = 0;
+ }
+
+ while (next_index <= index) {
+ last_codec = av_codec_next(last_codec);
+ next_index += 1;
+
+ if (!last_codec)
+ break;
+ }
+
+ return last_codec;
+}
+#endif
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+ static char name[128];
+ static struct spa_handle_factory f = {
+ SPA_VERSION_HANDLE_FACTORY,
+ .name = name,
+ .enum_interface_info = ffmpeg_enum_interface_info,
+ };
+
+#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)
+ avcodec_register_all();
+#endif
+
+ const AVCodec *c = find_codec_by_index(*index);
+
+ if (c == NULL)
+ return 0;
+
+ if (av_codec_is_encoder(c)) {
+ snprintf(name, sizeof(name), "encoder.%s", c->name);
+ f.get_size = spa_ffmpeg_enc_get_size;
+ f.init = ffmpeg_enc_init;
+ } else {
+ snprintf(name, sizeof(name), "decoder.%s", c->name);
+ f.get_size = spa_ffmpeg_dec_get_size;
+ f.init = ffmpeg_dec_init;
+ }
+
+ *factory = &f;
+ (*index)++;
+
+ return 1;
+}
diff --git a/spa/plugins/ffmpeg/ffmpeg.h b/spa/plugins/ffmpeg/ffmpeg.h
new file mode 100644
index 0000000..19078d8
--- /dev/null
+++ b/spa/plugins/ffmpeg/ffmpeg.h
@@ -0,0 +1,20 @@
+#ifndef SPA_FFMPEG_H
+#define SPA_FFMPEG_H
+
+#include <stdint.h>
+#include <stddef.h>
+
+struct spa_dict;
+struct spa_handle;
+struct spa_support;
+struct spa_handle_factory;
+
+int spa_ffmpeg_dec_init(struct spa_handle *handle, const struct spa_dict *info,
+ const struct spa_support *support, uint32_t n_support);
+int spa_ffmpeg_enc_init(struct spa_handle *handle, const struct spa_dict *info,
+ const struct spa_support *support, uint32_t n_support);
+
+size_t spa_ffmpeg_dec_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params);
+size_t spa_ffmpeg_enc_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params);
+
+#endif
diff --git a/spa/plugins/ffmpeg/meson.build b/spa/plugins/ffmpeg/meson.build
new file mode 100644
index 0000000..0e41ecb
--- /dev/null
+++ b/spa/plugins/ffmpeg/meson.build
@@ -0,0 +1,9 @@
+ffmpeg_sources = ['ffmpeg.c',
+ 'ffmpeg-dec.c',
+ 'ffmpeg-enc.c']
+
+ffmpeglib = shared_library('spa-ffmpeg',
+ ffmpeg_sources,
+ dependencies : [ spa_dep, avcodec_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'ffmpeg')
diff --git a/spa/plugins/jack/jack-client.c b/spa/plugins/jack/jack-client.c
new file mode 100644
index 0000000..b3cdcc7
--- /dev/null
+++ b/spa/plugins/jack/jack-client.c
@@ -0,0 +1,113 @@
+/* Spa JACK Client
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include "jack-client.h"
+
+static int jack_process(jack_nframes_t nframes, void *arg)
+{
+ struct spa_jack_client *client = arg;
+
+ jack_get_cycle_times(client->client,
+ &client->current_frames, &client->current_usecs,
+ &client->next_usecs, &client->period_usecs);
+
+ jack_transport_query (client->client, &client->pos);
+
+ client->buffer_size = nframes;
+
+ spa_jack_client_emit_process(client);
+
+ return 0;
+}
+
+static void jack_shutdown(void* arg)
+{
+ struct spa_jack_client *client = arg;
+
+ spa_jack_client_emit_shutdown(client);
+
+ spa_hook_list_init(&client->listener_list);
+ client->client = NULL;
+}
+
+static int status_to_result(jack_status_t status)
+{
+ int res;
+
+ if (status & JackInvalidOption)
+ res = -EINVAL;
+ else if (status & JackServerFailed)
+ res = -ECONNREFUSED;
+ else if (status & JackVersionError)
+ res = -EPROTO;
+ else if (status & JackInitFailure)
+ res = -EIO;
+ else
+ res = -EFAULT;
+
+ return res;
+}
+
+int spa_jack_client_open(struct spa_jack_client *client,
+ const char *client_name, const char *server_name)
+{
+ jack_status_t status;
+
+ if (client->client)
+ return 0;
+
+ client->client = jack_client_open(client_name,
+ JackNoStartServer, &status, NULL);
+
+ if (client->client == NULL)
+ return status_to_result(status);
+
+ spa_hook_list_init(&client->listener_list);
+
+ jack_set_process_callback(client->client, jack_process, client);
+ jack_on_shutdown(client->client, jack_shutdown, client);
+ client->frame_rate = jack_get_sample_rate(client->client);
+ client->buffer_size = jack_get_buffer_size(client->client);
+
+ return 0;
+}
+
+
+int spa_jack_client_close(struct spa_jack_client *client)
+{
+ if (client->client == NULL)
+ return 0;
+
+ spa_jack_client_emit_destroy(client);
+
+ if (jack_client_close(client->client) != 0)
+ return -EIO;
+
+ spa_hook_list_init(&client->listener_list);
+ client->client = NULL;
+
+ return 0;
+}
diff --git a/spa/plugins/jack/jack-client.h b/spa/plugins/jack/jack-client.h
new file mode 100644
index 0000000..b758ba0
--- /dev/null
+++ b/spa/plugins/jack/jack-client.h
@@ -0,0 +1,84 @@
+/* Spa JACK Client
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_JACK_CLIENT_H
+#define SPA_JACK_CLIENT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/support/log.h>
+
+#include <jack/jack.h>
+
+struct spa_jack_client_events {
+#define SPA_VERSION_JACK_CLIENT_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*process) (void *data);
+
+ void (*shutdown) (void *data);
+};
+
+struct spa_jack_client {
+ struct spa_log *log;
+
+ jack_client_t *client;
+
+ jack_nframes_t frame_rate;
+ jack_nframes_t buffer_size;
+ jack_nframes_t current_frames;
+ jack_time_t current_usecs;
+ jack_time_t next_usecs;
+ float period_usecs;
+ jack_position_t pos;
+
+ struct spa_hook_list listener_list;
+};
+
+#define spa_jack_client_emit(c,m,v,...) spa_hook_list_call(&(c)->listener_list, \
+ struct spa_jack_client_events, \
+ m, v, ##__VA_ARGS__)
+#define spa_jack_client_emit_destroy(c) spa_jack_client_emit(c, destroy, 0)
+#define spa_jack_client_emit_process(c) spa_jack_client_emit(c, process, 0)
+#define spa_jack_client_emit_shutdown(c) spa_jack_client_emit(c, shutdown, 0)
+
+#define spa_jack_client_add_listener(c,listener,events,data) \
+ spa_hook_list_append(&(c)->listener_list, listener, events, data)
+
+int spa_jack_client_open(struct spa_jack_client *client,
+ const char *client_name, const char *server_name);
+int spa_jack_client_close(struct spa_jack_client *client);
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_JACK_CLIENT_H */
diff --git a/spa/plugins/jack/jack-device.c b/spa/plugins/jack/jack-device.c
new file mode 100644
index 0000000..0ae67d0
--- /dev/null
+++ b/spa/plugins/jack/jack-device.c
@@ -0,0 +1,457 @@
+/* Spa JACK Device
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+
+#include <spa/support/log.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#include "jack-client.h"
+
+#define NAME "jack-device"
+
+#define MAX_DEVICES 64
+
+#define DEFAULT_SERVER "default"
+
+struct props {
+ char server[128];
+};
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->server, DEFAULT_SERVER, 64);
+}
+
+struct node {
+ enum spa_direction direction;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_hook_list hooks;
+
+ struct props props;
+
+ struct node nodes[2];
+ uint32_t n_nodes;
+
+ uint32_t profile;
+
+ struct spa_jack_client client;
+};
+
+static int emit_node(struct impl *this, uint32_t id)
+{
+ struct spa_dict_item items[6];
+ struct spa_device_object_info info;
+ char jack_client[64];
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+ if (this->nodes[id].direction == SPA_DIRECTION_INPUT)
+ info.factory_name = SPA_NAME_API_JACK_SINK;
+ else
+ info.factory_name = SPA_NAME_API_JACK_SOURCE;
+
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ snprintf(jack_client, sizeof(jack_client), "pointer:%p", &this->client);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_JACK_CLIENT, jack_client);
+ info.props = &SPA_DICT_INIT(items, 1);
+
+ spa_device_emit_object_info(&this->hooks, id, &info);
+
+ return 0;
+}
+
+static int activate_profile(struct impl *this, uint32_t id)
+{
+ int res = 0;
+ uint32_t i, n;
+ const char ** ports;
+
+ spa_log_debug(this->log, "profile %d", id);
+ if (this->profile == id)
+ return 0;
+
+ for (i = 0; i < this->n_nodes; i++)
+ spa_device_emit_object_info(&this->hooks, i, NULL);
+ this->n_nodes = 0;
+
+ spa_jack_client_close(&this->client);
+
+ if (id == 0)
+ goto done;
+
+ res = spa_jack_client_open(&this->client, "PipeWire", NULL);
+ if (res < 0) {
+ spa_log_error(this->log, NAME" %p: can't open client: %s",
+ this, spa_strerror(res));
+ return res;
+ }
+ n = 0;
+ ports = jack_get_ports(this->client.client,
+ NULL, JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsPhysical|JackPortIsOutput);
+ if (ports) {
+ jack_free(ports);
+ this->nodes[n].direction = SPA_DIRECTION_OUTPUT;
+ emit_node(this, n++);
+ }
+
+ ports = jack_get_ports(this->client.client,
+ NULL, JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsPhysical|JackPortIsInput);
+ if (ports) {
+ jack_free(ports);
+ this->nodes[n].direction = SPA_DIRECTION_INPUT;
+ emit_node(this, n++);
+ }
+ this->n_nodes = n;
+done:
+ this->profile = id;
+
+ return res;
+}
+
+static int emit_info(struct impl *this, bool full)
+{
+ int err = 0;
+ struct spa_dict_item items[10];
+ struct spa_device_info dinfo;
+ struct spa_param_info params[2];
+ char name[200];
+
+ dinfo = SPA_DEVICE_INFO_INIT();
+
+ dinfo.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS;
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "jack");
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NICK, "jack");
+ if (spa_streq(this->props.server, "default"))
+ snprintf(name, sizeof(name), "JACK Client");
+ else
+ snprintf(name, sizeof(name), "JACK Client (%s)", this->props.server);
+ items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, name);
+ items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_DESCRIPTION, name);
+ items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_API_JACK_SERVER, this->props.server);
+ items[5] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ dinfo.props = &SPA_DICT_INIT(items, 6);
+
+ dinfo.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE);
+ dinfo.n_params = SPA_N_ELEMENTS(params);
+ dinfo.params = params;
+
+ spa_device_emit_info(&this->hooks, &dinfo);
+
+ return err;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ if (events->info)
+ emit_info(this, true);
+
+ if (events->object_info) {
+ for (i = 0; i < this->n_nodes; i++)
+ emit_node(this, i);
+ }
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b,
+ uint32_t id, uint32_t index)
+{
+ struct spa_pod_frame f[2];
+ const char *name, *desc;
+
+ switch (index) {
+ case 0:
+ name = "off";
+ desc = "Off";
+ break;
+ case 1:
+ name = "on";
+ desc = "On";
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(name),
+ SPA_PARAM_PROFILE_description, SPA_POD_String(desc),
+ 0);
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static int impl_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_device_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_EnumProfile:
+ {
+ switch (result.index) {
+ case 0:
+ case 1:
+ param = build_profile(this, &b, id, result.index);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Profile:
+ {
+ switch (result.index) {
+ case 0:
+ param = build_profile(this, &b, id, this->profile);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_device_emit_result(&this->hooks, seq, 0,
+ SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Profile:
+ {
+ uint32_t idx;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx))) < 0) {
+ spa_log_warn(this->log, "can't parse profile");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+ activate_profile(this, idx);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ activate_profile(this, 0);
+ 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;
+ const char *str;
+
+ 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);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_JACK_SERVER)))
+ snprintf(this->props.server, 64, "%s", str);
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_jack_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_JACK_DEVICE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/jack/jack-sink.c b/spa/plugins/jack/jack-sink.c
new file mode 100644
index 0000000..e9ca5a8
--- /dev/null
+++ b/spa/plugins/jack/jack-sink.c
@@ -0,0 +1,924 @@
+/* Spa jack client
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <jack/jack.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.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/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+
+#include "jack-client.h"
+
+#define NAME "jack-sink"
+
+#define MAX_PORTS 128
+#define MAX_BUFFERS 8
+#define MAX_SAMPLES 8192
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+ struct spa_list link;
+};
+
+struct port {
+ uint32_t id;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_dict_item items[4];
+ struct spa_dict props;
+ struct spa_param_info params[5];
+
+ unsigned int have_format:1;
+ struct spa_audio_info current_format;
+ int stride;
+
+ struct spa_io_buffers *io;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ jack_port_t *jack_port;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[5];
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ struct port in_ports[MAX_PORTS];
+ uint32_t n_in_ports;
+
+ struct spa_audio_info current_format;
+
+ struct spa_jack_client *client;
+ struct spa_hook client_listener;
+
+ unsigned int started:1;
+};
+
+#define CHECK_IN_PORT(this,p) ((p) < this->n_in_ports)
+#define CHECK_PORT(this,d,p) (d == SPA_DIRECTION_INPUT && CHECK_IN_PORT(this,p))
+#define GET_IN_PORT(this,p) (&this->in_ports[p])
+#define GET_PORT(this,d,p) GET_IN_PORT(this,p)
+
+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_PropInfo:
+ return 0;
+
+ case SPA_PARAM_Props:
+ return 0;
+
+ case SPA_PARAM_EnumFormat:
+ case SPA_PARAM_Format:
+ switch (result.index) {
+ case 0:
+ param = spa_format_audio_dsp_build(&b,
+ id, &this->current_format.info.dsp);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (this->started)
+ return 0;
+
+ this->started = true;
+ break;
+
+ case SPA_NODE_COMMAND_Pause:
+ if (!this->started)
+ return 0;
+
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ struct spa_dict_item items[5];
+ char latency[64];
+ snprintf(latency, sizeof(latency), "%d/%d",
+ this->client->buffer_size, this->client->frame_rate);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Sink");
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_NAME, "JACK System");
+ items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+ items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_PAUSE_ON_IDLE, "false");
+ items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_LATENCY, latency);
+ this->info.props = &SPA_DICT_INIT_ARRAY(items);
+ 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) {
+ char* aliases[2];
+ int n_aliases, n_items;
+
+ aliases[0] = alloca(jack_port_name_size());
+ aliases[1] = alloca(jack_port_name_size());
+ n_aliases = jack_port_get_aliases(port->jack_port, aliases);
+ n_items = 1;
+ port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME,
+ jack_port_short_name(port->jack_port));
+ if (n_aliases > 0)
+ port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, aliases[0]);
+ if (n_aliases > 1)
+ port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, aliases[1]);
+ port->props = SPA_DICT_INIT(port->items, n_items);
+
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, port->id, &port->info);
+
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ for (i = 0; i < this->n_in_ports; i++)
+ emit_port_info(this, GET_IN_PORT(this, i), true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static void client_process(void *data)
+{
+ struct impl *this = data;
+
+ if (this->clock) {
+ struct spa_io_clock *c = this->clock;
+ c->nsec = this->client->current_usecs * SPA_NSEC_PER_USEC;
+ c->rate = SPA_FRACTION(1, this->client->frame_rate);
+ c->position = this->client->current_frames;
+ c->duration = this->client->buffer_size;
+ c->delay = 0;
+ c->rate_diff = 1.0;
+ c->next_nsec = this->client->next_usecs * SPA_NSEC_PER_USEC;
+ }
+ if (this->position) {
+ jack_position_t *jp = &this->client->pos;
+ struct spa_io_position *p = this->position;
+ struct spa_io_segment *s;
+
+ p->n_segments = 1;
+ s = &p->segments[0];
+
+ s->flags = 0;
+ s->position = jp->frame;
+ s->rate = 1.0;
+ if (jp->valid & JackPositionBBT) {
+ s->bar.flags = SPA_IO_SEGMENT_BAR_FLAG_VALID;
+ if (jp->valid & JackBBTFrameOffset)
+ s->bar.offset = jp->bbt_offset;
+ else
+ s->bar.offset = 0;
+ s->bar.signature_num = jp->beats_per_bar;
+ s->bar.signature_denom = jp->beat_type;
+ s->bar.bpm = jp->beats_per_minute;
+ s->bar.beat = jp->bar * jp->beats_per_bar + jp->beat;
+ }
+ }
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+}
+
+static const struct spa_jack_client_events client_events = {
+ SPA_VERSION_JACK_CLIENT_EVENTS,
+ .process = client_process,
+};
+
+static int init_port(struct impl *this, struct port *port)
+{
+ 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;
+
+ port->items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio");
+ port->props = SPA_DICT_INIT(port->items, 1);
+ port->info.props = &port->props;
+
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+
+ return 0;
+}
+
+static int init_ports(struct impl *this)
+{
+ const char **ports;
+ uint32_t i;
+ jack_client_t *client = this->client->client;
+ int res;
+
+ ports = jack_get_ports(client,
+ NULL, JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsPhysical|JackPortIsInput);
+ if (ports == NULL) {
+ spa_log_error(this->log, NAME" %p: can't enumerate ports", this);
+ res = -ENODEV;
+ goto exit;
+ }
+
+ for (i = 0; ports[i]; i++) {
+ struct port *port = GET_IN_PORT(this, i);
+ jack_port_t *p = jack_port_by_name(client, ports[i]);
+ char *aliases[2];
+ int n_aliases;
+
+ port->id = i;
+ port->jack_port = jack_port_register(client,
+ jack_port_short_name(p),
+ jack_port_type(p),
+ JackPortIsOutput, 0);
+ if (port->jack_port == NULL) {
+ spa_log_error(this->log, NAME" %p: jack_port_register() %d (%s) failed",
+ this, i, ports[i]);
+ res = -EFAULT;
+ goto exit_free;
+ }
+
+ aliases[0] = alloca(jack_port_name_size());
+ aliases[1] = alloca(jack_port_name_size());
+
+ n_aliases = jack_port_get_aliases(p, aliases);
+ if (n_aliases > 0)
+ jack_port_set_alias(port->jack_port, aliases[0]);
+ if (n_aliases > 1)
+ jack_port_set_alias(port->jack_port, aliases[1]);
+
+ init_port(this, port);
+ }
+ this->n_in_ports = i;
+
+ this->current_format.info.dsp = SPA_AUDIO_INFO_DSP_INIT(
+ .format = SPA_AUDIO_FORMAT_DSP_F32);
+
+ spa_jack_client_add_listener(this->client,
+ &this->client_listener,
+ &client_events, this);
+
+ jack_activate(client);
+
+ for (i = 0; ports[i]; i++) {
+ struct port *port = GET_IN_PORT(this, i);
+ if (jack_connect(client, jack_port_name(port->jack_port), ports[i])) {
+ spa_log_warn(this->log, NAME" %p: Failed to connect %s to %s",
+ this, jack_port_name(port->jack_port), ports[i]);
+ }
+ }
+
+ res = 0;
+exit_free:
+ jack_free(ports);
+exit:
+ return res;
+}
+
+
+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(struct impl *this,
+ 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)
+{
+ switch (index) {
+ case 0:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ 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_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_dsp_build(&b, id, &port->current_format.info.dsp);
+ break;
+
+ case SPA_PARAM_Buffers:
+ {
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ MAX_SAMPLES * port->stride,
+ 16 * port->stride,
+ MAX_SAMPLES * port->stride),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride));
+ break;
+ }
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ this->started = false;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int res;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio &&
+ info.media_subtype != SPA_MEDIA_SUBTYPE_dsp)
+ return -EINVAL;
+
+ if (spa_format_audio_dsp_parse(format, &info.info.dsp) < 0)
+ return -EINVAL;
+
+ if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32)
+ return -EINVAL;
+
+ port->stride = 4;
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ default:
+ return -ENOENT;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ 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);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->flags = 0;
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ uint32_t i;
+ int res = 0;
+
+ spa_log_trace(this->log, NAME" %p: process %d", this, this->n_in_ports);
+
+ for (i = 0; i < this->n_in_ports; i++) {
+ struct port *port = GET_IN_PORT(this, i);
+ struct spa_io_buffers *io = port->io;
+ struct buffer *b;
+ struct spa_data *src;
+ uint32_t n_frames = this->client->buffer_size;
+ void *dst;
+
+ dst = jack_port_get_buffer(port->jack_port, n_frames);
+
+ if (io == NULL ||
+ io->status != SPA_STATUS_HAVE_DATA ||
+ io->buffer_id >= port->n_buffers) {
+ memset(dst, 0, n_frames * sizeof(float));
+ continue;
+ }
+
+ spa_log_trace(this->log, NAME" %p: port %d: buffer %d", this, i, io->buffer_id);
+ b = &port->buffers[io->buffer_id];
+ src = &b->outbuf->datas[0];
+
+ spa_memcpy(dst, src->data, n_frames * port->stride);
+
+ io->status = SPA_STATUS_NEED_DATA;
+
+ res |= SPA_STATUS_NEED_DATA;
+ }
+ return res;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ 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;
+ const char *str;
+
+ 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);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_JACK_CLIENT)))
+ sscanf(str, "pointer:%p", &this->client);
+
+ if (this->client == NULL) {
+ spa_log_error(this->log, NAME" %p: missing "SPA_KEY_API_JACK_CLIENT
+ " property", this);
+ return -EINVAL;
+ }
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_output_ports = MAX_PORTS;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READ);
+ this->params[3] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ this->params[4] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = 5;
+
+ init_ports(this);
+
+ 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, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the JACK API" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_jack_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_JACK_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/jack/jack-source.c b/spa/plugins/jack/jack-source.c
new file mode 100644
index 0000000..7004eed
--- /dev/null
+++ b/spa/plugins/jack/jack-source.c
@@ -0,0 +1,946 @@
+/* Spa jack client
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <jack/jack.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.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/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+
+#include "jack-client.h"
+
+#define NAME "jack-source"
+
+#define MAX_PORTS 128
+#define MAX_BUFFERS 8
+#define MAX_SAMPLES 8192
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+ struct spa_list link;
+};
+
+struct port {
+ uint32_t id;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_dict_item items[4];
+ struct spa_dict props;
+ struct spa_param_info params[5];
+
+ unsigned int have_format:1;
+ struct spa_audio_info current_format;
+ int stride;
+
+ struct spa_io_buffers *io;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list empty;
+
+ jack_port_t *jack_port;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[5];
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ struct port out_ports[MAX_PORTS];
+ uint32_t n_out_ports;
+
+ struct spa_audio_info current_format;
+
+ struct spa_jack_client *client;
+ struct spa_hook client_listener;
+
+ unsigned int started:1;
+};
+
+#define CHECK_OUT_PORT(this,p) ((p) < this->n_out_ports)
+#define CHECK_PORT(this,d,p) (d == SPA_DIRECTION_OUTPUT && CHECK_OUT_PORT(this,p))
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) GET_OUT_PORT(this,p)
+
+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_PropInfo:
+ return 0;
+
+ case SPA_PARAM_Props:
+ return 0;
+
+ case SPA_PARAM_EnumFormat:
+ case SPA_PARAM_Format:
+ switch (result.index) {
+ case 0:
+ param = spa_format_audio_dsp_build(&b,
+ id, &this->current_format.info.dsp);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+
+static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+ struct buffer *b = &port->buffers[id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ spa_list_append(&port->empty, &b->link);
+ }
+}
+
+static struct buffer *dequeue_buffer(struct impl *this, struct port *port)
+{
+ struct buffer *b;
+
+ if (spa_list_is_empty(&port->empty))
+ return NULL;
+
+ b = spa_list_first(&port->empty, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ return b;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (this->started)
+ return 0;
+
+ this->started = true;
+ break;
+
+ case SPA_NODE_COMMAND_Pause:
+ if (!this->started)
+ return 0;
+
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ struct spa_dict_item items[5];
+ char latency[64];
+ snprintf(latency, sizeof(latency), "%d/%d",
+ this->client->buffer_size, this->client->frame_rate);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Source");
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_NAME, "JACK System");
+ items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+ items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_PAUSE_ON_IDLE, "false");
+ items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_LATENCY, latency);
+ this->info.props = &SPA_DICT_INIT_ARRAY(items);
+ 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) {
+ char* aliases[2];
+ int n_aliases, n_items;
+
+ aliases[0] = alloca(jack_port_name_size());
+ aliases[1] = alloca(jack_port_name_size());
+ n_aliases = jack_port_get_aliases(port->jack_port, aliases);
+ n_items = 1;
+ port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME,
+ jack_port_short_name(port->jack_port));
+ if (n_aliases > 0)
+ port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, aliases[0]);
+ if (n_aliases > 1)
+ port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, aliases[1]);
+ port->props = SPA_DICT_INIT(port->items, n_items);
+
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ for (i = 0; i < this->n_out_ports; i++)
+ emit_port_info(this, GET_OUT_PORT(this, i), true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static void client_process(void *data)
+{
+ struct impl *this = data;
+ int res;
+
+ res = spa_node_process(&this->node);
+
+ if (res != SPA_STATUS_OK)
+ spa_node_call_ready(&this->callbacks, res);
+}
+
+static const struct spa_jack_client_events client_events = {
+ SPA_VERSION_JACK_CLIENT_EVENTS,
+ .process = client_process,
+};
+
+static int init_port(struct impl *this, struct port *port)
+{
+ 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;
+
+ port->items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio");
+ port->props = SPA_DICT_INIT(port->items, 1);
+ port->info.props = &port->props;
+
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+
+ spa_list_init(&port->empty);
+ return 0;
+}
+
+static int init_ports(struct impl *this)
+{
+ const char **ports;
+ jack_client_t *client = this->client->client;
+ uint32_t i;
+ int res;
+
+ ports = jack_get_ports(client,
+ NULL, JACK_DEFAULT_AUDIO_TYPE,
+ JackPortIsPhysical|JackPortIsOutput);
+ if (ports == NULL) {
+ spa_log_error(this->log, NAME" %p: can't enumerate ports", this);
+ res = -ENODEV;
+ goto exit;
+ }
+
+ for (i = 0; ports[i]; i++) {
+ struct port *port = GET_OUT_PORT(this, i);
+ jack_port_t *p = jack_port_by_name(client, ports[i]);
+ char *aliases[2];
+ int n_aliases;
+
+ port->id = i;
+ port->jack_port = jack_port_register(client,
+ jack_port_short_name(p),
+ jack_port_type(p),
+ JackPortIsInput, 0);
+ if (port->jack_port == NULL) {
+ spa_log_error(this->log, NAME" %p: jack_port_register() %d (%s) failed",
+ this, i, ports[i]);
+ res = -EFAULT;
+ goto exit_free;
+ }
+
+ aliases[0] = alloca(jack_port_name_size());
+ aliases[1] = alloca(jack_port_name_size());
+
+ n_aliases = jack_port_get_aliases(p, aliases);
+ if (n_aliases > 0)
+ jack_port_set_alias(port->jack_port, aliases[0]);
+ if (n_aliases > 1)
+ jack_port_set_alias(port->jack_port, aliases[1]);
+
+ init_port(this, port);
+ }
+ this->n_out_ports = i;
+
+ this->current_format.info.dsp = SPA_AUDIO_INFO_DSP_INIT(
+ .format = SPA_AUDIO_FORMAT_DSP_F32);
+
+ spa_jack_client_add_listener(this->client,
+ &this->client_listener,
+ &client_events, this);
+
+ jack_activate(client);
+
+ for (i = 0; ports[i]; i++) {
+ struct port *port = GET_OUT_PORT(this, i);
+ if (jack_connect(client, ports[i], jack_port_name(port->jack_port))) {
+ spa_log_warn(this->log, NAME" %p: Failed to connect %s to %s",
+ this, jack_port_name(port->jack_port), ports[i]);
+ }
+ }
+
+ res = 0;
+exit_free:
+ jack_free(ports);
+exit:
+ return res;
+}
+
+
+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(struct impl *this,
+ 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)
+{
+ switch (index) {
+ case 0:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ 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_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_dsp_build(&b, id, &port->current_format.info.dsp);
+ break;
+
+ case SPA_PARAM_Buffers:
+ {
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ MAX_SAMPLES * port->stride,
+ 16 * port->stride,
+ MAX_SAMPLES * port->stride),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride));
+ break;
+ }
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ spa_list_init(&port->empty);
+ this->started = false;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int res;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio &&
+ info.media_subtype != SPA_MEDIA_SUBTYPE_dsp)
+ return -EINVAL;
+
+ if (spa_format_audio_dsp_parse(format, &info.info.dsp) < 0)
+ return -EINVAL;
+
+ if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32)
+ return -EINVAL;
+
+ port->stride = 4;
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ default:
+ return -ENOENT;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ 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);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->flags = 0;
+ spa_list_append(&port->empty, &b->link);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_OUT_PORT(this, port_id), -EINVAL);
+
+ port = GET_OUT_PORT(this, port_id);
+
+ spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL);
+
+ reuse_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ uint32_t i;
+ int res = 0;
+
+ spa_log_trace(this->log, NAME" %p: process %d", this, this->n_out_ports);
+
+ for (i = 0; i < this->n_out_ports; i++) {
+ struct port *port = GET_OUT_PORT(this, i);
+ struct spa_io_buffers *io = port->io;
+ struct buffer *b;
+ struct spa_data *d;
+ const void *src;
+ uint32_t n_frames = this->client->buffer_size;
+
+ if (io == NULL || io->status == SPA_STATUS_HAVE_DATA)
+ continue;
+
+ if (io->buffer_id < port->n_buffers) {
+ reuse_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if ((b = dequeue_buffer(this, port)) == NULL) {
+ spa_log_trace(this->log, NAME" %p: out of buffers", this);
+ io->status = -EPIPE;
+ continue;
+ }
+
+ src = jack_port_get_buffer(port->jack_port, n_frames);
+
+ d = &b->outbuf->datas[0];
+ spa_memcpy(d->data, src, n_frames * port->stride);
+ d->chunk->offset = 0;
+ d->chunk->size = n_frames * port->stride;
+ d->chunk->stride = port->stride;
+ d->chunk->flags = 0;
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ res |= SPA_STATUS_HAVE_DATA;
+ }
+ return res;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ 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;
+ const char *str;
+
+ 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);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_JACK_CLIENT)))
+ sscanf(str, "pointer:%p", &this->client);
+
+ if (this->client == NULL) {
+ spa_log_error(this->log, NAME" %p: missing "SPA_KEY_API_JACK_CLIENT
+ " property", this);
+ return -EINVAL;
+ }
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_output_ports = MAX_PORTS;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READ);
+ this->params[3] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ this->params[4] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = 5;
+
+ init_ports(this);
+
+ 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, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Record audio with the JACK API" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_jack_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_JACK_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/jack/meson.build b/spa/plugins/jack/meson.build
new file mode 100644
index 0000000..312a540
--- /dev/null
+++ b/spa/plugins/jack/meson.build
@@ -0,0 +1,12 @@
+spa_jack_sources = [
+'plugin.c',
+ 'jack-client.c',
+ 'jack-device.c',
+ 'jack-sink.c',
+ 'jack-source.c']
+
+spa_jack = shared_library('spa-jack',
+ spa_jack_sources,
+ dependencies : [ spa_dep, jack_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'jack')
diff --git a/spa/plugins/jack/plugin.c b/spa/plugins/jack/plugin.c
new file mode 100644
index 0000000..4aa9cbd
--- /dev/null
+++ b/spa/plugins/jack/plugin.c
@@ -0,0 +1,54 @@
+/* Spa jack plugin
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_jack_device_factory;
+extern const struct spa_handle_factory spa_jack_source_factory;
+extern const struct spa_handle_factory spa_jack_sink_factory;
+
+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_jack_device_factory;
+ break;
+ case 1:
+ *factory = &spa_jack_source_factory;
+ break;
+ case 2:
+ *factory = &spa_jack_sink_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/libcamera/libcamera-client.c b/spa/plugins/libcamera/libcamera-client.c
new file mode 100644
index 0000000..a31a41f
--- /dev/null
+++ b/spa/plugins/libcamera/libcamera-client.c
@@ -0,0 +1,247 @@
+/* Spa libcamera client
+ *
+ * Copyright (C) 2020, Collabora Ltd.
+ * Author: Raghavendra Rao Sidlagatta <raghavendra.rao@collabora.com>
+ *
+ * libcamera-client.c
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+
+#include "libcamera.h"
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+
+ struct spa_hook_list hooks;
+
+ uint64_t info_all;
+ struct spa_device_info info;
+
+ struct spa_source source;
+};
+
+static int emit_object_info(struct impl *this, uint32_t id)
+{
+ struct spa_device_object_info info;
+ struct spa_dict_item items[20];
+ uint32_t n_items = 0;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+
+ info.type = SPA_TYPE_INTERFACE_Device;
+ info.factory_name = SPA_NAME_API_LIBCAMERA_DEVICE;
+ info.change_mask = (SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS);
+ info.flags = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API,"libcamera-client");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "libcamera");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Video/Device");
+
+ info.props = &SPA_DICT_INIT(items, n_items);
+ spa_device_emit_object_info(&this->hooks, id, &info);
+
+ return 1;
+}
+
+static const struct spa_dict_item device_info_items[] = {
+ { SPA_KEY_DEVICE_API, "libcamera" },
+ { SPA_KEY_DEVICE_NICK, "libcamera-client" },
+ { SPA_KEY_API_UDEV_MATCH, "libcamera" },
+};
+
+
+static void emit_device_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(device_info_items);
+ spa_device_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void impl_hook_removed(struct spa_hook *hook)
+{
+ return;
+}
+
+static int
+impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_device_info(this, true);
+
+ emit_object_info(this, 0);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ listener->removed = impl_hook_removed;
+ listener->priv = this;
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+
+ if(this->dev.camera) {
+ deleteLibCamera(this->dev.camera);
+ this->dev.camera = NULL;
+ }
+ 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;
+
+ 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);
+ libcamera_log_topic_init(this->log);
+
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+
+ if (this->main_loop == NULL) {
+ spa_log_error(this->log, "a main-loop is needed");
+ return -EINVAL;
+ }
+ spa_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ this->info = SPA_DEVICE_INFO_INIT();
+ this->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS;
+ this->info.flags = 0;
+
+ if(this->dev.camera == NULL)
+ this->dev.camera = (LibCamera*)newLibCamera();
+ if(this->dev.camera != NULL)
+ libcamera_set_log(this->dev.camera, this->dev.log);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_libcamera_client_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_LIBCAMERA_ENUM_CLIENT,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/libcamera/libcamera-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp
new file mode 100644
index 0000000..ff206a4
--- /dev/null
+++ b/spa/plugins/libcamera/libcamera-device.cpp
@@ -0,0 +1,332 @@
+/* Spa libcamera Device
+ *
+ * Copyright (C) 2020, Collabora Ltd.
+ * Author: Raghavendra Rao Sidlagatta <raghavendra.rao@collabora.com>
+ * Copyright (C) 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * libcamera-device.cpp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <sys/ioctl.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/result.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/pod/builder.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/param/param.h>
+
+#include "libcamera.h"
+#include "libcamera-manager.hpp"
+
+#include <libcamera/camera.h>
+#include <libcamera/property_ids.h>
+
+using namespace libcamera;
+
+namespace {
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device = {};
+
+ struct spa_log *log;
+
+ std::string device_id;
+
+ struct spa_hook_list hooks;
+
+ std::shared_ptr<CameraManager> manager;
+ std::shared_ptr<Camera> camera;
+
+ impl(spa_log *log,
+ std::shared_ptr<CameraManager> manager,
+ std::shared_ptr<Camera> camera,
+ std::string device_id);
+};
+
+}
+
+static std::string cameraModel(const Camera *camera)
+{
+ const ControlList &props = camera->properties();
+
+ if (auto model = props.get(properties::Model))
+ return std::move(model.value());
+
+ return camera->id();
+}
+
+static const char *cameraLoc(const Camera *camera)
+{
+ const ControlList &props = camera->properties();
+
+ if (auto location = props.get(properties::Location)) {
+ switch (location.value()) {
+ case properties::CameraLocationFront:
+ return "front";
+ case properties::CameraLocationBack:
+ return "back";
+ case properties::CameraLocationExternal:
+ return "external";
+ }
+ }
+
+ return nullptr;
+}
+
+static int emit_info(struct impl *impl, bool full)
+{
+ struct spa_dict_item items[10];
+ struct spa_dict dict;
+ uint32_t n_items = 0;
+ struct spa_device_info info;
+ struct spa_param_info params[2];
+ char path[256], model[256], name[256];
+
+ 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);
+ 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()))
+ ADD_ITEM(SPA_KEY_API_LIBCAMERA_LOCATION, location);
+
+ snprintf(model, sizeof(model), "%s", cameraModel(impl->camera.get()).c_str());
+ ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_NAME, model);
+ ADD_ITEM(SPA_KEY_DEVICE_DESCRIPTION, model);
+ snprintf(name, sizeof(name), "libcamera_device.%s", impl->device_id.c_str());
+ ADD_ITEM(SPA_KEY_DEVICE_NAME, name);
+#undef ADD_ITEM
+
+ dict = SPA_DICT_INIT(items, n_items);
+ info.props = &dict;
+
+ info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_WRITE);
+ info.n_params = SPA_N_ELEMENTS(params);
+ info.params = params;
+
+ spa_device_emit_info(&impl->hooks, &info);
+
+ if (true) {
+ struct spa_device_object_info oinfo;
+
+ oinfo = SPA_DEVICE_OBJECT_INFO_INIT();
+ oinfo.type = SPA_TYPE_INTERFACE_Node;
+ oinfo.factory_name = SPA_NAME_API_LIBCAMERA_SOURCE;
+ oinfo.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ oinfo.props = &dict;
+
+ spa_device_emit_object_info(&impl->hooks, 0, &oinfo);
+ }
+ return 0;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *impl = (struct impl*)object;
+ struct spa_hook_list save;
+ int res = 0;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&impl->hooks, &save, listener, events, data);
+
+ if (events->info || events->object_info)
+ res = emit_info(impl, true);
+
+ spa_hook_list_join(&impl->hooks, &save);
+
+ return res;
+}
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *impl = (struct impl*) object;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+
+ spa_device_emit_result(&impl->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ return -ENOTSUP;
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *impl;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ impl = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &impl->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ std::destroy_at(reinterpret_cast<impl *>(handle));
+ return 0;
+}
+
+impl::impl(spa_log *log,
+ std::shared_ptr<CameraManager> manager,
+ std::shared_ptr<Camera> camera,
+ std::string device_id)
+ : handle({ SPA_VERSION_HANDLE, impl_get_interface, impl_clear }),
+ log(log),
+ device_id(std::move(device_id)),
+ manager(std::move(manager)),
+ camera(std::move(camera))
+{
+ libcamera_log_topic_init(log);
+
+ spa_hook_list_init(&hooks);
+
+ device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ 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)
+{
+ const char *str;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ auto log = static_cast<spa_log *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log));
+
+ auto manager = libcamera_manager_acquire(res);
+ if (!manager) {
+ spa_log_error(log, "can't start camera manager: %s", spa_strerror(res));
+ return res;
+ }
+
+ std::string device_id;
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_LIBCAMERA_PATH)))
+ device_id = str;
+
+ auto camera = manager->get(device_id);
+ if (!camera) {
+ spa_log_error(log, "unknown camera id %s", device_id.c_str());
+ return -ENOENT;
+ }
+
+ new (handle) impl(log, std::move(manager), std::move(camera), std::move(device_id));
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+extern "C" {
+const struct spa_handle_factory spa_libcamera_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_LIBCAMERA_DEVICE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+}
diff --git a/spa/plugins/libcamera/libcamera-manager.cpp b/spa/plugins/libcamera/libcamera-manager.cpp
new file mode 100644
index 0000000..afba403
--- /dev/null
+++ b/spa/plugins/libcamera/libcamera-manager.cpp
@@ -0,0 +1,488 @@
+/* Spa libcamera manager
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <utility>
+#include <mutex>
+#include <optional>
+#include <queue>
+
+#include <libcamera/camera.h>
+#include <libcamera/camera_manager.h>
+
+#include <linux/videodev2.h>
+
+using namespace libcamera;
+
+#include <spa/support/log.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+
+#include "libcamera.h"
+#include "libcamera-manager.hpp"
+
+#define MAX_DEVICES 64
+
+namespace {
+
+struct device {
+ uint32_t id;
+ std::shared_ptr<Camera> camera;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device = {};
+
+ struct spa_log *log;
+ struct spa_loop_utils *loop_utils;
+
+ struct spa_hook_list hooks;
+
+ static constexpr uint64_t info_all = SPA_DEVICE_CHANGE_MASK_FLAGS | SPA_DEVICE_CHANGE_MASK_PROPS;
+ struct spa_device_info info = SPA_DEVICE_INFO_INIT();
+
+ std::shared_ptr<CameraManager> manager;
+ void addCamera(std::shared_ptr<libcamera::Camera> camera);
+ void removeCamera(std::shared_ptr<libcamera::Camera> camera);
+
+ struct device devices[MAX_DEVICES];
+ uint32_t n_devices = 0;
+
+ struct hotplug_event {
+ enum class type { add, remove } type;
+ std::shared_ptr<Camera> camera;
+ };
+
+ std::mutex hotplug_events_lock;
+ std::queue<hotplug_event> hotplug_events;
+ struct spa_source *hotplug_event_source;
+
+ impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_source);
+
+ ~impl()
+ {
+ spa_loop_utils_destroy_source(loop_utils, hotplug_event_source);
+ }
+};
+
+}
+
+static std::weak_ptr<CameraManager> global_manager;
+
+std::shared_ptr<CameraManager> libcamera_manager_acquire(int& res)
+{
+ if (auto manager = global_manager.lock())
+ return manager;
+
+ auto manager = std::make_shared<CameraManager>();
+ if ((res = manager->start()) < 0)
+ return {};
+
+ global_manager = manager;
+
+ return manager;
+}
+static uint32_t get_free_id(struct impl *impl)
+{
+ for (std::size_t i = 0; i < MAX_DEVICES; i++)
+ if (impl->devices[i].camera == nullptr)
+ return i;
+ return 0;
+}
+
+static struct device *add_device(struct impl *impl, std::shared_ptr<Camera> camera)
+{
+ struct device *device;
+ uint32_t id;
+
+ if (impl->n_devices >= MAX_DEVICES)
+ return NULL;
+ id = get_free_id(impl);;
+ device = &impl->devices[id];
+ device->id = get_free_id(impl);;
+ device->camera = std::move(camera);
+ impl->n_devices++;
+ return device;
+}
+
+static struct device *find_device(struct impl *impl, const Camera *camera)
+{
+ uint32_t i;
+ for (i = 0; i < impl->n_devices; i++) {
+ if (impl->devices[i].camera.get() == camera)
+ return &impl->devices[i];
+ }
+ return NULL;
+}
+
+static void remove_device(struct impl *impl, struct device *device)
+{
+ uint32_t old = --impl->n_devices;
+ device->camera.reset();
+ *device = std::move(impl->devices[old]);
+ impl->devices[old].camera = nullptr;
+}
+
+static void clear_devices(struct impl *impl)
+{
+ while (impl->n_devices > 0)
+ impl->devices[--impl->n_devices].camera.reset();
+}
+
+static int emit_object_info(struct impl *impl, struct device *device)
+{
+ struct spa_device_object_info info;
+ uint32_t id = device->id;
+ struct spa_dict_item items[20];
+ struct spa_dict dict;
+ uint32_t n_items = 0;
+ char path[256];
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+
+ info.type = SPA_TYPE_INTERFACE_Device;
+ info.factory_name = SPA_NAME_API_LIBCAMERA_DEVICE;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.flags = 0;
+
+#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value)
+ ADD_ITEM(SPA_KEY_DEVICE_ENUM_API,"libcamera.manager");
+ ADD_ITEM(SPA_KEY_DEVICE_API, "libcamera");
+ ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device");
+ snprintf(path, sizeof(path), "%s", device->camera->id().c_str());
+ ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, path);
+#undef ADD_ITEM
+
+ dict = SPA_DICT_INIT(items, n_items);
+ info.props = &dict;
+ spa_device_emit_object_info(&impl->hooks, id, &info);
+
+ return 1;
+}
+
+static void try_add_camera(struct impl *impl, std::shared_ptr<Camera> camera)
+{
+ struct device *device;
+
+ if ((device = find_device(impl, camera.get())) != NULL)
+ return;
+
+ if ((device = add_device(impl, std::move(camera))) == NULL)
+ return;
+
+ spa_log_info(impl->log, "camera added: id:%d %s", device->id,
+ device->camera->id().c_str());
+ emit_object_info(impl, device);
+}
+
+static void try_remove_camera(struct impl *impl, const Camera *camera)
+{
+ struct device *device;
+
+ if ((device = find_device(impl, camera)) == NULL)
+ return;
+
+ spa_log_info(impl->log, "camera removed: id:%d %s", device->id,
+ device->camera->id().c_str());
+ spa_device_emit_object_info(&impl->hooks, device->id, NULL);
+ remove_device(impl, device);
+}
+
+static void consume_hotplug_event(struct impl *impl, impl::hotplug_event& event)
+{
+ auto& [ type, camera ] = event;
+
+ switch (type) {
+ case impl::hotplug_event::type::add:
+ spa_log_info(impl->log, "camera appeared: %s", camera->id().c_str());
+ try_add_camera(impl, std::move(camera));
+ break;
+ case impl::hotplug_event::type::remove:
+ spa_log_info(impl->log, "camera disappeared: %s", camera->id().c_str());
+ try_remove_camera(impl, camera.get());
+ break;
+ }
+}
+
+static void on_hotplug_event(void *data, std::uint64_t)
+{
+ auto impl = static_cast<struct impl *>(data);
+
+ for (;;) {
+ std::optional<impl::hotplug_event> event;
+
+ {
+ std::unique_lock guard(impl->hotplug_events_lock);
+
+ if (!impl->hotplug_events.empty()) {
+ event = std::move(impl->hotplug_events.front());
+ impl->hotplug_events.pop();
+ }
+ }
+
+ if (!event)
+ break;
+
+ consume_hotplug_event(impl, *event);
+ }
+}
+
+void impl::addCamera(std::shared_ptr<Camera> camera)
+{
+ {
+ std::unique_lock guard(hotplug_events_lock);
+ hotplug_events.push({ hotplug_event::type::add, std::move(camera) });
+ }
+
+ spa_loop_utils_signal_event(loop_utils, hotplug_event_source);
+}
+
+void impl::removeCamera(std::shared_ptr<Camera> camera)
+{
+ {
+ std::unique_lock guard(hotplug_events_lock);
+ hotplug_events.push({ hotplug_event::type::remove, std::move(camera) });
+ }
+
+ spa_loop_utils_signal_event(loop_utils, hotplug_event_source);
+}
+
+static void start_monitor(struct impl *impl)
+{
+ impl->manager->cameraAdded.connect(impl, &impl::addCamera);
+ impl->manager->cameraRemoved.connect(impl, &impl::removeCamera);
+}
+
+static int stop_monitor(struct impl *impl)
+{
+ if (impl->manager) {
+ impl->manager->cameraAdded.disconnect(impl, &impl::addCamera);
+ impl->manager->cameraRemoved.disconnect(impl, &impl::removeCamera);
+ }
+ clear_devices (impl);
+ return 0;
+}
+
+static void collect_existing_devices(struct impl *impl)
+{
+ auto cameras = impl->manager->cameras();
+
+ for (std::shared_ptr<Camera>& camera : cameras)
+ try_add_camera(impl, std::move(camera));
+}
+
+static const struct spa_dict_item device_info_items[] = {
+ { SPA_KEY_DEVICE_API, "libcamera" },
+ { SPA_KEY_DEVICE_NICK, "libcamera-manager" },
+};
+
+static void emit_device_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) {
+ struct spa_dict dict;
+ dict = SPA_DICT_INIT_ARRAY(device_info_items);
+ impl->info.props = &dict;
+ spa_device_emit_info(&impl->hooks, &impl->info);
+ impl->info.change_mask = old;
+ }
+}
+
+static void impl_hook_removed(struct spa_hook *hook)
+{
+ struct impl *impl = (struct impl*)hook->priv;
+ if (spa_hook_list_is_empty(&impl->hooks)) {
+ stop_monitor(impl);
+ impl->manager.reset();
+ }
+}
+
+static int
+impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ int res;
+ struct impl *impl = (struct impl*) object;
+ struct spa_hook_list save;
+ bool had_manager = !!impl->manager;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ if (!impl->manager && !(impl->manager = libcamera_manager_acquire(res)))
+ return res;
+
+ spa_hook_list_isolate(&impl->hooks, &save, listener, events, data);
+
+ emit_device_info(impl, true);
+
+ if (had_manager) {
+ for (std::size_t i = 0; i < impl->n_devices; i++)
+ emit_object_info(impl, &impl->devices[i]);
+ }
+ else {
+ collect_existing_devices(impl);
+ start_monitor(impl);
+ }
+
+ spa_hook_list_join(&impl->hooks, &save);
+
+ listener->removed = impl_hook_removed;
+ listener->priv = impl;
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *impl;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ impl = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &impl->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ auto impl = reinterpret_cast<struct impl *>(handle);
+
+ stop_monitor(impl);
+ std::destroy_at(impl);
+
+ return 0;
+}
+
+impl::impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_source)
+ : handle({ SPA_VERSION_HANDLE, impl_get_interface, impl_clear }),
+ log(log),
+ loop_utils(loop_utils),
+ hotplug_event_source(hotplug_event_source)
+{
+ libcamera_log_topic_init(log);
+
+ spa_hook_list_init(&hooks);
+
+ device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ 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)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ auto log = static_cast<spa_log *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log));
+
+ auto loop_utils = static_cast<spa_loop_utils *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils));
+ if (!loop_utils) {
+ spa_log_error(log, "a " SPA_TYPE_INTERFACE_LoopUtils " is needed");
+ return -EINVAL;
+ }
+
+ auto hotplug_event_source = spa_loop_utils_add_event(loop_utils, on_hotplug_event, handle);
+ if (!hotplug_event_source) {
+ int res = -errno;
+ spa_log_error(log, "failed to create hotplug event: %m");
+ return res;
+ }
+
+ new (handle) impl(log, loop_utils, hotplug_event_source);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+extern "C" {
+const struct spa_handle_factory spa_libcamera_manager_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_LIBCAMERA_ENUM_MANAGER,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+}
diff --git a/spa/plugins/libcamera/libcamera-manager.hpp b/spa/plugins/libcamera/libcamera-manager.hpp
new file mode 100644
index 0000000..4336a39
--- /dev/null
+++ b/spa/plugins/libcamera/libcamera-manager.hpp
@@ -0,0 +1,29 @@
+/* Spa libcamera support
+ *
+ * Copyright © 2021 wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <memory>
+
+#include <libcamera/camera_manager.h>
+
+std::shared_ptr<libcamera::CameraManager> libcamera_manager_acquire(int& res);
diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp
new file mode 100644
index 0000000..d2ca670
--- /dev/null
+++ b/spa/plugins/libcamera/libcamera-source.cpp
@@ -0,0 +1,1073 @@
+/* Spa libcamera Source
+ *
+ * Copyright (C) 2020, Collabora Ltd.
+ * Author: Raghavendra Rao Sidlagatta <raghavendra.rao@collabora.com>
+ * Copyright (C) 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * libcamera-source.cpp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <deque>
+#include <optional>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/monitor/device.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/control/control.h>
+#include <spa/pod/filter.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/stream.h>
+#include <libcamera/formats.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/framebuffer_allocator.h>
+
+#include "libcamera.h"
+#include "libcamera-manager.hpp"
+
+using namespace libcamera;
+
+namespace {
+
+#define MAX_BUFFERS 32
+#define MASK_BUFFERS 31
+
+#define BUFFER_FLAG_OUTSTANDING (1<<0)
+#define BUFFER_FLAG_ALLOCATED (1<<1)
+#define BUFFER_FLAG_MAPPED (1<<2)
+
+struct buffer {
+ uint32_t id;
+ uint32_t flags;
+ struct spa_list link;
+ struct spa_buffer *outbuf;
+ struct spa_meta_header *h;
+ struct spa_meta_videotransform *videotransform;
+ void *ptr;
+};
+
+#define MAX_CONTROLS 64
+
+struct control {
+ uint32_t id;
+ uint32_t ctrl_id;
+ double value;
+};
+
+struct port {
+ struct impl *impl;
+
+ std::optional<spa_video_info> current_format;
+
+ struct spa_fraction rate = {};
+ StreamConfiguration streamConfig;
+
+ uint32_t memtype = 0;
+
+ struct control controls[MAX_CONTROLS];
+ uint32_t n_controls = 0;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers = 0;
+ struct spa_list queue;
+ struct spa_ringbuffer ring = SPA_RINGBUFFER_INIT();
+ uint32_t ring_ids[MAX_BUFFERS];
+
+ static constexpr uint64_t info_all = SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_PARAMS;
+ struct spa_port_info info = SPA_PORT_INFO_INIT();
+ struct spa_io_buffers *io = nullptr;
+ struct spa_io_sequence *control = nullptr;
+#define PORT_PropInfo 0
+#define PORT_EnumFormat 1
+#define PORT_Meta 2
+#define PORT_IO 3
+#define PORT_Format 4
+#define PORT_Buffers 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ uint32_t fmt_index = 0;
+ PixelFormat enum_fmt;
+ uint32_t size_index = 0;
+
+ port(struct impl *impl)
+ : impl(impl)
+ {
+ spa_list_init(&queue);
+
+ params[PORT_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+
+ info.flags = SPA_PORT_FLAG_LIVE | SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL;
+ info.params = params;
+ info.n_params = N_PORT_PARAMS;
+ }
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node = {};
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *system;
+
+ static constexpr uint64_t info_all =
+ SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ struct spa_node_info info = SPA_NODE_INFO_INIT();
+#define NODE_PropInfo 0
+#define NODE_Props 1
+#define NODE_EnumFormat 2
+#define NODE_Format 3
+#define N_NODE_PARAMS 4
+ struct spa_param_info params[N_NODE_PARAMS];
+
+ std::string device_id;
+ std::string device_name;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks = {};
+
+ std::array<port, 1> out_ports;
+
+ struct spa_io_position *position = nullptr;
+ struct spa_io_clock *clock = nullptr;
+
+ std::shared_ptr<CameraManager> manager;
+ std::shared_ptr<Camera> camera;
+
+ FrameBufferAllocator *allocator = nullptr;
+ std::vector<std::unique_ptr<libcamera::Request>> requestPool;
+ std::deque<libcamera::Request *> pendingRequests;
+
+ void requestComplete(libcamera::Request *request);
+
+ std::unique_ptr<CameraConfiguration> config;
+
+ struct spa_source source = {};
+
+ ControlList ctrls;
+ bool active = false;
+ bool acquired = false;
+
+ 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);
+};
+
+}
+
+#define CHECK_PORT(impl,direction,port_id) ((direction) == SPA_DIRECTION_OUTPUT && (port_id) == 0)
+
+#define GET_OUT_PORT(impl,p) (&impl->out_ports[p])
+#define GET_PORT(impl,d,p) GET_OUT_PORT(impl,p)
+
+#include "libcamera-utils.cpp"
+
+static int port_get_format(struct impl *impl, struct port *port,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct spa_pod_frame f;
+
+ if (!port->current_format)
+ return -EIO;
+ if (index > 0)
+ return 0;
+
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_Format);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(port->current_format->media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(port->current_format->media_subtype),
+ 0);
+
+ switch (port->current_format->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(port->current_format->info.raw.format),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format->info.raw.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format->info.raw.framerate),
+ 0);
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ case SPA_MEDIA_SUBTYPE_jpeg:
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format->info.mjpg.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format->info.mjpg.framerate),
+ 0);
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format->info.h264.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format->info.h264.framerate),
+ 0);
+ break;
+ default:
+ return -EIO;
+ }
+
+ *param = (struct spa_pod*)spa_pod_builder_pop(builder, &f);
+
+ return 1;
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *impl = (struct impl*)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;
+ int res;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ switch (result.index) {
+ case 0:
+ param = (struct spa_pod*)spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
+ SPA_PROP_INFO_description, SPA_POD_String("The libcamera device"),
+ SPA_PROP_INFO_type, SPA_POD_String(impl->device_id.c_str()));
+ break;
+ case 1:
+ param = (struct spa_pod*)spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName),
+ SPA_PROP_INFO_description, SPA_POD_String("The libcamera device name"),
+ SPA_PROP_INFO_type, SPA_POD_String(impl->device_name.c_str()));
+ break;
+ default:
+ return spa_libcamera_enum_controls(impl,
+ GET_OUT_PORT(impl, 0),
+ seq, result.index - 2, num, filter);
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ switch (result.index) {
+ case 0:
+ param = (struct spa_pod*)spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_device, SPA_POD_String(impl->device_id.c_str()),
+ SPA_PROP_deviceName, SPA_POD_String(impl->device_name.c_str()));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_EnumFormat:
+ return spa_libcamera_enum_format(impl, GET_OUT_PORT(impl, 0),
+ seq, start, num, filter);
+ case SPA_PARAM_Format:
+ if ((res = port_get_format(impl, GET_OUT_PORT(impl, 0), result.index, filter, &param, &b)) <= 0)
+ return res;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *impl = (struct impl*)object;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ struct spa_pod_prop *prop;
+
+ if (param == NULL) {
+ impl->device_id.clear();
+ impl->device_name.clear();
+ return 0;
+ }
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ char device[128];
+
+ switch (prop->key) {
+ case SPA_PROP_device:
+ strncpy(device, (char *)SPA_POD_CONTENTS(struct spa_pod_string, &prop->value),
+ sizeof(device)-1);
+ impl->device_id = device;
+ break;
+ default:
+ spa_libcamera_set_control(impl, prop);
+ break;
+ }
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *impl = (struct impl*)object;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ impl->clock = (struct spa_io_clock*)data;
+ break;
+ case SPA_IO_Position:
+ impl->position = (struct spa_io_position*)data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *impl = (struct impl*)object;
+ int res;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ struct port *port = GET_OUT_PORT(impl, 0);
+
+ if (!port->current_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if ((res = spa_libcamera_stream_on(impl)) < 0)
+ return res;
+ break;
+ }
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = spa_libcamera_stream_off(impl)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_DEVICE_API, "libcamera" },
+ { SPA_KEY_MEDIA_CLASS, "Video/Source" },
+ { SPA_KEY_MEDIA_ROLE, "Camera" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+static void emit_node_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) {
+ struct spa_dict dict = SPA_DICT_INIT_ARRAY(info_items);
+ impl->info.props = &dict;
+ spa_node_emit_info(&impl->hooks, &impl->info);
+ impl->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *impl, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&impl->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *impl = (struct impl*)object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&impl->hooks, &save, listener, events, data);
+
+ emit_node_info(impl, true);
+ emit_port_info(impl, GET_OUT_PORT(impl, 0), true);
+
+ spa_hook_list_join(&impl->hooks, &save);
+
+ return 0;
+}
+
+static int impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *impl = (struct impl*)object;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+
+ impl->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct impl *impl = (struct impl*)object;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+
+ spa_node_emit_result(&impl->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object,
+ enum spa_direction direction,
+ uint32_t port_id, const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object,
+ enum spa_direction direction,
+ uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *impl = (struct impl*)object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL);
+
+ port = GET_PORT(impl, 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_PropInfo:
+ return spa_libcamera_enum_controls(impl, port, seq, start, num, filter);
+
+ case SPA_PARAM_EnumFormat:
+ return spa_libcamera_enum_format(impl, port, seq, start, num, filter);
+
+ case SPA_PARAM_Format:
+ if((res = port_get_format(impl, port, result.index, filter, &param, &b)) <= 0)
+ return res;
+ break;
+ case SPA_PARAM_Buffers:
+ {
+ if (!port->current_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ /* Get the number of buffers to be used from libcamera and send the same to pipewire
+ * so that exact number of buffers are allocated
+ */
+ uint32_t n_buffers = port->streamConfig.bufferCount;
+
+ 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_size, SPA_POD_Int(port->streamConfig.frameSize),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->streamConfig.stride));
+ break;
+ }
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = (struct spa_pod*)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;
+ case 1:
+ param = (struct spa_pod*)spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_videotransform)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = (struct spa_pod*)spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = (struct spa_pod*)spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 2:
+ param = (struct spa_pod*)spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Control),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_sequence)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int port_set_format(struct impl *impl, struct port *port,
+ uint32_t flags, const struct spa_pod *format)
+{
+ struct spa_video_info info;
+ int res;
+
+ if (format == NULL) {
+ if (!port->current_format)
+ return 0;
+
+ spa_libcamera_stream_off(impl);
+ spa_libcamera_clear_buffers(impl, port);
+ port->current_format.reset();
+
+ spa_libcamera_close(impl);
+ goto done;
+ } else {
+ spa_zero(info);
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_video) {
+ spa_log_error(impl->log, "media type must be video");
+ return -EINVAL;
+ }
+
+ switch (info.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ if (spa_format_video_raw_parse(format, &info.info.raw) < 0) {
+ spa_log_error(impl->log, "can't parse video raw");
+ return -EINVAL;
+ }
+
+ if (port->current_format && info.media_type == port->current_format->media_type &&
+ info.media_subtype == port->current_format->media_subtype &&
+ info.info.raw.format == port->current_format->info.raw.format &&
+ info.info.raw.size.width == port->current_format->info.raw.size.width &&
+ info.info.raw.size.height == port->current_format->info.raw.size.height &&
+ info.info.raw.flags == port->current_format->info.raw.flags &&
+ (!(info.info.raw.flags & SPA_VIDEO_FLAG_MODIFIER) ||
+ (info.info.raw.modifier == port->current_format->info.raw.modifier)))
+ return 0;
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ if (spa_format_video_mjpg_parse(format, &info.info.mjpg) < 0)
+ return -EINVAL;
+
+ if (port->current_format && info.media_type == port->current_format->media_type &&
+ info.media_subtype == port->current_format->media_subtype &&
+ info.info.mjpg.size.width == port->current_format->info.mjpg.size.width &&
+ info.info.mjpg.size.height == port->current_format->info.mjpg.size.height)
+ return 0;
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ if (spa_format_video_h264_parse(format, &info.info.h264) < 0)
+ return -EINVAL;
+
+ if (port->current_format && info.media_type == port->current_format->media_type &&
+ info.media_subtype == port->current_format->media_subtype &&
+ info.info.h264.size.width == port->current_format->info.h264.size.width &&
+ info.info.h264.size.height == port->current_format->info.h264.size.height)
+ return 0;
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+
+ if (port->current_format && !(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
+ spa_libcamera_use_buffers(impl, port, NULL, 0);
+ port->current_format.reset();
+ }
+
+ if (spa_libcamera_set_format(impl, port, &info, flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) < 0)
+ return -EINVAL;
+
+ if (!(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
+ port->current_format = info;
+ }
+
+ done:
+ impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->current_format) {
+ impl->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ impl->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(impl, port, false);
+ emit_node_info(impl, false);
+
+ return 0;
+}
+
+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 *impl = (struct impl*)object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(object != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL);
+
+ port = GET_PORT(impl, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(impl, port, flags, param);
+ break;
+ default:
+ res = -ENOENT;
+ }
+ return res;
+}
+
+static int impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *impl = (struct impl*)object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL);
+
+ port = GET_PORT(impl, direction, port_id);
+
+ if (port->n_buffers) {
+ spa_libcamera_stream_off(impl);
+ if ((res = spa_libcamera_clear_buffers(impl, port)) < 0)
+ return res;
+ }
+ if (n_buffers > 0 && !port->current_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+ if (buffers == NULL)
+ return 0;
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) {
+ res = spa_libcamera_alloc_buffers(impl, port, buffers, n_buffers);
+ } else {
+ res = spa_libcamera_use_buffers(impl, port, buffers, n_buffers);
+ }
+ return res;
+}
+
+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 *impl = (struct impl*)object;
+ struct port *port;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL);
+
+ port = GET_PORT(impl, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = (struct spa_io_buffers*)data;
+ break;
+ case SPA_IO_Control:
+ port->control = (struct spa_io_sequence*)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 *impl = (struct impl*)object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+
+ port = GET_OUT_PORT(impl, port_id);
+
+ spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL);
+
+ res = spa_libcamera_buffer_recycle(impl, port, buffer_id);
+
+ return res;
+}
+
+static int process_control(struct impl *impl, struct spa_pod_sequence *control)
+{
+ struct spa_pod_control *c;
+
+ SPA_POD_SEQUENCE_FOREACH(control, c) {
+ switch (c->type) {
+ case SPA_CONTROL_Properties:
+ {
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) &c->value;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ spa_libcamera_set_control(impl, prop);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *impl = (struct impl*)object;
+ int res;
+ struct spa_io_buffers *io;
+ struct port *port;
+ struct buffer *b;
+
+ spa_return_val_if_fail(impl != NULL, -EINVAL);
+
+ port = GET_OUT_PORT(impl, 0);
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (port->control)
+ process_control(impl, &port->control->sequence);
+
+ spa_log_trace(impl->log, "%p; status %d", impl, io->status);
+
+ if (io->status == SPA_STATUS_HAVE_DATA) {
+ return SPA_STATUS_HAVE_DATA;
+ }
+
+ if (io->buffer_id < port->n_buffers) {
+ if ((res = spa_libcamera_buffer_recycle(impl, port, io->buffer_id)) < 0)
+ return res;
+
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (spa_list_is_empty(&port->queue)) {
+ return SPA_STATUS_OK;
+ }
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+
+ spa_log_trace(impl->log, "%p: dequeue buffer %d", impl, b->id);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *impl;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ impl = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &impl->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ std::destroy_at(reinterpret_cast<impl *>(handle));
+ return 0;
+}
+
+impl::impl(spa_log *log, spa_loop *data_loop, spa_system *system,
+ std::shared_ptr<CameraManager> manager, std::shared_ptr<Camera> camera, std::string device_id)
+ : handle({ SPA_VERSION_HANDLE, impl_get_interface, impl_clear }),
+ log(log),
+ data_loop(data_loop),
+ system(system),
+ device_id(std::move(device_id)),
+ out_ports{{this}},
+ manager(std::move(manager)),
+ camera(std::move(camera))
+{
+ libcamera_log_topic_init(log);
+
+ spa_hook_list_init(&hooks);
+
+ node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ params[NODE_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+
+ info.max_output_ports = 1;
+ info.flags = SPA_NODE_FLAG_RT;
+ info.params = params;
+ info.n_params = N_NODE_PARAMS;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ 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)
+{
+ const char *str;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ auto log = static_cast<spa_log *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log));
+ auto data_loop = static_cast<spa_loop *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop));
+ auto system = static_cast<spa_system *>(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System));
+
+ if (!data_loop) {
+ spa_log_error(log, "a data_loop is needed");
+ return -EINVAL;
+ }
+
+ if (!system) {
+ spa_log_error(log, "a system is needed");
+ return -EINVAL;
+ }
+
+ auto manager = libcamera_manager_acquire(res);
+ if (!manager) {
+ spa_log_error(log, "can't start camera manager: %s", spa_strerror(res));
+ return res;
+ }
+
+ std::string device_id;
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_LIBCAMERA_PATH)))
+ device_id = str;
+
+ auto camera = manager->get(device_id);
+ if (!camera) {
+ spa_log_error(log, "unknown camera id %s", device_id.c_str());
+ return -ENOENT;
+ }
+
+ new (handle) impl(log, data_loop, system,
+ std::move(manager), std::move(camera), std::move(device_id));
+
+ return 0;
+}
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+extern "C" {
+const struct spa_handle_factory spa_libcamera_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_LIBCAMERA_SOURCE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
+}
diff --git a/spa/plugins/libcamera/libcamera-utils.cpp b/spa/plugins/libcamera/libcamera-utils.cpp
new file mode 100644
index 0000000..af07484
--- /dev/null
+++ b/spa/plugins/libcamera/libcamera-utils.cpp
@@ -0,0 +1,990 @@
+/* Spa
+ *
+ * Copyright (C) 2020, Collabora Ltd.
+ * Author: Raghavendra Rao Sidlagatta <raghavendra.rao@collabora.com>
+ * Copyright (C) 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * libcamera-utils.cpp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sched.h>
+#include <errno.h>
+#include <sys/mman.h>
+#include <poll.h>
+#include <limits.h>
+
+#include <linux/media.h>
+
+int spa_libcamera_open(struct impl *impl)
+{
+ if (impl->acquired)
+ return 0;
+
+ spa_log_info(impl->log, "open camera %s", impl->device_id.c_str());
+ impl->camera->acquire();
+
+ impl->allocator = new FrameBufferAllocator(impl->camera);
+
+ impl->acquired = true;
+ return 0;
+}
+
+int spa_libcamera_close(struct impl *impl)
+{
+ struct port *port = &impl->out_ports[0];
+ if (!impl->acquired)
+ return 0;
+ if (impl->active || port->current_format)
+ return 0;
+
+ spa_log_info(impl->log, "close camera %s", impl->device_id.c_str());
+ delete impl->allocator;
+ impl->allocator = nullptr;
+
+ impl->camera->release();
+
+ impl->acquired = false;
+ return 0;
+}
+
+static void spa_libcamera_get_config(struct impl *impl)
+{
+ if (impl->config)
+ return;
+
+ StreamRoles roles;
+ roles.push_back(StreamRole::VideoRecording);
+ impl->config = impl->camera->generateConfiguration(roles);
+}
+
+static int spa_libcamera_buffer_recycle(struct impl *impl, struct port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+ int res;
+
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING))
+ return 0;
+
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUTSTANDING);
+
+ if (buffer_id >= impl->requestPool.size()) {
+ spa_log_warn(impl->log, "invalid buffer_id %u >= %zu",
+ buffer_id, impl->requestPool.size());
+ return -EINVAL;
+ }
+ Request *request = impl->requestPool[buffer_id].get();
+ Stream *stream = port->streamConfig.stream();
+ FrameBuffer *buffer = impl->allocator->buffers(stream)[buffer_id].get();
+ if ((res = request->addBuffer(stream, buffer)) < 0) {
+ spa_log_warn(impl->log, "can't add buffer %u for request: %s",
+ buffer_id, spa_strerror(res));
+ return -ENOMEM;
+ }
+ if (!impl->active) {
+ impl->pendingRequests.push_back(request);
+ return 0;
+ } else {
+ request->controls().merge(impl->ctrls);
+ impl->ctrls.clear();
+ if ((res = impl->camera->queueRequest(request)) < 0) {
+ spa_log_warn(impl->log, "can't queue buffer %u: %s",
+ buffer_id, spa_strerror(res));
+ return res == -EACCES ? -EBUSY : res;
+ }
+ }
+ return 0;
+}
+
+static int allocBuffers(struct impl *impl, struct port *port, unsigned int count)
+{
+ int res;
+
+ if ((res = impl->allocator->allocate(port->streamConfig.stream())) < 0)
+ return res;
+
+ for (unsigned int i = 0; i < count; i++) {
+ std::unique_ptr<Request> request = impl->camera->createRequest(i);
+ if (!request) {
+ impl->requestPool.clear();
+ return -ENOMEM;
+ }
+ impl->requestPool.push_back(std::move(request));
+ }
+ return res;
+}
+
+static void freeBuffers(struct impl *impl, struct port *port)
+{
+ impl->pendingRequests.clear();
+ impl->requestPool.clear();
+ impl->allocator->free(port->streamConfig.stream());
+}
+
+static int spa_libcamera_clear_buffers(struct impl *impl, struct port *port)
+{
+ uint32_t i;
+
+ if (port->n_buffers == 0)
+ return 0;
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d;
+
+ b = &port->buffers[i];
+ d = b->outbuf->datas;
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) {
+ spa_log_debug(impl->log, "queueing outstanding buffer %p", b);
+ spa_libcamera_buffer_recycle(impl, port, i);
+ }
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) {
+ munmap(SPA_PTROFF(b->ptr, -d[0].mapoffset, void),
+ d[0].maxsize - d[0].mapoffset);
+ }
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_ALLOCATED)) {
+ close(d[0].fd);
+ }
+ d[0].type = SPA_ID_INVALID;
+ }
+
+ freeBuffers(impl, port);
+ port->n_buffers = 0;
+
+ return 0;
+}
+
+struct format_info {
+ PixelFormat pix;
+ uint32_t format;
+ uint32_t media_type;
+ uint32_t media_subtype;
+};
+
+#define MAKE_FMT(pix,fmt,mt,mst) { pix, SPA_VIDEO_FORMAT_ ##fmt, SPA_MEDIA_TYPE_ ##mt, SPA_MEDIA_SUBTYPE_ ##mst }
+static const struct format_info format_info[] = {
+ /* RGB formats */
+ MAKE_FMT(formats::RGB565, RGB16, video, raw),
+ MAKE_FMT(formats::RGB565_BE, RGB16, video, raw),
+ MAKE_FMT(formats::RGB888, RGB, video, raw),
+ MAKE_FMT(formats::BGR888, BGR, video, raw),
+ MAKE_FMT(formats::XRGB8888, xRGB, video, raw),
+ MAKE_FMT(formats::XBGR8888, xBGR, video, raw),
+ MAKE_FMT(formats::RGBX8888, RGBx, video, raw),
+ MAKE_FMT(formats::BGRX8888, BGRx, video, raw),
+ MAKE_FMT(formats::ARGB8888, ARGB, video, raw),
+ MAKE_FMT(formats::ABGR8888, ABGR, video, raw),
+ MAKE_FMT(formats::RGBA8888, RGBA, video, raw),
+ MAKE_FMT(formats::BGRA8888, BGRA, video, raw),
+
+ MAKE_FMT(formats::YUYV, YUY2, video, raw),
+ MAKE_FMT(formats::YVYU, YVYU, video, raw),
+ MAKE_FMT(formats::UYVY, UYVY, video, raw),
+ MAKE_FMT(formats::VYUY, VYUY, video, raw),
+
+ MAKE_FMT(formats::NV12, NV12, video, raw),
+ MAKE_FMT(formats::NV21, NV21, video, raw),
+ MAKE_FMT(formats::NV16, NV16, video, raw),
+ MAKE_FMT(formats::NV61, NV61, video, raw),
+ MAKE_FMT(formats::NV24, NV24, video, raw),
+
+ MAKE_FMT(formats::YUV420, I420, video, raw),
+ MAKE_FMT(formats::YVU420, YV12, video, raw),
+ MAKE_FMT(formats::YUV422, Y42B, video, raw),
+
+ MAKE_FMT(formats::MJPEG, ENCODED, video, mjpg),
+#undef MAKE_FMT
+};
+
+static const struct format_info *video_format_to_info(const PixelFormat &pix) {
+ size_t i;
+
+ for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) {
+ if (format_info[i].pix == pix)
+ return &format_info[i];
+ }
+ return NULL;
+}
+
+static const struct format_info *find_format_info_by_media_type(uint32_t type,
+ uint32_t subtype, uint32_t format, int startidx)
+{
+ size_t i;
+
+ for (i = startidx; i < SPA_N_ELEMENTS(format_info); i++) {
+ if ((format_info[i].media_type == type) &&
+ (format_info[i].media_subtype == subtype) &&
+ (format == 0 || format_info[i].format == format))
+ return &format_info[i];
+ }
+ return NULL;
+}
+
+static int score_size(Size &a, Size &b)
+{
+ int x, y;
+ x = (int)a.width - (int)b.width;
+ y = (int)a.height - (int)b.height;
+ return x * x + y * y;
+}
+
+static int
+spa_libcamera_enum_format(struct impl *impl, struct port *port, int seq,
+ uint32_t start, uint32_t num, const struct spa_pod *filter)
+{
+ int res;
+ const struct format_info *info;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[2];
+ struct spa_result_node_params result;
+ struct spa_pod *fmt;
+ uint32_t i, count = 0, num_sizes;
+ PixelFormat format;
+ Size frameSize;
+ SizeRange sizeRange = SizeRange();
+
+ spa_libcamera_get_config(impl);
+
+ const StreamConfiguration& streamConfig = impl->config->at(0);
+ const StreamFormats &formats = streamConfig.formats();
+
+ result.id = SPA_PARAM_EnumFormat;
+ result.next = start;
+
+ if (result.next == 0) {
+ port->fmt_index = 0;
+ port->size_index = 0;
+ }
+next:
+ result.index = result.next++;
+
+next_fmt:
+ if (port->fmt_index >= formats.pixelformats().size())
+ goto enum_end;
+
+ format = formats.pixelformats()[port->fmt_index];
+
+ spa_log_debug(impl->log, "format: %s", format.toString().c_str());
+
+ info = video_format_to_info(format);
+ if (info == NULL) {
+ spa_log_debug(impl->log, "unknown format");
+ port->fmt_index++;
+ goto next_fmt;
+ }
+
+ num_sizes = formats.sizes(format).size();
+ if (num_sizes > 0 && port->size_index <= num_sizes) {
+ if (port->size_index == 0) {
+ Size wanted = Size(640, 480), test;
+ int score, best = INT_MAX;
+ for (i = 0; i < num_sizes; i++) {
+ test = formats.sizes(format)[i];
+ score = score_size(wanted, test);
+ if (score < best) {
+ best = score;
+ frameSize = test;
+ }
+ }
+ }
+ else {
+ frameSize = formats.sizes(format)[port->size_index - 1];
+ }
+ } else if (port->size_index < 1) {
+ sizeRange = formats.range(format);
+ if (sizeRange.hStep == 0 || sizeRange.vStep == 0) {
+ port->size_index = 0;
+ port->fmt_index++;
+ goto next_fmt;
+ }
+ } else {
+ port->size_index = 0;
+ port->fmt_index++;
+ goto next_fmt;
+ }
+ port->size_index++;
+
+ 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(info->media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(info->media_subtype),
+ 0);
+
+ if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_format, 0);
+ spa_pod_builder_id(&b, info->format);
+ }
+ if (info->pix.modifier()) {
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_modifier, 0);
+ spa_pod_builder_long(&b, info->pix.modifier());
+ }
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_size, 0);
+
+ if (sizeRange.hStep != 0 && sizeRange.vStep != 0) {
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Step, 0);
+ spa_pod_builder_frame(&b, &f[1]);
+ spa_pod_builder_rectangle(&b,
+ sizeRange.min.width,
+ sizeRange.min.height);
+ spa_pod_builder_rectangle(&b,
+ sizeRange.min.width,
+ sizeRange.min.height);
+ spa_pod_builder_rectangle(&b,
+ sizeRange.max.width,
+ sizeRange.max.height);
+ spa_pod_builder_rectangle(&b,
+ sizeRange.hStep,
+ sizeRange.vStep);
+ spa_pod_builder_pop(&b, &f[1]);
+
+ } else {
+ spa_pod_builder_rectangle(&b, frameSize.width, frameSize.height);
+ }
+
+ fmt = (struct spa_pod*) spa_pod_builder_pop(&b, &f[0]);
+
+ if (spa_pod_filter(&b, &result.param, fmt, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ enum_end:
+ res = 0;
+ return res;
+}
+
+static int spa_libcamera_set_format(struct impl *impl, struct port *port,
+ struct spa_video_info *format, bool try_only)
+{
+ const struct format_info *info = NULL;
+ uint32_t video_format;
+ struct spa_rectangle *size = NULL;
+ struct spa_fraction *framerate = NULL;
+ CameraConfiguration::Status validation;
+ int res;
+
+ switch (format->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ video_format = format->info.raw.format;
+ size = &format->info.raw.size;
+ framerate = &format->info.raw.framerate;
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ case SPA_MEDIA_SUBTYPE_jpeg:
+ video_format = SPA_VIDEO_FORMAT_ENCODED;
+ size = &format->info.mjpg.size;
+ framerate = &format->info.mjpg.framerate;
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ video_format = SPA_VIDEO_FORMAT_ENCODED;
+ size = &format->info.h264.size;
+ framerate = &format->info.h264.framerate;
+ break;
+ default:
+ video_format = SPA_VIDEO_FORMAT_ENCODED;
+ break;
+ }
+
+ info = find_format_info_by_media_type(format->media_type,
+ format->media_subtype, video_format, 0);
+ if (info == NULL || size == NULL || framerate == NULL) {
+ spa_log_error(impl->log, "unknown media type %d %d %d", format->media_type,
+ format->media_subtype, video_format);
+ return -EINVAL;
+ }
+ StreamConfiguration& streamConfig = impl->config->at(0);
+
+ streamConfig.pixelFormat = info->pix;
+ streamConfig.size.width = size->width;
+ streamConfig.size.height = size->height;
+ streamConfig.bufferCount = 8;
+
+ validation = impl->config->validate();
+ if (validation == CameraConfiguration::Invalid)
+ return -EINVAL;
+
+ if (try_only)
+ return 0;
+
+ if ((res = spa_libcamera_open(impl)) < 0)
+ return res;
+
+ res = impl->camera->configure(impl->config.get());
+ if (res != 0)
+ goto error;
+
+ port->streamConfig = impl->config->at(0);
+
+ if ((res = allocBuffers(impl, port, port->streamConfig.bufferCount)) < 0)
+ goto error;
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_RATE;
+ port->info.flags = SPA_PORT_FLAG_CAN_ALLOC_BUFFERS |
+ SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ port->info.rate = SPA_FRACTION(port->rate.num, port->rate.denom);
+
+ return 0;
+error:
+ spa_libcamera_close(impl);
+ return res;
+
+}
+
+static int
+spa_libcamera_enum_controls(struct impl *impl, struct port *port, int seq,
+ uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ const ControlInfoMap &info = impl->camera->controls();
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[2];
+ struct spa_result_node_params result;
+ struct spa_pod *ctrl;
+ uint32_t count = 0, skip;
+ int res;
+ const ControlId *ctrl_id;
+ ControlInfo ctrl_info;
+
+ result.id = SPA_PARAM_PropInfo;
+ result.next = start;
+
+ auto it = info.begin();
+ for (skip = result.next; skip; skip--)
+ it++;
+
+ if (false) {
+next:
+ it++;
+ }
+ result.index = result.next++;
+ if (it == info.end())
+ goto enum_end;
+
+ ctrl_id = it->first;
+ ctrl_info = it->second;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_id, SPA_POD_Id(ctrl_id->id()),
+ SPA_PROP_INFO_description, SPA_POD_String(ctrl_id->name().c_str()),
+ 0);
+
+ switch (ctrl_id->type()) {
+ case ControlTypeBool: {
+ bool def;
+ if (ctrl_info.def().isNone())
+ def = ctrl_info.min().get<bool>();
+ else
+ def = ctrl_info.def().get<bool>();
+
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(
+ def),
+ 0);
+ } break;
+ case ControlTypeFloat: {
+ float min = ctrl_info.min().get<float>();
+ float max = ctrl_info.max().get<float>();
+ float def;
+
+ if (ctrl_info.def().isNone())
+ def = (min + max) / 2;
+ else
+ def = ctrl_info.def().get<float>();
+
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
+ def, min, max),
+ 0);
+ } break;
+ case ControlTypeInteger32: {
+ int32_t min = ctrl_info.min().get<int32_t>();
+ int32_t max = ctrl_info.max().get<int32_t>();
+ int32_t def;
+
+ if (ctrl_info.def().isNone())
+ def = (min + max) / 2;
+ else
+ def = ctrl_info.def().get<int32_t>();
+
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(
+ def, min, max),
+ 0);
+ } break;
+ default:
+ goto next;
+ }
+
+ ctrl = (struct spa_pod*) spa_pod_builder_pop(&b, &f[0]);
+
+ if (spa_pod_filter(&b, &result.param, ctrl, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+enum_end:
+ res = 0;
+ return res;
+}
+
+struct val {
+ uint32_t type;
+ float f_val;
+ int32_t i_val;
+ bool b_val;
+ uint32_t id;
+};
+
+static int do_update_ctrls(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *impl = (struct impl *)user_data;
+ const struct val *d = (const struct val *)data;
+ switch (d->type) {
+ case ControlTypeBool:
+ impl->ctrls.set(d->id, d->b_val);
+ break;
+ case ControlTypeFloat:
+ impl->ctrls.set(d->id, d->f_val);
+ break;
+ case ControlTypeInteger32:
+ //impl->ctrls.set(d->id, (int32_t)d->i_val);
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+
+static int
+spa_libcamera_set_control(struct impl *impl, const struct spa_pod_prop *prop)
+{
+ const ControlInfoMap &info = impl->camera->controls();
+ const ControlId *ctrl_id;
+ int res;
+ struct val d;
+
+ auto v = info.idmap().find(prop->key);
+ if (v == info.idmap().end())
+ return -ENOENT;
+
+ ctrl_id = v->second;
+
+ d.type = ctrl_id->type();
+ d.id = ctrl_id->id();
+
+ switch (d.type) {
+ case ControlTypeBool:
+ if ((res = spa_pod_get_bool(&prop->value, &d.b_val)) < 0)
+ goto done;
+ break;
+ case ControlTypeFloat:
+ if ((res = spa_pod_get_float(&prop->value, &d.f_val)) < 0)
+ goto done;
+ break;
+ case ControlTypeInteger32:
+ if ((res = spa_pod_get_int(&prop->value, &d.i_val)) < 0)
+ goto done;
+ break;
+ default:
+ res = -EINVAL;
+ goto done;
+ }
+ spa_loop_invoke(impl->data_loop, do_update_ctrls, 0, &d, sizeof(d), true, impl);
+ res = 0;
+done:
+ return res;
+}
+
+
+static void libcamera_on_fd_events(struct spa_source *source)
+{
+ struct impl *impl = (struct impl*) source->data;
+ struct spa_io_buffers *io;
+ struct port *port = &impl->out_ports[0];
+ uint32_t index, buffer_id;
+ struct buffer *b;
+ uint64_t cnt;
+
+ if (source->rmask & SPA_IO_ERR) {
+ spa_log_error(impl->log, "libcamera %p: error %08x", impl, source->rmask);
+ if (impl->source.loop)
+ spa_loop_remove_source(impl->data_loop, &impl->source);
+ return;
+ }
+
+ if (!(source->rmask & SPA_IO_IN)) {
+ spa_log_warn(impl->log, "libcamera %p: spurious wakeup %d", impl, source->rmask);
+ return;
+ }
+
+ if (spa_system_eventfd_read(impl->system, impl->source.fd, &cnt) < 0) {
+ spa_log_error(impl->log, "Failed to read on event fd");
+ return;
+ }
+
+ if (spa_ringbuffer_get_read_index(&port->ring, &index) < 1) {
+ spa_log_error(impl->log, "nothing is queued");
+ return;
+ }
+ buffer_id = port->ring_ids[index & MASK_BUFFERS];
+ spa_ringbuffer_read_update(&port->ring, index + 1);
+
+ b = &port->buffers[buffer_id];
+ spa_list_append(&port->queue, &b->link);
+
+ io = port->io;
+ if (io == NULL) {
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+ spa_libcamera_buffer_recycle(impl, port, b->id);
+ } else if (io->status != SPA_STATUS_HAVE_DATA) {
+ if (io->buffer_id < port->n_buffers)
+ spa_libcamera_buffer_recycle(impl, port, io->buffer_id);
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ spa_log_trace(impl->log, "libcamera %p: now queued %d", impl, b->id);
+ }
+ spa_node_call_ready(&impl->callbacks, SPA_STATUS_HAVE_DATA);
+}
+
+static int spa_libcamera_use_buffers(struct impl *impl, struct port *port,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ return -ENOTSUP;
+}
+
+static const struct {
+ Transform libcamera_transform;
+ uint32_t spa_transform_value;
+} transform_map[] = {
+ { Transform::Identity, SPA_META_TRANSFORMATION_None },
+ { Transform::Rot0, SPA_META_TRANSFORMATION_None },
+ { Transform::HFlip, SPA_META_TRANSFORMATION_Flipped },
+ { Transform::VFlip, SPA_META_TRANSFORMATION_Flipped180 },
+ { Transform::HVFlip, SPA_META_TRANSFORMATION_180 },
+ { Transform::Rot180, SPA_META_TRANSFORMATION_180 },
+ { Transform::Transpose, SPA_META_TRANSFORMATION_Flipped90 },
+ { Transform::Rot90, SPA_META_TRANSFORMATION_90 },
+ { Transform::Rot270, SPA_META_TRANSFORMATION_270 },
+ { Transform::Rot180Transpose, SPA_META_TRANSFORMATION_Flipped270 },
+};
+
+static uint32_t libcamera_transform_to_spa_transform_value(Transform transform)
+{
+ for (const auto& t : transform_map) {
+ if (t.libcamera_transform == transform)
+ return t.spa_transform_value;
+ }
+ return SPA_META_TRANSFORMATION_None;
+}
+
+static int
+mmap_init(struct impl *impl, struct port *port,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ unsigned int i, j;
+ struct spa_data *d;
+ Stream *stream = impl->config->at(0).stream();
+ const std::vector<std::unique_ptr<FrameBuffer>> &bufs =
+ impl->allocator->buffers(stream);
+
+ if (n_buffers > 0) {
+ if (bufs.size() != n_buffers)
+ return -EINVAL;
+
+ d = buffers[0]->datas;
+
+ if (d[0].type != SPA_ID_INVALID &&
+ d[0].type & (1u << SPA_DATA_DmaBuf)) {
+ port->memtype = SPA_DATA_DmaBuf;
+ } else if (d[0].type != SPA_ID_INVALID &&
+ d[0].type & (1u << SPA_DATA_MemFd)) {
+ port->memtype = SPA_DATA_MemFd;
+ } else if (d[0].type & (1u << SPA_DATA_MemPtr)) {
+ port->memtype = SPA_DATA_MemPtr;
+ } else {
+ spa_log_error(impl->log, "v4l2: can't use buffers of type %d", d[0].type);
+ return -EINVAL;
+ }
+ }
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+
+ if (buffers[i]->n_datas < 1) {
+ spa_log_error(impl->log, "invalid buffer data");
+ return -EINVAL;
+ }
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->flags = BUFFER_FLAG_OUTSTANDING;
+ b->h = (struct spa_meta_header*)spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ b->videotransform = (struct spa_meta_videotransform*)spa_buffer_find_meta_data(
+ buffers[i], SPA_META_VideoTransform, sizeof(*b->videotransform));
+ if (b->videotransform) {
+ b->videotransform->transform =
+ libcamera_transform_to_spa_transform_value(impl->config->transform);
+ spa_log_debug(impl->log, "Setting videotransform for buffer %d to %u (from %s)",
+ i, b->videotransform->transform, transformToString(impl->config->transform));
+
+ }
+
+ d = buffers[i]->datas;
+ for(j = 0; j < buffers[i]->n_datas; ++j) {
+ 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;
+
+ if (port->memtype == SPA_DATA_DmaBuf ||
+ port->memtype == SPA_DATA_MemFd) {
+ d[j].fd = bufs[i]->planes()[j].fd.get();
+ spa_log_debug(impl->log, "Got fd = %ld for buffer: #%d", d[j].fd, i);
+ d[j].data = NULL;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_ALLOCATED);
+ }
+ else if(port->memtype == SPA_DATA_MemPtr) {
+ d[j].fd = -1;
+ d[j].data = mmap(NULL,
+ d[j].maxsize + d[j].mapoffset,
+ PROT_READ, MAP_SHARED,
+ bufs[i]->planes()[j].fd.get(),
+ 0);
+ if (d[j].data == MAP_FAILED) {
+ spa_log_error(impl->log, "mmap: %m");
+ continue;
+ }
+ b->ptr = d[j].data;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED);
+ spa_log_debug(impl->log, "mmap ptr:%p", d[j].data);
+ } else {
+ spa_log_error(impl->log, "invalid buffer type");
+ return -EIO;
+ }
+ }
+ spa_libcamera_buffer_recycle(impl, port, i);
+ }
+ port->n_buffers = n_buffers;
+ spa_log_debug(impl->log, "we have %d buffers", n_buffers);
+
+ return 0;
+}
+
+static int
+spa_libcamera_alloc_buffers(struct impl *impl, struct port *port,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ int res;
+
+ if (port->n_buffers > 0)
+ return -EIO;
+
+ if ((res = mmap_init(impl, port, buffers, n_buffers)) < 0)
+ return res;
+
+ return 0;
+}
+
+
+void impl::requestComplete(libcamera::Request *request)
+{
+ struct impl *impl = this;
+ struct port *port = &impl->out_ports[0];
+ Stream *stream = port->streamConfig.stream();
+ uint32_t index, buffer_id;
+ struct buffer *b;
+
+ spa_log_debug(impl->log, "request complete");
+
+ buffer_id = request->cookie();
+ b = &port->buffers[buffer_id];
+
+ if ((request->status() == Request::RequestCancelled)) {
+ spa_log_debug(impl->log, "Request was cancelled");
+ request->reuse();
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+ spa_libcamera_buffer_recycle(impl, port, b->id);
+ return;
+ }
+ FrameBuffer *buffer = request->findBuffer(stream);
+ if (buffer == nullptr) {
+ spa_log_warn(impl->log, "unknown buffer");
+ return;
+ }
+ const FrameMetadata &fmd = buffer->metadata();
+
+
+
+ if (impl->clock) {
+ impl->clock->nsec = fmd.timestamp;
+ impl->clock->rate = port->rate;
+ 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;
+ }
+ if (b->h) {
+ b->h->flags = 0;
+ b->h->offset = 0;
+ b->h->seq = fmd.sequence;
+ b->h->pts = fmd.timestamp;
+ b->h->dts_offset = 0;
+ }
+ request->reuse();
+
+ spa_ringbuffer_get_write_index(&port->ring, &index);
+ port->ring_ids[index & MASK_BUFFERS] = buffer_id;
+ spa_ringbuffer_write_update(&port->ring, index + 1);
+
+ if (spa_system_eventfd_write(impl->system, impl->source.fd, 1) < 0)
+ spa_log_error(impl->log, "Failed to write on event fd");
+
+}
+
+static int spa_libcamera_stream_on(struct impl *impl)
+{
+ struct port *port = &impl->out_ports[0];
+ int res;
+
+ if (!port->current_format) {
+ spa_log_error(impl->log, "Exting %s with -EIO", __FUNCTION__);
+ return -EIO;
+ }
+
+ if (impl->active)
+ return 0;
+
+ impl->camera->requestCompleted.connect(impl, &impl::requestComplete);
+
+ spa_log_info(impl->log, "starting camera %s", impl->device_id.c_str());
+ if ((res = impl->camera->start()) < 0)
+ goto error;
+
+ for (Request *req : impl->pendingRequests) {
+ if ((res = impl->camera->queueRequest(req)) < 0)
+ goto error_stop;
+ }
+ impl->pendingRequests.clear();
+
+ 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);
+ impl->source.mask = SPA_IO_IN | SPA_IO_ERR;
+ impl->source.rmask = 0;
+ if (impl->source.fd < 0) {
+ spa_log_error(impl->log, "Failed to create eventfd: %s", spa_strerror(impl->source.fd));
+ res = impl->source.fd;
+ goto error_stop;
+ }
+ spa_loop_add_source(impl->data_loop, &impl->source);
+
+ impl->active = true;
+
+ return 0;
+
+error_stop:
+ impl->camera->stop();
+error:
+ impl->camera->requestCompleted.disconnect(impl, &impl::requestComplete);
+ return res == -EACCES ? -EBUSY : res;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *impl = (struct impl *)user_data;
+ if (impl->source.loop)
+ spa_loop_remove_source(loop, &impl->source);
+ return 0;
+}
+
+static int spa_libcamera_stream_off(struct impl *impl)
+{
+ struct port *port = &impl->out_ports[0];
+ int res;
+
+ if (!impl->active) {
+ for (std::unique_ptr<Request> &req : impl->requestPool)
+ req->reuse();
+ return 0;
+ }
+
+ impl->active = false;
+ spa_log_info(impl->log, "stopping camera %s", impl->device_id.c_str());
+ impl->pendingRequests.clear();
+
+ if ((res = impl->camera->stop()) < 0) {
+ spa_log_warn(impl->log, "error stopping camera %s: %s",
+ impl->device_id.c_str(), spa_strerror(res));
+ }
+
+ impl->camera->requestCompleted.disconnect(impl, &impl::requestComplete);
+
+ spa_loop_invoke(impl->data_loop, do_remove_source, 0, NULL, 0, true, impl);
+ if (impl->source.fd >= 0) {
+ spa_system_close(impl->system, impl->source.fd);
+ impl->source.fd = -1;
+ }
+
+ spa_list_init(&port->queue);
+
+ return 0;
+}
diff --git a/spa/plugins/libcamera/libcamera.c b/spa/plugins/libcamera/libcamera.c
new file mode 100644
index 0000000..bcf24d2
--- /dev/null
+++ b/spa/plugins/libcamera/libcamera.c
@@ -0,0 +1,57 @@
+/* Spa libcamera support
+ *
+ * Copyright © 2020 collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+
+#include "libcamera.h"
+
+struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.libcamera");
+struct spa_log_topic *libcamera_log_topic = &log_topic;
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *factory = &spa_libcamera_manager_factory;
+ break;
+ case 1:
+ *factory = &spa_libcamera_device_factory;
+ break;
+ case 2:
+ *factory = &spa_libcamera_source_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/libcamera/libcamera.h b/spa/plugins/libcamera/libcamera.h
new file mode 100644
index 0000000..1a4d618
--- /dev/null
+++ b/spa/plugins/libcamera/libcamera.h
@@ -0,0 +1,51 @@
+/* Spa libcamera support
+ *
+ * Copyright © 2020 collabora
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <linux/media.h>
+
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+extern const struct spa_handle_factory spa_libcamera_source_factory;
+extern const struct spa_handle_factory spa_libcamera_manager_factory;
+extern const struct spa_handle_factory spa_libcamera_device_factory;
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT libcamera_log_topic
+extern struct spa_log_topic *libcamera_log_topic;
+
+static inline void libcamera_log_topic_init(struct spa_log *log)
+{
+ spa_log_topic_init(log, libcamera_log_topic);
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/spa/plugins/libcamera/meson.build b/spa/plugins/libcamera/meson.build
new file mode 100644
index 0000000..abb1a42
--- /dev/null
+++ b/spa/plugins/libcamera/meson.build
@@ -0,0 +1,17 @@
+libcamera_sources = [
+ 'libcamera.c',
+ 'libcamera-manager.cpp',
+ 'libcamera-device.cpp',
+ 'libcamera-source.cpp'
+]
+
+libdrm_dep = dependency('libdrm', version : '>= 2.4.98',
+ required : get_option('libcamera'))
+summary({'libdrm': libdrm_dep.found()}, bool_yn: true, section: 'Backend')
+if libdrm_dep.found()
+ libcameralib = shared_library('spa-libcamera',
+ libcamera_sources,
+ dependencies : [ spa_dep, libudev_dep, libcamera_dep, pthread_lib, libdrm_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'libcamera')
+endif
diff --git a/spa/plugins/meson.build b/spa/plugins/meson.build
new file mode 100644
index 0000000..d641dd8
--- /dev/null
+++ b/spa/plugins/meson.build
@@ -0,0 +1,58 @@
+if alsa_dep.found()
+ subdir('alsa')
+endif
+if get_option('avb').require(host_machine.system() == 'linux', error_message: 'AVB support is only available on Linux').allowed()
+ subdir('avb')
+endif
+if get_option('audioconvert').allowed()
+ subdir('audioconvert')
+endif
+if get_option('audiomixer').allowed()
+ subdir('audiomixer')
+endif
+if get_option('control').allowed()
+ subdir('control')
+endif
+if get_option('audiotestsrc').allowed()
+ subdir('audiotestsrc')
+endif
+if bluez_deps_found
+ subdir('bluez5')
+endif
+if avcodec_dep.found()
+ subdir('ffmpeg')
+endif
+if jack_dep.found()
+ subdir('jack')
+endif
+if get_option('support').allowed()
+ subdir('support')
+endif
+if get_option('test').allowed()
+ subdir('test')
+endif
+if get_option('videoconvert').allowed()
+ subdir('videoconvert')
+endif
+if get_option('videotestsrc').allowed()
+ subdir('videotestsrc')
+endif
+if get_option('volume').allowed()
+ subdir('volume')
+endif
+if vulkan_headers
+ subdir('vulkan')
+endif
+
+v4l2_header_found = cc.has_header('linux/videodev2.h', required: get_option('v4l2'))
+v4l2_supported = libudev_dep.found() and v4l2_header_found
+summary({'V4L2 kernel header': v4l2_header_found}, bool_yn: true, section: 'Backend')
+summary({'V4L2 enabled': v4l2_supported}, bool_yn: true, section: 'Backend')
+if v4l2_supported
+ subdir('v4l2')
+endif
+if libcamera_dep.found()
+ subdir('libcamera')
+endif
+
+subdir('aec')
diff --git a/spa/plugins/support/cpu-arm.c b/spa/plugins/support/cpu-arm.c
new file mode 100644
index 0000000..6cd68d8
--- /dev/null
+++ b/spa/plugins/support/cpu-arm.c
@@ -0,0 +1,137 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <spa/utils/string.h>
+
+#define MAX_BUFFER 4096
+
+static char *get_cpuinfo_line(char *cpuinfo, const char *tag)
+{
+ char *line, *end, *colon;
+
+ if (!(line = strstr(cpuinfo, tag)))
+ return NULL;
+
+ if (!(end = strchr(line, '\n')))
+ return NULL;
+
+ if (!(colon = strchr(line, ':')))
+ return NULL;
+
+ if (++colon >= end)
+ return NULL;
+
+ return strndup(colon, end - colon);
+}
+
+static int
+arm_init(struct impl *impl)
+{
+ uint32_t flags = 0;
+ char *cpuinfo, *line, buffer[MAX_BUFFER];
+ int arch;
+
+ if (!(cpuinfo = spa_cpu_read_file("/proc/cpuinfo", buffer, sizeof(buffer)))) {
+ spa_log_warn(impl->log, "%p: Can't read cpuinfo", impl);
+ return 1;
+ }
+
+ if ((line = get_cpuinfo_line(cpuinfo, "CPU architecture"))) {
+ arch = strtoul(line, NULL, 0);
+ if (arch >= 6)
+ flags |= SPA_CPU_FLAG_ARMV6;
+ if (arch >= 8)
+ flags |= SPA_CPU_FLAG_ARMV8;
+
+ free(line);
+ }
+
+ if ((line = get_cpuinfo_line(cpuinfo, "Features"))) {
+ char *state = NULL;
+ char *current = strtok_r(line, " ", &state);
+
+ do {
+#if defined (__aarch64__)
+ if (spa_streq(current, "asimd"))
+ flags |= SPA_CPU_FLAG_NEON;
+ else if (spa_streq(current, "fp"))
+ flags |= SPA_CPU_FLAG_VFPV3 | SPA_CPU_FLAG_VFP;
+#else
+ if (spa_streq(current, "vfp"))
+ flags |= SPA_CPU_FLAG_VFP;
+ else if (spa_streq(current, "neon"))
+ flags |= SPA_CPU_FLAG_NEON;
+ else if (spa_streq(current, "vfpv3"))
+ flags |= SPA_CPU_FLAG_VFPV3;
+#endif
+ } while ((current = strtok_r(NULL, " ", &state)));
+
+ free(line);
+ }
+
+ impl->flags = flags;
+
+ return 0;
+}
+
+
+static int arm_zero_denormals(void *object, bool enable)
+{
+#if defined(__aarch64__)
+ uint64_t cw;
+ if (enable)
+ __asm__ __volatile__(
+ "mrs %0, fpcr \n"
+ "orr %0, %0, #0x1000000 \n"
+ "msr fpcr, %0 \n"
+ "isb \n"
+ : "=r"(cw)::"memory");
+ else
+ __asm__ __volatile__(
+ "mrs %0, fpcr \n"
+ "and %0, %0, #~0x1000000 \n"
+ "msr fpcr, %0 \n"
+ "isb \n"
+ : "=r"(cw)::"memory");
+#elif (defined(__VFP_FP__) && !defined(__SOFTFP__))
+ uint32_t cw;
+ if (enable)
+ __asm__ __volatile__(
+ "vmrs %0, fpscr \n"
+ "orr %0, %0, #0x1000000 \n"
+ "vmsr fpscr, %0 \n"
+ : "=r"(cw)::"memory");
+ else
+ __asm__ __volatile__(
+ "vmrs %0, fpscr \n"
+ "and %0, %0, #~0x1000000 \n"
+ "vmsr fpscr, %0 \n"
+ : "=r"(cw)::"memory");
+#endif
+ return 0;
+}
diff --git a/spa/plugins/support/cpu-x86.c b/spa/plugins/support/cpu-x86.c
new file mode 100644
index 0000000..722f7c9
--- /dev/null
+++ b/spa/plugins/support/cpu-x86.c
@@ -0,0 +1,204 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <cpuid.h>
+
+static int
+x86_init(struct impl *impl)
+{
+ uint32_t flags;
+
+ unsigned int vendor;
+ unsigned int model, family;
+ unsigned int max_level, ext_level, has_osxsave;
+ unsigned int eax, ebx, ecx, edx;
+
+
+ max_level = __get_cpuid_max(0, &vendor);
+ if (max_level < 1)
+ return 0;
+
+ __cpuid(1, eax, ebx, ecx, edx);
+
+ model = (eax >> 4) & 0x0f;
+ family = (eax >> 8) & 0x0f;
+
+ if (vendor == signature_INTEL_ebx ||
+ vendor == signature_AMD_ebx) {
+ unsigned int extended_model, extended_family;
+
+ extended_model = (eax >> 12) & 0xf0;
+ extended_family = (eax >> 20) & 0xff;
+ if (family == 0x0f) {
+ family += extended_family;
+ model += extended_model;
+ } else if (family == 0x06)
+ model += extended_model;
+ }
+ (void)model;
+
+ flags = 0;
+ if (ecx & bit_SSE3)
+ flags |= SPA_CPU_FLAG_SSE3;
+ if (ecx & bit_SSSE3)
+ flags |= SPA_CPU_FLAG_SSSE3;
+ if (ecx & bit_SSE4_1)
+ flags |= SPA_CPU_FLAG_SSE41;
+ if (ecx & bit_SSE4_2)
+ flags |= SPA_CPU_FLAG_SSE42;
+ if (ecx & bit_AVX)
+ flags |= SPA_CPU_FLAG_AVX;
+ has_osxsave = ecx & bit_OSXSAVE;
+ if (ecx & bit_FMA)
+ flags |= SPA_CPU_FLAG_FMA3;
+
+ if (edx & bit_CMOV)
+ flags |= SPA_CPU_FLAG_CMOV;
+ if (edx & bit_MMX)
+ flags |= SPA_CPU_FLAG_MMX;
+ if (edx & bit_MMXEXT)
+ flags |= SPA_CPU_FLAG_MMXEXT;
+ if (edx & bit_SSE)
+ flags |= SPA_CPU_FLAG_SSE;
+ if (edx & bit_SSE2)
+ flags |= SPA_CPU_FLAG_SSE2;
+
+
+ if (max_level >= 7) {
+ __cpuid_count(7, 0, eax, ebx, ecx, edx);
+
+ if (ebx & bit_BMI)
+ flags |= SPA_CPU_FLAG_BMI1;
+ if (ebx & bit_AVX2)
+ flags |= SPA_CPU_FLAG_AVX2;
+ if (ebx & bit_BMI2)
+ flags |= SPA_CPU_FLAG_BMI2;
+#define AVX512_BITS (bit_AVX512F | bit_AVX512DQ | bit_AVX512CD | bit_AVX512BW | bit_AVX512VL)
+ if ((ebx & AVX512_BITS) == AVX512_BITS)
+ flags |= SPA_CPU_FLAG_AVX512;
+ }
+
+ /* Check cpuid level of extended features. */
+ __cpuid (0x80000000, ext_level, ebx, ecx, edx);
+
+ if (ext_level >= 0x80000001) {
+ __cpuid (0x80000001, eax, ebx, ecx, edx);
+
+ if (edx & bit_3DNOW)
+ flags |= SPA_CPU_FLAG_3DNOW;
+ if (edx & bit_3DNOWP)
+ flags |= SPA_CPU_FLAG_3DNOWEXT;
+ if (edx & bit_MMX)
+ flags |= SPA_CPU_FLAG_MMX;
+ if (edx & bit_MMXEXT)
+ flags |= SPA_CPU_FLAG_MMXEXT;
+ if (ecx & bit_FMA4)
+ flags |= SPA_CPU_FLAG_FMA4;
+ if (ecx & bit_XOP)
+ flags |= SPA_CPU_FLAG_XOP;
+ }
+
+ /* Get XCR_XFEATURE_ENABLED_MASK register with xgetbv. */
+#define XCR_XFEATURE_ENABLED_MASK 0x0
+#define XSTATE_FP 0x1
+#define XSTATE_SSE 0x2
+#define XSTATE_YMM 0x4
+#define XSTATE_OPMASK 0x20
+#define XSTATE_ZMM 0x40
+#define XSTATE_HI_ZMM 0x80
+
+#define XCR_AVX_ENABLED_MASK \
+ (XSTATE_SSE | XSTATE_YMM)
+#define XCR_AVX512F_ENABLED_MASK \
+ (XSTATE_SSE | XSTATE_YMM | XSTATE_OPMASK | XSTATE_ZMM | XSTATE_HI_ZMM)
+
+ if (has_osxsave)
+ asm (".byte 0x0f; .byte 0x01; .byte 0xd0"
+ : "=a" (eax), "=d" (edx)
+ : "c" (XCR_XFEATURE_ENABLED_MASK));
+ else
+ eax = 0;
+
+ /* Check if AVX registers are supported. */
+ if ((eax & XCR_AVX_ENABLED_MASK) != XCR_AVX_ENABLED_MASK) {
+ flags &= ~(SPA_CPU_FLAG_AVX |
+ SPA_CPU_FLAG_AVX2 |
+ SPA_CPU_FLAG_FMA3 |
+ SPA_CPU_FLAG_FMA4 |
+ SPA_CPU_FLAG_XOP);
+ }
+
+ /* Check if AVX512F registers are supported. */
+ if ((eax & XCR_AVX512F_ENABLED_MASK) != XCR_AVX512F_ENABLED_MASK) {
+ flags &= ~SPA_CPU_FLAG_AVX512;
+ }
+
+ if (flags & SPA_CPU_FLAG_AVX512)
+ impl->max_align = 64;
+ else if (flags & (SPA_CPU_FLAG_AVX2 |
+ SPA_CPU_FLAG_AVX |
+ SPA_CPU_FLAG_XOP |
+ SPA_CPU_FLAG_FMA4 |
+ SPA_CPU_FLAG_FMA3))
+ impl->max_align = 32;
+ else if (flags & (SPA_CPU_FLAG_AESNI |
+ SPA_CPU_FLAG_SSE42 |
+ SPA_CPU_FLAG_SSE41 |
+ SPA_CPU_FLAG_SSSE3 |
+ SPA_CPU_FLAG_SSE3 |
+ SPA_CPU_FLAG_SSE2 |
+ SPA_CPU_FLAG_SSE))
+ impl->max_align = 16;
+ else
+ impl->max_align = 8;
+
+ impl->flags = flags;
+
+ return 0;
+}
+
+#if defined(HAVE_SSE)
+#include <xmmintrin.h>
+#endif
+
+static int x86_zero_denormals(void *object, bool enable)
+{
+#if defined(HAVE_SSE)
+ struct impl *impl = object;
+ if (impl->flags & SPA_CPU_FLAG_SSE) {
+ unsigned int mxcsr;
+ mxcsr = _mm_getcsr();
+ if (enable)
+ mxcsr |= 0x8040;
+ else
+ mxcsr &= ~0x8040;
+ _mm_setcsr(mxcsr);
+ spa_log_debug(impl->log, "%p: zero-denormals:%s",
+ impl, enable ? "on" : "off");
+ }
+ return 0;
+#else
+ return -ENOTSUP;
+#endif
+}
diff --git a/spa/plugins/support/cpu.c b/spa/plugins/support/cpu.c
new file mode 100644
index 0000000..fc13c16
--- /dev/null
+++ b/spa/plugins/support/cpu.c
@@ -0,0 +1,313 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <stdio.h>
+#include <sched.h>
+#include <fcntl.h>
+
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#include <sys/sysctl.h>
+#endif
+
+#include <spa/support/log.h>
+#include <spa/support/cpu.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/type.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.cpu");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_cpu cpu;
+
+ struct spa_log *log;
+
+ uint32_t flags;
+ uint32_t force;
+ uint32_t count;
+ uint32_t max_align;
+ uint32_t vm_type;
+};
+
+char *spa_cpu_read_file(const char *name, char *buffer, size_t len)
+{
+ int n, fd;
+
+ if ((fd = open(name, O_RDONLY | O_CLOEXEC, 0)) < 0)
+ return NULL;
+
+ if ((n = read(fd, buffer, len-1)) < 0) {
+ close(fd);
+ return NULL;
+ }
+ buffer[n] = '\0';
+ close(fd);
+ return buffer;
+}
+
+# if defined (__i386__) || defined (__x86_64__)
+#include "cpu-x86.c"
+#define init(t) x86_init(t)
+#define impl_cpu_zero_denormals x86_zero_denormals
+# elif defined (__arm__) || defined (__aarch64__)
+#include "cpu-arm.c"
+#define init(t) arm_init(t)
+#define impl_cpu_zero_denormals arm_zero_denormals
+# else
+#define init(t)
+#define impl_cpu_zero_denormals NULL
+#endif
+
+static uint32_t
+impl_cpu_get_flags(void *object)
+{
+ struct impl *impl = object;
+ if (impl->force != SPA_CPU_FORCE_AUTODETECT)
+ return impl->force;
+ return impl->flags;
+}
+
+static int
+impl_cpu_force_flags(void *object, uint32_t flags)
+{
+ struct impl *impl = object;
+ impl->force = flags;
+ return 0;
+}
+
+#ifndef __FreeBSD__
+static uint32_t get_count(struct impl *this)
+{
+ cpu_set_t cpuset;
+ CPU_ZERO(&cpuset);
+ if (sched_getaffinity(0, sizeof(cpuset), &cpuset) == 0)
+ return CPU_COUNT(&cpuset);
+ return 1;
+}
+#else
+static uint32_t get_count(struct impl *this)
+{
+ static const int mib[] = {CTL_HW, HW_NCPU};
+ int r;
+ size_t rSize = sizeof(r);
+ if(-1 == sysctl(mib, 2, &r, &rSize, 0, 0))
+ return 1;
+ return r;
+}
+#endif
+
+static uint32_t
+impl_cpu_get_count(void *object)
+{
+ struct impl *impl = object;
+ return impl->count;
+}
+
+static uint32_t
+impl_cpu_get_max_align(void *object)
+{
+ struct impl *impl = object;
+ return impl->max_align;
+}
+
+static uint32_t
+impl_cpu_get_vm_type(void *object)
+{
+ struct impl *impl = object;
+
+ if (impl->vm_type != 0)
+ return impl->vm_type;
+
+#if defined(__i386__) || defined(__x86_64__) || defined(__arm__) || defined(__aarch64__)
+ static const char *const dmi_vendors[] = {
+ "/sys/class/dmi/id/product_name", /* Test this before sys_vendor to detect KVM over QEMU */
+ "/sys/class/dmi/id/sys_vendor",
+ "/sys/class/dmi/id/board_vendor",
+ "/sys/class/dmi/id/bios_vendor"
+ };
+ static const struct {
+ const char *vendor;
+ int id;
+ } dmi_vendor_table[] = {
+ { "KVM", SPA_CPU_VM_KVM },
+ { "QEMU", SPA_CPU_VM_QEMU },
+ { "VMware", SPA_CPU_VM_VMWARE }, /* https://kb.vmware.com/s/article/1009458 */
+ { "VMW", SPA_CPU_VM_VMWARE },
+ { "innotek GmbH", SPA_CPU_VM_ORACLE },
+ { "Oracle Corporation", SPA_CPU_VM_ORACLE },
+ { "Xen", SPA_CPU_VM_XEN },
+ { "Bochs", SPA_CPU_VM_BOCHS },
+ { "Parallels", SPA_CPU_VM_PARALLELS },
+ /* https://wiki.freebsd.org/bhyve */
+ { "BHYVE", SPA_CPU_VM_BHYVE },
+ };
+
+ SPA_FOR_EACH_ELEMENT_VAR(dmi_vendors, dv) {
+ char buffer[256], *s;
+
+ if ((s = spa_cpu_read_file(*dv, buffer, sizeof(buffer))) == NULL)
+ continue;
+
+ SPA_FOR_EACH_ELEMENT_VAR(dmi_vendor_table, t) {
+ if (spa_strstartswith(s, t->vendor)) {
+ spa_log_debug(impl->log, "Virtualization %s found in DMI (%s)",
+ s, *dv);
+ impl->vm_type = t->id;
+ goto done;
+ }
+ }
+ }
+done:
+#endif
+ return impl->vm_type;
+}
+
+static const struct spa_cpu_methods impl_cpu = {
+ SPA_VERSION_CPU_METHODS,
+ .get_flags = impl_cpu_get_flags,
+ .force_flags = impl_cpu_force_flags,
+ .get_count = impl_cpu_get_count,
+ .get_max_align = impl_cpu_get_max_align,
+ .get_vm_type = impl_cpu_get_vm_type,
+ .zero_denormals = impl_cpu_zero_denormals,
+};
+
+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_CPU))
+ *interface = &this->cpu;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static 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;
+ const char *str;
+
+ 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->cpu.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_CPU,
+ SPA_VERSION_CPU,
+ &impl_cpu, this);
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(this->log, &log_topic);
+
+ this->flags = 0;
+ this->force = SPA_CPU_FORCE_AUTODETECT;
+ this->max_align = 16;
+ this->count = get_count(this);
+ init(this);
+
+ if (info) {
+ if ((str = spa_dict_lookup(info, SPA_KEY_CPU_FORCE)) != NULL)
+ this->flags = atoi(str);
+ if ((str = spa_dict_lookup(info, SPA_KEY_CPU_VM_TYPE)) != NULL)
+ this->vm_type = atoi(str);
+ if ((str = spa_dict_lookup(info, SPA_KEY_CPU_ZERO_DENORMALS)) != NULL)
+ spa_cpu_zero_denormals(&this->cpu, spa_atob(str));
+ }
+
+ spa_log_debug(this->log, "%p: count:%d align:%d flags:%08x",
+ this, this->count, this->max_align, this->flags);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_CPU,},
+};
+
+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_support_cpu_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_SUPPORT_CPU,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/support/dbus.c b/spa/plugins/support/dbus.c
new file mode 100644
index 0000000..a6b5e58
--- /dev/null
+++ b/spa/plugins/support/dbus.c
@@ -0,0 +1,592 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/type.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+#include <spa/support/plugin.h>
+#include <spa/support/dbus.h>
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.dbus");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_dbus dbus;
+
+ struct spa_log *log;
+ struct spa_loop_utils *utils;
+
+ struct spa_list connection_list;
+};
+
+struct source_data {
+ struct spa_list link;
+ struct spa_source *source;
+ struct connection *conn;
+};
+
+#define connection_emit(c,m,v,...) spa_hook_list_call(&c->listener_list, struct spa_dbus_connection_events, m, v, ##__VA_ARGS__)
+#define connection_emit_destroy(c) connection_emit(c, destroy, 0)
+#define connection_emit_disconnected(c) connection_emit(c, disconnected, 0)
+
+struct connection {
+ struct spa_list link;
+
+ struct spa_dbus_connection this;
+ struct impl *impl;
+ enum spa_dbus_type type;
+ DBusConnection *conn;
+ struct spa_source *dispatch_event;
+ struct spa_list source_list;
+
+ struct spa_hook_list listener_list;
+};
+
+static void source_data_free(void *data)
+{
+ struct source_data *d = data;
+ struct connection *conn = d->conn;
+ struct impl *impl = conn->impl;
+
+ spa_list_remove(&d->link);
+ spa_loop_utils_destroy_source(impl->utils, d->source);
+ free(d);
+}
+
+static void dispatch_cb(void *userdata)
+{
+ struct connection *conn = userdata;
+ struct impl *impl = conn->impl;
+
+ if (dbus_connection_dispatch(conn->conn) == DBUS_DISPATCH_COMPLETE)
+ spa_loop_utils_enable_idle(impl->utils, conn->dispatch_event, false);
+}
+
+static void dispatch_status(DBusConnection *conn, DBusDispatchStatus status, void *userdata)
+{
+ struct connection *c = userdata;
+ struct impl *impl = c->impl;
+
+ spa_loop_utils_enable_idle(impl->utils, c->dispatch_event,
+ status == DBUS_DISPATCH_COMPLETE ? false : true);
+}
+
+static inline uint32_t dbus_to_io(DBusWatch *watch)
+{
+ uint32_t mask;
+ unsigned int flags;
+
+ /* no watch flags for disabled watches */
+ if (!dbus_watch_get_enabled(watch))
+ return 0;
+
+ flags = dbus_watch_get_flags(watch);
+ mask = SPA_IO_HUP | SPA_IO_ERR;
+
+ if (flags & DBUS_WATCH_READABLE)
+ mask |= SPA_IO_IN;
+ if (flags & DBUS_WATCH_WRITABLE)
+ mask |= SPA_IO_OUT;
+
+ return mask;
+}
+
+static inline unsigned int io_to_dbus(uint32_t mask)
+{
+ unsigned int flags = 0;
+
+ if (mask & SPA_IO_IN)
+ flags |= DBUS_WATCH_READABLE;
+ if (mask & SPA_IO_OUT)
+ flags |= DBUS_WATCH_WRITABLE;
+ if (mask & SPA_IO_HUP)
+ flags |= DBUS_WATCH_HANGUP;
+ if (mask & SPA_IO_ERR)
+ flags |= DBUS_WATCH_ERROR;
+ return flags;
+}
+
+static void
+handle_io_event(void *userdata, int fd, uint32_t mask)
+{
+ DBusWatch *watch = userdata;
+
+ if (!dbus_watch_get_enabled(watch)) {
+ fprintf(stderr, "Asked to handle disabled watch: %p %i", (void *) watch, fd);
+ return;
+ }
+ dbus_watch_handle(watch, io_to_dbus(mask));
+}
+
+static dbus_bool_t add_watch(DBusWatch *watch, void *userdata)
+{
+ struct connection *conn = userdata;
+ struct impl *impl = conn->impl;
+ struct source_data *data;
+
+ spa_log_debug(impl->log, "add watch %p %d", watch, dbus_watch_get_unix_fd(watch));
+
+ data = calloc(1, sizeof(struct source_data));
+ data->conn = conn;
+ /* we dup because dbus tends to add the same fd multiple times and our epoll
+ * implementation does not like that */
+ data->source = spa_loop_utils_add_io(impl->utils,
+ dup(dbus_watch_get_unix_fd(watch)),
+ dbus_to_io(watch), true, handle_io_event, watch);
+ spa_list_append(&conn->source_list, &data->link);
+
+ dbus_watch_set_data(watch, data, source_data_free);
+ return TRUE;
+}
+
+static void remove_watch(DBusWatch *watch, void *userdata)
+{
+ struct connection *conn = userdata;
+ struct impl *impl = conn->impl;
+ spa_log_debug(impl->log, "remove watch %p", watch);
+ dbus_watch_set_data(watch, NULL, NULL);
+}
+
+static void toggle_watch(DBusWatch *watch, void *userdata)
+{
+ struct connection *conn = userdata;
+ struct impl *impl = conn->impl;
+ struct source_data *data;
+
+ spa_log_debug(impl->log, "toggle watch %p", watch);
+
+ if ((data = dbus_watch_get_data(watch)) == NULL)
+ return;
+
+ spa_loop_utils_update_io(impl->utils, data->source, dbus_to_io(watch));
+}
+
+static void
+handle_timer_event(void *userdata, uint64_t expirations)
+{
+ DBusTimeout *timeout = userdata;
+ uint64_t t;
+ struct timespec ts;
+ struct source_data *data;
+ struct connection *conn;
+ struct impl *impl;
+
+ if ((data = dbus_timeout_get_data(timeout)) == NULL)
+ return;
+
+ conn = data->conn;
+ impl = conn->impl;
+
+ spa_log_debug(impl->log, "timeout %p conn:%p impl:%p", timeout, conn, impl);
+
+ if (dbus_timeout_get_enabled(timeout)) {
+ t = dbus_timeout_get_interval(timeout) * SPA_NSEC_PER_MSEC;
+ ts.tv_sec = t / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = t % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(impl->utils,
+ data->source, &ts, NULL, false);
+ dbus_timeout_handle(timeout);
+ }
+}
+
+static dbus_bool_t add_timeout(DBusTimeout *timeout, void *userdata)
+{
+ struct connection *conn = userdata;
+ struct impl *impl = conn->impl;
+ struct timespec ts;
+ struct source_data *data;
+ uint64_t t;
+
+ if (!dbus_timeout_get_enabled(timeout))
+ return FALSE;
+
+ spa_log_debug(impl->log, "add timeout %p conn:%p impl:%p", timeout, conn, impl);
+
+ data = calloc(1, sizeof(struct source_data));
+ data->conn = conn;
+ data->source = spa_loop_utils_add_timer(impl->utils, handle_timer_event, timeout);
+ spa_list_append(&conn->source_list, &data->link);
+
+ dbus_timeout_set_data(timeout, data, source_data_free);
+
+ t = dbus_timeout_get_interval(timeout) * SPA_NSEC_PER_MSEC;
+ ts.tv_sec = t / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = t % SPA_NSEC_PER_SEC;
+ spa_loop_utils_update_timer(impl->utils, data->source, &ts, NULL, false);
+
+ return TRUE;
+}
+
+static void remove_timeout(DBusTimeout *timeout, void *userdata)
+{
+ struct connection *conn = userdata;
+ struct impl *impl = conn->impl;
+ spa_log_debug(impl->log, "remove timeout %p conn:%p impl:%p", timeout, conn, impl);
+ dbus_timeout_set_data(timeout, NULL, NULL);
+}
+
+static void toggle_timeout(DBusTimeout *timeout, void *userdata)
+{
+ struct connection *conn = userdata;
+ struct impl *impl = conn->impl;
+ struct source_data *data;
+ struct timespec ts, *tsp;
+
+ if ((data = dbus_timeout_get_data(timeout)) == NULL)
+ return;
+
+ spa_log_debug(impl->log, "toggle timeout %p conn:%p impl:%p", timeout, conn, impl);
+
+ if (dbus_timeout_get_enabled(timeout)) {
+ uint64_t t = dbus_timeout_get_interval(timeout) * SPA_NSEC_PER_MSEC;
+ ts.tv_sec = t / SPA_NSEC_PER_SEC;
+ ts.tv_nsec = t % SPA_NSEC_PER_SEC;
+ tsp = &ts;
+ } else {
+ tsp = NULL;
+ }
+ spa_loop_utils_update_timer(impl->utils, data->source, tsp, NULL, false);
+}
+
+static void wakeup_main(void *userdata)
+{
+ struct connection *this = userdata;
+ struct impl *impl = this->impl;
+
+ spa_loop_utils_enable_idle(impl->utils, this->dispatch_event, true);
+}
+
+static void connection_close(struct connection *this);
+
+static DBusHandlerResult filter_message (DBusConnection *connection,
+ DBusMessage *message, void *user_data)
+{
+ struct connection *this = user_data;
+ struct impl *impl = this->impl;
+
+ if (dbus_message_is_signal(message, DBUS_INTERFACE_LOCAL, "Disconnected")) {
+ spa_log_debug(impl->log, "dbus connection %p disconnected", this);
+ connection_close(this);
+ connection_emit_disconnected(this);
+ }
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+static const char *type_to_string(enum spa_dbus_type type) {
+ switch (type) {
+ case SPA_DBUS_TYPE_SESSION: return "session";
+ case SPA_DBUS_TYPE_SYSTEM: return "system";
+ case SPA_DBUS_TYPE_STARTER: return "starter";
+ default: return "unknown";
+ }
+}
+
+static void *
+impl_connection_get(struct spa_dbus_connection *conn)
+{
+ struct connection *this = SPA_CONTAINER_OF(conn, struct connection, this);
+ struct impl *impl = this->impl;
+ DBusError error;
+
+ if (this->conn != NULL)
+ return this->conn;
+
+ dbus_error_init(&error);
+
+ this->conn = dbus_bus_get_private((DBusBusType)this->type, &error);
+ if (this->conn == NULL)
+ goto error;
+
+ dbus_connection_set_exit_on_disconnect(this->conn, false);
+ if (!dbus_connection_add_filter(this->conn, filter_message, this, NULL))
+ goto error_filter;
+
+ dbus_connection_set_dispatch_status_function(this->conn, dispatch_status, this, NULL);
+ dbus_connection_set_watch_functions(this->conn, add_watch, remove_watch, toggle_watch, this,
+ NULL);
+ dbus_connection_set_timeout_functions(this->conn, add_timeout, remove_timeout,
+ toggle_timeout, this, NULL);
+ dbus_connection_set_wakeup_main_function(this->conn, wakeup_main, this, NULL);
+
+ return this->conn;
+
+error:
+ spa_log_error(impl->log, "Failed to connect to %s bus: %s", type_to_string(this->type), error.message);
+ dbus_error_free(&error);
+ errno = ECONNREFUSED;
+ return NULL;
+error_filter:
+ spa_log_error(impl->log, "Failed to create filter");
+ dbus_connection_close(this->conn);
+ dbus_connection_unref(this->conn);
+ this->conn = NULL;
+ errno = ENOMEM;
+ return NULL;
+}
+
+
+static void connection_close(struct connection *this)
+{
+ if (this->conn) {
+ dbus_connection_remove_filter(this->conn, filter_message, this);
+ dbus_connection_close(this->conn);
+
+ /* Someone may still hold a ref to the handle from get(), so the
+ * unref below may not be the final one. For that case, reset
+ * all callbacks we defined to be sure they are not called. */
+ dbus_connection_set_dispatch_status_function(this->conn, NULL, NULL, NULL);
+ dbus_connection_set_watch_functions(this->conn, NULL, NULL, NULL, NULL, NULL);
+ dbus_connection_set_timeout_functions(this->conn, NULL, NULL, NULL, NULL, NULL);
+ dbus_connection_set_wakeup_main_function(this->conn, NULL, NULL, NULL);
+
+ dbus_connection_unref(this->conn);
+ }
+ this->conn = NULL;
+}
+
+static void connection_free(struct connection *conn)
+{
+ struct impl *impl = conn->impl;
+ struct source_data *data;
+
+ spa_list_remove(&conn->link);
+
+ connection_close(conn);
+
+ spa_list_consume(data, &conn->source_list, link)
+ source_data_free(data);
+
+ spa_loop_utils_destroy_source(impl->utils, conn->dispatch_event);
+
+ spa_hook_list_clean(&conn->listener_list);
+
+ free(conn);
+}
+
+static void
+impl_connection_destroy(struct spa_dbus_connection *conn)
+{
+ struct connection *this = SPA_CONTAINER_OF(conn, struct connection, this);
+ struct impl *impl = this->impl;
+
+ connection_emit_destroy(this);
+
+ spa_log_debug(impl->log, "destroy conn %p", this);
+ connection_free(this);
+}
+
+static void
+impl_connection_add_listener(struct spa_dbus_connection *conn,
+ struct spa_hook *listener,
+ const struct spa_dbus_connection_events *events,
+ void *data)
+{
+ struct connection *this = SPA_CONTAINER_OF(conn, struct connection, this);
+ spa_hook_list_append(&this->listener_list, listener, events, data);
+}
+
+static const struct spa_dbus_connection impl_connection = {
+ SPA_VERSION_DBUS_CONNECTION,
+ impl_connection_get,
+ impl_connection_destroy,
+ impl_connection_add_listener,
+};
+
+static struct spa_dbus_connection *
+impl_get_connection(void *object,
+ enum spa_dbus_type type)
+{
+ struct impl *impl = object;
+ struct connection *conn;
+ int res;
+
+ conn = calloc(1, sizeof(struct connection));
+ conn->this = impl_connection;
+ conn->impl = impl;
+ conn->type = type;
+ conn->dispatch_event = spa_loop_utils_add_idle(impl->utils,
+ false, dispatch_cb, conn);
+ if (conn->dispatch_event == NULL)
+ goto no_event;
+
+ spa_list_init(&conn->source_list);
+ spa_hook_list_init(&conn->listener_list);
+
+ spa_list_append(&impl->connection_list, &conn->link);
+
+ spa_log_debug(impl->log, "new conn %p", conn);
+
+ return &conn->this;
+
+no_event:
+ res = -errno;
+ spa_log_error(impl->log, "Failed to create idle event: %m");
+ free(conn);
+ errno = -res;
+ return NULL;
+}
+
+static const struct spa_dbus_methods impl_dbus = {
+ SPA_VERSION_DBUS_METHODS,
+ .get_connection = impl_get_connection,
+};
+
+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_DBus))
+ *interface = &this->dbus;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *impl = (struct impl *) handle;
+ struct connection *conn;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ spa_list_consume(conn, &impl->connection_list, link)
+ connection_free(conn);
+ 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;
+
+ 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;
+ spa_list_init(&this->connection_list);
+
+ this->dbus.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_DBus,
+ SPA_VERSION_DBUS,
+ &impl_dbus, this);
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(this->log, &log_topic);
+
+ this->utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
+
+ if (this->utils == NULL) {
+ spa_log_error(this->log, "a LoopUtils is needed");
+ return -EINVAL;
+ }
+
+ spa_log_debug(this->log, "%p: initialized", this);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_DBus,},
+};
+
+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_handle_factory dbus_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_SUPPORT_DBUS,
+ 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 = &dbus_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/support/evl-plugin.c b/spa/plugins/support/evl-plugin.c
new file mode 100644
index 0000000..e140cf7
--- /dev/null
+++ b/spa/plugins/support/evl-plugin.c
@@ -0,0 +1,47 @@
+/* Spa Support plugin
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_support_evl_system_factory;
+
+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_support_evl_system_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/support/evl-system.c b/spa/plugins/support/evl-system.c
new file mode 100644
index 0000000..58130a0
--- /dev/null
+++ b/spa/plugins/support/evl-system.c
@@ -0,0 +1,460 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <sys/eventfd.h>
+#include <sys/signalfd.h>
+
+#include <evl/evl.h>
+#include <evl/timer.h>
+
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/type.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#define NAME "evl-system"
+
+#define MAX_POLL 512
+
+struct poll_entry {
+ int pfd;
+ int fd;
+ uint32_t events;
+ void *data;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_system system;
+
+ struct spa_log *log;
+
+ struct poll_entry entries[MAX_POLL];
+ uint32_t n_entries;
+
+ uint32_t n_xbuf;
+ int attached;
+ int pid;
+};
+
+static ssize_t impl_read(void *object, int fd, void *buf, size_t count)
+{
+ return oob_read(fd, buf, count);
+}
+
+static ssize_t impl_write(void *object, int fd, const void *buf, size_t count)
+{
+ return oob_write(fd, buf, count);
+}
+
+static int impl_ioctl(void *object, int fd, unsigned long request, ...)
+{
+ int res;
+ va_list ap;
+ long arg;
+
+ va_start(ap, request);
+ arg = va_arg(ap, long);
+ res = oob_ioctl(fd, request, arg);
+ va_end(ap);
+
+ return res;
+}
+
+static int impl_close(void *object, int fd)
+{
+ return close(fd);
+}
+
+static inline int clock_id_to_evl(int clockid)
+{
+ switch(clockid) {
+ case CLOCK_MONOTONIC:
+ return EVL_CLOCK_MONOTONIC;
+ case CLOCK_REALTIME:
+ return EVL_CLOCK_REALTIME;
+ default:
+ return -clockid;
+ }
+}
+
+/* clock */
+static int impl_clock_gettime(void *object,
+ int clockid, struct timespec *value)
+{
+ return evl_read_clock(clock_id_to_evl(clockid), value);
+}
+
+static int impl_clock_getres(void *object,
+ int clockid, struct timespec *res)
+{
+ return evl_get_clock_resolution(clock_id_to_evl(clockid), res);
+}
+
+/* poll */
+static int impl_pollfd_create(void *object, int flags)
+{
+ int retval;
+ retval = evl_new_poll();
+ return retval;
+}
+
+static inline struct poll_entry *find_entry(struct impl *impl, int pfd, int fd)
+{
+ uint32_t i;
+ for (i = 0; i < impl->n_entries; i++) {
+ struct poll_entry *e = &impl->entries[i];
+ if (e->pfd == pfd && e->fd == fd)
+ return e;
+ }
+ return NULL;
+}
+
+static int impl_pollfd_add(void *object, int pfd, int fd, uint32_t events, void *data)
+{
+ struct impl *impl = object;
+ struct poll_entry *e;
+
+ if (impl->n_entries == MAX_POLL)
+ return -ENOSPC;
+
+ e = &impl->entries[impl->n_entries++];
+ e->pfd = pfd;
+ e->fd = fd;
+ e->events = events;
+ e->data = data;
+ return evl_add_pollfd(pfd, fd, e->events);
+}
+
+static int impl_pollfd_mod(void *object, int pfd, int fd, uint32_t events, void *data)
+{
+ struct impl *impl = object;
+ struct poll_entry *e;
+
+ e = find_entry(impl, pfd, fd);
+ if (e == NULL)
+ return -ENOENT;
+
+ e->events = events;
+ e->data = data;
+ return evl_mod_pollfd(pfd, fd, e->events);
+}
+
+static int impl_pollfd_del(void *object, int pfd, int fd)
+{
+ struct impl *impl = object;
+ struct poll_entry *e;
+
+ e = find_entry(impl, pfd, fd);
+ if (e == NULL)
+ return -ENOENT;
+
+ e->pfd = -1;
+ e->fd = -1;
+ return evl_del_pollfd(pfd, fd);
+}
+
+static int impl_pollfd_wait(void *object, int pfd,
+ struct spa_poll_event *ev, int n_ev, int timeout)
+{
+ struct impl *impl = object;
+ struct evl_poll_event pollset[n_ev];
+ struct timespec tv;
+ int i, j, res;
+
+ if (impl->attached == 0) {
+ res = evl_attach_self("evl-thread-%d-%p", impl->pid, impl);
+ if (res < 0)
+ return res;
+ impl->attached = res;
+ }
+
+ if (timeout == -1) {
+ tv.tv_sec = 0;
+ tv.tv_nsec = 0;
+ } else {
+ tv.tv_sec = timeout / SPA_MSEC_PER_SEC;
+ tv.tv_nsec = (timeout % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC;
+ }
+ res = evl_timedpoll(pfd, pollset, n_ev, &tv);
+ if (SPA_UNLIKELY(res < 0))
+ return res;
+
+ for (i = 0, j = 0; i < res; i++) {
+ struct poll_entry *e;
+
+ e = find_entry(impl, pfd, pollset[i].fd);
+ if (e == NULL)
+ continue;
+
+ ev[j].events = pollset[i].events;
+ ev[j].data = e->data;
+ j++;
+ }
+ return j;
+}
+
+/* timers */
+static int impl_timerfd_create(void *object, int clockid, int flags)
+{
+ int cid;
+
+ switch (clockid) {
+ case CLOCK_MONOTONIC:
+ cid = EVL_CLOCK_MONOTONIC;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return evl_new_timer(cid);
+}
+
+static int impl_timerfd_settime(void *object,
+ int fd, int flags,
+ const struct itimerspec *new_value,
+ struct itimerspec *old_value)
+{
+ struct itimerspec val = *new_value;
+
+ if (!(flags & SPA_FD_TIMER_ABSTIME)) {
+ struct timespec now;
+
+ evl_read_clock(EVL_CLOCK_MONOTONIC, &now);
+ val.it_value.tv_sec += now.tv_sec;
+ val.it_value.tv_nsec += now.tv_nsec;
+ if (val.it_value.tv_nsec >= 1000000000) {
+ val.it_value.tv_sec++;
+ val.it_value.tv_nsec -= 1000000000;
+ }
+ }
+ return evl_set_timer(fd, &val, old_value);
+}
+
+static int impl_timerfd_gettime(void *object,
+ int fd, struct itimerspec *curr_value)
+{
+ return evl_get_timer(fd, curr_value);
+
+}
+static int impl_timerfd_read(void *object, int fd, uint64_t *expirations)
+{
+ uint32_t ticks;
+ if (oob_read(fd, &ticks, sizeof(ticks)) != sizeof(ticks))
+ return -errno;
+ *expirations = ticks;
+ return 0;
+}
+
+/* events */
+static int impl_eventfd_create(void *object, int flags)
+{
+ struct impl *impl = object;
+ int res;
+
+ res = evl_new_xbuf(1024, 1024, "xbuf-%d-%p-%d", impl->pid, impl, impl->n_xbuf);
+ if (res < 0)
+ return res;
+
+ impl->n_xbuf++;
+
+ if (flags & SPA_FD_NONBLOCK)
+ fcntl(res, F_SETFL, fcntl(res, F_GETFL) | O_NONBLOCK);
+
+ return res;
+}
+
+static int impl_eventfd_write(void *object, int fd, uint64_t count)
+{
+ if (write(fd, &count, sizeof(uint64_t)) != sizeof(uint64_t))
+ return -errno;
+ return 0;
+}
+
+static int impl_eventfd_read(void *object, int fd, uint64_t *count)
+{
+ if (oob_read(fd, count, sizeof(uint64_t)) != sizeof(uint64_t))
+ return -errno;
+ return 0;
+}
+
+/* signals */
+static int impl_signalfd_create(void *object, int signal, int flags)
+{
+ sigset_t mask;
+ int res, fl = 0;
+
+ if (flags & SPA_FD_CLOEXEC)
+ fl |= SFD_CLOEXEC;
+ if (flags & SPA_FD_NONBLOCK)
+ fl |= SFD_NONBLOCK;
+
+ sigemptyset(&mask);
+ sigaddset(&mask, signal);
+ res = signalfd(-1, &mask, fl);
+ sigprocmask(SIG_BLOCK, &mask, NULL);
+
+ return res;
+}
+
+static int impl_signalfd_read(void *object, int fd, int *signal)
+{
+ struct signalfd_siginfo signal_info;
+ int len;
+
+ len = read(fd, &signal_info, sizeof signal_info);
+ if (!(len == -1 && errno == EAGAIN) && len != sizeof signal_info)
+ return -errno;
+
+ *signal = signal_info.ssi_signo;
+
+ return 0;
+}
+
+static const struct spa_system_methods impl_system = {
+ SPA_VERSION_SYSTEM_METHODS,
+ .read = impl_read,
+ .write = impl_write,
+ .ioctl = impl_ioctl,
+ .close = impl_close,
+ .clock_gettime = impl_clock_gettime,
+ .clock_getres = impl_clock_getres,
+ .pollfd_create = impl_pollfd_create,
+ .pollfd_add = impl_pollfd_add,
+ .pollfd_mod = impl_pollfd_mod,
+ .pollfd_del = impl_pollfd_del,
+ .pollfd_wait = impl_pollfd_wait,
+ .timerfd_create = impl_timerfd_create,
+ .timerfd_settime = impl_timerfd_settime,
+ .timerfd_gettime = impl_timerfd_gettime,
+ .timerfd_read = impl_timerfd_read,
+ .eventfd_create = impl_eventfd_create,
+ .eventfd_write = impl_eventfd_write,
+ .eventfd_read = impl_eventfd_read,
+ .signalfd_create = impl_signalfd_create,
+ .signalfd_read = impl_signalfd_read,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *impl;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ impl = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_System))
+ *interface = &impl->system;
+ 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 *impl;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ impl = (struct impl *) handle;
+ impl->system.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_System,
+ SPA_VERSION_SYSTEM,
+ &impl_system, impl);
+
+ impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+
+ impl->pid = getpid();
+
+ if ((res = evl_attach_self("evl-system-%d-%p", impl->pid, impl)) < 0) {
+ spa_log_error(impl->log, NAME " %p: init failed: %s", impl, spa_strerror(res));
+ return res;
+ }
+
+ spa_log_debug(impl->log, NAME " %p: initialized", impl);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_System,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_support_evl_system_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_SUPPORT_SYSTEM,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info
+};
diff --git a/spa/plugins/support/journal.c b/spa/plugins/support/journal.c
new file mode 100644
index 0000000..e0e58eb
--- /dev/null
+++ b/spa/plugins/support/journal.c
@@ -0,0 +1,341 @@
+/* Spa
+ *
+ * Copyright © 2020 Sergey Bugaev
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <stdio.h>
+#include <limits.h>
+#include <syslog.h>
+#include <sys/stat.h>
+
+#include <spa/support/log.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/type.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+
+#include <systemd/sd-journal.h>
+
+#include "log-patterns.h"
+
+#define NAME "journal"
+
+#define DEFAULT_LOG_LEVEL SPA_LOG_LEVEL_INFO
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_log log;
+
+ /* if non-null, we'll additionally forward all logging to there */
+ struct spa_log *chain_log;
+
+ struct spa_list patterns;
+};
+
+static SPA_PRINTF_FUNC(7,0) void
+impl_log_logtv(void *object,
+ 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)
+{
+ struct impl *impl = object;
+ char line_buffer[32];
+ char file_buffer[strlen("CODE_FILE=") + strlen(file) + 1];
+ char message_buffer[LINE_MAX];
+ int priority;
+ size_t sz = 0;
+
+ if (impl->chain_log != NULL) {
+ va_list args_copy;
+ va_copy(args_copy, args);
+ spa_log_logtv(impl->chain_log,
+ level, topic,
+ file, line, func, fmt, args_copy);
+ va_end(args_copy);
+ }
+
+ /* convert SPA log level to syslog priority */
+ switch (level) {
+ case SPA_LOG_LEVEL_ERROR:
+ priority = LOG_ERR;
+ break;
+ case SPA_LOG_LEVEL_WARN:
+ priority = LOG_WARNING;
+ break;
+ case SPA_LOG_LEVEL_INFO:
+ priority = LOG_INFO;
+ break;
+ case SPA_LOG_LEVEL_DEBUG:
+ case SPA_LOG_LEVEL_TRACE:
+ default:
+ priority = LOG_DEBUG;
+ break;
+ }
+
+ if (topic)
+ sz = spa_scnprintf(message_buffer, sizeof(message_buffer),
+ "%s: ", topic->topic);
+
+ /* we'll be using the low-level journal API, which expects us to provide
+ * the location explicitly. line and file are to be passed as preformatted
+ * entries, whereas the function name is passed as-is, and converted into
+ * a field inside sd_journal_send_with_location(). */
+ snprintf(line_buffer, sizeof(line_buffer), "CODE_LINE=%d", line);
+ snprintf(file_buffer, sizeof(file_buffer), "CODE_FILE=%s", file);
+ vsnprintf(message_buffer + sz, sizeof(message_buffer) - sz, fmt, args);
+
+ sd_journal_send_with_location(file_buffer, line_buffer, func,
+ "MESSAGE=%s", message_buffer,
+ "PRIORITY=%i", priority,
+ NULL);
+}
+
+static SPA_PRINTF_FUNC(6,7) void
+impl_log_log(void *object,
+ enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ impl_log_logtv(object, level, NULL, file, line, func, fmt, args);
+ va_end(args);
+}
+
+static SPA_PRINTF_FUNC(6,0) void
+impl_log_logv(void *object,
+ enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt,
+ va_list args)
+{
+ impl_log_logtv(object, level, NULL, file, line, func, fmt, args);
+}
+
+static SPA_PRINTF_FUNC(7,8) void
+impl_log_logt(void *object,
+ 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;
+ va_start(args, fmt);
+ impl_log_logtv(object, level, topic, file, line, func, fmt, args);
+ va_end(args);
+}
+
+static void
+impl_log_topic_init(void *object, struct spa_log_topic *t)
+{
+ struct impl *impl = object;
+ enum spa_log_level level = impl->log.level;
+
+ support_log_topic_init(&impl->patterns, level, t);
+}
+
+static const struct spa_log_methods impl_log = {
+ SPA_VERSION_LOG_METHODS,
+ .log = impl_log_log,
+ .logv = impl_log_logv,
+ .logt = impl_log_logt,
+ .logtv = impl_log_logtv,
+ .topic_init = impl_log_topic_init,
+};
+
+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_Log))
+ *interface = &this->log;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+ support_log_free_patterns(&this->patterns);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+/** Determine if our stderr goes straight to the journal */
+static int
+stderr_is_connected_to_journal(void)
+{
+ const char *journal_stream;
+ unsigned long long journal_device, journal_inode;
+ struct stat stderr_stat;
+
+ /* when a service's stderr is connected to the journal, systemd sets
+ * JOURNAL_STREAM in the environment of that service to device:inode
+ * of its stderr. if the variable is not set, clearly our stderr is
+ * not connected to the journal */
+ journal_stream = getenv("JOURNAL_STREAM");
+ if (journal_stream == NULL)
+ return 0;
+
+ /* if it *is* set, that doesn't immediately mean that *our* stderr
+ * is (still) connected to the journal. to know for sure, we have to
+ * compare our actual stderr to the stream systemd has created for
+ * the service we're a part of */
+
+ if (sscanf(journal_stream, "%llu:%llu", &journal_device, &journal_inode) != 2)
+ return 0;
+
+ if (fstat(STDERR_FILENO, &stderr_stat) < 0)
+ return 0;
+
+ return stderr_stat.st_dev == journal_device && stderr_stat.st_ino == journal_inode;
+}
+
+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;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ impl = (struct impl *) handle;
+
+ impl->log.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Log,
+ SPA_VERSION_LOG,
+ &impl_log, impl);
+ impl->log.level = DEFAULT_LOG_LEVEL;
+
+ spa_list_init(&impl->patterns);
+
+ if (info) {
+ if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LEVEL)) != NULL)
+ impl->log.level = atoi(str);
+ if ((str = spa_dict_lookup(info, SPA_KEY_LOG_PATTERNS)) != NULL)
+ support_log_parse_patterns(&impl->patterns, str);
+ }
+
+ /* if our stderr goes to the journal, there's no point in logging both
+ * via the native journal API and by printing to stderr, that would just
+ * result in message duplication */
+ if (stderr_is_connected_to_journal())
+ impl->chain_log = NULL;
+ else
+ impl->chain_log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+
+ spa_log_debug(&impl->log, NAME " %p: initialized", impl);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Log,},
+};
+
+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_handle_factory journal_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ .name = SPA_NAME_SUPPORT_LOG,
+ .info = NULL,
+ .get_size = impl_get_size,
+ .init = impl_init,
+ .enum_interface_info = 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 = &journal_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/support/log-patterns.c b/spa/plugins/support/log-patterns.c
new file mode 100644
index 0000000..4a35b1e
--- /dev/null
+++ b/spa/plugins/support/log-patterns.c
@@ -0,0 +1,108 @@
+/* Spa
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <fnmatch.h>
+
+#include <spa/support/log.h>
+#include <spa/utils/list.h>
+#include <spa/utils/json.h>
+
+#include "log-patterns.h"
+
+struct support_log_pattern {
+ struct spa_list link;
+ enum spa_log_level level;
+ char pattern[];
+};
+
+void
+support_log_topic_init(struct spa_list *patterns, enum spa_log_level default_level,
+ struct spa_log_topic *t)
+{
+ enum spa_log_level level = default_level;
+ const char *topic = t->topic;
+ struct support_log_pattern *pattern;
+
+ spa_list_for_each(pattern, patterns, link) {
+ if (fnmatch(pattern->pattern, topic, 0) != 0)
+ continue;
+ level = pattern->level;
+ t->has_custom_level = true;
+ }
+
+ t->level = level;
+}
+
+int
+support_log_parse_patterns(struct spa_list *patterns, const char *jsonstr)
+{
+ struct spa_json iter, array, elem;
+ int res = 0;
+
+ spa_json_init(&iter, jsonstr, strlen(jsonstr));
+
+ if (spa_json_enter_array(&iter, &array) < 0)
+ return -EINVAL;
+
+ while (spa_json_enter_object(&array, &elem) > 0) {
+ char pattern[512] = {0};
+
+ while (spa_json_get_string(&elem, pattern, sizeof(pattern)) > 0) {
+ struct support_log_pattern *p;
+ const char *val;
+ int len;
+ int lvl;
+
+ if ((len = spa_json_next(&elem, &val)) <= 0)
+ break;
+
+ if (!spa_json_is_int(val, len))
+ break;
+
+ if ((res = spa_json_parse_int(val, len, &lvl)) < 0)
+ break;
+
+ SPA_CLAMP(lvl, SPA_LOG_LEVEL_NONE, SPA_LOG_LEVEL_TRACE);
+
+ p = calloc(1, sizeof(*p) + strlen(pattern) + 1);
+ p->level = lvl;
+ strcpy(p->pattern, pattern);
+ spa_list_append(patterns, &p->link);
+ }
+ }
+
+ return res;
+}
+
+void
+support_log_free_patterns(struct spa_list *patterns)
+{
+ struct support_log_pattern *p;
+
+ spa_list_consume(p, patterns, link) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+}
diff --git a/spa/plugins/support/log-patterns.h b/spa/plugins/support/log-patterns.h
new file mode 100644
index 0000000..a10b445
--- /dev/null
+++ b/spa/plugins/support/log-patterns.h
@@ -0,0 +1,13 @@
+#ifndef LOG_PATTERNS_H
+#define LOG_PATTERNS_H
+
+#include <spa/support/log.h>
+
+struct spa_list;
+
+void support_log_topic_init(struct spa_list *patterns, enum spa_log_level default_level,
+ struct spa_log_topic *t);
+int support_log_parse_patterns(struct spa_list *patterns, const char *jsonstr);
+void support_log_free_patterns(struct spa_list *patterns);
+
+#endif /* LOG_PATTERNS_H */
diff --git a/spa/plugins/support/logger.c b/spa/plugins/support/logger.c
new file mode 100644
index 0000000..615a49b
--- /dev/null
+++ b/spa/plugins/support/logger.c
@@ -0,0 +1,415 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <stdio.h>
+#include <time.h>
+#include <fnmatch.h>
+
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/system.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/type.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/ansi.h>
+
+#include "log-patterns.h"
+
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#define CLOCK_MONOTONIC_RAW CLOCK_MONOTONIC
+#endif
+
+#define NAME "logger"
+
+#define DEFAULT_LOG_LEVEL SPA_LOG_LEVEL_INFO
+
+#define TRACE_BUFFER (16*1024)
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_log log;
+
+ FILE *file;
+ bool close_file;
+
+ struct spa_system *system;
+ struct spa_source source;
+ struct spa_ringbuffer trace_rb;
+ uint8_t trace_data[TRACE_BUFFER];
+
+ unsigned int have_source:1;
+ unsigned int colors:1;
+ unsigned int timestamp:1;
+ unsigned int line:1;
+
+ struct spa_list patterns;
+};
+
+static SPA_PRINTF_FUNC(7,0) void
+impl_log_logtv(void *object,
+ 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)
+{
+#define RESERVED_LENGTH 24
+
+ struct impl *impl = object;
+ char timestamp[15] = {0};
+ char topicstr[32] = {0};
+ char filename[64] = {0};
+ char location[1000 + RESERVED_LENGTH], *p, *s;
+ static const char * const levels[] = { "-", "E", "W", "I", "D", "T", "*T*" };
+ const char *prefix = "", *suffix = "";
+ int size, len;
+ bool do_trace;
+
+ if ((do_trace = (level == SPA_LOG_LEVEL_TRACE && impl->have_source)))
+ level++;
+
+ if (impl->colors) {
+ if (level <= SPA_LOG_LEVEL_ERROR)
+ prefix = SPA_ANSI_BOLD_RED;
+ else if (level <= SPA_LOG_LEVEL_WARN)
+ prefix = SPA_ANSI_BOLD_YELLOW;
+ else if (level <= SPA_LOG_LEVEL_INFO)
+ prefix = SPA_ANSI_BOLD_GREEN;
+ if (prefix[0])
+ suffix = SPA_ANSI_RESET;
+ }
+
+ p = location;
+ len = sizeof(location) - RESERVED_LENGTH;
+
+ if (impl->timestamp) {
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC_RAW, &now);
+ spa_scnprintf(timestamp, sizeof(timestamp), "[%05lu.%06lu]",
+ (now.tv_sec & 0x1FFFFFFF) % 100000, now.tv_nsec / 1000);
+ }
+
+ if (topic && topic->topic)
+ spa_scnprintf(topicstr, sizeof(topicstr), " %-12s | ", topic->topic);
+
+
+ if (impl->line && line != 0) {
+ s = strrchr(file, '/');
+ spa_scnprintf(filename, sizeof(filename), "[%16.16s:%5i %s()]",
+ s ? s + 1 : file, line, func);
+ }
+
+ size = spa_scnprintf(p, len, "%s[%s]%s%s%s ", prefix, levels[level],
+ timestamp, topicstr, filename);
+ /*
+ * it is assumed that at this point `size` <= `len`,
+ * which is reasonable as long as file names and function names
+ * don't become very long
+ */
+ size += spa_vscnprintf(p + size, len - size, fmt, args);
+
+ /*
+ * `RESERVED_LENGTH` bytes are reserved for printing the suffix
+ * (at the moment it's "... (truncated)\x1B[0m\n" at its longest - 21 bytes),
+ * its length must be less than `RESERVED_LENGTH` (including the null byte),
+ * otherwise a stack buffer overrun could ensue
+ */
+
+ /* if the message could not fit entirely... */
+ if (size >= len - 1) {
+ size = len - 1; /* index of the null byte */
+ len = sizeof(location);
+ size += spa_scnprintf(p + size, len - size, "... (truncated)");
+ }
+ else {
+ len = sizeof(location);
+ }
+
+ size += spa_scnprintf(p + size, len - size, "%s\n", suffix);
+
+ if (SPA_UNLIKELY(do_trace)) {
+ uint32_t index;
+
+ spa_ringbuffer_get_write_index(&impl->trace_rb, &index);
+ spa_ringbuffer_write_data(&impl->trace_rb, impl->trace_data, TRACE_BUFFER,
+ index & (TRACE_BUFFER - 1), location, size);
+ spa_ringbuffer_write_update(&impl->trace_rb, index + size);
+
+ if (spa_system_eventfd_write(impl->system, impl->source.fd, 1) < 0)
+ fprintf(impl->file, "error signaling eventfd: %s\n", strerror(errno));
+ } else
+ fputs(location, impl->file);
+
+#undef RESERVED_LENGTH
+}
+
+static SPA_PRINTF_FUNC(6,0) void
+impl_log_logv(void *object,
+ enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt,
+ va_list args)
+{
+ impl_log_logtv(object, level, NULL, file, line, func, fmt, args);
+}
+
+static SPA_PRINTF_FUNC(7,8) void
+impl_log_logt(void *object,
+ 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;
+ va_start(args, fmt);
+ impl_log_logtv(object, level, topic, file, line, func, fmt, args);
+ va_end(args);
+}
+
+static SPA_PRINTF_FUNC(6,7) void
+impl_log_log(void *object,
+ enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ impl_log_logtv(object, level, NULL, file, line, func, fmt, args);
+ va_end(args);
+}
+
+static void on_trace_event(struct spa_source *source)
+{
+ struct impl *impl = source->data;
+ int32_t avail;
+ uint32_t index;
+ uint64_t count;
+
+ if (spa_system_eventfd_read(impl->system, source->fd, &count) < 0)
+ fprintf(impl->file, "failed to read event fd: %s", strerror(errno));
+
+ while ((avail = spa_ringbuffer_get_read_index(&impl->trace_rb, &index)) > 0) {
+ int32_t offset, first;
+
+ if (avail > TRACE_BUFFER) {
+ index += avail - TRACE_BUFFER;
+ avail = TRACE_BUFFER;
+ }
+ offset = index & (TRACE_BUFFER - 1);
+ first = SPA_MIN(avail, TRACE_BUFFER - offset);
+
+ fwrite(impl->trace_data + offset, first, 1, impl->file);
+ if (SPA_UNLIKELY(avail > first)) {
+ fwrite(impl->trace_data, avail - first, 1, impl->file);
+ }
+ spa_ringbuffer_read_update(&impl->trace_rb, index + avail);
+ }
+}
+
+static void
+impl_log_topic_init(void *object, struct spa_log_topic *t)
+{
+ struct impl *impl = object;
+ enum spa_log_level level = impl->log.level;
+
+ support_log_topic_init(&impl->patterns, level, t);
+}
+
+static const struct spa_log_methods impl_log = {
+ SPA_VERSION_LOG_METHODS,
+ .log = impl_log_log,
+ .logv = impl_log_logv,
+ .logt = impl_log_logt,
+ .logtv = impl_log_logtv,
+ .topic_init = impl_log_topic_init,
+};
+
+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_Log))
+ *interface = &this->log;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ support_log_free_patterns(&this->patterns);
+
+ if (this->close_file && this->file != NULL)
+ fclose(this->file);
+
+ if (this->have_source) {
+ spa_loop_remove_source(this->source.loop, &this->source);
+ spa_system_close(this->system, this->source.fd);
+ this->have_source = false;
+ }
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct spa_loop *loop = NULL;
+ const char *str;
+
+ 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.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Log,
+ SPA_VERSION_LOG,
+ &impl_log, this);
+ this->log.level = DEFAULT_LOG_LEVEL;
+
+ loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ this->system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+ spa_list_init(&this->patterns);
+
+ if (loop != NULL && this->system != NULL) {
+ this->source.func = on_trace_event;
+ this->source.data = this;
+ this->source.fd = spa_system_eventfd_create(this->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ this->source.mask = SPA_IO_IN;
+ this->source.rmask = 0;
+
+ if (this->source.fd < 0) {
+ fprintf(stderr, "Warning: failed to create eventfd: %m");
+ } else {
+ spa_loop_add_source(loop, &this->source);
+ this->have_source = true;
+ }
+ }
+ if (info) {
+ if ((str = spa_dict_lookup(info, SPA_KEY_LOG_TIMESTAMP)) != NULL)
+ this->timestamp = spa_atob(str);
+ 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)
+ this->colors = spa_atob(str);
+ if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LEVEL)) != NULL)
+ this->log.level = atoi(str);
+ if ((str = spa_dict_lookup(info, SPA_KEY_LOG_FILE)) != NULL) {
+ this->file = fopen(str, "we");
+ if (this->file == NULL)
+ fprintf(stderr, "Warning: failed to open file %s: (%m)", str);
+ else
+ this->close_file = true;
+ }
+ if ((str = spa_dict_lookup(info, SPA_KEY_LOG_PATTERNS)) != NULL)
+ support_log_parse_patterns(&this->patterns, str);
+ }
+ if (this->file == NULL)
+ this->file = stderr;
+ if (!isatty(fileno(this->file)))
+ this->colors = false;
+
+ spa_ringbuffer_init(&this->trace_rb);
+
+ spa_log_debug(&this->log, NAME " %p: initialized", this);
+
+ setlinebuf(this->file);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Log,},
+};
+
+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_support_logger_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ .name = SPA_NAME_SUPPORT_LOG,
+ .info = NULL,
+ .get_size = impl_get_size,
+ .init = impl_init,
+ .enum_interface_info = impl_enum_interface_info,
+};
diff --git a/spa/plugins/support/loop.c b/spa/plugins/support/loop.c
new file mode 100644
index 0000000..3c3e020
--- /dev/null
+++ b/spa/plugins/support/loop.c
@@ -0,0 +1,1024 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <pthread.h>
+
+#include <spa/support/loop.h>
+#include <spa/support/system.h>
+#include <spa/support/log.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/list.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/type.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/string.h>
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.loop");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#define MAX_ALIGN 8
+#define ITEM_ALIGN 8
+#define DATAS_SIZE (4096*8)
+#define MAX_EP 32
+
+/** \cond */
+
+struct invoke_item {
+ size_t item_size;
+ spa_invoke_func_t func;
+ uint32_t seq;
+ void *data;
+ size_t size;
+ bool block;
+ void *user_data;
+ int res;
+};
+
+static int loop_signal_event(void *object, struct spa_source *source);
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_loop loop;
+ struct spa_loop_control control;
+ struct spa_loop_utils utils;
+
+ struct spa_log *log;
+ struct spa_system *system;
+
+ struct spa_list source_list;
+ struct spa_list destroy_list;
+ struct spa_hook_list hooks_list;
+
+ int poll_fd;
+ pthread_t thread;
+ int enter_count;
+
+ struct spa_source *wakeup;
+ int ack_fd;
+
+ struct spa_ringbuffer buffer;
+ uint8_t *buffer_data;
+ uint8_t buffer_mem[DATAS_SIZE + MAX_ALIGN];
+
+ uint32_t flush_count;
+ unsigned int polling:1;
+};
+
+struct source_impl {
+ struct spa_source source;
+
+ struct impl *impl;
+ struct spa_list link;
+
+ union {
+ spa_source_io_func_t io;
+ spa_source_idle_func_t idle;
+ spa_source_event_func_t event;
+ spa_source_timer_func_t timer;
+ spa_source_signal_func_t signal;
+ } func;
+
+ struct spa_source *fallback;
+
+ bool close;
+ bool enabled;
+};
+/** \endcond */
+
+static int loop_add_source(void *object, struct spa_source *source)
+{
+ struct impl *impl = object;
+ source->loop = &impl->loop;
+ source->priv = NULL;
+ source->rmask = 0;
+ return spa_system_pollfd_add(impl->system, impl->poll_fd, source->fd, source->mask, source);
+}
+
+static int loop_update_source(void *object, struct spa_source *source)
+{
+ struct impl *impl = object;
+
+ spa_assert(source->loop == &impl->loop);
+
+ return spa_system_pollfd_mod(impl->system, impl->poll_fd, source->fd, source->mask, source);
+}
+
+static void detach_source(struct spa_source *source)
+{
+ struct spa_poll_event *e;
+
+ source->loop = NULL;
+ source->rmask = 0;
+
+ if ((e = source->priv)) {
+ /* active in an iteration of the loop, remove it from there */
+ e->data = NULL;
+ source->priv = NULL;
+ }
+}
+
+static int remove_from_poll(struct impl *impl, struct spa_source *source)
+{
+ spa_assert(source->loop == &impl->loop);
+
+ return spa_system_pollfd_del(impl->system, impl->poll_fd, source->fd);
+}
+
+static int loop_remove_source(void *object, struct spa_source *source)
+{
+ struct impl *impl = object;
+ spa_assert(!impl->polling);
+
+ int res = remove_from_poll(impl, source);
+ detach_source(source);
+
+ return res;
+}
+
+static void flush_items(struct impl *impl)
+{
+ uint32_t index, flush_count;
+ int32_t avail;
+ int res;
+
+ flush_count = ++impl->flush_count;
+ avail = spa_ringbuffer_get_read_index(&impl->buffer, &index);
+ while (avail > 0) {
+ struct invoke_item *item;
+ bool block;
+ spa_invoke_func_t func;
+
+ item = SPA_PTROFF(impl->buffer_data, index & (DATAS_SIZE - 1), struct invoke_item);
+ block = item->block;
+ func = item->func;
+
+ spa_log_trace_fp(impl->log, "%p: flush item %p", impl, item);
+ /* first we remove the function from the item so that recursive
+ * calls don't call the callback again. We can't update the
+ * read index before we call the function because then the item
+ * might get overwritten. */
+ item->func = NULL;
+ if (func)
+ item->res = func(&impl->loop, true, item->seq, item->data,
+ item->size, item->user_data);
+
+ /* if this function did a recursive invoke, it now flushed the
+ * ringbuffer and we can exit */
+ if (flush_count != impl->flush_count)
+ break;
+
+ index += item->item_size;
+ avail -= item->item_size;
+ spa_ringbuffer_read_update(&impl->buffer, index);
+
+ if (block) {
+ if ((res = spa_system_eventfd_write(impl->system, impl->ack_fd, 1)) < 0)
+ spa_log_warn(impl->log, "%p: failed to write event fd:%d: %s",
+ impl, impl->ack_fd, spa_strerror(res));
+ }
+ }
+}
+
+static int
+loop_invoke_inthread(struct impl *impl,
+ spa_invoke_func_t func,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ bool block,
+ void *user_data)
+{
+ /* we should probably have a second ringbuffer for the in-thread pending
+ * callbacks. A recursive callback when flushing will insert itself
+ * before this one. */
+ flush_items(impl);
+ return func ? func(&impl->loop, true, seq, data, size, user_data) : 0;
+}
+
+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 invoke_item *item;
+ int res;
+ int32_t filled;
+ uint32_t avail, idx, offset, l0;
+
+ /* the ringbuffer can only be written to from one thread, if we are
+ * in the same thread as the loop, don't write into the ringbuffer
+ * but try to emit the calback right away after flushing what we have */
+ if (impl->thread == 0 || pthread_equal(impl->thread, pthread_self()))
+ return loop_invoke_inthread(impl, func, seq, data, size, block, user_data);
+
+ filled = spa_ringbuffer_get_write_index(&impl->buffer, &idx);
+ if (filled < 0 || filled > DATAS_SIZE) {
+ spa_log_warn(impl->log, "%p: queue xrun %d", impl, filled);
+ return -EPIPE;
+ }
+ avail = DATAS_SIZE - filled;
+ if (avail < sizeof(struct invoke_item)) {
+ spa_log_warn(impl->log, "%p: queue full %d", impl, avail);
+ return -EPIPE;
+ }
+ offset = idx & (DATAS_SIZE - 1);
+
+ /* l0 is remaining size in ringbuffer, this should always be larger than
+ * invoke_item, see below */
+ l0 = DATAS_SIZE - offset;
+
+ item = SPA_PTROFF(impl->buffer_data, offset, struct invoke_item);
+ item->func = func;
+ item->seq = seq;
+ item->size = size;
+ item->block = block;
+ item->user_data = user_data;
+ 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", impl, item, filled);
+
+ if (l0 >= item->item_size) {
+ /* item + size fit in current ringbuffer idx */
+ item->data = SPA_PTROFF(item, sizeof(struct invoke_item), void);
+ if (l0 < sizeof(struct invoke_item) + item->item_size) {
+ /* not enough space for next invoke_item, fill up till the end
+ * so that the next item will be at the start */
+ item->item_size = l0;
+ }
+ } else {
+ /* item does not fit, place the invoke_item at idx and start the
+ * data at the start of the ringbuffer */
+ item->data = impl->buffer_data;
+ item->item_size = SPA_ROUND_UP_N(l0 + size, ITEM_ALIGN);
+ }
+ if (avail < item->item_size) {
+ spa_log_warn(impl->log, "%p: queue full %d, need %zd", impl, avail,
+ item->item_size);
+ return -EPIPE;
+ }
+ if (data && size > 0)
+ memcpy(item->data, data, size);
+
+ spa_ringbuffer_write_update(&impl->buffer, idx + item->item_size);
+
+ loop_signal_event(impl, impl->wakeup);
+
+ if (block) {
+ uint64_t count = 1;
+
+ spa_loop_control_hook_before(&impl->hooks_list);
+
+ if ((res = spa_system_eventfd_read(impl->system, impl->ack_fd, &count)) < 0)
+ spa_log_warn(impl->log, "%p: failed to read event fd:%d: %s",
+ impl, impl->ack_fd, spa_strerror(res));
+
+ spa_loop_control_hook_after(&impl->hooks_list);
+
+ res = item->res;
+ }
+ else {
+ if (seq != SPA_ID_INVALID)
+ res = SPA_RESULT_RETURN_ASYNC(seq);
+ else
+ res = 0;
+ }
+ return res;
+}
+
+static void wakeup_func(void *data, uint64_t count)
+{
+ struct impl *impl = data;
+ flush_items(impl);
+}
+
+static int loop_get_fd(void *object)
+{
+ struct impl *impl = object;
+ return impl->poll_fd;
+}
+
+static void
+loop_add_hook(void *object,
+ struct spa_hook *hook,
+ const struct spa_loop_control_hooks *hooks,
+ void *data)
+{
+ struct impl *impl = object;
+ spa_hook_list_append(&impl->hooks_list, hook, hooks, data);
+}
+
+static void loop_enter(void *object)
+{
+ struct impl *impl = object;
+ pthread_t thread_id = pthread_self();
+
+ if (impl->enter_count == 0) {
+ spa_return_if_fail(impl->thread == 0);
+ impl->thread = thread_id;
+ impl->enter_count = 1;
+ } else {
+ spa_return_if_fail(impl->enter_count > 0);
+ spa_return_if_fail(impl->thread == thread_id);
+ impl->enter_count++;
+ }
+ spa_log_trace(impl->log, "%p: enter %lu", impl, impl->thread);
+}
+
+static void loop_leave(void *object)
+{
+ struct impl *impl = object;
+ pthread_t thread_id = pthread_self();
+
+ spa_return_if_fail(impl->enter_count > 0);
+ spa_return_if_fail(impl->thread == thread_id);
+
+ spa_log_trace(impl->log, "%p: leave %lu", impl, impl->thread);
+
+ if (--impl->enter_count == 0) {
+ impl->thread = 0;
+ flush_items(impl);
+ impl->polling = false;
+ }
+}
+
+static inline void free_source(struct source_impl *s)
+{
+ detach_source(&s->source);
+ free(s);
+}
+
+static inline void process_destroy(struct impl *impl)
+{
+ struct source_impl *source, *tmp;
+
+ spa_list_for_each_safe(source, tmp, &impl->destroy_list, link)
+ free_source(source);
+
+ spa_list_init(&impl->destroy_list);
+}
+
+struct cancellation_handler_data {
+ struct spa_poll_event *ep;
+ int ep_count;
+};
+
+static void cancellation_handler(void *closure)
+{
+ const struct cancellation_handler_data *data = closure;
+
+ for (int i = 0; i < data->ep_count; i++) {
+ struct spa_source *s = data->ep[i].data;
+ if (SPA_LIKELY(s)) {
+ s->rmask = 0;
+ s->priv = NULL;
+ }
+ }
+}
+
+static int loop_iterate(void *object, int timeout)
+{
+ struct impl *impl = object;
+ struct spa_poll_event ep[MAX_EP], *e;
+ int i, nfds;
+
+ impl->polling = true;
+ spa_loop_control_hook_before(&impl->hooks_list);
+
+ nfds = spa_system_pollfd_wait(impl->system, impl->poll_fd, ep, SPA_N_ELEMENTS(ep), timeout);
+
+ spa_loop_control_hook_after(&impl->hooks_list);
+ impl->polling = false;
+
+ struct cancellation_handler_data cdata = { ep, nfds };
+ pthread_cleanup_push(cancellation_handler, &cdata);
+
+ /* first we set all the rmasks, then call the callbacks. The reason is that
+ * some callback might also want to look at other sources it manages and
+ * can then reset the rmask to suppress the callback */
+ for (i = 0; i < nfds; i++) {
+ struct spa_source *s = ep[i].data;
+
+ spa_assert(s->loop == &impl->loop);
+
+ s->rmask = ep[i].events;
+ /* already active in another iteration of the loop,
+ * remove it from that iteration */
+ if (SPA_UNLIKELY(e = s->priv))
+ e->data = NULL;
+ s->priv = &ep[i];
+ }
+
+ if (SPA_UNLIKELY(!spa_list_is_empty(&impl->destroy_list)))
+ process_destroy(impl);
+
+ for (i = 0; i < nfds; i++) {
+ struct spa_source *s = ep[i].data;
+ if (SPA_LIKELY(s && s->rmask))
+ s->func(s);
+ }
+
+ pthread_cleanup_pop(true);
+
+ return nfds;
+}
+
+static void source_io_func(struct spa_source *source)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ spa_log_trace_fp(s->impl->log, "%p: io %08x", s, source->rmask);
+ s->func.io(source->data, source->fd, source->rmask);
+}
+
+static struct spa_source *loop_add_io(void *object,
+ int fd,
+ uint32_t mask,
+ bool close, spa_source_io_func_t func, void *data)
+{
+ struct impl *impl = object;
+ struct source_impl *source;
+ int res;
+
+ source = calloc(1, sizeof(struct source_impl));
+ if (source == NULL)
+ goto error_exit;
+
+ source->source.func = source_io_func;
+ source->source.data = data;
+ source->source.fd = fd;
+ source->source.mask = mask;
+ source->impl = impl;
+ source->close = close;
+ source->func.io = func;
+
+ if ((res = loop_add_source(impl, &source->source)) < 0) {
+ if (res != -EPERM)
+ goto error_exit_free;
+
+ /* file fds (stdin/stdout/...) give EPERM in epoll. Those fds always
+ * return from epoll with the mask set, so we can handle this with
+ * an idle source */
+ source->source.rmask = mask;
+ source->fallback = spa_loop_utils_add_idle(&impl->utils,
+ mask & (SPA_IO_IN | SPA_IO_OUT) ? true : false,
+ (spa_source_idle_func_t) source_io_func, source);
+ spa_log_trace(impl->log, "%p: adding fallback %p", impl,
+ source->fallback);
+ }
+
+ spa_list_insert(&impl->source_list, &source->link);
+
+ return &source->source;
+
+error_exit_free:
+ free(source);
+ errno = -res;
+error_exit:
+ return NULL;
+}
+
+static int loop_update_io(void *object, struct spa_source *source, uint32_t mask)
+{
+ struct impl *impl = object;
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ int res;
+
+ spa_assert(s->impl == object);
+ spa_assert(source->func == source_io_func);
+
+ spa_log_trace(impl->log, "%p: update %08x -> %08x", s, source->mask, mask);
+ source->mask = mask;
+
+ if (s->fallback)
+ res = spa_loop_utils_enable_idle(&impl->utils, s->fallback,
+ mask & (SPA_IO_IN | SPA_IO_OUT) ? true : false);
+ else
+ res = loop_update_source(object, source);
+ return res;
+}
+
+static void source_idle_func(struct spa_source *source)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ s->func.idle(source->data);
+}
+
+static int loop_enable_idle(void *object, struct spa_source *source, bool enabled)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ int res = 0;
+
+ spa_assert(s->impl == object);
+ spa_assert(source->func == source_idle_func);
+
+ if (enabled && !s->enabled) {
+ if ((res = spa_system_eventfd_write(s->impl->system, source->fd, 1)) < 0)
+ spa_log_warn(s->impl->log, "%p: failed to write idle fd:%d: %s",
+ source, source->fd, spa_strerror(res));
+ } else if (!enabled && s->enabled) {
+ uint64_t count;
+ if ((res = spa_system_eventfd_read(s->impl->system, source->fd, &count)) < 0)
+ spa_log_warn(s->impl->log, "%p: failed to read idle fd:%d: %s",
+ source, source->fd, spa_strerror(res));
+ }
+ s->enabled = enabled;
+ return res;
+}
+
+static struct spa_source *loop_add_idle(void *object,
+ bool enabled, spa_source_idle_func_t func, void *data)
+{
+ struct impl *impl = object;
+ struct source_impl *source;
+ int res;
+
+ source = calloc(1, sizeof(struct source_impl));
+ if (source == NULL)
+ goto error_exit;
+
+ if ((res = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_exit_free;
+
+ source->source.func = source_idle_func;
+ source->source.data = data;
+ source->source.fd = res;
+ source->impl = impl;
+ source->close = true;
+ source->source.mask = SPA_IO_IN;
+ source->func.idle = func;
+
+ if ((res = loop_add_source(impl, &source->source)) < 0)
+ goto error_exit_close;
+
+ spa_list_insert(&impl->source_list, &source->link);
+
+ if (enabled)
+ loop_enable_idle(impl, &source->source, true);
+
+ return &source->source;
+
+error_exit_close:
+ spa_system_close(impl->system, source->source.fd);
+error_exit_free:
+ free(source);
+ errno = -res;
+error_exit:
+ return NULL;
+}
+
+static void source_event_func(struct spa_source *source)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ uint64_t count = 0;
+ int res;
+
+ if ((res = spa_system_eventfd_read(s->impl->system, source->fd, &count)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(s->impl->log, "%p: failed to read event fd:%d: %s",
+ source, source->fd, spa_strerror(res));
+ return;
+ }
+ s->func.event(source->data, count);
+}
+
+static struct spa_source *loop_add_event(void *object,
+ spa_source_event_func_t func, void *data)
+{
+ struct impl *impl = object;
+ struct source_impl *source;
+ int res;
+
+ source = calloc(1, sizeof(struct source_impl));
+ if (source == NULL)
+ goto error_exit;
+
+ if ((res = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_exit_free;
+
+ source->source.func = source_event_func;
+ source->source.data = data;
+ source->source.fd = res;
+ source->source.mask = SPA_IO_IN;
+ source->impl = impl;
+ source->close = true;
+ source->func.event = func;
+
+ if ((res = loop_add_source(impl, &source->source)) < 0)
+ goto error_exit_close;
+
+ spa_list_insert(&impl->source_list, &source->link);
+
+ return &source->source;
+
+error_exit_close:
+ spa_system_close(impl->system, source->source.fd);
+error_exit_free:
+ free(source);
+ errno = -res;
+error_exit:
+ return NULL;
+}
+
+static int loop_signal_event(void *object, struct spa_source *source)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ int res;
+
+ spa_assert(s->impl == object);
+ spa_assert(source->func == source_event_func);
+
+ if (SPA_UNLIKELY((res = spa_system_eventfd_write(s->impl->system, source->fd, 1)) < 0))
+ spa_log_warn(s->impl->log, "%p: failed to write event fd:%d: %s",
+ source, source->fd, spa_strerror(res));
+ return res;
+}
+
+static void source_timer_func(struct spa_source *source)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ uint64_t expirations = 0;
+ int res;
+
+ if (SPA_UNLIKELY((res = spa_system_timerfd_read(s->impl->system,
+ source->fd, &expirations)) < 0)) {
+ if (res != -EAGAIN)
+ spa_log_warn(s->impl->log, "%p: failed to read timer fd:%d: %s",
+ source, source->fd, spa_strerror(res));
+ return;
+ }
+ s->func.timer(source->data, expirations);
+}
+
+static struct spa_source *loop_add_timer(void *object,
+ spa_source_timer_func_t func, void *data)
+{
+ struct impl *impl = object;
+ struct source_impl *source;
+ int res;
+
+ source = calloc(1, sizeof(struct source_impl));
+ if (source == NULL)
+ goto error_exit;
+
+ if ((res = spa_system_timerfd_create(impl->system, CLOCK_MONOTONIC,
+ SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_exit_free;
+
+ source->source.func = source_timer_func;
+ source->source.data = data;
+ source->source.fd = res;
+ source->source.mask = SPA_IO_IN;
+ source->impl = impl;
+ source->close = true;
+ source->func.timer = func;
+
+ if ((res = loop_add_source(impl, &source->source)) < 0)
+ goto error_exit_close;
+
+ spa_list_insert(&impl->source_list, &source->link);
+
+ return &source->source;
+
+error_exit_close:
+ spa_system_close(impl->system, source->source.fd);
+error_exit_free:
+ free(source);
+ errno = -res;
+error_exit:
+ return NULL;
+}
+
+static int
+loop_update_timer(void *object, struct spa_source *source,
+ struct timespec *value, struct timespec *interval, bool absolute)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ struct itimerspec its;
+ int flags = 0, res;
+
+ spa_assert(s->impl == object);
+ spa_assert(source->func == source_timer_func);
+
+ spa_zero(its);
+ if (SPA_LIKELY(value)) {
+ its.it_value = *value;
+ } else if (interval) {
+ its.it_value = *interval;
+ absolute = true;
+ }
+ if (SPA_UNLIKELY(interval))
+ its.it_interval = *interval;
+ if (SPA_LIKELY(absolute))
+ flags |= SPA_FD_TIMER_ABSTIME;
+
+ if (SPA_UNLIKELY((res = spa_system_timerfd_settime(s->impl->system, source->fd, flags, &its, NULL)) < 0))
+ return res;
+
+ return 0;
+}
+
+static void source_signal_func(struct spa_source *source)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+ int res, signal_number = 0;
+
+ if ((res = spa_system_signalfd_read(s->impl->system, source->fd, &signal_number)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(s->impl->log, "%p: failed to read signal fd:%d: %s",
+ source, source->fd, spa_strerror(res));
+ return;
+ }
+ s->func.signal(source->data, signal_number);
+}
+
+static struct spa_source *loop_add_signal(void *object,
+ int signal_number,
+ spa_source_signal_func_t func, void *data)
+{
+ struct impl *impl = object;
+ struct source_impl *source;
+ int res;
+
+ source = calloc(1, sizeof(struct source_impl));
+ if (source == NULL)
+ goto error_exit;
+
+ if ((res = spa_system_signalfd_create(impl->system,
+ signal_number, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_exit_free;
+
+ source->source.func = source_signal_func;
+ source->source.data = data;
+ source->source.fd = res;
+ source->source.mask = SPA_IO_IN;
+ source->impl = impl;
+ source->close = true;
+ source->func.signal = func;
+
+ if ((res = loop_add_source(impl, &source->source)) < 0)
+ goto error_exit_close;
+
+ spa_list_insert(&impl->source_list, &source->link);
+
+ return &source->source;
+
+error_exit_close:
+ spa_system_close(impl->system, source->source.fd);
+error_exit_free:
+ free(source);
+ errno = -res;
+error_exit:
+ return NULL;
+}
+
+static void loop_destroy_source(void *object, struct spa_source *source)
+{
+ struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source);
+
+ spa_assert(s->impl == object);
+
+ spa_log_trace(s->impl->log, "%p ", s);
+
+ spa_list_remove(&s->link);
+
+ if (s->fallback)
+ loop_destroy_source(s->impl, s->fallback);
+ else
+ remove_from_poll(s->impl, source);
+
+ if (source->fd != -1 && s->close) {
+ spa_system_close(s->impl->system, source->fd);
+ source->fd = -1;
+ }
+
+ if (!s->impl->polling)
+ free_source(s);
+ else
+ spa_list_insert(&s->impl->destroy_list, &s->link);
+}
+
+static const struct spa_loop_methods impl_loop = {
+ SPA_VERSION_LOOP_METHODS,
+ .add_source = loop_add_source,
+ .update_source = loop_update_source,
+ .remove_source = loop_remove_source,
+ .invoke = loop_invoke,
+};
+
+static const struct spa_loop_control_methods impl_loop_control = {
+ SPA_VERSION_LOOP_CONTROL_METHODS,
+ .get_fd = loop_get_fd,
+ .add_hook = loop_add_hook,
+ .enter = loop_enter,
+ .leave = loop_leave,
+ .iterate = loop_iterate,
+};
+
+static const struct spa_loop_utils_methods impl_loop_utils = {
+ SPA_VERSION_LOOP_UTILS_METHODS,
+ .add_io = loop_add_io,
+ .update_io = loop_update_io,
+ .add_idle = loop_add_idle,
+ .enable_idle = loop_enable_idle,
+ .add_event = loop_add_event,
+ .signal_event = loop_signal_event,
+ .add_timer = loop_add_timer,
+ .update_timer = loop_update_timer,
+ .add_signal = loop_add_signal,
+ .destroy_source = loop_destroy_source,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *impl;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ impl = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Loop))
+ *interface = &impl->loop;
+ else if (spa_streq(type, SPA_TYPE_INTERFACE_LoopControl))
+ *interface = &impl->control;
+ else if (spa_streq(type, SPA_TYPE_INTERFACE_LoopUtils))
+ *interface = &impl->utils;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *impl;
+ struct source_impl *source;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ impl = (struct impl *) handle;
+
+ 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_system_close(impl->system, impl->ack_fd);
+ spa_system_close(impl->system, impl->poll_fd);
+
+ 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;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ impl = (struct impl *) handle;
+ impl->loop.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Loop,
+ SPA_VERSION_LOOP,
+ &impl_loop, impl);
+ impl->control.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_LoopControl,
+ SPA_VERSION_LOOP_CONTROL,
+ &impl_loop_control, impl);
+ impl->utils.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_LoopUtils,
+ SPA_VERSION_LOOP_UTILS,
+ &impl_loop_utils, impl);
+
+ impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(impl->log, &log_topic);
+ impl->system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+
+ if (impl->system == NULL) {
+ spa_log_error(impl->log, "%p: a System is needed", impl);
+ res = -EINVAL;
+ goto error_exit;
+ }
+
+ if ((res = spa_system_pollfd_create(impl->system, SPA_FD_CLOEXEC)) < 0) {
+ spa_log_error(impl->log, "%p: can't create pollfd: %s",
+ impl, spa_strerror(res));
+ goto error_exit;
+ }
+ impl->poll_fd = res;
+
+ spa_list_init(&impl->source_list);
+ spa_list_init(&impl->destroy_list);
+ spa_hook_list_init(&impl->hooks_list);
+
+ impl->buffer_data = SPA_PTR_ALIGN(impl->buffer_mem, MAX_ALIGN, uint8_t);
+ spa_ringbuffer_init(&impl->buffer);
+
+ impl->wakeup = loop_add_event(impl, wakeup_func, impl);
+ if (impl->wakeup == NULL) {
+ res = -errno;
+ spa_log_error(impl->log, "%p: can't create wakeup event: %m", impl);
+ goto error_exit_free_poll;
+ }
+ 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",
+ impl, spa_strerror(res));
+ goto error_exit_free_wakeup;
+ }
+ impl->ack_fd = res;
+
+ 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:
+ return res;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Loop,},
+ {SPA_TYPE_INTERFACE_LoopControl,},
+ {SPA_TYPE_INTERFACE_LoopUtils,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_support_loop_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_SUPPORT_LOOP,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info
+};
diff --git a/spa/plugins/support/meson.build b/spa/plugins/support/meson.build
new file mode 100644
index 0000000..1672d38
--- /dev/null
+++ b/spa/plugins/support/meson.build
@@ -0,0 +1,70 @@
+spa_support_sources = [
+ 'cpu.c',
+ 'logger.c',
+ 'log-patterns.c',
+ 'loop.c',
+ 'node-driver.c',
+ 'null-audio-sink.c',
+ 'plugin.c',
+ 'system.c'
+]
+
+simd_cargs = []
+
+if have_sse
+ simd_cargs += [sse_args, '-DHAVE_SSE']
+endif
+
+spa_support_lib = shared_library('spa-support',
+ spa_support_sources,
+ c_args : [ simd_cargs ],
+ dependencies : [ spa_dep, pthread_lib, epoll_shim_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'support')
+spa_support_dep = declare_dependency(link_with: spa_support_lib)
+
+if get_option('evl').allowed()
+ evl_inc = include_directories('/usr/evl/include')
+ evl_lib = cc.find_library('evl',
+ dirs: ['/usr/evl/lib/'],
+ required: get_option('evl'))
+
+ spa_evl_sources = ['evl-system.c', 'evl-plugin.c']
+
+ spa_evl_lib = shared_library('spa-evl',
+ spa_evl_sources,
+ include_directories : [ evl_inc],
+ dependencies : [ spa_dep, pthread_lib, evl_lib ],
+ install : true,
+ install_dir : spa_plugindir / 'support')
+endif
+
+if dbus_dep.found()
+ spa_dbus_sources = ['dbus.c']
+
+ spa_dbus_lib = shared_library('spa-dbus',
+ spa_dbus_sources,
+ dependencies : [ spa_dep, dbus_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'support')
+ spa_dbus_dep = declare_dependency(link_with: spa_dbus_lib)
+else
+ spa_dbus_dep = declare_dependency()
+endif
+
+
+if systemd_dep.found()
+ spa_journal_sources = [
+ 'journal.c',
+ 'log-patterns.c',
+ ]
+
+ spa_journal_lib = shared_library('spa-journal',
+ spa_journal_sources,
+ dependencies : [ spa_dep, systemd_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'support')
+ spa_journal_dep = declare_dependency(link_with: spa_journal_lib)
+else
+ spa_journal_dep = declare_dependency()
+endif
diff --git a/spa/plugins/support/node-driver.c b/spa/plugins/support/node-driver.c
new file mode 100644
index 0000000..9701a47
--- /dev/null
+++ b/spa/plugins/support/node-driver.c
@@ -0,0 +1,492 @@
+/* Spa
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/keys.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/param/param.h>
+
+#define NAME "driver"
+
+#define DEFAULT_FREEWHEEL false
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ bool freewheel;
+ char clock_name[64];
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct props props;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[1];
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct spa_io_position *position;
+ struct spa_io_clock *clock;
+
+ struct spa_source timer_source;
+ struct itimerspec timerspec;
+
+ bool started;
+ bool following;
+ uint64_t next_time;
+};
+
+static void reset_props(struct props *props)
+{
+ props->freewheel = DEFAULT_FREEWHEEL;
+ spa_scnprintf(props->clock_name, sizeof(props->clock_name),
+ "%s", DEFAULT_CLOCK_NAME);
+}
+
+static void set_timeout(struct impl *this, uint64_t next_time)
+{
+ spa_log_trace(this->log, "set timeout %"PRIu64, next_time);
+ this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC;
+ this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC;
+ spa_system_timerfd_settime(this->data_system,
+ this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+ int res;
+
+ if ((res = spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now)) < 0)
+ return res;
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ if (this->following) {
+ set_timeout(this, 0);
+ } else {
+ set_timeout(this, this->next_time);
+ }
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ set_timers(this);
+ return 0;
+}
+
+static int reassign_follower(struct impl *this)
+{
+ bool following;
+
+ if (this->clock)
+ SPA_FLAG_UPDATE(this->clock->flags,
+ SPA_IO_CLOCK_FLAG_FREEWHEEL, this->props.freewheel);
+
+ if (!this->started)
+ return 0;
+
+ following = is_following(this);
+ if (following != this->following) {
+ spa_log_debug(this->log, NAME" %p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+ return 0;
+}
+
+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);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ if (size > 0 && size < sizeof(struct spa_io_clock))
+ return -EINVAL;
+ this->clock = data;
+ if (this->clock)
+ spa_scnprintf(this->clock->name, sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ break;
+ case SPA_IO_Position:
+ if (size > 0 && size < sizeof(struct spa_io_position))
+ return -EINVAL;
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ reassign_follower(this);
+
+ return 0;
+}
+
+static void on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t expirations, nsec, duration;
+ uint32_t rate;
+ int res;
+
+ spa_log_trace(this->log, "timeout");
+
+ if ((res = spa_system_timerfd_read(this->data_system,
+ this->timer_source.fd, &expirations)) < 0) {
+ if (res != EAGAIN)
+ spa_log_error(this->log, NAME " %p: timerfd error: %s",
+ this, spa_strerror(res));
+ return;
+ }
+
+ nsec = this->next_time;
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ this->next_time = nsec + duration * SPA_NSEC_PER_SEC / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = nsec;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->delay = 0;
+ this->clock->rate_diff = 1.0;
+ this->clock->next_nsec = this->next_time;
+ }
+
+ spa_node_call_ready(&this->callbacks,
+ SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int do_start(struct impl *this)
+{
+ if (this->started)
+ return 0;
+
+ this->following = is_following(this);
+ set_timers(this);
+ this->started = true;
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ if (!this->started)
+ return 0;
+ this->started = false;
+ set_timeout(this, 0);
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ do_start(this);
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ do_stop(this);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+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);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+
+ 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_process(void *object)
+{
+ struct impl *this = object;
+ struct timespec now;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_log_trace(this->log, "process %d", this->props.freewheel);
+
+ if (this->props.freewheel) {
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+ set_timeout(this, this->next_time);
+ }
+ return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .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 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);
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this);
+ spa_system_close(this->data_system, this->timer_source.fd);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ 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);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data_loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data_system is needed");
+ return -EINVAL;
+ }
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 0;
+ this->info.max_output_ports = 0;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 0;
+
+ this->timer_source.func = on_timeout;
+ 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->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;
+
+ reset_props(&this->props);
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "node.freewheel")) {
+ this->props.freewheel = spa_atob(s);
+ } else if (spa_streq(k, "clock.name")) {
+ spa_scnprintf(this->props.clock_name,
+ sizeof(this->props.clock_name), "%s", s);
+ }
+ }
+ spa_loop_add_source(this->data_loop, &this->timer_source);
+
+ 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_support_node_driver_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_SUPPORT_NODE_DRIVER,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/support/null-audio-sink.c b/spa/plugins/support/null-audio-sink.c
new file mode 100644
index 0000000..e5b9f04
--- /dev/null
+++ b/spa/plugins/support/null-audio-sink.c
@@ -0,0 +1,1001 @@
+/* Spa
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/json.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/debug/types.h>
+#include <spa/debug/mem.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/control/control.h>
+
+#define NAME "null-audio-sink"
+
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+struct props {
+ uint32_t channels;
+ uint32_t rate;
+ uint32_t n_pos;
+ uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
+ char clock_name[64];
+ unsigned int debug:1;
+};
+
+static void reset_props(struct props *props)
+{
+ props->channels = 0;
+ props->rate = 0;
+ props->n_pos = 0;
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+ props->debug = false;
+}
+
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_RATE 48000
+
+#define MAX_BUFFERS 16
+#define MAX_PORTS 1
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+};
+
+struct impl;
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+
+ bool have_format;
+ struct spa_audio_info current_format;
+ uint32_t blocks;
+ size_t bpf;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ uint32_t quantum_limit;
+
+ struct props props;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[2];
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct port port;
+
+ unsigned int started:1;
+ unsigned int following:1;
+ struct spa_source timer_source;
+ struct itimerspec timerspec;
+ uint64_t next_time;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS)
+
+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_IO:
+ {
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ 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 void set_timeout(struct impl *this, uint64_t next_time)
+{
+ spa_log_trace(this->log, "set timeout %"PRIu64, next_time);
+ this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC;
+ this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC;
+ spa_system_timerfd_settime(this->data_system,
+ this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL);
+}
+
+static int set_timers(struct impl *this)
+{
+ struct timespec now;
+ int res;
+
+ if ((res = spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now)) < 0)
+ return res;
+ this->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ if (this->following) {
+ set_timeout(this, 0);
+ } else {
+ set_timeout(this, this->next_time);
+ }
+ return 0;
+}
+
+static inline bool is_following(struct impl *this)
+{
+ return this->position && this->clock && this->position->clock.id != this->clock->id;
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct impl *this = user_data;
+ set_timers(this);
+ return 0;
+}
+
+static int reassign_follower(struct impl *this)
+{
+ bool following;
+
+ if (!this->started)
+ return 0;
+
+ following = is_following(this);
+ if (following != this->following) {
+ spa_log_debug(this->log, NAME" %p: reassign follower %d->%d", this, this->following, following);
+ this->following = following;
+ spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this);
+ }
+ return 0;
+}
+
+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);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ if (size > 0 && size < sizeof(struct spa_io_clock))
+ return -EINVAL;
+ this->clock = data;
+ if (this->clock != NULL) {
+ spa_scnprintf(this->clock->name,
+ sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ }
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ reassign_follower(this);
+
+ return 0;
+}
+
+static void on_timeout(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ uint64_t expirations, nsec, duration = 10;
+ uint32_t rate;
+ int res;
+
+ spa_log_trace(this->log, "timeout");
+
+ if ((res = spa_system_timerfd_read(this->data_system,
+ this->timer_source.fd, &expirations)) < 0) {
+ if (res != EAGAIN)
+ spa_log_error(this->log, NAME " %p: timerfd error: %s",
+ this, spa_strerror(res));
+ return;
+ }
+
+ nsec = this->next_time;
+
+ if (SPA_LIKELY(this->position)) {
+ duration = this->position->clock.duration;
+ rate = this->position->clock.rate.denom;
+ } else {
+ duration = 1024;
+ rate = 48000;
+ }
+
+ this->next_time = nsec + duration * SPA_NSEC_PER_SEC / rate;
+
+ if (SPA_LIKELY(this->clock)) {
+ this->clock->nsec = nsec;
+ this->clock->position += duration;
+ this->clock->duration = duration;
+ this->clock->delay = 0;
+ this->clock->rate_diff = 1.0;
+ this->clock->next_nsec = this->next_time;
+ }
+
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+
+ set_timeout(this, this->next_time);
+}
+
+static int do_start(struct impl *this)
+{
+ if (this->started)
+ return 0;
+
+ this->following = is_following(this);
+ set_timers(this);
+ this->started = true;
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ if (!this->started)
+ return 0;
+ this->started = false;
+ set_timeout(this, 0);
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ do_start(this);
+ break;
+ }
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ do_stop(this);
+ break;
+
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+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);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+port_enum_formats(struct impl *this,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct spa_pod_frame f[1];
+
+ switch (index) {
+ case 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_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(3,
+ SPA_AUDIO_FORMAT_F32P,
+ SPA_AUDIO_FORMAT_F32P,
+ SPA_AUDIO_FORMAT_F32),
+ 0);
+
+ if (this->props.rate != 0) {
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(this->props.rate),
+ 0);
+ } else {
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(DEFAULT_RATE, 1, INT32_MAX),
+ 0);
+ }
+ if (this->props.channels != 0) {
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(this->props.channels),
+ 0);
+ } else {
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(DEFAULT_CHANNELS, 1, INT32_MAX),
+ 0);
+ }
+ if (this->props.n_pos != 0) {
+ spa_pod_builder_prop(builder, SPA_FORMAT_AUDIO_position, 0);
+ spa_pod_builder_array(builder, sizeof(uint32_t), SPA_TYPE_Id,
+ this->props.n_pos, this->props.pos);
+ }
+ *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;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(this, direction, port_id,
+ result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * port->bpf,
+ 16 * port->bpf,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->bpf));
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_info(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ this->started = false;
+ }
+ 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)
+{
+ int res;
+ struct port *port = &this->port;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+
+ if (info.info.raw.format == SPA_AUDIO_FORMAT_F32) {
+ port->bpf = 4 * info.info.raw.channels;
+ port->blocks = 1;
+ } else if (info.info.raw.format == SPA_AUDIO_FORMAT_F32P) {
+ port->bpf = 4;
+ port->blocks = info.info.raw.channels;
+ } else
+ return -EINVAL;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[3] = 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_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;
+ }
+ return 0;
+}
+
+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;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->flags = 0;
+ b->outbuf = buffers[i];
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (io->status != SPA_STATUS_HAVE_DATA)
+ return io->status;
+ if (io->buffer_id >= port->n_buffers) {
+ io->status = -EINVAL;
+ return io->status;
+ }
+ if (this->props.debug) {
+ struct buffer *b;
+ uint32_t i;
+
+ b = &port->buffers[io->buffer_id];
+ for (i = 0; i < b->outbuf->n_datas; i++) {
+ uint32_t offs, size;
+ struct spa_data *d = b->outbuf->datas;
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->maxsize - offs, d->chunk->size);
+ spa_debug_mem(i, SPA_PTROFF(d[i].data, offs, void), SPA_MIN(16u, size));;
+ }
+ }
+ io->status = SPA_STATUS_OK;
+ return SPA_STATUS_HAVE_DATA;
+}
+
+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_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .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,
+ .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 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);
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this);
+ spa_system_close(this->data_system, this->timer_source.fd);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static inline 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.n_pos = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ this->props.n_pos < SPA_AUDIO_MAX_CHANNELS) {
+ this->props.pos[this->props.n_pos++] = channel_from_name(v);
+ }
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ 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);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data_loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data_system is needed");
+ return -EINVAL;
+ }
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = 1;
+ reset_props(&this->props);
+
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF |
+ SPA_PORT_FLAG_LIVE;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 4;
+
+ this->timer_source.func = on_timeout;
+ 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->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;
+
+ spa_loop_add_source(this->data_loop, &this->timer_source);
+
+ 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_AUDIO_CHANNELS)) {
+ this->props.channels = atoi(s);
+ } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) {
+ this->props.rate = atoi(s);
+ } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
+ parse_position(this, s, strlen(s));
+ } else if (spa_streq(k, "clock.name")) {
+ spa_scnprintf(this->props.clock_name,
+ sizeof(this->props.clock_name),
+ "%s", s);
+ }
+ }
+ if (this->props.n_pos > 0)
+ this->props.channels = this->props.n_pos;
+
+ spa_log_info(this->log, NAME " %p: initialized", this);
+
+ 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, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Consume audio samples" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_support_null_audio_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ "support.null-audio-sink",
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/support/plugin.c b/spa/plugins/support/plugin.c
new file mode 100644
index 0000000..29bd8a9
--- /dev/null
+++ b/spa/plugins/support/plugin.c
@@ -0,0 +1,67 @@
+/* Spa Support plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_support_logger_factory;
+extern const struct spa_handle_factory spa_support_system_factory;
+extern const struct spa_handle_factory spa_support_cpu_factory;
+extern const struct spa_handle_factory spa_support_loop_factory;
+extern const struct spa_handle_factory spa_support_node_driver_factory;
+extern const struct spa_handle_factory spa_support_null_audio_sink_factory;
+
+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_support_logger_factory;
+ break;
+ case 1:
+ *factory = &spa_support_system_factory;
+ break;
+ case 2:
+ *factory = &spa_support_cpu_factory;
+ break;
+ case 3:
+ *factory = &spa_support_loop_factory;
+ break;
+ case 4:
+ *factory = &spa_support_node_driver_factory;
+ break;
+ case 5:
+ *factory = &spa_support_null_audio_sink_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/support/system.c b/spa/plugins/support/system.c
new file mode 100644
index 0000000..e7efec9
--- /dev/null
+++ b/spa/plugins/support/system.c
@@ -0,0 +1,384 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <sys/timerfd.h>
+#include <sys/eventfd.h>
+#include <sys/signalfd.h>
+
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/type.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+
+static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.system");
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT &log_topic
+
+#ifndef TFD_TIMER_CANCEL_ON_SET
+# define TFD_TIMER_CANCEL_ON_SET (1 << 1)
+#endif
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_system system;
+ struct spa_log *log;
+};
+
+static ssize_t impl_read(void *object, int fd, void *buf, size_t count)
+{
+ ssize_t res = read(fd, buf, count);
+ return res < 0 ? -errno : res;
+}
+
+static ssize_t impl_write(void *object, int fd, const void *buf, size_t count)
+{
+ ssize_t res = write(fd, buf, count);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_ioctl(void *object, int fd, unsigned long request, ...)
+{
+ int res;
+ va_list ap;
+ long arg;
+
+ va_start(ap, request);
+ arg = va_arg(ap, long);
+ res = ioctl(fd, request, arg);
+ va_end(ap);
+
+ return res < 0 ? -errno : res;
+}
+
+static int impl_close(void *object, int fd)
+{
+ struct impl *impl = object;
+ int res = close(fd);
+ spa_log_debug(impl->log, "%p: close fd:%d", impl, fd);
+ return res < 0 ? -errno : res;
+}
+
+/* clock */
+static int impl_clock_gettime(void *object,
+ int clockid, struct timespec *value)
+{
+ int res = clock_gettime(clockid, value);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_clock_getres(void *object,
+ int clockid, struct timespec *res)
+{
+ int r = clock_getres(clockid, res);
+ return r < 0 ? -errno : r;
+}
+
+/* poll */
+static int impl_pollfd_create(void *object, int flags)
+{
+ struct impl *impl = object;
+ int fl = 0, res;
+ if (flags & SPA_FD_CLOEXEC)
+ fl |= EPOLL_CLOEXEC;
+ res = epoll_create1(fl);
+ spa_log_debug(impl->log, "%p: new fd:%d", impl, res);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_pollfd_add(void *object, int pfd, int fd, uint32_t events, void *data)
+{
+ struct epoll_event ep;
+ int res;
+
+ spa_zero(ep);
+ ep.events = events;
+ ep.data.ptr = data;
+
+ res = epoll_ctl(pfd, EPOLL_CTL_ADD, fd, &ep);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_pollfd_mod(void *object, int pfd, int fd, uint32_t events, void *data)
+{
+ struct epoll_event ep;
+ int res;
+
+ spa_zero(ep);
+ ep.events = events;
+ ep.data.ptr = data;
+
+ res = epoll_ctl(pfd, EPOLL_CTL_MOD, fd, &ep);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_pollfd_del(void *object, int pfd, int fd)
+{
+ int res = epoll_ctl(pfd, EPOLL_CTL_DEL, fd, NULL);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_pollfd_wait(void *object, int pfd,
+ struct spa_poll_event *ev, int n_ev, int timeout)
+{
+ struct epoll_event ep[n_ev];
+ int i, nfds;
+
+ if (SPA_UNLIKELY((nfds = epoll_wait(pfd, ep, n_ev, timeout)) < 0))
+ return -errno;
+
+ for (i = 0; i < nfds; i++) {
+ ev[i].events = ep[i].events;
+ ev[i].data = ep[i].data.ptr;
+ }
+ return nfds;
+}
+
+/* timers */
+static int impl_timerfd_create(void *object, int clockid, int flags)
+{
+ struct impl *impl = object;
+ int fl = 0, res;
+ if (flags & SPA_FD_CLOEXEC)
+ fl |= TFD_CLOEXEC;
+ if (flags & SPA_FD_NONBLOCK)
+ fl |= TFD_NONBLOCK;
+ res = timerfd_create(clockid, fl);
+ spa_log_debug(impl->log, "%p: new fd:%d", impl, res);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_timerfd_settime(void *object,
+ int fd, int flags,
+ const struct itimerspec *new_value,
+ struct itimerspec *old_value)
+{
+ int fl = 0, res;
+ if (flags & SPA_FD_TIMER_ABSTIME)
+ fl |= TFD_TIMER_ABSTIME;
+ if (flags & SPA_FD_TIMER_CANCEL_ON_SET)
+ fl |= TFD_TIMER_CANCEL_ON_SET;
+ res = timerfd_settime(fd, fl, new_value, old_value);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_timerfd_gettime(void *object,
+ int fd, struct itimerspec *curr_value)
+{
+ int res = timerfd_gettime(fd, curr_value);
+ return res < 0 ? -errno : res;
+
+}
+static int impl_timerfd_read(void *object, int fd, uint64_t *expirations)
+{
+ if (read(fd, expirations, sizeof(uint64_t)) != sizeof(uint64_t))
+ return -errno;
+ return 0;
+}
+
+/* events */
+static int impl_eventfd_create(void *object, int flags)
+{
+ struct impl *impl = object;
+ int fl = 0, res;
+ if (flags & SPA_FD_CLOEXEC)
+ fl |= EFD_CLOEXEC;
+ if (flags & SPA_FD_NONBLOCK)
+ fl |= EFD_NONBLOCK;
+ if (flags & SPA_FD_EVENT_SEMAPHORE)
+ fl |= EFD_SEMAPHORE;
+ res = eventfd(0, fl);
+ spa_log_debug(impl->log, "%p: new fd:%d", impl, res);
+ return res < 0 ? -errno : res;
+}
+
+static int impl_eventfd_write(void *object, int fd, uint64_t count)
+{
+ if (write(fd, &count, sizeof(uint64_t)) != sizeof(uint64_t))
+ return -errno;
+ return 0;
+}
+
+static int impl_eventfd_read(void *object, int fd, uint64_t *count)
+{
+ if (read(fd, count, sizeof(uint64_t)) != sizeof(uint64_t))
+ return -errno;
+ return 0;
+}
+
+/* signals */
+static int impl_signalfd_create(void *object, int signal, int flags)
+{
+ struct impl *impl = object;
+ sigset_t mask;
+ int res, fl = 0;
+
+ if (flags & SPA_FD_CLOEXEC)
+ fl |= SFD_CLOEXEC;
+ if (flags & SPA_FD_NONBLOCK)
+ fl |= SFD_NONBLOCK;
+
+ sigemptyset(&mask);
+ sigaddset(&mask, signal);
+ res = signalfd(-1, &mask, fl);
+ sigprocmask(SIG_BLOCK, &mask, NULL);
+ spa_log_debug(impl->log, "%p: new fd:%d", impl, res);
+
+ return res < 0 ? -errno : res;
+}
+
+static int impl_signalfd_read(void *object, int fd, int *signal)
+{
+ struct signalfd_siginfo signal_info;
+ int len;
+
+ len = read(fd, &signal_info, sizeof signal_info);
+ if (!(len == -1 && errno == EAGAIN) && len != sizeof signal_info)
+ return -errno;
+
+ *signal = signal_info.ssi_signo;
+
+ return 0;
+}
+
+static const struct spa_system_methods impl_system = {
+ SPA_VERSION_SYSTEM_METHODS,
+ .read = impl_read,
+ .write = impl_write,
+ .ioctl = impl_ioctl,
+ .close = impl_close,
+ .clock_gettime = impl_clock_gettime,
+ .clock_getres = impl_clock_getres,
+ .pollfd_create = impl_pollfd_create,
+ .pollfd_add = impl_pollfd_add,
+ .pollfd_mod = impl_pollfd_mod,
+ .pollfd_del = impl_pollfd_del,
+ .pollfd_wait = impl_pollfd_wait,
+ .timerfd_create = impl_timerfd_create,
+ .timerfd_settime = impl_timerfd_settime,
+ .timerfd_gettime = impl_timerfd_gettime,
+ .timerfd_read = impl_timerfd_read,
+ .eventfd_create = impl_eventfd_create,
+ .eventfd_write = impl_eventfd_write,
+ .eventfd_read = impl_eventfd_read,
+ .signalfd_create = impl_signalfd_create,
+ .signalfd_read = impl_signalfd_read,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *impl;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ impl = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_System))
+ *interface = &impl->system;
+ 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 *impl;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ impl = (struct impl *) handle;
+ impl->system.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_System,
+ SPA_VERSION_SYSTEM,
+ &impl_system, impl);
+
+ impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(impl->log, &log_topic);
+
+ spa_log_debug(impl->log, "%p: initialized", impl);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_System,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_support_system_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_SUPPORT_SYSTEM,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info
+};
diff --git a/spa/plugins/test/fakesink.c b/spa/plugins/test/fakesink.c
new file mode 100644
index 0000000..16bb012
--- /dev/null
+++ b/spa/plugins/test/fakesink.c
@@ -0,0 +1,846 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/param.h>
+#include <spa/param/format.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/filter.h>
+
+#define NAME "fakesink"
+
+struct props {
+ bool live;
+};
+
+#define MAX_BUFFERS 16
+#define MAX_PORTS 1
+
+struct buffer {
+ uint32_t id;
+ struct spa_buffer *outbuf;
+ bool outstanding;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+
+ bool have_format;
+ uint8_t format_buffer[1024];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list ready;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[1];
+ struct props props;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct spa_source timer_source;
+ struct itimerspec timerspec;
+
+ bool started;
+ uint64_t start_time;
+ uint64_t elapsed_time;
+
+ uint64_t buffer_count;
+
+ struct port port;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS)
+
+#define DEFAULT_LIVE false
+
+static void reset_props(struct impl *this, struct props *props)
+{
+ props->live = DEFAULT_LIVE;
+}
+
+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_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ 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_Props:
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_live, SPA_POD_Bool(this->props.live));
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct port *port = &this->port;
+
+ if (param == NULL) {
+ reset_props(this, &this->props);
+ return 0;
+ }
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_live, SPA_POD_OPT_Bool(&this->props.live));
+
+ if (this->props.live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ else
+ port->info.flags &= ~SPA_PORT_FLAG_LIVE;
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static void set_timer(struct impl *this, bool enabled)
+{
+ if (this->callbacks.funcs || this->props.live) {
+ if (enabled) {
+ if (this->props.live) {
+ uint64_t next_time = this->start_time + this->elapsed_time;
+ this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC;
+ this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC;
+ } else {
+ this->timerspec.it_value.tv_sec = 0;
+ this->timerspec.it_value.tv_nsec = 1;
+ }
+ } else {
+ 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);
+ }
+}
+
+static inline int read_timer(struct impl *this)
+{
+ uint64_t expirations;
+ int res = 0;
+
+ if (this->callbacks.funcs || 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, NAME " %p: timerfd error: %s",
+ this, spa_strerror(res));
+ }
+ }
+ return res;
+}
+
+static void render_buffer(struct impl *this, struct buffer *b)
+{
+}
+
+static int consume_buffer(struct impl *this)
+{
+ struct port *port = &this->port;
+ struct buffer *b;
+ struct spa_io_buffers *io = port->io;
+ int n_bytes;
+
+ if (read_timer(this) < 0)
+ return 0;
+
+ if (spa_list_is_empty(&port->ready)) {
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+ }
+ if (spa_list_is_empty(&port->ready)) {
+ spa_log_error(this->log, NAME " %p: no buffers", this);
+ return -EPIPE;
+ }
+
+ b = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ n_bytes = b->outbuf->datas[0].maxsize;
+
+ spa_log_trace(this->log, NAME " %p: dequeue buffer %d", this, b->id);
+
+ render_buffer(this, b);
+
+ b->outbuf->datas[0].chunk->offset = 0;
+ b->outbuf->datas[0].chunk->size = n_bytes;
+ b->outbuf->datas[0].chunk->stride = n_bytes;
+
+ if (b->h) {
+ b->h->seq = this->buffer_count;
+ b->h->pts = this->start_time + this->elapsed_time;
+ b->h->dts_offset = 0;
+ }
+
+ this->buffer_count++;
+ this->elapsed_time = this->buffer_count;
+ set_timer(this, true);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_NEED_DATA;
+ b->outstanding = true;
+
+ return SPA_STATUS_NEED_DATA;
+}
+
+static void on_input(struct spa_source *source)
+{
+ struct impl *this = source->data;
+
+ consume_buffer(this);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ struct timespec now;
+
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (this->started)
+ return 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (this->props.live)
+ this->start_time = SPA_TIMESPEC_TO_NSEC(&now);
+ else
+ this->start_time = 0;
+ this->buffer_count = 0;
+ this->elapsed_time = 0;
+
+ this->started = true;
+ set_timer(this, true);
+ break;
+ }
+ case SPA_NODE_COMMAND_Pause:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (!this->started)
+ return 0;
+
+ this->started = false;
+ set_timer(this, false);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (this->data_loop == NULL && callbacks != NULL) {
+ spa_log_error(this->log, "a data_loop is needed for async operation");
+ return -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(struct impl *this, int seq, struct port *port,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ return -ENOTSUP;
+}
+
+static int port_get_format(struct impl *this, struct port *port,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ if (!port->have_format)
+ return -EIO;
+
+ if (index > 0)
+ return 0;
+
+ *param = SPA_PTROFF(port->format_buffer, 0, struct spa_pod);
+
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ 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, seq, port,
+ result.index, filter, &param, &b)) <= 0)
+ return res;
+ break;
+ case SPA_PARAM_Format:
+ if ((res = port_get_format(this, port,
+ result.index, filter, &param, &b)) <= 0)
+ return res;
+ break;
+ case SPA_PARAM_Buffers:
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, 32),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(128),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1));
+ 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 clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ spa_list_init(&port->ready);
+ this->started = false;
+ set_timer(this, false);
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags, const struct spa_pod *format)
+{
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ if (SPA_POD_SIZE(format) > sizeof(port->format_buffer))
+ return -ENOSPC;
+ memcpy(port->format_buffer, format, SPA_POD_SIZE(format));
+ port->have_format = true;
+ }
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(this, port, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->outstanding = true;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ if (id == SPA_IO_Buffers)
+ port->io = data;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) {
+ struct buffer *b = &port->buffers[io->buffer_id];
+
+ if (!b->outstanding) {
+ spa_log_warn(this->log, NAME " %p: buffer %u in use", this,
+ io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+
+ spa_log_trace(this->log, NAME " %p: queue buffer %u", this, io->buffer_id);
+
+ spa_list_append(&port->ready, &b->link);
+ b->outstanding = false;
+
+ io->buffer_id = SPA_ID_INVALID;
+ io->status = SPA_STATUS_OK;
+ }
+ if (this->callbacks.funcs == NULL)
+ return consume_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .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 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);
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (this->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);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+
+ 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);
+ 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);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 0;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 1;
+ reset_props(this, &this->props);
+
+
+ this->timer_source.func = on_input;
+ 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->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;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF;
+ if (this->props.live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_IO, 0);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 4;
+
+ spa_list_init(&port->ready);
+
+ 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_fakesink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ NAME,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/test/fakesrc.c b/spa/plugins/test/fakesrc.c
new file mode 100644
index 0000000..f414543
--- /dev/null
+++ b/spa/plugins/test/fakesrc.c
@@ -0,0 +1,876 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/param.h>
+#include <spa/param/format.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/filter.h>
+
+#define NAME "fakesrc"
+
+struct props {
+ bool live;
+ uint32_t pattern;
+};
+
+#define MAX_BUFFERS 16
+#define MAX_PORTS 1
+
+struct buffer {
+ uint32_t id;
+ struct spa_buffer *outbuf;
+ bool outstanding;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+
+ bool have_format;
+ uint8_t format_buffer[1024];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list empty;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[1];
+ struct props props;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct spa_source timer_source;
+ struct itimerspec timerspec;
+
+ bool started;
+ uint64_t start_time;
+ uint64_t elapsed_time;
+
+ uint64_t buffer_count;
+ bool underrun;
+
+ struct port port;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_PORTS)
+
+#define DEFAULT_LIVE false
+#define DEFAULT_PATTERN 0
+
+static void reset_props(struct impl *this, struct props *props)
+{
+ props->live = DEFAULT_LIVE;
+ props->pattern = DEFAULT_PATTERN;
+}
+
+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_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ 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_Props:
+ {
+ struct props *p = &this->props;
+
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_live, SPA_POD_Bool(p->live),
+ SPA_PROP_patternType, SPA_POD_CHOICE_ENUM_Id(2, p->pattern, p->pattern));
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct port *port = &this->port;
+
+ if (param == NULL) {
+ reset_props(this, p);
+ return 0;
+ }
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_live, SPA_POD_OPT_Bool(&p->live),
+ SPA_PROP_patternType, SPA_POD_OPT_Id(&p->pattern));
+
+ if (p->live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ else
+ port->info.flags &= ~SPA_PORT_FLAG_LIVE;
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int fill_buffer(struct impl *this, struct buffer *b)
+{
+ return 0;
+}
+
+static void set_timer(struct impl *this, bool enabled)
+{
+ if (this->callbacks.funcs || this->props.live) {
+ if (enabled) {
+ if (this->props.live) {
+ uint64_t next_time = this->start_time + this->elapsed_time;
+ this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC;
+ this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC;
+ } else {
+ this->timerspec.it_value.tv_sec = 0;
+ this->timerspec.it_value.tv_nsec = 1;
+ }
+ } else {
+ 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);
+ }
+}
+
+static inline int read_timer(struct impl *this)
+{
+ uint64_t expirations;
+ int res = 0;
+
+ if (this->callbacks.funcs || 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, NAME " %p: timerfd error: %s",
+ this, spa_strerror(res));
+ }
+ }
+ return res;
+}
+
+static int make_buffer(struct impl *this)
+{
+ struct buffer *b;
+ struct port *port = &this->port;
+ struct spa_io_buffers *io = port->io;
+ int n_bytes;
+
+ if (read_timer(this) < 0)
+ return 0;
+
+ if (spa_list_is_empty(&port->empty)) {
+ set_timer(this, false);
+ this->underrun = true;
+ spa_log_error(this->log, NAME " %p: out of buffers", this);
+ return -EPIPE;
+ }
+ b = spa_list_first(&port->empty, struct buffer, link);
+ spa_list_remove(&b->link);
+ b->outstanding = true;
+
+ n_bytes = b->outbuf->datas[0].maxsize;
+
+ spa_log_trace(this->log, NAME " %p: dequeue buffer %d", this, b->id);
+
+ fill_buffer(this, b);
+
+ b->outbuf->datas[0].chunk->offset = 0;
+ b->outbuf->datas[0].chunk->size = n_bytes;
+ b->outbuf->datas[0].chunk->stride = n_bytes;
+
+ if (b->h) {
+ b->h->seq = this->buffer_count;
+ b->h->pts = this->start_time + this->elapsed_time;
+ b->h->dts_offset = 0;
+ }
+
+ this->buffer_count++;
+ this->elapsed_time = this->buffer_count;
+ set_timer(this, true);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static void on_output(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ int res;
+
+ res = make_buffer(this);
+
+ if (res == SPA_STATUS_HAVE_DATA && this->callbacks.funcs)
+ spa_node_call_ready(&this->callbacks, res);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ struct timespec now;
+
+ if (!port->have_format)
+ return -EIO;
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (this->started)
+ return 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (this->props.live)
+ this->start_time = SPA_TIMESPEC_TO_NSEC(&now);
+ else
+ this->start_time = 0;
+ this->buffer_count = 0;
+ this->elapsed_time = 0;
+
+ this->started = true;
+ set_timer(this, true);
+ break;
+ }
+ case SPA_NODE_COMMAND_Pause:
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (!this->started)
+ return 0;
+
+ this->started = false;
+ set_timer(this, false);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (this->data_loop == NULL && callbacks != NULL) {
+ spa_log_error(this->log, "a data_loop is needed for async operation");
+ return -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(struct impl *this, struct port *port,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ return 0;
+}
+
+static int port_get_format(struct impl *this, struct port *port,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ if (!port->have_format)
+ return -EIO;
+ if (index > 0)
+ return 0;
+
+ *param = SPA_PTROFF(port->format_buffer, 0, struct spa_pod);
+
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ 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, port,
+ result.index, filter, &param, &b)) <= 0)
+ return res;
+ break;
+ case SPA_PARAM_Format:
+ if ((res = port_get_format(this, port,
+ result.index, filter, &param, &b)) <= 0)
+ return res;
+ break;
+ case SPA_PARAM_Buffers:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(32, 2, 32),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(128),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1));
+ break;
+ default:
+ return 0;
+ }
+ 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 clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ spa_list_init(&port->empty);
+ this->started = false;
+ set_timer(this, false);
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ if (SPA_POD_SIZE(format) > sizeof(port->format_buffer))
+ return -ENOSPC;
+ memcpy(port->format_buffer, format, SPA_POD_SIZE(format));
+ port->have_format = true;
+ }
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(this, port, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t flags,
+ uint32_t port_id,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->outstanding = false;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ }
+ spa_list_append(&port->empty, &b->link);
+ }
+ port->n_buffers = n_buffers;
+ this->underrun = false;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ if (id == SPA_IO_Buffers)
+ port->io = data;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+ struct buffer *b = &port->buffers[id];
+ spa_return_if_fail(b->outstanding);
+
+ spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id);
+
+ b->outstanding = false;
+ spa_list_append(&port->empty, &b->link);
+
+ if (this->underrun) {
+ set_timer(this, true);
+ this->underrun = false;
+ }
+}
+
+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(port_id == 0, -EINVAL);
+ port = &this->port;
+
+ spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL);
+
+ reuse_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (io->buffer_id < port->n_buffers) {
+ reuse_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (this->callbacks.funcs == NULL)
+ return make_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .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 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);
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (this->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);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+
+ 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);
+ 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);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 0;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 1;
+ reset_props(this, &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->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;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF;
+ if (this->props.live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_IO, 0);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 4;
+
+ spa_list_init(&port->empty);
+
+ 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_fakesrc_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ NAME,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/test/meson.build b/spa/plugins/test/meson.build
new file mode 100644
index 0000000..950ee7c
--- /dev/null
+++ b/spa/plugins/test/meson.build
@@ -0,0 +1,7 @@
+test_sources = ['fakesrc.c', 'fakesink.c', 'plugin.c']
+
+testlib = shared_library('spa-test',
+ test_sources,
+ dependencies : [ spa_dep, pthread_lib ],
+ install : true,
+ install_dir : spa_plugindir / 'test')
diff --git a/spa/plugins/test/plugin.c b/spa/plugins/test/plugin.c
new file mode 100644
index 0000000..409230f
--- /dev/null
+++ b/spa/plugins/test/plugin.c
@@ -0,0 +1,50 @@
+/* Spa Test plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_fakesrc_factory;
+extern const struct spa_handle_factory spa_fakesink_factory;
+
+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_fakesrc_factory;
+ break;
+ case 1:
+ *factory = &spa_fakesink_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/v4l2/meson.build b/spa/plugins/v4l2/meson.build
new file mode 100644
index 0000000..648583f
--- /dev/null
+++ b/spa/plugins/v4l2/meson.build
@@ -0,0 +1,10 @@
+v4l2_sources = ['v4l2.c',
+ 'v4l2-device.c',
+ 'v4l2-udev.c',
+ 'v4l2-source.c']
+
+v4l2lib = shared_library('spa-v4l2',
+ v4l2_sources,
+ dependencies : [ spa_dep, libudev_dep, libinotify_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'v4l2')
diff --git a/spa/plugins/v4l2/v4l2-device.c b/spa/plugins/v4l2/v4l2-device.c
new file mode 100644
index 0000000..53fb033
--- /dev/null
+++ b/spa/plugins/v4l2/v4l2-device.c
@@ -0,0 +1,291 @@
+/* Spa V4l2 Source
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <linux/videodev2.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/pod/builder.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/param/param.h>
+
+#include "v4l2.h"
+
+#define NAME "v4l2-device"
+
+static const char default_device[] = "/dev/video0";
+
+struct props {
+ char device[64];
+ char product_id[6];
+ char vendor_id[6];
+ int device_fd;
+};
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, default_device, 64);
+}
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+
+ struct props props;
+
+ struct spa_hook_list hooks;
+
+ struct spa_v4l2_device dev;
+};
+
+static int emit_info(struct impl *this, bool full)
+{
+ int res;
+ struct spa_dict_item items[12];
+ uint32_t n_items = 0;
+ struct spa_device_info info;
+ struct spa_param_info params[2];
+ char path[128], version[16], capabilities[16], device_caps[16];
+
+ if ((res = spa_v4l2_open(&this->dev, this->props.device)) < 0)
+ return res;
+
+ 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), "v4l2:%s", this->props.device);
+ ADD_ITEM(SPA_KEY_OBJECT_PATH, path);
+ ADD_ITEM(SPA_KEY_DEVICE_API, "v4l2");
+ ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device");
+ if (this->props.product_id[0])
+ ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_ID, this->props.product_id);
+ if (this->props.vendor_id[0])
+ ADD_ITEM(SPA_KEY_DEVICE_VENDOR_ID, this->props.vendor_id);
+ ADD_ITEM(SPA_KEY_API_V4L2_PATH, (char *)this->props.device);
+ ADD_ITEM(SPA_KEY_API_V4L2_CAP_DRIVER, (char *)this->dev.cap.driver);
+ ADD_ITEM(SPA_KEY_API_V4L2_CAP_CARD, (char *)this->dev.cap.card);
+ ADD_ITEM(SPA_KEY_API_V4L2_CAP_BUS_INFO, (char *)this->dev.cap.bus_info);
+ snprintf(version, sizeof(version), "%u.%u.%u",
+ (this->dev.cap.version >> 16) & 0xFF,
+ (this->dev.cap.version >> 8) & 0xFF,
+ (this->dev.cap.version) & 0xFF);
+ ADD_ITEM(SPA_KEY_API_V4L2_CAP_VERSION, version);
+ snprintf(capabilities, sizeof(capabilities), "%08x", this->dev.cap.capabilities);
+ ADD_ITEM(SPA_KEY_API_V4L2_CAP_CAPABILITIES, capabilities);
+ snprintf(device_caps, sizeof(device_caps), "%08x", this->dev.cap.device_caps);
+ ADD_ITEM(SPA_KEY_API_V4L2_CAP_DEVICE_CAPS, device_caps);
+#undef ADD_ITEM
+ info.props = &SPA_DICT_INIT(items, n_items);
+
+ info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_WRITE);
+ info.n_params = 0;
+ info.params = params;
+
+ spa_device_emit_info(&this->hooks, &info);
+
+ if (spa_v4l2_is_capture(&this->dev)) {
+ struct spa_device_object_info oinfo;
+
+ oinfo = SPA_DEVICE_OBJECT_INFO_INIT();
+ oinfo.type = SPA_TYPE_INTERFACE_Node;
+ oinfo.factory_name = SPA_NAME_API_V4L2_SOURCE;
+ oinfo.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ oinfo.props = &SPA_DICT_INIT(items, n_items);
+
+ spa_device_emit_object_info(&this->hooks, 0, &oinfo);
+ }
+
+ spa_v4l2_close(&this->dev);
+
+ return 0;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ int res = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ if (events->info || events->object_info)
+ res = emit_info(this, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return res;
+}
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ return -ENOTSUP;
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static 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;
+ const char *str;
+
+ 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_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+ this->dev.log = this->log;
+ this->dev.fd = -1;
+
+ reset_props(&this->props);
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_V4L2_PATH)))
+ strncpy(this->props.device, str, 63);
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_PRODUCT_ID)))
+ strncpy(this->props.product_id, str, 5);
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_VENDOR_ID)))
+ strncpy(this->props.vendor_id, str, 5);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+const struct spa_handle_factory spa_v4l2_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_V4L2_DEVICE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/v4l2/v4l2-source.c b/spa/plugins/v4l2/v4l2-source.c
new file mode 100644
index 0000000..b6eae49
--- /dev/null
+++ b/spa/plugins/v4l2/v4l2-source.c
@@ -0,0 +1,1055 @@
+/* Spa V4l2 Source
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <linux/videodev2.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.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/pod/filter.h>
+#include <spa/control/control.h>
+
+#include "v4l2.h"
+
+static const char default_device[] = "/dev/video0";
+
+struct props {
+ char device[64];
+ char device_name[128];
+ int device_fd;
+};
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, default_device, 64);
+}
+
+#define MAX_BUFFERS 32
+
+#define BUFFER_FLAG_OUTSTANDING (1<<0)
+#define BUFFER_FLAG_ALLOCATED (1<<1)
+#define BUFFER_FLAG_MAPPED (1<<2)
+
+struct buffer {
+ uint32_t id;
+ uint32_t flags;
+ struct spa_list link;
+ struct spa_buffer *outbuf;
+ struct spa_meta_header *h;
+ struct v4l2_buffer v4l2_buffer;
+ void *ptr;
+};
+
+#define MAX_CONTROLS 64
+
+struct control {
+ uint32_t id;
+ uint32_t ctrl_id;
+ double value;
+};
+
+struct port {
+ struct impl *impl;
+
+ bool alloc_buffers;
+ bool have_expbuf;
+
+ bool next_fmtdesc;
+ struct v4l2_fmtdesc fmtdesc;
+ bool next_frmsize;
+ struct v4l2_frmsizeenum frmsize;
+ struct v4l2_frmivalenum frmival;
+
+ bool have_format;
+ struct spa_video_info current_format;
+ struct spa_fraction rate;
+
+ struct spa_v4l2_device dev;
+
+ bool have_query_ext_ctrl;
+ struct v4l2_format fmt;
+ enum v4l2_buf_type type;
+ enum v4l2_memory memtype;
+
+ struct control controls[MAX_CONTROLS];
+ uint32_t n_controls;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+ struct spa_list queue;
+
+ struct spa_source source;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_io_buffers *io;
+ struct spa_io_sequence *control;
+#define PORT_PropInfo 0
+#define PORT_EnumFormat 1
+#define PORT_Meta 2
+#define PORT_IO 3
+#define PORT_Format 4
+#define PORT_Buffers 5
+#define PORT_Latency 6
+#define N_PORT_PARAMS 7
+ struct spa_param_info params[N_PORT_PARAMS];
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+
+ uint64_t info_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
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct port out_ports[1];
+
+ struct spa_io_position *position;
+ struct spa_io_clock *clock;
+
+ struct spa_latency_info latency[2];
+};
+
+#define CHECK_PORT(this,direction,port_id) ((direction) == SPA_DIRECTION_OUTPUT && (port_id) == 0)
+
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) GET_OUT_PORT(this,p)
+
+#include "v4l2-utils.c"
+
+static int port_get_format(struct port *port,
+ uint32_t index,
+ const struct spa_pod *filter,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct spa_pod_frame f;
+
+ if (!port->have_format)
+ return -EIO;
+ if (index > 0)
+ return 0;
+
+ spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_Format);
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_mediaType, SPA_POD_Id(port->current_format.media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(port->current_format.media_subtype),
+ 0);
+
+ switch (port->current_format.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(port->current_format.info.raw.format),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.raw.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.raw.framerate),
+ 0);
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ case SPA_MEDIA_SUBTYPE_jpeg:
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.mjpg.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.mjpg.framerate),
+ 0);
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ spa_pod_builder_add(builder,
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.h264.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.h264.framerate),
+ 0);
+ break;
+ default:
+ return -EIO;
+ }
+
+ *param = spa_pod_builder_pop(builder, &f);
+
+ return 1;
+}
+
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ 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;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
+ SPA_PROP_INFO_description, SPA_POD_String("The V4L2 device"),
+ SPA_PROP_INFO_type, SPA_POD_String(p->device));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName),
+ SPA_PROP_INFO_description, SPA_POD_String("The V4L2 device name"),
+ SPA_PROP_INFO_type, SPA_POD_String(p->device_name));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceFd),
+ SPA_PROP_INFO_description, SPA_POD_String("The V4L2 fd"),
+ SPA_PROP_INFO_type, SPA_POD_Int(p->device_fd));
+ break;
+ default:
+ return spa_v4l2_enum_controls(this, seq, result.index - 3, num, filter);
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_device, SPA_POD_String(p->device),
+ SPA_PROP_deviceName, SPA_POD_String(p->device_name),
+ SPA_PROP_deviceFd, SPA_POD_Int(p->device_fd));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_EnumFormat:
+ return spa_v4l2_enum_format(this, seq, start, num, filter);
+ case SPA_PARAM_Format:
+ if((res = port_get_format(GET_OUT_PORT(this, 0),
+ result.index, filter, &param, &b)) <= 0)
+ return res;
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ 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_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ struct spa_pod_prop *prop;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_device:
+ strncpy(p->device,
+ (char *)SPA_POD_CONTENTS(struct spa_pod_string, &prop->value),
+ sizeof(p->device)-1);
+ break;
+ default:
+ spa_v4l2_set_control(this, prop->key, prop);
+ break;
+ }
+ }
+
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ 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);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = GET_OUT_PORT(this, 0);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_ParamBegin:
+ if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_ParamEnd:
+ if (port->have_format)
+ return 0;
+ if ((res = spa_v4l2_close(&port->dev)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Start:
+ {
+ if (!port->have_format) {
+ spa_log_error(this->log, "no format");
+ return -EIO;
+ }
+ if (port->n_buffers == 0) {
+ spa_log_error(this->log, "no buffers");
+ return -EIO;
+ }
+
+ if ((res = spa_v4l2_stream_on(this)) < 0)
+ return res;
+ break;
+ }
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = spa_v4l2_stream_off(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_DEVICE_API, "v4l2" },
+ { SPA_KEY_MEDIA_CLASS, "Video/Source" },
+ { SPA_KEY_MEDIA_ROLE, "Camera" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+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(info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, GET_OUT_PORT(this, 0), true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object,
+ enum spa_direction direction,
+ uint32_t port_id, const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object,
+ enum spa_direction direction,
+ uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ return spa_v4l2_enum_controls(this, seq, start, num, filter);
+
+ case SPA_PARAM_EnumFormat:
+ return spa_v4l2_enum_format(this, seq, start, num, filter);
+
+ case SPA_PARAM_Format:
+ if((res = port_get_format(port, result.index, filter, &param, &b)) <= 0)
+ return res;
+ break;
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(4, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(port->fmt.fmt.pix.sizeimage),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->fmt.fmt.pix.bytesperline));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Control),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_sequence)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ param = spa_latency_build(&b, id, &this->latency[result.index]);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ struct spa_video_info info;
+ int res;
+
+ if (port->have_format) {
+ spa_v4l2_stream_off(this);
+ spa_v4l2_clear_buffers(this);
+ }
+ if (format == NULL) {
+ if (!port->have_format)
+ return 0;
+
+ port->have_format = false;
+ port->dev.have_format = false;
+ spa_v4l2_close(&port->dev);
+ goto done;
+ } else {
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_video) {
+ spa_log_error(this->log, "media type must be video");
+ return -EINVAL;
+ }
+
+ switch (info.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ if (spa_format_video_raw_parse(format, &info.info.raw) < 0) {
+ spa_log_error(this->log, "can't parse video raw");
+ return -EINVAL;
+ }
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ if (spa_format_video_mjpg_parse(format, &info.info.mjpg) < 0)
+ return -EINVAL;
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ if (spa_format_video_h264_parse(format, &info.info.h264) < 0)
+ return -EINVAL;
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+
+ if (port->have_format && !SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
+ port->have_format = false;
+ }
+
+ if ((res = spa_v4l2_set_format(this, &info, flags)) < 0)
+ return res;
+
+ if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_TEST_ONLY)) {
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ done:
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 0);
+ }
+ emit_port_info(this, port, false);
+ emit_node_info(this, false);
+
+ return 0;
+}
+
+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)
+{
+ int res;
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(object != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ this->latency[info.direction] = info;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[PORT_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_port_info(this, port, false);
+ break;
+ }
+ case SPA_PARAM_Format:
+ res = port_set_format(object, port, flags, param);
+ break;
+ default:
+ res = -ENOENT;
+ }
+ return res;
+}
+
+static int impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (port->n_buffers) {
+ spa_v4l2_stream_off(this);
+ if ((res = spa_v4l2_clear_buffers(this)) < 0)
+ return res;
+ }
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+ if (buffers == NULL)
+ return 0;
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) {
+ res = spa_v4l2_alloc_buffers(this, buffers, n_buffers);
+ } else {
+ res = spa_v4l2_use_buffers(this, buffers, n_buffers);
+ }
+ return res;
+}
+
+static int impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ case SPA_IO_Control:
+ port->control = 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;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+
+ port = GET_OUT_PORT(this, port_id);
+
+ spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL);
+
+ res = spa_v4l2_buffer_recycle(this, buffer_id);
+
+ return res;
+}
+
+static int process_control(struct impl *this, struct spa_pod_sequence *control)
+{
+ struct spa_pod_control *c;
+
+ SPA_POD_SEQUENCE_FOREACH(control, c) {
+ switch (c->type) {
+ case SPA_CONTROL_Properties:
+ {
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) &c->value;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ spa_v4l2_set_control(this, prop->key, prop);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ int res;
+ struct spa_io_buffers *io;
+ struct port *port;
+ struct buffer *b;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = GET_OUT_PORT(this, 0);
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (port->control)
+ process_control(this, &port->control->sequence);
+
+ spa_log_trace(this->log, "%p; status %d", this, io->status);
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (io->buffer_id < port->n_buffers) {
+ if ((res = spa_v4l2_buffer_recycle(this, io->buffer_id)) < 0)
+ return res;
+
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (spa_list_is_empty(&port->queue))
+ return SPA_STATUS_OK;
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+
+ spa_log_trace(this->log, "%p: dequeue buffer %d", this, b->id);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static 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;
+ const char *str;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ v4l2_log_topic_init(this->log);
+
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data_loop is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 0);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+ reset_props(&this->props);
+
+ port = GET_OUT_PORT(this, 0);
+ port->impl = this;
+ spa_list_init(&port->queue);
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[PORT_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READ);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ port->alloc_buffers = true;
+ port->have_expbuf = true;
+ port->have_query_ext_ctrl = true;
+ port->dev.log = this->log;
+ port->dev.fd = -1;
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_V4L2_PATH))) {
+ strncpy(this->props.device, str, 63);
+ if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0)
+ return res;
+ spa_v4l2_close(&port->dev);
+ }
+
+ 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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+const struct spa_handle_factory spa_v4l2_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_V4L2_SOURCE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/v4l2/v4l2-udev.c b/spa/plugins/v4l2/v4l2-udev.c
new file mode 100644
index 0000000..e7f9265
--- /dev/null
+++ b/spa/plugins/v4l2/v4l2-udev.c
@@ -0,0 +1,754 @@
+/* Spa V4l2 udev monitor
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/inotify.h>
+#include <fcntl.h>
+
+#include <libudev.h>
+
+#include <spa/support/log.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+
+#define NAME "v4l2-udev"
+
+#define MAX_DEVICES 64
+
+#define ACTION_ADD 0
+#define ACTION_REMOVE 1
+#define ACTION_DISABLE 2
+
+struct device {
+ uint32_t id;
+ struct udev_device *dev;
+ unsigned int accessible:1;
+ unsigned int ignored:1;
+ unsigned int emitted:1;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+
+ struct spa_hook_list hooks;
+
+ uint64_t info_all;
+ struct spa_device_info info;
+
+ struct udev *udev;
+ struct udev_monitor *umonitor;
+
+ struct device devices[MAX_DEVICES];
+ uint32_t n_devices;
+
+ struct spa_source source;
+ struct spa_source notify;
+};
+
+static int impl_udev_open(struct impl *this)
+{
+ if (this->udev == NULL) {
+ this->udev = udev_new();
+ if (this->udev == NULL)
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static int impl_udev_close(struct impl *this)
+{
+ if (this->udev != NULL)
+ udev_unref(this->udev);
+ this->udev = NULL;
+ return 0;
+}
+
+static struct device *add_device(struct impl *this, uint32_t id, struct udev_device *dev)
+{
+ struct device *device;
+
+ if (this->n_devices >= MAX_DEVICES)
+ return NULL;
+ device = &this->devices[this->n_devices++];
+ spa_zero(*device);
+ device->id = id;
+ udev_device_ref(dev);
+ device->dev = dev;
+ return device;
+}
+
+static struct device *find_device(struct impl *this, uint32_t id)
+{
+ uint32_t i;
+ for (i = 0; i < this->n_devices; i++) {
+ if (this->devices[i].id == id)
+ return &this->devices[i];
+ }
+ return NULL;
+}
+
+static void remove_device(struct impl *this, struct device *device)
+{
+ udev_device_unref(device->dev);
+ *device = this->devices[--this->n_devices];
+}
+
+static void clear_devices(struct impl *this)
+{
+ uint32_t i;
+ for (i = 0; i < this->n_devices; i++)
+ udev_device_unref(this->devices[i].dev);
+ this->n_devices = 0;
+}
+
+static uint32_t get_device_id(struct impl *this, struct udev_device *dev)
+{
+ const char *str;
+
+ if ((str = udev_device_get_devnode(dev)) == NULL)
+ return SPA_ID_INVALID;
+
+ if (!(str = strrchr(str, '/')))
+ return SPA_ID_INVALID;
+
+ if (strlen(str) <= 6 || strncmp(str, "/video", 6) != 0)
+ return SPA_ID_INVALID;
+
+ return atoi(str + 6);
+}
+
+static int dehex(char x)
+{
+ if (x >= '0' && x <= '9')
+ return x - '0';
+ if (x >= 'A' && x <= 'F')
+ return x - 'A' + 10;
+ if (x >= 'a' && x <= 'f')
+ return x - 'a' + 10;
+ return -1;
+}
+
+static void unescape(const char *src, char *dst)
+{
+ const char *s;
+ char *d;
+ int h1 = 0, h2 = 0;
+ enum { TEXT, BACKSLASH, EX, FIRST } state = TEXT;
+
+ for (s = src, d = dst; *s; s++) {
+ switch (state) {
+ case TEXT:
+ if (*s == '\\')
+ state = BACKSLASH;
+ else
+ *(d++) = *s;
+ break;
+
+ case BACKSLASH:
+ if (*s == 'x')
+ state = EX;
+ else {
+ *(d++) = '\\';
+ *(d++) = *s;
+ state = TEXT;
+ }
+ break;
+
+ case EX:
+ h1 = dehex(*s);
+ if (h1 < 0) {
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *s;
+ state = TEXT;
+ } else
+ state = FIRST;
+ break;
+
+ case FIRST:
+ h2 = dehex(*s);
+ if (h2 < 0) {
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *(s-1);
+ *(d++) = *s;
+ } else
+ *(d++) = (char) (h1 << 4) | h2;
+ state = TEXT;
+ break;
+ }
+ }
+ switch (state) {
+ case TEXT:
+ break;
+ case BACKSLASH:
+ *(d++) = '\\';
+ break;
+ case EX:
+ *(d++) = '\\';
+ *(d++) = 'x';
+ break;
+ case FIRST:
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *(s-1);
+ break;
+ }
+ *d = 0;
+}
+
+static int emit_object_info(struct impl *this, struct device *device)
+{
+ struct spa_device_object_info info;
+ uint32_t id = device->id;
+ struct udev_device *dev = device->dev;
+ const char *str;
+ struct spa_dict_item items[20];
+ uint32_t n_items = 0;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+
+ info.type = SPA_TYPE_INTERFACE_Device;
+ info.factory_name = SPA_NAME_API_V4L2_DEVICE;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.flags = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API,"udev");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "v4l2");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Video/Device");
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_V4L2_PATH, udev_device_get_devnode(dev));
+
+ if ((str = udev_device_get_property_value(dev, "USEC_INITIALIZED")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PLUGGED_USEC, str);
+
+ str = udev_device_get_property_value(dev, "ID_PATH");
+ if (!(str && *str))
+ str = udev_device_get_syspath(dev);
+ if (str && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str);
+ }
+ if ((str = udev_device_get_devpath(dev)) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_ID")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_ID, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_BUS")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "SUBSYSTEM")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_VENDOR_ID")) && *str) {
+ int32_t val;
+ if (spa_atoi32(str, &val, 16)) {
+ char *dec = alloca(12); /* 0xffff is max */
+ snprintf(dec, 12, "0x%04x", val);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, dec);
+ }
+ }
+ str = udev_device_get_property_value(dev, "ID_VENDOR_FROM_DATABASE");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_VENDOR_ENC");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_VENDOR");
+ } else {
+ char *t = alloca(strlen(str) + 1);
+ unescape(str, t);
+ str = t;
+ }
+ }
+ if (str && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_NAME, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_MODEL_ID")) && *str) {
+ int32_t val;
+ if (spa_atoi32(str, &val, 16)) {
+ char *dec = alloca(12); /* 0xffff is max */
+ snprintf(dec, 12, "0x%04x", val);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, dec);
+ }
+ }
+
+ str = udev_device_get_property_value(dev, "ID_MODEL_FROM_DATABASE");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_MODEL_ENC");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_MODEL");
+ if (!(str && *str))
+ str = udev_device_get_property_value(dev, "ID_V4L_PRODUCT");
+ } else {
+ char *t = alloca(strlen(str) + 1);
+ unescape(str, t);
+ str = t;
+ }
+ }
+ if (str && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_NAME, str);
+
+ if ((str = udev_device_get_property_value(dev, "ID_SERIAL")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SERIAL, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_V4L_CAPABILITIES")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_CAPABILITIES, str);
+ }
+ info.props = &SPA_DICT_INIT(items, n_items);
+ spa_device_emit_object_info(&this->hooks, id, &info);
+ device->emitted = true;
+
+ return 1;
+}
+
+static bool check_access(struct impl *this, struct device *device)
+{
+ char path[128];
+
+ snprintf(path, sizeof(path), "/dev/video%u", device->id);
+ device->accessible = access(path, R_OK|W_OK) >= 0;
+ spa_log_debug(this->log, "%s accessible:%u", path, device->accessible);
+
+ return device->accessible;
+}
+
+static void process_device(struct impl *this, uint32_t action, struct udev_device *dev)
+{
+ uint32_t id;
+ struct device *device;
+ bool emitted;
+
+ if ((id = get_device_id(this, dev)) == SPA_ID_INVALID)
+ return;
+
+ device = find_device(this, id);
+ if (device && device->ignored)
+ return;
+
+ switch (action) {
+ case ACTION_ADD:
+ if (device == NULL)
+ device = add_device(this, id, dev);
+ if (device == NULL)
+ return;
+ if (!check_access(this, device))
+ return;
+ emit_object_info(this, device);
+ break;
+
+ case ACTION_REMOVE:
+ if (device == NULL)
+ return;
+ emitted = device->emitted;
+ remove_device(this, device);
+ if (emitted)
+ spa_device_emit_object_info(&this->hooks, id, NULL);
+ break;
+
+ case ACTION_DISABLE:
+ if (device == NULL)
+ return;
+ if (device->emitted) {
+ device->emitted = false;
+ spa_device_emit_object_info(&this->hooks, id, NULL);
+ }
+ break;
+ }
+}
+
+static int stop_inotify(struct impl *this)
+{
+ if (this->notify.fd == -1)
+ return 0;
+ spa_log_info(this->log, "stop inotify");
+ spa_loop_remove_source(this->main_loop, &this->notify);
+ close(this->notify.fd);
+ this->notify.fd = -1;
+ return 0;
+}
+
+static void impl_on_notify_events(struct spa_source *source)
+{
+ bool deleted = false;
+ struct impl *this = source->data;
+ union {
+ unsigned char name[sizeof(struct inotify_event) + NAME_MAX + 1];
+ struct inotify_event e; /* for appropriate alignment */
+ } buf;
+
+ while (true) {
+ ssize_t len;
+ const struct inotify_event *event;
+ void *p, *e;
+
+ len = read(source->fd, &buf, sizeof(buf));
+ if (len < 0 && errno != EAGAIN)
+ break;
+ if (len <= 0)
+ break;
+
+ e = SPA_PTROFF(&buf, len, void);
+
+ for (p = &buf; p < e;
+ p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) {
+ unsigned int id;
+ struct device *device;
+
+ event = (const struct inotify_event *) p;
+
+ if ((event->mask & IN_ATTRIB)) {
+ bool access;
+ if (sscanf(event->name, "video%u", &id) != 1)
+ continue;
+ if ((device = find_device(this, id)) == NULL)
+ continue;
+ access = check_access(this, device);
+ if (access && !device->emitted)
+ process_device(this, ACTION_ADD, device->dev);
+ else if (!access && device->emitted)
+ process_device(this, ACTION_DISABLE, device->dev);
+ }
+ /* /dev/ might have been removed */
+ if ((event->mask & (IN_DELETE_SELF | IN_MOVE_SELF)))
+ deleted = true;
+ }
+ }
+ if (deleted)
+ stop_inotify(this);
+}
+
+static int start_inotify(struct impl *this)
+{
+ int res, notify_fd;
+
+ if (this->notify.fd != -1)
+ return 0;
+
+ if ((notify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) < 0)
+ return -errno;
+
+ res = inotify_add_watch(notify_fd, "/dev",
+ IN_ATTRIB | IN_CLOSE_WRITE | IN_DELETE_SELF | IN_MOVE_SELF);
+ if (res < 0) {
+ res = -errno;
+ close(notify_fd);
+
+ if (res == -ENOENT) {
+ spa_log_debug(this->log, "/dev/ does not exist yet");
+ return 0;
+ }
+ spa_log_error(this->log, "inotify_add_watch() failed: %s", spa_strerror(res));
+ return res;
+ }
+ spa_log_info(this->log, "start inotify");
+ this->notify.func = impl_on_notify_events;
+ this->notify.data = this;
+ this->notify.fd = notify_fd;
+ this->notify.mask = SPA_IO_IN | SPA_IO_ERR;
+
+ spa_loop_add_source(this->main_loop, &this->notify);
+
+ return 0;
+}
+
+static void impl_on_fd_events(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct udev_device *dev;
+ const char *action;
+
+ dev = udev_monitor_receive_device(this->umonitor);
+ if (dev == NULL)
+ return;
+
+ if ((action = udev_device_get_action(dev)) == NULL)
+ action = "change";
+
+ spa_log_debug(this->log, "action %s", action);
+
+ start_inotify(this);
+
+ if (spa_streq(action, "add") ||
+ spa_streq(action, "change")) {
+ process_device(this, ACTION_ADD, dev);
+ } else if (spa_streq(action, "remove")) {
+ process_device(this, ACTION_REMOVE, dev);
+ }
+ udev_device_unref(dev);
+}
+
+static int start_monitor(struct impl *this)
+{
+ int res;
+
+ if (this->umonitor != NULL)
+ return 0;
+
+ this->umonitor = udev_monitor_new_from_netlink(this->udev, "udev");
+ if (this->umonitor == NULL)
+ return -ENOMEM;
+
+ udev_monitor_filter_add_match_subsystem_devtype(this->umonitor,
+ "video4linux", NULL);
+ udev_monitor_enable_receiving(this->umonitor);
+
+ this->source.func = impl_on_fd_events;
+ this->source.data = this;
+ this->source.fd = udev_monitor_get_fd(this->umonitor);
+ this->source.mask = SPA_IO_IN | SPA_IO_ERR;
+
+ spa_log_debug(this->log, "monitor %p", this->umonitor);
+ spa_loop_add_source(this->main_loop, &this->source);
+
+ if ((res = start_inotify(this)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int stop_monitor(struct impl *this)
+{
+ if (this->umonitor == NULL)
+ return 0;
+
+ clear_devices (this);
+
+ spa_loop_remove_source(this->main_loop, &this->source);
+ udev_monitor_unref(this->umonitor);
+ this->umonitor = NULL;
+
+ stop_inotify(this);
+
+ return 0;
+}
+
+static int enum_devices(struct impl *this)
+{
+ struct udev_enumerate *enumerate;
+ struct udev_list_entry *devices;
+
+ enumerate = udev_enumerate_new(this->udev);
+ if (enumerate == NULL)
+ return -ENOMEM;
+
+ udev_enumerate_add_match_subsystem(enumerate, "video4linux");
+ udev_enumerate_scan_devices(enumerate);
+
+ for (devices = udev_enumerate_get_list_entry(enumerate); devices;
+ devices = udev_list_entry_get_next(devices)) {
+ struct udev_device *dev;
+
+ dev = udev_device_new_from_syspath(this->udev, udev_list_entry_get_name(devices));
+ if (dev == NULL)
+ continue;
+
+ process_device(this, ACTION_ADD, dev);
+
+ udev_device_unref(dev);
+ }
+ udev_enumerate_unref(enumerate);
+
+ return 0;
+}
+
+static const struct spa_dict_item device_info_items[] = {
+ { SPA_KEY_DEVICE_API, "udev" },
+ { SPA_KEY_DEVICE_NICK, "v4l2-udev" },
+ { SPA_KEY_API_UDEV_MATCH, "video4linux" },
+};
+
+static void emit_device_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(device_info_items);
+ spa_device_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void impl_hook_removed(struct spa_hook *hook)
+{
+ struct impl *this = hook->priv;
+ if (spa_hook_list_is_empty(&this->hooks)) {
+ stop_monitor(this);
+ impl_udev_close(this);
+ }
+}
+
+static int
+impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ int res;
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ if ((res = impl_udev_open(this)) < 0)
+ return res;
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_device_info(this, true);
+
+ if ((res = enum_devices(this)) < 0)
+ return res;
+
+ if ((res = start_monitor(this)) < 0)
+ return res;
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ listener->removed = impl_hook_removed;
+ listener->priv = this;
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+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_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ stop_monitor(this);
+ impl_udev_close(this);
+ 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;
+
+ 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->notify.fd = -1;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+
+ if (this->main_loop == NULL) {
+ spa_log_error(this->log, "a main-loop is needed");
+ return -EINVAL;
+ }
+ spa_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ this->info = SPA_DEVICE_INFO_INIT();
+ this->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS;
+ this->info.flags = 0;
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+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);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_v4l2_udev_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_V4L2_ENUM_UDEV,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c
new file mode 100644
index 0000000..8ffda13
--- /dev/null
+++ b/spa/plugins/v4l2/v4l2-utils.c
@@ -0,0 +1,1743 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sched.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <poll.h>
+
+#include <spa/utils/result.h>
+
+static int xioctl(int fd, int request, void *arg)
+{
+ int err;
+
+ do {
+ err = ioctl(fd, request, arg);
+ } while (err == -1 && errno == EINTR);
+
+ return err;
+}
+
+int spa_v4l2_open(struct spa_v4l2_device *dev, const char *path)
+{
+ struct stat st;
+ int err;
+
+ if (dev->fd != -1)
+ return 0;
+
+ if (path == NULL) {
+ spa_log_error(dev->log, "Device property not set");
+ return -EIO;
+ }
+
+ spa_log_info(dev->log, "device is '%s'", path);
+
+ dev->fd = open(path, O_RDWR | O_NONBLOCK, 0);
+ if (dev->fd == -1) {
+ err = errno;
+ spa_log_error(dev->log, "Cannot open '%s': %d, %s",
+ path, err, strerror(err));
+ goto error;
+ }
+
+ if (fstat(dev->fd, &st) < 0) {
+ err = errno;
+ spa_log_error(dev->log, "Cannot identify '%s': %d, %s",
+ path, err, strerror(err));
+ goto error_close;
+ }
+
+ if (!S_ISCHR(st.st_mode)) {
+ spa_log_error(dev->log, "%s is no device", path);
+ err = ENODEV;
+ goto error_close;
+ }
+
+ if (xioctl(dev->fd, VIDIOC_QUERYCAP, &dev->cap) < 0) {
+ err = errno;
+ spa_log_error(dev->log, "'%s' QUERYCAP: %m", path);
+ goto error_close;
+ }
+ snprintf(dev->path, sizeof(dev->path), "%s", path);
+ return 0;
+
+error_close:
+ close(dev->fd);
+ dev->fd = -1;
+error:
+ return -err;
+}
+
+int spa_v4l2_is_capture(struct spa_v4l2_device *dev)
+{
+ uint32_t caps = dev->cap.capabilities;
+ if ((caps & V4L2_CAP_DEVICE_CAPS))
+ caps = dev->cap.device_caps;
+ return (caps & V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE;
+}
+
+int spa_v4l2_close(struct spa_v4l2_device *dev)
+{
+ if (dev->fd == -1)
+ return 0;
+
+ if (dev->active || dev->have_format)
+ return 0;
+
+ spa_log_info(dev->log, "close '%s'", dev->path);
+
+ if (close(dev->fd))
+ spa_log_warn(dev->log, "close: %m");
+
+ dev->fd = -1;
+ return 0;
+}
+
+static int spa_v4l2_buffer_recycle(struct impl *this, uint32_t buffer_id)
+{
+ struct port *port = &this->out_ports[0];
+ struct buffer *b = &port->buffers[buffer_id];
+ struct spa_v4l2_device *dev = &port->dev;
+ int err;
+
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING))
+ return 0;
+
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUTSTANDING);
+ spa_log_trace(this->log, "v4l2 %p: recycle buffer %d", this, buffer_id);
+
+ if (xioctl(dev->fd, VIDIOC_QBUF, &b->v4l2_buffer) < 0) {
+ err = errno;
+ spa_log_error(this->log, "'%s' VIDIOC_QBUF: %m", this->props.device);
+ return -err;
+ }
+
+ return 0;
+}
+
+static int spa_v4l2_clear_buffers(struct impl *this)
+{
+ struct port *port = &this->out_ports[0];
+ struct v4l2_requestbuffers reqbuf;
+ uint32_t i;
+
+ if (port->n_buffers == 0)
+ return 0;
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d;
+
+ b = &port->buffers[i];
+ d = b->outbuf->datas;
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) {
+ spa_log_debug(this->log, "queueing outstanding buffer %p", b);
+ spa_v4l2_buffer_recycle(this, i);
+ }
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) {
+ munmap(b->ptr, d[0].maxsize);
+ }
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_ALLOCATED)) {
+ spa_log_debug(this->log, "close %d", (int) d[0].fd);
+ close(d[0].fd);
+ }
+ d[0].type = SPA_ID_INVALID;
+ }
+
+ spa_zero(reqbuf);
+ reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ reqbuf.memory = port->memtype;
+ reqbuf.count = 0;
+
+ if (xioctl(port->dev.fd, VIDIOC_REQBUFS, &reqbuf) < 0) {
+ spa_log_warn(this->log, "VIDIOC_REQBUFS: %m");
+ }
+ port->n_buffers = 0;
+
+ return 0;
+}
+
+
+struct format_info {
+ uint32_t fourcc;
+ uint32_t format;
+ uint32_t media_type;
+ uint32_t media_subtype;
+};
+
+#define VIDEO SPA_MEDIA_TYPE_video
+#define IMAGE SPA_MEDIA_TYPE_image
+
+#define RAW SPA_MEDIA_SUBTYPE_raw
+
+#define BAYER SPA_MEDIA_SUBTYPE_bayer
+#define MJPG SPA_MEDIA_SUBTYPE_mjpg
+#define JPEG SPA_MEDIA_SUBTYPE_jpeg
+#define DV SPA_MEDIA_SUBTYPE_dv
+#define MPEGTS SPA_MEDIA_SUBTYPE_mpegts
+#define H264 SPA_MEDIA_SUBTYPE_h264
+#define H263 SPA_MEDIA_SUBTYPE_h263
+#define MPEG1 SPA_MEDIA_SUBTYPE_mpeg1
+#define MPEG2 SPA_MEDIA_SUBTYPE_mpeg2
+#define MPEG4 SPA_MEDIA_SUBTYPE_mpeg4
+#define XVID SPA_MEDIA_SUBTYPE_xvid
+#define VC1 SPA_MEDIA_SUBTYPE_vc1
+#define VP8 SPA_MEDIA_SUBTYPE_vp8
+
+#define FORMAT_UNKNOWN SPA_VIDEO_FORMAT_UNKNOWN
+#define FORMAT_ENCODED SPA_VIDEO_FORMAT_ENCODED
+#define FORMAT_RGB15 SPA_VIDEO_FORMAT_RGB15
+#define FORMAT_BGR15 SPA_VIDEO_FORMAT_BGR15
+#define FORMAT_RGB16 SPA_VIDEO_FORMAT_RGB16
+#define FORMAT_BGR SPA_VIDEO_FORMAT_BGR
+#define FORMAT_RGB SPA_VIDEO_FORMAT_RGB
+#define FORMAT_BGRA SPA_VIDEO_FORMAT_BGRA
+#define FORMAT_BGRx SPA_VIDEO_FORMAT_BGRx
+#define FORMAT_ARGB SPA_VIDEO_FORMAT_ARGB
+#define FORMAT_xRGB SPA_VIDEO_FORMAT_xRGB
+#define FORMAT_GRAY8 SPA_VIDEO_FORMAT_GRAY8
+#define FORMAT_GRAY16_LE SPA_VIDEO_FORMAT_GRAY16_LE
+#define FORMAT_GRAY16_BE SPA_VIDEO_FORMAT_GRAY16_BE
+#define FORMAT_YVU9 SPA_VIDEO_FORMAT_YVU9
+#define FORMAT_YV12 SPA_VIDEO_FORMAT_YV12
+#define FORMAT_YUY2 SPA_VIDEO_FORMAT_YUY2
+#define FORMAT_YVYU SPA_VIDEO_FORMAT_YVYU
+#define FORMAT_UYVY SPA_VIDEO_FORMAT_UYVY
+#define FORMAT_Y42B SPA_VIDEO_FORMAT_Y42B
+#define FORMAT_Y41B SPA_VIDEO_FORMAT_Y41B
+#define FORMAT_YUV9 SPA_VIDEO_FORMAT_YUV9
+#define FORMAT_I420 SPA_VIDEO_FORMAT_I420
+#define FORMAT_NV12 SPA_VIDEO_FORMAT_NV12
+#define FORMAT_NV12_64Z32 SPA_VIDEO_FORMAT_NV12_64Z32
+#define FORMAT_NV21 SPA_VIDEO_FORMAT_NV21
+#define FORMAT_NV16 SPA_VIDEO_FORMAT_NV16
+#define FORMAT_NV61 SPA_VIDEO_FORMAT_NV61
+#define FORMAT_NV24 SPA_VIDEO_FORMAT_NV24
+
+static const struct format_info format_info[] = {
+ /* RGB formats */
+ {V4L2_PIX_FMT_RGB332, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_ARGB555, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_XRGB555, FORMAT_RGB15, VIDEO, RAW},
+ {V4L2_PIX_FMT_ARGB555X, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_XRGB555X, FORMAT_BGR15, VIDEO, RAW},
+ {V4L2_PIX_FMT_RGB565, FORMAT_RGB16, VIDEO, RAW},
+ {V4L2_PIX_FMT_RGB565X, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_BGR666, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_BGR24, FORMAT_BGR, VIDEO, RAW},
+ {V4L2_PIX_FMT_RGB24, FORMAT_RGB, VIDEO, RAW},
+ {V4L2_PIX_FMT_ABGR32, FORMAT_BGRA, VIDEO, RAW},
+ {V4L2_PIX_FMT_XBGR32, FORMAT_BGRx, VIDEO, RAW},
+ {V4L2_PIX_FMT_ARGB32, FORMAT_ARGB, VIDEO, RAW},
+ {V4L2_PIX_FMT_XRGB32, FORMAT_xRGB, VIDEO, RAW},
+
+ /* Deprecated Packed RGB Image Formats (alpha ambiguity) */
+ {V4L2_PIX_FMT_RGB444, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_RGB555, FORMAT_RGB15, VIDEO, RAW},
+ {V4L2_PIX_FMT_RGB555X, FORMAT_BGR15, VIDEO, RAW},
+ {V4L2_PIX_FMT_BGR32, FORMAT_BGRx, VIDEO, RAW},
+ {V4L2_PIX_FMT_RGB32, FORMAT_xRGB, VIDEO, RAW},
+
+ /* Grey formats */
+ {V4L2_PIX_FMT_GREY, FORMAT_GRAY8, VIDEO, RAW},
+ {V4L2_PIX_FMT_Y4, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_Y6, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_Y10, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_Y12, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_Y16, FORMAT_GRAY16_LE, VIDEO, RAW},
+ {V4L2_PIX_FMT_Y16_BE, FORMAT_GRAY16_BE, VIDEO, RAW},
+ {V4L2_PIX_FMT_Y10BPACK, FORMAT_UNKNOWN, VIDEO, RAW},
+
+ /* Palette formats */
+ {V4L2_PIX_FMT_PAL8, FORMAT_UNKNOWN, VIDEO, RAW},
+
+ /* Chrominance formats */
+ {V4L2_PIX_FMT_UV8, FORMAT_UNKNOWN, VIDEO, RAW},
+
+ /* Luminance+Chrominance formats */
+ {V4L2_PIX_FMT_YVU410, FORMAT_YVU9, VIDEO, RAW},
+ {V4L2_PIX_FMT_YVU420, FORMAT_YV12, VIDEO, RAW},
+ {V4L2_PIX_FMT_YVU420M, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUYV, FORMAT_YUY2, VIDEO, RAW},
+ {V4L2_PIX_FMT_YYUV, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_YVYU, FORMAT_YVYU, VIDEO, RAW},
+ {V4L2_PIX_FMT_UYVY, FORMAT_UYVY, VIDEO, RAW},
+ {V4L2_PIX_FMT_VYUY, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV422P, FORMAT_Y42B, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV411P, FORMAT_Y41B, VIDEO, RAW},
+ {V4L2_PIX_FMT_Y41P, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV444, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV555, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV565, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV32, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV410, FORMAT_YUV9, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV420, FORMAT_I420, VIDEO, RAW},
+ {V4L2_PIX_FMT_YUV420M, FORMAT_I420, VIDEO, RAW},
+ {V4L2_PIX_FMT_HI240, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_HM12, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_M420, FORMAT_UNKNOWN, VIDEO, RAW},
+
+ /* two planes -- one Y, one Cr + Cb interleaved */
+ {V4L2_PIX_FMT_NV12, FORMAT_NV12, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV12M, FORMAT_NV12, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV12MT, FORMAT_NV12_64Z32, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV12MT_16X16, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV21, FORMAT_NV21, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV21M, FORMAT_NV21, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV16, FORMAT_NV16, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV16M, FORMAT_NV16, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV61, FORMAT_NV61, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV61M, FORMAT_NV61, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV24, FORMAT_NV24, VIDEO, RAW},
+ {V4L2_PIX_FMT_NV42, FORMAT_UNKNOWN, VIDEO, RAW},
+
+ /* Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm */
+ {V4L2_PIX_FMT_SBGGR8, FORMAT_UNKNOWN, VIDEO, BAYER},
+ {V4L2_PIX_FMT_SGBRG8, FORMAT_UNKNOWN, VIDEO, BAYER},
+ {V4L2_PIX_FMT_SGRBG8, FORMAT_UNKNOWN, VIDEO, BAYER},
+ {V4L2_PIX_FMT_SRGGB8, FORMAT_UNKNOWN, VIDEO, BAYER},
+
+ /* compressed formats */
+ {V4L2_PIX_FMT_MJPEG, FORMAT_ENCODED, VIDEO, MJPG},
+ {V4L2_PIX_FMT_JPEG, FORMAT_ENCODED, VIDEO, MJPG},
+ {V4L2_PIX_FMT_PJPG, FORMAT_ENCODED, VIDEO, MJPG},
+ {V4L2_PIX_FMT_DV, FORMAT_ENCODED, VIDEO, DV},
+ {V4L2_PIX_FMT_MPEG, FORMAT_ENCODED, VIDEO, MPEGTS},
+ {V4L2_PIX_FMT_H264, FORMAT_ENCODED, VIDEO, H264},
+ {V4L2_PIX_FMT_H264_NO_SC, FORMAT_ENCODED, VIDEO, H264},
+ {V4L2_PIX_FMT_H264_MVC, FORMAT_ENCODED, VIDEO, H264},
+ {V4L2_PIX_FMT_H263, FORMAT_ENCODED, VIDEO, H263},
+ {V4L2_PIX_FMT_MPEG1, FORMAT_ENCODED, VIDEO, MPEG1},
+ {V4L2_PIX_FMT_MPEG2, FORMAT_ENCODED, VIDEO, MPEG2},
+ {V4L2_PIX_FMT_MPEG4, FORMAT_ENCODED, VIDEO, MPEG4},
+ {V4L2_PIX_FMT_XVID, FORMAT_ENCODED, VIDEO, XVID},
+ {V4L2_PIX_FMT_VC1_ANNEX_G, FORMAT_ENCODED, VIDEO, VC1},
+ {V4L2_PIX_FMT_VC1_ANNEX_L, FORMAT_ENCODED, VIDEO, VC1},
+ {V4L2_PIX_FMT_VP8, FORMAT_ENCODED, VIDEO, VP8},
+
+ /* Vendor-specific formats */
+ {V4L2_PIX_FMT_WNVA, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_SN9C10X, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_PWC1, FORMAT_UNKNOWN, VIDEO, RAW},
+ {V4L2_PIX_FMT_PWC2, FORMAT_UNKNOWN, VIDEO, RAW},
+};
+
+static const struct format_info *fourcc_to_format_info(uint32_t fourcc)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i) {
+ if (i->fourcc == fourcc)
+ return i;
+ }
+ return NULL;
+}
+
+#if 0
+static const struct format_info *video_format_to_format_info(uint32_t format)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i) {
+ if (i->format == format)
+ return i;
+ }
+ return NULL;
+}
+#endif
+
+static const struct format_info *find_format_info_by_media_type(uint32_t type,
+ uint32_t subtype,
+ uint32_t format,
+ int startidx)
+{
+ size_t i;
+
+ for (i = startidx; i < SPA_N_ELEMENTS(format_info); i++) {
+ const struct format_info *fi = &format_info[i];
+ if ((fi->media_type == type) &&
+ (fi->media_subtype == subtype) &&
+ (format == 0 || fi->format == format))
+ return fi;
+ }
+ return NULL;
+}
+
+static uint32_t
+enum_filter_format(uint32_t media_type, int32_t media_subtype,
+ const struct spa_pod *filter, uint32_t index)
+{
+ uint32_t video_format = 0;
+
+ switch (media_type) {
+ case SPA_MEDIA_TYPE_video:
+ case SPA_MEDIA_TYPE_image:
+ if (media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+ const struct spa_pod_prop *p;
+ const struct spa_pod *val;
+ uint32_t n_values, choice;
+ const uint32_t *values;
+
+ if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_format)))
+ return SPA_VIDEO_FORMAT_UNKNOWN;
+
+ val = spa_pod_get_values(&p->value, &n_values, &choice);
+
+ if (val->type != SPA_TYPE_Id)
+ return SPA_VIDEO_FORMAT_UNKNOWN;
+
+ values = SPA_POD_BODY(val);
+
+ if (choice == SPA_CHOICE_None) {
+ if (index == 0)
+ video_format = values[0];
+ } else {
+ if (index + 1 < n_values)
+ video_format = values[index + 1];
+ }
+ } else {
+ if (index == 0)
+ video_format = SPA_VIDEO_FORMAT_ENCODED;
+ }
+ }
+ return video_format;
+}
+
+static bool
+filter_framesize(struct v4l2_frmsizeenum *frmsize,
+ const struct spa_rectangle *min,
+ const struct spa_rectangle *max,
+ const struct spa_rectangle *step)
+{
+ if (frmsize->type == V4L2_FRMSIZE_TYPE_DISCRETE) {
+ if (frmsize->discrete.width < min->width ||
+ frmsize->discrete.height < min->height ||
+ frmsize->discrete.width > max->width ||
+ frmsize->discrete.height > max->height) {
+ return false;
+ }
+ } else if (frmsize->type == V4L2_FRMSIZE_TYPE_CONTINUOUS ||
+ frmsize->type == V4L2_FRMSIZE_TYPE_STEPWISE) {
+ /* FIXME, use LCM */
+ frmsize->stepwise.step_width *= step->width;
+ frmsize->stepwise.step_height *= step->height;
+
+ if (frmsize->stepwise.max_width < min->width ||
+ frmsize->stepwise.max_height < min->height ||
+ frmsize->stepwise.min_width > max->width ||
+ frmsize->stepwise.min_height > max->height)
+ return false;
+
+ frmsize->stepwise.min_width = SPA_MAX(frmsize->stepwise.min_width, min->width);
+ frmsize->stepwise.min_height = SPA_MAX(frmsize->stepwise.min_height, min->height);
+ frmsize->stepwise.max_width = SPA_MIN(frmsize->stepwise.max_width, max->width);
+ frmsize->stepwise.max_height = SPA_MIN(frmsize->stepwise.max_height, max->height);
+ } else
+ return false;
+
+ return true;
+}
+
+static int compare_fraction(struct v4l2_fract *f1, const struct spa_fraction *f2)
+{
+ uint64_t n1, n2;
+
+ /* fractions are reduced when set, so we can quickly see if they're equal */
+ if (f1->denominator == f2->num && f1->numerator == f2->denom)
+ return 0;
+
+ /* extend to 64 bits */
+ n1 = ((int64_t) f1->denominator) * f2->denom;
+ n2 = ((int64_t) f1->numerator) * f2->num;
+ if (n1 < n2)
+ return -1;
+ return 1;
+}
+
+static bool
+filter_framerate(struct v4l2_frmivalenum *frmival,
+ const struct spa_fraction *min,
+ const struct spa_fraction *max,
+ const struct spa_fraction *step)
+{
+ if (frmival->type == V4L2_FRMIVAL_TYPE_DISCRETE) {
+ if (compare_fraction(&frmival->discrete, min) < 0 ||
+ compare_fraction(&frmival->discrete, max) > 0)
+ return false;
+ } else if (frmival->type == V4L2_FRMIVAL_TYPE_CONTINUOUS ||
+ frmival->type == V4L2_FRMIVAL_TYPE_STEPWISE) {
+ /* FIXME, use LCM */
+ 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)
+ 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, max) > 0) {
+ frmival->stepwise.max.denominator = max->num;
+ frmival->stepwise.max.numerator = max->denom;
+ }
+ } else
+ return false;
+
+ return true;
+}
+
+#define FOURCC_ARGS(f) (f)&0x7f,((f)>>8)&0x7f,((f)>>16)&0x7f,((f)>>24)&0x7f
+
+static int
+spa_v4l2_enum_format(struct impl *this, int seq,
+ uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct port *port = &this->out_ports[0];
+ int res, n_fractions;
+ const struct format_info *info;
+ struct spa_pod_choice *choice;
+ uint32_t filter_media_type, filter_media_subtype, video_format;
+ struct spa_v4l2_device *dev = &port->dev;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[2];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ if ((res = spa_v4l2_open(dev, this->props.device)) < 0)
+ return res;
+
+ result.id = SPA_PARAM_EnumFormat;
+ result.next = start;
+
+ if (result.next == 0) {
+ spa_zero(port->fmtdesc);
+ port->fmtdesc.index = 0;
+ port->fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ port->next_fmtdesc = true;
+ spa_zero(port->frmsize);
+ port->next_frmsize = true;
+ spa_zero(port->frmival);
+ }
+
+ if (filter) {
+ if ((res = spa_format_parse(filter, &filter_media_type, &filter_media_subtype)) < 0)
+ return res;
+ }
+
+ if (false) {
+ next_fmtdesc:
+ port->fmtdesc.index++;
+ port->next_fmtdesc = true;
+ }
+
+ next:
+ result.index = result.next++;
+
+ while (port->next_fmtdesc) {
+ if (filter) {
+ struct v4l2_format fmt;
+
+ video_format = enum_filter_format(filter_media_type,
+ filter_media_subtype,
+ filter, port->fmtdesc.index);
+
+ if (video_format == SPA_VIDEO_FORMAT_UNKNOWN)
+ goto enum_end;
+
+ info = find_format_info_by_media_type(filter_media_type,
+ filter_media_subtype,
+ video_format, 0);
+ if (info == NULL)
+ goto next_fmtdesc;
+
+ port->fmtdesc.pixelformat = info->fourcc;
+
+ spa_zero(fmt);
+ fmt.type = port->fmtdesc.type;
+ fmt.fmt.pix.pixelformat = info->fourcc;
+ fmt.fmt.pix.field = V4L2_FIELD_ANY;
+ fmt.fmt.pix.width = 0;
+ fmt.fmt.pix.height = 0;
+
+ if ((res = xioctl(dev->fd, VIDIOC_TRY_FMT, &fmt)) < 0) {
+ spa_log_debug(this->log, "'%s' VIDIOC_TRY_FMT %08x: %m",
+ this->props.device, info->fourcc);
+ goto next_fmtdesc;
+ }
+ if (fmt.fmt.pix.pixelformat != info->fourcc) {
+ spa_log_debug(this->log, "'%s' VIDIOC_TRY_FMT wanted %.4s gave %.4s",
+ this->props.device, (char*)&info->fourcc,
+ (char*)&fmt.fmt.pix.pixelformat);
+ goto next_fmtdesc;
+ }
+
+ } else {
+ if ((res = xioctl(dev->fd, VIDIOC_ENUM_FMT, &port->fmtdesc)) < 0) {
+ if (errno == EINVAL)
+ goto enum_end;
+
+ res = -errno;
+ spa_log_error(this->log, "'%s' VIDIOC_ENUM_FMT: %m",
+ this->props.device);
+ goto exit;
+ }
+ }
+ port->next_fmtdesc = false;
+ port->frmsize.index = 0;
+ port->frmsize.pixel_format = port->fmtdesc.pixelformat;
+ port->next_frmsize = true;
+ }
+ if (!(info = fourcc_to_format_info(port->fmtdesc.pixelformat)))
+ goto next_fmtdesc;
+
+ next_frmsize:
+ while (port->next_frmsize) {
+ if (filter) {
+ const struct spa_pod_prop *p;
+ struct spa_pod *val;
+ uint32_t n_vals, choice;
+
+ /* check if we have a fixed frame size */
+ if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_size)))
+ goto do_frmsize;
+
+ val = spa_pod_get_values(&p->value, &n_vals, &choice);
+ if (val->type != SPA_TYPE_Rectangle)
+ goto enum_end;
+
+ if (choice == SPA_CHOICE_None) {
+ const struct spa_rectangle *values = SPA_POD_BODY(val);
+
+ if (port->frmsize.index > 0)
+ goto next_fmtdesc;
+
+ port->frmsize.type = V4L2_FRMSIZE_TYPE_DISCRETE;
+ port->frmsize.discrete.width = values[0].width;
+ port->frmsize.discrete.height = values[0].height;
+ goto have_size;
+ }
+ }
+ do_frmsize:
+ if ((res = xioctl(dev->fd, VIDIOC_ENUM_FRAMESIZES, &port->frmsize)) < 0) {
+ if (errno == EINVAL)
+ goto next_fmtdesc;
+
+ res = -errno;
+ spa_log_error(this->log, "'%s' VIDIOC_ENUM_FRAMESIZES: %m",
+ this->props.device);
+ goto exit;
+ }
+ if (filter) {
+ static const struct spa_rectangle step = {1, 1};
+
+ const struct spa_rectangle *values;
+ const struct spa_pod_prop *p;
+ struct spa_pod *val;
+ uint32_t choice, i, n_values;
+
+ /* check if we have a fixed frame size */
+ if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_size)))
+ goto have_size;
+
+ val = spa_pod_get_values(&p->value, &n_values, &choice);
+ if (val->type != SPA_TYPE_Rectangle)
+ goto have_size;
+
+ values = SPA_POD_BODY_CONST(val);
+
+ if (choice == SPA_CHOICE_Range && n_values > 2) {
+ if (filter_framesize(&port->frmsize, &values[1], &values[2], &step))
+ goto have_size;
+ } else if (choice == SPA_CHOICE_Step && n_values > 3) {
+ if (filter_framesize(&port->frmsize, &values[1], &values[2], &values[3]))
+ goto have_size;
+ } else if (choice == SPA_CHOICE_Enum) {
+ for (i = 1; i < n_values; i++) {
+ if (filter_framesize(&port->frmsize, &values[i], &values[i], &step))
+ goto have_size;
+ }
+ }
+ /* nothing matches the filter, get next frame size */
+ port->frmsize.index++;
+ continue;
+ }
+
+ have_size:
+ if (port->frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
+ /* we have a fixed size, use this to get the frame intervals */
+ port->frmival.index = 0;
+ port->frmival.pixel_format = port->frmsize.pixel_format;
+ port->frmival.width = port->frmsize.discrete.width;
+ port->frmival.height = port->frmsize.discrete.height;
+ port->next_frmsize = false;
+ } else if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS ||
+ port->frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) {
+ /* we have a non fixed size, fix to something sensible to get the
+ * framerate */
+ port->frmival.index = 0;
+ port->frmival.pixel_format = port->frmsize.pixel_format;
+ port->frmival.width = port->frmsize.stepwise.min_width;
+ port->frmival.height = port->frmsize.stepwise.min_height;
+ port->next_frmsize = false;
+ } else {
+ port->frmsize.index++;
+ }
+ }
+
+ 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(info->media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(info->media_subtype),
+ 0);
+
+ if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_format, 0);
+ spa_pod_builder_id(&b, info->format);
+ }
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_size, 0);
+ if (port->frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
+ spa_pod_builder_rectangle(&b,
+ port->frmsize.discrete.width,
+ port->frmsize.discrete.height);
+ } else if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS ||
+ port->frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) {
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(&b, &f[1]);
+
+ spa_pod_builder_rectangle(&b,
+ port->frmsize.stepwise.min_width,
+ port->frmsize.stepwise.min_height);
+ spa_pod_builder_rectangle(&b,
+ port->frmsize.stepwise.min_width,
+ port->frmsize.stepwise.min_height);
+ spa_pod_builder_rectangle(&b,
+ port->frmsize.stepwise.max_width,
+ port->frmsize.stepwise.max_height);
+
+ if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) {
+ choice->body.type = SPA_CHOICE_Range;
+ } else {
+ choice->body.type = SPA_CHOICE_Step;
+ spa_pod_builder_rectangle(&b,
+ port->frmsize.stepwise.max_width,
+ port->frmsize.stepwise.max_height);
+ }
+ spa_pod_builder_pop(&b, &f[1]);
+ }
+
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_framerate, 0);
+
+ n_fractions = 0;
+
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(&b, &f[1]);
+ port->frmival.index = 0;
+
+ while (true) {
+ if ((res = xioctl(dev->fd, VIDIOC_ENUM_FRAMEINTERVALS, &port->frmival)) < 0) {
+ res = -errno;
+ if (errno == EINVAL) {
+ port->frmsize.index++;
+ port->next_frmsize = true;
+ if (port->frmival.index == 0)
+ goto next_frmsize;
+ break;
+ }
+ spa_log_error(this->log, "'%s' VIDIOC_ENUM_FRAMEINTERVALS: %m",
+ this->props.device);
+ goto exit;
+ }
+ if (filter) {
+ static const struct spa_fraction step = {1, 1};
+
+ const struct spa_fraction *values;
+ const struct spa_pod_prop *p;
+ struct spa_pod *val;
+ uint32_t i, n_values, choice;
+
+ if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_framerate)))
+ goto have_framerate;
+
+ val = spa_pod_get_values(&p->value, &n_values, &choice);
+
+ if (val->type != SPA_TYPE_Fraction)
+ goto enum_end;
+
+ values = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ if (filter_framerate(&port->frmival, &values[0], &values[0], &step))
+ goto have_framerate;
+ break;
+
+ case SPA_CHOICE_Range:
+ if (n_values > 2 && filter_framerate(&port->frmival, &values[1], &values[2], &step))
+ goto have_framerate;
+ break;
+
+ case SPA_CHOICE_Step:
+ if (n_values > 3 && filter_framerate(&port->frmival, &values[1], &values[2], &values[3]))
+ goto have_framerate;
+ break;
+
+ case SPA_CHOICE_Enum:
+ for (i = 1; i < n_values; i++) {
+ if (filter_framerate(&port->frmival, &values[i], &values[i], &step))
+ goto have_framerate;
+ }
+ break;
+ default:
+ break;
+ }
+ port->frmival.index++;
+ continue;
+ }
+
+ have_framerate:
+
+ if (port->frmival.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
+ choice->body.type = SPA_CHOICE_Enum;
+ if (n_fractions == 0)
+ spa_pod_builder_fraction(&b,
+ port->frmival.discrete.denominator,
+ port->frmival.discrete.numerator);
+ spa_pod_builder_fraction(&b,
+ port->frmival.discrete.denominator,
+ port->frmival.discrete.numerator);
+ port->frmival.index++;
+ } 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, 25, 1);
+ spa_pod_builder_fraction(&b,
+ port->frmival.stepwise.min.denominator,
+ port->frmival.stepwise.min.numerator);
+ spa_pod_builder_fraction(&b,
+ port->frmival.stepwise.max.denominator,
+ port->frmival.stepwise.max.numerator);
+
+ if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS) {
+ choice->body.type = SPA_CHOICE_Range;
+ } else {
+ choice->body.type = SPA_CHOICE_Step;
+ spa_pod_builder_fraction(&b,
+ port->frmival.stepwise.step.denominator,
+ port->frmival.stepwise.step.numerator);
+ }
+
+ port->frmsize.index++;
+ port->next_frmsize = true;
+ break;
+ }
+ n_fractions++;
+ }
+ if (n_fractions <= 1)
+ choice->body.type = SPA_CHOICE_None;
+
+ spa_pod_builder_pop(&b, &f[1]);
+ result.param = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ enum_end:
+ res = 0;
+ exit:
+ spa_v4l2_close(dev);
+ return res;
+}
+
+static int spa_v4l2_set_format(struct impl *this, struct spa_video_info *format, uint32_t flags)
+{
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+ int res, cmd;
+ struct v4l2_format reqfmt, fmt;
+ struct v4l2_streamparm streamparm;
+ const struct format_info *info = NULL;
+ uint32_t video_format;
+ struct spa_rectangle *size = NULL;
+ struct spa_fraction *framerate = NULL;
+ bool match;
+
+ spa_zero(fmt);
+ fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+ spa_zero(streamparm);
+ streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+ switch (format->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ video_format = format->info.raw.format;
+ size = &format->info.raw.size;
+ framerate = &format->info.raw.framerate;
+ break;
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ case SPA_MEDIA_SUBTYPE_jpeg:
+ video_format = SPA_VIDEO_FORMAT_ENCODED;
+ size = &format->info.mjpg.size;
+ framerate = &format->info.mjpg.framerate;
+ break;
+ case SPA_MEDIA_SUBTYPE_h264:
+ video_format = SPA_VIDEO_FORMAT_ENCODED;
+ size = &format->info.h264.size;
+ framerate = &format->info.h264.framerate;
+ break;
+ default:
+ video_format = SPA_VIDEO_FORMAT_ENCODED;
+ break;
+ }
+
+ info = find_format_info_by_media_type(format->media_type,
+ format->media_subtype, video_format, 0);
+ if (info == NULL || size == NULL || framerate == NULL) {
+ spa_log_error(this->log, "unknown media type %d %d %d", format->media_type,
+ format->media_subtype, video_format);
+ return -EINVAL;
+ }
+
+ fmt.fmt.pix.pixelformat = info->fourcc;
+ fmt.fmt.pix.field = V4L2_FIELD_ANY;
+ fmt.fmt.pix.width = size->width;
+ fmt.fmt.pix.height = size->height;
+ streamparm.parm.capture.timeperframe.numerator = framerate->denom;
+ streamparm.parm.capture.timeperframe.denominator = framerate->num;
+
+ spa_log_debug(this->log, "set %.4s %dx%d %d/%d", (char *)&fmt.fmt.pix.pixelformat,
+ fmt.fmt.pix.width, fmt.fmt.pix.height,
+ streamparm.parm.capture.timeperframe.denominator,
+ streamparm.parm.capture.timeperframe.numerator);
+
+ reqfmt = fmt;
+
+ if ((res = spa_v4l2_open(dev, this->props.device)) < 0)
+ return res;
+
+ cmd = (flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) ? VIDIOC_TRY_FMT : VIDIOC_S_FMT;
+ if (xioctl(dev->fd, cmd, &fmt) < 0) {
+ res = -errno;
+ spa_log_error(this->log, "'%s' VIDIOC_S_FMT: %m",
+ this->props.device);
+ return res;
+ }
+
+ /* some cheap USB cam's won't accept any change */
+ if (xioctl(dev->fd, VIDIOC_S_PARM, &streamparm) < 0)
+ spa_log_warn(this->log, "VIDIOC_S_PARM: %m");
+
+ match = (reqfmt.fmt.pix.pixelformat == fmt.fmt.pix.pixelformat &&
+ reqfmt.fmt.pix.width == fmt.fmt.pix.width &&
+ reqfmt.fmt.pix.height == fmt.fmt.pix.height);
+
+ if (!match && !SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST)) {
+ spa_log_error(this->log, "wanted %.4s %dx%d, got %.4s %dx%d",
+ (char *)&reqfmt.fmt.pix.pixelformat,
+ reqfmt.fmt.pix.width, reqfmt.fmt.pix.height,
+ (char *)&fmt.fmt.pix.pixelformat,
+ fmt.fmt.pix.width, fmt.fmt.pix.height);
+ return -EINVAL;
+ }
+
+ if (flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)
+ return match ? 0 : 1;
+
+ spa_log_info(this->log, "'%s' got %.4s %dx%d %d/%d",
+ dev->path, (char *)&fmt.fmt.pix.pixelformat,
+ fmt.fmt.pix.width, fmt.fmt.pix.height,
+ streamparm.parm.capture.timeperframe.denominator,
+ streamparm.parm.capture.timeperframe.numerator);
+
+ dev->have_format = true;
+ size->width = fmt.fmt.pix.width;
+ size->height = fmt.fmt.pix.height;
+ port->rate.denom = framerate->num = streamparm.parm.capture.timeperframe.denominator;
+ port->rate.num = framerate->denom = streamparm.parm.capture.timeperframe.numerator;
+
+ port->fmt = fmt;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_RATE;
+ port->info.flags = (port->alloc_buffers ? SPA_PORT_FLAG_CAN_ALLOC_BUFFERS : 0) |
+ SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ port->info.rate = SPA_FRACTION(port->rate.num, port->rate.denom);
+
+ return match ? 0 : 1;
+}
+
+static int query_ext_ctrl_ioctl(struct port *port, struct v4l2_query_ext_ctrl *qctrl)
+{
+ struct spa_v4l2_device *dev = &port->dev;
+ struct v4l2_queryctrl qc;
+ int res;
+
+ if (port->have_query_ext_ctrl) {
+ res = xioctl(dev->fd, VIDIOC_QUERY_EXT_CTRL, qctrl);
+ if (errno != ENOTTY)
+ return res;
+ port->have_query_ext_ctrl = false;
+ }
+ spa_zero(qc);
+ qc.id = qctrl->id;
+ res = xioctl(dev->fd, VIDIOC_QUERYCTRL, &qc);
+ if (res == 0) {
+ qctrl->type = qc.type;
+ memcpy(qctrl->name, qc.name, sizeof(qctrl->name));
+ qctrl->minimum = qc.minimum;
+ if (qc.type == V4L2_CTRL_TYPE_BITMASK) {
+ qctrl->maximum = (__u32)qc.maximum;
+ qctrl->default_value = (__u32)qc.default_value;
+ } else {
+ qctrl->maximum = qc.maximum;
+ qctrl->default_value = qc.default_value;
+ }
+ qctrl->step = qc.step;
+ qctrl->flags = qc.flags;
+ qctrl->elems = 1;
+ qctrl->nr_of_dims = 0;
+ memset(qctrl->dims, 0, sizeof(qctrl->dims));
+ switch (qctrl->type) {
+ case V4L2_CTRL_TYPE_INTEGER64:
+ qctrl->elem_size = sizeof(__s64);
+ break;
+ case V4L2_CTRL_TYPE_STRING:
+ qctrl->elem_size = qc.maximum + 1;
+ break;
+ default:
+ qctrl->elem_size = sizeof(__s32);
+ break;
+ }
+ memset(qctrl->reserved, 0, sizeof(qctrl->reserved));
+ }
+ qctrl->id = qc.id;
+ return res;
+}
+
+static struct {
+ uint32_t v4l2_id;
+ uint32_t spa_id;
+} control_map[] = {
+ { V4L2_CID_BRIGHTNESS, SPA_PROP_brightness },
+ { V4L2_CID_CONTRAST, SPA_PROP_contrast },
+ { V4L2_CID_SATURATION, SPA_PROP_saturation },
+ { V4L2_CID_HUE, SPA_PROP_hue },
+ { V4L2_CID_GAMMA, SPA_PROP_gamma },
+ { V4L2_CID_EXPOSURE, SPA_PROP_exposure },
+ { V4L2_CID_GAIN, SPA_PROP_gain },
+ { V4L2_CID_SHARPNESS, SPA_PROP_sharpness },
+};
+
+static uint32_t control_to_prop_id(struct impl *impl, uint32_t control_id)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(control_map, c) {
+ if (c->v4l2_id == control_id)
+ return c->spa_id;
+ }
+ return SPA_PROP_START_CUSTOM + control_id;
+}
+
+static uint32_t prop_id_to_control(struct impl *impl, uint32_t prop_id)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(control_map, c) {
+ if (c->spa_id == prop_id)
+ return c->v4l2_id;
+ }
+ if (prop_id >= SPA_PROP_START_CUSTOM)
+ return prop_id - SPA_PROP_START_CUSTOM;
+ return SPA_ID_INVALID;
+}
+
+static int
+spa_v4l2_enum_controls(struct impl *this, int seq,
+ uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+ struct v4l2_query_ext_ctrl queryctrl;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint32_t prop_id, ctrl_id;
+ uint8_t buffer[1024];
+ int res;
+ const unsigned next_fl = V4L2_CTRL_FLAG_NEXT_CTRL | V4L2_CTRL_FLAG_NEXT_COMPOUND;
+ struct spa_pod_frame f[2];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ if ((res = spa_v4l2_open(dev, this->props.device)) < 0)
+ return res;
+
+ result.id = SPA_PARAM_PropInfo;
+ result.next = start;
+ next:
+ result.index = result.next;
+
+ spa_zero(queryctrl);
+
+ if (result.next == 0) {
+ result.next |= next_fl;
+ port->n_controls = 0;
+ }
+
+ queryctrl.id = result.next;
+ spa_log_debug(this->log, "test control %08x", queryctrl.id);
+
+ if (query_ext_ctrl_ioctl(port, &queryctrl) != 0) {
+ if (errno == ENOTTY)
+ goto enum_end;
+ if (errno == EINVAL) {
+ if (queryctrl.id != next_fl)
+ goto enum_end;
+
+ if (result.next & next_fl)
+ result.next = V4L2_CID_USER_BASE;
+ else if (result.next >= V4L2_CID_USER_BASE && result.next < V4L2_CID_LASTP1)
+ result.next++;
+ else if (result.next >= V4L2_CID_LASTP1)
+ result.next = V4L2_CID_PRIVATE_BASE;
+ else
+ goto enum_end;
+ goto next;
+ }
+ res = -errno;
+ spa_log_error(this->log, "'%s' VIDIOC_QUERYCTRL: %m", this->props.device);
+ spa_v4l2_close(dev);
+ return res;
+ }
+ if (result.next & next_fl)
+ result.next = queryctrl.id | next_fl;
+ else
+ result.next++;
+
+ if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED)
+ goto next;
+
+ if (port->n_controls >= MAX_CONTROLS)
+ goto enum_end;
+
+ ctrl_id = queryctrl.id & ~next_fl;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ prop_id = control_to_prop_id(this, ctrl_id);
+
+ port->controls[port->n_controls].id = prop_id;
+ port->controls[port->n_controls].ctrl_id = ctrl_id;
+ port->controls[port->n_controls].value = queryctrl.default_value;
+
+ spa_log_debug(this->log, "Control '%s' %d %d", queryctrl.name, prop_id, ctrl_id);
+
+ port->n_controls++;
+
+ switch (queryctrl.type) {
+ case V4L2_CTRL_TYPE_INTEGER:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_id, SPA_POD_Id(prop_id),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_STEP_Int(
+ (int32_t)queryctrl.default_value,
+ (int32_t)queryctrl.minimum,
+ (int32_t)queryctrl.maximum,
+ (int32_t)queryctrl.step),
+ SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name));
+ break;
+ case V4L2_CTRL_TYPE_BOOLEAN:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_id, SPA_POD_Id(prop_id),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool((bool)queryctrl.default_value),
+ SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name));
+ break;
+ case V4L2_CTRL_TYPE_MENU:
+ {
+ struct v4l2_querymenu querymenu;
+ struct spa_pod_builder_state state;
+
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_id, SPA_POD_Id(prop_id),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_ENUM_Int(1, (int32_t)queryctrl.default_value),
+ SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name),
+ 0);
+
+ spa_zero(querymenu);
+ querymenu.id = queryctrl.id;
+
+ spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+
+ spa_pod_builder_get_state(&b, &state);
+ spa_pod_builder_push_struct(&b, &f[1]);
+ for (querymenu.index = queryctrl.minimum;
+ querymenu.index <= queryctrl.maximum;
+ querymenu.index++) {
+ if (xioctl(dev->fd, VIDIOC_QUERYMENU, &querymenu) == 0) {
+ spa_pod_builder_int(&b, querymenu.index);
+ spa_pod_builder_string(&b, (const char *)querymenu.name);
+ }
+ }
+ if (spa_pod_builder_pop(&b, &f[1]) == NULL) {
+ spa_log_warn(this->log, "can't create Control '%s' overflow %d",
+ queryctrl.name, b.state.offset);
+ spa_pod_builder_reset(&b, &state);
+ spa_pod_builder_none(&b);
+ }
+ param = spa_pod_builder_pop(&b, &f[0]);
+ break;
+ }
+ case V4L2_CTRL_TYPE_INTEGER_MENU:
+ case V4L2_CTRL_TYPE_BITMASK:
+ case V4L2_CTRL_TYPE_BUTTON:
+ case V4L2_CTRL_TYPE_INTEGER64:
+ case V4L2_CTRL_TYPE_STRING:
+ default:
+ goto next;
+
+ }
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ enum_end:
+ res = 0;
+ spa_v4l2_close(dev);
+ return res;
+}
+
+static int
+spa_v4l2_set_control(struct impl *this, uint32_t id,
+ const struct spa_pod_prop *prop)
+{
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+ struct v4l2_control control;
+ int res;
+
+ spa_zero(control);
+ control.id = prop_id_to_control(this, prop->key);
+ if (control.id == SPA_ID_INVALID)
+ return -ENOENT;
+
+ if ((res = spa_v4l2_open(dev, this->props.device)) < 0)
+ return res;
+
+ switch (SPA_POD_TYPE(&prop->value)) {
+ case SPA_TYPE_Bool:
+ {
+ bool val;
+ if ((res = spa_pod_get_bool(&prop->value, &val)) < 0)
+ goto done;
+ control.value = val;
+ break;
+ }
+ case SPA_TYPE_Int:
+ {
+ int32_t val;
+ if ((res = spa_pod_get_int(&prop->value, &val)) < 0)
+ goto done;
+ control.value = val;
+ break;
+ }
+ default:
+ res = -EINVAL;
+ goto done;
+ }
+ if (xioctl(dev->fd, VIDIOC_S_CTRL, &control) < 0) {
+ res = -errno;
+ goto done;
+ }
+
+ res = 0;
+
+done:
+ spa_v4l2_close(dev);
+ return res;
+}
+
+static int mmap_read(struct impl *this)
+{
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+ struct v4l2_buffer buf;
+ struct buffer *b;
+ struct spa_data *d;
+ int64_t pts;
+
+ spa_zero(buf);
+ buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ buf.memory = port->memtype;
+
+ if (xioctl(dev->fd, VIDIOC_DQBUF, &buf) < 0)
+ return -errno;
+
+ pts = SPA_TIMEVAL_TO_NSEC(&buf.timestamp);
+ spa_log_trace(this->log, "v4l2 %p: have output %d", this, buf.index);
+
+ if (this->clock) {
+ this->clock->nsec = pts;
+ this->clock->rate = port->rate;
+ this->clock->position = buf.sequence;
+ this->clock->duration = 1;
+ this->clock->delay = 0;
+ this->clock->rate_diff = 1.0;
+ this->clock->next_nsec = pts + 1000000000LL / port->rate.denom;
+ }
+
+ b = &port->buffers[buf.index];
+ if (b->h) {
+ b->h->flags = 0;
+ if (buf.flags & V4L2_BUF_FLAG_ERROR)
+ b->h->flags |= SPA_META_HEADER_FLAG_CORRUPTED;
+ b->h->offset = 0;
+ b->h->seq = buf.sequence;
+ b->h->pts = pts;
+ b->h->dts_offset = 0;
+ }
+
+ d = b->outbuf->datas;
+ d[0].chunk->offset = 0;
+ d[0].chunk->size = buf.bytesused;
+ d[0].chunk->stride = port->fmt.fmt.pix.bytesperline;
+ d[0].chunk->flags = 0;
+ if (buf.flags & V4L2_BUF_FLAG_ERROR)
+ d[0].chunk->flags |= SPA_CHUNK_FLAG_CORRUPTED;
+
+ spa_list_append(&port->queue, &b->link);
+ return 0;
+}
+
+static void v4l2_on_fd_events(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct spa_io_buffers *io;
+ struct port *port = &this->out_ports[0];
+ struct buffer *b;
+
+ if (source->rmask & SPA_IO_ERR) {
+ struct port *port = &this->out_ports[0];
+ spa_log_error(this->log, "'%p' error %08x", this->props.device, source->rmask);
+ if (port->source.loop)
+ spa_loop_remove_source(this->data_loop, &port->source);
+ return;
+ }
+
+ if (!(source->rmask & SPA_IO_IN)) {
+ spa_log_warn(this->log, "v4l2 %p: spurious wakeup %d", this, source->rmask);
+ return;
+ }
+
+ if (mmap_read(this) < 0)
+ return;
+
+ if (spa_list_is_empty(&port->queue))
+ return;
+
+ io = port->io;
+ if (io == NULL) {
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+ spa_v4l2_buffer_recycle(this, b->id);
+ }
+ else if (io->status != SPA_STATUS_HAVE_DATA) {
+ if (io->buffer_id < port->n_buffers)
+ spa_v4l2_buffer_recycle(this, io->buffer_id);
+
+ b = spa_list_first(&port->queue, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ spa_log_trace(this->log, "v4l2 %p: now queued %d", this, b->id);
+ }
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+}
+
+static int spa_v4l2_use_buffers(struct impl *this, struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+ struct v4l2_requestbuffers reqbuf;
+ unsigned int i;
+ struct spa_data *d;
+
+ if (n_buffers > 0) {
+ d = buffers[0]->datas;
+
+ if (d[0].type == SPA_DATA_MemFd ||
+ (d[0].type == SPA_DATA_MemPtr && d[0].data != NULL)) {
+ port->memtype = V4L2_MEMORY_USERPTR;
+ } else if (d[0].type == SPA_DATA_DmaBuf) {
+ port->memtype = V4L2_MEMORY_DMABUF;
+ } else {
+ spa_log_error(this->log, "can't use buffers of type %d", d[0].type);
+ return -EINVAL;
+ }
+ }
+
+ spa_zero(reqbuf);
+ reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ reqbuf.memory = port->memtype;
+ reqbuf.count = n_buffers;
+
+ if (xioctl(dev->fd, VIDIOC_REQBUFS, &reqbuf) < 0) {
+ spa_log_error(this->log, "'%s' VIDIOC_REQBUFS %m", this->props.device);
+ return -errno;
+ }
+ spa_log_debug(this->log, "got %d buffers", reqbuf.count);
+ if (reqbuf.count < n_buffers) {
+ spa_log_error(this->log, "'%s' can't allocate enough buffers %d < %d",
+ this->props.device, reqbuf.count, n_buffers);
+ return -ENOMEM;
+ }
+
+ for (i = 0; i < reqbuf.count; i++) {
+ struct buffer *b;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->flags = BUFFER_FLAG_OUTSTANDING;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ spa_log_debug(this->log, "import buffer %p", buffers[i]);
+
+ if (buffers[i]->n_datas < 1) {
+ spa_log_error(this->log, "invalid memory on buffer %p", buffers[i]);
+ return -EINVAL;
+ }
+ d = buffers[i]->datas;
+
+ spa_zero(b->v4l2_buffer);
+ b->v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ b->v4l2_buffer.memory = port->memtype;
+ b->v4l2_buffer.index = i;
+
+ if (port->memtype == V4L2_MEMORY_USERPTR) {
+ if (d[0].data == NULL) {
+ void *data;
+
+ data = mmap(NULL,
+ d[0].maxsize,
+ PROT_READ | PROT_WRITE, MAP_SHARED,
+ d[0].fd,
+ d[0].mapoffset);
+ if (data == MAP_FAILED)
+ return -errno;
+
+ b->ptr = data;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED);
+ }
+ else
+ b->ptr = d[0].data;
+
+ b->v4l2_buffer.m.userptr = (unsigned long) b->ptr;
+ b->v4l2_buffer.length = d[0].maxsize;
+ }
+ else if (port->memtype == V4L2_MEMORY_DMABUF) {
+ b->v4l2_buffer.m.fd = d[0].fd;
+ }
+ else {
+ spa_log_error(this->log, "invalid port memory %d",
+ port->memtype);
+ return -EIO;
+ }
+
+ spa_v4l2_buffer_recycle(this, i);
+ }
+ port->n_buffers = reqbuf.count;
+
+ return 0;
+}
+
+static int
+mmap_init(struct impl *this,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+ struct v4l2_requestbuffers reqbuf;
+ unsigned int i;
+ bool use_expbuf = false;
+
+ port->memtype = V4L2_MEMORY_MMAP;
+
+ spa_zero(reqbuf);
+ reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ reqbuf.memory = port->memtype;
+ reqbuf.count = n_buffers;
+
+ if (xioctl(dev->fd, VIDIOC_REQBUFS, &reqbuf) < 0) {
+ spa_log_error(this->log, "'%s' VIDIOC_REQBUFS: %m", this->props.device);
+ return -errno;
+ }
+
+ spa_log_debug(this->log, "got %d buffers", reqbuf.count);
+
+ if (reqbuf.count < n_buffers) {
+ spa_log_error(this->log, "'%s' can't allocate enough buffers (%d < %d)",
+ this->props.device, reqbuf.count, n_buffers);
+ return -ENOMEM;
+ }
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d;
+
+ if (buffers[i]->n_datas < 1) {
+ spa_log_error(this->log, "invalid buffer data");
+ return -EINVAL;
+ }
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->flags = BUFFER_FLAG_OUTSTANDING;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ spa_zero(b->v4l2_buffer);
+ b->v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ b->v4l2_buffer.memory = port->memtype;
+ b->v4l2_buffer.index = i;
+
+ if (xioctl(dev->fd, VIDIOC_QUERYBUF, &b->v4l2_buffer) < 0) {
+ spa_log_error(this->log, "'%s' VIDIOC_QUERYBUF: %m", this->props.device);
+ return -errno;
+ }
+
+ if (b->v4l2_buffer.flags & V4L2_BUF_FLAG_QUEUED) {
+ /* some drivers can give us an already queued buffer. */
+ spa_log_warn(this->log, "buffer %d was already queued", i);
+ n_buffers = i;
+ break;
+ }
+
+ d = buffers[i]->datas;
+ d[0].mapoffset = 0;
+ d[0].maxsize = b->v4l2_buffer.length;
+ d[0].chunk->offset = 0;
+ d[0].chunk->size = 0;
+ d[0].chunk->stride = port->fmt.fmt.pix.bytesperline;
+ d[0].chunk->flags = 0;
+
+ spa_log_debug(this->log, "data types %08x", d[0].type);
+
+ if (port->have_expbuf &&
+ d[0].type != SPA_ID_INVALID &&
+ (d[0].type & ((1u << SPA_DATA_DmaBuf)|(1u<<SPA_DATA_MemFd)))) {
+ struct v4l2_exportbuffer expbuf;
+
+ spa_zero(expbuf);
+ expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ expbuf.index = i;
+ expbuf.flags = O_CLOEXEC | O_RDONLY;
+ if (xioctl(dev->fd, VIDIOC_EXPBUF, &expbuf) < 0) {
+ if (errno == ENOTTY || errno == EINVAL) {
+ spa_log_debug(this->log, "'%s' VIDIOC_EXPBUF not supported: %m",
+ this->props.device);
+ port->have_expbuf = false;
+ goto fallback;
+ }
+ spa_log_error(this->log, "'%s' VIDIOC_EXPBUF: %m", this->props.device);
+ return -errno;
+ }
+ if (d[0].type & (1u<<SPA_DATA_DmaBuf))
+ d[0].type = SPA_DATA_DmaBuf;
+ else
+ d[0].type = SPA_DATA_MemFd;
+ d[0].flags = SPA_DATA_FLAG_READABLE;
+ d[0].fd = expbuf.fd;
+ d[0].data = NULL;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_ALLOCATED);
+ spa_log_debug(this->log, "EXPBUF fd:%d", expbuf.fd);
+ use_expbuf = true;
+ } else if (d[0].type & (1u << SPA_DATA_MemPtr)) {
+fallback:
+ d[0].type = SPA_DATA_MemPtr;
+ d[0].flags = SPA_DATA_FLAG_READABLE;
+ d[0].fd = -1;
+ d[0].mapoffset = b->v4l2_buffer.m.offset;
+ d[0].data = mmap(NULL,
+ b->v4l2_buffer.length,
+ PROT_READ, MAP_SHARED,
+ dev->fd,
+ b->v4l2_buffer.m.offset);
+ if (d[0].data == MAP_FAILED) {
+ spa_log_error(this->log, "'%s' mmap: %m", this->props.device);
+ return -errno;
+ }
+ b->ptr = d[0].data;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED);
+ spa_log_debug(this->log, "mmap offset:%u data:%p", d[0].mapoffset, b->ptr);
+ use_expbuf = false;
+ } else {
+ spa_log_error(this->log, "unsupported data type:%08x", d[0].type);
+ return -ENOTSUP;
+ }
+ spa_v4l2_buffer_recycle(this, i);
+ }
+ spa_log_info(this->log, "%s: have %u buffers using %s", dev->path, n_buffers,
+ use_expbuf ? "EXPBUF" : "MMAP");
+
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int userptr_init(struct impl *this)
+{
+ return -ENOTSUP;
+}
+
+static int read_init(struct impl *this)
+{
+ return -ENOTSUP;
+}
+
+static int
+spa_v4l2_alloc_buffers(struct impl *this,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ int res;
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+
+ if (port->n_buffers > 0)
+ return -EIO;
+
+ if (dev->cap.capabilities & V4L2_CAP_STREAMING) {
+ if ((res = mmap_init(this, buffers, n_buffers)) < 0)
+ if ((res = userptr_init(this)) < 0)
+ return res;
+ } else if (dev->cap.capabilities & V4L2_CAP_READWRITE) {
+ if ((res = read_init(this)) < 0)
+ return res;
+ } else {
+ spa_log_error(this->log, "invalid capabilities %08x",
+ dev->cap.capabilities);
+ return -EIO;
+ }
+
+ return 0;
+}
+
+static int spa_v4l2_stream_on(struct impl *this)
+{
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+ enum v4l2_buf_type type;
+
+ if (dev->fd == -1)
+ return -EIO;
+
+ if (!dev->have_format)
+ return -EIO;
+
+ if (dev->active)
+ return 0;
+
+ spa_log_debug(this->log, "starting");
+
+ type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ if (xioctl(dev->fd, VIDIOC_STREAMON, &type) < 0) {
+ spa_log_error(this->log, "'%s' VIDIOC_STREAMON: %m", this->props.device);
+ return -errno;
+ }
+
+ port->source.func = v4l2_on_fd_events;
+ port->source.data = this;
+ port->source.fd = dev->fd;
+ port->source.mask = SPA_IO_IN | SPA_IO_ERR;
+ port->source.rmask = 0;
+ spa_loop_add_source(this->data_loop, &port->source);
+
+ dev->active = true;
+
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct port *port = user_data;
+ if (port->source.loop)
+ spa_loop_remove_source(loop, &port->source);
+ return 0;
+}
+
+static int spa_v4l2_stream_off(struct impl *this)
+{
+ struct port *port = &this->out_ports[0];
+ struct spa_v4l2_device *dev = &port->dev;
+ enum v4l2_buf_type type;
+ uint32_t i;
+
+ if (!dev->active)
+ return 0;
+
+ if (dev->fd == -1)
+ return -EIO;
+
+ spa_log_debug(this->log, "stopping");
+
+ spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, port);
+
+ type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ if (xioctl(dev->fd, VIDIOC_STREAMOFF, &type) < 0) {
+ spa_log_error(this->log, "'%s' VIDIOC_STREAMOFF: %m", this->props.device);
+ return -errno;
+ }
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b;
+
+ b = &port->buffers[i];
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) {
+ if (xioctl(dev->fd, VIDIOC_QBUF, &b->v4l2_buffer) < 0)
+ spa_log_warn(this->log, "VIDIOC_QBUF: %s", strerror(errno));
+ }
+ }
+ spa_list_init(&port->queue);
+ dev->active = false;
+
+ return 0;
+}
diff --git a/spa/plugins/v4l2/v4l2.c b/spa/plugins/v4l2/v4l2.c
new file mode 100644
index 0000000..77fb4ce
--- /dev/null
+++ b/spa/plugins/v4l2/v4l2.c
@@ -0,0 +1,59 @@
+/* Spa V4l2 support
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+
+extern const struct spa_handle_factory spa_v4l2_source_factory;
+extern const struct spa_handle_factory spa_v4l2_udev_factory;
+extern const struct spa_handle_factory spa_v4l2_device_factory;
+
+struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.v4l2");
+struct spa_log_topic *v4l2_log_topic = &log_topic;
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *factory = &spa_v4l2_source_factory;
+ break;
+ case 1:
+ *factory = &spa_v4l2_udev_factory;
+ break;
+ case 2:
+ *factory = &spa_v4l2_device_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/v4l2/v4l2.h b/spa/plugins/v4l2/v4l2.h
new file mode 100644
index 0000000..e2293c7
--- /dev/null
+++ b/spa/plugins/v4l2/v4l2.h
@@ -0,0 +1,49 @@
+/* Spa V4l2 support
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/log.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT v4l2_log_topic
+extern struct spa_log_topic *v4l2_log_topic;
+
+static inline void v4l2_log_topic_init(struct spa_log *log)
+{
+ spa_log_topic_init(log, v4l2_log_topic);
+}
+
+struct spa_v4l2_device {
+ struct spa_log *log;
+ int fd;
+ struct v4l2_capability cap;
+ unsigned int active:1;
+ unsigned int have_format:1;
+ char path[64];
+};
+
+int spa_v4l2_open(struct spa_v4l2_device *dev, const char *path);
+int spa_v4l2_close(struct spa_v4l2_device *dev);
+int spa_v4l2_is_capture(struct spa_v4l2_device *dev);
diff --git a/spa/plugins/videoconvert/meson.build b/spa/plugins/videoconvert/meson.build
new file mode 100644
index 0000000..24673a5
--- /dev/null
+++ b/spa/plugins/videoconvert/meson.build
@@ -0,0 +1,15 @@
+videoconvert_sources = [
+ 'videoadapter.c',
+ 'plugin.c'
+]
+
+simd_cargs = []
+simd_dependencies = []
+
+videoconvertlib = shared_library('spa-videoconvert',
+ videoconvert_sources,
+ c_args : simd_cargs,
+ dependencies : [ spa_dep, mathlib ],
+ link_with : simd_dependencies,
+ install : true,
+ install_dir : spa_plugindir / 'videoconvert')
diff --git a/spa/plugins/videoconvert/plugin.c b/spa/plugins/videoconvert/plugin.c
new file mode 100644
index 0000000..0f24495
--- /dev/null
+++ b/spa/plugins/videoconvert/plugin.c
@@ -0,0 +1,46 @@
+/* Spa videoconvert plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_videoadapter_factory;
+
+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_videoadapter_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c
new file mode 100644
index 0000000..2f6f068
--- /dev/null
+++ b/spa/plugins/videoconvert/videoadapter.c
@@ -0,0 +1,1671 @@
+/* SPA
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/cpu.h>
+
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/buffer/alloc.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/param/param.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/latency-utils.h>
+#include <spa/debug/format.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT log_topic
+static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.videoadapter");
+
+#define DEFAULT_ALIGN 16
+
+#define MAX_PORTS 1
+
+/** \cond */
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_cpu *cpu;
+
+ uint32_t max_align;
+ enum spa_direction direction;
+
+ struct spa_node *target;
+
+ struct spa_node *follower;
+ struct spa_hook follower_listener;
+ uint32_t follower_flags;
+ struct spa_video_info follower_current_format;
+ struct spa_video_info default_format;
+
+ struct spa_handle *hnd_convert;
+ struct spa_node *convert;
+ struct spa_hook convert_listener;
+ uint32_t convert_flags;
+
+ uint32_t n_buffers;
+ struct spa_buffer **buffers;
+
+ struct spa_io_buffers io_buffers;
+ struct spa_io_rate_match io_rate_match;
+ struct spa_io_position *io_position;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define IDX_EnumFormat 0
+#define IDX_PropInfo 1
+#define IDX_Props 2
+#define IDX_Format 3
+#define IDX_EnumPortConfig 4
+#define IDX_PortConfig 5
+#define IDX_Latency 6
+#define IDX_ProcessLatency 7
+#define N_NODE_PARAMS 8
+ struct spa_param_info params[N_NODE_PARAMS];
+ uint32_t convert_params_flags[N_NODE_PARAMS];
+ uint32_t follower_params_flags[N_NODE_PARAMS];
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ unsigned int add_listener:1;
+ unsigned int have_format:1;
+ unsigned int started:1;
+ unsigned int driver:1;
+ unsigned int async:1;
+ unsigned int passthrough:1;
+ unsigned int follower_removing:1;
+};
+
+/** \endcond */
+
+static int follower_enum_params(struct impl *this,
+ 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) {
+ if (this->convert != NULL &&
+ (res = spa_node_enum_params_sync(this->convert,
+ 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,
+ id, &result->next, filter, &result->param, builder)) == 1) {
+ result->next |= 0x100000;
+ return res;
+ }
+ result->next = 0x200000;
+ }
+ 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;
+ uint8_t buffer[4096];
+ struct spa_pod_dynamic_builder b;
+ 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);
+
+ 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_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+
+ switch (id) {
+ case SPA_PARAM_EnumPortConfig:
+ case SPA_PARAM_PortConfig:
+ if (this->convert == NULL)
+ return 0;
+ res = spa_node_enum_params(this->convert, seq, id, start, num, filter);
+ return res;
+ case SPA_PARAM_PropInfo:
+ res = follower_enum_params(this,
+ id, IDX_PropInfo, &result, filter, &b.b);
+ break;
+ case SPA_PARAM_Props:
+ res = follower_enum_params(this,
+ id, IDX_Props, &result, filter, &b.b);
+ break;
+ case SPA_PARAM_ProcessLatency:
+ res = follower_enum_params(this,
+ id, IDX_ProcessLatency, &result, filter, &b.b);
+ break;
+ case SPA_PARAM_EnumFormat:
+ case SPA_PARAM_Format:
+ case SPA_PARAM_Latency:
+ res = spa_node_port_enum_params_sync(this->follower,
+ this->direction, 0,
+ id, &result.next, filter, &result.param, &b.b);
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (res == 1) {
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (res != 1)
+ return res;
+
+ if (count != num)
+ goto next;
+
+ return 0;
+}
+
+static int link_io(struct impl *this)
+{
+ int res;
+
+ if (this->convert == NULL)
+ return 0;
+
+ spa_log_debug(this->log, "%p: controls", this);
+
+ spa_zero(this->io_rate_match);
+ this->io_rate_match.rate = 1.0;
+
+ if ((res = spa_node_port_set_io(this->follower,
+ this->direction, 0,
+ SPA_IO_RateMatch,
+ &this->io_rate_match, sizeof(this->io_rate_match))) < 0) {
+ 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,
+ &this->io_rate_match, sizeof(this->io_rate_match))) < 0) {
+ spa_log_warn(this->log, "%p: set RateMatch on convert failed %d %s", this,
+ res, spa_strerror(res));
+ }
+
+ 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_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,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ SPA_IO_Buffers,
+ &this->io_buffers, sizeof(this->io_buffers))) < 0) {
+ spa_log_warn(this->log, "%p: set Buffers on convert failed %d %s", this,
+ res, spa_strerror(res));
+ return res;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint32_t i;
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ spa_log_debug(this->log, "%p: info full:%d change:%08"PRIx64,
+ this, full, this->info.change_mask);
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->info.n_params; i++) {
+ if (this->params[i].user > 0) {
+ this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[i].user = 0;
+ spa_log_debug(this->log, "param %d flags:%08x",
+ i, this->params[i].flags);
+ }
+ }
+ }
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static int debug_params(struct impl *this, struct spa_node *node,
+ enum spa_direction direction, uint32_t port_id, uint32_t id, struct spa_pod *filter,
+ const char *debug, int err)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ uint32_t state;
+ struct spa_pod *param;
+ int res, count = 0;
+
+ spa_log_error(this->log, "params %s: %d:%d (%s) %s",
+ spa_debug_type_find_name(spa_type_param, id),
+ direction, port_id, debug, err ? spa_strerror(err) : "no matching params");
+ if (err == -EBUSY)
+ return 0;
+
+ if (filter) {
+ spa_log_error(this->log, "with this filter:");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 2, NULL, filter);
+ } else {
+ spa_log_error(this->log, "there was no filter");
+ }
+
+ state = 0;
+ while (true) {
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ res = spa_node_port_enum_params_sync(node,
+ direction, port_id,
+ id, &state,
+ NULL, &param, &b);
+ if (res != 1) {
+ if (res < 0)
+ spa_log_error(this->log, " error: %s", spa_strerror(res));
+ break;
+ }
+ spa_log_error(this->log, "unmatched %s %d:", debug, count);
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 2, NULL, param);
+ count++;
+ }
+ if (count == 0)
+ spa_log_error(this->log, "could not get any %s", debug);
+
+ return 0;
+}
+
+static int negotiate_buffers(struct impl *this)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ uint32_t state;
+ struct spa_pod *param;
+ int res;
+ bool follower_alloc, conv_alloc;
+ uint32_t i, size, buffers, blocks, align, flags, stride = 0;
+ uint32_t *aligns;
+ struct spa_data *datas;
+ uint32_t follower_flags, conv_flags;
+
+ spa_log_debug(this->log, "%p: %d", this, this->n_buffers);
+
+ if (this->target == this->follower)
+ return 0;
+
+ if (this->n_buffers > 0)
+ return 0;
+
+ state = 0;
+ param = NULL;
+ if ((res = spa_node_port_enum_params_sync(this->follower,
+ this->direction, 0,
+ SPA_PARAM_Buffers, &state,
+ param, &param, &b)) < 0) {
+ if (res == -ENOENT)
+ param = NULL;
+ else {
+ debug_params(this, this->follower, this->direction, 0,
+ SPA_PARAM_Buffers, param, "follower buffers", res);
+ return res;
+ }
+ }
+
+ state = 0;
+ if ((res = spa_node_port_enum_params_sync(this->convert,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ SPA_PARAM_Buffers, &state,
+ param, &param, &b)) != 1) {
+ debug_params(this, this->convert,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ SPA_PARAM_Buffers, param, "convert buffers", res);
+ return -ENOTSUP;
+ }
+ if (param == NULL)
+ return -ENOTSUP;
+
+ spa_pod_fixate(param);
+
+ follower_flags = this->follower_flags;
+ conv_flags = this->convert_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);
+
+ flags = 0;
+ if (conv_alloc || follower_alloc) {
+ flags |= SPA_BUFFER_ALLOC_FLAG_NO_DATA;
+ if (conv_alloc)
+ follower_alloc = false;
+ }
+
+ align = DEFAULT_ALIGN;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamBuffers, NULL,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(&buffers),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(&blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(&size),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(&stride),
+ SPA_PARAM_BUFFERS_align, SPA_POD_OPT_Int(&align))) < 0)
+ return res;
+
+ spa_log_debug(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d",
+ this, buffers, blocks, size, stride, align, follower_alloc, conv_alloc);
+
+ align = SPA_MAX(align, this->max_align);
+
+ datas = alloca(sizeof(struct spa_data) * blocks);
+ memset(datas, 0, sizeof(struct spa_data) * blocks);
+ aligns = alloca(sizeof(uint32_t) * blocks);
+ for (i = 0; i < blocks; i++) {
+ datas[i].type = SPA_DATA_MemPtr;
+ datas[i].flags = SPA_DATA_FLAG_READWRITE | SPA_DATA_FLAG_DYNAMIC;
+ datas[i].maxsize = size;
+ aligns[i] = align;
+ }
+
+ free(this->buffers);
+ this->buffers = spa_buffer_alloc_array(buffers, flags, 0, NULL, blocks, datas, aligns);
+ if (this->buffers == NULL)
+ return -errno;
+ this->n_buffers = buffers;
+
+ if ((res = spa_node_port_use_buffers(this->convert,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0,
+ this->buffers, this->n_buffers)) < 0)
+ return res;
+
+ if ((res = spa_node_port_use_buffers(this->follower,
+ this->direction, 0,
+ follower_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0,
+ this->buffers, this->n_buffers)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int configure_format(struct impl *this, uint32_t flags, const struct spa_pod *format)
+{
+ int res;
+
+ spa_log_debug(this->log, "%p: configure format:", this);
+ if (format)
+ 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,
+ SPA_PARAM_Format, flags,
+ format)) < 0)
+ return res;
+ if (res > 0) {
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ uint32_t state = 0;
+ struct spa_pod *fmt;
+
+ /* format was changed to nearest compatible format */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ if ((res = spa_node_port_enum_params_sync(this->follower,
+ this->direction, 0,
+ SPA_PARAM_Format, &state,
+ NULL, &fmt, &b)) != 1)
+ return -EIO;
+
+ format = fmt;
+ }
+
+ if (this->target != this->follower && this->convert) {
+ if ((res = spa_node_port_set_param(this->convert,
+ SPA_DIRECTION_REVERSE(this->direction), 0,
+ SPA_PARAM_Format, flags,
+ format)) < 0)
+ return res;
+ }
+
+ this->have_format = format != NULL;
+ if (format == NULL) {
+ this->n_buffers = 0;
+ } else {
+ res = negotiate_buffers(this);
+ }
+
+ return res;
+}
+
+static int configure_convert(struct impl *this, uint32_t mode)
+{
+ struct spa_pod_builder b = { 0 };
+ 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);
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode));
+
+ return spa_node_set_param(this->convert, SPA_PARAM_PortConfig, 0, param);
+}
+
+extern const struct spa_handle_factory spa_videoconvert_factory;
+
+static const struct spa_node_events follower_node_events;
+
+static int reconfigure_mode(struct impl *this, bool passthrough,
+ enum spa_direction direction, struct spa_pod *format)
+{
+ int res = 0;
+ struct spa_hook l;
+
+ spa_log_info(this->log, "%p: passthrough mode %d", this, passthrough);
+
+ if (this->passthrough != passthrough) {
+ if (passthrough) {
+ /* remove converter split/merge ports */
+ configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none);
+ } else {
+ /* remove follower ports */
+ this->follower_removing = true;
+ spa_zero(l);
+ spa_node_add_listener(this->follower, &l, &follower_node_events, this);
+ spa_hook_remove(&l);
+ this->follower_removing = false;
+ }
+ }
+
+ /* set new target */
+ this->target = passthrough ? this->follower : this->convert;
+
+ if ((res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format)) < 0)
+ return res;
+
+ if (this->passthrough != passthrough) {
+ this->passthrough = passthrough;
+ if (passthrough) {
+ /* add follower ports */
+ spa_zero(l);
+ spa_node_add_listener(this->follower, &l, &follower_node_events, this);
+ spa_hook_remove(&l);
+ } else {
+ /* add converter ports */
+ configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp);
+ link_io(this);
+ }
+ }
+
+ 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++;
+
+ emit_node_info(this, false);
+
+ return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ int res = 0, res2 = 0;
+ struct impl *this = object;
+ struct spa_video_info info = { 0 };
+
+ spa_log_debug(this->log, "%p: set param %d", this, id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ if (this->started)
+ return -EIO;
+ 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)
+ return -EINVAL;
+
+ this->follower_current_format = info;
+ break;
+
+ case SPA_PARAM_PortConfig:
+ {
+ enum spa_direction dir;
+ enum spa_param_port_config_mode mode;
+ struct spa_pod *format = NULL;
+
+ if (this->started) {
+ spa_log_error(this->log, "was started");
+ return -EIO;
+ }
+
+ if (spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamPortConfig, NULL,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&dir),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0)
+ return -EINVAL;
+
+ if (format) {
+ struct spa_video_info info;
+
+ spa_zero(info);
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+ if (info.media_type != SPA_MEDIA_TYPE_video ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -ENOTSUP;
+
+ if (spa_format_video_raw_parse(format, &info.info.raw) >= 0)
+ this->default_format = info;
+ }
+
+ 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)
+ 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)
+ return res;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (this->target != this->follower) {
+ if ((res = spa_node_set_param(this->target, id, flags, param)) < 0)
+ return res;
+ }
+ break;
+ }
+
+ case SPA_PARAM_Props:
+ if (this->target != this->follower)
+ res = spa_node_set_param(this->target, id, flags, param);
+ res2 = spa_node_set_param(this->follower, id, flags, param);
+ if (res < 0 && res2 < 0)
+ return res;
+ res = 0;
+ break;
+ case SPA_PARAM_ProcessLatency:
+ res = spa_node_set_param(this->follower, id, flags, param);
+ break;
+ default:
+ res = -ENOTSUP;
+ break;
+ }
+ return res;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+ int res = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Position:
+ this->io_position = data;
+ break;
+ default:
+ break;
+ }
+
+ if (this->target)
+ res = spa_node_set_io(this->target, id, data, size);
+
+ if (this->target != this->follower)
+ res = spa_node_set_io(this->follower, id, data, size);
+
+ return res;
+}
+
+static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder *b, uint32_t id,
+ struct spa_pod_object *o1, struct spa_pod_object *o2)
+{
+ const struct spa_pod_prop *p1, *p2;
+ struct spa_pod_frame f;
+ struct spa_pod_builder_state state;
+ int res = 0;
+
+ if (o2 == NULL || SPA_POD_TYPE(o1) != SPA_POD_TYPE(o2))
+ return (struct spa_pod*)o1;
+
+ spa_pod_builder_push_object(b, &f, o1->body.type, o1->body.id);
+ p2 = NULL;
+ SPA_POD_OBJECT_FOREACH(o1, p1) {
+ p2 = spa_pod_object_find_prop(o2, p2, p1->key);
+ if (p2 != NULL) {
+ spa_pod_builder_get_state(b, &state);
+ res = spa_pod_filter_prop(b, p1, p2);
+ if (res < 0)
+ spa_pod_builder_reset(b, &state);
+ }
+ if (p2 == NULL || res < 0)
+ spa_pod_builder_raw_padded(b, p1, SPA_POD_PROP_SIZE(p1));
+ }
+ p1 = NULL;
+ SPA_POD_OBJECT_FOREACH(o2, p2) {
+ p1 = spa_pod_object_find_prop(o1, p1, p2->key);
+ if (p1 != NULL)
+ continue;
+ spa_pod_builder_raw_padded(b, p2, SPA_POD_PROP_SIZE(p2));
+ }
+ return spa_pod_builder_pop(b, &f);
+}
+
+static int negotiate_format(struct impl *this)
+{
+ uint32_t state;
+ struct spa_pod *format, *def;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ int res;
+
+ if (this->have_format)
+ return 0;
+
+ if (this->target == this->follower)
+ return 0;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ spa_log_debug(this->log, "%p: negiotiate", this);
+
+ spa_node_send_command(this->follower,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin));
+
+ 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) {
+ if (res == -ENOENT)
+ format = NULL;
+ else {
+ debug_params(this, this->follower, this->direction, 0,
+ SPA_PARAM_EnumFormat, format, "follower format", res);
+ goto done;
+ }
+ }
+ if (this->convert) {
+ 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,
+ 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);
+
+ format = merge_objects(this, &b, SPA_PARAM_Format,
+ (struct spa_pod_object*)format,
+ (struct spa_pod_object*)def);
+
+ spa_pod_fixate(format);
+
+ res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format);
+
+done:
+ spa_node_send_command(this->follower,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamEnd));
+
+ return res;
+}
+
+
+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_log_debug(this->log, "%p: command %d", this, SPA_NODE_COMMAND_ID(command));
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if ((res = negotiate_format(this)) < 0)
+ return res;
+ if ((res = negotiate_buffers(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ configure_format(this, 0, NULL);
+ SPA_FALLTHROUGH
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ break;
+ case SPA_NODE_COMMAND_Flush:
+ this->io_buffers.status = SPA_STATUS_OK;
+ break;
+ default:
+ break;
+ }
+
+ if ((res = spa_node_send_command(this->target, command)) < 0) {
+ spa_log_error(this->log, "%p: can't send command %d: %s",
+ this, SPA_NODE_COMMAND_ID(command),
+ spa_strerror(res));
+ return res;
+ }
+
+ if (this->target != this->follower) {
+ if ((res = spa_node_send_command(this->follower, command)) < 0) {
+ spa_log_error(this->log, "%p: can't send command %d: %s",
+ this, SPA_NODE_COMMAND_ID(command),
+ spa_strerror(res));
+ return res;
+ }
+ }
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ this->started = true;
+ break;
+ }
+ return res;
+}
+
+static void convert_node_info(void *data, const struct spa_node_info *info)
+{
+ struct impl *this = data;
+ uint32_t i;
+
+ spa_log_debug(this->log, "%p: info change:%08"PRIx64, this,
+ info->change_mask);
+
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t idx;
+
+ switch (info->params[i].id) {
+ case SPA_PARAM_EnumPortConfig:
+ idx = IDX_EnumPortConfig;
+ break;
+ case SPA_PARAM_PortConfig:
+ idx = IDX_PortConfig;
+ break;
+ case SPA_PARAM_PropInfo:
+ idx = IDX_PropInfo;
+ break;
+ case SPA_PARAM_Props:
+ idx = IDX_Props;
+ break;
+ default:
+ continue;
+ }
+ if (!this->add_listener &&
+ this->convert_params_flags[idx] == info->params[i].flags)
+ continue;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->convert_params_flags[idx] = info->params[i].flags;
+ this->params[idx].flags =
+ (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
+ (info->params[i].flags & SPA_PARAM_INFO_READWRITE);
+
+ if (!this->add_listener) {
+ this->params[idx].user++;
+ spa_log_debug(this->log, "param %d changed", info->params[i].id);
+ }
+ }
+ }
+ emit_node_info(this, false);
+}
+
+static void convert_port_info(void *data,
+ enum spa_direction direction, uint32_t port_id,
+ const struct spa_port_info *info)
+{
+ struct impl *this = data;
+
+ if (direction != this->direction) {
+ if (port_id == 0)
+ return;
+ else
+ port_id--;
+ }
+
+ spa_log_debug(this->log, "%p: port info %d:%d", this,
+ direction, port_id);
+
+ if (this->target != this->follower)
+ spa_node_emit_port_info(&this->hooks, direction, port_id, info);
+}
+
+static void convert_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct impl *this = data;
+
+ if (this->target == this->follower)
+ return;
+
+ spa_log_trace(this->log, "%p: result %d %d", this, seq, res);
+ spa_node_emit_result(&this->hooks, seq, res, type, result);
+}
+
+static const struct spa_node_events convert_node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .info = convert_node_info,
+ .port_info = convert_port_info,
+ .result = convert_result,
+};
+
+static void follower_info(void *data, const struct spa_node_info *info)
+{
+ struct impl *this = data;
+ uint32_t i;
+
+ spa_log_debug(this->log, "%p: info change:%08"PRIx64, this,
+ info->change_mask);
+
+ if (this->follower_removing)
+ return;
+
+ this->async = (info->flags & SPA_NODE_FLAG_ASYNC) != 0;
+
+ if (info->max_input_ports > 0)
+ this->direction = SPA_DIRECTION_INPUT;
+ else
+ this->direction = SPA_DIRECTION_OUTPUT;
+
+ if (this->direction == SPA_DIRECTION_INPUT) {
+ this->info.flags |= SPA_NODE_FLAG_IN_PORT_CONFIG;
+ this->info.max_input_ports = MAX_PORTS;
+ } else {
+ this->info.flags |= SPA_NODE_FLAG_OUT_PORT_CONFIG;
+ this->info.max_output_ports = MAX_PORTS;
+ }
+
+ spa_log_debug(this->log, "%p: follower info %s", this,
+ this->direction == SPA_DIRECTION_INPUT ?
+ "Input" : "Output");
+
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PROPS) {
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ this->info.props = info->props;
+ }
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t idx;
+
+ switch (info->params[i].id) {
+ case SPA_PARAM_PropInfo:
+ idx = IDX_PropInfo;
+ break;
+ case SPA_PARAM_Props:
+ idx = IDX_Props;
+ break;
+ case SPA_PARAM_ProcessLatency:
+ idx = IDX_ProcessLatency;
+ break;
+ default:
+ continue;
+ }
+ if (!this->add_listener &&
+ this->follower_params_flags[idx] == info->params[i].flags)
+ continue;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->follower_params_flags[idx] = info->params[i].flags;
+ this->params[idx].flags =
+ (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
+ (info->params[i].flags & SPA_PARAM_INFO_READWRITE);
+
+ if (!this->add_listener) {
+ this->params[idx].user++;
+ spa_log_debug(this->log, "param %d changed", info->params[i].id);
+ }
+ }
+ }
+ emit_node_info(this, false);
+
+ spa_zero(this->info.props);
+ this->info.change_mask &= ~SPA_NODE_CHANGE_MASK_PROPS;
+
+}
+
+static int recalc_latency(struct impl *this, enum spa_direction direction, uint32_t port_id)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ uint32_t index = 0;
+ struct spa_latency_info latency;
+ int res;
+
+ spa_log_debug(this->log, "%p: ", this);
+
+ if (this->target == this->follower)
+ return 0;
+
+ while (true) {
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if ((res = spa_node_port_enum_params_sync(this->follower,
+ direction, port_id, SPA_PARAM_Latency,
+ &index, NULL, &param, &b)) != 1)
+ return res;
+ if ((res = spa_latency_parse(param, &latency)) < 0)
+ return res;
+ if (latency.direction == direction)
+ break;
+ }
+ if ((res = spa_node_port_set_param(this->target,
+ SPA_DIRECTION_REVERSE(direction), 0,
+ SPA_PARAM_Latency, 0, param)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void follower_port_info(void *data,
+ enum spa_direction direction, uint32_t port_id,
+ const struct spa_port_info *info)
+{
+ struct impl *this = data;
+ uint32_t i;
+ int res;
+
+ if (this->follower_removing) {
+ spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
+ return;
+ }
+
+ spa_log_debug(this->log, "%p: follower port info %s %p %08"PRIx64, this,
+ this->direction == SPA_DIRECTION_INPUT ?
+ "Input" : "Output", info, info->change_mask);
+
+ if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t idx;
+
+ switch (info->params[i].id) {
+ case SPA_PARAM_EnumFormat:
+ idx = IDX_EnumFormat;
+ break;
+ case SPA_PARAM_Format:
+ idx = IDX_Format;
+ break;
+ case SPA_PARAM_Latency:
+ idx = IDX_Latency;
+ break;
+ default:
+ continue;
+ }
+ if (!this->add_listener &&
+ this->follower_params_flags[idx] == info->params[i].flags)
+ continue;
+
+ this->follower_params_flags[idx] = info->params[i].flags;
+ this->params[idx].flags =
+ (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) |
+ (info->params[i].flags & SPA_PARAM_INFO_READWRITE);
+
+ if (idx == IDX_Latency) {
+ res = recalc_latency(this, direction, port_id);
+ spa_log_debug(this->log, "latency: %d (%s)", res,
+ spa_strerror(res));
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ if (!this->add_listener) {
+ this->params[idx].user++;
+ spa_log_debug(this->log, "param %d changed", info->params[i].id);
+ }
+ }
+ }
+ emit_node_info(this, false);
+
+ if (this->target == this->follower)
+ spa_node_emit_port_info(&this->hooks, direction, port_id, info);
+}
+
+static void follower_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct impl *this = data;
+
+ if (this->target != this->follower)
+ return;
+
+ spa_log_trace(this->log, "%p: result %d %d", this, seq, res);
+ spa_node_emit_result(&this->hooks, seq, res, type, result);
+}
+
+static void follower_event(void *data, const struct spa_event *event)
+{
+ struct impl *this = data;
+
+ spa_log_trace(this->log, "%p: event %d", this, SPA_EVENT_TYPE(event));
+
+ switch (SPA_NODE_EVENT_ID(event)) {
+ case SPA_NODE_EVENT_Error:
+ /* Forward errors */
+ spa_node_emit_event(&this->hooks, event);
+ break;
+ default:
+ /* Ignore other events */
+ break;
+ }
+}
+
+static const struct spa_node_events follower_node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .info = follower_info,
+ .port_info = follower_port_info,
+ .result = follower_result,
+ .event = follower_event,
+};
+
+static int follower_ready(void *data, int status)
+{
+ struct impl *this = data;
+
+ spa_log_trace_fp(this->log, "%p: ready %d", this, status);
+
+ if (this->target != this->follower) {
+ this->driver = true;
+
+ if (this->direction == SPA_DIRECTION_OUTPUT) {
+ int retry = 8;
+ while (retry--) {
+ status = spa_node_process(this->convert);
+ if (status & SPA_STATUS_HAVE_DATA)
+ break;
+
+ if (status & SPA_STATUS_NEED_DATA) {
+ status = spa_node_process(this->follower);
+ if (!(status & SPA_STATUS_HAVE_DATA))
+ break;
+ }
+ }
+
+ }
+ }
+
+ return spa_node_call_ready(&this->callbacks, status);
+}
+
+static int follower_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_id)
+{
+ int res;
+ struct impl *this = data;
+
+ if (this->target != this->follower && this->convert)
+ res = spa_node_port_reuse_buffer(this->convert, port_id, buffer_id);
+ else
+ res = spa_node_call_reuse_buffer(&this->callbacks, port_id, buffer_id);
+
+ return res;
+}
+
+static int follower_xrun(void *data, uint64_t trigger, uint64_t delay, struct spa_pod *info)
+{
+ struct impl *this = data;
+ return spa_node_call_xrun(&this->callbacks, trigger, delay, info);
+}
+
+static const struct spa_node_callbacks follower_node_callbacks = {
+ SPA_VERSION_NODE_CALLBACKS,
+ .ready = follower_ready,
+ .reuse_buffer = follower_reuse_buffer,
+ .xrun = follower_xrun,
+};
+
+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 l;
+ 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);
+
+ if (events->info || events->port_info) {
+ this->add_listener = true;
+
+ spa_zero(l);
+ spa_node_add_listener(this->follower, &l, &follower_node_events, this);
+ spa_hook_remove(&l);
+
+ if (this->convert) {
+ spa_zero(l);
+ spa_node_add_listener(this->convert, &l, &convert_node_events, this);
+ spa_hook_remove(&l);
+ }
+ this->add_listener = false;
+
+ emit_node_info(this, 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_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ return spa_node_sync(this->follower, seq);
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (direction != this->direction)
+ return -EINVAL;
+
+ return spa_node_add_port(this->target, direction, port_id, props);
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (direction != this->direction)
+ return -EINVAL;
+
+ return spa_node_remove_port(this->target, direction, port_id);
+}
+
+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;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ if (direction != this->direction)
+ port_id++;
+
+ spa_log_debug(this->log, "%p: %d %u", this, seq, id);
+
+ return spa_node_port_enum_params(this->target, seq, direction, port_id, id,
+ start, num, filter);
+}
+
+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;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_log_debug(this->log, " %d %d %d %d", port_id, id, direction, this->direction);
+
+ if (direction != this->direction)
+ port_id++;
+
+ if ((res = spa_node_port_set_param(this->target, direction, port_id, id,
+ flags, param)) < 0)
+ return res;
+
+ if ((id == SPA_PARAM_Latency) &&
+ direction == this->direction) {
+ if ((res = spa_node_port_set_param(this->follower, direction, 0, id,
+ flags, param)) < 0)
+ return res;
+ }
+
+ return res;
+}
+
+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);
+
+ spa_log_debug(this->log, "set io %d %d %d %d", port_id, id, direction, this->direction);
+
+ if (direction != this->direction)
+ port_id++;
+
+ return spa_node_port_set_io(this->target, direction, port_id, id, data, size);
+}
+
+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;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (direction != this->direction)
+ port_id++;
+
+ spa_log_debug(this->log, "%p: %d %d:%d", this,
+ n_buffers, direction, port_id);
+
+ if ((res = spa_node_port_use_buffers(this->target,
+ direction, port_id, flags, buffers, n_buffers)) < 0)
+ return res;
+
+ return res;
+}
+
+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);
+
+ return spa_node_port_reuse_buffer(this->target, port_id, buffer_id);
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ int status = 0, fstatus, retry = 8;
+
+ spa_log_trace_fp(this->log, "%p: process convert:%p driver:%d",
+ this, this->convert, this->driver);
+
+ 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);
+ }
+
+ if (this->direction == SPA_DIRECTION_INPUT) {
+ /* an input node (sink).
+ * 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;
+ /* schedule the follower when the converter needed
+ * a recycled buffer */
+ if (status == -EPIPE || status == 0)
+ status = SPA_STATUS_HAVE_DATA;
+ else if (status < 0)
+ break;
+
+ 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);
+ if (fstatus < 0) {
+ status = fstatus;
+ break;
+ }
+ /* if the follower doesn't need more data or is
+ * drained we can stop */
+ if ((fstatus & SPA_STATUS_NEED_DATA) == 0 ||
+ (fstatus & SPA_STATUS_DRAINED))
+ break;
+ }
+ /* the converter needs more data */
+ if ((status & SPA_STATUS_NEED_DATA))
+ break;
+ }
+ } else if (!this->driver) {
+ bool done = false;
+ while (retry--) {
+ /* 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;
+ 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)
+ break;
+
+ if (status & SPA_STATUS_NEED_DATA) {
+ /* the converter needs more data, schedule the
+ * follower */
+ fstatus = spa_node_process(this->follower);
+ if (fstatus < 0) {
+ status = fstatus;
+ break;
+ }
+ /* if the follower didn't produce more data or is
+ * not drained we can stop now */
+ 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);
+ }
+ spa_log_trace_fp(this->log, "%p: process status:%d", this, status);
+
+ this->driver = false;
+
+ return status;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ spa_hook_remove(&this->follower_listener);
+ spa_node_set_callbacks(this->follower, NULL, NULL);
+
+ if (this->hnd_convert)
+ spa_handle_clear(this->hnd_convert);
+
+ if (this->buffers)
+ free(this->buffers);
+ this->buffers = NULL;
+
+ return 0;
+}
+
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ size_t size = 0;
+
+#if 0
+ size += spa_handle_factory_get_size(&spa_videoconvert_factory, params);
+#endif
+ size += sizeof(struct impl);
+
+ return size;
+}
+
+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;
+#if 0
+ void *iface;
+#endif
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ spa_log_topic_init(this->log, log_topic);
+
+ this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+
+ if (info == NULL ||
+ (str = spa_dict_lookup(info, "video.adapt.follower")) == NULL)
+ return -EINVAL;
+
+ sscanf(str, "pointer:%p", &this->follower);
+ if (this->follower == NULL)
+ return -EINVAL;
+
+ if (this->cpu)
+ this->max_align = spa_cpu_get_max_align(this->cpu);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+#if 0
+ this->hnd_convert = SPA_PTROFF(this, sizeof(struct impl), struct spa_handle);
+ spa_handle_factory_init(&spa_videoconvert_factory,
+ this->hnd_convert,
+ info, support, n_support);
+
+ spa_handle_get_interface(this->hnd_convert, SPA_TYPE_INTERFACE_Node, &iface);
+ this->convert = iface;
+#endif
+ this->target = this->convert;
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.flags = SPA_NODE_FLAG_RT |
+ 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);
+ this->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ 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_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ spa_node_add_listener(this->follower,
+ &this->follower_listener, &follower_node_events, this);
+ spa_node_set_callbacks(this->follower, &follower_node_callbacks, this);
+
+ if (this->convert)
+ spa_node_add_listener(this->convert,
+ &this->convert_listener, &convert_node_events, this);
+
+ reconfigure_mode(this, true, this->direction, NULL);
+
+ link_io(this);
+
+ 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_videoadapter_factory = {
+ .version = SPA_VERSION_HANDLE_FACTORY,
+ .name = SPA_NAME_VIDEO_ADAPT,
+ .get_size = impl_get_size,
+ .init = impl_init,
+ .enum_interface_info = impl_enum_interface_info,
+};
diff --git a/spa/plugins/videotestsrc/draw.c b/spa/plugins/videotestsrc/draw.c
new file mode 100644
index 0000000..20fed49
--- /dev/null
+++ b/spa/plugins/videotestsrc/draw.c
@@ -0,0 +1,288 @@
+/* Spa Video Test Source
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+typedef enum {
+ GRAY = 0,
+ YELLOW,
+ CYAN,
+ GREEN,
+ MAGENTA,
+ RED,
+ BLUE,
+ BLACK,
+ NEG_I,
+ WHITE,
+ POS_Q,
+ DARK_BLACK,
+ LIGHT_BLACK,
+ N_COLORS
+} Color;
+
+typedef struct _Pixel Pixel;
+
+struct _Pixel {
+ unsigned char R;
+ unsigned char G;
+ unsigned char B;
+ unsigned char Y;
+ unsigned char U;
+ unsigned char V;
+};
+
+static Pixel colors[N_COLORS] = {
+ {191, 191, 191, 0, 0, 0}, /* GRAY */
+ {191, 191, 0, 0, 0, 0}, /* YELLOW */
+ {0, 191, 191, 0, 0, 0}, /* CYAN */
+ {0, 191, 0, 0, 0, 0}, /* GREEN */
+ {191, 0, 191, 0, 0, 0}, /* MAGENTA */
+ {191, 0, 0, 0, 0, 0}, /* RED */
+ {0, 0, 191, 0, 0, 0}, /* BLUE */
+ {19, 19, 19, 0, 0, 0}, /* BLACK */
+ {0, 33, 76, 0, 0, 0}, /* NEGATIVE I */
+ {255, 255, 255, 0, 0, 0}, /* WHITE */
+ {49, 0, 107, 0, 0, 0}, /* POSITIVE Q */
+ {9, 9, 9, 0, 0, 0}, /* DARK BLACK */
+ {29, 29, 29, 0, 0, 0}, /* LIGHT BLACK */
+};
+
+/* YUV values are computed in init_colors() */
+
+typedef struct _DrawingData DrawingData;
+
+typedef void (*DrawPixelFunc) (DrawingData * dd, int x, Pixel * pixel);
+
+struct _DrawingData {
+ char *line;
+ int width;
+ int height;
+ int stride;
+ DrawPixelFunc draw_pixel;
+};
+
+static inline void update_yuv(Pixel * pixel)
+{
+ uint16_t y, u, v;
+
+ /* see https://en.wikipedia.org/wiki/YUV#Studio_swing_for_BT.601 */
+
+ y = 76 * pixel->R + 150 * pixel->G + 29 * pixel->B;
+ u = -43 * pixel->R - 84 * pixel->G + 127 * pixel->B;
+ v = 127 * pixel->R - 106 * pixel->G - 21 * pixel->B;
+
+ y = (y + 128) >> 8;
+ u = (u + 128) >> 8;
+ v = (v + 128) >> 8;
+
+ pixel->Y = y;
+ pixel->U = u + 128;
+ pixel->V = v + 128;
+}
+
+static void init_colors(void)
+{
+ int i;
+
+ if (colors[WHITE].Y != 0) {
+ /* already computed */
+ return;
+ }
+
+ for (i = 0; i < N_COLORS; i++) {
+ update_yuv(&colors[i]);
+ }
+}
+
+static void draw_pixel_rgb(DrawingData * dd, int x, Pixel * color)
+{
+ dd->line[3 * x + 0] = color->R;
+ dd->line[3 * x + 1] = color->G;
+ dd->line[3 * x + 2] = color->B;
+}
+
+static void draw_pixel_uyvy(DrawingData * dd, int x, Pixel * color)
+{
+ if (x & 1) {
+ /* odd pixel */
+ dd->line[2 * (x - 1) + 3] = color->Y;
+ } else {
+ /* even pixel */
+ dd->line[2 * x + 0] = color->U;
+ dd->line[2 * x + 1] = color->Y;
+ dd->line[2 * x + 2] = color->V;
+ }
+}
+
+static int drawing_data_init(DrawingData * dd, struct impl *this, char *data)
+{
+ struct port *port = &this->port;
+ struct spa_video_info *format = &port->current_format;
+ struct spa_rectangle *size = &format->info.raw.size;
+
+ if ((format->media_type != SPA_MEDIA_TYPE_video) ||
+ (format->media_subtype != SPA_MEDIA_SUBTYPE_raw))
+ return -ENOTSUP;
+
+ if (format->info.raw.format == SPA_VIDEO_FORMAT_RGB) {
+ dd->draw_pixel = draw_pixel_rgb;
+ } else if (format->info.raw.format == SPA_VIDEO_FORMAT_UYVY) {
+ dd->draw_pixel = draw_pixel_uyvy;
+ } else
+ return -ENOTSUP;
+
+ dd->line = data;
+ dd->width = size->width;
+ dd->height = size->height;
+ dd->stride = port->stride;
+
+ return 0;
+}
+
+static inline void draw_pixels(DrawingData * dd, int offset, Color color, int length)
+{
+ int x;
+
+ for (x = offset; x < offset + length; x++) {
+ dd->draw_pixel(dd, x, &colors[color]);
+ }
+}
+
+static inline void next_line(DrawingData * dd)
+{
+ dd->line += dd->stride;
+}
+
+static void draw_smpte_snow(DrawingData * dd)
+{
+ int h, w;
+ int y1, y2;
+ int i, j;
+
+ w = dd->width;
+ h = dd->height;
+ y1 = 2 * h / 3;
+ y2 = 3 * h / 4;
+
+ for (i = 0; i < y1; i++) {
+ for (j = 0; j < 7; j++) {
+ int x1 = j * w / 7;
+ int x2 = (j + 1) * w / 7;
+ draw_pixels(dd, x1, j, x2 - x1);
+ }
+ next_line(dd);
+ }
+
+ for (i = y1; i < y2; i++) {
+ for (j = 0; j < 7; j++) {
+ int x1 = j * w / 7;
+ int x2 = (j + 1) * w / 7;
+ Color c = (j & 1) ? BLACK : BLUE - j;
+
+ draw_pixels(dd, x1, c, x2 - x1);
+ }
+ next_line(dd);
+ }
+
+ for (i = y2; i < h; i++) {
+ int x = 0;
+
+ /* negative I */
+ draw_pixels(dd, x, NEG_I, w / 6);
+ x += w / 6;
+
+ /* white */
+ draw_pixels(dd, x, WHITE, w / 6);
+ x += w / 6;
+
+ /* positive Q */
+ draw_pixels(dd, x, POS_Q, w / 6);
+ x += w / 6;
+
+ /* pluge */
+ draw_pixels(dd, x, DARK_BLACK, w / 12);
+ x += w / 12;
+ draw_pixels(dd, x, BLACK, w / 12);
+ x += w / 12;
+ draw_pixels(dd, x, LIGHT_BLACK, w / 12);
+ x += w / 12;
+
+ /* war of the ants (a.k.a. snow) */
+ for (j = x; j < w; j++) {
+ Pixel p;
+ unsigned char r = rand();
+
+ p.R = r;
+ p.G = r;
+ p.B = r;
+ update_yuv(&p);
+ dd->draw_pixel(dd, j, &p);
+ }
+
+ next_line(dd);
+ }
+}
+
+static void draw_snow(DrawingData * dd)
+{
+ int x, y;
+
+ for (y = 0; y < dd->height; y++) {
+ for (x = 0; x < dd->width; x++) {
+ Pixel p;
+ unsigned char r = rand();
+
+ p.R = r;
+ p.G = r;
+ p.B = r;
+ update_yuv(&p);
+ dd->draw_pixel(dd, x, &p);
+ }
+
+ next_line(dd);
+ }
+}
+
+static int draw(struct impl *this, char *data)
+{
+ DrawingData dd;
+ int res;
+
+ init_colors();
+
+ if ((res = drawing_data_init(&dd, this, data)) < 0)
+ return res;
+
+ switch (this->props.pattern) {
+ case PATTERN_SMPTE_SNOW:
+ draw_smpte_snow(&dd);
+ break;
+ case PATTERN_SNOW:
+ draw_snow(&dd);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
diff --git a/spa/plugins/videotestsrc/meson.build b/spa/plugins/videotestsrc/meson.build
new file mode 100644
index 0000000..01a33ee
--- /dev/null
+++ b/spa/plugins/videotestsrc/meson.build
@@ -0,0 +1,7 @@
+videotestsrc_sources = ['videotestsrc.c', 'plugin.c']
+
+videotestsrclib = shared_library('spa-videotestsrc',
+ videotestsrc_sources,
+ dependencies : [ spa_dep, pthread_lib ],
+ install : true,
+ install_dir : spa_plugindir / 'videotestsrc')
diff --git a/spa/plugins/videotestsrc/plugin.c b/spa/plugins/videotestsrc/plugin.c
new file mode 100644
index 0000000..dd87ebf
--- /dev/null
+++ b/spa/plugins/videotestsrc/plugin.c
@@ -0,0 +1,46 @@
+/* Spa Video Test Source plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_videotestsrc_factory;
+
+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_videotestsrc_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/videotestsrc/videotestsrc.c b/spa/plugins/videotestsrc/videotestsrc.c
new file mode 100644
index 0000000..8d96b1b
--- /dev/null
+++ b/spa/plugins/videotestsrc/videotestsrc.c
@@ -0,0 +1,999 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+
+#define NAME "videotestsrc"
+
+#define FRAMES_TO_TIME(port,f) ((port->current_format.info.raw.framerate.denom * (f) * SPA_NSEC_PER_SEC) / \
+ (port->current_format.info.raw.framerate.num))
+
+enum pattern {
+ PATTERN_SMPTE_SNOW,
+ PATTERN_SNOW,
+};
+
+#define DEFAULT_LIVE true
+#define DEFAULT_PATTERN PATTERN_SMPTE_SNOW
+
+struct props {
+ bool live;
+ uint32_t pattern;
+};
+
+static void reset_props(struct props *props)
+{
+ props->live = DEFAULT_LIVE;
+ props->pattern = DEFAULT_PATTERN;
+}
+
+#define MAX_BUFFERS 16
+#define MAX_PORTS 1
+
+struct buffer {
+ uint32_t id;
+ struct spa_buffer *outbuf;
+ bool outstanding;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+
+ bool have_format;
+ struct spa_video_info current_format;
+ size_t bpp;
+ int stride;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list empty;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[2];
+ struct props props;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ bool async;
+ struct spa_source timer_source;
+ struct itimerspec timerspec;
+
+ bool started;
+ uint64_t start_time;
+ uint64_t elapsed_time;
+
+ uint64_t frame_count;
+
+ struct port port;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_PORTS)
+
+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_PropInfo:
+ {
+ struct props *p = &this->props;
+ struct spa_pod_frame f[2];
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_live),
+ SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"),
+ SPA_PROP_INFO_type, SPA_POD_Bool(p->live));
+ break;
+ case 1:
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_patternType),
+ SPA_PROP_INFO_description, SPA_POD_String("The pattern"),
+ SPA_PROP_INFO_type, SPA_POD_Int(p->pattern),
+ 0);
+ spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0),
+ spa_pod_builder_push_struct(&b, &f[1]);
+ spa_pod_builder_int(&b, PATTERN_SMPTE_SNOW);
+ spa_pod_builder_string(&b, "SMPTE snow");
+ spa_pod_builder_int(&b, PATTERN_SNOW);
+ spa_pod_builder_string(&b, "Snow");
+ spa_pod_builder_pop(&b, &f[1]);
+ param = spa_pod_builder_pop(&b, &f[0]);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_live, SPA_POD_Bool(p->live),
+ SPA_PROP_patternType, SPA_POD_Int(p->pattern));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct port *port = &this->port;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_live, SPA_POD_OPT_Bool(&p->live),
+ SPA_PROP_patternType, SPA_POD_OPT_Int(&p->pattern));
+
+ if (p->live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ else
+ port->info.flags &= ~SPA_PORT_FLAG_LIVE;
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+#include "draw.c"
+
+static int fill_buffer(struct impl *this, struct buffer *b)
+{
+ return draw(this, b->outbuf->datas[0].data);
+}
+
+static void set_timer(struct impl *this, bool enabled)
+{
+ if (this->async || this->props.live) {
+ if (enabled) {
+ if (this->props.live) {
+ uint64_t next_time = this->start_time + this->elapsed_time;
+ this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC;
+ this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC;
+ } else {
+ this->timerspec.it_value.tv_sec = 0;
+ this->timerspec.it_value.tv_nsec = 1;
+ }
+ } else {
+ 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);
+ }
+}
+
+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, NAME " %p: timerfd error: %s",
+ this, spa_strerror(res));
+ }
+ }
+ return res;
+}
+
+static int make_buffer(struct impl *this)
+{
+ struct buffer *b;
+ struct port *port = &this->port;
+ 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, NAME " %p: out of buffers", this);
+ return -EPIPE;
+ }
+ b = spa_list_first(&port->empty, struct buffer, link);
+ spa_list_remove(&b->link);
+ b->outstanding = true;
+
+ n_bytes = b->outbuf->datas[0].maxsize;
+
+ spa_log_trace(this->log, NAME " %p: dequeue buffer %d", this, b->id);
+
+ fill_buffer(this, b);
+
+ b->outbuf->datas[0].chunk->offset = 0;
+ b->outbuf->datas[0].chunk->size = n_bytes;
+ b->outbuf->datas[0].chunk->stride = port->stride;
+
+ if (b->h) {
+ b->h->seq = this->frame_count;
+ b->h->pts = this->start_time + this->elapsed_time;
+ b->h->dts_offset = 0;
+ }
+
+ this->frame_count++;
+ this->elapsed_time = FRAMES_TO_TIME(port, this->frame_count);
+ set_timer(this, true);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ return io->status;
+}
+
+static void on_output(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ int res;
+
+ res = make_buffer(this);
+
+ if (res == SPA_STATUS_HAVE_DATA)
+ spa_node_call_ready(&this->callbacks, res);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ struct timespec now;
+
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (this->started)
+ return 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (this->props.live)
+ this->start_time = SPA_TIMESPEC_TO_NSEC(&now);
+ else
+ this->start_time = 0;
+ this->frame_count = 0;
+ this->elapsed_time = 0;
+
+ this->started = true;
+ set_timer(this, true);
+ break;
+ }
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if (!this->started)
+ return 0;
+ this->started = false;
+ set_timer(this, false);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_MEDIA_CLASS, "Video/Source" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+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);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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)
+{
+ switch (index) {
+ case 0:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ 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(3,
+ SPA_VIDEO_FORMAT_RGB,
+ SPA_VIDEO_FORMAT_RGB,
+ SPA_VIDEO_FORMAT_UYVY),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(INT32_MAX, INT32_MAX)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25,1),
+ &SPA_FRACTION(0, 1),
+ &SPA_FRACTION(INT32_MAX, 1)));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ 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_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_video_raw_build(&b, id, &port->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ {
+ struct spa_video_info_raw *raw_info = &port->current_format.info.raw;
+
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(port->stride * raw_info->size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride));
+ break;
+ }
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ spa_list_init(&port->empty);
+ this->started = false;
+ set_timer(this, false);
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int res;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_video_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_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 == SPA_VIDEO_FORMAT_RGB)
+ port->bpp = 3;
+ else if (info.info.raw.format == SPA_VIDEO_FORMAT_UYVY)
+ port->bpp = 2;
+ else
+ return -EINVAL;
+
+ if (info.info.raw.size.width == 0 ||
+ info.info.raw.size.height == 0 ||
+ info.info.raw.framerate.num == 0 ||
+ info.info.raw.framerate.denom == 0)
+ return -EINVAL;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ struct spa_video_info_raw *raw_info = &port->current_format.info.raw;
+ port->stride = SPA_ROUND_UP_N(port->bpp * raw_info->size.width, 4);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(this, port, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->outstanding = false;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ return -EINVAL;
+ }
+ spa_list_append(&port->empty, &b->link);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+ struct buffer *b = &port->buffers[id];
+ spa_return_if_fail(b->outstanding);
+
+ spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id);
+
+ b->outstanding = false;
+ spa_list_append(&port->empty, &b->link);
+
+ if (!this->props.live)
+ set_timer(this, true);
+}
+
+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(port_id == 0, -EINVAL);
+ port = &this->port;
+ spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL);
+
+ reuse_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (io->buffer_id < port->n_buffers) {
+ reuse_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (!this->props.live)
+ return make_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .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 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);
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (this->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);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+
+ 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);
+ 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);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ 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->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;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF;
+ if (this->props.live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+ spa_list_init(&port->empty);
+
+ 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, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Generate a video test pattern" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_videotestsrc_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ NAME,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/volume/meson.build b/spa/plugins/volume/meson.build
new file mode 100644
index 0000000..2445e2b
--- /dev/null
+++ b/spa/plugins/volume/meson.build
@@ -0,0 +1,7 @@
+volume_sources = ['volume.c', 'plugin.c']
+
+volumelib = shared_library('spa-volume',
+ volume_sources,
+ dependencies : [ spa_dep ],
+ install : true,
+ install_dir : spa_plugindir / 'volume')
diff --git a/spa/plugins/volume/plugin.c b/spa/plugins/volume/plugin.c
new file mode 100644
index 0000000..2ceb250
--- /dev/null
+++ b/spa/plugins/volume/plugin.c
@@ -0,0 +1,46 @@
+/* Spa Volume plugin
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_volume_factory;
+
+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_volume_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/volume/volume.c b/spa/plugins/volume/volume.c
new file mode 100644
index 0000000..6323766
--- /dev/null
+++ b/spa/plugins/volume/volume.c
@@ -0,0 +1,894 @@
+/* Spa
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stddef.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/utils/list.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+
+#define NAME "volume"
+
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+
+#define DEFAULT_VOLUME 1.0
+#define DEFAULT_MUTE false
+
+struct props {
+ double volume;
+ bool mute;
+};
+
+static void reset_props(struct props *props)
+{
+ props->volume = DEFAULT_VOLUME;
+ props->mute = DEFAULT_MUTE;
+}
+
+#define MAX_BUFFERS 16
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+ struct spa_meta_header *h;
+ void *ptr;
+ size_t size;
+ struct spa_list link;
+};
+
+struct port {
+ enum spa_direction direction;
+ uint32_t id;
+
+ bool have_format;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_io_buffers *io;
+
+ struct spa_list empty;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ uint32_t quantum_limit;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[5];
+ struct props props;
+
+ struct spa_hook_list hooks;
+
+ struct spa_audio_info current_format;
+ int bpf;
+
+ struct port in_ports[1];
+ struct port out_ports[1];
+
+ bool started;
+};
+
+#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+#define CHECK_PORT(this,d,p) ((p) == 0)
+#define GET_IN_PORT(this,p) (&this->in_ports[p])
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
+
+static 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_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct props *p;
+ 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);
+
+ p = &this->props;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume),
+ SPA_PROP_INFO_description, SPA_POD_String("The volume"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute),
+ SPA_PROP_INFO_description, SPA_POD_String("Mute"),
+ SPA_PROP_INFO_type, SPA_POD_Bool(p->mute));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_Props:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_volume, SPA_POD_Float(p->volume),
+ SPA_PROP_mute, SPA_POD_Bool(p->mute));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume),
+ SPA_PROP_mute, SPA_POD_OPT_Bool(&p->mute));
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ this->started = true;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ this->started = false;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, GET_IN_PORT(this, 0), true);
+ emit_port_info(this, GET_OUT_PORT(this, 0), true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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)
+{
+ switch (index) {
+ case 0:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(2,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_S16),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(
+ DEFAULT_RATE, 1, INT32_MAX),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(
+ DEFAULT_CHANNELS, 1, INT32_MAX));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ 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_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &this->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->bpf,
+ 16 * this->bpf,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->bpf));
+ break;
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ spa_list_init(&port->empty);
+ }
+ 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);
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if (info.info.raw.format != SPA_AUDIO_FORMAT_S16 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+
+ this->bpf = 2 * info.info.raw.channels;
+ this->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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)
+{
+ spa_return_val_if_fail(object != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL);
+
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(object, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ 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);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->flags = direction == SPA_DIRECTION_INPUT ? BUFFER_FLAG_OUT : 0;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ b->ptr = d[0].data;
+ b->size = d[0].maxsize;
+ } else {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ return -EINVAL;
+ }
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT))
+ spa_list_append(&port->empty, &b->link);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static void recycle_buffer(struct impl *this, uint32_t id)
+{
+ struct port *port = GET_OUT_PORT(this, 0);
+ struct buffer *b = &port->buffers[id];
+
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_warn(this->log, NAME " %p: buffer %d not outstanding", this, id);
+ return;
+ }
+
+ spa_list_append(&port->empty, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ spa_log_trace(this->log, NAME " %p: recycle buffer %d", this, id);
+}
+
+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);
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ recycle_buffer(this, buffer_id);
+
+ return 0;
+}
+
+static struct buffer *find_free_buffer(struct impl *this, struct port *port)
+{
+ struct buffer *b;
+
+ if (spa_list_is_empty(&port->empty))
+ return NULL;
+
+ b = spa_list_first(&port->empty, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ return b;
+}
+
+static void do_volume(struct impl *this, struct spa_buffer *dbuf, struct spa_buffer *sbuf)
+{
+ uint32_t i, n_samples, n_bytes;
+ struct spa_data *sd, *dd;
+ int16_t *src, *dst;
+ double volume;
+ uint32_t written, towrite, savail, davail;
+ uint32_t sindex, dindex;
+
+ volume = this->props.volume;
+
+ sd = sbuf->datas;
+ dd = dbuf->datas;
+
+ savail = SPA_MIN(sd[0].chunk->size, sd[0].maxsize);
+ sindex = sd[0].chunk->offset;
+ davail = 0;
+ dindex = 0;
+ davail = dd[0].maxsize - davail;
+
+ towrite = SPA_MIN(savail, davail);
+ written = 0;
+
+ while (written < towrite) {
+ uint32_t soffset = sindex % sd[0].maxsize;
+ uint32_t doffset = dindex % dd[0].maxsize;
+
+ src = SPA_PTROFF(sd[0].data, soffset, int16_t);
+ dst = SPA_PTROFF(dd[0].data, doffset, int16_t);
+
+ n_bytes = SPA_MIN(towrite, sd[0].maxsize - soffset);
+ n_bytes = SPA_MIN(n_bytes, dd[0].maxsize - doffset);
+
+ n_samples = n_bytes / sizeof(int16_t);
+ for (i = 0; i < n_samples; i++)
+ dst[i] = src[i] * volume;
+
+ sindex += n_bytes;
+ dindex += n_bytes;
+ written += n_bytes;
+ }
+ dd[0].chunk->offset = 0;
+ dd[0].chunk->size = written;
+ dd[0].chunk->stride = 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;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ 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) {
+ recycle_buffer(this, 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;
+ }
+
+ if ((dbuf = find_free_buffer(this, out_port)) == NULL) {
+ spa_log_error(this->log, NAME " %p: out of buffers", this);
+ return -EPIPE;
+ }
+
+ sbuf = &in_port->buffers[input->buffer_id];
+
+ spa_log_trace(this->log, NAME " %p: do volume %d -> %d", this, sbuf->id, dbuf->id);
+ do_volume(this, dbuf->outbuf, sbuf->outbuf);
+
+ 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 int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "clock.quantum-limit"))
+ spa_atou32(s, &this->quantum_limit, 0);
+ }
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 2;
+ reset_props(&this->props);
+
+ port = GET_IN_PORT(this, 0);
+ port->direction = SPA_DIRECTION_INPUT;
+ port->id = 0;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_IN_PLACE;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+ spa_list_init(&port->empty);
+
+ port = GET_OUT_PORT(this, 0);
+ port->direction = SPA_DIRECTION_OUTPUT;
+ port->id = 0;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+ spa_list_init(&port->empty);
+
+ 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_volume_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ NAME,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/vulkan/meson.build b/spa/plugins/vulkan/meson.build
new file mode 100644
index 0000000..0657d21
--- /dev/null
+++ b/spa/plugins/vulkan/meson.build
@@ -0,0 +1,12 @@
+spa_vulkan_sources = [
+ 'plugin.c',
+ 'vulkan-compute-filter.c',
+ 'vulkan-compute-source.c',
+ 'vulkan-utils.c'
+]
+
+spa_vulkan = shared_library('spa-vulkan',
+ spa_vulkan_sources,
+ dependencies : [ spa_dep, vulkan_dep, mathlib ],
+ install : true,
+ install_dir : spa_plugindir / 'vulkan')
diff --git a/spa/plugins/vulkan/plugin.c b/spa/plugins/vulkan/plugin.c
new file mode 100644
index 0000000..e9f40ba
--- /dev/null
+++ b/spa/plugins/vulkan/plugin.c
@@ -0,0 +1,50 @@
+/* Spa vulkan plugin
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+
+extern const struct spa_handle_factory spa_vulkan_compute_filter_factory;
+extern const struct spa_handle_factory spa_vulkan_compute_source_factory;
+
+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_vulkan_compute_source_factory;
+ break;
+ case 1:
+ *factory = &spa_vulkan_compute_filter_factory;
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/vulkan/shaders/disk-intersection.comp b/spa/plugins/vulkan/shaders/disk-intersection.comp
new file mode 100644
index 0000000..7b92fdf
--- /dev/null
+++ b/spa/plugins/vulkan/shaders/disk-intersection.comp
@@ -0,0 +1,143 @@
+// The MIT License
+// Copyright © 2013 Inigo Quilez
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+// Other intersectors:
+//
+// Box: https://www.shadertoy.com/view/ld23DV
+// Triangle: https://www.shadertoy.com/view/MlGcDz
+// Capsule: https://www.shadertoy.com/view/Xt3SzX
+// Ellipsoid: https://www.shadertoy.com/view/MlsSzn
+// Sphere: https://www.shadertoy.com/view/4d2XWV
+// Capped Cylinder: https://www.shadertoy.com/view/4lcSRn
+// Disk: https://www.shadertoy.com/view/lsfGDB
+// Capped Cone: https://www.shadertoy.com/view/llcfRf
+// Rounded Box: https://www.shadertoy.com/view/WlSXRW
+// Rounded Cone: https://www.shadertoy.com/view/MlKfzm
+// Torus: https://www.shadertoy.com/view/4sBGDy
+// Sphere4: https://www.shadertoy.com/view/3tj3DW
+// Goursat: https://www.shadertoy.com/view/3lj3DW
+
+
+#define SC 3.0
+
+#if 1
+//
+// Elegant way to intersect a planar coordinate system (3x3 linear system)
+//
+vec3 intersectCoordSys(in vec3 o, in vec3 d, vec3 c, vec3 u, vec3 v)
+{
+ vec3 q = o - c;
+ return vec3(dot(cross(u, v), q),
+ dot(cross(q, u), d),
+ dot(cross(v, q), d)) / dot(cross(v, u), d);
+}
+
+#else
+//
+// Ugly (but faster) way to intersect a planar coordinate system: plane + projection
+//
+vec3 intersectCoordSys(in vec3 o, in vec3 d, vec3 c, vec3 u, vec3 v)
+{
+ vec3 q = o - c;
+ vec3 n = cross(u, v);
+ float t = -dot(n, q) / dot(d, n);
+ float r = dot(u, q + d * t);
+ float s = dot(v, q + d * t);
+ return vec3(t, s, r);
+}
+
+#endif
+
+vec3 hash3(float n)
+{
+ return fract(sin(vec3(n, n + 1.0, n + 2.0)) *
+ vec3(43758.5453123, 12578.1459123, 19642.3490423));
+}
+
+vec3 shade(in vec4 res)
+{
+ float ra = length(res.yz);
+ float an = atan(res.y, res.z) + 8.0 * iTime;
+ float pa = sin(3.0 * an);
+
+ vec3 cola =
+ 0.5 + 0.5 * sin((res.w / 64.0) * 3.5 + vec3(0.0, 1.0, 2.0));
+
+ vec3 col = vec3(0.0);
+ col += cola * 0.4 * (1.0 - smoothstep(0.90, 1.00, ra));
+ col +=
+ cola * 1.0 * (1.0 -
+ smoothstep(0.00, 0.03,
+ abs(ra - 0.8))) * (0.5 + 0.5 * pa);
+ col +=
+ cola * 1.0 * (1.0 -
+ smoothstep(0.00, 0.20,
+ abs(ra - 0.8))) * (0.5 + 0.5 * pa);
+ col +=
+ cola * 0.5 * (1.0 -
+ smoothstep(0.05, 0.10,
+ abs(ra - 0.5))) * (0.5 + 0.5 * pa);
+ col +=
+ cola * 0.7 * (1.0 -
+ smoothstep(0.00, 0.30,
+ abs(ra - 0.5))) * (0.5 + 0.5 * pa);
+
+ return col * 0.3;
+}
+
+vec3 render(in vec3 ro, in vec3 rd)
+{
+ // raytrace
+ vec3 col = vec3(0.0);
+ for (int i = 0; i < 64; i++) {
+ // position disk
+ vec3 r = 2.5 * (-1.0 + 2.0 * hash3(float (i)));
+ r *= SC;
+ // orientate disk
+ vec3 u = normalize(r.zxy);
+ vec3 v = normalize(cross(u, vec3(0.0, 1.0, 0.0)));
+
+ // intersect coord sys
+ vec3 tmp = intersectCoordSys(ro, rd, r, u, v);
+ tmp /= SC;
+ if (dot(tmp.yz, tmp.yz) < 1.0 && tmp.x > 0.0) {
+ // shade
+ col += shade(vec4(tmp, float (i)));
+ }
+ }
+
+ return col;
+}
+
+void mainImage(out vec4 fragColor, in vec2 fragCoord)
+{
+ vec2 q = fragCoord.xy / iResolution.xy;
+ vec2 p = -1.0 + 2.0 * q;
+ p.x *= iResolution.x / iResolution.y;
+
+ // camera
+ vec3 ro =
+ 2.0 * vec3(cos(0.5 * iTime * 1.1), 0.0,
+ sin(0.5 * iTime * 1.1));
+ vec3 ta = vec3(0.0, 0.0, 0.0);
+ // camera matrix
+ vec3 ww = normalize(ta - ro);
+ vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
+ vec3 vv = normalize(cross(uu, ww));
+ // create view ray
+ vec3 rd = normalize(p.x * uu + p.y * vv + 1.0 * ww);
+
+ vec3 col = render(ro * SC, rd);
+
+ fragColor = vec4(col, 1.0);
+}
+
+void mainVR(out vec4 fragColor, in vec2 fragCoord, in vec3 fragRayOri,
+ in vec3 fragRayDir)
+{
+ vec3 col = render(fragRayOri + vec3(0.0, 0.0, 0.0), fragRayDir);
+
+ fragColor = vec4(col, 1.0);
+}
diff --git a/spa/plugins/vulkan/shaders/filter-color.comp b/spa/plugins/vulkan/shaders/filter-color.comp
new file mode 100644
index 0000000..e08b715
--- /dev/null
+++ b/spa/plugins/vulkan/shaders/filter-color.comp
@@ -0,0 +1,39 @@
+void mainImage( out vec4 fragColor, in vec2 fragCoord )
+{
+ vec2 p = fragCoord.xy/iResolution.xy;
+
+ vec4 col = texture(iChannel0, p);
+
+
+ //Desaturate
+ if(p.x<.25)
+ {
+ col = vec4( (col.r+col.g+col.b)/3. );
+ }
+ //Invert
+ else if (p.x<.5)
+ {
+ col = vec4(1.) - texture(iChannel0, p);
+ }
+ //Chromatic aberration
+ else if (p.x<.75)
+ {
+ vec2 offset = vec2(.01,.0);
+ col.r = texture(iChannel0, p+offset.xy).r;
+ col.g = texture(iChannel0, p ).g;
+ col.b = texture(iChannel0, p+offset.yx).b;
+ }
+ //Color switching
+ else
+ {
+ col.rgb = texture(iChannel0, p).brg;
+ }
+
+
+ //Line
+ if( mod(abs(p.x+.5/iResolution.y),.25)<0.5/iResolution.y )
+ col = vec4(1.);
+
+
+ fragColor = col;
+}
diff --git a/spa/plugins/vulkan/shaders/filter.comp b/spa/plugins/vulkan/shaders/filter.comp
new file mode 100644
index 0000000..f1ca486
--- /dev/null
+++ b/spa/plugins/vulkan/shaders/filter.comp
@@ -0,0 +1,44 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+#define WORKGROUP_SIZE 32
+layout (local_size_x = WORKGROUP_SIZE, local_size_y = WORKGROUP_SIZE, local_size_z = 1 ) in;
+
+layout(rgba32f, set = 0, binding = 0) uniform image2D resultImage;
+layout(set = 0, binding = 1) uniform sampler2D iChannel0;
+
+layout( push_constant ) uniform Constants {
+ float time;
+ int frame;
+ int width;
+ int height;
+} PushConstant;
+
+float iTime;
+int iFrame;
+vec3 iResolution;
+vec4 iMouse;
+
+void mainImage( out vec4 fragColor, in vec2 fragCoord );
+
+void main()
+{
+ iTime = PushConstant.time;
+ iFrame = PushConstant.frame;
+ iResolution = vec3(float(PushConstant.width), float(PushConstant.height), 0.0);
+ iMouse = vec4(0.0, 0.0, 0.0, 0.0);
+ vec2 coord = vec2(float(gl_GlobalInvocationID.x),
+ iResolution.y - float(gl_GlobalInvocationID.y));
+ vec4 outColor;
+
+ if(coord.x >= iResolution.x || coord.y >= iResolution.y)
+ return;
+
+ mainImage(outColor, coord);
+
+ imageStore(resultImage, ivec2(gl_GlobalInvocationID.xy), outColor);
+}
+
+//#include "smearcam.comp"
+#include "filter-color.comp"
+//#include "filter-ripple.comp"
diff --git a/spa/plugins/vulkan/shaders/filter.spv b/spa/plugins/vulkan/shaders/filter.spv
new file mode 100644
index 0000000..5bfd245
--- /dev/null
+++ b/spa/plugins/vulkan/shaders/filter.spv
Binary files differ
diff --git a/spa/plugins/vulkan/shaders/main.comp b/spa/plugins/vulkan/shaders/main.comp
new file mode 100644
index 0000000..368b7cf
--- /dev/null
+++ b/spa/plugins/vulkan/shaders/main.comp
@@ -0,0 +1,50 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+#define WORKGROUP_SIZE 32
+layout (local_size_x = WORKGROUP_SIZE, local_size_y = WORKGROUP_SIZE, local_size_z = 1 ) in;
+
+layout(rgba32f, binding = 0) uniform image2D resultImage;
+
+layout( push_constant ) uniform Constants {
+ float time;
+ int frame;
+ int width;
+ int height;
+} PushConstant;
+
+float iTime;
+int iFrame;
+vec3 iResolution;
+vec4 iMouse;
+
+void mainImage( out vec4 fragColor, in vec2 fragCoord );
+
+void main()
+{
+ iTime = PushConstant.time;
+ iFrame = PushConstant.frame;
+ iResolution = vec3(float(PushConstant.width), float(PushConstant.height), 0.0);
+ iMouse = vec4(0.0, 0.0, 0.0, 0.0);
+ vec2 coord = vec2(float(gl_GlobalInvocationID.x),
+ iResolution.y - float(gl_GlobalInvocationID.y));
+ vec4 outColor;
+
+ if(coord.x >= iResolution.x || coord.y >= iResolution.y)
+ return;
+
+ mainImage(outColor, coord);
+
+ imageStore(resultImage, ivec2(gl_GlobalInvocationID.xy), outColor);
+}
+
+//#include "plasma-globe.comp"
+//#include "mandelbrot-distance.comp"
+#include "disk-intersection.comp"
+//#include "ring-twister.comp"
+//#include "gears.comp"
+//#include "protean-clouds.comp"
+//#include "flame.comp"
+//#include "shader.comp"
+//#include "raymarching-primitives.comp"
+//#include "3d-primitives.comp"
diff --git a/spa/plugins/vulkan/shaders/main.spv b/spa/plugins/vulkan/shaders/main.spv
new file mode 100644
index 0000000..5da1755
--- /dev/null
+++ b/spa/plugins/vulkan/shaders/main.spv
Binary files differ
diff --git a/spa/plugins/vulkan/vulkan-compute-filter.c b/spa/plugins/vulkan/vulkan-compute-filter.c
new file mode 100644
index 0000000..94efec3
--- /dev/null
+++ b/spa/plugins/vulkan/vulkan-compute-filter.c
@@ -0,0 +1,808 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.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/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+
+#include "vulkan-utils.h"
+
+#define NAME "vulkan-compute-filter"
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+
+ enum spa_direction direction;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+
+ bool have_format;
+ struct spa_video_info current_format;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list empty;
+ struct spa_list ready;
+ uint32_t stream_id;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+
+ struct spa_io_position *position;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[2];
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ bool started;
+
+ struct vulkan_state state;
+ struct port port[2];
+};
+
+#define CHECK_PORT(this,d,p) ((p) < 1)
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ 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) {
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Position:
+ if (size > 0 && size < sizeof(struct spa_io_position))
+ return -EINVAL;
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+ struct buffer *b = &port->buffers[id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_debug(this->log, NAME " %p: reuse buffer %d", this, id);
+
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ spa_list_append(&port->empty, &b->link);
+ }
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if (this->started)
+ return 0;
+
+ this->started = true;
+ spa_vulkan_start(&this->state);
+ break;
+
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if (!this->started)
+ return 0;
+
+ this->started = false;
+ spa_vulkan_stop(&this->state);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+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);
+ 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, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port[0], true);
+ emit_port_info(this, &this->port[1], true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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)
+{
+ switch (index) {
+ case 0:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port[direction];
+
+ 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_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_video_dsp_build(&b, id, &port->current_format.info.dsp);
+ break;
+
+ case SPA_PARAM_Buffers:
+ {
+ if (!port->have_format)
+ return -EIO;
+ if (this->position == NULL)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ spa_log_debug(this->log, NAME" %p: %dx%d stride %d", this,
+ this->position->video.size.width,
+ this->position->video.size.height,
+ this->position->video.stride);
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(this->position->video.stride *
+ this->position->video.size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->position->video.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;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers", this);
+ spa_vulkan_stop(&this->state);
+ spa_vulkan_use_buffers(&this->state, &this->state.streams[port->stream_id], 0, 0, NULL);
+ port->n_buffers = 0;
+ spa_list_init(&port->empty);
+ spa_list_init(&port->ready);
+ this->started = false;
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int res;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ spa_vulkan_unprepare(&this->state);
+ } else {
+ struct spa_video_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_video &&
+ info.media_subtype != SPA_MEDIA_SUBTYPE_dsp)
+ return -EINVAL;
+
+ if (spa_format_video_dsp_parse(format, &info.info.dsp) < 0)
+ return -EINVAL;
+
+ if (info.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32)
+ return -EINVAL;
+
+ this->state.constants.width = this->position->video.size.width;
+ this->state.constants.height = this->position->video.size.height;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port[direction];
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ default:
+ return -ENOENT;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port[direction];
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->flags = 0;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ spa_log_info(this->log, "%p: %d:%d add buffer %p", port, direction, port_id, b);
+ spa_list_append(&port->empty, &b->link);
+ }
+ spa_vulkan_use_buffers(&this->state, &this->state.streams[port->stream_id], flags, n_buffers, buffers);
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port[direction];
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+
+ port = &this->port[SPA_DIRECTION_OUTPUT];
+ spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL);
+
+ reuse_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *inport, *outport;
+ struct spa_io_buffers *inio, *outio;
+ struct buffer *b;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ inport = &this->port[SPA_DIRECTION_INPUT];
+ if ((inio = inport->io) == NULL)
+ return -EIO;
+
+ if (inio->status != SPA_STATUS_HAVE_DATA)
+ return inio->status;
+
+ if (inio->buffer_id >= inport->n_buffers) {
+ inio->status = -EINVAL;
+ return -EINVAL;
+ }
+
+ outport = &this->port[SPA_DIRECTION_OUTPUT];
+ if ((outio = outport->io) == NULL)
+ return -EIO;
+
+ if (outio->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (outio->buffer_id < outport->n_buffers) {
+ reuse_buffer(this, outport, outio->buffer_id);
+ outio->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (spa_list_is_empty(&outport->empty)) {
+ spa_log_debug(this->log, NAME " %p: out of buffers", this);
+ return -EPIPE;
+ }
+ b = &inport->buffers[inio->buffer_id];
+ this->state.streams[inport->stream_id].pending_buffer_id = b->id;
+ inio->status = SPA_STATUS_NEED_DATA;
+
+ b = spa_list_first(&outport->empty, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ this->state.streams[outport->stream_id].pending_buffer_id = b->id;
+
+ this->state.constants.time += 0.025;
+ this->state.constants.frame++;
+
+ spa_log_debug(this->log, "filter into %d", b->id);
+
+ spa_vulkan_process(&this->state);
+
+ b->outbuf->datas[0].chunk->offset = 0;
+ b->outbuf->datas[0].chunk->size = b->outbuf->datas[0].maxsize;
+ b->outbuf->datas[0].chunk->stride = this->position->video.stride;
+
+ outio->buffer_id = b->id;
+ outio->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+
+ 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);
+ this->state.log = this->log;
+ this->state.shaderName = "spa/plugins/vulkan/shaders/filter.spv";
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_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;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 2;
+
+ port = &this->port[0];
+ port->stream_id = 1;
+ port->direction = SPA_DIRECTION_INPUT;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS |
+ SPA_PORT_CHANGE_MASK_PROPS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_CAN_ALLOC_BUFFERS;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+ spa_vulkan_init_stream(&this->state, &this->state.streams[port->stream_id],
+ SPA_DIRECTION_INPUT, NULL);
+ spa_list_init(&port->empty);
+ spa_list_init(&port->ready);
+
+ port = &this->port[1];
+ port->stream_id = 0;
+ port->direction = SPA_DIRECTION_OUTPUT;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS |
+ SPA_PORT_CHANGE_MASK_PROPS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_CAN_ALLOC_BUFFERS;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+ spa_list_init(&port->empty);
+ spa_list_init(&port->ready);
+ spa_vulkan_init_stream(&this->state, &this->state.streams[port->stream_id],
+ SPA_DIRECTION_OUTPUT, NULL);
+
+ this->state.n_streams = 2;
+ spa_vulkan_prepare(&this->state);
+
+ 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, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Filter video frames using a vulkan compute shader" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_vulkan_compute_filter_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_VULKAN_COMPUTE_FILTER,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/vulkan/vulkan-compute-source.c b/spa/plugins/vulkan/vulkan-compute-source.c
new file mode 100644
index 0000000..4602e5d
--- /dev/null
+++ b/spa/plugins/vulkan/vulkan-compute-source.c
@@ -0,0 +1,1016 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+
+#include "vulkan-utils.h"
+
+#define NAME "vulkan-compute-source"
+
+#define FRAMES_TO_TIME(this,f) ((this->position->video.framerate.denom * (f) * SPA_NSEC_PER_SEC) / \
+ (this->position->video.framerate.num))
+
+#define DEFAULT_LIVE true
+
+struct props {
+ bool live;
+};
+
+static void reset_props(struct props *props)
+{
+ props->live = DEFAULT_LIVE;
+}
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+
+ bool have_format;
+ struct spa_video_info current_format;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct spa_list empty;
+ struct spa_list ready;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+ struct spa_param_info params[2];
+ struct props props;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ bool async;
+ struct spa_source timer_source;
+ struct itimerspec timerspec;
+
+ bool started;
+ uint64_t start_time;
+ uint64_t elapsed_time;
+
+ uint64_t frame_count;
+
+ struct vulkan_state state;
+ struct port port;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < 1)
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ 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_PropInfo:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_live),
+ SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"),
+ SPA_PROP_INFO_type, SPA_POD_Bool(p->live));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_live, SPA_POD_Bool(p->live));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ if (size > 0 && size < sizeof(struct spa_io_clock))
+ return -EINVAL;
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct port *port = &this->port;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_live, SPA_POD_OPT_Bool(&p->live));
+
+ if (p->live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ else
+ port->info.flags &= ~SPA_PORT_FLAG_LIVE;
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+
+static void set_timer(struct impl *this, bool enabled)
+{
+ if (this->async || this->props.live) {
+ if (enabled) {
+ if (this->props.live) {
+ uint64_t next_time = this->start_time + this->elapsed_time;
+ this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC;
+ this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC;
+ } else {
+ this->timerspec.it_value.tv_sec = 0;
+ this->timerspec.it_value.tv_nsec = 1;
+ }
+ } else {
+ 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);
+ }
+}
+
+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, NAME " %p: timerfd error: %s",
+ this, spa_strerror(res));
+ }
+ }
+ return res;
+}
+
+static int make_buffer(struct impl *this)
+{
+ struct buffer *b;
+ struct port *port = &this->port;
+ uint32_t n_bytes;
+ int res;
+
+ if (read_timer(this) < 0)
+ return 0;
+
+ if ((res = spa_vulkan_ready(&this->state)) < 0) {
+ res = SPA_STATUS_OK;
+ goto next;
+ }
+
+ if (spa_list_is_empty(&port->empty)) {
+ set_timer(this, false);
+ spa_log_error(this->log, NAME " %p: out of buffers", this);
+ return -EPIPE;
+ }
+ b = spa_list_first(&port->empty, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ n_bytes = b->outbuf->datas[0].maxsize;
+
+ spa_log_trace(this->log, NAME " %p: dequeue buffer %d", this, b->id);
+
+ this->state.constants.time = this->elapsed_time / (float) SPA_NSEC_PER_SEC;
+ this->state.constants.frame = this->frame_count;
+
+ this->state.streams[0].pending_buffer_id = b->id;
+ spa_vulkan_process(&this->state);
+
+ if (this->state.streams[0].ready_buffer_id != SPA_ID_INVALID) {
+ struct buffer *b = &port->buffers[this->state.streams[0].ready_buffer_id];
+
+ this->state.streams[0].ready_buffer_id = SPA_ID_INVALID;
+
+ spa_log_trace(this->log, NAME " %p: ready buffer %d", this, b->id);
+
+ b->outbuf->datas[0].chunk->offset = 0;
+ b->outbuf->datas[0].chunk->size = n_bytes;
+ b->outbuf->datas[0].chunk->stride = this->position->video.stride;
+
+ if (b->h) {
+ b->h->seq = this->frame_count;
+ b->h->pts = this->start_time + this->elapsed_time;
+ b->h->dts_offset = 0;
+ }
+
+ spa_list_append(&port->ready, &b->link);
+
+ res = SPA_STATUS_HAVE_DATA;
+ }
+next:
+ this->frame_count++;
+ this->elapsed_time = FRAMES_TO_TIME(this, this->frame_count);
+ set_timer(this, true);
+
+ return res;
+}
+
+static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id)
+{
+ struct buffer *b = &port->buffers[id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id);
+
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ spa_list_append(&port->empty, &b->link);
+
+ if (!this->props.live)
+ set_timer(this, true);
+ }
+}
+
+static void on_output(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct port *port = &this->port;
+ struct spa_io_buffers *io = port->io;
+ int res;
+
+ if (io == NULL)
+ return;
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return;
+
+ if (io->buffer_id < port->n_buffers) {
+ reuse_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ res = make_buffer(this);
+
+ if (!spa_list_is_empty(&port->ready)) {
+ struct buffer *b = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ }
+ spa_node_call_ready(&this->callbacks, res);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ struct timespec now;
+
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (this->started)
+ return 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (this->props.live)
+ this->start_time = SPA_TIMESPEC_TO_NSEC(&now);
+ else
+ this->start_time = 0;
+ this->frame_count = 0;
+ this->elapsed_time = 0;
+
+ this->started = true;
+ set_timer(this, true);
+ spa_vulkan_start(&this->state);
+ break;
+ }
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if (!this->started)
+ return 0;
+
+ this->started = false;
+ set_timer(this, false);
+ spa_vulkan_stop(&this->state);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_MEDIA_CLASS, "Video/Source" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+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);
+ 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,
+ SPA_DIRECTION_OUTPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *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)
+{
+ switch (index) {
+ case 0:
+ *param = spa_pod_builder_add_object(builder,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32));
+ break;
+ default:
+ return 0;
+ }
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ 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_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_video_dsp_build(&b, id, &port->current_format.info.dsp);
+ break;
+
+ case SPA_PARAM_Buffers:
+ {
+ if (!port->have_format)
+ return -EIO;
+ if (this->position == NULL)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ spa_log_debug(this->log, NAME" %p: %dx%d stride %d", this,
+ this->position->video.size.width,
+ this->position->video.size.height,
+ this->position->video.stride);
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(this->position->video.stride *
+ this->position->video.size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->position->video.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;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_debug(this->log, NAME " %p: clear buffers", this);
+ spa_vulkan_use_buffers(&this->state, &this->state.streams[0], 0, 0, NULL);
+ port->n_buffers = 0;
+ spa_list_init(&port->empty);
+ spa_list_init(&port->ready);
+ this->started = false;
+ set_timer(this, false);
+ }
+ return 0;
+}
+
+static int port_set_format(struct impl *this, struct port *port,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int res;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ spa_vulkan_unprepare(&this->state);
+ } else {
+ struct spa_video_info info = { 0 };
+
+ if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return res;
+
+ if (info.media_type != SPA_MEDIA_TYPE_video &&
+ info.media_subtype != SPA_MEDIA_SUBTYPE_dsp)
+ return -EINVAL;
+
+ if (spa_format_video_dsp_parse(format, &info.info.dsp) < 0)
+ return -EINVAL;
+
+ if (info.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32)
+ return -EINVAL;
+
+ this->state.constants.width = this->position->video.size.width;
+ this->state.constants.height = this->position->video.size.height;
+
+ port->current_format = info;
+ port->have_format = true;
+ spa_vulkan_prepare(&this->state);
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = 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;
+ struct port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ default:
+ return -ENOENT;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->outbuf = buffers[i];
+ b->flags = 0;
+ b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h));
+
+ spa_list_append(&port->empty, &b->link);
+ }
+ spa_vulkan_use_buffers(&this->state, &this->state.streams[0], flags, n_buffers, buffers);
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+ port = &this->port;
+ spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL);
+
+ reuse_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+ if ((io = port->io) == NULL)
+ return -EIO;
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (io->buffer_id < port->n_buffers) {
+ reuse_buffer(this, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (!this->props.live)
+ return make_buffer(this);
+ else
+ return SPA_STATUS_OK;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .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 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);
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (this->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);
+
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+
+ 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);
+ 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);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ 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->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 |
+ SPA_PORT_CHANGE_MASK_PROPS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_CAN_ALLOC_BUFFERS;
+ if (this->props.live)
+ port->info.flags |= SPA_PORT_FLAG_LIVE;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 5;
+ spa_list_init(&port->empty);
+ spa_list_init(&port->ready);
+
+ this->state.log = this->log;
+ spa_vulkan_init_stream(&this->state, &this->state.streams[0],
+ SPA_DIRECTION_OUTPUT, NULL);
+ this->state.shaderName = "spa/plugins/vulkan/shaders/main.spv";
+ this->state.n_streams = 1;
+
+ 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, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Generate video frames using a vulkan compute shader" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_vulkan_compute_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_VULKAN_COMPUTE_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/vulkan/vulkan-utils.c b/spa/plugins/vulkan/vulkan-utils.c
new file mode 100644
index 0000000..ae3337b
--- /dev/null
+++ b/spa/plugins/vulkan/vulkan-utils.c
@@ -0,0 +1,758 @@
+#include <vulkan/vulkan.h>
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <string.h>
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
+#include <alloca.h>
+#endif
+#include <errno.h>
+#include <stdio.h>
+#include <assert.h>
+#include <math.h>
+#include <time.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+#include <spa/debug/mem.h>
+
+#include "vulkan-utils.h"
+
+//#define ENABLE_VALIDATION
+
+#define VULKAN_INSTANCE_FUNCTION(name) \
+ PFN_##name name = (PFN_##name)vkGetInstanceProcAddr(s->instance, #name)
+
+static int vkresult_to_errno(VkResult result)
+{
+ switch (result) {
+ case VK_SUCCESS:
+ case VK_EVENT_SET:
+ case VK_EVENT_RESET:
+ return 0;
+ case VK_NOT_READY:
+ case VK_INCOMPLETE:
+ case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR:
+ return EBUSY;
+ case VK_TIMEOUT:
+ return ETIMEDOUT;
+ case VK_ERROR_OUT_OF_HOST_MEMORY:
+ case VK_ERROR_OUT_OF_DEVICE_MEMORY:
+ case VK_ERROR_MEMORY_MAP_FAILED:
+ case VK_ERROR_OUT_OF_POOL_MEMORY:
+ case VK_ERROR_FRAGMENTED_POOL:
+#ifdef VK_ERROR_FRAGMENTATION_EXT
+ case VK_ERROR_FRAGMENTATION_EXT:
+#endif
+ return ENOMEM;
+ case VK_ERROR_INITIALIZATION_FAILED:
+ return EIO;
+ case VK_ERROR_DEVICE_LOST:
+ case VK_ERROR_SURFACE_LOST_KHR:
+#ifdef VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT
+ case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT:
+#endif
+ return ENODEV;
+ case VK_ERROR_LAYER_NOT_PRESENT:
+ case VK_ERROR_EXTENSION_NOT_PRESENT:
+ case VK_ERROR_FEATURE_NOT_PRESENT:
+ return ENOENT;
+ case VK_ERROR_INCOMPATIBLE_DRIVER:
+ case VK_ERROR_FORMAT_NOT_SUPPORTED:
+ case VK_ERROR_INCOMPATIBLE_DISPLAY_KHR:
+ return ENOTSUP;
+ case VK_ERROR_TOO_MANY_OBJECTS:
+ return ENFILE;
+ case VK_SUBOPTIMAL_KHR:
+ case VK_ERROR_OUT_OF_DATE_KHR:
+ return EIO;
+ case VK_ERROR_INVALID_EXTERNAL_HANDLE:
+ case VK_ERROR_INVALID_SHADER_NV:
+#ifdef VK_ERROR_VALIDATION_FAILED_EXT
+ case VK_ERROR_VALIDATION_FAILED_EXT:
+#endif
+#ifdef VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT
+ case VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT:
+#endif
+#ifdef VK_ERROR_INVALID_DEVICE_ADDRESS_EXT
+ case VK_ERROR_INVALID_DEVICE_ADDRESS_EXT:
+#endif
+ return EINVAL;
+#ifdef VK_ERROR_NOT_PERMITTED_EXT
+ case VK_ERROR_NOT_PERMITTED_EXT:
+ return EPERM;
+#endif
+ default:
+ return EIO;
+ }
+}
+
+#define VK_CHECK_RESULT(f) \
+{ \
+ VkResult _result = (f); \
+ int _r = -vkresult_to_errno(_result); \
+ if (_result != VK_SUCCESS) { \
+ spa_log_error(s->log, "error: %d (%d %s)", _result, _r, spa_strerror(_r)); \
+ return _r; \
+ } \
+}
+#define CHECK(f) \
+{ \
+ int _res = (f); \
+ if (_res < 0) \
+ return _res; \
+}
+
+static int createInstance(struct vulkan_state *s)
+{
+ static const VkApplicationInfo applicationInfo = {
+ .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+ .pApplicationName = "PipeWire",
+ .applicationVersion = 0,
+ .pEngineName = "PipeWire Vulkan Engine",
+ .engineVersion = 0,
+ .apiVersion = VK_API_VERSION_1_1
+ };
+ static const char * const extensions[] = {
+ VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME
+ };
+ static const char * const checkLayers[] = {
+#ifdef ENABLE_VALIDATION
+ "VK_LAYER_KHRONOS_validation",
+#endif
+ NULL
+ };
+ uint32_t i, j, layerCount, n_layers = 0;
+ const char *layers[1];
+ vkEnumerateInstanceLayerProperties(&layerCount, NULL);
+
+ VkLayerProperties availableLayers[layerCount];
+ vkEnumerateInstanceLayerProperties(&layerCount, availableLayers);
+
+ for (i = 0; i < layerCount; i++) {
+ for (j = 0; j < SPA_N_ELEMENTS(checkLayers); j++) {
+ if (spa_streq(availableLayers[i].layerName, checkLayers[j]))
+ layers[n_layers++] = checkLayers[j];
+ }
+ }
+
+ const VkInstanceCreateInfo createInfo = {
+ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ .pApplicationInfo = &applicationInfo,
+ .enabledExtensionCount = 1,
+ .ppEnabledExtensionNames = extensions,
+ .enabledLayerCount = n_layers,
+ .ppEnabledLayerNames = layers,
+ };
+
+ VK_CHECK_RESULT(vkCreateInstance(&createInfo, NULL, &s->instance));
+
+ return 0;
+}
+
+static uint32_t getComputeQueueFamilyIndex(struct vulkan_state *s)
+{
+ uint32_t i, queueFamilyCount;
+ VkQueueFamilyProperties *queueFamilies;
+
+ vkGetPhysicalDeviceQueueFamilyProperties(s->physicalDevice, &queueFamilyCount, NULL);
+
+ queueFamilies = alloca(queueFamilyCount * sizeof(VkQueueFamilyProperties));
+ vkGetPhysicalDeviceQueueFamilyProperties(s->physicalDevice, &queueFamilyCount, queueFamilies);
+
+ for (i = 0; i < queueFamilyCount; i++) {
+ VkQueueFamilyProperties props = queueFamilies[i];
+
+ if (props.queueCount > 0 && (props.queueFlags & VK_QUEUE_COMPUTE_BIT))
+ break;
+ }
+ if (i == queueFamilyCount)
+ return -ENODEV;
+
+ return i;
+}
+
+static int findPhysicalDevice(struct vulkan_state *s)
+{
+ uint32_t deviceCount;
+ VkPhysicalDevice *devices;
+
+ vkEnumeratePhysicalDevices(s->instance, &deviceCount, NULL);
+ if (deviceCount == 0)
+ return -ENODEV;
+
+ devices = alloca(deviceCount * sizeof(VkPhysicalDevice));
+ vkEnumeratePhysicalDevices(s->instance, &deviceCount, devices);
+
+ s->physicalDevice = devices[0];
+
+ s->queueFamilyIndex = getComputeQueueFamilyIndex(s);
+
+ return 0;
+}
+
+static int createDevice(struct vulkan_state *s)
+{
+
+ const VkDeviceQueueCreateInfo queueCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
+ .queueFamilyIndex = s->queueFamilyIndex,
+ .queueCount = 1,
+ .pQueuePriorities = (const float[]) { 1.0f }
+ };
+ static const char * const extensions[] = {
+ VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME,
+ VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME
+ };
+ const VkDeviceCreateInfo deviceCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
+ .queueCreateInfoCount = 1,
+ .pQueueCreateInfos = &queueCreateInfo,
+ .enabledExtensionCount = 2,
+ .ppEnabledExtensionNames = extensions,
+ };
+
+ VK_CHECK_RESULT(vkCreateDevice(s->physicalDevice, &deviceCreateInfo, NULL, &s->device));
+
+ vkGetDeviceQueue(s->device, s->queueFamilyIndex, 0, &s->queue);
+
+ static const VkFenceCreateInfo fenceCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+ .flags = 0,
+ };
+ VK_CHECK_RESULT(vkCreateFence(s->device, &fenceCreateInfo, NULL, &s->fence));
+
+ return 0;
+}
+
+static uint32_t findMemoryType(struct vulkan_state *s,
+ uint32_t memoryTypeBits, VkMemoryPropertyFlags properties)
+{
+ uint32_t i;
+ VkPhysicalDeviceMemoryProperties memoryProperties;
+
+ vkGetPhysicalDeviceMemoryProperties(s->physicalDevice, &memoryProperties);
+
+ for (i = 0; i < memoryProperties.memoryTypeCount; i++) {
+ if ((memoryTypeBits & (1 << i)) &&
+ ((memoryProperties.memoryTypes[i].propertyFlags & properties) == properties))
+ return i;
+ }
+ return -1;
+}
+
+static int createDescriptors(struct vulkan_state *s)
+{
+ uint32_t i;
+
+ VkDescriptorPoolSize descriptorPoolSizes[2] = {
+ {
+ .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
+ .descriptorCount = 1,
+ },
+ {
+ .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorCount = s->n_streams - 1,
+ },
+ };
+ const VkDescriptorPoolCreateInfo descriptorPoolCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
+ .maxSets = s->n_streams,
+ .poolSizeCount = s->n_streams > 1 ? 2 : 1,
+ .pPoolSizes = descriptorPoolSizes,
+ };
+
+ VK_CHECK_RESULT(vkCreateDescriptorPool(s->device,
+ &descriptorPoolCreateInfo, NULL,
+ &s->descriptorPool));
+
+ VkDescriptorSetLayoutBinding descriptorSetLayoutBinding[s->n_streams];
+ descriptorSetLayoutBinding[0] = (VkDescriptorSetLayoutBinding) {
+ .binding = 0,
+ .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT
+ };
+ for (i = 1; i < s->n_streams; i++) {
+ descriptorSetLayoutBinding[i] = (VkDescriptorSetLayoutBinding) {
+ .binding = i,
+ .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT
+ };
+ };
+ const VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
+ .bindingCount = s->n_streams,
+ .pBindings = descriptorSetLayoutBinding
+ };
+ VK_CHECK_RESULT(vkCreateDescriptorSetLayout(s->device,
+ &descriptorSetLayoutCreateInfo, NULL,
+ &s->descriptorSetLayout));
+
+ const VkDescriptorSetAllocateInfo descriptorSetAllocateInfo = {
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
+ .descriptorPool = s->descriptorPool,
+ .descriptorSetCount = 1,
+ .pSetLayouts = &s->descriptorSetLayout
+ };
+
+ VK_CHECK_RESULT(vkAllocateDescriptorSets(s->device,
+ &descriptorSetAllocateInfo,
+ &s->descriptorSet));
+
+ const VkSamplerCreateInfo samplerInfo = {
+ .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
+ .magFilter = VK_FILTER_LINEAR,
+ .minFilter = VK_FILTER_LINEAR,
+ .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
+ .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK,
+ .unnormalizedCoordinates = VK_FALSE,
+ .compareEnable = VK_FALSE,
+ .compareOp = VK_COMPARE_OP_ALWAYS,
+ .mipLodBias = 0.0f,
+ .minLod = 0,
+ .maxLod = 5,
+ };
+ VK_CHECK_RESULT(vkCreateSampler(s->device, &samplerInfo, NULL, &s->sampler));
+
+ return 0;
+}
+
+static int updateDescriptors(struct vulkan_state *s)
+{
+ uint32_t i;
+ VkDescriptorImageInfo descriptorImageInfo[s->n_streams];
+ VkWriteDescriptorSet writeDescriptorSet[s->n_streams];
+
+ for (i = 0; i < s->n_streams; i++) {
+ struct vulkan_stream *p = &s->streams[i];
+
+ if (p->current_buffer_id == p->pending_buffer_id ||
+ p->pending_buffer_id == SPA_ID_INVALID)
+ continue;
+
+ p->current_buffer_id = p->pending_buffer_id;
+ p->busy_buffer_id = p->current_buffer_id;
+ p->pending_buffer_id = SPA_ID_INVALID;
+
+ descriptorImageInfo[i] = (VkDescriptorImageInfo) {
+ .sampler = s->sampler,
+ .imageView = p->buffers[p->current_buffer_id].view,
+ .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
+ };
+ writeDescriptorSet[i] = (VkWriteDescriptorSet) {
+ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+ .dstSet = s->descriptorSet,
+ .dstBinding = i,
+ .descriptorCount = 1,
+ .descriptorType = i == 0 ?
+ VK_DESCRIPTOR_TYPE_STORAGE_IMAGE :
+ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .pImageInfo = &descriptorImageInfo[i],
+ };
+ }
+ vkUpdateDescriptorSets(s->device, s->n_streams,
+ writeDescriptorSet, 0, NULL);
+
+ return 0;
+}
+
+static VkShaderModule createShaderModule(struct vulkan_state *s, const char* shaderFile)
+{
+ VkShaderModule shaderModule = VK_NULL_HANDLE;
+ VkResult result;
+ void *data;
+ int fd;
+ struct stat stat;
+
+ if ((fd = open(shaderFile, 0, O_RDONLY)) == -1) {
+ spa_log_error(s->log, "can't open %s: %m", shaderFile);
+ return VK_NULL_HANDLE;
+ }
+ if (fstat(fd, &stat) < 0) {
+ spa_log_error(s->log, "can't stat %s: %m", shaderFile);
+ close(fd);
+ return VK_NULL_HANDLE;
+ }
+
+ data = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+
+ const VkShaderModuleCreateInfo shaderModuleCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
+ .codeSize = stat.st_size,
+ .pCode = data,
+ };
+ result = vkCreateShaderModule(s->device,
+ &shaderModuleCreateInfo, 0, &shaderModule);
+
+ munmap(data, stat.st_size);
+ close(fd);
+
+ if (result != VK_SUCCESS) {
+ spa_log_error(s->log, "can't create shader %s: %m", shaderFile);
+ return VK_NULL_HANDLE;
+ }
+ return shaderModule;
+}
+
+static int createComputePipeline(struct vulkan_state *s, const char *shader_file)
+{
+ static const VkPushConstantRange range = {
+ .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
+ .offset = 0,
+ .size = sizeof(struct push_constants)
+ };
+
+ const VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
+ .setLayoutCount = 1,
+ .pSetLayouts = &s->descriptorSetLayout,
+ .pushConstantRangeCount = 1,
+ .pPushConstantRanges = &range,
+ };
+ VK_CHECK_RESULT(vkCreatePipelineLayout(s->device,
+ &pipelineLayoutCreateInfo, NULL,
+ &s->pipelineLayout));
+
+ s->computeShaderModule = createShaderModule(s, shader_file);
+ if (s->computeShaderModule == VK_NULL_HANDLE)
+ return -ENOENT;
+
+ const VkPipelineShaderStageCreateInfo shaderStageCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+ .stage = VK_SHADER_STAGE_COMPUTE_BIT,
+ .module = s->computeShaderModule,
+ .pName = "main",
+ };
+ const VkComputePipelineCreateInfo pipelineCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
+ .stage = shaderStageCreateInfo,
+ .layout = s->pipelineLayout,
+ };
+ VK_CHECK_RESULT(vkCreateComputePipelines(s->device, VK_NULL_HANDLE,
+ 1, &pipelineCreateInfo, NULL,
+ &s->pipeline));
+ return 0;
+}
+
+static int createCommandBuffer(struct vulkan_state *s)
+{
+ const VkCommandPoolCreateInfo commandPoolCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+ .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
+ .queueFamilyIndex = s->queueFamilyIndex,
+ };
+ VK_CHECK_RESULT(vkCreateCommandPool(s->device,
+ &commandPoolCreateInfo, NULL,
+ &s->commandPool));
+
+ const VkCommandBufferAllocateInfo commandBufferAllocateInfo = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+ .commandPool = s->commandPool,
+ .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+ .commandBufferCount = 1,
+ };
+ VK_CHECK_RESULT(vkAllocateCommandBuffers(s->device,
+ &commandBufferAllocateInfo,
+ &s->commandBuffer));
+
+ return 0;
+}
+
+static int runCommandBuffer(struct vulkan_state *s)
+{
+ static const VkCommandBufferBeginInfo beginInfo = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+ .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
+ };
+ VK_CHECK_RESULT(vkBeginCommandBuffer(s->commandBuffer, &beginInfo));
+
+ VkImageMemoryBarrier barrier[s->n_streams];
+ uint32_t i;
+
+ for (i = 0; i < s->n_streams; i++) {
+ struct vulkan_stream *p = &s->streams[i];
+
+ barrier[i]= (VkImageMemoryBarrier) {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .subresourceRange.levelCount = 1,
+ .subresourceRange.layerCount = 1,
+ .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcAccessMask = 0,
+ .dstAccessMask = 0,
+ .image = p->buffers[p->current_buffer_id].image,
+ };
+ }
+
+ vkCmdPipelineBarrier(s->commandBuffer,
+ VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ 0, 0, NULL, 0, NULL,
+ s->n_streams, barrier);
+
+ vkCmdBindPipeline(s->commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, s->pipeline);
+ vkCmdPushConstants (s->commandBuffer,
+ s->pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT,
+ 0, sizeof(struct push_constants), (const void *) &s->constants);
+ vkCmdBindDescriptorSets(s->commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE,
+ s->pipelineLayout, 0, 1, &s->descriptorSet, 0, NULL);
+
+ vkCmdDispatch(s->commandBuffer,
+ (uint32_t)ceil(s->constants.width / (float)WORKGROUP_SIZE),
+ (uint32_t)ceil(s->constants.height / (float)WORKGROUP_SIZE), 1);
+
+ VK_CHECK_RESULT(vkEndCommandBuffer(s->commandBuffer));
+
+ VK_CHECK_RESULT(vkResetFences(s->device, 1, &s->fence));
+
+ const VkSubmitInfo submitInfo = {
+ .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+ .commandBufferCount = 1,
+ .pCommandBuffers = &s->commandBuffer,
+ };
+ VK_CHECK_RESULT(vkQueueSubmit(s->queue, 1, &submitInfo, s->fence));
+ s->started = true;
+
+ return 0;
+}
+
+static void clear_buffers(struct vulkan_state *s, struct vulkan_stream *p)
+{
+ uint32_t i;
+
+ for (i = 0; i < p->n_buffers; i++) {
+ if (p->buffers[i].fd != -1)
+ close(p->buffers[i].fd);
+ vkFreeMemory(s->device, p->buffers[i].memory, NULL);
+ vkDestroyImage(s->device, p->buffers[i].image, NULL);
+ vkDestroyImageView(s->device, p->buffers[i].view, NULL);
+ }
+ p->n_buffers = 0;
+}
+
+static void clear_streams(struct vulkan_state *s)
+{
+ uint32_t i;
+ for (i = 0; i < s->n_streams; i++) {
+ struct vulkan_stream *p = &s->streams[i];
+ clear_buffers(s, p);
+ }
+}
+
+int spa_vulkan_use_buffers(struct vulkan_state *s, struct vulkan_stream *p, uint32_t flags,
+ uint32_t n_buffers, struct spa_buffer **buffers)
+{
+ uint32_t i;
+ VULKAN_INSTANCE_FUNCTION(vkGetMemoryFdKHR);
+
+ clear_buffers(s, p);
+
+ for (i = 0; i < n_buffers; i++) {
+ VkExternalMemoryImageCreateInfo extInfo;
+ VkImageCreateInfo imageCreateInfo = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = VK_FORMAT_R32G32B32A32_SFLOAT,
+ .extent.width = s->constants.width,
+ .extent.height = s->constants.height,
+ .extent.depth = 1,
+ .mipLevels = 1,
+ .arrayLayers = 1,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .tiling = VK_IMAGE_TILING_LINEAR,
+ .usage = p->direction == SPA_DIRECTION_OUTPUT ?
+ VK_IMAGE_USAGE_STORAGE_BIT:
+ VK_IMAGE_USAGE_SAMPLED_BIT,
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ };
+
+ if (!(flags & SPA_NODE_BUFFERS_FLAG_ALLOC)) {
+ extInfo = (VkExternalMemoryImageCreateInfo) {
+ .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
+ .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
+ };
+ imageCreateInfo.pNext = &extInfo;
+ }
+
+ VK_CHECK_RESULT(vkCreateImage(s->device,
+ &imageCreateInfo, NULL, &p->buffers[i].image));
+
+ VkMemoryRequirements memoryRequirements;
+ vkGetImageMemoryRequirements(s->device,
+ p->buffers[i].image, &memoryRequirements);
+
+ VkMemoryAllocateInfo allocateInfo = {
+ .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+ .allocationSize = memoryRequirements.size,
+ .memoryTypeIndex = findMemoryType(s,
+ memoryRequirements.memoryTypeBits,
+ VK_MEMORY_PROPERTY_HOST_COHERENT_BIT |
+ VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT),
+ };
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) {
+ VK_CHECK_RESULT(vkAllocateMemory(s->device,
+ &allocateInfo, NULL, &p->buffers[i].memory));
+
+ const VkMemoryGetFdInfoKHR getFdInfo = {
+ .sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR,
+ .memory = p->buffers[i].memory,
+ .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT
+ };
+ int fd;
+
+ VK_CHECK_RESULT(vkGetMemoryFdKHR(s->device, &getFdInfo, &fd));
+
+ spa_log_info(s->log, "export DMABUF %zd", memoryRequirements.size);
+
+// buffers[i]->datas[0].type = SPA_DATA_DmaBuf;
+ buffers[i]->datas[0].type = SPA_DATA_MemFd;
+ buffers[i]->datas[0].fd = fd;
+ buffers[i]->datas[0].flags = SPA_DATA_FLAG_READABLE;
+ buffers[i]->datas[0].mapoffset = 0;
+ buffers[i]->datas[0].maxsize = memoryRequirements.size;
+ p->buffers[i].fd = fd;
+ } else {
+ VkImportMemoryFdInfoKHR importInfo = {
+ .sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR,
+ .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
+ .fd = fcntl(buffers[i]->datas[0].fd, F_DUPFD_CLOEXEC, 0),
+ };
+ allocateInfo.pNext = &importInfo;
+ p->buffers[i].fd = -1;
+ spa_log_info(s->log, "import DMABUF");
+
+ VK_CHECK_RESULT(vkAllocateMemory(s->device,
+ &allocateInfo, NULL, &p->buffers[i].memory));
+ }
+ VK_CHECK_RESULT(vkBindImageMemory(s->device,
+ p->buffers[i].image, p->buffers[i].memory, 0));
+
+ VkImageViewCreateInfo viewInfo = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ .image = p->buffers[i].image,
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = VK_FORMAT_R32G32B32A32_SFLOAT,
+ .components.r = VK_COMPONENT_SWIZZLE_R,
+ .components.g = VK_COMPONENT_SWIZZLE_G,
+ .components.b = VK_COMPONENT_SWIZZLE_B,
+ .components.a = VK_COMPONENT_SWIZZLE_A,
+ .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .subresourceRange.levelCount = 1,
+ .subresourceRange.layerCount = 1,
+ };
+
+ VK_CHECK_RESULT(vkCreateImageView(s->device,
+ &viewInfo, NULL, &p->buffers[i].view));
+ }
+ p->n_buffers = n_buffers;
+
+ return 0;
+}
+
+int spa_vulkan_init_stream(struct vulkan_state *s, struct vulkan_stream *stream,
+ enum spa_direction direction, struct spa_dict *props)
+{
+ spa_zero(*stream);
+ stream->direction = direction;
+ stream->current_buffer_id = SPA_ID_INVALID;
+ stream->busy_buffer_id = SPA_ID_INVALID;
+ stream->ready_buffer_id = SPA_ID_INVALID;
+ return 0;
+}
+
+int spa_vulkan_prepare(struct vulkan_state *s)
+{
+ if (!s->prepared) {
+ CHECK(createInstance(s));
+ CHECK(findPhysicalDevice(s));
+ CHECK(createDevice(s));
+ CHECK(createDescriptors(s));
+ CHECK(createComputePipeline(s, s->shaderName));
+ CHECK(createCommandBuffer(s));
+ s->prepared = true;
+ }
+ return 0;
+}
+
+int spa_vulkan_unprepare(struct vulkan_state *s)
+{
+ if (s->prepared) {
+ vkDestroyShaderModule(s->device, s->computeShaderModule, NULL);
+ vkDestroySampler(s->device, s->sampler, NULL);
+ vkDestroyDescriptorPool(s->device, s->descriptorPool, NULL);
+ vkDestroyDescriptorSetLayout(s->device, s->descriptorSetLayout, NULL);
+ vkDestroyPipelineLayout(s->device, s->pipelineLayout, NULL);
+ vkDestroyPipeline(s->device, s->pipeline, NULL);
+ vkDestroyCommandPool(s->device, s->commandPool, NULL);
+ vkDestroyFence(s->device, s->fence, NULL);
+ vkDestroyDevice(s->device, NULL);
+ vkDestroyInstance(s->instance, NULL);
+ s->prepared = false;
+ }
+ return 0;
+}
+
+int spa_vulkan_start(struct vulkan_state *s)
+{
+ uint32_t i;
+
+ for (i = 0; i < s->n_streams; i++) {
+ struct vulkan_stream *p = &s->streams[i];
+ p->current_buffer_id = SPA_ID_INVALID;
+ p->busy_buffer_id = SPA_ID_INVALID;
+ p->ready_buffer_id = SPA_ID_INVALID;
+ }
+ return 0;
+}
+
+int spa_vulkan_stop(struct vulkan_state *s)
+{
+ VK_CHECK_RESULT(vkDeviceWaitIdle(s->device));
+ clear_streams(s);
+ s->started = false;
+ return 0;
+}
+
+int spa_vulkan_ready(struct vulkan_state *s)
+{
+ uint32_t i;
+ VkResult result;
+
+ if (!s->started)
+ return 0;
+
+ result = vkGetFenceStatus(s->device, s->fence);
+ if (result == VK_NOT_READY)
+ return -EBUSY;
+ VK_CHECK_RESULT(result);
+
+ s->started = false;
+
+ for (i = 0; i < s->n_streams; i++) {
+ struct vulkan_stream *p = &s->streams[i];
+ p->ready_buffer_id = p->busy_buffer_id;
+ p->busy_buffer_id = SPA_ID_INVALID;
+ }
+ return 0;
+}
+
+int spa_vulkan_process(struct vulkan_state *s)
+{
+ CHECK(updateDescriptors(s));
+ CHECK(runCommandBuffer(s));
+ VK_CHECK_RESULT(vkDeviceWaitIdle(s->device));
+
+ return 0;
+}
diff --git a/spa/plugins/vulkan/vulkan-utils.h b/spa/plugins/vulkan/vulkan-utils.h
new file mode 100644
index 0000000..c818322
--- /dev/null
+++ b/spa/plugins/vulkan/vulkan-utils.h
@@ -0,0 +1,86 @@
+#include <vulkan/vulkan.h>
+
+#include <spa/buffer/buffer.h>
+#include <spa/node/node.h>
+
+#define MAX_STREAMS 2
+#define MAX_BUFFERS 16
+#define WORKGROUP_SIZE 32
+
+struct pixel {
+ float r, g, b, a;
+};
+
+struct push_constants {
+ float time;
+ int frame;
+ int width;
+ int height;
+};
+
+struct vulkan_buffer {
+ int fd;
+ VkImage image;
+ VkImageView view;
+ VkDeviceMemory memory;
+};
+
+struct vulkan_stream {
+ enum spa_direction direction;
+
+ uint32_t pending_buffer_id;
+ uint32_t current_buffer_id;
+ uint32_t busy_buffer_id;
+ uint32_t ready_buffer_id;
+
+ struct vulkan_buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+};
+
+struct vulkan_state {
+ struct spa_log *log;
+
+ struct push_constants constants;
+
+ VkInstance instance;
+
+ VkPhysicalDevice physicalDevice;
+ VkDevice device;
+
+ VkPipeline pipeline;
+ VkPipelineLayout pipelineLayout;
+ const char *shaderName;
+ VkShaderModule computeShaderModule;
+
+ VkCommandPool commandPool;
+ VkCommandBuffer commandBuffer;
+
+ VkQueue queue;
+ uint32_t queueFamilyIndex;
+ VkFence fence;
+ unsigned int prepared:1;
+ unsigned int started:1;
+
+ VkDescriptorPool descriptorPool;
+ VkDescriptorSetLayout descriptorSetLayout;
+
+ VkSampler sampler;
+
+ uint32_t n_streams;
+ VkDescriptorSet descriptorSet;
+ struct vulkan_stream streams[MAX_STREAMS];
+};
+
+int spa_vulkan_init_stream(struct vulkan_state *s, struct vulkan_stream *stream, enum spa_direction,
+ struct spa_dict *props);
+
+int spa_vulkan_prepare(struct vulkan_state *s);
+int spa_vulkan_use_buffers(struct vulkan_state *s, struct vulkan_stream *stream, uint32_t flags,
+ uint32_t n_buffers, struct spa_buffer **buffers);
+int spa_vulkan_unprepare(struct vulkan_state *s);
+
+int spa_vulkan_start(struct vulkan_state *s);
+int spa_vulkan_stop(struct vulkan_state *s);
+int spa_vulkan_ready(struct vulkan_state *s);
+int spa_vulkan_process(struct vulkan_state *s);
+int spa_vulkan_cleanup(struct vulkan_state *s);
diff --git a/spa/tests/benchmark-dict.c b/spa/tests/benchmark-dict.c
new file mode 100644
index 0000000..c47845e
--- /dev/null
+++ b/spa/tests/benchmark-dict.c
@@ -0,0 +1,145 @@
+/* Spa
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+#include <assert.h>
+
+#include <spa/utils/dict.h>
+#include <spa/utils/string.h>
+
+#define MAX_COUNT 100000
+#define MAX_ITEMS 1000
+
+static struct spa_dict_item items[MAX_ITEMS];
+static char values[MAX_ITEMS][32];
+
+static void gen_values(void)
+{
+ uint32_t i, j, idx;
+ static const char chars[] = "abcdefghijklmnopqrstuvwxyz.:*ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ for (i = 0; i < MAX_ITEMS; i++) {
+ for (j = 0; j < 32; j++) {
+ idx = random() % sizeof(chars);
+ values[i][j] = chars[idx];
+ }
+ idx = random() % 16;
+ values[i][idx + 16] = 0;
+ }
+}
+
+static void gen_dict(struct spa_dict *dict, uint32_t n_items)
+{
+ uint32_t i, idx;
+
+ for (i = 0; i < n_items; i++) {
+ idx = random() % MAX_ITEMS;
+ items[i] = SPA_DICT_ITEM_INIT(values[idx], values[idx]);
+ }
+ dict->items = items;
+ dict->n_items = n_items;
+ dict->flags = 0;
+}
+
+static void test_query(const struct spa_dict *dict)
+{
+ uint32_t i, idx;
+ const char *str;
+
+ for (i = 0; i < MAX_COUNT; i++) {
+ idx = random() % dict->n_items;
+ str = spa_dict_lookup(dict, dict->items[idx].key);
+ assert(spa_streq(str, dict->items[idx].value));
+ }
+}
+
+static void test_lookup(struct spa_dict *dict)
+{
+ struct timespec ts;
+ uint64_t t1, t2, t3, t4;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ test_query(dict);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ fprintf(stderr, "%d elapsed %"PRIu64" count %u = %"PRIu64"/sec\n", dict->n_items,
+ t2 - t1, MAX_COUNT, MAX_COUNT * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1));
+
+ spa_dict_qsort(dict);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t3 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ fprintf(stderr, "%d sort elapsed %"PRIu64"\n", dict->n_items, t3 - t2);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t3 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ test_query(dict);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t4 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ fprintf(stderr, "%d elapsed %"PRIu64" count %u = %"PRIu64"/sec %f speedup\n", dict->n_items,
+ t4 - t3, MAX_COUNT, MAX_COUNT * (uint64_t)SPA_NSEC_PER_SEC / (t4 - t3),
+ (double)(t2 - t1) / (t4 - t2));
+}
+
+int main(int argc, char *argv[])
+{
+ struct spa_dict dict;
+
+ spa_zero(dict);
+ gen_values();
+
+ /* warmup */
+ gen_dict(&dict, 1000);
+ test_query(&dict);
+
+ gen_dict(&dict, 10);
+ test_lookup(&dict);
+
+ gen_dict(&dict, 20);
+ test_lookup(&dict);
+
+ gen_dict(&dict, 50);
+ test_lookup(&dict);
+
+ gen_dict(&dict, 100);
+ test_lookup(&dict);
+
+ gen_dict(&dict, 1000);
+ test_lookup(&dict);
+
+ return 0;
+}
diff --git a/spa/tests/benchmark-pod.c b/spa/tests/benchmark-pod.c
new file mode 100644
index 0000000..8af1a5f
--- /dev/null
+++ b/spa/tests/benchmark-pod.c
@@ -0,0 +1,294 @@
+/* Spa
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/pod/pod.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/debug/pod.h>
+
+#define MAX_COUNT 10000000
+
+static void test_builder(void)
+{
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { NULL, };
+ struct spa_pod_frame f[2];
+ struct timespec ts;
+ uint64_t t1, t2;
+ uint64_t count = 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ fprintf(stderr, "test_builder() : ");
+ for (count = 0; count < MAX_COUNT; count++) {
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, 0);
+ spa_pod_builder_prop(&b, SPA_FORMAT_mediaType, 0);
+ spa_pod_builder_id(&b, SPA_MEDIA_TYPE_video);
+ spa_pod_builder_prop(&b, SPA_FORMAT_mediaSubtype, 0);
+ spa_pod_builder_id(&b, SPA_MEDIA_SUBTYPE_raw);
+
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_format, 0);
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Enum, 0);
+ spa_pod_builder_id(&b, SPA_VIDEO_FORMAT_I420);
+ spa_pod_builder_id(&b, SPA_VIDEO_FORMAT_I420);
+ spa_pod_builder_id(&b, SPA_VIDEO_FORMAT_YUY2);
+ spa_pod_builder_pop(&b, &f[1]);
+
+ struct spa_rectangle size_min_max[] = { {1, 1}, {INT32_MAX, INT32_MAX} };
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_size, 0);
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Range, 0);
+ spa_pod_builder_rectangle(&b, 320, 240);
+ spa_pod_builder_raw(&b, size_min_max, sizeof(size_min_max));
+ spa_pod_builder_pop(&b, &f[1]);
+
+ struct spa_fraction rate_min_max[] = { {0, 1}, {INT32_MAX, 1} };
+ spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_framerate, 0);
+ spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Range, 0);
+ spa_pod_builder_fraction(&b, 25, 1);
+ spa_pod_builder_raw(&b, rate_min_max, sizeof(rate_min_max));
+ spa_pod_builder_pop(&b, &f[1]);
+
+ spa_pod_builder_pop(&b, &f[0]);
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+ if (t2 - t1 > 1 * SPA_NSEC_PER_SEC)
+ break;
+ }
+ fprintf(stderr, "elapsed %"PRIu64" count %"PRIu64" = %"PRIu64"/sec\n",
+ t2 - t1, count, count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1));
+}
+
+static void test_builder2(void)
+{
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { NULL, };
+ struct timespec ts;
+ uint64_t t1, t2;
+ uint64_t count = 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ fprintf(stderr, "test_builder2() : ");
+ for (count = 0; count < MAX_COUNT; count++) {
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, 0,
+ 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(3,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_YUY2),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(INT32_MAX, INT32_MAX)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25,1),
+ &SPA_FRACTION(0,1),
+ &SPA_FRACTION(INT32_MAX,1)));
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+ if (t2 - t1 > 1 * SPA_NSEC_PER_SEC)
+ break;
+ }
+ fprintf(stderr, "elapsed %"PRIu64" count %"PRIu64" = %"PRIu64"/sec\n",
+ t2 - t1, count, count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1));
+}
+
+static void test_parse(void)
+{
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { NULL, };
+ struct timespec ts;
+ uint64_t t1, t2;
+ uint64_t count = 0;
+ struct spa_pod *fmt;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ fmt = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, 0,
+ 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(3,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_YUY2),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(INT32_MAX, INT32_MAX)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25,1),
+ &SPA_FRACTION(0,1),
+ &SPA_FRACTION(INT32_MAX,1)));
+
+ spa_pod_fixate(fmt);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ fprintf(stderr, "test_parse() : ");
+ for (count = 0; count < MAX_COUNT; count++) {
+ struct {
+ uint32_t media_type;
+ uint32_t media_subtype;
+ uint32_t format;
+ struct spa_rectangle size;
+ struct spa_fraction framerate;
+ } vals;
+ struct spa_pod_prop *prop;
+
+ spa_zero(vals);
+
+ SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)fmt, prop) {
+ uint32_t n_vals, choice;
+ struct spa_pod *pod = spa_pod_get_values(&prop->value, &n_vals, &choice);
+
+ switch(prop->key) {
+ case SPA_FORMAT_mediaType:
+ spa_pod_get_id(pod, &vals.media_type);
+ break;
+ case SPA_FORMAT_mediaSubtype:
+ spa_pod_get_id(pod, &vals.media_subtype);
+ break;
+ case SPA_FORMAT_VIDEO_format:
+ spa_pod_get_id(pod, &vals.format);
+ break;
+ case SPA_FORMAT_VIDEO_size:
+ spa_pod_get_rectangle(pod, &vals.size);
+ break;
+ case SPA_FORMAT_VIDEO_framerate:
+ spa_pod_get_fraction(pod, &vals.framerate);
+ break;
+ default:
+ break;
+ }
+ }
+ spa_assert(vals.media_type == SPA_MEDIA_TYPE_video);
+ spa_assert(vals.media_subtype == SPA_MEDIA_SUBTYPE_raw);
+ spa_assert(vals.format == SPA_VIDEO_FORMAT_I420);
+ spa_assert(vals.size.width == 320 && vals.size.height == 240);
+ spa_assert(vals.framerate.num == 25 && vals.framerate.denom == 1);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+ if (t2 - t1 > 1 * SPA_NSEC_PER_SEC)
+ break;
+ }
+ fprintf(stderr, "elapsed %"PRIu64" count %"PRIu64" = %"PRIu64"/sec\n",
+ t2 - t1, count, count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1));
+}
+
+static void test_parser(void)
+{
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { NULL, };
+ struct timespec ts;
+ uint64_t t1, t2;
+ uint64_t count = 0;
+ struct spa_pod *fmt;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ fmt = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, 0,
+ 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(3,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_YUY2),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(INT32_MAX, INT32_MAX)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25,1),
+ &SPA_FRACTION(0,1),
+ &SPA_FRACTION(INT32_MAX,1)));
+
+ spa_pod_fixate(fmt);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t1 = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ fprintf(stderr, "test_parser() : ");
+ for (count = 0; count < MAX_COUNT; count++) {
+ struct {
+ uint32_t media_type;
+ uint32_t media_subtype;
+ uint32_t format;
+ struct spa_rectangle size;
+ struct spa_fraction framerate;
+ } vals;
+
+ spa_zero(vals);
+
+ spa_pod_parse_object(fmt,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&vals.media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&vals.media_subtype),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(&vals.format),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&vals.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&vals.framerate));
+
+ spa_assert(vals.media_type == SPA_MEDIA_TYPE_video);
+ spa_assert(vals.media_subtype == SPA_MEDIA_SUBTYPE_raw);
+ spa_assert(vals.format == SPA_VIDEO_FORMAT_I420);
+ spa_assert(vals.size.width == 320 && vals.size.height == 240);
+ spa_assert(vals.framerate.num == 25 && vals.framerate.denom == 1);
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ t2 = SPA_TIMESPEC_TO_NSEC(&ts);
+ if (t2 - t1 > 1 * SPA_NSEC_PER_SEC)
+ break;
+ }
+ fprintf(stderr, "elapsed %"PRIu64" count %"PRIu64" = %"PRIu64"/sec\n",
+ t2 - t1, count, count * (uint64_t)SPA_NSEC_PER_SEC / (t2 - t1));
+}
+
+int main(int argc, char *argv[])
+{
+ test_builder();
+ test_builder2();
+ test_parse();
+ test_parser();
+ return 0;
+}
diff --git a/spa/tests/meson.build b/spa/tests/meson.build
new file mode 100644
index 0000000..c73c887
--- /dev/null
+++ b/spa/tests/meson.build
@@ -0,0 +1,58 @@
+# Generate a compilation test for each SPA header, excluding the type-info.h
+# ones which have circular dependencies and take some effort to fix.
+# Do it for C++ if possible (picks up C++-specific errors), otherwise for C.
+find = find_program('find', required: false)
+summary({'find (for header testing)': find.found()}, bool_yn: true, section: 'Optional programs')
+if find.found()
+ spa_headers = run_command(find,
+ meson.project_source_root() / 'spa' / 'include',
+ '-name', '*.h',
+ '-not', '-name', 'type-info.h',
+ '-type', 'f',
+ '-printf', '%P\n',
+ check: false)
+ foreach spa_header : spa_headers.stdout().split('\n')
+ if spa_header.endswith('.h') # skip empty lines
+ ext = have_cpp ? 'cpp' : 'c'
+ src = configure_file(input: 'spa-include-test-template.c',
+ output: 'spa-include-test-@0@.@1@'.format(spa_header.underscorify(), ext),
+ configuration: {
+ 'INCLUDE': spa_header,
+ })
+ executable('spa-include-test-@0@'.format(spa_header.underscorify()),
+ src,
+ dependencies: [ spa_dep ],
+ install: false)
+ endif
+ endforeach
+endif
+
+benchmark_apps = [
+ 'stress-ringbuffer',
+ 'benchmark-pod',
+ 'benchmark-dict',
+]
+
+foreach a : benchmark_apps
+ benchmark('spa-' + a,
+ executable('spa-' + a, a + '.c',
+ dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib ],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir,
+ ),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ ]
+ )
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'spa-' + a)
+ configure_file(
+ input: installed_tests_template,
+ output: 'spa-' + a + '.test',
+ install_dir: installed_tests_metadir,
+ configuration: test_conf,
+ )
+ endif
+endforeach
diff --git a/spa/tests/spa-include-test-template.c b/spa/tests/spa-include-test-template.c
new file mode 100644
index 0000000..078e897
--- /dev/null
+++ b/spa/tests/spa-include-test-template.c
@@ -0,0 +1,5 @@
+#include <@INCLUDE@>
+
+int main(void) {
+ return 0;
+}
diff --git a/spa/tests/stress-ringbuffer.c b/spa/tests/stress-ringbuffer.c
new file mode 100644
index 0000000..6a7e98f
--- /dev/null
+++ b/spa/tests/stress-ringbuffer.c
@@ -0,0 +1,147 @@
+#include <unistd.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <sched.h>
+#include <errno.h>
+#include <semaphore.h>
+
+#include <spa/utils/ringbuffer.h>
+
+#define DEFAULT_SIZE 0x2000
+#define ARRAY_SIZE 63
+#define MAX_VALUE 0x10000
+
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#include <sys/param.h>
+#if (__FreeBSD_version >= 1400000 && __FreeBSD_version < 1400043) \
+ || (__FreeBSD_version < 1300523) || defined(__MidnightBSD__)
+static int sched_getcpu(void) { return -1; };
+#endif
+#endif
+
+static struct spa_ringbuffer rb;
+static uint32_t size;
+static void *data;
+static sem_t sem;
+
+static int fill_int_array(int *array, int start, int count)
+{
+ int i, j = start;
+ for (i = 0; i < count; i++) {
+ array[i] = j;
+ j = (j + 1) % MAX_VALUE;
+ }
+ return j;
+}
+
+static int cmp_array(int *array1, int *array2, int count)
+{
+ int i;
+ for (i = 0; i < count; i++)
+ if (array1[i] != array2[i]) {
+ printf("%d != %d at offset %d\n", array1[i], array2[i], i);
+ return 0;
+ }
+
+ return 1;
+}
+
+static void *reader_start(void *arg)
+{
+ int i = 0, a[ARRAY_SIZE], b[ARRAY_SIZE];
+
+ printf("reader started on cpu: %d\n", sched_getcpu());
+
+ i = fill_int_array(a, i, ARRAY_SIZE);
+
+ while (1) {
+ uint32_t index;
+ int32_t avail;
+
+ avail = spa_ringbuffer_get_read_index(&rb, &index);
+
+ if (avail >= (int32_t)(sizeof(b))) {
+ spa_ringbuffer_read_data(&rb, data, size, index % size, b, sizeof(b));
+ spa_ringbuffer_read_update(&rb, index + sizeof(b));
+
+ if (index >= INT32_MAX - sizeof(a))
+ break;
+
+ spa_assert(cmp_array(a, b, ARRAY_SIZE));
+ i = fill_int_array(a, i, ARRAY_SIZE);
+ }
+ }
+ sem_post(&sem);
+
+ return NULL;
+}
+
+static void *writer_start(void *arg)
+{
+ int i = 0, a[ARRAY_SIZE];
+ printf("writer started on cpu: %d\n", sched_getcpu());
+
+ i = fill_int_array(a, i, ARRAY_SIZE);
+
+ while (1) {
+ uint32_t index;
+ int32_t avail;
+
+ avail = size - spa_ringbuffer_get_write_index(&rb, &index);
+
+ if (avail >= (int32_t)(sizeof(a))) {
+ spa_ringbuffer_write_data(&rb, data, size, index % size, a, sizeof(a));
+ spa_ringbuffer_write_update(&rb, index + sizeof(a));
+
+ if (index >= INT32_MAX - sizeof(a))
+ break;
+
+ i = fill_int_array(a, i, ARRAY_SIZE);
+ }
+ }
+ sem_post(&sem);
+
+ return NULL;
+}
+
+#define exit_error(msg) \
+do { perror(msg); exit(EXIT_FAILURE); } while (0)
+
+int main(int argc, char *argv[])
+{
+ pthread_t reader_thread, writer_thread;
+ struct timespec ts;
+
+ printf("starting ringbuffer stress test\n");
+
+ if (argc > 1)
+ sscanf(argv[1], "%d", &size);
+ else
+ size = DEFAULT_SIZE;
+
+ printf("buffer size (bytes): %d\n", size);
+ printf("array size (bytes): %zd\n", sizeof(int) * ARRAY_SIZE);
+
+ spa_ringbuffer_init(&rb);
+ data = malloc(size);
+
+ if (sem_init(&sem, 0, 0) != 0)
+ exit_error("init_sem");
+
+ pthread_create(&reader_thread, NULL, reader_start, NULL);
+ pthread_create(&writer_thread, NULL, writer_start, NULL);
+
+ if (clock_gettime(CLOCK_REALTIME, &ts) != 0)
+ exit_error("clock_gettime");
+
+ ts.tv_sec += 2;
+
+ while (sem_timedwait(&sem, &ts) == -1 && errno == EINTR)
+ continue;
+ while (sem_timedwait(&sem, &ts) == -1 && errno == EINTR)
+ continue;
+
+ printf("read %u, written %u\n", rb.readindex, rb.writeindex);
+
+ return 0;
+}
diff --git a/spa/tools/meson.build b/spa/tools/meson.build
new file mode 100644
index 0000000..6f12e9c
--- /dev/null
+++ b/spa/tools/meson.build
@@ -0,0 +1,11 @@
+executable('spa-inspect', 'spa-inspect.c',
+ dependencies : [ spa_dep, dl_lib ],
+ install : true)
+
+executable('spa-monitor', 'spa-monitor.c',
+ dependencies : [ spa_dep, dl_lib ],
+ install : true)
+
+executable('spa-json-dump', 'spa-json-dump.c',
+ dependencies : [ spa_dep, dl_lib, ],
+ install : true)
diff --git a/spa/tools/spa-inspect.c b/spa/tools/spa-inspect.c
new file mode 100644
index 0000000..4e79104
--- /dev/null
+++ b/spa/tools/spa-inspect.c
@@ -0,0 +1,319 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <dlfcn.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log-impl.h>
+#include <spa/support/loop.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/pod/parser.h>
+#include <spa/param/param.h>
+#include <spa/param/format.h>
+#include <spa/debug/dict.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/format.h>
+#include <spa/debug/types.h>
+
+static SPA_LOG_IMPL(default_log);
+
+struct data {
+ struct spa_support support[4];
+ uint32_t n_support;
+ struct spa_log *log;
+ struct spa_loop loop;
+ struct spa_node *node;
+ struct spa_hook listener;
+};
+
+static void print_param(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ switch (type) {
+ case SPA_RESULT_TYPE_NODE_PARAMS:
+ {
+ const struct spa_result_node_params *r = result;
+
+ if (spa_pod_is_object_type(r->param, SPA_TYPE_OBJECT_Format))
+ spa_debug_format(16, NULL, r->param);
+ else
+ spa_debug_pod(16, NULL, r->param);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void
+inspect_node_params(struct data *data, struct spa_node *node,
+ uint32_t n_params, struct spa_param_info *params)
+{
+ int res;
+ uint32_t i;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .result = print_param,
+ };
+
+ for (i = 0; i < n_params; i++) {
+ printf("enumerating: %s:\n", spa_debug_type_find_name(spa_type_param, params[i].id));
+
+ if (!SPA_FLAG_IS_SET(params[i].flags, SPA_PARAM_INFO_READ))
+ continue;
+
+ spa_zero(listener);
+ spa_node_add_listener(node, &listener, &node_events, data);
+ res = spa_node_enum_params(node, 0, params[i].id, 0, UINT32_MAX, NULL);
+ spa_hook_remove(&listener);
+
+ if (res != 0) {
+ printf("error enum_params %d: %s", params[i].id, spa_strerror(res));
+ break;
+ }
+ }
+}
+
+static void
+inspect_port_params(struct data *data, struct spa_node *node,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t n_params, struct spa_param_info *params)
+{
+ int res;
+ uint32_t i;
+ struct spa_hook listener;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .result = print_param,
+ };
+
+ for (i = 0; i < n_params; i++) {
+ printf("param: %s: flags %c%c\n",
+ spa_debug_type_find_name(spa_type_param, params[i].id),
+ params[i].flags & SPA_PARAM_INFO_READ ? 'r' : '-',
+ params[i].flags & SPA_PARAM_INFO_WRITE ? 'w' : '-');
+
+ if (!SPA_FLAG_IS_SET(params[i].flags, SPA_PARAM_INFO_READ))
+ continue;
+
+ printf("values:\n");
+ spa_zero(listener);
+ spa_node_add_listener(node, &listener, &node_events, data);
+ res = spa_node_port_enum_params(node, 0,
+ direction, port_id,
+ params[i].id, 0, UINT32_MAX,
+ NULL);
+ spa_hook_remove(&listener);
+
+ if (res != 0) {
+ printf("error port_enum_params %d: %s", params[i].id, spa_strerror(res));
+ break;
+ }
+ }
+}
+
+static void node_info(void *_data, const struct spa_node_info *info)
+{
+ struct data *data = _data;
+
+ printf("node info: %08"PRIx64"\n", info->change_mask);
+ printf("max input ports: %u\n", info->max_input_ports);
+ printf("max output ports: %u\n", info->max_output_ports);
+
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PROPS) {
+ printf("node properties:\n");
+ spa_debug_dict(2, info->props);
+ }
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ inspect_node_params(data, data->node, info->n_params, info->params);
+ }
+}
+
+static void node_port_info(void *_data, enum spa_direction direction, uint32_t id,
+ const struct spa_port_info *info)
+{
+ struct data *data = _data;
+
+ printf(" %s port: %08x",
+ direction == SPA_DIRECTION_INPUT ? "input" : "output",
+ id);
+
+ if (info == NULL) {
+ printf(" removed\n");
+ }
+ else {
+ printf(" info:\n");
+ if (info->change_mask & SPA_PORT_CHANGE_MASK_PROPS) {
+ printf("port properties:\n");
+ spa_debug_dict(2, info->props);
+ }
+ if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ inspect_port_params(data, data->node, direction, id,
+ info->n_params, info->params);
+ }
+ }
+}
+
+static const struct spa_node_events node_events =
+{
+ SPA_VERSION_NODE_EVENTS,
+ .info = node_info,
+ .port_info = node_port_info,
+};
+
+static void inspect_node(struct data *data, struct spa_node *node)
+{
+ data->node = node;
+ spa_node_add_listener(node, &data->listener, &node_events, data);
+ spa_hook_remove(&data->listener);
+}
+
+static void inspect_factory(struct data *data, const struct spa_handle_factory *factory)
+{
+ int res;
+ struct spa_handle *handle;
+ void *interface;
+ const struct spa_interface_info *info;
+ uint32_t index;
+
+ printf("factory version:\t\t%d\n", factory->version);
+ printf("factory name:\t\t'%s'\n", factory->name);
+ if (factory->version < 1) {
+ printf("\tno further info for version %d < 1\n", factory->version);
+ return;
+ }
+
+ printf("factory info:\n");
+ if (factory->info)
+ spa_debug_dict(2, factory->info);
+ else
+ printf(" none\n");
+
+ printf("factory interfaces:\n");
+ for (index = 0;;) {
+ if ((res = spa_handle_factory_enum_interface_info(factory, &info, &index)) <= 0) {
+ if (res != 0)
+ printf("error spa_handle_factory_enum_interface_info: %s",
+ spa_strerror(res));
+ break;
+ }
+ printf(" interface: '%s'\n", info->type);
+ }
+
+ 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);
+ goto out;
+ }
+
+ printf("factory instance:\n");
+
+ for (index = 0;;) {
+ if ((res = spa_handle_factory_enum_interface_info(factory, &info, &index)) <= 0) {
+ if (res != 0)
+ printf("error spa_handle_factory_enum_interface_info: %s",
+ spa_strerror(res));
+ break;
+ }
+ printf(" interface: '%s'\n", info->type);
+
+ if ((res = spa_handle_get_interface(handle, info->type, &interface)) < 0) {
+ printf("can't get interface: %s: %d\n", info->type, res);
+ continue;
+ }
+
+ if (spa_streq(info->type, SPA_TYPE_INTERFACE_Node))
+ inspect_node(data, interface);
+ else
+ printf("skipping unknown interface\n");
+ }
+
+ if ((res = spa_handle_clear(handle)) < 0)
+ printf("failed to clear handle: %s\n", spa_strerror(res));
+
+out:
+ free(handle);
+}
+
+static const struct spa_loop_methods impl_loop = {
+ SPA_VERSION_LOOP_METHODS,
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ int res;
+ void *handle;
+ spa_handle_factory_enum_func_t enum_func;
+ uint32_t index;
+ const char *str;
+
+ if (argc < 2) {
+ printf("usage: %s <plugin.so>\n", argv[0]);
+ return -1;
+ }
+
+ data.log = &default_log.log;
+ data.loop.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Loop,
+ SPA_VERSION_LOOP,
+ &impl_loop, &data);
+
+ if ((str = getenv("SPA_DEBUG")))
+ data.log->level = atoi(str);
+
+ data.support[0] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data.log);
+ data.support[1] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, &data.loop);
+ data.support[2] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, &data.loop);
+ data.n_support = 3;
+
+ if ((handle = dlopen(argv[1], RTLD_NOW)) == NULL) {
+ printf("can't load %s\n", argv[1]);
+ return -1;
+ }
+ if ((enum_func = dlsym(handle, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ printf("can't find function\n");
+ return -1;
+ }
+
+ for (index = 0;;) {
+ const struct spa_handle_factory *factory;
+
+ if ((res = enum_func(&factory, &index)) <= 0) {
+ if (res != 0)
+ printf("error enum_func: %s", spa_strerror(res));
+ break;
+ }
+ inspect_factory(&data, factory);
+ }
+ return 0;
+}
diff --git a/spa/tools/spa-json-dump.c b/spa/tools/spa-json-dump.c
new file mode 100644
index 0000000..ae0c67d
--- /dev/null
+++ b/spa/tools/spa-json-dump.c
@@ -0,0 +1,165 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/json.h>
+
+static void encode_string(FILE *f, const char *val, int len)
+{
+ int i;
+ fprintf(f, "\"");
+ for (i = 0; i < len; i++) {
+ char v = val[i];
+ switch (v) {
+ case '\n':
+ fprintf(f, "\\n");
+ break;
+ case '\r':
+ fprintf(f, "\\r");
+ break;
+ case '\b':
+ fprintf(f, "\\b");
+ break;
+ case '\t':
+ fprintf(f, "\\t");
+ break;
+ case '\f':
+ fprintf(f, "\\f");
+ break;
+ case '\\': case '"':
+ fprintf(f, "\\%c", v);
+ break;
+ default:
+ if (v > 0 && v < 0x20)
+ fprintf(f, "\\u%04x", v);
+ else
+ fprintf(f, "%c", v);
+ break;
+ }
+ }
+ fprintf(f, "\"");
+}
+
+static int dump(FILE *file, int indent, struct spa_json *it, const char *value, int len)
+{
+ struct spa_json sub;
+ int count = 0;
+ char key[1024];
+
+ if (spa_json_is_array(value, len)) {
+ fprintf(file, "[");
+ spa_json_enter(it, &sub);
+ while ((len = spa_json_next(&sub, &value)) > 0) {
+ fprintf(file, "%s\n%*s", count++ > 0 ? "," : "",
+ indent+2, "");
+ dump(file, indent+2, &sub, value, len);
+ }
+ fprintf(file, "%s%*s]", count > 0 ? "\n" : "",
+ count > 0 ? indent : 0, "");
+ } else if (spa_json_is_object(value, len)) {
+ fprintf(file, "{");
+ spa_json_enter(it, &sub);
+ while (spa_json_get_string(&sub, key, sizeof(key)) > 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;
+ dump(file, indent+2, &sub, value, len);
+ }
+ fprintf(file, "%s%*s}", count > 0 ? "\n" : "",
+ count > 0 ? indent : 0, "");
+ } else if (spa_json_is_string(value, len) ||
+ spa_json_is_null(value, len) ||
+ spa_json_is_bool(value, len) ||
+ spa_json_is_int(value, len) ||
+ spa_json_is_float(value, len)) {
+ fprintf(file, "%.*s", len, value);
+ } else {
+ encode_string(file, value, len);
+ }
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ int fd, len, res, exit_code = EXIT_FAILURE;
+ void *data;
+ struct stat sbuf;
+ struct spa_json it;
+ const char *value;
+
+ if (argc < 2) {
+ fprintf(stderr, "usage: %s <spa-json-file>\n", argv[0]);
+ goto error;
+ }
+ if ((fd = open(argv[1], O_CLOEXEC | O_RDONLY)) < 0) {
+ fprintf(stderr, "error opening file '%s': %m\n", argv[1]);
+ goto error;
+ }
+ if (fstat(fd, &sbuf) < 0) {
+ fprintf(stderr, "error statting file '%s': %m\n", argv[1]);
+ goto error_close;
+ }
+ if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) {
+ fprintf(stderr, "error mmapping file '%s': %m\n", argv[1]);
+ goto error_close;
+ }
+
+ spa_json_init(&it, data, sbuf.st_size);
+ if ((len = spa_json_next(&it, &value)) <= 0) {
+ fprintf(stderr, "not a valid file '%s': %s\n", argv[1], spa_strerror(len));
+ goto error_unmap;
+ }
+ if (!spa_json_is_container(value, len)) {
+ spa_json_init(&it, data, sbuf.st_size);
+ value = "{";
+ len = 1;
+ }
+ if ((res = dump(stdout, 0, &it, value, len)) < 0) {
+ fprintf(stderr, "error parsing file '%s': %s\n", argv[1], spa_strerror(res));
+ goto error_unmap;
+ }
+ fprintf(stdout, "\n");
+ exit_code = EXIT_SUCCESS;
+
+error_unmap:
+ munmap(data, sbuf.st_size);
+error_close:
+ close(fd);
+error:
+ return exit_code;
+}
diff --git a/spa/tools/spa-monitor.c b/spa/tools/spa-monitor.c
new file mode 100644
index 0000000..28159f3
--- /dev/null
+++ b/spa/tools/spa-monitor.c
@@ -0,0 +1,229 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <poll.h>
+
+#include <spa/utils/string.h>
+#include <spa/support/log-impl.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+
+#include <spa/debug/dict.h>
+#include <spa/debug/pod.h>
+
+static SPA_LOG_IMPL(default_log);
+
+struct data {
+ struct spa_log *log;
+ struct spa_loop main_loop;
+
+ struct spa_support support[3];
+ uint32_t n_support;
+
+ unsigned int n_sources;
+ struct spa_source sources[16];
+
+ bool rebuild_fds;
+ struct pollfd fds[16];
+ unsigned int n_fds;
+};
+
+
+static void inspect_info(struct data *data, const struct spa_device_object_info *info)
+{
+ spa_debug_dict(0, info->props);
+}
+
+static void on_device_info(void *_data, const struct spa_device_info *info)
+{
+ spa_debug_dict(0, info->props);
+}
+
+static void on_device_object_info(void *_data, uint32_t id, const struct spa_device_object_info *info)
+{
+ struct data *data = _data;
+
+ if (info == NULL) {
+ fprintf(stderr, "removed: %u\n", id);
+ }
+ else {
+ fprintf(stderr, "added/changed: %u\n", id);
+ inspect_info(data, info);
+ }
+}
+
+static int do_add_source(void *object, struct spa_source *source)
+{
+ struct data *data = object;
+
+ data->sources[data->n_sources] = *source;
+ data->n_sources++;
+ data->rebuild_fds = true;
+
+ return 0;
+}
+
+static const struct spa_loop_methods impl_loop = {
+ SPA_VERSION_LOOP_METHODS,
+ .add_source = do_add_source,
+};
+
+static const struct spa_device_events impl_device_events = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .info = on_device_info,
+ .object_info = on_device_object_info,
+};
+
+static void handle_device(struct data *data, struct spa_device *device)
+{
+ struct spa_hook listener;
+
+ spa_zero(listener);
+ spa_device_add_listener(device, &listener, &impl_device_events, data);
+
+ while (true) {
+ int r;
+ uint32_t i;
+
+ /* rebuild */
+ if (data->rebuild_fds) {
+ for (i = 0; i < data->n_sources; i++) {
+ struct spa_source *p = &data->sources[i];
+ data->fds[i].fd = p->fd;
+ data->fds[i].events = p->mask;
+ }
+ data->n_fds = data->n_sources;
+ data->rebuild_fds = false;
+ }
+
+ r = poll((struct pollfd *) data->fds, data->n_fds, -1);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ break;
+ }
+ if (r == 0) {
+ fprintf(stderr, "device %p: select timeout", device);
+ break;
+ }
+
+ /* after */
+ for (i = 0; i < data->n_sources; i++) {
+ struct spa_source *p = &data->sources[i];
+ p->func(p);
+ }
+ }
+ spa_hook_remove(&listener);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ int res;
+ void *handle;
+ spa_handle_factory_enum_func_t enum_func;
+ uint32_t fidx;
+
+ data.log = &default_log.log;
+ data.main_loop.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Loop,
+ SPA_VERSION_LOOP,
+ &impl_loop, &data);
+
+ data.support[0] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data.log);
+ data.support[1] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, &data.main_loop);
+ data.n_support = 2;
+
+ if (argc < 2) {
+ printf("usage: %s <plugin.so>\n", argv[0]);
+ return -1;
+ }
+
+ if ((handle = dlopen(argv[1], RTLD_NOW)) == NULL) {
+ printf("can't load %s\n", argv[1]);
+ return -1;
+ }
+ if ((enum_func = dlsym(handle, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ printf("can't find function\n");
+ return -1;
+ }
+
+ for (fidx = 0;;) {
+ const struct spa_handle_factory *factory;
+ uint32_t iidx;
+
+ if ((res = enum_func(&factory, &fidx)) <= 0) {
+ if (res != 0)
+ printf("can't enumerate factories: %d\n", res);
+ break;
+ }
+
+ if (factory->version < 1) {
+ printf("factories version %d < %d not supported\n",
+ factory->version, 1);
+ continue;
+ }
+
+ for (iidx = 0;;) {
+ const struct spa_interface_info *info;
+
+ if ((res =
+ spa_handle_factory_enum_interface_info(factory, &info, &iidx)) <= 0) {
+ if (res != 0)
+ printf("can't enumerate interfaces: %d\n", res);
+ break;
+ }
+
+ if (spa_streq(info->type, SPA_TYPE_INTERFACE_Device)) {
+ struct spa_handle *handle;
+ void *interface;
+
+ 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: %s\n", strerror(res));
+ continue;
+ }
+
+ if ((res =
+ spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Device,
+ &interface)) < 0) {
+ printf("can't get interface: %s\n", strerror(res));
+ continue;
+ }
+ handle_device(&data, interface);
+ }
+ }
+ }
+
+ return 0;
+}
diff --git a/src/daemon/client-rt.conf.in b/src/daemon/client-rt.conf.in
new file mode 100644
index 0000000..b73a904
--- /dev/null
+++ b/src/daemon/client-rt.conf.in
@@ -0,0 +1,114 @@
+# 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 ] ]
+ #}
+ #
+ # Loads a module with the given parameters.
+ # If ifexists is given, the module is ignored when it is not found.
+ # If nofail is given, module initialization failures are ignored.
+ #
+ # Uses realtime scheduling to boost the audio thread priorities
+ { name = libpipewire-module-rt
+ args = {
+ #rt.prio = 88
+ #rt.time.soft = -1
+ #rt.time.hard = -1
+ }
+ flags = [ ifexists nofail ]
+ }
+
+ # The native communication protocol.
+ { name = libpipewire-module-protocol-native }
+
+ # Allows creating nodes that run in the context of the
+ # client. Is used by all clients that want to provide
+ # data to PipeWire.
+ { name = libpipewire-module-client-node }
+
+ # 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
+}
+
+alsa.properties = {
+ #alsa.format = 0
+ #alsa.rate = 0
+ #alsa.channels = 0
+ #alsa.period-bytes = 0
+ #alsa.buffer-bytes = 0
+ #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
new file mode 100644
index 0000000..b465eb6
--- /dev/null
+++ b/src/daemon/client.conf.in
@@ -0,0 +1,85 @@
+# 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.conf.d/ for system-wide changes or in
+# ~/.config/pipewire/client.conf.d/ for local changes.
+#
+
+context.properties = {
+ ## Configure properties in the system.
+ #mem.warn-mlock = false
+ #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 ] ]
+ #}
+ #
+ # 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.
+ #
+
+ # 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
+}
diff --git a/src/daemon/filter-chain.conf.in b/src/daemon/filter-chain.conf.in
new file mode 100644
index 0000000..28d2a5a
--- /dev/null
+++ b/src/daemon/filter-chain.conf.in
@@ -0,0 +1,62 @@
+# Filter-chain config file for PipeWire version @VERSION@ #
+#
+# This is a base config file for running filters.
+#
+# Place filter fragments in @PIPEWIRE_CONFIG_DIR@/filter-chain.conf.d/
+# for system-wide changes or in ~/.config/pipewire/filter-chain.conf.d/
+# for local changes.
+#
+# Run the filters with pipewire -c filter-chain.conf
+#
+
+context.properties = {
+ ## Configure properties in the system.
+ #mem.warn-mlock = false
+ #mem.allow-mlock = true
+ #mem.mlock-all = false
+ log.level = 0
+}
+
+context.spa-libs = {
+ #<factory-name regex> = <library-name>
+ #
+ # Used to find spa factory names. It maps an spa factory name
+ # regular expression to a library name that should contain
+ # that factory.
+ #
+ audio.convert.* = audioconvert/libspa-audioconvert
+ support.* = support/libspa-support
+}
+
+context.modules = [
+ #{ name = <module-name>
+ # [ args = { <key> = <value> ... } ]
+ # [ flags = [ [ ifexists ] [ nofail ] ]
+ #}
+ #
+ # Loads a module with the given parameters.
+ # If ifexists is given, the module is ignored when it is not found.
+ # If nofail is given, module initialization failures are ignored.
+ #
+ # Uses realtime scheduling to boost the audio thread priorities
+ { name = libpipewire-module-rt
+ args = {
+ #rt.prio = 88
+ #rt.time.soft = -1
+ #rt.time.hard = -1
+ }
+ flags = [ ifexists nofail ]
+ }
+
+ # The native communication protocol.
+ { name = libpipewire-module-protocol-native }
+
+ # Allows creating nodes that run in the context of the
+ # client. Is used by all clients that want to provide
+ # data to PipeWire.
+ { name = libpipewire-module-client-node }
+
+ # Makes a factory for wrapping nodes in an adapter with a
+ # converter and resampler.
+ { name = libpipewire-module-adapter }
+]
diff --git a/src/daemon/filter-chain/demonic.conf b/src/daemon/filter-chain/demonic.conf
new file mode 100644
index 0000000..df52dba
--- /dev/null
+++ b/src/daemon/filter-chain/demonic.conf
@@ -0,0 +1,63 @@
+# filter-chain example config file for PipeWire version @VERSION@ #
+#
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
+#
+context.modules = [
+ { name = libpipewire-module-filter-chain
+ args = {
+ #audio.format = F32
+ #audio.rate = 48000
+ audio.channels = 2
+ audio.position = [ FL FR ]
+ node.description = "Demonic example"
+ media.name = "Demonic example"
+ filter.graph = {
+ nodes = [
+ {
+ name = rev
+ type = ladspa
+ plugin = revdelay_1605
+ label = revdelay
+ control = {
+ "Delay Time (s)" = 2.0
+ }
+ }
+ {
+ name = pitch
+ type = ladspa
+ plugin = am_pitchshift_1433
+ label = amPitchshift
+ control = {
+ "Pitch shift" = 0.6
+ }
+ }
+ {
+ name = rev2
+ type = ladspa
+ plugin = g2reverb
+ label = G2reverb
+ control = {
+ "Reverb tail" = 0.5
+ "Damping" = 0.9
+ }
+ }
+ ]
+ links = [
+ { output = "rev:Output" input = "pitch:Input" }
+ { output = "pitch:Output" input = "rev2:In L" }
+ ]
+ inputs = [ "rev:Input" ]
+ outputs = [ "rev2:Out L" ]
+ }
+ capture.props = {
+ node.name = "effect_input.filter-chain-demonic"
+ #media.class = Audio/Sink
+ }
+ playback.props = {
+ node.name = "effect_output.filter-chain-demonic"
+ #media.class = Audio/Source
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/meson.build b/src/daemon/filter-chain/meson.build
new file mode 100644
index 0000000..91d4458
--- /dev/null
+++ b/src/daemon/filter-chain/meson.build
@@ -0,0 +1,19 @@
+conf_files = [
+ [ 'demonic.conf', 'demonic.conf' ],
+ [ 'source-duplicate-FL.conf', 'source-duplicate-FL.conf' ],
+ [ 'sink-mix-FL-FR.conf', 'sink-mix-FL-FR.conf' ],
+ [ 'sink-make-LFE.conf', 'sink-make-LFE.conf' ],
+ [ 'sink-virtual-surround-5.1-kemar.conf', 'sink-virtual-surround-5.1-kemar.conf' ],
+ [ 'sink-virtual-surround-7.1-hesuvi.conf', 'sink-virtual-surround-7.1-hesuvi.conf' ],
+ [ 'sink-dolby-surround.conf', 'sink-dolby-surround.conf' ],
+ [ 'sink-eq6.conf', 'sink-eq6.conf' ],
+ [ 'sink-matrix-spatialiser.conf', 'sink-matrix-spatialiser.conf' ],
+ [ 'source-rnnoise.conf', 'source-rnnoise.conf' ],
+]
+
+foreach c : conf_files
+ configure_file(input : c.get(0),
+ output : c.get(1),
+ configuration : conf_config,
+ install_dir : pipewire_confdatadir / 'filter-chain')
+endforeach
diff --git a/src/daemon/filter-chain/sink-dolby-surround.conf b/src/daemon/filter-chain/sink-dolby-surround.conf
new file mode 100644
index 0000000..a53009f
--- /dev/null
+++ b/src/daemon/filter-chain/sink-dolby-surround.conf
@@ -0,0 +1,46 @@
+# Dolby Surround encoder 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 = "Dolby Surround Sink"
+ media.name = "Dolby Surround Sink"
+ filter.graph = {
+ nodes = [
+ {
+ type = builtin
+ name = mixer
+ label = mixer
+ control = { "Gain 1" = 0.5 "Gain 2" = 0.5 }
+ }
+ {
+ type = ladspa
+ name = enc
+ plugin = surround_encoder_1401
+ label = surroundEncoder
+ }
+ ]
+ links = [
+ { output = "mixer:Out" input = "enc:S" }
+ ]
+ inputs = [ "enc:L" "enc:R" "enc:C" null "mixer:In 1" "mixer:In 2" ]
+ outputs = [ "enc:Lt" "enc:Rt" ]
+ }
+ capture.props = {
+ node.name = "effect_input.dolby_surround"
+ media.class = Audio/Sink
+ audio.channels = 6
+ audio.position = [ FL FR FC LFE SL SR ]
+ }
+ playback.props = {
+ node.name = "effect_output.dolby_surround"
+ node.passive = true
+ audio.channels = 2
+ audio.position = [ FL FR ]
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/sink-eq6.conf b/src/daemon/filter-chain/sink-eq6.conf
new file mode 100644
index 0000000..4cdf21b
--- /dev/null
+++ b/src/daemon/filter-chain/sink-eq6.conf
@@ -0,0 +1,70 @@
+# 6 band sink equalizer
+#
+# 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 = "Equalizer Sink"
+ media.name = "Equalizer Sink"
+ filter.graph = {
+ nodes = [
+ {
+ type = builtin
+ name = eq_band_1
+ label = bq_lowshelf
+ control = { "Freq" = 100.0 "Q" = 1.0 "Gain" = 0.0 }
+ }
+ {
+ type = builtin
+ name = eq_band_2
+ label = bq_peaking
+ control = { "Freq" = 100.0 "Q" = 1.0 "Gain" = 0.0 }
+ }
+ {
+ type = builtin
+ name = eq_band_3
+ label = bq_peaking
+ control = { "Freq" = 500.0 "Q" = 1.0 "Gain" = 0.0 }
+ }
+ {
+ type = builtin
+ name = eq_band_4
+ label = bq_peaking
+ control = { "Freq" = 2000.0 "Q" = 1.0 "Gain" = 0.0 }
+ }
+ {
+ type = builtin
+ name = eq_band_5
+ label = bq_peaking
+ control = { "Freq" = 5000.0 "Q" = 1.0 "Gain" = 0.0 }
+ }
+ {
+ type = builtin
+ name = eq_band_6
+ label = bq_highshelf
+ control = { "Freq" = 5000.0 "Q" = 1.0 "Gain" = 0.0 }
+ }
+ ]
+ links = [
+ { output = "eq_band_1:Out" input = "eq_band_2:In" }
+ { output = "eq_band_2:Out" input = "eq_band_3:In" }
+ { output = "eq_band_3:Out" input = "eq_band_4:In" }
+ { output = "eq_band_4:Out" input = "eq_band_5:In" }
+ { output = "eq_band_5:Out" input = "eq_band_6:In" }
+ ]
+ }
+ audio.channels = 2
+ audio.position = [ FL FR ]
+ capture.props = {
+ node.name = "effect_input.eq6"
+ media.class = Audio/Sink
+ }
+ playback.props = {
+ node.name = "effect_output.eq6"
+ node.passive = true
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/sink-make-LFE.conf b/src/daemon/filter-chain/sink-make-LFE.conf
new file mode 100644
index 0000000..4ab770e
--- /dev/null
+++ b/src/daemon/filter-chain/sink-make-LFE.conf
@@ -0,0 +1,56 @@
+# An example filter chain that makes a stereo sink that mixes
+# the FL and FR channels to FL, FR, LFE
+#
+# Copy this file into a conf.d/ directory
+#
+context.modules = [
+ { name = libpipewire-module-filter-chain
+ args = {
+ node.description = "LFE example"
+ media.name = "LFE example"
+ filter.graph = {
+ nodes = [
+ { name = copyIL type = builtin label = copy }
+ { name = copyOL type = builtin label = copy }
+ { name = copyIR type = builtin label = copy }
+ { name = copyOR type = builtin label = copy }
+ {
+ name = mix
+ type = builtin
+ label = mixer
+ control = {
+ "Gain 1" = 0.5
+ "Gain 2" = 0.5
+ }
+ }
+ {
+ type = builtin
+ name = lpLFE
+ label = bq_lowpass
+ control = { "Freq" = 150.0 }
+ }
+ ]
+ links = [
+ { output = "copyIL:Out" input = "copyOL:In" }
+ { output = "copyIR:Out" input = "copyOR:In" }
+ { output = "copyIL:Out" input = "mix:In 1" }
+ { output = "copyIR:Out" input = "mix:In 2" }
+ { output = "mix:Out" input = "lpLFE:In" }
+ ]
+ inputs = [ "copyIL:In" "copyIR:In" ]
+ outputs = [ "copyOL:Out" "copyOR:Out" "lpLFE:Out"]
+ }
+ capture.props = {
+ node.name = "input_lfe"
+ audio.position = [ FL FR ]
+ media.class = "Audio/Sink"
+ }
+ playback.props = {
+ node.name = "output_lfe"
+ audio.position = [ FL FR LFE ]
+ stream.dont-remix = true
+ node.passive = true
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/sink-matrix-spatialiser.conf b/src/daemon/filter-chain/sink-matrix-spatialiser.conf
new file mode 100644
index 0000000..b39890c
--- /dev/null
+++ b/src/daemon/filter-chain/sink-matrix-spatialiser.conf
@@ -0,0 +1,41 @@
+# Matrix Spatialiser sink
+#
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
+#
+# ( Jean-Philippe Guillemin <hyp3ri0n@sfr.fr> )
+#
+
+context.modules = [
+ { name = libpipewire-module-filter-chain
+ args = {
+ node.description = "Matrix Spatialiser"
+ media.name = "Matrix Spatialiser"
+ filter.graph = {
+ nodes = [
+ {
+ type = ladspa
+ name = matrix
+ plugin = matrix_spatialiser_1422
+ label = matrixSpatialiser
+ control = {
+ "Width" = 80
+ }
+ }
+ ]
+ inputs = [ "matrix:Input L" "matrix:Input R" ]
+ outputs = [ "matrix:Output L" "matrix:Output R" ]
+ }
+ audio.channels = 2
+ audio.position = [ FL FR ]
+ capture.props = {
+ node.name = "effect_input.matrix_spatialiser"
+ media.class = Audio/Sink
+ }
+ playback.props = {
+ node.name = "effect_output.matrix_spatialiser"
+ node.passive = true
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/sink-mix-FL-FR.conf b/src/daemon/filter-chain/sink-mix-FL-FR.conf
new file mode 100644
index 0000000..8288fad
--- /dev/null
+++ b/src/daemon/filter-chain/sink-mix-FL-FR.conf
@@ -0,0 +1,40 @@
+# An example filter chain that makes a stereo sink that mixes
+# the FL and FR channels to a single FL channel
+#
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
+#
+context.modules = [
+ { name = libpipewire-module-filter-chain
+ args = {
+ node.description = "Mix example"
+ media.name = "Mix example"
+ filter.graph = {
+ nodes = [
+ {
+ name = mix
+ type = builtin
+ label = mixer
+ control = {
+ "Gain 1" = 0.5
+ "Gain 2" = 0.5
+ }
+ }
+ ]
+ inputs = [ "mix:In 1" "mix:In 2" ]
+ outputs = [ "mix:Out" ]
+ }
+ capture.props = {
+ node.name = "mix_input.mix-FL-FR-to-FL"
+ audio.position = [ FL FR ]
+ media.class = "Audio/Sink"
+ }
+ playback.props = {
+ node.name = "mix_output.mix-FL-FR-to-FL"
+ audio.position = [ FL ]
+ stream.dont-remix = true
+ node.passive = true
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf b/src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf
new file mode 100644
index 0000000..ee8929b
--- /dev/null
+++ b/src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf
@@ -0,0 +1,177 @@
+# Convolver 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 = "Virtual Surround Sink"
+ media.name = "Virtual Surround Sink"
+ filter.graph = {
+ nodes = [
+ {
+ type = builtin
+ label = convolver
+ name = convFL_L
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 0
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convFL_R
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 1
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convFR_L
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 1
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convFR_R
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 0
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convFC
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 2
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convLFE
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 3
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convSL_L
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 4
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convSL_R
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 5
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convSR_L
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 5
+ }
+ }
+ {
+ type = builtin
+ label = convolver
+ name = convSR_R
+ config = {
+ filename = "hrir_kemar/hrir-kemar.wav"
+ channel = 4
+ }
+ }
+ {
+ type = builtin
+ label = mixer
+ name = mixL
+ }
+ {
+ type = builtin
+ label = mixer
+ name = mixR
+ }
+ {
+ type = builtin
+ label = copy
+ name = copyFL
+ }
+ {
+ type = builtin
+ label = copy
+ name = copyFR
+ }
+ {
+ type = builtin
+ label = copy
+ name = copySL
+ }
+ {
+ type = builtin
+ label = copy
+ name = copySR
+ }
+ ]
+ links = [
+ { output = "copyFL:Out" input = "convFL_L:In" }
+ { output = "copyFL:Out" input = "convFL_R:In" }
+ { output = "copyFR:Out" input = "convFR_R:In" }
+ { output = "copyFR:Out" input = "convFR_L:In" }
+
+ { output = "copySL:Out" input = "convSL_L:In" }
+ { output = "copySL:Out" input = "convSL_R:In" }
+ { output = "copySR:Out" input = "convSR_R:In" }
+ { output = "copySR:Out" input = "convSR_L:In" }
+
+ { output = "convFL_L:Out" input = "mixL:In 1" }
+ { output = "convFR_L:Out" input = "mixL:In 2" }
+ { output = "convFC:Out" input = "mixL:In 3" }
+ { output = "convLFE:Out" input = "mixL:In 4" }
+ { output = "convSL_L:Out" input = "mixL:In 5" }
+ { output = "convSR_L:Out" input = "mixL:In 6" }
+
+ { output = "convFL_R:Out" input = "mixR:In 1" }
+ { output = "convFR_R:Out" input = "mixR:In 2" }
+ { output = "convFC:Out" input = "mixR:In 3" }
+ { output = "convLFE:Out" input = "mixR:In 4" }
+ { output = "convSL_R:Out" input = "mixR:In 5" }
+ { output = "convSR_R:Out" input = "mixR:In 6" }
+ ]
+ inputs = [ "copyFL:In" "copyFR:In" "convFC:In" "convLFE:In" "copySL:In" "copySR:In" ]
+ outputs = [ "mixL:Out" "mixR:Out" ]
+
+ }
+ capture.props = {
+ node.name = "effect_input.virtual-surround-5.1-kemar"
+ media.class = Audio/Sink
+ audio.channels = 6
+ audio.position = [ FL FR FC LFE SL SR]
+ }
+ playback.props = {
+ node.name = "effect_output.virtual-surround-5.1-kemar"
+ node.passive = true
+ audio.channels = 2
+ audio.position = [ FL FR ]
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf b/src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf
new file mode 100644
index 0000000..4aad310
--- /dev/null
+++ b/src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf
@@ -0,0 +1,101 @@
+# Convolver 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 = "Virtual Surround Sink"
+ media.name = "Virtual Surround Sink"
+ filter.graph = {
+ nodes = [
+ # duplicate inputs
+ { type = builtin label = copy name = copyFL }
+ { type = builtin label = copy name = copyFR }
+ { type = builtin label = copy name = copyFC }
+ { type = builtin label = copy name = copyRL }
+ { type = builtin label = copy name = copyRR }
+ { type = builtin label = copy name = copySL }
+ { type = builtin label = copy name = copySR }
+ { type = builtin label = copy name = copyLFE }
+
+ # apply hrir - HeSuVi 14-channel WAV (not the *-.wav variants) (note: */44/* in HeSuVi are the same, but resampled to 44100)
+ { type = builtin label = convolver name = convFL_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 0 } }
+ { type = builtin label = convolver name = convFL_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 1 } }
+ { type = builtin label = convolver name = convSL_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 2 } }
+ { type = builtin label = convolver name = convSL_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 3 } }
+ { type = builtin label = convolver name = convRL_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 4 } }
+ { type = builtin label = convolver name = convRL_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 5 } }
+ { type = builtin label = convolver name = convFC_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 6 } }
+ { type = builtin label = convolver name = convFR_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 7 } }
+ { type = builtin label = convolver name = convFR_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 8 } }
+ { type = builtin label = convolver name = convSR_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 9 } }
+ { type = builtin label = convolver name = convSR_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 10 } }
+ { type = builtin label = convolver name = convRR_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 11 } }
+ { type = builtin label = convolver name = convRR_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 12 } }
+ { type = builtin label = convolver name = convFC_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 13 } }
+
+ # treat LFE as FC
+ { type = builtin label = convolver name = convLFE_L config = { filename = "hrir_hesuvi/hrir.wav" channel = 6 } }
+ { type = builtin label = convolver name = convLFE_R config = { filename = "hrir_hesuvi/hrir.wav" channel = 13 } }
+
+ # stereo output
+ { type = builtin label = mixer name = mixL }
+ { type = builtin label = mixer name = mixR }
+ ]
+ links = [
+ # input
+ { output = "copyFL:Out" input="convFL_L:In" }
+ { output = "copyFL:Out" input="convFL_R:In" }
+ { output = "copySL:Out" input="convSL_L:In" }
+ { output = "copySL:Out" input="convSL_R:In" }
+ { output = "copyRL:Out" input="convRL_L:In" }
+ { output = "copyRL:Out" input="convRL_R:In" }
+ { output = "copyFC:Out" input="convFC_L:In" }
+ { output = "copyFR:Out" input="convFR_R:In" }
+ { output = "copyFR:Out" input="convFR_L:In" }
+ { output = "copySR:Out" input="convSR_R:In" }
+ { output = "copySR:Out" input="convSR_L:In" }
+ { output = "copyRR:Out" input="convRR_R:In" }
+ { output = "copyRR:Out" input="convRR_L:In" }
+ { output = "copyFC:Out" input="convFC_R:In" }
+ { output = "copyLFE:Out" input="convLFE_L:In" }
+ { output = "copyLFE:Out" input="convLFE_R:In" }
+
+ # output
+ { output = "convFL_L:Out" input="mixL:In 1" }
+ { output = "convFL_R:Out" input="mixR:In 1" }
+ { output = "convSL_L:Out" input="mixL:In 2" }
+ { output = "convSL_R:Out" input="mixR:In 2" }
+ { output = "convRL_L:Out" input="mixL:In 3" }
+ { output = "convRL_R:Out" input="mixR:In 3" }
+ { output = "convFC_L:Out" input="mixL:In 4" }
+ { output = "convFC_R:Out" input="mixR:In 4" }
+ { output = "convFR_R:Out" input="mixR:In 5" }
+ { output = "convFR_L:Out" input="mixL:In 5" }
+ { output = "convSR_R:Out" input="mixR:In 6" }
+ { output = "convSR_L:Out" input="mixL:In 6" }
+ { output = "convRR_R:Out" input="mixR:In 7" }
+ { output = "convRR_L:Out" input="mixL:In 7" }
+ { output = "convLFE_R:Out" input="mixR:In 8" }
+ { output = "convLFE_L:Out" input="mixL:In 8" }
+ ]
+ inputs = [ "copyFL:In" "copyFR:In" "copyFC:In" "copyLFE:In" "copyRL:In" "copyRR:In", "copySL:In", "copySR:In" ]
+ outputs = [ "mixL:Out" "mixR:Out" ]
+ }
+ capture.props = {
+ node.name = "effect_input.virtual-surround-7.1-hesuvi"
+ media.class = Audio/Sink
+ audio.channels = 8
+ audio.position = [ FL FR FC LFE RL RR SL SR ]
+ }
+ playback.props = {
+ node.name = "effect_output.virtual-surround-7.1-hesuvi"
+ node.passive = true
+ audio.channels = 2
+ audio.position = [ FL FR ]
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/source-duplicate-FL.conf b/src/daemon/filter-chain/source-duplicate-FL.conf
new file mode 100644
index 0000000..7e0158f
--- /dev/null
+++ b/src/daemon/filter-chain/source-duplicate-FL.conf
@@ -0,0 +1,52 @@
+# An example filter chain that makes a source that duplicates the FL channel
+# to FL and FR.
+#
+# Copy this file into a conf.d/ directory such as
+# ~/.config/pipewire/filter-chain.conf.d/
+#
+context.modules = [
+ { name = libpipewire-module-filter-chain
+ args = {
+ node.description = "Remap example"
+ media.name = "Remap example"
+ filter.graph = {
+ nodes = [
+ {
+ name = copyIL
+ type = builtin
+ label = copy
+ }
+ {
+ name = copyOL
+ type = builtin
+ label = copy
+ }
+ {
+ name = copyOR
+ type = builtin
+ label = copy
+ }
+ ]
+ links = [
+ # we can only tee from nodes, not inputs so we need
+ # to copy the inputs and then tee.
+ { output = "copyIL:Out" input = "copyOL:In" }
+ { output = "copyIL:Out" input = "copyOR:In" }
+ ]
+ inputs = [ "copyIL:In" ]
+ outputs = [ "copyOL:Out" "copyOR:Out" ]
+ }
+ capture.props = {
+ node.name = "remap_input.remap-FL-to-FL-FR"
+ audio.position = [ FL ]
+ stream.dont-remix = true
+ node.passive = true
+ }
+ playback.props = {
+ node.name = "remap_output.remap-FL-to-FL-FR"
+ audio.position = [ FL FR ]
+ media.class = "Audio/Source"
+ }
+ }
+ }
+]
diff --git a/src/daemon/filter-chain/source-rnnoise.conf b/src/daemon/filter-chain/source-rnnoise.conf
new file mode 100644
index 0000000..5babd81
--- /dev/null
+++ b/src/daemon/filter-chain/source-rnnoise.conf
@@ -0,0 +1,35 @@
+# Noise canceling source
+#
+# 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 = "Noise Canceling source"
+ media.name = "Noise Canceling source"
+ filter.graph = {
+ nodes = [
+ {
+ type = ladspa
+ name = rnnoise
+ plugin = librnnoise_ladspa
+ label = noise_suppressor_stereo
+ control = {
+ "VAD Threshold (%)" 50.0
+ }
+ }
+ ]
+ }
+ audio.position = [ FL FR ]
+ capture.props = {
+ node.name = "effect_input.rnnoise"
+ node.passive = true
+ }
+ playback.props = {
+ node.name = "effect_output.rnnoise"
+ media.class = Audio/Source
+ }
+ }
+ }
+]
diff --git a/src/daemon/jack.conf.in b/src/daemon/jack.conf.in
new file mode 100644
index 0000000..95a86cb
--- /dev/null
+++ b/src/daemon/jack.conf.in
@@ -0,0 +1,128 @@
+# JACK 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@/jack.conf.d/ for system-wide changes or in
+# ~/.config/pipewire/jack.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.
+ #
+ support.* = support/libspa-support
+}
+
+context.modules = [
+ #{ name = <module-name>
+ # [ args = { <key> = <value> ... } ]
+ # [ flags = [ [ ifexists ] [ nofail ] ]
+ #}
+ #
+ # Loads a module with the given parameters.
+ # If ifexists is given, the module is ignored when it is not found.
+ # If nofail is given, module initialization failures are ignored.
+ #
+ #
+ # Boost the data thread priority.
+ { name = libpipewire-module-rt
+ args = {
+ #rt.prio = 88
+ #rt.time.soft = -1
+ #rt.time.hard = -1
+ }
+ flags = [ ifexists nofail ]
+ }
+
+ # The native communication protocol.
+ { name = libpipewire-module-protocol-native }
+
+ # Allows creating nodes that run in the context of the
+ # client. Is used by all clients that want to provide
+ # data to PipeWire.
+ { name = libpipewire-module-client-node }
+
+ # Allows applications to create metadata objects. It creates
+ # a factory for Metadata objects.
+ { name = libpipewire-module-metadata }
+]
+
+# global properties for all jack clients
+jack.properties = {
+ #node.latency = 1024/48000
+ #node.rate = 1/48000
+ #node.quantum = 1024/48000
+ #node.lock-quantum = true
+ #node.force-quantum = 0
+ #jack.show-monitor = true
+ #jack.merge-monitor = true
+ #jack.short-name = false
+ #jack.filter-name = false
+ #jack.filter-char = " "
+ #
+ # allow: Don't restrict self connect requests
+ # fail-external: Fail self connect requests to external ports only
+ # ignore-external: Ignore self connect requests to external ports only
+ # fail-all: Fail all self connect requests
+ # ignore-all: Ignore all self connect requests
+ #jack.self-connect-mode = allow
+ #jack.locked-process = true
+ #jack.default-as-system = false
+ #jack.fix-midi-events = true
+ #jack.global-buffer-size = false
+}
+
+# client specific properties
+jack.rules = [
+ { matches = [
+ {
+ # all keys must match the value. ~ starts regex.
+ #client.name = "Carla"
+ #application.process.binary = "jack_simple_client"
+ #application.name = "~jack_simple_client.*"
+ }
+ ]
+ actions = {
+ update-props = {
+ #node.latency = 512/48000
+ }
+ }
+ }
+ { matches = [ { application.process.binary = "jack_bufsize" } ]
+ actions = {
+ update-props = {
+ jack.global-buffer-size = true # quantum set globally using metadata
+ }
+ }
+ }
+ { matches = [ { application.process.binary = "qsynth" } ]
+ actions = {
+ update-props = {
+ node.pause-on-idle = false # makes audio fade out when idle
+ node.passive = true # makes the sink and qsynth suspend
+ }
+ }
+ }
+ { matches = [ { client.name = "Mixxx" } ]
+ actions = {
+ update-props = {
+ jack.merge-monitor = false
+ }
+ }
+ }
+]
diff --git a/src/daemon/meson.build b/src/daemon/meson.build
new file mode 100644
index 0000000..5d5914e
--- /dev/null
+++ b/src/daemon/meson.build
@@ -0,0 +1,141 @@
+pipewire_daemon_sources = [
+ 'pipewire.c',
+]
+
+pipewire_c_args = [
+ '-DG_LOG_DOMAIN=g_log_domain_pipewire',
+]
+
+conf_config = configuration_data()
+conf_config.set('VERSION', '"@0@"'.format(pipewire_version))
+conf_config.set('PIPEWIRE_CONFIG_DIR', pipewire_configdir)
+conf_config.set('session_manager_path', pipewire_bindir / 'pipewire-media-session')
+conf_config.set('session_manager_args', '')
+conf_config.set('pipewire_path', pipewire_bindir / 'pipewire')
+conf_config.set('pipewire_pulse_path', pipewire_bindir / 'pipewire-pulse')
+conf_config.set('sm_comment', '#')
+conf_config.set('pulse_comment', '#')
+
+conf_config_uninstalled = conf_config
+conf_config_uninstalled.set('pipewire_path',
+ meson.project_build_root() / 'src' / 'daemon' / 'pipewire')
+conf_config_uninstalled.set('pipewire_pulse_path',
+ meson.project_build_root() / 'src' / 'daemon' / 'pipewire-pulse')
+conf_config_uninstalled.set('pulse_comment', '')
+
+build_ms = 'media-session' in get_option('session-managers')
+build_wp = 'wireplumber' in get_option('session-managers')
+default_sm = get_option('session-managers').get(0, '')
+
+summary({'Build media-session': build_ms,
+ 'Build wireplumber': build_wp,
+ 'Default session-manager': default_sm},
+ section: 'Session managers',
+ bool_yn: true)
+
+if build_wp
+ wp_proj = subproject('wireplumber', required : true)
+endif
+if build_ms
+ ms_proj = subproject('media-session', required : true)
+endif
+
+if default_sm == ''
+ summary({'No session manager': 'pw-uninstalled.sh will not work out of the box!'})
+elif default_sm == 'media-session'
+ ms_bindir = ms_proj.get_variable('media_session_bin_dir', pipewire_bindir)
+ conf_config.set('session_manager_path', ms_bindir / 'pipewire-media-session')
+
+ ms_uninstalled = ms_proj.get_variable('media_session_uninstalled')
+ conf_config_uninstalled.set('session_manager_path', ms_uninstalled.full_path())
+ conf_config_uninstalled.set('session_manager_args', 'pipewire-media-session')
+ conf_config_uninstalled.set('sm_comment', '')
+elif default_sm == 'wireplumber'
+ wp_bindir = wp_proj.get_variable('wireplumber_bin_dir', pipewire_bindir)
+ conf_config.set('session_manager_path', wp_bindir / 'wireplumber')
+
+ wp_uninstalled = wp_proj.get_variable('wireplumber_uninstalled')
+ conf_config_uninstalled.set('session_manager_path', wp_uninstalled.full_path())
+ conf_config_uninstalled.set('session_manager_args', 'wireplumber')
+ conf_config_uninstalled.set('sm_comment', '')
+else
+ conf_config_uninstalled.set('session_manager_path', default_sm)
+ conf_config_uninstalled.set('sm_comment', '')
+endif
+
+conf_files = [
+ 'pipewire.conf',
+ 'client.conf',
+ 'client-rt.conf',
+ 'filter-chain.conf',
+ 'jack.conf',
+ 'minimal.conf',
+ 'pipewire-pulse.conf',
+ 'pipewire-avb.conf',
+]
+
+foreach c : conf_files
+ configure_file(input : '@0@.in'.format(c),
+ output : c,
+ configuration : conf_config,
+ install_dir : pipewire_confdatadir)
+endforeach
+
+configure_file(input : 'pipewire.conf.in',
+ output : 'pipewire-uninstalled.conf',
+ configuration : conf_config_uninstalled)
+
+pipewire_exec = executable('pipewire',
+ pipewire_daemon_sources,
+ install: true,
+ c_args : pipewire_c_args,
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, pipewire_dep, ],
+)
+
+executable('pipewire-pulse',
+ pipewire_daemon_sources,
+ install: true,
+ c_args : pipewire_c_args,
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, pipewire_dep, ],
+)
+
+executable('pipewire-avb',
+ pipewire_daemon_sources,
+ install: true,
+ c_args : pipewire_c_args,
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, pipewire_dep, ],
+)
+
+ln = find_program('ln')
+
+custom_target('pipewire-uninstalled',
+ build_by_default: true,
+ install: false,
+ input: pipewire_exec,
+ output: 'pipewire-uninstalled',
+ command: [ln, '-fs', meson.project_build_root() + '/@INPUT@', '@OUTPUT@'],
+)
+
+#desktop_file = i18n.merge_file(
+# input : 'pipewire.desktop.in',
+# output : 'pipewire.desktop',
+# po_dir : po_dir,
+# type : 'desktop',
+# install : true,
+# install_dir : pipewire_sysconfdir / 'xdg' / 'autostart'
+#)
+#
+#desktop_utils = find_program('desktop-file-validate', required: false)
+#if desktop_utils.found()
+# test('Validate desktop file', desktop_utils,
+# args: [ desktop_file ],
+# )
+#endif
+
+subdir('filter-chain')
+if systemd.found()
+ subdir('systemd')
+endif
diff --git a/src/daemon/minimal.conf.in b/src/daemon/minimal.conf.in
new file mode 100644
index 0000000..4a3a2cb
--- /dev/null
+++ b/src/daemon/minimal.conf.in
@@ -0,0 +1,352 @@
+# Simple daemon 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@/minimal.conf.d/ for system-wide changes or in
+# ~/.config/pipewire/minimal.conf.d/ for local changes.
+#
+
+context.properties = {
+ ## Configure properties in the system.
+ #library.name.system = support/libspa-support
+ #context.data-loop.library.name.system = support/libspa-support
+ #support.dbus = true
+ #link.max-buffers = 64
+ link.max-buffers = 16 # version < 3 clients can't handle more
+ #mem.warn-mlock = false
+ #mem.allow-mlock = true
+ #mem.mlock-all = false
+ #clock.power-of-two-quantum = true
+ #log.level = 2
+ #cpu.zero.denormals = false
+
+ core.daemon = true # listening for socket connections
+ core.name = pipewire-0 # core name and socket name
+
+ ## Properties for the DSP configuration.
+ #default.clock.rate = 48000
+ #default.clock.allowed-rates = [ 48000 ]
+ #default.clock.quantum = 1024
+ #default.clock.min-quantum = 32
+ #default.clock.max-quantum = 2048
+ #default.clock.quantum-limit = 8192
+ #default.video.width = 640
+ #default.video.height = 480
+ #default.video.rate.num = 25
+ #default.video.rate.denom = 1
+ #
+ settings.check-quantum = true
+ settings.check-rate = true
+ #
+ # These overrides are only applied when running in a vm.
+ vm.overrides = {
+ default.clock.min-quantum = 1024
+ }
+}
+
+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
+ api.alsa.* = alsa/libspa-alsa
+ support.* = support/libspa-support
+}
+
+context.modules = [
+ #{ name = <module-name>
+ # [ args = { <key> = <value> ... } ]
+ # [ flags = [ [ ifexists ] [ nofail ] ]
+ #}
+ #
+ # Loads a module with the given parameters.
+ # If ifexists is given, the module is ignored when it is not found.
+ # If nofail is given, module initialization failures are ignored.
+ #
+
+ # Uses realtime scheduling to boost the audio thread priorities. This uses
+ # RTKit if the user doesn't have permission to use regular realtime
+ # scheduling.
+ { name = libpipewire-module-rt
+ args = {
+ nice.level = -11
+ #rt.prio = 88
+ #rt.time.soft = -1
+ #rt.time.hard = -1
+ }
+ flags = [ ifexists nofail ]
+ }
+
+ # The native communication protocol.
+ { name = libpipewire-module-protocol-native }
+
+ # The profile module. Allows application to access profiler
+ # and performance data. It provides an interface that is used
+ # by pw-top and pw-profiler.
+ { name = libpipewire-module-profiler }
+
+ # Allows applications to create metadata objects. It creates
+ # a factory for Metadata objects.
+ { name = libpipewire-module-metadata }
+
+ # Creates a factory for making nodes that run in the
+ # context of the PipeWire server.
+ { name = libpipewire-module-spa-node-factory }
+
+ # 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 }
+
+ # The access module can perform access checks and block
+ # new clients.
+ { name = libpipewire-module-access
+ args = {
+ # access.allowed to list an array of paths of allowed
+ # apps.
+ #access.allowed = [
+ # @session_manager_path@
+ #]
+
+ # An array of rejected paths.
+ #access.rejected = [ ]
+
+ # An array of paths with restricted access.
+ #access.restricted = [ ]
+
+ # Anything not in the above lists gets assigned the
+ # access.force permission.
+ #access.force = flatpak
+ }
+ }
+
+ # Makes a factory for wrapping nodes in an adapter with a
+ # converter and resampler.
+ { name = libpipewire-module-adapter }
+
+ # Makes a factory for creating links between ports.
+ { name = libpipewire-module-link-factory }
+]
+
+context.objects = [
+ #{ factory = <factory-name>
+ # [ args = { <key> = <value> ... } ]
+ # [ flags = [ [ nofail ] ]
+ #}
+ #
+ # 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 Spa:Pod:Object: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 } }
+ #{ factory = spa-node-factory args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }
+
+ # Make a default metadata store
+ { factory = metadata args = { metadata.name = default } }
+
+ # A default dummy driver. This handles nodes marked with the "node.always-driver"
+ # property when no other driver is currently active. JACK clients need this.
+ { factory = spa-node-factory
+ args = {
+ factory.name = support.node.driver
+ node.name = Dummy-Driver
+ node.group = pipewire.dummy
+ priority.driver = 20000
+ }
+ }
+ { factory = spa-node-factory
+ args = {
+ factory.name = support.node.driver
+ node.name = Freewheel-Driver
+ priority.driver = 19000
+ node.group = pipewire.freewheel
+ node.freewheel = true
+ }
+ }
+
+ # This creates a single PCM source device for the given
+ # alsa device path hw:0. You can change source to sink
+ # to make a sink in the same way.
+ { factory = adapter
+ args = {
+ factory.name = api.alsa.pcm.source
+ node.name = "system"
+ node.description = "system"
+ media.class = "Audio/Source"
+ api.alsa.path = "hw:0"
+ #api.alsa.period-size = 0
+ #api.alsa.period-num = 0
+ #api.alsa.headroom = 0
+ #api.alsa.start-delay = 0
+ #api.alsa.disable-mmap = false
+ #api.alsa.disable-batch = false
+ #api.alsa.use-chmap = false
+ #api.alsa.multirate = true
+ #latency.internal.rate = 0
+ #latency.internal.ns = 0
+ #clock.name = api.alsa.0
+ node.suspend-on-idle = true
+ #audio.format = "S32"
+ #audio.rate = 48000
+ #audio.allowed-rates = [ ]
+ #audio.channels = 4
+ #audio.position = [ FL FR RL RR ]
+ #resample.quality = 4
+ resample.disable = true
+ #monitor.channel-volumes = false
+ #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
+ channelmix.disable = true
+ #dither.noise = 0
+ #node.param.Props = {
+ # params = [
+ # audio.channels 6
+ # ]
+ #}
+ adapter.auto-port-config = {
+ mode = dsp
+ monitor = false
+ control = false
+ position = unknown # aux, preserve
+ }
+ #node.param.PortConfig = {
+ # direction = Output
+ # mode = dsp
+ # format = {
+ # mediaType = audio
+ # mediaSubtype = raw
+ # format = F32
+ # rate = 48000
+ # channels = 4
+ # position = [ FL FR RL RR ]
+ # }
+ #}
+ }
+ }
+ { factory = adapter
+ args = {
+ factory.name = api.alsa.pcm.sink
+ node.name = "system"
+ node.description = "system"
+ media.class = "Audio/Sink"
+ api.alsa.path = "hw:0"
+ #api.alsa.period-size = 0
+ #api.alsa.period-num = 0
+ #api.alsa.headroom = 0
+ #api.alsa.start-delay = 0
+ #api.alsa.disable-mmap = false
+ #api.alsa.disable-batch = false
+ #api.alsa.use-chmap = false
+ #api.alsa.multirate = true
+ #latency.internal.rate = 0
+ #latency.internal.ns = 0
+ #clock.name = api.alsa.0
+ node.suspend-on-idle = true
+ #audio.format = "S32"
+ #audio.rate = 48000
+ #audio.allowed-rates = [ ]
+ #audio.channels = 2
+ #audio.position = "FL,FR"
+ #resample.quality = 4
+ resample.disable = true
+ #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
+ channelmix.disable = true
+ #dither.noise = 0
+ #node.param.Props = {
+ # params = [
+ # audio.format S16
+ # ]
+ #}
+ adapter.auto-port-config = {
+ mode = dsp
+ monitor = false
+ control = false
+ position = unknown # aux, preserve
+ }
+ #node.param.PortConfig = {
+ # direction = Input
+ # mode = dsp
+ # monitor = true
+ # format = {
+ # mediaType = audio
+ # mediaSubtype = raw
+ # format = F32
+ # rate = 48000
+ # channels = 4
+ # }
+ #}
+ }
+ }
+ # This creates a new Source node. It will have input ports
+ # that you can link, to provide audio for this source.
+ #{ factory = adapter
+ # args = {
+ # factory.name = support.null-audio-sink
+ # node.name = "my-mic"
+ # node.description = "Microphone"
+ # media.class = "Audio/Source/Virtual"
+ # audio.position = "FL,FR"
+ # adapter.auto-port-config = {
+ # mode = dsp
+ # monitor = true
+ # position = preserve # unknown, aux, preserve
+ # }
+ # }
+ #}
+ # This creates a new link between the source and the virtual
+ # source ports.
+ #{ factory = link-factory
+ # args = {
+ # link.output.node = system
+ # link.output.port = capture_1
+ # link.input.node = my-mic
+ # link.input.port = input_FL
+ # link.passive = true
+ # }
+ #}
+ #{ factory = link-factory
+ # args = {
+ # link.output.node = system
+ # link.output.port = capture_2
+ # link.input.node = my-mic
+ # link.input.port = input_FR
+ # link.passive = true
+ # }
+ #}
+]
+
+context.exec = [
+ #{ path = <program-name> [ args = "<arguments>" ] }
+ #
+ # Execute the given program with arguments.
+ #
+ # You can optionally start the pulseaudio-server here as well
+ # but it is better to start it as a systemd service.
+ # It can be interesting to start another daemon here that listens
+ # on another address with the -a option (eg. -a tcp:4713).
+ #
+ #@pulse_comment@{ path = "@pipewire_path@" args = "-c pipewire-pulse.conf" }
+]
diff --git a/src/daemon/pipewire-avb.conf.in b/src/daemon/pipewire-avb.conf.in
new file mode 100644
index 0000000..68f89ca
--- /dev/null
+++ b/src/daemon/pipewire-avb.conf.in
@@ -0,0 +1,73 @@
+# PulseAudio config file for PipeWire version @VERSION@ #
+#
+# Copy and edit this file in @PIPEWIRE_CONFIG_DIR@ for system-wide changes
+# or in ~/.config/pipewire for local changes.
+#
+# It is also possible to place a file with an updated section in
+# @PIPEWIRE_CONFIG_DIR@/pipewire-pulse.conf.d/ for system-wide changes or in
+# ~/.config/pipewire/pipewire-pulse.conf.d/ for local changes.
+#
+
+context.properties = {
+ ## Configure properties in the system.
+ #mem.warn-mlock = false
+ #mem.allow-mlock = true
+ #mem.mlock-all = false
+ #log.level = 2
+
+ #default.clock.quantum-limit = 8192
+}
+
+context.spa-libs = {
+ audio.convert.* = audioconvert/libspa-audioconvert
+ support.* = support/libspa-support
+}
+
+context.modules = [
+ { name = libpipewire-module-rt
+ args = {
+ nice.level = -11
+ #rt.prio = 88
+ #rt.time.soft = -1
+ #rt.time.hard = -1
+ }
+ flags = [ ifexists nofail ]
+ }
+ { name = libpipewire-module-protocol-native }
+ { name = libpipewire-module-client-node }
+ { name = libpipewire-module-adapter }
+ { name = libpipewire-module-avb
+ args = {
+ # contents of avb.properties can also be placed here
+ # to have config per server.
+ }
+ }
+]
+
+# Extra modules can be loaded here. Setup in default.pa can be moved here
+context.exec = [
+ #{ path = "pactl" args = "load-module module-always-sink" }
+]
+
+stream.properties = {
+ #node.latency = 1024/48000
+ #node.autoconnect = true
+ #resample.quality = 4
+ #channelmix.normalize = false
+ #channelmix.mix-lfe = true
+ #channelmix.upmix = true
+ #channelmix.lfe-cutoff = 120
+ #channelmix.fc-cutoff = 6000
+ #channelmix.rear-delay = 12.0
+ #channelmix.stereo-widen = 0.1
+ #channelmix.hilbert-taps = 0
+}
+
+avb.properties = {
+ # the addresses this server listens on
+ #ifname = "eth0.2"
+ ifname = "enp3s0"
+ # These overrides are only applied when running in a vm.
+ vm.overrides = {
+ }
+}
diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in
new file mode 100644
index 0000000..18bca3a
--- /dev/null
+++ b/src/daemon/pipewire-pulse.conf.in
@@ -0,0 +1,160 @@
+# PulseAudio config file for PipeWire version @VERSION@ #
+#
+# Copy and edit this file in @PIPEWIRE_CONFIG_DIR@ for system-wide changes
+# or in ~/.config/pipewire for local changes.
+#
+# It is also possible to place a file with an updated section in
+# @PIPEWIRE_CONFIG_DIR@/pipewire-pulse.conf.d/ for system-wide changes or in
+# ~/.config/pipewire/pipewire-pulse.conf.d/ for local changes.
+#
+
+context.properties = {
+ ## Configure properties in the system.
+ #mem.warn-mlock = false
+ #mem.allow-mlock = true
+ #mem.mlock-all = false
+ #log.level = 2
+
+ #default.clock.quantum-limit = 8192
+}
+
+context.spa-libs = {
+ audio.convert.* = audioconvert/libspa-audioconvert
+ support.* = support/libspa-support
+}
+
+context.modules = [
+ { name = libpipewire-module-rt
+ args = {
+ nice.level = -11
+ #rt.prio = 88
+ #rt.time.soft = -1
+ #rt.time.hard = -1
+ }
+ flags = [ ifexists nofail ]
+ }
+ { name = libpipewire-module-protocol-native }
+ { name = libpipewire-module-client-node }
+ { name = libpipewire-module-adapter }
+ { name = libpipewire-module-metadata }
+
+ { name = libpipewire-module-protocol-pulse
+ args = {
+ # contents of pulse.properties can also be placed here
+ # to have config per server.
+ }
+ }
+]
+
+# Extra scripts can be started here. Setup in default.pa can be moved in
+# a script or in pulse.cmd below
+context.exec = [
+ #{ path = "pactl" args = "load-module module-always-sink" }
+ #{ path = "pactl" args = "upload-sample my-sample.wav my-sample" }
+ #{ path = "/usr/bin/sh" args = "~/.config/pipewire/default.pw" }
+]
+
+# Extra commands can be executed here.
+# load-module : loads a module with args and flags
+# args = "<module-name> <module-args>"
+# flags = [ "no-fail" ]
+pulse.cmd = [
+ { cmd = "load-module" args = "module-always-sink" flags = [ ] }
+ #{ cmd = "load-module" args = "module-switch-on-connect" }
+ #{ cmd = "load-module" args = "module-gsettings" flags = [ "nofail" ] }
+]
+
+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
+}
+
+pulse.properties = {
+ # the addresses this server listens on
+ server.address = [
+ "unix:native"
+ #"unix:/tmp/something" # absolute paths may be used
+ #"tcp:4713" # IPv4 and IPv6 on all addresses
+ #"tcp:[::]:9999" # IPv6 on all addresses
+ #"tcp:127.0.0.1:8888" # IPv4 on a single address
+ #
+ #{ address = "tcp:4713" # address
+ # max-clients = 64 # maximum number of clients
+ # listen-backlog = 32 # backlog in the server listen queue
+ # client.access = "restricted" # permissions for clients
+ #}
+ ]
+ #pulse.min.req = 256/48000 # 5ms
+ #pulse.default.req = 960/48000 # 20 milliseconds
+ #pulse.min.frag = 256/48000 # 5ms
+ #pulse.default.frag = 96000/48000 # 2 seconds
+ #pulse.default.tlength = 96000/48000 # 2 seconds
+ #pulse.min.quantum = 256/48000 # 5ms
+ #pulse.idle.timeout = 0 # don't pause after underruns
+ #pulse.default.format = F32
+ #pulse.default.position = [ FL FR ]
+ # These overrides are only applied when running in a vm.
+ vm.overrides = {
+ pulse.min.quantum = 1024/48000 # 22ms
+ }
+}
+
+# client/stream specific properties
+pulse.rules = [
+ {
+ matches = [
+ {
+ # all keys must match the value. ~ starts regex.
+ #client.name = "Firefox"
+ #application.process.binary = "teams"
+ #application.name = "~speech-dispatcher.*"
+ }
+ ]
+ actions = {
+ update-props = {
+ #node.latency = 512/48000
+ }
+ # Possible quirks:"
+ # force-s16-info forces sink and source info as S16 format
+ # remove-capture-dont-move removes the capture DONT_MOVE flag
+ #quirks = [ ]
+ }
+ }
+ {
+ # skype does not want to use devices that don't have an S16 sample format.
+ matches = [
+ { application.process.binary = "teams" }
+ { application.process.binary = "teams-insiders" }
+ { application.process.binary = "skypeforlinux" }
+ ]
+ actions = { quirks = [ force-s16-info ] }
+ }
+ {
+ # firefox marks the capture streams as don't move and then they
+ # can't be moved with pavucontrol or other tools.
+ matches = [ { application.process.binary = "firefox" } ]
+ actions = { quirks = [ remove-capture-dont-move ] }
+ }
+ {
+ # speech dispatcher asks for too small latency and then underruns.
+ matches = [ { application.name = "~speech-dispatcher.*" } ]
+ actions = {
+ update-props = {
+ pulse.min.req = 512/48000 # 10.6ms
+ pulse.min.quantum = 512/48000 # 10.6ms
+ pulse.idle.timeout = 5 # pause after 5 seconds of underrun
+ }
+ }
+ }
+]
diff --git a/src/daemon/pipewire.c b/src/daemon/pipewire.c
new file mode 100644
index 0000000..97569c0
--- /dev/null
+++ b/src/daemon/pipewire.c
@@ -0,0 +1,143 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <limits.h>
+#include <signal.h>
+#include <getopt.h>
+#include <libgen.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <pipewire/pipewire.h>
+
+#include <pipewire/i18n.h>
+
+#include "config.h"
+
+static void do_quit(void *data, int signal_number)
+{
+ struct pw_main_loop *loop = data;
+ pw_main_loop_quit(loop);
+}
+
+static void show_help(const char *name, const char *config_name)
+{
+ fprintf(stdout, _("%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -c, --config Load config (Default %s)\n"),
+ name,
+ config_name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct pw_context *context = NULL;
+ struct pw_main_loop *loop = NULL;
+ struct pw_properties *properties = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "config", required_argument, NULL, 'c' },
+ { "verbose", no_argument, NULL, 'v' },
+
+ { NULL, 0, NULL, 0}
+ };
+ int c, res = 0;
+ char path[PATH_MAX];
+ const char *config_name;
+ enum spa_log_level level = pw_log_level;
+
+ if (setenv("PIPEWIRE_INTERNAL", "1", 1) < 0)
+ fprintf(stderr, "can't set PIPEWIRE_INTERNAL env: %m");
+
+ snprintf(path, sizeof(path), "%s.conf", argv[0]);
+ config_name = basename(path);
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVc:v", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'v':
+ if (level < SPA_LOG_LEVEL_TRACE)
+ pw_log_set_level(++level);
+ break;
+ case 'h':
+ show_help(argv[0], config_name);
+ return 0;
+ case 'V':
+ fprintf(stdout, "%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'c':
+ config_name = optarg;
+ break;
+ default:
+ res = -EINVAL;
+ goto done;
+ }
+ }
+
+ properties = pw_properties_new(
+ PW_KEY_CONFIG_NAME, config_name,
+ NULL);
+
+ loop = pw_main_loop_new(&properties->dict);
+ if (loop == NULL) {
+ pw_log_error("failed to create main-loop: %m");
+ res = -errno;
+ goto done;
+ }
+
+ pw_loop_add_signal(pw_main_loop_get_loop(loop), SIGINT, do_quit, loop);
+ pw_loop_add_signal(pw_main_loop_get_loop(loop), SIGTERM, do_quit, loop);
+
+ context = pw_context_new(pw_main_loop_get_loop(loop), properties, 0);
+ properties = NULL;
+
+ if (context == NULL) {
+ pw_log_error("failed to create context: %m");
+ res = -errno;
+ goto done;
+ }
+
+ pw_log_info("start main loop");
+ pw_main_loop_run(loop);
+ pw_log_info("leave main loop");
+
+done:
+ pw_properties_free(properties);
+ if (context)
+ pw_context_destroy(context);
+ if (loop)
+ pw_main_loop_destroy(loop);
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in
new file mode 100644
index 0000000..4aa30a4
--- /dev/null
+++ b/src/daemon/pipewire.conf.in
@@ -0,0 +1,259 @@
+# Daemon config file for PipeWire version @VERSION@ #
+#
+# Copy and edit this file in @PIPEWIRE_CONFIG_DIR@ for system-wide changes
+# or in ~/.config/pipewire for local changes.
+#
+# It is also possible to place a file with an updated section in
+# @PIPEWIRE_CONFIG_DIR@/pipewire.conf.d/ for system-wide changes or in
+# ~/.config/pipewire/pipewire.conf.d/ for local changes.
+#
+
+context.properties = {
+ ## Configure properties in the system.
+ #library.name.system = support/libspa-support
+ #context.data-loop.library.name.system = support/libspa-support
+ #support.dbus = true
+ #link.max-buffers = 64
+ link.max-buffers = 16 # version < 3 clients can't handle more
+ #mem.warn-mlock = false
+ #mem.allow-mlock = true
+ #mem.mlock-all = false
+ #clock.power-of-two-quantum = true
+ #log.level = 2
+ #cpu.zero.denormals = false
+
+ core.daemon = true # listening for socket connections
+ core.name = pipewire-0 # core name and socket name
+
+ ## Properties for the DSP configuration.
+ #default.clock.rate = 48000
+ #default.clock.allowed-rates = [ 48000 ]
+ #default.clock.quantum = 1024
+ default.clock.min-quantum = 16
+ #default.clock.max-quantum = 2048
+ #default.clock.quantum-limit = 8192
+ #default.video.width = 640
+ #default.video.height = 480
+ #default.video.rate.num = 25
+ #default.video.rate.denom = 1
+ #
+ #settings.check-quantum = false
+ #settings.check-rate = false
+ #
+ # These overrides are only applied when running in a vm.
+ vm.overrides = {
+ default.clock.min-quantum = 1024
+ }
+}
+
+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
+ avb.* = avb/libspa-avb
+ api.alsa.* = alsa/libspa-alsa
+ api.v4l2.* = v4l2/libspa-v4l2
+ api.libcamera.* = libcamera/libspa-libcamera
+ api.bluez5.* = bluez5/libspa-bluez5
+ api.vulkan.* = vulkan/libspa-vulkan
+ api.jack.* = jack/libspa-jack
+ support.* = support/libspa-support
+ #videotestsrc = videotestsrc/libspa-videotestsrc
+ #audiotestsrc = audiotestsrc/libspa-audiotestsrc
+}
+
+context.modules = [
+ #{ name = <module-name>
+ # [ args = { <key> = <value> ... } ]
+ # [ flags = [ [ ifexists ] [ nofail ] ]
+ #}
+ #
+ # Loads a module with the given parameters.
+ # If ifexists is given, the module is ignored when it is not found.
+ # If nofail is given, module initialization failures are ignored.
+ #
+
+ # Uses realtime scheduling to boost the audio thread priorities. This uses
+ # RTKit if the user doesn't have permission to use regular realtime
+ # scheduling.
+ { name = libpipewire-module-rt
+ args = {
+ nice.level = -11
+ #rt.prio = 88
+ #rt.time.soft = -1
+ #rt.time.hard = -1
+ }
+ flags = [ ifexists nofail ]
+ }
+
+ # The native communication protocol.
+ { name = libpipewire-module-protocol-native }
+
+ # The profile module. Allows application to access profiler
+ # and performance data. It provides an interface that is used
+ # by pw-top and pw-profiler.
+ { name = libpipewire-module-profiler }
+
+ # Allows applications to create metadata objects. It creates
+ # a factory for Metadata objects.
+ { name = libpipewire-module-metadata }
+
+ # Creates a factory for making devices that run in the
+ # context of the PipeWire server.
+ { name = libpipewire-module-spa-device-factory }
+
+ # Creates a factory for making nodes that run in the
+ # context of the PipeWire server.
+ { name = libpipewire-module-spa-node-factory }
+
+ # 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 }
+
+ # 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 ]
+ }
+
+ # The access module can perform access checks and block
+ # new clients.
+ { name = libpipewire-module-access
+ args = {
+ # access.allowed to list an array of paths of allowed
+ # apps.
+ #access.allowed = [
+ # @session_manager_path@
+ #]
+
+ # An array of rejected paths.
+ #access.rejected = [ ]
+
+ # An array of paths with restricted access.
+ #access.restricted = [ ]
+
+ # Anything not in the above lists gets assigned the
+ # access.force permission.
+ #access.force = flatpak
+ }
+ }
+
+ # Makes a factory for wrapping nodes in an adapter with a
+ # converter and resampler.
+ { name = libpipewire-module-adapter }
+
+ # Makes a factory for creating links between ports.
+ { name = libpipewire-module-link-factory }
+
+ # Provides factories to make session manager objects.
+ { name = libpipewire-module-session-manager }
+
+ # Use libcanberra to play X11 Bell
+ { name = libpipewire-module-x11-bell
+ args = {
+ #sink.name = "@DEFAULT_SINK@"
+ #sample.name = "bell-window-system"
+ #x11.display = null
+ #x11.xauthority = null
+ }
+ flags = [ ifexists nofail ]
+ }
+]
+
+context.objects = [
+ #{ factory = <factory-name>
+ # [ args = { <key> = <value> ... } ]
+ # [ flags = [ [ nofail ] ]
+ #}
+ #
+ # 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 Spa:Pod:Object: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 } }
+ #{ 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-driver"
+ # property when no other driver is currently active. JACK clients need this.
+ { factory = spa-node-factory
+ args = {
+ factory.name = support.node.driver
+ node.name = Dummy-Driver
+ node.group = pipewire.dummy
+ priority.driver = 20000
+ }
+ }
+ { factory = spa-node-factory
+ args = {
+ factory.name = support.node.driver
+ node.name = Freewheel-Driver
+ priority.driver = 19000
+ node.group = pipewire.freewheel
+ node.freewheel = true
+ }
+ }
+ # This creates a new Source node. It will have input ports
+ # that you can link, to provide audio for this source.
+ #{ factory = adapter
+ # args = {
+ # factory.name = support.null-audio-sink
+ # node.name = "my-mic"
+ # node.description = "Microphone"
+ # media.class = "Audio/Source/Virtual"
+ # audio.position = "FL,FR"
+ # }
+ #}
+
+ # This creates a single PCM source device for the given
+ # alsa device path hw:0. You can change source to sink
+ # to make a sink in the same way.
+ #{ factory = adapter
+ # args = {
+ # factory.name = api.alsa.pcm.source
+ # node.name = "alsa-source"
+ # node.description = "PCM Source"
+ # media.class = "Audio/Source"
+ # api.alsa.path = "hw:0"
+ # api.alsa.period-size = 1024
+ # api.alsa.headroom = 0
+ # api.alsa.disable-mmap = false
+ # api.alsa.disable-batch = false
+ # audio.format = "S16LE"
+ # audio.rate = 48000
+ # audio.channels = 2
+ # audio.position = "FL,FR"
+ # }
+ #}
+]
+
+context.exec = [
+ #{ path = <program-name> [ args = "<arguments>" ] }
+ #
+ # Execute the given program with arguments.
+ #
+ # You can optionally start the session manager here,
+ # but it is better to start it as a systemd service.
+ # Run the session manager with -h for options.
+ #
+ @sm_comment@{ path = "@session_manager_path@" args = "@session_manager_args@" }
+ #
+ # You can optionally start the pulseaudio-server here as well
+ # but it is better to start it as a systemd service.
+ # It can be interesting to start another daemon here that listens
+ # on another address with the -a option (eg. -a tcp:4713).
+ #
+ @pulse_comment@{ path = "@pipewire_path@" args = "-c pipewire-pulse.conf" }
+]
diff --git a/src/daemon/pipewire.desktop.in b/src/daemon/pipewire.desktop.in
new file mode 100644
index 0000000..ebf0e30
--- /dev/null
+++ b/src/daemon/pipewire.desktop.in
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=1.0
+Name=PipeWire Media System
+Comment=Start the PipeWire Media System
+Exec=pipewire
+Terminal=false
+Type=Application
+X-GNOME-Autostart-Phase=Initialization
+X-KDE-autostart-phase=1
diff --git a/src/daemon/systemd/meson.build b/src/daemon/systemd/meson.build
new file mode 100644
index 0000000..482a44c
--- /dev/null
+++ b/src/daemon/systemd/meson.build
@@ -0,0 +1,6 @@
+if get_option('systemd-system-service').allowed()
+ subdir('system')
+endif
+if get_option('systemd-user-service').allowed()
+ subdir('user')
+endif
diff --git a/src/daemon/systemd/system/meson.build b/src/daemon/systemd/system/meson.build
new file mode 100644
index 0000000..84ca0b0
--- /dev/null
+++ b/src/daemon/systemd/system/meson.build
@@ -0,0 +1,15 @@
+systemd_system_services_dir = systemd.get_variable('systemdsystemunitdir', pkgconfig_define : [ 'rootprefix', prefix])
+if get_option('systemd-system-unit-dir') != ''
+ systemd_system_services_dir = get_option('systemd-system-unit-dir')
+endif
+
+install_data(sources : 'pipewire.socket',
+ install_dir : systemd_system_services_dir)
+
+systemd_config = configuration_data()
+systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire')
+
+configure_file(input : 'pipewire.service.in',
+ output : 'pipewire.service',
+ configuration : systemd_config,
+ install_dir : systemd_system_services_dir)
diff --git a/src/daemon/systemd/system/pipewire.service.in b/src/daemon/systemd/system/pipewire.service.in
new file mode 100644
index 0000000..8b75ba2
--- /dev/null
+++ b/src/daemon/systemd/system/pipewire.service.in
@@ -0,0 +1,35 @@
+[Unit]
+Description=PipeWire Multimedia Service
+
+# We require pipewire.socket to be active before starting the daemon, because
+# while it is possible to use the service without the socket, it is not clear
+# why it would be desirable.
+#
+# Installing pipewire and doing `systemctl start pipewire` will not get the
+# socket started, which might be confusing and problematic if the server is to
+# be restarted later on, as the client autospawn feature might kick in. Also, a
+# start of the socket unit will fail, adding to the confusion.
+#
+# After=pipewire.socket is not needed, as it is already implicit in the
+# socket-service relationship, see systemd.socket(5).
+Requires=pipewire.socket
+
+[Service]
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+RestrictNamespaces=yes
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+Type=simple
+AmbientCapabilities=CAP_SYS_NICE
+ExecStart=@PW_BINARY@
+Restart=on-failure
+RuntimeDirectory=pipewire
+RuntimeDirectoryPreserve=yes
+User=pipewire
+Environment=PIPEWIRE_RUNTIME_DIR=%t/pipewire
+
+[Install]
+Also=pipewire.socket
+WantedBy=default.target
diff --git a/src/daemon/systemd/system/pipewire.socket b/src/daemon/systemd/system/pipewire.socket
new file mode 100644
index 0000000..2e3cb71
--- /dev/null
+++ b/src/daemon/systemd/system/pipewire.socket
@@ -0,0 +1,12 @@
+[Unit]
+Description=PipeWire Multimedia System Socket
+
+[Socket]
+Priority=6
+ListenStream=%t/pipewire/pipewire-0
+SocketUser=pipewire
+SocketGroup=pipewire
+SocketMode=0660
+
+[Install]
+WantedBy=sockets.target
diff --git a/src/daemon/systemd/user/filter-chain.service.in b/src/daemon/systemd/user/filter-chain.service.in
new file mode 100644
index 0000000..542cbd7
--- /dev/null
+++ b/src/daemon/systemd/user/filter-chain.service.in
@@ -0,0 +1,21 @@
+[Unit]
+Description=PipeWire filter chain daemon
+
+After=pipewire.service pipewire-session-manager.service
+BindsTo=pipewire.service
+
+[Service]
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+RestrictNamespaces=yes
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+Type=simple
+ExecStart=@PW_BINARY@ -c filter-chain.conf
+Restart=on-failure
+Slice=session.slice
+
+[Install]
+Also=pipewire.socket
+WantedBy=default.target
diff --git a/src/daemon/systemd/user/meson.build b/src/daemon/systemd/user/meson.build
new file mode 100644
index 0000000..1022762
--- /dev/null
+++ b/src/daemon/systemd/user/meson.build
@@ -0,0 +1,27 @@
+systemd_user_services_dir = systemd.get_variable('systemduserunitdir', pkgconfig_define : [ 'prefix', prefix])
+if get_option('systemd-user-unit-dir') != ''
+ systemd_user_services_dir = get_option('systemd-user-unit-dir')
+endif
+
+install_data(
+ sources : ['pipewire.socket', 'pipewire-pulse.socket'],
+ install_dir : systemd_user_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_user_services_dir)
+
+configure_file(input : 'pipewire-pulse.service.in',
+ output : 'pipewire-pulse.service',
+ configuration : systemd_config,
+ install_dir : systemd_user_services_dir)
+
+configure_file(input : 'filter-chain.service.in',
+ output : 'filter-chain.service',
+ configuration : systemd_config,
+ install_dir : systemd_user_services_dir)
diff --git a/src/daemon/systemd/user/pipewire-pulse.service.in b/src/daemon/systemd/user/pipewire-pulse.service.in
new file mode 100644
index 0000000..73d22e5
--- /dev/null
+++ b/src/daemon/systemd/user/pipewire-pulse.service.in
@@ -0,0 +1,36 @@
+[Unit]
+Description=PipeWire PulseAudio
+
+# We require pipewire-pulse.socket to be active before starting the daemon, because
+# while it is possible to use the service without the socket, it is not clear
+# why it would be desirable.
+#
+# A user installing pipewire and doing `systemctl --user start pipewire-pulse`
+# will not get the socket started, which might be confusing and problematic if
+# the server is to be restarted later on, as the client autospawn feature
+# might kick in. Also, a start of the socket unit will fail, adding to the
+# confusion.
+#
+# After=pipewire-pulse.socket is not needed, as it is already implicit in the
+# socket-service relationship, see systemd.socket(5).
+Requires=pipewire-pulse.socket
+ConditionUser=!root
+Wants=pipewire.service pipewire-session-manager.service
+After=pipewire.service pipewire-session-manager.service
+Conflicts=pulseaudio.service
+
+[Service]
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+RestrictNamespaces=yes
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+Type=simple
+ExecStart=@PW_PULSE_BINARY@
+Restart=on-failure
+Slice=session.slice
+
+[Install]
+Also=pipewire-pulse.socket
+WantedBy=default.target
diff --git a/src/daemon/systemd/user/pipewire-pulse.socket b/src/daemon/systemd/user/pipewire-pulse.socket
new file mode 100644
index 0000000..1ae5eda
--- /dev/null
+++ b/src/daemon/systemd/user/pipewire-pulse.socket
@@ -0,0 +1,11 @@
+[Unit]
+Description=PipeWire PulseAudio
+ConditionUser=!root
+Conflicts=pulseaudio.socket
+
+[Socket]
+Priority=6
+ListenStream=%t/pulse/native
+
+[Install]
+WantedBy=sockets.target
diff --git a/src/daemon/systemd/user/pipewire.service.in b/src/daemon/systemd/user/pipewire.service.in
new file mode 100644
index 0000000..b9b1373
--- /dev/null
+++ b/src/daemon/systemd/user/pipewire.service.in
@@ -0,0 +1,32 @@
+[Unit]
+Description=PipeWire Multimedia Service
+
+# We require pipewire.socket to be active before starting the daemon, because
+# while it is possible to use the service without the socket, it is not clear
+# why it would be desirable.
+#
+# A user installing pipewire and doing `systemctl --user start pipewire`
+# will not get the socket started, which might be confusing and problematic if
+# the server is to be restarted later on, as the client autospawn feature
+# might kick in. Also, a start of the socket unit will fail, adding to the
+# confusion.
+#
+# After=pipewire.socket is not needed, as it is already implicit in the
+# socket-service relationship, see systemd.socket(5).
+Requires=pipewire.socket
+
+[Service]
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+RestrictNamespaces=yes
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+Type=simple
+ExecStart=@PW_BINARY@
+Restart=on-failure
+Slice=session.slice
+
+[Install]
+Also=pipewire.socket
+WantedBy=default.target
diff --git a/src/daemon/systemd/user/pipewire.socket b/src/daemon/systemd/user/pipewire.socket
new file mode 100644
index 0000000..232bbb8
--- /dev/null
+++ b/src/daemon/systemd/user/pipewire.socket
@@ -0,0 +1,9 @@
+[Unit]
+Description=PipeWire Multimedia System Socket
+
+[Socket]
+Priority=6
+ListenStream=%t/pipewire-0
+
+[Install]
+WantedBy=sockets.target
diff --git a/src/examples/audio-capture.c b/src/examples/audio-capture.c
new file mode 100644
index 0000000..4c1afbb
--- /dev/null
+++ b/src/examples/audio-capture.c
@@ -0,0 +1,209 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Audio capture using \ref pw_stream "pw_stream".
+ [title]
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <signal.h>
+
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_stream *stream;
+
+ struct spa_audio_info format;
+ unsigned move:1;
+};
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * b = pw_stream_dequeue_buffer(stream);
+ *
+ * .. consume stuff in the buffer ...
+ *
+ * pw_stream_queue_buffer(stream, b);
+ */
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ float *samples, max;
+ uint32_t c, n, n_channels, n_samples, peak;
+
+ if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((samples = buf->datas[0].data) == NULL)
+ return;
+
+ n_channels = data->format.info.raw.channels;
+ n_samples = buf->datas[0].chunk->size / sizeof(float);
+
+ /* move cursor up */
+ if (data->move)
+ fprintf(stdout, "%c[%dA", 0x1b, n_channels + 1);
+ fprintf(stdout, "captured %d samples\n", n_samples / n_channels);
+ for (c = 0; c < data->format.info.raw.channels; c++) {
+ max = 0.0f;
+ for (n = c; n < n_samples; n += n_channels)
+ max = fmaxf(max, fabsf(samples[n]));
+
+ peak = SPA_CLAMP(max * 30, 0, 39);
+
+ fprintf(stdout, "channel %d: |%*s%*s| peak:%f\n",
+ c, peak+1, "*", 40 - peak, "", max);
+ }
+ data->move = true;
+ fflush(stdout);
+
+ pw_stream_queue_buffer(data->stream, b);
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format changes.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+
+ /* NULL means to clear the format */
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0)
+ return;
+
+ /* only accept raw audio */
+ if (data->format.media_type != SPA_MEDIA_TYPE_audio ||
+ data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return;
+
+ /* call a helper function to parse the format for us. */
+ spa_format_audio_raw_parse(param, &data->format.info.raw);
+
+ fprintf(stdout, "capturing rate:%d channels:%d\n",
+ data->format.info.raw.rate, data->format.info.raw.channels);
+
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .param_changed = on_stream_param_changed,
+ .process = on_process,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct pw_properties *props;
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ pw_init(&argc, &argv);
+
+ /* make a main loop. If you already have another main loop, you can add
+ * the fd of this pipewire mainloop to it. */
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* Create a simple stream, the simple stream manages the core and remote
+ * objects for you if you don't need to deal with them.
+ *
+ * If you plan to autoconnect your stream, you need to provide at least
+ * media, category and role properties.
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to produce
+ * the data.
+ */
+ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Capture",
+ PW_KEY_MEDIA_ROLE, "Music",
+ NULL);
+ if (argc > 1)
+ /* Set stream target if given on command line */
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
+
+ /* uncomment if you want to capture from the sink monitor ports */
+ /* pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true"); */
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "audio-capture",
+ props,
+ &stream_events,
+ &data);
+
+ /* Make one parameter with the supported formats. The SPA_PARAM_EnumFormat
+ * id means that this is a format enumeration (of 1 value).
+ * We leave the channels and rate empty to accept the native graph
+ * rate and channels. */
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_F32));
+
+ /* Now connect this stream. We ask that our process function is
+ * called in a realtime thread. */
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, 1);
+
+ /* and wait while we let things run */
+ pw_main_loop_run(data.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/audio-dsp-filter.c b/src/examples/audio-dsp-filter.c
new file mode 100644
index 0000000..fbcc226
--- /dev/null
+++ b/src/examples/audio-dsp-filter.c
@@ -0,0 +1,180 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Audio filter using \ref pw_filter "pw_filter".
+ [title]
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <signal.h>
+
+#include <spa/pod/builder.h>
+#include <spa/param/latency-utils.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+
+struct data;
+
+struct port {
+ struct data *data;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_filter *filter;
+ struct port *in_port;
+ struct port *out_port;
+};
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * in = pw_filter_dequeue_buffer(filter, in_port);
+ * out = pw_filter_dequeue_buffer(filter, out_port);
+ *
+ * .. do stuff with buffers ...
+ *
+ * pw_filter_queue_buffer(filter, in_port, in);
+ * pw_filter_queue_buffer(filter, out_port, out);
+ *
+ * For DSP ports, there is a shortcut to directly dequeue, get
+ * the data and requeue the buffer with pw_filter_get_dsp_buffer().
+ *
+ *
+ */
+static void on_process(void *userdata, struct spa_io_position *position)
+{
+ struct data *data = userdata;
+ float *in, *out;
+ uint32_t n_samples = position->clock.duration;
+
+ pw_log_trace("do process %d", n_samples);
+
+ in = pw_filter_get_dsp_buffer(data->in_port, n_samples);
+ out = pw_filter_get_dsp_buffer(data->out_port, n_samples);
+
+ if (in == NULL || out == NULL)
+ return;
+
+ memcpy(out, in, n_samples * sizeof(float));
+}
+
+static const struct pw_filter_events filter_events = {
+ PW_VERSION_FILTER_EVENTS,
+ .process = on_process,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ pw_init(&argc, &argv);
+
+ /* make a main loop. If you already have another main loop, you can add
+ * the fd of this pipewire mainloop to it. */
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* Create a simple filter, the simple filter manages the core and remote
+ * objects for you if you don't need to deal with them.
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the filter state. The most important event
+ * you need to listen to is the process event where you need to process
+ * the data.
+ */
+ data.filter = pw_filter_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "audio-filter",
+ pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Filter",
+ PW_KEY_MEDIA_ROLE, "DSP",
+ NULL),
+ &filter_events,
+ &data);
+
+ /* make an audio DSP input port */
+ data.in_port = pw_filter_add_port(data.filter,
+ PW_DIRECTION_INPUT,
+ PW_FILTER_PORT_FLAG_MAP_BUFFERS,
+ sizeof(struct port),
+ pw_properties_new(
+ PW_KEY_FORMAT_DSP, "32 bit float mono audio",
+ PW_KEY_PORT_NAME, "input",
+ NULL),
+ NULL, 0);
+
+ /* make an audio DSP output port */
+ data.out_port = pw_filter_add_port(data.filter,
+ PW_DIRECTION_OUTPUT,
+ PW_FILTER_PORT_FLAG_MAP_BUFFERS,
+ sizeof(struct port),
+ pw_properties_new(
+ PW_KEY_FORMAT_DSP, "32 bit float mono audio",
+ PW_KEY_PORT_NAME, "output",
+ NULL),
+ NULL, 0);
+
+ params[0] = spa_process_latency_build(&b,
+ SPA_PARAM_ProcessLatency,
+ &SPA_PROCESS_LATENCY_INFO_INIT(
+ .ns = 10 * SPA_NSEC_PER_MSEC
+ ));
+
+
+ /* Now connect this filter. We ask that our process function is
+ * called in a realtime thread. */
+ if (pw_filter_connect(data.filter,
+ PW_FILTER_FLAG_RT_PROCESS,
+ params, 1) < 0) {
+ fprintf(stderr, "can't connect\n");
+ return -1;
+ }
+
+ /* and wait while we let things run */
+ pw_main_loop_run(data.loop);
+
+ pw_filter_destroy(data.filter);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/audio-dsp-src.c b/src/examples/audio-dsp-src.c
new file mode 100644
index 0000000..d7bf523
--- /dev/null
+++ b/src/examples/audio-dsp-src.c
@@ -0,0 +1,165 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Audio source using \ref pw_filter "pw_filter"
+ [title]
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <signal.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+#define DEFAULT_RATE 44100
+#define DEFAULT_FREQ 440
+#define DEFAULT_VOLUME 0.7
+
+struct data;
+
+struct port {
+ struct data *data;
+ double accumulator;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_filter *filter;
+ struct port *out_port;
+};
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * out = pw_filter_dequeue_buffer(filter, out_port);
+ *
+ * .. generate data in the buffer ...
+ *
+ * pw_filter_queue_buffer(filter, out_port, out);
+ *
+ * For DSP ports, there is a shortcut to directly dequeue, get
+ * the data and requeue the buffer with pw_filter_get_dsp_buffer().
+ */
+static void on_process(void *userdata, struct spa_io_position *position)
+{
+ struct data *data = userdata;
+ float *out;
+ struct port *out_port = data->out_port;
+ uint32_t i, n_samples = position->clock.duration;
+
+ pw_log_trace("do process %d", n_samples);
+
+ out = pw_filter_get_dsp_buffer(out_port, n_samples);
+ if (out == NULL)
+ return;
+
+ for (i = 0; i < n_samples; i++) {
+ out_port->accumulator += M_PI_M2 * DEFAULT_FREQ / DEFAULT_RATE;
+ if (out_port->accumulator >= M_PI_M2)
+ out_port->accumulator -= M_PI_M2;
+
+ *out++ = sin(out_port->accumulator) * DEFAULT_VOLUME;
+ }
+}
+
+static const struct pw_filter_events filter_events = {
+ PW_VERSION_FILTER_EVENTS,
+ .process = on_process,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+
+ pw_init(&argc, &argv);
+
+ /* make a main loop. If you already have another main loop, you can add
+ * the fd of this pipewire mainloop to it. */
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* Create a simple filter, the simple filter manages the core and remote
+ * objects for you if you don't need to deal with them.
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the filter state. The most important event
+ * you need to listen to is the process event where you need to process
+ * the data.
+ */
+ data.filter = pw_filter_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "audio-dsp-src",
+ pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Source",
+ PW_KEY_MEDIA_ROLE, "DSP",
+ PW_KEY_MEDIA_CLASS, "Stream/Output/Audio",
+ PW_KEY_NODE_AUTOCONNECT, "true",
+ NULL),
+ &filter_events,
+ &data);
+
+ /* make an audio DSP output port */
+ data.out_port = pw_filter_add_port(data.filter,
+ PW_DIRECTION_OUTPUT,
+ PW_FILTER_PORT_FLAG_MAP_BUFFERS,
+ sizeof(struct port),
+ pw_properties_new(
+ PW_KEY_FORMAT_DSP, "32 bit float mono audio",
+ PW_KEY_PORT_NAME, "output",
+ NULL),
+ NULL, 0);
+
+ /* Now connect this filter. We ask that our process function is
+ * called in a realtime thread. */
+ if (pw_filter_connect(data.filter,
+ PW_FILTER_FLAG_RT_PROCESS,
+ NULL, 0) < 0) {
+ fprintf(stderr, "can't connect\n");
+ return -1;
+ }
+
+ /* and wait while we let things run */
+ pw_main_loop_run(data.loop);
+
+ pw_filter_destroy(data.filter);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/audio-src.c b/src/examples/audio-src.c
new file mode 100644
index 0000000..1217732
--- /dev/null
+++ b/src/examples/audio-src.c
@@ -0,0 +1,186 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Audio source using \ref pw_stream "pw_stream".
+ [title]
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <signal.h>
+
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_VOLUME 0.7
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_stream *stream;
+
+ double accumulator;
+};
+
+static void fill_f32(struct data *d, void *dest, int n_frames)
+{
+ float *dst = dest, val;
+ int i, c;
+
+ for (i = 0; i < n_frames; i++) {
+ d->accumulator += M_PI_M2 * 440 / DEFAULT_RATE;
+ if (d->accumulator >= M_PI_M2)
+ d->accumulator -= M_PI_M2;
+
+ val = sin(d->accumulator) * DEFAULT_VOLUME;
+ for (c = 0; c < DEFAULT_CHANNELS; c++)
+ *dst++ = val;
+ }
+}
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * b = pw_stream_dequeue_buffer(stream);
+ *
+ * .. generate stuff in the buffer ...
+ *
+ * pw_stream_queue_buffer(stream, b);
+ */
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ int n_frames, stride;
+ uint8_t *p;
+
+ 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;
+
+ stride = sizeof(float) * DEFAULT_CHANNELS;
+ n_frames = SPA_MIN(b->requested, buf->datas[0].maxsize / stride);
+
+ fill_f32(data, p, n_frames);
+
+ 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);
+}
+
+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->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct pw_properties *props;
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ pw_init(&argc, &argv);
+
+ /* make a main loop. If you already have another main loop, you can add
+ * the fd of this pipewire mainloop to it. */
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* Create a simple stream, the simple stream manages the core and remote
+ * objects for you if you don't need to deal with them.
+ *
+ * If you plan to autoconnect your stream, you need to provide at least
+ * media, category and role properties.
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to produce
+ * the data.
+ */
+ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ NULL);
+ if (argc > 1)
+ /* Set stream target if given on command line */
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]);
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "audio-src",
+ 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.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/bluez-session.c b/src/examples/bluez-session.c
new file mode 100644
index 0000000..7d7e94c
--- /dev/null
+++ b/src/examples/bluez-session.c
@@ -0,0 +1,400 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Using the \ref spa_device "SPA Device API", among other things.
+ [title]
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <time.h>
+
+#include "config.h"
+
+#include <spa/monitor/device.h>
+#include <spa/node/node.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/debug/dict.h>
+
+#include "pipewire/pipewire.h"
+
+#define NAME "bluez-session"
+
+struct impl;
+struct object;
+
+struct node {
+ struct impl *impl;
+ struct object *object;
+ struct spa_list link;
+ uint32_t id;
+
+ struct spa_handle *handle;
+ struct pw_proxy *proxy;
+ struct spa_node *node;
+};
+
+struct object {
+ struct impl *impl;
+ struct spa_list link;
+ uint32_t id;
+
+ struct spa_handle *handle;
+ struct pw_proxy *proxy;
+ struct spa_device *device;
+ struct spa_hook listener;
+
+ struct spa_list node_list;
+};
+
+struct impl {
+ struct timespec now;
+
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct spa_handle *handle;
+ struct spa_device *device;
+ struct spa_hook listener;
+
+ struct spa_list device_list;
+};
+
+static struct node *find_node(struct object *obj, uint32_t id)
+{
+ struct node *node;
+
+ spa_list_for_each(node, &obj->node_list, link) {
+ if (node->id == id)
+ return node;
+ }
+ return NULL;
+}
+
+static void update_node(struct object *obj, struct node *node,
+ const struct spa_device_object_info *info)
+{
+ pw_log_debug("update node %u", node->id);
+ spa_debug_dict(0, info->props);
+}
+
+static struct node *create_node(struct object *obj, uint32_t id,
+ const struct spa_device_object_info *info)
+{
+ struct node *node;
+ struct impl *impl = obj->impl;
+ struct pw_context *context = impl->context;
+ struct spa_handle *handle;
+ int res;
+ void *iface;
+
+ pw_log_debug("new node %u", id);
+
+ if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Node))
+ return NULL;
+
+ handle = pw_context_load_spa_handle(context,
+ info->factory_name,
+ info->props);
+ if (handle == NULL) {
+ pw_log_error("can't make factory instance: %m");
+ goto exit;
+ }
+
+ if ((res = spa_handle_get_interface(handle, info->type, &iface)) < 0) {
+ pw_log_error("can't get %s interface: %s", info->type, spa_strerror(res));
+ goto unload_handle;
+ }
+
+ node = calloc(1, sizeof(*node));
+ if (node == NULL)
+ goto unload_handle;
+
+ node->impl = impl;
+ node->object = obj;
+ node->id = id;
+ node->handle = handle;
+ node->node = iface;
+ node->proxy = pw_core_export(impl->core,
+ info->type, info->props, node->node, 0);
+ if (node->proxy == NULL)
+ goto clean_node;
+
+ spa_list_append(&obj->node_list, &node->link);
+
+ update_node(obj, node, info);
+
+ return node;
+
+clean_node:
+ free(node);
+unload_handle:
+ pw_unload_spa_handle(handle);
+exit:
+ return NULL;
+}
+
+static void remove_node(struct object *obj, struct node *node)
+{
+ pw_log_debug("remove node %u", node->id);
+ spa_list_remove(&node->link);
+ pw_proxy_destroy(node->proxy);
+ free(node->handle);
+ free(node);
+}
+
+static void device_object_info(void *data, uint32_t id,
+ const struct spa_device_object_info *info)
+{
+ struct object *obj = data;
+ struct node *node;
+
+ node = find_node(obj, id);
+
+ if (info == NULL) {
+ if (node == NULL) {
+ pw_log_warn("object %p: unknown node %u", obj, id);
+ return;
+ }
+ remove_node(obj, node);
+ } else if (node == NULL) {
+ create_node(obj, id, info);
+ } else {
+ update_node(obj, node, info);
+ }
+
+}
+
+static const struct spa_device_events device_events = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .object_info = device_object_info
+};
+
+static struct object *find_object(struct impl *impl, uint32_t id)
+{
+ struct object *obj;
+
+ spa_list_for_each(obj, &impl->device_list, link) {
+ if (obj->id == id)
+ return obj;
+ }
+ return NULL;
+}
+
+static void update_object(struct impl *impl, struct object *obj,
+ const struct spa_device_object_info *info)
+{
+ pw_log_debug("update object %u", obj->id);
+ spa_debug_dict(0, info->props);
+}
+
+static struct object *create_object(struct impl *impl, uint32_t id,
+ const struct spa_device_object_info *info)
+{
+ struct pw_context *context = impl->context;
+ struct object *obj;
+ struct spa_handle *handle;
+ int res;
+ void *iface;
+
+ pw_log_debug("new object %u", id);
+
+ if (!spa_streq(info->type, SPA_TYPE_INTERFACE_Device))
+ return NULL;
+
+ handle = pw_context_load_spa_handle(context,
+ info->factory_name,
+ info->props);
+ if (handle == NULL) {
+ pw_log_error("can't make factory instance: %m");
+ goto exit;
+ }
+
+ if ((res = spa_handle_get_interface(handle, info->type, &iface)) < 0) {
+ pw_log_error("can't get %s interface: %s", info->type, spa_strerror(res));
+ goto unload_handle;
+ }
+
+ obj = calloc(1, sizeof(*obj));
+ if (obj == NULL)
+ goto unload_handle;
+
+ obj->impl = impl;
+ obj->id = id;
+ obj->handle = handle;
+ obj->device = iface;
+ obj->proxy = pw_core_export(impl->core,
+ info->type, info->props, obj->device, 0);
+ if (obj->proxy == NULL)
+ goto clean_object;
+
+ spa_list_init(&obj->node_list);
+
+ spa_device_add_listener(obj->device,
+ &obj->listener, &device_events, obj);
+
+ spa_list_append(&impl->device_list, &obj->link);
+
+ update_object(impl, obj, info);
+
+ return obj;
+
+clean_object:
+ free(obj);
+unload_handle:
+ pw_unload_spa_handle(handle);
+exit:
+ return NULL;
+}
+
+static void remove_object(struct impl *impl, struct object *obj)
+{
+ pw_log_debug("remove object %u", obj->id);
+ spa_list_remove(&obj->link);
+ spa_hook_remove(&obj->listener);
+ pw_proxy_destroy(obj->proxy);
+ free(obj->handle);
+ free(obj);
+}
+
+static void dbus_device_object_info(void *data, uint32_t id,
+ const struct spa_device_object_info *info)
+{
+ struct impl *impl = data;
+ struct object *obj;
+
+ obj = find_object(impl, id);
+
+ if (info == NULL) {
+ if (obj == NULL)
+ return;
+ remove_object(impl, obj);
+ } else if (obj == NULL) {
+ if (create_object(impl, id, info) == NULL)
+ return;
+ } else {
+ update_object(impl, obj, info);
+ }
+}
+
+static const struct spa_device_events dbus_device_events =
+{
+ SPA_VERSION_DEVICE_EVENTS,
+ .object_info = dbus_device_object_info,
+};
+
+static int start_monitor(struct impl *impl)
+{
+ struct spa_handle *handle;
+ int res;
+ void *iface;
+
+ handle = pw_context_load_spa_handle(impl->context, SPA_NAME_API_BLUEZ5_ENUM_DBUS, NULL);
+ if (handle == NULL) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Device, &iface)) < 0) {
+ pw_log_error("can't get MONITOR interface: %d", res);
+ goto out_unload;
+ }
+
+ impl->handle = handle;
+ impl->device = iface;
+
+ spa_device_add_listener(impl->device, &impl->listener, &dbus_device_events, impl);
+
+ return 0;
+
+ out_unload:
+ pw_unload_spa_handle(handle);
+ out:
+ return res;
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(impl->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+int main(int argc, char *argv[])
+{
+ struct impl impl = { 0, };
+ int res;
+
+ pw_init(&argc, &argv);
+
+ impl.loop = pw_main_loop_new(NULL);
+ impl.context = pw_context_new(pw_main_loop_get_loop(impl.loop), NULL, 0);
+
+ clock_gettime(CLOCK_MONOTONIC, &impl.now);
+
+ spa_list_init(&impl.device_list);
+
+ impl.core = pw_context_connect(impl.context, NULL, 0);
+ if (impl.core == NULL) {
+ pw_log_error(NAME" %p: can't connect %m", &impl);
+ return -1;
+ }
+
+ pw_core_add_listener(impl.core,
+ &impl.core_listener,
+ &core_events, &impl);
+
+ if ((res = start_monitor(&impl)) < 0) {
+ pw_log_error(NAME" %p: error starting monitor: %s", &impl, spa_strerror(res));
+ return -1;
+ }
+
+ pw_main_loop_run(impl.loop);
+
+ pw_context_destroy(impl.context);
+ pw_main_loop_destroy(impl.loop);
+
+ return 0;
+}
diff --git a/src/examples/export-sink.c b/src/examples/export-sink.c
new file mode 100644
index 0000000..ee8a57c
--- /dev/null
+++ b/src/examples/export-sink.c
@@ -0,0 +1,584 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Exporting and implementing a video sink SPA node, using \ref api_pw_core.
+ [title]
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <sys/mman.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/format.h>
+#include <spa/debug/pod.h>
+
+#include <pipewire/pipewire.h>
+
+#define WIDTH 640
+#define HEIGHT 480
+#define BPP 3
+
+#include "sdl.h"
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+#define MAX_BUFFERS 64
+
+#define DEFAULT_PARAM 0.1
+
+struct props {
+ double param;
+};
+
+static void reset_props(struct props *props)
+{
+ props->param = DEFAULT_PARAM;
+}
+
+struct data {
+ struct props props;
+
+ const char *path;
+
+ SDL_Renderer *renderer;
+ SDL_Window *window;
+ SDL_Texture *texture;
+
+ struct pw_main_loop *loop;
+
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct spa_node impl_node;
+ struct spa_hook_list hooks;
+ struct spa_io_buffers *io;
+ struct spa_io_sequence *io_notify;
+ uint32_t io_notify_size;
+ double param_accum;
+
+ uint8_t buffer[1024];
+
+ struct spa_video_info_raw format;
+ int32_t stride;
+
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_region region;
+
+ struct spa_buffer *buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+};
+
+static void handle_events(struct data *data)
+{
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ pw_main_loop_quit(data->loop);
+ break;
+ }
+ }
+}
+
+static void update_param(struct data *data)
+{
+ struct spa_pod_builder b = { 0, };
+ struct spa_pod_frame f[2];
+
+ if (data->io_notify == NULL)
+ return;
+
+ spa_pod_builder_init(&b, data->io_notify, data->io_notify_size);
+ spa_pod_builder_push_sequence(&b, &f[0], 0);
+ spa_pod_builder_control(&b, 0, SPA_CONTROL_Properties);
+ spa_pod_builder_push_object(&b, &f[1], SPA_TYPE_OBJECT_Props, 0);
+ spa_pod_builder_prop(&b, SPA_PROP_contrast, 0);
+ spa_pod_builder_float(&b, (sin(data->param_accum) * 127.0) + 127.0);
+ spa_pod_builder_pop(&b, &f[1]);
+ spa_pod_builder_pop(&b, &f[0]);
+
+ data->param_accum += M_PI_M2 / 30.0;
+ if (data->param_accum >= M_PI_M2)
+ data->param_accum -= M_PI_M2;
+}
+
+static int impl_send_command(void *object, const struct spa_command *command)
+{
+ return 0;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct data *d = object;
+ struct spa_hook_list save;
+ uint64_t old;
+
+ spa_hook_list_isolate(&d->hooks, &save, listener, events, data);
+
+ old = d->info.change_mask;
+ d->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ spa_node_emit_port_info(&d->hooks, SPA_DIRECTION_INPUT, 0, &d->info);
+ d->info.change_mask = old;
+
+ spa_hook_list_join(&d->hooks, &save);
+
+ return 0;
+}
+
+static int impl_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks, void *data)
+{
+ return 0;
+}
+
+static int impl_set_io(void *object,
+ uint32_t id, void *data, size_t size)
+{
+ return 0;
+}
+
+static int impl_port_set_io(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct data *d = object;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ d->io = data;
+ break;
+ case SPA_IO_Notify:
+ d->io_notify = data;
+ d->io_notify_size = size;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_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 data *d = 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;
+
+ 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:
+ {
+ SDL_RendererInfo info;
+
+ if (result.index != 0)
+ return 0;
+
+ SDL_GetRendererInfo(d->renderer, &info);
+ param = sdl_build_formats(&info, &b);
+ break;
+ }
+ case SPA_PARAM_Format:
+ if (result.index != 0 || d->format.format == 0)
+ return 0;
+ param = spa_format_video_raw_build(&b, id, &d->format);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (result.index != 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(d->stride * d->format.size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(d->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;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Notify),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_sequence) + 1024));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&d->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ 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 data *d = object;
+ Uint32 sdl_format;
+ void *dest;
+
+ if (format == NULL) {
+ spa_zero(d->format);
+ SDL_DestroyTexture(d->texture);
+ d->texture = NULL;
+ } else {
+ spa_debug_format(0, NULL, format);
+
+ spa_format_video_raw_parse(format, &d->format);
+
+ sdl_format = id_to_sdl_format(d->format.format);
+ if (sdl_format == SDL_PIXELFORMAT_UNKNOWN)
+ return -EINVAL;
+ if (d->format.size.width == 0 ||
+ d->format.size.height == 0)
+ return -EINVAL;
+
+ d->texture = SDL_CreateTexture(d->renderer,
+ sdl_format,
+ SDL_TEXTUREACCESS_STREAMING,
+ d->format.size.width,
+ d->format.size.height);
+ SDL_LockTexture(d->texture, NULL, &dest, &d->stride);
+ SDL_UnlockTexture(d->texture);
+
+ }
+ d->info.change_mask = SPA_PORT_CHANGE_MASK_PARAMS;
+ if (format) {
+ d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+
+ spa_node_emit_port_info(&d->hooks, direction, port_id, &d->info);
+ d->info.change_mask = 0;
+
+ return 0;
+}
+
+static int impl_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(object, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int impl_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 data *d = object;
+ uint32_t i;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++)
+ d->buffers[i] = buffers[i];
+ d->n_buffers = n_buffers;
+ return 0;
+}
+
+static int do_render(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *_data, size_t size, void *user_data)
+{
+ struct data *d = user_data;
+ const struct spa_buffer *buf = *(struct spa_buffer**)_data;
+ uint8_t *map;
+ void *sdata, *ddata;
+ int sstride, dstride, ostride;
+ uint32_t i;
+ uint8_t *src, *dst;
+ struct spa_meta *m;
+ struct spa_meta_region *r;
+
+ handle_events(d);
+
+ if (buf->datas[0].type == SPA_DATA_MemFd ||
+ buf->datas[0].type == SPA_DATA_DmaBuf) {
+ map = mmap(NULL, buf->datas[0].maxsize + buf->datas[0].mapoffset, PROT_READ,
+ MAP_PRIVATE, buf->datas[0].fd, 0);
+ sdata = SPA_PTROFF(map, buf->datas[0].mapoffset, uint8_t);
+ } else if (buf->datas[0].type == SPA_DATA_MemPtr) {
+ map = NULL;
+ sdata = buf->datas[0].data;
+ } else
+ return -EINVAL;
+
+ if (SDL_LockTexture(d->texture, NULL, &ddata, &dstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ return -EIO;
+ }
+
+ if ((m = spa_buffer_find_meta(buf, SPA_META_VideoDamage))) {
+ spa_meta_for_each(r, m) {
+ if (!spa_meta_region_is_valid(r))
+ break;
+ if (memcmp(&r->region, &d->region, sizeof(struct spa_region)) == 0)
+ break;
+ d->region = r->region;
+ fprintf(stderr, "region %dx%d->%dx%d\n",
+ r->region.position.x, r->region.position.y,
+ r->region.size.width, r->region.size.height);
+ }
+ }
+
+ sstride = buf->datas[0].chunk->stride;
+ ostride = SPA_MIN(sstride, dstride);
+
+ src = sdata;
+ dst = ddata;
+ for (i = 0; i < d->format.size.height; i++) {
+ memcpy(dst, src, ostride);
+ src += sstride;
+ dst += dstride;
+ }
+ SDL_UnlockTexture(d->texture);
+
+ SDL_RenderClear(d->renderer);
+ SDL_RenderCopy(d->renderer, d->texture, NULL, NULL);
+ SDL_RenderPresent(d->renderer);
+
+ if (map)
+ munmap(map, buf->datas[0].maxsize + buf->datas[0].mapoffset);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct data *d = object;
+ struct spa_buffer *buf;
+ int res;
+
+ if (d->io->status != SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_NEED_DATA;
+
+ if (d->io->buffer_id >= d->n_buffers)
+ return SPA_STATUS_NEED_DATA;
+
+ buf = d->buffers[d->io->buffer_id];
+
+ if ((res = pw_loop_invoke(pw_main_loop_get_loop(d->loop), do_render,
+ SPA_ID_INVALID, &buf, sizeof(struct spa_buffer *),
+ false, d)) < 0)
+ return res;
+
+ update_param(d);
+
+ return d->io->status = SPA_STATUS_NEED_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_add_listener,
+ .set_callbacks = impl_set_callbacks,
+ .set_io = impl_set_io,
+ .send_command = impl_send_command,
+ .port_set_io = impl_port_set_io,
+ .port_enum_params = impl_port_enum_params,
+ .port_set_param = impl_port_set_param,
+ .port_use_buffers = impl_port_use_buffers,
+ .process = impl_node_process,
+};
+
+static void make_node(struct data *data)
+{
+ struct pw_properties *props;
+
+ props = pw_properties_new(PW_KEY_NODE_AUTOCONNECT, "true", NULL);
+ if (data->path)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, data->path);
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Stream/Input/Video");
+ pw_properties_set(props, PW_KEY_MEDIA_TYPE, "Video");
+ pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, "Capture");
+ pw_properties_set(props, PW_KEY_MEDIA_ROLE, "Camera");
+
+ data->impl_node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, data);
+ pw_core_export(data->core, SPA_TYPE_INTERFACE_Node,
+ &props->dict, &data->impl_node, 0);
+ pw_properties_free(props);
+}
+
+static void set_permissions(struct data *data)
+{
+ struct pw_permission permissions[2];
+
+ /* an example, set specific permissions on one object, this is the
+ * core object. */
+ permissions[0] = PW_PERMISSION_INIT(PW_ID_CORE, PW_PERM_R | PW_PERM_X);
+ /* remove WX from all other objects */
+ permissions[1] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_R);
+
+ pw_client_update_permissions(
+ pw_core_get_client(data->core),
+ 2, permissions);
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *d = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE)
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+ data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
+ data.path = argc > 1 ? argv[1] : NULL;
+
+ spa_hook_list_init(&data.hooks);
+
+ data.info = SPA_PORT_INFO_INIT();
+ data.info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS;
+ data.info.flags = 0;
+ data.info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ data.params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ data.params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ data.params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ data.params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ data.params[4] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ data.info.params = data.params;
+ data.info.n_params = 5;
+
+ reset_props(&data.props);
+
+ 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;
+ }
+
+ data.core = pw_context_connect(data.context, NULL, 0);
+ if (data.core == NULL) {
+ printf("can't connect: %m\n");
+ return -1;
+ }
+ pw_core_add_listener(data.core, &data.core_listener, &core_events, &data);
+
+ set_permissions(&data);
+
+ make_node(&data);
+
+ pw_main_loop_run(data.loop);
+
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ return 0;
+}
diff --git a/src/examples/export-source.c b/src/examples/export-source.c
new file mode 100644
index 0000000..eb8fdb5
--- /dev/null
+++ b/src/examples/export-source.c
@@ -0,0 +1,566 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Exporting and implementing a video source SPA node, using \ref api_pw_core.
+ [title]
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <math.h>
+#include <sys/mman.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/pod/filter.h>
+#include <spa/debug/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+#define BUFFER_SAMPLES 128
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+ struct spa_buffer *buffer;
+ struct spa_list link;
+ void *ptr;
+ bool mapped;
+};
+
+struct data {
+ const char *path;
+
+ struct pw_main_loop *loop;
+
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_dict_item items[1];
+ struct spa_dict dict;
+ struct spa_param_info params[5];
+
+ struct spa_node impl_node;
+ struct spa_hook_list hooks;
+ struct spa_io_buffers *io;
+ struct spa_io_control *io_notify;
+ uint32_t io_notify_size;
+
+ struct spa_audio_info_raw format;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+ struct spa_list empty;
+
+ double accumulator;
+ double volume_accum;
+};
+
+static void update_volume(struct data *data)
+{
+ struct spa_pod_builder b = { 0, };
+ struct spa_pod_frame f[2];
+
+ if (data->io_notify == NULL)
+ return;
+
+ spa_pod_builder_init(&b, data->io_notify, data->io_notify_size);
+ spa_pod_builder_push_sequence(&b, &f[0], 0);
+ spa_pod_builder_control(&b, 0, SPA_CONTROL_Properties);
+ spa_pod_builder_push_object(&b, &f[1], SPA_TYPE_OBJECT_Props, 0);
+ spa_pod_builder_prop(&b, SPA_PROP_volume, 0);
+ spa_pod_builder_float(&b, (sin(data->volume_accum) / 2.0) + 0.5);
+ spa_pod_builder_pop(&b, &f[1]);
+ spa_pod_builder_pop(&b, &f[0]);
+
+ data->volume_accum += M_PI_M2 / 1000.0;
+ if (data->volume_accum >= M_PI_M2)
+ data->volume_accum -= M_PI_M2;
+}
+
+static int impl_send_command(void *object, const struct spa_command *command)
+{
+ return 0;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct data *d = object;
+ struct spa_hook_list save;
+ uint64_t old;
+
+ spa_hook_list_isolate(&d->hooks, &save, listener, events, data);
+
+ old = d->info.change_mask;
+ d->info.change_mask = d->info_all;
+ spa_node_emit_port_info(&d->hooks, SPA_DIRECTION_OUTPUT, 0, &d->info);
+ d->info.change_mask = old;
+
+ spa_hook_list_join(&d->hooks, &save);
+ return 0;
+}
+
+static int impl_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks, void *data)
+{
+ return 0;
+}
+
+static int impl_set_io(void *object,
+ uint32_t id, void *data, size_t size)
+{
+ return 0;
+}
+
+static int impl_port_set_io(void *object, enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct data *d = object;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ d->io = data;
+ break;
+ case SPA_IO_Notify:
+ d->io_notify = data;
+ d->io_notify_size = size;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_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 data *d = 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;
+
+ 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 (result.index != 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(5,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_S16P,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_F32P,
+ SPA_AUDIO_FORMAT_F32),
+ SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, INT32_MAX),
+ SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(44100, 1, INT32_MAX));
+ break;
+
+ case SPA_PARAM_Format:
+ if (result.index != 0)
+ return 0;
+ if (d->format.format == 0)
+ return 0;
+ param = spa_format_audio_raw_build(&b, id, &d->format);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, 32),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ BUFFER_SAMPLES * sizeof(float), 32, INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(sizeof(float)));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Notify),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_sequence) + 1024));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&d->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ 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 data *d = object;
+
+ if (format == NULL) {
+ spa_zero(d->format);
+ } else {
+ spa_debug_format(0, NULL, format);
+
+ if (spa_format_audio_raw_parse(format, &d->format) < 0)
+ return -EINVAL;
+
+ if (d->format.format != SPA_AUDIO_FORMAT_S16 &&
+ d->format.format != SPA_AUDIO_FORMAT_F32)
+ return -EINVAL;
+ if (d->format.rate == 0 ||
+ d->format.channels == 0 ||
+ d->format.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -EINVAL;
+ }
+
+ d->info.change_mask = SPA_PORT_CHANGE_MASK_PARAMS;
+ if (format) {
+ d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ d->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ d->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ spa_node_emit_port_info(&d->hooks, SPA_DIRECTION_OUTPUT, 0, &d->info);
+
+ return 0;
+}
+
+static int impl_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(object, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int impl_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 data *d = object;
+ uint32_t i;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &d->buffers[i];
+ struct spa_data *datas = buffers[i]->datas;
+
+ if (datas[0].data != NULL) {
+ b->ptr = datas[0].data;
+ b->mapped = false;
+ }
+ else if (datas[0].type == SPA_DATA_MemFd ||
+ datas[0].type == SPA_DATA_DmaBuf) {
+ b->ptr = mmap(NULL, datas[0].maxsize + datas[0].mapoffset, PROT_WRITE,
+ MAP_SHARED, datas[0].fd, 0);
+ if (b->ptr == MAP_FAILED) {
+ pw_log_error("failed to buffer mem");
+ return -errno;
+
+ }
+ b->ptr = SPA_PTROFF(b->ptr, datas[0].mapoffset, void);
+ b->mapped = true;
+ }
+ else {
+ pw_log_error("invalid buffer mem");
+ return -EINVAL;
+ }
+ b->id = i;
+ b->buffer = buffers[i];
+ pw_log_debug("got buffer %d size %d", i, datas[0].maxsize);
+ spa_list_append(&d->empty, &b->link);
+ }
+ d->n_buffers = n_buffers;
+ return 0;
+}
+
+static inline void reuse_buffer(struct data *d, uint32_t id)
+{
+ pw_log_trace("export-source %p: recycle buffer %d", d, id);
+ spa_list_append(&d->empty, &d->buffers[id].link);
+}
+
+static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct data *d = object;
+ reuse_buffer(d, buffer_id);
+ return 0;
+}
+
+static void fill_f32(struct data *d, void *dest, int avail)
+{
+ float *dst = dest;
+ int n_samples = avail / (sizeof(float) * d->format.channels);
+ int i;
+ uint32_t c;
+
+ for (i = 0; i < n_samples; i++) {
+ float val;
+
+ d->accumulator += M_PI_M2 * 440 / d->format.rate;
+ if (d->accumulator >= M_PI_M2)
+ d->accumulator -= M_PI_M2;
+
+ val = sin(d->accumulator);
+
+ for (c = 0; c < d->format.channels; c++)
+ *dst++ = val;
+ }
+}
+
+static void fill_s16(struct data *d, void *dest, int avail)
+{
+ int16_t *dst = dest;
+ int n_samples = avail / (sizeof(int16_t) * d->format.channels);
+ int i;
+ uint32_t c;
+
+ for (i = 0; i < n_samples; i++) {
+ int16_t val;
+
+ d->accumulator += M_PI_M2 * 440 / d->format.rate;
+ if (d->accumulator >= M_PI_M2)
+ d->accumulator -= M_PI_M2;
+
+ val = (int16_t) (sin(d->accumulator) * 32767.0);
+
+ for (c = 0; c < d->format.channels; c++)
+ *dst++ = val;
+ }
+}
+
+static int impl_node_process(void *object)
+{
+ struct data *d = object;
+ struct buffer *b;
+ int avail;
+ struct spa_io_buffers *io = d->io;
+ uint32_t maxsize, index = 0;
+ uint32_t filled, offset;
+ struct spa_data *od;
+
+ if (io->buffer_id < d->n_buffers) {
+ reuse_buffer(d, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+ if (spa_list_is_empty(&d->empty)) {
+ pw_log_error("export-source %p: out of buffers", d);
+ return -EPIPE;
+ }
+ b = spa_list_first(&d->empty, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ od = b->buffer->datas;
+
+ maxsize = od[0].maxsize;
+
+ filled = 0;
+ index = 0;
+ avail = maxsize - filled;
+ offset = index % maxsize;
+
+ if (offset + avail > maxsize)
+ avail = maxsize - offset;
+
+ if (d->format.format == SPA_AUDIO_FORMAT_S16)
+ fill_s16(d, SPA_PTROFF(b->ptr, offset, void), avail);
+ else if (d->format.format == SPA_AUDIO_FORMAT_F32)
+ fill_f32(d, SPA_PTROFF(b->ptr, offset, void), avail);
+
+ od[0].chunk->offset = 0;
+ od[0].chunk->size = avail;
+ od[0].chunk->stride = 0;
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ update_volume(d);
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_add_listener,
+ .set_callbacks = impl_set_callbacks,
+ .set_io = impl_set_io,
+ .send_command = impl_send_command,
+ .port_set_io = impl_port_set_io,
+ .port_enum_params = impl_port_enum_params,
+ .port_set_param = impl_port_set_param,
+ .port_use_buffers = impl_port_use_buffers,
+ .port_reuse_buffer = impl_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static void make_node(struct data *data)
+{
+ struct pw_properties *props;
+
+ props = pw_properties_new(PW_KEY_NODE_AUTOCONNECT, "true",
+ PW_KEY_NODE_EXCLUSIVE, "true",
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ NULL);
+ if (data->path)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, data->path);
+
+ data->impl_node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, data);
+ pw_core_export(data->core, SPA_TYPE_INTERFACE_Node,
+ &props->dict, &data->impl_node, 0);
+ pw_properties_free(props);
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *d = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE)
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+ data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
+ data.path = argc > 1 ? argv[1] : NULL;
+
+ data.info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ data.info = SPA_PORT_INFO_INIT();
+ data.info.flags = 0;
+ data.items[0] = SPA_DICT_ITEM_INIT(PW_KEY_FORMAT_DSP, "32 bit float mono audio");
+ data.dict = SPA_DICT_INIT_ARRAY(data.items);
+ data.info.props = &data.dict;
+ data.params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ data.params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ data.params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ data.params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ data.params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ data.info.params = data.params;
+ data.info.n_params = 5;
+
+ spa_list_init(&data.empty);
+ spa_hook_list_init(&data.hooks);
+
+ if ((data.core = pw_context_connect(data.context, NULL, 0)) == NULL) {
+ printf("can't connect: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core, &data.core_listener, &core_events, &data);
+
+ make_node(&data);
+
+ pw_main_loop_run(data.loop);
+
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ return 0;
+}
diff --git a/src/examples/export-spa-device.c b/src/examples/export-spa-device.c
new file mode 100644
index 0000000..4629a02
--- /dev/null
+++ b/src/examples/export-spa-device.c
@@ -0,0 +1,144 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Exporting and loading a SPA device, using \ref api_pw_core.
+ [title]
+ */
+
+#include <stdio.h>
+#include <sys/mman.h>
+#include <signal.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+
+#include <pipewire/impl.h>
+
+struct data {
+ struct pw_main_loop *loop;
+
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_impl_device *device;
+ const char *library;
+ const char *factory;
+ const char *path;
+};
+
+static int make_device(struct data *data)
+{
+ struct pw_impl_factory *factory;
+ struct pw_properties *props;
+
+ factory = pw_context_find_factory(data->context, "spa-device-factory");
+ if (factory == NULL)
+ return -1;
+
+ props = pw_properties_new(SPA_KEY_LIBRARY_NAME, data->library,
+ SPA_KEY_FACTORY_NAME, data->factory, NULL);
+
+ data->device = pw_impl_factory_create_object(factory,
+ NULL,
+ PW_TYPE_INTERFACE_Device,
+ PW_VERSION_DEVICE,
+ props, SPA_ID_INVALID);
+
+ pw_core_export(data->core, SPA_TYPE_INTERFACE_Device, NULL,
+ pw_impl_device_get_implementation(data->device), 0);
+
+ return 0;
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *d = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE)
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ struct pw_loop *l;
+
+ pw_init(&argc, &argv);
+
+ if (argc < 3) {
+ fprintf(stderr, "usage: %s <library> <factory>\n\n"
+ "\texample: %s v4l2/libspa-v4l2 api.v4l2.device\n\n",
+ argv[0], argv[0]);
+ return -1;
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit, &data);
+ data.context = pw_context_new(l, NULL, 0);
+ data.library = argv[1];
+ data.factory = argv[2];
+
+ pw_context_load_module(data.context, "libpipewire-module-spa-device-factory", NULL, NULL);
+
+ data.core = pw_context_connect(data.context, NULL, 0);
+ if (data.core == NULL) {
+ pw_log_error("can't connect %m");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core, &data.core_listener, &core_events, &data);
+
+ if (make_device(&data) < 0) {
+ pw_log_error("can't make device");
+ return -1;
+ }
+
+ pw_main_loop_run(data.loop);
+
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ return 0;
+}
diff --git a/src/examples/export-spa.c b/src/examples/export-spa.c
new file mode 100644
index 0000000..e2f27cf
--- /dev/null
+++ b/src/examples/export-spa.c
@@ -0,0 +1,183 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Exporting and loading a SPA node, using \ref api_pw_core.
+ [title]
+ */
+
+#include <stdio.h>
+#include <sys/mman.h>
+#include <signal.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+
+#include <pipewire/impl.h>
+
+struct data {
+ struct pw_main_loop *loop;
+
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct spa_node *node;
+ const char *library;
+ const char *factory;
+ const char *path;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+ uint32_t id;
+};
+
+static void proxy_event_bound(void *_data, uint32_t global_id)
+{
+ struct data *data = _data;
+ if (data->id != global_id) {
+ printf("node id: %u\n", global_id);
+ data->id = global_id;
+ }
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .bound = proxy_event_bound,
+};
+
+static int make_node(struct data *data)
+{
+ struct pw_properties *props;
+ struct spa_handle *hndl;
+ void *iface;
+ int res;
+
+ props = pw_properties_new(SPA_KEY_LIBRARY_NAME, data->library,
+ SPA_KEY_FACTORY_NAME, data->factory,
+ NULL);
+
+
+ hndl = pw_context_load_spa_handle(data->context, data->factory, &props->dict);
+ if (hndl == NULL)
+ return -errno;
+
+ if ((res = spa_handle_get_interface(hndl, SPA_TYPE_INTERFACE_Node, &iface)) < 0)
+ return res;
+
+ data->node = iface;
+
+ if (data->path) {
+ pw_properties_set(props, PW_KEY_NODE_AUTOCONNECT, "true");
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, data->path);
+ }
+
+ data->proxy = pw_core_export(data->core,
+ SPA_TYPE_INTERFACE_Node, &props->dict,
+ data->node, 0);
+ pw_properties_free(props);
+
+ if (data->proxy == NULL)
+ return -errno;
+
+ pw_proxy_add_listener(data->proxy,
+ &data->proxy_listener, &proxy_events, data);
+
+ return 0;
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *d = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE)
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ struct pw_loop *l;
+
+ pw_init(&argc, &argv);
+
+ if (argc < 3) {
+ fprintf(stderr, "usage: %s <library> <factory> [path]\n\n"
+ "\texample: %s v4l2/libspa-v4l2 api.v4l2.source\n\n",
+ argv[0], argv[0]);
+ return -1;
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit, &data);
+ data.context = pw_context_new(l, NULL, 0);
+ data.library = argv[1];
+ data.factory = argv[2];
+ if (argc > 3)
+ data.path = argv[3];
+
+ pw_context_load_module(data.context, "libpipewire-module-spa-node-factory", NULL, NULL);
+
+ data.core = pw_context_connect(data.context, NULL, 0);
+ if (data.core == NULL) {
+ printf("can't connect: %m\n");
+ return -1;
+ }
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+
+ if (make_node(&data) < 0) {
+ pw_log_error("can't make node");
+ return -1;
+ }
+
+ pw_main_loop_run(data.loop);
+
+ pw_proxy_destroy(data.proxy);
+ pw_core_disconnect(data.core);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ return 0;
+}
diff --git a/src/examples/local-v4l2.c b/src/examples/local-v4l2.c
new file mode 100644
index 0000000..1d70e9e
--- /dev/null
+++ b/src/examples/local-v4l2.c
@@ -0,0 +1,469 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Using libspa-v4l2
+ [title]
+ */
+
+#include <stdio.h>
+#include <sys/mman.h>
+
+#define WIDTH 640
+#define HEIGHT 480
+#define BPP 3
+#define MAX_BUFFERS 32
+
+#include "sdl.h"
+
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/pod/filter.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/debug/format.h>
+#include <spa/utils/names.h>
+
+#include <pipewire/impl.h>
+
+struct data {
+ SDL_Renderer *renderer;
+ SDL_Window *window;
+ SDL_Texture *texture;
+
+ struct pw_main_loop *loop;
+
+ struct pw_context *context;
+ struct pw_core *core;
+
+ struct spa_port_info info;
+ struct spa_param_info params[4];
+
+ struct spa_node impl_node;
+ struct spa_io_buffers *io;
+
+ struct spa_hook_list hooks;
+
+ struct spa_video_info_raw format;
+ int32_t stride;
+
+ struct spa_buffer *buffers[MAX_BUFFERS];
+ int n_buffers;
+
+ struct pw_proxy *out, *in, *link;
+};
+
+static void handle_events(struct data *data)
+{
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ pw_main_loop_quit(data->loop);
+ break;
+ }
+ }
+}
+
+static int impl_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return 0;
+}
+
+static int impl_send_command(void *object, const struct spa_command *command)
+{
+ return 0;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct data *d = object;
+ struct spa_hook_list save;
+
+ spa_hook_list_isolate(&d->hooks, &save, listener, events, data);
+
+ spa_node_emit_port_info(&d->hooks, SPA_DIRECTION_INPUT, 0, &d->info);
+
+ spa_hook_list_join(&d->hooks, &save);
+
+ return 0;
+}
+
+static int impl_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks, void *data)
+{
+ return 0;
+}
+
+static int impl_port_set_io(void *object, enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct data *d = object;
+
+ if (id == SPA_IO_Buffers)
+ d->io = data;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_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 data *d = 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;
+
+ 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:
+ {
+ SDL_RendererInfo info;
+
+ if (result.index > 0)
+ return 0;
+
+ SDL_GetRendererInfo(d->renderer, &info);
+ param = sdl_build_formats(&info, &b);
+ break;
+ }
+ case SPA_PARAM_Buffers:
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, 32),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(d->stride * d->format.size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(d->stride));
+ break;
+
+ case SPA_PARAM_Meta:
+ if (result.index > 0)
+ return 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 -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&d->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ 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 data *d = object;
+ Uint32 sdl_format;
+ void *dest;
+
+ if (format == NULL) {
+ spa_zero(d->format);
+ SDL_DestroyTexture(d->texture);
+ d->texture = NULL;
+ } else {
+ spa_debug_format(0, NULL, format);
+
+ spa_format_video_raw_parse(format, &d->format);
+
+ sdl_format = id_to_sdl_format(d->format.format);
+ if (sdl_format == SDL_PIXELFORMAT_UNKNOWN)
+ return -EINVAL;
+ if (d->format.size.width == 0 ||
+ d->format.size.height == 0)
+ return -EINVAL;
+
+ d->texture = SDL_CreateTexture(d->renderer,
+ sdl_format,
+ SDL_TEXTUREACCESS_STREAMING,
+ d->format.size.width,
+ d->format.size.height);
+ SDL_LockTexture(d->texture, NULL, &dest, &d->stride);
+ SDL_UnlockTexture(d->texture);
+ }
+
+ d->info.change_mask = SPA_PORT_CHANGE_MASK_PARAMS;
+ if (format) {
+ d->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ d->params[2] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ d->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ d->params[2] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ spa_node_emit_port_info(&d->hooks, SPA_DIRECTION_INPUT, 0, &d->info);
+
+ return 0;
+}
+
+static int impl_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ if (id == SPA_PARAM_Format) {
+ return port_set_format(object, direction, port_id, flags, param);
+ }
+ else
+ return -ENOENT;
+}
+
+static int impl_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 data *d = object;
+ uint32_t i;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++)
+ d->buffers[i] = buffers[i];
+ d->n_buffers = n_buffers;
+ return 0;
+}
+
+static int do_render(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *_data, size_t size, void *user_data)
+{
+ struct data *d = user_data;
+ struct spa_buffer *buf;
+ uint8_t *map;
+ void *sdata, *ddata;
+ int sstride, dstride, ostride;
+ uint32_t i;
+ uint8_t *src, *dst;
+
+ buf = d->buffers[d->io->buffer_id];
+
+ if (buf->datas[0].type == SPA_DATA_MemFd ||
+ buf->datas[0].type == SPA_DATA_DmaBuf) {
+ map = mmap(NULL, buf->datas[0].maxsize + buf->datas[0].mapoffset, PROT_READ,
+ MAP_PRIVATE, buf->datas[0].fd, 0);
+ sdata = SPA_PTROFF(map, buf->datas[0].mapoffset, uint8_t);
+ } else if (buf->datas[0].type == SPA_DATA_MemPtr) {
+ map = NULL;
+ sdata = buf->datas[0].data;
+ } else
+ return -EINVAL;
+
+ if (SDL_LockTexture(d->texture, NULL, &ddata, &dstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ return -EIO;
+ }
+ sstride = buf->datas[0].chunk->stride;
+ ostride = SPA_MIN(sstride, dstride);
+
+ src = sdata;
+ dst = ddata;
+ for (i = 0; i < d->format.size.height; i++) {
+ memcpy(dst, src, ostride);
+ src += sstride;
+ dst += dstride;
+ }
+ SDL_UnlockTexture(d->texture);
+
+ SDL_RenderClear(d->renderer);
+ SDL_RenderCopy(d->renderer, d->texture, NULL, NULL);
+ SDL_RenderPresent(d->renderer);
+
+ if (map)
+ munmap(map, buf->datas[0].maxsize + buf->datas[0].mapoffset);
+
+ return 0;
+}
+
+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,
+ SPA_ID_INVALID, NULL, 0, true, d)) < 0)
+ return res;
+
+ handle_events(d);
+
+ d->io->status = SPA_STATUS_NEED_DATA;
+
+ return SPA_STATUS_NEED_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_add_listener,
+ .set_callbacks = impl_set_callbacks,
+ .set_io = impl_set_io,
+ .send_command = impl_send_command,
+ .port_set_io = impl_port_set_io,
+ .port_enum_params = impl_port_enum_params,
+ .port_set_param = impl_port_set_param,
+ .port_use_buffers = impl_port_use_buffers,
+ .process = impl_node_process,
+};
+
+static int make_nodes(struct data *data)
+{
+ struct pw_properties *props;
+
+ data->impl_node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, data);
+
+ data->info = SPA_PORT_INFO_INIT();
+ data->info.change_mask =
+ SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ data->info.flags = 0;
+ data->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ data->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ data->params[2] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ data->params[3] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ data->info.params = data->params;
+ data->info.n_params = SPA_N_ELEMENTS(data->params);
+
+ data->in = pw_core_export(data->core,
+ SPA_TYPE_INTERFACE_Node,
+ NULL,
+ &data->impl_node,
+ 0);
+
+ props = pw_properties_new(
+ SPA_KEY_LIBRARY_NAME, "v4l2/libspa-v4l2",
+ SPA_KEY_FACTORY_NAME, SPA_NAME_API_V4L2_SOURCE,
+ NULL);
+
+ data->out = pw_core_create_object(data->core,
+ "spa-node-factory",
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ &props->dict, 0);
+
+
+ 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_properties_clear(props);
+
+ pw_properties_setf(props,
+ PW_KEY_LINK_OUTPUT_NODE, "%d", pw_proxy_get_bound_id(data->out));
+ pw_properties_setf(props,
+ PW_KEY_LINK_INPUT_NODE, "%d", pw_proxy_get_bound_id(data->in));
+
+ data->link = pw_core_create_object(data->core,
+ "link-factory",
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ &props->dict, 0);
+
+ pw_properties_free(props);
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+ data.context = pw_context_new(
+ pw_main_loop_get_loop(data.loop),
+ pw_properties_new(
+ PW_KEY_CORE_DAEMON, "false",
+ NULL), 0);
+
+ spa_hook_list_init(&data.hooks);
+
+ 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);
+
+ 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;
+ }
+
+ data.core = pw_context_connect_self(data.context, NULL, 0);
+ if (data.core == NULL) {
+ printf("can't connect to core: %m\n");
+ return -1;
+ }
+
+ make_nodes(&data);
+
+ pw_main_loop_run(data.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_deinit();
+
+ return 0;
+}
diff --git a/src/examples/meson.build b/src/examples/meson.build
new file mode 100644
index 0000000..e2f2600
--- /dev/null
+++ b/src/examples/meson.build
@@ -0,0 +1,51 @@
+# Examples, in order from simple to complicated
+examples = [
+ 'audio-src',
+ 'audio-dsp-src',
+ 'audio-dsp-filter',
+ 'audio-capture',
+ 'video-play',
+ 'video-src',
+ 'video-dsp-play',
+ 'video-play-pull',
+ 'video-play-reneg',
+ 'video-src-alloc',
+ 'video-src-reneg',
+ 'video-src-fixate',
+ 'video-play-fixate',
+ 'export-sink',
+ 'export-source',
+ 'export-spa',
+ 'export-spa-device',
+ 'bluez-session',
+ 'local-v4l2',
+]
+
+examples_extra_deps = {
+ 'video-src-fixate': [drm_dep],
+ 'video-play': [sdl_dep],
+ 'video-play-reneg': [sdl_dep],
+ 'video-play-fixate': [sdl_dep, drm_dep],
+ 'video-play-pull': [sdl_dep],
+ 'video-dsp-play': [sdl_dep],
+ 'local-v4l2': [sdl_dep],
+ 'export-sink': [sdl_dep],
+}
+
+foreach c : examples
+ deps = examples_extra_deps.get(c, [])
+
+ found = true
+ foreach dep : deps
+ found = found and dep.found()
+ endforeach
+
+ if found
+ executable(
+ c, c + '.c',
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir / 'examples',
+ dependencies : [pipewire_dep, mathlib] + deps,
+ )
+ endif
+endforeach
diff --git a/src/examples/sdl.h b/src/examples/sdl.h
new file mode 100644
index 0000000..74ea74a
--- /dev/null
+++ b/src/examples/sdl.h
@@ -0,0 +1,198 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ SDL2 video format conversions
+ [title]
+ */
+
+#include <SDL2/SDL.h>
+
+#include <spa/utils/type.h>
+#include <spa/pod/builder.h>
+#include <spa/param/video/raw.h>
+#include <spa/param/video/format.h>
+
+static struct {
+ Uint32 format;
+ uint32_t id;
+} sdl_video_formats[] = {
+#if SDL_BYTEORDER == SDL_BIG_ENDIAN
+ { SDL_PIXELFORMAT_UNKNOWN, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX1LSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_UNKNOWN, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX1LSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX1MSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX4LSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX4MSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX8, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB332, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB555, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_BGR555, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_ARGB4444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGBA4444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_ABGR4444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_BGRA4444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_ARGB1555, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGBA5551, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_ABGR1555, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_BGRA5551, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB565, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_BGR565, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB24, SPA_VIDEO_FORMAT_RGB,},
+ { SDL_PIXELFORMAT_RGB888, SPA_VIDEO_FORMAT_RGB,},
+ { SDL_PIXELFORMAT_RGBX8888, SPA_VIDEO_FORMAT_RGBx,},
+ { SDL_PIXELFORMAT_BGR24, SPA_VIDEO_FORMAT_BGR,},
+ { SDL_PIXELFORMAT_BGR888, SPA_VIDEO_FORMAT_BGR,},
+ { SDL_PIXELFORMAT_BGRX8888, SPA_VIDEO_FORMAT_BGRx,},
+ { SDL_PIXELFORMAT_ARGB2101010, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGBA8888, SPA_VIDEO_FORMAT_RGBA,},
+ { SDL_PIXELFORMAT_ARGB8888, SPA_VIDEO_FORMAT_ARGB,},
+ { SDL_PIXELFORMAT_BGRA8888, SPA_VIDEO_FORMAT_BGRA,},
+ { SDL_PIXELFORMAT_ABGR8888, SPA_VIDEO_FORMAT_ABGR,},
+ { SDL_PIXELFORMAT_YV12, SPA_VIDEO_FORMAT_YV12,},
+ { SDL_PIXELFORMAT_IYUV, SPA_VIDEO_FORMAT_I420,},
+ { SDL_PIXELFORMAT_YUY2, SPA_VIDEO_FORMAT_YUY2,},
+ { SDL_PIXELFORMAT_UYVY, SPA_VIDEO_FORMAT_UYVY,},
+ { SDL_PIXELFORMAT_YVYU, SPA_VIDEO_FORMAT_YVYU,},
+#if SDL_VERSION_ATLEAST(2,0,4)
+ { SDL_PIXELFORMAT_NV12, SPA_VIDEO_FORMAT_NV12,},
+ { SDL_PIXELFORMAT_NV21, SPA_VIDEO_FORMAT_NV21,},
+#endif
+#else
+ { SDL_PIXELFORMAT_UNKNOWN, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX1LSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_UNKNOWN, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX1LSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX1MSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX4LSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX4MSB, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_INDEX8, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB332, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB555, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_BGR555, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_ARGB4444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGBA4444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_ABGR4444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_BGRA4444, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_ARGB1555, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGBA5551, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_ABGR1555, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_BGRA5551, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB565, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_BGR565, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGB24, SPA_VIDEO_FORMAT_BGR,},
+ { SDL_PIXELFORMAT_RGB888, SPA_VIDEO_FORMAT_BGR,},
+ { SDL_PIXELFORMAT_RGBX8888, SPA_VIDEO_FORMAT_xBGR,},
+ { SDL_PIXELFORMAT_BGR24, SPA_VIDEO_FORMAT_RGB,},
+ { SDL_PIXELFORMAT_BGR888, SPA_VIDEO_FORMAT_RGB,},
+ { SDL_PIXELFORMAT_BGRX8888, SPA_VIDEO_FORMAT_xRGB,},
+ { SDL_PIXELFORMAT_ARGB2101010, SPA_VIDEO_FORMAT_UNKNOWN,},
+ { SDL_PIXELFORMAT_RGBA8888, SPA_VIDEO_FORMAT_ABGR,},
+ { SDL_PIXELFORMAT_ARGB8888, SPA_VIDEO_FORMAT_BGRA,},
+ { SDL_PIXELFORMAT_BGRA8888, SPA_VIDEO_FORMAT_ARGB,},
+ { SDL_PIXELFORMAT_ABGR8888, SPA_VIDEO_FORMAT_RGBA,},
+ { SDL_PIXELFORMAT_YV12, SPA_VIDEO_FORMAT_YV12,},
+ { SDL_PIXELFORMAT_IYUV, SPA_VIDEO_FORMAT_I420,},
+ { SDL_PIXELFORMAT_YUY2, SPA_VIDEO_FORMAT_YUY2,},
+ { SDL_PIXELFORMAT_UYVY, SPA_VIDEO_FORMAT_UYVY,},
+ { SDL_PIXELFORMAT_YVYU, SPA_VIDEO_FORMAT_YVYU,},
+#if SDL_VERSION_ATLEAST(2,0,4)
+ { SDL_PIXELFORMAT_NV12, SPA_VIDEO_FORMAT_NV12,},
+ { SDL_PIXELFORMAT_NV21, SPA_VIDEO_FORMAT_NV21,},
+#endif
+#endif
+};
+
+static inline uint32_t sdl_format_to_id(Uint32 format)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(sdl_video_formats, f) {
+ if (f->format == format)
+ return f->id;
+ }
+ return SPA_VIDEO_FORMAT_UNKNOWN;
+}
+
+static inline Uint32 id_to_sdl_format(uint32_t id)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(sdl_video_formats, f) {
+ if (f->id == id)
+ return f->format;
+ }
+ return SDL_PIXELFORMAT_UNKNOWN;
+}
+
+static inline struct spa_pod *sdl_build_formats(SDL_RendererInfo *info, struct spa_pod_builder *b)
+{
+ uint32_t i, c;
+ struct spa_pod_frame f[2];
+
+ /* make an object of type SPA_TYPE_OBJECT_Format and id SPA_PARAM_EnumFormat.
+ * The object type is important because it defines the properties that are
+ * acceptable. The id gives more context about what the object is meant to
+ * contain. In this case we enumerate supported formats. */
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+ /* add media type and media subtype properties */
+ spa_pod_builder_prop(b, SPA_FORMAT_mediaType, 0);
+ spa_pod_builder_id(b, SPA_MEDIA_TYPE_video);
+ spa_pod_builder_prop(b, SPA_FORMAT_mediaSubtype, 0);
+ spa_pod_builder_id(b, SPA_MEDIA_SUBTYPE_raw);
+
+ /* build an enumeration of formats */
+ spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_format, 0);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+ /* first the formats supported by the textures */
+ for (i = 0, c = 0; i < info->num_texture_formats; i++) {
+ uint32_t id = sdl_format_to_id(info->texture_formats[i]);
+ if (id == 0)
+ continue;
+ if (c++ == 0)
+ spa_pod_builder_id(b, id);
+ spa_pod_builder_id(b, id);
+ }
+ /* then all the other ones SDL can convert from/to */
+ SPA_FOR_EACH_ELEMENT_VAR(sdl_video_formats, f) {
+ uint32_t id = f->id;
+ if (id != SPA_VIDEO_FORMAT_UNKNOWN)
+ spa_pod_builder_id(b, id);
+ }
+ spa_pod_builder_id(b, SPA_VIDEO_FORMAT_RGBA_F32);
+ spa_pod_builder_pop(b, &f[1]);
+ /* add size and framerate ranges */
+ spa_pod_builder_add(b,
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(WIDTH, HEIGHT),
+ &SPA_RECTANGLE(1,1),
+ &SPA_RECTANGLE(info->max_texture_width,
+ info->max_texture_height)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25,1),
+ &SPA_FRACTION(0,1),
+ &SPA_FRACTION(30,1)),
+ 0);
+ return spa_pod_builder_pop(b, &f[0]);
+}
diff --git a/src/examples/video-dsp-play.c b/src/examples/video-dsp-play.c
new file mode 100644
index 0000000..6068e2a
--- /dev/null
+++ b/src/examples/video-dsp-play.c
@@ -0,0 +1,315 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Video input stream using \ref pw_filter "pw_filter".
+ [title]
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/debug/format.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+
+#define WIDTH 640
+#define HEIGHT 480
+#define BPP 3
+
+#define MAX_BUFFERS 64
+
+#include "sdl.h"
+
+struct pixel {
+ float r, g, b, a;
+};
+
+struct data {
+ const char *target;
+
+ SDL_Renderer *renderer;
+ SDL_Window *window;
+ SDL_Texture *texture;
+ SDL_Texture *cursor;
+
+ struct pw_main_loop *loop;
+
+ struct pw_filter *filter;
+ struct spa_hook filter_listener;
+
+ void *in_port;
+
+ struct spa_io_position *position;
+ struct spa_video_info_dsp format;
+
+ int counter;
+ SDL_Rect rect;
+ SDL_Rect cursor_rect;
+};
+
+static void handle_events(struct data *data)
+{
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ pw_main_loop_quit(data->loop);
+ break;
+ }
+ }
+}
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * b = pw_filter_dequeue_buffer(port);
+ *
+ * .. do stuff with buffer ...
+ *
+ * pw_filter_queue_buffer(port, b);
+ */
+static void
+on_process(void *_data, struct spa_io_position *position)
+{
+ struct data *data = _data;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ void *sdata, *ddata;
+ int sstride, dstride;
+ uint32_t i, j;
+ uint8_t *src, *dst;
+
+ b = NULL;
+ while (true) {
+ struct pw_buffer *t;
+ if ((t = pw_filter_dequeue_buffer(data->in_port)) == NULL)
+ break;
+ if (b)
+ pw_filter_queue_buffer(data->in_port, b);
+ b = t;
+ }
+ if (b == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+
+ pw_log_trace("new buffer %p %dx%d", buf,
+ data->position->video.size.width, data->position->video.size.height);
+
+ handle_events(data);
+
+ if ((sdata = buf->datas[0].data) == NULL) {
+ pw_log_error("no buffer data");
+ goto done;
+ }
+
+ if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) {
+ pw_log_error("Couldn't lock texture: %s", SDL_GetError());
+ goto done;
+ }
+
+ /* copy video image in texture */
+ sstride = buf->datas[0].chunk->stride;
+ if (sstride == 0)
+ sstride = buf->datas[0].chunk->size / data->position->video.size.height;
+
+ src = sdata;
+ dst = ddata;
+
+ for (i = 0; i < data->position->video.size.height; i++) {
+ struct pixel *p = (struct pixel *) src;
+ for (j = 0; j < data->position->video.size.width; j++) {
+ dst[j * 4 + 0] = SPA_CLAMP(p[j].r * 255.0f, 0, 255);
+ dst[j * 4 + 1] = SPA_CLAMP(p[j].g * 255.0f, 0, 255);
+ dst[j * 4 + 2] = SPA_CLAMP(p[j].b * 255.0f, 0, 255);
+ dst[j * 4 + 3] = SPA_CLAMP(p[j].a * 255.0f, 0, 255);
+ }
+ src += sstride;
+ dst += dstride;
+ }
+ SDL_UnlockTexture(data->texture);
+
+ SDL_RenderClear(data->renderer);
+ SDL_RenderCopy(data->renderer, data->texture, &data->rect, NULL);
+ SDL_RenderPresent(data->renderer);
+
+ done:
+ pw_filter_queue_buffer(data->in_port, b);
+}
+
+static void on_filter_state_changed(void *_data, enum pw_filter_state old,
+ enum pw_filter_state state, const char *error)
+{
+ struct data *data = _data;
+ fprintf(stderr, "filter state: \"%s\"\n", pw_filter_state_as_string(state));
+ switch (state) {
+ case PW_FILTER_STATE_UNCONNECTED:
+ pw_main_loop_quit(data->loop);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+on_filter_io_changed(void *_data, void *port_data, uint32_t id, void *area, uint32_t size)
+{
+ struct data *data = _data;
+
+ switch (id) {
+ case SPA_IO_Position:
+ data->position = area;
+ break;
+ }
+}
+
+static void
+on_filter_param_changed(void *_data, void *port_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_filter *filter = data->filter;
+
+ /* NULL means to clear the format */
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ /* call a helper function to parse the format for us. */
+ spa_format_video_dsp_parse(param, &data->format);
+
+ if (data->format.format != SPA_VIDEO_FORMAT_RGBA_F32) {
+ pw_filter_set_error(filter, -EINVAL, "unknown format");
+ return;
+ }
+
+ data->texture = SDL_CreateTexture(data->renderer,
+ SDL_PIXELFORMAT_RGBA32,
+ SDL_TEXTUREACCESS_STREAMING,
+ data->position->video.size.width,
+ data->position->video.size.height);
+ if (data->texture == NULL) {
+ pw_filter_set_error(filter, -errno, "can't create texture");
+ return;
+ }
+
+ data->rect.x = 0;
+ data->rect.y = 0;
+ data->rect.w = data->position->video.size.width;
+ data->rect.h = data->position->video.size.height;
+}
+
+/* these are the filter events we listen for */
+static const struct pw_filter_events filter_events = {
+ PW_VERSION_FILTER_EVENTS,
+ .state_changed = on_filter_state_changed,
+ .io_changed = on_filter_io_changed,
+ .param_changed = on_filter_param_changed,
+ .process = on_process,
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+
+ pw_init(&argc, &argv);
+
+ /* create a main loop */
+ data.loop = pw_main_loop_new(NULL);
+
+ data.target = argc > 1 ? argv[1] : NULL;
+
+ /* create a simple filter, the simple filter manages to core and remote
+ * objects for you if you don't need to deal with them
+ *
+ * If you plan to autoconnect your filter, you need to provide at least
+ * media, category and role properties
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the filter state. The most important event
+ * you need to listen to is the process event where you need to consume
+ * the data provided to you.
+ */
+ data.filter = pw_filter_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "video-dsp-play",
+ pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Video",
+ PW_KEY_MEDIA_CATEGORY, "Capture",
+ PW_KEY_MEDIA_ROLE, "DSP",
+ PW_KEY_NODE_AUTOCONNECT, data.target ? "true" : "false",
+ PW_KEY_TARGET_OBJECT, data.target,
+ PW_KEY_MEDIA_CLASS, "Stream/Input/Video",
+ NULL),
+ &filter_events,
+ &data);
+
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ if (SDL_CreateWindowAndRenderer
+ (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) {
+ fprintf(stderr, "can't create window: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ /* Make a new DSP port. This will automatically set up the right
+ * parameters for the port */
+ data.in_port = pw_filter_add_port(data.filter,
+ PW_DIRECTION_INPUT,
+ PW_FILTER_PORT_FLAG_MAP_BUFFERS,
+ 0,
+ pw_properties_new(
+ PW_KEY_FORMAT_DSP, "32 bit float RGBA video",
+ PW_KEY_PORT_NAME, "input",
+ NULL),
+ NULL, 0);
+
+ pw_filter_connect(data.filter,
+ 0, /* no flags */
+ NULL, 0);
+
+ /* do things until we quit the mainloop */
+ pw_main_loop_run(data.loop);
+
+ pw_filter_destroy(data.filter);
+ pw_main_loop_destroy(data.loop);
+
+ SDL_DestroyTexture(data.texture);
+ if (data.cursor)
+ SDL_DestroyTexture(data.cursor);
+ SDL_DestroyRenderer(data.renderer);
+ SDL_DestroyWindow(data.window);
+
+ return 0;
+}
diff --git a/src/examples/video-play-fixate.c b/src/examples/video-play-fixate.c
new file mode 100644
index 0000000..7477c5a
--- /dev/null
+++ b/src/examples/video-play-fixate.c
@@ -0,0 +1,516 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Video input stream using \ref pw_stream "pw_stream", with format fixation.
+ [title]
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <signal.h>
+#include <libdrm/drm_fourcc.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/debug/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define WIDTH 640
+#define HEIGHT 480
+
+#define MAX_BUFFERS 64
+#define MAX_MOD 8
+
+#include "sdl.h"
+
+struct pixel {
+ float r, g, b, a;
+};
+
+struct pw_version {
+ int major;
+ int minor;
+ int micro;
+};
+
+struct modifier_info {
+ uint32_t spa_format;
+ uint32_t n_modifiers;
+ uint64_t modifiers[MAX_MOD];
+};
+
+struct data {
+ const char *path;
+
+ SDL_Renderer *renderer;
+ SDL_Window *window;
+ SDL_Texture *texture;
+ SDL_Texture *cursor;
+
+ struct pw_main_loop *loop;
+ struct spa_source *reneg;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_video_info format;
+ int32_t stride;
+ struct spa_rectangle size;
+
+ uint32_t n_mod_info;
+ struct modifier_info mod_info[2];
+
+ int counter;
+};
+
+static struct pw_version parse_pw_version(const char* version) {
+ struct pw_version pw_version;
+ sscanf(version, "%d.%d.%d", &pw_version.major, &pw_version.minor,
+ &pw_version.micro);
+ return pw_version;
+}
+
+static bool has_pw_version(int major, int minor, int micro) {
+ struct pw_version pw_version = parse_pw_version(pw_get_library_version());
+ printf("PW Version: %d.%d.%d\n", pw_version.major, pw_version.minor,
+ pw_version.micro);
+ return major <= pw_version.major && minor <= pw_version.minor && micro <= pw_version.micro;
+}
+
+static void init_modifiers(struct data *data)
+{
+ data->n_mod_info = 1;
+ data->mod_info[0].spa_format = SPA_VIDEO_FORMAT_RGB;
+ data->mod_info[0].n_modifiers = 2;
+ data->mod_info[0].modifiers[0] = DRM_FORMAT_MOD_LINEAR;
+ data->mod_info[0].modifiers[1] = DRM_FORMAT_MOD_INVALID;
+}
+
+static void destroy_modifiers(struct data *data)
+{
+ data->mod_info[0].n_modifiers = 0;
+}
+
+static void strip_modifier(struct data *data, uint32_t spa_format, uint64_t modifier)
+{
+ if (data->mod_info[0].spa_format != spa_format)
+ return;
+ struct modifier_info *mod_info = &data->mod_info[0];
+ uint32_t counter = 0;
+ // Dropping of single modifiers is only supported on PipeWire 0.3.40 and newer.
+ // On older PipeWire just dropping all modifiers might work on Versions newer then 0.3.33/35
+ if (has_pw_version(0,3,40)) {
+ printf("Dropping a single modifier\n");
+ for (uint32_t i = 0; i < mod_info->n_modifiers; i++) {
+ if (mod_info->modifiers[i] == modifier)
+ continue;
+ mod_info->modifiers[counter++] = mod_info->modifiers[i];
+ }
+ } else {
+ printf("Dropping all modifiers\n");
+ counter = 0;
+ }
+ mod_info->n_modifiers = counter;
+}
+
+static void handle_events(struct data *data)
+{
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ pw_main_loop_quit(data->loop);
+ break;
+ }
+ }
+}
+
+static struct spa_pod *build_format(struct spa_pod_builder *b, SDL_RendererInfo *info, enum spa_video_format format,
+ uint64_t *modifiers, int modifier_count)
+{
+ struct spa_pod_frame f[2];
+ int i, c;
+
+ 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_video), 0);
+ spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0);
+ /* format */
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0);
+ /* modifiers */
+ if (modifier_count == 1 && modifiers[0] == DRM_FORMAT_MOD_INVALID) {
+ // we only support implicit modifiers, use shortpath to skip fixation phase
+ spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY);
+ spa_pod_builder_long(b, modifiers[0]);
+ } else if (modifier_count > 0) {
+ // build an enumeration of modifiers
+ spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+ // modifiers from the array
+ for (i = 0, c = 0; i < modifier_count; i++) {
+ spa_pod_builder_long(b, modifiers[i]);
+ if (c++ == 0)
+ spa_pod_builder_long(b, modifiers[i]);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size,
+ SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(WIDTH, HEIGHT),
+ &SPA_RECTANGLE(1,1),
+ &SPA_RECTANGLE(info->max_texture_width,
+ info->max_texture_height)),
+ 0);
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate,
+ SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25,1),
+ &SPA_FRACTION(0,1),
+ &SPA_FRACTION(30,1)),
+ 0);
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * b = pw_stream_dequeue_buffer(stream);
+ *
+ * .. do stuff with buffer ...
+ *
+ * pw_stream_queue_buffer(stream, b);
+ */
+static void
+on_process(void *_data)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ void *sdata, *ddata;
+ int sstride, dstride, ostride;
+ uint32_t i;
+ uint8_t *src, *dst;
+
+ b = NULL;
+ /* dequeue and queue old buffers, use the last available
+ * buffer */
+ while (true) {
+ struct pw_buffer *t;
+ if ((t = pw_stream_dequeue_buffer(stream)) == NULL)
+ break;
+ if (b)
+ pw_stream_queue_buffer(stream, b);
+ b = t;
+ }
+ if (b == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+
+ pw_log_info("new buffer %p", buf);
+
+ handle_events(data);
+
+ if (buf->datas[0].type == SPA_DATA_DmaBuf) {
+ // Simulate a failed import of a DmaBuf
+ // We should try another modifier
+ printf("Failed to import dmabuf, stripping modifier %"PRIu64"\n", data->format.info.raw.modifier);
+ strip_modifier(data, data->format.info.raw.format, data->format.info.raw.modifier);
+ pw_loop_signal_event(pw_main_loop_get_loop(data->loop), data->reneg);
+ goto done;
+ }
+
+ if ((sdata = buf->datas[0].data) == NULL)
+ goto done;
+
+ if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ goto done;
+ }
+
+ /* copy video image in texture */
+ sstride = buf->datas[0].chunk->stride;
+ if (sstride == 0)
+ sstride = buf->datas[0].chunk->size / data->size.height;
+ ostride = SPA_MIN(sstride, dstride);
+
+ src = sdata;
+ dst = ddata;
+
+ for (i = 0; i < data->size.height; i++) {
+ memcpy(dst, src, ostride);
+ src += sstride;
+ dst += dstride;
+ }
+ SDL_UnlockTexture(data->texture);
+
+ SDL_RenderClear(data->renderer);
+ /* now render the video */
+ SDL_RenderCopy(data->renderer, data->texture, NULL, NULL);
+ SDL_RenderPresent(data->renderer);
+
+ done:
+ pw_stream_queue_buffer(stream, b);
+}
+
+static void on_stream_state_changed(void *_data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct data *data = _data;
+ fprintf(stderr, "stream state: \"%s\"\n", pw_stream_state_as_string(state));
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_main_loop_quit(data->loop);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ default:
+ break;
+ }
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format changes.
+ *
+ * We are now supposed to call pw_stream_finish_format() with success or
+ * failure, depending on if we can support the format. Because we gave
+ * a list of supported formats, this should be ok.
+ *
+ * As part of pw_stream_finish_format() we can provide parameters that
+ * will control the buffer memory allocation. This includes the metadata
+ * that we would like on our buffer, the size, alignment, etc.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ uint8_t params_buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
+ const struct spa_pod *params[1];
+ Uint32 sdl_format;
+ void *d;
+
+ /* NULL means to clear the format */
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ fprintf(stderr, "got format:\n");
+ spa_debug_format(2, NULL, param);
+
+ if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0)
+ return;
+
+ if (data->format.media_type != SPA_MEDIA_TYPE_video ||
+ data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return;
+
+ /* call a helper function to parse the format for us. */
+ spa_format_video_raw_parse(param, &data->format.info.raw);
+ sdl_format = id_to_sdl_format(data->format.info.raw.format);
+ data->size = data->format.info.raw.size;
+
+ if (sdl_format == SDL_PIXELFORMAT_UNKNOWN) {
+ pw_stream_set_error(stream, -EINVAL, "unknown pixel format");
+ return;
+ }
+ if (data->size.width == 0 || data->size.height == 0) {
+ pw_stream_set_error(stream, -EINVAL, "invalid size");
+ return;
+ }
+
+ data->texture = SDL_CreateTexture(data->renderer,
+ sdl_format,
+ SDL_TEXTUREACCESS_STREAMING,
+ data->size.width,
+ data->size.height);
+ SDL_LockTexture(data->texture, NULL, &d, &data->stride);
+ SDL_UnlockTexture(data->texture);
+
+ /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size,
+ * number, stride etc of the buffers */
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<<SPA_DATA_MemPtr) | (1<<SPA_DATA_DmaBuf)));
+
+ /* we are done */
+ pw_stream_update_params(stream, params, 1);
+}
+
+/* these are the stream events we listen for */
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = on_stream_state_changed,
+ .param_changed = on_stream_param_changed,
+ .process = on_process,
+};
+
+static int build_formats(struct data *data, struct spa_pod_builder *b, const struct spa_pod **params)
+{
+ SDL_RendererInfo info;
+ int n_params = 0;
+
+ SDL_GetRendererInfo(data->renderer, &info);
+
+ if (data->mod_info[0].n_modifiers > 0) {
+ params[n_params++] = build_format(b, &info, SPA_VIDEO_FORMAT_RGB, data->mod_info[0].modifiers, data->mod_info[0].n_modifiers);
+ }
+ params[n_params++] = build_format(b, &info, SPA_VIDEO_FORMAT_RGB, NULL, 0);
+
+ for (int i=0; i < n_params; i++) {
+ spa_debug_format(2, NULL, params[i]);
+ }
+
+ return n_params;
+}
+
+static void reneg_format(void *_data, uint64_t expiration)
+{
+ struct data *data = (struct data*) _data;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_pod *params[2];
+ uint32_t n_params;
+
+ if (data->format.info.raw.format == 0)
+ return;
+
+ fprintf(stderr, "renegotiate formats:\n");
+ n_params = build_formats(data, &b, params);
+
+ pw_stream_update_params(data->stream, params, n_params);
+}
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct pw_properties *props;
+ int res, n_params;
+
+ pw_init(&argc, &argv);
+
+ /* create a main loop */
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* create a simple stream, the simple stream manages to core and remote
+ * objects for you if you don't need to deal with them
+ *
+ * If you plan to autoconnect your stream, you need to provide at least
+ * media, category and role properties
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to consume
+ * the data provided to you.
+ */
+ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+ PW_KEY_MEDIA_CATEGORY, "Capture",
+ PW_KEY_MEDIA_ROLE, "Camera",
+ NULL),
+ data.path = argc > 1 ? argv[1] : NULL;
+ if (data.path)
+ /* Set stream target if given on command line */
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path);
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "video-play-fixate",
+ props,
+ &stream_events,
+ &data);
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ init_modifiers(&data);
+
+ if (SDL_CreateWindowAndRenderer
+ (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) {
+ fprintf(stderr, "can't create window: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ /* build the extra parameters to connect with. To connect, we can provide
+ * a list of supported formats. We use a builder that writes the param
+ * object to the stack. */
+ printf("supported formats:\n");
+ n_params = build_formats(&data, &b, params);
+
+ /* now connect the stream, we need a direction (input/output),
+ * an optional target node to connect to, some flags and parameters
+ */
+ if ((res = pw_stream_connect(data.stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT | /* try to automatically connect this stream */
+ PW_STREAM_FLAG_MAP_BUFFERS, /* mmap the buffer data for us */
+ params, n_params)) /* extra parameters, see above */ < 0) {
+ fprintf(stderr, "can't connect: %s\n", spa_strerror(res));
+ return -1;
+ }
+
+ data.reneg = pw_loop_add_event(pw_main_loop_get_loop(data.loop), reneg_format, &data);
+
+ /* do things until we quit the mainloop */
+ pw_main_loop_run(data.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+
+ destroy_modifiers(&data);
+
+ SDL_DestroyTexture(data.texture);
+ if (data.cursor)
+ SDL_DestroyTexture(data.cursor);
+ SDL_DestroyRenderer(data.renderer);
+ SDL_DestroyWindow(data.window);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/video-play-pull.c b/src/examples/video-play-pull.c
new file mode 100644
index 0000000..fd0e305
--- /dev/null
+++ b/src/examples/video-play-pull.c
@@ -0,0 +1,588 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Video input stream using \ref pw_stream_trigger_process, for pull mode.
+ [title]
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/debug/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define WIDTH 640
+#define HEIGHT 480
+
+#define MAX_BUFFERS 64
+
+#include "sdl.h"
+
+struct pixel {
+ float r, g, b, a;
+};
+
+struct data {
+ const char *path;
+
+ SDL_Renderer *renderer;
+ SDL_Window *window;
+ SDL_Texture *texture;
+ SDL_Texture *cursor;
+
+ struct pw_main_loop *loop;
+ struct spa_source *timer;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_io_position *position;
+
+ struct spa_video_info format;
+ int32_t stride;
+ struct spa_rectangle size;
+
+ int counter;
+ SDL_Rect rect;
+ SDL_Rect cursor_rect;
+ bool is_yuv;
+ bool have_request_process;
+};
+
+static void handle_events(struct data *data)
+{
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ pw_main_loop_quit(data->loop);
+ break;
+ }
+ }
+}
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * b = pw_stream_dequeue_buffer(stream);
+ *
+ * .. do stuff with buffer ...
+ *
+ * pw_stream_queue_buffer(stream, b);
+ */
+static void
+on_process(void *_data)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ void *sdata, *ddata;
+ int sstride, dstride, ostride;
+ struct spa_meta_region *mc;
+ struct spa_meta_cursor *mcs;
+ uint32_t i, j;
+ uint8_t *src, *dst;
+ bool render_cursor = false;
+
+ b = NULL;
+ while (true) {
+ struct pw_buffer *t;
+ if ((t = pw_stream_dequeue_buffer(stream)) == NULL)
+ break;
+ if (b)
+ pw_stream_queue_buffer(stream, b);
+ b = t;
+ }
+ if (b == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+
+ pw_log_trace("new buffer %p", buf);
+
+ handle_events(data);
+
+ if ((sdata = buf->datas[0].data) == NULL)
+ goto done;
+
+ /* get the videocrop metadata if any */
+ if ((mc = spa_buffer_find_meta_data(buf, SPA_META_VideoCrop, sizeof(*mc))) &&
+ spa_meta_region_is_valid(mc)) {
+ data->rect.x = mc->region.position.x;
+ data->rect.y = mc->region.position.y;
+ data->rect.w = mc->region.size.width;
+ data->rect.h = mc->region.size.height;
+ }
+ /* get cursor metadata */
+ if ((mcs = spa_buffer_find_meta_data(buf, SPA_META_Cursor, sizeof(*mcs))) &&
+ spa_meta_cursor_is_valid(mcs)) {
+ struct spa_meta_bitmap *mb;
+ void *cdata;
+ int cstride;
+
+ data->cursor_rect.x = mcs->position.x;
+ data->cursor_rect.y = mcs->position.y;
+
+ mb = SPA_PTROFF(mcs, mcs->bitmap_offset, struct spa_meta_bitmap);
+ data->cursor_rect.w = mb->size.width;
+ data->cursor_rect.h = mb->size.height;
+
+ if (data->cursor == NULL) {
+ data->cursor = SDL_CreateTexture(data->renderer,
+ id_to_sdl_format(mb->format),
+ SDL_TEXTUREACCESS_STREAMING,
+ mb->size.width, mb->size.height);
+ SDL_SetTextureBlendMode(data->cursor, SDL_BLENDMODE_BLEND);
+ }
+
+
+ if (SDL_LockTexture(data->cursor, NULL, &cdata, &cstride) < 0) {
+ fprintf(stderr, "Couldn't lock cursor texture: %s\n", SDL_GetError());
+ goto done;
+ }
+
+ /* copy the cursor bitmap into the texture */
+ src = SPA_PTROFF(mb, mb->offset, uint8_t);
+ dst = cdata;
+ ostride = SPA_MIN(cstride, mb->stride);
+
+ for (i = 0; i < mb->size.height; i++) {
+ memcpy(dst, src, ostride);
+ dst += cstride;
+ src += mb->stride;
+ }
+ SDL_UnlockTexture(data->cursor);
+
+ render_cursor = true;
+ }
+
+ /* copy video image in texture */
+ if (data->is_yuv) {
+ sstride = data->stride;
+ SDL_UpdateYUVTexture(data->texture,
+ NULL,
+ sdata,
+ sstride,
+ SPA_PTROFF(sdata, sstride * data->size.height, void),
+ sstride / 2,
+ SPA_PTROFF(sdata, 5 * (sstride * data->size.height) / 4, void),
+ sstride / 2);
+ }
+ else {
+ if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ goto done;
+ }
+
+ sstride = buf->datas[0].chunk->stride;
+ if (sstride == 0)
+ sstride = buf->datas[0].chunk->size / data->size.height;
+ ostride = SPA_MIN(sstride, dstride);
+
+ src = sdata;
+ dst = ddata;
+
+ if (data->format.media_subtype == SPA_MEDIA_SUBTYPE_dsp) {
+ for (i = 0; i < data->size.height; i++) {
+ struct pixel *p = (struct pixel *) src;
+ for (j = 0; j < data->size.width; j++) {
+ dst[j * 4 + 0] = SPA_CLAMP(p[j].r * 255.0f, 0, 255);
+ dst[j * 4 + 1] = SPA_CLAMP(p[j].g * 255.0f, 0, 255);
+ dst[j * 4 + 2] = SPA_CLAMP(p[j].b * 255.0f, 0, 255);
+ dst[j * 4 + 3] = SPA_CLAMP(p[j].a * 255.0f, 0, 255);
+ }
+ src += sstride;
+ dst += dstride;
+ }
+ } else {
+ for (i = 0; i < data->size.height; i++) {
+ memcpy(dst, src, ostride);
+ src += sstride;
+ dst += dstride;
+ }
+ }
+ SDL_UnlockTexture(data->texture);
+ }
+
+ SDL_RenderClear(data->renderer);
+ /* now render the video and then the cursor if any */
+ SDL_RenderCopy(data->renderer, data->texture, &data->rect, NULL);
+ if (render_cursor) {
+ SDL_RenderCopy(data->renderer, data->cursor, NULL, &data->cursor_rect);
+ }
+ SDL_RenderPresent(data->renderer);
+
+ done:
+ pw_stream_queue_buffer(stream, b);
+}
+
+static void enable_timeouts(struct data *data, bool enabled)
+{
+ struct timespec timeout, interval, *to, *iv;
+
+ if (!enabled || data->have_request_process) {
+ to = iv = NULL;
+ } else {
+ timeout.tv_sec = 0;
+ timeout.tv_nsec = 1;
+ interval.tv_sec = 0;
+ interval.tv_nsec = 80 * SPA_NSEC_PER_MSEC;
+ to = &timeout;
+ iv = &interval;
+ }
+ pw_loop_update_timer(pw_main_loop_get_loop(data->loop),
+ data->timer, to, iv, false);
+}
+
+
+static void on_stream_state_changed(void *_data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct data *data = _data;
+ fprintf(stderr, "stream state: \"%s\"\n", pw_stream_state_as_string(state));
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_main_loop_quit(data->loop);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ enable_timeouts(data, false);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ enable_timeouts(data, true);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+on_stream_io_changed(void *_data, uint32_t id, void *area, uint32_t size)
+{
+ struct data *data = _data;
+
+ switch (id) {
+ case SPA_IO_Position:
+ data->position = area;
+ break;
+ }
+}
+
+static void
+on_trigger_done(void *_data)
+{
+ struct data *data = _data;
+ pw_log_trace("%p trigger done", data);
+}
+
+static void on_timeout(void *userdata, uint64_t expirations)
+{
+ struct data *data = userdata;
+ if (!data->have_request_process)
+ pw_stream_trigger_process(data->stream);
+}
+
+static void
+on_command(void *_data, const struct spa_command *command)
+{
+ struct data *data = _data;
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_RequestProcess:
+ data->have_request_process = true;
+ enable_timeouts(data, false);
+ pw_stream_trigger_process(data->stream);
+ break;
+ default:
+ break;
+ }
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format changes.
+ *
+ * We are now supposed to call pw_stream_finish_format() with success or
+ * failure, depending on if we can support the format. Because we gave
+ * a list of supported formats, this should be ok.
+ *
+ * As part of pw_stream_finish_format() we can provide parameters that
+ * will control the buffer memory allocation. This includes the metadata
+ * that we would like on our buffer, the size, alignment, etc.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ uint8_t params_buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
+ const struct spa_pod *params[5];
+ Uint32 sdl_format;
+ void *d;
+ int32_t mult, size;
+
+ /* NULL means to clear the format */
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ fprintf(stderr, "got format:\n");
+ spa_debug_format(2, NULL, param);
+
+ if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0)
+ return;
+
+ if (data->format.media_type != SPA_MEDIA_TYPE_video)
+ return;
+
+ switch (data->format.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ /* call a helper function to parse the format for us. */
+ spa_format_video_raw_parse(param, &data->format.info.raw);
+ sdl_format = id_to_sdl_format(data->format.info.raw.format);
+ data->size = SPA_RECTANGLE(data->format.info.raw.size.width,
+ data->format.info.raw.size.height);
+ mult = 1;
+ break;
+ case SPA_MEDIA_SUBTYPE_dsp:
+ spa_format_video_dsp_parse(param, &data->format.info.dsp);
+ if (data->format.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32)
+ return;
+ sdl_format = SDL_PIXELFORMAT_RGBA32;
+ data->size = SPA_RECTANGLE(data->position->video.size.width,
+ data->position->video.size.height);
+ mult = 4;
+ break;
+ default:
+ sdl_format = SDL_PIXELFORMAT_UNKNOWN;
+ break;
+ }
+
+ if (sdl_format == SDL_PIXELFORMAT_UNKNOWN) {
+ pw_stream_set_error(stream, -EINVAL, "unknown pixel format");
+ return;
+ }
+ if (data->size.width == 0 || data->size.height == 0) {
+ pw_stream_set_error(stream, -EINVAL, "invalid size");
+ return;
+ }
+
+ data->texture = SDL_CreateTexture(data->renderer,
+ sdl_format,
+ SDL_TEXTUREACCESS_STREAMING,
+ data->size.width,
+ data->size.height);
+ SDL_LockTexture(data->texture, NULL, &d, &data->stride);
+ SDL_UnlockTexture(data->texture);
+
+ switch(sdl_format) {
+ case SDL_PIXELFORMAT_YV12:
+ case SDL_PIXELFORMAT_IYUV:
+ size = (data->stride * data->size.height) * 3 / 2;
+ data->is_yuv = true;
+ break;
+ default:
+ size = data->stride * data->size.height;
+ break;
+ }
+
+ data->rect.x = 0;
+ data->rect.y = 0;
+ data->rect.w = data->size.width;
+ data->rect.h = data->size.height;
+
+ /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size,
+ * number, stride etc of the buffers */
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(size * mult),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride * mult),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<<SPA_DATA_MemPtr)));
+
+ /* a header metadata with timing information */
+ params[1] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ /* video cropping information */
+ params[2] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region)));
+#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \
+ sizeof(struct spa_meta_bitmap) + w * h * 4)
+ /* cursor information */
+ params[3] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor),
+ SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(
+ CURSOR_META_SIZE(64,64),
+ CURSOR_META_SIZE(1,1),
+ CURSOR_META_SIZE(256,256)));
+
+ /* we are done */
+ pw_stream_update_params(stream, params, 4);
+}
+
+/* these are the stream events we listen for */
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = on_stream_state_changed,
+ .io_changed = on_stream_io_changed,
+ .param_changed = on_stream_param_changed,
+ .process = on_process,
+ .trigger_done = on_trigger_done,
+ .command = on_command,
+};
+
+static int build_format(struct data *data, struct spa_pod_builder *b, const struct spa_pod **params)
+{
+ SDL_RendererInfo info;
+
+ SDL_GetRendererInfo(data->renderer, &info);
+ params[0] = sdl_build_formats(&info, b);
+
+ fprintf(stderr, "supported SDL formats:\n");
+ spa_debug_format(2, NULL, params[0]);
+
+ params[1] = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32));
+
+ fprintf(stderr, "supported DSP formats:\n");
+ spa_debug_format(2, NULL, params[1]);
+
+ return 2;
+}
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct pw_properties *props;
+ int res, n_params;
+
+ pw_init(&argc, &argv);
+
+ /* create a main loop */
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* the timer to pull in data */
+ data.timer = pw_loop_add_timer(pw_main_loop_get_loop(data.loop), on_timeout, &data);
+
+ /* create a simple stream, the simple stream manages to core and remote
+ * objects for you if you don't need to deal with them
+ *
+ * If you plan to autoconnect your stream, you need to provide at least
+ * media, category and role properties
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to consume
+ * the data provided to you.
+ */
+ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+ PW_KEY_MEDIA_CATEGORY, "Capture",
+ PW_KEY_MEDIA_ROLE, "Camera",
+ PW_KEY_PRIORITY_DRIVER, "10000",
+ NULL),
+ data.path = argc > 1 ? argv[1] : NULL;
+ if (data.path)
+ /* Set stream target if given on command line */
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path);
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "video-play",
+ props,
+ &stream_events,
+ &data);
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ if (SDL_CreateWindowAndRenderer
+ (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) {
+ fprintf(stderr, "can't create window: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ /* build the extra parameters to connect with. To connect, we can provide
+ * a list of supported formats. We use a builder that writes the param
+ * object to the stack. */
+ n_params = build_format(&data, &b, params);
+
+ /* now connect the stream, we need a direction (input/output),
+ * an optional target node to connect to, some flags and parameters
+ */
+ if ((res = pw_stream_connect(data.stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_DRIVER | /* we're driver, we pull */
+ PW_STREAM_FLAG_AUTOCONNECT | /* try to automatically connect this stream */
+ PW_STREAM_FLAG_MAP_BUFFERS, /* mmap the buffer data for us */
+ params, n_params)) /* extra parameters, see above */ < 0) {
+ fprintf(stderr, "can't connect: %s\n", spa_strerror(res));
+ return -1;
+ }
+
+ /* do things until we quit the mainloop */
+ pw_main_loop_run(data.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+
+ SDL_DestroyTexture(data.texture);
+ if (data.cursor)
+ SDL_DestroyTexture(data.cursor);
+ SDL_DestroyRenderer(data.renderer);
+ SDL_DestroyWindow(data.window);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/video-play-reneg.c b/src/examples/video-play-reneg.c
new file mode 100644
index 0000000..26b19cb
--- /dev/null
+++ b/src/examples/video-play-reneg.c
@@ -0,0 +1,438 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Video input stream using \ref pw_stream "pw_stream", with format renegotiation.
+ [title]
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/debug/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define WIDTH 640
+#define HEIGHT 480
+
+#define MAX_BUFFERS 64
+
+#include "sdl.h"
+
+struct pixel {
+ float r, g, b, a;
+};
+
+struct data {
+ const char *path;
+
+ SDL_Renderer *renderer;
+ SDL_Window *window;
+ SDL_Texture *texture;
+ SDL_Texture *cursor;
+
+ struct pw_main_loop *loop;
+ struct spa_source *timer;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_video_info format;
+ int32_t stride;
+ struct spa_rectangle size;
+
+ int counter;
+};
+
+static void handle_events(struct data *data)
+{
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ pw_main_loop_quit(data->loop);
+ break;
+ }
+ }
+}
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * b = pw_stream_dequeue_buffer(stream);
+ *
+ * .. do stuff with buffer ...
+ *
+ * pw_stream_queue_buffer(stream, b);
+ */
+static void
+on_process(void *_data)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ void *sdata, *ddata;
+ int sstride, dstride, ostride;
+ uint32_t i;
+ uint8_t *src, *dst;
+
+ b = NULL;
+ /* dequeue and queue old buffers, use the last available
+ * buffer */
+ while (true) {
+ struct pw_buffer *t;
+ if ((t = pw_stream_dequeue_buffer(stream)) == NULL)
+ break;
+ if (b)
+ pw_stream_queue_buffer(stream, b);
+ b = t;
+ }
+ if (b == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+
+ pw_log_info("new buffer %p", buf);
+
+ handle_events(data);
+
+ if ((sdata = buf->datas[0].data) == NULL)
+ goto done;
+
+ if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ goto done;
+ }
+
+ /* copy video image in texture */
+ sstride = buf->datas[0].chunk->stride;
+ if (sstride == 0)
+ sstride = buf->datas[0].chunk->size / data->size.height;
+ ostride = SPA_MIN(sstride, dstride);
+
+ src = sdata;
+ dst = ddata;
+
+ for (i = 0; i < data->size.height; i++) {
+ memcpy(dst, src, ostride);
+ src += sstride;
+ dst += dstride;
+ }
+ SDL_UnlockTexture(data->texture);
+
+ SDL_RenderClear(data->renderer);
+ /* now render the video */
+ SDL_RenderCopy(data->renderer, data->texture, NULL, NULL);
+ SDL_RenderPresent(data->renderer);
+
+ done:
+ pw_stream_queue_buffer(stream, b);
+}
+
+static void on_stream_state_changed(void *_data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct data *data = _data;
+ fprintf(stderr, "stream state: \"%s\"\n", pw_stream_state_as_string(state));
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_main_loop_quit(data->loop);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ pw_loop_update_timer(pw_main_loop_get_loop(data->loop),
+ data->timer, NULL, NULL, false);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ {
+ struct timespec timeout, interval;
+
+ timeout.tv_sec = 1;
+ timeout.tv_nsec = 0;
+ interval.tv_sec = 1;
+ interval.tv_nsec = 0;
+
+ pw_loop_update_timer(pw_main_loop_get_loop(data->loop),
+ data->timer, &timeout, &interval, false);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format changes.
+ *
+ * We are now supposed to call pw_stream_finish_format() with success or
+ * failure, depending on if we can support the format. Because we gave
+ * a list of supported formats, this should be ok.
+ *
+ * As part of pw_stream_finish_format() we can provide parameters that
+ * will control the buffer memory allocation. This includes the metadata
+ * that we would like on our buffer, the size, alignment, etc.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ uint8_t params_buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
+ const struct spa_pod *params[1];
+ Uint32 sdl_format;
+ void *d;
+
+ /* NULL means to clear the format */
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ fprintf(stderr, "got format:\n");
+ spa_debug_format(2, NULL, param);
+
+ if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0)
+ return;
+
+ if (data->format.media_type != SPA_MEDIA_TYPE_video ||
+ data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return;
+
+ /* call a helper function to parse the format for us. */
+ spa_format_video_raw_parse(param, &data->format.info.raw);
+ sdl_format = id_to_sdl_format(data->format.info.raw.format);
+ data->size = data->format.info.raw.size;
+
+ if (sdl_format == SDL_PIXELFORMAT_UNKNOWN) {
+ pw_stream_set_error(stream, -EINVAL, "unknown pixel format");
+ return;
+ }
+ if (data->size.width == 0 || data->size.height == 0) {
+ pw_stream_set_error(stream, -EINVAL, "invalid size");
+ return;
+ }
+
+ data->texture = SDL_CreateTexture(data->renderer,
+ sdl_format,
+ SDL_TEXTUREACCESS_STREAMING,
+ data->size.width,
+ data->size.height);
+ SDL_LockTexture(data->texture, NULL, &d, &data->stride);
+ SDL_UnlockTexture(data->texture);
+
+ /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size,
+ * number, stride etc of the buffers */
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<<SPA_DATA_MemPtr)));
+
+ /* we are done */
+ pw_stream_update_params(stream, params, 1);
+}
+
+/* these are the stream events we listen for */
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = on_stream_state_changed,
+ .param_changed = on_stream_param_changed,
+ .process = on_process,
+};
+
+static int build_format(struct data *data, struct spa_pod_builder *b, const struct spa_pod **params)
+{
+ SDL_RendererInfo info;
+
+ SDL_GetRendererInfo(data->renderer, &info);
+ params[0] = sdl_build_formats(&info, b);
+
+ fprintf(stderr, "supported SDL formats:\n");
+ spa_debug_format(2, NULL, params[0]);
+
+ return 1;
+}
+
+static int reneg_format(struct data *data)
+{
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_pod *params[2];
+ int32_t width, height;
+
+ if (data->format.info.raw.format == 0)
+ return -EBUSY;
+
+ width = data->counter & 1 ? 320 : 640;
+ height = data->counter & 1 ? 240 : 480;
+
+ fprintf(stderr, "renegotiate to %dx%d:\n", width, height);
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ 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_Id(data->format.info.raw.format),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(width, height)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&data->format.info.raw.framerate));
+
+ pw_stream_update_params(data->stream, params, 1);
+
+ data->counter++;
+ return 0;
+}
+
+static int reneg_buffers(struct data *data)
+{
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_pod *params[2];
+
+ fprintf(stderr, "renegotiate buffers\n");
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride));
+
+ pw_stream_update_params(data->stream, params, 1);
+
+ data->counter++;
+ return 0;
+}
+
+static void on_timeout(void *userdata, uint64_t expirations)
+{
+ struct data *data = userdata;
+ if (1)
+ reneg_format(data);
+ else
+ reneg_buffers(data);
+}
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct pw_properties *props;
+ int res, n_params;
+
+ pw_init(&argc, &argv);
+
+ /* create a main loop */
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* create a simple stream, the simple stream manages to core and remote
+ * objects for you if you don't need to deal with them
+ *
+ * If you plan to autoconnect your stream, you need to provide at least
+ * media, category and role properties
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to consume
+ * the data provided to you.
+ */
+ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+ PW_KEY_MEDIA_CATEGORY, "Capture",
+ PW_KEY_MEDIA_ROLE, "Camera",
+ NULL),
+ data.path = argc > 1 ? argv[1] : NULL;
+ if (data.path)
+ /* Set stream target if given on command line */
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path);
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "video-play-reneg",
+ props,
+ &stream_events,
+ &data);
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ if (SDL_CreateWindowAndRenderer
+ (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) {
+ fprintf(stderr, "can't create window: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ /* build the extra parameters to connect with. To connect, we can provide
+ * a list of supported formats. We use a builder that writes the param
+ * object to the stack. */
+ n_params = build_format(&data, &b, params);
+
+ /* now connect the stream, we need a direction (input/output),
+ * an optional target node to connect to, some flags and parameters
+ */
+ if ((res = pw_stream_connect(data.stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT | /* try to automatically connect this stream */
+ PW_STREAM_FLAG_MAP_BUFFERS, /* mmap the buffer data for us */
+ params, n_params)) /* extra parameters, see above */ < 0) {
+ fprintf(stderr, "can't connect: %s\n", spa_strerror(res));
+ return -1;
+ }
+
+ data.timer = pw_loop_add_timer(pw_main_loop_get_loop(data.loop), on_timeout, &data);
+
+ /* do things until we quit the mainloop */
+ pw_main_loop_run(data.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+
+ SDL_DestroyTexture(data.texture);
+ if (data.cursor)
+ SDL_DestroyTexture(data.cursor);
+ SDL_DestroyRenderer(data.renderer);
+ SDL_DestroyWindow(data.window);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/video-play.c b/src/examples/video-play.c
new file mode 100644
index 0000000..9cbbab6
--- /dev/null
+++ b/src/examples/video-play.c
@@ -0,0 +1,529 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Video input stream using \ref pw_stream "pw_stream".
+ [title]
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/debug/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define WIDTH 640
+#define HEIGHT 480
+
+#define MAX_BUFFERS 64
+
+#include "sdl.h"
+
+struct pixel {
+ float r, g, b, a;
+};
+
+struct data {
+ const char *path;
+
+ SDL_Renderer *renderer;
+ SDL_Window *window;
+ SDL_Texture *texture;
+ SDL_Texture *cursor;
+
+ struct pw_main_loop *loop;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_io_position *position;
+
+ struct spa_video_info format;
+ int32_t stride;
+ struct spa_rectangle size;
+
+ int counter;
+ SDL_Rect rect;
+ SDL_Rect cursor_rect;
+ bool is_yuv;
+};
+
+static void handle_events(struct data *data)
+{
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ pw_main_loop_quit(data->loop);
+ break;
+ }
+ }
+}
+
+/* our data processing function is in general:
+ *
+ * struct pw_buffer *b;
+ * b = pw_stream_dequeue_buffer(stream);
+ *
+ * .. do stuff with buffer ...
+ *
+ * pw_stream_queue_buffer(stream, b);
+ */
+static void
+on_process(void *_data)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ void *sdata, *ddata;
+ int sstride, dstride, ostride;
+ struct spa_meta_region *mc;
+ struct spa_meta_cursor *mcs;
+ uint32_t i, j;
+ uint8_t *src, *dst;
+ bool render_cursor = false;
+
+ b = NULL;
+ while (true) {
+ struct pw_buffer *t;
+ if ((t = pw_stream_dequeue_buffer(stream)) == NULL)
+ break;
+ if (b)
+ pw_stream_queue_buffer(stream, b);
+ b = t;
+ }
+ if (b == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+
+ pw_log_trace("new buffer %p", buf);
+
+ handle_events(data);
+
+ if ((sdata = buf->datas[0].data) == NULL)
+ goto done;
+
+ /* get the videocrop metadata if any */
+ if ((mc = spa_buffer_find_meta_data(buf, SPA_META_VideoCrop, sizeof(*mc))) &&
+ spa_meta_region_is_valid(mc)) {
+ data->rect.x = mc->region.position.x;
+ data->rect.y = mc->region.position.y;
+ data->rect.w = mc->region.size.width;
+ data->rect.h = mc->region.size.height;
+ }
+ /* get cursor metadata */
+ if ((mcs = spa_buffer_find_meta_data(buf, SPA_META_Cursor, sizeof(*mcs))) &&
+ spa_meta_cursor_is_valid(mcs)) {
+ struct spa_meta_bitmap *mb;
+ void *cdata;
+ int cstride;
+
+ data->cursor_rect.x = mcs->position.x;
+ data->cursor_rect.y = mcs->position.y;
+
+ mb = SPA_PTROFF(mcs, mcs->bitmap_offset, struct spa_meta_bitmap);
+ data->cursor_rect.w = mb->size.width;
+ data->cursor_rect.h = mb->size.height;
+
+ if (data->cursor == NULL) {
+ data->cursor = SDL_CreateTexture(data->renderer,
+ id_to_sdl_format(mb->format),
+ SDL_TEXTUREACCESS_STREAMING,
+ mb->size.width, mb->size.height);
+ SDL_SetTextureBlendMode(data->cursor, SDL_BLENDMODE_BLEND);
+ }
+
+
+ if (SDL_LockTexture(data->cursor, NULL, &cdata, &cstride) < 0) {
+ fprintf(stderr, "Couldn't lock cursor texture: %s\n", SDL_GetError());
+ goto done;
+ }
+
+ /* copy the cursor bitmap into the texture */
+ src = SPA_PTROFF(mb, mb->offset, uint8_t);
+ dst = cdata;
+ ostride = SPA_MIN(cstride, mb->stride);
+
+ for (i = 0; i < mb->size.height; i++) {
+ memcpy(dst, src, ostride);
+ dst += cstride;
+ src += mb->stride;
+ }
+ SDL_UnlockTexture(data->cursor);
+
+ render_cursor = true;
+ }
+
+ /* copy video image in texture */
+ if (data->is_yuv) {
+ sstride = data->stride;
+ SDL_UpdateYUVTexture(data->texture,
+ NULL,
+ sdata,
+ sstride,
+ SPA_PTROFF(sdata, sstride * data->size.height, void),
+ sstride / 2,
+ SPA_PTROFF(sdata, 5 * (sstride * data->size.height) / 4, void),
+ sstride / 2);
+ }
+ else {
+ if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) {
+ fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError());
+ goto done;
+ }
+
+ sstride = buf->datas[0].chunk->stride;
+ if (sstride == 0)
+ sstride = buf->datas[0].chunk->size / data->size.height;
+ ostride = SPA_MIN(sstride, dstride);
+
+ src = sdata;
+ dst = ddata;
+
+ if (data->format.media_subtype == SPA_MEDIA_SUBTYPE_dsp) {
+ for (i = 0; i < data->size.height; i++) {
+ struct pixel *p = (struct pixel *) src;
+ for (j = 0; j < data->size.width; j++) {
+ dst[j * 4 + 0] = SPA_CLAMP(p[j].r * 255.0f, 0, 255);
+ dst[j * 4 + 1] = SPA_CLAMP(p[j].g * 255.0f, 0, 255);
+ dst[j * 4 + 2] = SPA_CLAMP(p[j].b * 255.0f, 0, 255);
+ dst[j * 4 + 3] = SPA_CLAMP(p[j].a * 255.0f, 0, 255);
+ }
+ src += sstride;
+ dst += dstride;
+ }
+ } else {
+ for (i = 0; i < data->size.height; i++) {
+ memcpy(dst, src, ostride);
+ src += sstride;
+ dst += dstride;
+ }
+ }
+ SDL_UnlockTexture(data->texture);
+ }
+
+ SDL_RenderClear(data->renderer);
+ /* now render the video and then the cursor if any */
+ SDL_RenderCopy(data->renderer, data->texture, &data->rect, NULL);
+ if (render_cursor) {
+ SDL_RenderCopy(data->renderer, data->cursor, NULL, &data->cursor_rect);
+ }
+ SDL_RenderPresent(data->renderer);
+
+ done:
+ pw_stream_queue_buffer(stream, b);
+}
+
+static void on_stream_state_changed(void *_data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct data *data = _data;
+ fprintf(stderr, "stream state: \"%s\"\n", pw_stream_state_as_string(state));
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_main_loop_quit(data->loop);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ /* because we started inactive, activate ourselves now */
+ pw_stream_set_active(data->stream, true);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+on_stream_io_changed(void *_data, uint32_t id, void *area, uint32_t size)
+{
+ struct data *data = _data;
+
+ switch (id) {
+ case SPA_IO_Position:
+ data->position = area;
+ break;
+ }
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format changes.
+ *
+ * We are now supposed to call pw_stream_finish_format() with success or
+ * failure, depending on if we can support the format. Because we gave
+ * a list of supported formats, this should be ok.
+ *
+ * As part of pw_stream_finish_format() we can provide parameters that
+ * will control the buffer memory allocation. This includes the metadata
+ * that we would like on our buffer, the size, alignment, etc.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ uint8_t params_buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
+ const struct spa_pod *params[5];
+ Uint32 sdl_format;
+ void *d;
+ int32_t mult, size;
+
+ /* NULL means to clear the format */
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ fprintf(stderr, "got format:\n");
+ spa_debug_format(2, NULL, param);
+
+ if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0)
+ return;
+
+ if (data->format.media_type != SPA_MEDIA_TYPE_video)
+ return;
+
+ switch (data->format.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ /* call a helper function to parse the format for us. */
+ spa_format_video_raw_parse(param, &data->format.info.raw);
+ sdl_format = id_to_sdl_format(data->format.info.raw.format);
+ data->size = SPA_RECTANGLE(data->format.info.raw.size.width,
+ data->format.info.raw.size.height);
+ mult = 1;
+ break;
+ case SPA_MEDIA_SUBTYPE_dsp:
+ spa_format_video_dsp_parse(param, &data->format.info.dsp);
+ if (data->format.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32)
+ return;
+ sdl_format = SDL_PIXELFORMAT_RGBA32;
+ data->size = SPA_RECTANGLE(data->position->video.size.width,
+ data->position->video.size.height);
+ mult = 4;
+ break;
+ default:
+ sdl_format = SDL_PIXELFORMAT_UNKNOWN;
+ break;
+ }
+
+ if (sdl_format == SDL_PIXELFORMAT_UNKNOWN) {
+ pw_stream_set_error(stream, -EINVAL, "unknown pixel format");
+ return;
+ }
+ if (data->size.width == 0 || data->size.height == 0) {
+ pw_stream_set_error(stream, -EINVAL, "invalid size");
+ return;
+ }
+
+ data->texture = SDL_CreateTexture(data->renderer,
+ sdl_format,
+ SDL_TEXTUREACCESS_STREAMING,
+ data->size.width,
+ data->size.height);
+ SDL_LockTexture(data->texture, NULL, &d, &data->stride);
+ SDL_UnlockTexture(data->texture);
+
+ switch(sdl_format) {
+ case SDL_PIXELFORMAT_YV12:
+ case SDL_PIXELFORMAT_IYUV:
+ size = (data->stride * data->size.height) * 3 / 2;
+ data->is_yuv = true;
+ break;
+ default:
+ size = data->stride * data->size.height;
+ break;
+ }
+
+ data->rect.x = 0;
+ data->rect.y = 0;
+ data->rect.w = data->size.width;
+ data->rect.h = data->size.height;
+
+ /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size,
+ * number, stride etc of the buffers */
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(size * mult),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride * mult),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<<SPA_DATA_MemPtr)));
+
+ /* a header metadata with timing information */
+ params[1] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ /* video cropping information */
+ params[2] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region)));
+#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \
+ sizeof(struct spa_meta_bitmap) + w * h * 4)
+ /* cursor information */
+ params[3] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor),
+ SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(
+ CURSOR_META_SIZE(64,64),
+ CURSOR_META_SIZE(1,1),
+ CURSOR_META_SIZE(256,256)));
+
+ /* we are done */
+ pw_stream_update_params(stream, params, 4);
+}
+
+/* these are the stream events we listen for */
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = on_stream_state_changed,
+ .io_changed = on_stream_io_changed,
+ .param_changed = on_stream_param_changed,
+ .process = on_process,
+};
+
+static int build_format(struct data *data, struct spa_pod_builder *b, const struct spa_pod **params)
+{
+ SDL_RendererInfo info;
+
+ SDL_GetRendererInfo(data->renderer, &info);
+ params[0] = sdl_build_formats(&info, b);
+
+ fprintf(stderr, "supported SDL formats:\n");
+ spa_debug_format(2, NULL, params[0]);
+
+ params[1] = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32));
+
+ fprintf(stderr, "supported DSP formats:\n");
+ spa_debug_format(2, NULL, params[1]);
+
+ return 2;
+}
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct pw_properties *props;
+ int res, n_params;
+
+ pw_init(&argc, &argv);
+
+ /* create a main loop */
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* create a simple stream, the simple stream manages to core and remote
+ * objects for you if you don't need to deal with them
+ *
+ * If you plan to autoconnect your stream, you need to provide at least
+ * media, category and role properties
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to consume
+ * the data provided to you.
+ */
+ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video",
+ PW_KEY_MEDIA_CATEGORY, "Capture",
+ PW_KEY_MEDIA_ROLE, "Camera",
+ NULL),
+ data.path = argc > 1 ? argv[1] : NULL;
+ if (data.path)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path);
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "video-play",
+ props,
+ &stream_events,
+ &data);
+
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ if (SDL_CreateWindowAndRenderer
+ (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) {
+ fprintf(stderr, "can't create window: %s\n", SDL_GetError());
+ return -1;
+ }
+
+ /* build the extra parameters to connect with. To connect, we can provide
+ * a list of supported formats. We use a builder that writes the param
+ * object to the stack. */
+ n_params = build_format(&data, &b, params);
+
+ /* now connect the stream, we need a direction (input/output),
+ * an optional target node to connect to, some flags and parameters
+ */
+ if ((res = pw_stream_connect(data.stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT | /* try to automatically connect this stream */
+ PW_STREAM_FLAG_INACTIVE | /* we will activate ourselves */
+ PW_STREAM_FLAG_MAP_BUFFERS, /* mmap the buffer data for us */
+ params, n_params)) /* extra parameters, see above */ < 0) {
+ fprintf(stderr, "can't connect: %s\n", spa_strerror(res));
+ return -1;
+ }
+
+ /* do things until we quit the mainloop */
+ pw_main_loop_run(data.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+
+ SDL_DestroyTexture(data.texture);
+ if (data.cursor)
+ SDL_DestroyTexture(data.cursor);
+ SDL_DestroyRenderer(data.renderer);
+ SDL_DestroyWindow(data.window);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/video-src-alloc.c b/src/examples/video-src-alloc.c
new file mode 100644
index 0000000..ef364fd
--- /dev/null
+++ b/src/examples/video-src-alloc.c
@@ -0,0 +1,464 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Allocating buffer memory and sending fds to the server.
+ [title]
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <signal.h>
+#include <math.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+
+#include <spa/param/video/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#define BPP 3
+#define CURSOR_WIDTH 64
+#define CURSOR_HEIGHT 64
+#define CURSOR_BPP 4
+
+#define MAX_BUFFERS 64
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+struct data {
+ struct pw_thread_loop *loop;
+ struct spa_source *timer;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_video_info_raw format;
+ int32_t stride;
+
+ int counter;
+ uint32_t seq;
+
+ double crop;
+ double accumulator;
+};
+
+static void draw_elipse(uint32_t *dst, int width, int height, uint32_t color)
+{
+ int i, j, r1, r2, r12, r22, r122;
+
+ r1 = width/2;
+ r12 = r1 * r1;
+ r2 = height/2;
+ r22 = r2 * r2;
+ r122 = r12 * r22;
+
+ for (i = -r2; i < r2; i++) {
+ for (j = -r1; j < r1; j++) {
+ dst[(i + r2)*width+(j+r1)] =
+ (i * i * r12 + j * j * r22 <= r122) ? color : 0x00000000;
+ }
+ }
+}
+
+/* called when we should push a new buffer in the queue */
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ uint32_t i, j;
+ uint8_t *p;
+ struct spa_meta *m;
+ struct spa_meta_header *h;
+ struct spa_meta_region *mc;
+ struct spa_meta_cursor *mcs;
+
+ 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;
+
+ if ((h = spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h)))) {
+#if 0
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ h->pts = SPA_TIMESPEC_TO_NSEC(&now);
+#else
+ h->pts = -1;
+#endif
+ h->flags = 0;
+ h->seq = data->seq++;
+ h->dts_offset = 0;
+ }
+ if ((m = spa_buffer_find_meta(buf, SPA_META_VideoDamage))) {
+ struct spa_meta_region *r = spa_meta_first(m);
+
+ if (spa_meta_check(r, m)) {
+ r->region.position = SPA_POINT(0,0);
+ r->region.size = data->format.size;
+ r++;
+ }
+ if (spa_meta_check(r, m))
+ r->region = SPA_REGION(0,0,0,0);
+ }
+ if ((mc = spa_buffer_find_meta_data(buf, SPA_META_VideoCrop, sizeof(*mc)))) {
+ data->crop = (sin(data->accumulator) + 1.0) * 32.0;
+ mc->region.position.x = data->crop;
+ mc->region.position.y = data->crop;
+ mc->region.size.width = data->format.size.width - data->crop*2;
+ mc->region.size.height = data->format.size.height - data->crop*2;
+ }
+ if ((mcs = spa_buffer_find_meta_data(buf, SPA_META_Cursor, sizeof(*mcs)))) {
+ struct spa_meta_bitmap *mb;
+ uint32_t *bitmap, color;
+
+ mcs->id = 1;
+ mcs->position.x = (sin(data->accumulator) + 1.0) * 160.0 + 80;
+ mcs->position.y = (cos(data->accumulator) + 1.0) * 100.0 + 50;
+ mcs->hotspot.x = 0;
+ mcs->hotspot.y = 0;
+ mcs->bitmap_offset = sizeof(struct spa_meta_cursor);
+
+ mb = SPA_PTROFF(mcs, mcs->bitmap_offset, struct spa_meta_bitmap);
+ mb->format = SPA_VIDEO_FORMAT_ARGB;
+ mb->size.width = CURSOR_WIDTH;
+ mb->size.height = CURSOR_HEIGHT;
+ mb->stride = CURSOR_WIDTH * CURSOR_BPP;
+ mb->offset = sizeof(struct spa_meta_bitmap);
+
+ bitmap = SPA_PTROFF(mb, mb->offset, uint32_t);
+ color = (cos(data->accumulator) + 1.0) * (1 << 23);
+ color |= 0xff000000;
+
+ draw_elipse(bitmap, mb->size.width, mb->size.height, color);
+ }
+
+ for (i = 0; i < data->format.size.height; i++) {
+ for (j = 0; j < data->format.size.width * BPP; j++) {
+ p[j] = data->counter + j * i;
+ }
+ p += data->stride;
+ data->counter += 13;
+ }
+
+ data->accumulator += M_PI_M2 / 50.0;
+ if (data->accumulator >= M_PI_M2)
+ data->accumulator -= M_PI_M2;
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->size = data->format.size.height * data->stride;
+ buf->datas[0].chunk->stride = data->stride;
+
+ pw_stream_queue_buffer(data->stream, b);
+}
+
+/* trigger the graph when we are a driver */
+static void on_timeout(void *userdata, uint64_t expirations)
+{
+ struct data *data = userdata;
+ pw_log_trace("timeout");
+ pw_stream_trigger_process(data->stream);
+}
+
+/* when the stream is STREAMING, start the timer at 40ms intervals
+ * to produce and push a frame. In other states we PAUSE the timer. */
+static void on_stream_state_changed(void *_data, enum pw_stream_state old, enum pw_stream_state state,
+ const char *error)
+{
+ struct data *data = _data;
+
+ printf("stream state: \"%s\"\n", pw_stream_state_as_string(state));
+
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ printf("node id: %d\n", pw_stream_get_node_id(data->stream));
+ pw_loop_update_timer(pw_thread_loop_get_loop(data->loop),
+ data->timer, NULL, NULL, false);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ {
+ struct timespec timeout, interval;
+
+ timeout.tv_sec = 0;
+ timeout.tv_nsec = 1;
+ interval.tv_sec = 0;
+ interval.tv_nsec = 40 * SPA_NSEC_PER_MSEC;
+
+ if (pw_stream_is_driving(data->stream))
+ pw_loop_update_timer(pw_thread_loop_get_loop(data->loop),
+ data->timer, &timeout, &interval, false);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+/* we set the PW_STREAM_FLAG_ALLOC_BUFFERS flag when connecting so we need
+ * to provide buffer memory. */
+static void on_stream_add_buffer(void *_data, struct pw_buffer *buffer)
+{
+ struct data *data = _data;
+ struct spa_buffer *buf = buffer->buffer;
+ struct spa_data *d;
+#ifdef HAVE_MEMFD_CREATE
+ unsigned int seals;
+#endif
+
+ pw_log_info("add buffer %p", buffer);
+ d = buf->datas;
+
+ if ((d[0].type & (1<<SPA_DATA_MemFd)) == 0) {
+ pw_log_error("unsupported data type %08x", d[0].type);
+ return;
+ }
+
+ /* create the memfd on the buffer, set the type and flags */
+ d[0].type = SPA_DATA_MemFd;
+ d[0].flags = SPA_DATA_FLAG_READWRITE;
+#ifdef HAVE_MEMFD_CREATE
+ d[0].fd = memfd_create("video-src-memfd", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+#else
+ d[0].fd = -1;
+#endif
+ if (d[0].fd == -1) {
+ pw_log_error("can't create memfd: %m");
+ return;
+ }
+ d[0].mapoffset = 0;
+ d[0].maxsize = data->stride * data->format.size.height;
+
+ /* truncate to the right size before we set seals */
+ if (ftruncate(d[0].fd, d[0].maxsize) < 0) {
+ pw_log_error("can't truncate to %d: %m", d[0].maxsize);
+ return;
+ }
+#ifdef HAVE_MEMFD_CREATE
+ /* not enforced yet but server might require SEAL_SHRINK later */
+ seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL;
+ if (fcntl(d[0].fd, F_ADD_SEALS, seals) == -1) {
+ pw_log_warn("Failed to add seals: %m");
+ }
+#endif
+
+ /* now mmap so we can write to it in the process function above */
+ d[0].data = mmap(NULL, d[0].maxsize, PROT_READ|PROT_WRITE,
+ MAP_SHARED, d[0].fd, d[0].mapoffset);
+ if (d[0].data == MAP_FAILED) {
+ pw_log_error("can't mmap memory: %m");
+ return;
+ }
+}
+
+/* close the memfd we set on the buffers here */
+static void on_stream_remove_buffer(void *_data, struct pw_buffer *buffer)
+{
+ struct spa_buffer *buf = buffer->buffer;
+ struct spa_data *d;
+
+ d = buf->datas;
+ pw_log_info("remove buffer %p", buffer);
+
+ munmap(d[0].data, d[0].maxsize);
+ close(d[0].fd);
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format param.
+ *
+ * We are now supposed to call pw_stream_update_params() with success or
+ * failure, depending on if we can support the format. Because we gave
+ * a list of supported formats, this should be ok.
+ *
+ * As part of pw_stream_update_params() we can provide parameters that
+ * will control the buffer memory allocation. This includes the metadata
+ * that we would like on our buffer, the size, alignment, etc.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ uint8_t params_buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
+ const struct spa_pod *params[5];
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ spa_format_video_raw_parse(param, &data->format);
+
+ data->stride = SPA_ROUND_UP_N(data->format.size.width * BPP, 4);
+
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->format.size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<<SPA_DATA_MemFd));
+
+ params[1] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+
+ params[2] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage),
+ SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(
+ sizeof(struct spa_meta_region) * 16,
+ sizeof(struct spa_meta_region) * 1,
+ sizeof(struct spa_meta_region) * 16));
+ params[3] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region)));
+#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \
+ sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP)
+ params[4] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor),
+ SPA_PARAM_META_size, SPA_POD_Int(
+ CURSOR_META_SIZE(CURSOR_WIDTH,CURSOR_HEIGHT)));
+
+ pw_stream_update_params(stream, params, 5);
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .process = on_process,
+ .state_changed = on_stream_state_changed,
+ .param_changed = on_stream_param_changed,
+ .add_buffer = on_stream_add_buffer,
+ .remove_buffer = on_stream_remove_buffer,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_thread_loop_signal(data->loop, false);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ pw_init(&argc, &argv);
+
+ /* create a thread loop and start it */
+ data.loop = pw_thread_loop_new("video-src-alloc", NULL);
+
+ /* take the lock around all PipeWire functions. In callbacks, the lock
+ * is already taken for you but it's ok to lock again because the lock is
+ * recursive */
+ pw_thread_loop_lock(data.loop);
+
+ /* install some handlers to exit nicely */
+ pw_loop_add_signal(pw_thread_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_thread_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* start after the signal handlers are set */
+ pw_thread_loop_start(data.loop);
+
+ /* create a simple stream, the simple stream manages the core
+ * object for you if you don't want to deal with them.
+ *
+ * We're making a new video provider. We need to set the media-class
+ * property.
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to provide
+ * the data.
+ */
+ data.stream = pw_stream_new_simple(
+ pw_thread_loop_get_loop(data.loop),
+ "video-src-alloc",
+ pw_properties_new(
+ PW_KEY_MEDIA_CLASS, "Video/Source",
+ NULL),
+ &stream_events,
+ &data);
+
+ /* make a timer to schedule our frames */
+ data.timer = pw_loop_add_timer(pw_thread_loop_get_loop(data.loop), on_timeout, &data);
+
+ /* build the extra parameter for the connection. Here we make an
+ * EnumFormat parameter which lists the possible formats we can provide.
+ * The server will select a format that matches and informs us about this
+ * in the stream param_changed event.
+ */
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ 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_Id(SPA_VIDEO_FORMAT_RGB),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(4096, 4096)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(25, 1)));
+
+ /* now connect the stream, we need a direction (input/output),
+ * an optional target node to connect to, some flags and parameters.
+ *
+ * Here we pass PW_STREAM_FLAG_ALLOC_BUFFERS. We should in the
+ * add_buffer callback configure the buffer memory. This should be
+ * fd backed memory (memfd, dma-buf, ...) that can be shared with
+ * the server. */
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_DRIVER |
+ PW_STREAM_FLAG_ALLOC_BUFFERS,
+ params, 1);
+
+ /* unlock, run the loop and wait, this will trigger the callbacks */
+ pw_thread_loop_wait(data.loop);
+
+ /* unlock before stop */
+ pw_thread_loop_unlock(data.loop);
+ pw_thread_loop_stop(data.loop);
+
+ pw_stream_destroy(data.stream);
+
+ /* destroy after dependent objects are destroyed */
+ pw_thread_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/video-src-fixate.c b/src/examples/video-src-fixate.c
new file mode 100644
index 0000000..fdcc666
--- /dev/null
+++ b/src/examples/video-src-fixate.c
@@ -0,0 +1,602 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Fixating negotiated modifiers.
+ [title]
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <signal.h>
+#include <math.h>
+#include <libdrm/drm_fourcc.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+
+#include <spa/param/video/format-utils.h>
+#include <spa/debug/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define BPP 3
+#define CURSOR_WIDTH 64
+#define CURSOR_HEIGHT 64
+#define CURSOR_BPP 4
+
+#define MAX_BUFFERS 64
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+uint64_t supported_modifiers[] = {DRM_FORMAT_MOD_INVALID, DRM_FORMAT_MOD_LINEAR};
+
+struct data {
+ struct pw_thread_loop *loop;
+ struct spa_source *timer;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_video_info_raw format;
+ int32_t stride;
+
+ int counter;
+ uint32_t seq;
+
+ double crop;
+ double accumulator;
+};
+
+static void draw_elipse(uint32_t *dst, int width, int height, uint32_t color)
+{
+ int i, j, r1, r2, r12, r22, r122;
+
+ r1 = width/2;
+ r12 = r1 * r1;
+ r2 = height/2;
+ r22 = r2 * r2;
+ r122 = r12 * r22;
+
+ for (i = -r2; i < r2; i++) {
+ for (j = -r1; j < r1; j++) {
+ dst[(i + r2)*width+(j+r1)] =
+ (i * i * r12 + j * j * r22 <= r122) ? color : 0x00000000;
+ }
+ }
+}
+
+static struct spa_pod *fixate_format(struct spa_pod_builder *b, enum spa_video_format format,
+ uint64_t *modifier)
+{
+ struct spa_pod_frame f[1];
+
+ 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_video), 0);
+ spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0);
+ /* format */
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0);
+ /* modifiers */
+ if (modifier) {
+ // we only support implicit modifiers, use shortpath to skip fixation phase
+ spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY);
+ spa_pod_builder_long(b, *modifier);
+ }
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size,
+ SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1,1),
+ &SPA_RECTANGLE(4096,4096)),
+ 0);
+ // variable framerate
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate,
+ SPA_POD_Fraction(&SPA_FRACTION(25, 1)), 0);
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static struct spa_pod *build_format(struct spa_pod_builder *b, enum spa_video_format format,
+ uint64_t *modifiers, int modifier_count)
+{
+ struct spa_pod_frame f[2];
+ int i, c;
+
+ 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_video), 0);
+ spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0);
+ /* format */
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0);
+ /* modifiers */
+ if (modifier_count == 1 && modifiers[0] == DRM_FORMAT_MOD_INVALID) {
+ // we only support implicit modifiers, use shortpath to skip fixation phase
+ spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY);
+ spa_pod_builder_long(b, modifiers[0]);
+ } else if (modifier_count > 0) {
+ // build an enumeration of modifiers
+ spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+ // modifiers from the array
+ for (i = 0, c = 0; i < modifier_count; i++) {
+ spa_pod_builder_long(b, modifiers[i]);
+ if (c++ == 0)
+ spa_pod_builder_long(b, modifiers[i]);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size,
+ SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1,1),
+ &SPA_RECTANGLE(4096,4096)),
+ 0);
+ // variable framerate
+ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate,
+ SPA_POD_Fraction(&SPA_FRACTION(25, 1)), 0);
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+/* called when we should push a new buffer in the queue */
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ uint32_t i, j;
+ uint8_t *p;
+ struct spa_meta *m;
+ struct spa_meta_header *h;
+ struct spa_meta_region *mc;
+ struct spa_meta_cursor *mcs;
+
+ 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) {
+ printf("No data ptr\n");
+ goto done;
+ }
+
+ if ((h = spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h)))) {
+#if 0
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ h->pts = SPA_TIMESPEC_TO_NSEC(&now);
+#else
+ h->pts = -1;
+#endif
+ h->flags = 0;
+ h->seq = data->seq++;
+ h->dts_offset = 0;
+ }
+ if ((m = spa_buffer_find_meta(buf, SPA_META_VideoDamage))) {
+ struct spa_meta_region *r = spa_meta_first(m);
+
+ if (spa_meta_check(r, m)) {
+ r->region.position = SPA_POINT(0,0);
+ r->region.size = data->format.size;
+ r++;
+ }
+ if (spa_meta_check(r, m))
+ r->region = SPA_REGION(0,0,0,0);
+ }
+ if ((mc = spa_buffer_find_meta_data(buf, SPA_META_VideoCrop, sizeof(*mc)))) {
+ data->crop = (sin(data->accumulator) + 1.0) * 32.0;
+ mc->region.position.x = data->crop;
+ mc->region.position.y = data->crop;
+ mc->region.size.width = data->format.size.width - data->crop*2;
+ mc->region.size.height = data->format.size.height - data->crop*2;
+ }
+ if ((mcs = spa_buffer_find_meta_data(buf, SPA_META_Cursor, sizeof(*mcs)))) {
+ struct spa_meta_bitmap *mb;
+ uint32_t *bitmap, color;
+
+ mcs->id = 1;
+ mcs->position.x = (sin(data->accumulator) + 1.0) * 160.0 + 80;
+ mcs->position.y = (cos(data->accumulator) + 1.0) * 100.0 + 50;
+ mcs->hotspot.x = 0;
+ mcs->hotspot.y = 0;
+ mcs->bitmap_offset = sizeof(struct spa_meta_cursor);
+
+ mb = SPA_PTROFF(mcs, mcs->bitmap_offset, struct spa_meta_bitmap);
+ mb->format = SPA_VIDEO_FORMAT_ARGB;
+ mb->size.width = CURSOR_WIDTH;
+ mb->size.height = CURSOR_HEIGHT;
+ mb->stride = CURSOR_WIDTH * CURSOR_BPP;
+ mb->offset = sizeof(struct spa_meta_bitmap);
+
+ bitmap = SPA_PTROFF(mb, mb->offset, uint32_t);
+ color = (cos(data->accumulator) + 1.0) * (1 << 23);
+ color |= 0xff000000;
+
+ draw_elipse(bitmap, mb->size.width, mb->size.height, color);
+ }
+
+ for (i = 0; i < data->format.size.height; i++) {
+ for (j = 0; j < data->format.size.width * BPP; j++) {
+ p[j] = data->counter + j * i;
+ }
+ p += data->stride;
+ data->counter += 13;
+ }
+
+ data->accumulator += M_PI_M2 / 50.0;
+ if (data->accumulator >= M_PI_M2)
+ data->accumulator -= M_PI_M2;
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->size = data->format.size.height * data->stride;
+ buf->datas[0].chunk->stride = data->stride;
+
+done:
+ pw_stream_queue_buffer(data->stream, b);
+}
+
+/* trigger the graph when we are a driver */
+static void on_timeout(void *userdata, uint64_t expirations)
+{
+ struct data *data = userdata;
+ pw_log_trace("timeout");
+ pw_stream_trigger_process(data->stream);
+}
+
+/* when the stream is STREAMING, start the timer at 40ms intervals
+ * to produce and push a frame. In other states we PAUSE the timer. */
+static void on_stream_state_changed(void *_data, enum pw_stream_state old, enum pw_stream_state state,
+ const char *error)
+{
+ struct data *data = _data;
+
+ printf("stream state: \"%s\"\n", pw_stream_state_as_string(state));
+
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ printf("node id: %d\n", pw_stream_get_node_id(data->stream));
+ pw_loop_update_timer(pw_thread_loop_get_loop(data->loop),
+ data->timer, NULL, NULL, false);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ {
+ struct timespec timeout, interval;
+
+ timeout.tv_sec = 0;
+ timeout.tv_nsec = 1;
+ interval.tv_sec = 0;
+ interval.tv_nsec = 40 * SPA_NSEC_PER_MSEC;
+
+ if (pw_stream_is_driving(data->stream))
+ pw_loop_update_timer(pw_thread_loop_get_loop(data->loop),
+ data->timer, &timeout, &interval, false);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+/* we set the PW_STREAM_FLAG_ALLOC_BUFFERS flag when connecting so we need
+ * to provide buffer memory. */
+static void on_stream_add_buffer(void *_data, struct pw_buffer *buffer)
+{
+ printf("add_buffer\n");
+ struct data *data = _data;
+ struct spa_buffer *buf = buffer->buffer;
+ struct spa_data *d;
+#ifdef HAVE_MEMFD_CREATE
+ unsigned int seals;
+#endif
+
+ pw_log_info("add buffer %p", buffer);
+ d = buf->datas;
+
+ if ((d[0].type & (1<<SPA_DATA_DmaBuf)) > 0) {
+ printf("pretend to support dmabufs while setting the fd to -1\n");
+ d[0].type = SPA_DATA_DmaBuf;
+ d[0].fd = -1;
+ d[0].data = NULL;
+ return;
+ }
+
+ if ((d[0].type & (1<<SPA_DATA_MemFd)) == 0) {
+ pw_log_error("unsupported data type %08x", d[0].type);
+ return;
+ }
+
+ printf("use memfd\n");
+ /* create the memfd on the buffer, set the type and flags */
+ d[0].type = SPA_DATA_MemFd;
+ d[0].flags = SPA_DATA_FLAG_READWRITE;
+#ifdef HAVE_MEMFD_CREATE
+ d[0].fd = memfd_create("video-src-fixate-memfd", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+#else
+ d[0].fd = -1;
+#endif
+ if (d[0].fd == -1) {
+ pw_log_error("can't create memfd: %m");
+ return;
+ }
+ d[0].mapoffset = 0;
+ d[0].maxsize = data->stride * data->format.size.height;
+
+ /* truncate to the right size before we set seals */
+ if (ftruncate(d[0].fd, d[0].maxsize) < 0) {
+ pw_log_error("can't truncate to %d: %m", d[0].maxsize);
+ return;
+ }
+#ifdef HAVE_MEMFD_CREATE
+ /* not enforced yet but server might require SEAL_SHRINK later */
+ seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL;
+ if (fcntl(d[0].fd, F_ADD_SEALS, seals) == -1) {
+ pw_log_warn("Failed to add seals: %m");
+ }
+#endif
+
+ /* now mmap so we can write to it in the process function above */
+ d[0].data = mmap(NULL, d[0].maxsize, PROT_READ|PROT_WRITE,
+ MAP_SHARED, d[0].fd, d[0].mapoffset);
+ if (d[0].data == MAP_FAILED) {
+ pw_log_error("can't mmap memory: %m");
+ return;
+ }
+}
+
+/* close the memfd we set on the buffers here */
+static void on_stream_remove_buffer(void *_data, struct pw_buffer *buffer)
+{
+ printf("remove_buffer\n");
+ struct spa_buffer *buf = buffer->buffer;
+ struct spa_data *d;
+
+ d = buf->datas;
+ pw_log_info("remove buffer %p", buffer);
+ if ((d[0].type & (1<<SPA_DATA_DmaBuf)) == 0)
+ return;
+
+ munmap(d[0].data, d[0].maxsize);
+ close(d[0].fd);
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format param.
+ *
+ * We are now supposed to call pw_stream_update_params() with success or
+ * failure, depending on if we can support the format. Because we gave
+ * a list of supported formats, this should be ok.
+ *
+ * As part of pw_stream_update_params() we can provide parameters that
+ * will control the buffer memory allocation. This includes the metadata
+ * that we would like on our buffer, the size, alignment, etc.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ uint8_t params_buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
+ const struct spa_pod *params[5];
+ int blocks, size, stride, buffertypes;
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ printf("param changed: \n");
+ spa_debug_format(4, NULL, param);
+
+ spa_format_video_raw_parse(param, &data->format);
+
+ data->stride = SPA_ROUND_UP_N(data->format.size.width * BPP, 4);
+
+ const struct spa_pod_prop *prop_modifier;
+ // check if client supports modifier
+ if ((prop_modifier = spa_pod_find_prop(param, NULL, SPA_FORMAT_VIDEO_modifier)) == NULL) {
+ blocks = 1;
+ size = data->stride * data->format.size.height;
+ stride = data->stride;
+ buffertypes = (1<<SPA_DATA_MemFd);
+ } else {
+ // check if the modifier is fixated
+ if ((prop_modifier->flags & SPA_POD_PROP_FLAG_DONT_FIXATE) > 0) {
+ const struct spa_pod *pod_modifier = &prop_modifier->value;
+ printf("fixating format\n");
+
+ uint32_t n_modifiers = SPA_POD_CHOICE_N_VALUES(pod_modifier);
+ uint64_t *modifiers = SPA_POD_CHOICE_VALUES(pod_modifier);
+ uint64_t modifier;
+ // shortcut for the old gbm allocator path
+ if (n_modifiers == 1 && modifiers[0] == DRM_FORMAT_MOD_INVALID) {
+ modifier = modifiers[0];
+ } else {
+ // Use the allocator to find the best modifier from the list
+ modifier = modifiers[rand()%n_modifiers];
+ }
+
+ params[0] = fixate_format(&b, SPA_VIDEO_FORMAT_RGB, &modifier);
+
+ params[1] = build_format(&b, SPA_VIDEO_FORMAT_RGB,
+ supported_modifiers, sizeof(supported_modifiers)/sizeof(supported_modifiers[0]));
+ params[2] = build_format(&b, SPA_VIDEO_FORMAT_RGB,
+ NULL, 0);
+
+ printf("announcing fixated EnumFormats\n");
+ for (unsigned int i=0; i < 3; i++) {
+ spa_debug_format(4, NULL, params[i]);
+ }
+
+ pw_stream_update_params(stream, params, 3);
+ return;
+ }
+ printf("no fixation required\n");
+ blocks = 1;
+ size = data->stride * data->format.size.height;
+ stride = data->stride;
+ buffertypes = (1<<SPA_DATA_DmaBuf);
+ }
+
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(size),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(buffertypes));
+
+ params[1] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+
+ params[2] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage),
+ SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(
+ sizeof(struct spa_meta_region) * 16,
+ sizeof(struct spa_meta_region) * 1,
+ sizeof(struct spa_meta_region) * 16));
+ params[3] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region)));
+#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \
+ sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP)
+ params[4] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor),
+ SPA_PARAM_META_size, SPA_POD_Int(
+ CURSOR_META_SIZE(CURSOR_WIDTH,CURSOR_HEIGHT)));
+
+ pw_stream_update_params(stream, params, 5);
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .process = on_process,
+ .state_changed = on_stream_state_changed,
+ .param_changed = on_stream_param_changed,
+ .add_buffer = on_stream_add_buffer,
+ .remove_buffer = on_stream_remove_buffer,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_thread_loop_signal(data->loop, false);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ srand(32);
+
+ pw_init(&argc, &argv);
+
+ /* create a thread loop and start it */
+ data.loop = pw_thread_loop_new("video-src-fixate", NULL);
+
+ /* take the lock around all PipeWire functions. In callbacks, the lock
+ * is already taken for you but it's ok to lock again because the lock is
+ * recursive */
+ pw_thread_loop_lock(data.loop);
+
+ /* install some handlers to exit nicely */
+ pw_loop_add_signal(pw_thread_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_thread_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* start after the signal handlers are set */
+ pw_thread_loop_start(data.loop);
+
+ /* create a simple stream, the simple stream manages the core
+ * object for you if you don't want to deal with them.
+ *
+ * We're making a new video provider. We need to set the media-class
+ * property.
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to provide
+ * the data.
+ */
+ data.stream = pw_stream_new_simple(
+ pw_thread_loop_get_loop(data.loop),
+ "video-src-fixate",
+ pw_properties_new(
+ PW_KEY_MEDIA_CLASS, "Video/Source",
+ NULL),
+ &stream_events,
+ &data);
+
+ /* make a timer to schedule our frames */
+ data.timer = pw_loop_add_timer(pw_thread_loop_get_loop(data.loop), on_timeout, &data);
+
+ /* build the extra parameter for the connection. Here we make an
+ * EnumFormat parameter which lists the possible formats we can provide.
+ * The server will select a format that matches and informs us about this
+ * in the stream param_changed event.
+ */
+ params[0] = build_format(&b, SPA_VIDEO_FORMAT_RGB,
+ supported_modifiers, sizeof(supported_modifiers)/sizeof(supported_modifiers[0]));
+ params[1] = build_format(&b, SPA_VIDEO_FORMAT_RGB, NULL, 0);
+
+ printf("announcing starting EnumFormats\n");
+ for (unsigned int i=0; i < 2; i++) {
+ spa_debug_format(4, NULL, params[i]);
+ }
+
+ /* now connect the stream, we need a direction (input/output),
+ * an optional target node to connect to, some flags and parameters.
+ *
+ * Here we pass PW_STREAM_FLAG_ALLOC_BUFFERS. We should in the
+ * add_buffer callback configure the buffer memory. This should be
+ * fd backed memory (memfd, dma-buf, ...) that can be shared with
+ * the server. */
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_DRIVER |
+ PW_STREAM_FLAG_ALLOC_BUFFERS,
+ params, 2);
+
+ /* unlock, run the loop and wait, this will trigger the callbacks */
+ pw_thread_loop_wait(data.loop);
+
+ /* unlock before stop */
+ pw_thread_loop_unlock(data.loop);
+ pw_thread_loop_stop(data.loop);
+
+ pw_stream_destroy(data.stream);
+
+ /* destroy after dependent objects are destroyed */
+ pw_thread_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/video-src-reneg.c b/src/examples/video-src-reneg.c
new file mode 100644
index 0000000..172e7dc
--- /dev/null
+++ b/src/examples/video-src-reneg.c
@@ -0,0 +1,509 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Renegotiating video producer and consumer formats with \ref pw_stream
+ [title]
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <signal.h>
+#include <math.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+
+#include <spa/param/video/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#define BPP 3
+#define CURSOR_WIDTH 64
+#define CURSOR_HEIGHT 64
+#define CURSOR_BPP 4
+
+#define MAX_BUFFERS 64
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+struct data {
+ struct pw_thread_loop *loop;
+ struct spa_source *timer;
+ struct spa_source *reneg_timer;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_video_info_raw format;
+ int32_t stride;
+
+ int counter;
+ int cycle;
+ uint32_t seq;
+
+ double crop;
+ double accumulator;
+};
+
+static void draw_elipse(uint32_t *dst, int width, int height, uint32_t color)
+{
+ int i, j, r1, r2, r12, r22, r122;
+
+ r1 = width/2;
+ r12 = r1 * r1;
+ r2 = height/2;
+ r22 = r2 * r2;
+ r122 = r12 * r22;
+
+ for (i = -r2; i < r2; i++) {
+ for (j = -r1; j < r1; j++) {
+ dst[(i + r2)*width+(j+r1)] =
+ (i * i * r12 + j * j * r22 <= r122) ? color : 0x00000000;
+ }
+ }
+}
+
+/* called when we should push a new buffer in the queue */
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ uint32_t i, j;
+ uint8_t *p;
+ struct spa_meta *m;
+ struct spa_meta_header *h;
+ struct spa_meta_region *mc;
+ struct spa_meta_cursor *mcs;
+
+ pw_log_trace("timeout");
+
+ 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;
+
+ if ((h = spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h)))) {
+#if 0
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ h->pts = SPA_TIMESPEC_TO_NSEC(&now);
+#else
+ h->pts = -1;
+#endif
+ h->flags = 0;
+ h->seq = data->seq++;
+ h->dts_offset = 0;
+ }
+ if ((m = spa_buffer_find_meta(buf, SPA_META_VideoDamage))) {
+ struct spa_meta_region *r = spa_meta_first(m);
+
+ if (spa_meta_check(r, m)) {
+ r->region.position = SPA_POINT(0,0);
+ r->region.size = data->format.size;
+ r++;
+ }
+ if (spa_meta_check(r, m))
+ r->region = SPA_REGION(0,0,0,0);
+ }
+ if ((mc = spa_buffer_find_meta_data(buf, SPA_META_VideoCrop, sizeof(*mc)))) {
+ data->crop = (sin(data->accumulator) + 1.0) * 32.0;
+ mc->region.position.x = data->crop;
+ mc->region.position.y = data->crop;
+ mc->region.size.width = data->format.size.width - data->crop*2;
+ mc->region.size.height = data->format.size.height - data->crop*2;
+ }
+ if ((mcs = spa_buffer_find_meta_data(buf, SPA_META_Cursor, sizeof(*mcs)))) {
+ struct spa_meta_bitmap *mb;
+ uint32_t *bitmap, color;
+
+ mcs->id = 1;
+ mcs->position.x = (sin(data->accumulator) + 1.0) * 160.0 + 80;
+ mcs->position.y = (cos(data->accumulator) + 1.0) * 100.0 + 50;
+ mcs->hotspot.x = 0;
+ mcs->hotspot.y = 0;
+ mcs->bitmap_offset = sizeof(struct spa_meta_cursor);
+
+ mb = SPA_PTROFF(mcs, mcs->bitmap_offset, struct spa_meta_bitmap);
+ mb->format = SPA_VIDEO_FORMAT_ARGB;
+ mb->size.width = CURSOR_WIDTH;
+ mb->size.height = CURSOR_HEIGHT;
+ mb->stride = CURSOR_WIDTH * CURSOR_BPP;
+ mb->offset = sizeof(struct spa_meta_bitmap);
+
+ bitmap = SPA_PTROFF(mb, mb->offset, uint32_t);
+ color = (cos(data->accumulator) + 1.0) * (1 << 23);
+ color |= 0xff000000;
+
+ draw_elipse(bitmap, mb->size.width, mb->size.height, color);
+ }
+
+ for (i = 0; i < data->format.size.height; i++) {
+ for (j = 0; j < data->format.size.width * BPP; j++) {
+ p[j] = data->counter + j * i;
+ }
+ p += data->stride;
+ data->counter += 13;
+ }
+
+ data->accumulator += M_PI_M2 / 50.0;
+ if (data->accumulator >= M_PI_M2)
+ data->accumulator -= M_PI_M2;
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->size = data->format.size.height * data->stride;
+ buf->datas[0].chunk->stride = data->stride;
+
+ pw_stream_queue_buffer(data->stream, b);
+}
+
+/* called on timeout and we should start the graph */
+static void on_timeout(void *userdata, uint64_t expirations)
+{
+ struct data *data = userdata;
+ pw_log_trace("timeout");
+ pw_stream_trigger_process(data->stream);
+}
+
+/* when the stream is STREAMING, start the timer at 40ms intervals
+ * to produce and push a frame. In other states we PAUSE the timer. */
+static void on_stream_state_changed(void *_data, enum pw_stream_state old, enum pw_stream_state state,
+ const char *error)
+{
+ struct data *data = _data;
+
+ printf("stream state: \"%s\"\n", pw_stream_state_as_string(state));
+
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ printf("node id: %d\n", pw_stream_get_node_id(data->stream));
+ pw_loop_update_timer(pw_thread_loop_get_loop(data->loop),
+ data->timer, NULL, NULL, false);
+ pw_loop_update_timer(pw_thread_loop_get_loop(data->loop),
+ data->reneg_timer, NULL, NULL, false);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ {
+ struct timespec timeout, interval;
+
+ timeout.tv_sec = 0;
+ timeout.tv_nsec = 1;
+ interval.tv_sec = 0;
+ interval.tv_nsec = 40 * SPA_NSEC_PER_MSEC;
+
+ if (pw_stream_is_driving(data->stream))
+ pw_loop_update_timer(pw_thread_loop_get_loop(data->loop),
+ data->timer, &timeout, &interval, false);
+
+ timeout.tv_sec = 1;
+ timeout.tv_nsec = 0;
+ interval.tv_sec = 1;
+ interval.tv_nsec = 0;
+
+ pw_loop_update_timer(pw_thread_loop_get_loop(data->loop),
+ data->reneg_timer, &timeout, &interval, false);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+/* we set the PW_STREAM_FLAG_ALLOC_BUFFERS flag when connecting so we need
+ * to provide buffer memory. */
+static void on_stream_add_buffer(void *_data, struct pw_buffer *buffer)
+{
+ struct data *data = _data;
+ struct spa_buffer *buf = buffer->buffer;
+ struct spa_data *d;
+#ifdef HAVE_MEMFD_CREATE
+ unsigned int seals;
+#endif
+
+ pw_log_info("add buffer %p", buffer);
+ d = buf->datas;
+
+ if ((d[0].type & (1<<SPA_DATA_MemFd)) == 0) {
+ pw_log_error("unsupported data type %08x", d[0].type);
+ return;
+ }
+
+ /* create the memfd on the buffer, set the type and flags */
+ d[0].type = SPA_DATA_MemFd;
+ d[0].flags = SPA_DATA_FLAG_READWRITE;
+#ifdef HAVE_MEMFD_CREATE
+ d[0].fd = memfd_create("video-src-memfd", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+#else
+ d[0].fd = -1;
+#endif
+ if (d[0].fd == -1) {
+ pw_log_error("can't create memfd: %m");
+ return;
+ }
+ d[0].mapoffset = 0;
+ d[0].maxsize = data->stride * data->format.size.height;
+
+ /* truncate to the right size before we set seals */
+ if (ftruncate(d[0].fd, d[0].maxsize) < 0) {
+ pw_log_error("can't truncate to %d: %m", d[0].maxsize);
+ return;
+ }
+#ifdef HAVE_MEMFD_CREATE
+ /* not enforced yet but server might require SEAL_SHRINK later */
+ seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL;
+ if (fcntl(d[0].fd, F_ADD_SEALS, seals) == -1) {
+ pw_log_warn("Failed to add seals: %m");
+ }
+#endif
+
+ /* now mmap so we can write to it in the process function above */
+ d[0].data = mmap(NULL, d[0].maxsize, PROT_READ|PROT_WRITE,
+ MAP_SHARED, d[0].fd, d[0].mapoffset);
+ if (d[0].data == MAP_FAILED) {
+ pw_log_error("can't mmap memory: %m");
+ return;
+ }
+}
+
+/* close the memfd we set on the buffers here */
+static void on_stream_remove_buffer(void *_data, struct pw_buffer *buffer)
+{
+ struct spa_buffer *buf = buffer->buffer;
+ struct spa_data *d;
+
+ d = buf->datas;
+ pw_log_info("remove buffer %p", buffer);
+
+ munmap(d[0].data, d[0].maxsize);
+ close(d[0].fd);
+}
+
+/* Be notified when the stream param changes. We're only looking at the
+ * format param.
+ *
+ * We are now supposed to call pw_stream_update_params() with success or
+ * failure, depending on if we can support the format. Because we gave
+ * a list of supported formats, this should be ok.
+ *
+ * As part of pw_stream_update_params() we can provide parameters that
+ * will control the buffer memory allocation. This includes the metadata
+ * that we would like on our buffer, the size, alignment, etc.
+ */
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ uint8_t params_buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
+ const struct spa_pod *params[5];
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ pw_log_info("format changed");
+ spa_format_video_raw_parse(param, &data->format);
+
+ data->stride = SPA_ROUND_UP_N(data->format.size.width * BPP, 4);
+
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->format.size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<<SPA_DATA_MemFd));
+
+ params[1] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+
+ params[2] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage),
+ SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(
+ sizeof(struct spa_meta_region) * 16,
+ sizeof(struct spa_meta_region) * 1,
+ sizeof(struct spa_meta_region) * 16));
+ params[3] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region)));
+#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \
+ sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP)
+ params[4] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor),
+ SPA_PARAM_META_size, SPA_POD_Int(
+ CURSOR_META_SIZE(CURSOR_WIDTH,CURSOR_HEIGHT)));
+
+ pw_stream_update_params(stream, params, 5);
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .process = on_process,
+ .state_changed = on_stream_state_changed,
+ .param_changed = on_stream_param_changed,
+ .add_buffer = on_stream_add_buffer,
+ .remove_buffer = on_stream_remove_buffer,
+};
+
+static void on_reneg_timeout(void *userdata, uint64_t expirations)
+{
+ struct data *data = userdata;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_pod *params[2];
+ int32_t width, height;
+
+ width = data->cycle & 1 ? 320 : 640;
+ height = data->cycle & 1 ? 240 : 480;
+
+ fprintf(stderr, "renegotiate to %dx%d:\n", width, height);
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ 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_Id(SPA_VIDEO_FORMAT_RGB),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(width, height)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(25, 1)));
+
+ pw_stream_update_params(data->stream, params, 1);
+
+ data->cycle++;
+}
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_thread_loop_signal(data->loop, false);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ pw_init(&argc, &argv);
+
+ /* create a thread loop and start it */
+ data.loop = pw_thread_loop_new("video-src-alloc", NULL);
+
+ /* take the lock around all PipeWire functions. In callbacks, the lock
+ * is already taken for you but it's ok to lock again because the lock is
+ * recursive */
+ pw_thread_loop_lock(data.loop);
+
+ /* install some handlers to exit nicely */
+ pw_loop_add_signal(pw_thread_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_thread_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ /* start after the signal handlers are set */
+ pw_thread_loop_start(data.loop);
+
+ /* create a simple stream, the simple stream manages the core
+ * object for you if you don't want to deal with them.
+ *
+ * We're making a new video provider. We need to set the media-class
+ * property.
+ *
+ * Pass your events and a user_data pointer as the last arguments. This
+ * will inform you about the stream state. The most important event
+ * you need to listen to is the process event where you need to provide
+ * the data.
+ */
+ data.stream = pw_stream_new_simple(
+ pw_thread_loop_get_loop(data.loop),
+ "video-src-alloc",
+ pw_properties_new(
+ PW_KEY_MEDIA_CLASS, "Video/Source",
+ NULL),
+ &stream_events,
+ &data);
+
+ /* make a timer to schedule our frames */
+ data.timer = pw_loop_add_timer(pw_thread_loop_get_loop(data.loop),
+ on_timeout, &data);
+
+ /* make a timer to schedule renegotiation */
+ data.reneg_timer = pw_loop_add_timer(pw_thread_loop_get_loop(data.loop),
+ on_reneg_timeout, &data);
+
+ /* build the extra parameter for the connection. Here we make an
+ * EnumFormat parameter which lists the possible formats we can provide.
+ * The server will select a format that matches and informs us about this
+ * in the stream param_changed event.
+ */
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ 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_Id(SPA_VIDEO_FORMAT_RGB),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(4096, 4096)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(25, 1)));
+
+ /* now connect the stream, we need a direction (input/output),
+ * an optional target node to connect to, some flags and parameters.
+ *
+ * Here we pass PW_STREAM_FLAG_ALLOC_BUFFERS. We should in the
+ * add_buffer callback configure the buffer memory. This should be
+ * fd backed memory (memfd, dma-buf, ...) that can be shared with
+ * the server. */
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_DRIVER |
+ PW_STREAM_FLAG_ALLOC_BUFFERS,
+ params, 1);
+
+ /* unlock, run the loop and wait, this will trigger the callbacks */
+ pw_thread_loop_wait(data.loop);
+
+ /* unlock before stop */
+ pw_thread_loop_unlock(data.loop);
+ pw_thread_loop_stop(data.loop);
+
+ pw_stream_destroy(data.stream);
+
+ /* destroy after dependent objects are destroyed */
+ pw_thread_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/examples/video-src.c b/src/examples/video-src.c
new file mode 100644
index 0000000..d7e2051
--- /dev/null
+++ b/src/examples/video-src.c
@@ -0,0 +1,357 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ [title]
+ Video source using \ref pw_stream.
+ [title]
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <signal.h>
+#include <math.h>
+
+#include <spa/param/video/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#define BPP 3
+#define CURSOR_WIDTH 64
+#define CURSOR_HEIGHT 64
+#define CURSOR_BPP 4
+
+#define MAX_BUFFERS 64
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+struct data {
+ struct pw_main_loop *loop;
+ struct spa_source *timer;
+
+ struct pw_context *context;
+ struct pw_core *core;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_video_info_raw format;
+ int32_t stride;
+
+ int counter;
+ uint32_t seq;
+
+ double crop;
+ double accumulator;
+ int res;
+};
+
+static void draw_elipse(uint32_t *dst, int width, int height, uint32_t color)
+{
+ int i, j, r1, r2, r12, r22, r122;
+
+ r1 = width/2;
+ r12 = r1 * r1;
+ r2 = height/2;
+ r22 = r2 * r2;
+ r122 = r12 * r22;
+
+ for (i = -r2; i < r2; i++) {
+ for (j = -r1; j < r1; j++) {
+ dst[(i + r2)*width+(j+r1)] =
+ (i * i * r12 + j * j * r22 <= r122) ? color : 0x00000000;
+ }
+ }
+}
+
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ uint32_t i, j;
+ uint8_t *p;
+ struct spa_meta *m;
+ struct spa_meta_header *h;
+ struct spa_meta_region *mc;
+ struct spa_meta_cursor *mcs;
+
+ 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;
+
+ if ((h = spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h)))) {
+#if 0
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ h->pts = SPA_TIMESPEC_TO_NSEC(&now);
+#else
+ h->pts = -1;
+#endif
+ h->flags = 0;
+ h->seq = data->seq++;
+ h->dts_offset = 0;
+ }
+ if ((m = spa_buffer_find_meta(buf, SPA_META_VideoDamage))) {
+ struct spa_meta_region *r = spa_meta_first(m);
+
+ if (spa_meta_check(r, m)) {
+ r->region.position = SPA_POINT(0,0);
+ r->region.size = data->format.size;
+ r++;
+ }
+ if (spa_meta_check(r, m))
+ r->region = SPA_REGION(0,0,0,0);
+ }
+ if ((mc = spa_buffer_find_meta_data(buf, SPA_META_VideoCrop, sizeof(*mc)))) {
+ data->crop = (sin(data->accumulator) + 1.0) * 32.0;
+ mc->region.position.x = data->crop;
+ mc->region.position.y = data->crop;
+ mc->region.size.width = data->format.size.width - data->crop*2;
+ mc->region.size.height = data->format.size.height - data->crop*2;
+ }
+ if ((mcs = spa_buffer_find_meta_data(buf, SPA_META_Cursor, sizeof(*mcs)))) {
+ struct spa_meta_bitmap *mb;
+ uint32_t *bitmap, color;
+
+ mcs->id = 1;
+ mcs->position.x = (sin(data->accumulator) + 1.0) * 160.0 + 80;
+ mcs->position.y = (cos(data->accumulator) + 1.0) * 100.0 + 50;
+ mcs->hotspot.x = 0;
+ mcs->hotspot.y = 0;
+ mcs->bitmap_offset = sizeof(struct spa_meta_cursor);
+
+ mb = SPA_PTROFF(mcs, mcs->bitmap_offset, struct spa_meta_bitmap);
+ mb->format = SPA_VIDEO_FORMAT_ARGB;
+ mb->size.width = CURSOR_WIDTH;
+ mb->size.height = CURSOR_HEIGHT;
+ mb->stride = CURSOR_WIDTH * CURSOR_BPP;
+ mb->offset = sizeof(struct spa_meta_bitmap);
+
+ bitmap = SPA_PTROFF(mb, mb->offset, uint32_t);
+ color = (cos(data->accumulator) + 1.0) * (1 << 23);
+ color |= 0xff000000;
+
+ draw_elipse(bitmap, mb->size.width, mb->size.height, color);
+ }
+
+ for (i = 0; i < data->format.size.height; i++) {
+ for (j = 0; j < data->format.size.width * BPP; j++) {
+ p[j] = data->counter + j * i;
+ }
+ p += data->stride;
+ data->counter += 13;
+ }
+
+ data->accumulator += M_PI_M2 / 50.0;
+ if (data->accumulator >= M_PI_M2)
+ data->accumulator -= M_PI_M2;
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->size = data->format.size.height * data->stride;
+ buf->datas[0].chunk->stride = data->stride;
+
+ pw_stream_queue_buffer(data->stream, b);
+}
+
+static void on_timeout(void *userdata, uint64_t expirations)
+{
+ struct data *data = userdata;
+ pw_log_trace("timeout");
+ pw_stream_trigger_process(data->stream);
+}
+
+static void on_stream_state_changed(void *_data, enum pw_stream_state old, enum pw_stream_state state,
+ const char *error)
+{
+ struct data *data = _data;
+
+ printf("stream state: \"%s\"\n", pw_stream_state_as_string(state));
+
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_main_loop_quit(data->loop);
+ break;
+
+ case PW_STREAM_STATE_PAUSED:
+ printf("node id: %d\n", pw_stream_get_node_id(data->stream));
+ pw_loop_update_timer(pw_main_loop_get_loop(data->loop),
+ data->timer, NULL, NULL, false);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ {
+ struct timespec timeout, interval;
+
+ timeout.tv_sec = 0;
+ timeout.tv_nsec = 1;
+ interval.tv_sec = 0;
+ interval.tv_nsec = 40 * SPA_NSEC_PER_MSEC;
+
+ pw_loop_update_timer(pw_main_loop_get_loop(data->loop),
+ data->timer, &timeout, &interval, false);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void
+on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = _data;
+ struct pw_stream *stream = data->stream;
+ uint8_t params_buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
+ const struct spa_pod *params[5];
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ spa_format_video_raw_parse(param, &data->format);
+
+ data->stride = SPA_ROUND_UP_N(data->format.size.width * BPP, 4);
+
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->format.size.height),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride));
+
+ params[1] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+
+ params[2] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage),
+ SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(
+ sizeof(struct spa_meta_region) * 16,
+ sizeof(struct spa_meta_region) * 1,
+ sizeof(struct spa_meta_region) * 16));
+ params[3] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region)));
+#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \
+ sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP)
+ params[4] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor),
+ SPA_PARAM_META_size, SPA_POD_Int(
+ CURSOR_META_SIZE(CURSOR_WIDTH,CURSOR_HEIGHT)));
+
+ pw_stream_update_params(stream, params, 5);
+}
+
+static void
+on_trigger_done(void *_data)
+{
+ pw_log_trace("trigger done");
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .process = on_process,
+ .state_changed = on_stream_state_changed,
+ .param_changed = on_stream_param_changed,
+ .trigger_done = on_trigger_done,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
+
+ data.timer = pw_loop_add_timer(pw_main_loop_get_loop(data.loop), on_timeout, &data);
+
+ data.core = pw_context_connect(data.context, NULL, 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ data.res = -errno;
+ goto cleanup;
+ }
+
+ data.stream = pw_stream_new(data.core, "video-src",
+ pw_properties_new(
+ PW_KEY_MEDIA_CLASS, "Video/Source",
+ NULL));
+
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ 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_Id(SPA_VIDEO_FORMAT_RGB),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320, 240),
+ &SPA_RECTANGLE(1, 1),
+ &SPA_RECTANGLE(4096, 4096)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(25, 1)));
+
+ pw_stream_add_listener(data.stream,
+ &data.stream_listener,
+ &stream_events,
+ &data);
+
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_DRIVER |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, 1);
+
+ pw_main_loop_run(data.loop);
+
+cleanup:
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return data.res;
+}
diff --git a/src/gst/.editorconfig b/src/gst/.editorconfig
new file mode 100644
index 0000000..b6330b2
--- /dev/null
+++ b/src/gst/.editorconfig
@@ -0,0 +1,7 @@
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/src/gst/gstpipewire.c b/src/gst/gstpipewire.c
new file mode 100644
index 0000000..b87f8a3
--- /dev/null
+++ b/src/gst/gstpipewire.c
@@ -0,0 +1,69 @@
+/* PipeWire GStreamer Elements
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * SECTION:element-pipewiresrc
+ *
+ * <refsect2>
+ * <title>Example launch line</title>
+ * |[
+ * gst-launch -v pipewiresrc ! ximagesink
+ * ]| Shows PipeWire output in an X window.
+ * </refsect2>
+ */
+
+#include "config.h"
+
+#include "gstpipewiresrc.h"
+#include "gstpipewiresink.h"
+#include "gstpipewiredeviceprovider.h"
+
+GST_DEBUG_CATEGORY (pipewire_debug);
+
+static gboolean
+plugin_init (GstPlugin *plugin)
+{
+ pw_init (NULL, NULL);
+
+ gst_element_register (plugin, "pipewiresrc", GST_RANK_PRIMARY + 1,
+ GST_TYPE_PIPEWIRE_SRC);
+ gst_element_register (plugin, "pipewiresink", GST_RANK_NONE,
+ GST_TYPE_PIPEWIRE_SINK);
+
+#ifdef HAVE_GSTREAMER_DEVICE_PROVIDER
+ if (!gst_device_provider_register (plugin, "pipewiredeviceprovider",
+ GST_RANK_PRIMARY + 1, GST_TYPE_PIPEWIRE_DEVICE_PROVIDER))
+ return FALSE;
+#endif
+
+ GST_DEBUG_CATEGORY_INIT (pipewire_debug, "pipewire", 0, "PipeWire elements");
+
+ return TRUE;
+}
+
+GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
+ GST_VERSION_MINOR,
+ pipewire,
+ "Uses PipeWire to handle media streams",
+ plugin_init, PACKAGE_VERSION, "MIT/X11", "pipewire", "pipewire.org")
diff --git a/src/gst/gstpipewireclock.c b/src/gst/gstpipewireclock.c
new file mode 100644
index 0000000..44b1158
--- /dev/null
+++ b/src/gst/gstpipewireclock.c
@@ -0,0 +1,127 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <gst/gst.h>
+
+#include "gstpipewireclock.h"
+
+GST_DEBUG_CATEGORY_STATIC (gst_pipewire_clock_debug_category);
+#define GST_CAT_DEFAULT gst_pipewire_clock_debug_category
+
+G_DEFINE_TYPE (GstPipeWireClock, gst_pipewire_clock, GST_TYPE_SYSTEM_CLOCK);
+
+GstClock *
+gst_pipewire_clock_new (struct pw_stream *stream, GstClockTime last_time)
+{
+ GstPipeWireClock *clock;
+
+ clock = g_object_new (GST_TYPE_PIPEWIRE_CLOCK, NULL);
+ clock->stream = stream;
+ clock->last_time = last_time;
+ clock->time_offset = last_time;
+
+ return GST_CLOCK_CAST (clock);
+}
+
+static GstClockTime
+gst_pipewire_clock_get_internal_time (GstClock * clock)
+{
+ GstPipeWireClock *pclock = (GstPipeWireClock *) clock;
+ GstClockTime result;
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+#if 0
+ struct pw_time t;
+ if (pclock->stream == NULL ||
+ pw_stream_get_time (pclock->stream, &t) < 0 ||
+ t.rate.denom == 0)
+ return pclock->last_time;
+
+ result = gst_util_uint64_scale_int (t.ticks, GST_SECOND * t.rate.num, t.rate.denom);
+ result += SPA_TIMESPEC_TO_NSEC(&ts) - t.now;
+
+ result += pclock->time_offset;
+ pclock->last_time = result;
+
+ GST_DEBUG ("%"PRId64", %d/%d %"PRId64" %"PRId64,
+ t.ticks, t.rate.num, t.rate.denom, t.now, result);
+#else
+ result = SPA_TIMESPEC_TO_NSEC(&ts);
+ result += pclock->time_offset;
+ pclock->last_time = result;
+#endif
+
+ return result;
+}
+
+static void
+gst_pipewire_clock_finalize (GObject * object)
+{
+ GstPipeWireClock *clock = GST_PIPEWIRE_CLOCK (object);
+
+ GST_DEBUG_OBJECT (clock, "finalize");
+
+ G_OBJECT_CLASS (gst_pipewire_clock_parent_class)->finalize (object);
+}
+
+static void
+gst_pipewire_clock_class_init (GstPipeWireClockClass * klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GstClockClass *gstclock_class = GST_CLOCK_CLASS (klass);
+
+ gobject_class->finalize = gst_pipewire_clock_finalize;
+
+ gstclock_class->get_internal_time = gst_pipewire_clock_get_internal_time;
+
+ GST_DEBUG_CATEGORY_INIT (gst_pipewire_clock_debug_category, "pipewireclock", 0,
+ "debug category for pipewireclock object");
+}
+
+static void
+gst_pipewire_clock_init (GstPipeWireClock * clock)
+{
+ GST_OBJECT_FLAG_SET (clock, GST_CLOCK_FLAG_CAN_SET_MASTER);
+}
+
+void
+gst_pipewire_clock_reset (GstPipeWireClock * clock, GstClockTime time)
+{
+ GstClockTimeDiff time_offset;
+
+ if (clock->last_time >= time)
+ time_offset = clock->last_time - time;
+ else
+ time_offset = -(time - clock->last_time);
+
+ clock->time_offset = time_offset;
+
+ GST_DEBUG_OBJECT (clock,
+ "reset clock to %" GST_TIME_FORMAT ", last %" GST_TIME_FORMAT
+ ", offset %" GST_STIME_FORMAT, GST_TIME_ARGS (time),
+ GST_TIME_ARGS (clock->last_time), GST_STIME_ARGS (time_offset));
+}
diff --git a/src/gst/gstpipewireclock.h b/src/gst/gstpipewireclock.h
new file mode 100644
index 0000000..0a3e447
--- /dev/null
+++ b/src/gst/gstpipewireclock.h
@@ -0,0 +1,71 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __GST_PIPEWIRE_CLOCK_H__
+#define __GST_PIPEWIRE_CLOCK_H__
+
+#include <gst/gst.h>
+
+#include <pipewire/pipewire.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_PIPEWIRE_CLOCK \
+ (gst_pipewire_clock_get_type())
+#define GST_PIPEWIRE_CLOCK(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_PIPEWIRE_CLOCK,GstPipeWireClock))
+#define GST_PIPEWIRE_CLOCK_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_PIPEWIRE_CLOCK,GstPipeWireClockClass))
+#define GST_IS_PIPEWIRE_CLOCK(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_PIPEWIRE_CLOCK))
+#define GST_IS_PIPEWIRE_CLOCK_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_PIPEWIRE_CLOCK))
+#define GST_PIPEWIRE_CLOCK_GET_CLASS(klass) \
+ (G_TYPE_INSTANCE_GET_CLASS ((klass), GST_TYPE_PIPEWIRE_CLOCK, GstPipeWireClockClass))
+
+typedef struct _GstPipeWireClock GstPipeWireClock;
+typedef struct _GstPipeWireClockClass GstPipeWireClockClass;
+
+struct _GstPipeWireClock {
+ GstSystemClock parent;
+
+ struct pw_stream *stream;
+ GstClockTime last_time;
+ GstClockTimeDiff time_offset;
+};
+
+struct _GstPipeWireClockClass {
+ GstSystemClockClass parent_class;
+};
+
+GType gst_pipewire_clock_get_type (void);
+
+GstClock * gst_pipewire_clock_new (struct pw_stream *stream,
+ GstClockTime last_time);
+void gst_pipewire_clock_reset (GstPipeWireClock *clock,
+ GstClockTime time);
+
+G_END_DECLS
+
+#endif /* __GST_PIPEWIRE_CLOCK_H__ */
diff --git a/src/gst/gstpipewirecore.c b/src/gst/gstpipewirecore.c
new file mode 100644
index 0000000..6910f4e
--- /dev/null
+++ b/src/gst/gstpipewirecore.c
@@ -0,0 +1,202 @@
+/* GStreamer
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <spa/utils/result.h>
+
+#include "gstpipewirecore.h"
+
+/* a list of global cores indexed by fd. */
+G_LOCK_DEFINE_STATIC (cores_lock);
+static GList *cores;
+
+static void
+on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ GstPipeWireCore *core = data;
+
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE) {
+ core->last_error = res;
+ }
+ pw_thread_loop_signal(core->loop, FALSE);
+}
+
+static void on_core_done (void *data, uint32_t id, int seq)
+{
+ GstPipeWireCore * core = data;
+ if (id == PW_ID_CORE) {
+ core->last_seq = seq;
+ pw_thread_loop_signal (core->loop, FALSE);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static GstPipeWireCore *make_core (int fd)
+{
+ GstPipeWireCore *core;
+
+ core = g_new (GstPipeWireCore, 1);
+ core->refcount = 1;
+ core->fd = fd;
+ core->loop = pw_thread_loop_new ("pipewire-main-loop", NULL);
+ core->context = pw_context_new (pw_thread_loop_get_loop(core->loop), NULL, 0);
+ core->last_seq = -1;
+ core->last_error = 0;
+ GST_DEBUG ("loop %p context %p", core->loop, core->context);
+
+ if (pw_thread_loop_start (core->loop) < 0)
+ goto mainloop_failed;
+
+ pw_thread_loop_lock (core->loop);
+
+ if (fd == -1)
+ core->core = pw_context_connect (core->context, NULL, 0);
+ else
+ core->core = pw_context_connect_fd (core->context, fcntl(fd, F_DUPFD_CLOEXEC, 3), NULL, 0);
+
+ if (core->core == NULL)
+ goto connection_failed;
+
+ pw_core_add_listener(core->core,
+ &core->core_listener,
+ &core_events,
+ core);
+
+ pw_thread_loop_unlock (core->loop);
+
+ return core;
+
+mainloop_failed:
+ {
+ GST_ERROR ("error starting mainloop");
+ pw_context_destroy (core->context);
+ pw_thread_loop_destroy (core->loop);
+ g_free (core);
+ return NULL;
+ }
+connection_failed:
+ {
+ GST_ERROR ("error connect: %m");
+ pw_thread_loop_unlock (core->loop);
+ pw_context_destroy (core->context);
+ pw_thread_loop_destroy (core->loop);
+ g_free (core);
+ return NULL;
+ }
+}
+
+typedef struct {
+ int fd;
+} FindData;
+
+static gint
+core_find (GstPipeWireCore * core, FindData * data)
+{
+ /* fd's must match */
+ if (core->fd == data->fd)
+ return 0;
+ return 1;
+}
+
+GstPipeWireCore *gst_pipewire_core_get (int fd)
+{
+ GstPipeWireCore *core;
+ FindData data;
+ GList *found;
+
+ data.fd = fd;
+
+ G_LOCK (cores_lock);
+ found = g_list_find_custom (cores, &data, (GCompareFunc) core_find);
+ if (found != NULL) {
+ core = (GstPipeWireCore *) found->data;
+ core->refcount++;
+ GST_DEBUG ("found core %p", core);
+ } else {
+ core = make_core(fd);
+ if (core != NULL) {
+ GST_DEBUG ("created core %p", core);
+ /* add to list on success */
+ cores = g_list_prepend (cores, core);
+ } else {
+ GST_WARNING ("could not create core");
+ }
+ }
+ G_UNLOCK (cores_lock);
+
+ return core;
+}
+
+static void do_sync(GstPipeWireCore * core)
+{
+ struct timespec abstime;
+ core->pending_seq = pw_core_sync(core->core, 0, core->pending_seq);
+ pw_thread_loop_get_time (core->loop, &abstime,
+ GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC);
+ while (true) {
+ if (core->last_seq == core->pending_seq || core->last_error < 0)
+ break;
+ if (pw_thread_loop_timed_wait_full (core->loop, &abstime) < 0)
+ break;
+ }
+}
+
+void gst_pipewire_core_release (GstPipeWireCore *core)
+{
+ gboolean zero;
+
+ G_LOCK (cores_lock);
+ core->refcount--;
+ if ((zero = (core->refcount == 0))) {
+ GST_DEBUG ("closing core %p", core);
+ /* remove from list, we can release the mutex after removing the connection
+ * from the list because after that, nobody can access the connection anymore. */
+ cores = g_list_remove (cores, core);
+ }
+ G_UNLOCK (cores_lock);
+
+ if (zero) {
+ pw_thread_loop_lock (core->loop);
+ do_sync(core);
+
+ pw_core_disconnect (core->core);
+ pw_thread_loop_unlock (core->loop);
+ pw_thread_loop_stop (core->loop);
+ pw_context_destroy (core->context);
+ pw_thread_loop_destroy (core->loop);
+
+ free(core);
+ }
+}
diff --git a/src/gst/gstpipewirecore.h b/src/gst/gstpipewirecore.h
new file mode 100644
index 0000000..c64d466
--- /dev/null
+++ b/src/gst/gstpipewirecore.h
@@ -0,0 +1,60 @@
+/* GStreamer
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __GST_PIPEWIRE_CORE_H__
+#define __GST_PIPEWIRE_CORE_H__
+
+#include <gst/gst.h>
+
+#include <pipewire/pipewire.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GstPipeWireCore GstPipeWireCore;
+
+#define GST_PIPEWIRE_DEFAULT_TIMEOUT 30
+
+/**
+ * GstPipeWireCore:
+ *
+ * Opaque data structure.
+ */
+struct _GstPipeWireCore {
+ gint refcount;
+ int fd;
+ struct pw_thread_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ int last_error;
+ int last_seq;
+ int pending_seq;
+};
+
+GstPipeWireCore *gst_pipewire_core_get (int fd);
+void gst_pipewire_core_release (GstPipeWireCore *core);
+
+G_END_DECLS
+
+#endif /* __GST_PIPEWIRE_CORE_H__ */
diff --git a/src/gst/gstpipewiredeviceprovider.c b/src/gst/gstpipewiredeviceprovider.c
new file mode 100644
index 0000000..598a7c5
--- /dev/null
+++ b/src/gst/gstpipewiredeviceprovider.c
@@ -0,0 +1,754 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#include <gst/gst.h>
+
+#include "gstpipewireformat.h"
+#include "gstpipewiredeviceprovider.h"
+#include "gstpipewiresrc.h"
+#include "gstpipewiresink.h"
+
+GST_DEBUG_CATEGORY_EXTERN (pipewire_debug);
+#define GST_CAT_DEFAULT pipewire_debug
+
+G_DEFINE_TYPE (GstPipeWireDevice, gst_pipewire_device, GST_TYPE_DEVICE);
+
+enum
+{
+ PROP_ID = 1,
+ PROP_SERIAL,
+ PROP_FD_DEVICE,
+};
+
+static GstElement *
+gst_pipewire_device_create_element (GstDevice * device, const gchar * name)
+{
+ GstPipeWireDevice *pipewire_dev = GST_PIPEWIRE_DEVICE (device);
+ GstElement *elem;
+ gchar *serial_str;
+
+ elem = gst_element_factory_make (pipewire_dev->element, name);
+
+ serial_str = g_strdup_printf ("%"PRIu64, pipewire_dev->serial);
+ g_object_set (elem, "target-object", serial_str,
+ "fd", pipewire_dev->fd, NULL);
+ g_free (serial_str);
+
+ return elem;
+}
+
+static gboolean
+gst_pipewire_device_reconfigure_element (GstDevice * device, GstElement * element)
+{
+ GstPipeWireDevice *pipewire_dev = GST_PIPEWIRE_DEVICE (device);
+ gchar *serial_str;
+
+ if (spa_streq(pipewire_dev->element, "pipewiresrc")) {
+ if (!GST_IS_PIPEWIRE_SRC (element))
+ return FALSE;
+ } else if (spa_streq(pipewire_dev->element, "pipewiresink")) {
+ if (!GST_IS_PIPEWIRE_SINK (element))
+ return FALSE;
+ } else {
+ g_assert_not_reached ();
+ }
+
+ serial_str = g_strdup_printf ("%"PRIu64, pipewire_dev->serial);
+ g_object_set (element, "target-object", serial_str,
+ "fd", pipewire_dev->fd, NULL);
+ g_free (serial_str);
+
+ return TRUE;
+}
+
+
+static void
+gst_pipewire_device_get_property (GObject * object, guint prop_id,
+ GValue * value, GParamSpec * pspec)
+{
+ GstPipeWireDevice *device;
+
+ device = GST_PIPEWIRE_DEVICE_CAST (object);
+
+ switch (prop_id) {
+ case PROP_ID:
+ g_value_set_uint (value, device->id);
+ break;
+ case PROP_SERIAL:
+ g_value_set_uint64 (value, device->serial);
+ break;
+ case PROP_FD_DEVICE:
+ g_value_set_int (value, device->fd);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gst_pipewire_device_set_property (GObject * object, guint prop_id,
+ const GValue * value, GParamSpec * pspec)
+{
+ GstPipeWireDevice *device;
+
+ device = GST_PIPEWIRE_DEVICE_CAST (object);
+
+ switch (prop_id) {
+ case PROP_ID:
+ device->id = g_value_get_uint (value);
+ break;
+ case PROP_SERIAL:
+ device->serial = g_value_get_uint64 (value);
+ break;
+ case PROP_FD_DEVICE:
+ device->fd = g_value_get_int (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gst_pipewire_device_finalize (GObject * object)
+{
+ G_OBJECT_CLASS (gst_pipewire_device_parent_class)->finalize (object);
+}
+
+static void
+gst_pipewire_device_class_init (GstPipeWireDeviceClass * klass)
+{
+ GstDeviceClass *dev_class = GST_DEVICE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ dev_class->create_element = gst_pipewire_device_create_element;
+ dev_class->reconfigure_element = gst_pipewire_device_reconfigure_element;
+
+ object_class->get_property = gst_pipewire_device_get_property;
+ object_class->set_property = gst_pipewire_device_set_property;
+ object_class->finalize = gst_pipewire_device_finalize;
+
+ g_object_class_install_property (object_class, PROP_ID,
+ g_param_spec_uint ("id", "Id",
+ "The internal id of the PipeWire device", 0, G_MAXUINT32, SPA_ID_INVALID,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+ g_object_class_install_property (object_class, PROP_SERIAL,
+ g_param_spec_uint64 ("serial", "Serial",
+ "The internal serial of the PipeWire device", 0, G_MAXUINT64, SPA_ID_INVALID,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+ g_object_class_install_property (object_class,
+ PROP_FD_DEVICE,
+ g_param_spec_int ("fd", "Fd", "The fd to connect with", -1, G_MAXINT, -1,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+gst_pipewire_device_init (GstPipeWireDevice * device)
+{
+}
+
+G_DEFINE_TYPE (GstPipeWireDeviceProvider, gst_pipewire_device_provider,
+ GST_TYPE_DEVICE_PROVIDER);
+
+enum
+{
+ PROP_0,
+ PROP_CLIENT_NAME,
+ PROP_FD,
+ PROP_LAST
+};
+
+struct node_data {
+ struct spa_list link;
+ GstPipeWireDeviceProvider *self;
+ struct pw_node *proxy;
+ struct spa_hook proxy_listener;
+ uint32_t id;
+ uint64_t serial;
+ struct spa_hook node_listener;
+ struct pw_node_info *info;
+ GstCaps *caps;
+ GstDevice *dev;
+};
+
+struct port_data {
+ struct node_data *node_data;
+ struct pw_port *proxy;
+ struct spa_hook proxy_listener;
+ uint32_t id;
+ uint64_t serial;
+ struct spa_hook port_listener;
+};
+
+static struct node_data *find_node_data(struct spa_list *nodes, uint32_t id)
+{
+ struct node_data *n;
+ spa_list_for_each(n, nodes, link) {
+ if (n->id == id)
+ return n;
+ }
+ return NULL;
+}
+
+static GstDevice *
+new_node (GstPipeWireDeviceProvider *self, struct node_data *data)
+{
+ GstStructure *props;
+ const gchar *klass = NULL, *name = NULL;
+ GstPipeWireDeviceType type;
+ const struct pw_node_info *info = data->info;
+ const gchar *element = NULL;
+ GstPipeWireDevice *gstdev;
+
+ if (info->max_input_ports > 0 && info->max_output_ports == 0) {
+ type = GST_PIPEWIRE_DEVICE_TYPE_SINK;
+ element = "pipewiresink";
+ } else if (info->max_output_ports > 0 && info->max_input_ports == 0) {
+ type = GST_PIPEWIRE_DEVICE_TYPE_SOURCE;
+ element = "pipewiresrc";
+ } else {
+ return NULL;
+ }
+
+ props = gst_structure_new_empty ("pipewire-proplist");
+ if (info->props) {
+ const struct spa_dict_item *item;
+ 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);
+ }
+ if (klass == NULL)
+ klass = "unknown/unknown";
+ 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;
+ if (props)
+ gst_structure_free (props);
+
+ return GST_DEVICE (gstdev);
+}
+
+static void do_add_nodes(GstPipeWireDeviceProvider *self)
+{
+ struct node_data *nd;
+
+ spa_list_for_each(nd, &self->nodes, link) {
+ if (nd->dev != NULL)
+ continue;
+ pw_log_info("add node %d", nd->id);
+ nd->dev = new_node (self, nd);
+ if (nd->dev) {
+ if(self->list_only)
+ self->devices = g_list_prepend (self->devices, gst_object_ref_sink (nd->dev));
+ else
+ gst_device_provider_device_add (GST_DEVICE_PROVIDER (self), nd->dev);
+ }
+ }
+}
+
+static void resync(GstPipeWireDeviceProvider *self)
+{
+ self->seq = pw_core_sync(self->core->core, PW_ID_CORE, self->seq);
+ pw_log_debug("resync %d", self->seq);
+}
+
+static void
+on_core_done (void *data, uint32_t id, int seq)
+{
+ GstPipeWireDeviceProvider *self = data;
+
+ pw_log_debug("check %d %d", seq, self->seq);
+ if (id == PW_ID_CORE && seq == self->seq) {
+ do_add_nodes(self);
+ self->end = true;
+ if (self->core)
+ pw_thread_loop_signal (self->core->loop, FALSE);
+ }
+}
+
+
+static void
+on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ GstPipeWireDeviceProvider *self = data;
+
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE) {
+ self->error = res;
+ }
+ pw_thread_loop_signal(self->core->loop, FALSE);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void port_event_info(void *data, const struct pw_port_info *info)
+{
+ struct port_data *port_data = data;
+ struct node_data *node_data = port_data->node_data;
+ uint32_t i;
+
+ pw_log_debug("%p", port_data);
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ if (id == SPA_PARAM_EnumFormat &&
+ info->params[i].flags & SPA_PARAM_INFO_READ &&
+ node_data->caps == NULL) {
+ node_data->caps = gst_caps_new_empty ();
+ pw_port_enum_params(port_data->proxy, 0, id, 0, UINT32_MAX, NULL);
+ resync(node_data->self);
+ }
+ }
+ }
+}
+
+static void port_event_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct port_data *port_data = data;
+ struct node_data *node_data = port_data->node_data;
+ GstCaps *c1;
+
+ c1 = gst_caps_from_format (param);
+ if (c1 && node_data->caps)
+ gst_caps_append (node_data->caps, c1);
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+ .param = port_event_param
+};
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct node_data *node_data = data;
+ uint32_t i;
+
+ pw_log_debug("%p", node_data->proxy);
+
+ info = node_data->info = pw_node_info_update(node_data->info, info);
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ if (id == SPA_PARAM_EnumFormat &&
+ info->params[i].flags & SPA_PARAM_INFO_READ &&
+ node_data->caps == NULL) {
+ node_data->caps = gst_caps_new_empty ();
+ pw_node_enum_params(node_data->proxy, 0, id, 0, UINT32_MAX, NULL);
+ resync(node_data->self);
+ }
+ }
+ }
+}
+
+static void node_event_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct node_data *node_data = data;
+ GstCaps *c1;
+
+ c1 = gst_caps_from_format (param);
+ if (c1 && node_data->caps)
+ gst_caps_append (node_data->caps, c1);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = node_event_param
+};
+
+static void
+removed_node (void *data)
+{
+ struct node_data *nd = data;
+ pw_proxy_destroy((struct pw_proxy*)nd->proxy);
+}
+
+static void
+destroy_node (void *data)
+{
+ struct node_data *nd = data;
+ GstPipeWireDeviceProvider *self = nd->self;
+ GstDeviceProvider *provider = GST_DEVICE_PROVIDER (self);
+
+ pw_log_debug("destroy %p", nd);
+
+ if (nd->dev != NULL) {
+ gst_device_provider_device_remove (provider, GST_DEVICE (nd->dev));
+ }
+ if (nd->caps)
+ gst_caps_unref(nd->caps);
+ if (nd->info)
+ pw_node_info_free(nd->info);
+
+ spa_list_remove(&nd->link);
+}
+
+static const struct pw_proxy_events proxy_node_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = removed_node,
+ .destroy = destroy_node,
+};
+
+static void
+removed_port (void *data)
+{
+ struct port_data *pd = data;
+ pw_proxy_destroy((struct pw_proxy*)pd->proxy);
+}
+
+static void
+destroy_port (void *data)
+{
+ struct port_data *pd = data;
+ pw_log_debug("destroy %p", pd);
+}
+
+static const struct pw_proxy_events proxy_port_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = removed_port,
+ .destroy = destroy_port,
+};
+
+static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ GstPipeWireDeviceProvider *self = data;
+ GstDeviceProvider *provider = (GstDeviceProvider*)self;
+ struct node_data *nd;
+ const char *str;
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ struct pw_node *node;
+
+ node = pw_registry_bind(self->registry,
+ id, type, PW_VERSION_NODE, sizeof(*nd));
+ if (node == NULL)
+ goto no_mem;
+
+ if (props != NULL) {
+ str = spa_dict_lookup(props, PW_KEY_OBJECT_PATH);
+ if (str != NULL) {
+ if (g_str_has_prefix(str, "alsa:"))
+ gst_device_provider_hide_provider (provider, "pulsedeviceprovider");
+ else if (g_str_has_prefix(str, "v4l2:"))
+ gst_device_provider_hide_provider (provider, "v4l2deviceprovider");
+ else if (g_str_has_prefix(str, "libcamera:"))
+ gst_device_provider_hide_provider (provider, "libcameraprovider");
+ }
+ }
+
+ nd = pw_proxy_get_user_data((struct pw_proxy*)node);
+ nd->self = self;
+ nd->proxy = node;
+ nd->id = id;
+ if (!props || !spa_atou64(spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL), &nd->serial, 0))
+ nd->serial = SPA_ID_INVALID;
+ spa_list_append(&self->nodes, &nd->link);
+ pw_node_add_listener(node, &nd->node_listener, &node_events, nd);
+ pw_proxy_add_listener((struct pw_proxy*)node, &nd->proxy_listener, &proxy_node_events, nd);
+ resync(self);
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ struct pw_port *port;
+ struct port_data *pd;
+
+ if ((str = spa_dict_lookup(props, PW_KEY_NODE_ID)) == NULL)
+ return;
+
+ if ((nd = find_node_data(&self->nodes, atoi(str))) == NULL)
+ return;
+
+ port = pw_registry_bind(self->registry,
+ id, type, PW_VERSION_PORT, sizeof(*pd));
+ if (port == NULL)
+ goto no_mem;
+
+ pd = pw_proxy_get_user_data((struct pw_proxy*)port);
+ pd->node_data = nd;
+ pd->proxy = port;
+ pd->id = id;
+ if (!props || !spa_atou64(spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL), &pd->serial, 0))
+ pd->serial = SPA_ID_INVALID;
+ 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);
+ }
+
+ return;
+
+no_mem:
+ GST_ERROR_OBJECT(self, "failed to create proxy");
+ return;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static GList *
+gst_pipewire_device_provider_probe (GstDeviceProvider * provider)
+{
+ GstPipeWireDeviceProvider *self = GST_PIPEWIRE_DEVICE_PROVIDER (provider);
+
+ GST_DEBUG_OBJECT (self, "starting probe");
+
+ self->core = gst_pipewire_core_get(self->fd);
+ if (self->core == NULL) {
+ GST_ERROR_OBJECT (self, "Failed to connect");
+ goto failed;
+ }
+
+ GST_DEBUG_OBJECT (self, "connected");
+
+ pw_thread_loop_lock (self->core->loop);
+
+ spa_list_init(&self->nodes);
+ spa_list_init(&self->pending);
+ self->end = FALSE;
+ self->error = 0;
+ self->list_only = TRUE;
+ self->devices = NULL;
+ self->registry = pw_core_get_registry(self->core->core, PW_VERSION_REGISTRY, 0);
+
+ pw_core_add_listener(self->core->core, &self->core_listener, &core_events, self);
+ pw_registry_add_listener(self->registry, &self->registry_listener, &registry_events, self);
+
+ resync(self);
+
+ for (;;) {
+ if (self->error < 0)
+ break;
+ if (self->end)
+ break;
+ pw_thread_loop_wait (self->core->loop);
+ }
+
+ GST_DEBUG_OBJECT (self, "disconnect");
+
+ g_clear_pointer ((struct pw_proxy**)&self->registry, pw_proxy_destroy);
+ pw_thread_loop_unlock (self->core->loop);
+ g_clear_pointer (&self->core, gst_pipewire_core_release);
+
+ return self->devices;
+
+failed:
+ return NULL;
+}
+
+static gboolean
+gst_pipewire_device_provider_start (GstDeviceProvider * provider)
+{
+ GstPipeWireDeviceProvider *self = GST_PIPEWIRE_DEVICE_PROVIDER (provider);
+
+ GST_DEBUG_OBJECT (self, "starting provider");
+
+ self->core = gst_pipewire_core_get(self->fd);
+ if (self->core == NULL) {
+ GST_ERROR_OBJECT (self, "Failed to connect");
+ goto failed;
+ }
+
+ GST_DEBUG_OBJECT (self, "connected");
+
+ pw_thread_loop_lock (self->core->loop);
+
+ spa_list_init(&self->nodes);
+ spa_list_init(&self->pending);
+ self->end = FALSE;
+ self->error = 0;
+ self->list_only = FALSE;
+ self->registry = pw_core_get_registry(self->core->core, PW_VERSION_REGISTRY, 0);
+
+ pw_core_add_listener(self->core->core, &self->core_listener, &core_events, self);
+ pw_registry_add_listener(self->registry, &self->registry_listener, &registry_events, self);
+
+ resync(self);
+
+ for (;;) {
+ if (self->error < 0)
+ break;
+ if (self->end)
+ break;
+ pw_thread_loop_wait (self->core->loop);
+ }
+
+ GST_DEBUG_OBJECT (self, "started");
+
+ pw_thread_loop_unlock (self->core->loop);
+
+ return TRUE;
+
+failed:
+ return TRUE;
+}
+
+static void
+gst_pipewire_device_provider_stop (GstDeviceProvider * provider)
+{
+ GstPipeWireDeviceProvider *self = GST_PIPEWIRE_DEVICE_PROVIDER (provider);
+
+ GST_DEBUG_OBJECT (self, "stopping provider");
+
+ g_clear_pointer ((struct pw_proxy**)&self->registry, pw_proxy_destroy);
+ g_clear_pointer (&self->core, gst_pipewire_core_release);
+}
+
+static void
+gst_pipewire_device_provider_set_property (GObject * object,
+ guint prop_id, const GValue * value, GParamSpec * pspec)
+{
+ GstPipeWireDeviceProvider *self = GST_PIPEWIRE_DEVICE_PROVIDER (object);
+
+ switch (prop_id) {
+ case PROP_CLIENT_NAME:
+ g_free (self->client_name);
+ if (!g_value_get_string (value)) {
+ GST_WARNING_OBJECT (self,
+ "Empty PipeWire client name not allowed. "
+ "Resetting to default value");
+ self->client_name = g_strdup(pw_get_client_name ());
+ } else
+ self->client_name = g_value_dup_string (value);
+ break;
+
+ case PROP_FD:
+ self->fd = g_value_get_int (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gst_pipewire_device_provider_get_property (GObject * object,
+ guint prop_id, GValue * value, GParamSpec * pspec)
+{
+ GstPipeWireDeviceProvider *self = GST_PIPEWIRE_DEVICE_PROVIDER (object);
+
+ switch (prop_id) {
+ case PROP_CLIENT_NAME:
+ g_value_set_string (value, self->client_name);
+ break;
+
+ case PROP_FD:
+ g_value_set_int (value, self->fd);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gst_pipewire_device_provider_finalize (GObject * object)
+{
+ GstPipeWireDeviceProvider *self = GST_PIPEWIRE_DEVICE_PROVIDER (object);
+
+ g_free (self->client_name);
+
+ G_OBJECT_CLASS (gst_pipewire_device_provider_parent_class)->finalize (object);
+}
+
+static void
+gst_pipewire_device_provider_class_init (GstPipeWireDeviceProviderClass * klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GstDeviceProviderClass *dm_class = GST_DEVICE_PROVIDER_CLASS (klass);
+
+ gobject_class->set_property = gst_pipewire_device_provider_set_property;
+ gobject_class->get_property = gst_pipewire_device_provider_get_property;
+ gobject_class->finalize = gst_pipewire_device_provider_finalize;
+
+ dm_class->probe = gst_pipewire_device_provider_probe;
+ dm_class->start = gst_pipewire_device_provider_start;
+ dm_class->stop = gst_pipewire_device_provider_stop;
+
+ g_object_class_install_property (gobject_class,
+ PROP_CLIENT_NAME,
+ g_param_spec_string ("client-name", "Client Name",
+ "The PipeWire client_name_to_use", pw_get_client_name (),
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+ GST_PARAM_MUTABLE_READY));
+
+ g_object_class_install_property (gobject_class,
+ PROP_FD,
+ g_param_spec_int ("fd", "Fd", "The fd to connect with", -1, G_MAXINT, -1,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE));
+
+ gst_device_provider_class_set_static_metadata (dm_class,
+ "PipeWire Device Provider", "Sink/Source/Audio/Video",
+ "List and provide PipeWire source and sink devices",
+ "Wim Taymans <wim.taymans@gmail.com>");
+}
+
+static void
+gst_pipewire_device_provider_init (GstPipeWireDeviceProvider * self)
+{
+ self->client_name = g_strdup(pw_get_client_name ());
+ self->fd = -1;
+}
diff --git a/src/gst/gstpipewiredeviceprovider.h b/src/gst/gstpipewiredeviceprovider.h
new file mode 100644
index 0000000..c940ba4
--- /dev/null
+++ b/src/gst/gstpipewiredeviceprovider.h
@@ -0,0 +1,110 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#ifndef __GST_PIPEWIRE_DEVICE_PROVIDER_H__
+#define __GST_PIPEWIRE_DEVICE_PROVIDER_H__
+
+#include "config.h"
+
+#include <gst/gst.h>
+
+#include <pipewire/pipewire.h>
+#include <gst/gstpipewirecore.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GstPipeWireDevice GstPipeWireDevice;
+typedef struct _GstPipeWireDeviceClass GstPipeWireDeviceClass;
+
+#define GST_TYPE_PIPEWIRE_DEVICE (gst_pipewire_device_get_type())
+#define GST_IS_PIPEWIRE_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_PIPEWIRE_DEVICE))
+#define GST_IS_PIPEWIRE_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_PIPEWIRE_DEVICE))
+#define GST_PIPEWIRE_DEVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_PIPEWIRE_DEVICE, GstPipeWireDeviceClass))
+#define GST_PIPEWIRE_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_PIPEWIRE_DEVICE, GstPipeWireDevice))
+#define GST_PIPEWIRE_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_DEVICE, GstPipeWireDeviceClass))
+#define GST_PIPEWIRE_DEVICE_CAST(obj) ((GstPipeWireDevice *)(obj))
+
+typedef enum {
+ GST_PIPEWIRE_DEVICE_TYPE_UNKNOWN,
+ GST_PIPEWIRE_DEVICE_TYPE_SOURCE,
+ GST_PIPEWIRE_DEVICE_TYPE_SINK,
+} GstPipeWireDeviceType;
+
+struct _GstPipeWireDevice {
+ GstDevice parent;
+
+ GstPipeWireDeviceType type;
+ uint32_t id;
+ uint64_t serial;
+ int fd;
+ const gchar *element;
+};
+
+struct _GstPipeWireDeviceClass {
+ GstDeviceClass parent_class;
+};
+
+GType gst_pipewire_device_get_type (void);
+
+typedef struct _GstPipeWireDeviceProvider GstPipeWireDeviceProvider;
+typedef struct _GstPipeWireDeviceProviderClass GstPipeWireDeviceProviderClass;
+
+#define GST_TYPE_PIPEWIRE_DEVICE_PROVIDER (gst_pipewire_device_provider_get_type())
+#define GST_IS_PIPEWIRE_DEVICE_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_PIPEWIRE_DEVICE_PROVIDER))
+#define GST_IS_PIPEWIRE_DEVICE_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_PIPEWIRE_DEVICE_PROVIDER))
+#define GST_PIPEWIRE_DEVICE_PROVIDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_PIPEWIRE_DEVICE_PROVIDER, GstPipeWireDeviceProviderClass))
+#define GST_PIPEWIRE_DEVICE_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_PIPEWIRE_DEVICE_PROVIDER, GstPipeWireDeviceProvider))
+#define GST_PIPEWIRE_DEVICE_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_DEVICE_PROVIDER, GstPipeWireDeviceProviderClass))
+#define GST_PIPEWIRE_DEVICE_PROVIDER_CAST(obj) ((GstPipeWireDeviceProvider *)(obj))
+
+struct _GstPipeWireDeviceProvider {
+ GstDeviceProvider parent;
+
+ gchar *client_name;
+ int fd;
+
+ GstPipeWireCore *core;
+ struct spa_hook core_listener;
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+ struct spa_list nodes;
+ struct spa_list pending;
+ int seq;
+
+ int error;
+ gboolean end;
+ gboolean list_only;
+ GList *devices;
+};
+
+struct _GstPipeWireDeviceProviderClass {
+ GstDeviceProviderClass parent_class;
+};
+
+GType gst_pipewire_device_provider_get_type (void);
+
+G_END_DECLS
+
+#endif /* __GST_PIPEWIRE_DEVICE_PROVIDER_H__ */
diff --git a/src/gst/gstpipewireformat.c b/src/gst/gstpipewireformat.c
new file mode 100644
index 0000000..006c499
--- /dev/null
+++ b/src/gst/gstpipewireformat.c
@@ -0,0 +1,981 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include <gst/gst.h>
+#include <gst/allocators/gstdmabuf.h>
+#include <gst/video/video.h>
+#include <gst/audio/audio.h>
+
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/builder.h>
+
+#include "gstpipewireformat.h"
+
+#ifndef DRM_FORMAT_MOD_INVALID
+#define DRM_FORMAT_MOD_INVALID ((1ULL << 56) - 1)
+#endif
+
+#ifndef DRM_FORMAT_MOD_LINEAR
+#define DRM_FORMAT_MOD_LINEAR 0
+#endif
+
+struct media_type {
+ const char *name;
+ uint32_t media_type;
+ uint32_t media_subtype;
+};
+
+static const struct media_type media_type_map[] = {
+ { "video/x-raw", SPA_MEDIA_TYPE_video, SPA_MEDIA_SUBTYPE_raw },
+ { "audio/x-raw", SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_raw },
+ { "image/jpeg", SPA_MEDIA_TYPE_video, SPA_MEDIA_SUBTYPE_mjpg },
+ { "video/x-jpeg", SPA_MEDIA_TYPE_video, SPA_MEDIA_SUBTYPE_mjpg },
+ { "video/x-h264", SPA_MEDIA_TYPE_video, SPA_MEDIA_SUBTYPE_h264 },
+ { "audio/x-mulaw", SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_raw },
+ { "audio/x-alaw", SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_raw },
+ { "audio/mpeg", SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_mp3 },
+ { "audio/x-flac", SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_flac },
+ { NULL, }
+};
+
+static const uint32_t video_format_map[] = {
+ SPA_VIDEO_FORMAT_UNKNOWN,
+ SPA_VIDEO_FORMAT_ENCODED,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_YV12,
+ SPA_VIDEO_FORMAT_YUY2,
+ SPA_VIDEO_FORMAT_UYVY,
+ SPA_VIDEO_FORMAT_AYUV,
+ SPA_VIDEO_FORMAT_RGBx,
+ SPA_VIDEO_FORMAT_BGRx,
+ SPA_VIDEO_FORMAT_xRGB,
+ SPA_VIDEO_FORMAT_xBGR,
+ SPA_VIDEO_FORMAT_RGBA,
+ SPA_VIDEO_FORMAT_BGRA,
+ SPA_VIDEO_FORMAT_ARGB,
+ SPA_VIDEO_FORMAT_ABGR,
+ SPA_VIDEO_FORMAT_RGB,
+ SPA_VIDEO_FORMAT_BGR,
+ SPA_VIDEO_FORMAT_Y41B,
+ SPA_VIDEO_FORMAT_Y42B,
+ SPA_VIDEO_FORMAT_YVYU,
+ SPA_VIDEO_FORMAT_Y444,
+ SPA_VIDEO_FORMAT_v210,
+ SPA_VIDEO_FORMAT_v216,
+ SPA_VIDEO_FORMAT_NV12,
+ SPA_VIDEO_FORMAT_NV21,
+ SPA_VIDEO_FORMAT_GRAY8,
+ SPA_VIDEO_FORMAT_GRAY16_BE,
+ SPA_VIDEO_FORMAT_GRAY16_LE,
+ SPA_VIDEO_FORMAT_v308,
+ SPA_VIDEO_FORMAT_RGB16,
+ SPA_VIDEO_FORMAT_BGR16,
+ SPA_VIDEO_FORMAT_RGB15,
+ SPA_VIDEO_FORMAT_BGR15,
+ SPA_VIDEO_FORMAT_UYVP,
+ SPA_VIDEO_FORMAT_A420,
+ SPA_VIDEO_FORMAT_RGB8P,
+ SPA_VIDEO_FORMAT_YUV9,
+ SPA_VIDEO_FORMAT_YVU9,
+ SPA_VIDEO_FORMAT_IYU1,
+ SPA_VIDEO_FORMAT_ARGB64,
+ SPA_VIDEO_FORMAT_AYUV64,
+ SPA_VIDEO_FORMAT_r210,
+ SPA_VIDEO_FORMAT_I420_10BE,
+ SPA_VIDEO_FORMAT_I420_10LE,
+ SPA_VIDEO_FORMAT_I422_10BE,
+ SPA_VIDEO_FORMAT_I422_10LE,
+ SPA_VIDEO_FORMAT_Y444_10BE,
+ SPA_VIDEO_FORMAT_Y444_10LE,
+ SPA_VIDEO_FORMAT_GBR,
+ SPA_VIDEO_FORMAT_GBR_10BE,
+ SPA_VIDEO_FORMAT_GBR_10LE,
+ SPA_VIDEO_FORMAT_NV16,
+ SPA_VIDEO_FORMAT_NV24,
+ SPA_VIDEO_FORMAT_NV12_64Z32,
+ SPA_VIDEO_FORMAT_A420_10BE,
+ SPA_VIDEO_FORMAT_A420_10LE,
+ SPA_VIDEO_FORMAT_A422_10BE,
+ SPA_VIDEO_FORMAT_A422_10LE,
+ SPA_VIDEO_FORMAT_A444_10BE,
+ SPA_VIDEO_FORMAT_A444_10LE,
+ SPA_VIDEO_FORMAT_NV61,
+ SPA_VIDEO_FORMAT_P010_10BE,
+ SPA_VIDEO_FORMAT_P010_10LE,
+ SPA_VIDEO_FORMAT_IYU2,
+ SPA_VIDEO_FORMAT_VYUY,
+ SPA_VIDEO_FORMAT_GBRA,
+ SPA_VIDEO_FORMAT_GBRA_10BE,
+ SPA_VIDEO_FORMAT_GBRA_10LE,
+ SPA_VIDEO_FORMAT_GBR_12BE,
+ SPA_VIDEO_FORMAT_GBR_12LE,
+ SPA_VIDEO_FORMAT_GBRA_12BE,
+ SPA_VIDEO_FORMAT_GBRA_12LE,
+ SPA_VIDEO_FORMAT_I420_12BE,
+ SPA_VIDEO_FORMAT_I420_12LE,
+ SPA_VIDEO_FORMAT_I422_12BE,
+ SPA_VIDEO_FORMAT_I422_12LE,
+ SPA_VIDEO_FORMAT_Y444_12BE,
+ SPA_VIDEO_FORMAT_Y444_12LE,
+};
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define _FORMAT_LE(fmt) SPA_AUDIO_FORMAT_ ## fmt ## _OE
+#define _FORMAT_BE(fmt) SPA_AUDIO_FORMAT_ ## fmt
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+#define _FORMAT_LE(fmt) SPA_AUDIO_FORMAT_ ## fmt
+#define _FORMAT_BE(fmt) SPA_AUDIO_FORMAT_ ## fmt ## _OE
+#endif
+
+static const uint32_t audio_format_map[] = {
+ SPA_AUDIO_FORMAT_UNKNOWN,
+ SPA_AUDIO_FORMAT_ENCODED,
+ SPA_AUDIO_FORMAT_S8,
+ SPA_AUDIO_FORMAT_U8,
+ _FORMAT_LE (S16),
+ _FORMAT_BE (S16),
+ _FORMAT_LE (U16),
+ _FORMAT_BE (U16),
+ _FORMAT_LE (S24_32),
+ _FORMAT_BE (S24_32),
+ _FORMAT_LE (U24_32),
+ _FORMAT_BE (U24_32),
+ _FORMAT_LE (S32),
+ _FORMAT_BE (S32),
+ _FORMAT_LE (U32),
+ _FORMAT_BE (U32),
+ _FORMAT_LE (S24),
+ _FORMAT_BE (S24),
+ _FORMAT_LE (U24),
+ _FORMAT_BE (U24),
+ _FORMAT_LE (S20),
+ _FORMAT_BE (S20),
+ _FORMAT_LE (U20),
+ _FORMAT_BE (U20),
+ _FORMAT_LE (S18),
+ _FORMAT_BE (S18),
+ _FORMAT_LE (U18),
+ _FORMAT_BE (U18),
+ _FORMAT_LE (F32),
+ _FORMAT_BE (F32),
+ _FORMAT_LE (F64),
+ _FORMAT_BE (F64),
+};
+
+typedef struct {
+ struct spa_pod_builder b;
+ const struct media_type *type;
+ uint32_t id;
+ const GstCapsFeatures *cf;
+ const GstStructure *cs;
+ GPtrArray *array;
+} ConvertData;
+
+static const struct media_type *
+find_media_types (const char *name)
+{
+ int i;
+ for (i = 0; media_type_map[i].name; i++) {
+ if (spa_streq(media_type_map[i].name, name))
+ return &media_type_map[i];
+ }
+ return NULL;
+}
+
+static int find_index(const uint32_t *items, int n_items, uint32_t id)
+{
+ int i;
+ for (i = 0; i < n_items; i++)
+ if (items[i] == id)
+ return i;
+ return -1;
+}
+
+static const char *
+get_nth_string (const GValue *val, int idx)
+{
+ const GValue *v = NULL;
+ GType type = G_VALUE_TYPE (val);
+
+ if (type == G_TYPE_STRING && idx == 0)
+ v = val;
+ else if (type == GST_TYPE_LIST) {
+ GArray *array = g_value_peek_pointer (val);
+ if (idx < (int)(array->len + 1)) {
+ v = &g_array_index (array, GValue, SPA_MAX (idx - 1, 0));
+ }
+ }
+ if (v)
+ return g_value_get_string (v);
+
+ return NULL;
+}
+
+static bool
+get_nth_int (const GValue *val, int idx, int *res)
+{
+ const GValue *v = NULL;
+ GType type = G_VALUE_TYPE (val);
+
+ if (type == G_TYPE_INT && idx == 0) {
+ v = val;
+ } else if (type == GST_TYPE_INT_RANGE) {
+ if (idx == 0 || idx == 1) {
+ *res = gst_value_get_int_range_min (val);
+ return true;
+ } else if (idx == 2) {
+ *res = gst_value_get_int_range_max (val);
+ return true;
+ }
+ } else if (type == GST_TYPE_LIST) {
+ GArray *array = g_value_peek_pointer (val);
+ if (idx < (int)(array->len + 1)) {
+ v = &g_array_index (array, GValue, SPA_MAX (idx - 1, 0));
+ }
+ }
+ if (v) {
+ *res = g_value_get_int (v);
+ return true;
+ }
+ return false;
+}
+
+static gboolean
+get_nth_fraction (const GValue *val, int idx, struct spa_fraction *f)
+{
+ const GValue *v = NULL;
+ GType type = G_VALUE_TYPE (val);
+
+ if (type == GST_TYPE_FRACTION && idx == 0) {
+ v = val;
+ } else if (type == GST_TYPE_FRACTION_RANGE) {
+ if (idx == 0 || idx == 1) {
+ v = gst_value_get_fraction_range_min (val);
+ } else if (idx == 2) {
+ v = gst_value_get_fraction_range_max (val);
+ }
+ } else if (type == GST_TYPE_LIST) {
+ GArray *array = g_value_peek_pointer (val);
+ if (idx < (int)(array->len + 1)) {
+ v = &g_array_index (array, GValue, SPA_MAX (idx-1, 0));
+ }
+ }
+ if (v) {
+ f->num = gst_value_get_fraction_numerator (v);
+ f->denom = gst_value_get_fraction_denominator (v);
+ return true;
+ }
+ return false;
+}
+
+static gboolean
+get_nth_rectangle (const GValue *width, const GValue *height, int idx, struct spa_rectangle *r)
+{
+ const GValue *w = NULL, *h = NULL;
+ GType wt = G_VALUE_TYPE (width);
+ GType ht = G_VALUE_TYPE (height);
+
+ if (wt == G_TYPE_INT && ht == G_TYPE_INT && idx == 0) {
+ w = width;
+ h = height;
+ } else if (wt == GST_TYPE_INT_RANGE && ht == GST_TYPE_INT_RANGE) {
+ if (idx == 0 || idx == 1) {
+ r->width = gst_value_get_int_range_min (width);
+ r->height = gst_value_get_int_range_min (height);
+ return true;
+ } else if (idx == 2) {
+ r->width = gst_value_get_int_range_max (width);
+ r->height = gst_value_get_int_range_max (height);
+ return true;
+ } else if (idx == 3) {
+ r->width = gst_value_get_int_range_step (width);
+ r->height = gst_value_get_int_range_step (height);
+ if (r->width > 1 || r->height > 1)
+ return true;
+ else
+ return false;
+ }
+ } else if (wt == GST_TYPE_LIST && ht == GST_TYPE_LIST) {
+ GArray *wa = g_value_peek_pointer (width);
+ GArray *ha = g_value_peek_pointer (height);
+ if (idx < (int)(wa->len + 1))
+ w = &g_array_index (wa, GValue, SPA_MAX (idx-1, 0));
+ if (idx < (int)(ha->len + 1))
+ h = &g_array_index (ha, GValue, SPA_MAX (idx-1, 0));
+ }
+ if (w && h) {
+ r->width = g_value_get_int (w);
+ r->height = g_value_get_int (h);
+ return true;
+ }
+ return false;
+}
+
+static uint32_t
+get_range_type (const GValue *val)
+{
+ GType type = G_VALUE_TYPE (val);
+
+ if (type == GST_TYPE_LIST)
+ return SPA_CHOICE_Enum;
+ if (type == GST_TYPE_DOUBLE_RANGE || type == GST_TYPE_FRACTION_RANGE)
+ return SPA_CHOICE_Range;
+ if (type == GST_TYPE_INT_RANGE) {
+ if (gst_value_get_int_range_step (val) == 1)
+ return SPA_CHOICE_Range;
+ else
+ return SPA_CHOICE_Step;
+ }
+ if (type == GST_TYPE_INT64_RANGE) {
+ if (gst_value_get_int64_range_step (val) == 1)
+ return SPA_CHOICE_Range;
+ else
+ return SPA_CHOICE_Step;
+ }
+ return SPA_CHOICE_None;
+}
+
+static uint32_t
+get_range_type2 (const GValue *v1, const GValue *v2)
+{
+ uint32_t r1, r2;
+
+ r1 = get_range_type (v1);
+ r2 = get_range_type (v2);
+
+ if (r1 == r2)
+ return r1;
+ if (r1 == SPA_CHOICE_Step || r2 == SPA_CHOICE_Step)
+ return SPA_CHOICE_Step;
+ if (r1 == SPA_CHOICE_Range || r2 == SPA_CHOICE_Range)
+ return SPA_CHOICE_Range;
+ return SPA_CHOICE_Range;
+}
+
+static gboolean
+handle_video_fields (ConvertData *d)
+{
+ const GValue *value, *value2;
+ int i;
+ struct spa_pod_choice *choice;
+ struct spa_pod_frame f;
+
+ value = gst_structure_get_value (d->cs, "format");
+ if (value) {
+ const char *v;
+ int idx;
+ for (i = 0; (v = get_nth_string (value, i)); i++) {
+ if (i == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_VIDEO_format, 0);
+ spa_pod_builder_push_choice(&d->b, &f, get_range_type (value), 0);
+ }
+
+ idx = gst_video_format_from_string (v);
+ if (idx != GST_VIDEO_FORMAT_UNKNOWN && idx < (int)SPA_N_ELEMENTS (video_format_map))
+ spa_pod_builder_id (&d->b, video_format_map[idx]);
+ }
+ if (i > 0) {
+ choice = spa_pod_builder_pop(&d->b, &f);
+ if (i == 1)
+ choice->body.type = SPA_CHOICE_None;
+ }
+ }
+ value = gst_structure_get_value (d->cs, "width");
+ value2 = gst_structure_get_value (d->cs, "height");
+ if (value && value2) {
+ struct spa_rectangle v;
+ for (i = 0; get_nth_rectangle (value, value2, i, &v); i++) {
+ if (i == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_VIDEO_size, 0);
+ spa_pod_builder_push_choice(&d->b, &f, get_range_type2 (value, value2), 0);
+ }
+
+ spa_pod_builder_rectangle (&d->b, v.width, v.height);
+ }
+ if (i > 0) {
+ choice = spa_pod_builder_pop(&d->b, &f);
+ if (i == 1)
+ choice->body.type = SPA_CHOICE_None;
+ }
+ }
+
+ value = gst_structure_get_value (d->cs, "framerate");
+ if (value) {
+ struct spa_fraction v;
+ for (i = 0; get_nth_fraction (value, i, &v); i++) {
+ if (i == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_VIDEO_framerate, 0);
+ spa_pod_builder_push_choice(&d->b, &f, get_range_type (value), 0);
+ }
+
+ spa_pod_builder_fraction (&d->b, v.num, v.denom);
+ }
+ if (i > 0) {
+ choice = spa_pod_builder_pop(&d->b, &f);
+ if (i == 1)
+ choice->body.type = SPA_CHOICE_None;
+ }
+ }
+
+ value = gst_structure_get_value (d->cs, "max-framerate");
+ if (value) {
+ struct spa_fraction v;
+ for (i = 0; get_nth_fraction (value, i, &v); i++) {
+ if (i == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_VIDEO_maxFramerate, 0);
+ spa_pod_builder_push_choice(&d->b, &f, get_range_type (value), 0);
+ }
+
+ spa_pod_builder_fraction (&d->b, v.num, v.denom);
+ }
+ if (i > 0) {
+ choice = spa_pod_builder_pop(&d->b, &f);
+ if (i == 1)
+ choice->body.type = SPA_CHOICE_None;
+ }
+ }
+ return TRUE;
+}
+
+static void
+set_default_channels (struct spa_pod_builder *b, uint32_t channels)
+{
+ uint32_t position[SPA_AUDIO_MAX_CHANNELS] = {0};
+ gboolean ok = TRUE;
+
+ switch (channels) {
+ case 8:
+ position[6] = SPA_AUDIO_CHANNEL_SL;
+ position[7] = SPA_AUDIO_CHANNEL_SR;
+ SPA_FALLTHROUGH
+ case 6:
+ position[5] = SPA_AUDIO_CHANNEL_LFE;
+ SPA_FALLTHROUGH
+ case 5:
+ position[4] = SPA_AUDIO_CHANNEL_FC;
+ SPA_FALLTHROUGH
+ case 4:
+ position[2] = SPA_AUDIO_CHANNEL_RL;
+ position[3] = SPA_AUDIO_CHANNEL_RR;
+ SPA_FALLTHROUGH
+ case 2:
+ position[0] = SPA_AUDIO_CHANNEL_FL;
+ position[1] = SPA_AUDIO_CHANNEL_FR;
+ break;
+ case 1:
+ position[0] = SPA_AUDIO_CHANNEL_MONO;
+ break;
+ default:
+ ok = FALSE;
+ break;
+ }
+
+ if (ok)
+ spa_pod_builder_add (b, SPA_FORMAT_AUDIO_position,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, channels, position), 0);
+}
+
+static gboolean
+handle_audio_fields (ConvertData *d)
+{
+ const GValue *value;
+ struct spa_pod_choice *choice;
+ struct spa_pod_frame f;
+ int i = 0;
+
+ value = gst_structure_get_value (d->cs, "format");
+ if (value) {
+ const char *v;
+ int idx;
+ for (i = 0; (v = get_nth_string (value, i)); i++) {
+ if (i == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_AUDIO_format, 0);
+ spa_pod_builder_push_choice(&d->b, &f, get_range_type (value), 0);
+ }
+
+ idx = gst_audio_format_from_string (v);
+ if (idx < (int)SPA_N_ELEMENTS (audio_format_map))
+ spa_pod_builder_id (&d->b, audio_format_map[idx]);
+ }
+ if (i > 0) {
+ choice = spa_pod_builder_pop(&d->b, &f);
+ if (i == 1)
+ choice->body.type = SPA_CHOICE_None;
+ }
+ } else if (strcmp(d->type->name, "audio/x-mulaw") == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_AUDIO_format, 0);
+ spa_pod_builder_id (&d->b, SPA_AUDIO_FORMAT_ULAW);
+ } else if (strcmp(d->type->name, "audio/x-alaw") == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_AUDIO_format, 0);
+ spa_pod_builder_id (&d->b, SPA_AUDIO_FORMAT_ALAW);
+ } else if (strcmp(d->type->name, "audio/mpeg") == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_AUDIO_format, 0);
+ spa_pod_builder_id (&d->b, SPA_AUDIO_FORMAT_ENCODED);
+ } else if (strcmp(d->type->name, "audio/x-flac") == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_AUDIO_format, 0);
+ spa_pod_builder_id (&d->b, SPA_AUDIO_FORMAT_ENCODED);
+ }
+
+#if 0
+ value = gst_structure_get_value (d->cs, "layout");
+ if (value) {
+ const char *v;
+ for (i = 0; (v = get_nth_string (value, i)); i++) {
+ enum spa_audio_layout layout;
+
+ if (spa_streq(v, "interleaved"))
+ layout = SPA_AUDIO_LAYOUT_INTERLEAVED;
+ else if (spa_streq(v, "non-interleaved"))
+ layout = SPA_AUDIO_LAYOUT_NON_INTERLEAVED;
+ else
+ break;
+
+ if (i == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_AUDIO_layout, 0);
+ spa_pod_builder_push_choice(&d->b, &f, get_range_type (value), 0);
+ }
+
+ spa_pod_builder_id (&d->b, layout);
+ }
+ if (i > 0) {
+ choice = spa_pod_builder_pop(&d->b, &f);
+ if (i == 1)
+ choice->body.type = SPA_CHOICE_None;
+ }
+ }
+#endif
+ value = gst_structure_get_value (d->cs, "rate");
+ if (value) {
+ int v;
+ for (i = 0; get_nth_int (value, i, &v); i++) {
+ if (i == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_AUDIO_rate, 0);
+ spa_pod_builder_push_choice(&d->b, &f, get_range_type (value), 0);
+ }
+
+ spa_pod_builder_int (&d->b, v);
+ }
+ if (i > 0) {
+ choice = spa_pod_builder_pop(&d->b, &f);
+ if (i == 1)
+ choice->body.type = SPA_CHOICE_None;
+ }
+ }
+ value = gst_structure_get_value (d->cs, "channels");
+ if (value) {
+ int v;
+ for (i = 0; get_nth_int (value, i, &v); i++) {
+ if (i == 0) {
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_AUDIO_channels, 0);
+ spa_pod_builder_push_choice(&d->b, &f, get_range_type (value), 0);
+ }
+
+ spa_pod_builder_int (&d->b, v);
+ }
+ if (i > 0) {
+ choice = spa_pod_builder_pop(&d->b, &f);
+ if (i == 1) {
+ choice->body.type = SPA_CHOICE_None;
+ set_default_channels (&d->b, v);
+ }
+ }
+ }
+ return TRUE;
+}
+
+static int
+builder_overflow (void *event_data, uint32_t size)
+{
+ struct spa_pod_builder *b = event_data;
+ b->size = SPA_ROUND_UP_N (size, 512);
+ b->data = realloc (b->data, b->size);
+ if (b->data == NULL)
+ return -errno;
+ return 0;
+}
+
+static const struct spa_pod_builder_callbacks builder_callbacks = {
+ SPA_VERSION_POD_BUILDER_CALLBACKS,
+ .overflow = builder_overflow
+};
+
+static struct spa_pod *
+convert_1 (ConvertData *d)
+{
+ struct spa_pod_frame f;
+
+ if (!(d->type = find_media_types (gst_structure_get_name (d->cs))))
+ return NULL;
+
+ spa_pod_builder_set_callbacks(&d->b, &builder_callbacks, &d->b);
+
+ spa_pod_builder_push_object (&d->b, &f, SPA_TYPE_OBJECT_Format, d->id);
+
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_mediaType, 0);
+ spa_pod_builder_id(&d->b, d->type->media_type);
+
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_mediaSubtype, 0);
+ spa_pod_builder_id(&d->b, d->type->media_subtype);
+
+ if (d->cf && gst_caps_features_contains (d->cf, GST_CAPS_FEATURE_MEMORY_DMABUF)) {
+ struct spa_pod_frame f2;
+
+ spa_pod_builder_prop (&d->b, SPA_FORMAT_VIDEO_modifier,
+ (SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE));
+ spa_pod_builder_push_choice (&d->b, &f2, SPA_CHOICE_Enum, 0);
+ spa_pod_builder_long (&d->b, DRM_FORMAT_MOD_INVALID);
+ spa_pod_builder_long (&d->b, DRM_FORMAT_MOD_INVALID);
+ spa_pod_builder_long (&d->b, DRM_FORMAT_MOD_LINEAR);
+ spa_pod_builder_pop (&d->b, &f2);
+ }
+
+ if (d->type->media_type == SPA_MEDIA_TYPE_video)
+ handle_video_fields (d);
+ else if (d->type->media_type == SPA_MEDIA_TYPE_audio)
+ handle_audio_fields (d);
+
+ spa_pod_builder_pop (&d->b, &f);
+
+ return SPA_PTROFF (d->b.data, 0, struct spa_pod);
+}
+
+struct spa_pod *
+gst_caps_to_format (GstCaps *caps, guint index, uint32_t id)
+{
+ ConvertData d;
+ struct spa_pod *res;
+
+ g_return_val_if_fail (GST_IS_CAPS (caps), NULL);
+ g_return_val_if_fail (gst_caps_is_fixed (caps), NULL);
+
+ spa_zero (d);
+ d.cf = gst_caps_get_features (caps, index);
+ d.cs = gst_caps_get_structure (caps, index);
+ d.id = id;
+
+ res = convert_1 (&d);
+
+ return res;
+}
+
+static gboolean
+foreach_func (GstCapsFeatures *features,
+ GstStructure *structure,
+ ConvertData *d)
+{
+ struct spa_pod *fmt;
+ int idx;
+
+ spa_zero(d->b);
+ d->cf = features;
+ d->cs = structure;
+
+ if (d->cf && gst_caps_features_contains (d->cf, GST_CAPS_FEATURE_MEMORY_DMABUF))
+ idx = 0;
+ else
+ idx = -1;
+
+ if ((fmt = convert_1 (d)))
+ g_ptr_array_insert (d->array, idx, fmt);
+
+ return TRUE;
+}
+
+
+GPtrArray *
+gst_caps_to_format_all (GstCaps *caps, uint32_t id)
+{
+ ConvertData d;
+
+ spa_zero (d);
+ d.id = id;
+ d.array = g_ptr_array_new_full (gst_caps_get_size (caps), (GDestroyNotify)g_free);
+
+ gst_caps_foreach (caps, (GstCapsForeachFunc) foreach_func, &d);
+
+ return d.array;
+}
+
+typedef const char *(*id_to_string_func)(uint32_t id);
+
+static const char *video_id_to_string(uint32_t id)
+{
+ int idx;
+ if ((idx = find_index(video_format_map, SPA_N_ELEMENTS(video_format_map), id)) == -1)
+ return NULL;
+ return gst_video_format_to_string(idx);
+}
+
+static const char *audio_id_to_string(uint32_t id)
+{
+ int idx;
+ if ((idx = find_index(audio_format_map, SPA_N_ELEMENTS(audio_format_map), id)) == -1)
+ return NULL;
+ return gst_audio_format_to_string(idx);
+}
+
+static void
+handle_id_prop (const struct spa_pod_prop *prop, const char *key, id_to_string_func func, GstCaps *res)
+{
+ const char * str;
+ struct spa_pod *val;
+ uint32_t *id;
+ uint32_t i, n_items, choice;
+
+ val = spa_pod_get_values(&prop->value, &n_items, &choice);
+ if (val->type != SPA_TYPE_Id)
+ return;
+
+ id = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ if (!(str = func(id[0])))
+ return;
+ gst_caps_set_simple (res, key, G_TYPE_STRING, str, NULL);
+ break;
+ case SPA_CHOICE_Enum:
+ {
+ GValue list = { 0 }, v = { 0 };
+
+ g_value_init (&list, GST_TYPE_LIST);
+ for (i = 1; i < n_items; i++) {
+ if (!(str = func(id[i])))
+ continue;
+
+ g_value_init (&v, G_TYPE_STRING);
+ g_value_set_string (&v, str);
+ gst_value_list_append_and_take_value (&list, &v);
+ }
+ gst_caps_set_value (res, key, &list);
+ g_value_unset (&list);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void
+handle_int_prop (const struct spa_pod_prop *prop, const char *key, GstCaps *res)
+{
+ struct spa_pod *val;
+ uint32_t *ints;
+ uint32_t i, n_items, choice;
+
+ val = spa_pod_get_values(&prop->value, &n_items, &choice);
+ if (val->type != SPA_TYPE_Int)
+ return;
+
+ ints = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ gst_caps_set_simple (res, key, G_TYPE_INT, ints[0], NULL);
+ break;
+ case SPA_CHOICE_Range:
+ case SPA_CHOICE_Step:
+ {
+ if (n_items < 3)
+ return;
+ gst_caps_set_simple (res, key, GST_TYPE_INT_RANGE, ints[1], ints[2], NULL);
+ break;
+ }
+ case SPA_CHOICE_Enum:
+ {
+ GValue list = { 0 }, v = { 0 };
+
+ g_value_init (&list, GST_TYPE_LIST);
+ for (i = 1; i < n_items; i++) {
+ g_value_init (&v, G_TYPE_INT);
+ g_value_set_int (&v, ints[i]);
+ gst_value_list_append_and_take_value (&list, &v);
+ }
+ gst_caps_set_value (res, key, &list);
+ g_value_unset (&list);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void
+handle_rect_prop (const struct spa_pod_prop *prop, const char *width, const char *height, GstCaps *res)
+{
+ struct spa_pod *val;
+ struct spa_rectangle *rect;
+ uint32_t i, n_items, choice;
+
+ val = spa_pod_get_values(&prop->value, &n_items, &choice);
+ if (val->type != SPA_TYPE_Rectangle)
+ return;
+
+ rect = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ gst_caps_set_simple (res, width, G_TYPE_INT, rect[0].width,
+ height, G_TYPE_INT, rect[0].height, NULL);
+ break;
+ case SPA_CHOICE_Range:
+ case SPA_CHOICE_Step:
+ {
+ if (n_items < 3)
+ return;
+ gst_caps_set_simple (res, width, GST_TYPE_INT_RANGE, rect[1].width, rect[2].width,
+ height, GST_TYPE_INT_RANGE, rect[1].height, rect[2].height, NULL);
+ break;
+ }
+ case SPA_CHOICE_Enum:
+ {
+ GValue l1 = { 0 }, l2 = { 0 }, v1 = { 0 }, v2 = { 0 };
+
+ g_value_init (&l1, GST_TYPE_LIST);
+ g_value_init (&l2, GST_TYPE_LIST);
+ for (i = 1; i < n_items; i++) {
+ g_value_init (&v1, G_TYPE_INT);
+ g_value_set_int (&v1, rect[i].width);
+ gst_value_list_append_and_take_value (&l1, &v1);
+
+ g_value_init (&v2, G_TYPE_INT);
+ g_value_set_int (&v2, rect[i].height);
+ gst_value_list_append_and_take_value (&l2, &v2);
+ }
+ gst_caps_set_value (res, width, &l1);
+ gst_caps_set_value (res, height, &l2);
+ g_value_unset (&l1);
+ g_value_unset (&l2);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void
+handle_fraction_prop (const struct spa_pod_prop *prop, const char *key, GstCaps *res)
+{
+ struct spa_pod *val;
+ struct spa_fraction *fract;
+ uint32_t i, n_items, choice;
+
+ val = spa_pod_get_values(&prop->value, &n_items, &choice);
+ if (val->type != SPA_TYPE_Fraction)
+ return;
+
+ fract = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ gst_caps_set_simple (res, key, GST_TYPE_FRACTION, fract[0].num, fract[0].denom, NULL);
+ break;
+ case SPA_CHOICE_Range:
+ case SPA_CHOICE_Step:
+ {
+ if (n_items < 3)
+ return;
+ gst_caps_set_simple (res, key, GST_TYPE_FRACTION_RANGE, fract[1].num, fract[1].denom,
+ fract[2].num, fract[2].denom, NULL);
+ break;
+ }
+ case SPA_CHOICE_Enum:
+ {
+ GValue l1 = { 0 }, v1 = { 0 };
+
+ g_value_init (&l1, GST_TYPE_LIST);
+ for (i = 1; i < n_items; i++) {
+ g_value_init (&v1, GST_TYPE_FRACTION);
+ gst_value_set_fraction (&v1, fract[i].num, fract[i].denom);
+ gst_value_list_append_and_take_value (&l1, &v1);
+ }
+ gst_caps_set_value (res, key, &l1);
+ g_value_unset (&l1);
+ break;
+ }
+ default:
+ break;
+ }
+}
+GstCaps *
+gst_caps_from_format (const struct spa_pod *format)
+{
+ GstCaps *res = NULL;
+ uint32_t media_type, media_subtype;
+ const struct spa_pod_prop *prop = NULL;
+ const struct spa_pod_object *obj = (const struct spa_pod_object *) format;
+
+ if (spa_format_parse(format, &media_type, &media_subtype) < 0)
+ return res;
+
+ if (media_type == SPA_MEDIA_TYPE_video) {
+ if (media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+ res = gst_caps_new_empty_simple ("video/x-raw");
+ if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_format))) {
+ handle_id_prop (prop, "format", video_id_to_string, res);
+ }
+ }
+ else if (media_subtype == SPA_MEDIA_SUBTYPE_mjpg) {
+ res = gst_caps_new_empty_simple ("image/jpeg");
+ }
+ else if (media_subtype == SPA_MEDIA_SUBTYPE_h264) {
+ res = gst_caps_new_simple ("video/x-h264",
+ "stream-format", G_TYPE_STRING, "byte-stream",
+ "alignment", G_TYPE_STRING, "au",
+ NULL);
+ } else {
+ return NULL;
+ }
+ if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_size))) {
+ handle_rect_prop (prop, "width", "height", res);
+ }
+ if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_framerate))) {
+ handle_fraction_prop (prop, "framerate", res);
+ }
+ if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_maxFramerate))) {
+ handle_fraction_prop (prop, "max-framerate", res);
+ }
+ } else if (media_type == SPA_MEDIA_TYPE_audio) {
+ if (media_subtype == SPA_MEDIA_SUBTYPE_raw) {
+ res = gst_caps_new_simple ("audio/x-raw",
+ "layout", G_TYPE_STRING, "interleaved",
+ NULL);
+ if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_AUDIO_format))) {
+ handle_id_prop (prop, "format", audio_id_to_string, res);
+ }
+ if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_AUDIO_rate))) {
+ handle_int_prop (prop, "rate", res);
+ }
+ if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_AUDIO_channels))) {
+ handle_int_prop (prop, "channels", res);
+ }
+ }
+ }
+ return res;
+}
diff --git a/src/gst/gstpipewireformat.h b/src/gst/gstpipewireformat.h
new file mode 100644
index 0000000..e077986
--- /dev/null
+++ b/src/gst/gstpipewireformat.h
@@ -0,0 +1,42 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef _GST_PIPEWIRE_FORMAT_H_
+#define _GST_PIPEWIRE_FORMAT_H_
+
+#include <gst/gst.h>
+
+#include <spa/pod/pod.h>
+
+G_BEGIN_DECLS
+
+struct spa_pod * gst_caps_to_format (GstCaps *caps,
+ guint index, uint32_t id);
+GPtrArray * gst_caps_to_format_all (GstCaps *caps, uint32_t id);
+
+GstCaps * gst_caps_from_format (const struct spa_pod *format);
+
+G_END_DECLS
+
+#endif
diff --git a/src/gst/gstpipewirepool.c b/src/gst/gstpipewirepool.c
new file mode 100644
index 0000000..7ecb802
--- /dev/null
+++ b/src/gst/gstpipewirepool.c
@@ -0,0 +1,276 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <unistd.h>
+
+#include <gst/gst.h>
+
+#include <gst/allocators/gstfdmemory.h>
+#include <gst/allocators/gstdmabuf.h>
+
+#include <gst/video/gstvideometa.h>
+
+#include "gstpipewirepool.h"
+
+GST_DEBUG_CATEGORY_STATIC (gst_pipewire_pool_debug_category);
+#define GST_CAT_DEFAULT gst_pipewire_pool_debug_category
+
+G_DEFINE_TYPE (GstPipeWirePool, gst_pipewire_pool, GST_TYPE_BUFFER_POOL);
+
+enum
+{
+ ACTIVATED,
+ /* FILL ME */
+ LAST_SIGNAL
+};
+
+
+static guint pool_signals[LAST_SIGNAL] = { 0 };
+
+static GQuark pool_data_quark;
+
+GstPipeWirePool *
+gst_pipewire_pool_new (void)
+{
+ GstPipeWirePool *pool;
+
+ pool = g_object_new (GST_TYPE_PIPEWIRE_POOL, NULL);
+
+ return pool;
+}
+
+static void
+pool_data_destroy (gpointer user_data)
+{
+ GstPipeWirePoolData *data = user_data;
+
+ gst_object_unref (data->pool);
+ g_slice_free (GstPipeWirePoolData, data);
+}
+
+void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *b)
+{
+ GstBuffer *buf;
+ uint32_t i;
+ GstPipeWirePoolData *data;
+
+ GST_LOG_OBJECT (pool, "wrap buffer");
+
+ data = g_slice_new (GstPipeWirePoolData);
+
+ buf = gst_buffer_new ();
+
+ for (i = 0; i < b->buffer->n_datas; i++) {
+ struct spa_data *d = &b->buffer->datas[i];
+ GstMemory *gmem = NULL;
+
+ GST_LOG_OBJECT (pool, "wrap buffer %d %d", d->mapoffset, d->maxsize);
+ if (d->type == SPA_DATA_MemFd) {
+ GST_LOG_OBJECT (pool, "memory type MemFd");
+ gmem = gst_fd_allocator_alloc (pool->fd_allocator, dup(d->fd),
+ d->mapoffset + d->maxsize, GST_FD_MEMORY_FLAG_NONE);
+ gst_memory_resize (gmem, d->mapoffset, d->maxsize);
+ }
+ else if(d->type == SPA_DATA_DmaBuf) {
+ GST_LOG_OBJECT (pool, "memory type DmaBuf");
+ gmem = gst_fd_allocator_alloc (pool->dmabuf_allocator, dup(d->fd),
+ d->mapoffset + d->maxsize, GST_FD_MEMORY_FLAG_NONE);
+ gst_memory_resize (gmem, d->mapoffset, d->maxsize);
+ }
+ else if (d->type == SPA_DATA_MemPtr) {
+ GST_LOG_OBJECT (pool, "memory type MemPtr");
+ gmem = gst_memory_new_wrapped (0, d->data, d->maxsize, 0,
+ d->maxsize, NULL, NULL);
+ }
+ if (gmem)
+ gst_buffer_insert_memory (buf, i, gmem);
+ }
+
+ data->pool = gst_object_ref (pool);
+ data->owner = NULL;
+ data->header = spa_buffer_find_meta_data (b->buffer, SPA_META_Header, sizeof(*data->header));
+ data->flags = GST_BUFFER_FLAGS (buf);
+ data->b = b;
+ data->buf = buf;
+ data->crop = spa_buffer_find_meta_data (b->buffer, SPA_META_VideoCrop, sizeof(*data->crop));
+ if (data->crop)
+ gst_buffer_add_video_crop_meta(buf);
+ data->videotransform =
+ spa_buffer_find_meta_data (b->buffer, SPA_META_VideoTransform, sizeof(*data->videotransform));
+
+ gst_mini_object_set_qdata (GST_MINI_OBJECT_CAST (buf),
+ pool_data_quark,
+ data,
+ pool_data_destroy);
+ b->user_data = data;
+}
+
+GstPipeWirePoolData *gst_pipewire_pool_get_data (GstBuffer *buffer)
+{
+ return gst_mini_object_get_qdata (GST_MINI_OBJECT_CAST (buffer), pool_data_quark);
+}
+
+#if 0
+gboolean
+gst_pipewire_pool_add_buffer (GstPipeWirePool *pool, GstBuffer *buffer)
+{
+ g_return_val_if_fail (GST_IS_PIPEWIRE_POOL (pool), FALSE);
+ g_return_val_if_fail (GST_IS_BUFFER (buffer), FALSE);
+
+ GST_OBJECT_LOCK (pool);
+ g_queue_push_tail (&pool->available, buffer);
+ g_cond_signal (&pool->cond);
+ GST_OBJECT_UNLOCK (pool);
+
+ return TRUE;
+}
+
+gboolean
+gst_pipewire_pool_remove_buffer (GstPipeWirePool *pool, GstBuffer *buffer)
+{
+ gboolean res;
+
+ g_return_val_if_fail (GST_IS_PIPEWIRE_POOL (pool), FALSE);
+ g_return_val_if_fail (GST_IS_BUFFER (buffer), FALSE);
+
+ GST_OBJECT_LOCK (pool);
+ res = g_queue_remove (&pool->available, buffer);
+ GST_OBJECT_UNLOCK (pool);
+
+ return res;
+}
+#endif
+
+static GstFlowReturn
+acquire_buffer (GstBufferPool * pool, GstBuffer ** buffer,
+ GstBufferPoolAcquireParams * params)
+{
+ GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool);
+ GstPipeWirePoolData *data;
+ struct pw_buffer *b;
+
+ GST_OBJECT_LOCK (pool);
+ while (TRUE) {
+ if (G_UNLIKELY (GST_BUFFER_POOL_IS_FLUSHING (pool)))
+ goto flushing;
+
+ if ((b = pw_stream_dequeue_buffer(p->stream)))
+ break;
+
+ if (params && (params->flags & GST_BUFFER_POOL_ACQUIRE_FLAG_DONTWAIT))
+ goto no_more_buffers;
+
+ GST_WARNING ("queue empty");
+ g_cond_wait (&p->cond, GST_OBJECT_GET_LOCK (pool));
+ }
+
+ data = b->user_data;
+ *buffer = data->buf;
+
+ GST_OBJECT_UNLOCK (pool);
+ GST_DEBUG ("acquire buffer %p", *buffer);
+
+ return GST_FLOW_OK;
+
+flushing:
+ {
+ GST_OBJECT_UNLOCK (pool);
+ return GST_FLOW_FLUSHING;
+ }
+no_more_buffers:
+ {
+ GST_LOG_OBJECT (pool, "no more buffers");
+ GST_OBJECT_UNLOCK (pool);
+ return GST_FLOW_EOS;
+ }
+}
+
+static void
+flush_start (GstBufferPool * pool)
+{
+ GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool);
+
+ GST_DEBUG ("flush start");
+ GST_OBJECT_LOCK (pool);
+ g_cond_signal (&p->cond);
+ GST_OBJECT_UNLOCK (pool);
+}
+
+static void
+release_buffer (GstBufferPool * pool, GstBuffer *buffer)
+{
+ GST_DEBUG ("release buffer %p", buffer);
+}
+
+static gboolean
+do_start (GstBufferPool * pool)
+{
+ g_signal_emit (pool, pool_signals[ACTIVATED], 0, NULL);
+ return TRUE;
+}
+
+static void
+gst_pipewire_pool_finalize (GObject * object)
+{
+ GstPipeWirePool *pool = GST_PIPEWIRE_POOL (object);
+
+ GST_DEBUG_OBJECT (pool, "finalize");
+ g_object_unref (pool->fd_allocator);
+ g_object_unref (pool->dmabuf_allocator);
+
+ G_OBJECT_CLASS (gst_pipewire_pool_parent_class)->finalize (object);
+}
+
+static void
+gst_pipewire_pool_class_init (GstPipeWirePoolClass * klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GstBufferPoolClass *bufferpool_class = GST_BUFFER_POOL_CLASS (klass);
+
+ gobject_class->finalize = gst_pipewire_pool_finalize;
+
+ bufferpool_class->start = do_start;
+ bufferpool_class->flush_start = flush_start;
+ bufferpool_class->acquire_buffer = acquire_buffer;
+ bufferpool_class->release_buffer = release_buffer;
+
+ pool_signals[ACTIVATED] =
+ g_signal_new ("activated", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 0, G_TYPE_NONE);
+
+ GST_DEBUG_CATEGORY_INIT (gst_pipewire_pool_debug_category, "pipewirepool", 0,
+ "debug category for pipewirepool object");
+
+ pool_data_quark = g_quark_from_static_string ("GstPipeWirePoolDataQuark");
+}
+
+static void
+gst_pipewire_pool_init (GstPipeWirePool * pool)
+{
+ pool->fd_allocator = gst_fd_allocator_new ();
+ pool->dmabuf_allocator = gst_dmabuf_allocator_new ();
+ g_cond_init (&pool->cond);
+}
diff --git a/src/gst/gstpipewirepool.h b/src/gst/gstpipewirepool.h
new file mode 100644
index 0000000..acf8106
--- /dev/null
+++ b/src/gst/gstpipewirepool.h
@@ -0,0 +1,92 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __GST_PIPEWIRE_POOL_H__
+#define __GST_PIPEWIRE_POOL_H__
+
+#include <gst/gst.h>
+
+#include <pipewire/pipewire.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_PIPEWIRE_POOL \
+ (gst_pipewire_pool_get_type())
+#define GST_PIPEWIRE_POOL(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_PIPEWIRE_POOL,GstPipeWirePool))
+#define GST_PIPEWIRE_POOL_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_PIPEWIRE_POOL,GstPipeWirePoolClass))
+#define GST_IS_PIPEWIRE_POOL(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_PIPEWIRE_POOL))
+#define GST_IS_PIPEWIRE_POOL_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_PIPEWIRE_POOL))
+#define GST_PIPEWIRE_POOL_GET_CLASS(klass) \
+ (G_TYPE_INSTANCE_GET_CLASS ((klass), GST_TYPE_PIPEWIRE_POOL, GstPipeWirePoolClass))
+
+typedef struct _GstPipeWirePoolData GstPipeWirePoolData;
+typedef struct _GstPipeWirePool GstPipeWirePool;
+typedef struct _GstPipeWirePoolClass GstPipeWirePoolClass;
+
+struct _GstPipeWirePoolData {
+ GstPipeWirePool *pool;
+ void *owner;
+ struct spa_meta_header *header;
+ guint flags;
+ struct pw_buffer *b;
+ GstBuffer *buf;
+ gboolean queued;
+ struct spa_meta_region *crop;
+ struct spa_meta_videotransform *videotransform;
+};
+
+struct _GstPipeWirePool {
+ GstBufferPool parent;
+
+ struct pw_stream *stream;
+ struct pw_type *t;
+
+ GstAllocator *fd_allocator;
+ GstAllocator *dmabuf_allocator;
+
+ GCond cond;
+};
+
+struct _GstPipeWirePoolClass {
+ GstBufferPoolClass parent_class;
+};
+
+GType gst_pipewire_pool_get_type (void);
+
+GstPipeWirePool * gst_pipewire_pool_new (void);
+
+void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *buffer);
+
+GstPipeWirePoolData *gst_pipewire_pool_get_data (GstBuffer *buffer);
+
+//gboolean gst_pipewire_pool_add_buffer (GstPipeWirePool *pool, GstBuffer *buffer);
+//gboolean gst_pipewire_pool_remove_buffer (GstPipeWirePool *pool, GstBuffer *buffer);
+
+G_END_DECLS
+
+#endif /* __GST_PIPEWIRE_POOL_H__ */
diff --git a/src/gst/gstpipewiresink.c b/src/gst/gstpipewiresink.c
new file mode 100644
index 0000000..91bdfcc
--- /dev/null
+++ b/src/gst/gstpipewiresink.c
@@ -0,0 +1,924 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * SECTION:element-pipewiresink
+ *
+ * <refsect2>
+ * <title>Example launch line</title>
+ * |[
+ * gst-launch -v videotestsrc ! pipewiresink
+ * ]| Sends a test video source to PipeWire
+ * </refsect2>
+ */
+
+#define PW_ENABLE_DEPRECATED
+
+#include "config.h"
+#include "gstpipewiresink.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/socket.h>
+
+#include <spa/pod/builder.h>
+#include <spa/utils/result.h>
+
+#include <gst/video/video.h>
+
+#include "gstpipewireformat.h"
+
+GST_DEBUG_CATEGORY_STATIC (pipewire_sink_debug);
+#define GST_CAT_DEFAULT pipewire_sink_debug
+
+#define DEFAULT_PROP_MODE GST_PIPEWIRE_SINK_MODE_DEFAULT
+
+#define MIN_BUFFERS 8u
+
+enum
+{
+ PROP_0,
+ PROP_PATH,
+ PROP_TARGET_OBJECT,
+ PROP_CLIENT_NAME,
+ PROP_CLIENT_PROPERTIES,
+ PROP_STREAM_PROPERTIES,
+ PROP_MODE,
+ PROP_FD
+};
+
+GType
+gst_pipewire_sink_mode_get_type (void)
+{
+ static gsize mode_type = 0;
+ static const GEnumValue mode[] = {
+ {GST_PIPEWIRE_SINK_MODE_DEFAULT, "GST_PIPEWIRE_SINK_MODE_DEFAULT", "default"},
+ {GST_PIPEWIRE_SINK_MODE_RENDER, "GST_PIPEWIRE_SINK_MODE_RENDER", "render"},
+ {GST_PIPEWIRE_SINK_MODE_PROVIDE, "GST_PIPEWIRE_SINK_MODE_PROVIDE", "provide"},
+ {0, NULL, NULL},
+ };
+
+ if (g_once_init_enter (&mode_type)) {
+ GType tmp =
+ g_enum_register_static ("GstPipeWireSinkMode", mode);
+ g_once_init_leave (&mode_type, tmp);
+ }
+
+ return (GType) mode_type;
+}
+
+
+static GstStaticPadTemplate gst_pipewire_sink_template =
+GST_STATIC_PAD_TEMPLATE ("sink",
+ GST_PAD_SINK,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS_ANY
+ );
+
+#define gst_pipewire_sink_parent_class parent_class
+G_DEFINE_TYPE (GstPipeWireSink, gst_pipewire_sink, GST_TYPE_BASE_SINK);
+
+static void gst_pipewire_sink_set_property (GObject * object, guint prop_id,
+ const GValue * value, GParamSpec * pspec);
+static void gst_pipewire_sink_get_property (GObject * object, guint prop_id,
+ GValue * value, GParamSpec * pspec);
+
+static GstStateChangeReturn
+gst_pipewire_sink_change_state (GstElement * element, GstStateChange transition);
+
+static gboolean gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps);
+static GstCaps *gst_pipewire_sink_sink_fixate (GstBaseSink * bsink,
+ GstCaps * caps);
+
+static GstFlowReturn gst_pipewire_sink_render (GstBaseSink * psink,
+ GstBuffer * buffer);
+static gboolean gst_pipewire_sink_start (GstBaseSink * basesink);
+static gboolean gst_pipewire_sink_stop (GstBaseSink * basesink);
+
+static void
+gst_pipewire_sink_finalize (GObject * object)
+{
+ GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (object);
+
+ g_object_unref (pwsink->pool);
+
+ if (pwsink->stream_properties)
+ gst_structure_free (pwsink->stream_properties);
+ if (pwsink->client_properties)
+ gst_structure_free (pwsink->client_properties);
+ g_free (pwsink->path);
+ g_free (pwsink->target_object);
+ g_free (pwsink->client_name);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static gboolean
+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->pool), 0, 0, 0);
+ return TRUE;
+}
+
+static void
+gst_pipewire_sink_class_init (GstPipeWireSinkClass * klass)
+{
+ GObjectClass *gobject_class;
+ GstElementClass *gstelement_class;
+ GstBaseSinkClass *gstbasesink_class;
+
+ gobject_class = (GObjectClass *) klass;
+ gstelement_class = (GstElementClass *) klass;
+ gstbasesink_class = (GstBaseSinkClass *) klass;
+
+ gobject_class->finalize = gst_pipewire_sink_finalize;
+ gobject_class->set_property = gst_pipewire_sink_set_property;
+ gobject_class->get_property = gst_pipewire_sink_get_property;
+
+ g_object_class_install_property (gobject_class,
+ PROP_PATH,
+ g_param_spec_string ("path",
+ "Path",
+ "The sink path to connect to (NULL = default)",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_DEPRECATED));
+
+ g_object_class_install_property (gobject_class,
+ PROP_TARGET_OBJECT,
+ g_param_spec_string ("target-object",
+ "Target object",
+ "The sink name/serial to connect to (NULL = default)",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_CLIENT_NAME,
+ g_param_spec_string ("client-name",
+ "Client Name",
+ "The client name to use (NULL = default)",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_CLIENT_PROPERTIES,
+ g_param_spec_boxed ("client-properties",
+ "Client properties",
+ "List of PipeWire client properties",
+ GST_TYPE_STRUCTURE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_STREAM_PROPERTIES,
+ g_param_spec_boxed ("stream-properties",
+ "Stream properties",
+ "List of PipeWire stream properties",
+ GST_TYPE_STRUCTURE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_MODE,
+ g_param_spec_enum ("mode",
+ "Mode",
+ "The mode to operate in",
+ GST_TYPE_PIPEWIRE_SINK_MODE,
+ DEFAULT_PROP_MODE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_FD,
+ g_param_spec_int ("fd",
+ "Fd",
+ "The fd to connect with",
+ -1, G_MAXINT, -1,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ gstelement_class->change_state = gst_pipewire_sink_change_state;
+
+ gst_element_class_set_static_metadata (gstelement_class,
+ "PipeWire sink", "Sink/Video",
+ "Send video to PipeWire", "Wim Taymans <wim.taymans@gmail.com>");
+
+ gst_element_class_add_pad_template (gstelement_class,
+ gst_static_pad_template_get (&gst_pipewire_sink_template));
+
+ gstbasesink_class->set_caps = gst_pipewire_sink_setcaps;
+ gstbasesink_class->fixate = gst_pipewire_sink_sink_fixate;
+ gstbasesink_class->propose_allocation = gst_pipewire_sink_propose_allocation;
+ gstbasesink_class->start = gst_pipewire_sink_start;
+ gstbasesink_class->stop = gst_pipewire_sink_stop;
+ gstbasesink_class->render = gst_pipewire_sink_render;
+
+ GST_DEBUG_CATEGORY_INIT (pipewire_sink_debug, "pipewiresink", 0,
+ "PipeWire Sink");
+}
+
+static void
+pool_activated (GstPipeWirePool *pool, GstPipeWireSink *sink)
+{
+ GstStructure *config;
+ GstCaps *caps;
+ guint size;
+ guint min_buffers;
+ guint max_buffers;
+ const struct spa_pod *port_params[3];
+ struct spa_pod_builder b = { NULL };
+ uint8_t buffer[1024];
+ struct spa_pod_frame f;
+
+ config = gst_buffer_pool_get_config (GST_BUFFER_POOL (pool));
+ gst_buffer_pool_config_get_params (config, &caps, &size, &min_buffers, &max_buffers);
+
+ spa_pod_builder_init (&b, buffer, sizeof (buffer));
+ spa_pod_builder_push_object (&b, &f, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers);
+ if (size == 0)
+ spa_pod_builder_add (&b,
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX),
+ 0);
+ else
+ spa_pod_builder_add (&b,
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(size, size, INT32_MAX),
+ 0);
+
+ spa_pod_builder_add (&b,
+ SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX),
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(
+ SPA_MAX(MIN_BUFFERS, min_buffers),
+ SPA_MAX(MIN_BUFFERS, min_buffers),
+ max_buffers ? max_buffers : INT32_MAX),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(
+ (1<<SPA_DATA_MemFd) |
+ (1<<SPA_DATA_MemPtr)),
+ 0);
+ port_params[0] = spa_pod_builder_pop (&b, &f);
+
+ port_params[1] = spa_pod_builder_add_object (&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Int(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_header)));
+
+ port_params[2] = spa_pod_builder_add_object (&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Int(SPA_META_VideoCrop),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_region)));
+
+ pw_thread_loop_lock (sink->core->loop);
+ pw_stream_update_params (sink->stream, port_params, 3);
+ pw_thread_loop_unlock (sink->core->loop);
+}
+
+static void
+gst_pipewire_sink_init (GstPipeWireSink * sink)
+{
+ sink->pool = gst_pipewire_pool_new ();
+ sink->client_name = g_strdup(pw_get_client_name());
+ sink->mode = DEFAULT_PROP_MODE;
+ sink->fd = -1;
+
+ g_signal_connect (sink->pool, "activated", G_CALLBACK (pool_activated), sink);
+}
+
+static GstCaps *
+gst_pipewire_sink_sink_fixate (GstBaseSink * bsink, GstCaps * caps)
+{
+ GstStructure *structure;
+
+ caps = gst_caps_make_writable (caps);
+
+ structure = gst_caps_get_structure (caps, 0);
+
+ if (gst_structure_has_name (structure, "video/x-raw")) {
+ 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);
+
+ if (gst_structure_has_field (structure, "pixel-aspect-ratio"))
+ gst_structure_fixate_field_nearest_fraction (structure,
+ "pixel-aspect-ratio", 1, 1);
+ else
+ gst_structure_set (structure, "pixel-aspect-ratio", GST_TYPE_FRACTION, 1, 1,
+ NULL);
+
+ if (gst_structure_has_field (structure, "colorimetry"))
+ gst_structure_fixate_field_string (structure, "colorimetry", "bt601");
+ if (gst_structure_has_field (structure, "chroma-site"))
+ gst_structure_fixate_field_string (structure, "chroma-site", "mpeg2");
+
+ if (gst_structure_has_field (structure, "interlace-mode"))
+ gst_structure_fixate_field_string (structure, "interlace-mode",
+ "progressive");
+ else
+ gst_structure_set (structure, "interlace-mode", G_TYPE_STRING,
+ "progressive", NULL);
+ } else if (gst_structure_has_name (structure, "audio/x-raw")) {
+ gst_structure_fixate_field_string (structure, "format", "S16LE");
+ gst_structure_fixate_field_nearest_int (structure, "channels", 2);
+ gst_structure_fixate_field_nearest_int (structure, "rate", 44100);
+ } else if (gst_structure_has_name (structure, "audio/mpeg")) {
+ gst_structure_fixate_field_string (structure, "format", "Encoded");
+ gst_structure_fixate_field_nearest_int (structure, "channels", 2);
+ gst_structure_fixate_field_nearest_int (structure, "rate", 44100);
+ } else if (gst_structure_has_name (structure, "audio/x-flac")) {
+ gst_structure_fixate_field_string (structure, "format", "Encoded");
+ gst_structure_fixate_field_nearest_int (structure, "channels", 2);
+ gst_structure_fixate_field_nearest_int (structure, "rate", 44100);
+ }
+
+ caps = GST_BASE_SINK_CLASS (parent_class)->fixate (bsink, caps);
+
+ return caps;
+}
+
+static void
+gst_pipewire_sink_set_property (GObject * object, guint prop_id,
+ const GValue * value, GParamSpec * pspec)
+{
+ GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (object);
+
+ switch (prop_id) {
+ case PROP_PATH:
+ g_free (pwsink->path);
+ pwsink->path = g_value_dup_string (value);
+ break;
+
+ case PROP_TARGET_OBJECT:
+ g_free (pwsink->target_object);
+ pwsink->target_object = g_value_dup_string (value);
+ break;
+
+ case PROP_CLIENT_NAME:
+ g_free (pwsink->client_name);
+ pwsink->client_name = g_value_dup_string (value);
+ break;
+
+ case PROP_CLIENT_PROPERTIES:
+ if (pwsink->client_properties)
+ gst_structure_free (pwsink->client_properties);
+ pwsink->client_properties =
+ gst_structure_copy (gst_value_get_structure (value));
+ break;
+
+ case PROP_STREAM_PROPERTIES:
+ if (pwsink->stream_properties)
+ gst_structure_free (pwsink->stream_properties);
+ pwsink->stream_properties =
+ gst_structure_copy (gst_value_get_structure (value));
+ break;
+
+ case PROP_MODE:
+ pwsink->mode = g_value_get_enum (value);
+ break;
+
+ case PROP_FD:
+ pwsink->fd = g_value_get_int (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gst_pipewire_sink_get_property (GObject * object, guint prop_id,
+ GValue * value, GParamSpec * pspec)
+{
+ GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (object);
+
+ switch (prop_id) {
+ case PROP_PATH:
+ g_value_set_string (value, pwsink->path);
+ break;
+
+ case PROP_TARGET_OBJECT:
+ g_value_set_string (value, pwsink->target_object);
+ break;
+
+ case PROP_CLIENT_NAME:
+ g_value_set_string (value, pwsink->client_name);
+ break;
+
+ case PROP_CLIENT_PROPERTIES:
+ gst_value_set_structure (value, pwsink->client_properties);
+ break;
+
+ case PROP_STREAM_PROPERTIES:
+ gst_value_set_structure (value, pwsink->stream_properties);
+ break;
+
+ case PROP_MODE:
+ g_value_set_enum (value, pwsink->mode);
+ break;
+
+ case PROP_FD:
+ g_value_set_int (value, pwsink->fd);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+on_add_buffer (void *_data, struct pw_buffer *b)
+{
+ GstPipeWireSink *pwsink = _data;
+ gst_pipewire_pool_wrap_buffer (pwsink->pool, b);
+}
+
+static void
+on_remove_buffer (void *_data, struct pw_buffer *b)
+{
+ GstPipeWireSink *pwsink = _data;
+ GstPipeWirePoolData *data = b->user_data;
+
+ GST_LOG_OBJECT (pwsink, "remove buffer");
+
+ gst_buffer_unref (data->buf);
+}
+
+static void
+do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer)
+{
+ GstPipeWirePoolData *data;
+ gboolean res;
+ guint i;
+ struct spa_buffer *b;
+
+ data = gst_pipewire_pool_get_data(buffer);
+
+ b = data->b->buffer;
+
+ if (data->header) {
+ data->header->seq = GST_BUFFER_OFFSET (buffer);
+ data->header->pts = GST_BUFFER_PTS (buffer);
+ data->header->dts_offset = GST_BUFFER_DTS (buffer);
+ }
+ if (data->crop) {
+ GstVideoCropMeta *meta = gst_buffer_get_video_crop_meta (buffer);
+ if (meta) {
+ data->crop->region.position.x = meta->x;
+ data->crop->region.position.y = meta->y;
+ data->crop->region.size.width = meta->width;
+ data->crop->region.size.height = meta->width;
+ }
+ }
+ 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 = 0;
+ }
+
+ if ((res = pw_stream_queue_buffer (pwsink->stream, data->b)) < 0) {
+ g_warning ("can't send buffer %s", spa_strerror(res));
+ }
+}
+
+
+static void
+on_process (void *data)
+{
+ GstPipeWireSink *pwsink = data;
+ GST_DEBUG ("signal");
+ g_cond_signal (&pwsink->pool->cond);
+}
+
+static void
+on_state_changed (void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error)
+{
+ GstPipeWireSink *pwsink = data;
+
+ GST_DEBUG ("got stream state %d", state);
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ case PW_STREAM_STATE_CONNECTING:
+ case PW_STREAM_STATE_PAUSED:
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ if (pw_stream_is_driving (pwsink->stream))
+ pw_stream_trigger_process (pwsink->stream);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ GST_ELEMENT_ERROR (pwsink, RESOURCE, FAILED,
+ ("stream error: %s", error), (NULL));
+ break;
+ }
+ pw_thread_loop_signal (pwsink->core->loop, FALSE);
+}
+
+static void
+on_param_changed (void *data, uint32_t id, const struct spa_pod *param)
+{
+ GstPipeWireSink *pwsink = data;
+
+ if (param == NULL || id != SPA_PARAM_Format)
+ return;
+
+ if (gst_buffer_pool_is_active (GST_BUFFER_POOL_CAST (pwsink->pool)))
+ pool_activated (pwsink->pool, pwsink);
+}
+
+static gboolean
+gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps)
+{
+ GstPipeWireSink *pwsink;
+ GPtrArray *possible;
+ enum pw_stream_state state;
+ const char *error = NULL;
+ gboolean res = FALSE;
+ GstStructure *config;
+ guint size;
+ guint min_buffers;
+ guint max_buffers;
+ struct timespec abstime;
+
+ pwsink = GST_PIPEWIRE_SINK (bsink);
+
+ possible = gst_caps_to_format_all (caps, SPA_PARAM_EnumFormat);
+
+ pw_thread_loop_lock (pwsink->core->loop);
+ state = pw_stream_get_state (pwsink->stream, &error);
+
+ if (state == PW_STREAM_STATE_ERROR)
+ goto start_error;
+
+ if (state == PW_STREAM_STATE_UNCONNECTED) {
+ enum pw_stream_flags flags = 0;
+ uint32_t target_id;
+
+ if (pwsink->mode != GST_PIPEWIRE_SINK_MODE_PROVIDE)
+ flags |= PW_STREAM_FLAG_AUTOCONNECT;
+ else
+ flags |= PW_STREAM_FLAG_DRIVER;
+
+ target_id = pwsink->path ? (uint32_t)atoi(pwsink->path) : PW_ID_ANY;
+
+ if (pwsink->target_object) {
+ struct spa_dict_item items[2] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_TARGET_OBJECT, pwsink->target_object),
+ /* XXX deprecated but the portal and some example apps only
+ * provide the object id */
+ SPA_DICT_ITEM_INIT(PW_KEY_NODE_TARGET, NULL),
+ };
+ struct spa_dict dict = SPA_DICT_INIT_ARRAY(items);
+ uint64_t serial;
+
+ /* If target.object is a name, set it also to node.target */
+ if (spa_atou64(pwsink->target_object, &serial, 0)) {
+ dict.n_items = 1;
+ } else {
+ target_id = PW_ID_ANY;
+ items[1].value = pwsink->target_object;
+ }
+
+ pw_stream_update_properties (pwsink->stream, &dict);
+ }
+
+ pw_stream_connect (pwsink->stream,
+ PW_DIRECTION_OUTPUT,
+ target_id,
+ flags,
+ (const struct spa_pod **) possible->pdata,
+ possible->len);
+
+ pw_thread_loop_get_time (pwsink->core->loop, &abstime,
+ GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC);
+
+ while (TRUE) {
+ state = pw_stream_get_state (pwsink->stream, &error);
+
+ if (state >= PW_STREAM_STATE_PAUSED)
+ break;
+
+ if (state == PW_STREAM_STATE_ERROR)
+ goto start_error;
+
+ if (pw_thread_loop_timed_wait_full (pwsink->core->loop, &abstime) < 0) {
+ error = "timeout";
+ goto start_error;
+ }
+ }
+ }
+ res = TRUE;
+
+ config = gst_buffer_pool_get_config (GST_BUFFER_POOL_CAST (pwsink->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);
+ gst_buffer_pool_set_config (GST_BUFFER_POOL_CAST (pwsink->pool), config);
+
+ pw_thread_loop_unlock (pwsink->core->loop);
+
+ pwsink->negotiated = res;
+
+ return res;
+
+start_error:
+ {
+ GST_ERROR ("could not start stream: %s", error);
+ pw_thread_loop_unlock (pwsink->core->loop);
+ g_ptr_array_unref (possible);
+ return FALSE;
+ }
+}
+
+static GstFlowReturn
+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);
+
+ if (!pwsink->negotiated)
+ goto not_negotiated;
+
+ if (buffer->pool != GST_BUFFER_POOL_CAST (pwsink->pool) &&
+ !gst_buffer_pool_is_active (GST_BUFFER_POOL_CAST (pwsink->pool))) {
+ GstStructure *config;
+ GstCaps *caps;
+ guint size, min_buffers, max_buffers;
+
+ config = gst_buffer_pool_get_config (GST_BUFFER_POOL_CAST (pwsink->pool));
+ gst_buffer_pool_config_get_params (config, &caps, &size, &min_buffers, &max_buffers);
+
+ size = (size == 0) ? gst_buffer_get_size (buffer) : size;
+
+ gst_buffer_pool_config_set_params (config, caps, size, min_buffers, max_buffers);
+ gst_buffer_pool_set_config (GST_BUFFER_POOL_CAST (pwsink->pool), config);
+
+ gst_buffer_pool_set_active (GST_BUFFER_POOL_CAST (pwsink->pool), TRUE);
+ }
+
+ pw_thread_loop_lock (pwsink->core->loop);
+ if (pw_stream_get_state (pwsink->stream, &error) != PW_STREAM_STATE_STREAMING)
+ goto done_unlock;
+
+ if (buffer->pool != GST_BUFFER_POOL_CAST (pwsink->pool)) {
+ GstBuffer *b = NULL;
+ GstMapInfo info = { 0, };
+ GstBufferPoolAcquireParams params = { 0, };
+
+ pw_thread_loop_unlock (pwsink->core->loop);
+
+ if ((res = gst_buffer_pool_acquire_buffer (GST_BUFFER_POOL_CAST (pwsink->pool), &b, &params)) != GST_FLOW_OK)
+ goto done;
+
+ 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));
+ buffer = b;
+ unref_buffer = TRUE;
+
+ pw_thread_loop_lock (pwsink->core->loop);
+ if (pw_stream_get_state (pwsink->stream, &error) != PW_STREAM_STATE_STREAMING)
+ goto done_unlock;
+ }
+
+ GST_DEBUG ("push buffer");
+ do_send_buffer (pwsink, buffer);
+ if (unref_buffer)
+ gst_buffer_unref (buffer);
+
+ if (pw_stream_is_driving (pwsink->stream))
+ pw_stream_trigger_process (pwsink->stream);
+
+done_unlock:
+ pw_thread_loop_unlock (pwsink->core->loop);
+done:
+ return res;
+
+not_negotiated:
+ {
+ return GST_FLOW_NOT_NEGOTIATED;
+ }
+}
+
+static gboolean
+copy_properties (GQuark field_id,
+ const GValue *value,
+ gpointer user_data)
+{
+ struct pw_properties *properties = user_data;
+ GValue dst = { 0 };
+
+ if (g_value_type_transformable (G_VALUE_TYPE(value), G_TYPE_STRING)) {
+ g_value_init(&dst, G_TYPE_STRING);
+ if (g_value_transform(value, &dst)) {
+ pw_properties_set (properties,
+ g_quark_to_string (field_id),
+ g_value_get_string (&dst));
+ }
+ g_value_unset(&dst);
+ }
+ return TRUE;
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = on_state_changed,
+ .param_changed = on_param_changed,
+ .add_buffer = on_add_buffer,
+ .remove_buffer = on_remove_buffer,
+ .process = on_process,
+};
+
+static gboolean
+gst_pipewire_sink_start (GstBaseSink * basesink)
+{
+ GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (basesink);
+ struct pw_properties *props;
+
+ pwsink->negotiated = FALSE;
+
+ pw_thread_loop_lock (pwsink->core->loop);
+
+ props = pw_properties_new (NULL, NULL);
+ if (pwsink->client_name) {
+ pw_properties_set (props, PW_KEY_NODE_NAME, pwsink->client_name);
+ pw_properties_set (props, PW_KEY_NODE_DESCRIPTION, pwsink->client_name);
+ }
+ if (pwsink->stream_properties) {
+ gst_structure_foreach (pwsink->stream_properties, copy_properties, props);
+ }
+
+ if ((pwsink->stream = pw_stream_new (pwsink->core->core, pwsink->client_name, props)) == NULL)
+ goto no_stream;
+
+ pwsink->pool->stream = pwsink->stream;
+
+ pw_stream_add_listener(pwsink->stream,
+ &pwsink->stream_listener,
+ &stream_events,
+ pwsink);
+
+ pw_thread_loop_unlock (pwsink->core->loop);
+
+ return TRUE;
+
+no_stream:
+ {
+ GST_ELEMENT_ERROR (pwsink, RESOURCE, FAILED, ("can't create stream"), (NULL));
+ pw_thread_loop_unlock (pwsink->core->loop);
+ return FALSE;
+ }
+}
+
+static gboolean
+gst_pipewire_sink_stop (GstBaseSink * basesink)
+{
+ GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (basesink);
+
+ pw_thread_loop_lock (pwsink->core->loop);
+ if (pwsink->stream) {
+ pw_stream_destroy (pwsink->stream);
+ pwsink->stream = NULL;
+ pwsink->pool->stream = NULL;
+ }
+ pw_thread_loop_unlock (pwsink->core->loop);
+
+ pwsink->negotiated = FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+gst_pipewire_sink_open (GstPipeWireSink * pwsink)
+{
+ struct pw_properties *props;
+
+ GST_DEBUG_OBJECT (pwsink, "open");
+
+ pwsink->core = gst_pipewire_core_get(pwsink->fd);
+ if (pwsink->core == NULL)
+ goto connect_error;
+
+ pw_thread_loop_lock (pwsink->core->loop);
+
+ props = pw_properties_new (NULL, NULL);
+ if (pwsink->client_properties) {
+ gst_structure_foreach (pwsink->client_properties, copy_properties, props);
+ pw_core_update_properties (pwsink->core->core, &props->dict);
+ }
+ pw_properties_free(props);
+ pw_thread_loop_unlock (pwsink->core->loop);
+
+ return TRUE;
+
+ /* ERRORS */
+connect_error:
+ {
+ GST_ELEMENT_ERROR (pwsink, RESOURCE, FAILED,
+ ("Failed to connect"), (NULL));
+ return FALSE;
+ }
+}
+
+static gboolean
+gst_pipewire_sink_close (GstPipeWireSink * pwsink)
+{
+ pw_thread_loop_lock (pwsink->core->loop);
+ if (pwsink->stream) {
+ pw_stream_destroy (pwsink->stream);
+ pwsink->stream = NULL;
+ }
+ pw_thread_loop_unlock (pwsink->core->loop);
+
+ if (pwsink->core) {
+ gst_pipewire_core_release (pwsink->core);
+ pwsink->core = NULL;
+ }
+ return TRUE;
+}
+
+static GstStateChangeReturn
+gst_pipewire_sink_change_state (GstElement * element, GstStateChange transition)
+{
+ GstStateChangeReturn ret;
+ GstPipeWireSink *this = GST_PIPEWIRE_SINK_CAST (element);
+
+ switch (transition) {
+ case GST_STATE_CHANGE_NULL_TO_READY:
+ if (!gst_pipewire_sink_open (this))
+ goto open_failed;
+ break;
+ case GST_STATE_CHANGE_READY_TO_PAUSED:
+ break;
+ case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
+ /* uncork and start play */
+ pw_thread_loop_lock (this->core->loop);
+ pw_stream_set_active(this->stream, true);
+ pw_thread_loop_unlock (this->core->loop);
+ gst_buffer_pool_set_flushing(GST_BUFFER_POOL_CAST(this->pool), FALSE);
+ break;
+ case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
+ /* stop play ASAP by corking */
+ pw_thread_loop_lock (this->core->loop);
+ pw_stream_set_active(this->stream, false);
+ pw_thread_loop_unlock (this->core->loop);
+ gst_buffer_pool_set_flushing(GST_BUFFER_POOL_CAST(this->pool), TRUE);
+ break;
+ default:
+ break;
+ }
+
+ ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
+
+ switch (transition) {
+ case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
+ break;
+ case GST_STATE_CHANGE_PAUSED_TO_READY:
+ gst_buffer_pool_set_active(GST_BUFFER_POOL_CAST(this->pool), FALSE);
+ break;
+ case GST_STATE_CHANGE_READY_TO_NULL:
+ gst_pipewire_sink_close (this);
+ break;
+ default:
+ break;
+ }
+ return ret;
+
+ /* ERRORS */
+open_failed:
+ {
+ return GST_STATE_CHANGE_FAILURE;
+ }
+}
diff --git a/src/gst/gstpipewiresink.h b/src/gst/gstpipewiresink.h
new file mode 100644
index 0000000..703cd0f
--- /dev/null
+++ b/src/gst/gstpipewiresink.h
@@ -0,0 +1,110 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __GST_PIPEWIRE_SINK_H__
+#define __GST_PIPEWIRE_SINK_H__
+
+#include <gst/gst.h>
+#include <gst/base/gstbasesink.h>
+
+#include <pipewire/pipewire.h>
+#include <gst/gstpipewirepool.h>
+#include <gst/gstpipewirecore.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_PIPEWIRE_SINK \
+ (gst_pipewire_sink_get_type())
+#define GST_PIPEWIRE_SINK(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_PIPEWIRE_SINK,GstPipeWireSink))
+#define GST_PIPEWIRE_SINK_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_PIPEWIRE_SINK,GstPipeWireSinkClass))
+#define GST_IS_PIPEWIRE_SINK(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_PIPEWIRE_SINK))
+#define GST_IS_PIPEWIRE_SINK_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_PIPEWIRE_SINK))
+#define GST_PIPEWIRE_SINK_CAST(obj) \
+ ((GstPipeWireSink *) (obj))
+
+typedef struct _GstPipeWireSink GstPipeWireSink;
+typedef struct _GstPipeWireSinkClass GstPipeWireSinkClass;
+
+
+/**
+ * GstPipeWireSinkMode:
+ * @GST_PIPEWIRE_SINK_MODE_DEFAULT: the default mode as configured in the server
+ * @GST_PIPEWIRE_SINK_MODE_RENDER: try to render the media
+ * @GST_PIPEWIRE_SINK_MODE_PROVIDE: provide the media
+ *
+ * Different modes of operation.
+ */
+typedef enum
+{
+ GST_PIPEWIRE_SINK_MODE_DEFAULT,
+ GST_PIPEWIRE_SINK_MODE_RENDER,
+ GST_PIPEWIRE_SINK_MODE_PROVIDE,
+} GstPipeWireSinkMode;
+
+#define GST_TYPE_PIPEWIRE_SINK_MODE (gst_pipewire_sink_mode_get_type ())
+
+/**
+ * GstPipeWireSink:
+ *
+ * Opaque data structure.
+ */
+struct _GstPipeWireSink {
+ GstBaseSink element;
+
+ /*< private >*/
+ gchar *path;
+ gchar *target_object;
+ gchar *client_name;
+ int fd;
+
+ /* video state */
+ gboolean negotiated;
+
+ GstPipeWireCore *core;
+ struct spa_hook core_listener;
+ GstStructure *client_properties;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ GstStructure *stream_properties;
+ GstPipeWireSinkMode mode;
+
+ GstPipeWirePool *pool;
+};
+
+struct _GstPipeWireSinkClass {
+ GstBaseSinkClass parent_class;
+};
+
+GType gst_pipewire_sink_get_type (void);
+GType gst_pipewire_sink_mode_get_type (void);
+
+G_END_DECLS
+
+#endif /* __GST_PIPEWIRE_SINK_H__ */
diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c
new file mode 100644
index 0000000..7243cc1
--- /dev/null
+++ b/src/gst/gstpipewiresrc.c
@@ -0,0 +1,1439 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * SECTION:element-pipewiresrc
+ *
+ * <refsect2>
+ * <title>Example launch line</title>
+ * |[
+ * gst-launch -v pipewiresrc ! videoconvert ! ximagesink
+ * ]| Shows pipewire output in an X window.
+ * </refsect2>
+ */
+
+#define PW_ENABLE_DEPRECATED
+
+#include "config.h"
+#include "gstpipewiresrc.h"
+#include "gstpipewireformat.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <spa/param/video/format.h>
+#include <spa/pod/builder.h>
+#include <spa/utils/result.h>
+
+#include <gst/net/gstnetclientclock.h>
+#include <gst/allocators/gstfdmemory.h>
+#include <gst/allocators/gstdmabuf.h>
+#include <gst/video/video.h>
+
+#include "gstpipewireclock.h"
+
+static GQuark process_mem_data_quark;
+
+GST_DEBUG_CATEGORY_STATIC (pipewire_src_debug);
+#define GST_CAT_DEFAULT pipewire_src_debug
+
+#define DEFAULT_ALWAYS_COPY false
+#define DEFAULT_MIN_BUFFERS 8
+#define DEFAULT_MAX_BUFFERS INT32_MAX
+#define DEFAULT_RESEND_LAST false
+#define DEFAULT_KEEPALIVE_TIME 0
+
+enum
+{
+ PROP_0,
+ PROP_PATH,
+ PROP_TARGET_OBJECT,
+ PROP_CLIENT_NAME,
+ PROP_CLIENT_PROPERTIES,
+ PROP_STREAM_PROPERTIES,
+ PROP_ALWAYS_COPY,
+ PROP_MIN_BUFFERS,
+ PROP_MAX_BUFFERS,
+ PROP_FD,
+ PROP_RESEND_LAST,
+ PROP_KEEPALIVE_TIME,
+};
+
+
+static GstStaticPadTemplate gst_pipewire_src_template =
+GST_STATIC_PAD_TEMPLATE ("src",
+ GST_PAD_SRC,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS_ANY
+ );
+
+#define gst_pipewire_src_parent_class parent_class
+G_DEFINE_TYPE (GstPipeWireSrc, gst_pipewire_src, GST_TYPE_PUSH_SRC);
+
+static GstStateChangeReturn
+gst_pipewire_src_change_state (GstElement * element, GstStateChange transition);
+
+static gboolean gst_pipewire_src_send_event (GstElement * elem, GstEvent * event);
+
+static gboolean gst_pipewire_src_negotiate (GstBaseSrc * basesrc);
+
+static GstFlowReturn gst_pipewire_src_create (GstPushSrc * psrc,
+ GstBuffer ** buffer);
+static gboolean gst_pipewire_src_unlock (GstBaseSrc * basesrc);
+static gboolean gst_pipewire_src_unlock_stop (GstBaseSrc * basesrc);
+static gboolean gst_pipewire_src_start (GstBaseSrc * basesrc);
+static gboolean gst_pipewire_src_stop (GstBaseSrc * basesrc);
+static gboolean gst_pipewire_src_event (GstBaseSrc * src, GstEvent * event);
+static gboolean gst_pipewire_src_query (GstBaseSrc * src, GstQuery * query);
+
+static void
+gst_pipewire_src_set_property (GObject * object, guint prop_id,
+ const GValue * value, GParamSpec * pspec)
+{
+ GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (object);
+
+ switch (prop_id) {
+ case PROP_PATH:
+ g_free (pwsrc->path);
+ pwsrc->path = g_value_dup_string (value);
+ break;
+
+ case PROP_TARGET_OBJECT:
+ g_free (pwsrc->target_object);
+ pwsrc->target_object = g_value_dup_string (value);
+ break;
+
+ case PROP_CLIENT_NAME:
+ g_free (pwsrc->client_name);
+ pwsrc->client_name = g_value_dup_string (value);
+ break;
+
+ case PROP_CLIENT_PROPERTIES:
+ if (pwsrc->client_properties)
+ gst_structure_free (pwsrc->client_properties);
+ pwsrc->client_properties =
+ gst_structure_copy (gst_value_get_structure (value));
+ break;
+
+ case PROP_STREAM_PROPERTIES:
+ if (pwsrc->stream_properties)
+ gst_structure_free (pwsrc->stream_properties);
+ pwsrc->stream_properties =
+ gst_structure_copy (gst_value_get_structure (value));
+ break;
+
+ case PROP_ALWAYS_COPY:
+ pwsrc->always_copy = g_value_get_boolean (value);
+ break;
+
+ case PROP_MIN_BUFFERS:
+ pwsrc->min_buffers = g_value_get_int (value);
+ break;
+
+ case PROP_MAX_BUFFERS:
+ pwsrc->max_buffers = g_value_get_int (value);
+ break;
+
+ case PROP_FD:
+ pwsrc->fd = g_value_get_int (value);
+ break;
+
+ case PROP_RESEND_LAST:
+ pwsrc->resend_last = g_value_get_boolean (value);
+ break;
+
+ case PROP_KEEPALIVE_TIME:
+ pwsrc->keepalive_time = g_value_get_int (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gst_pipewire_src_get_property (GObject * object, guint prop_id,
+ GValue * value, GParamSpec * pspec)
+{
+ GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (object);
+
+ switch (prop_id) {
+ case PROP_PATH:
+ g_value_set_string (value, pwsrc->path);
+ break;
+
+ case PROP_TARGET_OBJECT:
+ g_value_set_string (value, pwsrc->target_object);
+ break;
+
+ case PROP_CLIENT_NAME:
+ g_value_set_string (value, pwsrc->client_name);
+ break;
+
+ case PROP_CLIENT_PROPERTIES:
+ gst_value_set_structure (value, pwsrc->client_properties);
+ break;
+
+ case PROP_STREAM_PROPERTIES:
+ gst_value_set_structure (value, pwsrc->stream_properties);
+ break;
+
+ case PROP_ALWAYS_COPY:
+ g_value_set_boolean (value, pwsrc->always_copy);
+ break;
+
+ case PROP_MIN_BUFFERS:
+ g_value_set_int (value, pwsrc->min_buffers);
+ break;
+
+ case PROP_MAX_BUFFERS:
+ g_value_set_int (value, pwsrc->max_buffers);
+ break;
+
+ case PROP_FD:
+ g_value_set_int (value, pwsrc->fd);
+ break;
+
+ case PROP_RESEND_LAST:
+ g_value_set_boolean (value, pwsrc->resend_last);
+ break;
+
+ case PROP_KEEPALIVE_TIME:
+ g_value_set_int (value, pwsrc->keepalive_time);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static GstClock *
+gst_pipewire_src_provide_clock (GstElement * elem)
+{
+ GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (elem);
+ GstClock *clock;
+
+ GST_OBJECT_LOCK (pwsrc);
+ if (!GST_OBJECT_FLAG_IS_SET (pwsrc, GST_ELEMENT_FLAG_PROVIDE_CLOCK))
+ goto clock_disabled;
+
+ if (pwsrc->clock && pwsrc->is_live)
+ clock = GST_CLOCK_CAST (gst_object_ref (pwsrc->clock));
+ else
+ clock = NULL;
+ GST_OBJECT_UNLOCK (pwsrc);
+
+ return clock;
+
+ /* ERRORS */
+clock_disabled:
+ {
+ GST_DEBUG_OBJECT (pwsrc, "clock provide disabled");
+ GST_OBJECT_UNLOCK (pwsrc);
+ return NULL;
+ }
+}
+
+static void
+gst_pipewire_src_finalize (GObject * object)
+{
+ GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (object);
+
+ if (pwsrc->stream_properties)
+ gst_structure_free (pwsrc->stream_properties);
+ if (pwsrc->client_properties)
+ gst_structure_free (pwsrc->client_properties);
+ if (pwsrc->clock)
+ gst_object_unref (pwsrc->clock);
+ g_free (pwsrc->path);
+ g_free (pwsrc->target_object);
+ g_free (pwsrc->client_name);
+ g_object_unref(pwsrc->pool);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+gst_pipewire_src_class_init (GstPipeWireSrcClass * klass)
+{
+ GObjectClass *gobject_class;
+ GstElementClass *gstelement_class;
+ GstBaseSrcClass *gstbasesrc_class;
+ GstPushSrcClass *gstpushsrc_class;
+
+ gobject_class = (GObjectClass *) klass;
+ gstelement_class = (GstElementClass *) klass;
+ gstbasesrc_class = (GstBaseSrcClass *) klass;
+ gstpushsrc_class = (GstPushSrcClass *) klass;
+
+ gobject_class->finalize = gst_pipewire_src_finalize;
+ gobject_class->set_property = gst_pipewire_src_set_property;
+ gobject_class->get_property = gst_pipewire_src_get_property;
+
+ g_object_class_install_property (gobject_class,
+ PROP_PATH,
+ g_param_spec_string ("path",
+ "Path",
+ "The source path to connect to (NULL = default)",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_DEPRECATED));
+
+ g_object_class_install_property (gobject_class,
+ PROP_TARGET_OBJECT,
+ g_param_spec_string ("target-object",
+ "Target object",
+ "The source name/serial to connect to (NULL = default)",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_CLIENT_NAME,
+ g_param_spec_string ("client-name",
+ "Client Name",
+ "The client name to use (NULL = default)",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_CLIENT_PROPERTIES,
+ g_param_spec_boxed ("client-properties",
+ "client properties",
+ "list of PipeWire client properties",
+ GST_TYPE_STRUCTURE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_STREAM_PROPERTIES,
+ g_param_spec_boxed ("stream-properties",
+ "stream properties",
+ "list of PipeWire stream properties",
+ GST_TYPE_STRUCTURE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_ALWAYS_COPY,
+ g_param_spec_boolean ("always-copy",
+ "Always copy",
+ "Always copy the buffer and data",
+ DEFAULT_ALWAYS_COPY,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_MIN_BUFFERS,
+ g_param_spec_int ("min-buffers",
+ "Min Buffers",
+ "Minimum number of buffers to negotiate with PipeWire",
+ 1, G_MAXINT, DEFAULT_MIN_BUFFERS,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_MAX_BUFFERS,
+ g_param_spec_int ("max-buffers",
+ "Max Buffers",
+ "Maximum number of buffers to negotiate with PipeWire",
+ 1, G_MAXINT, DEFAULT_MAX_BUFFERS,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_FD,
+ g_param_spec_int ("fd",
+ "Fd",
+ "The fd to connect with",
+ -1, G_MAXINT, -1,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_RESEND_LAST,
+ g_param_spec_boolean ("resend-last",
+ "Resend last",
+ "Resend last buffer on EOS",
+ DEFAULT_RESEND_LAST,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class,
+ PROP_KEEPALIVE_TIME,
+ g_param_spec_int ("keepalive-time",
+ "Keepalive Time",
+ "Periodically send last buffer (in milliseconds, 0 = disabled)",
+ 0, G_MAXINT, DEFAULT_KEEPALIVE_TIME,
+ 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;
+
+ gst_element_class_set_static_metadata (gstelement_class,
+ "PipeWire source", "Source/Video",
+ "Uses PipeWire to create video", "Wim Taymans <wim.taymans@gmail.com>");
+
+ gst_element_class_add_pad_template (gstelement_class,
+ gst_static_pad_template_get (&gst_pipewire_src_template));
+
+ gstbasesrc_class->negotiate = gst_pipewire_src_negotiate;
+ gstbasesrc_class->unlock = gst_pipewire_src_unlock;
+ gstbasesrc_class->unlock_stop = gst_pipewire_src_unlock_stop;
+ gstbasesrc_class->start = gst_pipewire_src_start;
+ gstbasesrc_class->stop = gst_pipewire_src_stop;
+ gstbasesrc_class->event = gst_pipewire_src_event;
+ gstbasesrc_class->query = gst_pipewire_src_query;
+ gstpushsrc_class->create = gst_pipewire_src_create;
+
+ GST_DEBUG_CATEGORY_INIT (pipewire_src_debug, "pipewiresrc", 0,
+ "PipeWire Source");
+
+ process_mem_data_quark = g_quark_from_static_string ("GstPipeWireSrcProcessMemQuark");
+}
+
+static void
+gst_pipewire_src_init (GstPipeWireSrc * src)
+{
+ /* we operate in time */
+ gst_base_src_set_format (GST_BASE_SRC (src), GST_FORMAT_TIME);
+
+ /* we're a live source, unless explicitly requested not to be */
+ gst_base_src_set_live (GST_BASE_SRC (src), TRUE);
+
+ GST_OBJECT_FLAG_SET (src, GST_ELEMENT_FLAG_PROVIDE_CLOCK);
+
+ src->always_copy = DEFAULT_ALWAYS_COPY;
+ src->min_buffers = DEFAULT_MIN_BUFFERS;
+ src->max_buffers = DEFAULT_MAX_BUFFERS;
+ src->fd = -1;
+ src->resend_last = DEFAULT_RESEND_LAST;
+ src->keepalive_time = DEFAULT_KEEPALIVE_TIME;
+
+ src->client_name = g_strdup(pw_get_client_name ());
+
+ src->pool = gst_pipewire_pool_new ();
+}
+
+static gboolean
+buffer_recycle (GstMiniObject *obj)
+{
+ GstPipeWireSrc *src;
+ GstPipeWirePoolData *data;
+ int res;
+
+ data = gst_pipewire_pool_get_data (GST_BUFFER_CAST(obj));
+
+ GST_OBJECT_LOCK (data->pool);
+ if (!obj->dispose) {
+ GST_OBJECT_UNLOCK (data->pool);
+ return TRUE;
+ }
+
+ GST_BUFFER_FLAGS (obj) = data->flags;
+ src = data->owner;
+
+ pw_thread_loop_lock (src->core->loop);
+ if (!obj->dispose) {
+ pw_thread_loop_unlock (src->core->loop);
+ GST_OBJECT_UNLOCK (data->pool);
+ return TRUE;
+ }
+
+ gst_mini_object_ref (obj);
+
+ data->queued = TRUE;
+
+ if ((res = pw_stream_queue_buffer (src->stream, data->b)) < 0)
+ GST_WARNING_OBJECT (src, "can't queue recycled buffer %p, %s", obj, spa_strerror(res));
+ else
+ GST_LOG_OBJECT (src, "recycle buffer %p", obj);
+
+ pw_thread_loop_unlock (src->core->loop);
+
+ GST_OBJECT_UNLOCK (data->pool);
+
+ return FALSE;
+}
+
+static void
+on_add_buffer (void *_data, struct pw_buffer *b)
+{
+ GstPipeWireSrc *pwsrc = _data;
+ GstPipeWirePoolData *data;
+
+ gst_pipewire_pool_wrap_buffer (pwsrc->pool, b);
+ data = b->user_data;
+ GST_DEBUG_OBJECT (pwsrc, "add buffer %p", data->buf);
+ data->owner = pwsrc;
+ data->queued = TRUE;
+ GST_MINI_OBJECT_CAST (data->buf)->dispose = buffer_recycle;
+}
+
+static void
+on_remove_buffer (void *_data, struct pw_buffer *b)
+{
+ GstPipeWireSrc *pwsrc = _data;
+ GstPipeWirePoolData *data = b->user_data;
+ GstBuffer *buf = data->buf;
+ int res;
+
+ GST_DEBUG_OBJECT (pwsrc, "remove buffer %p", buf);
+
+ GST_MINI_OBJECT_CAST (buf)->dispose = NULL;
+
+ if (data->queued) {
+ gst_buffer_unref (buf);
+ } else {
+ if ((res = pw_stream_queue_buffer (pwsrc->stream, b)) < 0)
+ GST_WARNING_OBJECT (pwsrc, "can't queue removed buffer %p, %s", buf, spa_strerror(res));
+ }
+}
+
+static const char * const transform_map[] = {
+ [SPA_META_TRANSFORMATION_None] = "rotate-0",
+ [SPA_META_TRANSFORMATION_90] = "rotate-90",
+ [SPA_META_TRANSFORMATION_180] = "rotate-180",
+ [SPA_META_TRANSFORMATION_270] = "rotate-270",
+ [SPA_META_TRANSFORMATION_Flipped] = "flip-rotate-0",
+ [SPA_META_TRANSFORMATION_Flipped90] = "flip-rotate-270",
+ [SPA_META_TRANSFORMATION_Flipped180] = "flip-rotate-180",
+ [SPA_META_TRANSFORMATION_Flipped270] = "flip-rotate-90",
+};
+
+static const char *spa_transform_value_to_gst_image_orientation(uint32_t transform_value)
+{
+ if (transform_value >= SPA_N_ELEMENTS(transform_map))
+ transform_value = SPA_META_TRANSFORMATION_None;
+
+ return transform_map[transform_value];
+}
+
+static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc)
+{
+ struct pw_buffer *b;
+ GstBuffer *buf;
+ GstPipeWirePoolData *data;
+ struct spa_meta_header *h;
+ struct spa_meta_region *crop;
+ struct spa_meta_videotransform *videotransform;
+ guint i;
+
+ b = pw_stream_dequeue_buffer (pwsrc->stream);
+ if (b == NULL)
+ return NULL;
+
+ data = b->user_data;
+
+ if (!GST_IS_BUFFER (data->buf)) {
+ GST_ERROR_OBJECT (pwsrc, "stream buffer %p is missing", data->buf);
+ return NULL;
+ }
+
+ if (!data->queued) {
+ GST_ERROR_OBJECT (pwsrc, "buffer %p was not recycled", data->buf);
+ return NULL;
+ }
+
+ GST_LOG_OBJECT (pwsrc, "got new buffer %p", data->buf);
+
+ buf = gst_buffer_new ();
+
+ data->queued = FALSE;
+ GST_BUFFER_PTS (buf) = GST_CLOCK_TIME_NONE;
+ GST_BUFFER_DTS (buf) = GST_CLOCK_TIME_NONE;
+
+ h = data->header;
+ if (h) {
+ GST_LOG_OBJECT (pwsrc, "pts %" G_GUINT64_FORMAT ", dts_offset %" G_GUINT64_FORMAT, h->pts, h->dts_offset);
+
+ if (GST_CLOCK_TIME_IS_VALID (h->pts)) {
+ GST_BUFFER_PTS (buf) = h->pts + GST_PIPEWIRE_CLOCK (pwsrc->clock)->time_offset;
+ if (GST_BUFFER_PTS (buf) + h->dts_offset > 0)
+ GST_BUFFER_DTS (buf) = GST_BUFFER_PTS (buf) + h->dts_offset;
+ }
+ GST_BUFFER_OFFSET (buf) = h->seq;
+ }
+ crop = data->crop;
+ if (crop) {
+ GstVideoCropMeta *meta = gst_buffer_get_video_crop_meta(buf);
+ if (meta) {
+ meta->x = crop->region.position.x;
+ meta->y = crop->region.position.y;
+ meta->width = crop->region.size.width;
+ meta->height = crop->region.size.height;
+ }
+ }
+
+ videotransform = data->videotransform;
+ if (videotransform) {
+ if (pwsrc->transform_value != videotransform->transform) {
+ GstEvent *tag_event;
+ const char* tag_string;
+
+ tag_string =
+ spa_transform_value_to_gst_image_orientation(videotransform->transform);
+
+ GST_LOG_OBJECT (pwsrc, "got new videotransform: %u / %s",
+ videotransform->transform, tag_string);
+
+ tag_event = gst_event_new_tag(gst_tag_list_new(GST_TAG_IMAGE_ORIENTATION,
+ tag_string, NULL));
+ gst_pad_push_event (GST_BASE_SRC_PAD (pwsrc), tag_event);
+
+ pwsrc->transform_value = videotransform->transform;
+ }
+ }
+
+ for (i = 0; i < b->buffer->n_datas; i++) {
+ struct spa_data *d = &b->buffer->datas[i];
+ GstMemory *pmem = gst_buffer_peek_memory (data->buf, i);
+ if (pmem) {
+ GstMemory *mem;
+ if (!pwsrc->always_copy)
+ mem = gst_memory_share (pmem, d->chunk->offset, d->chunk->size);
+ else
+ mem = gst_memory_copy (pmem, d->chunk->offset, d->chunk->size);
+ gst_buffer_insert_memory (buf, i, mem);
+ }
+ if (d->chunk->flags & SPA_CHUNK_FLAG_CORRUPTED)
+ GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_CORRUPTED);
+ }
+ if (!pwsrc->always_copy)
+ gst_buffer_add_parent_buffer_meta (buf, data->buf);
+ gst_buffer_unref (data->buf);
+ return buf;
+}
+
+static void
+on_process (void *_data)
+{
+ GstPipeWireSrc *pwsrc = _data;
+ pw_thread_loop_signal (pwsrc->core->loop, FALSE);
+}
+
+static void
+on_state_changed (void *data,
+ enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ GstPipeWireSrc *pwsrc = data;
+
+ GST_DEBUG ("got stream state %s", pw_stream_state_as_string (state));
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ case PW_STREAM_STATE_CONNECTING:
+ case PW_STREAM_STATE_PAUSED:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ case PW_STREAM_STATE_ERROR:
+ GST_ELEMENT_ERROR (pwsrc, RESOURCE, FAILED,
+ ("stream error: %s", error), (NULL));
+ break;
+ }
+ pw_thread_loop_signal (pwsrc->core->loop, FALSE);
+}
+
+static void
+parse_stream_properties (GstPipeWireSrc *pwsrc, const struct pw_properties *props)
+{
+ const gchar *var;
+ gboolean is_live;
+
+ GST_OBJECT_LOCK (pwsrc);
+ var = pw_properties_get (props, PW_KEY_STREAM_IS_LIVE);
+ is_live = pwsrc->is_live = var ? pw_properties_parse_bool(var) : TRUE;
+
+ var = pw_properties_get (props, PW_KEY_STREAM_LATENCY_MIN);
+ pwsrc->min_latency = var ? (GstClockTime) atoi (var) : 0;
+
+ var = pw_properties_get (props, PW_KEY_STREAM_LATENCY_MAX);
+ pwsrc->max_latency = var ? (GstClockTime) atoi (var) : GST_CLOCK_TIME_NONE;
+ GST_OBJECT_UNLOCK (pwsrc);
+
+ GST_DEBUG_OBJECT (pwsrc, "live %d", is_live);
+
+ gst_base_src_set_live (GST_BASE_SRC (pwsrc), is_live);
+}
+
+static gboolean
+gst_pipewire_src_stream_start (GstPipeWireSrc *pwsrc)
+{
+ const char *error = NULL;
+ struct timespec abstime;
+
+ pw_thread_loop_lock (pwsrc->core->loop);
+ GST_DEBUG_OBJECT (pwsrc, "doing stream start");
+
+ pw_thread_loop_get_time (pwsrc->core->loop, &abstime,
+ GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC);
+
+ while (TRUE) {
+ enum pw_stream_state state = pw_stream_get_state (pwsrc->stream, &error);
+
+ GST_DEBUG_OBJECT (pwsrc, "waiting for STREAMING, now %s", pw_stream_state_as_string (state));
+ if (state == PW_STREAM_STATE_STREAMING)
+ break;
+
+ if (state == PW_STREAM_STATE_ERROR)
+ goto start_error;
+
+ if (pwsrc->flushing) {
+ error = "flushing";
+ goto start_error;
+ }
+
+ if (pw_thread_loop_timed_wait_full (pwsrc->core->loop, &abstime) < 0) {
+ error = "timeout";
+ goto start_error;
+ }
+ }
+
+ parse_stream_properties (pwsrc, pw_stream_get_properties (pwsrc->stream));
+ GST_DEBUG_OBJECT (pwsrc, "signal started");
+ pwsrc->started = TRUE;
+ pw_thread_loop_signal (pwsrc->core->loop, FALSE);
+ pw_thread_loop_unlock (pwsrc->core->loop);
+
+ return TRUE;
+
+start_error:
+ {
+ GST_DEBUG_OBJECT (pwsrc, "error starting stream: %s", error);
+ pw_thread_loop_signal (pwsrc->core->loop, FALSE);
+ pw_thread_loop_unlock (pwsrc->core->loop);
+ return FALSE;
+ }
+}
+
+static enum pw_stream_state
+wait_started (GstPipeWireSrc *this)
+{
+ enum pw_stream_state state;
+ const char *error = NULL;
+ struct timespec abstime;
+
+ pw_thread_loop_lock (this->core->loop);
+
+ pw_thread_loop_get_time (this->core->loop, &abstime,
+ GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC);
+
+ while (TRUE) {
+ state = pw_stream_get_state (this->stream, &error);
+
+ GST_DEBUG_OBJECT (this, "waiting for started signal, state now %s",
+ pw_stream_state_as_string (state));
+
+ if (state == PW_STREAM_STATE_ERROR)
+ break;
+
+ if (this->flushing) {
+ state = PW_STREAM_STATE_ERROR;
+ break;
+ }
+
+ if (this->started)
+ break;
+
+ if (pw_thread_loop_timed_wait_full (this->core->loop, &abstime) < 0) {
+ state = PW_STREAM_STATE_ERROR;
+ break;
+ }
+ }
+ GST_DEBUG_OBJECT (this, "got started signal: %s",
+ pw_stream_state_as_string (state));
+ pw_thread_loop_unlock (this->core->loop);
+
+ return state;
+}
+
+static gboolean
+gst_pipewire_src_negotiate (GstBaseSrc * basesrc)
+{
+ GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (basesrc);
+ GstCaps *thiscaps;
+ GstCaps *caps = NULL;
+ GstCaps *peercaps = NULL;
+ gboolean result = FALSE;
+ GPtrArray *possible;
+ const char *error = NULL;
+ struct timespec abstime;
+ uint32_t target_id;
+
+ /* first see what is possible on our source pad */
+ thiscaps = gst_pad_query_caps (GST_BASE_SRC_PAD (basesrc), NULL);
+ GST_DEBUG_OBJECT (basesrc, "caps of src: %" GST_PTR_FORMAT, thiscaps);
+ /* nothing or anything is allowed, we're done */
+ if (thiscaps == NULL)
+ goto no_nego_needed;
+
+ if (G_UNLIKELY (gst_caps_is_empty (thiscaps)))
+ goto no_caps;
+
+ /* get the peer caps */
+ peercaps = gst_pad_peer_query_caps (GST_BASE_SRC_PAD (basesrc), thiscaps);
+ GST_DEBUG_OBJECT (basesrc, "caps of peer: %" GST_PTR_FORMAT, peercaps);
+ if (peercaps) {
+ /* The result is already a subset of our caps */
+ caps = peercaps;
+ gst_caps_unref (thiscaps);
+ } else {
+ /* no peer, work with our own caps then */
+ caps = thiscaps;
+ }
+ if (caps == NULL || gst_caps_is_empty (caps))
+ goto no_common_caps;
+
+ GST_DEBUG_OBJECT (basesrc, "have common caps: %" GST_PTR_FORMAT, caps);
+
+ /* open a connection with these caps */
+ possible = gst_caps_to_format_all (caps, SPA_PARAM_EnumFormat);
+ gst_caps_unref (caps);
+
+ /* first disconnect */
+ pw_thread_loop_lock (pwsrc->core->loop);
+ if (pw_stream_get_state(pwsrc->stream, &error) != PW_STREAM_STATE_UNCONNECTED) {
+ GST_DEBUG_OBJECT (basesrc, "disconnect capture");
+ pw_stream_disconnect (pwsrc->stream);
+ while (TRUE) {
+ enum pw_stream_state state = pw_stream_get_state (pwsrc->stream, &error);
+
+ GST_DEBUG_OBJECT (basesrc, "waiting for UNCONNECTED, now %s", pw_stream_state_as_string (state));
+ if (state == PW_STREAM_STATE_UNCONNECTED)
+ break;
+
+ if (state == PW_STREAM_STATE_ERROR || pwsrc->flushing) {
+ g_ptr_array_unref (possible);
+ goto connect_error;
+ }
+
+ pw_thread_loop_wait (pwsrc->core->loop);
+ }
+ }
+
+ target_id = pwsrc->path ? (uint32_t)atoi(pwsrc->path) : PW_ID_ANY;
+
+ if (pwsrc->target_object) {
+ struct spa_dict_item items[2] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_TARGET_OBJECT, pwsrc->target_object),
+ /* XXX deprecated but the portal and some example apps only
+ * provide the object id */
+ SPA_DICT_ITEM_INIT(PW_KEY_NODE_TARGET, NULL),
+ };
+ struct spa_dict dict = SPA_DICT_INIT_ARRAY(items);
+ uint64_t serial;
+
+ /* If target.object is a name, set it also to node.target */
+ if (spa_atou64(pwsrc->target_object, &serial, 0)) {
+ dict.n_items = 1;
+ } else {
+ target_id = PW_ID_ANY;
+ items[1].value = pwsrc->target_object;
+ }
+
+ pw_stream_update_properties (pwsrc->stream, &dict);
+ }
+
+ GST_DEBUG_OBJECT (basesrc, "connect capture with path %s, target-object %s",
+ pwsrc->path, pwsrc->target_object);
+ pwsrc->negotiated = FALSE;
+ pw_stream_connect (pwsrc->stream,
+ PW_DIRECTION_INPUT,
+ target_id,
+ PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_DONT_RECONNECT,
+ (const struct spa_pod **)possible->pdata,
+ possible->len);
+ g_ptr_array_free (possible, TRUE);
+
+ pw_thread_loop_get_time (pwsrc->core->loop, &abstime,
+ GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC);
+
+ while (TRUE) {
+ enum pw_stream_state state = pw_stream_get_state (pwsrc->stream, &error);
+
+ GST_DEBUG_OBJECT (basesrc, "waiting for NEGOTIATED, now %s", pw_stream_state_as_string (state));
+ if (state == PW_STREAM_STATE_ERROR || pwsrc->flushing)
+ goto connect_error;
+
+ if (pwsrc->negotiated)
+ break;
+
+ if (pw_thread_loop_timed_wait_full (pwsrc->core->loop, &abstime) < 0)
+ goto connect_error;
+ }
+ caps = pwsrc->caps;
+ pwsrc->caps = NULL;
+ pw_thread_loop_unlock (pwsrc->core->loop);
+
+ if (caps == NULL)
+ goto no_caps;
+
+ gst_pipewire_clock_reset (GST_PIPEWIRE_CLOCK (pwsrc->clock), 0);
+
+ GST_DEBUG_OBJECT (pwsrc, "set format %" GST_PTR_FORMAT, caps);
+ result = gst_base_src_set_caps (GST_BASE_SRC (pwsrc), caps);
+ gst_caps_unref (caps);
+
+ result = gst_pipewire_src_stream_start (pwsrc);
+
+ pwsrc->started = result;
+
+ return result;
+
+no_nego_needed:
+ {
+ GST_DEBUG_OBJECT (basesrc, "no negotiation needed");
+ if (thiscaps)
+ gst_caps_unref (thiscaps);
+ return TRUE;
+ }
+no_caps:
+ {
+ GST_ELEMENT_ERROR (basesrc, STREAM, FORMAT,
+ ("No supported formats found"),
+ ("This element did not produce valid caps"));
+ if (thiscaps)
+ gst_caps_unref (thiscaps);
+ return FALSE;
+ }
+no_common_caps:
+ {
+ GST_ELEMENT_ERROR (basesrc, STREAM, FORMAT,
+ ("No supported formats found"),
+ ("This element does not have formats in common with the peer"));
+ if (caps)
+ gst_caps_unref (caps);
+ return FALSE;
+ }
+connect_error:
+ {
+ GST_DEBUG_OBJECT (basesrc, "connect error");
+ pw_thread_loop_unlock (pwsrc->core->loop);
+ return FALSE;
+ }
+}
+
+static void
+on_param_changed (void *data, uint32_t id,
+ const struct spa_pod *param)
+{
+ GstPipeWireSrc *pwsrc = data;
+
+ if (param == NULL || id != SPA_PARAM_Format) {
+ GST_DEBUG_OBJECT (pwsrc, "clear format");
+ return;
+ }
+ if (pwsrc->caps)
+ gst_caps_unref(pwsrc->caps);
+ pwsrc->caps = gst_caps_from_format (param);
+
+ pwsrc->negotiated = pwsrc->caps != NULL;
+
+ if (pwsrc->negotiated) {
+ const struct spa_pod *params[4];
+ struct spa_pod_builder b = { NULL };
+ uint8_t buffer[512];
+ uint32_t buffers = CLAMP (16, pwsrc->min_buffers, pwsrc->max_buffers);
+ int buffertypes;
+
+ buffertypes = (1<<SPA_DATA_DmaBuf);
+ if (spa_pod_find_prop (param, NULL, SPA_FORMAT_VIDEO_modifier)) {
+ gst_caps_features_remove (gst_caps_get_features (pwsrc->caps, 0),
+ GST_CAPS_FEATURE_MEMORY_SYSTEM_MEMORY);
+ gst_caps_features_add (gst_caps_get_features (pwsrc->caps, 0),
+ GST_CAPS_FEATURE_MEMORY_DMABUF);
+ } else {
+ buffertypes |= ((1<<SPA_DATA_MemFd) | (1<<SPA_DATA_MemPtr));
+ }
+
+ GST_DEBUG_OBJECT (pwsrc, "we got format %" GST_PTR_FORMAT, pwsrc->caps);
+
+ spa_pod_builder_init (&b, buffer, sizeof (buffer));
+ params[0] = spa_pod_builder_add_object (&b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers,
+ pwsrc->min_buffers,
+ pwsrc->max_buffers),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_CHOICE_RANGE_Int(0, 1, INT32_MAX),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(buffertypes));
+
+ params[1] = spa_pod_builder_add_object (&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_header)));
+ params[2] = spa_pod_builder_add_object (&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_region)));
+ params[3] = spa_pod_builder_add_object (&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_videotransform)));
+
+ GST_DEBUG_OBJECT (pwsrc, "doing finish format");
+ pw_stream_update_params (pwsrc->stream, params, SPA_N_ELEMENTS(params));
+ } else {
+ GST_WARNING_OBJECT (pwsrc, "finish format with error");
+ pw_stream_set_error (pwsrc->stream, -EINVAL, "unhandled format");
+ }
+ pw_thread_loop_signal (pwsrc->core->loop, FALSE);
+}
+
+static gboolean
+gst_pipewire_src_unlock (GstBaseSrc * basesrc)
+{
+ GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (basesrc);
+
+ pw_thread_loop_lock (pwsrc->core->loop);
+ GST_DEBUG_OBJECT (pwsrc, "setting flushing");
+ pwsrc->flushing = TRUE;
+ pw_thread_loop_signal (pwsrc->core->loop, FALSE);
+ pw_thread_loop_unlock (pwsrc->core->loop);
+
+ return TRUE;
+}
+
+static gboolean
+gst_pipewire_src_unlock_stop (GstBaseSrc * basesrc)
+{
+ GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (basesrc);
+
+ pw_thread_loop_lock (pwsrc->core->loop);
+ GST_DEBUG_OBJECT (pwsrc, "unsetting flushing");
+ pwsrc->flushing = FALSE;
+ pw_thread_loop_unlock (pwsrc->core->loop);
+
+ return TRUE;
+}
+
+static gboolean
+gst_pipewire_src_event (GstBaseSrc * src, GstEvent * event)
+{
+ gboolean res = FALSE;
+
+ switch (GST_EVENT_TYPE (event)) {
+ case GST_EVENT_CUSTOM_UPSTREAM:
+ if (gst_video_event_is_force_key_unit (event)) {
+ GstClockTime running_time;
+ gboolean all_headers;
+ guint count;
+
+ gst_video_event_parse_upstream_force_key_unit (event,
+ &running_time, &all_headers, &count);
+
+ res = TRUE;
+ } else {
+ res = GST_BASE_SRC_CLASS (parent_class)->event (src, event);
+ }
+ break;
+ default:
+ res = GST_BASE_SRC_CLASS (parent_class)->event (src, event);
+ break;
+ }
+ return res;
+}
+
+static gboolean
+gst_pipewire_src_query (GstBaseSrc * src, GstQuery * query)
+{
+ gboolean res = FALSE;
+ GstPipeWireSrc *pwsrc;
+
+ pwsrc = GST_PIPEWIRE_SRC (src);
+
+ switch (GST_QUERY_TYPE (query)) {
+ case GST_QUERY_LATENCY:
+ GST_OBJECT_LOCK (pwsrc);
+ pwsrc->min_latency = 10000000;
+ pwsrc->max_latency = GST_CLOCK_TIME_NONE;
+ gst_query_set_latency (query, pwsrc->is_live, pwsrc->min_latency, pwsrc->max_latency);
+ GST_OBJECT_UNLOCK (pwsrc);
+ res = TRUE;
+ break;
+ default:
+ res = GST_BASE_SRC_CLASS (parent_class)->query (src, query);
+ break;
+ }
+ return res;
+}
+
+static GstFlowReturn
+gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer)
+{
+ GstPipeWireSrc *pwsrc;
+ GstClockTime pts, dts, base_time;
+ const char *error = NULL;
+ GstBuffer *buf;
+ gboolean update_time = FALSE, timeout = FALSE;
+
+ pwsrc = GST_PIPEWIRE_SRC (psrc);
+
+ pw_thread_loop_lock (pwsrc->core->loop);
+ if (!pwsrc->negotiated)
+ goto not_negotiated;
+
+ while (TRUE) {
+ enum pw_stream_state state;
+
+ if (pwsrc->flushing)
+ goto streaming_stopped;
+
+ if (pwsrc->stream == NULL)
+ goto streaming_error;
+
+ state = pw_stream_get_state (pwsrc->stream, &error);
+ if (state == PW_STREAM_STATE_ERROR)
+ goto streaming_error;
+
+ if (state != PW_STREAM_STATE_STREAMING)
+ goto streaming_stopped;
+
+ if (pwsrc->eos) {
+ if (pwsrc->last_buffer == NULL)
+ goto streaming_eos;
+ buf = pwsrc->last_buffer;
+ pwsrc->last_buffer = NULL;
+ update_time = TRUE;
+ GST_LOG_OBJECT (pwsrc, "EOS, send last buffer");
+ break;
+ } else if (timeout) {
+ if (pwsrc->last_buffer != NULL) {
+ update_time = TRUE;
+ buf = gst_buffer_ref(pwsrc->last_buffer);
+ GST_LOG_OBJECT (pwsrc, "timeout, send keepalive buffer");
+ break;
+ }
+ } else {
+ buf = dequeue_buffer (pwsrc);
+ GST_LOG_OBJECT (pwsrc, "popped buffer %p", buf);
+ if (buf != NULL) {
+ if (pwsrc->resend_last || pwsrc->keepalive_time > 0)
+ gst_buffer_replace (&pwsrc->last_buffer, buf);
+ break;
+ }
+ }
+ timeout = FALSE;
+ if (pwsrc->keepalive_time > 0) {
+ struct timespec abstime;
+ pw_thread_loop_get_time(pwsrc->core->loop, &abstime,
+ pwsrc->keepalive_time * SPA_NSEC_PER_MSEC);
+ if (pw_thread_loop_timed_wait_full (pwsrc->core->loop, &abstime) == -ETIMEDOUT)
+ timeout = TRUE;
+ } else {
+ pw_thread_loop_wait (pwsrc->core->loop);
+ }
+ }
+ pw_thread_loop_unlock (pwsrc->core->loop);
+
+ *buffer = buf;
+
+ if (pwsrc->is_live)
+ base_time = GST_ELEMENT_CAST (psrc)->base_time;
+ else
+ base_time = 0;
+
+ if (update_time) {
+ GstClock *clock = gst_element_get_clock (GST_ELEMENT_CAST (pwsrc));
+ if (clock != NULL) {
+ pts = dts = gst_clock_get_time (clock);
+ gst_object_unref (clock);
+ } else {
+ pts = dts = GST_CLOCK_TIME_NONE;
+ }
+ } else {
+ pts = GST_BUFFER_PTS (*buffer);
+ dts = GST_BUFFER_DTS (*buffer);
+ }
+
+ if (GST_CLOCK_TIME_IS_VALID (pts))
+ pts = (pts >= base_time ? pts - base_time : 0);
+ if (GST_CLOCK_TIME_IS_VALID (dts))
+ dts = (dts >= base_time ? dts - base_time : 0);
+
+ GST_LOG_OBJECT (pwsrc,
+ "pts %" G_GUINT64_FORMAT ", dts %" G_GUINT64_FORMAT
+ ", base-time %" GST_TIME_FORMAT " -> %" GST_TIME_FORMAT ", %" GST_TIME_FORMAT,
+ GST_BUFFER_PTS (*buffer), GST_BUFFER_DTS (*buffer), GST_TIME_ARGS (base_time),
+ GST_TIME_ARGS (pts), GST_TIME_ARGS (dts));
+
+ GST_BUFFER_PTS (*buffer) = pts;
+ GST_BUFFER_DTS (*buffer) = dts;
+
+ return GST_FLOW_OK;
+
+not_negotiated:
+ {
+ pw_thread_loop_unlock (pwsrc->core->loop);
+ return GST_FLOW_NOT_NEGOTIATED;
+ }
+streaming_eos:
+ {
+ pw_thread_loop_unlock (pwsrc->core->loop);
+ return GST_FLOW_EOS;
+ }
+streaming_error:
+ {
+ pw_thread_loop_unlock (pwsrc->core->loop);
+ return GST_FLOW_ERROR;
+ }
+streaming_stopped:
+ {
+ pw_thread_loop_unlock (pwsrc->core->loop);
+ return GST_FLOW_FLUSHING;
+ }
+}
+
+static gboolean
+gst_pipewire_src_start (GstBaseSrc * basesrc)
+{
+ return TRUE;
+}
+
+static gboolean
+gst_pipewire_src_stop (GstBaseSrc * basesrc)
+{
+ GstPipeWireSrc *pwsrc;
+
+ pwsrc = GST_PIPEWIRE_SRC (basesrc);
+
+ pw_thread_loop_lock (pwsrc->core->loop);
+ pwsrc->eos = false;
+ gst_buffer_replace (&pwsrc->last_buffer, NULL);
+ gst_caps_replace(&pwsrc->caps, NULL);
+ pw_thread_loop_unlock (pwsrc->core->loop);
+
+ return TRUE;
+}
+
+static gboolean
+copy_properties (GQuark field_id,
+ const GValue *value,
+ gpointer user_data)
+{
+ struct pw_properties *properties = user_data;
+ GValue dst = { 0 };
+
+ if (g_value_type_transformable (G_VALUE_TYPE(value), G_TYPE_STRING)) {
+ g_value_init(&dst, G_TYPE_STRING);
+ if (g_value_transform(value, &dst)) {
+ pw_properties_set (properties,
+ g_quark_to_string (field_id),
+ g_value_get_string (&dst));
+ }
+ g_value_unset(&dst);
+ }
+ return TRUE;
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = on_state_changed,
+ .param_changed = on_param_changed,
+ .add_buffer = on_add_buffer,
+ .remove_buffer = on_remove_buffer,
+ .process = on_process,
+};
+
+static gboolean
+gst_pipewire_src_open (GstPipeWireSrc * pwsrc)
+{
+ struct pw_properties *props;
+
+ GST_DEBUG_OBJECT (pwsrc, "open");
+
+ pwsrc->core = gst_pipewire_core_get(pwsrc->fd);
+ if (pwsrc->core == NULL)
+ goto connect_error;
+
+ pw_thread_loop_lock (pwsrc->core->loop);
+
+ props = pw_properties_new (NULL, NULL);
+ if (pwsrc->client_properties) {
+ gst_structure_foreach (pwsrc->client_properties, copy_properties, props);
+ pw_core_update_properties (pwsrc->core->core, &props->dict);
+ pw_properties_clear(props);
+ }
+ if (pwsrc->client_name) {
+ pw_properties_set (props, PW_KEY_NODE_NAME, pwsrc->client_name);
+ pw_properties_set (props, PW_KEY_NODE_DESCRIPTION, pwsrc->client_name);
+ }
+ if (pwsrc->stream_properties) {
+ gst_structure_foreach (pwsrc->stream_properties, copy_properties, props);
+ }
+
+ if ((pwsrc->stream = pw_stream_new (pwsrc->core->core,
+ pwsrc->client_name, props)) == NULL)
+ goto no_stream;
+
+
+ pw_stream_add_listener(pwsrc->stream,
+ &pwsrc->stream_listener,
+ &stream_events,
+ pwsrc);
+
+ pwsrc->clock = gst_pipewire_clock_new (pwsrc->stream, pwsrc->last_time);
+ pw_thread_loop_unlock (pwsrc->core->loop);
+
+ return TRUE;
+
+ /* ERRORS */
+connect_error:
+ {
+ GST_ELEMENT_ERROR (pwsrc, RESOURCE, FAILED, ("can't connect"), (NULL));
+ return FALSE;
+ }
+no_stream:
+ {
+ GST_ELEMENT_ERROR (pwsrc, RESOURCE, FAILED, ("can't create stream"), (NULL));
+ pw_thread_loop_unlock (pwsrc->core->loop);
+ gst_pipewire_core_release (pwsrc->core);
+ pwsrc->core = NULL;
+ return FALSE;
+ }
+}
+
+static void
+gst_pipewire_src_close (GstPipeWireSrc * pwsrc)
+{
+ pwsrc->last_time = gst_clock_get_time (pwsrc->clock);
+
+ GST_DEBUG_OBJECT (pwsrc, "close");
+
+ gst_element_post_message (GST_ELEMENT (pwsrc),
+ gst_message_new_clock_lost (GST_OBJECT_CAST (pwsrc), pwsrc->clock));
+
+ GST_OBJECT_LOCK (pwsrc);
+ GST_PIPEWIRE_CLOCK (pwsrc->clock)->stream = NULL;
+ g_clear_object (&pwsrc->clock);
+ GST_OBJECT_UNLOCK (pwsrc);
+
+ GST_OBJECT_LOCK (pwsrc->pool);
+ pw_thread_loop_lock (pwsrc->core->loop);
+ if (pwsrc->stream) {
+ pw_stream_destroy (pwsrc->stream);
+ pwsrc->stream = NULL;
+ }
+ pw_thread_loop_unlock (pwsrc->core->loop);
+ GST_OBJECT_UNLOCK (pwsrc->pool);
+
+ if (pwsrc->core) {
+ gst_pipewire_core_release (pwsrc->core);
+ pwsrc->core = NULL;
+ }
+}
+
+static gboolean
+gst_pipewire_src_send_event (GstElement * elem, GstEvent * event)
+{
+ GstPipeWireSrc *this = GST_PIPEWIRE_SRC_CAST (elem);
+ gboolean ret;
+
+ switch (GST_EVENT_TYPE (event)) {
+ case GST_EVENT_EOS:
+ GST_DEBUG_OBJECT (this, "got EOS");
+ pw_thread_loop_lock (this->core->loop);
+ this->eos = true;
+ pw_thread_loop_signal (this->core->loop, FALSE);
+ pw_thread_loop_unlock (this->core->loop);
+ ret = TRUE;
+ break;
+ default:
+ ret = GST_ELEMENT_CLASS (parent_class)->send_event (elem, event);
+ break;
+ }
+ return ret;
+}
+
+static GstStateChangeReturn
+gst_pipewire_src_change_state (GstElement * element, GstStateChange transition)
+{
+ GstStateChangeReturn ret;
+ GstPipeWireSrc *this = GST_PIPEWIRE_SRC_CAST (element);
+
+ switch (transition) {
+ case GST_STATE_CHANGE_NULL_TO_READY:
+ if (!gst_pipewire_src_open (this))
+ goto open_failed;
+ break;
+ case GST_STATE_CHANGE_READY_TO_PAUSED:
+ break;
+ case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
+ /* uncork and start recording */
+ pw_thread_loop_lock (this->core->loop);
+ pw_stream_set_active(this->stream, true);
+ pw_thread_loop_unlock (this->core->loop);
+ break;
+ case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
+ /* stop recording ASAP by corking */
+ pw_thread_loop_lock (this->core->loop);
+ pw_stream_set_active(this->stream, false);
+ pw_thread_loop_unlock (this->core->loop);
+ break;
+ default:
+ break;
+ }
+
+ ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
+
+ switch (transition) {
+ case GST_STATE_CHANGE_READY_TO_PAUSED:
+ if (wait_started (this) == PW_STREAM_STATE_ERROR)
+ goto open_failed;
+
+ if (gst_base_src_is_live (GST_BASE_SRC (element)))
+ ret = GST_STATE_CHANGE_NO_PREROLL;
+ break;
+ case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
+ break;
+ case GST_STATE_CHANGE_PAUSED_TO_READY:
+ pw_thread_loop_lock (this->core->loop);
+ this->negotiated = FALSE;
+ pw_thread_loop_unlock (this->core->loop);
+ break;
+ case GST_STATE_CHANGE_READY_TO_NULL:
+ gst_pipewire_src_close (this);
+ break;
+ default:
+ break;
+ }
+ return ret;
+
+ /* ERRORS */
+open_failed:
+ {
+ return GST_STATE_CHANGE_FAILURE;
+ }
+}
diff --git a/src/gst/gstpipewiresrc.h b/src/gst/gstpipewiresrc.h
new file mode 100644
index 0000000..e1def85
--- /dev/null
+++ b/src/gst/gstpipewiresrc.h
@@ -0,0 +1,110 @@
+/* GStreamer
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __GST_PIPEWIRE_SRC_H__
+#define __GST_PIPEWIRE_SRC_H__
+
+#include <gst/gst.h>
+#include <gst/base/gstpushsrc.h>
+
+#include <pipewire/pipewire.h>
+#include <gst/gstpipewirepool.h>
+#include <gst/gstpipewirecore.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_PIPEWIRE_SRC \
+ (gst_pipewire_src_get_type())
+#define GST_PIPEWIRE_SRC(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_PIPEWIRE_SRC,GstPipeWireSrc))
+#define GST_PIPEWIRE_SRC_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_PIPEWIRE_SRC,GstPipeWireSrcClass))
+#define GST_IS_PIPEWIRE_SRC(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_PIPEWIRE_SRC))
+#define GST_IS_PIPEWIRE_SRC_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_PIPEWIRE_SRC))
+#define GST_PIPEWIRE_SRC_CAST(obj) \
+ ((GstPipeWireSrc *) (obj))
+
+typedef struct _GstPipeWireSrc GstPipeWireSrc;
+typedef struct _GstPipeWireSrcClass GstPipeWireSrcClass;
+
+/**
+ * GstPipeWireSrc:
+ *
+ * Opaque data structure.
+ */
+struct _GstPipeWireSrc {
+ GstPushSrc element;
+
+ /*< private >*/
+ gchar *path;
+ gchar *target_object;
+ gchar *client_name;
+ gboolean always_copy;
+ gint min_buffers;
+ gint max_buffers;
+ int fd;
+ gboolean resend_last;
+ gint keepalive_time;
+
+ GstCaps *caps;
+
+ gboolean negotiated;
+ gboolean flushing;
+ gboolean started;
+ gboolean eos;
+
+ gboolean is_live;
+ GstClockTime min_latency;
+ GstClockTime max_latency;
+
+ GstStructure *client_properties;
+ GstPipeWireCore *core;
+ struct spa_hook core_listener;
+ int last_seq;
+ int pending_seq;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ GstBuffer *last_buffer;
+ GstStructure *stream_properties;
+
+ GstPipeWirePool *pool;
+ GstClock *clock;
+ GstClockTime last_time;
+
+ enum spa_meta_videotransform_value transform_value;
+};
+
+struct _GstPipeWireSrcClass {
+ GstPushSrcClass parent_class;
+};
+
+GType gst_pipewire_src_get_type (void);
+
+G_END_DECLS
+
+#endif /* __GST_PIPEWIRE_SRC_H__ */
diff --git a/src/gst/meson.build b/src/gst/meson.build
new file mode 100644
index 0000000..fd552f6
--- /dev/null
+++ b/src/gst/meson.build
@@ -0,0 +1,33 @@
+pipewire_gst_sources = [
+ 'gstpipewire.c',
+ 'gstpipewirecore.c',
+ 'gstpipewireclock.c',
+ 'gstpipewireformat.c',
+ 'gstpipewirepool.c',
+ 'gstpipewiresink.c',
+ 'gstpipewiresrc.c',
+]
+
+if get_option('gstreamer-device-provider').allowed()
+ pipewire_gst_sources += [ 'gstpipewiredeviceprovider.c' ]
+endif
+
+pipewire_gst_headers = [
+ 'gstpipewireclock.h',
+ 'gstpipewirecore.h',
+ 'gstpipewiredeviceprovider.h',
+ 'gstpipewireformat.h',
+ 'gstpipewirepool.h',
+ 'gstpipewiresink.h',
+ 'gstpipewiresrc.h',
+]
+
+pipewire_gst = shared_library('gstpipewire',
+ pipewire_gst_sources,
+ include_directories : [ configinc ],
+ dependencies : [ spa_dep, gst_dep, pipewire_dep ],
+ install : true,
+ install_dir : '@0@/gstreamer-1.0'.format(get_option('libdir')),
+)
+
+plugins = [pipewire_gst]
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..cefc329
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,15 @@
+
+subdir('pipewire')
+subdir('daemon')
+subdir('tools')
+subdir('modules')
+if get_option('examples').allowed()
+ subdir('examples')
+endif
+if get_option('tests').allowed()
+ subdir('tests')
+endif
+
+if gst_dep.length() != 0
+ subdir('gst')
+endif
diff --git a/src/modules/flatpak-utils.h b/src/modules/flatpak-utils.h
new file mode 100644
index 0000000..8952ac4
--- /dev/null
+++ b/src/modules/flatpak-utils.h
@@ -0,0 +1,156 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef FLATPAK_UTILS_H
+#define FLATPAK_UTILS_H
+
+#include <stdio.h>
+#include <string.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#ifdef HAVE_SYS_VFS_H
+#include <sys/vfs.h>
+#endif
+
+#include "config.h"
+
+#ifdef HAVE_GLIB2
+#include <glib.h>
+#endif
+
+#include <spa/utils/result.h>
+#include <pipewire/log.h>
+
+static int pw_check_flatpak_parse_metadata(const char *buf, size_t size, char **app_id, char **devices)
+{
+#ifdef HAVE_GLIB2
+ /*
+ * See flatpak-metadata(5)
+ *
+ * The .flatpak-info file is in GLib key_file .ini format.
+ */
+ g_autoptr(GKeyFile) metadata = NULL;
+ char *s;
+
+ metadata = g_key_file_new();
+ if (!g_key_file_load_from_data(metadata, buf, size, G_KEY_FILE_NONE, NULL))
+ return -EINVAL;
+
+ if (app_id) {
+ s = g_key_file_get_value(metadata, "Application", "name", NULL);
+ *app_id = s ? strdup(s) : NULL;
+ g_free(s);
+ }
+
+ if (devices) {
+ s = g_key_file_get_value(metadata, "Context", "devices", NULL);
+ *devices = s ? strdup(s) : NULL;
+ g_free(s);
+ }
+
+ return 0;
+#else
+ return -ENOTSUP;
+#endif
+}
+
+static int pw_check_flatpak(pid_t pid, char **app_id, char **devices)
+{
+#if defined(__linux__)
+ char root_path[2048];
+ int root_fd, info_fd, res;
+ struct stat stat_buf;
+
+ if (app_id)
+ *app_id = NULL;
+ if (devices)
+ *devices = NULL;
+
+ snprintf(root_path, sizeof(root_path), "/proc/%d/root", (int)pid);
+ root_fd = openat (AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+ if (root_fd == -1) {
+ res = -errno;
+ if (res == -EACCES) {
+ struct statfs buf;
+ /* Access to the root dir isn't allowed. This can happen if the root is on a fuse
+ * filesystem, such as in a toolbox container. We will never have a fuse rootfs
+ * in the flatpak case, so in that case its safe to ignore this and
+ * continue to detect other types of apps. */
+ if (statfs(root_path, &buf) == 0 &&
+ buf.f_type == 0x65735546) /* FUSE_SUPER_MAGIC */
+ return 0;
+ }
+ /* Not able to open the root dir shouldn't happen. Probably the app died and
+ * we're failing due to /proc/$pid not existing. In that case fail instead
+ * of treating this as privileged. */
+ pw_log_info("failed to open \"%s\": %s", root_path, spa_strerror(res));
+ return res;
+ }
+ info_fd = openat (root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+ close (root_fd);
+ if (info_fd == -1) {
+ if (errno == ENOENT) {
+ pw_log_debug("no .flatpak-info, client on the host");
+ /* No file => on the host */
+ return 0;
+ }
+ res = -errno;
+ pw_log_error("error opening .flatpak-info: %m");
+ return res;
+ }
+ if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) {
+ /* Some weird fd => failure, assume sandboxed */
+ pw_log_error("error fstat .flatpak-info: %m");
+ } else if (app_id || devices) {
+ /* Parse the application ID if needed */
+ const size_t size = stat_buf.st_size;
+
+ if (size > 0) {
+ void *buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, info_fd, 0);
+ if (buf != MAP_FAILED) {
+ res = pw_check_flatpak_parse_metadata(buf, size, app_id, devices);
+ munmap(buf, size);
+ } else {
+ res = -errno;
+ }
+ } else {
+ res = -EINVAL;
+ }
+
+ if (res == -EINVAL)
+ pw_log_error("PID %d .flatpak-info file is malformed",
+ (int)pid);
+ else if (res < 0)
+ pw_log_error("PID %d .flatpak-info parsing failed: %s",
+ (int)pid, spa_strerror(res));
+ }
+ close(info_fd);
+ return 1;
+#else
+ return 0;
+#endif
+}
+
+#endif /* FLATPAK_UTILS_H */
diff --git a/src/modules/meson.build b/src/modules/meson.build
new file mode 100644
index 0000000..89e4233
--- /dev/null
+++ b/src/modules/meson.build
@@ -0,0 +1,609 @@
+subdir('spa')
+
+# The list of "main" source files for modules, the ones that have the
+# doxygen documentation
+module_sources = [
+ 'module-access.c',
+ 'module-adapter.c',
+ 'module-avb.c',
+ 'module-client-device.c',
+ 'module-client-node.c',
+ 'module-combine-stream.c',
+ 'module-echo-cancel.c',
+ 'module-example-sink.c',
+ 'module-example-source.c',
+ 'module-fallback-sink.c',
+ 'module-filter-chain.c',
+ 'module-link-factory.c',
+ 'module-loopback.c',
+ 'module-metadata.c',
+ 'module-pipe-tunnel.c',
+ 'module-portal.c',
+ 'module-profiler.c',
+ 'module-protocol-native.c',
+ 'module-protocol-pulse.c',
+ 'module-protocol-simple.c',
+ 'module-pulse-tunnel.c',
+ 'module-rt.c',
+ 'module-raop-discover.c',
+ 'module-raop-sink.c',
+ 'module-rtp-source.c',
+ 'module-rtp-sink.c',
+ 'module-session-manager.c',
+ 'module-zeroconf-discover.c',
+ 'module-roc-source.c',
+ 'module-roc-sink.c',
+ 'module-x11-bell.c',
+]
+
+pipewire_module_access_deps = [spa_dep, mathlib, dl_lib, pipewire_dep]
+if flatpak_support
+ pipewire_module_access_deps += glib2_dep
+endif
+
+pipewire_module_access = shared_library('pipewire-module-access', [ 'module-access.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : pipewire_module_access_deps
+)
+
+pipewire_module_loopback = shared_library('pipewire-module-loopback',
+ [ 'module-loopback.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+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, audioconvert_dep
+]
+
+if lilv_lib.found()
+ filter_chain_sources += [
+ 'module-filter-chain/lv2_plugin.c'
+ ]
+ filter_chain_dependencies += [ lilv_lib ]
+endif
+
+
+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,
+)
+
+pipewire_module_echo_cancel_sources = [
+ 'module-echo-cancel.c',
+]
+
+pipewire_module_combine_stream = shared_library('pipewire-module-combine-stream',
+ [ 'module-combine-stream.c' ],
+ include_directories : [configinc],
+ install : false,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, dl_lib, pipewire_dep],
+)
+
+pipewire_module_echo_cancel = shared_library('pipewire-module-echo-cancel',
+ pipewire_module_echo_cancel_sources,
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_profiler = shared_library('pipewire-module-profiler',
+ [ 'module-profiler.c',
+ 'module-profiler/protocol-native.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_rt = shared_library('pipewire-module-rt', [ 'module-rt.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [dbus_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+build_module_rtkit = dbus_dep.found() and (get_option('legacy-rtkit') == true)
+if build_module_rtkit
+# TODO: This serves as a temporary alias to prevent breaking existing setups
+# while `module-rtkit` is being migrated to `module-rt`
+pipewire_module_rtkit = shared_library('pipewire-module-rtkit', [ 'module-rt.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [dbus_dep, mathlib, dl_lib, pipewire_dep],
+)
+endif
+summary({'rt': '@0@ RTKit'.format(build_module_rtkit ? 'with' : 'without')}, section: 'Optional Modules')
+
+build_module_portal = dbus_dep.found()
+if build_module_portal
+pipewire_module_portal = shared_library('pipewire-module-portal', [ 'module-portal.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [dbus_dep, mathlib, dl_lib, pipewire_dep],
+)
+endif
+summary({'portal': build_module_portal}, bool_yn: true, section: 'Optional Modules')
+
+pipewire_module_client_device = shared_library('pipewire-module-client-device',
+ [ 'module-client-device.c',
+ 'module-client-device/resource-device.c',
+ 'module-client-device/proxy-device.c',
+ 'module-client-device/protocol-native.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_link_factory = shared_library('pipewire-module-link-factory',
+ [ 'module-link-factory.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_protocol_deps = [mathlib, dl_lib, pipewire_dep]
+
+if systemd_dep.found()
+ pipewire_module_protocol_deps += systemd_dep
+endif
+
+pipewire_module_protocol_native = shared_library('pipewire-module-protocol-native',
+ [ 'module-protocol-native.c',
+ 'module-protocol-native/local-socket.c',
+ 'module-protocol-native/portal-screencast.c',
+ 'module-protocol-native/protocol-native.c',
+ 'module-protocol-native/v0/protocol-native.c',
+ 'module-protocol-native/protocol-footer.c',
+ 'module-protocol-native/connection.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : pipewire_module_protocol_deps,
+)
+
+pipewire_module_protocol_pulse_deps = pipewire_module_protocol_deps
+
+pipewire_module_protocol_pulse_sources = [
+ 'module-protocol-pulse.c',
+ 'module-protocol-pulse/client.c',
+ 'module-protocol-pulse/collect.c',
+ 'module-protocol-pulse/cmd.c',
+ 'module-protocol-pulse/extension.c',
+ 'module-protocol-pulse/extensions/ext-device-manager.c',
+ 'module-protocol-pulse/extensions/ext-device-restore.c',
+ 'module-protocol-pulse/extensions/ext-stream-restore.c',
+ 'module-protocol-pulse/format.c',
+ 'module-protocol-pulse/manager.c',
+ 'module-protocol-pulse/message.c',
+ 'module-protocol-pulse/message-handler.c',
+ 'module-protocol-pulse/module.c',
+ 'module-protocol-pulse/operation.c',
+ 'module-protocol-pulse/pending-sample.c',
+ 'module-protocol-pulse/pulse-server.c',
+ 'module-protocol-pulse/quirks.c',
+ 'module-protocol-pulse/remap.c',
+ 'module-protocol-pulse/reply.c',
+ 'module-protocol-pulse/sample.c',
+ 'module-protocol-pulse/sample-play.c',
+ 'module-protocol-pulse/server.c',
+ 'module-protocol-pulse/stream.c',
+ 'module-protocol-pulse/utils.c',
+ 'module-protocol-pulse/volume.c',
+ 'module-protocol-pulse/modules/module-always-sink.c',
+ 'module-protocol-pulse/modules/module-combine-sink.c',
+ 'module-protocol-pulse/modules/module-echo-cancel.c',
+ 'module-protocol-pulse/modules/module-ladspa-sink.c',
+ 'module-protocol-pulse/modules/module-ladspa-source.c',
+ 'module-protocol-pulse/modules/module-loopback.c',
+ 'module-protocol-pulse/modules/module-native-protocol-tcp.c',
+ 'module-protocol-pulse/modules/module-null-sink.c',
+ 'module-protocol-pulse/modules/module-pipe-source.c',
+ 'module-protocol-pulse/modules/module-pipe-sink.c',
+ 'module-protocol-pulse/modules/module-raop-discover.c',
+ 'module-protocol-pulse/modules/module-remap-sink.c',
+ 'module-protocol-pulse/modules/module-remap-source.c',
+ 'module-protocol-pulse/modules/module-roc-sink.c',
+ 'module-protocol-pulse/modules/module-roc-sink-input.c',
+ 'module-protocol-pulse/modules/module-roc-source.c',
+ 'module-protocol-pulse/modules/module-rtp-recv.c',
+ 'module-protocol-pulse/modules/module-rtp-send.c',
+ 'module-protocol-pulse/modules/module-simple-protocol-tcp.c',
+ 'module-protocol-pulse/modules/module-switch-on-connect.c',
+ 'module-protocol-pulse/modules/module-tunnel-sink.c',
+ 'module-protocol-pulse/modules/module-tunnel-source.c',
+ 'module-protocol-pulse/modules/module-x11-bell.c',
+ 'module-protocol-pulse/modules/module-zeroconf-discover.c',
+]
+
+if dbus_dep.found()
+ pipewire_module_protocol_pulse_sources += [
+ 'module-protocol-pulse/dbus-name.c',
+ ]
+ pipewire_module_protocol_pulse_deps += dbus_dep
+endif
+
+if avahi_dep.found()
+ pipewire_module_protocol_pulse_sources += [
+ 'module-protocol-pulse/modules/module-zeroconf-publish.c',
+ 'module-zeroconf-discover/avahi-poll.c',
+ ]
+ pipewire_module_protocol_pulse_deps += avahi_dep
+ cdata.set('HAVE_AVAHI', true)
+endif
+
+if gio_dep.found()
+ pipewire_module_protocol_pulse_sources += [
+ 'module-protocol-pulse/modules/module-gsettings.c',
+ ]
+ pipewire_module_protocol_pulse_deps += gio_dep
+ cdata.set('HAVE_GIO', true)
+endif
+
+if flatpak_support
+ pipewire_module_protocol_pulse_deps += glib2_dep
+endif
+
+pipewire_module_protocol_pulse = shared_library('pipewire-module-protocol-pulse',
+ pipewire_module_protocol_pulse_sources,
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : pipewire_module_protocol_pulse_deps,
+)
+
+build_module_pulse_tunnel = pulseaudio_dep.found()
+if build_module_pulse_tunnel
+ pipewire_module_pulse_tunnel = shared_library('pipewire-module-pulse-tunnel',
+ [ 'module-pulse-tunnel.c',
+ 'module-protocol-pulse/format.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, pipewire_dep, pulseaudio_dep],
+)
+endif
+summary({'pulse-tunnel': build_module_pulse_tunnel}, bool_yn: true, section: 'Optional Modules')
+
+pipewire_module_pipe_tunnel = shared_library('pipewire-module-pipe-tunnel',
+ [ 'module-pipe-tunnel.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_protocol_simple = shared_library('pipewire-module-protocol-simple',
+ [ 'module-protocol-simple.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : pipewire_module_protocol_deps,
+)
+
+pipewire_module_example_sink = shared_library('pipewire-module-example-sink',
+ [ 'module-example-sink.c' ],
+ include_directories : [configinc],
+ install : false,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_example_sink = shared_library('pipewire-module-example-source',
+ [ 'module-example-source.c' ],
+ include_directories : [configinc],
+ install : false,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_client_node = shared_library('pipewire-module-client-node',
+ [ 'module-client-node.c',
+ 'module-client-node/remote-node.c',
+ 'module-client-node/client-node.c',
+ 'module-client-node/protocol-native.c',
+ 'module-client-node/v0/client-node.c',
+ 'module-client-node/v0/transport.c',
+ 'module-client-node/v0/protocol-native.c',
+ 'spa/spa-node.c', ],
+ include_directories : [configinc],
+ link_with : pipewire_module_protocol_native,
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+pipewire_module_metadata = shared_library('pipewire-module-metadata',
+ [ 'module-metadata.c',
+ 'module-metadata/proxy-metadata.c',
+ 'module-metadata/metadata.c',
+ 'module-metadata/protocol-native.c'],
+ include_directories : [configinc],
+ link_with : pipewire_module_protocol_native,
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep],
+)
+
+test('pw-test-protocol-native',
+ executable('pw-test-protocol-native',
+ [ 'module-protocol-native/test-connection.c',
+ 'module-protocol-native/connection.c' ],
+ c_args : libpipewire_c_args,
+ include_directories : [configinc ],
+ dependencies : [spa_dep, pipewire_dep],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir,
+ ),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ 'PIPEWIRE_CONFIG_DIR=@0@'.format(pipewire_dep.get_variable('confdatadir')),
+ 'PIPEWIRE_MODULE_DIR=@0@'.format(pipewire_dep.get_variable('moduledir')),
+ ]
+)
+
+if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'pw-test-protocol-native')
+ configure_file(
+ input: installed_tests_template,
+ output: 'pw-test-protocol-native.test',
+ install_dir: installed_tests_metadir,
+ configuration: test_conf
+ )
+endif
+
+pipewire_module_adapter = shared_library('pipewire-module-adapter',
+ [ 'module-adapter.c',
+ 'module-adapter/adapter.c',
+ 'spa/spa-node.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+pipewire_module_session_manager = shared_library('pipewire-module-session-manager',
+ [ 'module-session-manager.c',
+ 'module-session-manager/client-endpoint/client-endpoint.c',
+ 'module-session-manager/client-endpoint/endpoint-stream.c',
+ 'module-session-manager/client-endpoint/endpoint.c',
+ 'module-session-manager/client-session/client-session.c',
+ 'module-session-manager/client-session/endpoint-link.c',
+ 'module-session-manager/client-session/session.c',
+ 'module-session-manager/endpoint-link.c',
+ 'module-session-manager/endpoint-stream.c',
+ 'module-session-manager/endpoint.c',
+ 'module-session-manager/protocol-native.c',
+ 'module-session-manager/proxy-session-manager.c',
+ 'module-session-manager/session.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_zeroconf_discover = avahi_dep.found()
+if build_module_zeroconf_discover
+pipewire_module_zeroconf_discover = shared_library('pipewire-module-zeroconf-discover',
+ [ 'module-zeroconf-discover.c',
+ 'module-protocol-pulse/format.c',
+ 'module-zeroconf-discover/avahi-poll.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep],
+)
+endif
+summary({'zeroconf-discover': build_module_zeroconf_discover}, bool_yn: true, section: 'Optional Modules')
+
+build_module_raop_discover = avahi_dep.found()
+if build_module_raop_discover
+pipewire_module_raop_discover = shared_library('pipewire-module-raop-discover',
+ [ 'module-raop-discover.c',
+ 'module-zeroconf-discover/avahi-poll.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep],
+)
+endif
+summary({'raop-discover (needs Avahi)': build_module_raop_discover}, bool_yn: true, section: 'Optional Modules')
+
+build_module_raop = openssl_lib.found()
+if build_module_raop
+pipewire_module_raop_sink = shared_library('pipewire-module-raop-sink',
+ [ 'module-raop-sink.c',
+ 'module-raop/rtsp-client.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, openssl_lib],
+)
+endif
+summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules')
+
+roc_dep = dependency('roc', required: get_option('roc'))
+summary({'ROC': roc_dep.found()}, bool_yn: true, section: 'Streaming between daemons')
+
+pipewire_module_rtp_source = shared_library('pipewire-module-rtp-source',
+ [ 'module-rtp-source.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+pipewire_module_rtp_sink = shared_library('pipewire-module-rtp-sink',
+ [ 'module-rtp-sink.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+build_module_roc = roc_dep.found()
+if build_module_roc
+pipewire_module_roc_sink = shared_library('pipewire-module-roc-sink',
+ [ 'module-roc-sink.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, roc_dep],
+)
+
+pipewire_module_roc_source = shared_library('pipewire-module-roc-source',
+ [ 'module-roc-source.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, roc_dep],
+)
+endif
+summary({'roc-sink': build_module_roc}, bool_yn: true, section: 'Optional Modules')
+summary({'roc-source': build_module_roc}, bool_yn: true, section: 'Optional Modules')
+
+build_module_x11_bell = x11_dep.found() and canberra_dep.found()
+if build_module_x11_bell
+pipewire_module_x11_bell = shared_library('pipewire-module-x11-bell',
+ [ 'module-x11-bell.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, x11_dep, xfixes_dep, canberra_dep],
+)
+endif
+summary({'x11-bell': build_module_x11_bell}, bool_yn: true, section: 'Optional Modules')
+
+pipewire_module_fallback_sink = shared_library('pipewire-module-fallback-sink',
+ [ 'module-fallback-sink.c' ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+
+build_module_avb = get_option('avb').require(host_machine.system() == 'linux', error_message: 'AVB support is only available on Linux').allowed()
+if build_module_avb
+pipewire_module_avb = shared_library('pipewire-module-avb',
+ [ 'module-avb.c',
+ 'module-avb/avb.c',
+ 'module-avb/adp.c',
+ 'module-avb/acmp.c',
+ 'module-avb/aecp.c',
+ 'module-avb/aecp-aem.c',
+ 'module-avb/avdecc.c',
+ 'module-avb/maap.c',
+ 'module-avb/mmrp.c',
+ 'module-avb/mrp.c',
+ 'module-avb/msrp.c',
+ 'module-avb/mvrp.c',
+ 'module-avb/srp.c',
+ 'module-avb/stream.c'
+ ],
+ include_directories : [configinc],
+ install : true,
+ install_dir : modules_install_dir,
+ install_rpath: modules_install_dir,
+ dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep],
+)
+endif
+summary({'avb': build_module_avb}, bool_yn: true, section: 'Optional Modules')
diff --git a/src/modules/module-access.c b/src/modules/module-access.c
new file mode 100644
index 0000000..db12ad7
--- /dev/null
+++ b/src/modules/module-access.c
@@ -0,0 +1,364 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#ifdef HAVE_SYS_VFS_H
+#include <sys/vfs.h>
+#endif
+#ifdef HAVE_SYS_MOUNT_H
+#include <sys/mount.h>
+#endif
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+
+#include "flatpak-utils.h"
+
+/** \page page_module_access PipeWire Module: Access
+ *
+ *
+ * The `access` module performs access checks on clients. The access check
+ * is only performed once per client, subsequent checks return the same
+ * resolution.
+ *
+ * Permissions assigned to a client are configured as arguments to this
+ * module, see the example configuration below. A special use-case is Flatpak
+ * where the permission management is delegated.
+ *
+ * This module sets the \ref PW_KEY_ACCESS property to one of
+ * - `allowed`: the client is explicitly allowed to access all resources
+ * - `rejected`: the client does not have access to any resources and a
+ * resource error is generated
+ * - `restricted`: the client is restricted, see note below
+ * - `flatpak`: restricted, special case for clients running inside flatpak,
+ * see note below
+ * - `$access.force`: the value of the `access.force` argument given in the
+ * module configuration.
+ * - `unrestricted`: the client is allowed to access all resources. This is the
+ * default for clients not listed in any of the `access.*` options
+ * unless the client requested reduced permissions in \ref
+ * PW_KEY_CLIENT_ACCESS.
+ *
+ * \note Clients with a resolution other than `allowed` or `rejected` rely
+ * on an external actor to update that property once permission is
+ * granted or rejected.
+ *
+ * For connections from applications running inside Flatpak not mediated
+ * by a portal, the `access` module itself sets the `pipewire.access.portal.app_id`
+ * property to the Flatpak application ID.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - ``access.allowed = []``: an array of paths of allowed applications
+ * - ``access.rejected = []``: an array of paths of rejected applications
+ * - ``access.restricted = []``: an array of paths of restricted applications
+ * - ``access.force = <str>``: forces an external permissions check (e.g. a flatpak
+ * portal)
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_ACCESS
+ * - \ref PW_KEY_CLIENT_ACCESS
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-access
+ * args = {
+ * access.allowed = [
+ * /usr/bin/pipewire-media-session
+ * /usr/bin/important-thing
+ * ]
+ *
+ * access.rejected = [
+ * /usr/bin/microphone-snooper
+ * ]
+ *
+ * #access.restricted = [ ]
+ *
+ * # Anything not in the above lists gets assigned the
+ * # access.force permission.
+ * #access.force = flatpak
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ * \see pw_resource_error
+ * \see pw_impl_client_update_permissions
+ */
+
+#define NAME "access"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "[ access.force=flatpak ] " \
+ "[ access.allowed=<cmd-line> ] " \
+ "[ access.rejected=<cmd-line> ] " \
+ "[ access.restricted=<cmd-line> ] " \
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Perform access check" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+ struct pw_properties *properties;
+
+ struct spa_hook context_listener;
+ struct spa_hook module_listener;
+};
+
+static int check_cmdline(struct pw_impl_client *client, int pid, const char *str)
+{
+ char path[2048], key[1024];
+ ssize_t len;
+ int fd, res;
+ struct spa_json it[2];
+
+ sprintf(path, "/proc/%u/cmdline", pid);
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ res = -errno;
+ goto exit;
+ }
+ if ((len = read(fd, path, sizeof(path)-1)) < 0) {
+ res = -errno;
+ goto exit_close;
+ }
+ path[len] = '\0';
+
+ spa_json_init(&it[0], str, strlen(str));
+ if ((res = spa_json_enter_array(&it[0], &it[1])) <= 0)
+ goto exit_close;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_streq(path, key)) {
+ res = 1;
+ goto exit_close;
+ }
+ }
+ res = 0;
+exit_close:
+ close(fd);
+exit:
+ return res;
+}
+
+static void
+context_check_access(void *data, struct pw_impl_client *client)
+{
+ struct impl *impl = data;
+ struct pw_permission permissions[1];
+ struct spa_dict_item items[2];
+ const struct pw_properties *props;
+ const char *str, *access;
+ char *flatpak_app_id = NULL;
+ int nitems = 0;
+ int pid, res;
+
+ pid = -EINVAL;
+ if ((props = pw_impl_client_get_properties(client)) != NULL) {
+ if ((str = pw_properties_get(props, PW_KEY_ACCESS)) != NULL) {
+ pw_log_info("client %p: has already access: '%s'", client, str);
+ return;
+ }
+ pw_properties_fetch_int32(props, PW_KEY_SEC_PID, &pid);
+ }
+
+ if (pid < 0) {
+ pw_log_info("client %p: no trusted pid found, assuming not sandboxed", client);
+ access = "no-pid";
+ goto granted;
+ } else {
+ pw_log_info("client %p has trusted pid %d", client, pid);
+ }
+
+ if (impl->properties && (str = pw_properties_get(impl->properties, "access.allowed")) != NULL) {
+ res = check_cmdline(client, pid, str);
+ if (res < 0) {
+ pw_log_warn("%p: client %p allowed check failed: %s",
+ impl, client, spa_strerror(res));
+ } else if (res > 0) {
+ access = "allowed";
+ goto granted;
+ }
+ }
+
+ if (impl->properties && (str = pw_properties_get(impl->properties, "access.rejected")) != NULL) {
+ res = check_cmdline(client, pid, str);
+ if (res < 0) {
+ pw_log_warn("%p: client %p rejected check failed: %s",
+ impl, client, spa_strerror(res));
+ } else if (res > 0) {
+ res = -EACCES;
+ access = "rejected";
+ goto rejected;
+ }
+ }
+
+ if (impl->properties && (str = pw_properties_get(impl->properties, "access.restricted")) != NULL) {
+ res = check_cmdline(client, pid, str);
+ if (res < 0) {
+ pw_log_warn("%p: client %p restricted check failed: %s",
+ impl, client, spa_strerror(res));
+ }
+ else if (res > 0) {
+ pw_log_debug(" %p: restricted client %p added", impl, client);
+ access = "restricted";
+ goto wait_permissions;
+ }
+ }
+ if (impl->properties &&
+ (access = pw_properties_get(impl->properties, "access.force")) != NULL)
+ goto wait_permissions;
+
+ res = pw_check_flatpak(pid, &flatpak_app_id, NULL);
+ if (res != 0) {
+ if (res < 0) {
+ if (res == -EACCES) {
+ access = "unrestricted";
+ goto granted;
+ }
+ pw_log_warn("%p: client %p sandbox check failed: %s",
+ impl, client, spa_strerror(res));
+ }
+ else if (res > 0) {
+ pw_log_debug(" %p: flatpak client %p added", impl, client);
+ }
+ access = "flatpak";
+ items[nitems++] = SPA_DICT_ITEM_INIT("pipewire.access.portal.app_id",
+ flatpak_app_id);
+ goto wait_permissions;
+ }
+
+ if ((access = pw_properties_get(props, PW_KEY_CLIENT_ACCESS)) == NULL)
+ access = "unrestricted";
+
+ if (spa_streq(access, "unrestricted") || spa_streq(access, "allowed"))
+ goto granted;
+ else
+ goto wait_permissions;
+
+granted:
+ pw_log_info("%p: client %p '%s' access granted", impl, client, access);
+ items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+ pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
+
+ permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_ALL);
+ pw_impl_client_update_permissions(client, 1, permissions);
+ goto done;
+
+wait_permissions:
+ pw_log_info("%p: client %p wait for '%s' permissions",
+ impl, client, access);
+ items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+ pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
+ goto done;
+
+rejected:
+ pw_resource_error(pw_impl_client_get_core_resource(client), res, access);
+ items[nitems++] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
+ pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, nitems));
+ goto done;
+
+done:
+ free(flatpak_app_id);
+ return;
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .check_access = context_check_access,
+};
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ spa_hook_remove(&impl->context_listener);
+ spa_hook_remove(&impl->module_listener);
+
+ pw_properties_free(impl->properties);
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = NULL;
+
+ impl->context = context;
+ impl->properties = props;
+
+ pw_context_add_listener(context, &impl->context_listener, &context_events, impl);
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+}
diff --git a/src/modules/module-adapter.c b/src/modules/module-adapter.c
new file mode 100644
index 0000000..edc183b
--- /dev/null
+++ b/src/modules/module-adapter.c
@@ -0,0 +1,394 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/node/node.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include "modules/spa/spa-node.h"
+#include "module-adapter/adapter.h"
+
+/** \page page_module_adapter PipeWire Module: Adapter
+ */
+#define NAME "adapter"
+
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FACTORY_USAGE SPA_KEY_FACTORY_NAME"=<factory-name> " \
+ "["SPA_KEY_LIBRARY_NAME"=<library-name>] " \
+ ADAPTER_USAGE
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Manage adapter nodes" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct factory_data {
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct spa_list node_list;
+
+ struct pw_context *context;
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+};
+
+struct node_data {
+ struct factory_data *data;
+ struct spa_list link;
+ struct pw_impl_node *adapter;
+ struct pw_impl_node *follower;
+ struct spa_handle *handle;
+ struct spa_hook adapter_listener;
+ struct pw_resource *resource;
+ struct pw_resource *bound_resource;
+ struct spa_hook resource_listener;
+ uint32_t new_id;
+ unsigned int linger;
+};
+
+static void resource_destroy(void *data)
+{
+ struct node_data *nd = data;
+
+ pw_log_debug("%p: destroy %p", nd, nd->adapter);
+ spa_hook_remove(&nd->resource_listener);
+ nd->bound_resource = NULL;
+ if (nd->adapter && !nd->linger)
+ pw_impl_node_destroy(nd->adapter);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy
+};
+
+static void node_destroy(void *data)
+{
+ struct node_data *nd = data;
+ pw_log_debug("%p: destroy %p", nd, nd->adapter);
+ spa_list_remove(&nd->link);
+ nd->adapter = NULL;
+}
+
+static void node_free(void *data)
+{
+ struct node_data *nd = data;
+
+ pw_log_debug("%p: free %p", nd, nd->follower);
+
+ if (nd->bound_resource != NULL)
+ spa_hook_remove(&nd->resource_listener);
+
+ spa_hook_remove(&nd->adapter_listener);
+
+ if (nd->follower)
+ pw_impl_node_destroy(nd->follower);
+ if (nd->handle)
+ pw_unload_spa_handle(nd->handle);
+}
+
+static void node_initialized(void *data)
+{
+ struct node_data *nd = data;
+ struct pw_impl_client *client;
+ struct pw_resource *bound_resource;
+ struct pw_global *global;
+ int res;
+
+ if (nd->resource == NULL)
+ return;
+
+ client = pw_resource_get_client(nd->resource);
+ global = pw_impl_node_get_global(nd->adapter);
+
+ res = pw_global_bind(global, client,
+ PW_PERM_ALL, PW_VERSION_NODE, nd->new_id);
+ if (res < 0)
+ goto error_bind;
+
+ if ((bound_resource = pw_impl_client_find_resource(client, nd->new_id)) == NULL) {
+ res = -EIO;
+ goto error_bind;
+ }
+
+ nd->bound_resource = bound_resource;
+ pw_resource_add_listener(bound_resource, &nd->resource_listener, &resource_events, nd);
+ return;
+
+error_bind:
+ pw_resource_errorf_id(nd->resource, nd->new_id, res,
+ "can't bind adapter node: %s", spa_strerror(res));
+ return;
+}
+
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .destroy = node_destroy,
+ .free = node_free,
+ .initialized = node_initialized,
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = _data;
+ struct pw_impl_client *client;
+ struct pw_impl_node *adapter, *follower;
+ struct spa_node *spa_follower;
+ const char *str;
+ int res;
+ struct node_data *nd;
+ bool linger, do_register;
+ struct spa_handle *handle = NULL;
+ const struct pw_properties *p;
+
+ if (properties == NULL)
+ goto error_properties;
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ linger = pw_properties_get_bool(properties, PW_KEY_OBJECT_LINGER, false);
+ do_register = pw_properties_get_bool(properties, PW_KEY_OBJECT_REGISTER, true);
+
+ p = pw_context_get_properties(d->context);
+ pw_properties_set(properties, "clock.quantum-limit",
+ pw_properties_get(p, "default.clock.quantum-limit"));
+
+ client = resource ? pw_resource_get_client(resource): NULL;
+ if (client && !linger) {
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ }
+
+ follower = NULL;
+ spa_follower = NULL;
+ str = pw_properties_get(properties, "adapt.follower.node");
+ if (str != NULL) {
+ if (sscanf(str, "pointer:%p", &follower) != 1)
+ goto error_properties;
+ spa_follower = pw_impl_node_get_implementation(follower);
+ }
+ str = pw_properties_get(properties, "adapt.follower.spa-node");
+ if (str != NULL) {
+ if (sscanf(str, "pointer:%p", &spa_follower) != 1)
+ goto error_properties;
+ }
+ if (spa_follower == NULL) {
+ void *iface;
+ const char *factory_name;
+
+ factory_name = pw_properties_get(properties, SPA_KEY_FACTORY_NAME);
+ if (factory_name == NULL)
+ goto error_properties;
+
+ handle = pw_context_load_spa_handle(d->context,
+ factory_name,
+ properties ? &properties->dict : NULL);
+ if (handle == NULL)
+ goto error_errno;
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0)
+ goto error_res;
+
+ spa_follower = iface;
+ }
+ if (spa_follower == NULL) {
+ res = -EINVAL;
+ goto error_res;
+ }
+
+ adapter = pw_adapter_new(pw_impl_module_get_context(d->module),
+ spa_follower,
+ properties,
+ sizeof(struct node_data));
+ properties = NULL;
+
+ if (adapter == NULL) {
+ if (errno == ENOMEM || errno == EBUSY)
+ goto error_errno;
+ else
+ goto error_usage;
+ }
+
+ nd = pw_adapter_get_user_data(adapter);
+ nd->data = d;
+ nd->adapter = adapter;
+ nd->follower = follower;
+ nd->handle = handle;
+ nd->resource = resource;
+ nd->new_id = new_id;
+ nd->linger = linger;
+ spa_list_append(&d->node_list, &nd->link);
+
+ pw_impl_node_add_listener(adapter, &nd->adapter_listener, &node_events, nd);
+
+ if (do_register)
+ pw_impl_node_register(adapter, NULL);
+ else
+ pw_impl_node_initialized(adapter);
+
+ return adapter;
+
+error_properties:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, "usage: " FACTORY_USAGE);
+ goto error_cleanup;
+error_errno:
+ res = -errno;
+error_res:
+ pw_resource_errorf_id(resource, new_id, res, "can't create node: %s", spa_strerror(res));
+ goto error_cleanup;
+error_usage:
+ res = -EINVAL;
+ pw_log_error("usage: "ADAPTER_USAGE);
+ pw_resource_errorf_id(resource, new_id, res, "usage: "ADAPTER_USAGE);
+ goto error_cleanup;
+error_cleanup:
+ pw_properties_free(properties);
+ if (handle)
+ pw_unload_spa_handle(handle);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ struct node_data *nd;
+
+ spa_hook_remove(&d->factory_listener);
+
+ spa_list_consume(nd, &d->node_list, link)
+ pw_impl_node_destroy(nd->adapter);
+
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ pw_log_debug("%p: destroy", d);
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "adapter",
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ pw_properties_new(
+ PW_KEY_FACTORY_USAGE, FACTORY_USAGE,
+ NULL),
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->context = context;
+ data->module = module;
+ spa_list_init(&data->node_list);
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener,
+ &factory_events, data);
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/module-adapter/adapter.c b/src/modules/module-adapter/adapter.c
new file mode 100644
index 0000000..19c5713
--- /dev/null
+++ b/src/modules/module-adapter/adapter.c
@@ -0,0 +1,548 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <math.h>
+#include <time.h>
+
+#include "config.h"
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type-info.h>
+#include <spa/param/format.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/format-utils.h>
+#include <spa/debug/types.h>
+#include <spa/utils/json-pod.h>
+
+#include "pipewire/pipewire.h"
+
+#include "modules/spa/spa-node.h"
+
+#define NAME "adapter"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct buffer {
+ struct spa_buffer buf;
+ struct spa_data datas[1];
+ struct spa_chunk chunk[1];
+};
+
+struct node {
+ struct pw_context *context;
+
+ struct pw_impl_node *node;
+ struct spa_hook node_listener;
+
+ struct spa_node *follower;
+
+ void *user_data;
+ enum pw_direction direction;
+ struct pw_properties *props;
+
+ uint32_t media_type;
+ uint32_t media_subtype;
+
+ struct spa_list ports;
+};
+
+/** \endcond */
+static void node_free(void *data)
+{
+ struct node *n = data;
+ spa_hook_remove(&n->node_listener);
+ pw_properties_free(n->props);
+}
+
+static void node_port_init(void *data, struct pw_impl_port *port)
+{
+ struct node *n = data;
+ const struct pw_properties *old;
+ enum pw_direction direction;
+ struct pw_properties *new;
+ const char *str, *path, *desc, *nick, *name, *node_name, *media_class, *prop_port_names, *override_device_prefix;
+ char position[8], *prefix;
+ bool is_monitor, is_device, is_duplex, is_virtual, is_control = false, no_device_port_prefix;
+
+ direction = pw_impl_port_get_direction(port);
+
+ old = pw_impl_port_get_properties(port);
+
+ is_monitor = pw_properties_get_bool(old, PW_KEY_PORT_MONITOR, false);
+ if (!is_monitor && direction != n->direction)
+ return;
+
+ if ((str = pw_properties_get(old, PW_KEY_FORMAT_DSP)) != NULL)
+ is_control = spa_streq(str, "8 bit raw midi");
+
+ path = pw_properties_get(n->props, PW_KEY_OBJECT_PATH);
+ media_class = pw_properties_get(n->props, PW_KEY_MEDIA_CLASS);
+
+ if (media_class != NULL &&
+ (strstr(media_class, "Sink") != NULL ||
+ strstr(media_class, "Source") != NULL))
+ is_device = true;
+ else
+ is_device = false;
+
+ is_duplex = media_class != NULL && strstr(media_class, "Duplex") != NULL;
+ is_virtual = media_class != NULL && strstr(media_class, "Virtual") != NULL;
+
+ new = pw_properties_new(NULL, NULL);
+
+ override_device_prefix = pw_properties_get(n->props, PW_KEY_NODE_DEVICE_PORT_NAME_PREFIX);
+
+ if (is_control)
+ prefix = direction == PW_DIRECTION_INPUT ?
+ "control" : "notify";
+ else if (is_duplex)
+ prefix = direction == PW_DIRECTION_INPUT ?
+ "playback" : "capture";
+ else if (is_virtual)
+ prefix = direction == PW_DIRECTION_INPUT ?
+ "input" : "capture";
+ else if (is_device)
+ prefix = direction == PW_DIRECTION_INPUT ?
+ override_device_prefix != NULL ?
+ strdup(override_device_prefix) : "playback"
+ : is_monitor ? "monitor" : override_device_prefix != NULL ?
+ strdup(override_device_prefix) : "capture";
+ else
+ prefix = direction == PW_DIRECTION_INPUT ?
+ "input" : is_monitor ? "monitor" : "output";
+
+ if ((str = pw_properties_get(old, PW_KEY_AUDIO_CHANNEL)) == NULL ||
+ spa_streq(str, "UNK")) {
+ snprintf(position, sizeof(position), "%d", pw_impl_port_get_id(port) + 1);
+ str = position;
+ }
+ if (direction == n->direction) {
+ if (is_device) {
+ pw_properties_set(new, PW_KEY_PORT_PHYSICAL, "true");
+ pw_properties_set(new, PW_KEY_PORT_TERMINAL, "true");
+ }
+ }
+
+ desc = pw_properties_get(n->props, PW_KEY_NODE_DESCRIPTION);
+ nick = pw_properties_get(n->props, PW_KEY_NODE_NICK);
+ name = pw_properties_get(n->props, PW_KEY_NODE_NAME);
+
+ if ((node_name = desc) == NULL && (node_name = nick) == NULL &&
+ (node_name = name) == NULL)
+ node_name = "node";
+
+ pw_properties_setf(new, PW_KEY_OBJECT_PATH, "%s:%s_%d",
+ path ? path : node_name, prefix, pw_impl_port_get_id(port));
+
+ no_device_port_prefix = is_device && !is_monitor
+ && override_device_prefix != NULL && strlen(override_device_prefix) == 0;
+
+ if (is_control)
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s", prefix);
+ else if (no_device_port_prefix)
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s", str);
+ else
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s_%s", prefix, str);
+
+ if ((node_name = nick) == NULL && (node_name = desc) == NULL &&
+ (node_name = name) == NULL)
+ node_name = "node";
+
+ if (is_control)
+ pw_properties_setf(new, PW_KEY_PORT_ALIAS, "%s:%s",
+ node_name, prefix);
+ else
+ pw_properties_setf(new, PW_KEY_PORT_ALIAS, "%s:%s_%s",
+ node_name, prefix, str);
+
+ prop_port_names = pw_properties_get(n->props, PW_KEY_NODE_CHANNELNAMES);
+ if (prop_port_names) {
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], prop_port_names, strlen(prop_port_names));
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], prop_port_names, strlen(prop_port_names));
+
+ uint32_t i;
+ for (i = 0; i < pw_impl_port_get_id(port) + 1; i++)
+ if (spa_json_get_string(&it[1], v, sizeof(v)) <= 0)
+ break;
+
+ if (i == pw_impl_port_get_id(port) + 1 && strlen(v) > 0) {
+ if (no_device_port_prefix) {
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s", v);
+ } else {
+ pw_properties_setf(new, PW_KEY_PORT_NAME, "%s_%s", prefix, v);
+ }
+ }
+ }
+
+ pw_impl_port_update_properties(port, &new->dict);
+ pw_properties_free(new);
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .free = node_free,
+ .port_init = node_port_init,
+};
+
+static int handle_node_param(struct pw_impl_node *node, const char *key, const char *value)
+{
+ const struct spa_type_info *ti;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct spa_pod *pod;
+ int res;
+
+ ti = spa_debug_type_find_short(spa_type_param, key);
+ if (ti == NULL)
+ return -ENOENT;
+
+ if ((res = spa_json_to_pod(&b, 0, ti, value, strlen(value))) < 0)
+ return res;
+
+ if ((pod = spa_pod_builder_deref(&b, 0)) == NULL)
+ return -ENOSPC;
+
+ if ((res = pw_impl_node_set_param(node, ti->type, 0, pod)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int find_format(struct spa_node *node, enum pw_direction direction,
+ uint32_t *media_type, uint32_t *media_subtype)
+{
+ uint32_t state = 0;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ int res;
+ struct spa_pod *format;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if ((res = spa_node_port_enum_params_sync(node,
+ direction == PW_DIRECTION_INPUT ?
+ SPA_DIRECTION_INPUT :
+ SPA_DIRECTION_OUTPUT, 0,
+ SPA_PARAM_EnumFormat, &state,
+ NULL, &format, &b)) != 1) {
+ res = res < 0 ? res : -ENOENT;
+ pw_log_warn("%p: can't get format: %s", node, spa_strerror(res));
+ return res;
+ }
+
+ if ((res = spa_format_parse(format, media_type, media_subtype)) < 0)
+ return res;
+
+ pw_log_debug("%p: %s/%s", node,
+ spa_debug_type_find_name(spa_type_media_type, *media_type),
+ spa_debug_type_find_name(spa_type_media_subtype, *media_subtype));
+ return 0;
+}
+
+static int do_auto_port_config(struct node *n, const char *str)
+{
+ uint32_t state = 0, i;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+#define POSITION_PRESERVE 0
+#define POSITION_AUX 1
+#define POSITION_UNKNOWN 2
+ int 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];
+ char key[1024], val[256];
+
+ spa_json_init(&it[0], str, strlen(str));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 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;
+
+ if (spa_streq(key, "mode")) {
+ mode = spa_debug_type_find_type_short(spa_type_param_port_config_mode, val);
+ if (mode == SPA_ID_INVALID)
+ mode = SPA_PARAM_PORT_CONFIG_MODE_none;
+ } else if (spa_streq(key, "monitor")) {
+ monitor = spa_atob(val);
+ } else if (spa_streq(key, "control")) {
+ control = spa_atob(val);
+ } else if (spa_streq(key, "position")) {
+ if (spa_streq(val, "unknown"))
+ position = POSITION_UNKNOWN;
+ else if (spa_streq(val, "aux"))
+ position = POSITION_AUX;
+ else
+ position = POSITION_PRESERVE;
+ }
+ }
+
+ 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(n->follower,
+ n->direction == PW_DIRECTION_INPUT ?
+ SPA_DIRECTION_INPUT :
+ SPA_DIRECTION_OUTPUT, 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)
+ 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)
+ continue;
+
+ format = info;
+ have_format = true;
+ }
+ 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;
+ }
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &format.info.raw);
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(n->direction),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode),
+ SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(monitor),
+ SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(control),
+ SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param));
+ pw_impl_node_set_param(n->node, SPA_PARAM_PortConfig, 0, param);
+
+ return 0;
+}
+
+struct info_data {
+ struct spa_hook listener;
+ struct spa_node *node;
+ struct pw_properties *props;
+ uint32_t n_input_ports;
+ uint32_t max_input_ports;
+ uint32_t n_output_ports;
+ uint32_t max_output_ports;
+};
+
+static void info_event(void *data, const struct spa_node_info *info)
+{
+ struct info_data *d = data;
+
+ pw_properties_update(d->props, info->props);
+
+ d->max_input_ports = info->max_input_ports;
+ d->max_output_ports = info->max_output_ports;
+}
+
+static void port_info_event(void *data, enum spa_direction direction, uint32_t port,
+ const struct spa_port_info *info)
+{
+ struct info_data *d = data;
+
+ if (direction == SPA_DIRECTION_OUTPUT)
+ d->n_output_ports++;
+ else if (direction == SPA_DIRECTION_INPUT)
+ d->n_input_ports++;
+}
+
+static const struct spa_node_events node_info_events = {
+ .version = SPA_VERSION_NODE_EVENTS,
+ .info = info_event,
+ .port_info = port_info_event,
+};
+
+struct pw_impl_node *pw_adapter_new(struct pw_context *context,
+ struct spa_node *follower,
+ struct pw_properties *props,
+ size_t user_data_size)
+{
+ struct pw_impl_node *node;
+ struct node *n;
+ const char *str, *factory_name;
+ enum pw_direction direction;
+ int res;
+ uint32_t media_type, media_subtype;
+ const struct spa_dict_item *it;
+ struct pw_properties *copy;
+ struct info_data info;
+
+ spa_zero(info);
+ info.node = follower;
+ info.props = props;
+
+ res = spa_node_add_listener(info.node, &info.listener, &node_info_events, &info);
+ if (res < 0)
+ goto error;
+
+ spa_hook_remove(&info.listener);
+
+ pw_log_debug("%p: in %d/%d out %d/%d", info.node,
+ info.n_input_ports, info.max_input_ports,
+ info.n_output_ports, info.max_output_ports);
+
+ if (info.n_output_ports > 0) {
+ direction = PW_DIRECTION_OUTPUT;
+ } else if (info.n_input_ports > 0) {
+ direction = PW_DIRECTION_INPUT;
+ } else {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_ID)) != NULL)
+ pw_properties_set(props, PW_KEY_NODE_SESSION, str);
+
+ if (pw_properties_get(props, "factory.mode") == NULL) {
+ if (direction == PW_DIRECTION_INPUT)
+ str = "merge";
+ else
+ str = "split";
+ pw_properties_set(props, "factory.mode", str);
+ }
+
+ if ((res = find_format(follower, direction, &media_type, &media_subtype)) < 0)
+ goto error;
+
+ if (media_type == SPA_MEDIA_TYPE_audio) {
+ pw_properties_setf(props, "audio.adapt.follower", "pointer:%p", follower);
+ pw_properties_set(props, SPA_KEY_LIBRARY_NAME, "audioconvert/libspa-audioconvert");
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_setf(props, PW_KEY_MEDIA_CLASS, "Audio/%s",
+ direction == PW_DIRECTION_INPUT ? "Sink" : "Source");
+ factory_name = SPA_NAME_AUDIO_ADAPT;
+ }
+ else if (media_type == SPA_MEDIA_TYPE_video) {
+ pw_properties_setf(props, "video.adapt.follower", "pointer:%p", follower);
+ pw_properties_set(props, SPA_KEY_LIBRARY_NAME, "videoconvert/libspa-videoconvert");
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_setf(props, PW_KEY_MEDIA_CLASS, "Video/%s",
+ direction == PW_DIRECTION_INPUT ? "Sink" : "Source");
+ factory_name = SPA_NAME_VIDEO_ADAPT;
+ } else {
+ res = -ENOTSUP;
+ goto error;
+ }
+
+ copy = pw_properties_new(NULL, NULL);
+ spa_dict_for_each(it, &props->dict) {
+ if (!spa_strstartswith(it->key, "node.param.") &&
+ !spa_strstartswith(it->key, "port.param."))
+ pw_properties_set(copy, it->key, it->value);
+ }
+ node = pw_spa_node_load(context,
+ factory_name,
+ PW_SPA_NODE_FLAG_ACTIVATE | PW_SPA_NODE_FLAG_NO_REGISTER,
+ copy, sizeof(struct node) + user_data_size);
+ if (node == NULL) {
+ res = -errno;
+ pw_log_error("can't load spa node: %m");
+ goto error;
+ }
+
+ n = pw_spa_node_get_user_data(node);
+ n->context = context;
+ n->node = node;
+ n->follower = follower;
+ n->direction = direction;
+ n->props = props;
+ n->media_type = media_type;
+ n->media_subtype = media_subtype;
+ spa_list_init(&n->ports);
+
+ if (user_data_size > 0)
+ n->user_data = SPA_PTROFF(n, sizeof(struct node), void);
+
+ pw_impl_node_add_listener(node, &n->node_listener, &node_events, n);
+
+ if ((str = pw_properties_get(props, "adapter.auto-port-config")) != NULL)
+ do_auto_port_config(n, str);
+
+ spa_dict_for_each(it, &props->dict) {
+ if (spa_strstartswith(it->key, "node.param.")) {
+ if ((res = handle_node_param(node, &it->key[11], it->value)) < 0)
+ pw_log_warn("can't set node param: %s", spa_strerror(res));
+ }
+ }
+ return node;
+
+error:
+ pw_properties_free(props);
+ errno = -res;
+ return NULL;
+}
+
+void *pw_adapter_get_user_data(struct pw_impl_node *node)
+{
+ struct node *n = pw_spa_node_get_user_data(node);
+ return n->user_data;
+}
diff --git a/src/modules/module-adapter/adapter.h b/src/modules/module-adapter/adapter.h
new file mode 100644
index 0000000..80fa9c7
--- /dev/null
+++ b/src/modules/module-adapter/adapter.h
@@ -0,0 +1,48 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_ADAPTER_H
+#define PIPEWIRE_ADAPTER_H
+
+#include <pipewire/impl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define ADAPTER_USAGE PW_KEY_NODE_NAME"=<string> "
+
+struct pw_impl_node *
+pw_adapter_new(struct pw_context *context,
+ struct spa_node *follower,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+void *pw_adapter_get_user_data(struct pw_impl_node *node);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_ADAPTER_H */
diff --git a/src/modules/module-avb.c b/src/modules/module-avb.c
new file mode 100644
index 0000000..2359c2d
--- /dev/null
+++ b/src/modules/module-avb.c
@@ -0,0 +1,129 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include "module-avb/avb.h"
+
+/** \page page_module_avb PipeWire Module: AVB
+ */
+
+#define NAME "avb"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE " "
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Manage an AVB network" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_avb *avb;
+};
+
+static void impl_free(struct impl *impl)
+{
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL)
+ goto error_errno;
+
+ impl->module = module;
+ impl->context = context;
+
+ impl->avb = pw_avb_new(context, props, 0);
+ if (impl->avb == NULL)
+ goto error_errno;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ if (impl)
+ impl_free(impl);
+ return res;
+}
diff --git a/src/modules/module-avb/aaf.h b/src/modules/module-avb/aaf.h
new file mode 100644
index 0000000..b444ce2
--- /dev/null
+++ b/src/modules/module-avb/aaf.h
@@ -0,0 +1,102 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AAF_H
+#define AVB_AAF_H
+
+struct avb_packet_aaf {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1;
+ unsigned version:3;
+ unsigned mr:1;
+ unsigned _r1:1;
+ unsigned gv:1;
+ unsigned tv:1;
+
+ uint8_t seq_num;
+
+ unsigned _r2:7;
+ unsigned tu:1;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned tv:1;
+ unsigned gv:1;
+ unsigned _r1:1;
+ unsigned mr:1;
+ unsigned version:3;
+ unsigned sv:1;
+
+ uint8_t seq_num;
+
+ unsigned tu:1;
+ unsigned _r2:7;
+#endif
+ uint64_t stream_id;
+ uint32_t timestamp;
+#define AVB_AAF_FORMAT_USER 0x00
+#define AVB_AAF_FORMAT_FLOAT_32BIT 0x01
+#define AVB_AAF_FORMAT_INT_32BIT 0x02
+#define AVB_AAF_FORMAT_INT_24BIT 0x03
+#define AVB_AAF_FORMAT_INT_16BIT 0x04
+#define AVB_AAF_FORMAT_AES3_32BIT 0x05
+ uint8_t format;
+
+#define AVB_AAF_PCM_NSR_USER 0x00
+#define AVB_AAF_PCM_NSR_8KHZ 0x01
+#define AVB_AAF_PCM_NSR_16KHZ 0x02
+#define AVB_AAF_PCM_NSR_32KHZ 0x03
+#define AVB_AAF_PCM_NSR_44_1KHZ 0x04
+#define AVB_AAF_PCM_NSR_48KHZ 0x05
+#define AVB_AAF_PCM_NSR_88_2KHZ 0x06
+#define AVB_AAF_PCM_NSR_96KHZ 0x07
+#define AVB_AAF_PCM_NSR_176_4KHZ 0x08
+#define AVB_AAF_PCM_NSR_192KHZ 0x09
+#define AVB_AAF_PCM_NSR_24KHZ 0x0A
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned nsr:4;
+ unsigned _r3:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned _r3:4;
+ unsigned nsr:4;
+#endif
+ uint8_t chan_per_frame;
+ uint8_t bit_depth;
+ uint16_t data_len;
+
+#define AVB_AAF_PCM_SP_NORMAL 0x00
+#define AVB_AAF_PCM_SP_SPARSE 0x01
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned _r4:3;
+ unsigned sp:1;
+ unsigned event:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned event:4;
+ unsigned sp:1;
+ unsigned _r4:3;
+#endif
+ uint8_t _r5;
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#endif /* AVB_AAF_H */
diff --git a/src/modules/module-avb/acmp.c b/src/modules/module-avb/acmp.c
new file mode 100644
index 0000000..a7a409a
--- /dev/null
+++ b/src/modules/module-avb/acmp.c
@@ -0,0 +1,476 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "acmp.h"
+#include "msrp.h"
+#include "internal.h"
+#include "stream.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct pending {
+ struct spa_list link;
+ uint64_t last_time;
+ uint64_t timeout;
+ uint16_t old_sequence_id;
+ uint16_t sequence_id;
+ uint16_t retry;
+ size_t size;
+ void *ptr;
+};
+
+struct acmp {
+ struct server *server;
+ struct spa_hook server_listener;
+
+#define PENDING_TALKER 0
+#define PENDING_LISTENER 1
+#define PENDING_CONTROLLER 2
+ struct spa_list pending[3];
+ uint16_t sequence_id[3];
+};
+
+static void *pending_new(struct acmp *acmp, uint32_t type, uint64_t now, uint32_t timeout_ms,
+ const void *m, size_t size)
+{
+ struct pending *p;
+ struct avb_ethernet_header *h;
+ struct avb_packet_acmp *pm;
+
+ p = calloc(1, sizeof(*p) + size);
+ if (p == NULL)
+ return NULL;
+ p->last_time = now;
+ p->timeout = timeout_ms * SPA_NSEC_PER_MSEC;
+ p->sequence_id = acmp->sequence_id[type]++;
+ p->size = size;
+ p->ptr = SPA_PTROFF(p, sizeof(*p), void);
+ memcpy(p->ptr, m, size);
+
+ h = p->ptr;
+ pm = SPA_PTROFF(h, sizeof(*h), void);
+ p->old_sequence_id = ntohs(pm->sequence_id);
+ pm->sequence_id = htons(p->sequence_id);
+ spa_list_append(&acmp->pending[type], &p->link);
+
+ return p->ptr;
+}
+
+static struct pending *pending_find(struct acmp *acmp, uint32_t type, uint16_t sequence_id)
+{
+ struct pending *p;
+ spa_list_for_each(p, &acmp->pending[type], link)
+ if (p->sequence_id == sequence_id)
+ return p;
+ return NULL;
+}
+
+static void pending_free(struct acmp *acmp, struct pending *p)
+{
+ spa_list_remove(&p->link);
+ free(p);
+}
+
+struct msg_info {
+ uint16_t type;
+ const char *name;
+ int (*handle) (struct acmp *acmp, uint64_t now, const void *m, int len);
+};
+
+static int reply_not_supported(struct acmp *acmp, uint8_t type, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+ memcpy(h, m, len);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, type);
+ AVB_PACKET_ACMP_SET_STATUS(reply, AVB_ACMP_STATUS_NOT_SUPPORTED);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static int retry_pending(struct acmp *acmp, uint64_t now, struct pending *p)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h = p->ptr;
+ p->retry++;
+ p->last_time = now;
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, p->ptr, p->size);
+}
+
+static int handle_connect_tx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+ const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+ int status = AVB_ACMP_STATUS_SUCCESS;
+ struct stream *stream;
+
+ if (be64toh(p->talker_guid) != server->entity_id)
+ return 0;
+
+ memcpy(buf, m, len);
+ stream = server_find_stream(server, SPA_DIRECTION_OUTPUT,
+ reply->talker_unique_id);
+ if (stream == NULL) {
+ status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
+ goto done;
+ }
+
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE);
+ reply->stream_id = htobe64(stream->id);
+
+ stream_activate(stream, now);
+
+ memcpy(reply->stream_dest_mac, stream->addr, 6);
+ reply->connection_count = htons(1);
+ reply->stream_vlan_id = htons(stream->vlan_id);
+
+done:
+ AVB_PACKET_ACMP_SET_STATUS(reply, status);
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, buf, len);
+}
+
+static int handle_connect_tx_response(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h;
+ const struct avb_packet_acmp *resp = SPA_PTROFF(m, sizeof(*h), void);
+ struct avb_packet_acmp *reply;
+ struct pending *pending;
+ uint16_t sequence_id;
+ struct stream *stream;
+ int res;
+
+ if (be64toh(resp->listener_guid) != server->entity_id)
+ return 0;
+
+ sequence_id = ntohs(resp->sequence_id);
+
+ pending = pending_find(acmp, PENDING_TALKER, sequence_id);
+ if (pending == NULL)
+ return 0;
+
+ h = pending->ptr;
+ pending->size = SPA_MIN((int)pending->size, len);
+ memcpy(h, m, pending->size);
+
+ reply = SPA_PTROFF(h, sizeof(*h), void);
+ reply->sequence_id = htons(pending->old_sequence_id);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE);
+
+ stream = server_find_stream(server, SPA_DIRECTION_INPUT,
+ ntohs(reply->listener_unique_id));
+ if (stream == NULL)
+ return 0;
+
+ stream->peer_id = be64toh(reply->stream_id);
+ memcpy(stream->addr, reply->stream_dest_mac, 6);
+ stream_activate(stream, now);
+
+ res = avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, pending->size);
+
+ pending_free(acmp, pending);
+
+ return res;
+}
+
+static int handle_disconnect_tx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_acmp *reply = SPA_PTROFF(h, sizeof(*h), void);
+ const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+ int status = AVB_ACMP_STATUS_SUCCESS;
+ struct stream *stream;
+
+ if (be64toh(p->talker_guid) != server->entity_id)
+ return 0;
+
+ memcpy(buf, m, len);
+ stream = server_find_stream(server, SPA_DIRECTION_OUTPUT,
+ reply->talker_unique_id);
+ if (stream == NULL) {
+ status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
+ goto done;
+ }
+
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE);
+
+ stream_deactivate(stream, now);
+
+done:
+ AVB_PACKET_ACMP_SET_STATUS(reply, status);
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, buf, len);
+}
+
+static int handle_disconnect_tx_response(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h;
+ struct avb_packet_acmp *reply;
+ const struct avb_packet_acmp *resp = SPA_PTROFF(m, sizeof(*h), void);
+ struct pending *pending;
+ uint16_t sequence_id;
+ struct stream *stream;
+ int res;
+
+ if (be64toh(resp->listener_guid) != server->entity_id)
+ return 0;
+
+ sequence_id = ntohs(resp->sequence_id);
+
+ pending = pending_find(acmp, PENDING_TALKER, sequence_id);
+ if (pending == NULL)
+ return 0;
+
+ h = pending->ptr;
+ pending->size = SPA_MIN((int)pending->size, len);
+ memcpy(h, m, pending->size);
+
+ reply = SPA_PTROFF(h, sizeof(*h), void);
+ reply->sequence_id = htons(pending->old_sequence_id);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE);
+
+ stream = server_find_stream(server, SPA_DIRECTION_INPUT,
+ reply->listener_unique_id);
+ if (stream == NULL)
+ return 0;
+
+ stream_deactivate(stream, now);
+
+ res = avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, pending->size);
+
+ pending_free(acmp, pending);
+
+ return res;
+}
+
+static int handle_connect_rx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h;
+ const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+ struct avb_packet_acmp *cmd;
+
+ if (be64toh(p->listener_guid) != server->entity_id)
+ return 0;
+
+ h = pending_new(acmp, PENDING_TALKER, now,
+ AVB_ACMP_TIMEOUT_CONNECT_TX_COMMAND_MS, m, len);
+ if (h == NULL)
+ return -errno;
+
+ cmd = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(cmd, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND);
+ AVB_PACKET_ACMP_SET_STATUS(cmd, AVB_ACMP_STATUS_SUCCESS);
+
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, len);
+}
+
+static int handle_ignore(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ return 0;
+}
+
+static int handle_disconnect_rx_command(struct acmp *acmp, uint64_t now, const void *m, int len)
+{
+ struct server *server = acmp->server;
+ struct avb_ethernet_header *h;
+ const struct avb_packet_acmp *p = SPA_PTROFF(m, sizeof(*h), void);
+ struct avb_packet_acmp *cmd;
+
+ if (be64toh(p->listener_guid) != server->entity_id)
+ return 0;
+
+ h = pending_new(acmp, PENDING_TALKER, now,
+ AVB_ACMP_TIMEOUT_DISCONNECT_TX_COMMAND_MS, m, len);
+ if (h == NULL)
+ return -errno;
+
+ cmd = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_ACMP_SET_MESSAGE_TYPE(cmd, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND);
+ AVB_PACKET_ACMP_SET_STATUS(cmd, AVB_ACMP_STATUS_SUCCESS);
+
+ return avb_server_send_packet(server, h->dest, AVB_TSN_ETH, h, len);
+}
+
+static const struct msg_info msg_info[] = {
+ { AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND, "connect-tx-command", handle_connect_tx_command, },
+ { AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE, "connect-tx-response", handle_connect_tx_response, },
+ { AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND, "disconnect-tx-command", handle_disconnect_tx_command, },
+ { AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE, "disconnect-tx-response", handle_disconnect_tx_response, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, "get-tx-state-command", NULL, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_RESPONSE, "get-tx-state-response", handle_ignore, },
+ { AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, "connect-rx-command", handle_connect_rx_command, },
+ { AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE, "connect-rx-response", handle_ignore, },
+ { AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND, "disconnect-rx-command", handle_disconnect_rx_command, },
+ { AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE, "disconnect-rx-response", handle_ignore, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_COMMAND, "get-rx-state-command", NULL, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_RESPONSE, "get-rx-state-response", handle_ignore, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_COMMAND, "get-tx-connection-command", NULL, },
+ { AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_RESPONSE, "get-tx-connection-response", handle_ignore, },
+};
+
+static inline const struct msg_info *find_msg_info(uint16_t type, const char *name)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(msg_info, i) {
+ if ((name == NULL && type == i->type) ||
+ (name != NULL && spa_streq(name, i->name)))
+ return i;
+ }
+ return NULL;
+}
+
+static int acmp_message(void *data, uint64_t now, const void *message, int len)
+{
+ struct acmp *acmp = data;
+ struct server *server = acmp->server;
+ const struct avb_ethernet_header *h = message;
+ const struct avb_packet_acmp *p = SPA_PTROFF(h, sizeof(*h), void);
+ const struct msg_info *info;
+ int message_type;
+
+ if (ntohs(h->type) != AVB_TSN_ETH)
+ return 0;
+ if (memcmp(h->dest, mac, 6) != 0 &&
+ memcmp(h->dest, server->mac_addr, 6) != 0)
+ return 0;
+
+ if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_ACMP)
+ return 0;
+
+ message_type = AVB_PACKET_ACMP_GET_MESSAGE_TYPE(p);
+
+ info = find_msg_info(message_type, NULL);
+ if (info == NULL)
+ return 0;
+
+ pw_log_info("got ACMP message %s", info->name);
+
+ if (info->handle == NULL)
+ return reply_not_supported(acmp, message_type | 1, message, len);
+
+ return info->handle(acmp, now, message, len);
+}
+
+static void acmp_destroy(void *data)
+{
+ struct acmp *acmp = data;
+ spa_hook_remove(&acmp->server_listener);
+ free(acmp);
+}
+
+static void check_timeout(struct acmp *acmp, uint64_t now, uint16_t type)
+{
+ struct pending *p, *t;
+
+ spa_list_for_each_safe(p, t, &acmp->pending[type], link) {
+ if (p->last_time + p->timeout > now)
+ continue;
+
+ if (p->retry == 0) {
+ pw_log_info("%p: pending timeout, retry", p);
+ retry_pending(acmp, now, p);
+ } else {
+ pw_log_info("%p: pending timeout, fail", p);
+ pending_free(acmp, p);
+ }
+ }
+}
+static void acmp_periodic(void *data, uint64_t now)
+{
+ struct acmp *acmp = data;
+ check_timeout(acmp, now, PENDING_TALKER);
+ check_timeout(acmp, now, PENDING_LISTENER);
+ check_timeout(acmp, now, PENDING_CONTROLLER);
+}
+
+static int do_help(struct acmp *acmp, const char *args, FILE *out)
+{
+ fprintf(out, "{ \"type\": \"help\","
+ "\"text\": \""
+ "/adp/help: this help \\n"
+ "\" }");
+ return 0;
+}
+
+static int acmp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+ struct acmp *acmp = data;
+ int res;
+
+ if (!spa_strstartswith(command, "/acmp/"))
+ return 0;
+
+ command += strlen("/acmp/");
+
+ if (spa_streq(command, "help"))
+ res = do_help(acmp, args, out);
+ else
+ res = -ENOTSUP;
+
+ return res;
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = acmp_destroy,
+ .message = acmp_message,
+ .periodic = acmp_periodic,
+ .command = acmp_command
+};
+
+struct avb_acmp *avb_acmp_register(struct server *server)
+{
+ struct acmp *acmp;
+
+ acmp = calloc(1, sizeof(*acmp));
+ if (acmp == NULL)
+ return NULL;
+
+ acmp->server = server;
+ spa_list_init(&acmp->pending[PENDING_TALKER]);
+ spa_list_init(&acmp->pending[PENDING_LISTENER]);
+ spa_list_init(&acmp->pending[PENDING_CONTROLLER]);
+
+ avdecc_server_add_listener(server, &acmp->server_listener, &server_events, acmp);
+
+ return (struct avb_acmp*)acmp;
+}
+
+void avb_acmp_unregister(struct avb_acmp *acmp)
+{
+ acmp_destroy(acmp);
+}
diff --git a/src/modules/module-avb/acmp.h b/src/modules/module-avb/acmp.h
new file mode 100644
index 0000000..5a41c66
--- /dev/null
+++ b/src/modules/module-avb/acmp.h
@@ -0,0 +1,99 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_ACMP_H
+#define AVB_ACMP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND 0
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE 1
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND 2
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE 3
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND 4
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_RESPONSE 5
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND 6
+#define AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_RESPONSE 7
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND 8
+#define AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_RESPONSE 9
+#define AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_COMMAND 10
+#define AVB_ACMP_MESSAGE_TYPE_GET_RX_STATE_RESPONSE 11
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_COMMAND 12
+#define AVB_ACMP_MESSAGE_TYPE_GET_TX_CONNECTION_RESPONSE 13
+
+#define AVB_ACMP_STATUS_SUCCESS 0
+#define AVB_ACMP_STATUS_LISTENER_UNKNOWN_ID 1
+#define AVB_ACMP_STATUS_TALKER_UNKNOWN_ID 2
+#define AVB_ACMP_STATUS_TALKER_DEST_MAC_FAIL 3
+#define AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX 4
+#define AVB_ACMP_STATUS_TALKER_NO_BANDWIDTH 5
+#define AVB_ACMP_STATUS_TALKER_EXCLUSIVE 6
+#define AVB_ACMP_STATUS_LISTENER_TALKER_TIMEOUT 7
+#define AVB_ACMP_STATUS_LISTENER_EXCLUSIVE 8
+#define AVB_ACMP_STATUS_STATE_UNAVAILABLE 9
+#define AVB_ACMP_STATUS_NOT_CONNECTED 10
+#define AVB_ACMP_STATUS_NO_SUCH_CONNECTION 11
+#define AVB_ACMP_STATUS_COULD_NOT_SEND_MESSAGE 12
+#define AVB_ACMP_STATUS_TALKER_MISBEHAVING 13
+#define AVB_ACMP_STATUS_LISTENER_MISBEHAVING 14
+#define AVB_ACMP_STATUS_RESERVED 15
+#define AVB_ACMP_STATUS_CONTROLLER_NOT_AUTHORIZED 16
+#define AVB_ACMP_STATUS_INCOMPATIBLE_REQUEST 17
+#define AVB_ACMP_STATUS_LISTENER_INVALID_CONNECTION 18
+#define AVB_ACMP_STATUS_NOT_SUPPORTED 31
+
+#define AVB_ACMP_TIMEOUT_CONNECT_TX_COMMAND_MS 2000
+#define AVB_ACMP_TIMEOUT_DISCONNECT_TX_COMMAND_MS 200
+#define AVB_ACMP_TIMEOUT_GET_TX_STATE_COMMAND 200
+#define AVB_ACMP_TIMEOUT_CONNECT_RX_COMMAND_MS 4500
+#define AVB_ACMP_TIMEOUT_DISCONNECT_RX_COMMAND_MS 500
+#define AVB_ACMP_TIMEOUT_GET_RX_STATE_COMMAND_MS 200
+#define AVB_ACMP_TIMEOUT_GET_TX_CONNECTION_COMMAND 200
+
+struct avb_packet_acmp {
+ struct avb_packet_header hdr;
+ uint64_t stream_id;
+ uint64_t controller_guid;
+ uint64_t talker_guid;
+ uint64_t listener_guid;
+ uint16_t talker_unique_id;
+ uint16_t listener_unique_id;
+ char stream_dest_mac[6];
+ uint16_t connection_count;
+ uint16_t sequence_id;
+ uint16_t flags;
+ uint16_t stream_vlan_id;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_ACMP_SET_MESSAGE_TYPE(p,v) AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_ACMP_SET_STATUS(p,v) AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_ACMP_GET_MESSAGE_TYPE(p) AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_ACMP_GET_STATUS(p) AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_acmp *avb_acmp_register(struct server *server);
+
+#endif /* AVB_ACMP_H */
diff --git a/src/modules/module-avb/adp.c b/src/modules/module-avb/adp.c
new file mode 100644
index 0000000..6b13c41
--- /dev/null
+++ b/src/modules/module-avb/adp.c
@@ -0,0 +1,381 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+
+#include "adp.h"
+#include "aecp-aem-descriptors.h"
+#include "internal.h"
+#include "utils.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct entity {
+ struct spa_list link;
+ uint64_t entity_id;
+ uint64_t last_time;
+ int valid_time;
+ unsigned advertise:1;
+ size_t len;
+ uint8_t buf[128];
+};
+
+struct adp {
+ struct server *server;
+ struct spa_hook server_listener;
+
+ struct spa_list entities;
+ uint32_t available_index;
+};
+
+static struct entity *find_entity_by_id(struct adp *adp, uint64_t id)
+{
+ struct entity *e;
+ spa_list_for_each(e, &adp->entities, link)
+ if (e->entity_id == id)
+ return e;
+ return NULL;
+}
+static void entity_free(struct entity *e)
+{
+ spa_list_remove(&e->link);
+ free(e);
+}
+
+static int send_departing(struct adp *adp, uint64_t now, struct entity *e)
+{
+ struct avb_ethernet_header *h = (void*)e->buf;
+ struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+
+ AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING);
+ p->available_index = htonl(adp->available_index++);
+ avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, e->buf, e->len);
+ e->last_time = now;
+ return 0;
+}
+
+static int send_advertise(struct adp *adp, uint64_t now, struct entity *e)
+{
+ struct avb_ethernet_header *h = (void*)e->buf;
+ struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+
+ AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE);
+ p->available_index = htonl(adp->available_index++);
+ avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, e->buf, e->len);
+ e->last_time = now;
+ return 0;
+}
+
+static int send_discover(struct adp *adp, uint64_t entity_id)
+{
+ uint8_t buf[128];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+ size_t len = sizeof(*h) + sizeof(*p);
+
+ spa_memzero(buf, sizeof(buf));
+ AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
+ AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
+ AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER);
+ p->entity_id = htonl(entity_id);
+ avb_server_send_packet(adp->server, mac, AVB_TSN_ETH, buf, len);
+ return 0;
+}
+
+static int adp_message(void *data, uint64_t now, const void *message, int len)
+{
+ struct adp *adp = data;
+ struct server *server = adp->server;
+ const struct avb_ethernet_header *h = message;
+ const struct avb_packet_adp *p = SPA_PTROFF(h, sizeof(*h), void);
+ struct entity *e;
+ int message_type;
+ char buf[128];
+ uint64_t entity_id;
+
+ if (ntohs(h->type) != AVB_TSN_ETH)
+ return 0;
+ if (memcmp(h->dest, mac, 6) != 0 &&
+ memcmp(h->dest, server->mac_addr, 6) != 0)
+ return 0;
+
+ if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_ADP ||
+ AVB_PACKET_GET_LENGTH(&p->hdr) < AVB_ADP_CONTROL_DATA_LENGTH)
+ return 0;
+
+ message_type = AVB_PACKET_ADP_GET_MESSAGE_TYPE(p);
+ entity_id = be64toh(p->entity_id);
+
+ e = find_entity_by_id(adp, entity_id);
+
+ switch (message_type) {
+ case AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE:
+ if (e == NULL) {
+ e = calloc(1, sizeof(*e));
+ if (e == NULL)
+ return -errno;
+
+ memcpy(e->buf, message, len);
+ e->len = len;
+ e->valid_time = AVB_PACKET_ADP_GET_VALID_TIME(p);
+ e->entity_id = entity_id;
+ spa_list_append(&adp->entities, &e->link);
+ pw_log_info("entity %s available",
+ avb_utils_format_id(buf, sizeof(buf), entity_id));
+ }
+ e->last_time = now;
+ break;
+ case AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING:
+ if (e != NULL) {
+ pw_log_info("entity %s departing",
+ avb_utils_format_id(buf, sizeof(buf), entity_id));
+ entity_free(e);
+ }
+ break;
+ case AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER:
+ pw_log_info("entity %s advertise",
+ avb_utils_format_id(buf, sizeof(buf), entity_id));
+ if (entity_id == 0UL) {
+ spa_list_for_each(e, &adp->entities, link)
+ if (e->advertise)
+ send_advertise(adp, now, e);
+ } else if (e != NULL &&
+ e->advertise && e->entity_id == entity_id) {
+ send_advertise(adp, now, e);
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static void adp_destroy(void *data)
+{
+ struct adp *adp = data;
+ spa_hook_remove(&adp->server_listener);
+ free(adp);
+}
+
+static void check_timeout(struct adp *adp, uint64_t now)
+{
+ struct entity *e, *t;
+ char buf[128];
+
+ spa_list_for_each_safe(e, t, &adp->entities, link) {
+ if (e->last_time + (e->valid_time + 2) * SPA_NSEC_PER_SEC > now)
+ continue;
+
+ pw_log_info("entity %s timeout",
+ avb_utils_format_id(buf, sizeof(buf), e->entity_id));
+
+ if (e->advertise)
+ send_departing(adp, now, e);
+
+ entity_free(e);
+ }
+}
+static void check_readvertize(struct adp *adp, uint64_t now, struct entity *e)
+{
+ char buf[128];
+
+ if (!e->advertise)
+ return;
+
+ if (e->last_time + (e->valid_time / 2) * SPA_NSEC_PER_SEC > now)
+ return;
+
+ pw_log_debug("entity %s readvertise",
+ avb_utils_format_id(buf, sizeof(buf), e->entity_id));
+
+ send_advertise(adp, now, e);
+}
+
+static int check_advertise(struct adp *adp, uint64_t now)
+{
+ struct server *server = adp->server;
+ const struct descriptor *d;
+ struct avb_aem_desc_entity *entity;
+ struct avb_aem_desc_avb_interface *avb_interface;
+ struct entity *e;
+ uint64_t entity_id;
+ struct avb_ethernet_header *h;
+ struct avb_packet_adp *p;
+ char buf[128];
+
+ d = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0);
+ if (d == NULL)
+ return 0;
+
+ entity = d->ptr;
+ entity_id = be64toh(entity->entity_id);
+
+ if ((e = find_entity_by_id(adp, entity_id)) != NULL) {
+ if (e->advertise)
+ check_readvertize(adp, now, e);
+ return 0;
+ }
+
+ d = server_find_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0);
+ avb_interface = d ? d->ptr : NULL;
+
+ pw_log_info("entity %s advertise",
+ avb_utils_format_id(buf, sizeof(buf), entity_id));
+
+ e = calloc(1, sizeof(*e));
+ if (e == NULL)
+ return -errno;
+
+ e->advertise = true;
+ e->valid_time = 10;
+ e->last_time = now;
+ e->entity_id = entity_id;
+ e->len = sizeof(*h) + sizeof(*p);
+
+ h = (void*)e->buf;
+ p = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
+ AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
+ AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE);
+ AVB_PACKET_ADP_SET_VALID_TIME(p, e->valid_time);
+
+ p->entity_id = entity->entity_id;
+ p->entity_model_id = entity->entity_model_id;
+ p->entity_capabilities = entity->entity_capabilities;
+ p->talker_stream_sources = entity->talker_stream_sources;
+ p->talker_capabilities = entity->talker_capabilities;
+ p->listener_stream_sinks = entity->listener_stream_sinks;
+ p->listener_capabilities = entity->listener_capabilities;
+ p->controller_capabilities = entity->controller_capabilities;
+ p->available_index = entity->available_index;
+ if (avb_interface) {
+ p->gptp_grandmaster_id = avb_interface->clock_identity;
+ p->gptp_domain_number = avb_interface->domain_number;
+ }
+ p->identify_control_index = 0;
+ p->interface_index = 0;
+ p->association_id = entity->association_id;
+
+ spa_list_append(&adp->entities, &e->link);
+
+ return 0;
+}
+
+static void adp_periodic(void *data, uint64_t now)
+{
+ struct adp *adp = data;
+ check_timeout(adp, now);
+ check_advertise(adp, now);
+}
+
+static int do_help(struct adp *adp, const char *args, FILE *out)
+{
+ fprintf(out, "{ \"type\": \"help\","
+ "\"text\": \""
+ "/adp/help: this help \\n"
+ "/adp/discover [{ \"entity-id\": <id> }] : trigger discover\\n"
+ "\" }");
+ return 0;
+}
+
+static int do_discover(struct adp *adp, const char *args, FILE *out)
+{
+ struct spa_json it[2];
+ char key[128];
+ uint64_t entity_id = 0ULL;
+
+ spa_json_init(&it[0], args, strlen(args));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+ return -EINVAL;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ int len;
+ const char *value;
+ uint64_t id_val;
+
+ if ((len = spa_json_next(&it[1], &value)) <= 0)
+ break;
+
+ if (spa_json_is_null(value, len))
+ continue;
+
+ if (spa_streq(key, "entity-id")) {
+ if (avb_utils_parse_id(value, len, &id_val) >= 0)
+ entity_id = id_val;
+ }
+ }
+ send_discover(adp, entity_id);
+ return 0;
+}
+
+static int adp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+ struct adp *adp = data;
+ int res;
+
+ if (!spa_strstartswith(command, "/adp/"))
+ return 0;
+
+ command += strlen("/adp/");
+
+ if (spa_streq(command, "help"))
+ res = do_help(adp, args, out);
+ else if (spa_streq(command, "discover"))
+ res = do_discover(adp, args, out);
+ else
+ res = -ENOTSUP;
+
+ return res;
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = adp_destroy,
+ .message = adp_message,
+ .periodic = adp_periodic,
+ .command = adp_command
+};
+
+struct avb_adp *avb_adp_register(struct server *server)
+{
+ struct adp *adp;
+
+ adp = calloc(1, sizeof(*adp));
+ if (adp == NULL)
+ return NULL;
+
+ adp->server = server;
+ spa_list_init(&adp->entities);
+
+ avdecc_server_add_listener(server, &adp->server_listener, &server_events, adp);
+
+ return (struct avb_adp*)adp;
+}
+
+void avb_adp_unregister(struct avb_adp *adp)
+{
+ adp_destroy(adp);
+}
diff --git a/src/modules/module-avb/adp.h b/src/modules/module-avb/adp.h
new file mode 100644
index 0000000..c546088
--- /dev/null
+++ b/src/modules/module-avb/adp.h
@@ -0,0 +1,105 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_ADP_H
+#define AVB_ADP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE 0
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING 1
+#define AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER 2
+
+#define AVB_ADP_ENTITY_CAPABILITY_EFU_MODE (1u<<0)
+#define AVB_ADP_ENTITY_CAPABILITY_ADDRESS_ACCESS_SUPPORTED (1u<<1)
+#define AVB_ADP_ENTITY_CAPABILITY_GATEWAY_ENTITY (1u<<2)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED (1u<<3)
+#define AVB_ADP_ENTITY_CAPABILITY_LEGACY_AVC (1u<<4)
+#define AVB_ADP_ENTITY_CAPABILITY_ASSOCIATION_ID_SUPPORTED (1u<<5)
+#define AVB_ADP_ENTITY_CAPABILITY_ASSOCIATION_ID_VALID (1u<<6)
+#define AVB_ADP_ENTITY_CAPABILITY_VENDOR_UNIQUE_SUPPORTED (1u<<7)
+#define AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED (1u<<8)
+#define AVB_ADP_ENTITY_CAPABILITY_CLASS_B_SUPPORTED (1u<<9)
+#define AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED (1u<<10)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_AUTHENTICATION_SUPPORTED (1u<<11)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_AUTHENTICATION_REQUIRED (1u<<12)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_PERSISTENT_ACQUIRE_SUPPORTED (1u<<13)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_IDENTIFY_CONTROL_INDEX_VALID (1u<<14)
+#define AVB_ADP_ENTITY_CAPABILITY_AEM_INTERFACE_INDEX_VALID (1u<<15)
+#define AVB_ADP_ENTITY_CAPABILITY_GENERAL_CONTROLLER_IGNORE (1u<<16)
+#define AVB_ADP_ENTITY_CAPABILITY_ENTITY_NOT_READY (1u<<17)
+
+#define AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED (1u<<0)
+#define AVB_ADP_TALKER_CAPABILITY_OTHER_SOURCE (1u<<9)
+#define AVB_ADP_TALKER_CAPABILITY_CONTROL_SOURCE (1u<<10)
+#define AVB_ADP_TALKER_CAPABILITY_MEDIA_CLOCK_SOURCE (1u<<11)
+#define AVB_ADP_TALKER_CAPABILITY_SMPTE_SOURCE (1u<<12)
+#define AVB_ADP_TALKER_CAPABILITY_MIDI_SOURCE (1u<<13)
+#define AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE (1u<<14)
+#define AVB_ADP_TALKER_CAPABILITY_VIDEO_SOURCE (1u<<15)
+
+#define AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED (1u<<0)
+#define AVB_ADP_LISTENER_CAPABILITY_OTHER_SINK (1u<<9)
+#define AVB_ADP_LISTENER_CAPABILITY_CONTROL_SINK (1u<<10)
+#define AVB_ADP_LISTENER_CAPABILITY_MEDIA_CLOCK_SINK (1u<<11)
+#define AVB_ADP_LISTENER_CAPABILITY_SMPTE_SINK (1u<<12)
+#define AVB_ADP_LISTENER_CAPABILITY_MIDI_SINK (1u<<13)
+#define AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK (1u<<14)
+#define AVB_ADP_LISTENER_CAPABILITY_VIDEO_SINK (1u<<15)
+
+#define AVB_ADP_CONTROLLER_CAPABILITY_IMPLEMENTED (1u<<0)
+#define AVB_ADP_CONTROLLER_CAPABILITY_LAYER3_PROXY (1u<<1)
+
+#define AVB_ADP_CONTROL_DATA_LENGTH 56
+
+struct avb_packet_adp {
+ struct avb_packet_header hdr;
+ uint64_t entity_id;
+ uint64_t entity_model_id;
+ uint32_t entity_capabilities;
+ uint16_t talker_stream_sources;
+ uint16_t talker_capabilities;
+ uint16_t listener_stream_sinks;
+ uint16_t listener_capabilities;
+ uint32_t controller_capabilities;
+ uint32_t available_index;
+ uint64_t gptp_grandmaster_id;
+ uint8_t gptp_domain_number;
+ uint8_t reserved0[3];
+ uint16_t identify_control_index;
+ uint16_t interface_index;
+ uint64_t association_id;
+ uint32_t reserved1;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_ADP_SET_MESSAGE_TYPE(p,v) AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_ADP_SET_VALID_TIME(p,v) AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_ADP_GET_MESSAGE_TYPE(p) AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_ADP_GET_VALID_TIME(p) AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_adp *avb_adp_register(struct server *server);
+
+#endif /* AVB_ADP_H */
diff --git a/src/modules/module-avb/aecp-aem-descriptors.h b/src/modules/module-avb/aecp-aem-descriptors.h
new file mode 100644
index 0000000..101c331
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem-descriptors.h
@@ -0,0 +1,247 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AECP_AEM_DESCRIPTORS_H
+#define AVB_AECP_AEM_DESCRIPTORS_H
+
+#include "internal.h"
+
+#define AVB_AEM_DESC_ENTITY 0x0000
+#define AVB_AEM_DESC_CONFIGURATION 0x0001
+#define AVB_AEM_DESC_AUDIO_UNIT 0x0002
+#define AVB_AEM_DESC_VIDEO_UNIT 0x0003
+#define AVB_AEM_DESC_SENSOR_UNIT 0x0004
+#define AVB_AEM_DESC_STREAM_INPUT 0x0005
+#define AVB_AEM_DESC_STREAM_OUTPUT 0x0006
+#define AVB_AEM_DESC_JACK_INPUT 0x0007
+#define AVB_AEM_DESC_JACK_OUTPUT 0x0008
+#define AVB_AEM_DESC_AVB_INTERFACE 0x0009
+#define AVB_AEM_DESC_CLOCK_SOURCE 0x000a
+#define AVB_AEM_DESC_MEMORY_OBJECT 0x000b
+#define AVB_AEM_DESC_LOCALE 0x000c
+#define AVB_AEM_DESC_STRINGS 0x000d
+#define AVB_AEM_DESC_STREAM_PORT_INPUT 0x000e
+#define AVB_AEM_DESC_STREAM_PORT_OUTPUT 0x000f
+#define AVB_AEM_DESC_EXTERNAL_PORT_INPUT 0x0010
+#define AVB_AEM_DESC_EXTERNAL_PORT_OUTPUT 0x0011
+#define AVB_AEM_DESC_INTERNAL_PORT_INPUT 0x0012
+#define AVB_AEM_DESC_INTERNAL_PORT_OUTPUT 0x0013
+#define AVB_AEM_DESC_AUDIO_CLUSTER 0x0014
+#define AVB_AEM_DESC_VIDEO_CLUSTER 0x0015
+#define AVB_AEM_DESC_SENSOR_CLUSTER 0x0016
+#define AVB_AEM_DESC_AUDIO_MAP 0x0017
+#define AVB_AEM_DESC_VIDEO_MAP 0x0018
+#define AVB_AEM_DESC_SENSOR_MAP 0x0019
+#define AVB_AEM_DESC_CONTROL 0x001a
+#define AVB_AEM_DESC_SIGNAL_SELECTOR 0x001b
+#define AVB_AEM_DESC_MIXER 0x001c
+#define AVB_AEM_DESC_MATRIX 0x001d
+#define AVB_AEM_DESC_MATRIX_SIGNAL 0x001e
+#define AVB_AEM_DESC_SIGNAL_SPLITTER 0x001f
+#define AVB_AEM_DESC_SIGNAL_COMBINER 0x0020
+#define AVB_AEM_DESC_SIGNAL_DEMULTIPLEXER 0x0021
+#define AVB_AEM_DESC_SIGNAL_MULTIPLEXER 0x0022
+#define AVB_AEM_DESC_SIGNAL_TRANSCODER 0x0023
+#define AVB_AEM_DESC_CLOCK_DOMAIN 0x0024
+#define AVB_AEM_DESC_CONTROL_BLOCK 0x0025
+#define AVB_AEM_DESC_INVALID 0xffff
+
+struct avb_aem_desc_entity {
+ uint64_t entity_id;
+ uint64_t entity_model_id;
+ uint32_t entity_capabilities;
+ uint16_t talker_stream_sources;
+ uint16_t talker_capabilities;
+ uint16_t listener_stream_sinks;
+ uint16_t listener_capabilities;
+ uint32_t controller_capabilities;
+ uint32_t available_index;
+ uint64_t association_id;
+ char entity_name[64];
+ uint16_t vendor_name_string;
+ uint16_t model_name_string;
+ char firmware_version[64];
+ char group_name[64];
+ char serial_number[64];
+ uint16_t configurations_count;
+ uint16_t current_configuration;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_descriptor_count {
+ uint16_t descriptor_type;
+ uint16_t descriptor_count;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_configuration {
+ char object_name[64];
+ uint16_t localized_description;
+ uint16_t descriptor_counts_count;
+ uint16_t descriptor_counts_offset;
+ struct avb_aem_desc_descriptor_count descriptor_counts[0];
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_sampling_rate {
+ uint32_t pull_frequency;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_audio_unit {
+ char object_name[64];
+ uint16_t localized_description;
+ uint16_t clock_domain_index;
+ uint16_t number_of_stream_input_ports;
+ uint16_t base_stream_input_port;
+ uint16_t number_of_stream_output_ports;
+ uint16_t base_stream_output_port;
+ uint16_t number_of_external_input_ports;
+ uint16_t base_external_input_port;
+ uint16_t number_of_external_output_ports;
+ uint16_t base_external_output_port;
+ uint16_t number_of_internal_input_ports;
+ uint16_t base_internal_input_port;
+ uint16_t number_of_internal_output_ports;
+ uint16_t base_internal_output_port;
+ uint16_t number_of_controls;
+ uint16_t base_control;
+ uint16_t number_of_signal_selectors;
+ uint16_t base_signal_selector;
+ uint16_t number_of_mixers;
+ uint16_t base_mixer;
+ uint16_t number_of_matrices;
+ uint16_t base_matrix;
+ uint16_t number_of_splitters;
+ uint16_t base_splitter;
+ uint16_t number_of_combiners;
+ uint16_t base_combiner;
+ uint16_t number_of_demultiplexers;
+ uint16_t base_demultiplexer;
+ uint16_t number_of_multiplexers;
+ uint16_t base_multiplexer;
+ uint16_t number_of_transcoders;
+ uint16_t base_transcoder;
+ uint16_t number_of_control_blocks;
+ uint16_t base_control_block;
+ uint32_t current_sampling_rate;
+ uint16_t sampling_rates_offset;
+ uint16_t sampling_rates_count;
+ struct avb_aem_desc_sampling_rate sampling_rates[0];
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_STREAM_FLAG_SYNC_SOURCE (1u<<0)
+#define AVB_AEM_DESC_STREAM_FLAG_CLASS_A (1u<<1)
+#define AVB_AEM_DESC_STREAM_FLAG_CLASS_B (1u<<2)
+#define AVB_AEM_DESC_STREAM_FLAG_SUPPORTS_ENCRYPTED (1u<<3)
+#define AVB_AEM_DESC_STREAM_FLAG_PRIMARY_BACKUP_SUPPORTED (1u<<4)
+#define AVB_AEM_DESC_STREAM_FLAG_PRIMARY_BACKUP_VALID (1u<<5)
+#define AVB_AEM_DESC_STREAM_FLAG_SECONDARY_BACKUP_SUPPORTED (1u<<6)
+#define AVB_AEM_DESC_STREAM_FLAG_SECONDARY_BACKUP_VALID (1u<<7)
+#define AVB_AEM_DESC_STREAM_FLAG_TERTIARY_BACKUP_SUPPORTED (1u<<8)
+#define AVB_AEM_DESC_STREAM_FLAG_TERTIARY_BACKUP_VALID (1u<<9)
+
+struct avb_aem_desc_stream {
+ char object_name[64];
+ uint16_t localized_description;
+ uint16_t clock_domain_index;
+ uint16_t stream_flags;
+ uint64_t current_format;
+ uint16_t formats_offset;
+ uint16_t number_of_formats;
+ uint64_t backup_talker_entity_id_0;
+ uint16_t backup_talker_unique_id_0;
+ uint64_t backup_talker_entity_id_1;
+ uint16_t backup_talker_unique_id_1;
+ uint64_t backup_talker_entity_id_2;
+ uint16_t backup_talker_unique_id_2;
+ uint64_t backedup_talker_entity_id;
+ uint16_t backedup_talker_unique;
+ uint16_t avb_interface_index;
+ uint32_t buffer_length;
+ uint64_t stream_formats[0];
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_GRANDMASTER_SUPPORTED (1<<0)
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_SUPPORTED (1<<1)
+#define AVB_AEM_DESC_AVB_INTERFACE_FLAG_SRP_SUPPORTED (1<<2)
+
+struct avb_aem_desc_avb_interface {
+ char object_name[64];
+ uint16_t localized_description;
+ uint8_t mac_address[6];
+ uint16_t interface_flags;
+ uint64_t clock_identity;
+ uint8_t priority1;
+ uint8_t clock_class;
+ uint16_t offset_scaled_log_variance;
+ uint8_t clock_accuracy;
+ uint8_t priority2;
+ uint8_t domain_number;
+ int8_t log_sync_interval;
+ int8_t log_announce_interval;
+ int8_t log_pdelay_interval;
+ uint16_t port_number;
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INTERNAL 0x0000
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_EXTERNAL 0x0001
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INPUT_STREAM 0x0002
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_MEDIA_CLOCK_STREAM 0x0003
+#define AVB_AEM_DESC_CLOCK_SOURCE_TYPE_EXPANSION 0xffff
+
+struct avb_aem_desc_clock_source {
+ char object_name[64];
+ uint16_t localized_description;
+ uint16_t clock_source_flags;
+ uint16_t clock_source_type;
+ uint64_t clock_source_identifier;
+ uint16_t clock_source_location_type;
+ uint16_t clock_source_location_index;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_locale {
+ char locale_identifier[64];
+ uint16_t number_of_strings;
+ uint16_t base_strings;
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_strings {
+ char string_0[64];
+ char string_1[64];
+ char string_2[64];
+ char string_3[64];
+ char string_4[64];
+ char string_5[64];
+ char string_6[64];
+} __attribute__ ((__packed__));
+
+struct avb_aem_desc_stream_port {
+ uint16_t clock_domain_index;
+ uint16_t port_flags;
+ uint16_t number_of_controls;
+ uint16_t base_control;
+ uint16_t number_of_clusters;
+ uint16_t base_cluster;
+ uint16_t number_of_maps;
+ uint16_t base_map;
+} __attribute__ ((__packed__));
+
+#endif /* AVB_AECP_AEM_DESCRIPTORS_H */
diff --git a/src/modules/module-avb/aecp-aem.c b/src/modules/module-avb/aecp-aem.c
new file mode 100644
index 0000000..d191330
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem.c
@@ -0,0 +1,285 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "aecp-aem.h"
+#include "aecp-aem-descriptors.h"
+
+static int reply_status(struct aecp *aecp, int status, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_aecp_header *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+ memcpy(buf, m, len);
+ AVB_PACKET_AECP_SET_MESSAGE_TYPE(reply, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+ AVB_PACKET_AECP_SET_STATUS(reply, status);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static int reply_not_implemented(struct aecp *aecp, const void *m, int len)
+{
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED, m, len);
+}
+
+static int reply_success(struct aecp *aecp, const void *m, int len)
+{
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_SUCCESS, m, len);
+}
+
+/* ACQUIRE_ENTITY */
+static int handle_acquire_entity(struct aecp *aecp, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ const struct avb_packet_aecp_aem *p = m;
+ const struct avb_packet_aecp_aem_acquire *ae;
+ const struct descriptor *desc;
+ uint16_t desc_type, desc_id;
+
+ ae = (const struct avb_packet_aecp_aem_acquire*)p->payload;
+
+ desc_type = ntohs(ae->descriptor_type);
+ desc_id = ntohs(ae->descriptor_id);
+
+ desc = server_find_descriptor(server, desc_type, desc_id);
+ if (desc == NULL)
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len);
+
+ if (desc_type != AVB_AEM_DESC_ENTITY || desc_id != 0)
+ return reply_not_implemented(aecp, m, len);
+
+ return reply_success(aecp, m, len);
+}
+
+/* LOCK_ENTITY */
+static int handle_lock_entity(struct aecp *aecp, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ const struct avb_packet_aecp_aem *p = m;
+ const struct avb_packet_aecp_aem_acquire *ae;
+ const struct descriptor *desc;
+ uint16_t desc_type, desc_id;
+
+ ae = (const struct avb_packet_aecp_aem_acquire*)p->payload;
+
+ desc_type = ntohs(ae->descriptor_type);
+ desc_id = ntohs(ae->descriptor_id);
+
+ desc = server_find_descriptor(server, desc_type, desc_id);
+ if (desc == NULL)
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len);
+
+ if (desc_type != AVB_AEM_DESC_ENTITY || desc_id != 0)
+ return reply_not_implemented(aecp, m, len);
+
+ return reply_success(aecp, m, len);
+}
+
+/* READ_DESCRIPTOR */
+static int handle_read_descriptor(struct aecp *aecp, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ const struct avb_ethernet_header *h = m;
+ const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+ struct avb_packet_aecp_aem *reply;
+ const struct avb_packet_aecp_aem_read_descriptor *rd;
+ uint16_t desc_type, desc_id;
+ const struct descriptor *desc;
+ uint8_t buf[2048];
+ size_t size, psize;
+
+ rd = (struct avb_packet_aecp_aem_read_descriptor*)p->payload;
+
+ desc_type = ntohs(rd->descriptor_type);
+ desc_id = ntohs(rd->descriptor_id);
+
+ pw_log_info("descriptor type:%04x index:%d", desc_type, desc_id);
+
+ desc = server_find_descriptor(server, desc_type, desc_id);
+ if (desc == NULL)
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len);
+
+ memcpy(buf, m, len);
+
+ psize = sizeof(*rd);
+ size = sizeof(*h) + sizeof(*reply) + psize;
+
+ memcpy(buf + size, desc->ptr, desc->size);
+ size += desc->size;
+ psize += desc->size;
+
+ h = (void*)buf;
+ reply = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_AECP_SET_MESSAGE_TYPE(&reply->aecp, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+ AVB_PACKET_AECP_SET_STATUS(&reply->aecp, AVB_AECP_AEM_STATUS_SUCCESS);
+ AVB_PACKET_SET_LENGTH(&reply->aecp.hdr, psize + 12);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, size);
+}
+
+/* GET_AVB_INFO */
+static int handle_get_avb_info(struct aecp *aecp, const void *m, int len)
+{
+ struct server *server = aecp->server;
+ const struct avb_ethernet_header *h = m;
+ const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+ struct avb_packet_aecp_aem *reply;
+ struct avb_packet_aecp_aem_get_avb_info *i;
+ struct avb_aem_desc_avb_interface *avb_interface;
+ uint16_t desc_type, desc_id;
+ const struct descriptor *desc;
+ uint8_t buf[2048];
+ size_t size, psize;
+
+ i = (struct avb_packet_aecp_aem_get_avb_info*)p->payload;
+
+ desc_type = ntohs(i->descriptor_type);
+ desc_id = ntohs(i->descriptor_id);
+
+ desc = server_find_descriptor(server, desc_type, desc_id);
+ if (desc == NULL)
+ return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len);
+
+ if (desc_type != AVB_AEM_DESC_AVB_INTERFACE || desc_id != 0)
+ return reply_not_implemented(aecp, m, len);
+
+ avb_interface = desc->ptr;
+
+ memcpy(buf, m, len);
+
+ psize = sizeof(*i);
+ size = sizeof(*h) + sizeof(*reply) + psize;
+
+ h = (void*)buf;
+ reply = SPA_PTROFF(h, sizeof(*h), void);
+ AVB_PACKET_AECP_SET_MESSAGE_TYPE(&reply->aecp, AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE);
+ AVB_PACKET_AECP_SET_STATUS(&reply->aecp, AVB_AECP_AEM_STATUS_SUCCESS);
+ AVB_PACKET_SET_LENGTH(&reply->aecp.hdr, psize + 12);
+
+ i = (struct avb_packet_aecp_aem_get_avb_info*)reply->payload;
+ i->gptp_grandmaster_id = avb_interface->clock_identity;
+ i->propagation_delay = htonl(0);
+ i->gptp_domain_number = avb_interface->domain_number;
+ i->flags = 0;
+ i->msrp_mappings_count = htons(0);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, size);
+}
+
+/* AEM_COMMAND */
+struct cmd_info {
+ uint16_t type;
+ const char *name;
+ int (*handle) (struct aecp *aecp, const void *p, int len);
+};
+
+static const struct cmd_info cmd_info[] = {
+ { AVB_AECP_AEM_CMD_ACQUIRE_ENTITY, "acquire-entity", handle_acquire_entity, },
+ { AVB_AECP_AEM_CMD_LOCK_ENTITY, "lock-entity", handle_lock_entity, },
+ { AVB_AECP_AEM_CMD_ENTITY_AVAILABLE, "entity-available", NULL, },
+ { AVB_AECP_AEM_CMD_CONTROLLER_AVAILABLE, "controller-available", NULL, },
+ { AVB_AECP_AEM_CMD_READ_DESCRIPTOR, "read-descriptor", handle_read_descriptor, },
+ { AVB_AECP_AEM_CMD_WRITE_DESCRIPTOR, "write-descriptor", NULL, },
+ { AVB_AECP_AEM_CMD_SET_CONFIGURATION, "set-configuration", NULL, },
+ { AVB_AECP_AEM_CMD_GET_CONFIGURATION, "get-configuration", NULL, },
+ { AVB_AECP_AEM_CMD_SET_STREAM_FORMAT, "set-stream-format", NULL, },
+ { AVB_AECP_AEM_CMD_GET_STREAM_FORMAT, "get-stream-format", NULL, },
+ { AVB_AECP_AEM_CMD_SET_VIDEO_FORMAT, "set-video-format", NULL, },
+ { AVB_AECP_AEM_CMD_GET_VIDEO_FORMAT, "get-video-format", NULL, },
+ { AVB_AECP_AEM_CMD_SET_SENSOR_FORMAT, "set-sensor-format", NULL, },
+ { AVB_AECP_AEM_CMD_GET_SENSOR_FORMAT, "get-sensor-format", NULL, },
+ { AVB_AECP_AEM_CMD_SET_STREAM_INFO, "set-stream-info", NULL, },
+ { AVB_AECP_AEM_CMD_GET_STREAM_INFO, "get-stream-info", NULL, },
+ { AVB_AECP_AEM_CMD_SET_NAME, "set-name", NULL, },
+ { AVB_AECP_AEM_CMD_GET_NAME, "get-name", NULL, },
+ { AVB_AECP_AEM_CMD_SET_ASSOCIATION_ID, "set-association-id", NULL, },
+ { AVB_AECP_AEM_CMD_GET_ASSOCIATION_ID, "get-association-id", NULL, },
+ { AVB_AECP_AEM_CMD_SET_SAMPLING_RATE, "set-sampling-rate", NULL, },
+ { AVB_AECP_AEM_CMD_GET_SAMPLING_RATE, "get-sampling-rate", NULL, },
+ { AVB_AECP_AEM_CMD_SET_CLOCK_SOURCE, "set-clock-source", NULL, },
+ { AVB_AECP_AEM_CMD_GET_CLOCK_SOURCE, "get-clock-source", NULL, },
+ { AVB_AECP_AEM_CMD_SET_CONTROL, "set-control", NULL, },
+ { AVB_AECP_AEM_CMD_GET_CONTROL, "get-control", NULL, },
+ { AVB_AECP_AEM_CMD_INCREMENT_CONTROL, "increment-control", NULL, },
+ { AVB_AECP_AEM_CMD_DECREMENT_CONTROL, "decrement-control", NULL, },
+ { AVB_AECP_AEM_CMD_SET_SIGNAL_SELECTOR, "set-signal-selector", NULL, },
+ { AVB_AECP_AEM_CMD_GET_SIGNAL_SELECTOR, "get-signal-selector", NULL, },
+ { AVB_AECP_AEM_CMD_SET_MIXER, "set-mixer", NULL, },
+ { AVB_AECP_AEM_CMD_GET_MIXER, "get-mixer", NULL, },
+ { AVB_AECP_AEM_CMD_SET_MATRIX, "set-matrix", NULL, },
+ { AVB_AECP_AEM_CMD_GET_MATRIX, "get-matrix", NULL, },
+ { AVB_AECP_AEM_CMD_START_STREAMING, "start-streaming", NULL, },
+ { AVB_AECP_AEM_CMD_STOP_STREAMING, "stop-streaming", NULL, },
+ { AVB_AECP_AEM_CMD_REGISTER_UNSOLICITED_NOTIFICATION, "register-unsolicited-notification", NULL, },
+ { AVB_AECP_AEM_CMD_DEREGISTER_UNSOLICITED_NOTIFICATION, "deregister-unsolicited-notification", NULL, },
+ { AVB_AECP_AEM_CMD_IDENTIFY_NOTIFICATION, "identify-notification", NULL, },
+ { AVB_AECP_AEM_CMD_GET_AVB_INFO, "get-avb-info", handle_get_avb_info, },
+ { AVB_AECP_AEM_CMD_GET_AS_PATH, "get-as-path", NULL, },
+ { AVB_AECP_AEM_CMD_GET_COUNTERS, "get-counters", NULL, },
+ { AVB_AECP_AEM_CMD_REBOOT, "reboot", NULL, },
+ { AVB_AECP_AEM_CMD_GET_AUDIO_MAP, "get-audio-map", NULL, },
+ { AVB_AECP_AEM_CMD_ADD_AUDIO_MAPPINGS, "add-audio-mappings", NULL, },
+ { AVB_AECP_AEM_CMD_REMOVE_AUDIO_MAPPINGS, "remove-audio-mappings", NULL, },
+ { AVB_AECP_AEM_CMD_GET_VIDEO_MAP, "get-video-map", NULL, },
+ { AVB_AECP_AEM_CMD_ADD_VIDEO_MAPPINGS, "add-video-mappings", NULL, },
+ { AVB_AECP_AEM_CMD_REMOVE_VIDEO_MAPPINGS, "remove-video-mappings", NULL, },
+ { AVB_AECP_AEM_CMD_GET_SENSOR_MAP, "get-sensor-map", NULL, }
+};
+
+static inline const struct cmd_info *find_cmd_info(uint16_t type, const char *name)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(cmd_info, i) {
+ if ((name == NULL && type == i->type) ||
+ (name != NULL && spa_streq(name, i->name)))
+ return i;
+ }
+ return NULL;
+}
+
+int avb_aecp_aem_handle_command(struct aecp *aecp, const void *m, int len)
+{
+ const struct avb_ethernet_header *h = m;
+ const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
+ uint16_t cmd_type;
+ const struct cmd_info *info;
+
+ cmd_type = AVB_PACKET_AEM_GET_COMMAND_TYPE(p);
+
+ info = find_cmd_info(cmd_type, NULL);
+ if (info == NULL)
+ return reply_not_implemented(aecp, m, len);
+
+ pw_log_info("aem command %s", info->name);
+
+ if (info->handle == NULL)
+ return reply_not_implemented(aecp, m, len);
+
+ return info->handle(aecp, m, len);
+}
+
+int avb_aecp_aem_handle_response(struct aecp *aecp, const void *m, int len)
+{
+ return 0;
+}
diff --git a/src/modules/module-avb/aecp-aem.h b/src/modules/module-avb/aecp-aem.h
new file mode 100644
index 0000000..dcf26b5
--- /dev/null
+++ b/src/modules/module-avb/aecp-aem.h
@@ -0,0 +1,345 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AEM_H
+#define AVB_AEM_H
+
+#include "aecp.h"
+
+#define AVB_AECP_AEM_STATUS_SUCCESS 0
+#define AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED 1
+#define AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR 2
+#define AVB_AECP_AEM_STATUS_ENTITY_LOCKED 3
+#define AVB_AECP_AEM_STATUS_ENTITY_ACQUIRED 4
+#define AVB_AECP_AEM_STATUS_NOT_AUTHENTICATED 5
+#define AVB_AECP_AEM_STATUS_AUTHENTICATION_DISABLED 6
+#define AVB_AECP_AEM_STATUS_BAD_ARGUMENTS 7
+#define AVB_AECP_AEM_STATUS_NO_RESOURCES 8
+#define AVB_AECP_AEM_STATUS_IN_PROGRESS 9
+#define AVB_AECP_AEM_STATUS_ENTITY_MISBEHAVING 10
+#define AVB_AECP_AEM_STATUS_NOT_SUPPORTED 11
+#define AVB_AECP_AEM_STATUS_STREAM_IS_RUNNING 12
+
+#define AVB_AECP_AEM_CMD_ACQUIRE_ENTITY 0x0000
+#define AVB_AECP_AEM_CMD_LOCK_ENTITY 0x0001
+#define AVB_AECP_AEM_CMD_ENTITY_AVAILABLE 0x0002
+#define AVB_AECP_AEM_CMD_CONTROLLER_AVAILABLE 0x0003
+#define AVB_AECP_AEM_CMD_READ_DESCRIPTOR 0x0004
+#define AVB_AECP_AEM_CMD_WRITE_DESCRIPTOR 0x0005
+#define AVB_AECP_AEM_CMD_SET_CONFIGURATION 0x0006
+#define AVB_AECP_AEM_CMD_GET_CONFIGURATION 0x0007
+#define AVB_AECP_AEM_CMD_SET_STREAM_FORMAT 0x0008
+#define AVB_AECP_AEM_CMD_GET_STREAM_FORMAT 0x0009
+#define AVB_AECP_AEM_CMD_SET_VIDEO_FORMAT 0x000a
+#define AVB_AECP_AEM_CMD_GET_VIDEO_FORMAT 0x000b
+#define AVB_AECP_AEM_CMD_SET_SENSOR_FORMAT 0x000c
+#define AVB_AECP_AEM_CMD_GET_SENSOR_FORMAT 0x000d
+#define AVB_AECP_AEM_CMD_SET_STREAM_INFO 0x000e
+#define AVB_AECP_AEM_CMD_GET_STREAM_INFO 0x000f
+#define AVB_AECP_AEM_CMD_SET_NAME 0x0010
+#define AVB_AECP_AEM_CMD_GET_NAME 0x0011
+#define AVB_AECP_AEM_CMD_SET_ASSOCIATION_ID 0x0012
+#define AVB_AECP_AEM_CMD_GET_ASSOCIATION_ID 0x0013
+#define AVB_AECP_AEM_CMD_SET_SAMPLING_RATE 0x0014
+#define AVB_AECP_AEM_CMD_GET_SAMPLING_RATE 0x0015
+#define AVB_AECP_AEM_CMD_SET_CLOCK_SOURCE 0x0016
+#define AVB_AECP_AEM_CMD_GET_CLOCK_SOURCE 0x0017
+#define AVB_AECP_AEM_CMD_SET_CONTROL 0x0018
+#define AVB_AECP_AEM_CMD_GET_CONTROL 0x0019
+#define AVB_AECP_AEM_CMD_INCREMENT_CONTROL 0x001a
+#define AVB_AECP_AEM_CMD_DECREMENT_CONTROL 0x001b
+#define AVB_AECP_AEM_CMD_SET_SIGNAL_SELECTOR 0x001c
+#define AVB_AECP_AEM_CMD_GET_SIGNAL_SELECTOR 0x001d
+#define AVB_AECP_AEM_CMD_SET_MIXER 0x001e
+#define AVB_AECP_AEM_CMD_GET_MIXER 0x001f
+#define AVB_AECP_AEM_CMD_SET_MATRIX 0x0020
+#define AVB_AECP_AEM_CMD_GET_MATRIX 0x0021
+#define AVB_AECP_AEM_CMD_START_STREAMING 0x0022
+#define AVB_AECP_AEM_CMD_STOP_STREAMING 0x0023
+#define AVB_AECP_AEM_CMD_REGISTER_UNSOLICITED_NOTIFICATION 0x0024
+#define AVB_AECP_AEM_CMD_DEREGISTER_UNSOLICITED_NOTIFICATION 0x0025
+#define AVB_AECP_AEM_CMD_IDENTIFY_NOTIFICATION 0x0026
+#define AVB_AECP_AEM_CMD_GET_AVB_INFO 0x0027
+#define AVB_AECP_AEM_CMD_GET_AS_PATH 0x0028
+#define AVB_AECP_AEM_CMD_GET_COUNTERS 0x0029
+#define AVB_AECP_AEM_CMD_REBOOT 0x002a
+#define AVB_AECP_AEM_CMD_GET_AUDIO_MAP 0x002b
+#define AVB_AECP_AEM_CMD_ADD_AUDIO_MAPPINGS 0x002c
+#define AVB_AECP_AEM_CMD_REMOVE_AUDIO_MAPPINGS 0x002d
+#define AVB_AECP_AEM_CMD_GET_VIDEO_MAP 0x002e
+#define AVB_AECP_AEM_CMD_ADD_VIDEO_MAPPINGS 0x002f
+#define AVB_AECP_AEM_CMD_REMOVE_VIDEO_MAPPINGS 0x0030
+#define AVB_AECP_AEM_CMD_GET_SENSOR_MAP 0x0031
+#define AVB_AECP_AEM_CMD_ADD_SENSOR_MAPPINGS 0x0032
+#define AVB_AECP_AEM_CMD_REMOVE_SENSOR_MAPPINGS 0x0033
+#define AVB_AECP_AEM_CMD_START_OPERATION 0x0034
+#define AVB_AECP_AEM_CMD_ABORT_OPERATION 0x0035
+#define AVB_AECP_AEM_CMD_OPERATION_STATUS 0x0036
+#define AVB_AECP_AEM_CMD_AUTH_ADD_KEY 0x0037
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_KEY 0x0038
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEY_LIST 0x0039
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEY 0x003a
+#define AVB_AECP_AEM_CMD_AUTH_ADD_KEY_TO_CHAIN 0x003b
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_KEY_FROM_CHAIN 0x003c
+#define AVB_AECP_AEM_CMD_AUTH_GET_KEYCHAIN_LIST 0x003d
+#define AVB_AECP_AEM_CMD_AUTH_GET_IDENTITY 0x003e
+#define AVB_AECP_AEM_CMD_AUTH_ADD_TOKEN 0x003f
+#define AVB_AECP_AEM_CMD_AUTH_DELETE_TOKEN 0x0040
+#define AVB_AECP_AEM_CMD_AUTHENTICATE 0x0041
+#define AVB_AECP_AEM_CMD_DEAUTHENTICATE 0x0042
+#define AVB_AECP_AEM_CMD_ENABLE_TRANSPORT_SECURITY 0x0043
+#define AVB_AECP_AEM_CMD_DISABLE_TRANSPORT_SECURITY 0x0044
+#define AVB_AECP_AEM_CMD_ENABLE_STREAM_ENCRYPTION 0x0045
+#define AVB_AECP_AEM_CMD_DISABLE_STREAM_ENCRYPTION 0x0046
+#define AVB_AECP_AEM_CMD_SET_MEMORY_OBJECT_LENGTH 0x0047
+#define AVB_AECP_AEM_CMD_GET_MEMORY_OBJECT_LENGTH 0x0048
+#define AVB_AECP_AEM_CMD_SET_STREAM_BACKUP 0x0049
+#define AVB_AECP_AEM_CMD_GET_STREAM_BACKUP 0x004a
+#define AVB_AECP_AEM_CMD_EXPANSION 0x7fff
+
+#define AVB_AEM_ACQUIRE_ENTITY_PERSISTENT_FLAG (1<<0)
+
+struct avb_packet_aecp_aem_acquire {
+ uint32_t flags;
+ uint64_t owner_guid;
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_lock {
+ uint32_t flags;
+ uint64_t locked_guid;
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_read_descriptor {
+ uint16_t configuration;
+ uint8_t reserved[2];
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_configuration {
+ uint16_t reserved;
+ uint16_t configuration_index;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_stream_format {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint64_t stream_format;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_video_format {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint32_t format_specific;
+ uint16_t aspect_ratio;
+ uint16_t color_space;
+ uint32_t frame_size;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_sensor_format {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint64_t sensor_format;
+} __attribute__ ((__packed__));
+
+
+#define AVB_AEM_STREAM_INFO_FLAG_CLASS_B (1u<<0)
+#define AVB_AEM_STREAM_INFO_FLAG_FAST_CONNECT (1u<<1)
+#define AVB_AEM_STREAM_INFO_FLAG_SAVED_STATE (1u<<2)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAMING_WAIT (1u<<3)
+#define AVB_AEM_STREAM_INFO_FLAG_ENCRYPTED_PDU (1u<<4)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_VLAN_ID_VALID (1u<<25)
+#define AVB_AEM_STREAM_INFO_FLAG_CONNECTED (1u<<26)
+#define AVB_AEM_STREAM_INFO_FLAG_MSRP_FAILURE_VALID (1u<<27)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_DEST_MAC_VALID (1u<<28)
+#define AVB_AEM_STREAM_INFO_FLAG_MSRP_ACC_LAT_VALID (1u<<29)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_ID_VALID (1u<<30)
+#define AVB_AEM_STREAM_INFO_FLAG_STREAM_FORMAT_VALID (1u<<31)
+
+struct avb_packet_aecp_aem_setget_stream_info {
+ uint16_t descriptor_type;
+ uint16_t descriptor_index;
+ uint32_t aem_stream_info_flags;
+ uint64_t stream_format;
+ uint64_t stream_id;
+ uint32_t msrp_accumulated_latency;
+ uint8_t stream_dest_mac[6];
+ uint8_t msrp_failure_code;
+ uint8_t reserved;
+ uint64_t msrp_failure_bridge_id;
+ uint16_t stream_vlan_id;
+ uint16_t reserved2;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_name {
+ uint16_t descriptor_type;
+ uint16_t descriptor_index;
+ uint16_t name_index;
+ uint16_t configuration_index;
+ char name[64];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_association_id {
+ uint16_t descriptor_type;
+ uint16_t descriptor_index;
+ uint64_t association_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_sampling_rate {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint32_t sampling_rate;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_clock_source {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t clock_source_index;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_control {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_incdec_control {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t index_count;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_signal_selector {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t signal_type;
+ uint16_t signal_index;
+ uint16_t signal_output;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_mixer {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_setget_matrix {
+ uint16_t descriptor_type;
+ uint16_t descriptor_index;
+ uint16_t matrix_column;
+ uint16_t matrix_row;
+ uint16_t region_width;
+ uint16_t region_height;
+ uint16_t rep_direction_value_count;
+ uint16_t item_offset;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_startstop_streaming {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_identify_notification {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_msrp_mapping {
+ uint8_t traffic_class;
+ uint8_t priority;
+ uint16_t vlan_id;
+} __attribute__ ((__packed__));
+
+#define AVB_AEM_AVB_INFO_FLAG_GPTP_GRANDMASTER_SUPPORTED (1u<<0)
+#define AVB_AEM_AVB_INFO_FLAG_GPTP_ENABLED (1u<<1)
+#define AVB_AEM_AVB_INFO_FLAG_SRP_ENABLED (1u<<2)
+
+struct avb_packet_aecp_aem_get_avb_info {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint64_t gptp_grandmaster_id;
+ uint32_t propagation_delay;
+ uint8_t gptp_domain_number;
+ uint8_t flags;
+ uint16_t msrp_mappings_count;
+ uint8_t msrp_mappings[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_get_as_path {
+ uint16_t descriptor_index;
+ uint16_t reserved;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_get_counters {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint32_t counters_valid;
+ uint8_t counters_block[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_reboot {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_start_operation {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t operation_id;
+ uint16_t operation_type;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem_operation_status {
+ uint16_t descriptor_type;
+ uint16_t descriptor_id;
+ uint16_t operation_id;
+ uint16_t percent_complete;
+} __attribute__ ((__packed__));
+
+struct avb_packet_aecp_aem {
+ struct avb_packet_aecp_header aecp;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned u:1;
+ unsigned cmd1:7;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned cmd1:7;
+ unsigned u:1;
+#endif
+ uint8_t cmd2;
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_AEM_SET_COMMAND_TYPE(p,v) ((p)->cmd1 = ((v) >> 8),(p)->cmd2 = (v))
+
+#define AVB_PACKET_AEM_GET_COMMAND_TYPE(p) ((p)->cmd1 << 8 | (p)->cmd2)
+
+int avb_aecp_aem_handle_command(struct aecp *aecp, const void *m, int len);
+int avb_aecp_aem_handle_response(struct aecp *aecp, const void *m, int len);
+
+#endif /* AVB_AEM_H */
diff --git a/src/modules/module-avb/aecp.c b/src/modules/module-avb/aecp.c
new file mode 100644
index 0000000..d581f81
--- /dev/null
+++ b/src/modules/module-avb/aecp.c
@@ -0,0 +1,168 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "aecp.h"
+#include "aecp-aem.h"
+#include "internal.h"
+
+static const uint8_t mac[6] = AVB_BROADCAST_MAC;
+
+struct msg_info {
+ uint16_t type;
+ const char *name;
+ int (*handle) (struct aecp *aecp, const void *p, int len);
+};
+
+static int reply_not_implemented(struct aecp *aecp, const void *p, int len)
+{
+ struct server *server = aecp->server;
+ uint8_t buf[len];
+ struct avb_ethernet_header *h = (void*)buf;
+ struct avb_packet_aecp_header *reply = SPA_PTROFF(h, sizeof(*h), void);
+
+ memcpy(h, p, len);
+ AVB_PACKET_AECP_SET_STATUS(reply, AVB_AECP_STATUS_NOT_IMPLEMENTED);
+
+ return avb_server_send_packet(server, h->src, AVB_TSN_ETH, buf, len);
+}
+
+static const struct msg_info msg_info[] = {
+ { AVB_AECP_MESSAGE_TYPE_AEM_COMMAND, "aem-command", avb_aecp_aem_handle_command, },
+ { AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE, "aem-response", avb_aecp_aem_handle_response, },
+ { AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_COMMAND, "address-access-command", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_RESPONSE, "address-access-response", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_AVC_COMMAND, "avc-command", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_AVC_RESPONSE, "avc-response", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_COMMAND, "vendor-unique-command", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_RESPONSE, "vendor-unique-response", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_EXTENDED_COMMAND, "extended-command", NULL, },
+ { AVB_AECP_MESSAGE_TYPE_EXTENDED_RESPONSE, "extended-response", NULL, },
+};
+
+static inline const struct msg_info *find_msg_info(uint16_t type, const char *name)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(msg_info, i) {
+ if ((name == NULL && type == i->type) ||
+ (name != NULL && spa_streq(name, i->name)))
+ return i;
+ }
+ return NULL;
+}
+
+static int aecp_message(void *data, uint64_t now, const void *message, int len)
+{
+ struct aecp *aecp = data;
+ struct server *server = aecp->server;
+ const struct avb_ethernet_header *h = message;
+ const struct avb_packet_aecp_header *p = SPA_PTROFF(h, sizeof(*h), void);
+ const struct msg_info *info;
+ int message_type;
+
+ if (ntohs(h->type) != AVB_TSN_ETH)
+ return 0;
+ if (memcmp(h->dest, mac, 6) != 0 &&
+ memcmp(h->dest, server->mac_addr, 6) != 0)
+ return 0;
+ if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_AECP)
+ return 0;
+
+ message_type = AVB_PACKET_AECP_GET_MESSAGE_TYPE(p);
+
+ info = find_msg_info(message_type, NULL);
+ if (info == NULL)
+ return reply_not_implemented(aecp, message, len);
+
+ pw_log_debug("got AECP message %s", info->name);
+
+ if (info->handle == NULL)
+ return reply_not_implemented(aecp, message, len);
+
+ return info->handle(aecp, message, len);
+}
+
+static void aecp_destroy(void *data)
+{
+ struct aecp *aecp = data;
+ spa_hook_remove(&aecp->server_listener);
+ free(aecp);
+}
+
+static int do_help(struct aecp *aecp, const char *args, FILE *out)
+{
+ fprintf(out, "{ \"type\": \"help\","
+ "\"text\": \""
+ "/adp/help: this help \\n"
+ "\" }");
+ return 0;
+}
+
+static int aecp_command(void *data, uint64_t now, const char *command, const char *args, FILE *out)
+{
+ struct aecp *aecp = data;
+ int res;
+
+ if (!spa_strstartswith(command, "/aecp/"))
+ return 0;
+
+ command += strlen("/aecp/");
+
+ if (spa_streq(command, "help"))
+ res = do_help(aecp, args, out);
+ else
+ res = -ENOTSUP;
+
+ return res;
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = aecp_destroy,
+ .message = aecp_message,
+ .command = aecp_command
+};
+
+struct avb_aecp *avb_aecp_register(struct server *server)
+{
+ struct aecp *aecp;
+
+ aecp = calloc(1, sizeof(*aecp));
+ if (aecp == NULL)
+ return NULL;
+
+ aecp->server = server;
+
+ avdecc_server_add_listener(server, &aecp->server_listener, &server_events, aecp);
+
+ return (struct avb_aecp*)aecp;
+}
+
+void avb_aecp_unregister(struct avb_aecp *aecp)
+{
+ aecp_destroy(aecp);
+}
diff --git a/src/modules/module-avb/aecp.h b/src/modules/module-avb/aecp.h
new file mode 100644
index 0000000..a3515f0
--- /dev/null
+++ b/src/modules/module-avb/aecp.h
@@ -0,0 +1,60 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_AECP_H
+#define AVB_AECP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_AECP_MESSAGE_TYPE_AEM_COMMAND 0
+#define AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE 1
+#define AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_COMMAND 2
+#define AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_RESPONSE 3
+#define AVB_AECP_MESSAGE_TYPE_AVC_COMMAND 4
+#define AVB_AECP_MESSAGE_TYPE_AVC_RESPONSE 5
+#define AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_COMMAND 6
+#define AVB_AECP_MESSAGE_TYPE_VENDOR_UNIQUE_RESPONSE 7
+#define AVB_AECP_MESSAGE_TYPE_EXTENDED_COMMAND 14
+#define AVB_AECP_MESSAGE_TYPE_EXTENDED_RESPONSE 15
+
+#define AVB_AECP_STATUS_SUCCESS 0
+#define AVB_AECP_STATUS_NOT_IMPLEMENTED 1
+
+struct avb_packet_aecp_header {
+ struct avb_packet_header hdr;
+ uint64_t target_guid;
+ uint64_t controller_guid;
+ uint16_t sequence_id;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_AECP_SET_MESSAGE_TYPE(p,v) AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_AECP_SET_STATUS(p,v) AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+
+#define AVB_PACKET_AECP_GET_MESSAGE_TYPE(p) AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_AECP_GET_STATUS(p) AVB_PACKET_GET_SUB2(&(p)->hdr)
+
+struct avb_aecp *avb_aecp_register(struct server *server);
+
+#endif /* AVB_AECP_H */
diff --git a/src/modules/module-avb/avb.c b/src/modules/module-avb/avb.c
new file mode 100644
index 0000000..2afdc21
--- /dev/null
+++ b/src/modules/module-avb/avb.c
@@ -0,0 +1,106 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "internal.h"
+
+#include <spa/support/cpu.h>
+
+struct pw_avb *pw_avb_new(struct pw_context *context,
+ struct pw_properties *props, size_t user_data_size)
+{
+ struct impl *impl;
+ const struct spa_support *support;
+ uint32_t n_support;
+ struct spa_cpu *cpu;
+ const char *str;
+ int res = 0;
+
+ impl = calloc(1, sizeof(*impl) + user_data_size);
+ if (impl == NULL)
+ goto error_exit;
+
+ if (props == NULL)
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ goto error_free;
+
+ support = pw_context_get_support(context, &n_support);
+ cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+
+ pw_context_conf_update_props(context, "avb.properties", props);
+
+ if ((str = pw_properties_get(props, "vm.overrides")) != NULL) {
+ if (cpu != NULL && spa_cpu_get_vm_type(cpu) != SPA_CPU_VM_NONE)
+ pw_properties_update_string(props, str, strlen(str));
+ pw_properties_set(props, "vm.overrides", NULL);
+ }
+
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+ impl->props = props;
+ impl->core = pw_context_get_object(context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error_free;
+ }
+
+ spa_list_init(&impl->servers);
+
+ avdecc_server_new(impl, &props->dict);
+
+ return (struct pw_avb*)impl;
+
+error_free:
+ free(impl);
+error_exit:
+ pw_properties_free(props);
+ if (res < 0)
+ errno = -res;
+ return NULL;
+}
+
+static void impl_free(struct impl *impl)
+{
+ struct server *s;
+
+ spa_list_consume(s, &impl->servers, link)
+ avdecc_server_free(s);
+ free(impl);
+}
+
+void pw_avb_destroy(struct pw_avb *avb)
+{
+ struct impl *impl = (struct impl*)avb;
+ impl_free(impl);
+}
diff --git a/src/modules/module-avb/avb.h b/src/modules/module-avb/avb.h
new file mode 100644
index 0000000..cad7dd2
--- /dev/null
+++ b/src/modules/module-avb/avb.h
@@ -0,0 +1,44 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_AVB_H
+#define PIPEWIRE_AVB_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct pw_context;
+struct pw_properties;
+struct pw_avb;
+
+struct pw_avb *pw_avb_new(struct pw_context *context,
+ struct pw_properties *props, size_t user_data_size);
+void pw_avb_destroy(struct pw_avb *avb);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_AVB_H */
diff --git a/src/modules/module-avb/avdecc.c b/src/modules/module-avb/avdecc.c
new file mode 100644
index 0000000..308ba48
--- /dev/null
+++ b/src/modules/module-avb/avdecc.c
@@ -0,0 +1,335 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/filter.h>
+#include <linux/net_tstamp.h>
+#include <limits.h>
+#include <net/if.h>
+#include <arpa/inet.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include <spa/support/cpu.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "avb.h"
+#include "packets.h"
+#include "internal.h"
+#include "stream.h"
+#include "acmp.h"
+#include "adp.h"
+#include "aecp.h"
+#include "maap.h"
+#include "mmrp.h"
+#include "msrp.h"
+#include "mvrp.h"
+#include "descriptors.h"
+#include "utils.h"
+
+#define DEFAULT_INTERVAL 1
+
+#define server_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct server_events, m, v, ##__VA_ARGS__)
+#define server_emit_destroy(s) server_emit(s, destroy, 0)
+#define server_emit_message(s,n,m,l) server_emit(s, message, 0, n, m, l)
+#define server_emit_periodic(s,n) server_emit(s, periodic, 0, n)
+#define server_emit_command(s,n,c,a,f) server_emit(s, command, 0, n, c, a, f)
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+ struct server *server = data;
+ struct timespec now;
+ clock_gettime(CLOCK_REALTIME, &now);
+ server_emit_periodic(server, SPA_TIMESPEC_TO_NSEC(&now));
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct server *server = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ server_emit_message(server, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+
+int avb_server_send_packet(struct server *server, const uint8_t dest[6],
+ uint16_t type, void *data, size_t size)
+{
+ struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data;
+ int res = 0;
+
+ memcpy(hdr->dest, dest, ETH_ALEN);
+ memcpy(hdr->src, server->mac_addr, ETH_ALEN);
+ hdr->type = htons(type);
+
+ if (send(server->source->fd, data, size, 0) < 0) {
+ res = -errno;
+ pw_log_warn("got send error: %m");
+ }
+ return res;
+}
+
+static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_t mac[6])
+{
+ struct sock_fprog filter;
+ struct sock_filter bpf_code[] = {
+ BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 12),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, eth, 0, 8),
+ BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 2),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, (dest[2] << 24) |
+ (dest[3] << 16) |
+ (dest[4] << 8) |
+ (dest[5]), 0, 2),
+ BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 0),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, (dest[0] << 8) |
+ (dest[1]), 3, 4),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, (mac[2] << 24) |
+ (mac[3] << 16) |
+ (mac[4] << 8) |
+ (mac[5]), 0, 3),
+ BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 0),
+ BPF_JUMP(BPF_JMP|BPF_JEQ, (mac[0] << 8) |
+ (mac[1]), 0, 1),
+ BPF_STMT(BPF_RET, 0x00040000),
+ BPF_STMT(BPF_RET, 0x00000000),
+ };
+ filter.len = sizeof(bpf_code) / 8;
+ filter.filter = bpf_code;
+
+ if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER,
+ &filter, sizeof(filter)) < 0) {
+ pw_log_error("setsockopt(ATTACH_FILTER) failed: %m");
+ return -errno;
+ }
+ return 0;
+}
+
+int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6])
+{
+ int fd, res;
+ struct ifreq req;
+ struct packet_mreq mreq;
+ struct sockaddr_ll sll;
+
+ fd = socket(AF_PACKET, SOCK_RAW|SOCK_NONBLOCK, htons(ETH_P_ALL));
+ if (fd < 0) {
+ pw_log_error("socket() failed: %m");
+ return -errno;
+ }
+
+ spa_zero(req);
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+ if (ioctl(fd, SIOCGIFINDEX, &req) < 0) {
+ res = -errno;
+ pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
+ goto error_close;
+ }
+ server->ifindex = req.ifr_ifindex;
+
+ spa_zero(req);
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+ if (ioctl(fd, SIOCGIFHWADDR, &req) < 0) {
+ res = -errno;
+ pw_log_error("SIOCGIFHWADDR %s failed: %m", server->ifname);
+ goto error_close;
+ }
+ memcpy(server->mac_addr, req.ifr_hwaddr.sa_data, sizeof(server->mac_addr));
+
+ server->entity_id = (uint64_t)server->mac_addr[0] << 56 |
+ (uint64_t)server->mac_addr[1] << 48 |
+ (uint64_t)server->mac_addr[2] << 40 |
+ (uint64_t)0xff << 32 |
+ (uint64_t)0xfe << 24 |
+ (uint64_t)server->mac_addr[3] << 16 |
+ (uint64_t)server->mac_addr[4] << 8 |
+ (uint64_t)server->mac_addr[5];
+
+ spa_zero(sll);
+ sll.sll_family = AF_PACKET;
+ sll.sll_protocol = htons(ETH_P_ALL);
+ sll.sll_ifindex = server->ifindex;
+ if (bind(fd, (struct sockaddr *) &sll, sizeof(sll)) < 0) {
+ res = -errno;
+ pw_log_error("bind() failed: %m");
+ goto error_close;
+ }
+
+ spa_zero(mreq);
+ mreq.mr_ifindex = server->ifindex;
+ mreq.mr_type = PACKET_MR_MULTICAST;
+ mreq.mr_alen = ETH_ALEN;
+ memcpy(mreq.mr_address, mac, ETH_ALEN);
+
+ if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
+ &mreq, sizeof(mreq)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
+ goto error_close;
+ }
+
+ if ((res = load_filter(fd, type, mac, server->mac_addr)) < 0)
+ goto error_close;
+
+ return fd;
+
+error_close:
+ close(fd);
+ return res;
+}
+
+static int setup_socket(struct server *server)
+{
+ struct impl *impl = server->impl;
+ int fd, res;
+ static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
+ struct timespec value, interval;
+
+ fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac);
+ if (fd < 0)
+ return fd;
+
+ pw_log_info("0x%"PRIx64" %d", server->entity_id, server->ifindex);
+
+ server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_socket_data, server);
+ if (server->source == NULL) {
+ res = -errno;
+ pw_log_error("server %p: can't create server source: %m", impl);
+ goto error_no_source;
+ }
+ server->timer = pw_loop_add_timer(impl->loop, on_timer_event, server);
+ if (server->timer == NULL) {
+ res = -errno;
+ pw_log_error("server %p: can't create timer source: %m", impl);
+ goto error_no_timer;
+ }
+ value.tv_sec = 0;
+ value.tv_nsec = 1;
+ interval.tv_sec = DEFAULT_INTERVAL;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->loop, server->timer, &value, &interval, false);
+
+ return 0;
+
+error_no_timer:
+ pw_loop_destroy_source(impl->loop, server->source);
+ server->source = NULL;
+error_no_source:
+ close(fd);
+ return res;
+}
+
+struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
+{
+ struct server *server;
+ int res = 0;
+
+ server = calloc(1, sizeof(*server));
+ if (server == NULL)
+ return NULL;
+
+ server->impl = impl;
+ spa_list_append(&impl->servers, &server->link);
+ server->ifname = strdup(spa_dict_lookup(props, "ifname"));
+ spa_hook_list_init(&server->listener_list);
+ spa_list_init(&server->descriptors);
+ spa_list_init(&server->streams);
+
+ server->debug_messages = false;
+
+ if ((res = setup_socket(server)) < 0)
+ goto error_free;
+
+ init_descriptors(server);
+
+ server->mrp = avb_mrp_new(server);
+ if (server->mrp == NULL)
+ goto error_free;
+
+ avb_aecp_register(server);
+ server->maap = avb_maap_register(server);
+ server->mmrp = avb_mmrp_register(server);
+ server->msrp = avb_msrp_register(server);
+ server->mvrp = avb_mvrp_register(server);
+ avb_adp_register(server);
+ avb_acmp_register(server);
+
+ server->domain_attr = avb_msrp_attribute_new(server->msrp,
+ AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN);
+ server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT;
+ server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT;
+ server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN);
+
+ avb_mrp_attribute_begin(server->domain_attr->mrp, 0);
+ avb_mrp_attribute_join(server->domain_attr->mrp, 0, true);
+
+ server_create_stream(server, SPA_DIRECTION_INPUT, 0);
+ server_create_stream(server, SPA_DIRECTION_OUTPUT, 0);
+
+ avb_maap_reserve(server->maap, 1);
+
+ return server;
+
+error_free:
+ free(server);
+ if (res < 0)
+ errno = -res;
+ return NULL;
+}
+
+void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
+ const struct server_events *events, void *data)
+{
+ spa_hook_list_append(&server->listener_list, listener, events, data);
+}
+
+void avdecc_server_free(struct server *server)
+{
+ struct impl *impl = server->impl;
+
+ spa_list_remove(&server->link);
+ if (server->source)
+ pw_loop_destroy_source(impl->loop, server->source);
+ if (server->timer)
+ pw_loop_destroy_source(impl->loop, server->source);
+ spa_hook_list_clean(&server->listener_list);
+ free(server);
+}
diff --git a/src/modules/module-avb/descriptors.h b/src/modules/module-avb/descriptors.h
new file mode 100644
index 0000000..56397e3
--- /dev/null
+++ b/src/modules/module-avb/descriptors.h
@@ -0,0 +1,274 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "aecp-aem.h"
+#include "aecp-aem-descriptors.h"
+#include "internal.h"
+
+void init_descriptors(struct server *server)
+{
+ server_add_descriptor(server, AVB_AEM_DESC_STRINGS, 0,
+ sizeof(struct avb_aem_desc_strings),
+ &(struct avb_aem_desc_strings)
+ {
+ .string_0 = "PipeWire",
+ .string_1 = "Configuration 1",
+ .string_2 = "Wim Taymans",
+ });
+ server_add_descriptor(server, AVB_AEM_DESC_LOCALE, 0,
+ sizeof(struct avb_aem_desc_locale),
+ &(struct avb_aem_desc_locale)
+ {
+ .locale_identifier = "en-EN",
+ .number_of_strings = htons(1),
+ .base_strings = htons(0)
+ });
+ server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0,
+ sizeof(struct avb_aem_desc_entity),
+ &(struct avb_aem_desc_entity)
+ {
+ .entity_id = htobe64(server->entity_id),
+ .entity_model_id = htobe64(0),
+ .entity_capabilities = htonl(
+ AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED |
+ AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED |
+ AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED |
+ AVB_ADP_ENTITY_CAPABILITY_AEM_IDENTIFY_CONTROL_INDEX_VALID |
+ AVB_ADP_ENTITY_CAPABILITY_AEM_INTERFACE_INDEX_VALID),
+
+ .talker_stream_sources = htons(8),
+ .talker_capabilities = htons(
+ AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED |
+ AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE),
+ .listener_stream_sinks = htons(8),
+ .listener_capabilities = htons(
+ AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED |
+ AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK),
+ .controller_capabilities = htons(0),
+ .available_index = htonl(0),
+ .association_id = htobe64(0),
+ .entity_name = "PipeWire",
+ .vendor_name_string = htons(2),
+ .model_name_string = htons(0),
+ .firmware_version = "0.3.48",
+ .group_name = "",
+ .serial_number = "",
+ .configurations_count = htons(1),
+ .current_configuration = htons(0)
+ });
+ struct {
+ struct avb_aem_desc_configuration desc;
+ struct avb_aem_desc_descriptor_count descriptor_counts[8];
+ } __attribute__ ((__packed__)) config =
+ {
+ {
+ .object_name = "Configuration 1",
+ .localized_description = htons(1),
+ .descriptor_counts_count = htons(8),
+ .descriptor_counts_offset = htons(
+ 4 + sizeof(struct avb_aem_desc_configuration)),
+ },
+ .descriptor_counts = {
+ { htons(AVB_AEM_DESC_AUDIO_UNIT), htons(1) },
+ { htons(AVB_AEM_DESC_STREAM_INPUT), htons(1) },
+ { htons(AVB_AEM_DESC_STREAM_OUTPUT), htons(1) },
+ { htons(AVB_AEM_DESC_AVB_INTERFACE), htons(1) },
+ { htons(AVB_AEM_DESC_CLOCK_SOURCE), htons(1) },
+ { htons(AVB_AEM_DESC_CONTROL), htons(2) },
+ { htons(AVB_AEM_DESC_LOCALE), htons(1) },
+ { htons(AVB_AEM_DESC_CLOCK_DOMAIN), htons(1) }
+ }
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_CONFIGURATION, 0,
+ sizeof(config), &config);
+
+ struct {
+ struct avb_aem_desc_audio_unit desc;
+ struct avb_aem_desc_sampling_rate sampling_rates[6];
+ } __attribute__ ((__packed__)) audio_unit =
+ {
+ {
+ .object_name = "PipeWire",
+ .localized_description = htons(0),
+ .clock_domain_index = htons(0),
+ .number_of_stream_input_ports = htons(1),
+ .base_stream_input_port = htons(0),
+ .number_of_stream_output_ports = htons(1),
+ .base_stream_output_port = htons(0),
+ .number_of_external_input_ports = htons(8),
+ .base_external_input_port = htons(0),
+ .number_of_external_output_ports = htons(8),
+ .base_external_output_port = htons(0),
+ .number_of_internal_input_ports = htons(0),
+ .base_internal_input_port = htons(0),
+ .number_of_internal_output_ports = htons(0),
+ .base_internal_output_port = htons(0),
+ .number_of_controls = htons(0),
+ .base_control = htons(0),
+ .number_of_signal_selectors = htons(0),
+ .base_signal_selector = htons(0),
+ .number_of_mixers = htons(0),
+ .base_mixer = htons(0),
+ .number_of_matrices = htons(0),
+ .base_matrix = htons(0),
+ .number_of_splitters = htons(0),
+ .base_splitter = htons(0),
+ .number_of_combiners = htons(0),
+ .base_combiner = htons(0),
+ .number_of_demultiplexers = htons(0),
+ .base_demultiplexer = htons(0),
+ .number_of_multiplexers = htons(0),
+ .base_multiplexer = htons(0),
+ .number_of_transcoders = htons(0),
+ .base_transcoder = htons(0),
+ .number_of_control_blocks = htons(0),
+ .base_control_block = htons(0),
+ .current_sampling_rate = htonl(48000),
+ .sampling_rates_offset = htons(
+ 4 + sizeof(struct avb_aem_desc_audio_unit)),
+ .sampling_rates_count = htons(6),
+ },
+ .sampling_rates = {
+ { .pull_frequency = htonl(44100) },
+ { .pull_frequency = htonl(48000) },
+ { .pull_frequency = htonl(88200) },
+ { .pull_frequency = htonl(96000) },
+ { .pull_frequency = htonl(176400) },
+ { .pull_frequency = htonl(192000) },
+ }
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_AUDIO_UNIT, 0,
+ sizeof(audio_unit), &audio_unit);
+
+ struct {
+ struct avb_aem_desc_stream desc;
+ uint64_t stream_formats[6];
+ } __attribute__ ((__packed__)) stream_input_0 =
+ {
+ {
+ .object_name = "Stream Input 1",
+ .localized_description = htons(0xffff),
+ .clock_domain_index = htons(0),
+ .stream_flags = htons(
+ AVB_AEM_DESC_STREAM_FLAG_SYNC_SOURCE |
+ AVB_AEM_DESC_STREAM_FLAG_CLASS_A),
+ .current_format = htobe64(0x00a0020840000800ULL),
+ .formats_offset = htons(
+ 4 + sizeof(struct avb_aem_desc_stream)),
+ .number_of_formats = htons(6),
+ .backup_talker_entity_id_0 = htobe64(0),
+ .backup_talker_unique_id_0 = htons(0),
+ .backup_talker_entity_id_1 = htobe64(0),
+ .backup_talker_unique_id_1 = htons(0),
+ .backup_talker_entity_id_2 = htobe64(0),
+ .backup_talker_unique_id_2 = htons(0),
+ .backedup_talker_entity_id = htobe64(0),
+ .backedup_talker_unique = htons(0),
+ .avb_interface_index = htons(0),
+ .buffer_length = htons(8)
+ },
+ .stream_formats = {
+ htobe64(0x00a0010860000800ULL),
+ htobe64(0x00a0020860000800ULL),
+ htobe64(0x00a0030860000800ULL),
+ htobe64(0x00a0040860000800ULL),
+ htobe64(0x00a0050860000800ULL),
+ htobe64(0x00a0060860000800ULL),
+ },
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_STREAM_INPUT, 0,
+ sizeof(stream_input_0), &stream_input_0);
+
+ struct {
+ struct avb_aem_desc_stream desc;
+ uint64_t stream_formats[6];
+ } __attribute__ ((__packed__)) stream_output_0 =
+ {
+ {
+ .object_name = "Stream Output 1",
+ .localized_description = htons(0xffff),
+ .clock_domain_index = htons(0),
+ .stream_flags = htons(
+ AVB_AEM_DESC_STREAM_FLAG_CLASS_A),
+ .current_format = htobe64(0x00a0020840000800ULL),
+ .formats_offset = htons(
+ 4 + sizeof(struct avb_aem_desc_stream)),
+ .number_of_formats = htons(6),
+ .backup_talker_entity_id_0 = htobe64(0),
+ .backup_talker_unique_id_0 = htons(0),
+ .backup_talker_entity_id_1 = htobe64(0),
+ .backup_talker_unique_id_1 = htons(0),
+ .backup_talker_entity_id_2 = htobe64(0),
+ .backup_talker_unique_id_2 = htons(0),
+ .backedup_talker_entity_id = htobe64(0),
+ .backedup_talker_unique = htons(0),
+ .avb_interface_index = htons(0),
+ .buffer_length = htons(8)
+ },
+ .stream_formats = {
+ htobe64(0x00a0010860000800ULL),
+ htobe64(0x00a0020860000800ULL),
+ htobe64(0x00a0030860000800ULL),
+ htobe64(0x00a0040860000800ULL),
+ htobe64(0x00a0050860000800ULL),
+ htobe64(0x00a0060860000800ULL),
+ },
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_STREAM_OUTPUT, 0,
+ sizeof(stream_output_0), &stream_output_0);
+
+ struct avb_aem_desc_avb_interface avb_interface = {
+ .localized_description = htons(0xffff),
+ .interface_flags = htons(
+ AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_GRANDMASTER_SUPPORTED),
+ .clock_identity = htobe64(0),
+ .priority1 = 0,
+ .clock_class = 0,
+ .offset_scaled_log_variance = htons(0),
+ .clock_accuracy = 0,
+ .priority2 = 0,
+ .domain_number = 0,
+ .log_sync_interval = 0,
+ .log_announce_interval = 0,
+ .log_pdelay_interval = 0,
+ .port_number = 0,
+ };
+ strncpy(avb_interface.object_name, server->ifname, 63);
+ memcpy(avb_interface.mac_address, server->mac_addr, 6);
+ server_add_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0,
+ sizeof(avb_interface), &avb_interface);
+
+ struct avb_aem_desc_clock_source clock_source = {
+ .object_name = "Stream Clock",
+ .localized_description = htons(0xffff),
+ .clock_source_flags = htons(0),
+ .clock_source_type = htons(
+ AVB_AEM_DESC_CLOCK_SOURCE_TYPE_INPUT_STREAM),
+ .clock_source_identifier = htobe64(0),
+ .clock_source_location_type = htons(AVB_AEM_DESC_STREAM_INPUT),
+ .clock_source_location_index = htons(0),
+ };
+ server_add_descriptor(server, AVB_AEM_DESC_CLOCK_SOURCE, 0,
+ sizeof(clock_source), &clock_source);
+}
diff --git a/src/modules/module-avb/iec61883.h b/src/modules/module-avb/iec61883.h
new file mode 100644
index 0000000..6ca8724
--- /dev/null
+++ b/src/modules/module-avb/iec61883.h
@@ -0,0 +1,110 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_IEC61883_H
+#define AVB_IEC61883_H
+
+#include "packets.h"
+
+struct avb_packet_iec61883 {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1;
+ unsigned version:3;
+ unsigned mr:1;
+ unsigned _r1:1;
+ unsigned gv:1;
+ unsigned tv:1;
+
+ uint8_t seq_num;
+
+ unsigned _r2:7;
+ unsigned tu:1;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned tv:1;
+ unsigned gv:1;
+ unsigned _r1:1;
+ unsigned mr:1;
+ unsigned version:3;
+ unsigned sv:1;
+
+ uint8_t seq_num;
+
+ unsigned tu:1;
+ unsigned _r2:7;
+#endif
+ uint64_t stream_id;
+ uint32_t timestamp;
+ uint32_t gateway_info;
+ uint16_t data_len;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ uint8_t tag:2;
+ uint8_t channel:6;
+
+ uint8_t tcode:4;
+ uint8_t app:4;
+
+ uint8_t qi1:2; /* CIP Quadlet Indicator 1 */
+ uint8_t sid:6; /* CIP Source ID */
+
+ uint8_t dbs; /* CIP Data Block Size */
+
+ uint8_t fn:2; /* CIP Fraction Number */
+ uint8_t qpc:3; /* CIP Quadlet Padding Count */
+ uint8_t sph:1; /* CIP Source Packet Header */
+ uint8_t _r3:2;
+
+ uint8_t dbc; /* CIP Data Block Continuity */
+
+ uint8_t qi2:2; /* CIP Quadlet Indicator 2 */
+ uint8_t format_id:6; /* CIP Format ID */
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ uint8_t channel:6;
+ uint8_t tag:2;
+
+ uint8_t app:4;
+ uint8_t tcode:4;
+
+ uint8_t sid:6; /* CIP Source ID */
+ uint8_t qi1:2; /* CIP Quadlet Indicator 1 */
+
+ uint8_t dbs; /* CIP Data Block Size */
+
+ uint8_t _r3:2;
+ uint8_t sph:1; /* CIP Source Packet Header */
+ uint8_t qpc:3; /* CIP Quadlet Padding Count */
+ uint8_t fn:2; /* CIP Fraction Number */
+
+ uint8_t dbc; /* CIP Data Block Continuity */
+
+ uint8_t format_id:6; /* CIP Format ID */
+ uint8_t qi2:2; /* CIP Quadlet Indicator 2 */
+#endif
+ uint8_t fdf; /* CIP Format Dependent Field */
+ uint16_t syt;
+
+ uint8_t payload[0];
+} __attribute__ ((__packed__));
+
+#endif /* AVB_IEC61883_H */
diff --git a/src/modules/module-avb/internal.h b/src/modules/module-avb/internal.h
new file mode 100644
index 0000000..9d29d92
--- /dev/null
+++ b/src/modules/module-avb/internal.h
@@ -0,0 +1,166 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_INTERNAL_H
+#define AVB_INTERNAL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <pipewire/pipewire.h>
+
+struct server;
+struct avb_mrp;
+
+#define AVB_TSN_ETH 0x22f0
+#define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 };
+
+struct impl {
+ struct pw_loop *loop;
+ struct pw_context *context;
+ struct spa_hook context_listener;
+ struct pw_core *core;
+ unsigned do_disconnect:1;
+
+ struct pw_properties *props;
+
+ struct spa_list servers;
+};
+
+struct server_events {
+#define AVB_VERSION_SERVER_EVENTS 0
+ uint32_t version;
+
+ /** the server is destroyed */
+ void (*destroy) (void *data);
+
+ int (*message) (void *data, uint64_t now, const void *message, int len);
+
+ void (*periodic) (void *data, uint64_t now);
+
+ int (*command) (void *data, uint64_t now, const char *command, const char *args, FILE *out);
+};
+
+struct descriptor {
+ struct spa_list link;
+ uint16_t type;
+ uint16_t index;
+ uint32_t size;
+ void *ptr;
+};
+
+struct server {
+ struct spa_list link;
+ struct impl *impl;
+
+ char *ifname;
+ uint8_t mac_addr[6];
+ uint64_t entity_id;
+ int ifindex;
+
+ struct spa_source *source;
+ struct spa_source *timer;
+
+ struct spa_hook_list listener_list;
+
+ struct spa_list descriptors;
+ struct spa_list streams;
+
+ unsigned debug_messages:1;
+
+ struct avb_mrp *mrp;
+ struct avb_mmrp *mmrp;
+ struct avb_mvrp *mvrp;
+ struct avb_msrp *msrp;
+ struct avb_maap *maap;
+
+ struct avb_msrp_attribute *domain_attr;
+};
+
+#include "stream.h"
+
+static inline const struct descriptor *server_find_descriptor(struct server *server,
+ uint16_t type, uint16_t index)
+{
+ struct descriptor *d;
+ spa_list_for_each(d, &server->descriptors, link) {
+ if (d->type == type &&
+ d->index == index)
+ return d;
+ }
+ return NULL;
+}
+static inline void *server_add_descriptor(struct server *server,
+ uint16_t type, uint16_t index, size_t size, void *ptr)
+{
+ struct descriptor *d;
+
+ if ((d = calloc(1, sizeof(struct descriptor) + size)) == NULL)
+ return NULL;
+
+ d->type = type;
+ d->index = index;
+ d->size = size;
+ d->ptr = SPA_PTROFF(d, sizeof(struct descriptor), void);
+ if (ptr)
+ memcpy(d->ptr, ptr, size);
+ spa_list_append(&server->descriptors, &d->link);
+ return d->ptr;
+}
+
+static inline struct stream *server_find_stream(struct server *server,
+ enum spa_direction direction, uint16_t index)
+{
+ struct stream *s;
+ spa_list_for_each(s, &server->streams, link) {
+ if (s->direction == direction &&
+ s->index == index)
+ return s;
+ }
+ return NULL;
+}
+
+struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props);
+void avdecc_server_free(struct server *server);
+
+void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
+ const struct server_events *events, void *data);
+
+int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]);
+
+int avb_server_send_packet(struct server *server, const uint8_t dest[6],
+ uint16_t type, void *data, size_t size);
+
+struct aecp {
+ struct server *server;
+ struct spa_hook server_listener;
+};
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* AVB_INTERNAL_H */
diff --git a/src/modules/module-avb/maap.c b/src/modules/module-avb/maap.c
new file mode 100644
index 0000000..7d195be
--- /dev/null
+++ b/src/modules/module-avb/maap.c
@@ -0,0 +1,469 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+
+#include "utils.h"
+#include "maap.h"
+
+#define MAAP_ALLOCATION_POOL_SIZE 0xFE00
+#define MAAP_ALLOCATION_POOL_BASE { 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 }
+static uint8_t maap_base[6] = MAAP_ALLOCATION_POOL_BASE;
+
+#define MAAP_PROBE_RETRANSMITS 3
+
+#define MAAP_PROBE_INTERVAL_MS 500
+#define MAAP_PROBE_INTERVAL_VAR_MS 100
+
+#define MAAP_ANNOUNCE_INTERVAL_MS 3000
+#define MAAP_ANNOUNCE_INTERVAL_VAR_MS 2000
+
+struct maap {
+ struct server *server;
+ struct spa_hook server_listener;
+
+ struct pw_properties *props;
+
+ struct spa_source *source;
+
+#define STATE_IDLE 0
+#define STATE_PROBE 1
+#define STATE_ANNOUNCE 2
+ uint32_t state;
+ uint64_t timeout;
+ uint32_t probe_count;
+
+ unsigned short xsubi[3];
+
+ uint16_t offset;
+ uint16_t count;
+};
+
+static const char *message_type_as_string(uint8_t message_type)
+{
+ switch (message_type) {
+ case AVB_MAAP_MESSAGE_TYPE_PROBE:
+ return "PROBE";
+ case AVB_MAAP_MESSAGE_TYPE_DEFEND:
+ return "DEFEND";
+ case AVB_MAAP_MESSAGE_TYPE_ANNOUNCE:
+ return "ANNOUNCE";
+ }
+ return "INVALID";
+}
+
+static void maap_message_debug(struct maap *maap, const struct avb_packet_maap *p)
+{
+ uint32_t v;
+ const uint8_t *addr;
+
+ v = AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p);
+ pw_log_info("message-type: %d (%s)", v, message_type_as_string(v));
+ pw_log_info(" maap-version: %d", AVB_PACKET_MAAP_GET_MAAP_VERSION(p));
+ pw_log_info(" length: %d", AVB_PACKET_GET_LENGTH(&p->hdr));
+
+ pw_log_info(" stream-id: 0x%"PRIx64, AVB_PACKET_MAAP_GET_STREAM_ID(p));
+ addr = AVB_PACKET_MAAP_GET_REQUEST_START(p);
+ pw_log_info(" request-start: %02x:%02x:%02x:%02x:%02x:%02x",
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+ pw_log_info(" request-count: %d", AVB_PACKET_MAAP_GET_REQUEST_COUNT(p));
+ addr = AVB_PACKET_MAAP_GET_CONFLICT_START(p);
+ pw_log_info(" conflict-start: %02x:%02x:%02x:%02x:%02x:%02x",
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+ pw_log_info(" conflict-count: %d", AVB_PACKET_MAAP_GET_CONFLICT_COUNT(p));
+}
+
+#define PROBE_TIMEOUT(n) ((n) + (MAAP_PROBE_INTERVAL_MS + \
+ drand48() * MAAP_PROBE_INTERVAL_VAR_MS) * SPA_NSEC_PER_MSEC)
+#define ANNOUNCE_TIMEOUT(n) ((n) + (MAAP_ANNOUNCE_INTERVAL_MS + \
+ drand48() * MAAP_ANNOUNCE_INTERVAL_VAR_MS) * SPA_NSEC_PER_MSEC)
+
+static int make_new_address(struct maap *maap, uint64_t now, int range)
+{
+ maap->offset = nrand48(maap->xsubi) % (MAAP_ALLOCATION_POOL_SIZE - range);
+ maap->count = range;
+ maap->state = STATE_PROBE;
+ maap->probe_count = MAAP_PROBE_RETRANSMITS;
+ maap->timeout = PROBE_TIMEOUT(now);
+ return 0;
+}
+
+static uint16_t maap_check_conflict(struct maap *maap, const uint8_t request_start[6],
+ uint16_t request_count, uint8_t conflict_start[6])
+{
+ uint16_t our_start, our_end;
+ uint16_t req_start, req_end;
+ uint16_t conf_start, conf_count = 0;
+
+ if (memcmp(request_start, maap_base, 4) != 0)
+ return 0;
+
+ our_start = maap->offset;
+ our_end = our_start + maap->count;
+ req_start = request_start[4] << 8 | request_start[5];
+ req_end = req_start + request_count;
+
+ if (our_start >= req_start && our_start <= req_end) {
+ conf_start = our_start;
+ conf_count = SPA_MIN(our_end, req_end) - our_start;
+ }
+ else if (req_start >= our_start && req_start <= our_end) {
+ conf_start = req_start;
+ conf_count = SPA_MIN(req_end, our_end) - req_start;
+ }
+ if (conf_count == 0)
+ return 0;
+
+ conflict_start[4] = conf_start >> 8;
+ conflict_start[5] = conf_start;
+ return conf_count;
+}
+
+static int send_packet(struct maap *maap, uint64_t now,
+ uint8_t type, const uint8_t conflict_start[6], uint16_t conflict_count)
+{
+ struct avb_ethernet_header *h;
+ struct avb_packet_maap *p;
+ uint8_t buf[1024];
+ uint8_t bmac[6] = AVB_MAAP_MAC;
+ int res = 0;
+ uint8_t start[6];
+
+ spa_memzero(buf, sizeof(buf));
+ h = (void*)buf;
+ p = SPA_PTROFF(h, sizeof(*h), void);
+
+ memcpy(h->dest, bmac, 6);
+ memcpy(h->src, maap->server->mac_addr, 6);
+ h->type = htons(AVB_TSN_ETH);
+
+ p->hdr.subtype = AVB_SUBTYPE_MAAP;
+ AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p));
+
+ AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1);
+ AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, type);
+
+ memcpy(start, maap_base, 4);
+ start[4] = maap->offset >> 8;
+ start[5] = maap->offset;
+ AVB_PACKET_MAAP_SET_REQUEST_START(p, start);
+ AVB_PACKET_MAAP_SET_REQUEST_COUNT(p, maap->count);
+ if (conflict_count) {
+ AVB_PACKET_MAAP_SET_CONFLICT_START(p, conflict_start);
+ AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p, conflict_count);
+ }
+
+ if (maap->server->debug_messages) {
+ pw_log_info("send: %d (%s)", type, message_type_as_string(type));
+ maap_message_debug(maap, p);
+ }
+
+ if (send(maap->source->fd, p, sizeof(*h) + sizeof(*p), 0) < 0) {
+ res = -errno;
+ pw_log_warn("got send error: %m");
+ }
+ return res;
+}
+
+static int handle_probe(struct maap *maap, uint64_t now, const struct avb_packet_maap *p)
+{
+ uint8_t conflict_start[6];
+ uint16_t conflict_count;
+
+ conflict_count = maap_check_conflict(maap, p->request_start, ntohs(p->request_count),
+ conflict_start);
+ if (conflict_count == 0)
+ return 0;
+
+ switch (maap->state) {
+ case STATE_PROBE:
+ make_new_address(maap, now, 8);
+ break;
+ case STATE_ANNOUNCE:
+ send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_DEFEND, conflict_start, conflict_count);
+ break;
+ }
+ return 0;
+}
+
+static int handle_defend(struct maap *maap, uint64_t now, const struct avb_packet_maap *p)
+{
+ uint8_t conflict_start[6];
+ uint16_t conflict_count;
+
+ conflict_count = maap_check_conflict(maap, p->conflict_start, ntohs(p->conflict_count),
+ conflict_start);
+ if (conflict_count != 0)
+ make_new_address(maap, now, 8);
+ return 0;
+}
+
+static int maap_message(struct maap *maap, uint64_t now, const void *message, int len)
+{
+ const struct avb_packet_maap *p = message;
+
+ if (AVB_PACKET_GET_SUBTYPE(&p->hdr) != AVB_SUBTYPE_MAAP)
+ return 0;
+
+ if (maap->server->debug_messages)
+ maap_message_debug(maap, p);
+
+ switch (AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p)) {
+ case AVB_MAAP_MESSAGE_TYPE_PROBE:
+ handle_probe(maap, now, p);
+ break;
+ case AVB_MAAP_MESSAGE_TYPE_DEFEND:
+ case AVB_MAAP_MESSAGE_TYPE_ANNOUNCE:
+ handle_defend(maap, now, p);
+ break;
+ }
+ return 0;
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct maap *maap = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ maap_message(maap, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+
+static int load_state(struct maap *maap)
+{
+ const char *str;
+ char key[512];
+ struct spa_json it[3];
+ bool have_offset = false;
+ int count = 0, offset = 0;
+
+ snprintf(key, sizeof(key), "maap.%s", maap->server->ifname);
+ pw_conf_load_state("module-avb", key, maap->props);
+
+ if ((str = pw_properties_get(maap->props, "maap.addresses")) == NULL)
+ return 0;
+
+ spa_json_init(&it[0], str, strlen(str));
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ return 0;
+
+ if (spa_json_enter_object(&it[1], &it[2]) <= 0)
+ return 0;
+
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(&it[2], &val)) <= 0)
+ break;
+
+ if (spa_streq(key, "start")) {
+ uint8_t addr[6];
+ if (avb_utils_parse_addr(val, len, addr) >= 0 &&
+ memcmp(addr, maap_base, 4) == 0) {
+ offset = addr[4] << 8 | addr[5];
+ have_offset = true;
+ }
+ }
+ else if (spa_streq(key, "count")) {
+ spa_json_parse_int(val, len, &count);
+ }
+ }
+ if (count > 0 && have_offset) {
+ maap->count = count;
+ maap->offset = offset;
+ maap->state = STATE_PROBE;
+ maap->probe_count = MAAP_PROBE_RETRANSMITS;
+ maap->timeout = PROBE_TIMEOUT(0);
+ }
+ return 0;
+}
+
+static int save_state(struct maap *maap)
+{
+ char *ptr;
+ size_t size;
+ FILE *f;
+ char key[512];
+ uint32_t count;
+
+ if ((f = open_memstream(&ptr, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "[ ");
+ fprintf(f, "{ \"start\": \"%02x:%02x:%02x:%02x:%02x:%02x\", ",
+ maap_base[0], maap_base[1], maap_base[2],
+ maap_base[3], (maap->offset >> 8) & 0xff,
+ maap->offset & 0xff);
+ fprintf(f, " \"count\": %u } ", maap->count);
+ fprintf(f, "]");
+ fclose(f);
+
+ count = pw_properties_set(maap->props, "maap.addresses", ptr);
+ free(ptr);
+
+ if (count > 0) {
+ snprintf(key, sizeof(key), "maap.%s", maap->server->ifname);
+ pw_conf_save_state("module-avb", key, maap->props);
+ }
+ return 0;
+}
+
+static void maap_periodic(void *data, uint64_t now)
+{
+ struct maap *maap = data;
+
+ if (now < maap->timeout)
+ return;
+
+ switch(maap->state) {
+ case STATE_IDLE:
+ break;
+ case STATE_PROBE:
+ send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_PROBE, NULL, 0);
+ if (--maap->probe_count == 0) {
+ maap->state = STATE_ANNOUNCE;
+ save_state(maap);
+ }
+ maap->timeout = PROBE_TIMEOUT(now);
+ break;
+ case STATE_ANNOUNCE:
+ send_packet(maap, now, AVB_MAAP_MESSAGE_TYPE_ANNOUNCE, NULL, 0);
+ maap->timeout = ANNOUNCE_TIMEOUT(now);
+ break;
+ }
+}
+
+static void maap_free(struct maap *maap)
+{
+ pw_loop_destroy_source(maap->server->impl->loop, maap->source);
+ spa_hook_remove(&maap->server_listener);
+ pw_properties_free(maap->props);
+ free(maap);
+}
+
+static void maap_destroy(void *data)
+{
+ struct maap *maap = data;
+ maap_free(maap);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = maap_destroy,
+ .periodic = maap_periodic,
+};
+
+struct avb_maap *avb_maap_register(struct server *server)
+{
+ struct maap *maap;
+ uint8_t bmac[6] = AVB_MAAP_MAC;
+ int fd, res;
+
+ fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac);
+ if (fd < 0) {
+ res = fd;
+ goto error;
+ }
+
+ maap = calloc(1, sizeof(*maap));
+ if (maap == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+ maap->props = pw_properties_new(NULL, NULL);
+ if (maap->props == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ maap->server = server;
+ pw_log_info("0x%"PRIx64" %d", server->entity_id, server->ifindex);
+
+ if (pw_getrandom(maap->xsubi, sizeof(maap->xsubi), 0) != sizeof(maap->xsubi)) {
+ res = -errno;
+ goto error_free;
+ }
+ load_state(maap);
+
+ maap->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, maap);
+ if (maap->source == NULL) {
+ res = -errno;
+ pw_log_error("maap %p: can't create maap source: %m", maap);
+ goto error_free;
+ }
+ avdecc_server_add_listener(server, &maap->server_listener, &server_events, maap);
+
+ return (struct avb_maap *)maap;
+
+error_free:
+ free(maap);
+error_close:
+ close(fd);
+error:
+ errno = -res;
+ return NULL;
+}
+
+int avb_maap_reserve(struct avb_maap *m, uint32_t count)
+{
+ struct maap *maap = (struct maap*)m;
+ if (count > maap->count)
+ make_new_address(maap, 0, count);
+ return 0;
+}
+
+int avb_maap_get_address(struct avb_maap *m, uint8_t addr[6], uint32_t index)
+{
+ struct maap *maap = (struct maap*)m;
+ uint16_t offset;
+
+ if (maap->state != STATE_ANNOUNCE)
+ return -EAGAIN;
+
+ memcpy(addr, maap_base, 6);
+ offset = maap->offset + index;
+ addr[4] = offset >> 8;
+ addr[5] = offset;
+ return 0;
+}
diff --git a/src/modules/module-avb/maap.h b/src/modules/module-avb/maap.h
new file mode 100644
index 0000000..6e56f8e
--- /dev/null
+++ b/src/modules/module-avb/maap.h
@@ -0,0 +1,70 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MAAP_H
+#define AVB_MAAP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_TSN_ETH 0x22f0
+#define AVB_MAAP_MAC { 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 };
+
+#define AVB_MAAP_MESSAGE_TYPE_PROBE 1
+#define AVB_MAAP_MESSAGE_TYPE_DEFEND 2
+#define AVB_MAAP_MESSAGE_TYPE_ANNOUNCE 3
+
+struct avb_packet_maap {
+ struct avb_packet_header hdr;
+ uint64_t stream_id;
+ uint8_t request_start[6];
+ uint16_t request_count;
+ uint8_t conflict_start[6];
+ uint16_t conflict_count;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p,v) AVB_PACKET_SET_SUB1(&(p)->hdr, v)
+#define AVB_PACKET_MAAP_SET_MAAP_VERSION(p,v) AVB_PACKET_SET_SUB2(&(p)->hdr, v)
+#define AVB_PACKET_MAAP_SET_STREAM_ID(p,v) ((p)->stream_id = htobe64(v))
+#define AVB_PACKET_MAAP_SET_REQUEST_START(p,v) memcpy((p)->request_start, (v), 6)
+#define AVB_PACKET_MAAP_SET_REQUEST_COUNT(p,v) ((p)->request_count = htons(v))
+#define AVB_PACKET_MAAP_SET_CONFLICT_START(p,v) memcpy((p)->conflict_start, (v), 6)
+#define AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p,v) ((p)->conflict_count = htons(v))
+
+#define AVB_PACKET_MAAP_GET_MESSAGE_TYPE(p) AVB_PACKET_GET_SUB1(&(p)->hdr)
+#define AVB_PACKET_MAAP_GET_MAAP_VERSION(p) AVB_PACKET_GET_SUB2(&(p)->hdr)
+#define AVB_PACKET_MAAP_GET_STREAM_ID(p) be64toh((p)->stream_id)
+#define AVB_PACKET_MAAP_GET_REQUEST_START(p) ((p)->request_start)
+#define AVB_PACKET_MAAP_GET_REQUEST_COUNT(p) ntohs((p)->request_count)
+#define AVB_PACKET_MAAP_GET_CONFLICT_START(p) ((p)->conflict_start)
+#define AVB_PACKET_MAAP_GET_CONFLICT_COUNT(p) ntohs((p)->conflict_count)
+
+struct avb_maap;
+
+struct avb_maap *avb_maap_register(struct server *server);
+
+int avb_maap_reserve(struct avb_maap *maap, uint32_t count);
+int avb_maap_get_address(struct avb_maap *maap, uint8_t addr[6], uint32_t index);
+
+#endif /* AVB_MAAP_H */
diff --git a/src/modules/module-avb/mmrp.c b/src/modules/module-avb/mmrp.c
new file mode 100644
index 0000000..022aea8
--- /dev/null
+++ b/src/modules/module-avb/mmrp.c
@@ -0,0 +1,233 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+
+#include "utils.h"
+#include "mmrp.h"
+
+static const uint8_t mmrp_mac[6] = AVB_MMRP_MAC;
+
+struct attr {
+ struct avb_mmrp_attribute attr;
+ struct spa_list link;
+};
+
+struct mmrp {
+ struct server *server;
+ struct spa_hook server_listener;
+
+ struct spa_source *source;
+
+ struct spa_list attributes;
+};
+
+static bool mmrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+ const struct avb_packet_mmrp_msg *msg = hdr;
+ uint8_t attr_type = msg->attribute_type;
+
+ if (!AVB_MMRP_ATTRIBUTE_TYPE_VALID(attr_type))
+ return false;
+
+ *hdr_size = sizeof(*msg);
+ *has_params = false;
+ return true;
+}
+
+static int mmrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+ struct mmrp *mmrp = data;
+ struct attr *a;
+ spa_list_for_each(a, &mmrp->attributes, link)
+ if (a->attr.type == attribute_type)
+ avb_mrp_attribute_update_state(a->attr.mrp, now, event);
+ return 0;
+}
+
+static void debug_service_requirement(const struct avb_packet_mmrp_service_requirement *t)
+{
+ char buf[128];
+ pw_log_info("service requirement");
+ pw_log_info(" %s", avb_utils_format_addr(buf, sizeof(buf), t->addr));
+}
+
+static int process_service_requirement(struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_mmrp_service_requirement *t = m;
+ struct attr *a;
+
+ debug_service_requirement(t);
+
+ spa_list_for_each(a, &mmrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ memcmp(a->attr.attr.service_requirement.addr, t->addr, 6) == 0)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static void debug_process_mac(const struct avb_packet_mmrp_mac *t)
+{
+ char buf[128];
+ pw_log_info("mac");
+ pw_log_info(" %s", avb_utils_format_addr(buf, sizeof(buf), t->addr));
+}
+
+static int process_mac(struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_mmrp_mac *t = m;
+ struct attr *a;
+
+ debug_process_mac(t);
+
+ spa_list_for_each(a, &mmrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ memcmp(a->attr.attr.mac.addr, t->addr, 6) == 0)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static const struct {
+ int (*dispatch) (struct mmrp *mmrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num);
+} dispatch[] = {
+ [AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT] = { process_service_requirement, },
+ [AVB_MMRP_ATTRIBUTE_TYPE_MAC] = { process_mac, },
+};
+
+static int mmrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+ uint8_t event, uint8_t param, int index)
+{
+ struct mmrp *mmrp = data;
+ return dispatch[attribute_type].dispatch(mmrp, now,
+ attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+ AVB_VERSION_MRP_PARSE_INFO,
+ .check_header = mmrp_check_header,
+ .attr_event = mmrp_attr_event,
+ .process = mmrp_process,
+};
+
+static int mmrp_message(struct mmrp *mmrp, uint64_t now, const void *message, int len)
+{
+ pw_log_debug("MMRP");
+ return avb_mrp_parse_packet(mmrp->server->mrp,
+ now, message, len, &info, mmrp);
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct mmrp *mmrp = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ mmrp_message(mmrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+static void mmrp_destroy(void *data)
+{
+ struct mmrp *mmrp = data;
+ spa_hook_remove(&mmrp->server_listener);
+ pw_loop_destroy_source(mmrp->server->impl->loop, mmrp->source);
+ free(mmrp);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = mmrp_destroy,
+};
+
+struct avb_mmrp_attribute *avb_mmrp_attribute_new(struct avb_mmrp *m,
+ uint8_t type)
+{
+ struct mmrp *mmrp = (struct mmrp*)m;
+ struct avb_mrp_attribute *attr;
+ struct attr *a;
+
+ attr = avb_mrp_attribute_new(mmrp->server->mrp, sizeof(struct attr));
+
+ a = attr->user_data;
+ a->attr.mrp = attr;
+ a->attr.type = type;
+ spa_list_append(&mmrp->attributes, &a->link);
+
+ return &a->attr;
+}
+
+struct avb_mmrp *avb_mmrp_register(struct server *server)
+{
+ struct mmrp *mmrp;
+ int fd, res;
+
+ fd = avb_server_make_socket(server, AVB_MMRP_ETH, mmrp_mac);
+ if (fd < 0) {
+ errno = -fd;
+ return NULL;
+ }
+ mmrp = calloc(1, sizeof(*mmrp));
+ if (mmrp == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+
+ mmrp->server = server;
+ spa_list_init(&mmrp->attributes);
+
+ mmrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, mmrp);
+ if (mmrp->source == NULL) {
+ res = -errno;
+ pw_log_error("mmrp %p: can't create mmrp source: %m", mmrp);
+ goto error_no_source;
+ }
+ avdecc_server_add_listener(server, &mmrp->server_listener, &server_events, mmrp);
+
+ return (struct avb_mmrp*)mmrp;
+
+error_no_source:
+ free(mmrp);
+error_close:
+ close(fd);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/module-avb/mmrp.h b/src/modules/module-avb/mmrp.h
new file mode 100644
index 0000000..b7bcf8c
--- /dev/null
+++ b/src/modules/module-avb/mmrp.h
@@ -0,0 +1,68 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MMRP_H
+#define AVB_MMRP_H
+
+#include "mrp.h"
+#include "internal.h"
+
+#define AVB_MMRP_ETH 0x88f6
+#define AVB_MMRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x20 }
+
+#define AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT 1
+#define AVB_MMRP_ATTRIBUTE_TYPE_MAC 2
+#define AVB_MMRP_ATTRIBUTE_TYPE_VALID(t) ((t)>=1 && (t)<=2)
+
+struct avb_packet_mmrp_msg {
+ uint8_t attribute_type;
+ uint8_t attribute_length;
+ uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+struct avb_packet_mmrp_service_requirement {
+ unsigned char addr[6];
+} __attribute__ ((__packed__));
+
+struct avb_packet_mmrp_mac {
+ unsigned char addr[6];
+} __attribute__ ((__packed__));
+
+struct avb_mmrp;
+
+struct avb_mmrp_attribute {
+ struct avb_mrp_attribute *mrp;
+ uint8_t type;
+ union {
+ struct avb_packet_mmrp_service_requirement service_requirement;
+ struct avb_packet_mmrp_mac mac;
+ } attr;
+};
+
+struct avb_mmrp_attribute *avb_mmrp_attribute_new(struct avb_mmrp *mmrp,
+ uint8_t type);
+
+struct avb_mmrp *avb_mmrp_register(struct server *server);
+
+#endif /* AVB_MMRP_H */
diff --git a/src/modules/module-avb/mrp.c b/src/modules/module-avb/mrp.c
new file mode 100644
index 0000000..7b6bc46
--- /dev/null
+++ b/src/modules/module-avb/mrp.c
@@ -0,0 +1,612 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "mrp.h"
+
+#define MRP_JOINTIMER_MS 100
+#define MRP_LVTIMER_MS 1000
+#define MRP_LVATIMER_MS 10000
+#define MRP_PERIODTIMER_MS 1000
+
+#define mrp_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct avb_mrp_events, m, v, ##__VA_ARGS__)
+#define mrp_emit_event(s,n,e) mrp_emit(s,event,0,n,e)
+#define mrp_emit_notify(s,n,a,e) mrp_emit(s,notify,0,n,a,e)
+
+#define mrp_attribute_emit(a,m,v,...) spa_hook_list_call(&a->listener_list, struct avb_mrp_attribute_events, m, v, ##__VA_ARGS__)
+#define mrp_attribute_emit_notify(a,n,e) mrp_attribute_emit(a,notify,0,n,e)
+
+
+struct mrp;
+
+struct attribute {
+ struct avb_mrp_attribute attr;
+ struct mrp *mrp;
+ struct spa_list link;
+ uint8_t applicant_state;
+ uint8_t registrar_state;
+ uint64_t leave_timeout;
+ unsigned joined:1;
+ struct spa_hook_list listener_list;
+};
+
+struct mrp {
+ struct server *server;
+ struct spa_hook server_listener;
+
+ struct spa_hook_list listener_list;
+
+ struct spa_list attributes;
+
+ uint64_t periodic_timeout;
+ uint64_t leave_all_timeout;
+ uint64_t join_timeout;
+};
+
+static void mrp_destroy(void *data)
+{
+ struct mrp *mrp = data;
+ spa_hook_remove(&mrp->server_listener);
+ free(mrp);
+}
+
+static void global_event(struct mrp *mrp, uint64_t now, uint8_t event)
+{
+ struct attribute *a;
+ spa_list_for_each(a, &mrp->attributes, link)
+ avb_mrp_attribute_update_state(&a->attr, now, event);
+ mrp_emit_event(mrp, now, event);
+}
+
+static void mrp_periodic(void *data, uint64_t now)
+{
+ struct mrp *mrp = data;
+ bool leave_all = false;
+ struct attribute *a;
+
+ if (now > mrp->periodic_timeout) {
+ if (mrp->periodic_timeout > 0)
+ global_event(mrp, now, AVB_MRP_EVENT_PERIODIC);
+ mrp->periodic_timeout = now + MRP_PERIODTIMER_MS * SPA_NSEC_PER_MSEC;
+ }
+ if (now > mrp->leave_all_timeout) {
+ if (mrp->leave_all_timeout > 0) {
+ global_event(mrp, now, AVB_MRP_EVENT_RX_LVA);
+ leave_all = true;
+ }
+ mrp->leave_all_timeout = now + (MRP_LVATIMER_MS + (random() % (MRP_LVATIMER_MS / 2)))
+ * SPA_NSEC_PER_MSEC;
+ }
+
+ if (now > mrp->join_timeout) {
+ if (mrp->join_timeout > 0) {
+ uint8_t event = leave_all ? AVB_MRP_EVENT_TX_LVA : AVB_MRP_EVENT_TX;
+ global_event(mrp, now, event);
+ }
+ mrp->join_timeout = now + MRP_JOINTIMER_MS * SPA_NSEC_PER_MSEC;
+ }
+
+ spa_list_for_each(a, &mrp->attributes, link) {
+ if (a->leave_timeout > 0 && now > a->leave_timeout) {
+ a->leave_timeout = 0;
+ avb_mrp_attribute_update_state(&a->attr, now, AVB_MRP_EVENT_LV_TIMER);
+ }
+ }
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = mrp_destroy,
+ .periodic = mrp_periodic,
+};
+
+int avb_mrp_parse_packet(struct avb_mrp *mrp, uint64_t now, const void *pkt, int len,
+ const struct avb_mrp_parse_info *info, void *data)
+{
+ uint8_t *e = SPA_PTROFF(pkt, len, uint8_t);
+ uint8_t *m = SPA_PTROFF(pkt, sizeof(struct avb_packet_mrp), uint8_t);
+
+ while (m < e && (m[0] != 0 || m[1] != 0)) {
+ const struct avb_packet_mrp_hdr *hdr = (const struct avb_packet_mrp_hdr*)m;
+ uint8_t attr_type = hdr->attribute_type;
+ uint8_t attr_len = hdr->attribute_length;
+ size_t hdr_size;
+ bool has_param;
+
+ if (!info->check_header(data, hdr, &hdr_size, &has_param))
+ return -EINVAL;
+
+ m += hdr_size;
+
+ while (m < e && (m[0] != 0 || m[1] != 0)) {
+ const struct avb_packet_mrp_vector *v =
+ (const struct avb_packet_mrp_vector*)m;
+ uint16_t i, num_values = AVB_MRP_VECTOR_GET_NUM_VALUES(v);
+ uint8_t event_len = (num_values+2)/3;
+ uint8_t param_len = has_param ? (num_values+3)/4 : 0;
+ int plen = sizeof(*v) + attr_len + event_len + param_len;
+ const uint8_t *first = v->first_value;
+ uint8_t event[3], param[4] = { 0, };
+
+ if (m + plen > e)
+ return -EPROTO;
+
+ if (v->lva)
+ info->attr_event(data, now, attr_type, AVB_MRP_EVENT_RX_LVA);
+
+ for (i = 0; i < num_values; i++) {
+ if (i % 3 == 0) {
+ uint8_t ep = first[attr_len + i/3];
+ event[2] = ep % 6; ep /= 6;
+ event[1] = ep % 6; ep /= 6;
+ event[0] = ep % 6;
+ }
+ if (has_param && (i % 4 == 0)) {
+ uint8_t ep = first[attr_len + event_len + i/4];
+ param[3] = ep % 4; ep /= 4;
+ param[2] = ep % 4; ep /= 4;
+ param[1] = ep % 4; ep /= 4;
+ param[0] = ep % 4;
+ }
+ info->process(data, now, attr_type, first,
+ event[i%3], param[i%4], i);
+ }
+ m += plen;
+ }
+ m += 2;
+ }
+ return 0;
+}
+
+const char *avb_mrp_notify_name(uint8_t notify)
+{
+ switch(notify) {
+ case AVB_MRP_NOTIFY_NEW:
+ return "new";
+ case AVB_MRP_NOTIFY_JOIN:
+ return "join";
+ case AVB_MRP_NOTIFY_LEAVE:
+ return "leave";
+ }
+ return "unknown";
+}
+
+const char *avb_mrp_send_name(uint8_t send)
+{
+ switch(send) {
+ case AVB_MRP_SEND_NEW:
+ return "new";
+ case AVB_MRP_SEND_JOININ:
+ return "joinin";
+ case AVB_MRP_SEND_IN:
+ return "in";
+ case AVB_MRP_SEND_JOINMT:
+ return "joinmt";
+ case AVB_MRP_SEND_MT:
+ return "mt";
+ case AVB_MRP_SEND_LV:
+ return "leave";
+ }
+ return "unknown";
+}
+
+struct avb_mrp_attribute *avb_mrp_attribute_new(struct avb_mrp *m,
+ size_t user_size)
+{
+ struct mrp *mrp = (struct mrp*)m;
+ struct attribute *a;
+
+ a = calloc(1, sizeof(*a) + user_size);
+ if (a == NULL)
+ return NULL;
+
+ a->mrp = mrp;
+ a->attr.user_data = SPA_PTROFF(a, sizeof(*a), void);
+ spa_hook_list_init(&a->listener_list);
+ spa_list_append(&mrp->attributes, &a->link);
+
+ return &a->attr;
+}
+
+void avb_mrp_attribute_destroy(struct avb_mrp_attribute *attr)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ spa_list_remove(&a->link);
+ free(a);
+}
+
+void avb_mrp_attribute_add_listener(struct avb_mrp_attribute *attr, struct spa_hook *listener,
+ const struct avb_mrp_attribute_events *events, void *data)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ spa_hook_list_append(&a->listener_list, listener, events, data);
+}
+
+void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now,
+ int event)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ struct mrp *mrp = a->mrp;
+ uint8_t notify = 0, state;
+ uint8_t send = 0;
+
+ state = a->registrar_state;
+
+ switch (event) {
+ case AVB_MRP_EVENT_BEGIN:
+ state = AVB_MRP_MT;
+ break;
+ case AVB_MRP_EVENT_RX_NEW:
+ notify = AVB_MRP_NOTIFY_NEW;
+ switch (state) {
+ case AVB_MRP_LV:
+ a->leave_timeout = 0;
+ break;
+ }
+ state = AVB_MRP_IN;
+ break;
+ case AVB_MRP_EVENT_RX_JOININ:
+ case AVB_MRP_EVENT_RX_JOINMT:
+ switch (state) {
+ case AVB_MRP_LV:
+ a->leave_timeout = 0;
+ break;
+ case AVB_MRP_MT:
+ notify = AVB_MRP_NOTIFY_JOIN;
+ break;
+ }
+ state = AVB_MRP_IN;
+ break;
+ case AVB_MRP_EVENT_RX_LV:
+ case AVB_MRP_EVENT_RX_LVA:
+ case AVB_MRP_EVENT_TX_LVA:
+ case AVB_MRP_EVENT_REDECLARE:
+ switch (state) {
+ case AVB_MRP_IN:
+ a->leave_timeout = now + MRP_LVTIMER_MS * SPA_NSEC_PER_MSEC;
+ //state = AVB_MRP_LV;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_FLUSH:
+ switch (state) {
+ case AVB_MRP_LV:
+ notify = AVB_MRP_NOTIFY_LEAVE;
+ break;
+ }
+ state = AVB_MRP_MT;
+ break;
+ case AVB_MRP_EVENT_LV_TIMER:
+ switch (state) {
+ case AVB_MRP_LV:
+ notify = AVB_MRP_NOTIFY_LEAVE;
+ state = AVB_MRP_MT;
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ if (notify) {
+ mrp_attribute_emit_notify(a, now, notify);
+ mrp_emit_notify(mrp, now, &a->attr, notify);
+ }
+
+ if (a->registrar_state != state || notify) {
+ pw_log_debug("attr %p: %d %d -> %d %d", a, event, a->registrar_state, state, notify);
+ a->registrar_state = state;
+ }
+
+ state = a->applicant_state;
+
+ switch (event) {
+ case AVB_MRP_EVENT_BEGIN:
+ state = AVB_MRP_VO;
+ break;
+ case AVB_MRP_EVENT_NEW:
+ switch (state) {
+ case AVB_MRP_VN:
+ case AVB_MRP_AN:
+ break;
+ default:
+ state = AVB_MRP_VN;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_JOIN:
+ switch (state) {
+ case AVB_MRP_VO:
+ case AVB_MRP_LO:
+ state = AVB_MRP_VP;
+ break;
+ case AVB_MRP_LA:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_AO:
+ state = AVB_MRP_AP;
+ break;
+ case AVB_MRP_QO:
+ state = AVB_MRP_QP;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_LV:
+ switch (state) {
+ case AVB_MRP_VP:
+ state = AVB_MRP_VO;
+ break;
+ case AVB_MRP_VN:
+ case AVB_MRP_AN:
+ case AVB_MRP_AA:
+ case AVB_MRP_QA:
+ state = AVB_MRP_LA;
+ break;
+ case AVB_MRP_AP:
+ state = AVB_MRP_AO;
+ break;
+ case AVB_MRP_QP:
+ state = AVB_MRP_QO;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_RX_JOININ:
+ switch (state) {
+ case AVB_MRP_VO:
+ state = AVB_MRP_AO;
+ break;
+ case AVB_MRP_VP:
+ state = AVB_MRP_AP;
+ break;
+ case AVB_MRP_AA:
+ state = AVB_MRP_QA;
+ break;
+ case AVB_MRP_AO:
+ state = AVB_MRP_QO;
+ break;
+ case AVB_MRP_AP:
+ state = AVB_MRP_QP;
+ break;
+ }
+ SPA_FALLTHROUGH;
+ case AVB_MRP_EVENT_RX_IN:
+ switch (state) {
+ case AVB_MRP_AA:
+ state = AVB_MRP_QA;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_RX_JOINMT:
+ case AVB_MRP_EVENT_RX_MT:
+ switch (state) {
+ case AVB_MRP_QA:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_QO:
+ state = AVB_MRP_AO;
+ break;
+ case AVB_MRP_QP:
+ state = AVB_MRP_AP;
+ break;
+ case AVB_MRP_LO:
+ state = AVB_MRP_VO;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_RX_LV:
+ case AVB_MRP_EVENT_RX_LVA:
+ case AVB_MRP_EVENT_REDECLARE:
+ switch (state) {
+ case AVB_MRP_VO:
+ case AVB_MRP_AO:
+ case AVB_MRP_QO:
+ state = AVB_MRP_LO;
+ break;
+ case AVB_MRP_AN:
+ state = AVB_MRP_VN;
+ break;
+ case AVB_MRP_AA:
+ case AVB_MRP_QA:
+ case AVB_MRP_AP:
+ case AVB_MRP_QP:
+ state = AVB_MRP_VP;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_PERIODIC:
+ switch (state) {
+ case AVB_MRP_QA:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_QP:
+ state = AVB_MRP_AP;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_TX:
+ switch (state) {
+ case AVB_MRP_VP:
+ case AVB_MRP_AA:
+ case AVB_MRP_AP:
+ if (a->registrar_state == AVB_MRP_IN)
+ send = AVB_MRP_SEND_JOININ;
+ else
+ send = AVB_MRP_SEND_JOINMT;
+ break;
+ case AVB_MRP_VN:
+ case AVB_MRP_AN:
+ send = AVB_MRP_SEND_NEW;
+ break;
+ case AVB_MRP_LA:
+ send = AVB_MRP_SEND_LV;
+ break;
+ case AVB_MRP_LO:
+ if (a->registrar_state == AVB_MRP_IN)
+ send = AVB_MRP_SEND_IN;
+ else
+ send = AVB_MRP_SEND_MT;
+ break;
+ }
+ switch (state) {
+ case AVB_MRP_VP:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_VN:
+ state = AVB_MRP_AN;
+ break;
+ case AVB_MRP_AN:
+ if(a->registrar_state == AVB_MRP_IN)
+ state = AVB_MRP_QA;
+ else
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_AA:
+ case AVB_MRP_AP:
+ state = AVB_MRP_QA;
+ break;
+ case AVB_MRP_LA:
+ case AVB_MRP_LO:
+ state = AVB_MRP_VO;
+ break;
+ }
+ break;
+ case AVB_MRP_EVENT_TX_LVA:
+ {
+ switch (state) {
+ case AVB_MRP_VP:
+ if (a->registrar_state == AVB_MRP_IN)
+ send = AVB_MRP_SEND_IN;
+ else
+ send = AVB_MRP_SEND_MT;
+ break;
+ case AVB_MRP_VN:
+ case AVB_MRP_AN:
+ send = AVB_MRP_SEND_NEW;
+ break;
+ case AVB_MRP_AA:
+ case AVB_MRP_QA:
+ case AVB_MRP_AP:
+ case AVB_MRP_QP:
+ if (a->registrar_state == AVB_MRP_IN)
+ send = AVB_MRP_SEND_JOININ;
+ else
+ send = AVB_MRP_SEND_JOINMT;
+ break;
+ }
+ switch (state) {
+ case AVB_MRP_VO:
+ case AVB_MRP_LA:
+ case AVB_MRP_AO:
+ case AVB_MRP_QO:
+ state = AVB_MRP_LO;
+ break;
+ case AVB_MRP_VP:
+ state = AVB_MRP_AA;
+ break;
+ case AVB_MRP_VN:
+ state = AVB_MRP_AN;
+ break;
+ case AVB_MRP_AN:
+ case AVB_MRP_AA:
+ case AVB_MRP_AP:
+ case AVB_MRP_QP:
+ state = AVB_MRP_QA;
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ if (a->applicant_state != state || send) {
+ pw_log_debug("attr %p: %d %d -> %d %d", a, event, a->applicant_state, state, send);
+ a->applicant_state = state;
+ }
+ if (a->joined)
+ a->attr.pending_send = send;
+}
+
+void avb_mrp_attribute_rx_event(struct avb_mrp_attribute *attr, uint64_t now, uint8_t event)
+{
+ static const int map[] = {
+ [AVB_MRP_ATTRIBUTE_EVENT_NEW] = AVB_MRP_EVENT_RX_NEW,
+ [AVB_MRP_ATTRIBUTE_EVENT_JOININ] = AVB_MRP_EVENT_RX_JOININ,
+ [AVB_MRP_ATTRIBUTE_EVENT_IN] = AVB_MRP_EVENT_RX_IN,
+ [AVB_MRP_ATTRIBUTE_EVENT_JOINMT] = AVB_MRP_EVENT_RX_JOINMT,
+ [AVB_MRP_ATTRIBUTE_EVENT_MT] = AVB_MRP_EVENT_RX_MT,
+ [AVB_MRP_ATTRIBUTE_EVENT_LV] = AVB_MRP_EVENT_RX_LV,
+ };
+ avb_mrp_attribute_update_state(attr, now, map[event]);
+}
+
+void avb_mrp_attribute_begin(struct avb_mrp_attribute *attr, uint64_t now)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ a->leave_timeout = 0;
+ avb_mrp_attribute_update_state(attr, now, AVB_MRP_EVENT_BEGIN);
+}
+
+void avb_mrp_attribute_join(struct avb_mrp_attribute *attr, uint64_t now, bool is_new)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ a->joined = true;
+ int event = is_new ? AVB_MRP_EVENT_NEW : AVB_MRP_EVENT_JOIN;
+ avb_mrp_attribute_update_state(attr, now, event);
+}
+
+void avb_mrp_attribute_leave(struct avb_mrp_attribute *attr, uint64_t now)
+{
+ struct attribute *a = SPA_CONTAINER_OF(attr, struct attribute, attr);
+ avb_mrp_attribute_update_state(attr, now, AVB_MRP_EVENT_LV);
+ a->joined = false;
+}
+
+void avb_mrp_destroy(struct avb_mrp *mrp)
+{
+ mrp_destroy(mrp);
+}
+
+struct avb_mrp *avb_mrp_new(struct server *server)
+{
+ struct mrp *mrp;
+
+ mrp = calloc(1, sizeof(*mrp));
+ if (mrp == NULL)
+ return NULL;
+
+ mrp->server = server;
+ spa_list_init(&mrp->attributes);
+ spa_hook_list_init(&mrp->listener_list);
+
+ avdecc_server_add_listener(server, &mrp->server_listener, &server_events, mrp);
+
+ return (struct avb_mrp*)mrp;
+}
+
+void avb_mrp_add_listener(struct avb_mrp *m, struct spa_hook *listener,
+ const struct avb_mrp_events *events, void *data)
+{
+ struct mrp *mrp = (struct mrp*)m;
+ spa_hook_list_append(&mrp->listener_list, listener, events, data);
+}
diff --git a/src/modules/module-avb/mrp.h b/src/modules/module-avb/mrp.h
new file mode 100644
index 0000000..0a05d4b
--- /dev/null
+++ b/src/modules/module-avb/mrp.h
@@ -0,0 +1,181 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MRP_H
+#define AVB_MRP_H
+
+#include "packets.h"
+#include "internal.h"
+
+#define AVB_MRP_PROTOCOL_VERSION 0
+
+struct avb_packet_mrp {
+ struct avb_ethernet_header eth;
+ uint8_t version;
+} __attribute__ ((__packed__));
+
+struct avb_packet_mrp_hdr {
+ uint8_t attribute_type;
+ uint8_t attribute_length;
+} __attribute__ ((__packed__));
+
+struct avb_packet_mrp_vector {
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned lva:3;
+ unsigned nv1:5;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned nv1:5;
+ unsigned lva:3;
+#endif
+ uint8_t nv2;
+ uint8_t first_value[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MRP_VECTOR_SET_NUM_VALUES(a,v) ((a)->nv1 = ((v) >> 8),(a)->nv2 = (v))
+#define AVB_MRP_VECTOR_GET_NUM_VALUES(a) ((a)->nv1 << 8 | (a)->nv2)
+
+struct avb_packet_mrp_footer {
+ uint16_t end_mark;
+} __attribute__ ((__packed__));
+
+/* applicant states */
+#define AVB_MRP_VO 0 /* Very anxious Observer */
+#define AVB_MRP_VP 1 /* Very anxious Passive */
+#define AVB_MRP_VN 2 /* Very anxious New */
+#define AVB_MRP_AN 3 /* Anxious New */
+#define AVB_MRP_AA 4 /* Anxious Active */
+#define AVB_MRP_QA 5 /* Quiet Active */
+#define AVB_MRP_LA 6 /* Leaving Active */
+#define AVB_MRP_AO 7 /* Anxious Observer */
+#define AVB_MRP_QO 8 /* Quiet Observer */
+#define AVB_MRP_AP 9 /* Anxious Passive */
+#define AVB_MRP_QP 10 /* Quiet Passive */
+#define AVB_MRP_LO 11 /* Leaving Observer */
+
+/* registrar states */
+#define AVB_MRP_IN 16
+#define AVB_MRP_LV 17
+#define AVB_MRP_MT 18
+
+/* events */
+#define AVB_MRP_EVENT_BEGIN 0
+#define AVB_MRP_EVENT_NEW 1
+#define AVB_MRP_EVENT_JOIN 2
+#define AVB_MRP_EVENT_LV 3
+#define AVB_MRP_EVENT_TX 4
+#define AVB_MRP_EVENT_TX_LVA 5
+#define AVB_MRP_EVENT_TX_LVAF 6
+#define AVB_MRP_EVENT_RX_NEW 7
+#define AVB_MRP_EVENT_RX_JOININ 8
+#define AVB_MRP_EVENT_RX_IN 9
+#define AVB_MRP_EVENT_RX_JOINMT 10
+#define AVB_MRP_EVENT_RX_MT 11
+#define AVB_MRP_EVENT_RX_LV 12
+#define AVB_MRP_EVENT_RX_LVA 13
+#define AVB_MRP_EVENT_FLUSH 14
+#define AVB_MRP_EVENT_REDECLARE 15
+#define AVB_MRP_EVENT_PERIODIC 16
+#define AVB_MRP_EVENT_LV_TIMER 17
+#define AVB_MRP_EVENT_LVA_TIMER 18
+
+/* attribute events */
+#define AVB_MRP_ATTRIBUTE_EVENT_NEW 0
+#define AVB_MRP_ATTRIBUTE_EVENT_JOININ 1
+#define AVB_MRP_ATTRIBUTE_EVENT_IN 2
+#define AVB_MRP_ATTRIBUTE_EVENT_JOINMT 3
+#define AVB_MRP_ATTRIBUTE_EVENT_MT 4
+#define AVB_MRP_ATTRIBUTE_EVENT_LV 5
+
+#define AVB_MRP_SEND_NEW 1
+#define AVB_MRP_SEND_JOININ 2
+#define AVB_MRP_SEND_IN 3
+#define AVB_MRP_SEND_JOINMT 4
+#define AVB_MRP_SEND_MT 5
+#define AVB_MRP_SEND_LV 6
+
+#define AVB_MRP_NOTIFY_NEW 1
+#define AVB_MRP_NOTIFY_JOIN 2
+#define AVB_MRP_NOTIFY_LEAVE 3
+
+const char *avb_mrp_notify_name(uint8_t notify);
+const char *avb_mrp_send_name(uint8_t send);
+
+struct avb_mrp_attribute {
+ uint8_t pending_send;
+ void *user_data;
+};
+
+struct avb_mrp_attribute_events {
+#define AVB_VERSION_MRP_ATTRIBUTE_EVENTS 0
+ uint32_t version;
+
+ void (*notify) (void *data, uint64_t now, uint8_t notify);
+};
+
+struct avb_mrp_attribute *avb_mrp_attribute_new(struct avb_mrp *mrp,
+ size_t user_size);
+void avb_mrp_attribute_destroy(struct avb_mrp_attribute *attr);
+
+void avb_mrp_attribute_update_state(struct avb_mrp_attribute *attr, uint64_t now, int event);
+
+void avb_mrp_attribute_rx_event(struct avb_mrp_attribute *attr, uint64_t now, uint8_t event);
+
+void avb_mrp_attribute_begin(struct avb_mrp_attribute *attr, uint64_t now);
+void avb_mrp_attribute_join(struct avb_mrp_attribute *attr, uint64_t now, bool is_new);
+void avb_mrp_attribute_leave(struct avb_mrp_attribute *attr, uint64_t now);
+
+void avb_mrp_attribute_add_listener(struct avb_mrp_attribute *attr, struct spa_hook *listener,
+ const struct avb_mrp_attribute_events *events, void *data);
+
+struct avb_mrp_parse_info {
+#define AVB_VERSION_MRP_PARSE_INFO 0
+ uint32_t version;
+
+ bool (*check_header) (void *data, const void *hdr, size_t *hdr_size, bool *has_params);
+
+ int (*attr_event) (void *data, uint64_t now, uint8_t attribute_type, uint8_t event);
+
+ int (*process) (void *data, uint64_t now, uint8_t attribute_type, const void *value,
+ uint8_t event, uint8_t param, int index);
+};
+
+int avb_mrp_parse_packet(struct avb_mrp *mrp, uint64_t now, const void *pkt, int size,
+ const struct avb_mrp_parse_info *cb, void *data);
+
+struct avb_mrp_events {
+#define AVB_VERSION_MRP_EVENTS 0
+ uint32_t version;
+
+ void (*event) (void *data, uint64_t now, uint8_t event);
+
+ void (*notify) (void *data, uint64_t now, struct avb_mrp_attribute *attr, uint8_t notify);
+};
+
+struct avb_mrp *avb_mrp_new(struct server *server);
+void avb_mrp_destroy(struct avb_mrp *mrp);
+
+void avb_mrp_add_listener(struct avb_mrp *mrp, struct spa_hook *listener,
+ const struct avb_mrp_events *events, void *data);
+
+#endif /* AVB_MRP_H */
diff --git a/src/modules/module-avb/msrp.c b/src/modules/module-avb/msrp.c
new file mode 100644
index 0000000..85d3ff9
--- /dev/null
+++ b/src/modules/module-avb/msrp.c
@@ -0,0 +1,459 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+
+#include "utils.h"
+#include "msrp.h"
+
+static const uint8_t msrp_mac[6] = AVB_MSRP_MAC;
+
+struct attr {
+ struct avb_msrp_attribute attr;
+ struct msrp *msrp;
+ struct spa_hook listener;
+ struct spa_list link;
+};
+
+struct msrp {
+ struct server *server;
+ struct spa_hook server_listener;
+ struct spa_hook mrp_listener;
+
+ struct spa_source *source;
+
+ struct spa_list attributes;
+};
+
+static void debug_msrp_talker_common(const struct avb_packet_msrp_talker *t)
+{
+ char buf[128];
+ pw_log_info(" stream-id: %s", avb_utils_format_id(buf, sizeof(buf), be64toh(t->stream_id)));
+ pw_log_info(" dest-addr: %s", avb_utils_format_addr(buf, sizeof(buf), t->dest_addr));
+ pw_log_info(" vlan-id: %d", ntohs(t->vlan_id));
+ pw_log_info(" tspec-max-frame-size: %d", ntohs(t->tspec_max_frame_size));
+ pw_log_info(" tspec-max-interval-frames: %d", ntohs(t->tspec_max_interval_frames));
+ pw_log_info(" priority: %d", t->priority);
+ pw_log_info(" rank: %d", t->rank);
+ pw_log_info(" accumulated-latency: %d", ntohl(t->accumulated_latency));
+}
+
+static void debug_msrp_talker(const struct avb_packet_msrp_talker *t)
+{
+ pw_log_info("talker");
+ debug_msrp_talker_common(t);
+}
+
+static void notify_talker(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+ pw_log_info("> notify talker: %s", avb_mrp_notify_name(notify));
+ debug_msrp_talker(&attr->attr.attr.talker);
+}
+
+static int process_talker(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_msrp_talker *t = m;
+ struct attr *a;
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ a->attr.attr.talker.stream_id == t->stream_id) {
+ a->attr.attr.talker = *t;
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ }
+ return 0;
+}
+static int encode_talker(struct msrp *msrp, struct attr *a, void *m)
+{
+ struct avb_packet_msrp_msg *msg = m;
+ struct avb_packet_mrp_vector *v;
+ struct avb_packet_msrp_talker *t;
+ struct avb_packet_mrp_footer *f;
+ uint8_t *ev;
+ size_t attr_list_length = sizeof(*v) + sizeof(*t) + sizeof(*f) + 1;
+
+ msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE;
+ msg->attribute_length = sizeof(*t);
+ msg->attribute_list_length = htons(attr_list_length);
+
+ v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+ v->lva = 0;
+ AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+ t = (struct avb_packet_msrp_talker *)v->first_value;
+ *t = a->attr.attr.talker;
+
+ ev = SPA_PTROFF(t, sizeof(*t), uint8_t);
+ *ev = a->attr.mrp->pending_send * 6 * 6;
+
+ f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+ f->end_mark = 0;
+
+ return attr_list_length + sizeof(*msg);
+}
+
+
+static void debug_msrp_talker_fail(const struct avb_packet_msrp_talker_fail *t)
+{
+ char buf[128];
+ pw_log_info("talker fail");
+ debug_msrp_talker_common(&t->talker);
+ pw_log_info(" bridge-id: %s", avb_utils_format_id(buf, sizeof(buf), be64toh(t->bridge_id)));
+ pw_log_info(" failure-code: %d", t->failure_code);
+}
+
+static int process_talker_fail(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_msrp_talker_fail *t = m;
+ struct attr *a;
+
+ debug_msrp_talker_fail(t);
+
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ a->attr.attr.talker_fail.talker.stream_id == t->talker.stream_id)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static void debug_msrp_listener(const struct avb_packet_msrp_listener *l, uint8_t param)
+{
+ char buf[128];
+ pw_log_info("listener");
+ pw_log_info(" %s", avb_utils_format_id(buf, sizeof(buf), be64toh(l->stream_id)));
+ pw_log_info(" %d", param);
+}
+
+static void notify_listener(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+ pw_log_info("> notify listener: %s", avb_mrp_notify_name(notify));
+ debug_msrp_listener(&attr->attr.attr.listener, attr->attr.param);
+}
+
+static int process_listener(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ const struct avb_packet_msrp_listener *l = m;
+ struct attr *a;
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attr_type &&
+ a->attr.attr.listener.stream_id == l->stream_id)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+static int encode_listener(struct msrp *msrp, struct attr *a, void *m)
+{
+ struct avb_packet_msrp_msg *msg = m;
+ struct avb_packet_mrp_vector *v;
+ struct avb_packet_msrp_listener *l;
+ struct avb_packet_mrp_footer *f;
+ uint8_t *ev;
+ size_t attr_list_length = sizeof(*v) + sizeof(*l) + sizeof(*f) + 1 + 1;
+
+ msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_LISTENER;
+ msg->attribute_length = sizeof(*l);
+ msg->attribute_list_length = htons(attr_list_length);
+
+ v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+ v->lva = 0;
+ AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+ l = (struct avb_packet_msrp_listener *)v->first_value;
+ *l = a->attr.attr.listener;
+
+ ev = SPA_PTROFF(l, sizeof(*l), uint8_t);
+ *ev = a->attr.mrp->pending_send * 6 * 6;
+
+ ev = SPA_PTROFF(ev, sizeof(*ev), uint8_t);
+ *ev = a->attr.param * 4 * 4 * 4;
+
+ f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+ f->end_mark = 0;
+
+ return attr_list_length + sizeof(*msg);
+}
+
+static void debug_msrp_domain(const struct avb_packet_msrp_domain *d)
+{
+ pw_log_info("domain");
+ pw_log_info(" id: %d", d->sr_class_id);
+ pw_log_info(" prio: %d", d->sr_class_priority);
+ pw_log_info(" vid: %d", ntohs(d->sr_class_vid));
+}
+
+static void notify_domain(struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+ pw_log_info("> notify domain: %s", avb_mrp_notify_name(notify));
+ debug_msrp_domain(&attr->attr.attr.domain);
+}
+
+static int process_domain(struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ struct attr *a;
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attr_type)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static int encode_domain(struct msrp *msrp, struct attr *a, void *m)
+{
+ struct avb_packet_msrp_msg *msg = m;
+ struct avb_packet_mrp_vector *v;
+ struct avb_packet_msrp_domain *d;
+ struct avb_packet_mrp_footer *f;
+ uint8_t *ev;
+ size_t attr_list_length = sizeof(*v) + sizeof(*d) + sizeof(*f) + 1;
+
+ msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN;
+ msg->attribute_length = sizeof(*d);
+ msg->attribute_list_length = htons(attr_list_length);
+
+ v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+ v->lva = 0;
+ AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+ d = (struct avb_packet_msrp_domain *)v->first_value;
+ *d = a->attr.attr.domain;
+
+ ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
+ *ev = a->attr.mrp->pending_send * 36;
+
+ f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+ f->end_mark = 0;
+
+ return attr_list_length + sizeof(*msg);
+}
+
+static const struct {
+ const char *name;
+ int (*process) (struct msrp *msrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num);
+ int (*encode) (struct msrp *msrp, struct attr *attr, void *m);
+ void (*notify) (struct msrp *msrp, uint64_t now, struct attr *attr, uint8_t notify);
+} dispatch[] = {
+ [AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE] = { "talker", process_talker, encode_talker, notify_talker, },
+ [AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED] = { "talker-fail", process_talker_fail, NULL, NULL },
+ [AVB_MSRP_ATTRIBUTE_TYPE_LISTENER] = { "listener", process_listener, encode_listener, notify_listener },
+ [AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN] = { "domain", process_domain, encode_domain, notify_domain, },
+};
+
+static bool msrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+ const struct avb_packet_msrp_msg *msg = hdr;
+ uint8_t attr_type = msg->attribute_type;
+
+ if (!AVB_MSRP_ATTRIBUTE_TYPE_VALID(attr_type))
+ return false;
+
+ *hdr_size = sizeof(*msg);
+ *has_params = attr_type == AVB_MSRP_ATTRIBUTE_TYPE_LISTENER;
+ return true;
+}
+
+static int msrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+ struct msrp *msrp = data;
+ struct attr *a;
+ spa_list_for_each(a, &msrp->attributes, link)
+ if (a->attr.type == attribute_type)
+ avb_mrp_attribute_update_state(a->attr.mrp, now, event);
+ return 0;
+}
+
+static int msrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+ uint8_t event, uint8_t param, int index)
+{
+ struct msrp *msrp = data;
+ return dispatch[attribute_type].process(msrp, now,
+ attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+ AVB_VERSION_MRP_PARSE_INFO,
+ .check_header = msrp_check_header,
+ .attr_event = msrp_attr_event,
+ .process = msrp_process,
+};
+
+
+static int msrp_message(struct msrp *msrp, uint64_t now, const void *message, int len)
+{
+ return avb_mrp_parse_packet(msrp->server->mrp,
+ now, message, len, &info, msrp);
+}
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct msrp *msrp = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ msrp_message(msrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+
+static void msrp_destroy(void *data)
+{
+ struct msrp *msrp = data;
+ spa_hook_remove(&msrp->server_listener);
+ pw_loop_destroy_source(msrp->server->impl->loop, msrp->source);
+ free(msrp);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = msrp_destroy,
+};
+
+static void msrp_notify(void *data, uint64_t now, uint8_t notify)
+{
+ struct attr *a = data;
+ struct msrp *msrp = a->msrp;
+ return dispatch[a->attr.type].notify(msrp, now, a, notify);
+}
+
+static const struct avb_mrp_attribute_events mrp_attr_events = {
+ AVB_VERSION_MRP_ATTRIBUTE_EVENTS,
+ .notify = msrp_notify,
+};
+
+struct avb_msrp_attribute *avb_msrp_attribute_new(struct avb_msrp *m,
+ uint8_t type)
+{
+ struct msrp *msrp = (struct msrp*)m;
+ struct avb_mrp_attribute *attr;
+ struct attr *a;
+
+ attr = avb_mrp_attribute_new(msrp->server->mrp, sizeof(struct attr));
+
+ a = attr->user_data;
+ a->msrp = msrp;
+ a->attr.mrp = attr;
+ a->attr.type = type;
+ spa_list_append(&msrp->attributes, &a->link);
+ avb_mrp_attribute_add_listener(attr, &a->listener, &mrp_attr_events, a);
+
+ return &a->attr;
+}
+
+static void msrp_event(void *data, uint64_t now, uint8_t event)
+{
+ struct msrp *msrp = data;
+ uint8_t buffer[2048];
+ struct avb_packet_mrp *p = (struct avb_packet_mrp*)buffer;
+ struct avb_packet_mrp_footer *f;
+ void *msg = SPA_PTROFF(buffer, sizeof(*p), void);
+ struct attr *a;
+ int len, count = 0;
+ size_t total = sizeof(*p) + 2;
+
+ p->version = AVB_MRP_PROTOCOL_VERSION;
+
+ spa_list_for_each(a, &msrp->attributes, link) {
+ if (!a->attr.mrp->pending_send)
+ continue;
+ if (dispatch[a->attr.type].encode == NULL)
+ continue;
+
+ pw_log_debug("send %s %s", dispatch[a->attr.type].name,
+ avb_mrp_send_name(a->attr.mrp->pending_send));
+
+ len = dispatch[a->attr.type].encode(msrp, a, msg);
+ if (len < 0)
+ break;
+
+ count++;
+ msg = SPA_PTROFF(msg, len, void);
+ total += len;
+ }
+ f = (struct avb_packet_mrp_footer *)msg;
+ f->end_mark = 0;
+
+ if (count > 0)
+ avb_server_send_packet(msrp->server, msrp_mac, AVB_MSRP_ETH,
+ buffer, total);
+}
+
+static const struct avb_mrp_events mrp_events = {
+ AVB_VERSION_MRP_EVENTS,
+ .event = msrp_event,
+};
+
+struct avb_msrp *avb_msrp_register(struct server *server)
+{
+ struct msrp *msrp;
+ int fd, res;
+
+ fd = avb_server_make_socket(server, AVB_MSRP_ETH, msrp_mac);
+ if (fd < 0) {
+ errno = -fd;
+ return NULL;
+ }
+ msrp = calloc(1, sizeof(*msrp));
+ if (msrp == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+
+ msrp->server = server;
+ spa_list_init(&msrp->attributes);
+
+ msrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, msrp);
+ if (msrp->source == NULL) {
+ res = -errno;
+ pw_log_error("msrp %p: can't create msrp source: %m", msrp);
+ goto error_no_source;
+ }
+ avdecc_server_add_listener(server, &msrp->server_listener, &server_events, msrp);
+ avb_mrp_add_listener(server->mrp, &msrp->mrp_listener, &mrp_events, msrp);
+
+ return (struct avb_msrp*)msrp;
+
+error_no_source:
+ free(msrp);
+error_close:
+ close(fd);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/module-avb/msrp.h b/src/modules/module-avb/msrp.h
new file mode 100644
index 0000000..0922e6b
--- /dev/null
+++ b/src/modules/module-avb/msrp.h
@@ -0,0 +1,134 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MSRP_H
+#define AVB_MSRP_H
+
+#include "internal.h"
+#include "mrp.h"
+
+#define AVB_MSRP_ETH 0x22ea
+#define AVB_MSRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0xe };
+
+#define AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE 1
+#define AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED 2
+#define AVB_MSRP_ATTRIBUTE_TYPE_LISTENER 3
+#define AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN 4
+#define AVB_MSRP_ATTRIBUTE_TYPE_VALID(t) ((t)>=1 && (t)<=4)
+
+struct avb_packet_msrp_msg {
+ uint8_t attribute_type;
+ uint8_t attribute_length;
+ uint16_t attribute_list_length;
+ uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MSRP_TSPEC_MAX_INTERVAL_FRAMES_DEFAULT 1
+#define AVB_MSRP_RANK_DEFAULT 1
+#define AVB_MSRP_PRIORITY_DEFAULT 3
+
+struct avb_packet_msrp_talker {
+ uint64_t stream_id;
+ uint8_t dest_addr[6];
+ uint16_t vlan_id;
+ uint16_t tspec_max_frame_size;
+ uint16_t tspec_max_interval_frames;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned priority:3;
+ unsigned rank:1;
+ unsigned reserved:4;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned reserved:4;
+ unsigned rank:1;
+ unsigned priority:3;
+#endif
+ uint32_t accumulated_latency;
+} __attribute__ ((__packed__));
+
+/* failure codes */
+#define AVB_MRP_FAIL_BANDWIDTH 1
+#define AVB_MRP_FAIL_BRIDGE 2
+#define AVB_MRP_FAIL_TC_BANDWIDTH 3
+#define AVB_MRP_FAIL_ID_BUSY 4
+#define AVB_MRP_FAIL_DSTADDR_BUSY 5
+#define AVB_MRP_FAIL_PREEMPTED 6
+#define AVB_MRP_FAIL_LATENCY_CHNG 7
+#define AVB_MRP_FAIL_PORT_NOT_AVB 8
+#define AVB_MRP_FAIL_DSTADDR_FULL 9
+#define AVB_MRP_FAIL_AVB_MRP_RESOURCE 10
+#define AVB_MRP_FAIL_MMRP_RESOURCE 11
+#define AVB_MRP_FAIL_DSTADDR_FAIL 12
+#define AVB_MRP_FAIL_PRIO_NOT_SR 13
+#define AVB_MRP_FAIL_FRAME_SIZE 14
+#define AVB_MRP_FAIL_FANIN_EXCEED 15
+#define AVB_MRP_FAIL_STREAM_CHANGE 16
+#define AVB_MRP_FAIL_VLAN_BLOCKED 17
+#define AVB_MRP_FAIL_VLAN_DISABLED 18
+#define AVB_MRP_FAIL_SR_PRIO_ERR 19
+
+struct avb_packet_msrp_talker_fail {
+ struct avb_packet_msrp_talker talker;
+ uint64_t bridge_id;
+ uint8_t failure_code;
+} __attribute__ ((__packed__));
+
+struct avb_packet_msrp_listener {
+ uint64_t stream_id;
+} __attribute__ ((__packed__));
+
+/* domain discovery */
+#define AVB_MSRP_CLASS_ID_DEFAULT 6
+#define AVB_DEFAULT_VLAN 2
+
+struct avb_packet_msrp_domain {
+ uint8_t sr_class_id;
+ uint8_t sr_class_priority;
+ uint16_t sr_class_vid;
+} __attribute__ ((__packed__));
+
+#define AVB_MSRP_LISTENER_PARAM_IGNORE 0
+#define AVB_MSRP_LISTENER_PARAM_ASKING_FAILED 1
+#define AVB_MSRP_LISTENER_PARAM_READY 2
+#define AVB_MSRP_LISTENER_PARAM_READY_FAILED 3
+
+struct avb_msrp_attribute {
+ struct avb_mrp_attribute *mrp;
+ uint8_t type;
+ uint8_t param;
+ union {
+ struct avb_packet_msrp_talker talker;
+ struct avb_packet_msrp_talker_fail talker_fail;
+ struct avb_packet_msrp_listener listener;
+ struct avb_packet_msrp_domain domain;
+ } attr;
+};
+
+struct avb_msrp;
+
+struct avb_msrp_attribute *avb_msrp_attribute_new(struct avb_msrp *msrp,
+ uint8_t type);
+
+struct avb_msrp *avb_msrp_register(struct server *server);
+
+#endif /* AVB_MSRP_H */
diff --git a/src/modules/module-avb/mvrp.c b/src/modules/module-avb/mvrp.c
new file mode 100644
index 0000000..2f5f6ea
--- /dev/null
+++ b/src/modules/module-avb/mvrp.c
@@ -0,0 +1,297 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+
+#include "mvrp.h"
+
+static const uint8_t mvrp_mac[6] = AVB_MVRP_MAC;
+
+struct attr {
+ struct avb_mvrp_attribute attr;
+ struct spa_hook listener;
+ struct spa_list link;
+ struct mvrp *mvrp;
+};
+
+struct mvrp {
+ struct server *server;
+ struct spa_hook server_listener;
+ struct spa_hook mrp_listener;
+
+ struct spa_source *source;
+
+ struct spa_list attributes;
+};
+
+static bool mvrp_check_header(void *data, const void *hdr, size_t *hdr_size, bool *has_params)
+{
+ const struct avb_packet_mvrp_msg *msg = hdr;
+ uint8_t attr_type = msg->attribute_type;
+
+ if (!AVB_MVRP_ATTRIBUTE_TYPE_VALID(attr_type))
+ return false;
+
+ *hdr_size = sizeof(*msg);
+ *has_params = false;
+ return true;
+}
+
+static int mvrp_attr_event(void *data, uint64_t now, uint8_t attribute_type, uint8_t event)
+{
+ struct mvrp *mvrp = data;
+ struct attr *a;
+ spa_list_for_each(a, &mvrp->attributes, link)
+ if (a->attr.type == attribute_type)
+ avb_mrp_attribute_rx_event(a->attr.mrp, now, event);
+ return 0;
+}
+
+static void debug_vid(const struct avb_packet_mvrp_vid *t)
+{
+ pw_log_info("vid");
+ pw_log_info(" %d", ntohs(t->vlan));
+}
+
+static int process_vid(struct mvrp *mvrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num)
+{
+ return mvrp_attr_event(mvrp, now, attr_type, event);
+}
+
+static int encode_vid(struct mvrp *mvrp, struct attr *a, void *m)
+{
+ struct avb_packet_mvrp_msg *msg = m;
+ struct avb_packet_mrp_vector *v;
+ struct avb_packet_mvrp_vid *d;
+ struct avb_packet_mrp_footer *f;
+ uint8_t *ev;
+ size_t attr_list_length = sizeof(*v) + sizeof(*d) + sizeof(*f) + 1;
+
+ msg->attribute_type = AVB_MVRP_ATTRIBUTE_TYPE_VID;
+ msg->attribute_length = sizeof(*d);
+
+ v = (struct avb_packet_mrp_vector *)msg->attribute_list;
+ v->lva = 0;
+ AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1);
+
+ d = (struct avb_packet_mvrp_vid *)v->first_value;
+ *d = a->attr.attr.vid;
+
+ ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
+ *ev = a->attr.mrp->pending_send * 36;
+
+ f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
+ f->end_mark = 0;
+
+ return attr_list_length + sizeof(*msg);
+}
+
+static void notify_vid(struct mvrp *mvrp, uint64_t now, struct attr *attr, uint8_t notify)
+{
+ pw_log_info("> notify vid: %s", avb_mrp_notify_name(notify));
+ debug_vid(&attr->attr.attr.vid);
+}
+
+static const struct {
+ const char *name;
+ int (*process) (struct mvrp *mvrp, uint64_t now, uint8_t attr_type,
+ const void *m, uint8_t event, uint8_t param, int num);
+ int (*encode) (struct mvrp *mvrp, struct attr *attr, void *m);
+ void (*notify) (struct mvrp *mvrp, uint64_t now, struct attr *attr, uint8_t notify);
+} dispatch[] = {
+ [AVB_MVRP_ATTRIBUTE_TYPE_VID] = { "vid", process_vid, encode_vid, notify_vid },
+};
+
+static int mvrp_process(void *data, uint64_t now, uint8_t attribute_type, const void *value,
+ uint8_t event, uint8_t param, int index)
+{
+ struct mvrp *mvrp = data;
+ return dispatch[attribute_type].process(mvrp, now,
+ attribute_type, value, event, param, index);
+}
+
+static const struct avb_mrp_parse_info info = {
+ AVB_VERSION_MRP_PARSE_INFO,
+ .check_header = mvrp_check_header,
+ .attr_event = mvrp_attr_event,
+ .process = mvrp_process,
+};
+
+static int mvrp_message(struct mvrp *mvrp, uint64_t now, const void *message, int len)
+{
+ pw_log_debug("MVRP");
+ return avb_mrp_parse_packet(mvrp->server->mrp,
+ now, message, len, &info, mvrp);
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct mvrp *mvrp = data;
+ struct timespec now;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ clock_gettime(CLOCK_REALTIME, &now);
+ mvrp_message(mvrp, SPA_TIMESPEC_TO_NSEC(&now), buffer, len);
+ }
+ }
+}
+
+static void mvrp_destroy(void *data)
+{
+ struct mvrp *mvrp = data;
+ spa_hook_remove(&mvrp->server_listener);
+ pw_loop_destroy_source(mvrp->server->impl->loop, mvrp->source);
+ free(mvrp);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = mvrp_destroy,
+};
+
+static void mvrp_notify(void *data, uint64_t now, uint8_t notify)
+{
+ struct attr *a = data;
+ struct mvrp *mvrp = a->mvrp;
+ return dispatch[a->attr.type].notify(mvrp, now, a, notify);
+}
+
+static const struct avb_mrp_attribute_events mrp_attr_events = {
+ AVB_VERSION_MRP_ATTRIBUTE_EVENTS,
+ .notify = mvrp_notify,
+};
+
+struct avb_mvrp_attribute *avb_mvrp_attribute_new(struct avb_mvrp *m,
+ uint8_t type)
+{
+ struct mvrp *mvrp = (struct mvrp*)m;
+ struct avb_mrp_attribute *attr;
+ struct attr *a;
+
+ attr = avb_mrp_attribute_new(mvrp->server->mrp, sizeof(struct attr));
+
+ a = attr->user_data;
+ a->attr.mrp = attr;
+ a->attr.type = type;
+ spa_list_append(&mvrp->attributes, &a->link);
+ avb_mrp_attribute_add_listener(attr, &a->listener, &mrp_attr_events, a);
+
+ return &a->attr;
+}
+
+static void mvrp_event(void *data, uint64_t now, uint8_t event)
+{
+ struct mvrp *mvrp = data;
+ uint8_t buffer[2048];
+ struct avb_packet_mrp *p = (struct avb_packet_mrp*)buffer;
+ struct avb_packet_mrp_footer *f;
+ void *msg = SPA_PTROFF(buffer, sizeof(*p), void);
+ struct attr *a;
+ int len, count = 0;
+ size_t total = sizeof(*p) + 2;
+
+ p->version = AVB_MRP_PROTOCOL_VERSION;
+
+ spa_list_for_each(a, &mvrp->attributes, link) {
+ if (!a->attr.mrp->pending_send)
+ continue;
+ if (dispatch[a->attr.type].encode == NULL)
+ continue;
+
+ pw_log_debug("send %s %s", dispatch[a->attr.type].name,
+ avb_mrp_send_name(a->attr.mrp->pending_send));
+
+ len = dispatch[a->attr.type].encode(mvrp, a, msg);
+ if (len < 0)
+ break;
+
+ count++;
+ msg = SPA_PTROFF(msg, len, void);
+ total += len;
+ }
+ f = (struct avb_packet_mrp_footer *)msg;
+ f->end_mark = 0;
+
+ if (count > 0)
+ avb_server_send_packet(mvrp->server, mvrp_mac, AVB_MVRP_ETH,
+ buffer, total);
+}
+
+static const struct avb_mrp_events mrp_events = {
+ AVB_VERSION_MRP_EVENTS,
+ .event = mvrp_event,
+};
+
+struct avb_mvrp *avb_mvrp_register(struct server *server)
+{
+ struct mvrp *mvrp;
+ int fd, res;
+
+ fd = avb_server_make_socket(server, AVB_MVRP_ETH, mvrp_mac);
+ if (fd < 0) {
+ errno = -fd;
+ return NULL;
+ }
+ mvrp = calloc(1, sizeof(*mvrp));
+ if (mvrp == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+
+ mvrp->server = server;
+ spa_list_init(&mvrp->attributes);
+
+ mvrp->source = pw_loop_add_io(server->impl->loop, fd, SPA_IO_IN, true, on_socket_data, mvrp);
+ if (mvrp->source == NULL) {
+ res = -errno;
+ pw_log_error("mvrp %p: can't create mvrp source: %m", mvrp);
+ goto error_no_source;
+ }
+ avdecc_server_add_listener(server, &mvrp->server_listener, &server_events, mvrp);
+ avb_mrp_add_listener(server->mrp, &mvrp->mrp_listener, &mrp_events, mvrp);
+
+ return (struct avb_mvrp*)mvrp;
+
+error_no_source:
+ free(mvrp);
+error_close:
+ close(fd);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/module-avb/mvrp.h b/src/modules/module-avb/mvrp.h
new file mode 100644
index 0000000..da3d5dc
--- /dev/null
+++ b/src/modules/module-avb/mvrp.h
@@ -0,0 +1,62 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_MVRP_H
+#define AVB_MVRP_H
+
+#include "mrp.h"
+#include "internal.h"
+
+#define AVB_MVRP_ETH 0x88f5
+#define AVB_MVRP_MAC { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x21 };
+
+struct avb_packet_mvrp_msg {
+ uint8_t attribute_type;
+ uint8_t attribute_length;
+ uint8_t attribute_list[0];
+} __attribute__ ((__packed__));
+
+#define AVB_MVRP_ATTRIBUTE_TYPE_VID 1
+#define AVB_MVRP_ATTRIBUTE_TYPE_VALID(t) ((t)==1)
+
+struct avb_packet_mvrp_vid {
+ uint16_t vlan;
+} __attribute__ ((__packed__));
+
+struct avb_mvrp;
+
+struct avb_mvrp_attribute {
+ struct avb_mrp_attribute *mrp;
+ uint8_t type;
+ union {
+ struct avb_packet_mvrp_vid vid;
+ } attr;
+};
+
+struct avb_mvrp_attribute *avb_mvrp_attribute_new(struct avb_mvrp *mvrp,
+ uint8_t type);
+
+struct avb_mvrp *avb_mvrp_register(struct server *server);
+
+#endif /* AVB_MVRP_H */
diff --git a/src/modules/module-avb/packets.h b/src/modules/module-avb/packets.h
new file mode 100644
index 0000000..f35738a
--- /dev/null
+++ b/src/modules/module-avb/packets.h
@@ -0,0 +1,101 @@
+/* Spa AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_PACKETS_H
+#define AVB_PACKETS_H
+
+#include <arpa/inet.h>
+
+#define AVB_SUBTYPE_61883_IIDC 0x00
+#define AVB_SUBTYPE_MMA_STREAM 0x01
+#define AVB_SUBTYPE_AAF 0x02
+#define AVB_SUBTYPE_CVF 0x03
+#define AVB_SUBTYPE_CRF 0x04
+#define AVB_SUBTYPE_TSCF 0x05
+#define AVB_SUBTYPE_SVF 0x06
+#define AVB_SUBTYPE_RVF 0x07
+#define AVB_SUBTYPE_AEF_CONTINUOUS 0x6E
+#define AVB_SUBTYPE_VSF_STREAM 0x6F
+#define AVB_SUBTYPE_EF_STREAM 0x7F
+#define AVB_SUBTYPE_NTSCF 0x82
+#define AVB_SUBTYPE_ESCF 0xEC
+#define AVB_SUBTYPE_EECF 0xED
+#define AVB_SUBTYPE_AEF_DISCRETE 0xEE
+#define AVB_SUBTYPE_ADP 0xFA
+#define AVB_SUBTYPE_AECP 0xFB
+#define AVB_SUBTYPE_ACMP 0xFC
+#define AVB_SUBTYPE_MAAP 0xFE
+#define AVB_SUBTYPE_EF_CONTROL 0xFF
+
+struct avb_ethernet_header {
+ uint8_t dest[6];
+ uint8_t src[6];
+ uint16_t type;
+} __attribute__ ((__packed__));
+
+struct avb_frame_header {
+ uint8_t dest[6];
+ uint8_t src[6];
+ uint16_t type; /* 802.1Q Virtual Lan 0x8100 */
+ uint16_t prio_cfi_id;
+ uint16_t etype;
+} __attribute__ ((__packed__));
+
+struct avb_packet_header {
+ uint8_t subtype;
+#if __BYTE_ORDER == __BIG_ENDIAN
+ unsigned sv:1; /* stream_id valid */
+ unsigned version:3;
+ unsigned subtype_data1:4;
+
+ unsigned subtype_data2:5;
+ unsigned len1:3;
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned subtype_data1:4;
+ unsigned version:3;
+ unsigned sv:1;
+
+ unsigned len1:3;
+ unsigned subtype_data2:5;
+#elif
+#error "Unknown byte order"
+#endif
+ uint8_t len2:8;
+} __attribute__ ((__packed__));
+
+#define AVB_PACKET_SET_SUBTYPE(p,v) ((p)->subtype = (v))
+#define AVB_PACKET_SET_SV(p,v) ((p)->sv = (v))
+#define AVB_PACKET_SET_VERSION(p,v) ((p)->version = (v))
+#define AVB_PACKET_SET_SUB1(p,v) ((p)->subtype_data1 = (v))
+#define AVB_PACKET_SET_SUB2(p,v) ((p)->subtype_data2 = (v))
+#define AVB_PACKET_SET_LENGTH(p,v) ((p)->len1 = ((v) >> 8),(p)->len2 = (v))
+
+#define AVB_PACKET_GET_SUBTYPE(p) ((p)->subtype)
+#define AVB_PACKET_GET_SV(p) ((p)->sv)
+#define AVB_PACKET_GET_VERSION(p) ((p)->version)
+#define AVB_PACKET_GET_SUB1(p) ((p)->subtype_data1)
+#define AVB_PACKET_GET_SUB2(p) ((p)->subtype_data2)
+#define AVB_PACKET_GET_LENGTH(p) ((p)->len1 << 8 | (p)->len2)
+
+#endif /* AVB_PACKETS_H */
diff --git a/src/modules/module-avb/srp.c b/src/modules/module-avb/srp.c
new file mode 100644
index 0000000..89d75f1
--- /dev/null
+++ b/src/modules/module-avb/srp.c
@@ -0,0 +1,59 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "srp.h"
+
+struct srp {
+ struct server *server;
+ struct spa_hook server_listener;
+};
+
+static void srp_destroy(void *data)
+{
+ struct srp *srp = data;
+ spa_hook_remove(&srp->server_listener);
+ free(srp);
+}
+
+static const struct server_events server_events = {
+ AVB_VERSION_SERVER_EVENTS,
+ .destroy = srp_destroy,
+};
+
+int avb_srp_register(struct server *server)
+{
+ struct srp *srp;
+
+ srp = calloc(1, sizeof(*srp));
+ if (srp == NULL)
+ return -errno;
+
+ srp->server = server;
+
+ avdecc_server_add_listener(server, &srp->server_listener, &server_events, srp);
+
+ return 0;
+}
diff --git a/src/modules/module-avb/srp.h b/src/modules/module-avb/srp.h
new file mode 100644
index 0000000..853321f
--- /dev/null
+++ b/src/modules/module-avb/srp.h
@@ -0,0 +1,32 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_SRP_H
+#define AVB_SRP_H
+
+#include "internal.h"
+
+int avb_srp_register(struct server *server);
+
+#endif /* AVB_SRP_H */
diff --git a/src/modules/module-avb/stream.c b/src/modules/module-avb/stream.c
new file mode 100644
index 0000000..0f5b148
--- /dev/null
+++ b/src/modules/module-avb/stream.c
@@ -0,0 +1,589 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/net_tstamp.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+
+#include <spa/debug/mem.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "iec61883.h"
+#include "stream.h"
+#include "utils.h"
+#include "aecp-aem-descriptors.h"
+
+static void on_stream_destroy(void *d)
+{
+ struct stream *stream = d;
+ spa_hook_remove(&stream->stream_listener);
+ stream->stream = NULL;
+}
+
+static void on_source_stream_process(void *data)
+{
+ struct stream *stream = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t index, n_bytes;
+ int32_t avail, wanted;
+
+ if ((buf = pw_stream_dequeue_buffer(stream->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ d = buf->buffer->datas;
+
+ wanted = buf->requested ? buf->requested * stream->stride : d[0].maxsize;
+
+ n_bytes = SPA_MIN(d[0].maxsize, (uint32_t)wanted);
+
+ avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+ if (avail < wanted) {
+ pw_log_debug("capture underrun %d < %d", avail, wanted);
+ memset(d[0].data, 0, n_bytes);
+ } else {
+ spa_ringbuffer_read_data(&stream->ring,
+ stream->buffer_data,
+ stream->buffer_size,
+ index % stream->buffer_size,
+ d[0].data, n_bytes);
+ index += n_bytes;
+ spa_ringbuffer_read_update(&stream->ring, index);
+ }
+
+ d[0].chunk->size = n_bytes;
+ d[0].chunk->stride = stream->stride;
+ d[0].chunk->offset = 0;
+ buf->size = n_bytes / stream->stride;
+
+ pw_stream_queue_buffer(stream->stream, buf);
+}
+
+static const struct pw_stream_events source_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = on_stream_destroy,
+ .process = on_source_stream_process
+};
+
+static inline void
+set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
+ uint32_t offset, struct iovec *iov, uint32_t len)
+{
+ iov[0].iov_len = SPA_MIN(len, size - offset);
+ iov[0].iov_base = SPA_PTROFF(buffer, offset, void);
+ iov[1].iov_len = len - iov[0].iov_len;
+ iov[1].iov_base = buffer;
+}
+
+static int flush_write(struct stream *stream, uint64_t current_time)
+{
+ int32_t avail;
+ uint32_t index;
+ uint64_t ptime, txtime;
+ int pdu_count;
+ ssize_t n;
+ struct avb_frame_header *h = (void*)stream->pdu;
+ struct avb_packet_iec61883 *p = SPA_PTROFF(h, sizeof(*h), void);
+ uint8_t dbc;
+
+ avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+ pdu_count = (avail / stream->stride) / stream->frames_per_pdu;
+
+ txtime = current_time + stream->t_uncertainty;
+ ptime = txtime + stream->mtt;
+ dbc = stream->dbc;
+
+ while (pdu_count--) {
+ *(uint64_t*)CMSG_DATA(stream->cmsg) = txtime;
+
+ set_iovec(&stream->ring,
+ stream->buffer_data,
+ stream->buffer_size,
+ index % stream->buffer_size,
+ &stream->iov[1], stream->payload_size);
+
+ p->seq_num = stream->pdu_seq++;
+ p->tv = 1;
+ p->timestamp = ptime;
+ p->dbc = dbc;
+
+ n = sendmsg(stream->source->fd, &stream->msg, MSG_NOSIGNAL);
+ if (n < 0 || n != (ssize_t)stream->pdu_size) {
+ pw_log_error("sendmsg() failed %zd != %zd: %m",
+ n, stream->pdu_size);
+ }
+ txtime += stream->pdu_period;
+ ptime += stream->pdu_period;
+ index += stream->payload_size;
+ dbc += stream->frames_per_pdu;
+ }
+ stream->dbc = dbc;
+ spa_ringbuffer_read_update(&stream->ring, index);
+ return 0;
+}
+
+static void on_sink_stream_process(void *data)
+{
+ struct stream *stream = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ int32_t filled;
+ uint32_t index, offs, avail, size;
+ struct timespec now;
+
+ if ((buf = pw_stream_dequeue_buffer(stream->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ d = buf->buffer->datas;
+
+ offs = SPA_MIN(d[0].chunk->offset, d[0].maxsize);
+ size = SPA_MIN(d[0].chunk->size, d[0].maxsize - offs);
+ avail = size - offs;
+
+ filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+
+ if (filled >= (int32_t)stream->buffer_size) {
+ pw_log_warn("playback overrun %d >= %zd", filled, stream->buffer_size);
+ } else {
+ spa_ringbuffer_write_data(&stream->ring,
+ stream->buffer_data,
+ stream->buffer_size,
+ index % stream->buffer_size,
+ SPA_PTROFF(d[0].data, offs, void), avail);
+ index += avail;
+ spa_ringbuffer_write_update(&stream->ring, index);
+ }
+ pw_stream_queue_buffer(stream->stream, buf);
+
+ clock_gettime(CLOCK_TAI, &now);
+ flush_write(stream, SPA_TIMESPEC_TO_NSEC(&now));
+}
+
+static void setup_pdu(struct stream *stream)
+{
+ struct avb_frame_header *h;
+ struct avb_packet_iec61883 *p;
+ ssize_t payload_size, hdr_size, pdu_size;
+
+ spa_memzero(stream->pdu, sizeof(stream->pdu));
+ h = (struct avb_frame_header*)stream->pdu;
+ p = SPA_PTROFF(h, sizeof(*h), void);
+
+ hdr_size = sizeof(*h) + sizeof(*p);
+ payload_size = stream->stride * stream->frames_per_pdu;
+ pdu_size = hdr_size + payload_size;
+
+ h->type = htons(0x8100);
+ h->prio_cfi_id = htons((stream->prio << 13) | stream->vlan_id);
+ h->etype = htons(0x22f0);
+
+ if (stream->direction == SPA_DIRECTION_OUTPUT) {
+ p->subtype = AVB_SUBTYPE_61883_IIDC;
+ p->sv = 1;
+ p->stream_id = htobe64(stream->id);
+ p->data_len = htons(payload_size+8);
+ p->tag = 0x1;
+ p->channel = 0x1f;
+ p->tcode = 0xa;
+ p->sid = 0x3f;
+ p->dbs = stream->info.info.raw.channels;
+ p->qi2 = 0x2;
+ p->format_id = 0x10;
+ p->fdf = 0x2;
+ p->syt = htons(0x0008);
+ }
+ stream->hdr_size = hdr_size;
+ stream->payload_size = payload_size;
+ stream->pdu_size = pdu_size;
+}
+
+static int setup_msg(struct stream *stream)
+{
+ stream->iov[0].iov_base = stream->pdu;
+ stream->iov[0].iov_len = stream->hdr_size;
+ stream->iov[1].iov_base = SPA_PTROFF(stream->pdu, stream->hdr_size, void);
+ stream->iov[1].iov_len = stream->payload_size;
+ stream->iov[2].iov_base = SPA_PTROFF(stream->pdu, stream->hdr_size, void);
+ stream->iov[2].iov_len = 0;
+ stream->msg.msg_name = &stream->sock_addr;
+ stream->msg.msg_namelen = sizeof(stream->sock_addr);
+ stream->msg.msg_iov = stream->iov;
+ stream->msg.msg_iovlen = 3;
+ stream->msg.msg_control = stream->control;
+ stream->msg.msg_controllen = sizeof(stream->control);
+ stream->cmsg = CMSG_FIRSTHDR(&stream->msg);
+ stream->cmsg->cmsg_level = SOL_SOCKET;
+ stream->cmsg->cmsg_type = SCM_TXTIME;
+ stream->cmsg->cmsg_len = CMSG_LEN(sizeof(__u64));
+ return 0;
+}
+
+static const struct pw_stream_events sink_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = on_stream_destroy,
+ .process = on_sink_stream_process
+};
+
+struct stream *server_create_stream(struct server *server,
+ enum spa_direction direction, uint16_t index)
+{
+ struct stream *stream;
+ const struct descriptor *desc;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ int res;
+
+ desc = server_find_descriptor(server,
+ direction == SPA_DIRECTION_INPUT ?
+ AVB_AEM_DESC_STREAM_INPUT :
+ AVB_AEM_DESC_STREAM_OUTPUT, index);
+ if (desc == NULL)
+ return NULL;
+
+ stream = calloc(1, sizeof(*stream));
+ if (stream == NULL)
+ return NULL;
+
+ stream->server = server;
+ stream->direction = direction;
+ stream->index = index;
+ stream->desc = desc;
+ spa_list_append(&server->streams, &stream->link);
+
+ stream->prio = AVB_MSRP_PRIORITY_DEFAULT;
+ stream->vlan_id = AVB_DEFAULT_VLAN;
+
+ stream->id = (uint64_t)server->mac_addr[0] << 56 |
+ (uint64_t)server->mac_addr[1] << 48 |
+ (uint64_t)server->mac_addr[2] << 40 |
+ (uint64_t)server->mac_addr[3] << 32 |
+ (uint64_t)server->mac_addr[4] << 24 |
+ (uint64_t)server->mac_addr[5] << 16 |
+ htons(index);
+
+ stream->vlan_attr = avb_mvrp_attribute_new(server->mvrp,
+ AVB_MVRP_ATTRIBUTE_TYPE_VID);
+ stream->vlan_attr->attr.vid.vlan = htons(stream->vlan_id);
+
+ stream->buffer_data = calloc(1, BUFFER_SIZE);
+ stream->buffer_size = BUFFER_SIZE;
+ spa_ringbuffer_init(&stream->ring);
+
+ if (direction == SPA_DIRECTION_INPUT) {
+ stream->stream = pw_stream_new(server->impl->core, "source",
+ pw_properties_new(
+ PW_KEY_MEDIA_CLASS, "Audio/Source",
+ PW_KEY_NODE_NAME, "avb.source",
+ PW_KEY_NODE_DESCRIPTION, "AVB Source",
+ PW_KEY_NODE_WANT_DRIVER, "true",
+ NULL));
+ } else {
+ stream->stream = pw_stream_new(server->impl->core, "sink",
+ pw_properties_new(
+ PW_KEY_MEDIA_CLASS, "Audio/Sink",
+ PW_KEY_NODE_NAME, "avb.sink",
+ PW_KEY_NODE_DESCRIPTION, "AVB Sink",
+ PW_KEY_NODE_WANT_DRIVER, "true",
+ NULL));
+ }
+ if (stream->stream == NULL)
+ goto error_free;
+
+ pw_stream_add_listener(stream->stream,
+ &stream->stream_listener,
+ direction == SPA_DIRECTION_INPUT ?
+ &source_stream_events :
+ &sink_stream_events,
+ stream);
+
+ stream->info.info.raw.format = SPA_AUDIO_FORMAT_S24_32_BE;
+ stream->info.info.raw.flags = SPA_AUDIO_FLAG_UNPOSITIONED;
+ stream->info.info.raw.rate = 48000;
+ stream->info.info.raw.channels = 8;
+ stream->stride = stream->info.info.raw.channels * 4;
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &stream->info.info.raw);
+
+ if ((res = pw_stream_connect(stream->stream,
+ pw_direction_reverse(direction),
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_INACTIVE |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ goto error_free_stream;
+
+ stream->frames_per_pdu = 6;
+ stream->pdu_period = SPA_NSEC_PER_SEC * stream->frames_per_pdu /
+ stream->info.info.raw.rate;
+
+ setup_pdu(stream);
+ setup_msg(stream);
+
+ stream->listener_attr = avb_msrp_attribute_new(server->msrp,
+ AVB_MSRP_ATTRIBUTE_TYPE_LISTENER);
+ stream->talker_attr = avb_msrp_attribute_new(server->msrp,
+ AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE);
+ stream->talker_attr->attr.talker.vlan_id = htons(stream->vlan_id);
+ stream->talker_attr->attr.talker.tspec_max_frame_size = htons(32 + stream->frames_per_pdu * stream->stride);
+ stream->talker_attr->attr.talker.tspec_max_interval_frames =
+ htons(AVB_MSRP_TSPEC_MAX_INTERVAL_FRAMES_DEFAULT);
+ stream->talker_attr->attr.talker.priority = stream->prio;
+ stream->talker_attr->attr.talker.rank = AVB_MSRP_RANK_DEFAULT;
+ stream->talker_attr->attr.talker.accumulated_latency = htonl(95);
+
+ return stream;
+
+error_free_stream:
+ pw_stream_destroy(stream->stream);
+ errno = -res;
+error_free:
+ free(stream);
+ return NULL;
+}
+
+void stream_destroy(struct stream *stream)
+{
+ avb_mrp_attribute_destroy(stream->listener_attr->mrp);
+ spa_list_remove(&stream->link);
+ free(stream);
+}
+
+static int setup_socket(struct stream *stream)
+{
+ struct server *server = stream->server;
+ int fd, res;
+ char buf[128];
+ struct ifreq req;
+
+ fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL));
+ if (fd < 0) {
+ pw_log_error("socket() failed: %m");
+ return -errno;
+ }
+
+ spa_zero(req);
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
+ res = ioctl(fd, SIOCGIFINDEX, &req);
+ if (res < 0) {
+ pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
+ res = -errno;
+ goto error_close;
+ }
+
+ spa_zero(stream->sock_addr);
+ stream->sock_addr.sll_family = AF_PACKET;
+ stream->sock_addr.sll_protocol = htons(ETH_P_TSN);
+ stream->sock_addr.sll_ifindex = req.ifr_ifindex;
+
+ if (stream->direction == SPA_DIRECTION_OUTPUT) {
+ struct sock_txtime txtime_cfg;
+
+ res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio,
+ sizeof(stream->prio));
+ if (res < 0) {
+ pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio);
+ res = -errno;
+ goto error_close;
+ }
+
+ txtime_cfg.clockid = CLOCK_TAI;
+ txtime_cfg.flags = 0;
+ res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
+ sizeof(txtime_cfg));
+ if (res < 0) {
+ pw_log_error("setsockopt(SO_TXTIME) failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+ } else {
+ struct packet_mreq mreq;
+
+ res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr));
+ if (res < 0) {
+ pw_log_error("bind() failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+
+ spa_zero(mreq);
+ mreq.mr_ifindex = req.ifr_ifindex;
+ mreq.mr_type = PACKET_MR_MULTICAST;
+ mreq.mr_alen = ETH_ALEN;
+ memcpy(&mreq.mr_address, stream->addr, ETH_ALEN);
+ res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
+ &mreq, sizeof(struct packet_mreq));
+
+ pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr));
+
+ if (res < 0) {
+ pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
+ res = -errno;
+ goto error_close;
+ }
+ }
+ return fd;
+
+error_close:
+ close(fd);
+ return res;
+}
+
+static void handle_iec61883_packet(struct stream *stream,
+ struct avb_packet_iec61883 *p, int len)
+{
+ uint32_t index, n_bytes;
+ int32_t filled;
+
+ filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+ n_bytes = ntohs(p->data_len) - 8;
+
+ if (filled + n_bytes > stream->buffer_size) {
+ pw_log_debug("capture overrun");
+ } else {
+ spa_ringbuffer_write_data(&stream->ring,
+ stream->buffer_data,
+ stream->buffer_size,
+ index % stream->buffer_size,
+ p->payload, n_bytes);
+ index += n_bytes;
+ spa_ringbuffer_write_update(&stream->ring, index);
+ }
+}
+
+static void on_socket_data(void *data, int fd, uint32_t mask)
+{
+ struct stream *stream = data;
+
+ if (mask & SPA_IO_IN) {
+ int len;
+ uint8_t buffer[2048];
+
+ len = recv(fd, buffer, sizeof(buffer), 0);
+
+ if (len < 0) {
+ pw_log_warn("got recv error: %m");
+ }
+ else if (len < (int)sizeof(struct avb_packet_header)) {
+ pw_log_warn("short packet received (%d < %d)", len,
+ (int)sizeof(struct avb_packet_header));
+ } else {
+ struct avb_frame_header *h = (void*)buffer;
+ struct avb_packet_iec61883 *p = SPA_PTROFF(h, sizeof(*h), void);
+
+ if (memcmp(h->dest, stream->addr, 6) != 0 ||
+ p->subtype != AVB_SUBTYPE_61883_IIDC)
+ return;
+
+ handle_iec61883_packet(stream, p, len - sizeof(*h));
+ }
+ }
+}
+
+int stream_activate(struct stream *stream, uint64_t now)
+{
+ struct server *server = stream->server;
+ struct avb_frame_header *h = (void*)stream->pdu;
+ int fd, res;
+
+ if (stream->source == NULL) {
+ if ((fd = setup_socket(stream)) < 0)
+ return fd;
+
+ stream->source = pw_loop_add_io(server->impl->loop, fd,
+ SPA_IO_IN, true, on_socket_data, stream);
+ if (stream->source == NULL) {
+ res = -errno;
+ pw_log_error("stream %p: can't create source: %m", stream);
+ close(fd);
+ return res;
+ }
+ }
+
+ avb_mrp_attribute_begin(stream->vlan_attr->mrp, now);
+ avb_mrp_attribute_join(stream->vlan_attr->mrp, now, true);
+
+ if (stream->direction == SPA_DIRECTION_INPUT) {
+ stream->listener_attr->attr.listener.stream_id = htobe64(stream->peer_id);
+ stream->listener_attr->param = AVB_MSRP_LISTENER_PARAM_READY;
+ avb_mrp_attribute_begin(stream->listener_attr->mrp, now);
+ avb_mrp_attribute_join(stream->listener_attr->mrp, now, true);
+
+ stream->talker_attr->attr.talker.stream_id = htobe64(stream->peer_id);
+ avb_mrp_attribute_begin(stream->talker_attr->mrp, now);
+ } else {
+ if ((res = avb_maap_get_address(server->maap, stream->addr, stream->index)) < 0)
+ return res;
+
+ stream->listener_attr->attr.listener.stream_id = htobe64(stream->id);
+ stream->listener_attr->param = AVB_MSRP_LISTENER_PARAM_IGNORE;
+ avb_mrp_attribute_begin(stream->listener_attr->mrp, now);
+
+ stream->talker_attr->attr.talker.stream_id = htobe64(stream->id);
+ memcpy(stream->talker_attr->attr.talker.dest_addr, stream->addr, 6);
+
+ stream->sock_addr.sll_halen = ETH_ALEN;
+ memcpy(&stream->sock_addr.sll_addr, stream->addr, ETH_ALEN);
+ memcpy(h->dest, stream->addr, 6);
+ memcpy(h->src, server->mac_addr, 6);
+ avb_mrp_attribute_begin(stream->talker_attr->mrp, now);
+ avb_mrp_attribute_join(stream->talker_attr->mrp, now, true);
+ }
+ pw_stream_set_active(stream->stream, true);
+ return 0;
+}
+
+int stream_deactivate(struct stream *stream, uint64_t now)
+{
+ pw_stream_set_active(stream->stream, false);
+
+ if (stream->source != NULL) {
+ pw_loop_destroy_source(stream->server->impl->loop, stream->source);
+ stream->source = NULL;
+ }
+
+ avb_mrp_attribute_leave(stream->vlan_attr->mrp, now);
+
+ if (stream->direction == SPA_DIRECTION_INPUT) {
+ avb_mrp_attribute_leave(stream->listener_attr->mrp, now);
+ } else {
+ avb_mrp_attribute_leave(stream->talker_attr->mrp, now);
+ }
+ return 0;
+}
diff --git a/src/modules/module-avb/stream.h b/src/modules/module-avb/stream.h
new file mode 100644
index 0000000..7062e25
--- /dev/null
+++ b/src/modules/module-avb/stream.h
@@ -0,0 +1,104 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_STREAM_H
+#define AVB_STREAM_H
+
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <linux/if_packet.h>
+#include <net/if.h>
+
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/audio/format.h>
+
+#include <pipewire/pipewire.h>
+
+#define BUFFER_SIZE (1u<<16)
+#define BUFFER_MASK (BUFFER_SIZE-1)
+
+struct stream {
+ struct spa_list link;
+
+ struct server *server;
+
+ uint16_t direction;
+ uint16_t index;
+ const struct descriptor *desc;
+ uint64_t id;
+ uint64_t peer_id;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ uint8_t addr[6];
+ struct spa_source *source;
+ int prio;
+ int vlan_id;
+ int mtt;
+ int t_uncertainty;
+ uint32_t frames_per_pdu;
+ int ptime_tolerance;
+
+ uint8_t pdu[2048];
+ size_t hdr_size;
+ size_t payload_size;
+ size_t pdu_size;
+ int64_t pdu_period;
+ uint8_t pdu_seq;
+ uint8_t prev_seq;
+ uint8_t dbc;
+
+ struct iovec iov[3];
+ struct sockaddr_ll sock_addr;
+ struct msghdr msg;
+ char control[CMSG_SPACE(sizeof(uint64_t))];
+ struct cmsghdr *cmsg;
+
+ struct spa_ringbuffer ring;
+ void *buffer_data;
+ size_t buffer_size;
+
+ uint64_t format;
+ uint32_t stride;
+ struct spa_audio_info info;
+
+ struct avb_msrp_attribute *talker_attr;
+ struct avb_msrp_attribute *listener_attr;
+ struct avb_mvrp_attribute *vlan_attr;
+};
+
+#include "msrp.h"
+#include "mvrp.h"
+#include "maap.h"
+
+struct stream *server_create_stream(struct server *server,
+ enum spa_direction direction, uint16_t index);
+
+void stream_destroy(struct stream *stream);
+
+int stream_activate(struct stream *stream, uint64_t now);
+int stream_deactivate(struct stream *stream, uint64_t now);
+
+#endif /* AVB_STREAM_H */
diff --git a/src/modules/module-avb/utils.h b/src/modules/module-avb/utils.h
new file mode 100644
index 0000000..f626722
--- /dev/null
+++ b/src/modules/module-avb/utils.h
@@ -0,0 +1,86 @@
+/* AVB support
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef AVB_UTILS_H
+#define AVB_UTILS_H
+
+#include <spa/utils/json.h>
+
+#include "internal.h"
+
+static inline char *avb_utils_format_id(char *str, size_t size, const uint64_t id)
+{
+ snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x:%04x",
+ (uint8_t)(id >> 56),
+ (uint8_t)(id >> 48),
+ (uint8_t)(id >> 40),
+ (uint8_t)(id >> 32),
+ (uint8_t)(id >> 24),
+ (uint8_t)(id >> 16),
+ (uint16_t)(id));
+ return str;
+}
+
+static inline int avb_utils_parse_id(const char *str, int len, uint64_t *id)
+{
+ char s[64];
+ uint8_t v[6];
+ uint16_t unique_id;
+ if (spa_json_parse_stringn(str, len, s, sizeof(s)) <= 0)
+ return -EINVAL;
+ if (sscanf(s, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx:%hx",
+ &v[0], &v[1], &v[2], &v[3],
+ &v[4], &v[5], &unique_id) == 7) {
+ *id = (uint64_t) v[0] << 56 |
+ (uint64_t) v[1] << 48 |
+ (uint64_t) v[2] << 40 |
+ (uint64_t) v[3] << 32 |
+ (uint64_t) v[4] << 24 |
+ (uint64_t) v[5] << 16 |
+ unique_id;
+ } else if (!spa_atou64(str, id, 0))
+ return -EINVAL;
+ return 0;
+}
+
+static inline char *avb_utils_format_addr(char *str, size_t size, const uint8_t addr[6])
+{
+ snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x",
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+ return str;
+}
+static inline int avb_utils_parse_addr(const char *str, int len, uint8_t addr[6])
+{
+ char s[64];
+ uint8_t v[6];
+ if (spa_json_parse_stringn(str, len, s, sizeof(s)) <= 0)
+ return -EINVAL;
+ if (sscanf(s, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
+ &v[0], &v[1], &v[2], &v[3], &v[4], &v[5]) != 6)
+ return -EINVAL;
+ memcpy(addr, v, 6);
+ return 0;
+}
+
+#endif /* AVB_UTILS_H */
diff --git a/src/modules/module-client-device.c b/src/modules/module-client-device.c
new file mode 100644
index 0000000..cf3bae3
--- /dev/null
+++ b/src/modules/module-client-device.c
@@ -0,0 +1,232 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include "module-client-device/client-device.h"
+
+/** \page page_module_client_device PipeWire Module: Client Device
+ */
+
+#define NAME "client-device"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Allow clients to create and control remote devices" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct pw_proxy *pw_core_spa_device_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct pw_protocol *pw_protocol_native_ext_client_device_init(struct pw_context *context);
+
+struct factory_data {
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_export_type export_spadevice;
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *data = _data;
+ struct pw_impl_factory *factory = data->factory;
+ void *result;
+ struct pw_resource *device_resource;
+ struct pw_impl_client *client;
+ int res;
+
+ if (resource == NULL) {
+ res = -EINVAL;
+ goto error_exit;
+ }
+
+ client = pw_resource_get_client(resource);
+ device_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (device_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_properties;
+ }
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_global_get_id(pw_impl_factory_get_global(factory)));
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_global_get_id(pw_impl_client_get_global(client)));
+
+ result = pw_client_device_new(device_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_device;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_properties:
+ pw_log_error("can't create properties: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create properties: %s", spa_strerror(res));
+ goto error_exit_free;
+error_device:
+ pw_log_error("can't create device: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create device: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(device_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export_spadevice.link);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+ factory = pw_context_create_factory(context,
+ "client-device",
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ pw_properties_new(
+ PW_KEY_FACTORY_USAGE, CLIENT_DEVICE_USAGE,
+ NULL),
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ data->export_spadevice.type = SPA_TYPE_INTERFACE_Device;
+ data->export_spadevice.func = pw_core_spa_device_export;
+ if ((res = pw_context_register_export_type(context, &data->export_spadevice)) < 0)
+ goto error;
+
+ pw_protocol_native_ext_client_device_init(context);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-client-device/client-device.h b/src/modules/module-client-device/client-device.h
new file mode 100644
index 0000000..ada2b7d
--- /dev/null
+++ b/src/modules/module-client-device/client-device.h
@@ -0,0 +1,44 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CLIENT_DEVICE_H
+#define PIPEWIRE_CLIENT_DEVICE_H
+
+#include <pipewire/impl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define CLIENT_DEVICE_USAGE "["PW_KEY_DEVICE_NAME"=<string>]"
+
+struct pw_device *
+pw_client_device_new(struct pw_resource *resource,
+ struct pw_properties *properties);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CLIENT_DEVICE_H */
diff --git a/src/modules/module-client-device/protocol-native.c b/src/modules/module-client-device/protocol-native.c
new file mode 100644
index 0000000..2734aac
--- /dev/null
+++ b/src/modules/module-client-device/protocol-native.c
@@ -0,0 +1,556 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include <pipewire/extensions/protocol-native.h>
+
+#define MAX_DICT 1024
+#define MAX_PARAM_INFO 128
+
+static inline void push_item(struct spa_pod_builder *b, const struct spa_dict_item *item)
+{
+ const char *str;
+ spa_pod_builder_string(b, item->key);
+ str = item->value;
+ if (spa_strstartswith(str, "pointer:"))
+ str = "";
+ spa_pod_builder_string(b, str);
+}
+
+static inline int parse_item(struct spa_pod_parser *prs, struct spa_dict_item *item)
+{
+ int res;
+ if ((res = spa_pod_parser_get(prs,
+ SPA_POD_String(&item->key),
+ SPA_POD_String(&item->value),
+ NULL)) < 0)
+ return res;
+ if (spa_strstartswith(item->value, "pointer:"))
+ item->value = "";
+ return 0;
+}
+
+#define parse_dict(prs,d) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(d)->n_items), NULL) < 0) \
+ return -EINVAL; \
+ if ((d)->n_items > 0) { \
+ if ((d)->n_items > MAX_DICT) \
+ return -ENOSPC; \
+ (d)->items = alloca((d)->n_items * sizeof(struct spa_dict_item)); \
+ for (i = 0; i < (d)->n_items; i++) { \
+ if (parse_item(prs, (struct spa_dict_item *) &(d)->items[i]) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+#define parse_param_info(prs,n_params,params) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(n_params)), NULL) < 0) \
+ return -EINVAL; \
+ if (n_params > 0) { \
+ if (n_params > MAX_PARAM_INFO) \
+ return -ENOSPC; \
+ params = alloca(n_params * sizeof(struct spa_param_info)); \
+ for (i = 0; i < n_params; i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Id(&(params[i]).id), \
+ SPA_POD_Int(&(params[i]).flags), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+static int device_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int device_demarshal_add_listener(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ return -ENOTSUP;
+}
+
+static int device_marshal_sync(void *object, int seq)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, SPA_DEVICE_METHOD_SYNC, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_sync(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ int seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct spa_device_methods, sync, 0, seq);
+ return 0;
+}
+
+static int device_marshal_enum_params(void *object, int seq,
+ uint32_t id, uint32_t index, uint32_t max,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, SPA_DEVICE_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(max),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, max;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&max),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct spa_device_methods, enum_params, 0,
+ seq, id, index, max, filter);
+ return 0;
+}
+
+static int device_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, SPA_DEVICE_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_set_param(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct spa_device_methods, set_param, 0,
+ id, flags, param);
+ return 0;
+}
+
+static void device_marshal_info(void *data,
+ const struct spa_device_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_proxy(proxy, SPA_DEVICE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ if (info) {
+ uint64_t change_mask = info->change_mask;
+
+ change_mask &= SPA_DEVICE_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS |
+ SPA_DEVICE_CHANGE_MASK_PARAMS;
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_add(b,
+ SPA_POD_Long(change_mask),
+ SPA_POD_Long(info->flags),
+ SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &info->props->items[i]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->n_params), NULL);
+ for (i = 0; i < info->n_params; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_Id(info->params[i].id),
+ SPA_POD_Int(info->params[i].flags), NULL);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ } else {
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(NULL), NULL);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod *ipod;
+ struct spa_device_info info = SPA_DEVICE_INFO_INIT(), *infop;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodStruct(&ipod)) < 0)
+ return -EINVAL;
+
+ if (ipod) {
+ struct spa_pod_parser p2;
+ struct spa_pod_frame f2;
+ infop = &info;
+
+ spa_pod_parser_pod(&p2, ipod);
+ if (spa_pod_parser_push_struct(&p2, &f2) < 0 ||
+ spa_pod_parser_get(&p2,
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Long(&info.flags), NULL) < 0)
+ return -EINVAL;
+
+ info.change_mask &= SPA_DEVICE_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS |
+ SPA_DEVICE_CHANGE_MASK_PARAMS;
+
+ parse_dict(&p2, &props);
+ if (props.n_items > 0)
+ info.props = &props;
+
+ parse_param_info(&p2, info.n_params, info.params);
+ }
+ else {
+ infop = NULL;
+ }
+ pw_resource_notify(resource, struct spa_device_events, info, 0, infop);
+ return 0;
+}
+
+static void device_marshal_result(void *data,
+ int seq, int res, uint32_t type, const void *result)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+
+ b = pw_protocol_native_begin_proxy(proxy, SPA_DEVICE_EVENT_RESULT, NULL);
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Int(res),
+ SPA_POD_Id(type),
+ NULL);
+
+ switch (type) {
+ case SPA_RESULT_TYPE_DEVICE_PARAMS:
+ {
+ const struct spa_result_device_params *r = result;
+ spa_pod_builder_add(b,
+ SPA_POD_Id(r->id),
+ SPA_POD_Int(r->index),
+ SPA_POD_Int(r->next),
+ SPA_POD_Pod(r->param),
+ NULL);
+ break;
+ }
+ default:
+ break;
+ }
+
+ spa_pod_builder_pop(b, &f[0]);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_result(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[1];
+ int seq, res;
+ uint32_t type;
+ const void *result;
+ struct spa_result_device_params params;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Int(&res),
+ SPA_POD_Id(&type),
+ NULL) < 0)
+ return -EINVAL;
+
+ switch (type) {
+ case SPA_RESULT_TYPE_DEVICE_PARAMS:
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Id(&params.id),
+ SPA_POD_Int(&params.index),
+ SPA_POD_Int(&params.next),
+ SPA_POD_PodObject(&params.param),
+ NULL) < 0)
+ return -EINVAL;
+
+ result = &params;
+ break;
+
+ default:
+ result = NULL;
+ break;
+ }
+
+ pw_resource_notify(resource, struct spa_device_events, result, 0, seq, res, type, result);
+ return 0;
+}
+
+static void device_marshal_event(void *data, const struct spa_event *event)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, SPA_DEVICE_EVENT_EVENT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(event));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_event(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_event *event;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodObject(&event)) < 0)
+ return -EINVAL;
+
+ pw_resource_notify(resource, struct spa_device_events, event, 0, event);
+ return 0;
+}
+
+static void device_marshal_object_info(void *data, uint32_t id,
+ const struct spa_device_object_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_proxy(proxy, SPA_DEVICE_EVENT_OBJECT_INFO, NULL);
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(id),
+ NULL);
+ if (info) {
+ uint64_t change_mask = info->change_mask;
+
+ change_mask &= SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_add(b,
+ SPA_POD_String(info->type),
+ SPA_POD_Long(change_mask),
+ SPA_POD_Long(info->flags),
+ SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &info->props->items[i]);
+ spa_pod_builder_pop(b, &f[1]);
+ } else {
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(NULL), NULL);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_object_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_device_object_info info = SPA_DEVICE_OBJECT_INFO_INIT(), *infop;
+ struct spa_pod *ipod;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_PodStruct(&ipod)) < 0)
+ return -EINVAL;
+
+ if (ipod) {
+ struct spa_pod_parser p2;
+ struct spa_pod_frame f2;
+ infop = &info;
+
+ spa_pod_parser_pod(&p2, ipod);
+ if (spa_pod_parser_push_struct(&p2, &f2) < 0 ||
+ spa_pod_parser_get(&p2,
+ SPA_POD_String(&info.type),
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Long(&info.flags), NULL) < 0)
+ return -EINVAL;
+
+ info.change_mask &= SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS;
+
+ parse_dict(&p2, &props);
+ if (props.n_items > 0)
+ info.props = &props;
+ } else {
+ infop = NULL;
+ }
+
+ pw_resource_notify(resource, struct spa_device_events, object_info, 0, id, infop);
+ return 0;
+}
+
+static const struct spa_device_methods pw_protocol_native_device_method_marshal = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = &device_marshal_add_listener,
+ .sync = &device_marshal_sync,
+ .enum_params = &device_marshal_enum_params,
+ .set_param = &device_marshal_set_param
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_device_method_demarshal[SPA_DEVICE_METHOD_NUM] =
+{
+ [SPA_DEVICE_METHOD_ADD_LISTENER] = { &device_demarshal_add_listener, 0 },
+ [SPA_DEVICE_METHOD_SYNC] = { &device_demarshal_sync, 0 },
+ [SPA_DEVICE_METHOD_ENUM_PARAMS] = { &device_demarshal_enum_params, 0 },
+ [SPA_DEVICE_METHOD_SET_PARAM] = { &device_demarshal_set_param, 0 },
+};
+
+static const struct spa_device_events pw_protocol_native_device_event_marshal = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .info = &device_marshal_info,
+ .result = &device_marshal_result,
+ .event = &device_marshal_event,
+ .object_info = &device_marshal_object_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_device_event_demarshal[SPA_DEVICE_EVENT_NUM] =
+{
+ [SPA_DEVICE_EVENT_INFO] = { &device_demarshal_info, 0 },
+ [SPA_DEVICE_EVENT_RESULT] = { &device_demarshal_result, 0 },
+ [SPA_DEVICE_EVENT_EVENT] = { &device_demarshal_event, 0 },
+ [SPA_DEVICE_EVENT_OBJECT_INFO] = { &device_demarshal_object_info, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_device_marshal = {
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ SPA_DEVICE_EVENT_NUM,
+ SPA_DEVICE_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_device_event_marshal,
+ .server_demarshal = pw_protocol_native_device_event_demarshal,
+ .server_marshal = &pw_protocol_native_device_method_marshal,
+ .client_demarshal = pw_protocol_native_device_method_demarshal,
+};
+
+struct pw_protocol *pw_protocol_native_ext_client_device_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+
+ if (protocol == NULL)
+ return NULL;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_device_marshal);
+
+ return protocol;
+}
diff --git a/src/modules/module-client-device/proxy-device.c b/src/modules/module-client-device/proxy-device.c
new file mode 100644
index 0000000..43bdf8e
--- /dev/null
+++ b/src/modules/module-client-device/proxy-device.c
@@ -0,0 +1,90 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include <spa/pod/parser.h>
+#include <spa/monitor/device.h>
+
+#include "pipewire/pipewire.h"
+
+struct device_data {
+ struct spa_device *device;
+ struct spa_hook device_listener;
+ struct spa_hook device_methods;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+};
+
+static void proxy_device_destroy(void *_data)
+{
+ struct device_data *data = _data;
+ spa_hook_remove(&data->device_listener);
+ spa_hook_remove(&data->device_methods);
+ spa_hook_remove(&data->proxy_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = proxy_device_destroy,
+};
+
+struct pw_proxy *pw_core_spa_device_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct spa_device *device = object;
+ struct spa_interface *iface, *diface;
+ struct pw_proxy *proxy;
+ struct device_data *data;
+
+ proxy = pw_core_create_object(core,
+ "client-device",
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ props,
+ user_data_size + sizeof(struct device_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct device_data);
+ data->device = device;
+ data->proxy = proxy;
+
+ iface = (struct spa_interface*)proxy;
+ diface = (struct spa_interface*)device;
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->device_methods,
+ diface->cb.funcs, diface->cb.data);
+ spa_device_add_listener(device, &data->device_listener,
+ iface->cb.funcs, iface->cb.data);
+
+ return proxy;
+}
diff --git a/src/modules/module-client-device/resource-device.c b/src/modules/module-client-device/resource-device.c
new file mode 100644
index 0000000..0f5a40b
--- /dev/null
+++ b/src/modules/module-client-device/resource-device.c
@@ -0,0 +1,155 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/impl.h>
+
+struct impl {
+ struct pw_context *context;
+ struct pw_impl_device *device;
+ struct spa_hook device_listener;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ unsigned int registered:1;
+};
+
+static void device_info(void *data, const struct spa_device_info *info)
+{
+ struct impl *impl = data;
+ if (!impl->registered) {
+ pw_impl_device_set_implementation(impl->device,
+ (struct spa_device*)impl->resource);
+ pw_impl_device_register(impl->device, NULL);
+ impl->registered = true;
+ }
+}
+
+static const struct spa_device_events object_events = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .info = device_info,
+};
+
+static void device_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ pw_log_debug("client-device %p: destroy", impl);
+
+ impl->resource = NULL;
+ spa_hook_remove(&impl->device_listener);
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->object_listener);
+ pw_impl_device_destroy(impl->device);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = device_resource_destroy,
+};
+
+static void device_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ pw_log_debug("client-device %p: destroy", impl);
+
+ impl->device = NULL;
+ spa_hook_remove(&impl->device_listener);
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->object_listener);
+ pw_resource_destroy(impl->resource);
+}
+
+static void device_initialized(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_device *device = impl->device;
+ struct pw_global *global = pw_impl_device_get_global(device);
+ uint32_t id = pw_global_get_id(global);
+
+ pw_log_debug("client-device %p: initialized global:%d", impl, id);
+ pw_resource_set_bound_id(impl->resource, id);
+}
+
+static const struct pw_impl_device_events device_events = {
+ PW_VERSION_IMPL_DEVICE_EVENTS,
+ .destroy = device_destroy,
+ .initialized = device_initialized,
+};
+
+struct pw_impl_device *pw_client_device_new(struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ struct pw_impl_device *device;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct pw_context *context = pw_impl_client_get_context(client);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+
+ device = pw_context_create_device(context, properties, sizeof(struct impl));
+ if (device == NULL)
+ return NULL;
+
+ impl = pw_impl_device_get_user_data(device);
+ impl->device = device;
+ impl->context = context;
+ impl->resource = resource;
+
+ pw_impl_device_add_listener(impl->device,
+ &impl->device_listener,
+ &device_events, impl);
+
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &resource_events,
+ impl);
+ pw_resource_add_object_listener(impl->resource,
+ &impl->object_listener,
+ &object_events,
+ impl);
+
+ return device;
+}
diff --git a/src/modules/module-client-node.c b/src/modules/module-client-node.c
new file mode 100644
index 0000000..4dc9bf8
--- /dev/null
+++ b/src/modules/module-client-node.c
@@ -0,0 +1,229 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include "module-client-node/v0/client-node.h"
+#include "module-client-node/client-node.h"
+
+/** \page page_module_client_node PipeWire Module: Client Node
+ */
+
+#define NAME "client-node"
+
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Allow clients to create and control remote nodes" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct pw_proxy *pw_core_node_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object, size_t user_data_size);
+struct pw_proxy *pw_core_spa_node_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object, size_t user_data_size);
+
+struct pw_protocol *pw_protocol_native_ext_client_node_init(struct pw_context *context);
+struct pw_protocol *pw_protocol_native_ext_client_node0_init(struct pw_context *context);
+
+struct factory_data {
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_export_type export_node;
+ struct pw_export_type export_spanode;
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ void *result;
+ struct pw_resource *node_resource;
+ struct pw_impl_client *client;
+ int res;
+
+ if (resource == NULL) {
+ res = -EINVAL;
+ goto error_exit;
+ }
+
+ client = pw_resource_get_client(resource);
+ node_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (node_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ if (version == 0) {
+ result = pw_impl_client_node0_new(node_resource, properties);
+ } else {
+ result = pw_impl_client_node_new(node_resource, properties, true);
+ }
+ if (result == NULL) {
+ res = -errno;
+ goto error_node;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_node:
+ pw_log_error("can't create node: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create node: %s", spa_strerror(res));
+ goto error_exit;
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export_node.link);
+ spa_list_remove(&d->export_spanode.link);
+
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "client-node",
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ data->export_node.type = PW_TYPE_INTERFACE_Node;
+ data->export_node.func = pw_core_node_export;
+ if ((res = pw_context_register_export_type(context, &data->export_node)) < 0)
+ goto error;
+
+ data->export_spanode.type = SPA_TYPE_INTERFACE_Node;
+ data->export_spanode.func = pw_core_spa_node_export;
+ if ((res = pw_context_register_export_type(context, &data->export_spanode)) < 0)
+ goto error_remove;
+
+ pw_protocol_native_ext_client_node_init(context);
+ pw_protocol_native_ext_client_node0_init(context);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+error_remove:
+ spa_list_remove(&data->export_node.link);
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-client-node/client-node.c b/src/modules/module-client-node/client-node.c
new file mode 100644
index 0000000..d5c133a
--- /dev/null
+++ b/src/modules/module-client-node/client-node.c
@@ -0,0 +1,1777 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <spa/support/system.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+#include "pipewire/private.h"
+
+#include "modules/spa/spa-node.h"
+#include "client-node.h"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/** \cond */
+
+#define MAX_BUFFERS 64
+#define MAX_METAS 16u
+#define MAX_DATAS 64u
+#define MAX_AREAS 2048
+
+#define CHECK_FREE_PORT(this,d,p) (p <= pw_map_get_size(&this->ports[d]) && !CHECK_PORT(this,d,p))
+#define CHECK_PORT(this,d,p) (pw_map_lookup(&this->ports[d], p) != NULL)
+#define GET_PORT(this,d,p) (pw_map_lookup(&this->ports[d], p))
+
+#define CHECK_PORT_BUFFER(this,b,p) (b < p->n_buffers)
+
+struct buffer {
+ struct spa_buffer *outbuf;
+ struct spa_buffer buffer;
+ struct spa_meta metas[MAX_METAS];
+ struct spa_data datas[MAX_DATAS];
+ struct pw_memblock *mem;
+};
+
+struct mix {
+ unsigned int valid:1;
+ uint32_t id;
+ struct port *port;
+ uint32_t peer_id;
+ uint32_t n_buffers;
+ struct buffer buffers[MAX_BUFFERS];
+};
+
+struct params {
+ uint32_t n_params;
+ struct spa_pod **params;
+};
+
+struct port {
+ struct pw_impl_port *port;
+ struct node *node;
+ struct impl *impl;
+
+ enum spa_direction direction;
+ uint32_t id;
+
+ struct spa_node mix_node;
+
+ struct spa_port_info info;
+ struct pw_properties *properties;
+
+ struct params params;
+
+ unsigned int removed:1;
+ unsigned int destroyed:1;
+
+ struct pw_array mix;
+};
+
+struct node {
+ struct spa_node node;
+
+ struct impl *impl;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct pw_resource *resource;
+ struct pw_impl_client *client;
+
+ struct spa_source data_source;
+ int writefd;
+
+ struct pw_map ports[2];
+
+ struct port dummy;
+
+ struct params params;
+};
+
+struct impl {
+ struct pw_impl_client_node this;
+
+ struct pw_context *context;
+
+ struct node node;
+
+ struct pw_map io_map;
+ struct pw_memblock *io_areas;
+
+ struct pw_memblock *activation;
+
+ struct spa_hook node_listener;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ uint32_t node_id;
+
+ uint32_t bind_node_version;
+ uint32_t bind_node_id;
+
+ int fds[2];
+ int other_fds[2];
+};
+
+#define pw_client_node_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_client_node_events,m,v,__VA_ARGS__)
+
+#define pw_client_node_resource_transport(r,...) \
+ pw_client_node_resource(r,transport,0,__VA_ARGS__)
+#define pw_client_node_resource_set_param(r,...) \
+ pw_client_node_resource(r,set_param,0,__VA_ARGS__)
+#define pw_client_node_resource_set_io(r,...) \
+ pw_client_node_resource(r,set_io,0,__VA_ARGS__)
+#define pw_client_node_resource_event(r,...) \
+ pw_client_node_resource(r,event,0,__VA_ARGS__)
+#define pw_client_node_resource_command(r,...) \
+ pw_client_node_resource(r,command,0,__VA_ARGS__)
+#define pw_client_node_resource_add_port(r,...) \
+ pw_client_node_resource(r,add_port,0,__VA_ARGS__)
+#define pw_client_node_resource_remove_port(r,...) \
+ pw_client_node_resource(r,remove_port,0,__VA_ARGS__)
+#define pw_client_node_resource_port_set_param(r,...) \
+ pw_client_node_resource(r,port_set_param,0,__VA_ARGS__)
+#define pw_client_node_resource_port_use_buffers(r,...) \
+ pw_client_node_resource(r,port_use_buffers,0,__VA_ARGS__)
+#define pw_client_node_resource_port_set_io(r,...) \
+ pw_client_node_resource(r,port_set_io,0,__VA_ARGS__)
+#define pw_client_node_resource_set_activation(r,...) \
+ pw_client_node_resource(r,set_activation,0,__VA_ARGS__)
+#define pw_client_node_resource_port_set_mix_info(r,...) \
+ pw_client_node_resource(r,port_set_mix_info,1,__VA_ARGS__)
+
+static int update_params(struct params *p, uint32_t n_params, const struct spa_pod **params)
+{
+ uint32_t i;
+ for (i = 0; i < p->n_params; i++)
+ free(p->params[i]);
+ p->n_params = n_params;
+ if (p->n_params == 0) {
+ free(p->params);
+ p->params = NULL;
+ } else {
+ struct spa_pod **np;
+ np = pw_reallocarray(p->params, p->n_params, sizeof(struct spa_pod *));
+ if (np == NULL) {
+ pw_log_error("%p: can't realloc: %m", p);
+ free(p->params);
+ p->params = NULL;
+ p->n_params = 0;
+ return -errno;
+ }
+ p->params = np;
+ }
+ for (i = 0; i < p->n_params; i++)
+ p->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ return 0;
+}
+
+static int
+do_port_use_buffers(struct impl *impl,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers);
+
+/** \endcond */
+
+static struct mix *find_mix(struct port *p, uint32_t mix_id)
+{
+ struct mix *mix;
+ size_t len;
+
+ if (mix_id == SPA_ID_INVALID)
+ mix_id = 0;
+ else
+ mix_id++;
+
+ len = pw_array_get_len(&p->mix, struct mix);
+ if (mix_id >= len) {
+ size_t need = sizeof(struct mix) * (mix_id + 1 - len);
+ void *ptr = pw_array_add(&p->mix, need);
+ if (ptr == NULL)
+ return NULL;
+ memset(ptr, 0, need);
+ }
+ mix = pw_array_get_unchecked(&p->mix, mix_id, struct mix);
+ return mix;
+}
+
+static void mix_init(struct mix *mix, struct port *p, uint32_t id)
+{
+ mix->valid = true;
+ mix->id = id;
+ mix->port = p;
+ mix->n_buffers = 0;
+}
+
+static struct mix *ensure_mix(struct impl *impl, struct port *p, uint32_t mix_id)
+{
+ struct mix *mix;
+
+ if ((mix = find_mix(p, mix_id)) == NULL)
+ return NULL;
+ if (mix->valid)
+ return mix;
+ mix_init(mix, p, mix_id);
+ return mix;
+}
+
+static void clear_data(struct node *this, struct spa_data *d)
+{
+ struct impl *impl = this->impl;
+
+ switch (d->type) {
+ case SPA_DATA_MemId:
+ {
+ uint32_t id;
+ struct pw_memblock *m;
+
+ id = SPA_PTR_TO_UINT32(d->data);
+ m = pw_mempool_find_id(this->client->pool, id);
+ if (m) {
+ pw_log_debug("%p: mem %d", impl, m->id);
+ pw_memblock_unref(m);
+ }
+ break;
+ }
+ case SPA_DATA_MemFd:
+ case SPA_DATA_DmaBuf:
+ pw_log_debug("%p: close fd:%d", impl, (int)d->fd);
+ close(d->fd);
+ break;
+ default:
+ break;
+ }
+}
+
+static int clear_buffers(struct node *this, struct mix *mix)
+{
+ uint32_t i, j;
+
+ for (i = 0; i < mix->n_buffers; i++) {
+ struct buffer *b = &mix->buffers[i];
+
+ spa_log_debug(this->log, "%p: clear buffer %d", this, i);
+
+ for (j = 0; j < b->buffer.n_datas; j++) {
+ struct spa_data *d = &b->datas[j];
+ clear_data(this, d);
+ }
+ pw_memblock_unref(b->mem);
+ }
+ mix->n_buffers = 0;
+ return 0;
+}
+
+static void mix_clear(struct node *this, struct mix *mix)
+{
+ struct port *port = mix->port;
+
+ if (!mix->valid)
+ return;
+ do_port_use_buffers(this->impl, port->direction, port->id,
+ mix->id, 0, NULL, 0);
+ mix->valid = false;
+}
+
+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 node *this = object;
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ bool found = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = 0;
+
+ while (true) {
+ struct spa_pod *param;
+
+ result.index = result.next++;
+ if (result.index >= this->params.n_params)
+ break;
+
+ param = this->params.params[result.index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ found = true;
+
+ if (result.index < start)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ if (spa_pod_filter(&b.b, &result.param, param, filter) == 0) {
+ pw_log_debug("%p: %d param %u", this, seq, result.index);
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (this->resource == NULL)
+ return param == NULL ? 0 : -EIO;
+
+ return pw_client_node_resource_set_param(this->resource, id, flags, param);
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct node *this = object;
+ struct impl *impl = this->impl;
+ struct pw_memmap *mm, *old;
+ uint32_t memid, mem_offset, mem_size;
+ uint32_t tag[5] = { impl->node_id, id, };
+
+ if (impl->this.flags & 1)
+ return 0;
+
+ old = pw_mempool_find_tag(this->client->pool, tag, sizeof(tag));
+
+ if (data) {
+ mm = pw_mempool_import_map(this->client->pool,
+ impl->context->pool, data, size, tag);
+ if (mm == NULL)
+ return -errno;
+
+ mem_offset = mm->offset;
+ memid = mm->block->id;
+ mem_size = size;
+ }
+ else {
+ memid = SPA_ID_INVALID;
+ mem_offset = mem_size = 0;
+ }
+ pw_memmap_free(old);
+
+ if (this->resource == NULL)
+ return data == NULL ? 0 : -EIO;
+
+ return pw_client_node_resource_set_io(this->resource,
+ id,
+ memid,
+ mem_offset, mem_size);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ pw_log_debug("%p: send command %d", this, SPA_COMMAND_TYPE(command));
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ return pw_client_node_resource_command(this->resource, command);
+}
+
+
+static void emit_port_info(struct node *this, struct port *port)
+{
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct node *this = object;
+ struct spa_hook_list save;
+ union pw_map_item *item;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ pw_array_for_each(item, &this->ports[SPA_DIRECTION_INPUT].items) {
+ if (item->data)
+ emit_port_info(this, item->data);
+ }
+ pw_array_for_each(item, &this->ports[SPA_DIRECTION_OUTPUT].items) {
+ if (item->data)
+ emit_port_info(this, item->data);
+ }
+ 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 node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ pw_log_debug("%p: sync", this);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ return pw_resource_ping(this->resource, seq);
+}
+
+static void
+do_update_port(struct node *this,
+ struct port *port,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info)
+{
+ if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_PARAMS) {
+ spa_log_debug(this->log, "%p: port %u update %d params", this, port->id, n_params);
+ update_params(&port->params, n_params, params);
+ }
+
+ if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_INFO) {
+ pw_properties_free(port->properties);
+ port->properties = NULL;
+ port->info.props = NULL;
+ port->info.n_params = 0;
+ port->info.params = NULL;
+
+ if (info) {
+ port->info = *info;
+ if (info->props) {
+ port->properties = pw_properties_new_dict(info->props);
+ port->info.props = &port->properties->dict;
+ }
+ port->info.n_params = 0;
+ port->info.params = NULL;
+ spa_node_emit_port_info(&this->hooks, port->direction, port->id, info);
+ }
+ }
+}
+
+static void
+clear_port(struct node *this, struct port *port)
+{
+ struct mix *mix;
+
+ spa_log_debug(this->log, "%p: clear port %p", this, port);
+
+ do_update_port(this, port,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO, 0, NULL, NULL);
+
+ pw_array_for_each(mix, &port->mix)
+ mix_clear(this, mix);
+ pw_array_clear(&port->mix);
+ pw_array_init(&port->mix, sizeof(struct mix) * 2);
+
+ pw_map_insert_at(&this->ports[port->direction], port->id, NULL);
+
+ if (!port->removed)
+ spa_node_emit_port_info(&this->hooks, port->direction, port->id, NULL);
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_FREE_PORT(this, direction, port_id), -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ return pw_client_node_resource_add_port(this->resource, direction, port_id, props);
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ return pw_client_node_resource_remove_port(this->resource, direction, port_id);
+}
+
+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 node *this = object;
+ struct port *port;
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ bool found = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+ spa_return_val_if_fail(port != NULL, -EINVAL);
+
+ pw_log_debug("%p: seq:%d port %d.%d id:%u start:%u num:%u n_params:%d",
+ this, seq, direction, port_id, id, start, num, port->params.n_params);
+
+ result.id = id;
+ result.next = 0;
+
+ while (true) {
+ struct spa_pod *param;
+
+ result.index = result.next++;
+ if (result.index >= port->params.n_params)
+ break;
+
+ param = port->params.params[result.index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ found = true;
+
+ if (result.index < start)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ if (spa_pod_filter(&b.b, &result.param, param, filter) == 0) {
+ pw_log_debug("%p: %d param %u", this, seq, result.index);
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+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 node *this = object;
+ struct port *port;
+ struct mix *mix;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+ if(port == NULL)
+ return param == NULL ? 0 : -EINVAL;
+
+ pw_log_debug("%p: port %d.%d set param %s %d", this,
+ direction, port_id,
+ spa_debug_type_find_name(spa_type_param, id), id);
+
+ if (id == SPA_PARAM_Format) {
+ pw_array_for_each(mix, &port->mix)
+ clear_buffers(this, mix);
+ }
+ if (this->resource == NULL)
+ return param == NULL ? 0 : -EIO;
+
+ return pw_client_node_resource_port_set_param(this->resource,
+ direction, port_id,
+ id, flags,
+ param);
+}
+
+static int do_port_set_io(struct impl *impl,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct node *this = &impl->node;
+ uint32_t memid, mem_offset, mem_size;
+ struct port *port;
+ struct mix *mix;
+ uint32_t tag[5] = { impl->node_id, direction, port_id, mix_id, id };
+ struct pw_memmap *mm, *old;
+
+ pw_log_debug("%p: %s port %d.%d set io %p %zd", this,
+ direction == SPA_DIRECTION_INPUT ? "input" : "output",
+ port_id, mix_id, data, size);
+
+ port = GET_PORT(this, direction, port_id);
+ if (port == NULL)
+ return data == NULL ? 0 : -EINVAL;
+
+ if ((mix = find_mix(port, mix_id)) == NULL || !mix->valid)
+ return -EINVAL;
+
+ old = pw_mempool_find_tag(this->client->pool, tag, sizeof(tag));
+
+ if (data) {
+ mm = pw_mempool_import_map(this->client->pool,
+ impl->context->pool, data, size, tag);
+ if (mm == NULL)
+ return -errno;
+
+ mem_offset = mm->offset;
+ memid = mm->block->id;
+ mem_size = size;
+ }
+ else {
+ memid = SPA_ID_INVALID;
+ mem_offset = mem_size = 0;
+ }
+ pw_memmap_free(old);
+
+ if (this->resource == NULL)
+ return data == NULL ? 0 : -EIO;
+
+ return pw_client_node_resource_port_set_io(this->resource,
+ direction, port_id,
+ mix_id,
+ id,
+ memid,
+ mem_offset, mem_size);
+}
+
+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)
+{
+ /* ignore io on the node itself, we only care about the io on the
+ * port mixers, the io on the node ports itself is handled on the
+ * client side */
+ return -EINVAL;
+}
+
+static int
+do_port_use_buffers(struct impl *impl,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct node *this = &impl->node;
+ struct port *p;
+ struct mix *mix;
+ uint32_t i, j;
+ struct pw_client_node_buffer *mb;
+
+ p = GET_PORT(this, direction, port_id);
+ if (p == NULL)
+ return n_buffers == 0 ? 0 : -EINVAL;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ spa_log_debug(this->log, "%p: %s port %d.%d use buffers %p %u flags:%08x", this,
+ direction == SPA_DIRECTION_INPUT ? "input" : "output",
+ port_id, mix_id, buffers, n_buffers, flags);
+
+ if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
+ return -EINVAL;
+
+ if (direction == SPA_DIRECTION_OUTPUT) {
+ mix_id = SPA_ID_INVALID;
+ if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
+ return -EINVAL;
+ }
+
+ clear_buffers(this, mix);
+
+ if (n_buffers > 0) {
+ mb = alloca(n_buffers * sizeof(struct pw_client_node_buffer));
+ } else {
+ mb = NULL;
+ }
+
+ if (this->resource == NULL)
+ return n_buffers == 0 ? 0 : -EIO;
+
+ if (p->destroyed)
+ return 0;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &mix->buffers[i];
+ struct pw_memblock *mem, *m;
+ void *baseptr, *endptr;
+
+ b->outbuf = buffers[i];
+ memcpy(&b->buffer, buffers[i], sizeof(struct spa_buffer));
+ b->buffer.datas = b->datas;
+ b->buffer.metas = b->metas;
+
+ if (buffers[i]->n_metas > 0)
+ baseptr = buffers[i]->metas[0].data;
+ else if (buffers[i]->n_datas > 0)
+ baseptr = buffers[i]->datas[0].chunk;
+ else
+ return -EINVAL;
+
+ if ((mem = pw_mempool_find_ptr(impl->context->pool, baseptr)) == NULL)
+ return -EINVAL;
+
+ endptr = SPA_PTROFF(baseptr, buffers[i]->n_datas * sizeof(struct spa_chunk), void);
+ for (j = 0; j < buffers[i]->n_metas; j++) {
+ endptr = SPA_PTROFF(endptr, SPA_ROUND_UP_N(buffers[i]->metas[j].size, 8), void);
+ }
+ for (j = 0; j < buffers[i]->n_datas; j++) {
+ struct spa_data *d = &buffers[i]->datas[j];
+ if (d->type == SPA_DATA_MemPtr) {
+ if ((m = pw_mempool_find_ptr(impl->context->pool, d->data)) == NULL ||
+ m != mem)
+ return -EINVAL;
+ endptr = SPA_MAX(endptr, SPA_PTROFF(d->data, d->maxsize, void));
+ }
+ }
+ if (endptr > SPA_PTROFF(baseptr, mem->size, void))
+ return -EINVAL;
+
+ m = pw_mempool_import_block(this->client->pool, mem);
+ if (m == NULL)
+ return -errno;
+
+ b->mem = m;
+
+ mb[i].buffer = &b->buffer;
+ mb[i].mem_id = m->id;
+ mb[i].offset = SPA_PTRDIFF(baseptr, mem->map->ptr);
+ mb[i].size = SPA_PTRDIFF(endptr, baseptr);
+ spa_log_debug(this->log, "%p: buffer %d %d %d %d", this, i, mb[i].mem_id,
+ mb[i].offset, mb[i].size);
+
+ b->buffer.n_metas = SPA_MIN(buffers[i]->n_metas, MAX_METAS);
+ for (j = 0; j < b->buffer.n_metas; j++)
+ memcpy(&b->buffer.metas[j], &buffers[i]->metas[j], sizeof(struct spa_meta));
+
+ b->buffer.n_datas = SPA_MIN(buffers[i]->n_datas, MAX_DATAS);
+ for (j = 0; j < b->buffer.n_datas; j++) {
+ struct spa_data *d = &buffers[i]->datas[j];
+
+ memcpy(&b->datas[j], d, sizeof(struct spa_data));
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC)
+ continue;
+
+ switch (d->type) {
+ case SPA_DATA_DmaBuf:
+ case SPA_DATA_MemFd:
+ {
+ uint32_t flags = PW_MEMBLOCK_FLAG_DONT_CLOSE;
+
+ if (d->flags & SPA_DATA_FLAG_READABLE)
+ flags |= PW_MEMBLOCK_FLAG_READABLE;
+ if (d->flags & SPA_DATA_FLAG_WRITABLE)
+ flags |= PW_MEMBLOCK_FLAG_WRITABLE;
+
+ spa_log_debug(this->log, "mem %d type:%d fd:%d", j, d->type, (int)d->fd);
+ m = pw_mempool_import(this->client->pool,
+ flags, d->type, d->fd);
+ if (m == NULL)
+ return -errno;
+
+ b->datas[j].type = SPA_DATA_MemId;
+ b->datas[j].data = SPA_UINT32_TO_PTR(m->id);
+ break;
+ }
+ case SPA_DATA_MemPtr:
+ spa_log_debug(this->log, "mem %d %zd", j, SPA_PTRDIFF(d->data, baseptr));
+ b->datas[j].data = SPA_INT_TO_PTR(SPA_PTRDIFF(d->data, baseptr));
+ break;
+ default:
+ b->datas[j].type = SPA_ID_INVALID;
+ b->datas[j].data = NULL;
+ spa_log_error(this->log, "invalid memory type %d", d->type);
+ break;
+ }
+ }
+ }
+ mix->n_buffers = n_buffers;
+
+ return pw_client_node_resource_port_use_buffers(this->resource,
+ direction, port_id, mix_id, flags,
+ n_buffers, mb);
+}
+
+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 node *this = object;
+ struct impl *impl;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ impl = this->impl;
+
+ return do_port_use_buffers(impl, direction, port_id,
+ SPA_ID_INVALID, flags, buffers, n_buffers);
+}
+
+static int
+impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+ spa_log_trace_fp(this->log, "reuse buffer %d", buffer_id);
+
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct node *this = object;
+ struct impl *impl = this->impl;
+ struct pw_impl_node *n = impl->this.node;
+ struct timespec ts;
+
+ spa_log_trace_fp(this->log, "%p: send process driver:%p", this, impl->this.node->driver_node);
+
+ if (SPA_UNLIKELY(spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts) < 0))
+ spa_zero(ts);
+
+ n->rt.activation->status = PW_NODE_ACTIVATION_TRIGGERED;
+ n->rt.activation->signal_time = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ if (SPA_UNLIKELY(spa_system_eventfd_write(this->data_system, this->writefd, 1) < 0))
+ spa_log_warn(this->log, "%p: error %m", this);
+
+ return SPA_STATUS_OK;
+}
+
+static struct pw_node *
+client_node_get_node(void *data,
+ uint32_t version,
+ size_t user_data_size)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ uint32_t new_id = user_data_size;
+
+ pw_log_debug("%p: bind %u/%u", this, new_id, version);
+
+ impl->bind_node_version = version;
+ impl->bind_node_id = new_id;
+ pw_map_insert_at(&this->client->objects, new_id, NULL);
+
+ return NULL;
+}
+
+static int
+client_node_update(void *data,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_node_info *info)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ if (change_mask & PW_CLIENT_NODE_UPDATE_PARAMS) {
+ pw_log_debug("%p: update %d params", this, n_params);
+ update_params(&this->params, n_params, params);
+ }
+ if (change_mask & PW_CLIENT_NODE_UPDATE_INFO) {
+ spa_node_emit_info(&this->hooks, info);
+ }
+ pw_log_debug("%p: got node update", this);
+ return 0;
+}
+
+static int
+client_node_port_update(void *data,
+ 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)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct port *port;
+ bool remove;
+
+ spa_log_debug(this->log, "%p: got port update change:%08x params:%d",
+ this, change_mask, n_params);
+
+ remove = (change_mask == 0);
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (remove) {
+ if (port == NULL)
+ return 0;
+ port->destroyed = true;
+ clear_port(this, port);
+ } else {
+ struct port *target;
+
+ if (port == NULL) {
+ if (!CHECK_FREE_PORT(this, direction, port_id))
+ return -EINVAL;
+
+ target = &this->dummy;
+ spa_zero(this->dummy);
+ target->direction = direction;
+ target->id = port_id;
+ } else
+ target = port;
+
+ do_update_port(this,
+ target,
+ change_mask,
+ n_params, params,
+ info);
+ }
+ return 0;
+}
+
+static int client_node_set_active(void *data, bool active)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ spa_log_debug(this->log, "%p: active:%d", this, active);
+ return pw_impl_node_set_active(impl->this.node, active);
+}
+
+static int client_node_event(void *data, const struct spa_event *event)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ spa_node_emit_event(&this->hooks, event);
+ return 0;
+}
+
+static int client_node_port_buffers(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t n_buffers,
+ struct spa_buffer **buffers)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct port *p;
+ struct mix *mix;
+ uint32_t i, j;
+
+ spa_log_debug(this->log, "%p: %s port %d.%d buffers %p %u", this,
+ direction == SPA_DIRECTION_INPUT ? "input" : "output",
+ port_id, mix_id, buffers, n_buffers);
+
+ p = GET_PORT(this, direction, port_id);
+ spa_return_val_if_fail(p != NULL, -EINVAL);
+
+ if (direction == SPA_DIRECTION_OUTPUT)
+ mix_id = SPA_ID_INVALID;
+
+ if ((mix = find_mix(p, mix_id)) == NULL || !mix->valid)
+ return -EINVAL;
+
+ if (mix->n_buffers != n_buffers)
+ return -EINVAL;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *oldbuf, *newbuf;
+ struct buffer *b = &mix->buffers[i];
+
+ oldbuf = b->outbuf;
+ newbuf = buffers[i];
+
+ spa_log_debug(this->log, "buffer %d n_datas:%d", i, newbuf->n_datas);
+
+ if (oldbuf->n_datas != newbuf->n_datas)
+ return -EINVAL;
+
+ for (j = 0; j < b->buffer.n_datas; j++) {
+ struct spa_chunk *oldchunk = oldbuf->datas[j].chunk;
+ struct spa_data *d = &newbuf->datas[j];
+
+ /* overwrite everything except the chunk */
+ oldbuf->datas[j] = *d;
+ oldbuf->datas[j].chunk = oldchunk;
+
+ b->datas[j].type = d->type;
+ b->datas[j].fd = d->fd;
+
+ spa_log_debug(this->log, " data %d type:%d fl:%08x fd:%d, offs:%d max:%d",
+ j, d->type, d->flags, (int) d->fd, d->mapoffset,
+ d->maxsize);
+ }
+ }
+ mix->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static const struct pw_client_node_methods client_node_methods = {
+ PW_VERSION_CLIENT_NODE_METHODS,
+ .get_node = client_node_get_node,
+ .update = client_node_update,
+ .port_update = client_node_port_update,
+ .set_active = client_node_set_active,
+ .event = client_node_event,
+ .port_buffers = client_node_port_buffers,
+};
+
+static void node_on_data_fd_events(struct spa_source *source)
+{
+ struct node *this = source->data;
+
+ if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ spa_log_warn(this->log, "%p: got error", this);
+ return;
+ }
+
+ if (source->rmask & SPA_IO_IN) {
+ uint64_t cmd;
+ struct pw_impl_node *node = this->impl->this.node;
+
+ if (SPA_UNLIKELY(spa_system_eventfd_read(this->data_system,
+ this->data_source.fd, &cmd) < 0))
+ pw_log_warn("%p: read failed %m", this);
+ else if (SPA_UNLIKELY(cmd > 1))
+ pw_log_info("(%s-%u) client missed %"PRIu64" wakeups",
+ node->name, node->info.id, cmd - 1);
+
+ spa_log_trace_fp(this->log, "%p: got ready", this);
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+ }
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int
+node_init(struct node *this,
+ struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ 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);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data-loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data-system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ this->data_source.func = node_on_data_fd_events;
+ this->data_source.data = this;
+ this->data_source.fd = -1;
+ this->data_source.mask = SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP;
+ this->data_source.rmask = 0;
+
+ return 0;
+}
+
+static int node_clear(struct node *this)
+{
+ update_params(&this->params, 0, NULL);
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct spa_source *source = user_data;
+ spa_loop_remove_source(loop, source);
+ return 0;
+}
+
+static void client_node_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node *this = &impl->this;
+ struct node *node = &impl->node;
+
+ pw_log_debug("%p: destroy", node);
+
+ impl->node.resource = this->resource = NULL;
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->object_listener);
+
+ if (node->data_source.fd != -1) {
+ spa_loop_invoke(node->data_loop,
+ do_remove_source,
+ SPA_ID_INVALID,
+ NULL,
+ 0,
+ true,
+ &node->data_source);
+ }
+ if (this->node)
+ pw_impl_node_destroy(this->node);
+}
+
+static void client_node_resource_error(void *data, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct spa_result_node_error result;
+
+ pw_log_error("%p: error seq:%d %d (%s)", this, seq, res, message);
+ result.message = message;
+ spa_node_emit_result(&this->hooks, seq, res, SPA_RESULT_TYPE_NODE_ERROR, &result);
+}
+
+static void client_node_resource_pong(void *data, int seq)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ pw_log_debug("%p: got pong, emit result %d", this, seq);
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+}
+
+void pw_impl_client_node_registered(struct pw_impl_client_node *this, struct pw_global *global)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ struct pw_impl_node *node = this->node;
+ struct pw_impl_client *client = impl->node.client;
+ uint32_t node_id = global->id;
+
+ pw_log_debug("%p: %d", &impl->node, node_id);
+
+ impl->activation = pw_mempool_import_block(client->pool, node->activation);
+ if (impl->activation == NULL) {
+ pw_log_debug("%p: can't import block: %m", &impl->node);
+ return;
+ }
+ impl->node_id = node_id;
+
+ if (this->resource == NULL)
+ return;
+
+ pw_resource_set_bound_id(this->resource, node_id);
+
+ pw_client_node_resource_transport(this->resource,
+ impl->other_fds[0],
+ impl->other_fds[1],
+ impl->activation->id,
+ 0,
+ sizeof(struct pw_node_activation));
+
+ if (impl->bind_node_id) {
+ pw_global_bind(global, client, PW_PERM_ALL,
+ impl->bind_node_version, impl->bind_node_id);
+ }
+}
+
+static void node_initialized(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node *this = &impl->this;
+ struct node *node = &impl->node;
+ struct pw_global *global;
+ struct spa_system *data_system = impl->node.data_system;
+ size_t size;
+
+ impl->fds[0] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ impl->fds[1] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ impl->other_fds[0] = impl->fds[1];
+ impl->other_fds[1] = impl->fds[0];
+ node->data_source.fd = impl->fds[0];
+ node->writefd = impl->fds[1];
+
+ spa_loop_add_source(node->data_loop, &node->data_source);
+ pw_log_debug("%p: transport read-fd:%d write-fd:%d", node, impl->fds[0], impl->fds[1]);
+
+ size = sizeof(struct spa_io_buffers) * MAX_AREAS;
+
+ impl->io_areas = pw_mempool_alloc(impl->context->pool,
+ PW_MEMBLOCK_FLAG_READWRITE |
+ PW_MEMBLOCK_FLAG_MAP |
+ PW_MEMBLOCK_FLAG_SEAL,
+ SPA_DATA_MemFd, size);
+ if (impl->io_areas == NULL)
+ return;
+
+ pw_log_debug("%p: io areas %p", node, impl->io_areas->map->ptr);
+
+ if ((global = pw_impl_node_get_global(this->node)) != NULL)
+ pw_impl_client_node_registered(this, global);
+}
+
+static void node_free(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node *this = &impl->this;
+ struct node *node = &impl->node;
+ struct spa_system *data_system = node->data_system;
+ uint32_t tag[5] = { impl->node_id, };
+ struct pw_memmap *mm;
+
+ this->node = NULL;
+
+ pw_log_debug("%p: free", node);
+ node_clear(node);
+
+ spa_hook_remove(&impl->node_listener);
+
+ while ((mm = pw_mempool_find_tag(node->client->pool, tag, sizeof(uint32_t))) != NULL)
+ pw_memmap_free(mm);
+
+ if (this->resource)
+ pw_resource_destroy(this->resource);
+
+ if (impl->activation)
+ pw_memblock_unref(impl->activation);
+ if (impl->io_areas)
+ pw_memblock_unref(impl->io_areas);
+
+ pw_map_clear(&impl->node.ports[0]);
+ pw_map_clear(&impl->node.ports[1]);
+ pw_map_clear(&impl->io_map);
+
+ if (impl->fds[0] != -1)
+ spa_system_close(data_system, impl->fds[0]);
+ if (impl->fds[1] != -1)
+ spa_system_close(data_system, impl->fds[1]);
+ free(impl);
+}
+
+static int port_init_mix(void *data, struct pw_impl_port_mix *mix)
+{
+ struct port *port = data;
+ struct impl *impl = port->impl;
+ struct mix *m;
+
+ if ((m = ensure_mix(impl, port, mix->port.port_id)) == NULL)
+ return -ENOMEM;
+
+ mix->id = pw_map_insert_new(&impl->io_map, NULL);
+ if (mix->id == SPA_ID_INVALID) {
+ m->valid = false;
+ return -errno;
+ }
+ if (mix->id > MAX_AREAS) {
+ pw_map_remove(&impl->io_map, mix->id);
+ m->valid = false;
+ return -ENOMEM;
+ }
+
+ mix->io = SPA_PTROFF(impl->io_areas->map->ptr,
+ mix->id * sizeof(struct spa_io_buffers), void);
+ *mix->io = SPA_IO_BUFFERS_INIT;
+
+ m->peer_id = mix->peer_id;
+
+ pw_log_debug("%p: init mix id:%d io:%p base:%p", impl,
+ mix->id, mix->io, impl->io_areas->map->ptr);
+
+ return 0;
+}
+
+static int port_release_mix(void *data, struct pw_impl_port_mix *mix)
+{
+ struct port *port = data;
+ struct impl *impl = port->impl;
+ struct node *this = &impl->node;
+ struct mix *m;
+
+ pw_log_debug("%p: remove mix id:%d io:%p base:%p",
+ this, mix->id, mix->io, impl->io_areas->map->ptr);
+
+ if ((m = find_mix(port, mix->port.port_id)) == NULL || !m->valid)
+ return -EINVAL;
+
+ pw_map_remove(&impl->io_map, mix->id);
+ m->valid = false;
+
+ return 0;
+}
+
+static const struct pw_impl_port_implementation port_impl = {
+ PW_VERSION_PORT_IMPLEMENTATION,
+ .init_mix = port_init_mix,
+ .release_mix = port_release_mix,
+};
+
+static int
+impl_mix_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 port *port = object;
+
+ if (port->direction != direction)
+ return -ENOTSUP;
+
+ return impl_node_port_enum_params(&port->node->node, seq, direction, port->id,
+ id, start, num, filter);
+}
+
+static int
+impl_mix_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_mix_add_port(void *object, enum spa_direction direction, uint32_t mix_id,
+ const struct spa_dict *props)
+{
+ struct port *port = object;
+ pw_log_debug("%p: add port %d:%d.%d", object, direction, port->id, mix_id);
+ return 0;
+}
+
+static int
+impl_mix_remove_port(void *object, enum spa_direction direction, uint32_t mix_id)
+{
+ struct port *port = object;
+ pw_log_debug("%p: remove port %d:%d.%d", object, direction, port->id, mix_id);
+ return 0;
+}
+
+static int
+impl_mix_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t mix_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct port *port = object;
+ struct impl *impl = port->impl;
+
+ return do_port_use_buffers(impl, direction, port->id, mix_id, flags, buffers, n_buffers);
+}
+
+static int impl_mix_port_set_io(void *object,
+ enum spa_direction direction, uint32_t mix_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct port *p = object;
+ struct pw_impl_port *port = p->port;
+ struct impl *impl = port->owner_data;
+ struct node *this = &impl->node;
+ struct pw_impl_port_mix *mix;
+
+ mix = pw_map_lookup(&port->mix_port_map, mix_id);
+ if (mix == NULL)
+ return -EINVAL;
+
+ if (id == SPA_IO_Buffers) {
+ if (data && size >= sizeof(struct spa_io_buffers))
+ mix->io = data;
+ else
+ mix->io = NULL;
+
+ if (mix->io != NULL && this->resource && this->resource->version >= 4)
+ pw_client_node_resource_port_set_mix_info(this->resource,
+ direction, port->port_id,
+ mix->port.port_id, mix->peer_id, NULL);
+ }
+
+ return do_port_set_io(impl,
+ direction, port->port_id, mix->port.port_id,
+ id, data, size);
+}
+
+static int
+impl_mix_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct port *p = object;
+ return impl_node_port_reuse_buffer(&p->node->node, p->id, buffer_id);
+}
+
+static int impl_mix_process(void *object)
+{
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_port_mix = {
+ SPA_VERSION_NODE_METHODS,
+ .port_enum_params = impl_mix_port_enum_params,
+ .port_set_param = impl_mix_port_set_param,
+ .add_port = impl_mix_add_port,
+ .remove_port = impl_mix_remove_port,
+ .port_use_buffers = impl_mix_port_use_buffers,
+ .port_set_io = impl_mix_port_set_io,
+ .port_reuse_buffer = impl_mix_port_reuse_buffer,
+ .process = impl_mix_process,
+};
+
+static void node_port_init(void *data, struct pw_impl_port *port)
+{
+ struct impl *impl = data;
+ struct port *p = pw_impl_port_get_user_data(port);
+ struct node *this = &impl->node;
+
+ pw_log_debug("%p: port %p init", this, port);
+
+ *p = this->dummy;
+ p->port = port;
+ p->node = this;
+ p->direction = port->direction;
+ p->id = port->port_id;
+ p->impl = impl;
+ pw_array_init(&p->mix, sizeof(struct mix) * 2);
+ p->mix_node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_port_mix, p);
+ ensure_mix(impl, p, SPA_ID_INVALID);
+
+ pw_map_insert_at(&this->ports[p->direction], p->id, p);
+ return;
+}
+
+static void node_port_added(void *data, struct pw_impl_port *port)
+{
+ struct impl *impl = data;
+ struct port *p = pw_impl_port_get_user_data(port);
+
+ port->flags |= PW_IMPL_PORT_FLAG_NO_MIXER;
+
+ port->impl = SPA_CALLBACKS_INIT(&port_impl, p);
+ port->owner_data = impl;
+
+ pw_impl_port_set_mix(port, &p->mix_node,
+ PW_IMPL_PORT_MIX_FLAG_MULTI |
+ PW_IMPL_PORT_MIX_FLAG_MIX_ONLY);
+}
+
+static void node_port_removed(void *data, struct pw_impl_port *port)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct port *p = pw_impl_port_get_user_data(port);
+
+ pw_log_debug("%p: port %p remove", this, port);
+
+ p->removed = true;
+ clear_port(this, p);
+}
+
+static void node_peer_added(void *data, struct pw_impl_node *peer)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct pw_memblock *m;
+
+ if (peer == impl->this.node)
+ return;
+
+ m = pw_mempool_import_block(this->client->pool, peer->activation);
+ if (m == NULL) {
+ pw_log_debug("%p: can't ensure mem: %m", this);
+ return;
+ }
+ pw_log_debug("%p: peer %p id:%u added mem_id:%u", &impl->this, peer,
+ peer->info.id, m->id);
+
+ if (this->resource == NULL)
+ return;
+
+ pw_client_node_resource_set_activation(this->resource,
+ peer->info.id,
+ peer->source.fd,
+ m->id,
+ 0,
+ sizeof(struct pw_node_activation));
+}
+
+static void node_peer_removed(void *data, struct pw_impl_node *peer)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ struct pw_memblock *m;
+
+ if (peer == impl->this.node)
+ return;
+
+ m = pw_mempool_find_fd(this->client->pool, peer->activation->fd);
+ if (m == NULL) {
+ pw_log_warn("%p: unknown peer %p fd:%d", this, peer,
+ peer->source.fd);
+ return;
+ }
+ pw_log_debug("%p: peer %p %u removed", this, peer,
+ peer->info.id);
+
+ if (this->resource != NULL) {
+ pw_client_node_resource_set_activation(this->resource,
+ peer->info.id,
+ -1,
+ SPA_ID_INVALID,
+ 0,
+ 0);
+ }
+
+ pw_memblock_unref(m);
+}
+
+static void node_driver_changed(void *data, struct pw_impl_node *old, struct pw_impl_node *driver)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ pw_log_debug("%p: driver changed %p -> %p", this, old, driver);
+
+ node_peer_removed(data, old);
+ node_peer_added(data, driver);
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .free = node_free,
+ .initialized = node_initialized,
+ .port_init = node_port_init,
+ .port_added = node_port_added,
+ .port_removed = node_port_removed,
+ .peer_added = node_peer_added,
+ .peer_removed = node_peer_removed,
+ .driver_changed = node_driver_changed,
+};
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_node_resource_destroy,
+ .error = client_node_resource_error,
+ .pong = client_node_resource_pong,
+};
+
+/** Create a new client node
+ * \param client an owner \ref pw_client
+ * \param id an id
+ * \param name a name
+ * \param properties extra properties
+ * \return a newly allocated client node
+ *
+ * Create a new \ref pw_impl_node.
+ *
+ * \memberof pw_impl_client_node
+ */
+struct pw_impl_client_node *pw_impl_client_node_new(struct pw_resource *resource,
+ struct pw_properties *properties,
+ bool do_register)
+{
+ struct impl *impl;
+ struct pw_impl_client_node *this;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct pw_context *context = pw_impl_client_get_context(client);
+ const struct spa_support *support;
+ uint32_t n_support;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_exit_free;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d", client->global->id);
+
+ this = &impl->this;
+
+ impl->context = context;
+ impl->fds[0] = impl->fds[1] = -1;
+ pw_log_debug("%p: new", &impl->node);
+
+ support = pw_context_get_support(impl->context, &n_support);
+ node_init(&impl->node, NULL, support, n_support);
+ impl->node.impl = impl;
+ impl->node.resource = resource;
+ impl->node.client = client;
+ this->flags = do_register ? 0 : 1;
+
+ pw_map_init(&impl->node.ports[0], 64, 64);
+ pw_map_init(&impl->node.ports[1], 64, 64);
+ pw_map_init(&impl->io_map, 64, 64);
+
+ this->resource = resource;
+ this->node = pw_spa_node_new(context,
+ PW_SPA_NODE_FLAG_ASYNC |
+ (do_register ? 0 : PW_SPA_NODE_FLAG_NO_REGISTER),
+ (struct spa_node *)&impl->node.node,
+ NULL,
+ properties, 0);
+
+ if (this->node == NULL)
+ goto error_no_node;
+
+ this->node->remote = true;
+ this->flags = 0;
+
+ pw_resource_add_listener(this->resource,
+ &impl->resource_listener,
+ &resource_events,
+ impl);
+ pw_resource_add_object_listener(this->resource,
+ &impl->object_listener,
+ &client_node_methods,
+ impl);
+
+ this->node->port_user_data_size = sizeof(struct port);
+
+ pw_impl_node_add_listener(this->node, &impl->node_listener, &node_events, impl);
+
+ return this;
+
+error_no_node:
+ res = -errno;
+ node_clear(&impl->node);
+ properties = NULL;
+ goto error_exit_free;
+
+error_exit_free:
+ free(impl);
+error_exit_cleanup:
+ if (resource)
+ pw_resource_destroy(resource);
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+/** Destroy a client node
+ * \param node the client node to destroy
+ * \memberof pw_impl_client_node
+ */
+void pw_impl_client_node_destroy(struct pw_impl_client_node *node)
+{
+ pw_resource_destroy(node->resource);
+}
diff --git a/src/modules/module-client-node/client-node.h b/src/modules/module-client-node/client-node.h
new file mode 100644
index 0000000..f7e060a
--- /dev/null
+++ b/src/modules/module-client-node/client-node.h
@@ -0,0 +1,60 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CLIENT_NODE_H
+#define PIPEWIRE_CLIENT_NODE_H
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/client-node.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \class pw_impl_client_node
+ *
+ * PipeWire client node interface
+ */
+struct pw_impl_client_node {
+ struct pw_impl_node *node;
+
+ struct pw_resource *resource;
+ uint32_t flags;
+};
+
+struct pw_impl_client_node *
+pw_impl_client_node_new(struct pw_resource *resource,
+ struct pw_properties *properties,
+ bool do_register);
+
+void
+pw_impl_client_node_destroy(struct pw_impl_client_node *node);
+
+void pw_impl_client_node_registered(struct pw_impl_client_node *node, struct pw_global *global);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CLIENT_NODE_H */
diff --git a/src/modules/module-client-node/protocol-native.c b/src/modules/module-client-node/protocol-native.c
new file mode 100644
index 0000000..dd51692
--- /dev/null
+++ b/src/modules/module-client-node/protocol-native.c
@@ -0,0 +1,1259 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include <pipewire/extensions/protocol-native.h>
+#include <pipewire/extensions/client-node.h>
+
+#define MAX_DICT 1024
+#define MAX_PARAMS 4096
+#define MAX_PARAM_INFO 128
+#define MAX_BUFFERS 64
+#define MAX_METAS 16u
+#define MAX_DATAS 64u
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static inline void push_item(struct spa_pod_builder *b, const struct spa_dict_item *item)
+{
+ const char *str;
+ spa_pod_builder_string(b, item->key);
+ str = item->value;
+ if (spa_strstartswith(str, "pointer:"))
+ str = "";
+ spa_pod_builder_string(b, str);
+}
+
+static void push_dict(struct spa_pod_builder *b, const struct spa_dict *dict)
+{
+ uint32_t i, n_items;
+ struct spa_pod_frame f;
+
+ n_items = dict ? dict->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_int(b, n_items);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &dict->items[i]);
+ spa_pod_builder_pop(b, &f);
+}
+
+static inline int parse_item(struct spa_pod_parser *prs, struct spa_dict_item *item)
+{
+ int res;
+ if ((res = spa_pod_parser_get(prs,
+ SPA_POD_String(&item->key),
+ SPA_POD_String(&item->value),
+ NULL)) < 0)
+ return res;
+ if (spa_strstartswith(item->value, "pointer:"))
+ item->value = "";
+ return 0;
+}
+
+#define parse_dict(prs,d) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(d)->n_items), NULL) < 0) \
+ return -EINVAL; \
+ (d)->items = NULL; \
+ if ((d)->n_items > 0) { \
+ if ((d)->n_items > MAX_DICT) \
+ return -ENOSPC; \
+ (d)->items = alloca((d)->n_items * sizeof(struct spa_dict_item)); \
+ for (i = 0; i < (d)->n_items; i++) { \
+ if (parse_item(prs, (struct spa_dict_item *) &(d)->items[i]) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+#define parse_dict_struct(prs,f,dict) \
+do { \
+ if (spa_pod_parser_push_struct(prs, f) < 0) \
+ return -EINVAL; \
+ parse_dict(prs, dict); \
+ spa_pod_parser_pop(prs, f); \
+} while(0)
+
+#define parse_params(prs,n_params,params) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&n_params), NULL) < 0) \
+ return -EINVAL; \
+ params = NULL; \
+ if (n_params > 0) { \
+ if (n_params > MAX_PARAMS) \
+ return -ENOSPC; \
+ params = alloca(n_params * sizeof(struct spa_pod *)); \
+ for (i = 0; i < n_params; i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_PodObject(&params[i]), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+#define parse_param_info(prs,n_params,params) \
+do { \
+ uint32_t i; \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(n_params)), NULL) < 0) \
+ return -EINVAL; \
+ params = NULL; \
+ if (n_params > 0) { \
+ if (n_params > MAX_PARAM_INFO) \
+ return -ENOSPC; \
+ params = alloca(n_params * sizeof(struct spa_param_info)); \
+ for (i = 0; i < n_params; i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Id(&(params[i]).id), \
+ SPA_POD_Int(&(params[i]).flags), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+static int client_node_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_client_node_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static struct pw_node *
+client_node_marshal_get_node(void *object, uint32_t version, size_t user_data_size)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct pw_proxy *res;
+ uint32_t new_id;
+
+ res = pw_proxy_new(object, PW_TYPE_INTERFACE_Node, version, user_data_size);
+ if (res == NULL)
+ return NULL;
+
+ new_id = pw_proxy_get_id(res);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_GET_NODE, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(version),
+ SPA_POD_Int(new_id));
+
+ pw_protocol_native_end_proxy(proxy, b);
+
+ return (struct pw_node *) res;
+}
+
+static int
+client_node_marshal_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_node_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n_items, n_info_params;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params), NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info) {
+ uint64_t change_mask = info->change_mask;
+
+ change_mask &= SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+
+ n_items = info->props && (change_mask & SPA_NODE_CHANGE_MASK_PROPS) ?
+ info->props->n_items : 0;
+ n_info_params = (change_mask & SPA_NODE_CHANGE_MASK_PARAMS) ?
+ info->n_params : 0;
+
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->max_input_ports),
+ SPA_POD_Int(info->max_output_ports),
+ SPA_POD_Long(change_mask),
+ SPA_POD_Long(info->flags),
+ SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &info->props->items[i]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(n_info_params), NULL);
+ for (i = 0; i < n_info_params; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_Id(info->params[i].id),
+ SPA_POD_Int(info->params[i].flags), NULL);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+
+ } else {
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(NULL), NULL);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int
+client_node_marshal_port_update(void *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)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_PORT_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params), NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(params[i]), NULL);
+
+ if (info) {
+ uint64_t change_mask = info->change_mask;
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ change_mask &= SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_RATE |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_add(b,
+ SPA_POD_Long(change_mask),
+ SPA_POD_Long(info->flags),
+ SPA_POD_Int(info->rate.num),
+ SPA_POD_Int(info->rate.denom),
+ SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &info->props->items[i]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->n_params), NULL);
+ for (i = 0; i < info->n_params; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_Id(info->params[i].id),
+ SPA_POD_Int(info->params[i].flags), NULL);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+
+ } else {
+ spa_pod_builder_add(b,
+ SPA_POD_Pod(NULL), NULL);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_node_marshal_set_active(void *object, bool active)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_SET_ACTIVE, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Bool(active));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_node_marshal_event_method(void *object, const struct spa_event *event)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_EVENT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(event));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int
+client_node_marshal_port_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t n_buffers,
+ struct spa_buffer **buffers)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, j;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_NODE_METHOD_PORT_BUFFERS, NULL);
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(mix_id),
+ SPA_POD_Int(n_buffers), NULL);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i];
+
+ spa_pod_builder_add(b,
+ SPA_POD_Int(buf->n_datas), NULL);
+
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+ spa_pod_builder_add(b,
+ SPA_POD_Id(d->type),
+ SPA_POD_Fd(pw_protocol_native_add_proxy_fd(proxy, d->fd)),
+ SPA_POD_Int(d->flags),
+ SPA_POD_Int(d->mapoffset),
+ SPA_POD_Int(d->maxsize), NULL);
+ }
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_node_demarshal_transport(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t mem_id, offset, sz;
+ int64_t ridx, widx;
+ int readfd, writefd;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Fd(&ridx),
+ SPA_POD_Fd(&widx),
+ SPA_POD_Int(&mem_id),
+ SPA_POD_Int(&offset),
+ SPA_POD_Int(&sz)) < 0)
+ return -EINVAL;
+
+ readfd = pw_protocol_native_get_proxy_fd(proxy, ridx);
+ writefd = pw_protocol_native_get_proxy_fd(proxy, widx);
+
+ if (readfd < 0 || writefd < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, transport, 0,
+ readfd, writefd, mem_id,
+ offset, sz);
+ return 0;
+}
+
+static int client_node_demarshal_set_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, set_param, 0, id, flags, param);
+ return 0;
+}
+
+static int client_node_demarshal_event_event(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ const struct spa_event *event;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodObject(&event)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, event, 0, event);
+ return 0;
+}
+
+static int client_node_demarshal_command(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ const struct spa_command *command;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodObject(&command)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, command, 0, command);
+ return 0;
+}
+
+static int client_node_demarshal_add_port(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ int32_t direction, port_id;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, add_port, 0, direction, port_id,
+ props.n_items ? &props : NULL);
+ return 0;
+}
+
+static int client_node_demarshal_remove_port(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ int32_t direction, port_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, remove_port, 0, direction, port_id);
+ return 0;
+}
+
+static int client_node_demarshal_port_set_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t direction, port_id, id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, port_set_param, 0,
+ direction, port_id, id, flags, param);
+ return 0;
+}
+
+static int client_node_demarshal_port_use_buffers(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t direction, port_id, mix_id, flags, n_buffers, data_id;
+ struct pw_client_node_buffer *buffers;
+ uint32_t i, j;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&mix_id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Int(&n_buffers), NULL) < 0)
+ return -EINVAL;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ buffers = alloca(sizeof(struct pw_client_node_buffer) * n_buffers);
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i].buffer = alloca(sizeof(struct spa_buffer));
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&buffers[i].mem_id),
+ SPA_POD_Int(&buffers[i].offset),
+ SPA_POD_Int(&buffers[i].size),
+ SPA_POD_Int(&buf->n_metas), NULL) < 0)
+ return -EINVAL;
+
+ if (buf->n_metas > MAX_METAS)
+ return -ENOSPC;
+
+ buf->metas = alloca(sizeof(struct spa_meta) * buf->n_metas);
+ for (j = 0; j < buf->n_metas; j++) {
+ struct spa_meta *m = &buf->metas[j];
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Id(&m->type),
+ SPA_POD_Int(&m->size), NULL) < 0)
+ return -EINVAL;
+
+ m->data = NULL;
+ }
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&buf->n_datas), NULL) < 0)
+ return -EINVAL;
+
+ if (buf->n_datas > MAX_DATAS)
+ return -ENOSPC;
+
+ buf->datas = alloca(sizeof(struct spa_data) * buf->n_datas);
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Id(&d->type),
+ SPA_POD_Int(&data_id),
+ SPA_POD_Int(&d->flags),
+ SPA_POD_Int(&d->mapoffset),
+ SPA_POD_Int(&d->maxsize), NULL) < 0)
+ return -EINVAL;
+
+ d->fd = -1;
+ d->data = SPA_UINT32_TO_PTR(data_id);
+ d->chunk = NULL;
+ }
+ }
+ pw_proxy_notify(proxy, struct pw_client_node_events, port_use_buffers, 0,
+ direction,
+ port_id,
+ mix_id,
+ flags,
+ n_buffers, buffers);
+ return 0;
+}
+
+static int client_node_demarshal_port_set_io(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t direction, port_id, mix_id, id, memid, off, sz;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&mix_id),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&memid),
+ SPA_POD_Int(&off),
+ SPA_POD_Int(&sz)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, port_set_io, 0,
+ direction, port_id, mix_id,
+ id, memid,
+ off, sz);
+ return 0;
+}
+
+static int client_node_demarshal_set_activation(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t node_id, memid, off, sz;
+ int64_t sigidx;
+ int signalfd;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&node_id),
+ SPA_POD_Fd(&sigidx),
+ SPA_POD_Int(&memid),
+ SPA_POD_Int(&off),
+ SPA_POD_Int(&sz)) < 0)
+ return -EINVAL;
+
+ signalfd = pw_protocol_native_get_proxy_fd(proxy, sigidx);
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, set_activation, 0,
+ node_id,
+ signalfd,
+ memid,
+ off, sz);
+ return 0;
+}
+
+static int client_node_demarshal_port_set_mix_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t direction, port_id, mix_id, peer_id;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&mix_id),
+ SPA_POD_Int(&peer_id), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, port_set_mix_info, 1,
+ direction, port_id, mix_id,
+ peer_id, &props);
+ return 0;
+}
+
+static int client_node_demarshal_set_io(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, memid, off, sz;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&memid),
+ SPA_POD_Int(&off),
+ SPA_POD_Int(&sz)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_client_node_events, set_io, 0,
+ id, memid, off, sz);
+ return 0;
+}
+
+static int client_node_marshal_transport(void *data, int readfd, int writefd,
+ uint32_t mem_id, uint32_t offset, uint32_t size)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_TRANSPORT, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Fd(pw_protocol_native_add_resource_fd(resource, readfd)),
+ SPA_POD_Fd(pw_protocol_native_add_resource_fd(resource, writefd)),
+ SPA_POD_Int(mem_id),
+ SPA_POD_Int(offset),
+ SPA_POD_Int(size));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_set_param(void *data, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_node_marshal_event_event(void *data, const struct spa_event *event)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_EVENT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(event));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_command(void *data, const struct spa_command *command)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_COMMAND, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(command));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_add_port(void *data,
+ enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_ADD_PORT, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id), NULL);
+ push_dict(b, props);
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_remove_port(void *data,
+ enum spa_direction direction, uint32_t port_id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_REMOVE_PORT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_port_set_param(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_PORT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_port_use_buffers(void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t flags,
+ uint32_t n_buffers, struct pw_client_node_buffer *buffers)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, j;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_PORT_USE_BUFFERS, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(mix_id),
+ SPA_POD_Int(flags),
+ SPA_POD_Int(n_buffers), NULL);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i].buffer;
+
+ spa_pod_builder_add(b,
+ SPA_POD_Int(buffers[i].mem_id),
+ SPA_POD_Int(buffers[i].offset),
+ SPA_POD_Int(buffers[i].size),
+ SPA_POD_Int(buf->n_metas), NULL);
+
+ for (j = 0; j < buf->n_metas; j++) {
+ struct spa_meta *m = &buf->metas[j];
+ spa_pod_builder_add(b,
+ SPA_POD_Id(m->type),
+ SPA_POD_Int(m->size), NULL);
+ }
+ spa_pod_builder_add(b,
+ SPA_POD_Int(buf->n_datas), NULL);
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+ spa_pod_builder_add(b,
+ SPA_POD_Id(d->type),
+ SPA_POD_Int(SPA_PTR_TO_UINT32(d->data)),
+ SPA_POD_Int(d->flags),
+ SPA_POD_Int(d->mapoffset),
+ SPA_POD_Int(d->maxsize), NULL);
+ }
+ }
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_port_set_io(void *data,
+ uint32_t direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_PORT_SET_IO, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(mix_id),
+ SPA_POD_Id(id),
+ SPA_POD_Int(memid),
+ SPA_POD_Int(offset),
+ SPA_POD_Int(size));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_set_activation(void *data,
+ uint32_t node_id,
+ int signalfd,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_SET_ACTIVATION, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(node_id),
+ SPA_POD_Fd(pw_protocol_native_add_resource_fd(resource, signalfd)),
+ SPA_POD_Int(memid),
+ SPA_POD_Int(offset),
+ SPA_POD_Int(size));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_set_io(void *data,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_SET_IO, NULL);
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(memid),
+ SPA_POD_Int(offset),
+ SPA_POD_Int(size));
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int
+client_node_marshal_port_set_mix_info(void *data,
+ uint32_t direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t peer_id,
+ const struct spa_dict *props)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE_EVENT_PORT_SET_MIX_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(direction),
+ SPA_POD_Int(port_id),
+ SPA_POD_Int(mix_id),
+ SPA_POD_Int(peer_id), NULL);
+ push_dict(b, props);
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+
+static int client_node_demarshal_get_node(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int32_t version, new_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&version),
+ SPA_POD_Int(&new_id)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_node_methods, get_node, 0,
+ version, new_id);
+}
+
+static int client_node_demarshal_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_node_info info = SPA_NODE_INFO_INIT(), *infop = NULL;
+ struct spa_pod *ipod;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_params(&prs, n_params, params);
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+
+ if (ipod) {
+ struct spa_pod_parser p2;
+ struct spa_pod_frame f2;
+ infop = &info;
+
+ spa_pod_parser_pod(&p2, ipod);
+ if (spa_pod_parser_push_struct(&p2, &f2) < 0 ||
+ spa_pod_parser_get(&p2,
+ SPA_POD_Int(&info.max_input_ports),
+ SPA_POD_Int(&info.max_output_ports),
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Long(&info.flags), NULL) < 0)
+ return -EINVAL;
+
+ info.change_mask &= SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+
+ parse_dict(&p2, &props);
+ if (props.n_items > 0)
+ info.props = &props;
+
+ parse_param_info(&p2, info.n_params, info.params);
+ }
+
+ pw_resource_notify(resource, struct pw_client_node_methods, update, 0, change_mask,
+ n_params,
+ params, infop);
+ return 0;
+}
+
+static int client_node_demarshal_port_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t direction, port_id, change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_port_info info = SPA_PORT_INFO_INIT(), *infop = NULL;
+ struct spa_pod *ipod;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_params(&prs, n_params, params);
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+
+ if (ipod) {
+ struct spa_pod_parser p2;
+ struct spa_pod_frame f2;
+ infop = &info;
+
+ spa_pod_parser_pod(&p2, ipod);
+ if (spa_pod_parser_push_struct(&p2, &f2) < 0 ||
+ spa_pod_parser_get(&p2,
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Long(&info.flags),
+ SPA_POD_Int(&info.rate.num),
+ SPA_POD_Int(&info.rate.denom), NULL) < 0)
+ return -EINVAL;
+
+ info.change_mask &= SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_RATE |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+
+ parse_dict(&p2, &props);
+ if (props.n_items > 0)
+ info.props = &props;
+
+ parse_param_info(&p2, info.n_params, info.params);
+ }
+
+ pw_resource_notify(resource, struct pw_client_node_methods, port_update, 0, direction,
+ port_id,
+ change_mask,
+ n_params,
+ params, infop);
+ return 0;
+}
+
+static int client_node_demarshal_set_active(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ bool active;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Bool(&active)) < 0)
+ return -EINVAL;
+
+ pw_resource_notify(resource, struct pw_client_node_methods, set_active, 0, active);
+ return 0;
+}
+
+static int client_node_demarshal_event_method(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ const struct spa_event *event;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_PodObject(&event)) < 0)
+ return -EINVAL;
+
+ pw_resource_notify(resource, struct pw_client_node_methods, event, 0, event);
+ return 0;
+}
+
+static int client_node_demarshal_port_buffers(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t i, j, direction, port_id, mix_id, n_buffers;
+ int64_t data_fd;
+ struct spa_buffer **buffers = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&direction),
+ SPA_POD_Int(&port_id),
+ SPA_POD_Int(&mix_id),
+ SPA_POD_Int(&n_buffers), NULL) < 0)
+ return -EINVAL;
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ buffers = alloca(sizeof(struct spa_buffer*) * n_buffers);
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i] = alloca(sizeof(struct spa_buffer));
+
+ spa_zero(*buf);
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&buf->n_datas), NULL) < 0)
+ return -EINVAL;
+
+ if (buf->n_datas > MAX_DATAS)
+ return -ENOSPC;
+
+ buf->datas = alloca(sizeof(struct spa_data) * buf->n_datas);
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Id(&d->type),
+ SPA_POD_Fd(&data_fd),
+ SPA_POD_Int(&d->flags),
+ SPA_POD_Int(&d->mapoffset),
+ SPA_POD_Int(&d->maxsize), NULL) < 0)
+ return -EINVAL;
+
+ d->fd = pw_protocol_native_get_resource_fd(resource, data_fd);
+ }
+ }
+
+ pw_resource_notify(resource, struct pw_client_node_methods, port_buffers, 0,
+ direction, port_id, mix_id, n_buffers, buffers);
+
+ return 0;
+}
+
+static const struct pw_client_node_methods pw_protocol_native_client_node_method_marshal = {
+ PW_VERSION_CLIENT_NODE_METHODS,
+ .add_listener = &client_node_marshal_add_listener,
+ .get_node = &client_node_marshal_get_node,
+ .update = &client_node_marshal_update,
+ .port_update = &client_node_marshal_port_update,
+ .set_active = &client_node_marshal_set_active,
+ .event = &client_node_marshal_event_method,
+ .port_buffers = &client_node_marshal_port_buffers
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_node_method_demarshal[PW_CLIENT_NODE_METHOD_NUM] =
+{
+ [PW_CLIENT_NODE_METHOD_ADD_LISTENER] = { NULL, 0 },
+ [PW_CLIENT_NODE_METHOD_GET_NODE] = { &client_node_demarshal_get_node, 0 },
+ [PW_CLIENT_NODE_METHOD_UPDATE] = { &client_node_demarshal_update, 0 },
+ [PW_CLIENT_NODE_METHOD_PORT_UPDATE] = { &client_node_demarshal_port_update, 0 },
+ [PW_CLIENT_NODE_METHOD_SET_ACTIVE] = { &client_node_demarshal_set_active, 0 },
+ [PW_CLIENT_NODE_METHOD_EVENT] = { &client_node_demarshal_event_method, 0 },
+ [PW_CLIENT_NODE_METHOD_PORT_BUFFERS] = { &client_node_demarshal_port_buffers, 0 }
+};
+
+static const struct pw_client_node_events pw_protocol_native_client_node_event_marshal = {
+ PW_VERSION_CLIENT_NODE_EVENTS,
+ .transport = &client_node_marshal_transport,
+ .set_param = &client_node_marshal_set_param,
+ .set_io = &client_node_marshal_set_io,
+ .event = &client_node_marshal_event_event,
+ .command = &client_node_marshal_command,
+ .add_port = &client_node_marshal_add_port,
+ .remove_port = &client_node_marshal_remove_port,
+ .port_set_param = &client_node_marshal_port_set_param,
+ .port_use_buffers = &client_node_marshal_port_use_buffers,
+ .port_set_io = &client_node_marshal_port_set_io,
+ .set_activation = &client_node_marshal_set_activation,
+ .port_set_mix_info = &client_node_marshal_port_set_mix_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_node_event_demarshal[PW_CLIENT_NODE_EVENT_NUM] =
+{
+ [PW_CLIENT_NODE_EVENT_TRANSPORT] = { &client_node_demarshal_transport, 0 },
+ [PW_CLIENT_NODE_EVENT_SET_PARAM] = { &client_node_demarshal_set_param, 0 },
+ [PW_CLIENT_NODE_EVENT_SET_IO] = { &client_node_demarshal_set_io, 0 },
+ [PW_CLIENT_NODE_EVENT_EVENT] = { &client_node_demarshal_event_event, 0 },
+ [PW_CLIENT_NODE_EVENT_COMMAND] = { &client_node_demarshal_command, 0 },
+ [PW_CLIENT_NODE_EVENT_ADD_PORT] = { &client_node_demarshal_add_port, 0 },
+ [PW_CLIENT_NODE_EVENT_REMOVE_PORT] = { &client_node_demarshal_remove_port, 0 },
+ [PW_CLIENT_NODE_EVENT_PORT_SET_PARAM] = { &client_node_demarshal_port_set_param, 0 },
+ [PW_CLIENT_NODE_EVENT_PORT_USE_BUFFERS] = { &client_node_demarshal_port_use_buffers, 0 },
+ [PW_CLIENT_NODE_EVENT_PORT_SET_IO] = { &client_node_demarshal_port_set_io, 0 },
+ [PW_CLIENT_NODE_EVENT_SET_ACTIVATION] = { &client_node_demarshal_set_activation, 0 },
+ [PW_CLIENT_NODE_EVENT_PORT_SET_MIX_INFO] = { &client_node_demarshal_port_set_mix_info, 0 }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_node_marshal = {
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE,
+ 0,
+ PW_CLIENT_NODE_METHOD_NUM,
+ PW_CLIENT_NODE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_client_node_method_marshal,
+ .server_demarshal = &pw_protocol_native_client_node_method_demarshal,
+ .server_marshal = &pw_protocol_native_client_node_event_marshal,
+ .client_demarshal = pw_protocol_native_client_node_event_demarshal,
+};
+
+struct pw_protocol *pw_protocol_native_ext_client_node_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+
+ if (protocol == NULL)
+ return NULL;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_node_marshal);
+
+ return protocol;
+}
diff --git a/src/modules/module-client-node/remote-node.c b/src/modules/module-client-node/remote-node.c
new file mode 100644
index 0000000..051ab07
--- /dev/null
+++ b/src/modules/module-client-node/remote-node.c
@@ -0,0 +1,1339 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <errno.h>
+#include <time.h>
+#include <sys/mman.h>
+
+#include <spa/pod/parser.h>
+#include <spa/pod/dynamic.h>
+#include <spa/node/utils.h>
+#include <spa/utils/result.h>
+#include <spa/debug/types.h>
+
+#include "pipewire/pipewire.h"
+#include "pipewire/private.h"
+
+#include "pipewire/extensions/protocol-native.h"
+#include "pipewire/extensions/client-node.h"
+
+#define MAX_BUFFERS 64
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/** \cond */
+static bool mlock_warned = false;
+
+struct buffer {
+ uint32_t id;
+ struct spa_buffer *buf;
+ struct pw_memmap *mem;
+};
+
+struct mix {
+ struct spa_list link;
+ struct pw_impl_port *port;
+ uint32_t mix_id;
+ struct pw_impl_port_mix mix;
+ struct pw_array buffers;
+ bool active;
+};
+
+struct node_data {
+ struct pw_context *context;
+
+ struct pw_mempool *pool;
+
+ uint32_t remote_id;
+ int rtwritefd;
+ struct pw_memmap *activation;
+
+ struct spa_list mix[2];
+ struct spa_list free_mix;
+
+ struct pw_impl_node *node;
+ struct spa_hook node_listener;
+ unsigned int do_free:1;
+ unsigned int have_transport:1;
+ unsigned int allow_mlock:1;
+ unsigned int warn_mlock:1;
+
+ struct pw_client_node *client_node;
+ struct spa_hook client_node_listener;
+ struct spa_hook proxy_client_node_listener;
+
+ struct spa_list links;
+};
+
+struct link {
+ struct spa_list link;
+ struct node_data *data;
+ struct pw_memmap *map;
+ struct pw_node_target target;
+ uint32_t node_id;
+ int signalfd;
+};
+
+/** \endcond */
+
+static struct link *find_activation(struct spa_list *links, uint32_t node_id)
+{
+ struct link *l;
+
+ spa_list_for_each(l, links, link) {
+ if (l->node_id == node_id)
+ return l;
+ }
+ return NULL;
+}
+
+static int
+do_deactivate_link(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct link *link = user_data;
+ pw_log_trace("link %p deactivate", link);
+ spa_list_remove(&link->target.link);
+ return 0;
+}
+
+static void clear_link(struct node_data *data, struct link *link)
+{
+ struct pw_context *context = data->context;
+ pw_log_debug("link %p", link);
+ pw_loop_invoke(context->data_loop,
+ do_deactivate_link, SPA_ID_INVALID, NULL, 0, true, link);
+ pw_memmap_free(link->map);
+ spa_system_close(context->data_system, link->signalfd);
+ spa_list_remove(&link->link);
+ free(link);
+}
+
+static void clean_transport(struct node_data *data)
+{
+ struct link *l;
+ uint32_t tag[5] = { data->remote_id, };
+ struct pw_memmap *mm;
+
+ if (!data->have_transport)
+ return;
+
+ spa_list_consume(l, &data->links, link)
+ clear_link(data, l);
+
+ while ((mm = pw_mempool_find_tag(data->pool, tag, sizeof(uint32_t))) != NULL) {
+ if (mm->tag[1] == SPA_ID_INVALID)
+ spa_node_set_io(data->node->node, mm->tag[2], NULL, 0);
+
+ pw_memmap_free(mm);
+ }
+
+ pw_memmap_free(data->activation);
+ data->node->rt.activation = data->node->activation->map->ptr;
+
+ spa_system_close(data->context->data_system, data->rtwritefd);
+ data->have_transport = false;
+}
+
+static void mix_init(struct mix *mix, struct pw_impl_port *port, uint32_t mix_id)
+{
+ pw_log_debug("port %p: mix init %d.%d", port, port->port_id, mix_id);
+ mix->port = port;
+ mix->mix_id = mix_id;
+ pw_impl_port_init_mix(port, &mix->mix);
+ mix->active = false;
+ pw_array_init(&mix->buffers, 32);
+ pw_array_ensure_size(&mix->buffers, sizeof(struct buffer) * 64);
+}
+
+static int
+do_deactivate_mix(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct mix *mix = user_data;
+ spa_list_remove(&mix->mix.rt_link);
+ return 0;
+}
+
+static int
+deactivate_mix(struct node_data *data, struct mix *mix)
+{
+ if (mix->active) {
+ pw_log_debug("node %p: mix %p deactivate", data, mix);
+ pw_loop_invoke(data->context->data_loop,
+ do_deactivate_mix, SPA_ID_INVALID, NULL, 0, true, mix);
+ mix->active = false;
+ }
+ return 0;
+}
+
+static int
+do_activate_mix(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct mix *mix = user_data;
+
+ spa_list_append(&mix->port->rt.mix_list, &mix->mix.rt_link);
+ return 0;
+}
+
+static int
+activate_mix(struct node_data *data, struct mix *mix)
+{
+ if (!mix->active) {
+ pw_log_debug("node %p: mix %p activate", data, mix);
+ pw_loop_invoke(data->context->data_loop,
+ do_activate_mix, SPA_ID_INVALID, NULL, 0, false, mix);
+ mix->active = true;
+ }
+ return 0;
+}
+
+static struct mix *find_mix(struct node_data *data,
+ enum spa_direction direction, uint32_t port_id, uint32_t mix_id)
+{
+ struct mix *mix;
+
+ spa_list_for_each(mix, &data->mix[direction], link) {
+ if (mix->port->port_id == port_id &&
+ mix->mix_id == mix_id) {
+ pw_log_debug("port %p: found mix %d:%d.%d", mix->port,
+ direction, port_id, mix_id);
+ return mix;
+ }
+ }
+ return NULL;
+}
+
+static struct mix *ensure_mix(struct node_data *data,
+ enum spa_direction direction, uint32_t port_id, uint32_t mix_id)
+{
+ struct mix *mix;
+ struct pw_impl_port *port;
+
+ if ((mix = find_mix(data, direction, port_id, mix_id)))
+ return mix;
+
+ port = pw_impl_node_find_port(data->node, direction, port_id);
+ if (port == NULL)
+ return NULL;
+
+ if (spa_list_is_empty(&data->free_mix)) {
+ if ((mix = calloc(1, sizeof(*mix))) == NULL)
+ return NULL;
+ } else {
+ mix = spa_list_first(&data->free_mix, struct mix, link);
+ spa_list_remove(&mix->link);
+ }
+
+ mix_init(mix, port, mix_id);
+ spa_list_append(&data->mix[direction], &mix->link);
+
+ return mix;
+}
+
+
+static int client_node_transport(void *_data,
+ int readfd, int writefd, uint32_t mem_id, uint32_t offset, uint32_t size)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+
+ clean_transport(data);
+
+ data->activation = pw_mempool_map_id(data->pool, mem_id,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, NULL);
+ if (data->activation == NULL) {
+ pw_log_warn("remote-node %p: can't map activation: %m", proxy);
+ return -errno;
+ }
+
+ data->node->rt.activation = data->activation->ptr;
+
+ pw_log_debug("remote-node %p: fds:%d %d node:%u activation:%p",
+ proxy, readfd, writefd, data->remote_id, data->activation->ptr);
+
+ data->rtwritefd = writefd;
+ spa_system_close(data->context->data_system, data->node->source.fd);
+ data->node->source.fd = readfd;
+
+ data->have_transport = true;
+
+ if (data->node->active)
+ pw_client_node_set_active(data->client_node, true);
+
+ return 0;
+}
+
+static int add_node_update(struct node_data *data, uint32_t change_mask, uint32_t info_mask)
+{
+ struct pw_impl_node *node = data->node;
+ struct spa_node_info ni = SPA_NODE_INFO_INIT();
+ uint32_t n_params = 0;
+ struct spa_pod **params = NULL;
+ int res;
+
+ if (change_mask & PW_CLIENT_NODE_UPDATE_PARAMS) {
+ uint32_t i, idx, id;
+ uint8_t buf[4096];
+ struct spa_pod_dynamic_builder b;
+
+ for (i = 0; i < node->info.n_params; i++) {
+ struct spa_pod *param;
+
+ id = node->info.params[i].id;
+ if (id == SPA_PARAM_Invalid)
+ continue;
+
+ for (idx = 0;;) {
+ spa_pod_dynamic_builder_init(&b, buf, sizeof(buf), 4096);
+
+ res = spa_node_enum_params_sync(node->node,
+ id, &idx, NULL, &param, &b.b);
+ if (res == 1) {
+ void *p;
+ p = pw_reallocarray(params, n_params + 1, sizeof(struct spa_pod *));
+ if (p == NULL) {
+ res = -errno;
+ pw_log_error("realloc failed: %m");
+ } else {
+ params = p;
+ params[n_params++] = spa_pod_copy(param);
+ }
+ }
+ spa_pod_dynamic_builder_clean(&b);
+ if (res != 1)
+ break;
+ }
+ }
+ }
+ if (change_mask & PW_CLIENT_NODE_UPDATE_INFO) {
+ ni.max_input_ports = node->info.max_input_ports;
+ ni.max_output_ports = node->info.max_output_ports;
+ ni.change_mask = info_mask;
+ ni.flags = node->spa_flags;
+ ni.props = node->info.props;
+ ni.params = node->info.params;
+ ni.n_params = node->info.n_params;
+ }
+
+ res = pw_client_node_update(data->client_node,
+ change_mask,
+ n_params,
+ (const struct spa_pod **)params,
+ &ni);
+
+ if (params) {
+ while (n_params > 0)
+ free(params[--n_params]);
+ free(params);
+ }
+ return res;
+}
+
+static int add_port_update(struct node_data *data, struct pw_impl_port *port, uint32_t change_mask)
+{
+ struct spa_port_info pi = SPA_PORT_INFO_INIT();
+ uint32_t n_params = 0;
+ struct spa_pod **params = NULL;
+ int res;
+
+ if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_PARAMS) {
+ uint32_t i, idx, id;
+ uint8_t buf[4096];
+ struct spa_pod_dynamic_builder b;
+
+ for (i = 0; i < port->info.n_params; i++) {
+ struct spa_pod *param;
+
+ id = port->info.params[i].id;
+ if (id == SPA_PARAM_Invalid)
+ continue;
+
+ for (idx = 0;;) {
+ spa_pod_dynamic_builder_init(&b, buf, sizeof(buf), 4096);
+
+ res = spa_node_port_enum_params_sync(port->node->node,
+ port->direction, port->port_id,
+ id, &idx, NULL, &param, &b.b);
+ if (res == 1) {
+ void *p;
+ p = pw_reallocarray(params, n_params + 1, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ res = -errno;
+ pw_log_error("realloc failed: %m");
+ } else {
+ params = p;
+ params[n_params++] = spa_pod_copy(param);
+ }
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (res != 1)
+ break;
+
+ }
+ }
+ }
+ if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_INFO) {
+ pi.change_mask = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_RATE |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ pi.flags = port->spa_flags;
+ pi.rate = SPA_FRACTION(0, 1);
+ pi.props = &port->properties->dict;
+ SPA_FLAG_CLEAR(pi.flags, SPA_PORT_FLAG_DYNAMIC_DATA);
+ pi.n_params = port->info.n_params;
+ pi.params = port->info.params;
+ }
+
+ res = pw_client_node_port_update(data->client_node,
+ port->direction,
+ port->port_id,
+ change_mask,
+ n_params,
+ (const struct spa_pod **)params,
+ &pi);
+ if (params) {
+ while (n_params > 0)
+ free(params[--n_params]);
+ free(params);
+ }
+ return res;
+}
+
+static int
+client_node_set_param(void *_data, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ int res;
+
+ pw_log_debug("node %p: set_param %s:", proxy,
+ spa_debug_type_find_name(spa_type_param, id));
+
+ res = spa_node_set_param(data->node->node, id, flags, param);
+
+ if (res < 0) {
+ pw_log_error("node %p: set_param %s (%d) %p: %s", proxy,
+ spa_debug_type_find_name(spa_type_param, id),
+ id, param, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "node_set_param(%s) failed: %s",
+ spa_debug_type_find_name(spa_type_param, id),
+ spa_strerror(res));
+ }
+ return res;
+}
+
+static int
+client_node_set_io(void *_data,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct pw_memmap *old, *mm;
+ void *ptr;
+ uint32_t tag[5] = { data->remote_id, SPA_ID_INVALID, id, };
+ int res;
+
+ old = pw_mempool_find_tag(data->pool, tag, sizeof(tag));
+
+ if (memid == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ size = 0;
+ } else {
+ mm = pw_mempool_map_id(data->pool, memid,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, tag);
+ if (mm == NULL) {
+ pw_log_warn("can't map memory id %u: %m", memid);
+ res = -errno;
+ goto exit;
+ }
+ ptr = mm->ptr;
+ }
+
+ pw_log_debug("node %p: set io %s %p", proxy,
+ spa_debug_type_find_name(spa_type_io, id), ptr);
+
+ res = spa_node_set_io(data->node->node, id, ptr, size);
+
+ pw_memmap_free(old);
+exit:
+ if (res < 0) {
+ pw_log_error("node %p: set_io: %s", proxy, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "node_set_io failed: %s", spa_strerror(res));
+ }
+ return res;
+}
+
+static int client_node_event(void *data, const struct spa_event *event)
+{
+ pw_log_warn("unhandled node event %d", SPA_EVENT_TYPE(event));
+ return -ENOTSUP;
+}
+
+static int client_node_command(void *_data, const struct spa_command *command)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ int res;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Pause:
+ pw_log_debug("node %p: pause", proxy);
+
+ if ((res = pw_impl_node_set_state(data->node, PW_NODE_STATE_IDLE)) < 0) {
+ pw_log_warn("node %p: pause failed", proxy);
+ pw_proxy_error(proxy, res, "pause failed");
+ }
+
+ break;
+ case SPA_NODE_COMMAND_Start:
+ pw_log_debug("node %p: start", proxy);
+
+ if ((res = pw_impl_node_set_state(data->node, PW_NODE_STATE_RUNNING)) < 0) {
+ pw_log_warn("node %p: start failed", proxy);
+ pw_proxy_error(proxy, res, "start failed");
+ }
+ break;
+
+ case SPA_NODE_COMMAND_Suspend:
+ pw_log_debug("node %p: suspend", proxy);
+ if ((res = pw_impl_node_set_state(data->node, PW_NODE_STATE_SUSPENDED)) < 0) {
+ pw_log_warn("node %p: suspend failed", proxy);
+ pw_proxy_error(proxy, res, "suspend failed");
+ }
+ break;
+ case SPA_NODE_COMMAND_RequestProcess:
+ res = pw_impl_node_send_command(data->node, command);
+ break;
+ default:
+ pw_log_warn("unhandled node command %d", SPA_NODE_COMMAND_ID(command));
+ res = -ENOTSUP;
+ pw_proxy_errorf(proxy, res, "command %d not supported", SPA_NODE_COMMAND_ID(command));
+ }
+ return res;
+}
+
+static int
+client_node_add_port(void *_data, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ pw_log_warn("add port not supported");
+ pw_proxy_error(proxy, -ENOTSUP, "add port not supported");
+ return -ENOTSUP;
+}
+
+static int
+client_node_remove_port(void *_data, enum spa_direction direction, uint32_t port_id)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ pw_log_warn("remove port not supported");
+ pw_proxy_error(proxy, -ENOTSUP, "remove port not supported");
+ return -ENOTSUP;
+}
+
+static int clear_buffers(struct node_data *data, struct mix *mix)
+{
+ struct pw_impl_port *port = mix->port;
+ struct buffer *b;
+ int res;
+
+ pw_log_debug("port %p: clear %zd buffers mix:%d", port,
+ pw_array_get_len(&mix->buffers, struct buffer *),
+ mix->mix_id);
+
+ if ((res = pw_impl_port_use_buffers(port, &mix->mix, 0, NULL, 0)) < 0) {
+ pw_log_error("port %p: error clear buffers %s", port, spa_strerror(res));
+ return res;
+ }
+
+ pw_array_for_each(b, &mix->buffers) {
+ pw_log_debug("port %p: clear buffer %d map %p %p",
+ port, b->id, b->mem, b->buf);
+ pw_memmap_free(b->mem);
+ free(b->buf);
+ }
+ mix->buffers.size = 0;
+ return 0;
+}
+
+static int
+client_node_port_set_param(void *_data,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct pw_impl_port *port;
+ int res;
+
+ port = pw_impl_node_find_port(data->node, direction, port_id);
+ if (port == NULL) {
+ res = -EINVAL;
+ goto error_exit;
+ }
+
+ pw_log_debug("port %p: set_param %s %p", port,
+ spa_debug_type_find_name(spa_type_param, id), param);
+
+ res = pw_impl_port_set_param(port, id, flags, param);
+ if (res < 0)
+ goto error_exit;
+
+ if (id == SPA_PARAM_Format) {
+ struct mix *mix;
+ spa_list_for_each(mix, &data->mix[direction], link) {
+ if (mix->port->port_id == port_id)
+ clear_buffers(data, mix);
+ }
+ }
+ return res;
+
+error_exit:
+ pw_log_error("port %p: set_param %d %p: %s", port, id, param, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "port_set_param(%s) failed: %s",
+ spa_debug_type_find_name(spa_type_param, id),
+ spa_strerror(res));
+ return res;
+}
+
+static int
+client_node_port_use_buffers(void *_data,
+ enum spa_direction direction, uint32_t port_id, uint32_t mix_id,
+ uint32_t flags,
+ uint32_t n_buffers, struct pw_client_node_buffer *buffers)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct buffer *bid;
+ uint32_t i, j;
+ struct spa_buffer *b, **bufs;
+ struct mix *mix;
+ int res, prot;
+
+ mix = ensure_mix(data, direction, port_id, mix_id);
+ if (mix == NULL) {
+ res = -ENOENT;
+ goto error_exit;
+ }
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ prot = PW_MEMMAP_FLAG_READWRITE;
+
+ /* clear previous buffers */
+ clear_buffers(data, mix);
+
+ bufs = alloca(n_buffers * sizeof(struct spa_buffer *));
+
+ for (i = 0; i < n_buffers; i++) {
+ size_t size;
+ off_t offset;
+ struct pw_memmap *mm;
+
+ mm = pw_mempool_map_id(data->pool, buffers[i].mem_id,
+ prot, buffers[i].offset, buffers[i].size, NULL);
+ if (mm == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+
+ bid = pw_array_add(&mix->buffers, sizeof(struct buffer));
+ if (bid == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+ bid->id = i;
+ bid->mem = mm;
+
+ if (data->allow_mlock && mlock(mm->ptr, mm->size) < 0)
+ if (errno != ENOMEM || !mlock_warned) {
+ pw_log(data->warn_mlock ? SPA_LOG_LEVEL_WARN : SPA_LOG_LEVEL_DEBUG,
+ "Failed to mlock memory %p %u: %s",
+ mm->ptr, mm->size,
+ errno == ENOMEM ?
+ "This is not a problem but for best performance, "
+ "consider increasing RLIMIT_MEMLOCK" : strerror(errno));
+ mlock_warned |= errno == ENOMEM;
+ }
+
+ size = sizeof(struct spa_buffer);
+ for (j = 0; j < buffers[i].buffer->n_metas; j++)
+ size += sizeof(struct spa_meta);
+ for (j = 0; j < buffers[i].buffer->n_datas; j++)
+ size += sizeof(struct spa_data);
+
+ b = bid->buf = malloc(size);
+ if (b == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+ memcpy(b, buffers[i].buffer, sizeof(struct spa_buffer));
+
+ b->metas = SPA_PTROFF(b, sizeof(struct spa_buffer), struct spa_meta);
+ b->datas = SPA_PTROFF(b->metas, sizeof(struct spa_meta) * b->n_metas,
+ struct spa_data);
+
+ pw_log_debug("add buffer mem:%d id:%d offset:%u size:%u %p", mm->block->id,
+ bid->id, buffers[i].offset, buffers[i].size, bid->buf);
+
+ offset = 0;
+ for (j = 0; j < b->n_metas; j++) {
+ struct spa_meta *m = &b->metas[j];
+ memcpy(m, &buffers[i].buffer->metas[j], sizeof(struct spa_meta));
+ m->data = SPA_PTROFF(mm->ptr, offset, void);
+ offset += SPA_ROUND_UP_N(m->size, 8);
+ }
+
+ for (j = 0; j < b->n_datas; j++) {
+ struct spa_data *d = &b->datas[j];
+
+ memcpy(d, &buffers[i].buffer->datas[j], sizeof(struct spa_data));
+ d->chunk =
+ SPA_PTROFF(mm->ptr, offset + sizeof(struct spa_chunk) * j,
+ struct spa_chunk);
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC)
+ continue;
+
+ if (d->type == SPA_DATA_MemId) {
+ uint32_t mem_id = SPA_PTR_TO_UINT32(d->data);
+ struct pw_memblock *bm;
+
+ bm = pw_mempool_find_id(data->pool, mem_id);
+ if (bm == NULL) {
+ pw_log_error("unknown buffer mem %u", mem_id);
+ res = -ENODEV;
+ goto error_exit_cleanup;
+ }
+
+ d->fd = bm->fd;
+ d->type = bm->type;
+ d->data = NULL;
+
+ pw_log_debug(" data %d %u -> fd %d maxsize %d",
+ j, bm->id, bm->fd, d->maxsize);
+ } else if (d->type == SPA_DATA_MemPtr) {
+ int offs = SPA_PTR_TO_INT(d->data);
+ d->data = SPA_PTROFF(mm->ptr, offs, void);
+ d->fd = -1;
+ pw_log_debug(" data %d id:%u -> mem:%p offs:%d maxsize:%d",
+ j, bid->id, d->data, offs, d->maxsize);
+ } else {
+ pw_log_warn("unknown buffer data type %d", d->type);
+ }
+ }
+ bufs[i] = b;
+ }
+
+ if ((res = pw_impl_port_use_buffers(mix->port, &mix->mix, flags, bufs, n_buffers)) < 0)
+ goto error_exit_cleanup;
+
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) {
+ pw_client_node_port_buffers(data->client_node,
+ direction, port_id, mix_id,
+ n_buffers,
+ bufs);
+ }
+ return res;
+
+error_exit_cleanup:
+ clear_buffers(data, mix);
+error_exit:
+ pw_log_error("port %p: use_buffers: %d %s", mix, res, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "port_use_buffers error: %s", spa_strerror(res));
+ return res;
+}
+
+static int
+client_node_port_set_io(void *_data,
+ uint32_t direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct mix *mix;
+ struct pw_memmap *mm, *old;
+ void *ptr;
+ int res = 0;
+ uint32_t tag[5] = { data->remote_id, direction, port_id, mix_id, id };
+
+ mix = ensure_mix(data, direction, port_id, mix_id);
+ if (mix == NULL) {
+ res = -ENOENT;
+ goto exit;
+ }
+
+ old = pw_mempool_find_tag(data->pool, tag, sizeof(tag));
+
+ if (memid == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ size = 0;
+ }
+ else {
+ mm = pw_mempool_map_id(data->pool, memid,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, tag);
+ if (mm == NULL) {
+ pw_log_warn("can't map memory id %u: %m", memid);
+ res = -errno;
+ goto exit;
+ }
+ ptr = mm->ptr;
+ }
+
+ pw_log_debug("port %p: set io:%s new:%p old:%p", mix->port,
+ spa_debug_type_find_name(spa_type_io, id), ptr, mix->mix.io);
+
+ if (id == SPA_IO_Buffers) {
+ if (ptr == NULL && mix->mix.io)
+ deactivate_mix(data, mix);
+ }
+
+ if ((res = spa_node_port_set_io(mix->port->mix,
+ direction, mix->mix.port.port_id, id, ptr, size)) < 0) {
+ if (res == -ENOTSUP)
+ res = 0;
+ else
+ goto exit_free;
+ }
+ if (id == SPA_IO_Buffers) {
+ mix->mix.io = ptr;
+ if (ptr)
+ activate_mix(data, mix);
+ }
+exit_free:
+ pw_memmap_free(old);
+exit:
+ if (res < 0) {
+ pw_log_error("port %p: set_io: %s", mix, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "port_set_io failed: %s", spa_strerror(res));
+ }
+ return res;
+}
+
+static int link_signal_func(void *user_data)
+{
+ struct link *link = user_data;
+ struct spa_system *data_system = link->data->context->data_system;
+
+ pw_log_trace_fp("link %p: signal %p", link, link->target.activation);
+ if (SPA_UNLIKELY(spa_system_eventfd_write(data_system, link->signalfd, 1) < 0))
+ pw_log_warn("link %p: write failed %m", link);
+
+ return 0;
+}
+
+static int
+do_activate_link(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct link *link = user_data;
+ struct node_data *d = link->data;
+ pw_log_trace("link %p activate", link);
+ spa_list_append(&d->node->rt.target_list, &link->target.link);
+ return 0;
+}
+
+static int
+client_node_set_activation(void *_data,
+ uint32_t node_id,
+ int signalfd,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct node_data *data = _data;
+ struct pw_proxy *proxy = (struct pw_proxy*)data->client_node;
+ struct pw_impl_node *node = data->node;
+ struct pw_memmap *mm;
+ void *ptr;
+ struct link *link;
+ int res = 0;
+
+ if (data->remote_id == node_id) {
+ pw_log_debug("node %p: our activation %u: %u %u %u", node, node_id,
+ memid, offset, size);
+ spa_system_close(data->context->data_system, signalfd);
+ return 0;
+ }
+
+ if (memid == SPA_ID_INVALID) {
+ mm = ptr = NULL;
+ size = 0;
+ } else {
+ mm = pw_mempool_map_id(data->pool, memid,
+ PW_MEMMAP_FLAG_READWRITE, offset, size, NULL);
+ if (mm == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+ ptr = mm->ptr;
+ }
+ pw_log_debug("node %p: set activation %d %p %u %u", node, node_id, ptr, offset, size);
+
+ if (ptr) {
+ link = calloc(1, sizeof(struct link));
+ if (link == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+ link->data = data;
+ link->node_id = node_id;
+ link->map = mm;
+ link->target.activation = ptr;
+ link->signalfd = signalfd;
+ link->target.signal_func = link_signal_func;
+ link->target.data = link;
+ link->target.node = NULL;
+ spa_list_append(&data->links, &link->link);
+
+ pw_loop_invoke(data->context->data_loop,
+ do_activate_link, SPA_ID_INVALID, NULL, 0, false, link);
+
+ pw_log_debug("node %p: link %p: fd:%d id:%u state %p required %d, pending %d",
+ node, link, signalfd,
+ link->target.activation->position.clock.id,
+ &link->target.activation->state[0],
+ link->target.activation->state[0].required,
+ link->target.activation->state[0].pending);
+ } else {
+ link = find_activation(&data->links, node_id);
+ if (link == NULL) {
+ res = -ENOENT;
+ goto error_exit;
+ }
+ clear_link(data, link);
+ }
+ return res;
+
+error_exit:
+ pw_log_error("node %p: set activation %d: %s", node, node_id, spa_strerror(res));
+ pw_proxy_errorf(proxy, res, "set_activation: %s", spa_strerror(res));
+ return res;
+}
+
+static const struct pw_client_node_events client_node_events = {
+ PW_VERSION_CLIENT_NODE_EVENTS,
+ .transport = client_node_transport,
+ .set_param = client_node_set_param,
+ .set_io = client_node_set_io,
+ .event = client_node_event,
+ .command = client_node_command,
+ .add_port = client_node_add_port,
+ .remove_port = client_node_remove_port,
+ .port_set_param = client_node_port_set_param,
+ .port_use_buffers = client_node_port_use_buffers,
+ .port_set_io = client_node_port_set_io,
+ .set_activation = client_node_set_activation,
+};
+
+static void do_node_init(struct node_data *data)
+{
+ struct pw_impl_port *port;
+
+ pw_log_debug("%p: node %p init", data, data->node);
+ add_node_update(data, PW_CLIENT_NODE_UPDATE_PARAMS |
+ PW_CLIENT_NODE_UPDATE_INFO,
+ SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS);
+
+ spa_list_for_each(port, &data->node->input_ports, link) {
+ add_port_update(data, port,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO);
+ }
+ spa_list_for_each(port, &data->node->output_ports, link) {
+ add_port_update(data, port,
+ PW_CLIENT_NODE_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE_PORT_UPDATE_INFO);
+ }
+}
+
+static void clear_mix(struct node_data *data, struct mix *mix)
+{
+ pw_log_debug("port %p: mix clear %d.%d", mix->port, mix->port->port_id, mix->mix_id);
+
+ deactivate_mix(data, mix);
+
+ spa_list_remove(&mix->link);
+
+ clear_buffers(data, mix);
+ pw_array_clear(&mix->buffers);
+
+ spa_list_append(&data->free_mix, &mix->link);
+ pw_impl_port_release_mix(mix->port, &mix->mix);
+}
+
+static void clean_node(struct node_data *d)
+{
+ struct mix *mix;
+
+ if (d->have_transport) {
+ spa_list_consume(mix, &d->mix[SPA_DIRECTION_INPUT], link)
+ clear_mix(d, mix);
+ spa_list_consume(mix, &d->mix[SPA_DIRECTION_OUTPUT], link)
+ clear_mix(d, mix);
+ }
+ spa_list_consume(mix, &d->free_mix, link) {
+ spa_list_remove(&mix->link);
+ free(mix);
+ }
+ clean_transport(d);
+}
+
+static void node_destroy(void *data)
+{
+ struct node_data *d = data;
+
+ pw_log_debug("%p: destroy", d);
+
+ clean_node(d);
+}
+
+static void node_free(void *data)
+{
+ struct node_data *d = data;
+ pw_log_debug("%p: free", d);
+ d->node = NULL;
+}
+
+static void node_info_changed(void *data, const struct pw_node_info *info)
+{
+ struct node_data *d = data;
+ uint32_t change_mask, info_mask;
+
+ pw_log_debug("info changed %p", d);
+
+ if (d->client_node == NULL)
+ return;
+
+ change_mask = PW_CLIENT_NODE_UPDATE_INFO;
+ info_mask = SPA_NODE_CHANGE_MASK_FLAGS;
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) {
+ info_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ }
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ change_mask |= PW_CLIENT_NODE_UPDATE_PARAMS;
+ info_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ }
+ add_node_update(d, change_mask, info_mask);
+}
+
+static void node_port_info_changed(void *data, struct pw_impl_port *port,
+ const struct pw_port_info *info)
+{
+ struct node_data *d = data;
+ uint32_t change_mask = 0;
+
+ pw_log_debug("info changed %p", d);
+
+ if (d->client_node == NULL)
+ return;
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
+ change_mask |= PW_CLIENT_NODE_PORT_UPDATE_INFO;
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
+ change_mask |= PW_CLIENT_NODE_PORT_UPDATE_PARAMS;
+ change_mask |= PW_CLIENT_NODE_PORT_UPDATE_INFO;
+ }
+ add_port_update(d, port, change_mask);
+}
+
+static void node_port_removed(void *data, struct pw_impl_port *port)
+{
+ struct node_data *d = data;
+ struct mix *mix, *tmp;
+
+ pw_log_debug("removed %p", d);
+
+ if (d->client_node == NULL)
+ return;
+
+ pw_client_node_port_update(d->client_node,
+ port->direction,
+ port->port_id,
+ 0, 0, NULL, NULL);
+
+ spa_list_for_each_safe(mix, tmp, &d->mix[port->direction], link) {
+ if (mix->port == port)
+ clear_mix(d, mix);
+ }
+}
+
+static void node_active_changed(void *data, bool active)
+{
+ struct node_data *d = data;
+ pw_log_debug("active %d", active);
+
+ if (d->client_node == NULL)
+ return;
+
+ pw_client_node_set_active(d->client_node, active);
+}
+
+static void node_event(void *data, const struct spa_event *event)
+{
+ struct node_data *d = data;
+ pw_log_debug("%p", d);
+
+ if (d->client_node == NULL)
+ return;
+ pw_client_node_event(d->client_node, event);
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .destroy = node_destroy,
+ .free = node_free,
+ .info_changed = node_info_changed,
+ .port_info_changed = node_port_info_changed,
+ .port_removed = node_port_removed,
+ .active_changed = node_active_changed,
+ .event = node_event,
+};
+
+static void client_node_removed(void *_data)
+{
+ struct node_data *data = _data;
+ pw_log_debug("%p: removed", data);
+
+ spa_hook_remove(&data->proxy_client_node_listener);
+ spa_hook_remove(&data->client_node_listener);
+
+ if (data->node) {
+ spa_hook_remove(&data->node_listener);
+ pw_impl_node_set_state(data->node, PW_NODE_STATE_SUSPENDED);
+
+ clean_node(data);
+
+ if (data->do_free)
+ pw_impl_node_destroy(data->node);
+ }
+ data->client_node = NULL;
+}
+
+static void client_node_destroy(void *_data)
+{
+ struct node_data *data = _data;
+
+ pw_log_debug("%p: destroy", data);
+ client_node_removed(_data);
+}
+
+static void client_node_bound(void *_data, uint32_t global_id)
+{
+ struct node_data *data = _data;
+ pw_log_debug("%p: bound %u", data, global_id);
+ data->remote_id = global_id;
+}
+
+static const struct pw_proxy_events proxy_client_node_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = client_node_removed,
+ .destroy = client_node_destroy,
+ .bound = client_node_bound,
+};
+
+static int node_ready(void *d, int status)
+{
+ struct node_data *data = d;
+ struct pw_impl_node *node = data->node;
+ struct pw_node_activation *a = node->rt.activation;
+ struct spa_system *data_system = data->context->data_system;
+ struct timespec ts;
+ struct pw_impl_port *p;
+
+ pw_log_trace_fp("node %p: ready driver:%d exported:%d status:%d", node,
+ node->driver, node->exported, status);
+
+ if (status & SPA_STATUS_HAVE_DATA) {
+ spa_list_for_each(p, &node->rt.output_mix, rt.node_link)
+ spa_node_process(p->mix);
+ }
+
+ spa_system_clock_gettime(data_system, CLOCK_MONOTONIC, &ts);
+ a->status = PW_NODE_ACTIVATION_TRIGGERED;
+ a->signal_time = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ if (SPA_UNLIKELY(spa_system_eventfd_write(data_system, data->rtwritefd, 1) < 0))
+ pw_log_warn("node %p: write failed %m", node);
+
+ return 0;
+}
+
+static int node_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_id)
+{
+ return 0;
+}
+
+static int node_xrun(void *d, uint64_t trigger, uint64_t delay, struct spa_pod *info)
+{
+ struct node_data *data = d;
+ struct pw_impl_node *node = data->node;
+ struct pw_node_activation *a = node->rt.activation;
+
+ a->xrun_count++;
+ a->xrun_time = trigger;
+ a->xrun_delay = delay;
+ a->max_delay = SPA_MAX(a->max_delay, delay);
+
+ pw_log_debug("node %p: XRun! count:%u time:%"PRIu64" delay:%"PRIu64" max:%"PRIu64,
+ node, a->xrun_count, trigger, delay, a->max_delay);
+
+ pw_context_driver_emit_xrun(data->context, node);
+
+ return 0;
+}
+
+static const struct spa_node_callbacks node_callbacks = {
+ SPA_VERSION_NODE_CALLBACKS,
+ .ready = node_ready,
+ .reuse_buffer = node_reuse_buffer,
+ .xrun = node_xrun
+};
+
+static struct pw_proxy *node_export(struct pw_core *core, void *object, bool do_free,
+ size_t user_data_size)
+{
+ struct pw_impl_node *node = object;
+ struct pw_proxy *client_node;
+ struct node_data *data;
+
+ user_data_size = SPA_ROUND_UP_N(user_data_size, __alignof__(struct node_data));
+
+ client_node = pw_core_create_object(core,
+ "client-node",
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE,
+ &node->properties->dict,
+ user_data_size + sizeof(struct node_data));
+ if (client_node == NULL)
+ goto error;
+
+ data = pw_proxy_get_user_data(client_node);
+ data = SPA_PTROFF(data, user_data_size, struct node_data);
+ data->pool = pw_core_get_mempool(core);
+ data->node = node;
+ data->do_free = do_free;
+ data->context = pw_impl_node_get_context(node);
+ data->client_node = (struct pw_client_node *)client_node;
+ data->remote_id = SPA_ID_INVALID;
+
+
+ data->allow_mlock = pw_properties_get_bool(node->properties, "mem.allow-mlock",
+ data->context->settings.mem_allow_mlock);
+
+ data->warn_mlock = pw_properties_get_bool(node->properties, "mem.warn-mlock",
+ data->context->settings.mem_warn_mlock);
+
+ node->exported = true;
+
+ spa_list_init(&data->free_mix);
+ spa_list_init(&data->mix[0]);
+ spa_list_init(&data->mix[1]);
+
+ spa_list_init(&data->links);
+
+ pw_proxy_add_listener(client_node,
+ &data->proxy_client_node_listener,
+ &proxy_client_node_events, data);
+
+ spa_node_set_callbacks(node->node, &node_callbacks, data);
+ pw_impl_node_add_listener(node, &data->node_listener, &node_events, data);
+
+ pw_client_node_add_listener(data->client_node,
+ &data->client_node_listener,
+ &client_node_events,
+ data);
+ do_node_init(data);
+
+ return client_node;
+error:
+ if (do_free)
+ pw_impl_node_destroy(node);
+ return NULL;
+
+}
+
+struct pw_proxy *pw_core_node_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_impl_node *node = object;
+
+ if (props)
+ pw_impl_node_update_properties(node, props);
+ return node_export(core, object, false, user_data_size);
+}
+
+struct pw_proxy *pw_core_spa_node_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_impl_node *node;
+ struct pw_proxy *proxy;
+ const char *str;
+ bool do_register;
+
+ str = props ? spa_dict_lookup(props, PW_KEY_OBJECT_REGISTER) : NULL;
+ do_register = str ? pw_properties_parse_bool(str) : true;
+
+ node = pw_context_create_node(pw_core_get_context(core),
+ props ? pw_properties_new_dict(props) : NULL, 0);
+ if (node == NULL)
+ return NULL;
+
+ pw_impl_node_set_implementation(node, (struct spa_node*)object);
+
+ if (do_register)
+ pw_impl_node_register(node, NULL);
+
+ proxy = node_export(core, node, true, user_data_size);
+ if (proxy)
+ pw_impl_node_set_active(node, true);
+
+ return proxy;
+}
diff --git a/src/modules/module-client-node/v0/client-node.c b/src/modules/module-client-node/v0/client-node.c
new file mode 100644
index 0000000..e70a7c2
--- /dev/null
+++ b/src/modules/module-client-node/v0/client-node.c
@@ -0,0 +1,1447 @@
+/* PipeWire
+ *
+ * Copyright © 2015 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dlfcn.h>
+#include <sys/socket.h>
+#include <sys/mman.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/pod/filter.h>
+#include <spa/utils/keys.h>
+
+#define PW_ENABLE_DEPRECATED
+
+#include "pipewire/pipewire.h"
+#include "pipewire/private.h"
+
+#include "pipewire/context.h"
+#include "modules/spa/spa-node.h"
+#include "client-node.h"
+#include "transport.h"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/** \cond */
+
+#define MAX_INPUTS 64
+#define MAX_OUTPUTS 64
+
+#define MAX_BUFFERS 64
+
+#define CHECK_IN_PORT_ID(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_INPUTS)
+#define CHECK_OUT_PORT_ID(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_OUTPUTS)
+#define CHECK_PORT_ID(this,d,p) (CHECK_IN_PORT_ID(this,d,p) || CHECK_OUT_PORT_ID(this,d,p))
+#define CHECK_FREE_IN_PORT(this,d,p) (CHECK_IN_PORT_ID(this,d,p) && !(this)->in_ports[p].valid)
+#define CHECK_FREE_OUT_PORT(this,d,p) (CHECK_OUT_PORT_ID(this,d,p) && !(this)->out_ports[p].valid)
+#define CHECK_FREE_PORT(this,d,p) (CHECK_FREE_IN_PORT (this,d,p) || CHECK_FREE_OUT_PORT (this,d,p))
+#define CHECK_IN_PORT(this,d,p) (CHECK_IN_PORT_ID(this,d,p) && (this)->in_ports[p].valid)
+#define CHECK_OUT_PORT(this,d,p) (CHECK_OUT_PORT_ID(this,d,p) && (this)->out_ports[p].valid)
+#define CHECK_PORT(this,d,p) (CHECK_IN_PORT (this,d,p) || CHECK_OUT_PORT (this,d,p))
+
+#define GET_IN_PORT(this,p) (&this->in_ports[p])
+#define GET_OUT_PORT(this,p) (&this->out_ports[p])
+#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p))
+
+#define CHECK_PORT_BUFFER(this,b,p) (b < p->n_buffers)
+
+extern uint32_t pw_protocol_native0_type_from_v2(struct pw_impl_client *client, uint32_t type);
+extern uint32_t pw_protocol_native0_name_to_v2(struct pw_impl_client *client, const char *name);
+
+struct mem {
+ uint32_t id;
+ int ref;
+ int fd;
+ uint32_t type;
+ uint32_t flags;
+};
+
+struct buffer {
+ struct spa_buffer *outbuf;
+ struct spa_buffer buffer;
+ struct spa_meta metas[4];
+ struct spa_data datas[4];
+ bool outstanding;
+ uint32_t memid;
+};
+
+struct port {
+ uint32_t id;
+ enum spa_direction direction;
+
+ bool valid;
+ struct spa_port_info info;
+ struct pw_properties *properties;
+
+ bool have_format;
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct spa_io_buffers *io;
+
+ uint32_t n_buffers;
+ struct buffer buffers[MAX_BUFFERS];
+};
+
+struct node {
+ struct spa_node node;
+
+ struct impl *impl;
+
+ struct spa_log *log;
+ struct spa_loop *data_loop;
+ struct spa_system *data_system;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct spa_io_position *position;
+
+ struct pw_resource *resource;
+
+ struct spa_source data_source;
+ int writefd;
+
+ struct spa_node_info info;
+
+ uint32_t n_inputs;
+ uint32_t n_outputs;
+ struct port in_ports[MAX_INPUTS];
+ struct port out_ports[MAX_OUTPUTS];
+
+ uint32_t n_params;
+ struct spa_pod **params;
+
+ uint32_t seq;
+ uint32_t init_pending;
+};
+
+struct impl {
+ struct pw_impl_client_node0 this;
+
+ bool client_reuse;
+
+ struct pw_context *context;
+
+ struct node node;
+
+ struct pw_client_node0_transport *transport;
+
+ struct spa_hook node_listener;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ struct pw_array mems;
+
+ int fds[2];
+ int other_fds[2];
+
+ uint32_t input_ready;
+ bool out_pending;
+};
+
+/** \endcond */
+
+static struct mem *ensure_mem(struct impl *impl, int fd, uint32_t type, uint32_t flags)
+{
+ struct mem *m, *f = NULL;
+
+ pw_array_for_each(m, &impl->mems) {
+ if (m->ref <= 0)
+ f = m;
+ else if (m->fd == fd)
+ goto found;
+ }
+
+ if (f == NULL) {
+ m = pw_array_add(&impl->mems, sizeof(struct mem));
+ m->id = pw_array_get_len(&impl->mems, struct mem) - 1;
+ m->ref = 0;
+ }
+ else {
+ m = f;
+ }
+ m->fd = fd;
+ m->type = type;
+ m->flags = flags;
+
+ pw_client_node0_resource_add_mem(impl->node.resource,
+ m->id,
+ type,
+ m->fd,
+ m->flags);
+ found:
+ m->ref++;
+ return m;
+}
+
+
+static int clear_buffers(struct node *this, struct port *port)
+{
+ uint32_t i, j;
+ struct impl *impl = this->impl;
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct mem *m;
+
+ spa_log_debug(this->log, "node %p: clear buffer %d", this, i);
+
+ for (j = 0; j < b->buffer.n_datas; j++) {
+ struct spa_data *d = &b->datas[j];
+
+ if (d->type == SPA_DATA_DmaBuf ||
+ d->type == SPA_DATA_MemFd) {
+ uint32_t id;
+
+ id = SPA_PTR_TO_UINT32(b->buffer.datas[j].data);
+ m = pw_array_get_unchecked(&impl->mems, id, struct mem);
+ m->ref--;
+ }
+ }
+ m = pw_array_get_unchecked(&impl->mems, b->memid, struct mem);
+ m->ref--;
+ }
+ port->n_buffers = 0;
+ return 0;
+}
+
+static void emit_port_info(struct node *this, struct port *port)
+{
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+}
+
+static int impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct node *this = object;
+ struct spa_hook_list save;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ for (i = 0; i < MAX_INPUTS; i++) {
+ if (this->in_ports[i].valid)
+ emit_port_info(this, &this->in_ports[i]);
+ }
+ for (i = 0; i < MAX_OUTPUTS; i++) {
+ if (this->out_ports[i].valid)
+ emit_port_info(this, &this->out_ports[i]);
+ }
+ spa_hook_list_join(&this->hooks, &save);
+
+ 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 node *this = object;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ bool found = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = 0;
+
+ while (true) {
+ struct spa_pod *param;
+
+ result.index = result.next++;
+ if (result.index >= this->n_params)
+ break;
+
+ param = this->params[result.index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ found = true;
+
+ if (result.index < start)
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result.param, param, filter) != 0)
+ continue;
+
+ pw_log_debug("%p: %d param %u", this, seq, result.index);
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ pw_client_node0_resource_set_param(this->resource, this->seq, id, flags, param);
+
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct node *this = object;
+ int res = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch(id) {
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ res = -ENOTSUP;
+ break;
+ }
+ return res;
+}
+
+static inline void do_flush(struct node *this)
+{
+ if (spa_system_eventfd_write(this->data_system, this->writefd, 1) < 0)
+ spa_log_warn(this->log, "node %p: error flushing : %s", this, strerror(errno));
+
+}
+
+static int send_clock_update(struct node *this)
+{
+ struct pw_impl_client *client = this->resource->client;
+ uint32_t type = pw_protocol_native0_name_to_v2(client, SPA_TYPE_INFO_NODE_COMMAND_BASE "ClockUpdate");
+ struct timespec ts;
+ int64_t now;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ now = SPA_TIMESPEC_TO_NSEC(&ts);
+ pw_log_trace("%p: now %"PRIi64, this, now);
+
+ struct spa_command_node0_clock_update cu =
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_INIT(type,
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_TIME |
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_SCALE |
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_STATE |
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_LATENCY, /* change_mask */
+ SPA_USEC_PER_SEC, /* rate */
+ now / SPA_NSEC_PER_USEC, /* ticks */
+ now, /* monotonic_time */
+ 0, /* offset */
+ (1 << 16) | 1, /* scale */
+ SPA_CLOCK0_STATE_RUNNING, /* state */
+ SPA_COMMAND_NODE0_CLOCK_UPDATE_FLAG_LIVE, /* flags */
+ 0); /* latency */
+
+ pw_client_node0_resource_command(this->resource, this->seq, (const struct spa_command*)&cu);
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ if (SPA_NODE_COMMAND_ID(command) == SPA_NODE_COMMAND_Start) {
+ send_clock_update(this);
+ }
+
+ pw_client_node0_resource_command(this->resource, this->seq, command);
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ pw_log_debug("%p: sync %p", this, this->resource);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ this->init_pending = SPA_RESULT_RETURN_ASYNC(this->seq++);
+
+ return this->init_pending;
+}
+
+
+extern struct spa_pod *pw_protocol_native0_pod_from_v2(struct pw_impl_client *client, const struct spa_pod *pod);
+extern int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod,
+ struct spa_pod_builder *b);
+
+static void
+do_update_port(struct node *this,
+ 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)
+{
+ struct port *port;
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (!port->valid) {
+ spa_log_debug(this->log, "node %p: adding port %d, direction %d",
+ this, port_id, direction);
+ port->id = port_id;
+ port->direction = direction;
+ port->have_format = false;
+ port->valid = true;
+
+ if (direction == SPA_DIRECTION_INPUT)
+ this->n_inputs++;
+ else
+ this->n_outputs++;
+ }
+
+ if (change_mask & PW_CLIENT_NODE0_PORT_UPDATE_PARAMS) {
+ uint32_t i;
+
+ port->have_format = false;
+
+ spa_log_debug(this->log, "node %p: port %u update %d params", this, port_id, n_params);
+ for (i = 0; i < port->n_params; i++)
+ free(port->params[i]);
+ port->n_params = n_params;
+ if (port->n_params == 0) {
+ free(port->params);
+ port->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(port->params, port->n_params, sizeof(struct spa_pod *));
+ if (p == NULL) {
+ pw_log_error("%p: port %u can't realloc: %m", this, port_id);
+ free(port->params);
+ port->n_params = 0;
+ }
+ port->params = p;
+ }
+ for (i = 0; i < port->n_params; i++) {
+ port->params[i] = params[i] ?
+ pw_protocol_native0_pod_from_v2(this->resource->client, params[i]) : NULL;
+
+ if (port->params[i] && spa_pod_is_object_id(port->params[i], SPA_PARAM_Format))
+ port->have_format = true;
+ }
+ }
+
+ if (change_mask & PW_CLIENT_NODE0_PORT_UPDATE_INFO) {
+ pw_properties_free(port->properties);
+ port->properties = NULL;
+ port->info.props = NULL;
+ port->info.n_params = 0;
+ port->info.params = NULL;
+
+ if (info) {
+ port->info = *info;
+ if (info->props) {
+ port->properties = pw_properties_new_dict(info->props);
+ port->info.props = &port->properties->dict;
+ }
+ }
+ spa_node_emit_port_info(&this->hooks, direction, port_id, info);
+ }
+}
+
+static void
+clear_port(struct node *this,
+ struct port *port, enum spa_direction direction, uint32_t port_id)
+{
+ do_update_port(this,
+ direction,
+ port_id,
+ PW_CLIENT_NODE0_PORT_UPDATE_PARAMS |
+ PW_CLIENT_NODE0_PORT_UPDATE_INFO, 0, NULL, NULL);
+ clear_buffers(this, port);
+}
+
+static void do_uninit_port(struct node *this, enum spa_direction direction, uint32_t port_id)
+{
+ struct port *port;
+
+ spa_log_debug(this->log, "node %p: removing port %d", this, port_id);
+
+ if (direction == SPA_DIRECTION_INPUT) {
+ port = GET_IN_PORT(this, port_id);
+ this->n_inputs--;
+ } else {
+ port = GET_OUT_PORT(this, port_id);
+ this->n_outputs--;
+ }
+ clear_port(this, port, direction, port_id);
+ port->valid = false;
+ spa_node_emit_port_info(&this->hooks, direction, port_id, NULL);
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ struct node *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_FREE_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+ clear_port(this, port, direction, port_id);
+
+ return 0;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ struct node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ do_uninit_port(this, direction, port_id);
+
+ return 0;
+}
+
+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 node *this = object;
+ struct port *port;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ bool found = false;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ pw_log_debug("%p: %d port %d.%d %u %u %u", this, seq,
+ direction, port_id, id, start, num);
+
+ result.id = id;
+ result.next = 0;
+
+ while (true) {
+ struct spa_pod *param;
+
+ result.index = result.next++;
+ if (result.index >= port->n_params)
+ break;
+
+ param = port->params[result.index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ found = true;
+
+ if (result.index < start)
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ continue;
+
+ pw_log_debug("%p: %d param %u", this, seq, result.index);
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+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 node *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ pw_client_node0_resource_port_set_param(this->resource,
+ this->seq,
+ direction, port_id,
+ id, flags,
+ param);
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+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 node *this = object;
+ struct impl *impl;
+ struct pw_memblock *mem;
+ struct mem *m;
+ uint32_t memid, mem_offset, mem_size;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ impl = this->impl;
+
+ spa_log_debug(this->log, "node %p: port %d.%d set io %d %p", this,
+ direction, port_id, id, data);
+
+ if (id == SPA_IO_Buffers) {
+ struct port *port = GET_PORT(this, direction, port_id);
+ port->io = data;
+ }
+
+ if (this->resource == NULL)
+ return -EIO;
+
+
+ if (data) {
+ if ((mem = pw_mempool_find_ptr(impl->context->pool, data)) == NULL)
+ return -EINVAL;
+
+ mem_offset = SPA_PTRDIFF(data, mem->map->ptr);
+ mem_size = mem->size;
+ if (mem_size - mem_offset < size)
+ return -EINVAL;
+
+ mem_offset += mem->map->offset;
+ m = ensure_mem(impl, mem->fd, SPA_DATA_MemFd, mem->flags);
+ memid = m->id;
+ }
+ else {
+ memid = SPA_ID_INVALID;
+ mem_offset = mem_size = 0;
+ }
+
+ pw_client_node0_resource_port_set_io(this->resource,
+ this->seq,
+ direction, port_id,
+ id,
+ memid,
+ mem_offset, mem_size);
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+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 node *this = object;
+ struct impl *impl;
+ struct port *port;
+ uint32_t i, j;
+ struct pw_client_node0_buffer *mb;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ impl = this->impl;
+ spa_log_debug(this->log, "node %p: use buffers %p %u", this, buffers, n_buffers);
+
+ port = GET_PORT(this, direction, port_id);
+
+ if (!port->have_format)
+ return -EIO;
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0) {
+ mb = alloca(n_buffers * sizeof(struct pw_client_node0_buffer));
+ } else {
+ mb = NULL;
+ }
+
+ port->n_buffers = n_buffers;
+
+ if (this->resource == NULL)
+ return -EIO;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct pw_memblock *mem;
+ struct mem *m;
+ size_t data_size;
+ void *baseptr;
+
+ b->outbuf = buffers[i];
+ memcpy(&b->buffer, buffers[i], sizeof(struct spa_buffer));
+ b->buffer.datas = b->datas;
+ b->buffer.metas = b->metas;
+
+ if (buffers[i]->n_metas > 0)
+ baseptr = buffers[i]->metas[0].data;
+ else if (buffers[i]->n_datas > 0)
+ baseptr = buffers[i]->datas[0].chunk;
+ else
+ return -EINVAL;
+
+ if ((mem = pw_mempool_find_ptr(impl->context->pool, baseptr)) == NULL)
+ return -EINVAL;
+
+ data_size = 0;
+ for (j = 0; j < buffers[i]->n_metas; j++) {
+ data_size += buffers[i]->metas[j].size;
+ }
+ for (j = 0; j < buffers[i]->n_datas; j++) {
+ struct spa_data *d = buffers[i]->datas;
+ data_size += sizeof(struct spa_chunk);
+ if (d->type == SPA_DATA_MemPtr)
+ data_size += d->maxsize;
+ }
+
+ m = ensure_mem(impl, mem->fd, SPA_DATA_MemFd, mem->flags);
+ b->memid = m->id;
+
+ mb[i].buffer = &b->buffer;
+ mb[i].mem_id = b->memid;
+ mb[i].offset = SPA_PTRDIFF(baseptr, SPA_PTROFF(mem->map->ptr, mem->map->offset, void));
+ mb[i].size = data_size;
+
+ for (j = 0; j < buffers[i]->n_metas; j++)
+ memcpy(&b->buffer.metas[j], &buffers[i]->metas[j], sizeof(struct spa_meta));
+ b->buffer.n_metas = j;
+
+ for (j = 0; j < buffers[i]->n_datas; j++) {
+ struct spa_data *d = &buffers[i]->datas[j];
+
+ memcpy(&b->buffer.datas[j], d, sizeof(struct spa_data));
+
+ if (d->type == SPA_DATA_DmaBuf ||
+ d->type == SPA_DATA_MemFd) {
+ m = ensure_mem(impl, d->fd, d->type, d->flags);
+ b->buffer.datas[j].data = SPA_UINT32_TO_PTR(m->id);
+ } else if (d->type == SPA_DATA_MemPtr) {
+ b->buffer.datas[j].data = SPA_INT_TO_PTR(SPA_PTRDIFF(d->data, baseptr));
+ } else {
+ b->buffer.datas[j].type = SPA_ID_INVALID;
+ b->buffer.datas[j].data = 0;
+ spa_log_error(this->log, "invalid memory type %d", d->type);
+ }
+ }
+ }
+
+ pw_client_node0_resource_port_use_buffers(this->resource,
+ this->seq,
+ direction, port_id,
+ n_buffers, mb);
+
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int
+impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct node *this = object;
+ struct impl *impl;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(CHECK_OUT_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+ impl = this->impl;
+
+ spa_log_trace(this->log, "reuse buffer %d", buffer_id);
+
+ pw_client_node0_transport_add_message(impl->transport, (struct pw_client_node0_message *)
+ &PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER_INIT(port_id, buffer_id));
+ do_flush(this);
+
+ return 0;
+}
+
+static int impl_node_process_input(struct spa_node *node)
+{
+ struct node *this = SPA_CONTAINER_OF(node, struct node, node);
+ struct impl *impl = this->impl;
+// bool client_reuse = impl->client_reuse;
+ uint32_t i;
+ int res;
+
+ if (impl->input_ready == 0) {
+ /* the client is not ready to receive our buffers, recycle them */
+ pw_log_trace("node not ready, recycle buffers");
+ for (i = 0; i < MAX_INPUTS; i++) {
+ struct port *p = &this->in_ports[i];
+ struct spa_io_buffers *io = p->io;
+
+ if (!p->valid || io == NULL)
+ continue;
+
+ io->status = SPA_STATUS_NEED_DATA;
+ }
+ res = SPA_STATUS_NEED_DATA;
+ }
+ else {
+ for (i = 0; i < MAX_INPUTS; i++) {
+ struct port *p = &this->in_ports[i];
+ struct spa_io_buffers *io = p->io;
+
+ if (!p->valid || io == NULL)
+ continue;
+
+ pw_log_trace("set io status to %d %d", io->status, io->buffer_id);
+ impl->transport->inputs[p->id] = *io;
+
+ /* explicitly recycle buffers when the client is not going to do it */
+// if (!client_reuse && (pp = p->peer))
+// spa_node_port_reuse_buffer(pp->node->implementation,
+// pp->port_id, io->buffer_id);
+ }
+ pw_client_node0_transport_add_message(impl->transport,
+ &PW_CLIENT_NODE0_MESSAGE_INIT(PW_CLIENT_NODE0_MESSAGE_PROCESS_INPUT));
+ do_flush(this);
+
+ impl->input_ready--;
+ res = SPA_STATUS_OK;
+ }
+ return res;
+}
+
+#if 0
+/** this is used for clients providing data to pipewire and currently
+ * not supported in the compat layer */
+static int impl_node_process_output(struct spa_node *node)
+{
+ struct node *this;
+ struct impl *impl;
+ uint32_t i;
+
+ this = SPA_CONTAINER_OF(node, struct node, node);
+ impl = this->impl;
+
+ if (impl->out_pending)
+ goto done;
+
+ impl->out_pending = true;
+
+ for (i = 0; i < MAX_OUTPUTS; i++) {
+ struct port *p = &this->out_ports[i];
+ struct spa_io_buffers *io = p->io;
+
+ if (!p->valid || io == NULL)
+ continue;
+
+ impl->transport->outputs[p->id] = *io;
+
+ pw_log_trace("%d %d -> %d %d", io->status, io->buffer_id,
+ impl->transport->outputs[p->id].status,
+ impl->transport->outputs[p->id].buffer_id);
+ }
+
+ done:
+ pw_client_node0_transport_add_message(impl->transport,
+ &PW_CLIENT_NODE0_MESSAGE_INIT(PW_CLIENT_NODE0_MESSAGE_PROCESS_OUTPUT));
+ do_flush(this);
+
+ return SPA_STATUS_OK;
+}
+#endif
+
+static int impl_node_process(void *object)
+{
+ struct node *this = object;
+ struct impl *impl = this->impl;
+ struct pw_impl_node *n = impl->this.node;
+
+ return impl_node_process_input(n->node);
+}
+
+static int handle_node_message(struct node *this, struct pw_client_node0_message *message)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, node);
+ uint32_t i;
+
+ switch (PW_CLIENT_NODE0_MESSAGE_TYPE(message)) {
+ case PW_CLIENT_NODE0_MESSAGE_HAVE_OUTPUT:
+ for (i = 0; i < MAX_OUTPUTS; i++) {
+ struct port *p = &this->out_ports[i];
+ struct spa_io_buffers *io = p->io;
+ if (!p->valid || io == NULL)
+ continue;
+ *io = impl->transport->outputs[p->id];
+ pw_log_trace("have output %d %d", io->status, io->buffer_id);
+ }
+ impl->out_pending = false;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA);
+ break;
+
+ case PW_CLIENT_NODE0_MESSAGE_NEED_INPUT:
+ for (i = 0; i < MAX_INPUTS; i++) {
+ struct port *p = &this->in_ports[i];
+ struct spa_io_buffers *io = p->io;
+ if (!p->valid || io == NULL)
+ continue;
+ pw_log_trace("need input %d %d", i, p->id);
+ *io = impl->transport->inputs[p->id];
+ pw_log_trace("need input %d %d", io->status, io->buffer_id);
+ }
+ impl->input_ready++;
+ spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
+ break;
+
+ case PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER:
+ if (impl->client_reuse) {
+ struct pw_client_node0_message_port_reuse_buffer *p =
+ (struct pw_client_node0_message_port_reuse_buffer *) message;
+ spa_node_call_reuse_buffer(&this->callbacks, p->body.port_id.value,
+ p->body.buffer_id.value);
+ }
+ break;
+
+ default:
+ pw_log_warn("unhandled message %d", PW_CLIENT_NODE0_MESSAGE_TYPE(message));
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static void setup_transport(struct impl *impl)
+{
+ struct node *this = &impl->node;
+ uint32_t max_inputs = 0, max_outputs = 0, n_inputs = 0, n_outputs = 0;
+ struct spa_dict_item items[1];
+
+ n_inputs = this->n_inputs;
+ max_inputs = this->info.max_input_ports == 0 ? this->n_inputs : this->info.max_input_ports;
+ n_outputs = this->n_outputs;
+ max_outputs = this->info.max_output_ports == 0 ? this->n_outputs : this->info.max_output_ports;
+
+ impl->transport = pw_client_node0_transport_new(impl->context, max_inputs, max_outputs);
+ impl->transport->area->n_input_ports = n_inputs;
+ impl->transport->area->n_output_ports = n_outputs;
+
+ if (n_inputs > 0) {
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Stream/Input/Video");
+ } else {
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Stream/Output/Video");
+ }
+ pw_impl_node_update_properties(impl->this.node, &SPA_DICT_INIT(items, 1));
+}
+
+static void
+client_node0_done(void *data, int seq, int res)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ if (seq == 0 && res == 0 && impl->transport == NULL)
+ setup_transport(impl);
+
+ pw_log_debug("seq:%d res:%d pending:%d", seq, res, this->init_pending);
+ spa_node_emit_result(&this->hooks, seq, res, 0, NULL);
+
+ if (this->init_pending != SPA_ID_INVALID) {
+ spa_node_emit_result(&this->hooks, this->init_pending, res, 0, NULL);
+ this->init_pending = SPA_ID_INVALID;
+ }
+}
+
+static void
+client_node0_update(void *data,
+ uint32_t change_mask,
+ uint32_t max_input_ports,
+ uint32_t max_output_ports,
+ uint32_t n_params,
+ const struct spa_pod **params)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ if (change_mask & PW_CLIENT_NODE0_UPDATE_MAX_INPUTS)
+ this->info.max_input_ports = max_input_ports;
+ if (change_mask & PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS)
+ this->info.max_output_ports = max_output_ports;
+ if (change_mask & PW_CLIENT_NODE0_UPDATE_PARAMS) {
+ uint32_t i;
+ spa_log_debug(this->log, "node %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, this->n_params, sizeof(struct spa_pod *));
+ if (p == NULL) {
+ pw_log_error("%p: can't realloc: %m", this);
+ free(this->params);
+ this->n_params = 0;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++)
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ }
+ if (change_mask & (PW_CLIENT_NODE0_UPDATE_MAX_INPUTS | PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS)) {
+ spa_node_emit_info(&this->hooks, &this->info);
+ }
+
+ spa_log_debug(this->log, "node %p: got node update max_in %u, max_out %u", this,
+ this->info.max_input_ports, this->info.max_output_ports);
+}
+
+static void
+client_node0_port_update(void *data,
+ 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)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+ bool remove;
+
+ spa_log_debug(this->log, "node %p: got port update", this);
+ if (!CHECK_PORT_ID(this, direction, port_id))
+ return;
+
+ remove = (change_mask == 0);
+
+ if (remove) {
+ do_uninit_port(this, direction, port_id);
+ } else {
+ do_update_port(this,
+ direction,
+ port_id,
+ change_mask,
+ n_params, params, info);
+ }
+}
+
+static void client_node0_set_active(void *data, bool active)
+{
+ struct impl *impl = data;
+ pw_impl_node_set_active(impl->this.node, active);
+}
+
+static void client_node0_event(void *data, struct spa_event *event)
+{
+ struct impl *impl = data;
+ struct node *this = &impl->node;
+
+ switch (SPA_EVENT_TYPE(event)) {
+ case SPA_NODE0_EVENT_RequestClockUpdate:
+ send_clock_update(this);
+ break;
+ default:
+ spa_node_emit_event(&this->hooks, event);
+ }
+}
+
+static const struct pw_client_node0_methods client_node0_methods = {
+ PW_VERSION_CLIENT_NODE0_METHODS,
+ .done = client_node0_done,
+ .update = client_node0_update,
+ .port_update = client_node0_port_update,
+ .set_active = client_node0_set_active,
+ .event = client_node0_event,
+};
+
+static void node_on_data_fd_events(struct spa_source *source)
+{
+ struct node *this = source->data;
+ struct impl *impl = this->impl;
+
+ if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ spa_log_warn(this->log, "node %p: got error", this);
+ return;
+ }
+
+ if (source->rmask & SPA_IO_IN) {
+ struct pw_client_node0_message message;
+ uint64_t cmd;
+
+ if (spa_system_eventfd_read(this->data_system, this->data_source.fd, &cmd) < 0)
+ spa_log_warn(this->log, "node %p: error reading message: %s",
+ this, strerror(errno));
+
+ while (pw_client_node0_transport_next_message(impl->transport, &message) == 1) {
+ struct pw_client_node0_message *msg = alloca(SPA_POD_SIZE(&message));
+ pw_client_node0_transport_parse_message(impl->transport, msg);
+ handle_node_message(this, msg);
+ }
+ }
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int
+node_init(struct node *this,
+ struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ 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);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data-loop is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+ spa_hook_list_init(&this->hooks);
+
+ this->data_source.func = node_on_data_fd_events;
+ this->data_source.data = this;
+ this->data_source.fd = -1;
+ this->data_source.mask = SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP;
+ this->data_source.rmask = 0;
+
+ this->seq = 1;
+ this->init_pending = SPA_ID_INVALID;
+
+ return SPA_RESULT_RETURN_ASYNC(this->seq++);
+}
+
+static int node_clear(struct node *this)
+{
+ uint32_t i;
+
+ for (i = 0; i < MAX_INPUTS; i++) {
+ if (this->in_ports[i].valid)
+ clear_port(this, &this->in_ports[i], SPA_DIRECTION_INPUT, i);
+ }
+ for (i = 0; i < MAX_OUTPUTS; i++) {
+ if (this->out_ports[i].valid)
+ clear_port(this, &this->out_ports[i], SPA_DIRECTION_OUTPUT, i);
+ }
+
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct spa_source *source = user_data;
+ spa_loop_remove_source(loop, source);
+ return 0;
+}
+
+static void client_node0_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node0 *this = &impl->this;
+ struct node *node = &impl->node;
+
+ pw_log_debug("client-node %p: destroy", impl);
+
+ impl->node.resource = this->resource = NULL;
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->object_listener);
+
+ if (node->data_source.fd != -1) {
+ spa_loop_invoke(node->data_loop,
+ do_remove_source,
+ SPA_ID_INVALID,
+ NULL,
+ 0,
+ true,
+ &node->data_source);
+ }
+ if (this->node)
+ pw_impl_node_destroy(this->node);
+}
+
+static void node_initialized(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node0 *this = &impl->this;
+ struct pw_impl_node *node = this->node;
+ struct spa_system *data_system = impl->node.data_system;
+
+ if (this->resource == NULL)
+ return;
+
+ impl->fds[0] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ impl->fds[1] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
+ impl->node.data_source.fd = impl->fds[0];
+ impl->node.writefd = impl->fds[1];
+ impl->other_fds[0] = impl->fds[1];
+ impl->other_fds[1] = impl->fds[0];
+
+ spa_loop_add_source(impl->node.data_loop, &impl->node.data_source);
+ pw_log_debug("client-node %p: transport fd %d %d", node, impl->fds[0], impl->fds[1]);
+
+ pw_client_node0_resource_transport(this->resource,
+ pw_global_get_id(pw_impl_node_get_global(node)),
+ impl->other_fds[0],
+ impl->other_fds[1],
+ impl->transport);
+}
+
+static void node_free(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_client_node0 *this = &impl->this;
+ struct spa_system *data_system = impl->node.data_system;
+
+ this->node = NULL;
+
+ pw_log_debug("client-node %p: free", &impl->this);
+ node_clear(&impl->node);
+
+ if (impl->transport)
+ pw_client_node0_transport_destroy(impl->transport);
+
+ spa_hook_remove(&impl->node_listener);
+
+ if (this->resource)
+ pw_resource_destroy(this->resource);
+
+ pw_array_clear(&impl->mems);
+
+ if (impl->fds[0] != -1)
+ spa_system_close(data_system, impl->fds[0]);
+ if (impl->fds[1] != -1)
+ spa_system_close(data_system, impl->fds[1]);
+ free(impl);
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .free = node_free,
+ .initialized = node_initialized,
+};
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_node0_resource_destroy,
+};
+
+static void convert_properties(struct pw_properties *properties)
+{
+ static const struct {
+ const char *from, *to;
+ } props[] = {
+ { "pipewire.autoconnect", PW_KEY_NODE_AUTOCONNECT, },
+ /* XXX deprecated */
+ { "pipewire.target.node", PW_KEY_NODE_TARGET, }
+ };
+
+ const char *str;
+
+ SPA_FOR_EACH_ELEMENT_VAR(props, p) {
+ if ((str = pw_properties_get(properties, p->from)) != NULL) {
+ pw_properties_set(properties, p->to, str);
+ pw_properties_set(properties, p->from, NULL);
+ }
+ }
+}
+
+/** Create a new client node
+ * \param client an owner \ref pw_client
+ * \param id an id
+ * \param name a name
+ * \param properties extra properties
+ * \return a newly allocated client node
+ *
+ * Create a new \ref pw_impl_node.
+ *
+ * \memberof pw_impl_client_node
+ */
+struct pw_impl_client_node0 *pw_impl_client_node0_new(struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ struct pw_impl_client_node0 *this;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct pw_context *context = pw_impl_client_get_context(client);
+ const struct spa_support *support;
+ uint32_t n_support;
+ const char *name;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return NULL;
+
+ this = &impl->this;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_exit_free;
+ }
+ convert_properties(properties);
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d", client->global->id);
+
+ impl->context = context;
+ impl->fds[0] = impl->fds[1] = -1;
+ pw_log_debug("client-node %p: new", impl);
+
+ support = pw_context_get_support(impl->context, &n_support);
+
+ node_init(&impl->node, NULL, support, n_support);
+ impl->node.impl = impl;
+
+ pw_array_init(&impl->mems, 64);
+
+ if ((name = pw_properties_get(properties, "node.name")) == NULL)
+ name = "client-node";
+ pw_properties_set(properties, PW_KEY_MEDIA_TYPE, "Video");
+
+ impl->node.resource = resource;
+ this->resource = resource;
+ this->node = pw_spa_node_new(context,
+ PW_SPA_NODE_FLAG_ASYNC,
+ &impl->node.node,
+ NULL,
+ properties, 0);
+ if (this->node == NULL) {
+ res = -errno;
+ goto error_no_node;
+ }
+
+ impl->client_reuse = pw_properties_get_bool(properties, "pipewire.client.reuse", false);
+
+ pw_resource_add_listener(this->resource,
+ &impl->resource_listener,
+ &resource_events,
+ impl);
+ pw_resource_add_object_listener(this->resource,
+ &impl->object_listener,
+ &client_node0_methods,
+ impl);
+
+
+ pw_impl_node_add_listener(this->node, &impl->node_listener, &node_events, impl);
+
+ return this;
+
+error_no_node:
+ pw_resource_destroy(this->resource);
+ node_clear(&impl->node);
+error_exit_free:
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+/** Destroy a client node
+ * \param node the client node to destroy
+ * \memberof pw_impl_client_node
+ */
+void pw_impl_client_node0_destroy(struct pw_impl_client_node0 *node)
+{
+ pw_resource_destroy(node->resource);
+}
diff --git a/src/modules/module-client-node/v0/client-node.h b/src/modules/module-client-node/v0/client-node.h
new file mode 100644
index 0000000..04b77d4
--- /dev/null
+++ b/src/modules/module-client-node/v0/client-node.h
@@ -0,0 +1,101 @@
+/* PipeWire
+ *
+ * Copyright © 2015 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CLIENT_NODE0_H
+#define PIPEWIRE_CLIENT_NODE0_H
+
+#include <pipewire/impl.h>
+
+#include "ext-client-node.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** The state of the clock */
+enum spa_clock0_state {
+ SPA_CLOCK0_STATE_STOPPED, /*< the clock is stopped */
+ SPA_CLOCK0_STATE_PAUSED, /*< the clock is paused */
+ SPA_CLOCK0_STATE_RUNNING, /*< the clock is running */
+};
+
+struct spa_command_node0_clock_update_body {
+ struct spa_pod_object_body body;
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_TIME (1 << 0)
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_SCALE (1 << 1)
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_STATE (1 << 2)
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_LATENCY (1 << 3)
+ struct spa_pod_int change_mask SPA_ALIGNED(8);
+ struct spa_pod_int rate SPA_ALIGNED(8);
+ struct spa_pod_long ticks SPA_ALIGNED(8);
+ struct spa_pod_long monotonic_time SPA_ALIGNED(8);
+ struct spa_pod_long offset SPA_ALIGNED(8);
+ struct spa_pod_int scale SPA_ALIGNED(8);
+ struct spa_pod_int state SPA_ALIGNED(8);
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_FLAG_LIVE (1 << 0)
+ struct spa_pod_int flags SPA_ALIGNED(8);
+ struct spa_pod_long latency SPA_ALIGNED(8);
+};
+
+struct spa_command_node0_clock_update {
+ struct spa_pod pod;
+ struct spa_command_node0_clock_update_body body;
+};
+
+#define SPA_COMMAND_NODE0_CLOCK_UPDATE_INIT(type,change_mask,rate,ticks,monotonic_time,offset,scale,state,flags,latency) \
+ SPA_COMMAND_INIT_FULL(struct spa_command_node0_clock_update, \
+ sizeof(struct spa_command_node0_clock_update_body), 0, type, \
+ SPA_POD_INIT_Int(change_mask), \
+ SPA_POD_INIT_Int(rate), \
+ SPA_POD_INIT_Long(ticks), \
+ SPA_POD_INIT_Long(monotonic_time), \
+ SPA_POD_INIT_Long(offset), \
+ SPA_POD_INIT_Int(scale), \
+ SPA_POD_INIT_Int(state), \
+ SPA_POD_INIT_Int(flags), \
+ SPA_POD_INIT_Long(latency))
+
+
+/** \class pw_impl_client_node0
+ *
+ * PipeWire client node interface
+ */
+struct pw_impl_client_node0 {
+ struct pw_impl_node *node;
+
+ struct pw_resource *resource;
+};
+
+struct pw_impl_client_node0 *
+pw_impl_client_node0_new(struct pw_resource *resource,
+ struct pw_properties *properties);
+
+void
+pw_impl_client_node0_destroy(struct pw_impl_client_node0 *node);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CLIENT_NODE0_H */
diff --git a/src/modules/module-client-node/v0/ext-client-node.h b/src/modules/module-client-node/v0/ext-client-node.h
new file mode 100644
index 0000000..21ba597
--- /dev/null
+++ b/src/modules/module-client-node/v0/ext-client-node.h
@@ -0,0 +1,414 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __PIPEWIRE_EXT_CLIENT_NODE0_H__
+#define __PIPEWIRE_EXT_CLIENT_NODE0_H__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/param/param.h>
+#include <spa/node/node.h>
+
+#include <pipewire/proxy.h>
+
+#define PW_TYPE_INTERFACE_ClientNode PW_TYPE_INFO_INTERFACE_BASE "ClientNode"
+
+#define PW_VERSION_CLIENT_NODE0 0
+
+struct pw_client_node0_message;
+
+/** Shared structure between client and server \memberof pw_client_node */
+struct pw_client_node0_area {
+ uint32_t max_input_ports; /**< max input ports of the node */
+ uint32_t n_input_ports; /**< number of input ports of the node */
+ uint32_t max_output_ports; /**< max output ports of the node */
+ uint32_t n_output_ports; /**< number of output ports of the node */
+};
+
+/** \class pw_client_node0_transport
+ *
+ * \brief Transport object
+ *
+ * The transport object contains shared data and ringbuffers to exchange
+ * events and data between the server and the client in a low-latency and
+ * lockfree way.
+ */
+struct pw_client_node0_transport {
+ struct pw_client_node0_area *area; /**< the transport area */
+ struct spa_io_buffers *inputs; /**< array of buffer input io */
+ struct spa_io_buffers *outputs; /**< array of buffer output io */
+ void *input_data; /**< input memory for ringbuffer */
+ struct spa_ringbuffer *input_buffer; /**< ringbuffer for input memory */
+ void *output_data; /**< output memory for ringbuffer */
+ struct spa_ringbuffer *output_buffer; /**< ringbuffer for output memory */
+
+ /** Destroy a transport
+ * \param trans a transport to destroy
+ * \memberof pw_client_node0_transport
+ */
+ void (*destroy) (struct pw_client_node0_transport *trans);
+
+ /** Add a message to the transport
+ * \param trans the transport to send the message on
+ * \param message the message to add
+ * \return 0 on success, < 0 on error
+ *
+ * Write \a message to the shared ringbuffer.
+ */
+ int (*add_message) (struct pw_client_node0_transport *trans, struct pw_client_node0_message *message);
+
+ /** Get next message from a transport
+ * \param trans the transport to get the message of
+ * \param[out] message the message to read
+ * \return < 0 on error, 1 when a message is available,
+ * 0 when no more messages are available.
+ *
+ * Get the skeleton next message from \a trans into \a message. This function will
+ * only read the head and object body of the message.
+ *
+ * After the complete size of the message has been calculated, you should call
+ * \ref parse_message() to read the complete message contents.
+ */
+ int (*next_message) (struct pw_client_node0_transport *trans, struct pw_client_node0_message *message);
+
+ /** Parse the complete message on transport
+ * \param trans the transport to read from
+ * \param[out] message memory that can hold the complete message
+ * \return 0 on success, < 0 on error
+ *
+ * Use this function after \ref next_message().
+ */
+ int (*parse_message) (struct pw_client_node0_transport *trans, void *message);
+};
+
+#define pw_client_node0_transport_destroy(t) ((t)->destroy((t)))
+#define pw_client_node0_transport_add_message(t,m) ((t)->add_message((t), (m)))
+#define pw_client_node0_transport_next_message(t,m) ((t)->next_message((t), (m)))
+#define pw_client_node0_transport_parse_message(t,m) ((t)->parse_message((t), (m)))
+
+enum pw_client_node0_message_type {
+ PW_CLIENT_NODE0_MESSAGE_HAVE_OUTPUT, /*< signal that the node has output */
+ PW_CLIENT_NODE0_MESSAGE_NEED_INPUT, /*< signal that the node needs input */
+ PW_CLIENT_NODE0_MESSAGE_PROCESS_INPUT, /*< instruct the node to process input */
+ PW_CLIENT_NODE0_MESSAGE_PROCESS_OUTPUT, /*< instruct the node output is processed */
+ PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER, /*< reuse a buffer */
+};
+
+struct pw_client_node0_message_body {
+ struct spa_pod_int type SPA_ALIGNED(8); /*< one of enum pw_client_node0_message_type */
+};
+
+struct pw_client_node0_message {
+ struct spa_pod_struct pod;
+ struct pw_client_node0_message_body body;
+};
+
+struct pw_client_node0_message_port_reuse_buffer_body {
+ struct spa_pod_int type SPA_ALIGNED(8); /*< PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER */
+ struct spa_pod_int port_id SPA_ALIGNED(8); /*< port id */
+ struct spa_pod_int buffer_id SPA_ALIGNED(8); /*< buffer id to reuse */
+};
+
+struct pw_client_node0_message_port_reuse_buffer {
+ struct spa_pod_struct pod;
+ struct pw_client_node0_message_port_reuse_buffer_body body;
+};
+
+#define PW_CLIENT_NODE0_MESSAGE_TYPE(message) (((struct pw_client_node0_message*)(message))->body.type.value)
+
+#define PW_CLIENT_NODE0_MESSAGE_INIT(message) ((struct pw_client_node0_message) \
+ { { { sizeof(struct pw_client_node0_message_body), SPA_TYPE_Struct } }, \
+ { SPA_POD_INIT_Int(message) } })
+
+#define PW_CLIENT_NODE0_MESSAGE_INIT_FULL(type,size,message,...) (type) \
+ { { { size, SPA_TYPE_Struct } }, \
+ { SPA_POD_INIT_Int(message), ##__VA_ARGS__ } } \
+
+#define PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER_INIT(port_id,buffer_id) \
+ PW_CLIENT_NODE0_MESSAGE_INIT_FULL(struct pw_client_node0_message_port_reuse_buffer, \
+ sizeof(struct pw_client_node0_message_port_reuse_buffer_body), \
+ PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER, \
+ SPA_POD_INIT_Int(port_id), \
+ SPA_POD_INIT_Int(buffer_id))
+
+/** information about a buffer */
+struct pw_client_node0_buffer {
+ uint32_t mem_id; /**< the memory id for the metadata */
+ uint32_t offset; /**< offset in memory */
+ uint32_t size; /**< size in memory */
+ struct spa_buffer *buffer; /**< buffer describing metadata and buffer memory */
+};
+
+#define PW_CLIENT_NODE0_METHOD_DONE 0
+#define PW_CLIENT_NODE0_METHOD_UPDATE 1
+#define PW_CLIENT_NODE0_METHOD_PORT_UPDATE 2
+#define PW_CLIENT_NODE0_METHOD_SET_ACTIVE 3
+#define PW_CLIENT_NODE0_METHOD_EVENT 4
+#define PW_CLIENT_NODE0_METHOD_DESTROY 5
+#define PW_CLIENT_NODE0_METHOD_NUM 6
+
+/** \ref pw_client_node methods */
+struct pw_client_node0_methods {
+#define PW_VERSION_CLIENT_NODE0_METHODS 0
+ uint32_t version;
+
+ /** Complete an async operation */
+ void (*done) (void *object, int seq, int res);
+
+ /**
+ * Update the node ports and properties
+ *
+ * Update the maximum number of ports and the params of the
+ * client node.
+ * \param change_mask bitfield with changed parameters
+ * \param max_input_ports new max input ports
+ * \param max_output_ports new max output ports
+ * \param params new params
+ */
+ void (*update) (void *object,
+#define PW_CLIENT_NODE0_UPDATE_MAX_INPUTS (1 << 0)
+#define PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS (1 << 1)
+#define PW_CLIENT_NODE0_UPDATE_PARAMS (1 << 2)
+ uint32_t change_mask,
+ uint32_t max_input_ports,
+ uint32_t max_output_ports,
+ uint32_t n_params,
+ const struct spa_pod **params);
+
+ /**
+ * Update a node port
+ *
+ * Update the information of one port of a node.
+ * \param direction the direction of the port
+ * \param port_id the port id to update
+ * \param change_mask a bitfield of changed items
+ * \param n_params number of port parameters
+ * \param params array of port parameters
+ * \param info port information
+ */
+ void (*port_update) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+#define PW_CLIENT_NODE0_PORT_UPDATE_PARAMS (1 << 0)
+#define PW_CLIENT_NODE0_PORT_UPDATE_INFO (1 << 1)
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info);
+ /**
+ * Activate or deactivate the node
+ */
+ void (*set_active) (void *object, bool active);
+ /**
+ * Send an event to the node
+ * \param event the event to send
+ */
+ void (*event) (void *object, struct spa_event *event);
+ /**
+ * Destroy the client_node
+ */
+ void (*destroy) (void *object);
+};
+
+#define PW_CLIENT_NODE0_EVENT_ADD_MEM 0
+#define PW_CLIENT_NODE0_EVENT_TRANSPORT 1
+#define PW_CLIENT_NODE0_EVENT_SET_PARAM 2
+#define PW_CLIENT_NODE0_EVENT_EVENT 3
+#define PW_CLIENT_NODE0_EVENT_COMMAND 4
+#define PW_CLIENT_NODE0_EVENT_ADD_PORT 5
+#define PW_CLIENT_NODE0_EVENT_REMOVE_PORT 6
+#define PW_CLIENT_NODE0_EVENT_PORT_SET_PARAM 7
+#define PW_CLIENT_NODE0_EVENT_PORT_USE_BUFFERS 8
+#define PW_CLIENT_NODE0_EVENT_PORT_COMMAND 9
+#define PW_CLIENT_NODE0_EVENT_PORT_SET_IO 10
+#define PW_CLIENT_NODE0_EVENT_NUM 11
+
+/** \ref pw_client_node events */
+struct pw_client_node0_events {
+#define PW_VERSION_CLIENT_NODE0_EVENTS 0
+ uint32_t version;
+ /**
+ * Memory was added to a node
+ *
+ * \param mem_id the id of the memory
+ * \param type the memory type
+ * \param memfd the fd of the memory
+ * \param flags flags for the \a memfd
+ */
+ void (*add_mem) (void *data,
+ uint32_t mem_id,
+ uint32_t type,
+ int memfd,
+ uint32_t flags);
+ /**
+ * Notify of a new transport area
+ *
+ * The transport area is used to exchange real-time commands between
+ * the client and the server.
+ *
+ * \param node_id the node id created for this client node
+ * \param readfd fd for signal data can be read
+ * \param writefd fd for signal data can be written
+ * \param transport the shared transport area
+ */
+ void (*transport) (void *data,
+ uint32_t node_id,
+ int readfd,
+ int writefd,
+ struct pw_client_node0_transport *transport);
+ /**
+ * Notify of a property change
+ *
+ * When the server configures the properties on the node
+ * this event is sent
+ *
+ * \param seq a sequence number
+ * \param id the id of the parameter
+ * \param flags parameter flags
+ * \param param the param to set
+ */
+ void (*set_param) (void *data, uint32_t seq,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ /**
+ * Receive an event from the client node
+ * \param event the received event */
+ void (*event) (void *data, const struct spa_event *event);
+ /**
+ * Notify of a new node command
+ *
+ * \param seq a sequence number
+ * \param command the command
+ */
+ void (*command) (void *data, uint32_t seq, const struct spa_command *command);
+ /**
+ * A new port was added to the node
+ *
+ * The server can at any time add a port to the node when there
+ * are free ports available.
+ *
+ * \param seq a sequence number
+ * \param direction the direction of the port
+ * \param port_id the new port id
+ */
+ void (*add_port) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id);
+ /**
+ * A port was removed from the node
+ *
+ * \param seq a sequence number
+ * \param direction a port direction
+ * \param port_id the remove port id
+ */
+ void (*remove_port) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id);
+ /**
+ * A parameter was configured on the port
+ *
+ * \param seq a sequence number
+ * \param direction a port direction
+ * \param port_id the port id
+ * \param id the id of the parameter
+ * \param flags flags used when setting the param
+ * \param param the new param
+ */
+ void (*port_set_param) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ /**
+ * Notify the port of buffers
+ *
+ * \param seq a sequence number
+ * \param direction a port direction
+ * \param port_id the port id
+ * \param n_buffer the number of buffers
+ * \param buffers and array of buffer descriptions
+ */
+ void (*port_use_buffers) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t n_buffers,
+ struct pw_client_node0_buffer *buffers);
+ /**
+ * Notify of a new port command
+ *
+ * \param direction a port direction
+ * \param port_id the port id
+ * \param command the command
+ */
+ void (*port_command) (void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ const struct spa_command *command);
+
+ /**
+ * Configure the io area with \a id of \a port_id.
+ *
+ * \param seq a sequence number
+ * \param direction the direction of the port
+ * \param port_id the port id
+ * \param id the id of the io area to set
+ * \param mem_id the id of the memory to use
+ * \param offset offset of io area in memory
+ * \param size size of the io area
+ */
+ void (*port_set_io) (void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size);
+};
+#define pw_client_node0_resource(r,m,v,...) pw_resource_call(r, struct pw_client_node0_events, m, v, ##__VA_ARGS__)
+
+#define pw_client_node0_resource_add_mem(r,...) pw_client_node0_resource(r,add_mem,0,__VA_ARGS__)
+#define pw_client_node0_resource_transport(r,...) pw_client_node0_resource(r,transport,0,__VA_ARGS__)
+#define pw_client_node0_resource_set_param(r,...) pw_client_node0_resource(r,set_param,0,__VA_ARGS__)
+#define pw_client_node0_resource_event(r,...) pw_client_node0_resource(r,event,0,__VA_ARGS__)
+#define pw_client_node0_resource_command(r,...) pw_client_node0_resource(r,command,0,__VA_ARGS__)
+#define pw_client_node0_resource_add_port(r,...) pw_client_node0_resource(r,add_port,0,__VA_ARGS__)
+#define pw_client_node0_resource_remove_port(r,...) pw_client_node0_resource(r,remove_port,0,__VA_ARGS__)
+#define pw_client_node0_resource_port_set_param(r,...) pw_client_node0_resource(r,port_set_param,0,__VA_ARGS__)
+#define pw_client_node0_resource_port_use_buffers(r,...) pw_client_node0_resource(r,port_use_buffers,0,__VA_ARGS__)
+#define pw_client_node0_resource_port_command(r,...) pw_client_node0_resource(r,port_command,0,__VA_ARGS__)
+#define pw_client_node0_resource_port_set_io(r,...) pw_client_node0_resource(r,port_set_io,0,__VA_ARGS__)
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* __PIPEWIRE_EXT_CLIENT_NODE0_H__ */
diff --git a/src/modules/module-client-node/v0/protocol-native.c b/src/modules/module-client-node/v0/protocol-native.c
new file mode 100644
index 0000000..86d8fe9
--- /dev/null
+++ b/src/modules/module-client-node/v0/protocol-native.c
@@ -0,0 +1,534 @@
+/* PipeWire
+ *
+ * Copyright © 2017 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/parser.h>
+#include <spa/pod/builder.h>
+#include <spa/utils/type-info.h>
+
+#include "pipewire/impl.h"
+
+#include "pipewire/extensions/protocol-native.h"
+
+#include "ext-client-node.h"
+
+#include "transport.h"
+
+#define PW_PROTOCOL_NATIVE_FLAG_REMAP (1<<0)
+
+extern uint32_t pw_protocol_native0_find_type(struct pw_impl_client *client, const char *type);
+extern int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod,
+ struct spa_pod_builder *b);
+extern struct spa_pod * pw_protocol_native0_pod_from_v2(struct pw_impl_client *client,
+ const struct spa_pod *pod);
+extern uint32_t pw_protocol_native0_type_to_v2(struct pw_impl_client *client,
+ const struct spa_type_info *info, uint32_t type);
+
+static void
+client_node_marshal_add_mem(void *data,
+ uint32_t mem_id,
+ uint32_t type,
+ int memfd, uint32_t flags)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ const char *typename;
+
+ switch (type) {
+ case SPA_DATA_MemFd:
+ typename = "Spa:Enum:DataType:Fd:MemFd";
+ break;
+ case SPA_DATA_DmaBuf:
+ typename = "Spa:Enum:DataType:Fd:DmaBuf";
+ break;
+ default:
+ case SPA_DATA_MemPtr:
+ return;
+
+ }
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_ADD_MEM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", mem_id,
+ "I", pw_protocol_native0_find_type(client, typename),
+ "i", pw_protocol_native_add_resource_fd(resource, memfd),
+ "i", flags);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void client_node_marshal_transport(void *data, uint32_t node_id, int readfd, int writefd,
+ struct pw_client_node0_transport *transport)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct pw_client_node0_transport_info info;
+
+ pw_client_node0_transport_get_info(transport, &info);
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_TRANSPORT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", node_id,
+ "i", pw_protocol_native_add_resource_fd(resource, readfd),
+ "i", pw_protocol_native_add_resource_fd(resource, writefd),
+ "i", pw_protocol_native_add_resource_fd(resource, info.memfd),
+ "i", info.offset,
+ "i", info.size);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_set_param(void *data, uint32_t seq, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", seq,
+ "I", id,
+ "i", flags,
+ "P", param);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void client_node_marshal_event_event(void *data, const struct spa_event *event)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_EVENT, NULL);
+
+ spa_pod_builder_add_struct(b, "P", event);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_command(void *data, uint32_t seq, const struct spa_command *command)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_COMMAND, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b, "i", seq, NULL);
+ if (SPA_COMMAND_TYPE(command) == 0)
+ spa_pod_builder_add(b, "P", command, NULL);
+ else
+ pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)command, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_add_port(void *data,
+ uint32_t seq, enum spa_direction direction, uint32_t port_id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_ADD_PORT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_remove_port(void *data,
+ uint32_t seq, enum spa_direction direction, uint32_t port_id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_REMOVE_PORT, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_port_set_param(void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ const char *typename;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_SET_PARAM, NULL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ typename = "Spa:Enum:ParamId:Props";
+ break;
+ case SPA_PARAM_Format:
+ typename = "Spa:Enum:ParamId:Format";
+ break;
+ default:
+ return;
+ }
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id,
+ "I", pw_protocol_native0_find_type(client, typename),
+ "i", flags, NULL);
+ pw_protocol_native0_pod_to_v2(client, param, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_port_use_buffers(void *data,
+ uint32_t seq,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t n_buffers, struct pw_client_node0_buffer *buffers)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, j;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_USE_BUFFERS, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id,
+ "i", n_buffers, NULL);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct spa_buffer *buf = buffers[i].buffer;
+
+ spa_pod_builder_add(b,
+ "i", buffers[i].mem_id,
+ "i", buffers[i].offset,
+ "i", buffers[i].size,
+ "i", i,
+ "i", buf->n_metas, NULL);
+
+ for (j = 0; j < buf->n_metas; j++) {
+ struct spa_meta *m = &buf->metas[j];
+ spa_pod_builder_add(b,
+ "I", pw_protocol_native0_type_to_v2(client, spa_type_meta_type, m->type),
+ "i", m->size, NULL);
+ }
+ spa_pod_builder_add(b, "i", buf->n_datas, NULL);
+ for (j = 0; j < buf->n_datas; j++) {
+ struct spa_data *d = &buf->datas[j];
+ spa_pod_builder_add(b,
+ "I", pw_protocol_native0_type_to_v2(client, spa_type_data_type, d->type),
+ "i", SPA_PTR_TO_UINT32(d->data),
+ "i", d->flags,
+ "i", d->mapoffset,
+ "i", d->maxsize, NULL);
+ }
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_port_command(void *data,
+ uint32_t direction,
+ uint32_t port_id,
+ const struct spa_command *command)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_COMMAND, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", direction,
+ "i", port_id, NULL);
+ pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)command, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void
+client_node_marshal_port_set_io(void *data,
+ uint32_t seq,
+ uint32_t direction,
+ uint32_t port_id,
+ uint32_t id,
+ uint32_t memid,
+ uint32_t offset,
+ uint32_t size)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_SET_IO, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", seq,
+ "i", direction,
+ "i", port_id,
+ "I", id,
+ "i", memid,
+ "i", offset,
+ "i", size);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+
+static int client_node_demarshal_done(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t seq, res;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "i", &seq,
+ "i", &res) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_node0_methods, done, 0, seq, res);
+}
+
+static int client_node_demarshal_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t change_mask, max_input_ports, max_output_ports, n_params;
+ const struct spa_pod **params;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ "i", &change_mask,
+ "i", &max_input_ports,
+ "i", &max_output_ports,
+ "i", &n_params, NULL) < 0)
+ return -EINVAL;
+
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs, "O", &params[i], NULL) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_node0_methods, update, 0, change_mask,
+ max_input_ports,
+ max_output_ports,
+ n_params,
+ params);
+}
+
+static int client_node_demarshal_port_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t i, direction, port_id, change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_port_info info = { 0 }, *infop = NULL;
+ struct spa_dict props;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ "i", &direction,
+ "i", &port_id,
+ "i", &change_mask,
+ "i", &n_params, NULL) < 0)
+ return -EINVAL;
+
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs, "O", &params[i], NULL) < 0)
+ return -EINVAL;
+
+
+ if (spa_pod_parser_push_struct(&prs, &f[1]) >= 0) {
+ infop = &info;
+
+ if (spa_pod_parser_get(&prs,
+ "i", &info.flags,
+ "i", &info.rate,
+ "i", &props.n_items, NULL) < 0)
+ return -EINVAL;
+
+ if (props.n_items > 0) {
+ info.props = &props;
+
+ props.items = alloca(props.n_items * sizeof(struct spa_dict_item));
+ for (i = 0; i < props.n_items; i++) {
+ if (spa_pod_parser_get(&prs,
+ "s", &props.items[i].key,
+ "s", &props.items[i].value,
+ NULL) < 0)
+ return -EINVAL;
+ }
+ }
+ }
+
+ return pw_resource_notify(resource, struct pw_client_node0_methods, port_update, 0, direction,
+ port_id,
+ change_mask,
+ n_params,
+ params, infop);
+}
+
+static int client_node_demarshal_set_active(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int active;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "b", &active) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_node0_methods, set_active, 0, active);
+}
+
+static int client_node_demarshal_event_method(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ struct spa_event *event;
+ int res;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "O", &event) < 0)
+ return -EINVAL;
+
+ event = (struct spa_event*)pw_protocol_native0_pod_from_v2(client, (struct spa_pod *)event);
+
+ res = pw_resource_notify(resource, struct pw_client_node0_methods, event, 0, event);
+ free(event);
+
+ return res;
+}
+
+static int client_node_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int res;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, NULL) < 0)
+ return -EINVAL;
+
+ res = pw_resource_notify(resource, struct pw_client_node0_methods, destroy, 0);
+ pw_resource_destroy(resource);
+ return res;
+}
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_client_node_method_demarshal[] = {
+ { &client_node_demarshal_done, 0, 0 },
+ { &client_node_demarshal_update, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP },
+ { &client_node_demarshal_port_update, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP },
+ { &client_node_demarshal_set_active, 0, 0 },
+ { &client_node_demarshal_event_method, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP },
+ { &client_node_demarshal_destroy, 0, 0 },
+};
+
+static const struct pw_client_node0_events pw_protocol_native_client_node_event_marshal = {
+ PW_VERSION_CLIENT_NODE0_EVENTS,
+ &client_node_marshal_add_mem,
+ &client_node_marshal_transport,
+ &client_node_marshal_set_param,
+ &client_node_marshal_event_event,
+ &client_node_marshal_command,
+ &client_node_marshal_add_port,
+ &client_node_marshal_remove_port,
+ &client_node_marshal_port_set_param,
+ &client_node_marshal_port_use_buffers,
+ &client_node_marshal_port_command,
+ &client_node_marshal_port_set_io,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_node_marshal = {
+ PW_TYPE_INTERFACE_ClientNode,
+ PW_VERSION_CLIENT_NODE0,
+ PW_CLIENT_NODE0_METHOD_NUM,
+ PW_CLIENT_NODE0_EVENT_NUM,
+ 0,
+ NULL,
+ .server_demarshal = &pw_protocol_native_client_node_method_demarshal,
+ .server_marshal = &pw_protocol_native_client_node_event_marshal,
+ NULL,
+};
+
+struct pw_protocol *pw_protocol_native_ext_client_node0_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+
+ if (protocol == NULL)
+ return NULL;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_node_marshal);
+
+ return protocol;
+}
diff --git a/src/modules/module-client-node/v0/transport.c b/src/modules/module-client-node/v0/transport.c
new file mode 100644
index 0000000..fa84795
--- /dev/null
+++ b/src/modules/module-client-node/v0/transport.c
@@ -0,0 +1,262 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <errno.h>
+#include <sys/mman.h>
+
+#include <spa/utils/ringbuffer.h>
+#include <spa/node/io.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/private.h>
+
+#include "ext-client-node.h"
+
+#include "transport.h"
+
+/** \cond */
+
+#define INPUT_BUFFER_SIZE (1<<12)
+#define OUTPUT_BUFFER_SIZE (1<<12)
+
+struct transport {
+ struct pw_client_node0_transport trans;
+
+ struct pw_memblock *mem;
+ size_t offset;
+
+ struct pw_client_node0_message current;
+ uint32_t current_index;
+};
+/** \endcond */
+
+static size_t area_get_size(struct pw_client_node0_area *area)
+{
+ size_t size;
+ size = sizeof(struct pw_client_node0_area);
+ size += area->max_input_ports * sizeof(struct spa_io_buffers);
+ size += area->max_output_ports * sizeof(struct spa_io_buffers);
+ size += sizeof(struct spa_ringbuffer);
+ size += INPUT_BUFFER_SIZE;
+ size += sizeof(struct spa_ringbuffer);
+ size += OUTPUT_BUFFER_SIZE;
+ return size;
+}
+
+static void transport_setup_area(void *p, struct pw_client_node0_transport *trans)
+{
+ struct pw_client_node0_area *a;
+
+ trans->area = a = p;
+ p = SPA_PTROFF(p, sizeof(struct pw_client_node0_area), struct spa_io_buffers);
+
+ trans->inputs = p;
+ p = SPA_PTROFF(p, a->max_input_ports * sizeof(struct spa_io_buffers), void);
+
+ trans->outputs = p;
+ p = SPA_PTROFF(p, a->max_output_ports * sizeof(struct spa_io_buffers), void);
+
+ trans->input_buffer = p;
+ p = SPA_PTROFF(p, sizeof(struct spa_ringbuffer), void);
+
+ trans->input_data = p;
+ p = SPA_PTROFF(p, INPUT_BUFFER_SIZE, void);
+
+ trans->output_buffer = p;
+ p = SPA_PTROFF(p, sizeof(struct spa_ringbuffer), void);
+
+ trans->output_data = p;
+ p = SPA_PTROFF(p, OUTPUT_BUFFER_SIZE, void);
+}
+
+static void transport_reset_area(struct pw_client_node0_transport *trans)
+{
+ uint32_t i;
+ struct pw_client_node0_area *a = trans->area;
+
+ for (i = 0; i < a->max_input_ports; i++) {
+ trans->inputs[i] = SPA_IO_BUFFERS_INIT;
+ }
+ for (i = 0; i < a->max_output_ports; i++) {
+ trans->outputs[i] = SPA_IO_BUFFERS_INIT;
+ }
+ spa_ringbuffer_init(trans->input_buffer);
+ spa_ringbuffer_init(trans->output_buffer);
+}
+
+static void destroy(struct pw_client_node0_transport *trans)
+{
+ struct transport *impl = (struct transport *) trans;
+
+ pw_log_debug("transport %p: destroy", trans);
+
+ pw_memblock_free(impl->mem);
+ free(impl);
+}
+
+static int add_message(struct pw_client_node0_transport *trans, struct pw_client_node0_message *message)
+{
+ struct transport *impl = (struct transport *) trans;
+ int32_t filled, avail;
+ uint32_t size, index;
+
+ if (impl == NULL || message == NULL)
+ return -EINVAL;
+
+ filled = spa_ringbuffer_get_write_index(trans->output_buffer, &index);
+ avail = OUTPUT_BUFFER_SIZE - filled;
+ size = SPA_POD_SIZE(message);
+ if (avail < (int)size)
+ return -ENOSPC;
+
+ spa_ringbuffer_write_data(trans->output_buffer,
+ trans->output_data, OUTPUT_BUFFER_SIZE,
+ index & (OUTPUT_BUFFER_SIZE - 1), message, size);
+ spa_ringbuffer_write_update(trans->output_buffer, index + size);
+
+ return 0;
+}
+
+static int next_message(struct pw_client_node0_transport *trans, struct pw_client_node0_message *message)
+{
+ struct transport *impl = (struct transport *) trans;
+ int32_t avail;
+
+ if (impl == NULL || message == NULL)
+ return -EINVAL;
+
+ avail = spa_ringbuffer_get_read_index(trans->input_buffer, &impl->current_index);
+ if (avail < (int) sizeof(struct pw_client_node0_message))
+ return 0;
+
+ spa_ringbuffer_read_data(trans->input_buffer,
+ trans->input_data, INPUT_BUFFER_SIZE,
+ impl->current_index & (INPUT_BUFFER_SIZE - 1),
+ &impl->current, sizeof(struct pw_client_node0_message));
+
+ if (avail < (int) SPA_POD_SIZE(&impl->current))
+ return 0;
+
+ *message = impl->current;
+
+ return 1;
+}
+
+static int parse_message(struct pw_client_node0_transport *trans, void *message)
+{
+ struct transport *impl = (struct transport *) trans;
+ uint32_t size;
+
+ if (impl == NULL || message == NULL)
+ return -EINVAL;
+
+ size = SPA_POD_SIZE(&impl->current);
+
+ spa_ringbuffer_read_data(trans->input_buffer,
+ trans->input_data, INPUT_BUFFER_SIZE,
+ impl->current_index & (INPUT_BUFFER_SIZE - 1), message, size);
+ spa_ringbuffer_read_update(trans->input_buffer, impl->current_index + size);
+
+ return 0;
+}
+
+/** Create a new transport
+ * \param max_input_ports maximum number of input_ports
+ * \param max_output_ports maximum number of output_ports
+ * \return a newly allocated \ref pw_client_node0_transport
+ * \memberof pw_client_node0_transport
+ */
+struct pw_client_node0_transport *
+pw_client_node0_transport_new(struct pw_context *context,
+ uint32_t max_input_ports, uint32_t max_output_ports)
+{
+ struct transport *impl;
+ struct pw_client_node0_transport *trans;
+ struct pw_client_node0_area area = { 0 };
+
+ area.max_input_ports = max_input_ports;
+ area.n_input_ports = 0;
+ area.max_output_ports = max_output_ports;
+ area.n_output_ports = 0;
+
+ impl = calloc(1, sizeof(struct transport));
+ if (impl == NULL)
+ return NULL;
+
+ pw_log_debug("transport %p: new %d %d", impl, max_input_ports, max_output_ports);
+
+ trans = &impl->trans;
+ impl->offset = 0;
+
+ impl->mem = pw_mempool_alloc(context->pool,
+ PW_MEMBLOCK_FLAG_READWRITE |
+ PW_MEMBLOCK_FLAG_MAP |
+ PW_MEMBLOCK_FLAG_SEAL,
+ SPA_DATA_MemFd, area_get_size(&area));
+ if (impl->mem == NULL) {
+ free(impl);
+ return NULL;
+ }
+
+ memcpy(impl->mem->map->ptr, &area, sizeof(struct pw_client_node0_area));
+ transport_setup_area(impl->mem->map->ptr, trans);
+ transport_reset_area(trans);
+
+ trans->destroy = destroy;
+ trans->add_message = add_message;
+ trans->next_message = next_message;
+ trans->parse_message = parse_message;
+
+ return trans;
+}
+
+struct pw_client_node0_transport *
+pw_client_node0_transport_new_from_info(struct pw_client_node0_transport_info *info)
+{
+ errno = ENOTSUP;
+ return NULL;
+}
+
+/** Get transport info
+ * \param trans the transport to get info of
+ * \param[out] info transport info
+ * \return 0 on success
+ *
+ * Fill \a info with the transport info of \a trans. This information can be
+ * passed to the client to set up the shared transport.
+ *
+ * \memberof pw_client_node0_transport
+ */
+int pw_client_node0_transport_get_info(struct pw_client_node0_transport *trans,
+ struct pw_client_node0_transport_info *info)
+{
+ struct transport *impl = (struct transport *) trans;
+
+ info->memfd = impl->mem->fd;
+ info->offset = impl->offset;
+ info->size = impl->mem->size;
+
+ return 0;
+}
diff --git a/src/modules/module-client-node/v0/transport.h b/src/modules/module-client-node/v0/transport.h
new file mode 100644
index 0000000..3abcd6d
--- /dev/null
+++ b/src/modules/module-client-node/v0/transport.h
@@ -0,0 +1,59 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__
+#define __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <string.h>
+
+#include <spa/utils/defs.h>
+
+#include <pipewire/mem.h>
+
+/** information about the transport region \memberof pw_client_node */
+struct pw_client_node0_transport_info {
+ int memfd; /**< the memfd of the transport area */
+ uint32_t offset; /**< offset to map \a memfd at */
+ uint32_t size; /**< size of memfd mapping */
+};
+
+struct pw_client_node0_transport *
+pw_client_node0_transport_new(struct pw_context *context, uint32_t max_input_ports, uint32_t max_output_ports);
+
+struct pw_client_node0_transport *
+pw_client_node0_transport_new_from_info(struct pw_client_node0_transport_info *info);
+
+int
+pw_client_node0_transport_get_info(struct pw_client_node0_transport *trans,
+ struct pw_client_node0_transport_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__ */
diff --git a/src/modules/module-combine-stream.c b/src/modules/module-combine-stream.c
new file mode 100644
index 0000000..88b5644
--- /dev/null
+++ b/src/modules/module-combine-stream.c
@@ -0,0 +1,1063 @@
+/* PipeWire
+ *
+ * Copyright © 2023 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.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/utils/ringbuffer.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_combine_stream PipeWire Module: Combine Stream
+ *
+ * The combine stream can make:
+ *
+ * - a new virtual sink that forwards audio to other sinks
+ * - a new virtual source that combines audio from other sources
+ *
+ * ## Module Options
+ *
+ * - `node.name`: a unique name for the stream
+ * - `node.description`: a human readable name for the stream
+ * - `combine.mode` = capture | playback | sink | source, default sink
+ * - `combine.props = {}`: properties to be passed to the sink/source
+ * - `stream.props = {}`: properties to be passed to the streams
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Stream options
+ *
+ * - `audio.position`: Set the stream channel map. By default this is the same channel
+ * map as the combine stream.
+ * - `combine.audio.position`: map the combine audio positions to the stream positions.
+ * combine input channels are mapped one-by-one to stream output channels.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-combine-stream
+ * args = {
+ * combine.mode = sink
+ * node.name = "combine_sink"
+ * node.description = "My Combine Sink"
+ * combine.props = {
+ * audio.position = [ FL FR ]
+ * }
+ * stream.props = {
+ * }
+ * stream.rules = [
+ * {
+ * matches = [
+ * # any of the items in matches needs to match, if one does,
+ * # actions are emited.
+ * {
+ * # all keys must match the value. ~ in value starts regex.
+ * #node.name = "~alsa_input.*"
+ * media.class = "Audio/Sink"
+ * }
+ * ]
+ * actions = {
+ * create-stream = {
+ * #combine.audio.position = [ FL FR ]
+ * #audio.position = [ FL FR ]
+ * }
+ * }
+ * }
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * Below is an example configuration that makes a 5.1 virtual audio sink
+ * from 3 separate stereo sinks.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-combine-stream
+ * args = {
+ * combine.mode = sink
+ * node.name = "combine_sink_5_1"
+ * node.description = "My 5.1 Combine Sink"
+ * combine.props = {
+ * audio.position = [ FL FR FC LFE SL SR ]
+ * }
+ * stream.props = {
+ * stream.dont-remix = true # link matching channels without remixing
+ * }
+ * stream.rules = [
+ * { matches = [
+ * { media.class = "Audio/Sink"
+ * node.name = "alsa_output.usb-Topping_E30-00.analog-stereo"
+ * } ]
+ * actions = { create-stream = {
+ * combine.audio.position = [ FL FR ]
+ * audio.position = [ FL FR ]
+ * } } }
+ * { matches = [
+ * { media.class = "Audio/Sink"
+ * node.name = "alsa_output.usb-BEHRINGER_UMC404HD_192k-00.pro-output-0"
+ * } ]
+ * actions = { create-stream = {
+ * combine.audio.position = [ FC LFE ]
+ * audio.position = [ AUX0 AUX1 ]
+ * } } }
+ * { matches = [
+ * { media.class = "Audio/Sink"
+ * node.name = "alsa_output.pci-0000_00_1b.0.analog-stereo"
+ * } ]
+ * actions = { create-stream = {
+ * combine.audio.position = [ SL SR ]
+ * audio.position = [ FL FR ]
+ * } } }
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * Below is an example configuration that makes a 4.0 virtual audio source
+ * from 2 separate stereo sources.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-combine-stream
+ * args = {
+ * combine.mode = source
+ * node.name = "combine_source_4_0"
+ * node.description = "My 4.0 Combine Source"
+ * combine.props = {
+ * audio.position = [ FL FR SL SR ]
+ * }
+ * stream.props = {
+ * stream.dont-remix = true
+ * }
+ * stream.rules = [
+ * { matches = [
+ * { media.class = "Audio/Source"
+ * node.name = "alsa_input.usb-046d_HD_Pro_Webcam_C920_09D53E1F-02.analog-stereo"
+ * } ]
+ * actions = { create-stream = {
+ * audio.position = [ FL FR ]
+ * combine.audio.position = [ FL FR ]
+ * } } }
+ * { matches = [
+ * { media.class = "Audio/Source"
+ * node.name = "alsa_input.usb-046d_0821_9534DE90-00.analog-stereo"
+ * } ]
+ * actions = { create-stream = {
+ * audio.position = [ FL FR ]
+ * combine.audio.position = [ SL SR ]
+ * } } }
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "combine-stream"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define MODULE_USAGE "[ node.latency=<latency as fraction> ] " \
+ "[ combine.mode=<mode of stream, playback|capture|sink|source>, default:sink ] " \
+ "[ node.name=<name of the stream> ] " \
+ "[ node.description=<description of the stream> ] " \
+ "[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ] " \
+ "[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] " \
+ "[ combine.props=<properties> ] " \
+ "[ stream.props=<properties> ] " \
+ "[ stream.rules=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Combine multiple streams into a single stream" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+ struct pw_data_loop *data_loop;
+
+ struct pw_properties *props;
+
+#define MODE_SINK 0
+#define MODE_SOURCE 1
+#define MODE_CAPTURE 2
+#define MODE_PLAYBACK 3
+ uint32_t mode;
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_properties *combine_props;
+ struct pw_stream *combine;
+ struct spa_hook combine_listener;
+ struct pw_stream_events combine_events;
+ uint32_t combine_id;
+
+ struct pw_properties *stream_props;
+
+ struct spa_audio_info_raw info;
+
+ unsigned int do_disconnect:1;
+
+ struct spa_list streams;
+ uint32_t n_streams;
+};
+
+struct stream {
+ uint32_t id;
+
+ struct impl *impl;
+
+ struct spa_list link;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct pw_stream_events stream_events;
+
+ struct spa_audio_info_raw info;
+ uint32_t remap[SPA_AUDIO_MAX_CHANNELS];
+
+ unsigned int ready:1;
+ unsigned int added: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));
+}
+
+static struct stream *find_stream(struct impl *impl, uint32_t id)
+{
+ struct stream *s;
+ spa_list_for_each(s, &impl->streams, link)
+ if (s->id == id)
+ return s;
+ return NULL;
+}
+
+static int do_add_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;
+ if (!s->added) {
+ spa_list_append(&impl->streams, &s->link);
+ impl->n_streams++;
+ s->added = true;
+ }
+ return 0;
+}
+
+static int do_remove_stream(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *data, size_t size, void *user_data)
+{
+ struct stream *s = user_data;
+ if (s->added) {
+ spa_list_remove(&s->link);
+ s->impl->n_streams--;
+ s->added = false;
+ }
+ return 0;
+}
+
+static void destroy_stream(struct stream *s)
+{
+ pw_log_debug("destroy stream %d", s->id);
+
+ pw_data_loop_invoke(s->impl->data_loop, do_remove_stream, 0, NULL, 0, true, s);
+
+ if (s->stream) {
+ spa_hook_remove(&s->stream_listener);
+ pw_stream_destroy(s->stream);
+ }
+ free(s);
+}
+
+static void stream_destroy(void *d)
+{
+ struct stream *s = d;
+ spa_hook_remove(&s->stream_listener);
+ s->stream = NULL;
+ destroy_stream(s);
+}
+
+static void stream_input_process(void *d)
+{
+ struct stream *s = d, *t;
+ struct impl *impl = s->impl;
+ bool ready = true;
+
+ s->ready = true;
+ pw_log_debug("stream ready %p", s);
+ spa_list_for_each(t, &impl->streams, link) {
+ if (!t->ready) {
+ ready = false;
+ break;
+ }
+ }
+ if (ready) {
+ pw_log_debug("do trigger");
+ pw_stream_trigger_process(impl->combine);
+ }
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct stream *s = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ stream_destroy(s);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+};
+
+struct stream_info {
+ struct impl *impl;
+ uint32_t id;
+ const struct spa_dict *props;
+ struct pw_properties *stream_props;
+};
+
+static int create_stream(struct stream_info *info)
+{
+ struct impl *impl = info->impl;
+ int res;
+ uint32_t n_params, i, j;
+ const struct spa_pod *params[1];
+ const char *str, *node_name;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ struct spa_audio_info_raw remap_info, tmp_info;
+ struct stream *s;
+ enum pw_stream_flags flags;
+ enum pw_direction direction;
+
+ node_name = spa_dict_lookup(info->props, "node.name");
+ if (node_name == NULL)
+ node_name = spa_dict_lookup(info->props, "object.serial");
+ if (node_name == NULL)
+ return -EIO;
+
+ pw_log_info("create stream for %d %s", info->id, node_name);
+
+ s = calloc(1, sizeof(*s));
+ if (s == NULL)
+ goto error_errno;
+
+ s->id = info->id;
+ s->impl = impl;
+
+ s->info = impl->info;
+ if ((str = pw_properties_get(info->stream_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&s->info, str, strlen(str));
+ 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));
+ if (remap_info.channels == 0)
+ remap_info = s->info;
+
+ tmp_info = impl->info;
+ for (i = 0; i < remap_info.channels; i++) {
+ s->remap[i] = i;
+ for (j = 0; j < tmp_info.channels; j++) {
+ if (tmp_info.position[j] == remap_info.position[i]) {
+ s->remap[i] = j;
+ break;
+ }
+ }
+ pw_log_info("remap %d -> %d", i, s->remap[i]);
+ }
+
+ str = pw_properties_get(impl->props, PW_KEY_NODE_DESCRIPTION);
+ if (str == NULL)
+ str = pw_properties_get(impl->props, PW_KEY_NODE_NAME);
+ if (str == NULL)
+ str = node_name;
+
+ 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);
+ 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);
+
+ str = pw_properties_get(impl->props, PW_KEY_NODE_NAME);
+ if (str == NULL)
+ str = "combine_stream";
+
+ 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);
+ if (pw_properties_get(info->stream_props, PW_KEY_TARGET_OBJECT) == NULL)
+ pw_properties_set(info->stream_props, PW_KEY_TARGET_OBJECT, node_name);
+
+ s->stream = pw_stream_new(impl->core, "Combine stream", info->stream_props);
+ info->stream_props = NULL;
+ 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;
+
+ 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);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &s->info);
+
+ if ((res = pw_stream_connect(s->stream,
+ direction, PW_ID_ANY, flags, params, n_params)) < 0)
+ goto error;
+
+ pw_data_loop_invoke(impl->data_loop, do_add_stream, 0, NULL, 0, true, s);
+ return 0;
+
+error_errno:
+ res = -errno;
+error:
+ if (s)
+ destroy_stream(s);
+ return res;
+}
+
+static int rule_matched(void *data, const char *location, const char *action,
+ const char *str, size_t len)
+{
+ struct stream_info *i = data;
+ struct impl *impl = i->impl;
+ int res = 0;
+
+ if (spa_streq(action, "create-stream")) {
+ i->stream_props = pw_properties_copy(impl->stream_props);
+
+ pw_properties_update_string(i->stream_props, str, len);
+
+ res = create_stream(i);
+
+ pw_properties_free(i->stream_props);
+ }
+
+ return res;
+}
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct impl *impl = data;
+ const char *str;
+ struct stream_info info;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Node) || props == NULL)
+ return;
+
+ if (id == impl->combine_id)
+ return;
+
+ spa_zero(info);
+ info.impl = impl;
+ info.id = id;
+ info.props = props;
+
+ str = pw_properties_get(impl->props, "stream.rules");
+ if (str == NULL) {
+ if (impl->mode == MODE_CAPTURE || impl->mode == MODE_SINK)
+ str = "[ { matches = [ { media.class = \"Audio/Sink\" } ] "
+ " actions = { create-stream = {} } } ]";
+ else if (impl->mode == MODE_PLAYBACK || impl->mode == MODE_SOURCE)
+ str = "[ { matches = [ { media.class = \"Audio/Source\" } ] "
+ " actions = { create-stream = {} } } ]";
+ }
+ pw_conf_match_rules(str, strlen(str), NAME, props, rule_matched, &info);
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct impl *impl = data;
+ struct stream *s;
+
+ s = find_stream(impl, id);
+ if (s == NULL)
+ return;
+
+ destroy_stream(s);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void combine_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->combine_listener);
+ impl->combine = NULL;
+}
+
+static void combine_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ impl->combine_id = pw_stream_get_node_id(impl->combine);
+ pw_log_info("got combine id %d", impl->combine_id);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ default:
+ break;
+ }
+}
+
+static void combine_input_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *in, *out;
+ struct stream *s;
+
+ if ((in = pw_stream_dequeue_buffer(impl->combine)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ spa_list_for_each(s, &impl->streams, link) {
+ uint32_t j;
+
+ if (s->stream == NULL)
+ continue;
+
+ if ((out = pw_stream_dequeue_buffer(s->stream)) == NULL) {
+ pw_log_warn("out of playback buffers: %m");
+ goto do_trigger;
+ }
+
+ for (j = 0; j < out->buffer->n_datas; j++) {
+ struct spa_data *ds, *dd;
+ uint32_t outsize = 0, remap;
+ int32_t stride = 0;
+
+ dd = &out->buffer->datas[j];
+
+ remap = s->remap[j];
+ if (remap < in->buffer->n_datas) {
+ uint32_t offs, size;
+
+ ds = &in->buffer->datas[remap];
+
+ offs = SPA_MIN(ds->chunk->offset, ds->maxsize);
+ size = SPA_MIN(ds->chunk->size, ds->maxsize - offs);
+
+ memcpy(dd->data,
+ SPA_PTROFF(ds->data, offs, void), size);
+
+ outsize = SPA_MAX(outsize, size);
+ stride = SPA_MAX(stride, ds->chunk->stride);
+ } else {
+ memset(dd->data, 0, outsize);
+ }
+ dd->chunk->offset = 0;
+ dd->chunk->size = outsize;
+ dd->chunk->stride = stride;
+ }
+ pw_stream_queue_buffer(s->stream, out);
+do_trigger:
+ pw_stream_trigger_process(s->stream);
+ }
+ pw_stream_queue_buffer(impl->combine, in);
+}
+
+static void combine_output_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *in, *out;
+ struct stream *s;
+
+ if ((out = pw_stream_dequeue_buffer(impl->combine)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ spa_list_for_each(s, &impl->streams, link) {
+ uint32_t j;
+
+ if (s->stream == NULL)
+ continue;
+
+ if ((in = pw_stream_dequeue_buffer(s->stream)) == NULL) {
+ pw_log_warn("%p: out of capture buffers: %m", s);
+ continue;
+ }
+ s->ready = false;
+
+ for (j = 0; j < in->buffer->n_datas; j++) {
+ struct spa_data *ds, *dd;
+ uint32_t outsize = 0, remap;
+ int32_t stride = 0;
+
+ 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;
+
+ dd = &out->buffer->datas[remap];
+
+ offs = SPA_MIN(ds->chunk->offset, ds->maxsize);
+ size = SPA_MIN(ds->chunk->size, ds->maxsize - offs);
+ size = SPA_MIN(size, dd->maxsize);
+
+ memcpy(dd->data,
+ SPA_PTROFF(ds->data, offs, void), size);
+
+ outsize = SPA_MAX(outsize, size);
+ stride = SPA_MAX(stride, ds->chunk->stride);
+
+ dd->chunk->offset = 0;
+ dd->chunk->size = outsize;
+ dd->chunk->stride = stride;
+ }
+ }
+ pw_stream_queue_buffer(s->stream, in);
+ }
+ pw_stream_queue_buffer(impl->combine, out);
+}
+
+static const struct pw_stream_events combine_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = combine_destroy,
+ .state_changed = combine_state_changed,
+};
+
+static int create_combine(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ enum pw_direction direction;
+ enum pw_stream_flags flags;
+
+ impl->combine = pw_stream_new(impl->core, "Combine stream", impl->combine_props);
+ impl->combine_props = NULL;
+
+ if (impl->combine == NULL)
+ return -errno;
+
+ flags = PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS;
+
+ impl->combine_events = combine_events;
+
+ if (impl->mode == MODE_SINK || impl->mode == MODE_CAPTURE) {
+ direction = PW_DIRECTION_INPUT;
+ impl->combine_events.process = combine_input_process;
+ } else {
+ direction = PW_DIRECTION_OUTPUT;
+ impl->combine_events.process = combine_output_process;
+ flags |= PW_STREAM_FLAG_TRIGGER;
+ }
+
+ pw_stream_add_listener(impl->combine,
+ &impl->combine_listener,
+ &impl->combine_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->combine,
+ direction, PW_ID_ANY, flags, params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_removed(void *d)
+{
+ struct impl *impl = d;
+ if (impl->core) {
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ }
+ if (impl->registry) {
+ spa_hook_remove(&impl->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)impl->registry);
+ impl->registry = NULL;
+ }
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .removed = core_removed,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ struct stream *s;
+
+ spa_list_consume(s, &impl->streams, link)
+ destroy_stream(s);
+
+ if (impl->combine)
+ pw_stream_destroy(impl->combine);
+
+ if (impl->registry) {
+ spa_hook_remove(&impl->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)impl->registry);
+ impl->registry = NULL;
+ }
+ if (impl->core) {
+ spa_hook_remove(&impl->core_listener);
+ if (impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+ impl->core = NULL;
+ }
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->combine_props);
+ pw_properties_free(impl->props);
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void copy_props(const struct pw_properties *props, struct pw_properties *target,
+ const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(target, key) == NULL)
+ pw_properties_set(target, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ struct impl *impl;
+ const char *str, *prefix;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+ impl->data_loop = pw_context_get_data_loop(context);
+
+ spa_list_init(&impl->streams);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ if ((str = pw_properties_get(props, "combine.mode")) == NULL)
+ str = "sink";
+
+ if (spa_streq(str, "sink")) {
+ impl->mode = MODE_SINK;
+ prefix = "sink";
+ } else if (spa_streq(str, "capture")) {
+ impl->mode = MODE_CAPTURE;
+ prefix = "capture";
+ } else if (spa_streq(str, "source")) {
+ impl->mode = MODE_SOURCE;
+ prefix = "source";
+ } else if (spa_streq(str, "playback")) {
+ impl->mode = MODE_PLAYBACK;
+ prefix = "playback";
+ } else {
+ pw_log_warn("unknown combine.mode '%s', using 'sink'", str);
+ impl->mode = MODE_SINK;
+ prefix = "sink";
+ }
+
+ impl->combine_props = pw_properties_new(NULL, NULL);
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->combine_props == NULL || impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_GROUP, "combine-%s-%u-%u",
+ prefix, pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "combine-%s-%u-%u",
+ prefix, pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, "resample.prefill") == NULL)
+ pw_properties_set(props, "resample.prefill", "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) {
+ if (impl->mode == MODE_SINK)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ else if (impl->mode == MODE_SOURCE)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ }
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "combine-%s-%u-%u",
+ prefix, pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "Combine %s", prefix);
+
+ if ((str = pw_properties_get(props, "combine.props")) != NULL)
+ pw_properties_update_string(impl->combine_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(props, impl->combine_props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(props, impl->combine_props, SPA_KEY_AUDIO_POSITION);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_NAME);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_GROUP);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_LATENCY);
+ copy_props(props, impl->combine_props, PW_KEY_NODE_VIRTUAL);
+ copy_props(props, impl->combine_props, PW_KEY_MEDIA_CLASS);
+ copy_props(props, impl->combine_props, "resample.prefill");
+
+ parse_audio_info(impl->combine_props, &impl->info);
+
+ copy_props(props, impl->stream_props, PW_KEY_NODE_GROUP);
+ copy_props(props, impl->stream_props, PW_KEY_NODE_VIRTUAL);
+ copy_props(props, impl->stream_props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(props, impl->stream_props, "resample.prefill");
+
+ if (pw_properties_get(impl->stream_props, PW_KEY_MEDIA_ROLE) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_ROLE, "filter");
+ if (pw_properties_get(impl->stream_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(impl->stream_props, PW_KEY_NODE_PASSIVE, "true");
+ if (pw_properties_get(impl->stream_props, PW_KEY_NODE_DONT_RECONNECT) == NULL)
+ pw_properties_set(impl->stream_props, PW_KEY_NODE_DONT_RECONNECT, "true");
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_combine(impl)) < 0)
+ goto error;
+
+ impl->registry = pw_core_get_registry(impl->core, PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(impl->registry, &impl->registry_listener,
+ &registry_events, impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c
new file mode 100644
index 0000000..34c16c7
--- /dev/null
+++ b/src/modules/module-echo-cancel.c
@@ -0,0 +1,1350 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ * © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <spa/debug/types.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/latency-utils.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/dynamic.h>
+#include <spa/support/plugin.h>
+#include <spa/utils/json.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/string.h>
+#include <spa/support/plugin-loader.h>
+#include <spa/interfaces/audio/aec.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/pipewire.h>
+
+#include <pipewire/extensions/profiler.h>
+
+/** \page page_module_echo_cancel PipeWire Module: Echo Cancel
+ *
+ * The `echo-cancel` module performs echo cancellation. The module creates
+ * virtual `echo-cancel-capture` source and `echo-cancel-playback` sink
+ * nodes and the associated streams.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `capture.props = {}`: properties to be passed to the capture stream
+ * - `source.props = {}`: properties to be passed to the source stream
+ * - `sink.props = {}`: properties to be passed to the sink stream
+ * - `playback.props = {}`: properties to be passed to the playback stream
+ * - `library.name = <str>`: the echo cancellation library Currently supported:
+ * `aec/libspa-aec-webrtc`. Leave unset to use the default method (`aec/libspa-aec-webrtc`).
+ * - `aec.args = <str>`: arguments to pass to the echo cancellation method
+ * - `monitor.mode`: Instead of making a sink, make a stream that captures from
+ * the monitor ports of the default sink.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LINK_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_REMOTE_NAME
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-echo-cancel
+ * args = {
+ * # library.name = aec/libspa-aec-webrtc
+ * # node.latency = 1024/48000
+ * # monitor.mode = false
+ * capture.props = {
+ * node.name = "Echo Cancellation Capture"
+ * }
+ * source.props = {
+ * node.name = "Echo Cancellation Source"
+ * }
+ * sink.props = {
+ * node.name = "Echo Cancellation Sink"
+ * }
+ * playback.props = {
+ * node.name = "Echo Cancellation Playback"
+ * }
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ */
+
+/**
+ * .--------. .---------. .--------. .----------. .-------.
+ * | source | --> | capture | --> | | --> | source | --> | app |
+ * '--------' '---------' | echo | '----------' '-------'
+ * | cancel |
+ * .--------. .---------. | | .----------. .--------.
+ * | app | --> | sink | --> | | --> | playback | --> | sink |
+ * '--------' '---------' '--------' '----------' '--------'
+ */
+#define NAME "echo-cancel"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+/* Hopefully this is enough for any combination of AEC engine and resampler
+ * input requirement for rate matching */
+#define MAX_BUFSIZE_MS 100
+#define DELAY_MS 0
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Echo Cancellation" },
+ { PW_KEY_MODULE_USAGE, " [ remote.name=<remote> ] "
+ "[ node.latency=<latency as fraction> ] "
+ "[ audio.rate=<sample rate> ] "
+ "[ audio.channels=<number of channels> ] "
+ "[ audio.position=<channel map> ] "
+ "[ buffer.max_size=<max buffer size in ms> ] "
+ "[ buffer.play_delay=<delay as fraction> ] "
+ "[ library.name =<library name> ] "
+ "[ aec.args=<aec arguments> ] "
+ "[ capture.props=<properties> ] "
+ "[ source.props=<properties> ] "
+ "[ sink.props=<properties> ] "
+ "[ playback.props=<properties> ] " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct spa_audio_info_raw info;
+
+ struct pw_properties *capture_props;
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+ struct spa_audio_info_raw capture_info;
+
+ struct pw_properties *source_props;
+ struct pw_stream *source;
+ struct spa_hook source_listener;
+ struct spa_audio_info_raw source_info;
+
+ void *rec_buffer[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t rec_ringsize;
+ struct spa_ringbuffer rec_ring;
+
+ struct pw_properties *playback_props;
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+ struct spa_audio_info_raw playback_info;
+
+ struct pw_properties *sink_props;
+ struct pw_stream *sink;
+ struct spa_hook sink_listener;
+ void *play_buffer[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t play_ringsize;
+ struct spa_ringbuffer play_ring;
+ struct spa_ringbuffer play_delayed_ring;
+ struct spa_audio_info_raw sink_info;
+
+ void *out_buffer[SPA_AUDIO_MAX_CHANNELS];
+ uint32_t out_ringsize;
+ struct spa_ringbuffer out_ring;
+
+ struct spa_audio_aec *aec;
+ uint32_t aec_blocksize;
+
+ unsigned int capture_ready:1;
+ unsigned int sink_ready:1;
+
+ unsigned int do_disconnect:1;
+
+ uint32_t max_buffer_size;
+ uint32_t buffer_delay;
+ uint32_t current_delay;
+
+ struct spa_handle *spa_handle;
+ struct spa_plugin_loader *loader;
+
+ bool monitor_mode;
+};
+
+static void process(struct impl *impl)
+{
+ struct pw_buffer *cout;
+ struct pw_buffer *pout = NULL;
+ float rec_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
+ float play_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
+ float play_delayed_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
+ float out_buf[impl->info.channels][impl->aec_blocksize / sizeof(float)];
+ const float *rec[impl->info.channels];
+ const float *play[impl->info.channels];
+ const float *play_delayed[impl->info.channels];
+ float *out[impl->info.channels];
+ struct spa_data *dd;
+ uint32_t i, size;
+ uint32_t rindex, pindex, oindex, pdindex, avail;
+ int32_t stride = 0;
+
+ if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
+ pw_log_debug("out of playback buffers: %m");
+ goto done;
+ }
+
+ size = impl->aec_blocksize;
+
+ /* First read a block from the playback and capture ring buffers */
+
+ spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
+ spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
+ spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
+
+ for (i = 0; i < impl->info.channels; i++) {
+ /* captured samples, with echo from sink */
+ rec[i] = &rec_buf[i][0];
+ /* echo from sink */
+ play[i] = &play_buf[i][0];
+ /* echo from sink delayed */
+ play_delayed[i] = &play_delayed_buf[i][0];
+ /* filtered samples, without echo from sink */
+ out[i] = &out_buf[i][0];
+
+ stride = 0;
+ spa_ringbuffer_read_data(&impl->rec_ring, impl->rec_buffer[i],
+ impl->rec_ringsize, rindex % impl->rec_ringsize,
+ (void*)rec[i], size);
+
+ stride = 0;
+ spa_ringbuffer_read_data(&impl->play_ring, impl->play_buffer[i],
+ impl->play_ringsize, pindex % impl->play_ringsize,
+ (void *)play[i], size);
+
+ stride = 0;
+ spa_ringbuffer_read_data(&impl->play_delayed_ring, impl->play_buffer[i],
+ impl->play_ringsize, pdindex % impl->play_ringsize,
+ (void *)play_delayed[i], size);
+
+ if (pout != NULL) {
+ /* output to sink, just copy */
+ dd = &pout->buffer->datas[i];
+ memcpy(dd->data, play[i], size);
+
+ dd->chunk->offset = 0;
+ dd->chunk->size = size;
+ dd->chunk->stride = stride;
+ }
+ }
+
+ spa_ringbuffer_read_update(&impl->rec_ring, rindex + size);
+ spa_ringbuffer_read_update(&impl->play_ring, pindex + size);
+ spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size);
+
+ if (impl->playback != NULL)
+ pw_stream_queue_buffer(impl->playback, pout);
+
+ if (SPA_UNLIKELY (impl->current_delay < impl->buffer_delay)) {
+ uint32_t delay_left = impl->buffer_delay - impl->current_delay;
+ uint32_t silence_size;
+
+ /* don't run the canceller until play_buffer has been filled,
+ * copy silence to output in the meantime */
+ silence_size = SPA_MIN(size, delay_left * sizeof(float));
+ for (i = 0; i < impl->info.channels; i++)
+ memset(out[i], 0, silence_size);
+ impl->current_delay += silence_size / sizeof(float);
+ pw_log_debug("current_delay %d", impl->current_delay);
+
+ if (silence_size != size) {
+ const float *pd[impl->info.channels];
+ float *o[impl->info.channels];
+
+ for (i = 0; i < impl->info.channels; i++) {
+ pd[i] = play_delayed[i] + delay_left;
+ o[i] = out[i] + delay_left;
+ }
+ spa_audio_aec_run(impl->aec, rec, pd, o, size / sizeof(float) - delay_left);
+ }
+ } else {
+ /* run the canceller */
+ spa_audio_aec_run(impl->aec, rec, play_delayed, out, size / sizeof(float));
+ }
+
+ /* Next, copy over the output to the output ringbuffer */
+ avail = spa_ringbuffer_get_write_index(&impl->out_ring, &oindex);
+ if (avail + size > impl->out_ringsize) {
+ uint32_t rindex, drop;
+
+ /* Drop enough so we have size bytes left */
+ drop = avail + size - impl->out_ringsize;
+ pw_log_debug("output ringbuffer xrun %d + %u > %u, dropping %u",
+ avail, size, impl->out_ringsize, drop);
+
+ spa_ringbuffer_get_read_index(&impl->out_ring, &rindex);
+ spa_ringbuffer_read_update(&impl->out_ring, rindex + drop);
+
+ avail += drop;
+ }
+
+ for (i = 0; i < impl->info.channels; i++) {
+ /* captured samples, with echo from sink */
+ spa_ringbuffer_write_data(&impl->out_ring, impl->out_buffer[i],
+ impl->out_ringsize, oindex % impl->out_ringsize,
+ (void *)out[i], size);
+ }
+
+ spa_ringbuffer_write_update(&impl->out_ring, oindex + size);
+
+ /* And finally take data from the output ringbuffer and make it
+ * available on the source */
+
+ avail = spa_ringbuffer_get_read_index(&impl->out_ring, &oindex);
+ while (avail >= size) {
+ if ((cout = pw_stream_dequeue_buffer(impl->source)) == NULL) {
+ pw_log_debug("out of source buffers: %m");
+ break;
+ }
+
+ for (i = 0; i < impl->info.channels; i++) {
+ dd = &cout->buffer->datas[i];
+ spa_ringbuffer_read_data(&impl->out_ring, impl->out_buffer[i],
+ impl->out_ringsize, oindex % impl->out_ringsize,
+ (void *)dd->data, size);
+ dd->chunk->offset = 0;
+ dd->chunk->size = size;
+ dd->chunk->stride = 0;
+ }
+
+ pw_stream_queue_buffer(impl->source, cout);
+
+ oindex += size;
+ spa_ringbuffer_read_update(&impl->out_ring, oindex);
+ avail -= size;
+ }
+
+done:
+ impl->sink_ready = false;
+ impl->capture_ready = false;
+}
+
+static void capture_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->capture_listener);
+ impl->capture = NULL;
+}
+
+static void capture_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t i, index, offs, size;
+ int32_t avail;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->capture)) == NULL) {
+ pw_log_debug("out of capture buffers: %m");
+ return;
+ }
+ d = &buf->buffer->datas[0];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ avail = spa_ringbuffer_get_write_index(&impl->rec_ring, &index);
+
+ if (avail + size > impl->rec_ringsize) {
+ uint32_t rindex, drop;
+
+ /* Drop enough so we have size bytes left */
+ drop = avail + size - impl->rec_ringsize;
+ pw_log_debug("capture ringbuffer xrun %d + %u > %u, dropping %u",
+ avail, size, impl->rec_ringsize, drop);
+
+ spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
+ spa_ringbuffer_read_update(&impl->rec_ring, rindex + drop);
+
+ avail += drop;
+ }
+
+ /* If we don't know what size to push yet, use the canceller blocksize
+ * if it has a specific requirement, else keep the block size the same
+ * on input and output or what the resampler needs */
+ if (impl->aec_blocksize == 0) {
+ impl->aec_blocksize = size;
+ pw_log_debug("Setting AEC block size to %u", impl->aec_blocksize);
+ }
+
+ for (i = 0; i < impl->info.channels; i++) {
+ /* captured samples, with echo from sink */
+ d = &buf->buffer->datas[i];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ spa_ringbuffer_write_data(&impl->rec_ring, impl->rec_buffer[i],
+ impl->rec_ringsize, index % impl->rec_ringsize,
+ SPA_PTROFF(d->data, offs, void), size);
+ }
+
+ spa_ringbuffer_write_update(&impl->rec_ring, index + size);
+
+ if (avail + size >= impl->aec_blocksize) {
+ impl->capture_ready = true;
+ if (impl->sink_ready)
+ process(impl);
+ }
+
+ pw_stream_queue_buffer(impl->capture, buf);
+}
+
+static void capture_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->source, false);
+ pw_stream_flush(impl->capture, false);
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("%p: input unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("%p: input error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void source_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ int res;
+
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->source, false);
+ pw_stream_flush(impl->capture, false);
+
+ if (old == PW_STREAM_STATE_STREAMING) {
+ pw_log_debug("%p: deactivate %s", impl, impl->aec->name);
+ res = spa_audio_aec_deactivate(impl->aec);
+ if (res < 0 && res != -EOPNOTSUPP) {
+ pw_log_error("aec plugin %s deactivate failed: %s", impl->aec->name, spa_strerror(res));
+ }
+ }
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ pw_log_debug("%p: activate %s", impl, impl->aec->name);
+ res = spa_audio_aec_activate(impl->aec);
+ if (res < 0 && res != -EOPNOTSUPP) {
+ pw_log_error("aec plugin %s activate failed: %s", impl->aec->name, spa_strerror(res));
+ }
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("%p: input unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("%p: input error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void input_param_latency_changed(struct impl *impl, const struct spa_pod *param)
+{
+ struct spa_latency_info latency;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ pw_stream_update_params(impl->source, params, 1);
+ else
+ pw_stream_update_params(impl->capture, params, 1);
+}
+static struct spa_pod* get_props_param(struct impl* impl, struct spa_pod_builder* b)
+{
+ if (spa_audio_aec_get_params(impl->aec, NULL) > 0) {
+ struct spa_pod_frame f[2];
+ spa_pod_builder_push_object(
+ b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ spa_pod_builder_prop(b, SPA_PROP_params, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+
+ spa_audio_aec_get_params(impl->aec, b);
+
+ spa_pod_builder_pop(b, &f[1]);
+ return spa_pod_builder_pop(b, &f[0]);
+ }
+ return NULL;
+}
+
+static void input_param_changed(void *data, uint32_t id, const struct spa_pod* param)
+{
+ struct spa_pod_object* obj = (struct spa_pod_object*)param;
+ const struct spa_pod_prop* prop;
+ struct impl* impl = data;
+ switch (id) {
+ case SPA_PARAM_Latency:
+ input_param_latency_changed(impl, param);
+ break;
+ case SPA_PARAM_Props:
+ if (param != NULL) {
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ const struct spa_pod* params[1];
+ SPA_POD_OBJECT_FOREACH(obj, prop)
+ {
+ if (prop->key == SPA_PROP_params) {
+ spa_audio_aec_set_params(impl->aec, &prop->value);
+ }
+ }
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ params[0] = get_props_param(impl, &b.b);
+ if (params[0]) {
+ pw_stream_update_params(impl->capture, params, 1);
+ pw_stream_update_params(impl->playback, params, 1);
+ }
+ spa_pod_dynamic_builder_clean(&b);
+ } else {
+ pw_log_warn("param is null");
+ }
+ break;
+ }
+}
+
+static const struct pw_stream_events capture_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = capture_destroy,
+ .state_changed = capture_state_changed,
+ .process = capture_process,
+ .param_changed = input_param_changed
+};
+
+static void source_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->source_listener);
+ impl->source = NULL;
+}
+
+static const struct pw_stream_events source_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = source_destroy,
+ .state_changed = source_state_changed,
+ .param_changed = input_param_changed
+};
+
+static void output_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->sink, false);
+ if (impl->playback != NULL)
+ pw_stream_flush(impl->playback, false);
+ if (old == PW_STREAM_STATE_STREAMING) {
+ impl->current_delay = 0;
+ }
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("%p: output unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("%p: output error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void output_param_latency_changed(struct impl *impl, const struct spa_pod *param)
+{
+ struct spa_latency_info latency;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ pw_stream_update_params(impl->sink, params, 1);
+ else if (impl->playback != NULL)
+ pw_stream_update_params(impl->playback, params, 1);
+}
+
+static void output_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ const struct spa_pod_prop *prop;
+ struct impl *impl = data;
+ switch (id) {
+ case SPA_PARAM_Latency:
+ output_param_latency_changed(impl, param);
+ break;
+ case SPA_PARAM_Props:
+ if (param != NULL) {
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ const struct spa_pod* params[1];
+
+ SPA_POD_OBJECT_FOREACH(obj, prop)
+ {
+ if (prop->key == SPA_PROP_params) {
+ spa_audio_aec_set_params(impl->aec, &prop->value);
+ }
+ }
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ params[0] = get_props_param(impl, &b.b);
+ if (params[0] != NULL) {
+ pw_stream_update_params(impl->capture, params, 1);
+ pw_stream_update_params(impl->playback, params, 1);
+ }
+ spa_pod_dynamic_builder_clean(&b);
+ }
+ break;
+ }
+}
+
+static void sink_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->sink_listener);
+ impl->sink = NULL;
+}
+
+static void sink_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t i, index, offs, size;
+ int32_t avail;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->sink)) == NULL) {
+ pw_log_debug("out of sink buffers: %m");
+ return;
+ }
+ d = &buf->buffer->datas[0];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ avail = spa_ringbuffer_get_write_index(&impl->play_ring, &index);
+
+ if (avail + size > impl->play_ringsize) {
+ uint32_t rindex, drop;
+
+ /* Drop enough so we have size bytes left */
+ drop = avail + size - impl->play_ringsize;
+ pw_log_debug("sink ringbuffer xrun %d + %u > %u, dropping %u",
+ avail, size, impl->play_ringsize, drop);
+
+ spa_ringbuffer_get_read_index(&impl->play_ring, &rindex);
+ spa_ringbuffer_read_update(&impl->play_ring, rindex + drop);
+
+ spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &rindex);
+ spa_ringbuffer_read_update(&impl->play_delayed_ring, rindex + drop);
+
+ avail += drop;
+ }
+
+ if (impl->aec_blocksize == 0) {
+ impl->aec_blocksize = size;
+ pw_log_debug("Setting AEC block size to %u", impl->aec_blocksize);
+ }
+
+ for (i = 0; i < impl->info.channels; i++) {
+ /* echo from sink */
+ d = &buf->buffer->datas[i];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ spa_ringbuffer_write_data(&impl->play_ring, impl->play_buffer[i],
+ impl->play_ringsize, index % impl->play_ringsize,
+ SPA_PTROFF(d->data, offs, void), size);
+ }
+ spa_ringbuffer_write_update(&impl->play_ring, index + size);
+
+ if (avail + size >= impl->aec_blocksize) {
+ impl->sink_ready = true;
+ if (impl->capture_ready)
+ process(impl);
+ }
+
+ pw_stream_queue_buffer(impl->sink, buf);
+}
+
+static void playback_destroy(void *d)
+{
+ struct impl *impl = d;
+ if (impl->playback != NULL) {
+ spa_hook_remove(&impl->playback_listener);
+ impl->playback = NULL;
+ }
+}
+
+static const struct pw_stream_events playback_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = playback_destroy,
+ .state_changed = output_state_changed,
+ .param_changed = output_param_changed
+};
+static const struct pw_stream_events sink_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = sink_destroy,
+ .process = sink_process,
+ .state_changed = output_state_changed,
+ .param_changed = output_param_changed
+};
+
+static int setup_streams(struct impl *impl)
+{
+ int res;
+ uint32_t n_params, i;
+ uint32_t offsets[512];
+ const struct spa_pod *params[512];
+ struct spa_pod_dynamic_builder b;
+ uint32_t index;
+
+ impl->capture = pw_stream_new(impl->core,
+ "Echo-Cancel Capture", impl->capture_props);
+ impl->capture_props = NULL;
+ if (impl->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->capture,
+ &impl->capture_listener,
+ &capture_events, impl);
+
+ impl->source = pw_stream_new(impl->core,
+ "Echo-Cancel Source", impl->source_props);
+ impl->source_props = NULL;
+ if (impl->source == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->source,
+ &impl->source_listener,
+ &source_events, impl);
+
+ if (impl->monitor_mode) {
+ impl->playback = NULL;
+ } else {
+ impl->playback = pw_stream_new(impl->core,
+ "Echo-Cancel Playback", impl->playback_props);
+ impl->playback_props = NULL;
+ if (impl->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->playback,
+ &impl->playback_listener,
+ &playback_events, impl);
+ }
+
+ impl->sink = pw_stream_new(impl->core,
+ "Echo-Cancel Sink", impl->sink_props);
+ impl->sink_props = NULL;
+ if (impl->sink == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->sink,
+ &impl->sink_listener,
+ &sink_events, impl);
+
+ n_params = 0;
+ spa_pod_dynamic_builder_init(&b, NULL, 0, 4096);
+
+ offsets[n_params++] = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->capture_info);
+
+ int nbr_of_external_props = spa_audio_aec_enum_props(impl->aec, 0, NULL);
+ if (nbr_of_external_props > 0) {
+ for (int i = 0; i < nbr_of_external_props; i++) {
+ offsets[n_params++] = b.b.state.offset;
+ spa_audio_aec_enum_props(impl->aec, i, &b.b);
+ }
+ get_props_param(impl, &b.b);
+ }
+
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+
+ if ((res = pw_stream_connect(impl->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ spa_pod_dynamic_builder_clean(&b);
+ return res;
+ }
+
+ offsets[0] = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->source_info);
+
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+
+ if ((res = pw_stream_connect(impl->source,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ spa_pod_dynamic_builder_clean(&b);
+ return res;
+ }
+
+ offsets[0] = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->sink_info);
+
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+
+ if ((res = pw_stream_connect(impl->sink,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ impl->playback != NULL ?
+ PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS :
+ PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ spa_pod_dynamic_builder_clean(&b);
+ return res;
+ }
+
+ offsets[0] = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b, SPA_PARAM_EnumFormat, &impl->playback_info);
+
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offsets[i]);
+
+ if (impl->playback != NULL && (res = pw_stream_connect(impl->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ spa_pod_dynamic_builder_clean(&b);
+ return res;
+ }
+
+ spa_pod_dynamic_builder_clean(&b);
+
+ impl->rec_ringsize = sizeof(float) * impl->max_buffer_size * impl->info.rate / 1000;
+ impl->play_ringsize = sizeof(float) * ((impl->max_buffer_size * impl->info.rate / 1000) + impl->buffer_delay);
+ impl->out_ringsize = sizeof(float) * impl->max_buffer_size * impl->info.rate / 1000;
+ for (i = 0; i < impl->info.channels; i++) {
+ impl->rec_buffer[i] = malloc(impl->rec_ringsize);
+ impl->play_buffer[i] = malloc(impl->play_ringsize);
+ impl->out_buffer[i] = malloc(impl->out_ringsize);
+ }
+ spa_ringbuffer_init(&impl->rec_ring);
+ spa_ringbuffer_init(&impl->play_ring);
+ spa_ringbuffer_init(&impl->play_delayed_ring);
+ spa_ringbuffer_init(&impl->out_ring);
+
+ spa_ringbuffer_get_write_index(&impl->play_ring, &index);
+ spa_ringbuffer_write_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
+ spa_ringbuffer_get_read_index(&impl->play_ring, &index);
+ spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ if (res == -ENOENT) {
+ pw_log_info("id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ } else {
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ }
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ uint32_t i;
+ if (impl->capture)
+ pw_stream_destroy(impl->capture);
+ if (impl->source)
+ pw_stream_destroy(impl->source);
+ if (impl->playback)
+ pw_stream_destroy(impl->playback);
+ if (impl->sink)
+ pw_stream_destroy(impl->sink);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+ if (impl->spa_handle)
+ spa_plugin_loader_unload(impl->loader, impl->spa_handle);
+ pw_properties_free(impl->capture_props);
+ pw_properties_free(impl->source_props);
+ pw_properties_free(impl->playback_props);
+ pw_properties_free(impl->sink_props);
+
+ for (i = 0; i < impl->info.channels; i++) {
+ if (impl->rec_buffer[i])
+ free(impl->rec_buffer[i]);
+ if (impl->play_buffer[i])
+ free(impl->play_buffer[i]);
+ if (impl->out_buffer[i])
+ free(impl->out_buffer[i]);
+ }
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static 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));
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->capture_props, key) == NULL)
+ pw_properties_set(impl->capture_props, key, str);
+ if (pw_properties_get(impl->source_props, key) == NULL)
+ pw_properties_set(impl->source_props, key, str);
+ if (pw_properties_get(impl->playback_props, key) == NULL)
+ pw_properties_set(impl->playback_props, key, str);
+ if (pw_properties_get(impl->sink_props, key) == NULL)
+ pw_properties_set(impl->sink_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props, *aec_props;
+ struct impl *impl;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ const char *str;
+ const char *path;
+ int res = 0;
+ struct spa_handle *handle = NULL;
+ void *iface;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->capture_props = pw_properties_new(NULL, NULL);
+ impl->source_props = pw_properties_new(NULL, NULL);
+ impl->playback_props = pw_properties_new(NULL, NULL);
+ impl->sink_props = pw_properties_new(NULL, NULL);
+ if (impl->source_props == NULL || impl->sink_props == NULL ||
+ impl->capture_props == NULL || impl->playback_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->monitor_mode = false;
+ if ((str = pw_properties_get(props, "monitor.mode")) != NULL)
+ impl->monitor_mode = pw_properties_parse_bool(str);
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_GROUP, "echo-cancel-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "echo-cancel-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, "resample.prefill") == NULL)
+ pw_properties_set(props, "resample.prefill", "true");
+
+ parse_audio_info(props, &impl->info);
+
+ impl->capture_info = impl->info;
+ impl->source_info = impl->info;
+ impl->sink_info = impl->info;
+ impl->playback_info = impl->info;
+
+ if ((str = pw_properties_get(props, "capture.props")) != NULL)
+ pw_properties_update_string(impl->capture_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "source.props")) != NULL)
+ pw_properties_update_string(impl->source_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "sink.props")) != NULL)
+ pw_properties_update_string(impl->sink_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "playback.props")) != NULL)
+ pw_properties_update_string(impl->playback_props, str, strlen(str));
+
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(impl->capture_props, PW_KEY_NODE_NAME, "echo-cancel-capture");
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->capture_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Capture");
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(impl->capture_props, PW_KEY_NODE_PASSIVE, "true");
+
+ if (pw_properties_get(impl->source_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(impl->source_props, PW_KEY_NODE_NAME, "echo-cancel-source");
+ if (pw_properties_get(impl->source_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->source_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Source");
+ if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(impl->playback_props, PW_KEY_NODE_NAME, "echo-cancel-playback");
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->playback_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Playback");
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(impl->playback_props, PW_KEY_NODE_PASSIVE, "true");
+
+ if (pw_properties_get(impl->sink_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_NODE_NAME, "echo-cancel-sink");
+ if (pw_properties_get(impl->sink_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_NODE_DESCRIPTION, "Echo-Cancel Sink");
+ if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS,
+ impl->monitor_mode ? "Stream/Input/Audio" : "Audio/Sink");
+ if (impl->monitor_mode) {
+ if (pw_properties_get(impl->sink_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_NODE_PASSIVE, "true");
+ if (pw_properties_get(impl->sink_props, PW_KEY_STREAM_MONITOR) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_STREAM_MONITOR, "true");
+ if (pw_properties_get(impl->sink_props, PW_KEY_STREAM_CAPTURE_SINK) == NULL)
+ pw_properties_set(impl->sink_props, PW_KEY_STREAM_CAPTURE_SINK, "true");
+ }
+
+ if ((str = pw_properties_get(props, "aec.method")) != NULL)
+ pw_log_warn("aec.method is not supported anymore use library.name");
+
+ /* Use webrtc as default */
+ if ((path = pw_properties_get(props, "library.name")) == NULL)
+ path = "aec/libspa-aec-webrtc";
+
+ const struct spa_support *support;
+ uint32_t n_support;
+
+ support = pw_context_get_support(context, &n_support);
+ impl->loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader);
+ if (impl->loader == NULL) {
+ pw_log_error("a plugin loader is needed");
+ return -EINVAL;
+ }
+
+ struct spa_dict_item info_items[] = {
+ { SPA_KEY_LIBRARY_NAME, path },
+ };
+ struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+ handle = spa_plugin_loader_load(impl->loader, SPA_NAME_AEC, &info);
+ if (handle == NULL) {
+ pw_log_error("aec plugin %s not available library.name %s", SPA_NAME_AEC, path);
+ return -ENOENT;
+ }
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_AUDIO_AEC, &iface)) < 0) {
+ pw_log_error("can't get %s interface %d", SPA_TYPE_INTERFACE_AUDIO_AEC, res);
+ return res;
+ }
+ impl->aec = iface;
+ impl->spa_handle = handle;
+
+ if (impl->aec->iface.version > SPA_VERSION_AUDIO_AEC) {
+ pw_log_error("codec plugin %s has incompatible ABI version (%d > %d)",
+ SPA_NAME_AEC, impl->aec->iface.version, SPA_VERSION_AUDIO_AEC);
+ res = -ENOENT;
+ goto error;
+ }
+
+ pw_log_info("Using plugin AEC %s with version %d", impl->aec->name,
+ impl->aec->iface.version);
+
+ if ((str = pw_properties_get(props, "aec.args")) != NULL)
+ aec_props = pw_properties_new_string(str);
+ else
+ aec_props = pw_properties_new(NULL, NULL);
+
+ res = spa_audio_aec_init(impl->aec, &aec_props->dict, &impl->info);
+
+ pw_properties_free(aec_props);
+
+ if (res < 0) {
+ pw_log_error("aec plugin %s create failed: %s", impl->aec->name,
+ spa_strerror(res));
+ goto error;
+ }
+
+ if (impl->aec->latency) {
+ unsigned int num, denom, req_num, req_denom;
+ unsigned int factor = 0;
+ unsigned int new_num = 0;
+
+ spa_assert_se(sscanf(impl->aec->latency, "%u/%u", &num, &denom) == 2);
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_LATENCY)) != NULL) {
+ sscanf(str, "%u/%u", &req_num, &req_denom);
+ factor = (req_num * denom) / (req_denom * num);
+ new_num = req_num / factor * factor;
+ }
+
+ if (factor == 0 || new_num == 0) {
+ pw_log_info("Setting node latency to %s", impl->aec->latency);
+ pw_properties_set(props, PW_KEY_NODE_LATENCY, impl->aec->latency);
+ impl->aec_blocksize = sizeof(float) * impl->info.rate * num / denom;
+ } else {
+ pw_log_info("Setting node latency to %u/%u", new_num, req_denom);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", new_num, req_denom);
+ impl->aec_blocksize = sizeof(float) * impl->info.rate * num / denom * factor;
+ }
+ } else {
+ /* Implementation doesn't care about the block size */
+ impl->aec_blocksize = 0;
+ }
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, SPA_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, "resample.prefill");
+
+ if ((str = pw_properties_get(impl->capture_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&impl->capture_info, str, strlen(str));
+ if ((str = pw_properties_get(impl->source_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&impl->source_info, str, strlen(str));
+ if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&impl->sink_info, str, strlen(str));
+ if ((str = pw_properties_get(impl->playback_props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(&impl->playback_info, str, strlen(str));
+
+ if (impl->capture_info.channels != impl->info.channels)
+ impl->capture_info = impl->info;
+ if (impl->source_info.channels != impl->info.channels)
+ impl->source_info = impl->info;
+ if (impl->sink_info.channels != impl->info.channels)
+ impl->sink_info = impl->info;
+ if (impl->playback_info.channels != impl->info.channels)
+ impl->playback_info = impl->info;
+
+ impl->max_buffer_size = pw_properties_get_uint32(props,"buffer.max_size", MAX_BUFSIZE_MS);
+
+ if ((str = pw_properties_get(props, "buffer.play_delay")) != NULL) {
+ int req_num, req_denom;
+ if (sscanf(str, "%u/%u", &req_num, &req_denom) == 2) {
+ if (req_denom != 0) {
+ impl->buffer_delay = (impl->info.rate*req_num)/req_denom;
+ } else {
+ impl->buffer_delay = DELAY_MS * impl->info.rate / 1000;
+ pw_log_warn("Sample rate for buffer.play_delay is 0 using default");
+ }
+ } else {
+ impl->buffer_delay = DELAY_MS * impl->info.rate / 1000;
+ pw_log_warn("Wrong value/format for buffer.play_delay using default");
+ }
+ } else {
+ impl->buffer_delay = DELAY_MS * impl->info.rate / 1000;
+ }
+
+ pw_properties_free(props);
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ setup_streams(impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ pw_properties_free(props);
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-example-sink.c b/src/modules/module-example-sink.c
new file mode 100644
index 0000000..d999922
--- /dev/null
+++ b/src/modules/module-example-sink.c
@@ -0,0 +1,498 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.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/utils/ringbuffer.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_example_sink PipeWire Module: Example Sink
+ *
+ * The example sink is a good starting point for writing a custom
+ * sink. We refer to the source code for more information.
+ *
+ * ## Module Options
+ *
+ * - `node.name`: a unique name for the stream
+ * - `node.description`: a human readable name for the stream
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-example-sink
+ * args = {
+ * node.name = "example_sink"
+ * node.description = "My Example Sink"
+ * stream.props = {
+ * audio.position = [ FL FR ]
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "example-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define MODULE_USAGE "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ audio.format=<format, default:"DEFAULT_FORMAT"> ] " \
+ "[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ] " \
+ "[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] " \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "An example audio sink" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ unsigned int do_disconnect:1;
+};
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ default:
+ break;
+ }
+}
+
+static void playback_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ void *data;
+ uint32_t offs, size;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
+ data = SPA_PTROFF(bd->data, offs, void);
+
+ /* write buffer contents here */
+ pw_log_info("got buffer of size %d and data %p", size, data);
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .process = playback_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->stream = pw_stream_new(impl->core, "example sink", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &playback_stream_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(const struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ struct impl *impl;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "example-sink-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+ pw_properties_get(props, PW_KEY_NODE_NAME));
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ res = -EINVAL;
+ pw_log_error( "can't parse audio format");
+ goto error;
+ }
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-example-source.c b/src/modules/module-example-source.c
new file mode 100644
index 0000000..1db62df
--- /dev/null
+++ b/src/modules/module-example-source.c
@@ -0,0 +1,504 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.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/utils/ringbuffer.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_example_source PipeWire Module: Example Source
+ *
+ * The example source is a good starting point for writing a custom
+ * source. We refer to the source code for more information.
+ *
+ * ## Module Options
+ *
+ * - `node.name`: a unique name for the stream
+ * - `node.description`: a human readable name for the stream
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-example-source
+ * args = {
+ * node.name = "example_source"
+ * node.description = "My Example Source"
+ * stream.props = {
+ * audio.position = [ FL FR ]
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "example-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define MODULE_USAGE "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ audio.format=<format, default:"DEFAULT_FORMAT"> ] " \
+ "[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS) "> ] " \
+ "[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] " \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "An example audio source" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ unsigned int do_disconnect:1;
+ unsigned int unloading:1;
+};
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ default:
+ break;
+ }
+}
+
+static void capture_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ void *data;
+ uint32_t size;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+
+ data = bd->data;
+ size = buf->requested ? buf->requested * impl->frame_size : bd->maxsize;
+
+ /* fill buffer contents here */
+ pw_log_info("fill buffer data %p with up to %u bytes", data, size);
+
+ bd->chunk->size = size;
+ bd->chunk->stride = impl->frame_size;
+ bd->chunk->offset = 0;
+ buf->size = size / impl->frame_size;
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static const struct pw_stream_events capture_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .process = capture_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->stream = pw_stream_new(impl->core, "example source", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &capture_stream_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ impl->unloading = true;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(const struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ struct pw_properties *props = NULL;
+ struct impl *impl;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "example-source-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+ pw_properties_get(props, PW_KEY_NODE_NAME));
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ res = -EINVAL;
+ pw_log_error( "can't parse audio format");
+ goto error;
+ }
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-fallback-sink.c b/src/modules/module-fallback-sink.c
new file mode 100644
index 0000000..9a277d1
--- /dev/null
+++ b/src/modules/module-fallback-sink.c
@@ -0,0 +1,473 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_fallback_sink PipeWire Module: Fallback Sink
+ *
+ * Fallback sink, which appear dynamically when no other sinks are
+ * present. This is only useful for Pulseaudio compatibility.
+ */
+
+#define NAME "fallback-sink"
+
+#define DEFAULT_SINK_NAME "auto_null"
+#define DEFAULT_SINK_DESCRIPTION _("Dummy Output")
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE ("[ sink.name=<str> ] " \
+ "[ sink.description=<str> ] ")
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Dynamically appearing fallback sink" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct bitmap {
+ uint8_t *data;
+ size_t size;
+ size_t items;
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct pw_registry *registry;
+ struct pw_proxy *sink;
+
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook registry_listener;
+ struct spa_hook sink_listener;
+
+ struct pw_properties *properties;
+
+ struct bitmap sink_ids;
+ struct bitmap fallback_sink_ids;
+
+ int check_seq;
+
+ unsigned int do_disconnect:1;
+ unsigned int scheduled:1;
+};
+
+static int bitmap_add(struct bitmap *map, uint32_t i)
+{
+ const uint32_t pos = (i >> 3);
+ const uint8_t mask = 1 << (i & 0x7);
+
+ if (pos >= map->size) {
+ size_t new_size = map->size + pos + 16;
+ void *p;
+
+ p = realloc(map->data, new_size);
+ if (!p)
+ return -errno;
+
+ memset((uint8_t*)p + map->size, 0, new_size - map->size);
+ map->data = p;
+ map->size = new_size;
+ }
+
+ if (map->data[pos] & mask)
+ return 1;
+
+ map->data[pos] |= mask;
+ ++map->items;
+
+ return 0;
+}
+
+static bool bitmap_remove(struct bitmap *map, uint32_t i)
+{
+ const uint32_t pos = (i >> 3);
+ const uint8_t mask = 1 << (i & 0x7);
+
+ if (pos >= map->size)
+ return false;
+
+ if (!(map->data[pos] & mask))
+ return false;
+
+ map->data[pos] &= ~mask;
+ --map->items;
+
+ return true;
+}
+
+static void bitmap_free(struct bitmap *map)
+{
+ free(map->data);
+ spa_zero(*map);
+}
+
+static int add_id(struct bitmap *map, uint32_t id)
+{
+ int res;
+
+ if (id == SPA_ID_INVALID)
+ return -EINVAL;
+
+ if ((res = bitmap_add(map, id)) < 0)
+ pw_log_error("%s", spa_strerror(res));
+
+ return res;
+}
+
+static void reschedule_check(struct impl *impl)
+{
+ if (!impl->scheduled)
+ return;
+
+ impl->check_seq = pw_core_sync(impl->core, 0, impl->check_seq);
+}
+
+static void schedule_check(struct impl *impl)
+{
+ if (impl->scheduled)
+ return;
+
+ impl->scheduled = true;
+ impl->check_seq = pw_core_sync(impl->core, 0, impl->check_seq);
+}
+
+static void sink_proxy_removed(void *data)
+{
+ struct impl *impl = data;
+
+ pw_proxy_destroy(impl->sink);
+}
+
+static void sink_proxy_bound(void *data, uint32_t id)
+{
+ struct impl *impl = data;
+
+ add_id(&impl->sink_ids, id);
+ add_id(&impl->fallback_sink_ids, id);
+
+ reschedule_check(impl);
+ schedule_check(impl);
+}
+
+static void sink_proxy_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ pw_log_debug("fallback dummy sink destroyed");
+
+ spa_hook_remove(&impl->sink_listener);
+ impl->sink = NULL;
+}
+
+static const struct pw_proxy_events sink_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = sink_proxy_removed,
+ .bound = sink_proxy_bound,
+ .destroy = sink_proxy_destroy,
+};
+
+static int sink_create(struct impl *impl)
+{
+ if (impl->sink)
+ return 0;
+
+ pw_log_info("creating fallback dummy sink");
+
+ impl->sink = pw_core_create_object(impl->core,
+ "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,
+ impl->properties ? &impl->properties->dict : NULL, 0);
+ if (impl->sink == NULL)
+ return -errno;
+
+ pw_proxy_add_listener(impl->sink, &impl->sink_listener, &sink_proxy_events, impl);
+
+ return 0;
+}
+
+static void sink_destroy(struct impl *impl)
+{
+ if (!impl->sink)
+ return;
+
+ pw_log_info("removing fallback dummy sink");
+ pw_proxy_destroy(impl->sink);
+}
+
+static void check_sinks(struct impl *impl)
+{
+ int res;
+
+ pw_log_debug("seeing %zu sink(s), %zu fallback sink(s)",
+ impl->sink_ids.items, impl->fallback_sink_ids.items);
+
+ if (impl->sink_ids.items > impl->fallback_sink_ids.items) {
+ sink_destroy(impl);
+ } else {
+ if ((res = sink_create(impl)) < 0)
+ pw_log_error("error creating sink: %s", spa_strerror(res));
+ }
+}
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct impl *impl = data;
+ const char *str;
+
+ reschedule_check(impl);
+
+ if (!props)
+ return;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Node))
+ return;
+
+ str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+ if (!(spa_streq(str, "Audio/Sink") || spa_streq(str, "Audio/Sink/Virtual")))
+ return;
+
+ add_id(&impl->sink_ids, id);
+ schedule_check(impl);
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct impl *impl = data;
+
+ reschedule_check(impl);
+
+ bitmap_remove(&impl->fallback_sink_ids, id);
+ if (bitmap_remove(&impl->sink_ids, id))
+ schedule_check(impl);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void core_done(void *data, uint32_t id, int seq)
+{
+ struct impl *impl = data;
+
+ if (seq == impl->check_seq) {
+ impl->scheduled = false;
+ check_sinks(impl);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = core_done,
+};
+
+static void core_proxy_removed(void *data)
+{
+ struct impl *impl = data;
+
+ if (impl->registry) {
+ spa_hook_remove(&impl->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)impl->registry);
+ impl->registry = NULL;
+ }
+}
+
+static void core_proxy_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ spa_hook_remove(&impl->core_listener);
+ spa_hook_remove(&impl->core_proxy_listener);
+ impl->core = NULL;
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = core_proxy_destroy,
+ .removed = core_proxy_removed,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ sink_destroy(impl);
+
+ if (impl->registry) {
+ spa_hook_remove(&impl->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)impl->registry);
+ impl->registry = NULL;
+ }
+
+ if (impl->core) {
+ spa_hook_remove(&impl->core_listener);
+ spa_hook_remove(&impl->core_proxy_listener);
+ if (impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+ impl->core = NULL;
+ }
+
+ if (impl->properties) {
+ pw_properties_free(impl->properties);
+ impl->properties = NULL;
+ }
+
+ bitmap_free(&impl->sink_ids);
+ bitmap_free(&impl->fallback_sink_ids);
+
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ struct impl *impl = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ impl->module = module;
+ impl->context = context;
+
+ props = pw_properties_new_string(args);
+ if (props == NULL)
+ goto error_errno;
+
+ impl->properties = pw_properties_new(NULL, NULL);
+ if (impl->properties == NULL)
+ goto error_errno;
+
+ if ((str = pw_properties_get(props, "sink.name")) == NULL)
+ str = DEFAULT_SINK_NAME;
+ pw_properties_set(impl->properties, PW_KEY_NODE_NAME, str);
+
+ if ((str = pw_properties_get(props, "sink.description")) == NULL)
+ str = DEFAULT_SINK_DESCRIPTION;
+ pw_properties_set(impl->properties, PW_KEY_NODE_DESCRIPTION, str);
+
+ pw_properties_setf(impl->properties, SPA_KEY_AUDIO_RATE, "%u", 48000);
+ pw_properties_setf(impl->properties, SPA_KEY_AUDIO_CHANNELS, "%u", 2);
+ pw_properties_set(impl->properties, SPA_KEY_AUDIO_POSITION, "FL,FR");
+
+ pw_properties_set(impl->properties, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ pw_properties_set(impl->properties, PW_KEY_FACTORY_NAME, "support.null-audio-sink");
+ pw_properties_set(impl->properties, PW_KEY_NODE_VIRTUAL, "true");
+ pw_properties_set(impl->properties, "monitor.channel-volumes", "true");
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener, &core_proxy_events,
+ impl);
+
+ pw_core_add_listener(impl->core, &impl->core_listener, &core_events, impl);
+
+ impl->registry = pw_core_get_registry(impl->core,
+ PW_VERSION_REGISTRY, 0);
+ if (impl->registry == NULL)
+ goto error_errno;
+
+ pw_registry_add_listener(impl->registry,
+ &impl->registry_listener,
+ &registry_events, impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ schedule_check(impl);
+
+ pw_properties_free(props);
+ return 0;
+
+error_errno:
+ res = -errno;
+error:
+ if (props)
+ pw_properties_free(props);
+ if (impl)
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c
new file mode 100644
index 0000000..fd41d20
--- /dev/null
+++ b/src/modules/module-filter-chain.c
@@ -0,0 +1,2411 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <dlfcn.h>
+#include <unistd.h>
+
+#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/pod/dynamic.h>
+#include <spa/debug/types.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
+
+/**
+ * \page page_module_filter_chain PipeWire Module: Filter-Chain
+ *
+ * The filter-chain allows you to create an arbitrary processing graph
+ * from LADSPA, LV2 and builtin filters. This filter can be made into a
+ * virtual sink/source or between any 2 nodes in the graph.
+ *
+ * The filter chain is built with 2 streams, a capture stream providing
+ * the input to the filter chain and a playback stream sending out the
+ * filtered stream to the next nodes in the graph.
+ *
+ * Because both ends of the filter-chain are built with streams, the session
+ * manager can manage the configuration and connection with the sinks and
+ * sources automatically.
+ *
+ * ## Module Options
+ *
+ * - `node.description`: a human readable name for the filter chain
+ * - `filter.graph = []`: a description of the filter graph to run, see below
+ * - `capture.props = {}`: properties to be passed to the input stream
+ * - `playback.props = {}`: properties to be passed to the output stream
+ *
+ * ## Filter graph description
+ *
+ * The general structure of the graph description is as follows:
+ *
+ *\code{.unparsed}
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = <ladspa | lv2 | builtin>
+ * name = <name>
+ * plugin = <plugin>
+ * label = <label>
+ * config = {
+ * <configkey> = <value> ...
+ * }
+ * control = {
+ * <controlname|controlindex> = <value> ...
+ * }
+ * }
+ * ...
+ * ]
+ * links = [
+ * { output = <portname> input = <portname> }
+ * ...
+ * ]
+ * inputs = [ <portname> ... ]
+ * outputs = [ <portname> ... ]
+ * }
+ *\endcode
+ *
+ * ### Nodes
+ *
+ * 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` or `builtin`
+ * - `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 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 this is the name of the filter to use
+ *
+ * - `config` contains a filter specific configuration section. The convolver
+ * plugin needs this.
+ * - `control` contains the initial values for the control ports of the filter.
+ *
+ * ### Links
+ *
+ * Links can be made between ports of nodes. The `portname` is given as
+ * `<node_name>:<port_name>`.
+ *
+ * You can tee the output of filters to multiple other filters. You need to
+ * use a mixer if you want the output of multiple filters to go into one
+ * filter input port.
+ *
+ * links can be omited when the graph has just 1 filter.
+ *
+ * ### Inputs and Outputs
+ *
+ * These are the entry and exit ports into the graph definition. Their number
+ * defines the number of channels used by the filter-chain.
+ *
+ * The `<portname>` can be `null` when a channel is to be ignored.
+ *
+ * Each input/output in the graph can only be linked to one filter input/output.
+ * You need to use the copy builtin filter if the stream signal needs to be routed
+ * to multiple filters. You need to use the mixer builtin plugin if multiple graph
+ * outputs need to go to one output stream.
+ *
+ * inputs and outputs can be omitted, in which case the filter-chain will use all
+ * inputs from the first filter and all outputs from the last filter node. The
+ * graph will then be duplicated as many times to match the number of input/output
+ * channels of the streams.
+ *
+ * ## Builtin filters
+ *
+ * There are some useful builtin filters available. You select them with the label
+ * of the filter node.
+ *
+ * ### Mixer
+ *
+ * Use the `mixer` plugin if you have multiple input signals that need to be mixed together.
+ *
+ * The mixer plugin has up to 8 input ports labeled "In 1" to "In 8" and each with
+ * a gain control labeled "Gain 1" to "Gain 8". There is an output port labeled
+ * "Out". Unused input ports will be ignoded and not cause overhead.
+ *
+ * ### Copy
+ *
+ * Use the `copy` plugin if you need to copy a stream input signal to multiple filters.
+ *
+ * It has one input port "In" and one output port "Out".
+ *
+ * ### Biquads
+ *
+ * Biquads can be used to do all kinds of filtering. They are also used when creating
+ * equalizers.
+ *
+ * All biquad filters have an input port "In" and an output port "Out". They have
+ * a "Freq", "Q" and "Gain" control. Their meaning depends on the particular biquad that
+ * is used. The following labels can be used:
+ *
+ * - `bq_lowpass` a lowpass filter.
+ * - `bq_highpass` a highpass filter.
+ * - `bq_bandpass` a bandpass filter.
+ * - `bq_lowshelf` a low shelf filter.
+ * - `bq_highshelf` a high shelf filter.
+ * - `bq_peaking` a peaking filter.
+ * - `bq_notch` a notch filter.
+ * - `bq_allpass` an allpass filter.
+ *
+ * ### Convolver
+ *
+ * The convolver can be used to apply an impulse response to a signal. It is usually used
+ * for reverbs or virtual surround. The convolver is implemented with a fast FFT
+ * implementation.
+ *
+ * The convolver has an input port "In" and an output port "Out". It requires a config
+ * section in the node declaration in this format:
+ *
+ *\code{.unparsed}
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = builtin
+ * name = ...
+ * label = convolver
+ * config = {
+ * blocksize = ...
+ * tailsize = ...
+ * gain = ...
+ * delay = ...
+ * filename = ...
+ * offset = ...
+ * length = ...
+ * channel = ...
+ * }
+ * ...
+ * }
+ * }
+ * ...
+ * }
+ *\endcode
+ *
+ * - `blocksize` specifies the size of the blocks to use in the FFT. It is a value
+ * between 64 and 256. When not specified, this value is
+ * 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.
+ * - `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
+ * `length` will be used as the number of coefficients.
+ * - `/dirac` creates a [Dirac function](https://en.wikipedia.org/wiki/Dirac_delta_function) that
+ * can be used as gain.
+ * - A filename to load as the IR. This needs to be a file format supported
+ * by sndfile.
+ * - `offset` The sample offset in the file as the start of the IR.
+ * - `length` The number of samples to use as the IR.
+ * - `channel` The channel to use from the file as the IR.
+ *
+ * ### Delay
+ *
+ * The delay can be used to delay a signal in time.
+ *
+ * The delay has an input port "In" and an output port "Out". It also has
+ * a "Delay (s)" control port. It requires a config section in the node declaration
+ * in this format:
+ *
+ *\code{.unparsed}
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = builtin
+ * name = ...
+ * label = delay
+ * config = {
+ * "max-delay" = ...
+ * }
+ * control = {
+ * "Delay (s)" = ...
+ * }
+ * ...
+ * }
+ * }
+ * ...
+ * }
+ *\endcode
+ *
+ * - `max-delay` the maximum delay in seconds. The "Delay (s)" parameter will
+ * be clamped to this value.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior. Most options can be added to the global
+ * configuration or the individual streams:
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LINK_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_NODE_NAME: See notes below. If not specified, defaults to
+ * 'filter-chain-<pid>-<module-id>'.
+ *
+ * Stream only properties:
+ *
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_NODE_NAME: if not given per stream, the global node.name will be
+ * prefixed with 'input.' and 'output.' to generate a capture and playback
+ * stream node.name respectively.
+ *
+ * ## Example configuration of a virtual source
+ *
+ * This example uses the rnnoise LADSPA plugin to create a new
+ * virtual source.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-filter-chain
+ * args = {
+ * node.description = "Noise Canceling source"
+ * media.name = "Noise Canceling source"
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = ladspa
+ * name = rnnoise
+ * plugin = ladspa/librnnoise_ladspa
+ * label = noise_suppressor_stereo
+ * control = {
+ * "VAD Threshold (%)" 50.0
+ * }
+ * }
+ * ]
+ * }
+ * capture.props = {
+ * node.name = "capture.rnnoise_source"
+ * node.passive = true
+ * }
+ * playback.props = {
+ * node.name = "rnnoise_source"
+ * media.class = Audio/Source
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * ## Example configuration of a Dolby Surround encoder virtual Sink
+ *
+ * This example uses the ladpsa surround encoder to encode a 5.1 signal
+ * to a stereo Dolby Surround signal.
+ *
+ *\code{.unparsed}
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-filter-chain
+ * args = {
+ * node.description = "Dolby Surround Sink"
+ * media.name = "Dolby Surround Sink"
+ * filter.graph = {
+ * nodes = [
+ * {
+ * type = builtin
+ * name = mixer
+ * label = mixer
+ * control = { "Gain 1" = 0.5 "Gain 2" = 0.5 }
+ * }
+ * {
+ * type = ladspa
+ * name = enc
+ * plugin = surround_encoder_1401
+ * label = surroundEncoder
+ * }
+ * ]
+ * links = [
+ * { output = "mixer:Out" input = "enc:S" }
+ * ]
+ * inputs = [ "enc:L" "enc:R" "enc:C" null "mixer:In 1" "mixer:In 2" ]
+ * outputs = [ "enc:Lt" "enc:Rt" ]
+ * }
+ * capture.props = {
+ * node.name = "effect_input.dolby_surround"
+ * media.class = Audio/Sink
+ * audio.channels = 6
+ * audio.position = [ FL FR FC LFE SL SR ]
+ * }
+ * playback.props = {
+ * node.name = "effect_output.dolby_surround"
+ * node.passive = true
+ * audio.channels = 2
+ * audio.position = [ FL FR ]
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create filter chain streams" },
+ { PW_KEY_MODULE_USAGE, " [ remote.name=<remote> ] "
+ "[ node.latency=<latency as fraction> ] "
+ "[ node.description=<description of the nodes> ] "
+ "[ audio.rate=<sample rate> ] "
+ "[ audio.channels=<number of channels> ] "
+ "[ audio.position=<channel map> ] "
+ "filter.graph = [ "
+ " nodes = [ "
+ " { "
+ " type = <ladspa | lv2 | builtin> "
+ " name = <name> "
+ " plugin = <plugin> "
+ " label = <label> "
+ " config = { "
+ " <configkey> = <value> ... "
+ " } "
+ " control = { "
+ " <controlname|controlindex> = <value> ... "
+ " } "
+ " } "
+ " ] "
+ " links = [ "
+ " { output = <portname> input = <portname> } ... "
+ " ] "
+ " inputs = [ <portname> ... ] "
+ " outputs = [ <portname> ... ] "
+ "] "
+ "[ capture.props=<properties> ] "
+ "[ playback.props=<properties> ] " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/pipewire.h>
+
+#define MAX_HNDL 64
+#define MAX_SAMPLES 8192
+
+static float silence_data[MAX_SAMPLES];
+static float discard_data[MAX_SAMPLES];
+
+struct plugin {
+ struct spa_list link;
+ int ref;
+ char type[64];
+ char path[PATH_MAX];
+
+ struct fc_plugin *plugin;
+ struct spa_list descriptor_list;
+};
+
+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;
+ 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;
+};
+
+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 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 impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct dsp_ops dsp;
+
+ struct spa_list plugin_list;
+
+ struct pw_properties *capture_props;
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+ struct spa_audio_info_raw capture_info;
+
+ struct pw_properties *playback_props;
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+ struct spa_audio_info_raw playback_info;
+
+ unsigned int do_disconnect:1;
+
+ long unsigned rate;
+
+ struct graph graph;
+};
+
+static int graph_instantiate(struct graph *graph);
+static void graph_cleanup(struct graph *graph);
+
+
+static void capture_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->capture_listener);
+ impl->capture = NULL;
+}
+
+static void capture_process(void *d)
+{
+ struct impl *impl = d;
+ pw_stream_trigger_process(impl->playback);
+}
+
+static void playback_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *in, *out;
+ struct graph *graph = &impl->graph;
+ uint32_t i, j, insize = 0, outsize = 0, n_hndl = graph->n_hndl;
+ int32_t stride = 0;
+ struct graph_port *port;
+ struct spa_data *bd;
+
+ if ((in = pw_stream_dequeue_buffer(impl->capture)) == NULL)
+ pw_log_debug("%p: out of capture buffers: %m", impl);
+
+ if ((out = pw_stream_dequeue_buffer(impl->playback)) == NULL)
+ pw_log_debug("%p: out of playback buffers: %m", impl);
+
+ if (in == NULL || out == NULL)
+ goto done;
+
+ for (i = 0, j = 0; i < in->buffer->n_datas; i++) {
+ uint32_t offs, size;
+
+ bd = &in->buffer->datas[i];
+
+ 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;
+
+ }
+ insize = i == 0 ? size : SPA_MIN(insize, size);
+ stride = SPA_MAX(stride, bd->chunk->stride);
+ }
+ outsize = insize;
+
+ for (i = 0; i < out->buffer->n_datas; i++) {
+ bd = &out->buffer->datas[i];
+
+ outsize = SPA_MIN(outsize, bd->maxsize);
+
+ port = i < graph->n_output ? &graph->output[i] : NULL;
+
+ if (port && port->desc)
+ port->desc->connect_port(*port->hndl, port->port, bd->data);
+ else
+ memset(bd->data, 0, outsize);
+
+ bd->chunk->offset = 0;
+ bd->chunk->size = outsize;
+ bd->chunk->stride = stride;
+ }
+
+ pw_log_trace_fp("%p: stride:%d in:%d out:%d requested:%"PRIu64" (%"PRIu64")", impl,
+ stride, insize, outsize, out->requested, out->requested * stride);
+
+ for (i = 0; i < n_hndl; i++) {
+ struct graph_hndl *hndl = &graph->hndl[i];
+ hndl->desc->run(*hndl->hndl, outsize / sizeof(float));
+ }
+
+done:
+ if (in != NULL)
+ pw_stream_queue_buffer(impl->capture, in);
+ if (out != NULL)
+ 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 : 48000;
+
+ 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, def);
+ } else {
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0);
+ spa_pod_builder_int(b, def);
+ spa_pod_builder_int(b, min);
+ spa_pod_builder_int(b, 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.0f ? false : true);
+ } else if (p->hint & FC_HINT_INTEGER) {
+ spa_pod_builder_int(b, port->control_data);
+ } else {
+ spa_pod_builder_float(b, port->control_data);
+ }
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static int set_control_value(struct node *node, const char *name, float *value)
+{
+ struct descriptor *desc;
+ struct port *port;
+ float old;
+
+ port = find_port(node, name, FC_PORT_INPUT | FC_PORT_CONTROL);
+ if (port == NULL)
+ return -ENOENT;
+
+ node = port->node;
+ desc = node->desc;
+
+ old = port->control_data;
+ port->control_data = value ? *value : desc->default_control[port->idx];
+ pw_log_info("control %d ('%s') from %f to %f", port->idx, name, old, port->control_data);
+ return old == port->control_data ? 0 : 1;
+}
+
+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 = 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 param_props_changed(struct impl *impl, const struct spa_pod *param)
+{
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ const struct spa_pod_prop *prop;
+ struct graph *graph = &impl->graph;
+ int changed = 0;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ if (prop->key == SPA_PROP_params)
+ changed += parse_params(graph, &prop->value);
+ }
+ if (changed > 0) {
+ 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 void param_latency_changed(struct impl *impl, const struct spa_pod *param)
+{
+ struct spa_latency_info latency;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ pw_stream_update_params(impl->capture, params, 1);
+ else
+ pw_stream_update_params(impl->playback, params, 1);
+}
+
+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;
+
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->playback, false);
+ pw_stream_flush(impl->capture, false);
+ graph_reset(graph);
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("module %p: unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("module %p: error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct graph *graph = &impl->graph;
+ int res;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ if (param == NULL) {
+ graph_cleanup(graph);
+ } else {
+ struct spa_audio_info_raw info;
+ spa_zero(info);
+ if ((res = spa_format_audio_raw_parse(param, &info)) < 0)
+ goto error;
+ if (info.rate == 0) {
+ res = -EINVAL;
+ goto error;
+ }
+ impl->rate = info.rate;
+ if ((res = graph_instantiate(graph)) < 0)
+ goto error;
+ }
+ break;
+ case SPA_PARAM_Props:
+ if (param != NULL)
+ param_props_changed(impl, param);
+ break;
+ case SPA_PARAM_Latency:
+ param_latency_changed(impl, param);
+ break;
+ }
+ return;
+
+error:
+ pw_stream_set_error(impl->capture, res, "can't start graph: %s",
+ spa_strerror(res));
+}
+
+static const struct pw_stream_events in_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = capture_destroy,
+ .process = capture_process,
+ .state_changed = state_changed,
+ .param_changed = param_changed
+};
+
+static void playback_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->playback_listener);
+ impl->playback = NULL;
+}
+
+static const struct pw_stream_events out_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = playback_destroy,
+ .process = playback_process,
+ .state_changed = state_changed,
+ .param_changed = param_changed
+};
+
+static int setup_streams(struct impl *impl)
+{
+ int res;
+ uint32_t i, n_params, *offs;
+ struct pw_array offsets;
+ const struct spa_pod **params = NULL;
+ struct spa_pod_dynamic_builder b;
+ struct graph *graph = &impl->graph;
+
+ impl->capture = pw_stream_new(impl->core,
+ "filter capture", impl->capture_props);
+ impl->capture_props = NULL;
+ if (impl->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->capture,
+ &impl->capture_listener,
+ &in_stream_events, impl);
+
+ impl->playback = pw_stream_new(impl->core,
+ "filter playback", impl->playback_props);
+ impl->playback_props = NULL;
+ if (impl->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->playback,
+ &impl->playback_listener,
+ &out_stream_events, impl);
+
+ spa_pod_dynamic_builder_init(&b, NULL, 0, 4096);
+ pw_array_init(&offsets, 512);
+
+ if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) == NULL) {
+ res = -errno;
+ goto done;
+ }
+ *offs = b.b.state.offset;
+ spa_format_audio_raw_build(&b.b,
+ SPA_PARAM_EnumFormat, &impl->capture_info);
+
+ for (i = 0; i < graph->n_control; i++) {
+ if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL)
+ *offs = b.b.state.offset;
+ get_prop_info(graph, &b.b, i);
+ }
+
+ if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL)
+ *offs = b.b.state.offset;
+ get_props_param(graph, &b.b);
+
+ n_params = pw_array_get_len(&offsets, uint32_t);
+ if (n_params == 0) {
+ res = -ENOMEM;
+ goto done;
+ }
+ if ((params = calloc(n_params, sizeof(struct spa_pod*))) == NULL) {
+ res = -errno;
+ goto done;
+ }
+
+ offs = offsets.data;
+ for (i = 0; i < n_params; i++)
+ params[i] = spa_pod_builder_deref(&b.b, offs[i]);
+
+ res = pw_stream_connect(impl->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params);
+
+ spa_pod_dynamic_builder_clean(&b);
+ if (res < 0)
+ goto done;
+
+ n_params = 0;
+ spa_pod_dynamic_builder_init(&b, NULL, 0, 4096);
+ params[n_params++] = spa_format_audio_raw_build(&b.b,
+ SPA_PARAM_EnumFormat, &impl->playback_info);
+
+ res = pw_stream_connect(impl->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS |
+ PW_STREAM_FLAG_TRIGGER,
+ params, n_params);
+ spa_pod_dynamic_builder_clean(&b);
+
+done:
+ free(params);
+ pw_array_clear(&offsets);
+
+ return res < 0 ? res : 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)
+{
+ if (--hndl->ref > 0)
+ return;
+
+ fc_plugin_free(hndl->plugin);
+
+ spa_list_remove(&hndl->link);
+ free(hndl);
+}
+
+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;
+
+ 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);
+
+ if (spa_streq(type, "builtin")) {
+ pl = load_builtin_plugin(support, n_support, &impl->dsp, path, NULL);
+ }
+ else if (spa_streq(type, "ladspa")) {
+ pl = load_ladspa_plugin(support, n_support, &impl->dsp, path, NULL);
+ }
+ else if (spa_streq(type, "lv2")) {
+#ifdef HAVE_LILV
+ pl = load_lv2_plugin(support, n_support, &impl->dsp, path, NULL);
+#else
+ pw_log_error("filter-chain is compiled without lv2 support");
+ pl = NULL;
+ errno = ENOTSUP;
+#endif
+ } else {
+ pw_log_error("invalid plugin type '%s'", type);
+ pl = NULL;
+ errno = EINVAL;
+ }
+ 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'", 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;
+ }
+ }
+
+ 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)
+{
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(config, &val)) <= 0)
+ return len;
+
+ if (spa_json_is_null(val, len))
+ return 0;
+
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(config, val, len);
+
+ if ((node->config = malloc(len+1)) == NULL)
+ return -errno;
+
+ spa_json_parse_stringn(val, len, node->config, len+1);
+
+ return 0;
+}
+
+/**
+ * {
+ * "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_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 if (spa_json_next(json, &val) < 0)
+ break;
+ }
+ def_node = spa_list_first(&graph->node_list, struct node, link);
+ if ((out_port = find_port(def_node, output, FC_PORT_OUTPUT)) == NULL) {
+ pw_log_error("unknown output port %s", output);
+ return -ENOENT;
+ }
+ def_node = spa_list_last(&graph->node_list, struct node, link);
+ if ((in_port = find_port(def_node, input, FC_PORT_INPUT)) == 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);
+}
+
+/**
+ * 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 if (spa_json_next(json, &val) < 0)
+ break;
+ }
+
+ if (spa_streq(type, "builtin"))
+ snprintf(plugin, sizeof(plugin), "%s", "builtin");
+
+ 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 = 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;
+ if (d->deactivate)
+ d->deactivate(node->hndl[i]);
+ d->cleanup(node->hndl[i]);
+ node->hndl[i] = NULL;
+ }
+}
+
+static int port_ensure_data(struct port *port, uint32_t i)
+{
+ float *data;
+ if ((data = port->audio_data[i]) == NULL) {
+ data = calloc(1, MAX_SAMPLES * sizeof(float));
+ if (data == NULL) {
+ pw_log_error("cannot create port data: %m");
+ return -errno;
+ }
+ }
+ port->audio_data[i] = data;
+ return 0;
+}
+
+static void port_free_data(struct port *port, uint32_t i)
+{
+ free(port->audio_data[i]);
+ port->audio_data[i] = NULL;
+}
+
+static void node_free(struct node *node)
+{
+ uint32_t i, j;
+
+ spa_list_remove(&node->link);
+ for (i = 0; i < node->n_hndl; i++) {
+ for (j = 0; j < node->desc->n_output; j++)
+ port_free_data(&node->output_port[j], i);
+ }
+ node_cleanup(node);
+ descriptor_unref(node->desc);
+ free(node->input_port);
+ free(node->output_port);
+ free(node->control_port);
+ free(node->notify_port);
+ free(node);
+}
+
+static void graph_cleanup(struct graph *graph)
+{
+ struct node *node;
+ 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;
+ uint32_t i, j;
+ int res;
+
+ spa_list_for_each(node, &graph->node_list, link) {
+ float *sd = silence_data, *dd = discard_data;
+
+ node_cleanup(node);
+
+ desc = node->desc;
+ d = desc->desc;
+ if (d->flags & FC_DESCRIPTOR_SUPPORTS_NULL_DATA)
+ sd = dd = NULL;
+
+ for (i = 0; i < node->n_hndl; i++) {
+ pw_log_info("instantiate %s %d rate:%lu", d->name, i, impl->rate);
+ if ((node->hndl[i] = d->instantiate(d, impl->rate, i, node->config)) == NULL) {
+ pw_log_error("cannot create plugin instance: %m");
+ res = -errno;
+ goto error;
+ }
+ for (j = 0; j < desc->n_input; j++) {
+ port = &node->input_port[j];
+ d->connect_port(node->hndl[i], port->p, sd);
+
+ spa_list_for_each(link, &port->link_list, input_link) {
+ struct port *peer = link->output;
+ if ((res = port_ensure_data(peer, i)) < 0)
+ goto error;
+ pw_log_info("connect input port %s[%d]:%s %p",
+ node->name, i, d->ports[port->p].name,
+ peer->audio_data[i]);
+ d->connect_port(node->hndl[i], port->p, peer->audio_data[i]);
+ }
+ }
+ for (j = 0; j < desc->n_output; j++) {
+ port = &node->output_port[j];
+ if ((res = port_ensure_data(port, i)) < 0)
+ goto error;
+ pw_log_info("connect output port %s[%d]:%s %p",
+ node->name, i, d->ports[port->p].name,
+ port->audio_data[i]);
+ d->connect_port(node->hndl[i], port->p, port->audio_data[i]);
+ }
+ for (j = 0; j < desc->n_control; j++) {
+ port = &node->control_port[j];
+ d->connect_port(node->hndl[i], port->p, &port->control_data);
+ }
+ for (j = 0; j < desc->n_notify; j++) {
+ port = &node->notify_port[j];
+ d->connect_port(node->hndl[i], port->p, &port->control_data);
+ }
+ if (d->activate)
+ d->activate(node->hndl[i]);
+ }
+ }
+ return 0;
+error:
+ graph_cleanup(graph);
+ return res;
+}
+
+static 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;
+ }
+
+ if (impl->capture_info.channels == 0)
+ impl->capture_info.channels = n_input;
+ 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++;
+ }
+ 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 {
+ 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;
+ }
+ if (gp != NULL)
+ gp->next = false;
+ }
+ port->node->disabled = true;
+ } else {
+ 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;
+
+ 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--;
+ }
+
+ /* 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 = [ ]
+ * }
+ */
+static int load_graph(struct graph *graph, struct pw_properties *props)
+{
+ struct spa_json it[3];
+ struct spa_json inputs, outputs, *pinputs = NULL, *poutputs = 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);
+
+ if ((json = pw_properties_get(props, "filter.graph")) == NULL) {
+ pw_log_error("missing filter.graph property");
+ return -EINVAL;
+ }
+
+ 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;
+ }
+
+ 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_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;
+ }
+ }
+ 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 void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ if (res == -ENOENT) {
+ pw_log_info("message id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ } else {
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ }
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ /* disconnect both streams before destroying any of them */
+ if (impl->capture)
+ pw_stream_disconnect(impl->capture);
+ if (impl->playback)
+ pw_stream_disconnect(impl->playback);
+
+ if (impl->capture)
+ pw_stream_destroy(impl->capture);
+ if (impl->playback)
+ pw_stream_destroy(impl->playback);
+
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->capture_props);
+ pw_properties_free(impl->playback_props);
+ graph_free(&impl->graph);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static 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));
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->capture_props, key) == NULL)
+ pw_properties_set(impl->capture_props, key, str);
+ if (pw_properties_get(impl->playback_props, key) == NULL)
+ pw_properties_set(impl->playback_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct 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;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->capture_props = pw_properties_new(NULL, NULL);
+ impl->playback_props = pw_properties_new(NULL, NULL);
+ if (impl->capture_props == NULL || impl->playback_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+ impl->graph.impl = impl;
+
+ spa_list_init(&impl->plugin_list);
+
+ support = pw_context_get_support(impl->context, &n_support);
+
+ cpu_iface = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+ impl->dsp.cpu_flags = cpu_iface ? spa_cpu_get_flags(cpu_iface) : 0;
+ dsp_ops_init(&impl->dsp);
+
+ if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_GROUP, "filter-chain-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "filter-chain-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, "resample.prefill") == NULL)
+ pw_properties_set(props, "resample.prefill", "true");
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "filter-chain-%u-%u", pid, id);
+
+ if ((str = pw_properties_get(props, "capture.props")) != NULL)
+ pw_properties_update_string(impl->capture_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "playback.props")) != NULL)
+ pw_properties_update_string(impl->playback_props, str, strlen(str));
+
+ 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_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_NAME);
+ copy_props(impl, props, "resample.prefill");
+
+ parse_audio_info(impl->capture_props, &impl->capture_info);
+ parse_audio_info(impl->playback_props, &impl->playback_info);
+
+ if (impl->capture_info.rate && !impl->playback_info.rate)
+ impl->playback_info.rate = impl->capture_info.rate;
+ else if (impl->playback_info.rate && !impl->capture_info.rate)
+ impl->capture_info.rate = !impl->playback_info.rate;
+ else if (impl->capture_info.rate != impl->playback_info.rate) {
+ pw_log_warn("Both capture and playback rate are set, but"
+ " they are different. Using the highest of two. This behaviour"
+ " is deprecated, please use equal rates in the module config");
+ impl->playback_info.rate = impl->capture_info.rate =
+ SPA_MAX(impl->playback_info.rate, impl->capture_info.rate);
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_NAME)) == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_NAME,
+ "filter-chain-%u-%u", pid, id);
+ str = pw_properties_get(props, PW_KEY_NODE_NAME);
+ }
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(impl->capture_props, PW_KEY_NODE_NAME,
+ "input.%s", str);
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(impl->playback_props, PW_KEY_NODE_NAME,
+ "output.%s", str);
+
+ if (pw_properties_get(impl->capture_props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_setf(impl->capture_props, PW_KEY_MEDIA_NAME, "%s input",
+ pw_properties_get(impl->capture_props, PW_KEY_NODE_DESCRIPTION));
+ if (pw_properties_get(impl->playback_props, PW_KEY_MEDIA_NAME) == NULL)
+ 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));
+ goto error;
+ }
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+ pw_properties_free(props);
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ setup_streams(impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ pw_properties_free(props);
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-filter-chain/biquad.c b/src/modules/module-filter-chain/biquad.c
new file mode 100644
index 0000000..58c8dd0
--- /dev/null
+++ b/src/modules/module-filter-chain/biquad.c
@@ -0,0 +1,364 @@
+/* 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 = b0 * a0_inv;
+ bq->b1 = b1 * a0_inv;
+ bq->b2 = b2 * a0_inv;
+ bq->a1 = a1 * a0_inv;
+ bq->a2 = 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/biquad.h b/src/modules/module-filter-chain/biquad.h
new file mode 100644
index 0000000..650b263
--- /dev/null
+++ b/src/modules/module-filter-chain/biquad.h
@@ -0,0 +1,57 @@
+/* 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.
+ */
+
+#ifndef BIQUAD_H_
+#define BIQUAD_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* 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
+ * y2.
+ *
+ * We use double during the coefficients calculation for better accuracy, but
+ * float is used during the actual filtering for faster computation.
+ */
+struct biquad {
+ 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.
+ * Args:
+ * bq - The biquad filter we want to set.
+ * 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, double Q,
+ double gain);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* BIQUAD_H_ */
diff --git a/src/modules/module-filter-chain/builtin_plugin.c b/src/modules/module-filter-chain/builtin_plugin.c
new file mode 100644
index 0000000..068df99
--- /dev/null
+++ b/src/modules/module-filter-chain/builtin_plugin.c
@@ -0,0 +1,1035 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <float.h>
+#include <math.h>
+#ifdef HAVE_SNDFILE
+#include <sndfile.h>
+#endif
+
+#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
+
+static struct dsp_ops *dsp_ops;
+
+struct builtin {
+ unsigned long rate;
+ float *port[64];
+
+ struct biquad bq;
+ float freq;
+ float Q;
+ float gain;
+};
+
+static void *builtin_instantiate(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->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(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(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,
+};
+
+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,
+ },
+};
+
+static void bq_run(struct builtin *impl, unsigned long samples, int type)
+{
+ struct biquad *bq = &impl->bq;
+ float *out = impl->port[0];
+ float *in = impl->port[1];
+ 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) {
+ impl->freq = freq;
+ impl->Q = Q;
+ impl->gain = gain;
+ biquad_set(bq, type, freq * 2 / impl->rate, Q, gain);
+ }
+ dsp_ops_biquad_run(dsp_ops, bq, out, in, samples);
+}
+
+/** bq_lowpass */
+static void bq_lowpass_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_LOWPASS);
+}
+
+static const struct fc_descriptor bq_lowpass_desc = {
+ .name = "bq_lowpass",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_lowpass_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_highpass */
+static void bq_highpass_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_HIGHPASS);
+}
+
+static const struct fc_descriptor bq_highpass_desc = {
+ .name = "bq_highpass",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_highpass_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_bandpass */
+static void bq_bandpass_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_BANDPASS);
+}
+
+static const struct fc_descriptor bq_bandpass_desc = {
+ .name = "bq_bandpass",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_bandpass_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_lowshelf */
+static void bq_lowshelf_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_LOWSHELF);
+}
+
+static const struct fc_descriptor bq_lowshelf_desc = {
+ .name = "bq_lowshelf",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_lowshelf_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_highshelf */
+static void bq_highshelf_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_HIGHSHELF);
+}
+
+static const struct fc_descriptor bq_highshelf_desc = {
+ .name = "bq_highshelf",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_highshelf_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_peaking */
+static void bq_peaking_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_PEAKING);
+}
+
+static const struct fc_descriptor bq_peaking_desc = {
+ .name = "bq_peaking",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_peaking_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** bq_notch */
+static void bq_notch_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_NOTCH);
+}
+
+static const struct fc_descriptor bq_notch_desc = {
+ .name = "bq_notch",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_notch_run,
+ .cleanup = builtin_cleanup,
+};
+
+
+/** bq_allpass */
+static void bq_allpass_run(void * Instance, unsigned long SampleCount)
+{
+ struct builtin *impl = Instance;
+ bq_run(impl, SampleCount, BQ_ALLPASS);
+}
+
+static const struct fc_descriptor bq_allpass_desc = {
+ .name = "bq_allpass",
+
+ .n_ports = 5,
+ .ports = bq_ports,
+
+ .instantiate = builtin_instantiate,
+ .connect_port = builtin_connect_port,
+ .run = bq_allpass_run,
+ .cleanup = builtin_cleanup,
+};
+
+/** convolve */
+struct convolver_impl {
+ 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;
+
+ 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])
+ 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);
+ }
+ }
+
+ pw_log_debug("loading %s", filenames[best]);
+ float *samples = read_samples_from_sf(fs[best], infos[best], gain, delay,
+ offset, length, channel, rate, n_samples);
+
+ for (i = 0; i < MAX_RATES; i++)
+ if (fs[i])
+ 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 / M_PI;
+ h = length / 2;
+ for (i = 1; i < h; i += 2) {
+ v = (gain / i) * (0.43f + 0.57f * cosf(i * 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(float *samples, int *n_samples,
+ unsigned long in_rate, unsigned long out_rate, uint32_t quality)
+{
+ 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;
+}
+
+static void * convolver_instantiate(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, 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)
+ return NULL;
+
+ spa_json_init(&it[0], config, strlen(config));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+ 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[i] = 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 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)
+ samples = resample_buffer(samples, &n_samples,
+ rate, SampleRate, resample_quality);
+ }
+ if (samples == NULL) {
+ errno = ENOENT;
+ return NULL;
+ }
+
+ for (i = 0; i < MAX_RATES; i++)
+ if (filenames[i])
+ free(filenames[i]);
+
+ 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->rate = SampleRate;
+
+ impl->conv = convolver_new(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 {
+ 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_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) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_json_init(&it[0], config, strlen(config));
+ if (spa_json_enter_object(&it[0], &it[1]) <= 0)
+ 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 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->rate = SampleRate;
+ impl->buffer_samples = 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(delay * impl->rate, 0, 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,
+};
+
+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;
+ }
+ 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 struct fc_plugin builtin_plugin = {
+ .make_desc = builtin_make_desc
+};
+
+struct fc_plugin *load_builtin_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *plugin, const char *config)
+{
+ dsp_ops = dsp;
+ pffft_select_cpu(dsp->cpu_flags);
+ return &builtin_plugin;
+}
diff --git a/src/modules/module-filter-chain/convolver.c b/src/modules/module-filter-chain/convolver.c
new file mode 100644
index 0000000..6bed5b1
--- /dev/null
+++ b/src/modules/module-filter-chain/convolver.c
@@ -0,0 +1,425 @@
+/* PipeWire
+ *
+ * Copyright (c) 2017 HiFi-LoFi
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * Adapted from https://github.com/HiFi-LoFi/FFTConvolver
+ */
+#include "convolver.h"
+
+#include <spa/utils/defs.h>
+
+#include <math.h>
+
+static struct dsp_ops *dsp;
+
+struct convolver1 {
+ int blockSize;
+ int segSize;
+ int segCount;
+ int fftComplexSize;
+
+ float **segments;
+ float **segmentsIr;
+
+ float *fft_buffer;
+
+ void *fft;
+ void *ifft;
+
+ float *pre_mult;
+ float *conv;
+ float *overlap;
+
+ float *inputBuffer;
+ int inputBufferFill;
+
+ int current;
+ 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(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;
+ while (r < val)
+ r *= 2;
+ return r;
+}
+
+static void convolver1_reset(struct convolver1 *conv)
+{
+ int i;
+ for (i = 0; i < conv->segCount; i++)
+ fft_cpx_clear(conv->segments[i], conv->fftComplexSize);
+ dsp_ops_clear(dsp, conv->overlap, conv->blockSize);
+ dsp_ops_clear(dsp, conv->inputBuffer, conv->segSize);
+ fft_cpx_clear(conv->pre_mult, conv->fftComplexSize);
+ fft_cpx_clear(conv->conv, conv->fftComplexSize);
+ conv->inputBufferFill = 0;
+ conv->current = 0;
+}
+
+static struct convolver1 *convolver1_new(int block, const float *ir, int irlen)
+{
+ struct convolver1 *conv;
+ int i;
+
+ if (block == 0)
+ return NULL;
+
+ while (irlen > 0 && fabs(ir[irlen-1]) < 0.000001f)
+ irlen--;
+
+ conv = calloc(1, sizeof(*conv));
+ if (conv == NULL)
+ return NULL;
+
+ if (irlen == 0)
+ return conv;
+
+ 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(dsp, conv->segSize, true);
+ if (conv->fft == NULL)
+ goto error;
+ conv->ifft = dsp_ops_fft_new(dsp, conv->segSize, true);
+ if (conv->ifft == NULL)
+ goto error;
+
+ conv->fft_buffer = fft_alloc(conv->segSize);
+ if (conv->fft_buffer == NULL)
+ goto error;
+
+ conv->segments = calloc(sizeof(float*), conv->segCount);
+ conv->segmentsIr = calloc(sizeof(float*), conv->segCount);
+
+ 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);
+
+ dsp_ops_copy(dsp, conv->fft_buffer, &ir[i * conv->blockSize], copy);
+ if (copy < conv->segSize)
+ dsp_ops_clear(dsp, conv->fft_buffer + copy, conv->segSize - copy);
+
+ dsp_ops_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->scale = 1.0f / conv->segSize;
+ convolver1_reset(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);
+ 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(dsp, conv->fft);
+ if (conv->ifft)
+ dsp_ops_fft_free(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)
+{
+ int i, processed = 0;
+
+ if (conv == NULL || conv->segCount == 0) {
+ dsp_ops_clear(dsp, output, len);
+ return len;
+ }
+
+ while (processed < len) {
+ const int processing = SPA_MIN(len - processed, conv->blockSize - conv->inputBufferFill);
+ const int inputBufferPos = conv->inputBufferFill;
+
+ dsp_ops_copy(dsp, conv->inputBuffer + inputBufferPos, input + processed, processing);
+ if (inputBufferPos == 0 && processing < conv->blockSize)
+ dsp_ops_clear(dsp, conv->inputBuffer + processing, conv->blockSize - processing);
+
+ dsp_ops_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(dsp, conv->fft, conv->pre_mult,
+ conv->segmentsIr[1],
+ conv->segments[indexAudio],
+ conv->fftComplexSize, conv->scale);
+
+ for (i = 2; i < conv->segCount; i++) {
+ indexAudio = (conv->current + i) % conv->segCount;
+
+ dsp_ops_fft_cmuladd(dsp, conv->fft,
+ conv->pre_mult,
+ conv->pre_mult,
+ conv->segmentsIr[i],
+ conv->segments[indexAudio],
+ conv->fftComplexSize, conv->scale);
+ }
+ }
+ dsp_ops_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(dsp, conv->fft,
+ conv->conv,
+ conv->segments[conv->current],
+ conv->segmentsIr[0],
+ conv->fftComplexSize, conv->scale);
+ }
+
+ dsp_ops_fft_run(dsp, conv->ifft, -1, conv->conv, conv->fft_buffer);
+
+ dsp_ops_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(dsp, conv->overlap, conv->fft_buffer + conv->blockSize, conv->blockSize);
+
+ conv->current = (conv->current > 0) ? (conv->current - 1) : (conv->segCount - 1);
+ }
+
+ processed += processing;
+ }
+ return len;
+}
+
+struct convolver
+{
+ int headBlockSize;
+ int tailBlockSize;
+ struct convolver1 *headConvolver;
+ struct convolver1 *tailConvolver0;
+ float *tailOutput0;
+ float *tailPrecalculated0;
+ struct convolver1 *tailConvolver;
+ float *tailOutput;
+ float *tailPrecalculated;
+ float *tailInput;
+ int tailInputFill;
+ int precalculatedPos;
+};
+
+void convolver_reset(struct convolver *conv)
+{
+ if (conv->headConvolver)
+ convolver1_reset(conv->headConvolver);
+ if (conv->tailConvolver0) {
+ convolver1_reset(conv->tailConvolver0);
+ dsp_ops_clear(dsp, conv->tailOutput0, conv->tailBlockSize);
+ dsp_ops_clear(dsp, conv->tailPrecalculated0, conv->tailBlockSize);
+ }
+ if (conv->tailConvolver) {
+ convolver1_reset(conv->tailConvolver);
+ dsp_ops_clear(dsp, conv->tailOutput, conv->tailBlockSize);
+ dsp_ops_clear(dsp, conv->tailPrecalculated, conv->tailBlockSize);
+ }
+ 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 *conv;
+ int head_ir_len;
+
+ dsp = dsp_ops;
+
+ if (head_block == 0 || tail_block == 0)
+ return NULL;
+
+ head_block = SPA_MAX(1, head_block);
+ if (head_block > tail_block)
+ SPA_SWAP(head_block, tail_block);
+
+ while (irlen > 0 && fabs(ir[irlen-1]) < 0.000001f)
+ irlen--;
+
+ conv = calloc(1, sizeof(*conv));
+ if (conv == NULL)
+ return NULL;
+
+ if (irlen == 0)
+ return conv;
+
+ 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(conv->headBlockSize, ir, head_ir_len);
+
+ if (irlen > conv->tailBlockSize) {
+ int conv1IrLen = SPA_MIN(irlen - conv->tailBlockSize, conv->tailBlockSize);
+ conv->tailConvolver0 = convolver1_new(conv->headBlockSize, ir + conv->tailBlockSize, conv1IrLen);
+ conv->tailOutput0 = fft_alloc(conv->tailBlockSize);
+ conv->tailPrecalculated0 = fft_alloc(conv->tailBlockSize);
+ }
+
+ if (irlen > 2 * conv->tailBlockSize) {
+ int tailIrLen = irlen - (2 * conv->tailBlockSize);
+ conv->tailConvolver = convolver1_new(conv->tailBlockSize, ir + (2 * conv->tailBlockSize), tailIrLen);
+ conv->tailOutput = fft_alloc(conv->tailBlockSize);
+ conv->tailPrecalculated = fft_alloc(conv->tailBlockSize);
+ }
+
+ if (conv->tailConvolver0 || conv->tailConvolver)
+ conv->tailInput = fft_alloc(conv->tailBlockSize);
+
+ convolver_reset(conv);
+
+ return conv;
+}
+
+void convolver_free(struct convolver *conv)
+{
+ if (conv->headConvolver)
+ convolver1_free(conv->headConvolver);
+ if (conv->tailConvolver0)
+ convolver1_free(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);
+ free(conv);
+}
+
+int convolver_run(struct convolver *conv, const float *input, float *output, int length)
+{
+ convolver1_run(conv->headConvolver, input, output, length);
+
+ if (conv->tailInput) {
+ int processed = 0;
+
+ while (processed < length) {
+ int remaining = length - processed;
+ int processing = SPA_MIN(remaining, conv->headBlockSize - (conv->tailInputFill % conv->headBlockSize));
+
+ if (conv->tailPrecalculated0)
+ dsp_ops_sum(dsp, &output[processed], &output[processed],
+ &conv->tailPrecalculated0[conv->precalculatedPos],
+ processing);
+ if (conv->tailPrecalculated)
+ dsp_ops_sum(dsp, &output[processed], &output[processed],
+ &conv->tailPrecalculated[conv->precalculatedPos],
+ processing);
+ conv->precalculatedPos += processing;
+
+ dsp_ops_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,
+ conv->tailInput + blockOffset,
+ conv->tailOutput0 + blockOffset,
+ conv->headBlockSize);
+ if (conv->tailInputFill == conv->tailBlockSize)
+ SPA_SWAP(conv->tailPrecalculated0, conv->tailOutput0);
+ }
+
+ if (conv->tailPrecalculated &&
+ conv->tailInputFill == conv->tailBlockSize) {
+ SPA_SWAP(conv->tailPrecalculated, conv->tailOutput);
+ convolver1_run(conv->tailConvolver, conv->tailInput,
+ conv->tailOutput, conv->tailBlockSize);
+ }
+ if (conv->tailInputFill == conv->tailBlockSize) {
+ conv->tailInputFill = 0;
+ conv->precalculatedPos = 0;
+ }
+ processed += processing;
+ }
+ }
+ return 0;
+}
diff --git a/src/modules/module-filter-chain/convolver.h b/src/modules/module-filter-chain/convolver.h
new file mode 100644
index 0000000..1988114
--- /dev/null
+++ b/src/modules/module-filter-chain/convolver.h
@@ -0,0 +1,34 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include "dsp-ops.h"
+
+struct convolver *convolver_new(struct dsp_ops *dsp, int block, int tail, const float *ir, int irlen);
+void convolver_free(struct convolver *conv);
+
+void convolver_reset(struct convolver *conv);
+int convolver_run(struct convolver *conv, const float *input, float *output, int length);
diff --git a/src/modules/module-filter-chain/dsp-ops-avx.c b/src/modules/module-filter-chain/dsp-ops-avx.c
new file mode 100644
index 0000000..7ea5456
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops-avx.c
@@ -0,0 +1,85 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "dsp-ops.h"
+
+#include <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
new file mode 100644
index 0000000..a913f5a
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops-c.c
@@ -0,0 +1,174 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <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;
+ for (i = 0; i < n_samples; i++)
+ d[i] = s[i] * gain;
+}
+
+static inline void dsp_gain_add_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, float gain, uint32_t n_samples)
+{
+ uint32_t i;
+ const float *s = src;
+ float *d = dst;
+ for (i = 0; i < n_samples; i++)
+ d[i] += s[i] * gain;
+}
+
+
+void dsp_copy_c(struct dsp_ops *ops, void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src, uint32_t n_samples)
+{
+ if (dst != src)
+ spa_memcpy(dst, src, sizeof(float) * n_samples);
+}
+
+void dsp_mix_gain_c(struct dsp_ops *ops,
+ void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src[],
+ float gain[], uint32_t n_src, uint32_t n_samples)
+{
+ uint32_t i;
+ if (n_src == 0) {
+ dsp_clear_c(ops, dst, n_samples);
+ } else if (n_src == 1) {
+ if (dst != src[0])
+ dsp_copy_c(ops, dst, src[0], n_samples);
+ } else {
+ if (gain[0] == 1.0f)
+ dsp_copy_c(ops, dst, src[0], n_samples);
+ else
+ dsp_gain_c(ops, dst, src[0], gain[0], n_samples);
+
+ for (i = 1; i < n_src; i++) {
+ if (gain[i] == 1.0f)
+ dsp_add_c(ops, dst, src[i], n_samples);
+ else
+ dsp_gain_add_c(ops, dst, src[i], gain[i], n_samples);
+ }
+ }
+}
+
+void dsp_biquad_run_c(struct dsp_ops *ops, struct biquad *bq,
+ float *out, const float *in, uint32_t n_samples)
+{
+ float x1, x2, y1, y2;
+ float b0, b1, b2, a1, a2;
+ uint32_t i;
+
+ x1 = bq->x1;
+ x2 = bq->x2;
+ y1 = bq->y1;
+ y2 = bq->y2;
+ b0 = bq->b0;
+ b1 = bq->b1;
+ b2 = bq->b2;
+ a1 = bq->a1;
+ a2 = bq->a2;
+ for (i = 0; i < n_samples; i++) {
+ float x = in[i];
+ float y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
+ out[i] = y;
+ x2 = x1;
+ x1 = x;
+ y2 = y1;
+ y1 = y;
+ }
+#define F(x) (-FLT_MIN < (x) && (x) < FLT_MIN ? 0.0f : (x))
+ bq->x1 = F(x1);
+ bq->x2 = F(x2);
+ bq->y1 = F(y1);
+ bq->y2 = F(y2);
+#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_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
new file mode 100644
index 0000000..bcc3499
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops-sse.c
@@ -0,0 +1,142 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/utils/defs.h>
+
+#include "dsp-ops.h"
+
+#include <xmmintrin.h>
+
+void dsp_mix_gain_sse(struct dsp_ops *ops,
+ void * SPA_RESTRICT dst,
+ const void * SPA_RESTRICT src[],
+ float gain[], uint32_t n_src, uint32_t n_samples)
+{
+ if (n_src == 0) {
+ memset(dst, 0, n_samples * sizeof(float));
+ } else if (n_src == 1) {
+ if (dst != src[0])
+ spa_memcpy(dst, src[0], n_samples * sizeof(float));
+ } else {
+ uint32_t n, i, unrolled;
+ __m128 in[4], g;
+ const float **s = (const float **)src;
+ float *d = dst;
+
+ if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) {
+ unrolled = n_samples & ~15;
+ for (i = 0; i < n_src; i++) {
+ if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) {
+ unrolled = 0;
+ break;
+ }
+ }
+ } else
+ unrolled = 0;
+
+ for (n = 0; n < unrolled; n += 16) {
+ g = _mm_set1_ps(gain[0]);
+ in[0] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 0]));
+ in[1] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 4]));
+ in[2] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 8]));
+ in[3] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+12]));
+
+ for (i = 1; i < n_src; i++) {
+ g = _mm_set1_ps(gain[i]);
+ in[0] = _mm_add_ps(in[0], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 0])));
+ in[1] = _mm_add_ps(in[1], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 4])));
+ in[2] = _mm_add_ps(in[2], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 8])));
+ in[3] = _mm_add_ps(in[3], _mm_mul_ps(g, _mm_load_ps(&s[i][n+12])));
+ }
+ _mm_store_ps(&d[n+ 0], in[0]);
+ _mm_store_ps(&d[n+ 4], in[1]);
+ _mm_store_ps(&d[n+ 8], in[2]);
+ _mm_store_ps(&d[n+12], in[3]);
+ }
+ for (; n < n_samples; n++) {
+ g = _mm_set_ss(gain[0]);
+ in[0] = _mm_mul_ss(g, _mm_load_ss(&s[0][n]));
+ for (i = 1; i < n_src; i++) {
+ g = _mm_set_ss(gain[i]);
+ in[0] = _mm_add_ss(in[0], _mm_mul_ss(g, _mm_load_ss(&s[i][n])));
+ }
+ _mm_store_ss(&d[n], in[0]);
+ }
+ }
+}
+
+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.c b/src/modules/module-filter-chain/dsp-ops.c
new file mode 100644
index 0000000..d35d5c9
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops.c
@@ -0,0 +1,114 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <spa/support/cpu.h>
+#include <spa/utils/defs.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "dsp-ops.h"
+
+struct dsp_info {
+ uint32_t cpu_flags;
+
+ struct dsp_ops_funcs funcs;
+};
+
+static 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.sum = dsp_sum_avx,
+ .funcs.fft_new = dsp_fft_new_c,
+ .funcs.fft_free = dsp_fft_free_c,
+ .funcs.fft_run = dsp_fft_run_c,
+ .funcs.fft_cmul = dsp_fft_cmul_c,
+ .funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+ },
+#endif
+#if defined (HAVE_SSE)
+ { SPA_CPU_FLAG_SSE,
+ .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.sum = dsp_sum_sse,
+ .funcs.fft_new = dsp_fft_new_c,
+ .funcs.fft_free = dsp_fft_free_c,
+ .funcs.fft_run = dsp_fft_run_c,
+ .funcs.fft_cmul = dsp_fft_cmul_c,
+ .funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+ },
+#endif
+ { 0,
+ .funcs.clear = dsp_clear_c,
+ .funcs.copy = dsp_copy_c,
+ .funcs.mix_gain = dsp_mix_gain_c,
+ .funcs.biquad_run = dsp_biquad_run_c,
+ .funcs.sum = dsp_sum_c,
+ .funcs.fft_new = dsp_fft_new_c,
+ .funcs.fft_free = dsp_fft_free_c,
+ .funcs.fft_run = dsp_fft_run_c,
+ .funcs.fft_cmul = dsp_fft_cmul_c,
+ .funcs.fft_cmuladd = dsp_fft_cmuladd_c,
+ },
+};
+
+#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a)
+
+static const struct dsp_info *find_dsp_info(uint32_t cpu_flags)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(dsp_table, t) {
+ if (MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags))
+ return t;
+ }
+ return NULL;
+}
+
+static void impl_dsp_ops_free(struct dsp_ops *ops)
+{
+ spa_zero(*ops);
+}
+
+int dsp_ops_init(struct dsp_ops *ops)
+{
+ const struct dsp_info *info;
+
+ info = find_dsp_info(ops->cpu_flags);
+ if (info == NULL)
+ return -ENOTSUP;
+
+ ops->priv = info;
+ ops->free = impl_dsp_ops_free;
+ ops->funcs = info->funcs;
+
+ return 0;
+}
diff --git a/src/modules/module-filter-chain/dsp-ops.h b/src/modules/module-filter-chain/dsp-ops.h
new file mode 100644
index 0000000..bd77062
--- /dev/null
+++ b/src/modules/module-filter-chain/dsp-ops.h
@@ -0,0 +1,140 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#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);
+};
+
+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);
+
+#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_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_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_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.h b/src/modules/module-filter-chain/ladspa.h
new file mode 100644
index 0000000..b1a9c4e
--- /dev/null
+++ b/src/modules/module-filter-chain/ladspa.h
@@ -0,0 +1,603 @@
+/* ladspa.h
+
+ Linux Audio Developer's Simple Plugin API Version 1.1[LGPL].
+ Copyright (C) 2000-2002 Richard W.E. Furse, Paul Barton-Davis,
+ Stefan Westerfeld.
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public License
+ as published by the Free Software Foundation; either version 2.1 of
+ the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ USA. */
+
+#ifndef LADSPA_INCLUDED
+#define LADSPA_INCLUDED
+
+#define LADSPA_VERSION "1.1"
+#define LADSPA_VERSION_MAJOR 1
+#define LADSPA_VERSION_MINOR 1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*****************************************************************************/
+
+/* Overview:
+
+ There is a large number of synthesis packages in use or development
+ on the Linux platform at this time. This API (`The Linux Audio
+ Developer's Simple Plugin API') attempts to give programmers the
+ ability to write simple `plugin' audio processors in C/C++ and link
+ them dynamically (`plug') into a range of these packages (`hosts').
+ It should be possible for any host and any plugin to communicate
+ completely through this interface.
+
+ This API is deliberately short and simple. To achieve compatibility
+ with a range of promising Linux sound synthesis packages it
+ attempts to find the `greatest common divisor' in their logical
+ behaviour. Having said this, certain limiting decisions are
+ implicit, notably the use of a fixed type (LADSPA_Data) for all
+ data transfer and absence of a parameterised `initialisation'
+ phase. See below for the LADSPA_Data typedef.
+
+ Plugins are expected to distinguish between control and audio
+ data. Plugins have `ports' that are inputs or outputs for audio or
+ control data and each plugin is `run' for a `block' corresponding
+ to a short time interval measured in samples. Audio data is
+ communicated using arrays of LADSPA_Data, allowing a block of audio
+ to be processed by the plugin in a single pass. Control data is
+ communicated using single LADSPA_Data values. Control data has a
+ single value at the start of a call to the `run()' or `run_adding()'
+ function, and may be considered to remain this value for its
+ duration. The plugin may assume that all its input and output ports
+ have been connected to the relevant data location (see the
+ `connect_port()' function below) before it is asked to run.
+
+ Plugins will reside in shared object files suitable for dynamic
+ linking by dlopen() and family. The file will provide a number of
+ `plugin types' that can be used to instantiate actual plugins
+ (sometimes known as `plugin instances') that can be connected
+ together to perform tasks.
+
+ This API contains very limited error-handling. */
+
+/*****************************************************************************/
+
+/* Fundamental data type passed in and out of plugin. This data type
+ is used to communicate audio samples and control values. It is
+ assumed that the plugin will work sensibly given any numeric input
+ value although it may have a preferred range (see hints below).
+
+ For audio it is generally assumed that 1.0f is the `0dB' reference
+ amplitude and is a `normal' signal level. */
+
+typedef float LADSPA_Data;
+
+/*****************************************************************************/
+
+/* Special Plugin Properties:
+
+ Optional features of the plugin type are encapsulated in the
+ LADSPA_Properties type. This is assembled by ORing individual
+ properties together. */
+
+typedef int LADSPA_Properties;
+
+/* Property LADSPA_PROPERTY_REALTIME indicates that the plugin has a
+ real-time dependency (e.g. listens to a MIDI device) and so its
+ output must not be cached or subject to significant latency. */
+#define LADSPA_PROPERTY_REALTIME 0x1
+
+/* Property LADSPA_PROPERTY_INPLACE_BROKEN indicates that the plugin
+ may cease to work correctly if the host elects to use the same data
+ location for both input and output (see connect_port()). This
+ should be avoided as enabling this flag makes it impossible for
+ hosts to use the plugin to process audio `in-place.' */
+#define LADSPA_PROPERTY_INPLACE_BROKEN 0x2
+
+/* Property LADSPA_PROPERTY_HARD_RT_CAPABLE indicates that the plugin
+ is capable of running not only in a conventional host but also in a
+ `hard real-time' environment. To qualify for this the plugin must
+ satisfy all of the following:
+
+ (1) The plugin must not use malloc(), free() or other heap memory
+ management within its run() or run_adding() functions. All new
+ memory used in run() must be managed via the stack. These
+ restrictions only apply to the run() function.
+
+ (2) The plugin will not attempt to make use of any library
+ functions with the exceptions of functions in the ANSI standard C
+ and C maths libraries, which the host is expected to provide.
+
+ (3) The plugin will not access files, devices, pipes, sockets, IPC
+ or any other mechanism that might result in process or thread
+ blocking.
+
+ (4) The plugin will take an amount of time to execute a run() or
+ run_adding() call approximately of form (A+B*SampleCount) where A
+ and B depend on the machine and host in use. This amount of time
+ may not depend on input signals or plugin state. The host is left
+ the responsibility to perform timings to estimate upper bounds for
+ A and B. */
+#define LADSPA_PROPERTY_HARD_RT_CAPABLE 0x4
+
+#define LADSPA_IS_REALTIME(x) ((x) & LADSPA_PROPERTY_REALTIME)
+#define LADSPA_IS_INPLACE_BROKEN(x) ((x) & LADSPA_PROPERTY_INPLACE_BROKEN)
+#define LADSPA_IS_HARD_RT_CAPABLE(x) ((x) & LADSPA_PROPERTY_HARD_RT_CAPABLE)
+
+/*****************************************************************************/
+
+/* Plugin Ports:
+
+ Plugins have `ports' that are inputs or outputs for audio or
+ data. Ports can communicate arrays of LADSPA_Data (for audio
+ inputs/outputs) or single LADSPA_Data values (for control
+ input/outputs). This information is encapsulated in the
+ LADSPA_PortDescriptor type which is assembled by ORing individual
+ properties together.
+
+ Note that a port must be an input or an output port but not both
+ and that a port must be a control or audio port but not both. */
+
+typedef int LADSPA_PortDescriptor;
+
+/* Property LADSPA_PORT_INPUT indicates that the port is an input. */
+#define LADSPA_PORT_INPUT 0x1
+
+/* Property LADSPA_PORT_OUTPUT indicates that the port is an output. */
+#define LADSPA_PORT_OUTPUT 0x2
+
+/* Property LADSPA_PORT_CONTROL indicates that the port is a control
+ port. */
+#define LADSPA_PORT_CONTROL 0x4
+
+/* Property LADSPA_PORT_AUDIO indicates that the port is a audio
+ port. */
+#define LADSPA_PORT_AUDIO 0x8
+
+#define LADSPA_IS_PORT_INPUT(x) ((x) & LADSPA_PORT_INPUT)
+#define LADSPA_IS_PORT_OUTPUT(x) ((x) & LADSPA_PORT_OUTPUT)
+#define LADSPA_IS_PORT_CONTROL(x) ((x) & LADSPA_PORT_CONTROL)
+#define LADSPA_IS_PORT_AUDIO(x) ((x) & LADSPA_PORT_AUDIO)
+
+/*****************************************************************************/
+
+/* Plugin Port Range Hints:
+
+ The host may wish to provide a representation of data entering or
+ leaving a plugin (e.g. to generate a GUI automatically). To make
+ this more meaningful, the plugin should provide `hints' to the host
+ describing the usual values taken by the data.
+
+ Note that these are only hints. The host may ignore them and the
+ plugin must not assume that data supplied to it is meaningful. If
+ the plugin receives invalid input data it is expected to continue
+ to run without failure and, where possible, produce a sensible
+ output (e.g. a high-pass filter given a negative cutoff frequency
+ might switch to an all-pass mode).
+
+ Hints are meaningful for all input and output ports but hints for
+ input control ports are expected to be particularly useful.
+
+ More hint information is encapsulated in the
+ LADSPA_PortRangeHintDescriptor type which is assembled by ORing
+ individual hint types together. Hints may require further
+ LowerBound and UpperBound information.
+
+ All the hint information for a particular port is aggregated in the
+ LADSPA_PortRangeHint structure. */
+
+typedef int LADSPA_PortRangeHintDescriptor;
+
+/* Hint LADSPA_HINT_BOUNDED_BELOW indicates that the LowerBound field
+ of the LADSPA_PortRangeHint should be considered meaningful. The
+ value in this field should be considered the (inclusive) lower
+ bound of the valid range. If LADSPA_HINT_SAMPLE_RATE is also
+ specified then the value of LowerBound should be multiplied by the
+ sample rate. */
+#define LADSPA_HINT_BOUNDED_BELOW 0x1
+
+/* Hint LADSPA_HINT_BOUNDED_ABOVE indicates that the UpperBound field
+ of the LADSPA_PortRangeHint should be considered meaningful. The
+ value in this field should be considered the (inclusive) upper
+ bound of the valid range. If LADSPA_HINT_SAMPLE_RATE is also
+ specified then the value of UpperBound should be multiplied by the
+ sample rate. */
+#define LADSPA_HINT_BOUNDED_ABOVE 0x2
+
+/* Hint LADSPA_HINT_TOGGLED indicates that the data item should be
+ considered a Boolean toggle. Data less than or equal to zero should
+ be considered `off' or `false,' and data above zero should be
+ considered `on' or `true.' LADSPA_HINT_TOGGLED may not be used in
+ conjunction with any other hint except LADSPA_HINT_DEFAULT_0 or
+ LADSPA_HINT_DEFAULT_1. */
+#define LADSPA_HINT_TOGGLED 0x4
+
+/* Hint LADSPA_HINT_SAMPLE_RATE indicates that any bounds specified
+ should be interpreted as multiples of the sample rate. For
+ instance, a frequency range from 0Hz to the Nyquist frequency (half
+ the sample rate) could be requested by this hint in conjunction
+ with LowerBound = 0 and UpperBound = 0.5. Hosts that support bounds
+ at all must support this hint to retain meaning. */
+#define LADSPA_HINT_SAMPLE_RATE 0x8
+
+/* Hint LADSPA_HINT_LOGARITHMIC indicates that it is likely that the
+ user will find it more intuitive to view values using a logarithmic
+ scale. This is particularly useful for frequencies and gains. */
+#define LADSPA_HINT_LOGARITHMIC 0x10
+
+/* Hint LADSPA_HINT_INTEGER indicates that a user interface would
+ probably wish to provide a stepped control taking only integer
+ values. Any bounds set should be slightly wider than the actual
+ integer range required to avoid floating point rounding errors. For
+ instance, the integer set {0,1,2,3} might be described as [-0.1,
+ 3.1]. */
+#define LADSPA_HINT_INTEGER 0x20
+
+/* The various LADSPA_HINT_HAS_DEFAULT_* hints indicate a `normal'
+ value for the port that is sensible as a default. For instance,
+ this value is suitable for use as an initial value in a user
+ interface or as a value the host might assign to a control port
+ when the user has not provided one. Defaults are encoded using a
+ mask so only one default may be specified for a port. Some of the
+ hints make use of lower and upper bounds, in which case the
+ relevant bound or bounds must be available and
+ LADSPA_HINT_SAMPLE_RATE must be applied as usual. The resulting
+ default must be rounded if LADSPA_HINT_INTEGER is present. Default
+ values were introduced in LADSPA v1.1. */
+#define LADSPA_HINT_DEFAULT_MASK 0x3C0
+
+/* This default values indicates that no default is provided. */
+#define LADSPA_HINT_DEFAULT_NONE 0x0
+
+/* This default hint indicates that the suggested lower bound for the
+ port should be used. */
+#define LADSPA_HINT_DEFAULT_MINIMUM 0x40
+
+/* This default hint indicates that a low value between the suggested
+ lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.75 +
+ log(upper) * 0.25). Otherwise, this should be (lower * 0.75 + upper
+ * 0.25). */
+#define LADSPA_HINT_DEFAULT_LOW 0x80
+
+/* This default hint indicates that a middle value between the
+ suggested lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.5 +
+ log(upper) * 0.5). Otherwise, this should be (lower * 0.5 + upper *
+ 0.5). */
+#define LADSPA_HINT_DEFAULT_MIDDLE 0xC0
+
+/* This default hint indicates that a high value between the suggested
+ lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.25 +
+ log(upper) * 0.75). Otherwise, this should be (lower * 0.25 + upper
+ * 0.75). */
+#define LADSPA_HINT_DEFAULT_HIGH 0x100
+
+/* This default hint indicates that the suggested upper bound for the
+ port should be used. */
+#define LADSPA_HINT_DEFAULT_MAXIMUM 0x140
+
+/* This default hint indicates that the number 0 should be used. Note
+ that this default may be used in conjunction with
+ LADSPA_HINT_TOGGLED. */
+#define LADSPA_HINT_DEFAULT_0 0x200
+
+/* This default hint indicates that the number 1 should be used. Note
+ that this default may be used in conjunction with
+ LADSPA_HINT_TOGGLED. */
+#define LADSPA_HINT_DEFAULT_1 0x240
+
+/* This default hint indicates that the number 100 should be used. */
+#define LADSPA_HINT_DEFAULT_100 0x280
+
+/* This default hint indicates that the Hz frequency of `concert A'
+ should be used. This will be 440 unless the host uses an unusual
+ tuning convention, in which case it may be within a few Hz. */
+#define LADSPA_HINT_DEFAULT_440 0x2C0
+
+#define LADSPA_IS_HINT_BOUNDED_BELOW(x) ((x) & LADSPA_HINT_BOUNDED_BELOW)
+#define LADSPA_IS_HINT_BOUNDED_ABOVE(x) ((x) & LADSPA_HINT_BOUNDED_ABOVE)
+#define LADSPA_IS_HINT_TOGGLED(x) ((x) & LADSPA_HINT_TOGGLED)
+#define LADSPA_IS_HINT_SAMPLE_RATE(x) ((x) & LADSPA_HINT_SAMPLE_RATE)
+#define LADSPA_IS_HINT_LOGARITHMIC(x) ((x) & LADSPA_HINT_LOGARITHMIC)
+#define LADSPA_IS_HINT_INTEGER(x) ((x) & LADSPA_HINT_INTEGER)
+
+#define LADSPA_IS_HINT_HAS_DEFAULT(x) ((x) & LADSPA_HINT_DEFAULT_MASK)
+#define LADSPA_IS_HINT_DEFAULT_MINIMUM(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MINIMUM)
+#define LADSPA_IS_HINT_DEFAULT_LOW(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_LOW)
+#define LADSPA_IS_HINT_DEFAULT_MIDDLE(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MIDDLE)
+#define LADSPA_IS_HINT_DEFAULT_HIGH(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_HIGH)
+#define LADSPA_IS_HINT_DEFAULT_MAXIMUM(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MAXIMUM)
+#define LADSPA_IS_HINT_DEFAULT_0(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_0)
+#define LADSPA_IS_HINT_DEFAULT_1(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_1)
+#define LADSPA_IS_HINT_DEFAULT_100(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_100)
+#define LADSPA_IS_HINT_DEFAULT_440(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_440)
+
+typedef struct _LADSPA_PortRangeHint {
+
+ /* Hints about the port. */
+ LADSPA_PortRangeHintDescriptor HintDescriptor;
+
+ /* Meaningful when hint LADSPA_HINT_BOUNDED_BELOW is active. When
+ LADSPA_HINT_SAMPLE_RATE is also active then this value should be
+ multiplied by the relevant sample rate. */
+ LADSPA_Data LowerBound;
+
+ /* Meaningful when hint LADSPA_HINT_BOUNDED_ABOVE is active. When
+ LADSPA_HINT_SAMPLE_RATE is also active then this value should be
+ multiplied by the relevant sample rate. */
+ LADSPA_Data UpperBound;
+
+} LADSPA_PortRangeHint;
+
+/*****************************************************************************/
+
+/* Plugin Handles:
+
+ This plugin handle indicates a particular instance of the plugin
+ concerned. It is valid to compare this to NULL (0 for C++) but
+ otherwise the host should not attempt to interpret it. The plugin
+ may use it to reference internal instance data. */
+
+typedef void * LADSPA_Handle;
+
+/*****************************************************************************/
+
+/* Descriptor for a Type of Plugin:
+
+ This structure is used to describe a plugin type. It provides a
+ number of functions to examine the type, instantiate it, link it to
+ buffers and workspaces and to run it. */
+
+typedef struct _LADSPA_Descriptor {
+
+ /* This numeric identifier indicates the plugin type
+ uniquely. Plugin programmers may reserve ranges of IDs from a
+ central body to avoid clashes. Hosts may assume that IDs are
+ below 0x1000000. */
+ unsigned long UniqueID;
+
+ /* This identifier can be used as a unique, case-sensitive
+ identifier for the plugin type within the plugin file. Plugin
+ types should be identified by file and label rather than by index
+ or plugin name, which may be changed in new plugin
+ versions. Labels must not contain white-space characters. */
+ const char * Label;
+
+ /* This indicates a number of properties of the plugin. */
+ LADSPA_Properties Properties;
+
+ /* This member points to the null-terminated name of the plugin
+ (e.g. "Sine Oscillator"). */
+ const char * Name;
+
+ /* This member points to the null-terminated string indicating the
+ maker of the plugin. This can be an empty string but not NULL. */
+ const char * Maker;
+
+ /* This member points to the null-terminated string indicating any
+ copyright applying to the plugin. If no Copyright applies the
+ string "None" should be used. */
+ const char * Copyright;
+
+ /* This indicates the number of ports (input AND output) present on
+ the plugin. */
+ unsigned long PortCount;
+
+ /* This member indicates an array of port descriptors. Valid indices
+ vary from 0 to PortCount-1. */
+ const LADSPA_PortDescriptor * PortDescriptors;
+
+ /* This member indicates an array of null-terminated strings
+ describing ports (e.g. "Frequency (Hz)"). Valid indices vary from
+ 0 to PortCount-1. */
+ const char * const * PortNames;
+
+ /* This member indicates an array of range hints for each port (see
+ above). Valid indices vary from 0 to PortCount-1. */
+ const LADSPA_PortRangeHint * PortRangeHints;
+
+ /* This may be used by the plugin developer to pass any custom
+ implementation data into an instantiate call. It must not be used
+ or interpreted by the host. It is expected that most plugin
+ writers will not use this facility as LADSPA_Handle should be
+ used to hold instance data. */
+ void * ImplementationData;
+
+ /* This member is a function pointer that instantiates a plugin. A
+ handle is returned indicating the new plugin instance. The
+ instantiation function accepts a sample rate as a parameter. The
+ plugin descriptor from which this instantiate function was found
+ must also be passed. This function must return NULL if
+ instantiation fails.
+
+ Note that instance initialisation should generally occur in
+ activate() rather than here. */
+ LADSPA_Handle (*instantiate)(const struct _LADSPA_Descriptor * Descriptor,
+ unsigned long SampleRate);
+
+ /* This member is a function pointer that connects a port on an
+ instantiated plugin to a memory location at which a block of data
+ for the port will be read/written. The data location is expected
+ to be an array of LADSPA_Data for audio ports or a single
+ LADSPA_Data value for control ports. Memory issues will be
+ managed by the host. The plugin must read/write the data at these
+ locations every time run() or run_adding() is called and the data
+ present at the time of this connection call should not be
+ considered meaningful.
+
+ connect_port() may be called more than once for a plugin instance
+ to allow the host to change the buffers that the plugin is
+ reading or writing. These calls may be made before or after
+ activate() or deactivate() calls.
+
+ connect_port() must be called at least once for each port before
+ run() or run_adding() is called. When working with blocks of
+ LADSPA_Data the plugin should pay careful attention to the block
+ size passed to the run function as the block allocated may only
+ just be large enough to contain the block of samples.
+
+ Plugin writers should be aware that the host may elect to use the
+ same buffer for more than one port and even use the same buffer
+ for both input and output (see LADSPA_PROPERTY_INPLACE_BROKEN).
+ However, overlapped buffers or use of a single buffer for both
+ audio and control data may result in unexpected behaviour. */
+ void (*connect_port)(LADSPA_Handle Instance,
+ unsigned long Port,
+ LADSPA_Data * DataLocation);
+
+ /* This member is a function pointer that initialises a plugin
+ instance and activates it for use. This is separated from
+ instantiate() to aid real-time support and so that hosts can
+ reinitialise a plugin instance by calling deactivate() and then
+ activate(). In this case the plugin instance must reset all state
+ information dependent on the history of the plugin instance
+ except for any data locations provided by connect_port() and any
+ gain set by set_run_adding_gain(). If there is nothing for
+ activate() to do then the plugin writer may provide a NULL rather
+ than an empty function.
+
+ When present, hosts must call this function once before run() (or
+ run_adding()) is called for the first time. This call should be
+ made as close to the run() call as possible and indicates to
+ real-time plugins that they are now live. Plugins should not rely
+ on a prompt call to run() after activate(). activate() may not be
+ called again unless deactivate() is called first. Note that
+ connect_port() may be called before or after a call to
+ activate(). */
+ void (*activate)(LADSPA_Handle Instance);
+
+ /* This method is a function pointer that runs an instance of a
+ plugin for a block. Two parameters are required: the first is a
+ handle to the particular instance to be run and the second
+ indicates the block size (in samples) for which the plugin
+ instance may run.
+
+ Note that if an activate() function exists then it must be called
+ before run() or run_adding(). If deactivate() is called for a
+ plugin instance then the plugin instance may not be reused until
+ activate() has been called again.
+
+ If the plugin has the property LADSPA_PROPERTY_HARD_RT_CAPABLE
+ then there are various things that the plugin should not do
+ within the run() or run_adding() functions (see above). */
+ void (*run)(LADSPA_Handle Instance,
+ unsigned long SampleCount);
+
+ /* This method is a function pointer that runs an instance of a
+ plugin for a block. This has identical behaviour to run() except
+ in the way data is output from the plugin. When run() is used,
+ values are written directly to the memory areas associated with
+ the output ports. However when run_adding() is called, values
+ must be added to the values already present in the memory
+ areas. Furthermore, output values written must be scaled by the
+ current gain set by set_run_adding_gain() (see below) before
+ addition.
+
+ run_adding() is optional. When it is not provided by a plugin,
+ this function pointer must be set to NULL. When it is provided,
+ the function set_run_adding_gain() must be provided also. */
+ void (*run_adding)(LADSPA_Handle Instance,
+ unsigned long SampleCount);
+
+ /* This method is a function pointer that sets the output gain for
+ use when run_adding() is called (see above). If this function is
+ never called the gain is assumed to default to 1. Gain
+ information should be retained when activate() or deactivate()
+ are called.
+
+ This function should be provided by the plugin if and only if the
+ run_adding() function is provided. When it is absent this
+ function pointer must be set to NULL. */
+ void (*set_run_adding_gain)(LADSPA_Handle Instance,
+ LADSPA_Data Gain);
+
+ /* This is the counterpart to activate() (see above). If there is
+ nothing for deactivate() to do then the plugin writer may provide
+ a NULL rather than an empty function.
+
+ Hosts must deactivate all activated units after they have been
+ run() (or run_adding()) for the last time. This call should be
+ made as close to the last run() call as possible and indicates to
+ real-time plugins that they are no longer live. Plugins should
+ not rely on prompt deactivation. Note that connect_port() may be
+ called before or after a call to deactivate().
+
+ Deactivation is not similar to pausing as the plugin instance
+ will be reinitialised when activate() is called to reuse it. */
+ void (*deactivate)(LADSPA_Handle Instance);
+
+ /* Once an instance of a plugin has been finished with it can be
+ deleted using the following function. The instance handle passed
+ ceases to be valid after this call.
+
+ If activate() was called for a plugin instance then a
+ corresponding call to deactivate() must be made before cleanup()
+ is called. */
+ void (*cleanup)(LADSPA_Handle Instance);
+
+} LADSPA_Descriptor;
+
+/**********************************************************************/
+
+/* Accessing a Plugin: */
+
+/* The exact mechanism by which plugins are loaded is host-dependent,
+ however all most hosts will need to know is the name of shared
+ object file containing the plugin types. To allow multiple hosts to
+ share plugin types, hosts may wish to check for environment
+ variable LADSPA_PATH. If present, this should contain a
+ colon-separated path indicating directories that should be searched
+ (in order) when loading plugin types.
+
+ A plugin programmer must include a function called
+ "ladspa_descriptor" with the following function prototype within
+ the shared object file. This function will have C-style linkage (if
+ you are using C++ this is taken care of by the `extern "C"' clause
+ at the top of the file).
+
+ A host will find the plugin shared object file by one means or
+ another, find the ladspa_descriptor() function, call it, and
+ proceed from there.
+
+ Plugin types are accessed by index (not ID) using values from 0
+ upwards. Out of range indexes must result in this function
+ returning NULL, so the plugin count can be determined by checking
+ for the least index that results in NULL being returned. */
+
+const LADSPA_Descriptor * ladspa_descriptor(unsigned long Index);
+
+/* Datatype corresponding to the ladspa_descriptor() function. */
+typedef const LADSPA_Descriptor *
+(*LADSPA_Descriptor_Function)(unsigned long Index);
+
+/**********************************************************************/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LADSPA_INCLUDED */
+
+/* EOF */
diff --git a/src/modules/module-filter-chain/ladspa_plugin.c b/src/modules/module-filter-chain/ladspa_plugin.c
new file mode 100644
index 0000000..195d1ab
--- /dev/null
+++ b/src/modules/module-filter-chain/ladspa_plugin.c
@@ -0,0 +1,275 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <dlfcn.h>
+#include <math.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_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 char *config)
+{
+ struct fc_plugin *pl = NULL;
+
+ if (plugin[0] != '/') {
+ const char *search_dirs, *p;
+ 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(NULL, ":", &len, &search_dirs))) {
+ 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/lv2_plugin.c b/src/modules/module-filter-chain/lv2_plugin.c
new file mode 100644
index 0000000..f0bd836
--- /dev/null
+++ b/src/modules/module-filter-chain/lv2_plugin.c
@@ -0,0 +1,522 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <dlfcn.h>
+#include <math.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>
+
+#if defined __has_include
+# if __has_include (<lv2/atom/atom.h>)
+
+ #include <lv2/atom/atom.h>
+ #include <lv2/buf-size/buf-size.h>
+ #include <lv2/worker/worker.h>
+ #include <lv2/options/options.h>
+ #include <lv2/parameters/parameters.h>
+
+# else
+
+ #include <lv2/lv2plug.in/ns/ext/atom/atom.h>
+ #include <lv2/lv2plug.in/ns/ext/buf-size/buf-size.h>
+ #include <lv2/lv2plug.in/ns/ext/worker/worker.h>
+ #include <lv2/lv2plug.in/ns/ext/options/options.h>
+ #include <lv2/lv2plug.in/ns/ext/parameters/parameters.h>
+
+# endif
+
+#endif
+
+#include "plugin.h"
+
+static struct context *_context;
+
+typedef struct URITable {
+ struct pw_array array;
+} URITable;
+
+static void uri_table_init(URITable *table)
+{
+ pw_array_init(&table->array, 1024);
+}
+
+static void uri_table_destroy(URITable *table)
+{
+ char **p;
+ pw_array_for_each(p, &table->array)
+ free(*p);
+ pw_array_clear(&table->array);
+}
+
+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;
+}
+
+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, char*);
+ return NULL;
+}
+
+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;
+ LilvNode *lv2_ControlPort;
+ LilvNode *lv2_Optional;
+ LilvNode *atom_AtomPort;
+ LilvNode *atom_Sequence;
+ LilvNode *urid_map;
+ LilvNode *powerOf2BlockLength;
+ LilvNode *fixedBlockLength;
+ LilvNode *boundedBlockLength;
+ LilvNode* worker_schedule;
+ LilvNode* worker_iface;
+
+ URITable uri_table;
+ LV2_URID_Map map;
+ LV2_Feature map_feature;
+ LV2_URID_Unmap unmap;
+ LV2_Feature unmap_feature;
+
+ LV2_URID atom_Int;
+ LV2_URID atom_Float;
+};
+
+#define context_map(c,uri) ((c)->map.map((c)->map.handle,(uri)))
+
+static void context_free(struct context *c)
+{
+ if (c->world) {
+ lilv_node_free(c->worker_schedule);
+ lilv_node_free(c->powerOf2BlockLength);
+ lilv_node_free(c->fixedBlockLength);
+ lilv_node_free(c->boundedBlockLength);
+ lilv_node_free(c->urid_map);
+ lilv_node_free(c->atom_Sequence);
+ lilv_node_free(c->atom_AtomPort);
+ lilv_node_free(c->lv2_Optional);
+ lilv_node_free(c->lv2_ControlPort);
+ lilv_node_free(c->lv2_AudioPort);
+ lilv_node_free(c->lv2_OutputPort);
+ lilv_node_free(c->lv2_InputPort);
+ lilv_world_free(c->world);
+ }
+ uri_table_destroy(&c->uri_table);
+ free(c);
+}
+
+static const LV2_Feature buf_size_features[3] = {
+ { LV2_BUF_SIZE__powerOf2BlockLength, NULL },
+ { LV2_BUF_SIZE__fixedBlockLength, NULL },
+ { LV2_BUF_SIZE__boundedBlockLength, NULL },
+};
+
+static struct context *context_new(const struct spa_support *support, uint32_t n_support)
+{
+ struct context *c;
+
+ c = calloc(1, sizeof(*c));
+ if (c == NULL)
+ return NULL;
+
+ uri_table_init(&c->uri_table);
+ c->world = lilv_world_new();
+ if (c->world == NULL)
+ goto error;
+
+ lilv_world_load_all(c->world);
+
+ c->lv2_InputPort = lilv_new_uri(c->world, LV2_CORE__InputPort);
+ c->lv2_OutputPort = lilv_new_uri(c->world, LV2_CORE__OutputPort);
+ c->lv2_AudioPort = lilv_new_uri(c->world, LV2_CORE__AudioPort);
+ c->lv2_ControlPort = lilv_new_uri(c->world, LV2_CORE__ControlPort);
+ c->lv2_Optional = lilv_new_uri(c->world, LV2_CORE__connectionOptional);
+ c->atom_AtomPort = lilv_new_uri(c->world, LV2_ATOM__AtomPort);
+ c->atom_Sequence = lilv_new_uri(c->world, LV2_ATOM__Sequence);
+ c->urid_map = lilv_new_uri(c->world, LV2_URID__map);
+ c->powerOf2BlockLength = lilv_new_uri(c->world, LV2_BUF_SIZE__powerOf2BlockLength);
+ c->fixedBlockLength = lilv_new_uri(c->world, LV2_BUF_SIZE__fixedBlockLength);
+ c->boundedBlockLength = lilv_new_uri(c->world, LV2_BUF_SIZE__boundedBlockLength);
+ c->worker_schedule = lilv_new_uri(c->world, LV2_WORKER__schedule);
+ c->worker_iface = lilv_new_uri(c->world, LV2_WORKER__interface);
+
+ c->map.handle = &c->uri_table;
+ c->map.map = uri_table_map;
+ c->map_feature.URI = LV2_URID_MAP_URI;
+ c->map_feature.data = &c->map;
+ c->unmap.handle = &c->uri_table;
+ c->unmap.unmap = uri_table_unmap;
+ c->unmap_feature.URI = LV2_URID_UNMAP_URI;
+ c->unmap_feature.data = &c->unmap;
+
+ 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)
+{
+ if (_context == NULL) {
+ _context = context_new(support, n_support);
+ if (_context == NULL)
+ return NULL;
+ }
+ _context->ref++;
+ return _context;
+}
+
+static void context_unref(struct context *context)
+{
+ if (--_context->ref == 0) {
+ context_free(_context);
+ _context = NULL;
+ }
+}
+
+struct plugin {
+ struct fc_plugin plugin;
+ struct context *c;
+ const LilvPlugin *p;
+};
+
+struct descriptor {
+ struct fc_descriptor desc;
+ struct plugin *p;
+};
+
+struct instance {
+ struct descriptor *desc;
+ LilvInstance *instance;
+ LV2_Worker_Schedule work_schedule;
+ LV2_Feature work_schedule_feature;
+ LV2_Options_Option options[6];
+ LV2_Feature options_feature;
+
+ const LV2_Feature *features[7];
+
+ const LV2_Worker_Interface *work_iface;
+
+ int32_t block_length;
+};
+
+static int
+do_respond(struct spa_loop *loop, bool async, uint32_t seq, const void *data,
+ size_t size, void *user_data)
+{
+ struct instance *i = (struct instance*)user_data;
+ i->work_iface->work_response(i->instance->lv2_handle, size, data);
+ return 0;
+}
+
+/** Called by the plugin to respond to non-RT work. */
+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);
+ return LV2_WORKER_SUCCESS;
+}
+
+static int
+do_schedule(struct spa_loop *loop, bool async, uint32_t seq, const void *data,
+ size_t size, void *user_data)
+{
+ struct instance *i = (struct instance*)user_data;
+ i->work_iface->work(i->instance->lv2_handle, work_respond, i, size, data);
+ return 0;
+}
+
+/** Called by the plugin to schedule non-RT work. */
+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);
+ return LV2_WORKER_SUCCESS;
+}
+
+static void *lv2_instantiate(const struct fc_descriptor *desc,
+ unsigned long SampleRate, int index, const char *config)
+{
+ struct descriptor *d = (struct descriptor*)desc;
+ struct plugin *p = d->p;
+ struct context *c = p->c;
+ struct instance *i;
+ uint32_t n_features = 0;
+ static const int32_t min_block_length = 1;
+ static const int32_t max_block_length = 8192;
+ static const int32_t seq_size = 32768;
+ float fsample_rate = SampleRate;
+
+ i = calloc(1, sizeof(*i));
+ if (i == NULL)
+ return NULL;
+
+ i->block_length = 1024;
+ i->desc = d;
+ i->features[n_features++] = &c->map_feature;
+ i->features[n_features++] = &c->unmap_feature;
+ i->features[n_features++] = &buf_size_features[0];
+ i->features[n_features++] = &buf_size_features[1];
+ i->features[n_features++] = &buf_size_features[2];
+ if (lilv_plugin_has_feature(p->p, c->worker_schedule)) {
+ i->work_schedule.handle = i;
+ i->work_schedule.schedule_work = work_schedule;
+ i->work_schedule_feature.URI = LV2_WORKER__schedule;
+ i->work_schedule_feature.data = &i->work_schedule;
+ i->features[n_features++] = &i->work_schedule_feature;
+ }
+
+ i->options[0] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, LV2_BUF_SIZE__minBlockLength), sizeof(int32_t),
+ c->atom_Int, &min_block_length };
+ i->options[1] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, LV2_BUF_SIZE__maxBlockLength), sizeof(int32_t),
+ c->atom_Int, &max_block_length };
+ i->options[2] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, LV2_BUF_SIZE__sequenceSize), sizeof(int32_t),
+ c->atom_Int, &seq_size };
+ i->options[3] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, "http://lv2plug.in/ns/ext/buf-size#nominalBlockLength"), sizeof(int32_t),
+ c->atom_Int, &i->block_length },
+ i->options[4] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0,
+ context_map(c, LV2_PARAMETERS__sampleRate), sizeof(float),
+ c->atom_Float, &fsample_rate };
+ i->options[5] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, NULL };
+
+ i->options_feature.URI = LV2_OPTIONS__options;
+ i->options_feature.data = i->options;
+ i->features[n_features++] = &i->options_feature;
+
+ i->instance = lilv_plugin_instantiate(p->p, SampleRate, i->features);
+ if (i->instance == NULL) {
+ free(i);
+ return NULL;
+ }
+ if (lilv_plugin_has_extension_data(p->p, c->worker_iface)) {
+ i->work_iface = (const LV2_Worker_Interface*)
+ lilv_instance_get_extension_data(i->instance, LV2_WORKER__interface);
+ }
+
+ return i;
+}
+
+static void lv2_cleanup(void *instance)
+{
+ struct instance *i = instance;
+ lilv_instance_free(i->instance);
+ free(i);
+}
+
+static void lv2_connect_port(void *instance, unsigned long port, float *data)
+{
+ struct instance *i = instance;
+ lilv_instance_connect_port(i->instance, port, data);
+}
+
+static void lv2_activate(void *instance)
+{
+ struct instance *i = instance;
+ lilv_instance_activate(i->instance);
+}
+
+static void lv2_deactivate(void *instance)
+{
+ struct instance *i = instance;
+ lilv_instance_deactivate(i->instance);
+}
+
+static void lv2_run(void *instance, unsigned long SampleCount)
+{
+ struct instance *i = instance;
+ lilv_instance_run(i->instance, SampleCount);
+ if (i->work_iface != NULL && i->work_iface->end_run != NULL)
+ i->work_iface->end_run(i->instance);
+}
+
+static void lv2_free(const struct fc_descriptor *desc)
+{
+ struct descriptor *d = (struct descriptor*)desc;
+ free((char*)d->desc.name);
+ free(d->desc.ports);
+ free(d);
+}
+
+static const struct fc_descriptor *lv2_make_desc(struct fc_plugin *plugin, const char *name)
+{
+ struct plugin *p = (struct plugin *)plugin;
+ struct context *c = p->c;
+ struct descriptor *desc;
+ uint32_t i;
+ float *mins, *maxes, *controls;
+
+ desc = calloc(1, sizeof(*desc));
+ if (desc == NULL)
+ return NULL;
+
+ desc->p = p;
+ desc->desc.instantiate = lv2_instantiate;
+ desc->desc.cleanup = lv2_cleanup;
+ desc->desc.connect_port = lv2_connect_port;
+ desc->desc.activate = lv2_activate;
+ desc->desc.deactivate = lv2_deactivate;
+ desc->desc.run = lv2_run;
+
+ desc->desc.free = lv2_free;
+
+ desc->desc.name = strdup(name);
+ 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));
+
+ mins = alloca(desc->desc.n_ports * sizeof(float));
+ maxes = alloca(desc->desc.n_ports * sizeof(float));
+ controls = alloca(desc->desc.n_ports * sizeof(float));
+
+ lilv_plugin_get_port_ranges_float(p->p, mins, maxes, controls);
+
+ 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];
+
+ 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;
+ if (lilv_port_is_a(p->p, port, c->lv2_OutputPort))
+ fp->flags |= FC_PORT_OUTPUT;
+ if (lilv_port_is_a(p->p, port, c->lv2_ControlPort))
+ fp->flags |= FC_PORT_CONTROL;
+ if (lilv_port_is_a(p->p, port, c->lv2_AudioPort))
+ fp->flags |= FC_PORT_AUDIO;
+
+ fp->hint = 0;
+ fp->min = mins[i];
+ fp->max = maxes[i];
+ fp->def = controls[i];
+ }
+ return &desc->desc;
+}
+
+static void lv2_unload(struct fc_plugin *plugin)
+{
+ struct plugin *p = (struct plugin *)plugin;
+ context_unref(p->c);
+ free(p);
+}
+
+struct fc_plugin *load_lv2_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *ops, const char *plugin_uri, const char *config)
+{
+ struct context *c;
+ const LilvPlugins *plugins;
+ const LilvPlugin *plugin;
+ LilvNode *uri;
+ int res;
+ struct plugin *p;
+
+ c = context_ref(support, n_support);
+ if (c == NULL)
+ return NULL;
+
+ uri = lilv_new_uri(c->world, plugin_uri);
+ if (uri == NULL) {
+ pw_log_warn("invalid URI %s", plugin_uri);
+ res = -EINVAL;
+ goto error_unref;
+ }
+
+ plugins = lilv_world_get_all_plugins(c->world);
+ plugin = lilv_plugins_get_by_uri(plugins, uri);
+ lilv_node_free(uri);
+
+ if (plugin == NULL) {
+ pw_log_warn("can't load plugin %s", plugin_uri);
+ res = -EINVAL;
+ goto error_unref;
+ }
+
+ p = calloc(1, sizeof(*p));
+ if (!p) {
+ res = -errno;
+ goto error_unref;
+ }
+ p->p = plugin;
+ p->c = c;
+
+ p->plugin.make_desc = lv2_make_desc;
+ p->plugin.unload = lv2_unload;
+
+ return &p->plugin;
+
+error_unref:
+ context_unref(c);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/module-filter-chain/pffft.c b/src/modules/module-filter-chain/pffft.c
new file mode 100644
index 0000000..0370191
--- /dev/null
+++ b/src/modules/module-filter-chain/pffft.c
@@ -0,0 +1,2381 @@
+/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com )
+
+ Based on original fortran 77 code from FFTPACKv4 from NETLIB
+ (http://www.netlib.org/fftpack), authored by Dr Paul Swarztrauber
+ of NCAR, in 1985.
+
+ As confirmed by the NCAR fftpack software curators, the following
+ FFTPACKv5 license applies to FFTPACKv4 sources. My changes are
+ released under the same terms.
+
+ FFTPACK license:
+
+ http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html
+
+ Copyright (c) 2004 the University Corporation for Atmospheric
+ Research ("UCAR"). All rights reserved. Developed by NCAR's
+ Computational and Information Systems Laboratory, UCAR,
+ www.cisl.ucar.edu.
+
+ Redistribution and use of the Software in source and binary forms,
+ with or without modification, is permitted provided that the
+ following conditions are met:
+
+ - Neither the names of NCAR's Computational and Information Systems
+ Laboratory, the University Corporation for Atmospheric Research,
+ nor the names of its sponsors or contributors may be used to
+ endorse or promote products derived from this Software without
+ specific prior written permission.
+
+ - Redistributions of source code must retain the above copyright
+ notices, this list of conditions, and the disclaimer below.
+
+ - Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions, and the disclaimer below in the
+ documentation and/or other materials provided with the
+ distribution.
+
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+ SOFTWARE.
+
+ PFFFT : a Pretty Fast FFT.
+
+ This file is largerly based on the original FFTPACK implementation, modified in
+ order to take advantage of SIMD instructions of modern CPUs.
+*/
+
+/*
+ ChangeLog:
+ - 2011/10/02, version 1: This is the very first release of this file.
+*/
+
+#include "pffft.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+#include <stdint.h>
+#include <assert.h>
+
+#include <spa/support/cpu.h>
+
+/* detect compiler flavour */
+#if defined(_MSC_VER)
+#define COMPILER_MSVC
+#elif defined(__GNUC__)
+#define COMPILER_GCC
+#endif
+
+#if defined(COMPILER_GCC)
+#define ALWAYS_INLINE(return_type) inline return_type __attribute__ ((always_inline))
+#define NEVER_INLINE(return_type) return_type __attribute__ ((noinline))
+#define RESTRICT __restrict
+#define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ varname__[size__];
+#elif defined(COMPILER_MSVC)
+#define ALWAYS_INLINE(return_type) __forceinline return_type
+#define NEVER_INLINE(return_type) __declspec(noinline) return_type
+#define RESTRICT __restrict
+#define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ *varname__ = (type__*)_alloca(size__ * sizeof(type__))
+#endif
+
+/*
+ vector support macros: the rest of the code is independent of
+ SSE/Altivec/NEON -- adding support for other platforms with 4-element
+ vectors should be limited to these macros
+*/
+
+// define PFFFT_SIMD_DISABLE if you want to use scalar code instead of simd code
+//#define PFFFT_SIMD_DISABLE
+
+/*
+ Altivec support macros
+*/
+#if !defined(PFFFT_SIMD_DISABLE) && (defined(HAVE_ALTIVEC))
+typedef vector float v4sf;
+#define SIMD_SZ 4
+#define VZERO() ((vector float) vec_splat_u8(0))
+#define VMUL(a,b) vec_madd(a,b, VZERO())
+#define VADD(a,b) vec_add(a,b)
+#define VMADD(a,b,c) vec_madd(a,b,c)
+#define VSUB(a,b) vec_sub(a,b)
+inline v4sf ld_ps1(const float *p)
+{
+ v4sf v = vec_lde(0, p);
+ return vec_splat(vec_perm(v, v, vec_lvsl(0, p)), 0);
+}
+
+#define LD_PS1(p) ld_ps1(&p)
+#define INTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = vec_mergeh(in1, in2); out2 = vec_mergel(in1, in2); out1 = tmp__; }
+#define UNINTERLEAVE2(in1, in2, out1, out2) { \
+ vector unsigned char vperm1 = (vector unsigned char)(0,1,2,3,8,9,10,11,16,17,18,19,24,25,26,27); \
+ vector unsigned char vperm2 = (vector unsigned char)(4,5,6,7,12,13,14,15,20,21,22,23,28,29,30,31); \
+ v4sf tmp__ = vec_perm(in1, in2, vperm1); out2 = vec_perm(in1, in2, vperm2); out1 = tmp__; \
+ }
+#define VTRANSPOSE4(x0,x1,x2,x3) { \
+ v4sf y0 = vec_mergeh(x0, x2); \
+ v4sf y1 = vec_mergel(x0, x2); \
+ v4sf y2 = vec_mergeh(x1, x3); \
+ v4sf y3 = vec_mergel(x1, x3); \
+ x0 = vec_mergeh(y0, y2); \
+ x1 = vec_mergel(y0, y2); \
+ x2 = vec_mergeh(y1, y3); \
+ x3 = vec_mergel(y1, y3); \
+ }
+#define VSWAPHL(a,b) vec_perm(a,b, (vector unsigned char)(16,17,18,19,20,21,22,23,8,9,10,11,12,13,14,15))
+#define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0xF) == 0)
+#define pffft_funcs pffft_funcs_altivec
+#define new_setup_simd new_setup_altivec
+#define zreorder_simd zreorder_altivec
+#define zconvolve_accumulate_simd zconvolve_accumulate_altivec
+#define zconvolve_simd zconvolve_altivec
+#define transform_simd transform_altivec
+
+/*
+ SSE1 support macros
+*/
+#elif !defined(PFFFT_SIMD_DISABLE) && (defined(HAVE_SSE))
+
+#include <xmmintrin.h>
+typedef __m128 v4sf;
+#define SIMD_SZ 4 // 4 floats by simd vector -- this is pretty much hardcoded in the preprocess/finalize functions anyway so you will have to work if you want to enable AVX with its 256-bit vectors.
+#define VZERO() _mm_setzero_ps()
+#define VMUL(a,b) _mm_mul_ps(a,b)
+#define VADD(a,b) _mm_add_ps(a,b)
+#define VMADD(a,b,c) _mm_add_ps(_mm_mul_ps(a,b), c)
+#define VSUB(a,b) _mm_sub_ps(a,b)
+#define LD_PS1(p) _mm_set1_ps(p)
+#define INTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = _mm_unpacklo_ps(in1, in2); out2 = _mm_unpackhi_ps(in1, in2); out1 = tmp__; }
+#define UNINTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = _mm_shuffle_ps(in1, in2, _MM_SHUFFLE(2,0,2,0)); out2 = _mm_shuffle_ps(in1, in2, _MM_SHUFFLE(3,1,3,1)); out1 = tmp__; }
+#define VTRANSPOSE4(x0,x1,x2,x3) _MM_TRANSPOSE4_PS(x0,x1,x2,x3)
+#define VSWAPHL(a,b) _mm_shuffle_ps(b, a, _MM_SHUFFLE(3,2,1,0))
+#define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0xF) == 0)
+#define pffft_funcs pffft_funcs_sse
+#define new_setup_simd new_setup_sse
+#define zreorder_simd zreorder_sse
+#define zconvolve_accumulate_simd zconvolve_accumulate_sse
+#define zconvolve_simd zconvolve_sse
+#define transform_simd transform_sse
+
+/*
+ ARM NEON support macros
+*/
+#elif !defined(PFFFT_SIMD_DISABLE) && (defined(HAVE_NEON))
+#include <arm_neon.h>
+typedef float32x4_t v4sf;
+#define SIMD_SZ 4
+#define VZERO() vdupq_n_f32(0)
+#define VMUL(a,b) vmulq_f32(a,b)
+#define VADD(a,b) vaddq_f32(a,b)
+#define VMADD(a,b,c) vmlaq_f32(c,a,b)
+#define VSUB(a,b) vsubq_f32(a,b)
+#define LD_PS1(p) vld1q_dup_f32(&(p))
+#define INTERLEAVE2(in1, in2, out1, out2) { float32x4x2_t tmp__ = vzipq_f32(in1,in2); out1=tmp__.val[0]; out2=tmp__.val[1]; }
+#define UNINTERLEAVE2(in1, in2, out1, out2) { float32x4x2_t tmp__ = vuzpq_f32(in1,in2); out1=tmp__.val[0]; out2=tmp__.val[1]; }
+#define VTRANSPOSE4(x0,x1,x2,x3) { \
+ float32x4x2_t t0_ = vzipq_f32(x0, x2); \
+ float32x4x2_t t1_ = vzipq_f32(x1, x3); \
+ float32x4x2_t u0_ = vzipq_f32(t0_.val[0], t1_.val[0]); \
+ float32x4x2_t u1_ = vzipq_f32(t0_.val[1], t1_.val[1]); \
+ x0 = u0_.val[0]; x1 = u0_.val[1]; x2 = u1_.val[0]; x3 = u1_.val[1]; \
+ }
+// marginally faster version
+//# define VTRANSPOSE4(x0,x1,x2,x3) { asm("vtrn.32 %q0, %q1;\n vtrn.32 %q2,%q3\n vswp %f0,%e2\n vswp %f1,%e3" : "+w"(x0), "+w"(x1), "+w"(x2), "+w"(x3)::); }
+#define VSWAPHL(a,b) vcombine_f32(vget_low_f32(b), vget_high_f32(a))
+#define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x3) == 0)
+#define pffft_funcs pffft_funcs_neon
+#define new_setup_simd new_setup_neon
+#define zreorder_simd zreorder_neon
+#define zconvolve_accumulate_simd zconvolve_accumulate_neon
+#define zconvolve_simd zconvolve_neon
+#define transform_simd transform_neon
+#else
+#if !defined(PFFFT_SIMD_DISABLE)
+#warning "building with simd disabled !\n";
+#define PFFFT_SIMD_DISABLE // fallback to scalar code
+#endif
+#endif
+
+// fallback mode for situations where SSE/Altivec are not available, use scalar mode instead
+#ifdef PFFFT_SIMD_DISABLE
+typedef float v4sf;
+#define SIMD_SZ 1
+#define VZERO() 0.f
+#define VMUL(a,b) ((a)*(b))
+#define VADD(a,b) ((a)+(b))
+#define VMADD(a,b,c) ((a)*(b)+(c))
+#define VSUB(a,b) ((a)-(b))
+#define LD_PS1(p) (p)
+#define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x3) == 0)
+#define pffft_funcs pffft_funcs_c
+#define new_setup_simd new_setup_c
+#define zreorder_simd zreorder_c
+#define zconvolve_accumulate_simd zconvolve_accumulate_c
+#define zconvolve_simd zconvolve_c
+#define transform_simd transform_c
+#endif
+
+// shortcuts for complex multiplcations
+#define VCPLXMUL(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VSUB(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VADD(ai,tmp); }
+#define VCPLXMULCONJ(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VADD(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VSUB(ai,tmp); }
+#ifndef SVMUL
+// multiply a scalar with a vector
+#define SVMUL(f,v) VMUL(LD_PS1(f),v)
+#endif
+
+#if !defined(PFFFT_SIMD_DISABLE)
+typedef union v4sf_union {
+ v4sf v;
+ float f[4];
+} v4sf_union;
+
+#include <string.h>
+
+#define assertv4(v,f0,f1,f2,f3) assert(v.f[0] == (f0) && v.f[1] == (f1) && v.f[2] == (f2) && v.f[3] == (f3))
+
+/* detect bugs with the vector support macros */
+static void validate_pffft_simd(void)
+{
+ float f[16] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
+ v4sf_union a0, a1, a2, a3, t, u;
+ memcpy(a0.f, f, 4 * sizeof(float));
+ memcpy(a1.f, f + 4, 4 * sizeof(float));
+ memcpy(a2.f, f + 8, 4 * sizeof(float));
+ memcpy(a3.f, f + 12, 4 * sizeof(float));
+
+ t = a0;
+ u = a1;
+ t.v = VZERO();
+ printf("VZERO=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]);
+ assertv4(t, 0, 0, 0, 0);
+ t.v = VADD(a1.v, a2.v);
+ printf("VADD(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2],
+ t.f[3]);
+ assertv4(t, 12, 14, 16, 18);
+ t.v = VMUL(a1.v, a2.v);
+ printf("VMUL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2],
+ t.f[3]);
+ assertv4(t, 32, 45, 60, 77);
+ t.v = VMADD(a1.v, a2.v, a0.v);
+ printf("VMADD(4:7,8:11,0:3)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1],
+ t.f[2], t.f[3]);
+ assertv4(t, 32, 46, 62, 80);
+
+ INTERLEAVE2(a1.v, a2.v, t.v, u.v);
+ printf("INTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n",
+ t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]);
+ assertv4(t, 4, 8, 5, 9);
+ assertv4(u, 6, 10, 7, 11);
+ UNINTERLEAVE2(a1.v, a2.v, t.v, u.v);
+ printf("UNINTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n",
+ t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]);
+ assertv4(t, 4, 6, 8, 10);
+ assertv4(u, 5, 7, 9, 11);
+
+ t.v = LD_PS1(f[15]);
+ printf("LD_PS1(15)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2],
+ t.f[3]);
+ assertv4(t, 15, 15, 15, 15);
+ t.v = VSWAPHL(a1.v, a2.v);
+ printf("VSWAPHL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2],
+ t.f[3]);
+ assertv4(t, 8, 9, 6, 7);
+ VTRANSPOSE4(a0.v, a1.v, a2.v, a3.v);
+ printf
+ ("VTRANSPOSE4(0:3,4:7,8:11,12:15)=[%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g]\n",
+ a0.f[0], a0.f[1], a0.f[2], a0.f[3], a1.f[0], a1.f[1], a1.f[2],
+ a1.f[3], a2.f[0], a2.f[1], a2.f[2], a2.f[3], a3.f[0], a3.f[1],
+ a3.f[2], a3.f[3]);
+ assertv4(a0, 0, 4, 8, 12);
+ assertv4(a1, 1, 5, 9, 13);
+ assertv4(a2, 2, 6, 10, 14);
+ assertv4(a3, 3, 7, 11, 15);
+}
+#else
+static void validate_pffft_simd(void)
+{
+} // allow test_pffft.c to call this function even when simd is not available..
+#endif //!PFFFT_SIMD_DISABLE
+
+/*
+ passf2 and passb2 has been merged here, fsign = -1 for passf2, +1 for passb2
+*/
+static NEVER_INLINE(void) passf2_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1, float fsign)
+{
+ int k, i;
+ int l1ido = l1 * ido;
+ if (ido <= 2) {
+ for (k = 0; k < l1ido; k += ido, ch += ido, cc += 2 * ido) {
+ ch[0] = VADD(cc[0], cc[ido + 0]);
+ ch[l1ido] = VSUB(cc[0], cc[ido + 0]);
+ ch[1] = VADD(cc[1], cc[ido + 1]);
+ ch[l1ido + 1] = VSUB(cc[1], cc[ido + 1]);
+ }
+ } else {
+ for (k = 0; k < l1ido; k += ido, ch += ido, cc += 2 * ido) {
+ for (i = 0; i < ido - 1; i += 2) {
+ v4sf tr2 = VSUB(cc[i + 0], cc[i + ido + 0]);
+ v4sf ti2 = VSUB(cc[i + 1], cc[i + ido + 1]);
+ v4sf wr = LD_PS1(wa1[i]), wi =
+ VMUL(LD_PS1(fsign), LD_PS1(wa1[i + 1]));
+ ch[i] = VADD(cc[i + 0], cc[i + ido + 0]);
+ ch[i + 1] = VADD(cc[i + 1], cc[i + ido + 1]);
+ VCPLXMUL(tr2, ti2, wr, wi);
+ ch[i + l1ido] = tr2;
+ ch[i + l1ido + 1] = ti2;
+ }
+ }
+ }
+}
+
+/*
+ passf3 and passb3 has been merged here, fsign = -1 for passf3, +1 for passb3
+*/
+static NEVER_INLINE(void) passf3_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1, const float *wa2,
+ float fsign)
+{
+ static const float taur = -0.5f;
+ float taui = 0.866025403784439f * fsign;
+ int i, k;
+ v4sf tr2, ti2, cr2, ci2, cr3, ci3, dr2, di2, dr3, di3;
+ int l1ido = l1 * ido;
+ float wr1, wi1, wr2, wi2;
+ assert(ido > 2);
+ for (k = 0; k < l1ido; k += ido, cc += 3 * ido, ch += ido) {
+ for (i = 0; i < ido - 1; i += 2) {
+ tr2 = VADD(cc[i + ido], cc[i + 2 * ido]);
+ cr2 = VADD(cc[i], SVMUL(taur, tr2));
+ ch[i] = VADD(cc[i], tr2);
+ ti2 = VADD(cc[i + ido + 1], cc[i + 2 * ido + 1]);
+ ci2 = VADD(cc[i + 1], SVMUL(taur, ti2));
+ ch[i + 1] = VADD(cc[i + 1], ti2);
+ cr3 = SVMUL(taui, VSUB(cc[i + ido], cc[i + 2 * ido]));
+ ci3 =
+ SVMUL(taui,
+ VSUB(cc[i + ido + 1], cc[i + 2 * ido + 1]));
+ dr2 = VSUB(cr2, ci3);
+ dr3 = VADD(cr2, ci3);
+ di2 = VADD(ci2, cr3);
+ di3 = VSUB(ci2, cr3);
+ wr1 = wa1[i], wi1 = fsign * wa1[i + 1], wr2 =
+ wa2[i], wi2 = fsign * wa2[i + 1];
+ VCPLXMUL(dr2, di2, LD_PS1(wr1), LD_PS1(wi1));
+ ch[i + l1ido] = dr2;
+ ch[i + l1ido + 1] = di2;
+ VCPLXMUL(dr3, di3, LD_PS1(wr2), LD_PS1(wi2));
+ ch[i + 2 * l1ido] = dr3;
+ ch[i + 2 * l1ido + 1] = di3;
+ }
+ }
+} /* passf3 */
+
+static NEVER_INLINE(void) passf4_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1, const float *wa2,
+ const float *wa3, float fsign)
+{
+ /* isign == -1 for forward transform and +1 for backward transform */
+
+ int i, k;
+ v4sf ci2, ci3, ci4, cr2, cr3, cr4, ti1, ti2, ti3, ti4, tr1, tr2, tr3,
+ tr4;
+ int l1ido = l1 * ido;
+ if (ido == 2) {
+ for (k = 0; k < l1ido; k += ido, ch += ido, cc += 4 * ido) {
+ tr1 = VSUB(cc[0], cc[2 * ido + 0]);
+ tr2 = VADD(cc[0], cc[2 * ido + 0]);
+ ti1 = VSUB(cc[1], cc[2 * ido + 1]);
+ ti2 = VADD(cc[1], cc[2 * ido + 1]);
+ ti4 =
+ VMUL(VSUB(cc[1 * ido + 0], cc[3 * ido + 0]),
+ LD_PS1(fsign));
+ tr4 =
+ VMUL(VSUB(cc[3 * ido + 1], cc[1 * ido + 1]),
+ LD_PS1(fsign));
+ tr3 = VADD(cc[ido + 0], cc[3 * ido + 0]);
+ ti3 = VADD(cc[ido + 1], cc[3 * ido + 1]);
+
+ ch[0 * l1ido + 0] = VADD(tr2, tr3);
+ ch[0 * l1ido + 1] = VADD(ti2, ti3);
+ ch[1 * l1ido + 0] = VADD(tr1, tr4);
+ ch[1 * l1ido + 1] = VADD(ti1, ti4);
+ ch[2 * l1ido + 0] = VSUB(tr2, tr3);
+ ch[2 * l1ido + 1] = VSUB(ti2, ti3);
+ ch[3 * l1ido + 0] = VSUB(tr1, tr4);
+ ch[3 * l1ido + 1] = VSUB(ti1, ti4);
+ }
+ } else {
+ for (k = 0; k < l1ido; k += ido, ch += ido, cc += 4 * ido) {
+ for (i = 0; i < ido - 1; i += 2) {
+ float wr1, wi1, wr2, wi2, wr3, wi3;
+ tr1 = VSUB(cc[i + 0], cc[i + 2 * ido + 0]);
+ tr2 = VADD(cc[i + 0], cc[i + 2 * ido + 0]);
+ ti1 = VSUB(cc[i + 1], cc[i + 2 * ido + 1]);
+ ti2 = VADD(cc[i + 1], cc[i + 2 * ido + 1]);
+ tr4 =
+ VMUL(VSUB
+ (cc[i + 3 * ido + 1],
+ cc[i + 1 * ido + 1]), LD_PS1(fsign));
+ ti4 =
+ VMUL(VSUB
+ (cc[i + 1 * ido + 0],
+ cc[i + 3 * ido + 0]), LD_PS1(fsign));
+ tr3 =
+ VADD(cc[i + ido + 0], cc[i + 3 * ido + 0]);
+ ti3 =
+ VADD(cc[i + ido + 1], cc[i + 3 * ido + 1]);
+
+ ch[i] = VADD(tr2, tr3);
+ cr3 = VSUB(tr2, tr3);
+ ch[i + 1] = VADD(ti2, ti3);
+ ci3 = VSUB(ti2, ti3);
+
+ cr2 = VADD(tr1, tr4);
+ cr4 = VSUB(tr1, tr4);
+ ci2 = VADD(ti1, ti4);
+ ci4 = VSUB(ti1, ti4);
+ wr1 = wa1[i], wi1 = fsign * wa1[i + 1];
+ VCPLXMUL(cr2, ci2, LD_PS1(wr1), LD_PS1(wi1));
+ wr2 = wa2[i], wi2 = fsign * wa2[i + 1];
+ ch[i + l1ido] = cr2;
+ ch[i + l1ido + 1] = ci2;
+
+ VCPLXMUL(cr3, ci3, LD_PS1(wr2), LD_PS1(wi2));
+ wr3 = wa3[i], wi3 = fsign * wa3[i + 1];
+ ch[i + 2 * l1ido] = cr3;
+ ch[i + 2 * l1ido + 1] = ci3;
+
+ VCPLXMUL(cr4, ci4, LD_PS1(wr3), LD_PS1(wi3));
+ ch[i + 3 * l1ido] = cr4;
+ ch[i + 3 * l1ido + 1] = ci4;
+ }
+ }
+ }
+} /* passf4 */
+
+/*
+ passf5 and passb5 has been merged here, fsign = -1 for passf5, +1 for passb5
+*/
+static NEVER_INLINE(void) passf5_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1, const float *wa2,
+ const float *wa3, const float *wa4,
+ float fsign)
+{
+ static const float tr11 = .309016994374947f;
+ const float ti11 = .951056516295154f * fsign;
+ static const float tr12 = -.809016994374947f;
+ const float ti12 = .587785252292473f * fsign;
+
+ /* Local variables */
+ int i, k;
+ v4sf ci2, ci3, ci4, ci5, di3, di4, di5, di2, cr2, cr3, cr5, cr4, ti2,
+ ti3, ti4, ti5, dr3, dr4, dr5, dr2, tr2, tr3, tr4, tr5;
+
+ float wr1, wi1, wr2, wi2, wr3, wi3, wr4, wi4;
+
+#define cc_ref(a_1,a_2) cc[(a_2-1)*ido + a_1 + 1]
+#define ch_ref(a_1,a_3) ch[(a_3-1)*l1*ido + a_1 + 1]
+
+ assert(ido > 2);
+ for (k = 0; k < l1; ++k, cc += 5 * ido, ch += ido) {
+ for (i = 0; i < ido - 1; i += 2) {
+ ti5 = VSUB(cc_ref(i, 2), cc_ref(i, 5));
+ ti2 = VADD(cc_ref(i, 2), cc_ref(i, 5));
+ ti4 = VSUB(cc_ref(i, 3), cc_ref(i, 4));
+ ti3 = VADD(cc_ref(i, 3), cc_ref(i, 4));
+ tr5 = VSUB(cc_ref(i - 1, 2), cc_ref(i - 1, 5));
+ tr2 = VADD(cc_ref(i - 1, 2), cc_ref(i - 1, 5));
+ tr4 = VSUB(cc_ref(i - 1, 3), cc_ref(i - 1, 4));
+ tr3 = VADD(cc_ref(i - 1, 3), cc_ref(i - 1, 4));
+ ch_ref(i - 1, 1) =
+ VADD(cc_ref(i - 1, 1), VADD(tr2, tr3));
+ ch_ref(i, 1) = VADD(cc_ref(i, 1), VADD(ti2, ti3));
+ cr2 =
+ VADD(cc_ref(i - 1, 1),
+ VADD(SVMUL(tr11, tr2), SVMUL(tr12, tr3)));
+ ci2 =
+ VADD(cc_ref(i, 1),
+ VADD(SVMUL(tr11, ti2), SVMUL(tr12, ti3)));
+ cr3 =
+ VADD(cc_ref(i - 1, 1),
+ VADD(SVMUL(tr12, tr2), SVMUL(tr11, tr3)));
+ ci3 =
+ VADD(cc_ref(i, 1),
+ VADD(SVMUL(tr12, ti2), SVMUL(tr11, ti3)));
+ cr5 = VADD(SVMUL(ti11, tr5), SVMUL(ti12, tr4));
+ ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4));
+ cr4 = VSUB(SVMUL(ti12, tr5), SVMUL(ti11, tr4));
+ ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4));
+ dr3 = VSUB(cr3, ci4);
+ dr4 = VADD(cr3, ci4);
+ di3 = VADD(ci3, cr4);
+ di4 = VSUB(ci3, cr4);
+ dr5 = VADD(cr2, ci5);
+ dr2 = VSUB(cr2, ci5);
+ di5 = VSUB(ci2, cr5);
+ di2 = VADD(ci2, cr5);
+ wr1 = wa1[i], wi1 = fsign * wa1[i + 1], wr2 =
+ wa2[i], wi2 = fsign * wa2[i + 1];
+ wr3 = wa3[i], wi3 = fsign * wa3[i + 1], wr4 =
+ wa4[i], wi4 = fsign * wa4[i + 1];
+ VCPLXMUL(dr2, di2, LD_PS1(wr1), LD_PS1(wi1));
+ ch_ref(i - 1, 2) = dr2;
+ ch_ref(i, 2) = di2;
+ VCPLXMUL(dr3, di3, LD_PS1(wr2), LD_PS1(wi2));
+ ch_ref(i - 1, 3) = dr3;
+ ch_ref(i, 3) = di3;
+ VCPLXMUL(dr4, di4, LD_PS1(wr3), LD_PS1(wi3));
+ ch_ref(i - 1, 4) = dr4;
+ ch_ref(i, 4) = di4;
+ VCPLXMUL(dr5, di5, LD_PS1(wr4), LD_PS1(wi4));
+ ch_ref(i - 1, 5) = dr5;
+ ch_ref(i, 5) = di5;
+ }
+ }
+#undef ch_ref
+#undef cc_ref
+}
+
+static NEVER_INLINE(void) radf2_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1)
+{
+ static const float minus_one = -1.f;
+ int i, k, l1ido = l1 * ido;
+ for (k = 0; k < l1ido; k += ido) {
+ v4sf a = cc[k], b = cc[k + l1ido];
+ ch[2 * k] = VADD(a, b);
+ ch[2 * (k + ido) - 1] = VSUB(a, b);
+ }
+ if (ido < 2)
+ return;
+ if (ido != 2) {
+ for (k = 0; k < l1ido; k += ido) {
+ for (i = 2; i < ido; i += 2) {
+ v4sf tr2 = cc[i - 1 + k + l1ido], ti2 =
+ cc[i + k + l1ido];
+ v4sf br = cc[i - 1 + k], bi = cc[i + k];
+ VCPLXMULCONJ(tr2, ti2, LD_PS1(wa1[i - 2]),
+ LD_PS1(wa1[i - 1]));
+ ch[i + 2 * k] = VADD(bi, ti2);
+ ch[2 * (k + ido) - i] = VSUB(ti2, bi);
+ ch[i - 1 + 2 * k] = VADD(br, tr2);
+ ch[2 * (k + ido) - i - 1] = VSUB(br, tr2);
+ }
+ }
+ if (ido % 2 == 1)
+ return;
+ }
+ for (k = 0; k < l1ido; k += ido) {
+ ch[2 * k + ido] = SVMUL(minus_one, cc[ido - 1 + k + l1ido]);
+ ch[2 * k + ido - 1] = cc[k + ido - 1];
+ }
+} /* radf2 */
+
+static NEVER_INLINE(void) radb2_ps(int ido, int l1, const v4sf * cc, v4sf * ch,
+ const float *wa1)
+{
+ static const float minus_two = -2;
+ int i, k, l1ido = l1 * ido;
+ v4sf a, b, c, d, tr2, ti2;
+ for (k = 0; k < l1ido; k += ido) {
+ a = cc[2 * k];
+ b = cc[2 * (k + ido) - 1];
+ ch[k] = VADD(a, b);
+ ch[k + l1ido] = VSUB(a, b);
+ }
+ if (ido < 2)
+ return;
+ if (ido != 2) {
+ for (k = 0; k < l1ido; k += ido) {
+ for (i = 2; i < ido; i += 2) {
+ a = cc[i - 1 + 2 * k];
+ b = cc[2 * (k + ido) - i - 1];
+ c = cc[i + 0 + 2 * k];
+ d = cc[2 * (k + ido) - i + 0];
+ ch[i - 1 + k] = VADD(a, b);
+ tr2 = VSUB(a, b);
+ ch[i + 0 + k] = VSUB(c, d);
+ ti2 = VADD(c, d);
+ VCPLXMUL(tr2, ti2, LD_PS1(wa1[i - 2]),
+ LD_PS1(wa1[i - 1]));
+ ch[i - 1 + k + l1ido] = tr2;
+ ch[i + 0 + k + l1ido] = ti2;
+ }
+ }
+ if (ido % 2 == 1)
+ return;
+ }
+ for (k = 0; k < l1ido; k += ido) {
+ a = cc[2 * k + ido - 1];
+ b = cc[2 * k + ido];
+ ch[k + ido - 1] = VADD(a, a);
+ ch[k + ido - 1 + l1ido] = SVMUL(minus_two, b);
+ }
+} /* radb2 */
+
+static void radf3_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1, const float *wa2)
+{
+ static const float taur = -0.5f;
+ static const float taui = 0.866025403784439f;
+ int i, k, ic;
+ v4sf ci2, di2, di3, cr2, dr2, dr3, ti2, ti3, tr2, tr3, wr1, wi1, wr2,
+ wi2;
+ for (k = 0; k < l1; k++) {
+ cr2 = VADD(cc[(k + l1) * ido], cc[(k + 2 * l1) * ido]);
+ ch[3 * k * ido] = VADD(cc[k * ido], cr2);
+ ch[(3 * k + 2) * ido] =
+ SVMUL(taui,
+ VSUB(cc[(k + l1 * 2) * ido], cc[(k + l1) * ido]));
+ ch[ido - 1 + (3 * k + 1) * ido] =
+ VADD(cc[k * ido], SVMUL(taur, cr2));
+ }
+ if (ido == 1)
+ return;
+ for (k = 0; k < l1; k++) {
+ for (i = 2; i < ido; i += 2) {
+ ic = ido - i;
+ wr1 = LD_PS1(wa1[i - 2]);
+ wi1 = LD_PS1(wa1[i - 1]);
+ dr2 = cc[i - 1 + (k + l1) * ido];
+ di2 = cc[i + (k + l1) * ido];
+ VCPLXMULCONJ(dr2, di2, wr1, wi1);
+
+ wr2 = LD_PS1(wa2[i - 2]);
+ wi2 = LD_PS1(wa2[i - 1]);
+ dr3 = cc[i - 1 + (k + l1 * 2) * ido];
+ di3 = cc[i + (k + l1 * 2) * ido];
+ VCPLXMULCONJ(dr3, di3, wr2, wi2);
+
+ cr2 = VADD(dr2, dr3);
+ ci2 = VADD(di2, di3);
+ ch[i - 1 + 3 * k * ido] =
+ VADD(cc[i - 1 + k * ido], cr2);
+ ch[i + 3 * k * ido] = VADD(cc[i + k * ido], ci2);
+ tr2 = VADD(cc[i - 1 + k * ido], SVMUL(taur, cr2));
+ ti2 = VADD(cc[i + k * ido], SVMUL(taur, ci2));
+ tr3 = SVMUL(taui, VSUB(di2, di3));
+ ti3 = SVMUL(taui, VSUB(dr3, dr2));
+ ch[i - 1 + (3 * k + 2) * ido] = VADD(tr2, tr3);
+ ch[ic - 1 + (3 * k + 1) * ido] = VSUB(tr2, tr3);
+ ch[i + (3 * k + 2) * ido] = VADD(ti2, ti3);
+ ch[ic + (3 * k + 1) * ido] = VSUB(ti3, ti2);
+ }
+ }
+} /* radf3 */
+
+static void radb3_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1, const float *wa2)
+{
+ static const float taur = -0.5f;
+ static const float taui = 0.866025403784439f;
+ static const float taui_2 = 0.866025403784439f * 2;
+ int i, k, ic;
+ v4sf ci2, ci3, di2, di3, cr2, cr3, dr2, dr3, ti2, tr2;
+ for (k = 0; k < l1; k++) {
+ tr2 = cc[ido - 1 + (3 * k + 1) * ido];
+ tr2 = VADD(tr2, tr2);
+ cr2 = VMADD(LD_PS1(taur), tr2, cc[3 * k * ido]);
+ ch[k * ido] = VADD(cc[3 * k * ido], tr2);
+ ci3 = SVMUL(taui_2, cc[(3 * k + 2) * ido]);
+ ch[(k + l1) * ido] = VSUB(cr2, ci3);
+ ch[(k + 2 * l1) * ido] = VADD(cr2, ci3);
+ }
+ if (ido == 1)
+ return;
+ for (k = 0; k < l1; k++) {
+ for (i = 2; i < ido; i += 2) {
+ ic = ido - i;
+ tr2 =
+ VADD(cc[i - 1 + (3 * k + 2) * ido],
+ cc[ic - 1 + (3 * k + 1) * ido]);
+ cr2 = VMADD(LD_PS1(taur), tr2, cc[i - 1 + 3 * k * ido]);
+ ch[i - 1 + k * ido] =
+ VADD(cc[i - 1 + 3 * k * ido], tr2);
+ ti2 =
+ VSUB(cc[i + (3 * k + 2) * ido],
+ cc[ic + (3 * k + 1) * ido]);
+ ci2 = VMADD(LD_PS1(taur), ti2, cc[i + 3 * k * ido]);
+ ch[i + k * ido] = VADD(cc[i + 3 * k * ido], ti2);
+ cr3 =
+ SVMUL(taui,
+ VSUB(cc[i - 1 + (3 * k + 2) * ido],
+ cc[ic - 1 + (3 * k + 1) * ido]));
+ ci3 =
+ SVMUL(taui,
+ VADD(cc[i + (3 * k + 2) * ido],
+ cc[ic + (3 * k + 1) * ido]));
+ dr2 = VSUB(cr2, ci3);
+ dr3 = VADD(cr2, ci3);
+ di2 = VADD(ci2, cr3);
+ di3 = VSUB(ci2, cr3);
+ VCPLXMUL(dr2, di2, LD_PS1(wa1[i - 2]),
+ LD_PS1(wa1[i - 1]));
+ ch[i - 1 + (k + l1) * ido] = dr2;
+ ch[i + (k + l1) * ido] = di2;
+ VCPLXMUL(dr3, di3, LD_PS1(wa2[i - 2]),
+ LD_PS1(wa2[i - 1]));
+ ch[i - 1 + (k + 2 * l1) * ido] = dr3;
+ ch[i + (k + 2 * l1) * ido] = di3;
+ }
+ }
+} /* radb3 */
+
+static NEVER_INLINE(void) radf4_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch,
+ const float *RESTRICT wa1,
+ const float *RESTRICT wa2,
+ const float *RESTRICT wa3)
+{
+ static const float minus_hsqt2 = (float)-0.7071067811865475;
+ int i, k, l1ido = l1 * ido;
+ {
+ const v4sf *RESTRICT cc_ = cc, *RESTRICT cc_end = cc + l1ido;
+ v4sf *RESTRICT ch_ = ch;
+ while (cc < cc_end) {
+ // this loop represents between 25% and 40% of total radf4_ps cost !
+ v4sf a0 = cc[0], a1 = cc[l1ido];
+ v4sf a2 = cc[2 * l1ido], a3 = cc[3 * l1ido];
+ v4sf tr1 = VADD(a1, a3);
+ v4sf tr2 = VADD(a0, a2);
+ ch[2 * ido - 1] = VSUB(a0, a2);
+ ch[2 * ido] = VSUB(a3, a1);
+ ch[0] = VADD(tr1, tr2);
+ ch[4 * ido - 1] = VSUB(tr2, tr1);
+ cc += ido;
+ ch += 4 * ido;
+ }
+ cc = cc_;
+ ch = ch_;
+ }
+ if (ido < 2)
+ return;
+ if (ido != 2) {
+ for (k = 0; k < l1ido; k += ido) {
+ const v4sf *RESTRICT pc = (v4sf *) (cc + 1 + k);
+ for (i = 2; i < ido; i += 2, pc += 2) {
+ int ic = ido - i;
+ v4sf wr, wi, cr2, ci2, cr3, ci3, cr4, ci4;
+ v4sf tr1, ti1, tr2, ti2, tr3, ti3, tr4, ti4;
+
+ cr2 = pc[1 * l1ido + 0];
+ ci2 = pc[1 * l1ido + 1];
+ wr = LD_PS1(wa1[i - 2]);
+ wi = LD_PS1(wa1[i - 1]);
+ VCPLXMULCONJ(cr2, ci2, wr, wi);
+
+ cr3 = pc[2 * l1ido + 0];
+ ci3 = pc[2 * l1ido + 1];
+ wr = LD_PS1(wa2[i - 2]);
+ wi = LD_PS1(wa2[i - 1]);
+ VCPLXMULCONJ(cr3, ci3, wr, wi);
+
+ cr4 = pc[3 * l1ido];
+ ci4 = pc[3 * l1ido + 1];
+ wr = LD_PS1(wa3[i - 2]);
+ wi = LD_PS1(wa3[i - 1]);
+ VCPLXMULCONJ(cr4, ci4, wr, wi);
+
+ /* at this point, on SSE, five of "cr2 cr3 cr4 ci2 ci3 ci4" should be loaded in registers */
+
+ tr1 = VADD(cr2, cr4);
+ tr4 = VSUB(cr4, cr2);
+ tr2 = VADD(pc[0], cr3);
+ tr3 = VSUB(pc[0], cr3);
+ ch[i - 1 + 4 * k] = VADD(tr1, tr2);
+ ch[ic - 1 + 4 * k + 3 * ido] = VSUB(tr2, tr1); // at this point tr1 and tr2 can be disposed
+ ti1 = VADD(ci2, ci4);
+ ti4 = VSUB(ci2, ci4);
+ ch[i - 1 + 4 * k + 2 * ido] = VADD(ti4, tr3);
+ ch[ic - 1 + 4 * k + 1 * ido] = VSUB(tr3, ti4); // dispose tr3, ti4
+ ti2 = VADD(pc[1], ci3);
+ ti3 = VSUB(pc[1], ci3);
+ ch[i + 4 * k] = VADD(ti1, ti2);
+ ch[ic + 4 * k + 3 * ido] = VSUB(ti1, ti2);
+ ch[i + 4 * k + 2 * ido] = VADD(tr4, ti3);
+ ch[ic + 4 * k + 1 * ido] = VSUB(tr4, ti3);
+ }
+ }
+ if (ido % 2 == 1)
+ return;
+ }
+ for (k = 0; k < l1ido; k += ido) {
+ v4sf a = cc[ido - 1 + k + l1ido], b =
+ cc[ido - 1 + k + 3 * l1ido];
+ v4sf c = cc[ido - 1 + k], d = cc[ido - 1 + k + 2 * l1ido];
+ v4sf ti1 = SVMUL(minus_hsqt2, VADD(a, b));
+ v4sf tr1 = SVMUL(minus_hsqt2, VSUB(b, a));
+ ch[ido - 1 + 4 * k] = VADD(tr1, c);
+ ch[ido - 1 + 4 * k + 2 * ido] = VSUB(c, tr1);
+ ch[4 * k + 1 * ido] = VSUB(ti1, d);
+ ch[4 * k + 3 * ido] = VADD(ti1, d);
+ }
+} /* radf4 */
+
+static NEVER_INLINE(void) radb4_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch,
+ const float *RESTRICT wa1,
+ const float *RESTRICT wa2,
+ const float *RESTRICT wa3)
+{
+ static const float minus_sqrt2 = (float)-1.414213562373095;
+ static const float two = 2.f;
+ int i, k, l1ido = l1 * ido;
+ v4sf ci2, ci3, ci4, cr2, cr3, cr4, ti1, ti2, ti3, ti4, tr1, tr2, tr3,
+ tr4;
+ {
+ const v4sf *RESTRICT cc_ = cc, *RESTRICT ch_end = ch + l1ido;
+ v4sf *ch_ = ch;
+ while (ch < ch_end) {
+ v4sf a = cc[0], b = cc[4 * ido - 1];
+ v4sf c = cc[2 * ido], d = cc[2 * ido - 1];
+ tr3 = SVMUL(two, d);
+ tr2 = VADD(a, b);
+ tr1 = VSUB(a, b);
+ tr4 = SVMUL(two, c);
+ ch[0 * l1ido] = VADD(tr2, tr3);
+ ch[2 * l1ido] = VSUB(tr2, tr3);
+ ch[1 * l1ido] = VSUB(tr1, tr4);
+ ch[3 * l1ido] = VADD(tr1, tr4);
+
+ cc += 4 * ido;
+ ch += ido;
+ }
+ cc = cc_;
+ ch = ch_;
+ }
+ if (ido < 2)
+ return;
+ if (ido != 2) {
+ for (k = 0; k < l1ido; k += ido) {
+ const v4sf *RESTRICT pc = (v4sf *) (cc - 1 + 4 * k);
+ v4sf *RESTRICT ph = (v4sf *) (ch + k + 1);
+ for (i = 2; i < ido; i += 2) {
+
+ tr1 = VSUB(pc[i], pc[4 * ido - i]);
+ tr2 = VADD(pc[i], pc[4 * ido - i]);
+ ti4 = VSUB(pc[2 * ido + i], pc[2 * ido - i]);
+ tr3 = VADD(pc[2 * ido + i], pc[2 * ido - i]);
+ ph[0] = VADD(tr2, tr3);
+ cr3 = VSUB(tr2, tr3);
+
+ ti3 =
+ VSUB(pc[2 * ido + i + 1],
+ pc[2 * ido - i + 1]);
+ tr4 =
+ VADD(pc[2 * ido + i + 1],
+ pc[2 * ido - i + 1]);
+ cr2 = VSUB(tr1, tr4);
+ cr4 = VADD(tr1, tr4);
+
+ ti1 = VADD(pc[i + 1], pc[4 * ido - i + 1]);
+ ti2 = VSUB(pc[i + 1], pc[4 * ido - i + 1]);
+
+ ph[1] = VADD(ti2, ti3);
+ ph += l1ido;
+ ci3 = VSUB(ti2, ti3);
+ ci2 = VADD(ti1, ti4);
+ ci4 = VSUB(ti1, ti4);
+ VCPLXMUL(cr2, ci2, LD_PS1(wa1[i - 2]),
+ LD_PS1(wa1[i - 1]));
+ ph[0] = cr2;
+ ph[1] = ci2;
+ ph += l1ido;
+ VCPLXMUL(cr3, ci3, LD_PS1(wa2[i - 2]),
+ LD_PS1(wa2[i - 1]));
+ ph[0] = cr3;
+ ph[1] = ci3;
+ ph += l1ido;
+ VCPLXMUL(cr4, ci4, LD_PS1(wa3[i - 2]),
+ LD_PS1(wa3[i - 1]));
+ ph[0] = cr4;
+ ph[1] = ci4;
+ ph = ph - 3 * l1ido + 2;
+ }
+ }
+ if (ido % 2 == 1)
+ return;
+ }
+ for (k = 0; k < l1ido; k += ido) {
+ int i0 = 4 * k + ido;
+ v4sf c = cc[i0 - 1], d = cc[i0 + 2 * ido - 1];
+ v4sf a = cc[i0 + 0], b = cc[i0 + 2 * ido + 0];
+ tr1 = VSUB(c, d);
+ tr2 = VADD(c, d);
+ ti1 = VADD(b, a);
+ ti2 = VSUB(b, a);
+ ch[ido - 1 + k + 0 * l1ido] = VADD(tr2, tr2);
+ ch[ido - 1 + k + 1 * l1ido] =
+ SVMUL(minus_sqrt2, VSUB(ti1, tr1));
+ ch[ido - 1 + k + 2 * l1ido] = VADD(ti2, ti2);
+ ch[ido - 1 + k + 3 * l1ido] =
+ SVMUL(minus_sqrt2, VADD(ti1, tr1));
+ }
+} /* radb4 */
+
+static void radf5_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1, const float *wa2,
+ const float *wa3, const float *wa4)
+{
+ static const float tr11 = .309016994374947f;
+ static const float ti11 = .951056516295154f;
+ static const float tr12 = -.809016994374947f;
+ static const float ti12 = .587785252292473f;
+
+ /* System generated locals */
+ int cc_offset, ch_offset;
+
+ /* Local variables */
+ int i, k, ic;
+ v4sf ci2, di2, ci4, ci5, di3, di4, di5, ci3, cr2, cr3, dr2, dr3, dr4,
+ dr5, cr5, cr4, ti2, ti3, ti5, ti4, tr2, tr3, tr4, tr5;
+ int idp2;
+
+#define cc_ref(a_1,a_2,a_3) cc[((a_3)*l1 + (a_2))*ido + a_1]
+#define ch_ref(a_1,a_2,a_3) ch[((a_3)*5 + (a_2))*ido + a_1]
+
+ /* Parameter adjustments */
+ ch_offset = 1 + ido * 6;
+ ch -= ch_offset;
+ cc_offset = 1 + ido * (1 + l1);
+ cc -= cc_offset;
+
+ /* Function Body */
+ for (k = 1; k <= l1; ++k) {
+ cr2 = VADD(cc_ref(1, k, 5), cc_ref(1, k, 2));
+ ci5 = VSUB(cc_ref(1, k, 5), cc_ref(1, k, 2));
+ cr3 = VADD(cc_ref(1, k, 4), cc_ref(1, k, 3));
+ ci4 = VSUB(cc_ref(1, k, 4), cc_ref(1, k, 3));
+ ch_ref(1, 1, k) = VADD(cc_ref(1, k, 1), VADD(cr2, cr3));
+ ch_ref(ido, 2, k) =
+ VADD(cc_ref(1, k, 1),
+ VADD(SVMUL(tr11, cr2), SVMUL(tr12, cr3)));
+ ch_ref(1, 3, k) = VADD(SVMUL(ti11, ci5), SVMUL(ti12, ci4));
+ ch_ref(ido, 4, k) =
+ VADD(cc_ref(1, k, 1),
+ VADD(SVMUL(tr12, cr2), SVMUL(tr11, cr3)));
+ ch_ref(1, 5, k) = VSUB(SVMUL(ti12, ci5), SVMUL(ti11, ci4));
+ //printf("pffft: radf5, k=%d ch_ref=%f, ci4=%f\n", k, ch_ref(1, 5, k), ci4);
+ }
+ if (ido == 1) {
+ return;
+ }
+ idp2 = ido + 2;
+ for (k = 1; k <= l1; ++k) {
+ for (i = 3; i <= ido; i += 2) {
+ ic = idp2 - i;
+ dr2 = LD_PS1(wa1[i - 3]);
+ di2 = LD_PS1(wa1[i - 2]);
+ dr3 = LD_PS1(wa2[i - 3]);
+ di3 = LD_PS1(wa2[i - 2]);
+ dr4 = LD_PS1(wa3[i - 3]);
+ di4 = LD_PS1(wa3[i - 2]);
+ dr5 = LD_PS1(wa4[i - 3]);
+ di5 = LD_PS1(wa4[i - 2]);
+ VCPLXMULCONJ(dr2, di2, cc_ref(i - 1, k, 2),
+ cc_ref(i, k, 2));
+ VCPLXMULCONJ(dr3, di3, cc_ref(i - 1, k, 3),
+ cc_ref(i, k, 3));
+ VCPLXMULCONJ(dr4, di4, cc_ref(i - 1, k, 4),
+ cc_ref(i, k, 4));
+ VCPLXMULCONJ(dr5, di5, cc_ref(i - 1, k, 5),
+ cc_ref(i, k, 5));
+ cr2 = VADD(dr2, dr5);
+ ci5 = VSUB(dr5, dr2);
+ cr5 = VSUB(di2, di5);
+ ci2 = VADD(di2, di5);
+ cr3 = VADD(dr3, dr4);
+ ci4 = VSUB(dr4, dr3);
+ cr4 = VSUB(di3, di4);
+ ci3 = VADD(di3, di4);
+ ch_ref(i - 1, 1, k) =
+ VADD(cc_ref(i - 1, k, 1), VADD(cr2, cr3));
+ ch_ref(i, 1, k) = VSUB(cc_ref(i, k, 1), VADD(ci2, ci3)); //
+ tr2 =
+ VADD(cc_ref(i - 1, k, 1),
+ VADD(SVMUL(tr11, cr2), SVMUL(tr12, cr3)));
+ ti2 = VSUB(cc_ref(i, k, 1), VADD(SVMUL(tr11, ci2), SVMUL(tr12, ci3))); //
+ tr3 =
+ VADD(cc_ref(i - 1, k, 1),
+ VADD(SVMUL(tr12, cr2), SVMUL(tr11, cr3)));
+ ti3 = VSUB(cc_ref(i, k, 1), VADD(SVMUL(tr12, ci2), SVMUL(tr11, ci3))); //
+ tr5 = VADD(SVMUL(ti11, cr5), SVMUL(ti12, cr4));
+ ti5 = VADD(SVMUL(ti11, ci5), SVMUL(ti12, ci4));
+ tr4 = VSUB(SVMUL(ti12, cr5), SVMUL(ti11, cr4));
+ ti4 = VSUB(SVMUL(ti12, ci5), SVMUL(ti11, ci4));
+ ch_ref(i - 1, 3, k) = VSUB(tr2, tr5);
+ ch_ref(ic - 1, 2, k) = VADD(tr2, tr5);
+ ch_ref(i, 3, k) = VADD(ti2, ti5);
+ ch_ref(ic, 2, k) = VSUB(ti5, ti2);
+ ch_ref(i - 1, 5, k) = VSUB(tr3, tr4);
+ ch_ref(ic - 1, 4, k) = VADD(tr3, tr4);
+ ch_ref(i, 5, k) = VADD(ti3, ti4);
+ ch_ref(ic, 4, k) = VSUB(ti4, ti3);
+ }
+ }
+#undef cc_ref
+#undef ch_ref
+} /* radf5 */
+
+static void radb5_ps(int ido, int l1, const v4sf * RESTRICT cc,
+ v4sf * RESTRICT ch, const float *wa1, const float *wa2,
+ const float *wa3, const float *wa4)
+{
+ static const float tr11 = .309016994374947f;
+ static const float ti11 = .951056516295154f;
+ static const float tr12 = -.809016994374947f;
+ static const float ti12 = .587785252292473f;
+
+ int cc_offset, ch_offset;
+
+ /* Local variables */
+ int i, k, ic;
+ v4sf ci2, ci3, ci4, ci5, di3, di4, di5, di2, cr2, cr3, cr5, cr4, ti2,
+ ti3, ti4, ti5, dr3, dr4, dr5, dr2, tr2, tr3, tr4, tr5;
+ int idp2;
+
+#define cc_ref(a_1,a_2,a_3) cc[((a_3)*5 + (a_2))*ido + a_1]
+#define ch_ref(a_1,a_2,a_3) ch[((a_3)*l1 + (a_2))*ido + a_1]
+
+ /* Parameter adjustments */
+ ch_offset = 1 + ido * (1 + l1);
+ ch -= ch_offset;
+ cc_offset = 1 + ido * 6;
+ cc -= cc_offset;
+
+ /* Function Body */
+ for (k = 1; k <= l1; ++k) {
+ ti5 = VADD(cc_ref(1, 3, k), cc_ref(1, 3, k));
+ ti4 = VADD(cc_ref(1, 5, k), cc_ref(1, 5, k));
+ tr2 = VADD(cc_ref(ido, 2, k), cc_ref(ido, 2, k));
+ tr3 = VADD(cc_ref(ido, 4, k), cc_ref(ido, 4, k));
+ ch_ref(1, k, 1) = VADD(cc_ref(1, 1, k), VADD(tr2, tr3));
+ cr2 =
+ VADD(cc_ref(1, 1, k),
+ VADD(SVMUL(tr11, tr2), SVMUL(tr12, tr3)));
+ cr3 =
+ VADD(cc_ref(1, 1, k),
+ VADD(SVMUL(tr12, tr2), SVMUL(tr11, tr3)));
+ ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4));
+ ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4));
+ ch_ref(1, k, 2) = VSUB(cr2, ci5);
+ ch_ref(1, k, 3) = VSUB(cr3, ci4);
+ ch_ref(1, k, 4) = VADD(cr3, ci4);
+ ch_ref(1, k, 5) = VADD(cr2, ci5);
+ }
+ if (ido == 1) {
+ return;
+ }
+ idp2 = ido + 2;
+ for (k = 1; k <= l1; ++k) {
+ for (i = 3; i <= ido; i += 2) {
+ ic = idp2 - i;
+ ti5 = VADD(cc_ref(i, 3, k), cc_ref(ic, 2, k));
+ ti2 = VSUB(cc_ref(i, 3, k), cc_ref(ic, 2, k));
+ ti4 = VADD(cc_ref(i, 5, k), cc_ref(ic, 4, k));
+ ti3 = VSUB(cc_ref(i, 5, k), cc_ref(ic, 4, k));
+ tr5 = VSUB(cc_ref(i - 1, 3, k), cc_ref(ic - 1, 2, k));
+ tr2 = VADD(cc_ref(i - 1, 3, k), cc_ref(ic - 1, 2, k));
+ tr4 = VSUB(cc_ref(i - 1, 5, k), cc_ref(ic - 1, 4, k));
+ tr3 = VADD(cc_ref(i - 1, 5, k), cc_ref(ic - 1, 4, k));
+ ch_ref(i - 1, k, 1) =
+ VADD(cc_ref(i - 1, 1, k), VADD(tr2, tr3));
+ ch_ref(i, k, 1) = VADD(cc_ref(i, 1, k), VADD(ti2, ti3));
+ cr2 =
+ VADD(cc_ref(i - 1, 1, k),
+ VADD(SVMUL(tr11, tr2), SVMUL(tr12, tr3)));
+ ci2 =
+ VADD(cc_ref(i, 1, k),
+ VADD(SVMUL(tr11, ti2), SVMUL(tr12, ti3)));
+ cr3 =
+ VADD(cc_ref(i - 1, 1, k),
+ VADD(SVMUL(tr12, tr2), SVMUL(tr11, tr3)));
+ ci3 =
+ VADD(cc_ref(i, 1, k),
+ VADD(SVMUL(tr12, ti2), SVMUL(tr11, ti3)));
+ cr5 = VADD(SVMUL(ti11, tr5), SVMUL(ti12, tr4));
+ ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4));
+ cr4 = VSUB(SVMUL(ti12, tr5), SVMUL(ti11, tr4));
+ ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4));
+ dr3 = VSUB(cr3, ci4);
+ dr4 = VADD(cr3, ci4);
+ di3 = VADD(ci3, cr4);
+ di4 = VSUB(ci3, cr4);
+ dr5 = VADD(cr2, ci5);
+ dr2 = VSUB(cr2, ci5);
+ di5 = VSUB(ci2, cr5);
+ di2 = VADD(ci2, cr5);
+ VCPLXMUL(dr2, di2, LD_PS1(wa1[i - 3]),
+ LD_PS1(wa1[i - 2]));
+ VCPLXMUL(dr3, di3, LD_PS1(wa2[i - 3]),
+ LD_PS1(wa2[i - 2]));
+ VCPLXMUL(dr4, di4, LD_PS1(wa3[i - 3]),
+ LD_PS1(wa3[i - 2]));
+ VCPLXMUL(dr5, di5, LD_PS1(wa4[i - 3]),
+ LD_PS1(wa4[i - 2]));
+
+ ch_ref(i - 1, k, 2) = dr2;
+ ch_ref(i, k, 2) = di2;
+ ch_ref(i - 1, k, 3) = dr3;
+ ch_ref(i, k, 3) = di3;
+ ch_ref(i - 1, k, 4) = dr4;
+ ch_ref(i, k, 4) = di4;
+ ch_ref(i - 1, k, 5) = dr5;
+ ch_ref(i, k, 5) = di5;
+ }
+ }
+#undef cc_ref
+#undef ch_ref
+} /* radb5 */
+
+static NEVER_INLINE(v4sf *) rfftf1_ps(int n, const v4sf * input_readonly,
+ v4sf * work1, v4sf * work2,
+ const float *wa, const int *ifac)
+{
+ v4sf *in = (v4sf *) input_readonly;
+ v4sf *out = (in == work2 ? work1 : work2);
+ int nf = ifac[1], k1;
+ int l2 = n;
+ int iw = n - 1;
+ assert(in != out && work1 != work2);
+ for (k1 = 1; k1 <= nf; ++k1) {
+ int kh = nf - k1;
+ int ip = ifac[kh + 2];
+ int l1 = l2 / ip;
+ int ido = n / l2;
+ iw -= (ip - 1) * ido;
+ switch (ip) {
+ case 5:{
+ int ix2 = iw + ido;
+ int ix3 = ix2 + ido;
+ int ix4 = ix3 + ido;
+ radf5_ps(ido, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3], &wa[ix4]);
+ } break;
+ case 4:{
+ int ix2 = iw + ido;
+ int ix3 = ix2 + ido;
+ radf4_ps(ido, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3]);
+ } break;
+ case 3:{
+ int ix2 = iw + ido;
+ radf3_ps(ido, l1, in, out, &wa[iw], &wa[ix2]);
+ } break;
+ case 2:
+ radf2_ps(ido, l1, in, out, &wa[iw]);
+ break;
+ default:
+ assert(0);
+ break;
+ }
+ l2 = l1;
+ if (out == work2) {
+ out = work1;
+ in = work2;
+ } else {
+ out = work2;
+ in = work1;
+ }
+ }
+ return in; /* this is in fact the output .. */
+} /* rfftf1 */
+
+static NEVER_INLINE(v4sf *) rfftb1_ps(int n, const v4sf * input_readonly,
+ v4sf * work1, v4sf * work2,
+ const float *wa, const int *ifac)
+{
+ v4sf *in = (v4sf *) input_readonly;
+ v4sf *out = (in == work2 ? work1 : work2);
+ int nf = ifac[1], k1;
+ int l1 = 1;
+ int iw = 0;
+ assert(in != out);
+ for (k1 = 1; k1 <= nf; k1++) {
+ int ip = ifac[k1 + 1];
+ int l2 = ip * l1;
+ int ido = n / l2;
+ switch (ip) {
+ case 5:{
+ int ix2 = iw + ido;
+ int ix3 = ix2 + ido;
+ int ix4 = ix3 + ido;
+ radb5_ps(ido, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3], &wa[ix4]);
+ } break;
+ case 4:{
+ int ix2 = iw + ido;
+ int ix3 = ix2 + ido;
+ radb4_ps(ido, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3]);
+ } break;
+ case 3:{
+ int ix2 = iw + ido;
+ radb3_ps(ido, l1, in, out, &wa[iw], &wa[ix2]);
+ } break;
+ case 2:
+ radb2_ps(ido, l1, in, out, &wa[iw]);
+ break;
+ default:
+ assert(0);
+ break;
+ }
+ l1 = l2;
+ iw += (ip - 1) * ido;
+
+ if (out == work2) {
+ out = work1;
+ in = work2;
+ } else {
+ out = work2;
+ in = work1;
+ }
+ }
+ return in; /* this is in fact the output .. */
+}
+
+static int decompose(int n, int *ifac, const int *ntryh)
+{
+ int nl = n, nf = 0, i, j = 0;
+ for (j = 0; ntryh[j]; ++j) {
+ int ntry = ntryh[j];
+ while (nl != 1) {
+ int nq = nl / ntry;
+ int nr = nl - ntry * nq;
+ if (nr == 0) {
+ ifac[2 + nf++] = ntry;
+ nl = nq;
+ if (ntry == 2 && nf != 1) {
+ for (i = 2; i <= nf; ++i) {
+ int ib = nf - i + 2;
+ ifac[ib + 1] = ifac[ib];
+ }
+ ifac[2] = 2;
+ }
+ } else
+ break;
+ }
+ }
+ ifac[0] = n;
+ ifac[1] = nf;
+ return nf;
+}
+
+static void rffti1_ps(int n, float *wa, int *ifac)
+{
+ static const int ntryh[] = { 4, 2, 3, 5, 0 };
+ int k1, j, ii;
+
+ int nf = decompose(n, ifac, ntryh);
+ float argh = (2 * M_PI) / n;
+ int is = 0;
+ int nfm1 = nf - 1;
+ int l1 = 1;
+ for (k1 = 1; k1 <= nfm1; k1++) {
+ int ip = ifac[k1 + 1];
+ int ld = 0;
+ int l2 = l1 * ip;
+ int ido = n / l2;
+ int ipm = ip - 1;
+ for (j = 1; j <= ipm; ++j) {
+ float argld;
+ int i = is, fi = 0;
+ ld += l1;
+ argld = ld * argh;
+ for (ii = 3; ii <= ido; ii += 2) {
+ i += 2;
+ fi += 1;
+ wa[i - 2] = cos(fi * argld);
+ wa[i - 1] = sin(fi * argld);
+ }
+ is += ido;
+ }
+ l1 = l2;
+ }
+} /* rffti1 */
+
+static void cffti1_ps(int n, float *wa, int *ifac)
+{
+ static const int ntryh[] = { 5, 3, 4, 2, 0 };
+ int k1, j, ii;
+
+ int nf = decompose(n, ifac, ntryh);
+ float argh = (2 * M_PI) / (float)n;
+ int i = 1;
+ int l1 = 1;
+ for (k1 = 1; k1 <= nf; k1++) {
+ int ip = ifac[k1 + 1];
+ int ld = 0;
+ int l2 = l1 * ip;
+ int ido = n / l2;
+ int idot = ido + ido + 2;
+ int ipm = ip - 1;
+ for (j = 1; j <= ipm; j++) {
+ float argld;
+ int i1 = i, fi = 0;
+ wa[i - 1] = 1;
+ wa[i] = 0;
+ ld += l1;
+ argld = ld * argh;
+ for (ii = 4; ii <= idot; ii += 2) {
+ i += 2;
+ fi += 1;
+ wa[i - 1] = cos(fi * argld);
+ wa[i] = sin(fi * argld);
+ }
+ if (ip > 5) {
+ wa[i1 - 1] = wa[i - 1];
+ wa[i1] = wa[i];
+ }
+ }
+ l1 = l2;
+ }
+} /* cffti1 */
+
+static v4sf *cfftf1_ps(int n, const v4sf * input_readonly, v4sf * work1, v4sf * work2,
+ const float *wa, const int *ifac, int isign)
+{
+ v4sf *in = (v4sf *) input_readonly;
+ v4sf *out = (in == work2 ? work1 : work2);
+ int nf = ifac[1], k1;
+ int l1 = 1;
+ int iw = 0;
+ assert(in != out && work1 != work2);
+ for (k1 = 2; k1 <= nf + 1; k1++) {
+ int ip = ifac[k1];
+ int l2 = ip * l1;
+ int ido = n / l2;
+ int idot = ido + ido;
+ switch (ip) {
+ case 5:{
+ int ix2 = iw + idot;
+ int ix3 = ix2 + idot;
+ int ix4 = ix3 + idot;
+ passf5_ps(idot, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3], &wa[ix4], isign);
+ } break;
+ case 4:{
+ int ix2 = iw + idot;
+ int ix3 = ix2 + idot;
+ passf4_ps(idot, l1, in, out, &wa[iw], &wa[ix2],
+ &wa[ix3], isign);
+ } break;
+ case 2:{
+ passf2_ps(idot, l1, in, out, &wa[iw], isign);
+ }
+ break;
+ case 3:{
+ int ix2 = iw + idot;
+ passf3_ps(idot, l1, in, out, &wa[iw], &wa[ix2],
+ isign);
+ } break;
+ default:
+ assert(0);
+ }
+ l1 = l2;
+ iw += (ip - 1) * idot;
+ if (out == work2) {
+ out = work1;
+ in = work2;
+ } else {
+ out = work2;
+ in = work1;
+ }
+ }
+
+ return in; /* this is in fact the output .. */
+}
+
+struct PFFFT_Setup {
+ int N;
+ int Ncvec; // nb of complex simd vectors (N/4 if PFFFT_COMPLEX, N/8 if PFFFT_REAL)
+ int ifac[15];
+ pffft_transform_t transform;
+ v4sf *data; // allocated room for twiddle coefs
+ float *e; // points into 'data' , N/4*3 elements
+ float *twiddle; // points into 'data', N/4 elements
+};
+
+struct funcs {
+ PFFFT_Setup * (*new_setup) (int N, pffft_transform_t transform);
+ void (*transform) (PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction, int ordered);
+ void (*zreorder)(PFFFT_Setup *setup, const float *input, float *output, pffft_direction_t direction);
+ void (*zconvolve_accumulate)(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, const float *dft_c, float *dft_ab, float scaling);
+ void (*zconvolve)(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab, float scaling);
+ int (*simd_size)(void);
+ void (*validate)(void);
+};
+
+static PFFFT_Setup *new_setup_simd(int N, pffft_transform_t transform)
+{
+ PFFFT_Setup *s = (PFFFT_Setup *) malloc(sizeof(PFFFT_Setup));
+ int k, m;
+ /* unfortunately, the fft size must be a multiple of 16 for complex FFTs
+ and 32 for real FFTs -- a lot of stuff would need to be rewritten to
+ handle other cases (or maybe just switch to a scalar fft, I don't know..) */
+ if (transform == PFFFT_REAL) {
+ assert((N % (2 * SIMD_SZ * SIMD_SZ)) == 0 && N > 0);
+ }
+ if (transform == PFFFT_COMPLEX) {
+ assert((N % (SIMD_SZ * SIMD_SZ)) == 0 && N > 0);
+ }
+ //assert((N % 32) == 0);
+ s->N = N;
+ s->transform = transform;
+ /* nb of complex simd vectors */
+ s->Ncvec = (transform == PFFFT_REAL ? N / 2 : N) / SIMD_SZ;
+ s->data = (v4sf *) pffft_aligned_malloc(2 * s->Ncvec * sizeof(v4sf));
+ s->e = (float *)s->data;
+ s->twiddle =
+ (float *)(s->data + (2 * s->Ncvec * (SIMD_SZ - 1)) / SIMD_SZ);
+
+ if (transform == PFFFT_REAL) {
+ for (k = 0; k < s->Ncvec; ++k) {
+ int i = k / SIMD_SZ;
+ int j = k % SIMD_SZ;
+ for (m = 0; m < SIMD_SZ - 1; ++m) {
+ float A = -2 * M_PI * (m + 1) * k / N;
+ s->e[(2 * (i * 3 + m) + 0) * SIMD_SZ + j] =
+ cos(A);
+ s->e[(2 * (i * 3 + m) + 1) * SIMD_SZ + j] =
+ sin(A);
+ }
+ }
+ rffti1_ps(N / SIMD_SZ, s->twiddle, s->ifac);
+ } else {
+ for (k = 0; k < s->Ncvec; ++k) {
+ int i = k / SIMD_SZ;
+ int j = k % SIMD_SZ;
+ for (m = 0; m < SIMD_SZ - 1; ++m) {
+ float A = -2 * M_PI * (m + 1) * k / N;
+ s->e[(2 * (i * 3 + m) + 0) * SIMD_SZ + j] =
+ cos(A);
+ s->e[(2 * (i * 3 + m) + 1) * SIMD_SZ + j] =
+ sin(A);
+ }
+ }
+ cffti1_ps(N / SIMD_SZ, s->twiddle, s->ifac);
+ }
+
+ /* check that N is decomposable with allowed prime factors */
+ for (k = 0, m = 1; k < s->ifac[1]; ++k) {
+ m *= s->ifac[2 + k];
+ }
+ if (m != N / SIMD_SZ) {
+ pffft_destroy_setup(s);
+ s = 0;
+ }
+
+ return s;
+}
+
+#if !defined(PFFFT_SIMD_DISABLE)
+
+/* [0 0 1 2 3 4 5 6 7 8] -> [0 8 7 6 5 4 3 2 1] */
+static void reversed_copy(int N, const v4sf * in, int in_stride, v4sf * out)
+{
+ v4sf g0, g1;
+ int k;
+ INTERLEAVE2(in[0], in[1], g0, g1);
+ in += in_stride;
+
+ *--out = VSWAPHL(g0, g1); // [g0l, g0h], [g1l g1h] -> [g1l, g0h]
+ for (k = 1; k < N; ++k) {
+ v4sf h0, h1;
+ INTERLEAVE2(in[0], in[1], h0, h1);
+ in += in_stride;
+ *--out = VSWAPHL(g1, h0);
+ *--out = VSWAPHL(h0, h1);
+ g1 = h1;
+ }
+ *--out = VSWAPHL(g1, g0);
+}
+
+static void unreversed_copy(int N, const v4sf * in, v4sf * out, int out_stride)
+{
+ v4sf g0, g1, h0, h1;
+ int k;
+ g0 = g1 = in[0];
+ ++in;
+ for (k = 1; k < N; ++k) {
+ h0 = *in++;
+ h1 = *in++;
+ g1 = VSWAPHL(g1, h0);
+ h0 = VSWAPHL(h0, h1);
+ UNINTERLEAVE2(h0, g1, out[0], out[1]);
+ out += out_stride;
+ g1 = h1;
+ }
+ h0 = *in++;
+ h1 = g0;
+ g1 = VSWAPHL(g1, h0);
+ h0 = VSWAPHL(h0, h1);
+ UNINTERLEAVE2(h0, g1, out[0], out[1]);
+}
+
+static void zreorder_simd(PFFFT_Setup * setup, const float *in, float *out,
+ pffft_direction_t direction)
+{
+ int k, N = setup->N, Ncvec = setup->Ncvec;
+ const v4sf *vin = (const v4sf *)in;
+ v4sf *vout = (v4sf *) out;
+ assert(in != out);
+ if (setup->transform == PFFFT_REAL) {
+ int k, dk = N / 32;
+ if (direction == PFFFT_FORWARD) {
+ for (k = 0; k < dk; ++k) {
+ INTERLEAVE2(vin[k * 8 + 0], vin[k * 8 + 1],
+ vout[2 * (0 * dk + k) + 0],
+ vout[2 * (0 * dk + k) + 1]);
+ INTERLEAVE2(vin[k * 8 + 4], vin[k * 8 + 5],
+ vout[2 * (2 * dk + k) + 0],
+ vout[2 * (2 * dk + k) + 1]);
+ }
+ reversed_copy(dk, vin + 2, 8, (v4sf *) (out + N / 2));
+ reversed_copy(dk, vin + 6, 8, (v4sf *) (out + N));
+ } else {
+ for (k = 0; k < dk; ++k) {
+ UNINTERLEAVE2(vin[2 * (0 * dk + k) + 0],
+ vin[2 * (0 * dk + k) + 1],
+ vout[k * 8 + 0], vout[k * 8 + 1]);
+ UNINTERLEAVE2(vin[2 * (2 * dk + k) + 0],
+ vin[2 * (2 * dk + k) + 1],
+ vout[k * 8 + 4], vout[k * 8 + 5]);
+ }
+ unreversed_copy(dk, (v4sf *) (in + N / 4),
+ (v4sf *) (out + N - 6 * SIMD_SZ), -8);
+ unreversed_copy(dk, (v4sf *) (in + 3 * N / 4),
+ (v4sf *) (out + N - 2 * SIMD_SZ), -8);
+ }
+ } else {
+ if (direction == PFFFT_FORWARD) {
+ for (k = 0; k < Ncvec; ++k) {
+ int kk = (k / 4) + (k % 4) * (Ncvec / 4);
+ INTERLEAVE2(vin[k * 2], vin[k * 2 + 1],
+ vout[kk * 2], vout[kk * 2 + 1]);
+ }
+ } else {
+ for (k = 0; k < Ncvec; ++k) {
+ int kk = (k / 4) + (k % 4) * (Ncvec / 4);
+ UNINTERLEAVE2(vin[kk * 2], vin[kk * 2 + 1],
+ vout[k * 2], vout[k * 2 + 1]);
+ }
+ }
+ }
+}
+
+static void pffft_cplx_finalize(int Ncvec, const v4sf * in, v4sf * out, const v4sf * e)
+{
+ int k, dk = Ncvec / SIMD_SZ; // number of 4x4 matrix blocks
+ v4sf r0, i0, r1, i1, r2, i2, r3, i3;
+ v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1;
+ assert(in != out);
+ for (k = 0; k < dk; ++k) {
+ r0 = in[8 * k + 0];
+ i0 = in[8 * k + 1];
+ r1 = in[8 * k + 2];
+ i1 = in[8 * k + 3];
+ r2 = in[8 * k + 4];
+ i2 = in[8 * k + 5];
+ r3 = in[8 * k + 6];
+ i3 = in[8 * k + 7];
+ VTRANSPOSE4(r0, r1, r2, r3);
+ VTRANSPOSE4(i0, i1, i2, i3);
+ VCPLXMUL(r1, i1, e[k * 6 + 0], e[k * 6 + 1]);
+ VCPLXMUL(r2, i2, e[k * 6 + 2], e[k * 6 + 3]);
+ VCPLXMUL(r3, i3, e[k * 6 + 4], e[k * 6 + 5]);
+
+ sr0 = VADD(r0, r2);
+ dr0 = VSUB(r0, r2);
+ sr1 = VADD(r1, r3);
+ dr1 = VSUB(r1, r3);
+ si0 = VADD(i0, i2);
+ di0 = VSUB(i0, i2);
+ si1 = VADD(i1, i3);
+ di1 = VSUB(i1, i3);
+
+ /*
+ transformation for each column is:
+
+ [1 1 1 1 0 0 0 0] [r0]
+ [1 0 -1 0 0 -1 0 1] [r1]
+ [1 -1 1 -1 0 0 0 0] [r2]
+ [1 0 -1 0 0 1 0 -1] [r3]
+ [0 0 0 0 1 1 1 1] * [i0]
+ [0 1 0 -1 1 0 -1 0] [i1]
+ [0 0 0 0 1 -1 1 -1] [i2]
+ [0 -1 0 1 1 0 -1 0] [i3]
+ */
+
+ r0 = VADD(sr0, sr1);
+ i0 = VADD(si0, si1);
+ r1 = VADD(dr0, di1);
+ i1 = VSUB(di0, dr1);
+ r2 = VSUB(sr0, sr1);
+ i2 = VSUB(si0, si1);
+ r3 = VSUB(dr0, di1);
+ i3 = VADD(di0, dr1);
+
+ *out++ = r0;
+ *out++ = i0;
+ *out++ = r1;
+ *out++ = i1;
+ *out++ = r2;
+ *out++ = i2;
+ *out++ = r3;
+ *out++ = i3;
+ }
+}
+
+static void pffft_cplx_preprocess(int Ncvec, const v4sf * in, v4sf * out,
+ const v4sf * e)
+{
+ int k, dk = Ncvec / SIMD_SZ; // number of 4x4 matrix blocks
+ v4sf r0, i0, r1, i1, r2, i2, r3, i3;
+ v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1;
+ assert(in != out);
+ for (k = 0; k < dk; ++k) {
+ r0 = in[8 * k + 0];
+ i0 = in[8 * k + 1];
+ r1 = in[8 * k + 2];
+ i1 = in[8 * k + 3];
+ r2 = in[8 * k + 4];
+ i2 = in[8 * k + 5];
+ r3 = in[8 * k + 6];
+ i3 = in[8 * k + 7];
+
+ sr0 = VADD(r0, r2);
+ dr0 = VSUB(r0, r2);
+ sr1 = VADD(r1, r3);
+ dr1 = VSUB(r1, r3);
+ si0 = VADD(i0, i2);
+ di0 = VSUB(i0, i2);
+ si1 = VADD(i1, i3);
+ di1 = VSUB(i1, i3);
+
+ r0 = VADD(sr0, sr1);
+ i0 = VADD(si0, si1);
+ r1 = VSUB(dr0, di1);
+ i1 = VADD(di0, dr1);
+ r2 = VSUB(sr0, sr1);
+ i2 = VSUB(si0, si1);
+ r3 = VADD(dr0, di1);
+ i3 = VSUB(di0, dr1);
+
+ VCPLXMULCONJ(r1, i1, e[k * 6 + 0], e[k * 6 + 1]);
+ VCPLXMULCONJ(r2, i2, e[k * 6 + 2], e[k * 6 + 3]);
+ VCPLXMULCONJ(r3, i3, e[k * 6 + 4], e[k * 6 + 5]);
+
+ VTRANSPOSE4(r0, r1, r2, r3);
+ VTRANSPOSE4(i0, i1, i2, i3);
+
+ *out++ = r0;
+ *out++ = i0;
+ *out++ = r1;
+ *out++ = i1;
+ *out++ = r2;
+ *out++ = i2;
+ *out++ = r3;
+ *out++ = i3;
+ }
+}
+
+static ALWAYS_INLINE(void) pffft_real_finalize_4x4(const v4sf * in0,
+ const v4sf * in1,
+ const v4sf * in,
+ const v4sf * e, v4sf * out)
+{
+ v4sf r0, i0, r1, i1, r2, i2, r3, i3;
+ v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1;
+ r0 = *in0;
+ i0 = *in1;
+ r1 = *in++;
+ i1 = *in++;
+ r2 = *in++;
+ i2 = *in++;
+ r3 = *in++;
+ i3 = *in++;
+ VTRANSPOSE4(r0, r1, r2, r3);
+ VTRANSPOSE4(i0, i1, i2, i3);
+
+ /*
+ transformation for each column is:
+
+ [1 1 1 1 0 0 0 0] [r0]
+ [1 0 -1 0 0 -1 0 1] [r1]
+ [1 0 -1 0 0 1 0 -1] [r2]
+ [1 -1 1 -1 0 0 0 0] [r3]
+ [0 0 0 0 1 1 1 1] * [i0]
+ [0 -1 0 1 -1 0 1 0] [i1]
+ [0 -1 0 1 1 0 -1 0] [i2]
+ [0 0 0 0 -1 1 -1 1] [i3]
+ */
+
+ //cerr << "matrix initial, before e , REAL:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n";
+ //cerr << "matrix initial, before e, IMAG :\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n";
+
+ VCPLXMUL(r1, i1, e[0], e[1]);
+ VCPLXMUL(r2, i2, e[2], e[3]);
+ VCPLXMUL(r3, i3, e[4], e[5]);
+
+ //cerr << "matrix initial, real part:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n";
+ //cerr << "matrix initial, imag part:\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n";
+
+ sr0 = VADD(r0, r2);
+ dr0 = VSUB(r0, r2);
+ sr1 = VADD(r1, r3);
+ dr1 = VSUB(r3, r1);
+ si0 = VADD(i0, i2);
+ di0 = VSUB(i0, i2);
+ si1 = VADD(i1, i3);
+ di1 = VSUB(i3, i1);
+
+ r0 = VADD(sr0, sr1);
+ r3 = VSUB(sr0, sr1);
+ i0 = VADD(si0, si1);
+ i3 = VSUB(si1, si0);
+ r1 = VADD(dr0, di1);
+ r2 = VSUB(dr0, di1);
+ i1 = VSUB(dr1, di0);
+ i2 = VADD(dr1, di0);
+
+ *out++ = r0;
+ *out++ = i0;
+ *out++ = r1;
+ *out++ = i1;
+ *out++ = r2;
+ *out++ = i2;
+ *out++ = r3;
+ *out++ = i3;
+
+}
+
+static NEVER_INLINE(void) pffft_real_finalize(int Ncvec, const v4sf * in,
+ v4sf * out, const v4sf * e)
+{
+ int k, dk = Ncvec / SIMD_SZ; // number of 4x4 matrix blocks
+ /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */
+
+ v4sf_union cr, ci, *uout = (v4sf_union *) out;
+ v4sf save = in[7], zero = VZERO();
+ float xr0, xi0, xr1, xi1, xr2, xi2, xr3, xi3;
+ static const float s = M_SQRT2 / 2;
+
+ cr.v = in[0];
+ ci.v = in[Ncvec * 2 - 1];
+ assert(in != out);
+ pffft_real_finalize_4x4(&zero, &zero, in + 1, e, out);
+
+ /*
+ [cr0 cr1 cr2 cr3 ci0 ci1 ci2 ci3]
+
+ [Xr(1)] ] [1 1 1 1 0 0 0 0]
+ [Xr(N/4) ] [0 0 0 0 1 s 0 -s]
+ [Xr(N/2) ] [1 0 -1 0 0 0 0 0]
+ [Xr(3N/4)] [0 0 0 0 1 -s 0 s]
+ [Xi(1) ] [1 -1 1 -1 0 0 0 0]
+ [Xi(N/4) ] [0 0 0 0 0 -s -1 -s]
+ [Xi(N/2) ] [0 -1 0 1 0 0 0 0]
+ [Xi(3N/4)] [0 0 0 0 0 -s 1 -s]
+ */
+
+ xr0 = (cr.f[0] + cr.f[2]) + (cr.f[1] + cr.f[3]);
+ uout[0].f[0] = xr0;
+ xi0 = (cr.f[0] + cr.f[2]) - (cr.f[1] + cr.f[3]);
+ uout[1].f[0] = xi0;
+ xr2 = (cr.f[0] - cr.f[2]);
+ uout[4].f[0] = xr2;
+ xi2 = (cr.f[3] - cr.f[1]);
+ uout[5].f[0] = xi2;
+ xr1 = ci.f[0] + s * (ci.f[1] - ci.f[3]);
+ uout[2].f[0] = xr1;
+ xi1 = -ci.f[2] - s * (ci.f[1] + ci.f[3]);
+ uout[3].f[0] = xi1;
+ xr3 = ci.f[0] - s * (ci.f[1] - ci.f[3]);
+ uout[6].f[0] = xr3;
+ xi3 = ci.f[2] - s * (ci.f[1] + ci.f[3]);
+ uout[7].f[0] = xi3;
+
+ for (k = 1; k < dk; ++k) {
+ v4sf save_next = in[8 * k + 7];
+ pffft_real_finalize_4x4(&save, &in[8 * k + 0], in + 8 * k + 1,
+ e + k * 6, out + k * 8);
+ save = save_next;
+ }
+
+}
+
+static ALWAYS_INLINE(void) pffft_real_preprocess_4x4(const v4sf * in,
+ const v4sf * e, v4sf * out,
+ int first)
+{
+ v4sf r0 = in[0], i0 = in[1], r1 = in[2], i1 = in[3], r2 = in[4], i2 =
+ in[5], r3 = in[6], i3 = in[7];
+ /*
+ transformation for each column is:
+
+ [1 1 1 1 0 0 0 0] [r0]
+ [1 0 0 -1 0 -1 -1 0] [r1]
+ [1 -1 -1 1 0 0 0 0] [r2]
+ [1 0 0 -1 0 1 1 0] [r3]
+ [0 0 0 0 1 -1 1 -1] * [i0]
+ [0 -1 1 0 1 0 0 1] [i1]
+ [0 0 0 0 1 1 -1 -1] [i2]
+ [0 1 -1 0 1 0 0 1] [i3]
+ */
+
+ v4sf sr0 = VADD(r0, r3), dr0 = VSUB(r0, r3);
+ v4sf sr1 = VADD(r1, r2), dr1 = VSUB(r1, r2);
+ v4sf si0 = VADD(i0, i3), di0 = VSUB(i0, i3);
+ v4sf si1 = VADD(i1, i2), di1 = VSUB(i1, i2);
+
+ r0 = VADD(sr0, sr1);
+ r2 = VSUB(sr0, sr1);
+ r1 = VSUB(dr0, si1);
+ r3 = VADD(dr0, si1);
+ i0 = VSUB(di0, di1);
+ i2 = VADD(di0, di1);
+ i1 = VSUB(si0, dr1);
+ i3 = VADD(si0, dr1);
+
+ VCPLXMULCONJ(r1, i1, e[0], e[1]);
+ VCPLXMULCONJ(r2, i2, e[2], e[3]);
+ VCPLXMULCONJ(r3, i3, e[4], e[5]);
+
+ VTRANSPOSE4(r0, r1, r2, r3);
+ VTRANSPOSE4(i0, i1, i2, i3);
+
+ if (!first) {
+ *out++ = r0;
+ *out++ = i0;
+ }
+ *out++ = r1;
+ *out++ = i1;
+ *out++ = r2;
+ *out++ = i2;
+ *out++ = r3;
+ *out++ = i3;
+}
+
+static NEVER_INLINE(void) pffft_real_preprocess(int Ncvec, const v4sf * in,
+ v4sf * out, const v4sf * e)
+{
+ int k, dk = Ncvec / SIMD_SZ; // number of 4x4 matrix blocks
+ /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */
+
+ v4sf_union Xr, Xi, *uout = (v4sf_union *) out;
+ float cr0, ci0, cr1, ci1, cr2, ci2, cr3, ci3;
+ static const float s = M_SQRT2;
+ assert(in != out);
+ for (k = 0; k < 4; ++k) {
+ Xr.f[k] = ((float *)in)[8 * k];
+ Xi.f[k] = ((float *)in)[8 * k + 4];
+ }
+
+ pffft_real_preprocess_4x4(in, e, out + 1, 1); // will write only 6 values
+
+ /*
+ [Xr0 Xr1 Xr2 Xr3 Xi0 Xi1 Xi2 Xi3]
+
+ [cr0] [1 0 2 0 1 0 0 0]
+ [cr1] [1 0 0 0 -1 0 -2 0]
+ [cr2] [1 0 -2 0 1 0 0 0]
+ [cr3] [1 0 0 0 -1 0 2 0]
+ [ci0] [0 2 0 2 0 0 0 0]
+ [ci1] [0 s 0 -s 0 -s 0 -s]
+ [ci2] [0 0 0 0 0 -2 0 2]
+ [ci3] [0 -s 0 s 0 -s 0 -s]
+ */
+ for (k = 1; k < dk; ++k) {
+ pffft_real_preprocess_4x4(in + 8 * k, e + k * 6,
+ out - 1 + k * 8, 0);
+ }
+
+ cr0 = (Xr.f[0] + Xi.f[0]) + 2 * Xr.f[2];
+ uout[0].f[0] = cr0;
+ cr1 = (Xr.f[0] - Xi.f[0]) - 2 * Xi.f[2];
+ uout[0].f[1] = cr1;
+ cr2 = (Xr.f[0] + Xi.f[0]) - 2 * Xr.f[2];
+ uout[0].f[2] = cr2;
+ cr3 = (Xr.f[0] - Xi.f[0]) + 2 * Xi.f[2];
+ uout[0].f[3] = cr3;
+ ci0 = 2 * (Xr.f[1] + Xr.f[3]);
+ uout[2 * Ncvec - 1].f[0] = ci0;
+ ci1 = s * (Xr.f[1] - Xr.f[3]) - s * (Xi.f[1] + Xi.f[3]);
+ uout[2 * Ncvec - 1].f[1] = ci1;
+ ci2 = 2 * (Xi.f[3] - Xi.f[1]);
+ uout[2 * Ncvec - 1].f[2] = ci2;
+ ci3 = -s * (Xr.f[1] - Xr.f[3]) - s * (Xi.f[1] + Xi.f[3]);
+ uout[2 * Ncvec - 1].f[3] = ci3;
+}
+
+static void transform_simd(PFFFT_Setup * setup, const float *finput,
+ float *foutput, float * scratch,
+ pffft_direction_t direction, int ordered)
+{
+ int k, Ncvec = setup->Ncvec;
+ int nf_odd = (setup->ifac[1] & 1);
+
+ // temporary buffer is allocated on the stack if the scratch pointer is NULL
+ int stack_allocate = (scratch == 0 ? Ncvec * 2 : 1);
+ VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate);
+
+ const v4sf *vinput = (const v4sf *)finput;
+ v4sf *voutput = (v4sf *) foutput;
+ v4sf *buff[2] = { voutput, scratch ? (v4sf*)scratch : scratch_on_stack };
+ int ib = (nf_odd ^ ordered ? 1 : 0);
+
+ assert(VALIGNED(finput) && VALIGNED(foutput));
+
+ //assert(finput != foutput);
+ if (direction == PFFFT_FORWARD) {
+ ib = !ib;
+ if (setup->transform == PFFFT_REAL) {
+ ib = (rfftf1_ps(Ncvec * 2, vinput, buff[ib], buff[!ib],
+ setup->twiddle,
+ &setup->ifac[0]) == buff[0] ? 0 : 1);
+ pffft_real_finalize(Ncvec, buff[ib], buff[!ib],
+ (v4sf *) setup->e);
+ } else {
+ v4sf *tmp = buff[ib];
+ for (k = 0; k < Ncvec; ++k) {
+ UNINTERLEAVE2(vinput[k * 2], vinput[k * 2 + 1],
+ tmp[k * 2], tmp[k * 2 + 1]);
+ }
+ ib = (cfftf1_ps(Ncvec, buff[ib], buff[!ib], buff[ib],
+ setup->twiddle, &setup->ifac[0],
+ -1) == buff[0] ? 0 : 1);
+ pffft_cplx_finalize(Ncvec, buff[ib], buff[!ib],
+ (v4sf *) setup->e);
+ }
+ if (ordered) {
+ pffft_zreorder(setup, (float *)buff[!ib],
+ (float *)buff[ib], PFFFT_FORWARD);
+ } else
+ ib = !ib;
+ } else {
+ if (vinput == buff[ib]) {
+ ib = !ib; // may happen when finput == foutput
+ }
+ if (ordered) {
+ pffft_zreorder(setup, (float *)vinput,
+ (float *)buff[ib], PFFFT_BACKWARD);
+ vinput = buff[ib];
+ ib = !ib;
+ }
+ if (setup->transform == PFFFT_REAL) {
+ pffft_real_preprocess(Ncvec, vinput, buff[ib],
+ (v4sf *) setup->e);
+ ib = (rfftb1_ps
+ (Ncvec * 2, buff[ib], buff[0], buff[1],
+ setup->twiddle,
+ &setup->ifac[0]) == buff[0] ? 0 : 1);
+ } else {
+ pffft_cplx_preprocess(Ncvec, vinput, buff[ib],
+ (v4sf *) setup->e);
+ ib = (cfftf1_ps
+ (Ncvec, buff[ib], buff[0], buff[1],
+ setup->twiddle, &setup->ifac[0],
+ +1) == buff[0] ? 0 : 1);
+ for (k = 0; k < Ncvec; ++k) {
+ INTERLEAVE2(buff[ib][k * 2],
+ buff[ib][k * 2 + 1],
+ buff[ib][k * 2],
+ buff[ib][k * 2 + 1]);
+ }
+ }
+ }
+
+ if (buff[ib] != voutput) {
+ /* extra copy required -- this situation should only happen when finput == foutput */
+ assert(finput == foutput);
+ for (k = 0; k < Ncvec; ++k) {
+ v4sf a = buff[ib][2 * k], b = buff[ib][2 * k + 1];
+ voutput[2 * k] = a;
+ voutput[2 * k + 1] = b;
+ }
+ ib = !ib;
+ }
+ assert(buff[ib] == voutput);
+}
+
+static void zconvolve_accumulate_simd(PFFFT_Setup * s, const float *a, const float *b,
+ const float *c, float *ab, float scaling)
+{
+ const int Ncvec2 = s->Ncvec * 2;
+ const v4sf *RESTRICT va = (const v4sf *)a;
+ const v4sf *RESTRICT vb = (const v4sf *)b;
+ v4sf *RESTRICT vab = (v4sf *) ab;
+ v4sf *RESTRICT vc = (v4sf *) c;
+ v4sf vscal = LD_PS1(scaling);
+ float ar, ai, br, bi, cr, ci;
+ int i;
+
+#ifdef __arm__
+ __builtin_prefetch(va);
+ __builtin_prefetch(vb);
+ __builtin_prefetch(c);
+ __builtin_prefetch(va + 2);
+ __builtin_prefetch(vb + 2);
+ __builtin_prefetch(c + 2);
+ __builtin_prefetch(va + 4);
+ __builtin_prefetch(vb + 4);
+ __builtin_prefetch(c + 4);
+ __builtin_prefetch(va + 6);
+ __builtin_prefetch(vb + 6);
+ __builtin_prefetch(c + 6);
+#endif
+
+ assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab));
+ ar = ((v4sf_union *) va)[0].f[0];
+ ai = ((v4sf_union *) va)[1].f[0];
+ br = ((v4sf_union *) vb)[0].f[0];
+ bi = ((v4sf_union *) vb)[1].f[0];
+ cr = ((v4sf_union *) vc)[0].f[0];
+ ci = ((v4sf_union *) vc)[1].f[0];
+
+ for (i = 0; i < Ncvec2; i += 4) {
+ v4sf ar, ai, br, bi;
+ ar = va[i + 0];
+ ai = va[i + 1];
+ br = vb[i + 0];
+ bi = vb[i + 1];
+ VCPLXMUL(ar, ai, br, bi);
+ vab[i + 0] = VMADD(ar, vscal, vc[i + 0]);
+ vab[i + 1] = VMADD(ai, vscal, vc[i + 1]);
+ ar = va[i + 2];
+ ai = va[i + 3];
+ br = vb[i + 2];
+ bi = vb[i + 3];
+ VCPLXMUL(ar, ai, br, bi);
+ vab[i + 2] = VMADD(ar, vscal, vc[i + 2]);
+ vab[i + 3] = VMADD(ai, vscal, vc[i + 3]);
+ }
+ if (s->transform == PFFFT_REAL) {
+ ((v4sf_union *) vab)[0].f[0] = cr + ar * br * scaling;
+ ((v4sf_union *) vab)[1].f[0] = ci + ai * bi * scaling;
+ }
+}
+
+static void zconvolve_simd(PFFFT_Setup * s, const float *a, const float *b,
+ float *ab, float scaling)
+{
+ v4sf vscal = LD_PS1(scaling);
+ const v4sf * RESTRICT va = (const v4sf*)a;
+ const v4sf * RESTRICT vb = (const v4sf*)b;
+ v4sf * RESTRICT vab = (v4sf*)ab;
+ float sar, sai, sbr, sbi;
+ const int Ncvec2 = s->Ncvec * 2;
+ int i;
+
+#ifdef __arm__
+ __builtin_prefetch(va);
+ __builtin_prefetch(vb);
+ __builtin_prefetch(vab);
+ __builtin_prefetch(va+2);
+ __builtin_prefetch(vb+2);
+ __builtin_prefetch(vab+2);
+ __builtin_prefetch(va+4);
+ __builtin_prefetch(vb+4);
+ __builtin_prefetch(vab+4);
+ __builtin_prefetch(va+6);
+ __builtin_prefetch(vb+6);
+ __builtin_prefetch(vab+6);
+#endif
+
+ assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab));
+ sar = ((v4sf_union*)va)[0].f[0];
+ sai = ((v4sf_union*)va)[1].f[0];
+ sbr = ((v4sf_union*)vb)[0].f[0];
+ sbi = ((v4sf_union*)vb)[1].f[0];
+
+ /* default routine, works fine for non-arm cpus with current compilers */
+ for (i = 0; i < Ncvec2; i += 4) {
+ v4sf var, vai, vbr, vbi;
+ var = va[i + 0];
+ vai = va[i + 1];
+ vbr = vb[i + 0];
+ vbi = vb[i + 1];
+ VCPLXMUL(var, vai, vbr, vbi);
+ vab[i + 0] = VMUL(var, vscal);
+ vab[i + 1] = VMUL(vai, vscal);
+ var = va[i + 2];
+ vai = va[i + 3];
+ vbr = vb[i + 2];
+ vbi = vb[i + 3];
+ VCPLXMUL(var, vai, vbr, vbi);
+ vab[i + 2] = VMUL(var, vscal);
+ vab[i + 3] = VMUL(vai, vscal);
+ }
+
+ if (s->transform == PFFFT_REAL) {
+ ((v4sf_union*)vab)[0].f[0] = sar * sbr * scaling;
+ ((v4sf_union*)vab)[1].f[0] = sai * sbi * scaling;
+ }
+}
+
+#else // defined(PFFFT_SIMD_DISABLE)
+
+// standard routine using scalar floats, without SIMD stuff.
+
+static void zreorder_simd(PFFFT_Setup * setup, const float *in, float *out,
+ pffft_direction_t direction)
+{
+ int k, N = setup->N;
+ if (setup->transform == PFFFT_COMPLEX) {
+ for (k = 0; k < 2 * N; ++k)
+ out[k] = in[k];
+ return;
+ } else if (direction == PFFFT_FORWARD) {
+ float x_N = in[N - 1];
+ for (k = N - 1; k > 1; --k)
+ out[k] = in[k - 1];
+ out[0] = in[0];
+ out[1] = x_N;
+ } else {
+ float x_N = in[1];
+ for (k = 1; k < N - 1; ++k)
+ out[k] = in[k + 1];
+ out[0] = in[0];
+ out[N - 1] = x_N;
+ }
+}
+
+static void transform_simd(PFFFT_Setup * setup, const float *input,
+ float *output, float *scratch,
+ pffft_direction_t direction, int ordered)
+{
+ int Ncvec = setup->Ncvec;
+ int nf_odd = (setup->ifac[1] & 1);
+
+ // temporary buffer is allocated on the stack if the scratch pointer is NULL
+ int stack_allocate = (scratch == 0 ? Ncvec * 2 : 1);
+ VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate);
+ float *buff[2];
+ int ib;
+ if (scratch == 0)
+ scratch = scratch_on_stack;
+ buff[0] = output;
+ buff[1] = scratch;
+
+ if (setup->transform == PFFFT_COMPLEX)
+ ordered = 0; // it is always ordered.
+ ib = (nf_odd ^ ordered ? 1 : 0);
+
+ if (direction == PFFFT_FORWARD) {
+ if (setup->transform == PFFFT_REAL) {
+ ib = (rfftf1_ps(Ncvec * 2, input, buff[ib], buff[!ib],
+ setup->twiddle,
+ &setup->ifac[0]) == buff[0] ? 0 : 1);
+ } else {
+ ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib],
+ setup->twiddle, &setup->ifac[0],
+ -1) == buff[0] ? 0 : 1);
+ }
+ if (ordered) {
+ pffft_zreorder(setup, buff[ib], buff[!ib],
+ PFFFT_FORWARD);
+ ib = !ib;
+ }
+ } else {
+ if (input == buff[ib]) {
+ ib = !ib; // may happen when finput == foutput
+ }
+ if (ordered) {
+ pffft_zreorder(setup, input, buff[!ib], PFFFT_BACKWARD);
+ input = buff[!ib];
+ }
+ if (setup->transform == PFFFT_REAL) {
+ ib = (rfftb1_ps(Ncvec * 2, input, buff[ib], buff[!ib],
+ setup->twiddle,
+ &setup->ifac[0]) == buff[0] ? 0 : 1);
+ } else {
+ ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib],
+ setup->twiddle, &setup->ifac[0],
+ +1) == buff[0] ? 0 : 1);
+ }
+ }
+ if (buff[ib] != output) {
+ int k;
+ // extra copy required -- this situation should happens only when finput == foutput
+ assert(input == output);
+ for (k = 0; k < Ncvec; ++k) {
+ float a = buff[ib][2 * k], b = buff[ib][2 * k + 1];
+ output[2 * k] = a;
+ output[2 * k + 1] = b;
+ }
+ ib = !ib;
+ }
+ assert(buff[ib] == output);
+}
+
+
+static void zconvolve_accumulate_simd(PFFFT_Setup * s, const float *a,
+ const float *b, const float *c, float *ab, float scaling)
+{
+ int i, Ncvec2 = s->Ncvec * 2;
+
+ if (s->transform == PFFFT_REAL) {
+ // take care of the fftpack ordering
+ ab[0] = c[0] + a[0] * b[0] * scaling;
+ ab[Ncvec2 - 1] = c[Ncvec2 - 1] + a[Ncvec2 - 1] * b[Ncvec2 - 1] * scaling;
+ ++ab;
+ ++c;
+ ++a;
+ ++b;
+ Ncvec2 -= 2;
+ }
+ for (i = 0; i < Ncvec2; i += 2) {
+ float ar, ai, br, bi;
+ ar = a[i + 0];
+ ai = a[i + 1];
+ br = b[i + 0];
+ bi = b[i + 1];
+ VCPLXMUL(ar, ai, br, bi);
+ ab[i + 0] = c[i + 0] + ar * scaling;
+ ab[i + 1] = c[i + 1] + ai * scaling;
+ }
+}
+
+static void zconvolve_simd(PFFFT_Setup * s, const float *a,
+ const float *b, float *ab, float scaling)
+{
+ int i, Ncvec2 = s->Ncvec * 2;
+
+ if (s->transform == PFFFT_REAL) {
+ // take care of the fftpack ordering
+ ab[0] = a[0] * b[0] * scaling;
+ ab[Ncvec2 - 1] =
+ a[Ncvec2 - 1] * b[Ncvec2 - 1] * scaling;
+ ++ab;
+ ++a;
+ ++b;
+ Ncvec2 -= 2;
+ }
+ for (i = 0; i < Ncvec2; i += 2) {
+ float ar, ai, br, bi;
+ ar = a[i + 0];
+ ai = a[i + 1];
+ br = b[i + 0];
+ bi = b[i + 1];
+ VCPLXMUL(ar, ai, br, bi);
+ ab[i + 0] = ar * scaling;
+ ab[i + 1] = ai * scaling;
+ }
+}
+#endif // defined(PFFFT_SIMD_DISABLE)
+
+static int simd_size_simd(void)
+{
+ return SIMD_SZ;
+}
+
+struct funcs pffft_funcs = {
+ .new_setup = new_setup_simd,
+ .transform = transform_simd,
+ .zreorder = zreorder_simd,
+ .zconvolve_accumulate = zconvolve_accumulate_simd,
+ .zconvolve = zconvolve_simd,
+ .simd_size = simd_size_simd,
+ .validate = validate_pffft_simd,
+};
+
+#if defined(PFFFT_SIMD_DISABLE)
+
+extern struct funcs pffft_funcs_c;
+#if (defined(HAVE_SSE))
+extern struct funcs pffft_funcs_sse;
+#endif
+#if (defined(HAVE_ALTIVEC))
+extern struct funcs pffft_funcs_altivec;
+#endif
+#if (defined(HAVE_NEON))
+extern struct funcs pffft_funcs_neon;
+#endif
+
+static struct funcs *funcs = &pffft_funcs_c;
+
+/* SSE and co like 16-bytes aligned pointers */
+#define MALLOC_V4SF_ALIGNMENT 64 // with a 64-byte alignment, we are even aligned on L2 cache lines...
+void *pffft_aligned_malloc(size_t nb_bytes)
+{
+ void *p, *p0 = malloc(nb_bytes + MALLOC_V4SF_ALIGNMENT);
+ if (!p0)
+ return (void *)0;
+ p = (void *)(((size_t)p0 + MALLOC_V4SF_ALIGNMENT) &
+ (~((size_t)(MALLOC_V4SF_ALIGNMENT - 1))));
+ *((void **)p - 1) = p0;
+ return p;
+}
+
+void pffft_aligned_free(void *p)
+{
+ if (p)
+ free(*((void **)p - 1));
+}
+
+int pffft_simd_size(void)
+{
+ return funcs->simd_size();
+}
+
+PFFFT_Setup *pffft_new_setup(int N, pffft_transform_t transform)
+{
+ return funcs->new_setup(N, transform);
+}
+
+void pffft_destroy_setup(PFFFT_Setup * s)
+{
+ pffft_aligned_free(s->data);
+ free(s);
+}
+
+void pffft_transform(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction)
+{
+ return funcs->transform(setup, input, output, work, direction, 0);
+}
+
+void pffft_transform_ordered(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction)
+{
+ return funcs->transform(setup, input, output, work, direction, 1);
+}
+
+void pffft_zreorder(PFFFT_Setup *setup, const float *input, float *output, pffft_direction_t direction)
+{
+ return funcs->zreorder(setup, input, output, direction);
+}
+
+void pffft_zconvolve_accumulate(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, const float *c, float *dft_ab, float scaling)
+{
+ return funcs->zconvolve_accumulate(setup, dft_a, dft_b, c, dft_ab, scaling);
+}
+
+void pffft_zconvolve(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab, float scaling)
+{
+ return funcs->zconvolve(setup, dft_a, dft_b, dft_ab, scaling);
+}
+
+void pffft_select_cpu(int flags)
+{
+ funcs = &pffft_funcs_c;
+#if defined(HAVE_SSE)
+ if (flags & SPA_CPU_FLAG_SSE)
+ funcs = &pffft_funcs_sse;
+#endif
+#if defined(HAVE_NEON)
+ if (flags & SPA_CPU_FLAG_NEON)
+ funcs = &pffft_funcs_neon;
+#endif
+#if defined(HAVE_ALTIVEC)
+ if (flags & SPA_CPU_FLAG_ALTIVEC)
+ funcs = &pffft_funcs_altivec;
+#endif
+}
+
+#endif
diff --git a/src/modules/module-filter-chain/pffft.h b/src/modules/module-filter-chain/pffft.h
new file mode 100644
index 0000000..ac8e271
--- /dev/null
+++ b/src/modules/module-filter-chain/pffft.h
@@ -0,0 +1,181 @@
+/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com )
+
+ Based on original fortran 77 code from FFTPACKv4 from NETLIB,
+ authored by Dr Paul Swarztrauber of NCAR, in 1985.
+
+ As confirmed by the NCAR fftpack software curators, the following
+ FFTPACKv5 license applies to FFTPACKv4 sources. My changes are
+ released under the same terms.
+
+ FFTPACK license:
+
+ http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html
+
+ Copyright (c) 2004 the University Corporation for Atmospheric
+ Research ("UCAR"). All rights reserved. Developed by NCAR's
+ Computational and Information Systems Laboratory, UCAR,
+ www.cisl.ucar.edu.
+
+ Redistribution and use of the Software in source and binary forms,
+ with or without modification, is permitted provided that the
+ following conditions are met:
+
+ - Neither the names of NCAR's Computational and Information Systems
+ Laboratory, the University Corporation for Atmospheric Research,
+ nor the names of its sponsors or contributors may be used to
+ endorse or promote products derived from this Software without
+ specific prior written permission.
+
+ - Redistributions of source code must retain the above copyright
+ notices, this list of conditions, and the disclaimer below.
+
+ - Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions, and the disclaimer below in the
+ documentation and/or other materials provided with the
+ distribution.
+
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+ SOFTWARE.
+*/
+
+/*
+ PFFFT : a Pretty Fast FFT.
+
+ This is basically an adaptation of the single precision fftpack
+ (v4) as found on netlib taking advantage of SIMD instruction found
+ on cpus such as intel x86 (SSE1), powerpc (Altivec), and arm (NEON).
+
+ For architectures where no SIMD instruction is available, the code
+ falls back to a scalar version.
+
+ Restrictions:
+
+ - 1D transforms only, with 32-bit single precision.
+
+ - supports only transforms for inputs of length N of the form
+ N=(2^a)*(3^b)*(5^c), a >= 5, b >=0, c >= 0 (32, 48, 64, 96, 128,
+ 144, 160, etc are all acceptable lengths). Performance is best for
+ 128<=N<=8192.
+
+ - all (float*) pointers in the functions below are expected to
+ have an "simd-compatible" alignment, that is 16 bytes on x86 and
+ powerpc CPUs.
+
+ You can allocate such buffers with the functions
+ pffft_aligned_malloc / pffft_aligned_free (or with stuff like
+ posix_memalign..)
+
+*/
+
+#ifndef PFFFT_H
+#define PFFFT_H
+
+#include <stddef.h> // for size_t
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ /* opaque struct holding internal stuff (precomputed twiddle factors)
+ this struct can be shared by many threads as it contains only
+ read-only data.
+ */
+ typedef struct PFFFT_Setup PFFFT_Setup;
+
+ /* direction of the transform */
+ typedef enum { PFFFT_FORWARD, PFFFT_BACKWARD } pffft_direction_t;
+
+ /* type of transform */
+ typedef enum { PFFFT_REAL, PFFFT_COMPLEX } pffft_transform_t;
+
+ /*
+ prepare for performing transforms of size N -- the returned
+ PFFFT_Setup structure is read-only so it can safely be shared by
+ multiple concurrent threads.
+ */
+ PFFFT_Setup *pffft_new_setup(int N, pffft_transform_t transform);
+ void pffft_destroy_setup(PFFFT_Setup *);
+ /*
+ Perform a Fourier transform , The z-domain data is stored in the
+ most efficient order for transforming it back, or using it for
+ convolution. If you need to have its content sorted in the
+ "usual" way, that is as an array of interleaved complex numbers,
+ either use pffft_transform_ordered , or call pffft_zreorder after
+ the forward fft, and before the backward fft.
+
+ Transforms are not scaled: PFFFT_BACKWARD(PFFFT_FORWARD(x)) = N*x.
+ Typically you will want to scale the backward transform by 1/N.
+
+ The 'work' pointer should point to an area of N (2*N for complex
+ fft) floats, properly aligned. If 'work' is NULL, then stack will
+ be used instead (this is probably the best strategy for small
+ FFTs, say for N < 16384).
+
+ input and output may alias.
+ */
+ void pffft_transform(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction);
+
+ /*
+ Similar to pffft_transform, but makes sure that the output is
+ ordered as expected (interleaved complex numbers). This is
+ similar to calling pffft_transform and then pffft_zreorder.
+
+ input and output may alias.
+ */
+ void pffft_transform_ordered(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction);
+
+ /*
+ call pffft_zreorder(.., PFFFT_FORWARD) after pffft_transform(...,
+ PFFFT_FORWARD) if you want to have the frequency components in
+ the correct "canonical" order, as interleaved complex numbers.
+
+ (for real transforms, both 0-frequency and half frequency
+ components, which are real, are assembled in the first entry as
+ F(0)+i*F(n/2+1). Note that the original fftpack did place
+ F(n/2+1) at the end of the arrays).
+
+ input and output should not alias.
+ */
+ void pffft_zreorder(PFFFT_Setup *setup, const float *input, float *output, pffft_direction_t direction);
+
+ /*
+ Perform a multiplication of the frequency components of dft_a and
+ dft_b and accumulate them into dft_ab. The arrays should have
+ been obtained with pffft_transform(.., PFFFT_FORWARD) and should
+ *not* have been reordered with pffft_zreorder (otherwise just
+ perform the operation yourself as the dft coefs are stored as
+ interleaved complex numbers).
+
+ the operation performed is: dft_ab = dft_c + (dft_a * fdt_b)*scaling
+
+ The dft_a, dft_b and dft_ab pointers may alias.
+ */
+ void pffft_zconvolve_accumulate(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, const float *dft_c, float *dft_ab, float scaling);
+
+ void pffft_zconvolve(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab, float scaling);
+
+ /*
+ the float buffers must have the correct alignment (16-byte boundary
+ on intel and powerpc). This function may be used to obtain such
+ correctly aligned buffers.
+ */
+ void *pffft_aligned_malloc(size_t nb_bytes);
+ void pffft_aligned_free(void *);
+
+ /* return 4 or 1 depending on whether support for SSE/Altivec instructions was enabled when building pffft.c */
+ int pffft_simd_size(void);
+
+ void pffft_select_cpu(int flags);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // PFFFT_H
diff --git a/src/modules/module-filter-chain/plugin.h b/src/modules/module-filter-chain/plugin.h
new file mode 100644
index 0000000..72946e4
--- /dev/null
+++ b/src/modules/module-filter-chain/plugin.h
@@ -0,0 +1,112 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PLUGIN_H
+#define PLUGIN_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include <errno.h>
+#include <stdio.h>
+#include <limits.h>
+
+#include <spa/support/plugin.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+#include <spa/utils/string.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_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 (*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);
+}
+
+struct fc_plugin *load_ladspa_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *path, const char *config);
+struct fc_plugin *load_lv2_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *path, const char *config);
+struct fc_plugin *load_builtin_plugin(const struct spa_support *support, uint32_t n_support,
+ struct dsp_ops *dsp, const char *path, const char *config);
+
+#endif /* PLUGIN_H */
diff --git a/src/modules/module-link-factory.c b/src/modules/module-link-factory.c
new file mode 100644
index 0000000..509b211
--- /dev/null
+++ b/src/modules/module-link-factory.c
@@ -0,0 +1,565 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/impl.h>
+
+/** \page page_module_link_factory PipeWire Module: Link Factory
+ */
+
+#define NAME "link-factory"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FACTORY_USAGE PW_KEY_LINK_OUTPUT_NODE"=<output-node> " \
+ "["PW_KEY_LINK_OUTPUT_PORT"=<output-port>] " \
+ PW_KEY_LINK_INPUT_NODE"=<input-node> " \
+ "["PW_KEY_LINK_INPUT_PORT"=<input-port>] " \
+ "["PW_KEY_OBJECT_LINGER"=<bool>] " \
+ "["PW_KEY_LINK_PASSIVE"=<bool>]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Allow clients to create links" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct factory_data {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct spa_list link_list;
+
+ struct pw_work_queue *work;
+};
+
+struct link_data {
+ struct factory_data *data;
+ struct spa_list l;
+ struct pw_impl_link *link;
+ struct spa_hook link_listener;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ struct pw_resource *factory_resource;
+ uint32_t new_id;
+ bool linger;
+};
+
+static void resource_destroy(void *data)
+{
+ struct link_data *ld = data;
+ spa_hook_remove(&ld->resource_listener);
+ ld->resource = NULL;
+ if (ld->global)
+ pw_global_destroy(ld->global);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy
+};
+
+static void global_destroy(void *data)
+{
+ struct link_data *ld = data;
+ struct factory_data *d = ld->data;
+ pw_work_queue_cancel(d->work, ld, SPA_ID_INVALID);
+ spa_hook_remove(&ld->global_listener);
+ ld->global = NULL;
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy
+};
+
+static void link_destroy(void *data)
+{
+ struct link_data *ld = data;
+ spa_list_remove(&ld->l);
+ spa_hook_remove(&ld->link_listener);
+ if (ld->global)
+ spa_hook_remove(&ld->global_listener);
+ if (ld->resource)
+ spa_hook_remove(&ld->resource_listener);
+}
+
+static void link_initialized(void *data)
+{
+ struct link_data *ld = data;
+ struct pw_impl_client *client;
+ int res;
+
+ if (ld->factory_resource == NULL)
+ return;
+
+ client = pw_resource_get_client(ld->factory_resource);
+ ld->global = pw_impl_link_get_global(ld->link);
+ pw_global_add_listener(ld->global, &ld->global_listener, &global_events, ld);
+
+ res = pw_global_bind(ld->global, client, PW_PERM_ALL, PW_VERSION_LINK, ld->new_id);
+ if (res < 0)
+ goto error_bind;
+
+ if (!ld->linger) {
+ ld->resource = pw_impl_client_find_resource(client, ld->new_id);
+ if (ld->resource == NULL) {
+ res = -ENOENT;
+ goto error_bind;
+ }
+ pw_resource_add_listener(ld->resource, &ld->resource_listener, &resource_events, ld);
+ }
+ return;
+
+error_bind:
+ pw_resource_errorf_id(ld->factory_resource, ld->new_id, res,
+ "can't bind link: %s", spa_strerror(res));
+}
+
+static void destroy_link(void *obj, void *data, int res, uint32_t id)
+{
+ struct link_data *ld = data;
+ if (ld->global)
+ pw_global_destroy(ld->global);
+}
+
+static void link_state_changed(void *data, enum pw_link_state old,
+ enum pw_link_state state, const char *error)
+{
+ struct link_data *ld = data;
+ struct factory_data *d = ld->data;
+
+ switch (state) {
+ case PW_LINK_STATE_ERROR:
+ if (ld->linger)
+ pw_work_queue_add(d->work, ld, 0, destroy_link, ld);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_impl_link_events link_events = {
+ PW_VERSION_IMPL_LINK_EVENTS,
+ .destroy = link_destroy,
+ .initialized = link_initialized,
+ .state_changed = link_state_changed
+};
+
+static struct pw_impl_port *get_port(struct pw_impl_node *node, enum spa_direction direction)
+{
+ struct pw_impl_port *p;
+ struct pw_context *context = pw_impl_node_get_context(node);
+ int res;
+
+ p = pw_impl_node_find_port(node, direction, PW_ID_ANY);
+
+ if (p == NULL || pw_impl_port_is_linked(p)) {
+ uint32_t port_id;
+
+ port_id = pw_impl_node_get_free_port_id(node, direction);
+ if (port_id == SPA_ID_INVALID)
+ return NULL;
+
+ p = pw_context_create_port(context, direction, port_id, NULL, 0);
+ if (p == NULL)
+ return NULL;
+
+ if ((res = pw_impl_port_add(p, node)) < 0) {
+ pw_log_warn("can't add port: %s", spa_strerror(res));
+ errno = -res;
+ return NULL;
+ }
+ }
+ return p;
+}
+
+struct find_port {
+ uint32_t id;
+ const char *name;
+ enum spa_direction direction;
+ struct pw_impl_node *node;
+ struct pw_impl_port *port;
+};
+
+static int find_port_func(void *data, struct pw_global *global)
+{
+ struct find_port *find = data;
+ const char *str;
+ const struct pw_properties *props;
+
+ if (!pw_global_is_type(global, PW_TYPE_INTERFACE_Port))
+ return 0;
+ if (pw_global_get_id(global) == find->id)
+ goto found;
+
+ props = pw_global_get_properties(global);
+ if ((str = pw_properties_get(props, PW_KEY_OBJECT_PATH)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ return 0;
+found:
+ find->port = pw_global_get_object(global);
+ return 1;
+}
+
+static int find_node_port_func(void *data, struct pw_impl_port *port)
+{
+ struct find_port *find = data;
+ const char *str;
+ const struct pw_properties *props;
+
+ if (pw_impl_port_get_id(port) == find->id)
+ goto found;
+
+ props = pw_impl_port_get_properties(port);
+ if ((str = pw_properties_get(props, PW_KEY_PORT_NAME)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_PORT_ALIAS)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_OBJECT_PATH)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ return 0;
+found:
+ find->port = port;
+ return 1;
+}
+
+static struct pw_impl_port *find_port(struct pw_context *context,
+ struct pw_impl_node *node, enum spa_direction direction, const char *name)
+{
+ struct find_port find = {
+ .id = SPA_ID_INVALID,
+ .name = name,
+ .direction = direction,
+ .node = node
+ };
+ spa_atou32(name, &find.id, 0);
+
+ 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 (node != NULL) {
+ /* find port by local id */
+ if (find.id != SPA_ID_INVALID) {
+ find.port = pw_impl_node_find_port(node, find.direction, find.id);
+ if (find.port != NULL)
+ return find.port;
+ }
+ /* find port by local name */
+ if (pw_impl_node_for_each_port(find.node, find.direction,
+ find_node_port_func, &find) == 1)
+ return find.port;
+
+ } else {
+ /* find port by name */
+ if (pw_context_for_each_global(context, find_port_func, &find) == 1)
+ return find.port;
+ }
+ return NULL;
+}
+
+struct find_node {
+ uint32_t id;
+ const char *name;
+ struct pw_impl_node *node;
+};
+
+static int find_node_func(void *data, struct pw_global *global)
+{
+ struct find_node *find = data;
+ const char *str;
+ const struct pw_properties *props;
+
+ if (!pw_global_is_type(global, PW_TYPE_INTERFACE_Node))
+ return 0;
+ if (pw_global_get_id(global) == find->id)
+ goto found;
+
+ props = pw_global_get_properties(global);
+ if ((str = pw_properties_get(props, PW_KEY_NODE_NAME)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_NODE_NICK)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_NODE_DESCRIPTION)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ if ((str = pw_properties_get(props, PW_KEY_OBJECT_PATH)) != NULL &&
+ spa_streq(str, find->name))
+ goto found;
+ return 0;
+found:
+ find->node = pw_global_get_object(global);
+ return 1;
+}
+
+static struct pw_impl_node *find_node(struct pw_context *context, const char *name)
+{
+ struct find_node find = {
+ .id = SPA_ID_INVALID,
+ .name = name,
+ };
+ spa_atou32(name, &find.id, 0);
+
+ if (find.id != SPA_ID_INVALID) {
+ struct pw_global *global = pw_context_find_global(context, find.id);
+ if (global != NULL && pw_global_is_type(global, PW_TYPE_INTERFACE_Node))
+ return pw_global_get_object(global);
+ }
+ if (pw_context_for_each_global(context, find_node_func, &find) == 1)
+ return find.node;
+ return NULL;
+}
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = _data;
+ struct pw_impl_client *client = NULL;
+ struct pw_impl_node *output_node, *input_node;
+ struct pw_impl_port *outport = NULL, *inport = NULL;
+ struct pw_context *context = d->context;
+ struct pw_impl_link *link;
+ const char *output_node_str, *input_node_str;
+ const char *output_port_str, *input_port_str;
+ struct link_data *ld;
+ int res;
+ bool linger;
+
+ if (properties == NULL)
+ goto error_properties;
+
+ if ((output_node_str = pw_properties_get(properties, PW_KEY_LINK_OUTPUT_NODE)) != NULL)
+ output_node = find_node(context, output_node_str);
+ else
+ output_node = NULL;
+
+ if ((output_port_str = pw_properties_get(properties, PW_KEY_LINK_OUTPUT_PORT)) != NULL)
+ outport = find_port(context, output_node, SPA_DIRECTION_OUTPUT, output_port_str);
+ else if (output_node != NULL)
+ outport = get_port(output_node, SPA_DIRECTION_OUTPUT);
+ if (outport == NULL)
+ goto error_output_port;
+
+ if ((input_node_str = pw_properties_get(properties, PW_KEY_LINK_INPUT_NODE)) != NULL)
+ input_node = find_node(context, input_node_str);
+ else
+ input_node = NULL;
+
+ if ((input_port_str = pw_properties_get(properties, PW_KEY_LINK_INPUT_PORT)) != NULL)
+ inport = find_port(context, input_node, SPA_DIRECTION_INPUT, input_port_str);
+ else if (input_node != NULL)
+ inport = get_port(input_node, SPA_DIRECTION_INPUT);
+ if (inport == NULL)
+ goto error_input_port;
+
+ linger = pw_properties_get_bool(properties, PW_KEY_OBJECT_LINGER, false);
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ client = resource ? pw_resource_get_client(resource) : NULL;
+ if (client && !linger)
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+
+
+ link = pw_context_create_link(context, outport, inport, NULL, properties, sizeof(struct link_data));
+ properties = NULL;
+ if (link == NULL) {
+ res = -errno;
+ goto error_create_link;
+ }
+
+ ld = pw_impl_link_get_user_data(link);
+ ld->data = d;
+ ld->factory_resource = resource;
+ ld->link = link;
+ ld->new_id = new_id;
+ ld->linger = linger;
+ spa_list_append(&d->link_list, &ld->l);
+
+ pw_impl_link_add_listener(link, &ld->link_listener, &link_events, ld);
+ if ((res = pw_impl_link_register(link, NULL)) < 0)
+ goto error_link_register;
+
+ return link;
+
+error_properties:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, NAME": no properties. usage:"FACTORY_USAGE);
+ goto error_exit;
+error_output_port:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, NAME": unknown output port %s", output_port_str);
+ goto error_exit;
+error_input_port:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, NAME": unknown input port %s", input_port_str);
+ goto error_exit;
+error_create_link:
+ pw_resource_errorf_id(resource, new_id, res, NAME": can't link ports %d and %d: %s",
+ pw_impl_port_get_info(outport)->id, pw_impl_port_get_info(inport)->id,
+ spa_strerror(res));
+ goto error_exit;
+error_link_register:
+ pw_resource_errorf_id(resource, new_id, res, NAME": can't register link: %s", spa_strerror(res));
+ goto error_exit;
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ struct link_data *ld, *t;
+
+ spa_hook_remove(&d->factory_listener);
+
+ spa_list_for_each_safe(ld, t, &d->link_list, l)
+ pw_impl_link_destroy(ld->link);
+
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "link-factory",
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ pw_properties_new(
+ PW_KEY_FACTORY_USAGE, FACTORY_USAGE,
+ NULL),
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+ data->context = context;
+ data->work = pw_context_get_work_queue(context);
+
+ spa_list_init(&data->link_list);
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c
new file mode 100644
index 0000000..7d66539
--- /dev/null
+++ b/src/modules/module-loopback.c
@@ -0,0 +1,737 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/latency-utils.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+/** \page page_module_loopback PipeWire Module: Loopback
+ *
+ * The loopback module passes the output of a capture stream unmodified to a playback stream.
+ * It can be used to construct a link between a source and sink but also to
+ * create new virtual sinks or sources or to remap channel between streams.
+ *
+ * Because both ends of the loopback are built with streams, the session manager can
+ * manage the configuration and connection with the sinks and sources.
+ *
+ * ## Module Options
+ *
+ * - `node.description`: a human readable name for the loopback streams
+ * - `target.delay.sec`: delay in seconds as float (Since 0.3.60)
+ * - `capture.props = {}`: properties to be passed to the input stream
+ * - `playback.props = {}`: properties to be passed to the output stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior. Most options can be added to the global
+ * configuration or the individual streams:
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LINK_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_NODE_NAME: See notes below. If not specified, defaults to
+ * 'loopback-<pid>-<module-id>'.
+ *
+ * Stream only properties:
+ *
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_NODE_NAME: if not given per stream, the global node.name will be
+ * prefixed with 'input.' and 'output.' to generate a capture and playback
+ * stream node.name respectively.
+ *
+ * ## Example configuration of a virtual sink
+ *
+ * This Virtual sink routes stereo input to the rear channels of a 7.1 sink.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-loopback
+ * args = {
+ * node.description = "CM106 Stereo Pair 2"
+ * #target.delay.sec = 1.5
+ * capture.props = {
+ * node.name = "CM106_stereo_pair_2"
+ * media.class = "Audio/Sink"
+ * audio.position = [ FL FR ]
+ * }
+ * playback.props = {
+ * node.name = "playback.CM106_stereo_pair_2"
+ * audio.position = [ RL RR ]
+ * target.object = "alsa_output.usb-0d8c_USB_Sound_Device-00.analog-surround-71"
+ * node.dont-reconnect = true
+ * stream.dont-remix = true
+ * node.passive = true
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * `pw-loopback` : a tool that loads the loopback module with given parameters.
+ */
+
+#define NAME "loopback"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create loopback streams" },
+ { PW_KEY_MODULE_USAGE, " [ remote.name=<remote> ] "
+ "[ node.latency=<latency as fraction> ] "
+ "[ node.description=<description of the nodes> ] "
+ "[ audio.rate=<sample rate> ] "
+ "[ audio.channels=<number of channels> ] "
+ "[ audio.position=<channel map> ] "
+ "[ target.delay.sec=<delay as seconds in float> ] "
+ "[ capture.props=<properties> ] "
+ "[ playback.props=<properties> ] " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#include <stdlib.h>
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/pipewire.h>
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+ struct spa_audio_info_raw capture_info;
+ struct spa_latency_info capture_latency;
+
+ struct pw_properties *playback_props;
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+ struct spa_audio_info_raw playback_info;
+ struct spa_latency_info playback_latency;
+
+ unsigned int do_disconnect:1;
+ unsigned int recalc_delay:1;
+
+ float target_delay;
+ struct spa_ringbuffer buffer;
+ uint8_t *buffer_data;
+ uint32_t buffer_size;
+};
+
+static void capture_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->capture_listener);
+ impl->capture = NULL;
+}
+
+static void recalculate_delay(struct impl *impl)
+{
+ uint32_t target = impl->capture_info.rate * impl->target_delay, cdelay, pdelay;
+ uint32_t delay, w;
+ struct pw_time pwt;
+
+ pw_stream_get_time_n(impl->playback, &pwt, sizeof(pwt));
+ pdelay = pwt.delay;
+ pw_stream_get_time_n(impl->capture, &pwt, sizeof(pwt));
+ cdelay = pwt.delay;
+
+ delay = target - SPA_MIN(target, pdelay + cdelay);
+ delay = SPA_MIN(delay, impl->buffer_size / 4);
+
+ spa_ringbuffer_get_write_index(&impl->buffer, &w);
+ spa_ringbuffer_read_update(&impl->buffer, w - (delay * 4));
+
+ pw_log_info("target:%d c:%d + p:%d + delay:%d = (%d)",
+ target, cdelay, pdelay, delay,
+ cdelay + pdelay + delay);
+}
+
+static void capture_process(void *d)
+{
+ struct impl *impl = d;
+ pw_stream_trigger_process(impl->playback);
+}
+
+static void playback_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *in, *out;
+ uint32_t i;
+
+ if (impl->recalc_delay) {
+ recalculate_delay(impl);
+ impl->recalc_delay = false;
+ }
+
+ if ((in = pw_stream_dequeue_buffer(impl->capture)) == NULL)
+ pw_log_debug("out of capture buffers: %m");
+
+ if ((out = pw_stream_dequeue_buffer(impl->playback)) == NULL)
+ pw_log_debug("out of playback buffers: %m");
+
+ if (in != NULL && out != NULL) {
+ uint32_t outsize = UINT32_MAX;
+ int32_t stride = 0;
+ struct spa_data *d;
+ const void *src[in->buffer->n_datas];
+ uint32_t r, w, buffer_size;
+
+ for (i = 0; i < in->buffer->n_datas; i++) {
+ uint32_t offs, size;
+
+ d = &in->buffer->datas[i];
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ src[i] = SPA_PTROFF(d->data, offs, void);
+ outsize = SPA_MIN(outsize, size);
+ stride = SPA_MAX(stride, d->chunk->stride);
+ }
+ if (impl->buffer_size > 0) {
+ buffer_size = impl->buffer_size;
+ spa_ringbuffer_get_write_index(&impl->buffer, &w);
+ for (i = 0; i < in->buffer->n_datas; i++) {
+ void *buffer_data = &impl->buffer_data[i * buffer_size];
+ spa_ringbuffer_write_data(&impl->buffer,
+ buffer_data, buffer_size,
+ w % buffer_size, src[i], outsize);
+ src[i] = buffer_data;
+ }
+ w += outsize;
+ spa_ringbuffer_write_update(&impl->buffer, w);
+ spa_ringbuffer_get_read_index(&impl->buffer, &r);
+ } else {
+ r = 0;
+ buffer_size = outsize;
+ }
+ for (i = 0; i < out->buffer->n_datas; i++) {
+ d = &out->buffer->datas[i];
+
+ outsize = SPA_MIN(outsize, d->maxsize);
+
+ if (i < in->buffer->n_datas)
+ spa_ringbuffer_read_data(&impl->buffer,
+ src[i], buffer_size,
+ r % buffer_size,
+ d->data, outsize);
+ else
+ memset(d->data, 0, outsize);
+
+ d->chunk->offset = 0;
+ d->chunk->size = outsize;
+ d->chunk->stride = stride;
+ }
+ if (impl->buffer_size > 0) {
+ r += outsize;
+ spa_ringbuffer_read_update(&impl->buffer, r);
+ }
+ }
+
+ if (in != NULL)
+ pw_stream_queue_buffer(impl->capture, in);
+ if (out != NULL)
+ pw_stream_queue_buffer(impl->playback, out);
+}
+
+static void param_latency_changed(struct impl *impl, const struct spa_pod *param,
+ struct spa_latency_info *info, struct pw_stream *other)
+{
+ struct spa_latency_info latency;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return;
+
+ *info = latency;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+ pw_stream_update_params(other, params, 1);
+
+ impl->recalc_delay = true;
+}
+
+static void stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = data;
+ switch (state) {
+ case PW_STREAM_STATE_PAUSED:
+ pw_stream_flush(impl->playback, false);
+ pw_stream_flush(impl->capture, false);
+ impl->recalc_delay = true;
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("module %p: unconnected", impl);
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_info("module %p: error: %s", impl, error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void recalculate_buffer(struct impl *impl)
+{
+ if (impl->target_delay > 0.0f) {
+ uint32_t delay = impl->capture_info.rate * impl->target_delay;
+ void *data;
+
+ impl->buffer_size = (delay + (1u<<15)) * 4;
+ data = realloc(impl->buffer_data, impl->buffer_size * impl->capture_info.channels);
+ if (data == NULL) {
+ pw_log_warn("can't allocate delay buffer, delay disabled: %m");
+ impl->buffer_size = 0;
+ free(impl->buffer_data);
+ }
+ impl->buffer_data = data;
+ spa_ringbuffer_init(&impl->buffer);
+ } else {
+ impl->buffer_size = 0;
+ free(impl->buffer_data);
+ impl->buffer_data = NULL;
+ }
+ pw_log_info("configured delay:%f buffer:%d", impl->target_delay, impl->buffer_size);
+ impl->recalc_delay = true;
+}
+
+static void capture_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct impl *impl = data;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ {
+ struct spa_audio_info_raw info;
+ if (param == NULL)
+ return;
+ if (spa_format_audio_raw_parse(param, &info) < 0)
+ return;
+ if (info.rate == 0 ||
+ info.channels == 0 ||
+ info.channels > SPA_AUDIO_MAX_CHANNELS)
+ return;
+
+ impl->capture_info = info;
+ recalculate_buffer(impl);
+ break;
+ }
+ case SPA_PARAM_Latency:
+ param_latency_changed(impl, param, &impl->capture_latency, impl->playback);
+ break;
+ }
+}
+
+static const struct pw_stream_events in_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = capture_destroy,
+ .process = capture_process,
+ .state_changed = stream_state_changed,
+ .param_changed = capture_param_changed,
+};
+
+static void playback_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->playback_listener);
+ impl->playback = NULL;
+}
+
+static void playback_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct impl *impl = data;
+
+ switch (id) {
+ case SPA_PARAM_Latency:
+ param_latency_changed(impl, param, &impl->playback_latency, impl->capture);
+ break;
+ }
+}
+static const struct pw_stream_events out_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = playback_destroy,
+ .process = playback_process,
+ .state_changed = stream_state_changed,
+ .param_changed = playback_param_changed,
+};
+
+static int setup_streams(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->capture = pw_stream_new(impl->core,
+ "loopback capture", impl->capture_props);
+ impl->capture_props = NULL;
+ if (impl->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->capture,
+ &impl->capture_listener,
+ &in_stream_events, impl);
+
+ impl->playback = pw_stream_new(impl->core,
+ "loopback playback", impl->playback_props);
+ impl->playback_props = NULL;
+ if (impl->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->playback,
+ &impl->playback_listener,
+ &out_stream_events, impl);
+
+ /* connect playback first to activate it before capture triggers it */
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &impl->playback_info);
+ if ((res = pw_stream_connect(impl->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS |
+ PW_STREAM_FLAG_TRIGGER,
+ params, n_params)) < 0)
+ return res;
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &impl->capture_info);
+ if ((res = pw_stream_connect(impl->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ if (res == -ENOENT) {
+ pw_log_info("message id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ } else {
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ }
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ /* deactivate both streams before destroying any of them */
+ if (impl->capture)
+ pw_stream_set_active(impl->capture, false);
+ if (impl->playback)
+ pw_stream_set_active(impl->playback, false);
+
+ if (impl->capture)
+ pw_stream_destroy(impl->capture);
+ if (impl->playback)
+ pw_stream_destroy(impl->playback);
+
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->capture_props);
+ pw_properties_free(impl->playback_props);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static 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));
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->capture_props, key) == NULL)
+ pw_properties_set(impl->capture_props, key, str);
+ if (pw_properties_get(impl->playback_props, key) == NULL)
+ pw_properties_set(impl->playback_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct 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;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->capture_props = pw_properties_new(NULL, NULL);
+ impl->playback_props = pw_properties_new(NULL, NULL);
+ if (impl->capture_props == NULL || impl->playback_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_GROUP, "loopback-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_LINK_GROUP, "loopback-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, "resample.prefill") == NULL)
+ pw_properties_set(props, "resample.prefill", "true");
+
+ if ((str = pw_properties_get(props, "capture.props")) != NULL)
+ pw_properties_update_string(impl->capture_props, str, strlen(str));
+ if ((str = pw_properties_get(props, "playback.props")) != NULL)
+ pw_properties_update_string(impl->playback_props, str, strlen(str));
+
+ if ((str = pw_properties_get(props, "target.delay.sec")) != NULL)
+ spa_atof(str, &impl->target_delay);
+ if (impl->target_delay > 0.0f &&
+ pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL)
+ /* a source and sink (USB) usually have a 1.5 quantum delay, so we use
+ * a 2 times smaller quantum to compensate */
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u",
+ (unsigned)(impl->target_delay * 48000 / 3), 48000);
+
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LINK_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_NAME);
+ copy_props(impl, props, "resample.prefill");
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_NAME)) == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_NAME,
+ "loopback-%u-%u", pid, id);
+ str = pw_properties_get(props, PW_KEY_NODE_NAME);
+ }
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(impl->capture_props, PW_KEY_NODE_NAME,
+ "input.%s", str);
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(impl->playback_props, PW_KEY_NODE_NAME,
+ "output.%s", str);
+ if (pw_properties_get(impl->capture_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->capture_props, PW_KEY_NODE_DESCRIPTION, str);
+ if (pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(impl->playback_props, PW_KEY_NODE_DESCRIPTION, str);
+
+ parse_audio_info(impl->capture_props, &impl->capture_info);
+ parse_audio_info(impl->playback_props, &impl->playback_info);
+
+ if (pw_properties_get(impl->capture_props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_setf(impl->capture_props, PW_KEY_MEDIA_NAME, "%s input",
+ pw_properties_get(impl->capture_props, PW_KEY_NODE_DESCRIPTION));
+ if (pw_properties_get(impl->playback_props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_setf(impl->playback_props, PW_KEY_MEDIA_NAME, "%s output",
+ pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION));
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_properties_free(props);
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ setup_streams(impl);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ pw_properties_free(props);
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-metadata.c b/src/modules/module-metadata.c
new file mode 100644
index 0000000..b8619a4
--- /dev/null
+++ b/src/modules/module-metadata.c
@@ -0,0 +1,241 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/metadata.h>
+
+/** \page page_module_metadata PipeWire Module: Metadata
+ */
+
+#define NAME "metadata"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Allow clients to create metadata store" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+
+void * pw_metadata_new(struct pw_context *context, struct pw_resource *resource,
+ struct pw_properties *properties);
+
+struct pw_proxy *pw_core_metadata_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object, size_t user_data_size);
+
+int pw_protocol_native_ext_metadata_init(struct pw_context *context);
+
+struct factory_data {
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_export_type export_metadata;
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *data = _data;
+ struct pw_context *context = pw_impl_module_get_context(data->module);
+ void *result;
+ struct pw_resource *metadata_resource = NULL;
+ struct pw_impl_client *client = resource ? pw_resource_get_client(resource) : NULL;
+ int res;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(data->factory)->id);
+ pw_properties_setf(properties, PW_KEY_MODULE_ID, "%d",
+ pw_impl_module_get_info(data->module)->id);
+
+ if (pw_properties_get(properties, PW_KEY_METADATA_NAME) == NULL)
+ pw_properties_set(properties, PW_KEY_METADATA_NAME, "default");
+
+ if (client) {
+ metadata_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (metadata_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+
+ result = pw_metadata_new(context, metadata_resource, properties);
+ if (result == NULL) {
+ properties = NULL;
+ res = -errno;
+ goto error_node;
+ }
+ } else {
+ result = pw_context_create_metadata(context, NULL, properties, 0);
+ if (result == NULL) {
+ properties = NULL;
+ res = -errno;
+ goto error_node;
+ }
+ pw_impl_metadata_register(result, NULL);
+ }
+ return result;
+
+error_resource:
+ pw_resource_errorf_id(resource, new_id, res,
+ "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_node:
+ pw_resource_errorf_id(resource, new_id, res,
+ "can't create metadata: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ if (metadata_resource)
+ pw_resource_remove(metadata_resource);
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export_metadata.link);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if ((res = pw_protocol_native_ext_metadata_init(context)) < 0)
+ return res;
+
+ factory = pw_context_create_factory(context,
+ "metadata",
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_log_debug("module %p: new", module);
+
+ pw_impl_factory_set_implementation(factory,
+ &impl_factory,
+ data);
+
+ data->export_metadata.type = PW_TYPE_INTERFACE_Metadata;
+ data->export_metadata.func = pw_core_metadata_export;
+ if ((res = pw_context_register_export_type(context, &data->export_metadata)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-metadata/metadata.c b/src/modules/module-metadata/metadata.c
new file mode 100644
index 0000000..d863bea
--- /dev/null
+++ b/src/modules/module-metadata/metadata.c
@@ -0,0 +1,316 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include <pipewire/extensions/metadata.h>
+
+#define NAME "metadata"
+
+struct impl {
+ struct spa_hook context_listener;
+
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ struct pw_metadata *metadata;
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ int pending;
+};
+
+struct resource_data {
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+ struct spa_hook metadata_listener;
+ struct spa_hook impl_resource_listener;
+ int pong_seq;
+};
+
+#define pw_metadata_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_metadata_events,m,v,__VA_ARGS__)
+
+#define pw_metadata_resource_property(r,...) \
+ pw_metadata_resource(r,property,0,__VA_ARGS__)
+
+static int metadata_property(void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct resource_data *d = data;
+ struct pw_resource *resource = d->resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct impl *impl = d->impl;
+
+ if (impl->pending == 0 || d->pong_seq != 0) {
+ if (pw_impl_client_check_permissions(client, subject, PW_PERM_R) >= 0)
+ pw_metadata_resource_property(d->resource, subject, key, type, value);
+ }
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property,
+};
+
+static int metadata_set_property(void *object,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct pw_resource *resource = d->resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ int res;
+
+ if ((res = pw_impl_client_check_permissions(client, subject, PW_PERM_R | PW_PERM_M)) < 0)
+ goto error;
+
+ pw_metadata_set_property(impl->metadata, subject, key, type, value);
+ return 0;
+
+error:
+ pw_resource_errorf(resource, res, "set property error for id %d: %s",
+ subject, spa_strerror(res));
+ return res;
+}
+
+static int metadata_clear(void *object)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ pw_metadata_clear(impl->metadata);
+ return 0;
+}
+
+static const struct pw_metadata_methods metadata_methods = {
+ PW_VERSION_METADATA_METHODS,
+ .set_property = metadata_set_property,
+ .clear = metadata_clear,
+};
+
+
+static void global_unbind(void *data)
+{
+ struct resource_data *d = data;
+ if (d->resource) {
+ spa_hook_remove(&d->resource_listener);
+ spa_hook_remove(&d->object_listener);
+ spa_hook_remove(&d->metadata_listener);
+ spa_hook_remove(&d->impl_resource_listener);
+ }
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = global_unbind,
+};
+
+static void remove_pending(struct resource_data *d)
+{
+ if (d->pong_seq != 0) {
+ pw_impl_client_set_busy(pw_resource_get_client(d->resource), false);
+ d->pong_seq = 0;
+ d->impl->pending--;
+ }
+}
+
+static void impl_resource_destroy(void *data)
+{
+ struct resource_data *d = data;
+ remove_pending(d);
+}
+
+static void impl_resource_pong(void *data, int seq)
+{
+ struct resource_data *d = data;
+ if (d->pong_seq == seq)
+ remove_pending(d);
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions, PW_TYPE_INTERFACE_Metadata, version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* listen for when the resource goes away */
+ pw_resource_add_listener(resource,
+ &data->resource_listener,
+ &resource_events, data);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &metadata_methods, data);
+
+ pw_impl_client_set_busy(client, true);
+
+ /* implementation events -> resource */
+ pw_metadata_add_listener(impl->metadata,
+ &data->metadata_listener,
+ &metadata_events, data);
+
+ pw_resource_add_listener(impl->resource,
+ &data->impl_resource_listener,
+ &impl_resource_events, data);
+
+ data->pong_seq = pw_resource_ping(impl->resource, data->pong_seq);
+ impl->pending++;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+
+static void context_global_removed(void *data, struct pw_global *global)
+{
+ struct impl *impl = data;
+ pw_metadata_set_property(impl->metadata,
+ pw_global_get_id(global), NULL, NULL, NULL);
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .global_removed = context_global_removed,
+};
+
+static void global_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->context_listener);
+ spa_hook_remove(&impl->resource_listener);
+ impl->resource = NULL;
+ impl->metadata = NULL;
+ if (impl->global)
+ pw_global_destroy(impl->global);
+ free(impl);
+}
+
+static const struct pw_resource_events global_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = global_resource_destroy,
+};
+
+void *
+pw_metadata_new(struct pw_context *context, struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ pw_resource_install_marshal(resource, true);
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+ impl->metadata = (struct pw_metadata*)resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ pw_context_add_listener(context, &impl->context_listener,
+ &context_events, impl);
+
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+
+ pw_resource_set_bound_id(resource, pw_global_get_id(impl->global));
+ pw_global_register(impl->global);
+
+ pw_resource_add_listener(resource,
+ &impl->resource_listener,
+ &global_resource_events, impl);
+
+ return impl;
+}
diff --git a/src/modules/module-metadata/protocol-native.c b/src/modules/module-metadata/protocol-native.c
new file mode 100644
index 0000000..54958db
--- /dev/null
+++ b/src/modules/module-metadata/protocol-native.c
@@ -0,0 +1,354 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/pipewire.h>
+
+#include <pipewire/extensions/protocol-native.h>
+#include <pipewire/extensions/metadata.h>
+
+static int metadata_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_metadata_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ pw_resource_add_object_listener(resource, listener, events, data);
+
+ b = pw_protocol_native_begin_resource(resource, PW_METADATA_METHOD_ADD_LISTENER, NULL);
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int metadata_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_metadata_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int metadata_resource_demarshal_add_listener(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ return -ENOTSUP;
+}
+
+static const struct pw_metadata_events pw_protocol_native_metadata_client_event_marshal;
+
+static int metadata_proxy_demarshal_add_listener(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_hook listener;
+ int res;
+
+ spa_zero(listener);
+ res = pw_proxy_notify(proxy, struct pw_metadata_methods, add_listener, 0,
+ &listener, &pw_protocol_native_metadata_client_event_marshal, object);
+ spa_hook_remove(&listener);
+
+ return res;
+}
+
+static void metadata_marshal_set_property(struct spa_pod_builder *b, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(subject),
+ SPA_POD_String(key),
+ SPA_POD_String(type),
+ SPA_POD_String(value));
+}
+
+static int metadata_proxy_marshal_set_property(void *object, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_proxy(proxy, PW_METADATA_METHOD_SET_PROPERTY, NULL);
+ metadata_marshal_set_property(b, subject, key, type, value);
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+static int metadata_resource_marshal_set_property(void *object, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_resource(resource, PW_METADATA_METHOD_SET_PROPERTY, NULL);
+ metadata_marshal_set_property(b, subject, key, type, value);
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int metadata_demarshal_set_property(struct spa_pod_parser *prs, uint32_t *subject,
+ char **key, char **type, char **value)
+{
+ return spa_pod_parser_get_struct(prs,
+ SPA_POD_Int(subject),
+ SPA_POD_String(key),
+ SPA_POD_String(type),
+ SPA_POD_String(value));
+}
+
+static int metadata_proxy_demarshal_set_property(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t subject;
+ char *key, *type, *value;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (metadata_demarshal_set_property(&prs, &subject, &key, &type, &value) < 0)
+ return -EINVAL;
+ return pw_proxy_notify(proxy, struct pw_metadata_methods, set_property, 0, subject, key, type, value);
+}
+
+static int metadata_resource_demarshal_set_property(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t subject;
+ char *key, *type, *value;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (metadata_demarshal_set_property(&prs, &subject, &key, &type, &value) < 0)
+ return -EINVAL;
+ return pw_resource_notify(resource, struct pw_metadata_methods, set_property, 0, subject, key, type, value);
+}
+
+static void metadata_marshal_clear(struct spa_pod_builder *b)
+{
+ spa_pod_builder_add_struct(b, SPA_POD_None());
+}
+
+static int metadata_proxy_marshal_clear(void *object)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_proxy(proxy, PW_METADATA_METHOD_CLEAR, NULL);
+ metadata_marshal_clear(b);
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+static int metadata_resource_marshal_clear(void *object)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_resource(resource, PW_METADATA_METHOD_CLEAR, NULL);
+ metadata_marshal_clear(b);
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int metadata_proxy_demarshal_clear(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, SPA_POD_None()) < 0)
+ return -EINVAL;
+ pw_proxy_notify(proxy, struct pw_metadata_methods, clear, 0);
+ return 0;
+}
+
+static int metadata_resource_demarshal_clear(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, SPA_POD_None()) < 0)
+ return -EINVAL;
+ pw_resource_notify(resource, struct pw_metadata_methods, clear, 0);
+ return 0;
+}
+
+static void metadata_marshal_property(struct spa_pod_builder *b, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(subject),
+ SPA_POD_String(key),
+ SPA_POD_String(type),
+ SPA_POD_String(value));
+}
+
+static int metadata_proxy_marshal_property(void *object, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_proxy(proxy, PW_METADATA_EVENT_PROPERTY, NULL);
+ metadata_marshal_property(b, subject, key, type, value);
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int metadata_resource_marshal_property(void *object, uint32_t subject,
+ const char *key, const char *type, const char *value)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+ b = pw_protocol_native_begin_resource(resource, PW_METADATA_EVENT_PROPERTY, NULL);
+ metadata_marshal_property(b, subject, key, type, value);
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int metadata_demarshal_property(struct spa_pod_parser *prs,
+ uint32_t *subject, char **key, char **type, char **value)
+{
+ return spa_pod_parser_get_struct(prs,
+ SPA_POD_Int(subject),
+ SPA_POD_String(key),
+ SPA_POD_String(type),
+ SPA_POD_String(value));
+}
+
+static int metadata_proxy_demarshal_property(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t subject;
+ char *key, *type, *value;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (metadata_demarshal_property(&prs,
+ &subject, &key, &type, &value) < 0)
+ return -EINVAL;
+ pw_proxy_notify(proxy, struct pw_metadata_events, property, 0, subject, key, type, value);
+ return 0;
+}
+
+static int metadata_resource_demarshal_property(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t subject;
+ char *key, *type, *value;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (metadata_demarshal_property(&prs,
+ &subject, &key, &type, &value) < 0)
+ return -EINVAL;
+ pw_resource_notify(resource, struct pw_metadata_events, property, 0, subject, key, type, value);
+ return 0;
+}
+
+static const struct pw_metadata_methods pw_protocol_native_metadata_client_method_marshal = {
+ PW_VERSION_METADATA_METHODS,
+ .add_listener = &metadata_proxy_marshal_add_listener,
+ .set_property = &metadata_proxy_marshal_set_property,
+ .clear = &metadata_proxy_marshal_clear,
+};
+static const struct pw_metadata_methods pw_protocol_native_metadata_server_method_marshal = {
+ PW_VERSION_METADATA_METHODS,
+ .add_listener = &metadata_resource_marshal_add_listener,
+ .set_property = &metadata_resource_marshal_set_property,
+ .clear = &metadata_resource_marshal_clear,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_metadata_client_method_demarshal[PW_METADATA_METHOD_NUM] =
+{
+ [PW_METADATA_METHOD_ADD_LISTENER] = { &metadata_proxy_demarshal_add_listener, 0 },
+ [PW_METADATA_METHOD_SET_PROPERTY] = { &metadata_proxy_demarshal_set_property, PW_PERM_W },
+ [PW_METADATA_METHOD_CLEAR] = { &metadata_proxy_demarshal_clear, PW_PERM_W },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_metadata_server_method_demarshal[PW_METADATA_METHOD_NUM] =
+{
+ [PW_METADATA_METHOD_ADD_LISTENER] = { &metadata_resource_demarshal_add_listener, 0 },
+ [PW_METADATA_METHOD_SET_PROPERTY] = { &metadata_resource_demarshal_set_property, PW_PERM_W },
+ [PW_METADATA_METHOD_CLEAR] = { &metadata_resource_demarshal_clear, PW_PERM_W },
+};
+
+static const struct pw_metadata_events pw_protocol_native_metadata_client_event_marshal = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = &metadata_proxy_marshal_property,
+};
+
+static const struct pw_metadata_events pw_protocol_native_metadata_server_event_marshal = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = &metadata_resource_marshal_property,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_metadata_client_event_demarshal[PW_METADATA_EVENT_NUM] =
+{
+ [PW_METADATA_EVENT_PROPERTY] = { &metadata_proxy_demarshal_property, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_metadata_server_event_demarshal[PW_METADATA_EVENT_NUM] =
+{
+ [PW_METADATA_EVENT_PROPERTY] = { &metadata_resource_demarshal_property, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_metadata_marshal = {
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ 0,
+ PW_METADATA_METHOD_NUM,
+ PW_METADATA_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_metadata_client_method_marshal,
+ .server_demarshal = pw_protocol_native_metadata_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_metadata_server_event_marshal,
+ .client_demarshal = pw_protocol_native_metadata_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_metadata_impl_marshal = {
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_METADATA_EVENT_NUM,
+ PW_METADATA_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_metadata_client_event_marshal,
+ .server_demarshal = pw_protocol_native_metadata_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_metadata_server_method_marshal,
+ .client_demarshal = pw_protocol_native_metadata_client_method_demarshal,
+};
+
+int pw_protocol_native_ext_metadata_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+ if (protocol == NULL)
+ return -EPROTO;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_metadata_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_metadata_impl_marshal);
+ return 0;
+}
diff --git a/src/modules/module-metadata/proxy-metadata.c b/src/modules/module-metadata/proxy-metadata.c
new file mode 100644
index 0000000..e8b747f
--- /dev/null
+++ b/src/modules/module-metadata/proxy-metadata.c
@@ -0,0 +1,92 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include <spa/pod/parser.h>
+
+#include "pipewire/pipewire.h"
+#include "pipewire/extensions/metadata.h"
+
+struct object_data {
+ struct pw_metadata *object;
+ struct spa_hook object_listener;
+ struct spa_hook object_methods;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+};
+
+static void proxy_object_destroy(void *_data)
+{
+ struct object_data *data = _data;
+ spa_hook_remove(&data->proxy_listener);
+ spa_hook_remove(&data->object_listener);
+ spa_hook_remove(&data->object_methods);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = proxy_object_destroy,
+};
+
+struct pw_proxy *pw_core_metadata_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_metadata *meta = object;
+ struct spa_interface *iface, *miface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "metadata",
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+ data->object = object;
+ data->proxy = proxy;
+
+ iface = (struct spa_interface*)proxy;
+ miface = (struct spa_interface*)meta;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ miface->cb.funcs, miface->cb.data);
+ pw_metadata_add_listener(meta, &data->object_listener,
+ iface->cb.funcs, iface->cb.data);
+
+ return proxy;
+}
diff --git a/src/modules/module-pipe-tunnel.c b/src/modules/module-pipe-tunnel.c
new file mode 100644
index 0000000..a593305
--- /dev/null
+++ b/src/modules/module-pipe-tunnel.c
@@ -0,0 +1,730 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.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/utils/ringbuffer.h>
+#include <spa/utils/dll.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+/** \page page_module_pipe_tunnel PipeWire Module: Unix Pipe Tunnel
+ *
+ * The pipe-tunnel module provides a source or sink that tunnels all audio to
+ * or from a unix pipe respectively.
+ *
+ * ## Module Options
+ *
+ * - `tunnel.mode`: the desired tunnel to create. (Default `playback`)
+ * - `pipe.filename`: the filename of the pipe.
+ * - `stream.props`: Extra properties for the local stream.
+ *
+ * When `tunnel.mode` is `capture`, a capture stream on the default source is
+ * created. Samples read from the pipe will be the contents of the captured source.
+ *
+ * When `tunnel.mode` is `sink`, a sink node is created. Samples read from the
+ * pipe will be the samples played on the sink.
+ *
+ * When `tunnel.mode` is `playback`, a playback stream on the default sink is
+ * created. Samples written to the pipe will be played on the sink.
+ *
+ * When `tunnel.mode` is `source`, a source node is created. Samples written to
+ * the pipe will be made available to streams connected to the source.
+ *
+ * When `pipe.filename` is not given, a default fifo in `/tmp/fifo_input` or
+ * `/tmp/fifo_output` will be created that can be written and read respectively,
+ * depending on the selected `tunnel.mode`.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_TARGET_OBJECT to specify the remote name or serial id to link to
+ *
+ * When not otherwise specified, the pipe will accept or produce a
+ * 16 bits, stereo, 48KHz sample stream.
+ *
+ * ## Example configuration of a pipe playback stream
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-pipe-tunnel
+ * args = {
+ * tunnel.mode = playback
+ * # Set the pipe name to tunnel to
+ * pipe.filename = "/tmp/fifo_output"
+ * #audio.format=<sample format>
+ * #audio.rate=<sample rate>
+ * #audio.channels=<number of channels>
+ * #audio.position=<channel map>
+ * #target.object=<remote target node>
+ * stream.props = {
+ * # extra sink properties
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "pipe-tunnel"
+
+#define DEFAULT_CAPTURE_FILENAME "/tmp/fifo_input"
+#define DEFAULT_PLAYBACK_FILENAME "/tmp/fifo_output"
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "[ remote.name=<remote> ] " \
+ "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ target.object=<remote node target name or serial> ] "\
+ "[ audio.format=<sample format> ] " \
+ "[ audio.rate=<sample rate> ] " \
+ "[ audio.channels=<number of channels> ] " \
+ "[ audio.position=<channel map> ] " \
+ "[ tunnel.mode=capture|playback|sink|source " \
+ "[ pipe.filename=<filename> ]" \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create a UNIX pipe tunnel" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+#define MODE_PLAYBACK 0
+#define MODE_CAPTURE 1
+#define MODE_SINK 2
+#define MODE_SOURCE 3
+ uint32_t mode;
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ char *filename;
+ unsigned int unlink_fifo;
+ int fd;
+
+ struct pw_properties *stream_props;
+ enum pw_direction direction;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ unsigned int do_disconnect:1;
+ uint32_t leftover_count;
+ uint8_t *leftover;
+};
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ default:
+ break;
+ }
+}
+
+static void playback_stream_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ uint32_t i, size, offs;
+ ssize_t written;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ for (i = 0; i < buf->buffer->n_datas; i++) {
+ struct spa_data *d;
+ d = &buf->buffer->datas[i];
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ while (size > 0) {
+ written = write(impl->fd, SPA_MEMBER(d->data, offs, void), size);
+ if (written < 0) {
+ if (errno == EINTR) {
+ /* retry if interrupted */
+ continue;
+ } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ /* Don't continue writing */
+ break;
+ } else {
+ pw_log_warn("Failed to write to pipe sink");
+ }
+ }
+ offs += written;
+ size -= written;
+ }
+ }
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static void capture_stream_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t req;
+ ssize_t nread;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ d = &buf->buffer->datas[0];
+
+ if ((req = buf->requested * impl->frame_size) == 0)
+ req = 4096 * impl->frame_size;
+
+ req = SPA_MIN(req, d->maxsize);
+
+ d->chunk->offset = 0;
+ d->chunk->stride = impl->frame_size;
+ d->chunk->size = SPA_MIN(req, impl->leftover_count);
+ memcpy(d->data, impl->leftover, d->chunk->size);
+ req -= d->chunk->size;
+
+ nread = read(impl->fd, SPA_PTROFF(d->data, d->chunk->size, void), req);
+ if (nread < 0) {
+ const bool important = !(errno == EINTR
+ || errno == EAGAIN
+ || errno == EWOULDBLOCK);
+
+ if (important)
+ pw_log_warn("failed to read from pipe (%s): %s",
+ impl->filename, strerror(errno));
+ }
+ else {
+ d->chunk->size += nread;
+ }
+
+ impl->leftover_count = d->chunk->size % impl->frame_size;
+ d->chunk->size -= impl->leftover_count;
+ memcpy(impl->leftover, SPA_PTROFF(d->data, d->chunk->size, void), impl->leftover_count);
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .process = playback_stream_process
+};
+
+static const struct pw_stream_events capture_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .process = capture_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->stream = pw_stream_new(impl->core, "pipe", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ if (impl->direction == PW_DIRECTION_OUTPUT) {
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &capture_stream_events, impl);
+ } else {
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &playback_stream_events, impl);
+ }
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ impl->direction,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int create_fifo(struct impl *impl)
+{
+ struct stat st;
+ const char *filename;
+ bool do_unlink_fifo = false;
+ int fd = -1, res;
+
+ if ((filename = pw_properties_get(impl->props, "pipe.filename")) == NULL)
+ filename = impl->direction == PW_DIRECTION_INPUT ?
+ DEFAULT_CAPTURE_FILENAME :
+ DEFAULT_PLAYBACK_FILENAME;
+
+ if (mkfifo(filename, 0666) < 0) {
+ if (errno != EEXIST) {
+ res = -errno;
+ pw_log_error("mkfifo('%s'): %s", filename, spa_strerror(res));
+ goto error;
+ }
+ } else {
+ /*
+ * Our umask is 077, so the pipe won't be created with the
+ * requested permissions. Let's fix the permissions with chmod().
+ */
+ if (chmod(filename, 0666) < 0)
+ pw_log_warn("chmod('%s'): %s", filename, spa_strerror(-errno));
+
+ do_unlink_fifo = true;
+ }
+
+ if ((fd = open(filename, O_RDWR | O_CLOEXEC | O_NONBLOCK, 0)) < 0) {
+ res = -errno;
+ pw_log_error("open('%s'): %s", filename, spa_strerror(res));
+ goto error;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ res = -errno;
+ pw_log_error("fstat('%s'): %s", filename, spa_strerror(res));
+ goto error;
+ }
+
+ if (!S_ISFIFO(st.st_mode)) {
+ res = -EINVAL;
+ pw_log_error("'%s' is not a FIFO.", filename);
+ goto error;
+ }
+ pw_log_info("%s fifo '%s' with format:%s channels:%d rate:%d",
+ impl->direction == PW_DIRECTION_OUTPUT ? "reading from" : "writing to",
+ filename,
+ spa_debug_type_find_name(spa_type_audio_format, impl->info.format),
+ impl->info.channels, impl->info.rate);
+
+ impl->filename = strdup(filename);
+ impl->unlink_fifo = do_unlink_fifo;
+ impl->fd = fd;
+ return 0;
+
+error:
+ if (do_unlink_fifo)
+ unlink(filename);
+ if (fd >= 0)
+ close(fd);
+ return res;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ if (impl->filename) {
+ if (impl->unlink_fifo)
+ unlink(impl->filename);
+ free(impl->filename);
+ }
+ if (impl->fd >= 0)
+ close(impl->fd);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl->leftover);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static 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));
+}
+
+static int calc_frame_size(const struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ struct impl *impl;
+ const char *str, *media_class = NULL;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ impl->fd = -1;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ if ((str = pw_properties_get(props, "tunnel.mode")) == NULL)
+ str = "playback";
+
+ if (spa_streq(str, "capture")) {
+ impl->mode = MODE_CAPTURE;
+ impl->direction = PW_DIRECTION_INPUT;
+ } else if (spa_streq(str, "playback")) {
+ impl->mode = MODE_PLAYBACK;
+ impl->direction = PW_DIRECTION_OUTPUT;
+ }else if (spa_streq(str, "sink")) {
+ impl->mode = MODE_SINK;
+ impl->direction = PW_DIRECTION_INPUT;
+ media_class = "Audio/Sink";
+ } else if (spa_streq(str, "source")) {
+ impl->mode = MODE_SOURCE;
+ impl->direction = PW_DIRECTION_OUTPUT;
+ media_class = "Audio/Source";
+ } else {
+ pw_log_error("invalid tunnel.mode '%s'", str);
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, media_class);
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+ copy_props(impl, props, PW_KEY_TARGET_OBJECT);
+ copy_props(impl, props, "pipe.filename");
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ pw_log_error("unsupported audio format:%d channels:%d",
+ impl->info.format, impl->info.channels);
+ res = -EINVAL;
+ goto error;
+ }
+ if (impl->info.rate != 0 &&
+ pw_properties_get(props, PW_KEY_NODE_RATE) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_RATE,
+ "1/%u", impl->info.rate),
+
+ copy_props(impl, props, PW_KEY_NODE_RATE);
+
+ impl->leftover = calloc(1, impl->frame_size);
+ if (impl->leftover == NULL) {
+ res = -errno;
+ pw_log_error("can't alloc leftover buffer: %m");
+ goto error;
+ }
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_fifo(impl)) < 0)
+ goto error;
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-portal.c b/src/modules/module-portal.c
new file mode 100644
index 0000000..ee5aa73
--- /dev/null
+++ b/src/modules/module-portal.c
@@ -0,0 +1,348 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2019 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <dbus/dbus.h>
+
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+#include <spa/support/dbus.h>
+
+#include "pipewire/context.h"
+#include "pipewire/impl-client.h"
+#include "pipewire/log.h"
+#include "pipewire/module.h"
+#include "pipewire/utils.h"
+
+/** \page page_module_portal PipeWire Module: Portal
+ *
+ * The `portal` module performs access control management for clients started
+ * inside an XDG portal.
+ *
+ * The module connects to the session DBus and subscribes to
+ * `NameOwnerChanged` signals for the `org.freedesktop.portal.Desktop` name.
+ * The PID of the DBus name owner is the portal.
+ *
+ * A client connection from the portal PID to PipeWire gets assigned a \ref
+ * PW_KEY_ACCESS of `"portal"` and set to permissions ALL - it is the
+ * responsibility of the portal to limit the permissions before passing the
+ * connection on to the client. See \ref page_access for details on
+ * permissions.
+ *
+ * Clients connecting from other PIDs are ignored by this module.
+ *
+ * ## Module Options
+ *
+ * There are no module-specific options.
+ *
+ * ## General options
+ *
+ * There are no general options for this module.
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-portal }
+ * ]
+ *\endcode
+ *
+ */
+
+#define NAME "portal"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct impl {
+ struct pw_context *context;
+ struct pw_properties *properties;
+
+ struct spa_dbus_connection *conn;
+ DBusConnection *bus;
+
+ struct spa_hook context_listener;
+ struct spa_hook module_listener;
+
+ DBusPendingCall *portal_pid_pending;
+ pid_t portal_pid;
+};
+
+static void
+context_check_access(void *data, struct pw_impl_client *client)
+{
+ struct impl *impl = data;
+ const struct pw_properties *props;
+ struct pw_permission permissions[1];
+ struct spa_dict_item items[1];
+ pid_t pid;
+
+ if (impl->portal_pid == 0)
+ return;
+
+ if ((props = pw_impl_client_get_properties(client)) == NULL)
+ return;
+
+ if (pw_properties_fetch_int32(props, PW_KEY_SEC_PID, &pid) < 0)
+ return;
+
+ if (pid != impl->portal_pid)
+ return;
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, "portal");
+ pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, 1));
+
+ pw_log_info("%p: portal managed client %p added", impl, client);
+
+ /* portal makes this connection and will change the permissions before
+ * handing this connection to the client */
+ permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_ALL);
+ pw_impl_client_update_permissions(client, 1, permissions);
+ return;
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .check_access = context_check_access,
+};
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ spa_hook_remove(&impl->context_listener);
+ spa_hook_remove(&impl->module_listener);
+
+ if (impl->bus)
+ dbus_connection_unref(impl->bus);
+ spa_dbus_connection_destroy(impl->conn);
+
+ pw_properties_free(impl->properties);
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void on_portal_pid_received(DBusPendingCall *pending,
+ void *user_data)
+{
+ struct impl *impl = user_data;
+ DBusMessage *m;
+ DBusError error;
+ uint32_t portal_pid = 0;
+
+ m = dbus_pending_call_steal_reply(pending);
+ dbus_pending_call_unref(pending);
+ impl->portal_pid_pending = NULL;
+
+ if (!m) {
+ pw_log_error("Failed to receive portal pid");
+ return;
+ }
+ if (dbus_message_is_error(m, DBUS_ERROR_NAME_HAS_NO_OWNER)) {
+ pw_log_info("Portal is not running");
+ return;
+ }
+ if (dbus_message_get_type(m) == DBUS_MESSAGE_TYPE_ERROR) {
+ const char *message = "unknown";
+ dbus_message_get_args(m, NULL, DBUS_TYPE_STRING, &message, DBUS_TYPE_INVALID);
+ pw_log_warn("Failed to receive portal pid: %s: %s",
+ dbus_message_get_error_name(m), message);
+ return;
+ }
+
+ dbus_error_init(&error);
+ dbus_message_get_args(m, &error, DBUS_TYPE_UINT32, &portal_pid,
+ DBUS_TYPE_INVALID);
+ dbus_message_unref(m);
+
+ if (dbus_error_is_set(&error)) {
+ impl->portal_pid = 0;
+ pw_log_warn("Could not get portal pid: %s", error.message);
+ dbus_error_free(&error);
+ } else {
+ pw_log_info("Got portal pid %d", portal_pid);
+ impl->portal_pid = portal_pid;
+ }
+}
+
+static void update_portal_pid(struct impl *impl)
+{
+ DBusMessage *m;
+ const char *name;
+ DBusPendingCall *pending;
+
+ impl->portal_pid = 0;
+
+ m = dbus_message_new_method_call("org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "GetConnectionUnixProcessID");
+
+ name = "org.freedesktop.portal.Desktop";
+ dbus_message_append_args(m,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID);
+
+ dbus_connection_send_with_reply(impl->bus, m, &pending, -1);
+ dbus_pending_call_set_notify(pending, on_portal_pid_received, impl, NULL);
+ if (impl->portal_pid_pending != NULL) {
+ dbus_pending_call_cancel(impl->portal_pid_pending);
+ dbus_pending_call_unref(impl->portal_pid_pending);
+ }
+ impl->portal_pid_pending = pending;
+}
+
+static DBusHandlerResult name_owner_changed_handler(DBusConnection *connection,
+ DBusMessage *message,
+ void *user_data)
+{
+ struct impl *impl = user_data;
+ const char *name;
+ const char *old_owner;
+ const char *new_owner;
+
+ if (!dbus_message_is_signal(message, "org.freedesktop.DBus",
+ "NameOwnerChanged"))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (!dbus_message_get_args(message, NULL,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old_owner,
+ DBUS_TYPE_STRING, &new_owner,
+ DBUS_TYPE_INVALID)) {
+ pw_log_error("Failed to get OwnerChanged args");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+
+ if (!spa_streq(name, "org.freedesktop.portal.Desktop"))
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+ if (spa_streq(new_owner, "")) {
+ impl->portal_pid = 0;
+ if (impl->portal_pid_pending != NULL) {
+ dbus_pending_call_cancel(impl->portal_pid_pending);
+ dbus_pending_call_unref(impl->portal_pid_pending);
+ }
+ }
+ else {
+ update_portal_pid(impl);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static int init_dbus_connection(struct impl *impl)
+{
+ DBusError error;
+
+ impl->bus = spa_dbus_connection_get(impl->conn);
+ if (impl->bus == NULL)
+ return -EIO;
+
+ /* XXX: we don't handle dbus reconnection yet, so ref the handle instead */
+ dbus_connection_ref(impl->bus);
+
+ dbus_error_init(&error);
+
+ dbus_bus_add_match(impl->bus,
+ "type='signal',\
+ sender='org.freedesktop.DBus',\
+ interface='org.freedesktop.DBus',\
+ member='NameOwnerChanged'",
+ &error);
+ if (dbus_error_is_set(&error)) {
+ pw_log_error("Failed to add name owner changed listener: %s",
+ error.message);
+ dbus_error_free(&error);
+ return -EIO;
+ }
+
+ dbus_connection_add_filter(impl->bus, name_owner_changed_handler,
+ impl, NULL);
+
+ update_portal_pid(impl);
+
+ return 0;
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ struct spa_dbus *dbus;
+ const struct spa_support *support;
+ uint32_t n_support;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ support = pw_context_get_support(context, &n_support);
+
+ dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ if (dbus == NULL)
+ return -ENOTSUP;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -ENOMEM;
+
+ pw_log_debug("module %p: new", impl);
+
+ impl->context = context;
+ impl->properties = args ? pw_properties_new_string(args) : NULL;
+
+ impl->conn = spa_dbus_get_connection(dbus, SPA_DBUS_TYPE_SESSION);
+ if (impl->conn == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ if ((res = init_dbus_connection(impl)) < 0)
+ goto error;
+
+ pw_context_add_listener(context, &impl->context_listener, &context_events, impl);
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ return 0;
+
+ error:
+ free(impl);
+ pw_log_error("Failed to connect to session bus: %s", spa_strerror(res));
+ return res;
+}
diff --git a/src/modules/module-profiler.c b/src/modules/module-profiler.c
new file mode 100644
index 0000000..e519e08
--- /dev/null
+++ b/src/modules/module-profiler.c
@@ -0,0 +1,463 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/param/profiler.h>
+
+#include <pipewire/private.h>
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+/** \page page_module_profiler PipeWire Module: Profiler
+ *
+ * The profiler module provides a Profiler interface for applications that
+ * can be used to receive profiling information.
+ *
+ * Use tools like pw-top and pw-profiler to collect profiling information
+ * about the pipewire graph.
+ *
+ * ## Example configuration
+ *
+ * The module has no arguments and is usually added to the config file of
+ * the main pipewire daemon.
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-profiler }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * - `pw-top`: a tool to display realtime profiler data
+ * - `pw-profiler`: a tool to collect and render profiler data
+ */
+
+#define NAME "profiler"
+
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define TMP_BUFFER (16 * 1024)
+#define MAX_BUFFER (8 * 1024 * 1024)
+#define MIN_FLUSH (16 * 1024)
+#define DEFAULT_IDLE 5
+#define DEFAULT_INTERVAL 1
+
+int pw_protocol_native_ext_profiler_init(struct pw_context *context);
+
+#define pw_profiler_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_profiler_events,m,v,__VA_ARGS__)
+
+#define pw_profiler_resource_profile(r,...) \
+ pw_profiler_resource(r,profile,0,__VA_ARGS__)
+
+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_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+ struct pw_properties *properties;
+
+ struct spa_hook context_listener;
+ struct spa_hook module_listener;
+
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ int64_t count;
+ uint32_t busy;
+ uint32_t empty;
+ struct spa_source *flush_timeout;
+ unsigned int flushing:1;
+ unsigned int listening:1;
+
+ struct spa_ringbuffer buffer;
+ uint8_t tmp[TMP_BUFFER];
+ uint8_t data[MAX_BUFFER];
+
+ uint8_t flush[MAX_BUFFER + sizeof(struct spa_pod_struct)];
+};
+
+struct resource_data {
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+};
+
+static void start_flush(struct impl *impl)
+{
+ struct timespec value, interval;
+
+ value.tv_sec = 0;
+ value.tv_nsec = 1;
+ interval.tv_sec = DEFAULT_INTERVAL;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->context->main_loop,
+ impl->flush_timeout, &value, &interval, false);
+ impl->flushing = true;
+}
+
+static void stop_flush(struct impl *impl)
+{
+ struct timespec value, interval;
+
+ if (!impl->flushing)
+ return;
+
+ value.tv_sec = 0;
+ value.tv_nsec = 0;
+ interval.tv_sec = 0;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->context->main_loop,
+ impl->flush_timeout, &value, &interval, false);
+ impl->flushing = false;
+}
+
+static void flush_timeout(void *data, uint64_t expirations)
+{
+ struct impl *impl = data;
+ int32_t avail;
+ uint32_t idx;
+ struct spa_pod_struct *p;
+ struct pw_resource *resource;
+
+ avail = spa_ringbuffer_get_read_index(&impl->buffer, &idx);
+
+ pw_log_trace("%p avail %d", impl, avail);
+
+ if (avail <= 0) {
+ if (++impl->empty == DEFAULT_IDLE)
+ stop_flush(impl);
+ return;
+ }
+ impl->empty = 0;
+
+ p = (struct spa_pod_struct *)impl->flush;
+ *p = SPA_POD_INIT_Struct(avail);
+
+ spa_ringbuffer_read_data(&impl->buffer, impl->data, MAX_BUFFER,
+ idx % MAX_BUFFER,
+ SPA_PTROFF(p, sizeof(struct spa_pod_struct), void), avail);
+ spa_ringbuffer_read_update(&impl->buffer, idx + avail);
+
+ spa_list_for_each(resource, &impl->global->resource_list, link)
+ pw_profiler_resource_profile(resource, &p->pod);
+}
+
+static void context_do_profile(void *data, struct pw_impl_node *node)
+{
+ struct impl *impl = data;
+ struct spa_pod_builder b;
+ struct spa_pod_frame f[2];
+ struct pw_node_activation *a = node->rt.activation;
+ struct spa_io_position *pos = &a->position;
+ struct pw_node_target *t;
+ int32_t filled;
+ uint32_t idx, avail;
+
+ if (SPA_FLAG_IS_SET(pos->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL))
+ return;
+
+ spa_pod_builder_init(&b, impl->tmp, sizeof(impl->tmp));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_Profiler, 0);
+
+ spa_pod_builder_prop(&b, SPA_PROFILER_info, 0);
+ spa_pod_builder_add_struct(&b,
+ SPA_POD_Long(impl->count),
+ SPA_POD_Float(a->cpu_load[0]),
+ SPA_POD_Float(a->cpu_load[1]),
+ SPA_POD_Float(a->cpu_load[2]),
+ SPA_POD_Int(a->xrun_count));
+
+ spa_pod_builder_prop(&b, SPA_PROFILER_clock, 0);
+ spa_pod_builder_add_struct(&b,
+ SPA_POD_Int(pos->clock.flags),
+ 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_builder_prop(&b, SPA_PROFILER_driverBlock, 0);
+ spa_pod_builder_add_struct(&b,
+ SPA_POD_Int(node->info.id),
+ SPA_POD_String(node->name),
+ SPA_POD_Long(a->prev_signal_time),
+ SPA_POD_Long(a->signal_time),
+ SPA_POD_Long(a->awake_time),
+ SPA_POD_Long(a->finish_time),
+ SPA_POD_Int(a->status),
+ SPA_POD_Fraction(&node->latency));
+
+ spa_list_for_each(t, &node->rt.target_list, link) {
+ struct pw_impl_node *n = t->node;
+ struct pw_node_activation *na;
+ struct spa_fraction latency;
+
+ if (n == NULL || n == node)
+ continue;
+
+ latency = n->latency;
+ if (n->force_quantum != 0)
+ latency.num = n->force_quantum;
+ if (n->force_rate != 0)
+ latency.denom = n->force_rate;
+ else if (n->rate.denom != 0)
+ latency.denom = n->rate.denom;
+
+ na = n->rt.activation;
+ spa_pod_builder_prop(&b, SPA_PROFILER_followerBlock, 0);
+ spa_pod_builder_add_struct(&b,
+ SPA_POD_Int(n->info.id),
+ SPA_POD_String(n->name),
+ SPA_POD_Long(a->signal_time),
+ SPA_POD_Long(na->signal_time),
+ SPA_POD_Long(na->awake_time),
+ SPA_POD_Long(na->finish_time),
+ SPA_POD_Int(na->status),
+ SPA_POD_Fraction(&latency));
+ }
+ spa_pod_builder_pop(&b, &f[0]);
+
+ if (b.state.offset > sizeof(impl->tmp))
+ goto done;
+
+ filled = spa_ringbuffer_get_write_index(&impl->buffer, &idx);
+ if (filled < 0 || filled > MAX_BUFFER) {
+ pw_log_warn("%p: queue xrun %d", impl, filled);
+ goto done;
+ }
+ avail = MAX_BUFFER - filled;
+ if (avail < b.state.offset) {
+ pw_log_warn("%p: queue full %d < %d", impl, avail, b.state.offset);
+ goto done;
+ }
+ spa_ringbuffer_write_data(&impl->buffer,
+ impl->data, MAX_BUFFER,
+ idx % MAX_BUFFER,
+ b.data, b.state.offset);
+ spa_ringbuffer_write_update(&impl->buffer, idx + b.state.offset);
+
+ if (!impl->flushing || filled + b.state.offset > MIN_FLUSH)
+ start_flush(impl);
+done:
+ impl->count++;
+}
+
+static const struct pw_context_driver_events context_events = {
+ PW_VERSION_CONTEXT_DRIVER_EVENTS,
+ .incomplete = context_do_profile,
+ .complete = context_do_profile,
+};
+
+static int do_stop(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct impl *impl = user_data;
+ spa_hook_remove(&impl->context_listener);
+ return 0;
+}
+
+static void stop_listener(struct impl *impl)
+{
+ if (impl->listening) {
+ pw_loop_invoke(impl->context->data_loop,
+ do_stop, SPA_ID_INVALID, NULL, 0, true, impl);
+ impl->listening = false;
+ }
+}
+
+static void resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ if (--impl->busy == 0) {
+ pw_log_info("%p: stopping profiler", impl);
+ stop_listener(impl);
+ }
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy,
+};
+
+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;
+ spa_hook_list_append(&impl->context->driver_listener_list,
+ &impl->context_listener,
+ &context_events, impl);
+ return 0;
+}
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_global *global = impl->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_Profiler, version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+ pw_global_add_resource(global, resource);
+
+ pw_resource_add_listener(resource, &data->resource_listener,
+ &resource_events, impl);
+
+ if (++impl->busy == 1) {
+ pw_log_info("%p: starting profiler", impl);
+ pw_loop_invoke(impl->context->data_loop,
+ do_start, SPA_ID_INVALID, NULL, 0, false, impl);
+ impl->listening = true;
+ }
+ return 0;
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ if (impl->global != NULL)
+ pw_global_destroy(impl->global);
+
+ spa_hook_remove(&impl->module_listener);
+
+ pw_properties_free(impl->properties);
+
+ pw_loop_destroy_source(pw_context_get_main_loop(impl->context), impl->flush_timeout);
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ stop_listener(impl);
+ stop_flush(impl);
+
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ struct pw_loop *main_loop = pw_context_get_main_loop(context);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_protocol_native_ext_profiler_init(context);
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+
+ impl->context = context;
+ impl->properties = props;
+
+ spa_ringbuffer_init(&impl->buffer);
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Profiler,
+ PW_VERSION_PROFILER,
+ pw_properties_copy(props),
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return -errno;
+ }
+ pw_properties_setf(impl->properties, PW_KEY_OBJECT_ID, "%d", impl->global->id);
+ pw_properties_setf(impl->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(impl->global));
+
+ impl->flush_timeout = pw_loop_add_timer(main_loop, flush_timeout, impl);
+
+ pw_global_update_keys(impl->global, &impl->properties->dict, keys);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ pw_global_register(impl->global);
+
+ pw_global_add_listener(impl->global, &impl->global_listener, &global_events, impl);
+
+ return 0;
+}
diff --git a/src/modules/module-profiler/protocol-native.c b/src/modules/module-profiler/protocol-native.c
new file mode 100644
index 0000000..ac0644c
--- /dev/null
+++ b/src/modules/module-profiler/protocol-native.c
@@ -0,0 +1,128 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/pipewire.h>
+
+#include <pipewire/extensions/protocol-native.h>
+#include <pipewire/extensions/profiler.h>
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static int profiler_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_profiler_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int profiler_demarshal_add_listener(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ return -ENOTSUP;
+}
+
+static void profiler_resource_marshal_profile(void *object, const struct spa_pod *pod)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PROFILER_EVENT_PROFILE, NULL);
+
+ spa_pod_builder_add_struct(b, SPA_POD_Pod(pod));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int profiler_proxy_demarshal_profile(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ struct spa_pod *pod;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ if (spa_pod_parser_get_struct(&prs, SPA_POD_Pod(&pod)) < 0)
+ return -EINVAL;
+
+ pw_proxy_notify(proxy, struct pw_profiler_events, profile, 0, pod);
+ return 0;
+}
+
+
+static const struct pw_profiler_methods pw_protocol_native_profiler_client_method_marshal = {
+ PW_VERSION_PROFILER_METHODS,
+ .add_listener = &profiler_proxy_marshal_add_listener,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_profiler_server_method_demarshal[PW_PROFILER_METHOD_NUM] =
+{
+ [PW_PROFILER_METHOD_ADD_LISTENER] = { &profiler_demarshal_add_listener, 0 },
+};
+
+static const struct pw_profiler_events pw_protocol_native_profiler_server_event_marshal = {
+ PW_VERSION_PROFILER_EVENTS,
+ .profile = &profiler_resource_marshal_profile,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_profiler_client_event_demarshal[PW_PROFILER_EVENT_NUM] =
+{
+ [PW_PROFILER_EVENT_PROFILE] = { &profiler_proxy_demarshal_profile, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_profiler_marshal = {
+ PW_TYPE_INTERFACE_Profiler,
+ PW_VERSION_PROFILER,
+ 0,
+ PW_PROFILER_METHOD_NUM,
+ PW_PROFILER_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_profiler_client_method_marshal,
+ .server_demarshal = pw_protocol_native_profiler_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_profiler_server_event_marshal,
+ .client_demarshal = pw_protocol_native_profiler_client_event_demarshal,
+};
+
+int pw_protocol_native_ext_profiler_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+ if (protocol == NULL)
+ return -EPROTO;
+
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_profiler_marshal);
+ return 0;
+}
diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c
new file mode 100644
index 0000000..6fdc79b
--- /dev/null
+++ b/src/modules/module-protocol-native.c
@@ -0,0 +1,1528 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <sys/file.h>
+#include <ctype.h>
+#ifdef HAVE_PWD_H
+#include <pwd.h>
+#endif
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#include <sys/ucred.h>
+#endif
+
+#include <spa/pod/iter.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-daemon.h>
+#endif
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/protocol-native.h>
+
+#include "pipewire/private.h"
+
+#include "modules/module-protocol-native/connection.h"
+#include "modules/module-protocol-native/defs.h"
+#include "modules/module-protocol-native/protocol-footer.h"
+
+
+#define NAME "protocol-native"
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
+
+#undef spa_debug
+#define spa_debug(...) pw_logt_debug(mod_topic_connection, __VA_ARGS__)
+
+#include <spa/debug/pod.h>
+#include <spa/debug/types.h>
+
+/** \page page_module_protocol_native PipeWire Module: Protocol Native
+ *
+ * The native protocol module implements the PipeWire communication between
+ * 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.
+ *
+ * ## Module Options
+ *
+ * The module has no options.
+ *
+ * ## General Options
+ *
+ * The name of the core is obtained as:
+ *
+ * - PIPEWIRE_CORE : the environment variable with the name of the core
+ * - \ref PW_KEY_CORE_NAME : in the context properties
+ * - a name based on the process id
+ *
+ * The context will also become a server if:
+ *
+ * - PIPEWIRE_DAEMON : the environment is true
+ * - \ref PW_KEY_CORE_DAEMON : in the context properties is true
+ *
+ * The socket will be located in the directory obtained by looking at the
+ * following environment variables:
+ *
+ * - PIPEWIRE_RUNTIME_DIR
+ * - XDG_RUNTIME_DIR
+ * - USERPROFILE
+ *
+ * The socket address will be written into the notification file descriptor
+ * if the following environment variable is set:
+ *
+ * - PIPEWIRE_NOTIFICATION_FD
+ *
+ * When a client connect, the connection will be made to:
+ *
+ * - PIPEWIRE_REMOTE : the environment with the remote name
+ * - \ref PW_KEY_REMOTE_NAME : the property in the context.
+ * - The default remote named "pipewire-0"
+ *
+ * A Special remote named "internal" can be used to make a connection to the
+ * 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.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ { name = libpipewire-module-protocol-native }
+ * ]
+ *\endcode
+ */
+
+#ifndef UNIX_PATH_MAX
+#define UNIX_PATH_MAX 108
+#endif
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Native protocol using unix sockets" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+/* Required for s390x */
+#ifndef SO_PEERSEC
+#define SO_PEERSEC 31
+#endif
+
+static bool debug_messages = 0;
+
+#define LOCK_SUFFIX ".lock"
+#define LOCK_SUFFIXLEN 5
+
+void pw_protocol_native_init(struct pw_protocol *protocol);
+void pw_protocol_native0_init(struct pw_protocol *protocol);
+
+struct protocol_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_protocol *protocol;
+
+ struct server *local;
+};
+
+struct client {
+ struct pw_protocol_client this;
+ struct pw_context *context;
+
+ struct spa_source *source;
+
+ struct pw_protocol_native_connection *connection;
+ struct spa_hook conn_listener;
+
+ int ref;
+
+ struct footer_core_global_state footer_state;
+
+ unsigned int connected:1;
+ unsigned int disconnecting:1;
+ unsigned int need_flush:1;
+ unsigned int paused:1;
+};
+
+static void client_unref(struct client *impl)
+{
+ if (--impl->ref == 0)
+ free(impl);
+}
+
+struct server {
+ struct pw_protocol_server this;
+
+ int fd_lock;
+ struct sockaddr_un addr;
+ char lock_addr[UNIX_PATH_MAX + LOCK_SUFFIXLEN];
+
+ struct pw_loop *loop;
+ struct spa_source *source;
+ struct spa_source *resume;
+ unsigned int activated:1;
+};
+
+struct client_data {
+ struct pw_impl_client *client;
+ struct spa_hook client_listener;
+
+ struct spa_list protocol_link;
+ struct server *server;
+
+ struct spa_source *source;
+ struct pw_protocol_native_connection *connection;
+ struct spa_hook conn_listener;
+
+ struct footer_client_global_state footer_state;
+
+ unsigned int busy:1;
+ unsigned int need_flush:1;
+
+ struct protocol_compat_v2 compat_v2;
+};
+
+static void debug_msg(const char *prefix, const struct pw_protocol_native_message *msg, bool hex)
+{
+ struct spa_pod *pod;
+ pw_logt_debug(mod_topic_connection,
+ "%s: id:%d op:%d size:%d seq:%d", prefix,
+ msg->id, msg->opcode, msg->size, msg->seq);
+
+ if ((pod = get_first_pod_from_data(msg->data, msg->size, 0)) != NULL)
+ spa_debug_pod(0, NULL, pod);
+ else
+ hex = true;
+ if (hex)
+ spa_debug_mem(0, msg->data, msg->size);
+
+ pw_logt_debug(mod_topic_connection, "%s ****", prefix);
+
+}
+
+static void pre_demarshal(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message *msg,
+ void *object, const struct footer_demarshal *opcodes, size_t n_opcodes)
+{
+ struct spa_pod *footer = NULL;
+ struct spa_pod_parser parser;
+ struct spa_pod_frame f[2];
+ uint32_t opcode;
+ int ret;
+
+ footer = pw_protocol_native_connection_get_footer(conn, msg);
+ if (footer == NULL)
+ return; /* No valid footer. Ignore silently. */
+
+ /*
+ * Version 3 footer
+ *
+ * spa_pod Struct { [Id opcode, Struct { ... }]* }
+ */
+
+ spa_pod_parser_pod(&parser, footer);
+ if (spa_pod_parser_push_struct(&parser, &f[0]) < 0) {
+ pw_log_error("malformed message footer");
+ return;
+ }
+
+ while (1) {
+ if (spa_pod_parser_get_id(&parser, &opcode) < 0)
+ break;
+ if (spa_pod_parser_push_struct(&parser, &f[1]) < 0)
+ break;
+ if (opcode < n_opcodes) {
+ if ((ret = opcodes[opcode].demarshal(object, &parser)) < 0)
+ pw_log_error("failed processing message footer (opcode %u): %d (%s)",
+ opcode, ret, spa_strerror(ret));
+ } else {
+ /* Ignore (don't log errors), in case we need to extend this later. */
+ pw_log_debug("unknown message footer opcode %u", opcode);
+ }
+ spa_pod_parser_pop(&parser, &f[1]);
+ }
+}
+
+static int
+process_messages(struct client_data *data)
+{
+ struct pw_protocol_native_connection *conn = data->connection;
+ struct pw_impl_client *client = data->client;
+ struct pw_context *context = client->context;
+ const struct pw_protocol_native_message *msg;
+ struct pw_resource *resource;
+ int res;
+
+ context->current_client = client;
+
+ /* when the client is busy processing an async action, stop processing messages
+ * for the client until it finishes the action */
+ while (!data->busy) {
+ const struct pw_protocol_native_demarshal *demarshal;
+ const struct pw_protocol_marshal *marshal;
+ uint32_t permissions, required;
+
+ res = pw_protocol_native_connection_get_next(conn, &msg);
+ if (res < 0) {
+ if (res == -EAGAIN)
+ break;
+ goto error;
+ }
+ if (res == 0)
+ break;
+
+ if (client->core_resource == NULL) {
+ res = -EPROTO;
+ goto error;
+ }
+
+ client->recv_seq = msg->seq;
+
+ pw_log_trace("%p: got message %d from %u", client->protocol,
+ msg->opcode, msg->id);
+
+ if (debug_messages)
+ debug_msg("<<<<<< in", msg, false);
+
+ pre_demarshal(conn, msg, client, footer_client_demarshal,
+ SPA_N_ELEMENTS(footer_client_demarshal));
+
+ resource = pw_impl_client_find_resource(client, msg->id);
+ if (resource == NULL) {
+ pw_resource_errorf(client->core_resource,
+ -ENOENT, "unknown resource %u op:%u", msg->id, msg->opcode);
+ continue;
+ }
+
+ marshal = pw_resource_get_marshal(resource);
+ if (marshal == NULL || msg->opcode >= marshal->n_client_methods) {
+ pw_resource_errorf_id(resource, msg->id,
+ -ENOSYS, "invalid method id:%u op:%u",
+ msg->id, msg->opcode);
+ continue;
+ }
+
+ demarshal = marshal->server_demarshal;
+ if (!demarshal[msg->opcode].func) {
+ pw_resource_errorf_id(resource, msg->id,
+ -ENOTSUP, "function not supported id:%u op:%u",
+ msg->id, msg->opcode);
+ continue;
+ }
+
+ permissions = pw_resource_get_permissions(resource);
+ required = demarshal[msg->opcode].permissions | PW_PERM_X;
+
+ if ((required & permissions) != required) {
+ pw_resource_errorf_id(resource, msg->id,
+ -EACCES, "no permission to call method %u on %u "
+ "(requires "PW_PERMISSION_FORMAT", have "PW_PERMISSION_FORMAT")",
+ msg->opcode, msg->id,
+ PW_PERMISSION_ARGS(required), PW_PERMISSION_ARGS(permissions));
+ continue;
+ }
+
+ resource->refcount++;
+ pw_protocol_native_connection_enter(conn);
+ res = demarshal[msg->opcode].func(resource, msg);
+ pw_protocol_native_connection_leave(conn);
+ pw_resource_unref(resource);
+
+ if (res < 0) {
+ pw_resource_errorf_id(resource, msg->id,
+ res, "invalid message id:%u op:%u (%s)",
+ msg->id, msg->opcode, spa_strerror(res));
+ debug_msg("*invalid message*", msg, true);
+ }
+ }
+ res = 0;
+done:
+ context->current_client = NULL;
+
+ return res;
+
+error:
+ pw_resource_errorf(client->core_resource, res, "client error %d (%s)",
+ res, spa_strerror(res));
+ goto done;
+}
+
+static void
+client_busy_changed(void *data, bool busy)
+{
+ struct client_data *c = data;
+ struct server *s = c->server;
+ struct pw_impl_client *client = c->client;
+ uint32_t mask = c->source->mask;
+
+ c->busy = busy;
+
+ SPA_FLAG_UPDATE(mask, SPA_IO_IN, !busy);
+
+ pw_log_debug("%p: busy changed %d", client->protocol, busy);
+ pw_loop_update_io(client->context->main_loop, c->source, mask);
+
+ if (!busy)
+ pw_loop_signal_event(s->loop, s->resume);
+}
+
+static void handle_client_error(struct pw_impl_client *client, int res, const char *msg)
+{
+ if (res == -EPIPE || res == -ECONNRESET)
+ pw_log_info("%p: %s: client %p disconnected", client->protocol, msg, client);
+ else
+ pw_log_error("%p: %s: client %p error %d (%s)", client->protocol, msg,
+ client, res, spa_strerror(res));
+ if (!client->destroyed)
+ pw_impl_client_destroy(client);
+}
+
+static void
+connection_data(void *data, int fd, uint32_t mask)
+{
+ struct client_data *this = data;
+ struct pw_impl_client *client = this->client;
+ int res;
+
+ client->refcount++;
+
+ if (mask & SPA_IO_HUP) {
+ res = -EPIPE;
+ goto error;
+ }
+ if (mask & SPA_IO_ERR) {
+ res = -EIO;
+ goto error;
+ }
+ if (mask & SPA_IO_IN) {
+ if ((res = process_messages(this)) < 0)
+ goto error;
+ }
+ if (mask & SPA_IO_OUT || this->need_flush) {
+ this->need_flush = false;
+ res = pw_protocol_native_connection_flush(this->connection);
+ if (res >= 0) {
+ pw_loop_update_io(client->context->main_loop,
+ this->source, this->source->mask & ~SPA_IO_OUT);
+ } else if (res != -EAGAIN)
+ goto error;
+ }
+done:
+ pw_impl_client_unref(client);
+ return;
+error:
+ handle_client_error(client, res, "connection_data");
+ goto done;
+}
+
+static void client_destroy(void *data)
+{
+ struct client_data *this = data;
+ pw_log_debug("%p: destroy", this);
+ spa_list_remove(&this->protocol_link);
+}
+
+static void client_free(void *data)
+{
+ struct client_data *this = data;
+ struct pw_impl_client *client = this->client;
+
+ pw_log_debug("%p: free", this);
+ spa_hook_remove(&this->client_listener);
+
+ if (this->source)
+ pw_loop_destroy_source(client->context->main_loop, this->source);
+ if (this->connection)
+ pw_protocol_native_connection_destroy(this->connection);
+
+ pw_map_clear(&this->compat_v2.types);
+}
+
+static const struct pw_impl_client_events client_events = {
+ PW_VERSION_IMPL_CLIENT_EVENTS,
+ .destroy = client_destroy,
+ .free = client_free,
+ .busy_changed = client_busy_changed,
+};
+
+static void on_server_connection_destroy(void *data)
+{
+ struct client_data *this = data;
+ spa_hook_remove(&this->conn_listener);
+}
+
+static void on_start(void *data, uint32_t version)
+{
+ struct client_data *this = data;
+ struct pw_impl_client *client = this->client;
+
+ pw_log_debug("version %d", version);
+
+ if (client->core_resource != NULL)
+ pw_resource_remove(client->core_resource);
+
+ if (pw_global_bind(pw_impl_core_get_global(client->core), client,
+ PW_PERM_ALL, version, 0) < 0)
+ return;
+
+ if (version == 0)
+ client->compat_v2 = &this->compat_v2;
+
+ return;
+}
+
+static void on_server_need_flush(void *data)
+{
+ struct client_data *this = data;
+ struct pw_impl_client *client = this->client;
+
+ pw_log_trace("need flush");
+ this->need_flush = true;
+
+ if (this->source && !(this->source->mask & SPA_IO_OUT)) {
+ pw_loop_update_io(client->context->main_loop,
+ this->source, this->source->mask | SPA_IO_OUT);
+ }
+}
+
+static const struct pw_protocol_native_connection_events server_conn_events = {
+ PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS,
+ .destroy = on_server_connection_destroy,
+ .start = on_start,
+ .need_flush = on_server_need_flush,
+};
+
+static bool check_print(const uint8_t *buffer, int len)
+{
+ int i;
+ while (len > 1 && buffer[len-1] == 0)
+ len--;
+ for (i = 0; i < len; i++)
+ if (!isprint(buffer[i]))
+ return false;
+ return true;
+}
+
+static struct client_data *client_new(struct server *s, int fd)
+{
+ struct client_data *this;
+ struct pw_impl_client *client;
+ struct pw_protocol *protocol = s->this.protocol;
+ socklen_t len;
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+ struct xucred xucred;
+#else
+ struct ucred ucred;
+#endif
+ struct pw_context *context = protocol->context;
+ struct pw_properties *props;
+ uint8_t buffer[1024];
+ struct protocol_data *d = pw_protocol_get_user_data(protocol);
+ int i, res;
+
+ props = pw_properties_new(PW_KEY_PROTOCOL, "protocol-native", NULL);
+ if (props == NULL)
+ goto exit;
+
+#if defined(__linux__)
+ len = sizeof(ucred);
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) < 0) {
+ pw_log_warn("server %p: no peercred: %m", s);
+ } else {
+ pw_properties_setf(props, PW_KEY_SEC_PID, "%d", ucred.pid);
+ pw_properties_setf(props, PW_KEY_SEC_UID, "%d", ucred.uid);
+ pw_properties_setf(props, PW_KEY_SEC_GID, "%d", ucred.gid);
+ }
+
+ len = sizeof(buffer);
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERSEC, buffer, &len) < 0) {
+ if (errno == ENOPROTOOPT)
+ pw_log_info("server %p: security label not available", s);
+ else
+ pw_log_warn("server %p: security label error: %m", s);
+ } else {
+ if (!check_print(buffer, len)) {
+ char *hex, *p;
+ static const char *ch = "0123456789abcdef";
+
+ p = hex = alloca(len * 2 + 10);
+ p += snprintf(p, 5, "hex:");
+ for(i = 0; i < (int)len; i++)
+ p += snprintf(p, 3, "%c%c",
+ ch[buffer[i] >> 4], ch[buffer[i] & 0xf]);
+ pw_properties_set(props, PW_KEY_SEC_LABEL, hex);
+
+ } else {
+ /* buffer is not null terminated, must use length explicitly */
+ pw_properties_setf(props, PW_KEY_SEC_LABEL, "%.*s",
+ (int)len, buffer);
+ }
+ }
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
+ len = sizeof(xucred);
+ if (getsockopt(fd, 0, LOCAL_PEERCRED, &xucred, &len) < 0) {
+ pw_log_warn("server %p: no peercred: %m", s);
+ } else {
+#if __FreeBSD__ >= 13
+ pw_properties_setf(props, PW_KEY_SEC_PID, "%d", xucred.cr_pid);
+#endif
+ pw_properties_setf(props, PW_KEY_SEC_UID, "%d", xucred.cr_uid);
+ pw_properties_setf(props, PW_KEY_SEC_GID, "%d", xucred.cr_gid);
+ // this is what Linuxulator does at the moment, see sys/compat/linux/linux_socket.c
+ pw_properties_set(props, PW_KEY_SEC_LABEL, "unconfined");
+ }
+#endif
+
+ pw_properties_setf(props, PW_KEY_MODULE_ID, "%d", d->module->global->id);
+
+ client = pw_context_create_client(s->this.core,
+ protocol, props, sizeof(struct client_data));
+ if (client == NULL)
+ goto exit;
+
+ this = pw_impl_client_get_user_data(client);
+ spa_list_append(&s->this.client_list, &this->protocol_link);
+
+ this->server = s;
+ this->client = client;
+ pw_map_init(&this->compat_v2.types, 0, 32);
+
+ pw_impl_client_add_listener(client, &this->client_listener, &client_events, this);
+
+ this->source = pw_loop_add_io(pw_context_get_main_loop(context),
+ fd, SPA_IO_ERR | SPA_IO_HUP, true,
+ connection_data, this);
+ if (this->source == NULL) {
+ res = -errno;
+ goto cleanup_client;
+ }
+
+ this->connection = pw_protocol_native_connection_new(protocol->context, fd);
+ if (this->connection == NULL) {
+ res = -errno;
+ goto cleanup_client;
+ }
+
+ pw_protocol_native_connection_add_listener(this->connection,
+ &this->conn_listener,
+ &server_conn_events,
+ this);
+
+ if ((res = pw_impl_client_register(client, NULL)) < 0)
+ goto cleanup_client;
+
+ if (!client->busy)
+ pw_loop_update_io(pw_context_get_main_loop(context),
+ this->source, this->source->mask | SPA_IO_IN);
+
+ return this;
+
+cleanup_client:
+ pw_impl_client_destroy(client);
+ errno = -res;
+exit:
+ return NULL;
+}
+
+static const char *
+get_runtime_dir(void)
+{
+ const char *runtime_dir;
+
+ runtime_dir = getenv("PIPEWIRE_RUNTIME_DIR");
+ if (runtime_dir == NULL)
+ runtime_dir = getenv("XDG_RUNTIME_DIR");
+ if (runtime_dir == NULL)
+ runtime_dir = getenv("USERPROFILE");
+ return runtime_dir;
+}
+
+
+static int init_socket_name(struct server *s, const char *name)
+{
+ int name_size;
+ const char *runtime_dir;
+ bool path_is_absolute;
+
+ path_is_absolute = name[0] == '/';
+
+ runtime_dir = get_runtime_dir();
+
+ pw_log_debug("name:%s runtime_dir:%s", name, runtime_dir);
+
+ if (runtime_dir == NULL && !path_is_absolute) {
+ pw_log_error("server %p: name %s is not an absolute path and no runtime dir found. "
+ "Set one of PIPEWIRE_RUNTIME_DIR, XDG_RUNTIME_DIR or "
+ "USERPROFILE in the environment", s, name);
+ return -ENOENT;
+ }
+
+ s->addr.sun_family = AF_LOCAL;
+ if (path_is_absolute)
+ name_size = snprintf(s->addr.sun_path, sizeof(s->addr.sun_path),
+ "%s", name) + 1;
+ else
+ name_size = snprintf(s->addr.sun_path, sizeof(s->addr.sun_path),
+ "%s/%s", runtime_dir, name) + 1;
+
+ if (name_size > (int) sizeof(s->addr.sun_path)) {
+ if (path_is_absolute)
+ pw_log_error("server %p: socket path \"%s\" plus null terminator exceeds %i bytes",
+ s, name, (int) sizeof(s->addr.sun_path));
+ else
+ pw_log_error("server %p: socket path \"%s/%s\" plus null terminator exceeds %i bytes",
+ s, runtime_dir, name, (int) sizeof(s->addr.sun_path));
+ *s->addr.sun_path = 0;
+ return -ENAMETOOLONG;
+ }
+ return 0;
+}
+
+static int lock_socket(struct server *s)
+{
+ int res;
+
+ 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,
+ (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));
+
+ if (s->fd_lock < 0) {
+ res = -errno;
+ pw_log_error("server %p: unable to open lockfile '%s': %m", s, s->lock_addr);
+ goto err;
+ }
+
+ if (flock(s->fd_lock, LOCK_EX | LOCK_NB) < 0) {
+ res = -errno;
+ pw_log_error("server %p: unable to lock lockfile '%s': %m"
+ " (maybe another daemon is running)",
+ s, s->lock_addr);
+ goto err_fd;
+ }
+ return 0;
+
+err_fd:
+ close(s->fd_lock);
+ s->fd_lock = -1;
+err:
+ *s->lock_addr = 0;
+ *s->addr.sun_path = 0;
+ return res;
+}
+
+static void
+socket_data(void *data, int fd, uint32_t mask)
+{
+ struct server *s = data;
+ struct client_data *client;
+ struct sockaddr_un name;
+ socklen_t length;
+ int client_fd;
+
+ length = sizeof(name);
+ client_fd = accept4(fd, (struct sockaddr *) &name, &length, SOCK_CLOEXEC);
+ if (client_fd < 0) {
+ pw_log_error("server %p: failed to accept: %m", s);
+ return;
+ }
+
+ client = client_new(s, client_fd);
+ if (client == NULL) {
+ pw_log_error("server %p: failed to create client", s);
+ close(client_fd);
+ return;
+ }
+}
+
+static int write_socket_address(struct server *s)
+{
+ long v;
+ int fd, res = 0;
+ char *endptr;
+ const char *env = getenv("PIPEWIRE_NOTIFICATION_FD");
+
+ if (env == NULL || env[0] == '\0')
+ return 0;
+
+ errno = 0;
+ v = strtol(env, &endptr, 10);
+ if (endptr[0] != '\0')
+ errno = EINVAL;
+ if (errno != 0) {
+ res = -errno;
+ pw_log_error("server %p: strtol() failed with error: %m", s);
+ goto error;
+ }
+ fd = (int)v;
+ if (v != fd) {
+ res = -ERANGE;
+ pw_log_error("server %p: invalid fd %ld: %s", s, v, spa_strerror(res));
+ goto error;
+ }
+ if (dprintf(fd, "%s\n", s->addr.sun_path) < 0) {
+ res = -errno;
+ pw_log_error("server %p: dprintf() failed with error: %m", s);
+ goto error;
+ }
+ close(fd);
+ unsetenv("PIPEWIRE_NOTIFICATION_FD");
+ return 0;
+
+error:
+ return res;
+}
+
+static int add_socket(struct pw_protocol *protocol, struct server *s)
+{
+ socklen_t size;
+ int fd = -1, res;
+ bool activated = false;
+
+#ifdef HAVE_SYSTEMD
+ {
+ int i, n = sd_listen_fds(0);
+ for (i = 0; i < n; ++i) {
+ if (sd_is_socket_unix(SD_LISTEN_FDS_START + i, SOCK_STREAM,
+ 1, s->addr.sun_path, 0) > 0) {
+ fd = SD_LISTEN_FDS_START + i;
+ activated = true;
+ pw_log_info("server %p: Found socket activation socket for '%s'",
+ s, s->addr.sun_path);
+ break;
+ }
+ }
+ }
+#endif
+
+ if (fd < 0) {
+ struct stat socket_stat;
+
+ if ((fd = socket(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ 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;
+ }
+ } else if (socket_stat.st_mode & S_IWUSR || socket_stat.st_mode & S_IWGRP) {
+ unlink(s->addr.sun_path);
+ }
+
+ size = offsetof(struct sockaddr_un, sun_path) + strlen(s->addr.sun_path);
+ if (bind(fd, (struct sockaddr *) &s->addr, size) < 0) {
+ res = -errno;
+ pw_log_error("server %p: bind() failed with error: %m", s);
+ goto error_close;
+ }
+
+ if (listen(fd, 128) < 0) {
+ res = -errno;
+ pw_log_error("server %p: listen() failed with error: %m", s);
+ goto error_close;
+ }
+ }
+
+ 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) {
+ res = -errno;
+ goto error_close;
+ }
+ s->source = pw_loop_add_io(s->loop, fd, SPA_IO_IN, true, socket_data, s);
+ if (s->source == NULL) {
+ res = -errno;
+ goto error_close;
+ }
+ return 0;
+
+error_close:
+ close(fd);
+error:
+ return res;
+
+}
+
+static int impl_steal_fd(struct pw_protocol_client *client)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+ int fd;
+
+ if (impl->source == NULL)
+ return -EIO;
+
+ fd = fcntl(impl->source->fd, F_DUPFD_CLOEXEC, 3);
+ if (fd < 0)
+ return -errno;
+
+ pw_protocol_client_disconnect(client);
+ return fd;
+}
+
+static int
+process_remote(struct client *impl)
+{
+ const struct pw_protocol_native_message *msg;
+ struct pw_protocol_native_connection *conn = impl->connection;
+ struct pw_core *this = impl->this.core;
+ int res = 0;
+
+ impl->ref++;
+ while (!impl->disconnecting && !impl->paused) {
+ struct pw_proxy *proxy;
+ const struct pw_protocol_native_demarshal *demarshal;
+ const struct pw_protocol_marshal *marshal;
+
+ res = pw_protocol_native_connection_get_next(conn, &msg);
+ if (res < 0) {
+ if (res == -EAGAIN)
+ res = 0;
+ break;
+ }
+ if (res == 0)
+ break;
+
+ pw_log_trace("%p: got message %d from %u seq:%d",
+ this, msg->opcode, msg->id, msg->seq);
+
+ this->recv_seq = msg->seq;
+
+ if (debug_messages)
+ debug_msg("<<<<<< in", msg, false);
+
+ pre_demarshal(conn, msg, this, footer_core_demarshal,
+ SPA_N_ELEMENTS(footer_core_demarshal));
+
+ proxy = pw_core_find_proxy(this, msg->id);
+ if (proxy == NULL || proxy->zombie) {
+ if (proxy == NULL)
+ pw_log_error("%p: could not find proxy %u", this, msg->id);
+ else
+ pw_log_debug("%p: zombie proxy %u", this, msg->id);
+
+ /* FIXME close fds */
+ continue;
+ }
+
+ marshal = pw_proxy_get_marshal(proxy);
+ if (marshal == NULL || msg->opcode >= marshal->n_server_methods) {
+ pw_log_error("%p: invalid method %u for %u (%d)",
+ this, msg->opcode, msg->id,
+ marshal ? marshal->n_server_methods : (uint32_t)-1);
+ continue;
+ }
+
+ demarshal = marshal->client_demarshal;
+ if (!demarshal[msg->opcode].func) {
+ pw_log_error("%p: function %d not implemented on %u",
+ this, msg->opcode, msg->id);
+ continue;
+ }
+ proxy->refcount++;
+ pw_protocol_native_connection_enter(conn);
+ res = demarshal[msg->opcode].func(proxy, msg);
+ pw_protocol_native_connection_leave(conn);
+ pw_proxy_unref(proxy);
+
+ if (res < 0) {
+ pw_log_error("%p: invalid message received %u for %u: %s",
+ this, msg->opcode, msg->id, spa_strerror(res));
+ debug_msg("*invalid*", msg, true);
+ }
+ res = 0;
+ }
+ client_unref(impl);
+ return res;
+}
+
+static void
+on_remote_data(void *data, int fd, uint32_t mask)
+{
+ struct client *impl = data;
+ struct pw_core *this = impl->this.core;
+ struct pw_proxy *core_proxy = (struct pw_proxy*)this;
+ struct pw_protocol_native_connection *conn = impl->connection;
+ struct pw_context *context = pw_core_get_context(this);
+ struct pw_loop *loop = pw_context_get_main_loop(context);
+ int res;
+
+ core_proxy->refcount++;
+ impl->ref++;
+
+ if (mask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ res = -EPIPE;
+ goto error;
+ }
+ if (mask & SPA_IO_IN) {
+ if ((res = process_remote(impl)) < 0)
+ goto error;
+ }
+ if (mask & SPA_IO_OUT || impl->need_flush) {
+ if (!impl->connected) {
+ socklen_t len = sizeof res;
+
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) {
+ res = -errno;
+ pw_log_error("getsockopt: %m");
+ goto error;
+ }
+ if (res != 0) {
+ res = -res;
+ goto error;
+ }
+ impl->connected = true;
+ pw_log_debug("%p: connected, fd %d", impl, fd);
+ }
+ impl->need_flush = false;
+ res = pw_protocol_native_connection_flush(conn);
+ if (res >= 0) {
+ pw_loop_update_io(loop, impl->source,
+ impl->source->mask & ~SPA_IO_OUT);
+ } else if (res != -EAGAIN)
+ goto error;
+ }
+
+done:
+ client_unref(impl);
+ pw_proxy_unref(core_proxy);
+ return;
+error:
+ pw_log_debug("%p: got connection error %d (%s)", impl, res, spa_strerror(res));
+ if (impl->source) {
+ pw_loop_destroy_source(loop, impl->source);
+ impl->source = NULL;
+ }
+ pw_proxy_notify(core_proxy,
+ struct pw_core_events, error, 0, 0,
+ this->recv_seq, res, "connection error");
+ goto done;
+}
+
+static int impl_connect_fd(struct pw_protocol_client *client, int fd, bool do_close)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+
+ impl->connected = false;
+ impl->disconnecting = false;
+
+ pw_protocol_native_connection_set_fd(impl->connection, fd);
+ impl->source = pw_loop_add_io(impl->context->main_loop,
+ fd,
+ SPA_IO_IN | SPA_IO_OUT | SPA_IO_HUP | SPA_IO_ERR,
+ do_close, on_remote_data, impl);
+ if (impl->source == NULL)
+ return -errno;
+
+ return 0;
+}
+
+static void impl_disconnect(struct pw_protocol_client *client)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+
+ impl->disconnecting = true;
+
+ if (impl->source)
+ pw_loop_destroy_source(impl->context->main_loop, impl->source);
+ impl->source = NULL;
+
+ pw_protocol_native_connection_set_fd(impl->connection, -1);
+}
+
+static void impl_destroy(struct pw_protocol_client *client)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+
+ impl_disconnect(client);
+
+ if (impl->connection)
+ pw_protocol_native_connection_destroy(impl->connection);
+ impl->connection = NULL;
+
+ spa_list_remove(&client->link);
+ client_unref(impl);
+}
+
+static int impl_set_paused(struct pw_protocol_client *client, bool paused)
+{
+ struct client *impl = SPA_CONTAINER_OF(client, struct client, this);
+ uint32_t mask;
+
+ if (impl->source == NULL)
+ return -EIO;
+
+ mask = impl->source->mask;
+
+ impl->paused = paused;
+
+ SPA_FLAG_UPDATE(mask, SPA_IO_IN, !paused);
+
+ pw_log_debug("%p: paused %d", client->protocol, paused);
+ pw_loop_update_io(impl->context->main_loop, impl->source, mask);
+
+ return paused ? 0 : process_remote(impl);
+}
+
+static int pw_protocol_native_connect_internal(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data)
+{
+ int res, sv[2];
+ struct pw_protocol *protocol = client->protocol;
+ struct protocol_data *d = pw_protocol_get_user_data(protocol);
+ struct server *s = d->local;
+ struct pw_permission permissions[1];
+ struct client_data *c;
+
+ pw_log_debug("server %p: internal connect", s);
+
+ if (socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sv) < 0) {
+ res = -errno;
+ pw_log_error("server %p: socketpair() failed with error: %m", s);
+ goto error;
+ }
+
+ c = client_new(s, sv[0]);
+ if (c == NULL) {
+ res = -errno;
+ pw_log_error("server %p: failed to create client: %m", s);
+ goto error_close;
+ }
+ permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_ALL);
+ pw_impl_client_update_permissions(c->client, 1, permissions);
+
+ res = pw_protocol_client_connect_fd(client, sv[1], true);
+done:
+ if (done_callback)
+ done_callback(data, res);
+ return res;
+
+error_close:
+ close(sv[0]);
+ close(sv[1]);
+error:
+ goto done;
+}
+
+static void on_client_connection_destroy(void *data)
+{
+ struct client *impl = data;
+ spa_hook_remove(&impl->conn_listener);
+}
+
+static void on_client_need_flush(void *data)
+{
+ struct client *impl = data;
+
+ pw_log_trace("need flush");
+ impl->need_flush = true;
+
+ if (impl->source && !(impl->source->mask & SPA_IO_OUT)) {
+ pw_loop_update_io(impl->context->main_loop,
+ impl->source, impl->source->mask | SPA_IO_OUT);
+ }
+}
+
+static const struct pw_protocol_native_connection_events client_conn_events = {
+ PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS,
+ .destroy = on_client_connection_destroy,
+ .need_flush = on_client_need_flush,
+};
+
+static struct pw_protocol_client *
+impl_new_client(struct pw_protocol *protocol,
+ struct pw_core *core,
+ const struct spa_dict *props)
+{
+ struct client *impl;
+ struct pw_protocol_client *this;
+ const char *str = NULL;
+ int res;
+
+ if ((impl = calloc(1, sizeof(struct client))) == NULL)
+ return NULL;
+
+ pw_log_debug("%p: new client %p", protocol, impl);
+
+ this = &impl->this;
+ this->protocol = protocol;
+ this->core = core;
+
+ impl->ref = 1;
+ impl->context = protocol->context;
+ impl->connection = pw_protocol_native_connection_new(protocol->context, -1);
+ if (impl->connection == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+ pw_protocol_native_connection_add_listener(impl->connection,
+ &impl->conn_listener,
+ &client_conn_events,
+ impl);
+
+ if (props) {
+ str = spa_dict_lookup(props, PW_KEY_REMOTE_INTENTION);
+ if (str == NULL &&
+ (str = spa_dict_lookup(props, PW_KEY_REMOTE_NAME)) != NULL &&
+ spa_streq(str, "internal"))
+ str = "internal";
+ }
+ if (str == NULL)
+ str = "generic";
+
+ pw_log_debug("%p: connect %s", protocol, str);
+
+ if (spa_streq(str, "screencast"))
+ this->connect = pw_protocol_native_connect_portal_screencast;
+ else if (spa_streq(str, "internal"))
+ this->connect = pw_protocol_native_connect_internal;
+ else
+ this->connect = pw_protocol_native_connect_local_socket;
+
+ this->steal_fd = impl_steal_fd;
+ this->connect_fd = impl_connect_fd;
+ this->disconnect = impl_disconnect;
+ this->destroy = impl_destroy;
+ this->set_paused = impl_set_paused;
+
+ spa_list_append(&protocol->client_list, &this->link);
+
+ return this;
+
+error_free:
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+static void destroy_server(struct pw_protocol_server *server)
+{
+ struct server *s = SPA_CONTAINER_OF(server, struct server, this);
+ struct client_data *data, *tmp;
+
+ pw_log_debug("%p: server %p", s->this.protocol, s);
+
+ spa_list_remove(&server->link);
+
+ spa_list_for_each_safe(data, tmp, &server->client_list, protocol_link)
+ pw_impl_client_destroy(data->client);
+
+ if (s->source)
+ pw_loop_destroy_source(s->loop, s->source);
+ if (s->resume)
+ pw_loop_destroy_source(s->loop, s->resume);
+ if (s->addr.sun_path[0] && !s->activated)
+ unlink(s->addr.sun_path);
+ if (s->lock_addr[0])
+ unlink(s->lock_addr);
+ if (s->fd_lock != -1)
+ close(s->fd_lock);
+ free(s);
+}
+
+static void do_resume(void *_data, uint64_t count)
+{
+ struct server *server = _data;
+ struct pw_protocol_server *this = &server->this;
+ struct client_data *data, *tmp;
+ int res;
+
+ pw_log_debug("flush");
+
+ spa_list_for_each_safe(data, tmp, &this->client_list, protocol_link) {
+ data->client->refcount++;
+ if ((res = process_messages(data)) < 0)
+ handle_client_error(data->client, res, "do_resume");
+ pw_impl_client_unref(data->client);
+ }
+ return;
+}
+
+static const char *
+get_server_name(const struct spa_dict *props)
+{
+ const char *name = NULL;
+
+ name = getenv("PIPEWIRE_CORE");
+ if (name == NULL && props != NULL)
+ name = spa_dict_lookup(props, PW_KEY_CORE_NAME);
+ if (name == NULL)
+ name = PW_DEFAULT_REMOTE;
+ return name;
+}
+
+static struct server *
+create_server(struct pw_protocol *protocol,
+ struct pw_impl_core *core,
+ const struct spa_dict *props)
+{
+ struct pw_protocol_server *this;
+ struct server *s;
+
+ if ((s = calloc(1, sizeof(struct server))) == NULL)
+ return NULL;
+
+ s->fd_lock = -1;
+
+ this = &s->this;
+ this->protocol = protocol;
+ this->core = core;
+ spa_list_init(&this->client_list);
+ this->destroy = destroy_server;
+
+ spa_list_append(&protocol->server_list, &this->link);
+
+ pw_log_debug("%p: created server %p", protocol, this);
+
+ return s;
+}
+
+static struct pw_protocol_server *
+impl_add_server(struct pw_protocol *protocol,
+ struct pw_impl_core *core,
+ const struct spa_dict *props)
+{
+ struct pw_protocol_server *this;
+ struct server *s;
+ const char *name;
+ int res;
+
+ if ((s = create_server(protocol, core, props)) == NULL)
+ return NULL;
+
+ this = &s->this;
+
+ name = get_server_name(props);
+
+ if ((res = init_socket_name(s, name)) < 0)
+ goto error;
+
+ if ((res = lock_socket(s)) < 0)
+ goto error;
+
+ if ((res = add_socket(protocol, s)) < 0)
+ goto error;
+
+ if ((s->resume = pw_loop_add_event(s->loop, do_resume, s)) == NULL)
+ goto error;
+
+ pw_log_info("%p: Listening on '%s'", protocol, name);
+
+ return this;
+
+error:
+ destroy_server(this);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_protocol_implementation protocol_impl = {
+ PW_VERSION_PROTOCOL_IMPLEMENTATION,
+ .new_client = impl_new_client,
+ .add_server = impl_add_server,
+};
+
+static struct spa_pod_builder *
+impl_ext_begin_proxy(struct pw_proxy *proxy, uint8_t opcode, struct pw_protocol_native_message **msg)
+{
+ struct client *impl = SPA_CONTAINER_OF(proxy->core->conn, struct client, this);
+ return pw_protocol_native_connection_begin(impl->connection, proxy->id, opcode, msg);
+}
+
+static uint32_t impl_ext_add_proxy_fd(struct pw_proxy *proxy, int fd)
+{
+ struct client *impl = SPA_CONTAINER_OF(proxy->core->conn, struct client, this);
+ return pw_protocol_native_connection_add_fd(impl->connection, fd);
+}
+
+static int impl_ext_get_proxy_fd(struct pw_proxy *proxy, uint32_t index)
+{
+ struct client *impl = SPA_CONTAINER_OF(proxy->core->conn, struct client, this);
+ return pw_protocol_native_connection_get_fd(impl->connection, index);
+}
+
+static void assert_single_pod(struct spa_pod_builder *builder)
+{
+ /*
+ * Check the invariant that the message we just marshaled
+ * consists of at most one POD.
+ */
+ struct spa_pod *pod = builder->data;
+ spa_assert(builder->data == NULL ||
+ builder->state.offset < sizeof(struct spa_pod) ||
+ builder->state.offset == SPA_POD_SIZE(pod));
+}
+
+static int impl_ext_end_proxy(struct pw_proxy *proxy,
+ struct spa_pod_builder *builder)
+{
+ struct pw_core *core = proxy->core;
+ struct client *impl = SPA_CONTAINER_OF(core->conn, struct client, this);
+ assert_single_pod(builder);
+ marshal_core_footers(&impl->footer_state, core, builder);
+ return core->send_seq = pw_protocol_native_connection_end(impl->connection, builder);
+}
+
+static struct spa_pod_builder *
+impl_ext_begin_resource(struct pw_resource *resource,
+ uint8_t opcode, struct pw_protocol_native_message **msg)
+{
+ struct client_data *data = resource->client->user_data;
+ return pw_protocol_native_connection_begin(data->connection, resource->id, opcode, msg);
+}
+
+static uint32_t impl_ext_add_resource_fd(struct pw_resource *resource, int fd)
+{
+ struct client_data *data = resource->client->user_data;
+ return pw_protocol_native_connection_add_fd(data->connection, fd);
+}
+static int impl_ext_get_resource_fd(struct pw_resource *resource, uint32_t index)
+{
+ struct client_data *data = resource->client->user_data;
+ return pw_protocol_native_connection_get_fd(data->connection, index);
+}
+
+static int impl_ext_end_resource(struct pw_resource *resource,
+ struct spa_pod_builder *builder)
+{
+ struct client_data *data = resource->client->user_data;
+ struct pw_impl_client *client = resource->client;
+ assert_single_pod(builder);
+ marshal_client_footers(&data->footer_state, client, builder);
+ return client->send_seq = pw_protocol_native_connection_end(data->connection, builder);
+}
+static const struct pw_protocol_native_ext protocol_ext_impl = {
+ PW_VERSION_PROTOCOL_NATIVE_EXT,
+ .begin_proxy = impl_ext_begin_proxy,
+ .add_proxy_fd = impl_ext_add_proxy_fd,
+ .get_proxy_fd = impl_ext_get_proxy_fd,
+ .end_proxy = impl_ext_end_proxy,
+ .begin_resource = impl_ext_begin_resource,
+ .add_resource_fd = impl_ext_add_resource_fd,
+ .get_resource_fd = impl_ext_get_resource_fd,
+ .end_resource = impl_ext_end_resource,
+};
+
+static void module_destroy(void *data)
+{
+ struct protocol_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+
+ pw_protocol_destroy(d->protocol);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static int need_server(struct pw_context *context, const struct spa_dict *props)
+{
+ const char *val = NULL;
+
+ val = getenv("PIPEWIRE_DAEMON");
+ if (val == NULL && props != NULL)
+ val = spa_dict_lookup(props, PW_KEY_CORE_DAEMON);
+ if (val && pw_properties_parse_bool(val))
+ return 1;
+ return 0;
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_protocol *this;
+ struct protocol_data *d;
+ const struct pw_properties *props;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+ PW_LOG_TOPIC_INIT(mod_topic_connection);
+
+ if (pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native) != NULL)
+ return 0;
+
+ this = pw_protocol_new(context, PW_TYPE_INFO_PROTOCOL_Native, sizeof(struct protocol_data));
+ if (this == NULL)
+ return -errno;
+
+ debug_messages = mod_topic_connection->level >= SPA_LOG_LEVEL_DEBUG;
+
+ this->implementation = &protocol_impl;
+ this->extension = &protocol_ext_impl;
+
+ pw_protocol_native_init(this);
+ pw_protocol_native0_init(this);
+
+ pw_log_debug("%p: new debug:%d", this, debug_messages);
+
+ d = pw_protocol_get_user_data(this);
+ d->protocol = this;
+ d->module = module;
+
+ props = pw_context_get_properties(context);
+ d->local = create_server(this, context->core, &props->dict);
+
+ if (need_server(context, &props->dict)) {
+ if (impl_add_server(this, context->core, &props->dict) == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+ }
+
+ pw_impl_module_add_listener(module, &d->module_listener, &module_events, d);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error_cleanup:
+ pw_protocol_destroy(this);
+ return res;
+}
diff --git a/src/modules/module-protocol-native/connection.c b/src/modules/module-protocol-native/connection.c
new file mode 100644
index 0000000..1ba256c
--- /dev/null
+++ b/src/modules/module-protocol-native/connection.c
@@ -0,0 +1,866 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+
+#include <pipewire/pipewire.h>
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+PW_LOG_TOPIC_EXTERN(mod_topic_connection);
+
+#undef spa_debug
+#define spa_debug(...) pw_logt_debug(mod_topic_connection, __VA_ARGS__)
+#include <spa/debug/pod.h>
+
+#include "connection.h"
+#include "defs.h"
+
+#define MAX_BUFFER_SIZE (1024 * 32)
+#define MAX_FDS 1024u
+#define MAX_FDS_MSG 28
+
+#define HDR_SIZE_V0 8
+#define HDR_SIZE 16
+
+struct buffer {
+ uint8_t *buffer_data;
+ size_t buffer_size;
+ size_t buffer_maxsize;
+ int fds[MAX_FDS];
+ uint32_t n_fds;
+
+ uint32_t seq;
+ size_t offset;
+ size_t fds_offset;
+ struct pw_protocol_native_message msg;
+};
+
+struct reenter_item {
+ void *old_buffer_data;
+ struct pw_protocol_native_message return_msg;
+ struct spa_list link;
+};
+
+struct impl {
+ struct pw_protocol_native_connection this;
+ struct pw_context *context;
+
+ struct buffer in, out;
+ struct spa_pod_builder builder;
+
+ struct spa_list reenter_stack;
+ uint32_t pending_reentering;
+
+ uint32_t version;
+ size_t hdr_size;
+};
+
+/** \endcond */
+
+/** Get an fd from a connection
+ *
+ * \param conn the connection
+ * \param index the index of the fd to get
+ * \return the fd at \a index or -ENOENT when no such fd exists
+ *
+ * \memberof pw_protocol_native_connection
+ */
+int pw_protocol_native_connection_get_fd(struct pw_protocol_native_connection *conn, uint32_t index)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ struct buffer *buf = &impl->in;
+
+ if (index == SPA_ID_INVALID)
+ return -1;
+
+ if (index >= buf->msg.n_fds)
+ return -ENOENT;
+
+ return buf->msg.fds[index];
+}
+
+/** Add an fd to a connection
+ *
+ * \param conn the connection
+ * \param fd the fd to add
+ * \return the index of the fd or SPA_IDX_INVALID when an error occurred
+ *
+ * \memberof pw_protocol_native_connection
+ */
+uint32_t pw_protocol_native_connection_add_fd(struct pw_protocol_native_connection *conn, int fd)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ struct buffer *buf = &impl->out;
+ uint32_t index, i;
+
+ if (fd < 0)
+ return SPA_IDX_INVALID;
+
+ for (i = 0; i < buf->msg.n_fds; i++) {
+ if (buf->msg.fds[i] == fd)
+ return i;
+ }
+
+ index = buf->msg.n_fds;
+ if (index + buf->n_fds >= MAX_FDS) {
+ pw_log_error("connection %p: too many fds (%d)", conn, MAX_FDS);
+ return SPA_IDX_INVALID;
+ }
+
+ buf->msg.fds[index] = fcntl(fd, F_DUPFD_CLOEXEC, 0);
+ if (buf->msg.fds[index] == -1) {
+ pw_log_error("connection %p: can't DUP fd:%d %m", conn, fd);
+ return SPA_IDX_INVALID;
+ }
+ buf->msg.n_fds++;
+ pw_log_debug("connection %p: add fd %d (new fd:%d) at index %d",
+ conn, fd, buf->msg.fds[index], index);
+
+ return index;
+}
+
+static void *connection_ensure_size(struct pw_protocol_native_connection *conn, struct buffer *buf, size_t size)
+{
+ int res;
+
+ if (buf->buffer_size + size > buf->buffer_maxsize) {
+ void *np;
+ size_t ns;
+
+ ns = SPA_ROUND_UP_N(buf->buffer_size + size, MAX_BUFFER_SIZE);
+ np = realloc(buf->buffer_data, ns);
+ if (np == NULL) {
+ res = -errno;
+ free(buf->buffer_data);
+ buf->buffer_maxsize = 0;
+ spa_hook_list_call(&conn->listener_list,
+ struct pw_protocol_native_connection_events,
+ error, 0, res);
+ errno = -res;
+ return NULL;
+ }
+ buf->buffer_maxsize = ns;
+ buf->buffer_data = np;
+ pw_log_debug("connection %p: resize buffer to %zd %zd %zd",
+ conn, buf->buffer_size, size, buf->buffer_maxsize);
+ }
+ return (uint8_t *) buf->buffer_data + buf->buffer_size;
+}
+
+static void handle_connection_error(struct pw_protocol_native_connection *conn, int res)
+{
+ if (res == EPIPE || res == ECONNRESET)
+ pw_log_info("connection %p: could not recvmsg on fd:%d: %s", conn, conn->fd, strerror(res));
+ else
+ pw_log_error("connection %p: could not recvmsg on fd:%d: %s", conn, conn->fd, strerror(res));
+}
+
+static size_t cmsg_data_length(const struct cmsghdr *cmsg)
+{
+ const void *begin = CMSG_DATA(cmsg);
+ const void *end = SPA_PTROFF(cmsg, cmsg->cmsg_len, void);
+
+ spa_assert(begin <= end);
+
+ return SPA_PTRDIFF(end, begin);
+}
+
+static void close_all_fds(struct msghdr *msg, struct cmsghdr *from)
+{
+ for (; from != NULL; from = CMSG_NXTHDR(msg, from)) {
+ if (from->cmsg_level != SOL_SOCKET || from->cmsg_type != SCM_RIGHTS)
+ continue;
+
+ size_t n_fds = cmsg_data_length(from) / sizeof(int);
+ for (size_t i = 0; i < n_fds; i++) {
+ const void *p = SPA_PTROFF(CMSG_DATA(from), sizeof(int) * i, void);
+ int fd;
+
+ memcpy(&fd, p, sizeof(fd));
+ pw_log_debug("%p: close fd:%d", msg, fd);
+ close(fd);
+ }
+ }
+}
+
+static int refill_buffer(struct pw_protocol_native_connection *conn, struct buffer *buf)
+{
+ ssize_t len;
+ struct cmsghdr *cmsg = NULL;
+ struct msghdr msg = { 0 };
+ struct iovec iov[1];
+ union {
+ char cmsgbuf[CMSG_SPACE(MAX_FDS_MSG * sizeof(int))];
+ struct cmsghdr align;
+ } cmsgbuf;
+ int n_fds = 0;
+ size_t avail;
+
+ avail = buf->buffer_maxsize - buf->buffer_size;
+
+ iov[0].iov_base = buf->buffer_data + buf->buffer_size;
+ iov[0].iov_len = avail;
+ msg.msg_iov = iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = &cmsgbuf;
+ msg.msg_controllen = sizeof(cmsgbuf);
+ msg.msg_flags = MSG_CMSG_CLOEXEC | MSG_DONTWAIT;
+
+ while (true) {
+ len = recvmsg(conn->fd, &msg, msg.msg_flags);
+ if (msg.msg_flags & MSG_CTRUNC)
+ goto cmsgs_truncated;
+ if (len == 0 && avail != 0)
+ return -EPIPE;
+ else if (len < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno != EAGAIN && errno != EWOULDBLOCK)
+ goto recv_error;
+ return -EAGAIN;
+ }
+ break;
+ }
+
+ buf->buffer_size += len;
+
+ /* handle control messages */
+ for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+ if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
+ continue;
+
+ n_fds = cmsg_data_length(cmsg) / sizeof(int);
+ if (n_fds + buf->n_fds > MAX_FDS)
+ goto too_many_fds;
+ memcpy(&buf->fds[buf->n_fds], CMSG_DATA(cmsg), n_fds * sizeof(int));
+ buf->n_fds += n_fds;
+ }
+ pw_log_trace("connection %p: %d read %zd bytes and %d fds", conn, conn->fd, len,
+ n_fds);
+
+ return 0;
+
+ /* ERRORS */
+recv_error:
+ handle_connection_error(conn, errno);
+ return -errno;
+
+cmsgs_truncated:
+ close_all_fds(&msg, CMSG_FIRSTHDR(&msg));
+ return -EPROTO;
+
+too_many_fds:
+ close_all_fds(&msg, cmsg);
+ return -EPROTO;
+}
+
+static void clear_buffer(struct buffer *buf, bool fds)
+{
+ uint32_t i;
+ if (fds) {
+ for (i = 0; i < buf->n_fds; i++) {
+ pw_log_debug("%p: close fd:%d", buf, buf->fds[i]);
+ close(buf->fds[i]);
+ }
+ }
+ buf->n_fds = 0;
+ buf->buffer_size = 0;
+ buf->offset = 0;
+ buf->fds_offset = 0;
+}
+
+/** Prepare connection for calling from reentered context.
+ *
+ * This ensures that message buffers returned by get_next are not invalidated by additional
+ * calls made after enter. Leave invalidates the buffers at the higher stack level.
+ *
+ * \memberof pw_protocol_native_connection
+ */
+void pw_protocol_native_connection_enter(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+
+ /* Postpone processing until get_next is actually called */
+ ++impl->pending_reentering;
+}
+
+static void pop_reenter_stack(struct impl *impl, uint32_t count)
+{
+ while (count > 0) {
+ struct reenter_item *item;
+
+ item = spa_list_last(&impl->reenter_stack, struct reenter_item, link);
+ spa_list_remove(&item->link);
+
+ free(item->return_msg.fds);
+ free(item->old_buffer_data);
+ free(item);
+
+ --count;
+ }
+}
+
+void pw_protocol_native_connection_leave(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+
+ if (impl->pending_reentering > 0) {
+ --impl->pending_reentering;
+ } else {
+ pw_log_trace("connection %p: reenter: pop", impl);
+ pop_reenter_stack(impl, 1);
+ }
+}
+
+static int ensure_stack_level(struct impl *impl, struct pw_protocol_native_message **msg)
+{
+ void *data;
+ struct buffer *buf = &impl->in;
+ struct reenter_item *item, *new_item = NULL;
+
+ item = spa_list_last(&impl->reenter_stack, struct reenter_item, link);
+
+ if (SPA_LIKELY(impl->pending_reentering == 0)) {
+ new_item = item;
+ } else {
+ uint32_t new_count;
+
+ pw_log_trace("connection %p: reenter: push %d levels",
+ impl, impl->pending_reentering);
+
+ /* Append empty item(s) to the reenter stack */
+ for (new_count = 0; new_count < impl->pending_reentering; ++new_count) {
+ new_item = calloc(1, sizeof(struct reenter_item));
+ if (new_item == NULL) {
+ pop_reenter_stack(impl, new_count);
+ return -ENOMEM;
+ }
+ spa_list_append(&impl->reenter_stack, &new_item->link);
+ }
+
+ /*
+ * Stack level increased: we have to switch to a new message data buffer, because
+ * data of returned messages is contained in the buffer and might still be in
+ * use on the lower stack levels.
+ *
+ * We stash the buffer for the previous stack level, and allocate a new one for
+ * the new stack level. If there was a previous buffer for the previous level, we
+ * know its contents are no longer in use (the only active buffer at that stack
+ * level is buf->buffer_data), and we can recycle it as the new buffer (realloc
+ * instead of calloc).
+ *
+ * The current data contained in the buffer needs to be copied to the new buffer.
+ */
+
+ data = realloc(item->old_buffer_data, buf->buffer_maxsize);
+ if (data == NULL) {
+ pop_reenter_stack(impl, new_count);
+ return -ENOMEM;
+ }
+
+ item->old_buffer_data = buf->buffer_data;
+
+ memcpy(data, buf->buffer_data, buf->buffer_size);
+ buf->buffer_data = data;
+
+ impl->pending_reentering = 0;
+ }
+ if (new_item == NULL)
+ return -EIO;
+
+ /* Ensure fds buffer is allocated */
+ if (SPA_UNLIKELY(new_item->return_msg.fds == NULL)) {
+ data = calloc(MAX_FDS, sizeof(int));
+ if (data == NULL)
+ return -ENOMEM;
+ new_item->return_msg.fds = data;
+ }
+
+ *msg = &new_item->return_msg;
+
+ return 0;
+}
+
+/** Make a new connection object for the given socket
+ *
+ * \param fd the socket
+ * \returns a newly allocated connection object
+ *
+ * \memberof pw_protocol_native_connection
+ */
+struct pw_protocol_native_connection *pw_protocol_native_connection_new(struct pw_context *context, int fd)
+{
+ struct impl *impl;
+ struct pw_protocol_native_connection *this;
+ struct reenter_item *reenter_item;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return NULL;
+
+ impl->context = context;
+
+ this = &impl->this;
+
+ pw_log_debug("connection %p: new fd:%d", this, fd);
+
+ this->fd = fd;
+ spa_hook_list_init(&this->listener_list);
+
+ impl->hdr_size = HDR_SIZE;
+ impl->version = 3;
+
+ impl->out.buffer_data = calloc(1, MAX_BUFFER_SIZE);
+ impl->out.buffer_maxsize = MAX_BUFFER_SIZE;
+ impl->in.buffer_data = calloc(1, MAX_BUFFER_SIZE);
+ impl->in.buffer_maxsize = MAX_BUFFER_SIZE;
+
+ reenter_item = calloc(1, sizeof(struct reenter_item));
+
+ if (impl->out.buffer_data == NULL || impl->in.buffer_data == NULL || reenter_item == NULL)
+ goto no_mem;
+
+ spa_list_init(&impl->reenter_stack);
+ spa_list_append(&impl->reenter_stack, &reenter_item->link);
+
+ return this;
+
+no_mem:
+ free(impl->out.buffer_data);
+ free(impl->in.buffer_data);
+ free(reenter_item);
+ free(impl);
+ return NULL;
+}
+
+int pw_protocol_native_connection_set_fd(struct pw_protocol_native_connection *conn, int fd)
+{
+ pw_log_debug("connection %p: fd:%d", conn, fd);
+ conn->fd = fd;
+ return 0;
+}
+
+/** Destroy a connection
+ *
+ * \param conn the connection to destroy
+ *
+ * \memberof pw_protocol_native_connection
+ */
+void pw_protocol_native_connection_destroy(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+
+ pw_log_debug("connection %p: destroy", conn);
+
+ spa_hook_list_call(&conn->listener_list, struct pw_protocol_native_connection_events, destroy, 0);
+
+ spa_hook_list_clean(&conn->listener_list);
+
+ clear_buffer(&impl->out, true);
+ clear_buffer(&impl->in, true);
+ free(impl->out.buffer_data);
+ free(impl->in.buffer_data);
+
+ while (!spa_list_is_empty(&impl->reenter_stack))
+ pop_reenter_stack(impl, 1);
+
+ free(impl);
+}
+
+static int prepare_packet(struct pw_protocol_native_connection *conn, struct buffer *buf)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ uint8_t *data;
+ size_t size, len;
+ uint32_t *p;
+
+ data = buf->buffer_data + buf->offset;
+ size = buf->buffer_size - buf->offset;
+
+ if (size < impl->hdr_size)
+ return impl->hdr_size;
+
+ p = (uint32_t *) data;
+
+ buf->msg.id = p[0];
+ buf->msg.opcode = p[1] >> 24;
+ len = p[1] & 0xffffff;
+
+ if (buf->msg.id == 0 && buf->msg.opcode == 1) {
+ if (p[3] >= 4) {
+ pw_log_warn("old version detected");
+ impl->version = 0;
+ impl->hdr_size = HDR_SIZE_V0;
+ } else {
+ impl->version = 3;
+ impl->hdr_size = HDR_SIZE;
+ }
+ spa_hook_list_call(&conn->listener_list,
+ struct pw_protocol_native_connection_events,
+ start, 0, impl->version);
+ }
+ if (impl->version >= 3) {
+ buf->msg.seq = p[2];
+ buf->msg.n_fds = p[3];
+ } else {
+ buf->msg.seq = 0;
+ buf->msg.n_fds = 0;
+ }
+
+ data += impl->hdr_size;
+ size -= impl->hdr_size;
+ buf->msg.fds = &buf->fds[buf->fds_offset];
+
+ if (buf->msg.n_fds + buf->fds_offset > MAX_FDS)
+ return -EPROTO;
+
+ if (size < len)
+ return len;
+
+ buf->msg.size = len;
+ buf->msg.data = data;
+
+ buf->offset += impl->hdr_size + len;
+ buf->fds_offset += buf->msg.n_fds;
+
+ if (buf->offset >= buf->buffer_size)
+ clear_buffer(buf, false);
+
+ return 0;
+}
+
+/** Move to the next packet in the connection
+ *
+ * \param conn the connection
+ * \param opcode address of result opcode
+ * \param dest_id address of result destination id
+ * \param dt pointer to packet data
+ * \param sz size of packet data
+ * \return true on success
+ *
+ * Get the next packet in \a conn and store the opcode and destination
+ * id as well as the packet data and size.
+ *
+ * \memberof pw_protocol_native_connection
+ */
+int
+pw_protocol_native_connection_get_next(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message **msg)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ int len, res;
+ struct buffer *buf;
+ struct pw_protocol_native_message *return_msg;
+ int *fds;
+
+ if ((res = ensure_stack_level(impl, &return_msg)) < 0)
+ return res;
+
+ buf = &impl->in;
+
+ while (1) {
+ len = prepare_packet(conn, buf);
+ if (len < 0)
+ return len;
+ if (len == 0)
+ break;
+
+ if (connection_ensure_size(conn, buf, len) == NULL)
+ return -errno;
+ if ((res = refill_buffer(conn, buf)) < 0)
+ return res;
+ }
+
+ /* Returned msg struct should be safe vs. reentering */
+ fds = return_msg->fds;
+ *return_msg = buf->msg;
+ if (buf->msg.n_fds > 0) {
+ memcpy(fds, buf->msg.fds, buf->msg.n_fds * sizeof(int));
+ }
+ return_msg->fds = fds;
+
+ *msg = return_msg;
+
+ return 1;
+}
+
+/** Get footer data from the tail of the current packet.
+ *
+ * \param conn the connection
+ * \param msg current message
+ * \return footer POD, or NULL if no valid footer present
+ *
+ * \memberof pw_protocol_native_connection
+ */
+struct spa_pod *pw_protocol_native_connection_get_footer(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message *msg)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ struct spa_pod *pod;
+
+ if (impl->version != 3)
+ return NULL;
+
+ /*
+ * Protocol version 3 footer: a single SPA POD
+ */
+
+ /* Footer immediately follows the message POD, if it is present */
+ if ((pod = get_first_pod_from_data(msg->data, msg->size, 0)) == NULL)
+ return NULL;
+ pod = get_first_pod_from_data(msg->data, msg->size, SPA_POD_SIZE(pod));
+ if (pod == NULL)
+ return NULL;
+ pw_log_trace("connection %p: recv message footer, size:%zu",
+ conn, (size_t)SPA_POD_SIZE(pod));
+ return pod;
+}
+
+static inline void *begin_write(struct pw_protocol_native_connection *conn, uint32_t size)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ uint32_t *p;
+ struct buffer *buf = &impl->out;
+ /* header and size for payload */
+ if ((p = connection_ensure_size(conn, buf, impl->hdr_size + size)) == NULL)
+ return NULL;
+
+ return SPA_PTROFF(p, impl->hdr_size, void);
+}
+
+static int builder_overflow(void *data, uint32_t size)
+{
+ struct impl *impl = data;
+ struct spa_pod_builder *b = &impl->builder;
+
+ b->size = SPA_ROUND_UP_N(size, 4096);
+ if ((b->data = begin_write(&impl->this, b->size)) == NULL)
+ return -errno;
+ return 0;
+}
+
+static const struct spa_pod_builder_callbacks builder_callbacks = {
+ SPA_VERSION_POD_BUILDER_CALLBACKS,
+ .overflow = builder_overflow
+};
+
+struct spa_pod_builder *
+pw_protocol_native_connection_begin(struct pw_protocol_native_connection *conn,
+ uint32_t id, uint8_t opcode,
+ struct pw_protocol_native_message **msg)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ struct buffer *buf = &impl->out;
+
+ buf->msg.id = id;
+ buf->msg.opcode = opcode;
+ impl->builder = SPA_POD_BUILDER_INIT(NULL, 0);
+ spa_pod_builder_set_callbacks(&impl->builder, &builder_callbacks, impl);
+ if (impl->version >= 3) {
+ buf->msg.n_fds = 0;
+ buf->msg.fds = &buf->fds[buf->n_fds];
+ } else {
+ buf->msg.n_fds = buf->n_fds;
+ buf->msg.fds = &buf->fds[0];
+ }
+
+ buf->msg.seq = buf->seq;
+ if (msg)
+ *msg = &buf->msg;
+ return &impl->builder;
+}
+
+int
+pw_protocol_native_connection_end(struct pw_protocol_native_connection *conn,
+ struct spa_pod_builder *builder)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ uint32_t *p, size = builder->state.offset;
+ struct buffer *buf = &impl->out;
+ int res;
+
+ if ((p = connection_ensure_size(conn, buf, impl->hdr_size + size)) == NULL)
+ return -errno;
+
+ p[0] = buf->msg.id;
+ p[1] = (buf->msg.opcode << 24) | (size & 0xffffff);
+ if (impl->version >= 3) {
+ p[2] = buf->msg.seq;
+ p[3] = buf->msg.n_fds;
+ }
+
+ buf->buffer_size += impl->hdr_size + size;
+ if (impl->version >= 3)
+ buf->n_fds += buf->msg.n_fds;
+ else
+ buf->n_fds = buf->msg.n_fds;
+
+ if (mod_topic_connection->level >= SPA_LOG_LEVEL_DEBUG) {
+ pw_logt_debug(mod_topic_connection,
+ ">>>>>>>>> out: id:%d op:%d size:%d seq:%d",
+ buf->msg.id, buf->msg.opcode, size, buf->msg.seq);
+ spa_debug_pod(0, NULL, SPA_PTROFF(p, impl->hdr_size, struct spa_pod));
+ pw_logt_debug(mod_topic_connection,
+ ">>>>>>>>> out: done");
+ }
+
+ buf->seq = (buf->seq + 1) & SPA_ASYNC_SEQ_MASK;
+ res = SPA_RESULT_RETURN_ASYNC(buf->msg.seq);
+
+ spa_hook_list_call(&conn->listener_list,
+ struct pw_protocol_native_connection_events, need_flush, 0);
+
+ return res;
+}
+
+/** Flush the connection object
+ *
+ * \param conn the connection object
+ * \return 0 on success < 0 error code on error
+ *
+ * Write the queued messages on the connection to the socket
+ *
+ * \memberof pw_protocol_native_connection
+ */
+int pw_protocol_native_connection_flush(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+ ssize_t sent, outsize;
+ struct msghdr msg = { 0 };
+ struct iovec iov[1];
+ struct cmsghdr *cmsg;
+ union {
+ char cmsgbuf[CMSG_SPACE(MAX_FDS_MSG * sizeof(int))];
+ struct cmsghdr align;
+ } cmsgbuf;
+ int res = 0, *fds;
+ uint32_t fds_len, to_close, n_fds, outfds, i;
+ struct buffer *buf;
+ void *data;
+ size_t size;
+
+ buf = &impl->out;
+ data = buf->buffer_data;
+ size = buf->buffer_size;
+ fds = buf->fds;
+ n_fds = buf->n_fds;
+ to_close = 0;
+
+ while (size > 0) {
+ if (n_fds > MAX_FDS_MSG) {
+ outfds = MAX_FDS_MSG;
+ outsize = SPA_MIN(sizeof(uint32_t), size);
+ } else {
+ outfds = n_fds;
+ outsize = size;
+ }
+
+ fds_len = outfds * sizeof(int);
+
+ iov[0].iov_base = data;
+ iov[0].iov_len = outsize;
+ msg.msg_iov = iov;
+ msg.msg_iovlen = 1;
+
+ if (outfds > 0) {
+ msg.msg_control = &cmsgbuf;
+ msg.msg_controllen = CMSG_SPACE(fds_len);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(fds_len);
+ memcpy(CMSG_DATA(cmsg), fds, fds_len);
+ msg.msg_controllen = cmsg->cmsg_len;
+ } else {
+ msg.msg_control = NULL;
+ msg.msg_controllen = 0;
+ }
+
+ while (true) {
+ sent = sendmsg(conn->fd, &msg, MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (sent < 0) {
+ if (errno == EINTR)
+ continue;
+ else {
+ res = -errno;
+ goto exit;
+ }
+ }
+ break;
+ }
+ pw_log_trace("connection %p: %d written %zd bytes and %u fds", conn, conn->fd, sent,
+ outfds);
+
+ size -= sent;
+ data = SPA_PTROFF(data, sent, void);
+ n_fds -= outfds;
+ fds += outfds;
+ to_close += outfds;
+ }
+
+ res = 0;
+
+exit:
+ if (size > 0)
+ memmove(buf->buffer_data, data, size);
+ buf->buffer_size = size;
+ for (i = 0; i < to_close; i++) {
+ pw_log_debug("%p: close fd:%d", conn, buf->fds[i]);
+ close(buf->fds[i]);
+ }
+ if (n_fds > 0)
+ memmove(buf->fds, fds, n_fds * sizeof(int));
+ buf->n_fds = n_fds;
+ return res;
+}
+
+/** Clear the connection object
+ *
+ * \param conn the connection object
+ * \return 0 on success
+ *
+ * Remove all queued messages from \a conn
+ *
+ * \memberof pw_protocol_native_connection
+ */
+int pw_protocol_native_connection_clear(struct pw_protocol_native_connection *conn)
+{
+ struct impl *impl = SPA_CONTAINER_OF(conn, struct impl, this);
+
+ clear_buffer(&impl->out, true);
+ clear_buffer(&impl->in, true);
+
+ return 0;
+}
diff --git a/src/modules/module-protocol-native/connection.h b/src/modules/module-protocol-native/connection.h
new file mode 100644
index 0000000..d93829c
--- /dev/null
+++ b/src/modules/module-protocol-native/connection.h
@@ -0,0 +1,112 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PROTOCOL_NATIVE_CONNECTION_H
+#define PIPEWIRE_PROTOCOL_NATIVE_CONNECTION_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include <pipewire/extensions/protocol-native.h>
+
+struct pw_protocol_native_connection_events {
+#define PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*error) (void *data, int error);
+
+ void (*need_flush) (void *data);
+
+ void (*start) (void *data, uint32_t version);
+};
+
+/** \class pw_protocol_native_connection
+ *
+ * \brief Manages the connection between client and server
+ *
+ * The \ref pw_protocol_native_connection handles the connection between client
+ * and server on a given socket.
+ */
+struct pw_protocol_native_connection {
+ int fd; /**< the socket */
+
+ struct spa_hook_list listener_list;
+};
+
+static inline void
+pw_protocol_native_connection_add_listener(struct pw_protocol_native_connection *conn,
+ struct spa_hook *listener,
+ const struct pw_protocol_native_connection_events *events,
+ void *data)
+{
+ spa_hook_list_append(&conn->listener_list, listener, events, data);
+}
+
+struct pw_protocol_native_connection *
+pw_protocol_native_connection_new(struct pw_context *context, int fd);
+
+int pw_protocol_native_connection_set_fd(struct pw_protocol_native_connection *conn, int fd);
+
+void
+pw_protocol_native_connection_destroy(struct pw_protocol_native_connection *conn);
+
+int
+pw_protocol_native_connection_get_next(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message **msg);
+
+uint32_t pw_protocol_native_connection_add_fd(struct pw_protocol_native_connection *conn, int fd);
+int pw_protocol_native_connection_get_fd(struct pw_protocol_native_connection *conn, uint32_t index);
+
+struct spa_pod_builder *
+pw_protocol_native_connection_begin(struct pw_protocol_native_connection *conn,
+ uint32_t id, uint8_t opcode,
+ struct pw_protocol_native_message **msg);
+
+int
+pw_protocol_native_connection_end(struct pw_protocol_native_connection *conn,
+ struct spa_pod_builder *builder);
+
+int
+pw_protocol_native_connection_flush(struct pw_protocol_native_connection *conn);
+
+int
+pw_protocol_native_connection_clear(struct pw_protocol_native_connection *conn);
+
+void pw_protocol_native_connection_enter(struct pw_protocol_native_connection *conn);
+void pw_protocol_native_connection_leave(struct pw_protocol_native_connection *conn);
+
+struct spa_pod *pw_protocol_native_connection_get_footer(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message *msg);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_PROTOCOL_NATIVE_CONNECTION_H */
diff --git a/src/modules/module-protocol-native/defs.h b/src/modules/module-protocol-native/defs.h
new file mode 100644
index 0000000..dc3c625
--- /dev/null
+++ b/src/modules/module-protocol-native/defs.h
@@ -0,0 +1,49 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+int pw_protocol_native_connect_local_socket(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data);
+int pw_protocol_native_connect_portal_screencast(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data);
+
+static inline void *get_first_pod_from_data(void *data, uint32_t maxsize, uint64_t offset)
+{
+ void *pod;
+ if (maxsize <= offset)
+ return NULL;
+
+ /* spa_pod_parser_advance() rounds up, so round down here to compensate */
+ maxsize = SPA_ROUND_DOWN_N(maxsize - offset, 8);
+ if (maxsize < sizeof(struct spa_pod))
+ return NULL;
+
+ pod = SPA_PTROFF(data, offset, void);
+ if (SPA_POD_BODY_SIZE(pod) > maxsize - sizeof(struct spa_pod))
+ return NULL;
+ return pod;
+}
diff --git a/src/modules/module-protocol-native/local-socket.c b/src/modules/module-protocol-native/local-socket.c
new file mode 100644
index 0000000..cbae203
--- /dev/null
+++ b/src/modules/module-protocol-native/local-socket.c
@@ -0,0 +1,169 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <sys/file.h>
+#ifdef HAVE_PWD_H
+#include <pwd.h>
+#endif
+
+#include <pipewire/pipewire.h>
+
+#define DEFAULT_SYSTEM_RUNTIME_DIR "/run/pipewire"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static const char *
+get_remote(const struct spa_dict *props)
+{
+ const char *name;
+
+ name = getenv("PIPEWIRE_REMOTE");
+ if ((name == NULL || name[0] == '\0') && props)
+ name = spa_dict_lookup(props, PW_KEY_REMOTE_NAME);
+ if (name == NULL || name[0] == '\0')
+ name = PW_DEFAULT_REMOTE;
+ return name;
+}
+
+static const char *
+get_runtime_dir(void)
+{
+ const char *runtime_dir;
+
+ runtime_dir = getenv("PIPEWIRE_RUNTIME_DIR");
+ if (runtime_dir == NULL)
+ runtime_dir = getenv("XDG_RUNTIME_DIR");
+ if (runtime_dir == NULL)
+ runtime_dir = getenv("USERPROFILE");
+ return runtime_dir;
+}
+
+static const char *
+get_system_dir(void)
+{
+ return DEFAULT_SYSTEM_RUNTIME_DIR;
+}
+
+static int try_connect(struct pw_protocol_client *client,
+ const char *runtime_dir, const char *name,
+ void (*done_callback) (void *data, int res),
+ void *data)
+{
+ struct sockaddr_un addr;
+ socklen_t size;
+ int res, name_size, fd;
+
+ pw_log_info("connecting to '%s' runtime_dir:%s", name, runtime_dir);
+
+ if ((fd = socket(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ res = -errno;
+ goto error;
+ }
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_LOCAL;
+ if (runtime_dir == NULL)
+ name_size = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", name) + 1;
+ else
+ name_size = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/%s", runtime_dir, name) + 1;
+
+ 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",
+ client, name, (int) sizeof(addr.sun_path));
+ else
+ pw_log_error("client %p: socket path \"%s/%s\" plus null terminator exceeds %i bytes",
+ client, runtime_dir, name, (int) sizeof(addr.sun_path));
+ res = -ENAMETOOLONG;
+ goto error_close;
+ };
+
+ size = offsetof(struct sockaddr_un, sun_path) + name_size;
+
+ if (connect(fd, (struct sockaddr *) &addr, size) < 0) {
+ pw_log_debug("connect to '%s' failed: %m", name);
+ if (errno == ENOENT)
+ errno = EHOSTDOWN;
+ if (errno == EAGAIN) {
+ pw_log_info("client %p: connect pending, fd %d", client, fd);
+ } else {
+ res = -errno;
+ goto error_close;
+ }
+ }
+
+ res = pw_protocol_client_connect_fd(client, fd, true);
+
+ if (done_callback)
+ done_callback(data, res);
+
+ return res;
+
+error_close:
+ close(fd);
+error:
+ return res;
+}
+
+int pw_protocol_native_connect_local_socket(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data)
+{
+ const char *runtime_dir, *name;
+ int res;
+
+ name = get_remote(props);
+ if (name == NULL)
+ return -EINVAL;
+
+ if (name[0] == '/') {
+ res = try_connect(client, NULL, name, done_callback, data);
+ } else {
+ runtime_dir = get_runtime_dir();
+ if (runtime_dir != NULL) {
+ res = try_connect(client, runtime_dir, name, done_callback, data);
+ if (res >= 0)
+ goto exit;
+ }
+ runtime_dir = get_system_dir();
+ if (runtime_dir != NULL)
+ res = try_connect(client, runtime_dir, name, done_callback, data);
+ }
+exit:
+ return res;
+}
diff --git a/src/modules/module-protocol-native/portal-screencast.c b/src/modules/module-protocol-native/portal-screencast.c
new file mode 100644
index 0000000..7cb6614
--- /dev/null
+++ b/src/modules/module-protocol-native/portal-screencast.c
@@ -0,0 +1,41 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/socket.h>
+
+#include <pipewire/pipewire.h>
+
+int pw_protocol_native_connect_portal_screencast(struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int res),
+ void *data)
+{
+ return -ENOTSUP;
+}
diff --git a/src/modules/module-protocol-native/protocol-footer.c b/src/modules/module-protocol-native/protocol-footer.c
new file mode 100644
index 0000000..9b6fe62
--- /dev/null
+++ b/src/modules/module-protocol-native/protocol-footer.c
@@ -0,0 +1,152 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/private.h>
+
+#include "connection.h"
+#include "protocol-footer.h"
+#include "defs.h"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct footer_builder {
+ struct spa_pod_builder *builder;
+ struct spa_pod_frame outer;
+ struct spa_pod_frame inner;
+ unsigned int started:1;
+};
+
+#define FOOTER_BUILDER_INIT(builder) ((struct footer_builder) { (builder) })
+
+static void start_footer_entry(struct footer_builder *fb, uint32_t opcode)
+{
+ if (!fb->started) {
+ spa_pod_builder_push_struct(fb->builder, &fb->outer);
+ fb->started = true;
+ }
+
+ spa_pod_builder_id(fb->builder, opcode);
+ spa_pod_builder_push_struct(fb->builder, &fb->inner);
+}
+
+static void end_footer_entry(struct footer_builder *fb)
+{
+ spa_pod_builder_pop(fb->builder, &fb->inner);
+}
+
+static void end_footer(struct footer_builder *fb)
+{
+ if (!fb->started)
+ return;
+
+ spa_pod_builder_pop(fb->builder, &fb->outer);
+}
+
+void marshal_core_footers(struct footer_core_global_state *state, struct pw_core *core,
+ struct spa_pod_builder *builder)
+{
+ struct footer_builder fb = FOOTER_BUILDER_INIT(builder);
+
+ if (core->recv_generation != state->last_recv_generation) {
+ state->last_recv_generation = core->recv_generation;
+
+ pw_log_trace("core %p: send client registry generation:%"PRIu64,
+ core, core->recv_generation);
+
+ start_footer_entry(&fb, FOOTER_CLIENT_OPCODE_GENERATION);
+ spa_pod_builder_long(fb.builder, core->recv_generation);
+ end_footer_entry(&fb);
+ }
+
+ end_footer(&fb);
+}
+
+void marshal_client_footers(struct footer_client_global_state *state, struct pw_impl_client *client,
+ struct spa_pod_builder *builder)
+{
+ struct footer_builder fb = FOOTER_BUILDER_INIT(builder);
+
+ if (client->context->generation != client->sent_generation) {
+ client->sent_generation = client->context->generation;
+
+ pw_log_trace("impl-client %p: send server registry generation:%"PRIu64,
+ client, client->context->generation);
+
+ start_footer_entry(&fb, FOOTER_CORE_OPCODE_GENERATION);
+ spa_pod_builder_long(fb.builder, client->context->generation);
+ end_footer_entry(&fb);
+ }
+
+ end_footer(&fb);
+}
+
+int demarshal_core_generation(void *object, struct spa_pod_parser *parser)
+{
+ struct pw_core *core = object;
+ int64_t generation;
+
+ if (spa_pod_parser_get_long(parser, &generation) < 0)
+ return -EINVAL;
+
+ core->recv_generation = SPA_MAX(core->recv_generation,
+ (uint64_t)generation);
+
+ pw_log_trace("core %p: recv server registry generation:%"PRIu64,
+ core, generation);
+
+ return 0;
+}
+
+int demarshal_client_generation(void *object, struct spa_pod_parser *parser)
+{
+ struct pw_impl_client *client = object;
+ int64_t generation;
+
+ if (spa_pod_parser_get_long(parser, &generation) < 0)
+ return -EINVAL;
+
+ client->recv_generation = SPA_MAX(client->recv_generation,
+ (uint64_t)generation);
+
+ pw_log_trace("impl-client %p: recv client registry generation:%"PRIu64,
+ client, generation);
+
+ return 0;
+}
+
+const struct footer_demarshal footer_core_demarshal[FOOTER_CORE_OPCODE_LAST] = {
+ [FOOTER_CORE_OPCODE_GENERATION] = (struct footer_demarshal){ .demarshal = demarshal_core_generation },
+};
+
+const struct footer_demarshal footer_client_demarshal[FOOTER_CLIENT_OPCODE_LAST] = {
+ [FOOTER_CLIENT_OPCODE_GENERATION] = (struct footer_demarshal){ .demarshal = demarshal_client_generation },
+};
diff --git a/src/modules/module-protocol-native/protocol-footer.h b/src/modules/module-protocol-native/protocol-footer.h
new file mode 100644
index 0000000..bdbec04
--- /dev/null
+++ b/src/modules/module-protocol-native/protocol-footer.h
@@ -0,0 +1,59 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * Protocol footer.
+ *
+ * For passing around general state data that is not associated with
+ * messages sent to objects.
+ */
+
+enum {
+ FOOTER_CORE_OPCODE_GENERATION = 0,
+ FOOTER_CORE_OPCODE_LAST
+};
+
+enum {
+ FOOTER_CLIENT_OPCODE_GENERATION = 0,
+ FOOTER_CLIENT_OPCODE_LAST
+};
+
+struct footer_core_global_state {
+ uint64_t last_recv_generation;
+};
+
+struct footer_client_global_state {
+};
+
+struct footer_demarshal {
+ int (*demarshal)(void *object, struct spa_pod_parser *parser);
+};
+
+extern const struct footer_demarshal footer_core_demarshal[FOOTER_CORE_OPCODE_LAST];
+extern const struct footer_demarshal footer_client_demarshal[FOOTER_CLIENT_OPCODE_LAST];
+
+void marshal_core_footers(struct footer_core_global_state *state, struct pw_core *core,
+ struct spa_pod_builder *builder);
+void marshal_client_footers(struct footer_client_global_state *state, struct pw_impl_client *client,
+ struct spa_pod_builder *builder);
diff --git a/src/modules/module-protocol-native/protocol-native.c b/src/modules/module-protocol-native/protocol-native.c
new file mode 100644
index 0000000..1c2d83e
--- /dev/null
+++ b/src/modules/module-protocol-native/protocol-native.c
@@ -0,0 +1,2236 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/protocol-native.h>
+
+#include "connection.h"
+
+#define MAX_DICT 1024
+#define MAX_PARAM_INFO 128
+#define MAX_PERMISSIONS 4096
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+static int core_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_core_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int core_method_marshal_hello(void *object, uint32_t version)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_HELLO, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(version));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int core_method_marshal_sync(void *object, uint32_t id, int seq)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_SYNC, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int core_method_marshal_pong(void *object, uint32_t id, int seq)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_PONG, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(seq));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int core_method_marshal_error(void *object, uint32_t id, int seq, int res, const char *error)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_ERROR, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(seq),
+ SPA_POD_Int(res),
+ SPA_POD_String(error));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static struct pw_registry * core_method_marshal_get_registry(void *object,
+ uint32_t version, size_t user_data_size)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct pw_proxy *res;
+ uint32_t new_id;
+
+ res = pw_proxy_new(object, PW_TYPE_INTERFACE_Registry, version, user_data_size);
+ if (res == NULL)
+ return NULL;
+
+ new_id = pw_proxy_get_id(res);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_GET_REGISTRY, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(version),
+ SPA_POD_Int(new_id));
+
+ pw_protocol_native_end_proxy(proxy, b);
+
+ return (struct pw_registry *) res;
+}
+
+static inline void push_item(struct spa_pod_builder *b, const struct spa_dict_item *item)
+{
+ const char *str;
+ spa_pod_builder_string(b, item->key);
+ str = item->value;
+ if (spa_strstartswith(str, "pointer:"))
+ str = "";
+ spa_pod_builder_string(b, str);
+}
+
+static void push_dict(struct spa_pod_builder *b, const struct spa_dict *dict)
+{
+ uint32_t i, n_items;
+ struct spa_pod_frame f;
+
+ n_items = dict ? dict->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_int(b, n_items);
+ for (i = 0; i < n_items; i++)
+ push_item(b, &dict->items[i]);
+ spa_pod_builder_pop(b, &f);
+}
+
+static inline int parse_item(struct spa_pod_parser *prs, struct spa_dict_item *item)
+{
+ int res;
+ if ((res = spa_pod_parser_get(prs,
+ SPA_POD_String(&item->key),
+ SPA_POD_String(&item->value),
+ NULL)) < 0)
+ return res;
+ if (spa_strstartswith(item->value, "pointer:"))
+ item->value = "";
+ return 0;
+}
+
+#define parse_dict(prs,d) \
+do { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(d)->n_items), NULL) < 0) \
+ return -EINVAL; \
+ (d)->items = NULL; \
+ if ((d)->n_items > 0) { \
+ uint32_t i; \
+ if ((d)->n_items > MAX_DICT) \
+ return -ENOSPC; \
+ (d)->items = alloca((d)->n_items * sizeof(struct spa_dict_item)); \
+ for (i = 0; i < (d)->n_items; i++) { \
+ if (parse_item(prs, (struct spa_dict_item *) &(d)->items[i]) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+} while(0)
+
+#define parse_dict_struct(prs,f,dict) \
+do { \
+ if (spa_pod_parser_push_struct(prs, f) < 0) \
+ return -EINVAL; \
+ parse_dict(prs, dict); \
+ spa_pod_parser_pop(prs, f); \
+} while(0)
+
+static void push_params(struct spa_pod_builder *b, uint32_t n_params,
+ const struct spa_param_info *params)
+{
+ uint32_t i;
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_int(b, n_params);
+ for (i = 0; i < n_params; i++) {
+ spa_pod_builder_id(b, params[i].id);
+ spa_pod_builder_int(b, params[i].flags);
+ }
+ spa_pod_builder_pop(b, &f);
+}
+
+
+#define parse_params_struct(prs,f,params,n_params) \
+do { \
+ if (spa_pod_parser_push_struct(prs, f) < 0 || \
+ spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(n_params)), NULL) < 0) \
+ return -EINVAL; \
+ (params) = NULL; \
+ if ((n_params) > 0) { \
+ uint32_t i; \
+ if ((n_params) > MAX_PARAM_INFO) \
+ return -ENOSPC; \
+ (params) = alloca((n_params) * sizeof(struct spa_param_info)); \
+ for (i = 0; i < (n_params); i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Id(&(params)[i].id), \
+ SPA_POD_Int(&(params)[i].flags), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+ spa_pod_parser_pop(prs, f); \
+} while(0)
+
+
+#define parse_permissions_struct(prs,f,n_permissions,permissions) \
+do { \
+ if (spa_pod_parser_push_struct(prs, f) < 0 || \
+ spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(n_permissions)), NULL) < 0) \
+ return -EINVAL; \
+ (permissions) = NULL; \
+ if ((n_permissions) > 0) { \
+ uint32_t i; \
+ if ((n_permissions) > MAX_PERMISSIONS) \
+ return -ENOSPC; \
+ (permissions) = alloca((n_permissions) * sizeof(struct pw_permission)); \
+ for (i = 0; i < (n_permissions); i++) { \
+ if (spa_pod_parser_get(prs, \
+ SPA_POD_Int(&(permissions)[i].id), \
+ SPA_POD_Int(&(permissions)[i].permissions), NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+ spa_pod_parser_pop(prs, f); \
+} while(0)
+
+static void *
+core_method_marshal_create_object(void *object,
+ const char *factory_name,
+ const char *type, uint32_t version,
+ const struct spa_dict *props, size_t user_data_size)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ struct pw_proxy *res;
+ uint32_t new_id;
+
+ res = pw_proxy_new(object, type, version, user_data_size);
+ if (res == NULL)
+ return NULL;
+
+ new_id = pw_proxy_get_id(res);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_CREATE_OBJECT, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_String(factory_name),
+ SPA_POD_String(type),
+ SPA_POD_Int(version),
+ NULL);
+ push_dict(b, props);
+ spa_pod_builder_int(b, new_id);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_proxy(proxy, b);
+
+ return (void *)res;
+}
+
+static int
+core_method_marshal_destroy(void *object, void *p)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ uint32_t id = pw_proxy_get_id(p);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CORE_METHOD_DESTROY, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int core_event_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_core_info info = { .props = &props };
+ struct spa_pod_frame f[2];
+ struct spa_pod_parser prs;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0)
+ return -EINVAL;
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Int(&info.cookie),
+ SPA_POD_String(&info.user_name),
+ SPA_POD_String(&info.host_name),
+ SPA_POD_String(&info.version),
+ SPA_POD_String(&info.name),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_core_events, info, 0, &info);
+}
+
+static int core_event_demarshal_done(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ if (id == SPA_ID_INVALID)
+ return 0;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, done, 0, id, seq);
+}
+
+static int core_event_demarshal_ping(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, ping, 0, id, seq);
+}
+
+static int core_event_demarshal_error(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, res;
+ int seq;
+ const char *error;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq),
+ SPA_POD_Int(&res),
+ SPA_POD_String(&error)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, error, 0, id, seq, res, error);
+}
+
+static int core_event_demarshal_remove_id(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, remove_id, 0, id);
+}
+
+static int core_event_demarshal_bound_id(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, global_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&global_id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, bound_id, 0, id, global_id);
+}
+
+static int core_event_demarshal_add_mem(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, type, flags;
+ int64_t idx;
+ int fd;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Id(&type),
+ SPA_POD_Fd(&idx),
+ SPA_POD_Int(&flags)) < 0)
+ return -EINVAL;
+
+ fd = pw_protocol_native_get_proxy_fd(proxy, idx);
+
+ return pw_proxy_notify(proxy, struct pw_core_events, add_mem, 0, id, type, fd, flags);
+}
+
+static int core_event_demarshal_remove_mem(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_core_events, remove_mem, 0, id);
+}
+
+static void core_event_marshal_info(void *data, const struct pw_core_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->cookie),
+ SPA_POD_String(info->user_name),
+ SPA_POD_String(info->host_name),
+ SPA_POD_String(info->version),
+ SPA_POD_String(info->name),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_CORE_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_done(void *data, uint32_t id, int seq)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_DONE, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(seq));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_ping(void *data, uint32_t id, int seq)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct pw_protocol_native_message *msg;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_PING, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_error(void *data, uint32_t id, int seq, int res, const char *error)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_ERROR, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(seq),
+ SPA_POD_Int(res),
+ SPA_POD_String(error));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_remove_id(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_REMOVE_ID, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_bound_id(void *data, uint32_t id, uint32_t global_id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_BOUND_ID, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(global_id));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_add_mem(void *data, uint32_t id, uint32_t type, int fd, uint32_t flags)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_ADD_MEM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Id(type),
+ SPA_POD_Fd(pw_protocol_native_add_resource_fd(resource, fd)),
+ SPA_POD_Int(flags));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_event_marshal_remove_mem(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_EVENT_REMOVE_MEM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int core_method_demarshal_hello(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t version;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&version)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, hello, 0, version);
+}
+
+static int core_method_demarshal_sync(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, sync, 0, id, seq);
+}
+
+static int core_method_demarshal_pong(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, pong, 0, id, seq);
+}
+
+static int core_method_demarshal_error(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, res;
+ int seq;
+ const char *error;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&seq),
+ SPA_POD_Int(&res),
+ SPA_POD_String(&error)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, error, 0, id, seq, res, error);
+}
+
+static int core_method_demarshal_get_registry(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int32_t version, new_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&version),
+ SPA_POD_Int(&new_id)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, get_registry, 0, version, new_id);
+}
+
+static int core_method_demarshal_create_object(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t version, new_id;
+ const char *factory_name, *type;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_String(&factory_name),
+ SPA_POD_String(&type),
+ SPA_POD_Int(&version),
+ NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_Int(&new_id), NULL) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, create_object, 0, factory_name,
+ type, version,
+ &props, new_id);
+}
+
+static int core_method_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct pw_resource *r;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ pw_log_debug("client %p: destroy resource %u", client, id);
+
+ if ((r = pw_impl_client_find_resource(client, id)) == NULL)
+ goto no_resource;
+
+ return pw_resource_notify(resource, struct pw_core_methods, destroy, 0, r);
+
+ no_resource:
+ pw_log_debug("client %p: unknown resource %u op:%u", client, id, msg->opcode);
+ pw_resource_errorf(resource, -ENOENT, "unknown resource %d op:%u", id, msg->opcode);
+ return 0;
+}
+
+static int registry_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_registry_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void registry_marshal_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version, const struct spa_dict *props)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_EVENT_GLOBAL, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(permissions),
+ SPA_POD_String(type),
+ SPA_POD_Int(version),
+ NULL);
+ push_dict(b, props);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void registry_marshal_global_remove(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_EVENT_GLOBAL_REMOVE, NULL);
+
+ spa_pod_builder_add_struct(b, SPA_POD_Int(id));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int registry_demarshal_bind(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, version, new_id;
+ char *type;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_String(&type),
+ SPA_POD_Int(&version),
+ SPA_POD_Int(&new_id)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_registry_methods, bind, 0, id, type, version, new_id);
+}
+
+static int registry_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_registry_methods, destroy, 0, id);
+}
+
+static int module_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_module_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void module_marshal_info(void *data, const struct pw_module_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_MODULE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_String(info->name),
+ SPA_POD_String(info->filename),
+ SPA_POD_String(info->args),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_MODULE_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int module_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_module_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_String(&info.name),
+ SPA_POD_String(&info.filename),
+ SPA_POD_String(&info.args),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_module_events, info, 0, &info);
+}
+
+static int device_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_device_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void device_marshal_info(void *data, const struct pw_device_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_DEVICE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS ? info->props : NULL);
+ push_params(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_device_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+ parse_params_struct(&prs, &f[1], info.params, info.n_params);
+
+ return pw_proxy_notify(proxy, struct pw_device_events, info, 0, &info);
+}
+
+static void device_marshal_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_DEVICE_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int device_demarshal_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_device_events, param, 0,
+ seq, id, index, next, param);
+}
+
+static int device_marshal_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_DEVICE_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_subscribe_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_device_methods, subscribe_params, 0,
+ ids, n_ids);
+}
+
+static int device_marshal_enum_params(void *object, int seq,
+ uint32_t id, uint32_t index, uint32_t num, const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_DEVICE_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_device_methods, enum_params, 0,
+ seq, id, index, num, filter);
+}
+
+static int device_marshal_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_DEVICE_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int device_demarshal_set_param(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_device_methods, set_param, 0, id, flags, param);
+}
+
+static int factory_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_factory_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void factory_marshal_info(void *data, const struct pw_factory_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_FACTORY_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_String(info->name),
+ SPA_POD_String(info->type),
+ SPA_POD_Int(info->version),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_FACTORY_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int factory_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_factory_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_String(&info.name),
+ SPA_POD_String(&info.type),
+ SPA_POD_Int(&info.version),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_factory_events, info, 0, &info);
+}
+
+static int node_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_node_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void node_marshal_info(void *data, const struct pw_node_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_NODE_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->max_input_ports),
+ SPA_POD_Int(info->max_output_ports),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Int(info->n_input_ports),
+ SPA_POD_Int(info->n_output_ports),
+ SPA_POD_Id(info->state),
+ SPA_POD_String(info->error),
+ NULL);
+ push_dict(b, info->change_mask & PW_NODE_CHANGE_MASK_PROPS ? info->props : NULL);
+ push_params(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int node_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_node_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Int(&info.max_input_ports),
+ SPA_POD_Int(&info.max_output_ports),
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Int(&info.n_input_ports),
+ SPA_POD_Int(&info.n_output_ports),
+ SPA_POD_Id(&info.state),
+ SPA_POD_String(&info.error), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+ parse_params_struct(&prs, &f[1], info.params, info.n_params);
+
+ return pw_proxy_notify(proxy, struct pw_node_events, info, 0, &info);
+}
+
+static void node_marshal_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_NODE_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int node_demarshal_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_node_events, param, 0,
+ seq, id, index, next, param);
+}
+
+static int node_marshal_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int node_demarshal_subscribe_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, subscribe_params, 0,
+ ids, n_ids);
+}
+
+static int node_marshal_enum_params(void *object, int seq, uint32_t id,
+ uint32_t index, uint32_t num, const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int node_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, enum_params, 0,
+ seq, id, index, num, filter);
+}
+
+static int node_marshal_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int node_demarshal_set_param(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, set_param, 0, id, flags, param);
+}
+
+static int node_marshal_send_command(void *object, const struct spa_command *command)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_SEND_COMMAND, NULL);
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Pod(command));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int node_demarshal_send_command(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ const struct spa_command *command;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Pod(&command)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, send_command, 0, command);
+}
+
+static int port_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_port_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void port_marshal_info(void *data, const struct pw_port_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PORT_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->direction),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_PORT_CHANGE_MASK_PROPS ? info->props : NULL);
+ push_params(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int port_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_port_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Int(&info.direction),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+ parse_params_struct(&prs, &f[1], info.params, info.n_params);
+
+ return pw_proxy_notify(proxy, struct pw_port_events, info, 0, &info);
+}
+
+static void port_marshal_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PORT_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int port_demarshal_param(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_port_events, param, 0,
+ seq, id, index, next, param);
+}
+
+static int port_marshal_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_PORT_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int port_demarshal_subscribe_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_port_methods, subscribe_params, 0,
+ ids, n_ids);
+}
+
+static int port_marshal_enum_params(void *object, int seq, uint32_t id,
+ uint32_t index, uint32_t num, const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_PORT_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int port_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_port_methods, enum_params, 0,
+ seq, id, index, num, filter);
+}
+
+static int client_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_client_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void client_marshal_info(void *data, const struct pw_client_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_client_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Long(&info.change_mask), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_client_events, info, 0, &info);
+}
+
+static void client_marshal_permissions(void *data, uint32_t index, uint32_t n_permissions,
+ const struct pw_permission *permissions)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f[2];
+ uint32_t i, n = 0;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_EVENT_PERMISSIONS, NULL);
+
+ for (i = 0; i < n_permissions; i++) {
+ if (permissions[i].permissions != PW_PERM_INVALID)
+ n++;
+ }
+
+ spa_pod_builder_push_struct(b, &f[0]);
+ spa_pod_builder_int(b, index);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, n);
+
+ for (i = 0; i < n_permissions; i++) {
+ if (permissions[i].permissions == PW_PERM_INVALID)
+ continue;
+ spa_pod_builder_int(b, permissions[i].id);
+ spa_pod_builder_int(b, permissions[i].permissions);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ spa_pod_builder_pop(b, &f[0]);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_demarshal_permissions(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct pw_permission *permissions;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t index, n_permissions;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&index), NULL) < 0)
+ return -EINVAL;
+
+ parse_permissions_struct(&prs, &f[1], n_permissions, permissions);
+
+ return pw_proxy_notify(proxy, struct pw_client_events, permissions, 0, index, n_permissions, permissions);
+}
+
+static int client_marshal_error(void *object, uint32_t id, int res, const char *error)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_METHOD_ERROR, NULL);
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_Int(res),
+ SPA_POD_String(error));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_demarshal_error(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, res;
+ const char *error;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&res),
+ SPA_POD_String(&error)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_methods, error, 0, id, res, error);
+}
+
+static int client_marshal_get_permissions(void *object, uint32_t index, uint32_t num)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_METHOD_GET_PERMISSIONS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(index),
+ SPA_POD_Int(num));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_marshal_update_properties(void *object, const struct spa_dict *props)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_METHOD_UPDATE_PROPERTIES, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ push_dict(b, props);
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_demarshal_update_properties(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_resource_notify(resource, struct pw_client_methods, update_properties, 0,
+ &props);
+}
+
+static int client_demarshal_get_permissions(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t index, num;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_client_methods, get_permissions, 0, index, num);
+}
+
+static int client_marshal_update_permissions(void *object, uint32_t n_permissions,
+ const struct pw_permission *permissions)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_CLIENT_METHOD_UPDATE_PERMISSIONS, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_int(b, n_permissions);
+ for (i = 0; i < n_permissions; i++) {
+ spa_pod_builder_int(b, permissions[i].id);
+ spa_pod_builder_int(b, permissions[i].permissions);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_demarshal_update_permissions(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_permission *permissions;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[1];
+ uint32_t n_permissions;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ parse_permissions_struct(&prs, &f[0], n_permissions, permissions);
+
+ return pw_resource_notify(resource, struct pw_client_methods, update_permissions, 0,
+ n_permissions, permissions);
+}
+
+static int link_method_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_link_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static void link_marshal_info(void *data, const struct pw_link_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_LINK_EVENT_INFO, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->output_node_id),
+ SPA_POD_Int(info->output_port_id),
+ SPA_POD_Int(info->input_node_id),
+ SPA_POD_Int(info->input_port_id),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Int(info->state),
+ SPA_POD_String(info->error),
+ SPA_POD_Pod(info->format),
+ NULL);
+ push_dict(b, info->change_mask & PW_LINK_CHANGE_MASK_PROPS ? info->props : NULL);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int link_demarshal_info(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_link_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&info.id),
+ SPA_POD_Int(&info.output_node_id),
+ SPA_POD_Int(&info.output_port_id),
+ SPA_POD_Int(&info.input_node_id),
+ SPA_POD_Int(&info.input_port_id),
+ SPA_POD_Long(&info.change_mask),
+ SPA_POD_Int(&info.state),
+ SPA_POD_String(&info.error),
+ SPA_POD_Pod(&info.format), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_link_events, info, 0, &info);
+}
+
+static int registry_demarshal_global(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[2];
+ uint32_t id, permissions, version;
+ char *type;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get(&prs,
+ SPA_POD_Int(&id),
+ SPA_POD_Int(&permissions),
+ SPA_POD_String(&type),
+ SPA_POD_Int(&version), NULL) < 0)
+ return -EINVAL;
+
+ parse_dict_struct(&prs, &f[1], &props);
+
+ return pw_proxy_notify(proxy, struct pw_registry_events,
+ global, 0, id, permissions, type, version, &props);
+}
+
+static int registry_demarshal_global_remove(void *data, const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_registry_events, global_remove, 0, id);
+}
+
+static void * registry_marshal_bind(void *object, uint32_t id,
+ const char *type, uint32_t version, size_t user_data_size)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct pw_proxy *res;
+ uint32_t new_id;
+
+ res = pw_proxy_new(object, type, version, user_data_size);
+ if (res == NULL)
+ return NULL;
+
+ new_id = pw_proxy_get_id(res);
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_REGISTRY_METHOD_BIND, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id),
+ SPA_POD_String(type),
+ SPA_POD_Int(version),
+ SPA_POD_Int(new_id));
+
+ pw_protocol_native_end_proxy(proxy, b);
+
+ return (void *) res;
+}
+
+static int registry_marshal_destroy(void *object, uint32_t id)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy, PW_REGISTRY_METHOD_DESTROY, NULL);
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static const struct pw_core_methods pw_protocol_native_core_method_marshal = {
+ PW_VERSION_CORE_METHODS,
+ .add_listener = &core_method_marshal_add_listener,
+ .hello = &core_method_marshal_hello,
+ .sync = &core_method_marshal_sync,
+ .pong = &core_method_marshal_pong,
+ .error = &core_method_marshal_error,
+ .get_registry = &core_method_marshal_get_registry,
+ .create_object = &core_method_marshal_create_object,
+ .destroy = &core_method_marshal_destroy,
+};
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_core_method_demarshal[PW_CORE_METHOD_NUM] = {
+ [PW_CORE_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_CORE_METHOD_HELLO] = { &core_method_demarshal_hello, 0, },
+ [PW_CORE_METHOD_SYNC] = { &core_method_demarshal_sync, 0, },
+ [PW_CORE_METHOD_PONG] = { &core_method_demarshal_pong, 0, },
+ [PW_CORE_METHOD_ERROR] = { &core_method_demarshal_error, 0, },
+ [PW_CORE_METHOD_GET_REGISTRY] = { &core_method_demarshal_get_registry, 0, },
+ [PW_CORE_METHOD_CREATE_OBJECT] = { &core_method_demarshal_create_object, 0, },
+ [PW_CORE_METHOD_DESTROY] = { &core_method_demarshal_destroy, 0, }
+};
+
+static const struct pw_core_events pw_protocol_native_core_event_marshal = {
+ PW_VERSION_CORE_EVENTS,
+ .info = &core_event_marshal_info,
+ .done = &core_event_marshal_done,
+ .ping = &core_event_marshal_ping,
+ .error = &core_event_marshal_error,
+ .remove_id = &core_event_marshal_remove_id,
+ .bound_id = &core_event_marshal_bound_id,
+ .add_mem = &core_event_marshal_add_mem,
+ .remove_mem = &core_event_marshal_remove_mem,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_core_event_demarshal[PW_CORE_EVENT_NUM] =
+{
+ [PW_CORE_EVENT_INFO] = { &core_event_demarshal_info, 0, },
+ [PW_CORE_EVENT_DONE] = { &core_event_demarshal_done, 0, },
+ [PW_CORE_EVENT_PING] = { &core_event_demarshal_ping, 0, },
+ [PW_CORE_EVENT_ERROR] = { &core_event_demarshal_error, 0, },
+ [PW_CORE_EVENT_REMOVE_ID] = { &core_event_demarshal_remove_id, 0, },
+ [PW_CORE_EVENT_BOUND_ID] = { &core_event_demarshal_bound_id, 0, },
+ [PW_CORE_EVENT_ADD_MEM] = { &core_event_demarshal_add_mem, 0, },
+ [PW_CORE_EVENT_REMOVE_MEM] = { &core_event_demarshal_remove_mem, 0, },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_core_marshal = {
+ PW_TYPE_INTERFACE_Core,
+ PW_VERSION_CORE,
+ 0,
+ PW_CORE_METHOD_NUM,
+ PW_CORE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_core_method_marshal,
+ .server_demarshal = pw_protocol_native_core_method_demarshal,
+ .server_marshal = &pw_protocol_native_core_event_marshal,
+ .client_demarshal = pw_protocol_native_core_event_demarshal,
+};
+
+static const struct pw_registry_methods pw_protocol_native_registry_method_marshal = {
+ PW_VERSION_REGISTRY_METHODS,
+ .add_listener = &registry_method_marshal_add_listener,
+ .bind = &registry_marshal_bind,
+ .destroy = &registry_marshal_destroy,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_registry_method_demarshal[PW_REGISTRY_METHOD_NUM] =
+{
+ [PW_REGISTRY_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_REGISTRY_METHOD_BIND] = { &registry_demarshal_bind, 0, },
+ [PW_REGISTRY_METHOD_DESTROY] = { &registry_demarshal_destroy, 0, },
+};
+
+static const struct pw_registry_events pw_protocol_native_registry_event_marshal = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = &registry_marshal_global,
+ .global_remove = &registry_marshal_global_remove,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_registry_event_demarshal[PW_REGISTRY_EVENT_NUM] =
+{
+ [PW_REGISTRY_EVENT_GLOBAL] = { &registry_demarshal_global, 0, },
+ [PW_REGISTRY_EVENT_GLOBAL_REMOVE] = { &registry_demarshal_global_remove, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_registry_marshal = {
+ PW_TYPE_INTERFACE_Registry,
+ PW_VERSION_REGISTRY,
+ 0,
+ PW_REGISTRY_METHOD_NUM,
+ PW_REGISTRY_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_registry_method_marshal,
+ .server_demarshal = pw_protocol_native_registry_method_demarshal,
+ .server_marshal = &pw_protocol_native_registry_event_marshal,
+ .client_demarshal = pw_protocol_native_registry_event_demarshal,
+};
+
+static const struct pw_module_events pw_protocol_native_module_event_marshal = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = &module_marshal_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_module_event_demarshal[PW_MODULE_EVENT_NUM] =
+{
+ [PW_MODULE_EVENT_INFO] = { &module_demarshal_info, 0, },
+};
+
+
+static const struct pw_module_methods pw_protocol_native_module_method_marshal = {
+ PW_VERSION_MODULE_METHODS,
+ .add_listener = &module_method_marshal_add_listener,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_module_method_demarshal[PW_MODULE_METHOD_NUM] =
+{
+ [PW_MODULE_METHOD_ADD_LISTENER] = { NULL, 0, },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_module_marshal = {
+ PW_TYPE_INTERFACE_Module,
+ PW_VERSION_MODULE,
+ 0,
+ PW_MODULE_METHOD_NUM,
+ PW_MODULE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_module_method_marshal,
+ .server_demarshal = pw_protocol_native_module_method_demarshal,
+ .server_marshal = &pw_protocol_native_module_event_marshal,
+ .client_demarshal = pw_protocol_native_module_event_demarshal,
+};
+
+static const struct pw_factory_events pw_protocol_native_factory_event_marshal = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = &factory_marshal_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_factory_event_demarshal[PW_FACTORY_EVENT_NUM] =
+{
+ [PW_FACTORY_EVENT_INFO] = { &factory_demarshal_info, 0, },
+};
+
+static const struct pw_factory_methods pw_protocol_native_factory_method_marshal = {
+ PW_VERSION_FACTORY_METHODS,
+ .add_listener = &factory_method_marshal_add_listener,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_factory_method_demarshal[PW_FACTORY_METHOD_NUM] =
+{
+ [PW_FACTORY_METHOD_ADD_LISTENER] = { NULL, 0, },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_factory_marshal = {
+ PW_TYPE_INTERFACE_Factory,
+ PW_VERSION_FACTORY,
+ 0,
+ PW_FACTORY_METHOD_NUM,
+ PW_FACTORY_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_factory_method_marshal,
+ .server_demarshal = pw_protocol_native_factory_method_demarshal,
+ .server_marshal = &pw_protocol_native_factory_event_marshal,
+ .client_demarshal = pw_protocol_native_factory_event_demarshal,
+};
+
+static const struct pw_device_methods pw_protocol_native_device_method_marshal = {
+ PW_VERSION_DEVICE_METHODS,
+ .add_listener = &device_method_marshal_add_listener,
+ .subscribe_params = &device_marshal_subscribe_params,
+ .enum_params = &device_marshal_enum_params,
+ .set_param = &device_marshal_set_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_device_method_demarshal[PW_DEVICE_METHOD_NUM] = {
+ [PW_DEVICE_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_DEVICE_METHOD_SUBSCRIBE_PARAMS] = { &device_demarshal_subscribe_params, 0, },
+ [PW_DEVICE_METHOD_ENUM_PARAMS] = { &device_demarshal_enum_params, 0, },
+ [PW_DEVICE_METHOD_SET_PARAM] = { &device_demarshal_set_param, PW_PERM_W, },
+};
+
+static const struct pw_device_events pw_protocol_native_device_event_marshal = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = &device_marshal_info,
+ .param = &device_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_device_event_demarshal[PW_DEVICE_EVENT_NUM] = {
+ [PW_DEVICE_EVENT_INFO] = { &device_demarshal_info, 0, },
+ [PW_DEVICE_EVENT_PARAM] = { &device_demarshal_param, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_device_marshal = {
+ PW_TYPE_INTERFACE_Device,
+ PW_VERSION_DEVICE,
+ 0,
+ PW_DEVICE_METHOD_NUM,
+ PW_DEVICE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_device_method_marshal,
+ .server_demarshal = pw_protocol_native_device_method_demarshal,
+ .server_marshal = &pw_protocol_native_device_event_marshal,
+ .client_demarshal = pw_protocol_native_device_event_demarshal,
+};
+
+static const struct pw_node_methods pw_protocol_native_node_method_marshal = {
+ PW_VERSION_NODE_METHODS,
+ .add_listener = &node_method_marshal_add_listener,
+ .subscribe_params = &node_marshal_subscribe_params,
+ .enum_params = &node_marshal_enum_params,
+ .set_param = &node_marshal_set_param,
+ .send_command = &node_marshal_send_command,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_node_method_demarshal[PW_NODE_METHOD_NUM] =
+{
+ [PW_NODE_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_NODE_METHOD_SUBSCRIBE_PARAMS] = { &node_demarshal_subscribe_params, 0, },
+ [PW_NODE_METHOD_ENUM_PARAMS] = { &node_demarshal_enum_params, 0, },
+ [PW_NODE_METHOD_SET_PARAM] = { &node_demarshal_set_param, PW_PERM_W, },
+ [PW_NODE_METHOD_SEND_COMMAND] = { &node_demarshal_send_command, PW_PERM_W, },
+};
+
+static const struct pw_node_events pw_protocol_native_node_event_marshal = {
+ PW_VERSION_NODE_EVENTS,
+ .info = &node_marshal_info,
+ .param = &node_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_node_event_demarshal[PW_NODE_EVENT_NUM] = {
+ [PW_NODE_EVENT_INFO] = { &node_demarshal_info, 0, },
+ [PW_NODE_EVENT_PARAM] = { &node_demarshal_param, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_node_marshal = {
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ 0,
+ PW_NODE_METHOD_NUM,
+ PW_NODE_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_node_method_marshal,
+ .server_demarshal = pw_protocol_native_node_method_demarshal,
+ .server_marshal = &pw_protocol_native_node_event_marshal,
+ .client_demarshal = pw_protocol_native_node_event_demarshal,
+};
+
+
+static const struct pw_port_methods pw_protocol_native_port_method_marshal = {
+ PW_VERSION_PORT_METHODS,
+ .add_listener = &port_method_marshal_add_listener,
+ .subscribe_params = &port_marshal_subscribe_params,
+ .enum_params = &port_marshal_enum_params,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_port_method_demarshal[PW_PORT_METHOD_NUM] =
+{
+ [PW_PORT_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_PORT_METHOD_SUBSCRIBE_PARAMS] = { &port_demarshal_subscribe_params, 0, },
+ [PW_PORT_METHOD_ENUM_PARAMS] = { &port_demarshal_enum_params, 0, },
+};
+
+static const struct pw_port_events pw_protocol_native_port_event_marshal = {
+ PW_VERSION_PORT_EVENTS,
+ .info = &port_marshal_info,
+ .param = &port_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_port_event_demarshal[PW_PORT_EVENT_NUM] =
+{
+ [PW_PORT_EVENT_INFO] = { &port_demarshal_info, 0, },
+ [PW_PORT_EVENT_PARAM] = { &port_demarshal_param, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_port_marshal = {
+ PW_TYPE_INTERFACE_Port,
+ PW_VERSION_PORT,
+ 0,
+ PW_PORT_METHOD_NUM,
+ PW_PORT_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_port_method_marshal,
+ .server_demarshal = pw_protocol_native_port_method_demarshal,
+ .server_marshal = &pw_protocol_native_port_event_marshal,
+ .client_demarshal = pw_protocol_native_port_event_demarshal,
+};
+
+static const struct pw_client_methods pw_protocol_native_client_method_marshal = {
+ PW_VERSION_CLIENT_METHODS,
+ .add_listener = &client_method_marshal_add_listener,
+ .error = &client_marshal_error,
+ .update_properties = &client_marshal_update_properties,
+ .get_permissions = &client_marshal_get_permissions,
+ .update_permissions = &client_marshal_update_permissions,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_method_demarshal[PW_CLIENT_METHOD_NUM] =
+{
+ [PW_CLIENT_METHOD_ADD_LISTENER] = { NULL, 0, },
+ [PW_CLIENT_METHOD_ERROR] = { &client_demarshal_error, PW_PERM_W, },
+ [PW_CLIENT_METHOD_UPDATE_PROPERTIES] = { &client_demarshal_update_properties, PW_PERM_W, },
+ [PW_CLIENT_METHOD_GET_PERMISSIONS] = { &client_demarshal_get_permissions, 0, },
+ [PW_CLIENT_METHOD_UPDATE_PERMISSIONS] = { &client_demarshal_update_permissions, PW_PERM_W, },
+};
+
+static const struct pw_client_events pw_protocol_native_client_event_marshal = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = &client_marshal_info,
+ .permissions = &client_marshal_permissions,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_event_demarshal[PW_CLIENT_EVENT_NUM] =
+{
+ [PW_CLIENT_EVENT_INFO] = { &client_demarshal_info, 0, },
+ [PW_CLIENT_EVENT_PERMISSIONS] = { &client_demarshal_permissions, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_marshal = {
+ PW_TYPE_INTERFACE_Client,
+ PW_VERSION_CLIENT,
+ 0,
+ PW_CLIENT_METHOD_NUM,
+ PW_CLIENT_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_client_method_marshal,
+ .server_demarshal = pw_protocol_native_client_method_demarshal,
+ .server_marshal = &pw_protocol_native_client_event_marshal,
+ .client_demarshal = pw_protocol_native_client_event_demarshal,
+};
+
+
+static const struct pw_link_methods pw_protocol_native_link_method_marshal = {
+ PW_VERSION_LINK_METHODS,
+ .add_listener = &link_method_marshal_add_listener,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_link_method_demarshal[PW_LINK_METHOD_NUM] =
+{
+ [PW_LINK_METHOD_ADD_LISTENER] = { NULL, 0, },
+};
+
+static const struct pw_link_events pw_protocol_native_link_event_marshal = {
+ PW_VERSION_LINK_EVENTS,
+ .info = &link_marshal_info,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_link_event_demarshal[PW_LINK_EVENT_NUM] =
+{
+ [PW_LINK_EVENT_INFO] = { &link_demarshal_info, 0, }
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_link_marshal = {
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ 0,
+ PW_LINK_METHOD_NUM,
+ PW_LINK_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_link_method_marshal,
+ .server_demarshal = pw_protocol_native_link_method_demarshal,
+ .server_marshal = &pw_protocol_native_link_event_marshal,
+ .client_demarshal = pw_protocol_native_link_event_demarshal,
+};
+
+void pw_protocol_native_init(struct pw_protocol *protocol)
+{
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_core_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_registry_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_module_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_device_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_node_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_port_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_factory_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_link_marshal);
+}
diff --git a/src/modules/module-protocol-native/test-connection.c b/src/modules/module-protocol-native/test-connection.c
new file mode 100644
index 0000000..c7d2f69
--- /dev/null
+++ b/src/modules/module-protocol-native/test-connection.c
@@ -0,0 +1,225 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <sys/socket.h>
+
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/pipewire.h>
+
+#include "connection.h"
+
+#define NAME "protocol-native"
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+PW_LOG_TOPIC(mod_topic_connection, "conn." NAME);
+
+static void test_create(struct pw_protocol_native_connection *conn)
+{
+ const struct pw_protocol_native_message *msg;
+ int res;
+
+ res = pw_protocol_native_connection_get_next(conn, &msg);
+ spa_assert_se(res != 1);
+
+ res = pw_protocol_native_connection_get_fd(conn, 0);
+ spa_assert_se(res == -ENOENT);
+
+ res = pw_protocol_native_connection_flush(conn);
+ spa_assert_se(res == 0);
+
+ res = pw_protocol_native_connection_clear(conn);
+ spa_assert_se(res == 0);
+}
+
+static void write_message(struct pw_protocol_native_connection *conn, int fd)
+{
+ struct pw_protocol_native_message *msg;
+ struct spa_pod_builder *b;
+ int seq = -1, res;
+
+ b = pw_protocol_native_connection_begin(conn, 1, 5, &msg);
+ spa_assert_se(b != NULL);
+ spa_assert_se(msg->seq != -1);
+
+ seq = SPA_RESULT_RETURN_ASYNC(msg->seq);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(42),
+ SPA_POD_Id(SPA_TYPE_Object),
+ SPA_POD_Int(pw_protocol_native_connection_add_fd(conn, fd)));
+
+ res = pw_protocol_native_connection_end(conn, b);
+ spa_assert_se(seq == res);
+}
+
+static int read_message(struct pw_protocol_native_connection *conn,
+ const struct pw_protocol_native_message **pmsg)
+{
+ struct spa_pod_parser prs;
+ const struct pw_protocol_native_message *msg;
+ int res, fd;
+ uint32_t v_int, v_id, fdidx;
+
+ res = pw_protocol_native_connection_get_next(conn, &msg);
+ if (res != 1) {
+ pw_log_error("got %d", res);
+ return -1;
+ }
+
+ if (pmsg)
+ *pmsg = msg;
+
+ spa_assert_se(msg->opcode == 5);
+ spa_assert_se(msg->id == 1);
+ spa_assert_se(msg->data != NULL);
+ spa_assert_se(msg->size > 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&v_int),
+ SPA_POD_Id(&v_id),
+ SPA_POD_Int(&fdidx)) < 0)
+ spa_assert_not_reached();
+
+ fd = pw_protocol_native_connection_get_fd(conn, fdidx);
+ spa_assert_se(fd != -ENOENT);
+ pw_log_debug("got fd %d %d", fdidx, fd);
+ return 0;
+}
+
+static void test_read_write(struct pw_protocol_native_connection *in,
+ struct pw_protocol_native_connection *out)
+{
+ write_message(out, 1);
+ pw_protocol_native_connection_flush(out);
+ write_message(out, 2);
+ pw_protocol_native_connection_flush(out);
+ spa_assert_se(read_message(in, NULL) == 0);
+ spa_assert_se(read_message(in, NULL) == 0);
+ spa_assert_se(read_message(in, NULL) == -1);
+
+ write_message(out, 1);
+ write_message(out, 2);
+ pw_protocol_native_connection_flush(out);
+ spa_assert_se(read_message(in, NULL) == 0);
+ spa_assert_se(read_message(in, NULL) == 0);
+ spa_assert_se(read_message(in, NULL) == -1);
+}
+
+static void test_reentering(struct pw_protocol_native_connection *in,
+ struct pw_protocol_native_connection *out)
+{
+ const struct pw_protocol_native_message *msg1, *msg2;
+ int i;
+
+#define READ_MSG(idx) \
+ spa_assert_se(read_message(in, &msg ## idx) == 0); \
+ spa_assert_se((msg ## idx)->n_fds == 1); \
+ spa_assert_se((msg ## idx)->size < sizeof(buf ## idx)); \
+ fd ## idx = (msg ## idx)->fds[0]; \
+ memcpy(buf ## idx, (msg ## idx)->data, (msg ## idx)->size); \
+ size ## idx = (msg ## idx)->size
+
+#define CHECK_MSG(idx) \
+ spa_assert_se((msg ## idx)->fds[0] == fd ## idx); \
+ spa_assert_se(memcmp((msg ## idx)->data, buf ## idx, size ## idx) == 0)
+
+ for (i = 0; i < 50; ++i) {
+ int fd1, fd2;
+ char buf1[1024], buf2[1024];
+ int size1, size2;
+
+ write_message(out, 1);
+ write_message(out, 2);
+ write_message(out, 1);
+ write_message(out, 2);
+ write_message(out, 1);
+ pw_protocol_native_connection_flush(out);
+
+ READ_MSG(1);
+ pw_protocol_native_connection_enter(in); /* 1 */
+ READ_MSG(2);
+ CHECK_MSG(1);
+ pw_protocol_native_connection_enter(in); /* 2 */
+ pw_protocol_native_connection_leave(in); /* 2 */
+ CHECK_MSG(1);
+ CHECK_MSG(2);
+ pw_protocol_native_connection_enter(in); /* 2 */
+ pw_protocol_native_connection_enter(in); /* 3 */
+ spa_assert_se(read_message(in, NULL) == 0);
+ CHECK_MSG(1);
+ CHECK_MSG(2);
+ pw_protocol_native_connection_leave(in); /* 3 */
+ spa_assert_se(read_message(in, NULL) == 0);
+ CHECK_MSG(1);
+ CHECK_MSG(2);
+ pw_protocol_native_connection_leave(in); /* 2 */
+ CHECK_MSG(2);
+ spa_assert_se(read_message(in, NULL) == 0);
+ CHECK_MSG(1);
+ pw_protocol_native_connection_leave(in); /* 1 */
+ CHECK_MSG(1);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_protocol_native_connection *in, *out;
+ int fds[2];
+
+ pw_init(&argc, &argv);
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+ PW_LOG_TOPIC_INIT(mod_topic_connection);
+
+ loop = pw_main_loop_new(NULL);
+ spa_assert_se(loop != NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 0);
+
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) {
+ spa_assert_not_reached();
+ return -1;
+ }
+
+ in = pw_protocol_native_connection_new(context, fds[0]);
+ spa_assert_se(in != NULL);
+ out = pw_protocol_native_connection_new(context, fds[1]);
+ spa_assert_se(out != NULL);
+
+ test_create(in);
+ test_create(out);
+ test_read_write(in, out);
+ test_reentering(in, out);
+
+ pw_protocol_native_connection_destroy(in);
+ pw_protocol_native_connection_destroy(out);
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+
+ return 0;
+}
diff --git a/src/modules/module-protocol-native/v0/interfaces.h b/src/modules/module-protocol-native/v0/interfaces.h
new file mode 100644
index 0000000..dca9672
--- /dev/null
+++ b/src/modules/module-protocol-native/v0/interfaces.h
@@ -0,0 +1,534 @@
+/* PipeWire
+ *
+ * Copyright © 2016 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_INTERFACES_V0_H
+#define PIPEWIRE_INTERFACES_V0_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/param/param.h>
+#include <spa/node/node.h>
+
+#include <pipewire/pipewire.h>
+
+/** Core */
+
+#define PW_VERSION_CORE_V0 0
+
+#define PW_CORE_V0_METHOD_HELLO 0
+#define PW_CORE_V0_METHOD_UPDATE_TYPES 1
+#define PW_CORE_V0_METHOD_SYNC 2
+#define PW_CORE_V0_METHOD_GET_REGISTRY 3
+#define PW_CORE_V0_METHOD_CLIENT_UPDATE 4
+#define PW_CORE_V0_METHOD_PERMISSIONS 5
+#define PW_CORE_V0_METHOD_CREATE_OBJECT 6
+#define PW_CORE_V0_METHOD_DESTROY 7
+#define PW_CORE_V0_METHOD_NUM 8
+
+/**
+ * Key to update default permissions of globals without specific
+ * permissions. value is "[r][w][x]" */
+#define PW_CORE_PERMISSIONS_DEFAULT "permissions.default"
+
+/**
+ * Key to update specific permissions of a global. If the global
+ * did not have specific permissions, it will first be assigned
+ * the default permissions before it is updated.
+ * Value is "<global-id>:[r][w][x]"*/
+#define PW_CORE_PERMISSIONS_GLOBAL "permissions.global"
+
+/**
+ * Key to update specific permissions of all existing globals.
+ * This is equivalent to using \ref PW_CORE_PERMISSIONS_GLOBAL
+ * on each global id individually that did not have specific
+ * permissions.
+ * Value is "[r][w][x]" */
+#define PW_CORE_PERMISSIONS_EXISTING "permissions.existing"
+
+#define PW_LINK_OUTPUT_NODE_ID "link.output_node.id"
+#define PW_LINK_OUTPUT_PORT_ID "link.output_port.id"
+#define PW_LINK_INPUT_NODE_ID "link.input_node.id"
+#define PW_LINK_INPUT_PORT_ID "link.input_port.id"
+
+/**
+ * \struct pw_core_v0_methods
+ * \brief Core methods
+ *
+ * The core global object. This is a singleton object used for
+ * creating new objects in the remote PipeWire instance. It is
+ * also used for internal features.
+ */
+struct pw_core_v0_methods {
+#define PW_VERSION_CORE_V0_METHODS 0
+ uint32_t version;
+ /**
+ * Start a conversation with the server. This will send
+ * the core info and server types.
+ *
+ * All the existing resources for the client (except the core
+ * resource) will be destroyed.
+ */
+ void (*hello) (void *object);
+ /**
+ * Update the type map
+ *
+ * Send a type map update to the PipeWire server. The server uses this
+ * information to keep a mapping between client types and the server types.
+ * \param first_id the id of the first type
+ * \param types the types as a string
+ * \param n_types the number of types
+ */
+ void (*update_types) (void *object,
+ uint32_t first_id,
+ const char **types,
+ uint32_t n_types);
+ /**
+ * Do server roundtrip
+ *
+ * Ask the server to emit the 'done' event with \a id.
+ * Since methods are handled in-order and events are delivered
+ * in-order, this can be used as a barrier to ensure all previous
+ * methods and the resulting events have been handled.
+ * \param seq the sequence number passed to the done event
+ */
+ void (*sync) (void *object, uint32_t seq);
+ /**
+ * Get the registry object
+ *
+ * Create a registry object that allows the client to list and bind
+ * the global objects available from the PipeWire server
+ * \param version the client proxy id
+ * \param id the client proxy id
+ */
+ void (*get_registry) (void *object, uint32_t version, uint32_t new_id);
+ /**
+ * Update the client properties
+ * \param props the new client properties
+ */
+ void (*client_update) (void *object, const struct spa_dict *props);
+ /**
+ * Manage the permissions of the global objects
+ *
+ * Update the permissions of the global objects using the
+ * dictionary with properties.
+ *
+ * Globals can use the default permissions or can have specific
+ * permissions assigned to them.
+ *
+ * \param id the global id to change
+ * \param props dictionary with permission properties
+ */
+ void (*permissions) (void *object, const struct spa_dict *props);
+ /**
+ * Create a new object on the PipeWire server from a factory.
+ * Use a \a factory_name of "client-node" to create a
+ * \ref pw_client_node.
+ *
+ * \param factory_name the factory name to use
+ * \param type the interface to bind to
+ * \param version the version of the interface
+ * \param props extra properties
+ * \param new_id the client proxy id
+ */
+ void (*create_object) (void *object,
+ const char *factory_name,
+ uint32_t type,
+ uint32_t version,
+ const struct spa_dict *props,
+ uint32_t new_id);
+
+ /**
+ * Destroy an object id
+ *
+ * \param id the object id to destroy
+ */
+ void (*destroy) (void *object, uint32_t id);
+};
+
+#define PW_CORE_V0_EVENT_UPDATE_TYPES 0
+#define PW_CORE_V0_EVENT_DONE 1
+#define PW_CORE_V0_EVENT_ERROR 2
+#define PW_CORE_V0_EVENT_REMOVE_ID 3
+#define PW_CORE_V0_EVENT_INFO 4
+#define PW_CORE_V0_EVENT_NUM 5
+
+/** \struct pw_core_v0_events
+ * \brief Core events
+ * \ingroup pw_core_interface The pw_core interface
+ */
+struct pw_core_v0_events {
+#define PW_VERSION_CORE_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Update the type map
+ *
+ * Send a type map update to the client. The client uses this
+ * information to keep a mapping between server types and the client types.
+ * \param first_id the id of the first type
+ * \param types the types as a string
+ * \param n_types the number of \a types
+ */
+ void (*update_types) (void *data,
+ uint32_t first_id,
+ const char **types,
+ uint32_t n_types);
+ /**
+ * Emit a done event
+ *
+ * The done event is emitted as a result of a sync method with the
+ * same sequence number.
+ * \param seq the sequence number passed to the sync method call
+ */
+ void (*done) (void *data, uint32_t seq);
+ /**
+ * Fatal error event
+ *
+ * The error event is sent out when a fatal (non-recoverable)
+ * error has occurred. The id argument is the object where
+ * the error occurred, most often in response to a request to that
+ * object. The message is a brief description of the error,
+ * for (debugging) convenience.
+ * \param id object where the error occurred
+ * \param res error code
+ * \param error error description
+ */
+ void (*error) (void *data, uint32_t id, int res, const char *error, ...);
+ /**
+ * Remove an object ID
+ *
+ * This event is used internally by the object ID management
+ * logic. When a client deletes an object, the server will send
+ * this event to acknowledge that it has seen the delete request.
+ * When the client receives this event, it will know that it can
+ * safely reuse the object ID.
+ * \param id deleted object ID
+ */
+ void (*remove_id) (void *data, uint32_t id);
+ /**
+ * Notify new core info
+ *
+ * \param info new core info
+ */
+ void (*info) (void *data, struct pw_core_info *info);
+};
+
+#define pw_core_resource_v0_update_types(r,...) pw_resource_notify(r,struct pw_core_v0_events,update_types,__VA_ARGS__)
+#define pw_core_resource_v0_done(r,...) pw_resource_notify(r,struct pw_core_v0_events,done,__VA_ARGS__)
+#define pw_core_resource_v0_error(r,...) pw_resource_notify(r,struct pw_core_v0_events,error,__VA_ARGS__)
+#define pw_core_resource_v0_remove_id(r,...) pw_resource_notify(r,struct pw_core_v0_events,remove_id,__VA_ARGS__)
+#define pw_core_resource_v0_info(r,...) pw_resource_notify(r,struct pw_core_v0_events,info,__VA_ARGS__)
+
+
+#define PW_VERSION_REGISTRY_V0 0
+
+/** \page page_registry Registry
+ *
+ * \section page_registry_overview Overview
+ *
+ * The registry object is a singleton object that keeps track of
+ * global objects on the PipeWire instance. See also \ref page_global.
+ *
+ * Global objects typically represent an actual object in PipeWire
+ * (for example, a module or node) or they are singleton
+ * objects such as the core.
+ *
+ * When a client creates a registry object, the registry object
+ * will emit a global event for each global currently in the
+ * registry. Globals come and go as a result of device hotplugs or
+ * reconfiguration or other events, and the registry will send out
+ * global and global_remove events to keep the client up to date
+ * with the changes. To mark the end of the initial burst of
+ * events, the client can use the pw_core.sync methosd immediately
+ * after calling pw_core.get_registry.
+ *
+ * A client can bind to a global object by using the bind
+ * request. This creates a client-side proxy that lets the object
+ * emit events to the client and lets the client invoke methods on
+ * the object. See \ref page_proxy
+ *
+ * Clients can also change the permissions of the global objects that
+ * it can see. This is interesting when you want to configure a
+ * pipewire session before handing it to another application. You
+ * can, for example, hide certain existing or new objects or limit
+ * the access permissions on an object.
+ */
+#define PW_REGISTRY_V0_METHOD_BIND 0
+#define PW_REGISTRY_V0_METHOD_NUM 1
+
+/** Registry methods */
+struct pw_registry_v0_methods {
+#define PW_VERSION_REGISTRY_V0_METHODS 0
+ uint32_t version;
+ /**
+ * Bind to a global object
+ *
+ * Bind to the global object with \a id and use the client proxy
+ * with new_id as the proxy. After this call, methods can be
+ * send to the remote global object and events can be received
+ *
+ * \param id the global id to bind to
+ * \param type the interface type to bind to
+ * \param version the interface version to use
+ * \param new_id the client proxy to use
+ */
+ void (*bind) (void *object, uint32_t id, uint32_t type, uint32_t version, uint32_t new_id);
+};
+
+#define PW_REGISTRY_V0_EVENT_GLOBAL 0
+#define PW_REGISTRY_V0_EVENT_GLOBAL_REMOVE 1
+#define PW_REGISTRY_V0_EVENT_NUM 2
+
+/** Registry events */
+struct pw_registry_v0_events {
+#define PW_VERSION_REGISTRY_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify of a new global object
+ *
+ * The registry emits this event when a new global object is
+ * available.
+ *
+ * \param id the global object id
+ * \param parent_id the parent global id
+ * \param permissions the permissions of the object
+ * \param type the type of the interface
+ * \param version the version of the interface
+ * \param props extra properties of the global
+ */
+ void (*global) (void *data, uint32_t id, uint32_t parent_id,
+ uint32_t permissions, uint32_t type, uint32_t version,
+ const struct spa_dict *props);
+ /**
+ * Notify of a global object removal
+ *
+ * Emitted when a global object was removed from the registry.
+ * If the client has any bindings to the global, it should destroy
+ * those.
+ *
+ * \param id the id of the global that was removed
+ */
+ void (*global_remove) (void *data, uint32_t id);
+};
+
+#define pw_registry_resource_v0_global(r,...) pw_resource_notify(r,struct pw_registry_v0_events,global,__VA_ARGS__)
+#define pw_registry_resource_v0_global_remove(r,...) pw_resource_notify(r,struct pw_registry_v0_events,global_remove,__VA_ARGS__)
+
+
+#define PW_VERSION_MODULE_V0 0
+
+#define PW_MODULE_V0_EVENT_INFO 0
+#define PW_MODULE_V0_EVENT_NUM 1
+
+/** Module events */
+struct pw_module_v0_events {
+#define PW_VERSION_MODULE_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify module info
+ *
+ * \param info info about the module
+ */
+ void (*info) (void *data, struct pw_module_info *info);
+};
+
+#define pw_module_resource_v0_info(r,...) pw_resource_notify(r,struct pw_module_v0_events,info,__VA_ARGS__)
+
+#define PW_VERSION_NODE_V0 0
+
+#define PW_NODE_V0_EVENT_INFO 0
+#define PW_NODE_V0_EVENT_PARAM 1
+#define PW_NODE_V0_EVENT_NUM 2
+
+/** Node events */
+struct pw_node_v0_events {
+#define PW_VERSION_NODE_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify node info
+ *
+ * \param info info about the node
+ */
+ void (*info) (void *data, struct pw_node_info *info);
+ /**
+ * Notify a node param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define pw_node_resource_v0_info(r,...) pw_resource_notify(r,struct pw_node_v0_events,info,__VA_ARGS__)
+#define pw_node_resource_v0_param(r,...) pw_resource_notify(r,struct pw_node_v0_events,param,__VA_ARGS__)
+
+#define PW_NODE_V0_METHOD_ENUM_PARAMS 0
+#define PW_NODE_V0_METHOD_NUM 1
+
+/** Node methods */
+struct pw_node_v0_methods {
+#define PW_VERSION_NODE_V0_METHODS 0
+ uint32_t version;
+ /**
+ * Enumerate node parameters
+ *
+ * Start enumeration of node parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param id the parameter id to enum or PW_ID_ANY for all
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ void (*enum_params) (void *object, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+};
+
+#define PW_VERSION_PORT_V0 0
+
+#define PW_PORT_V0_EVENT_INFO 0
+#define PW_PORT_V0_EVENT_PARAM 1
+#define PW_PORT_V0_EVENT_NUM 2
+
+/** Port events */
+struct pw_port_v0_events {
+#define PW_VERSION_PORT_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify port info
+ *
+ * \param info info about the port
+ */
+ void (*info) (void *data, struct pw_port_info *info);
+ /**
+ * Notify a port param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define pw_port_resource_v0_info(r,...) pw_resource_notify(r,struct pw_port_v0_events,info,__VA_ARGS__)
+#define pw_port_resource_v0_param(r,...) pw_resource_notify(r,struct pw_port_v0_events,param,__VA_ARGS__)
+
+#define PW_PORT_V0_METHOD_ENUM_PARAMS 0
+#define PW_PORT_V0_METHOD_NUM 1
+
+/** Port methods */
+struct pw_port_v0_methods {
+#define PW_VERSION_PORT_V0_METHODS 0
+ uint32_t version;
+ /**
+ * Enumerate port parameters
+ *
+ * Start enumeration of port parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param id the parameter id to enumerate
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ void (*enum_params) (void *object, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+};
+
+#define PW_VERSION_FACTORY_V0 0
+
+#define PW_FACTORY_V0_EVENT_INFO 0
+#define PW_FACTORY_V0_EVENT_NUM 1
+
+/** Factory events */
+struct pw_factory_v0_events {
+#define PW_VERSION_FACTORY_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify factory info
+ *
+ * \param info info about the factory
+ */
+ void (*info) (void *data, struct pw_factory_info *info);
+};
+
+#define pw_factory_resource_v0_info(r,...) pw_resource_notify(r,struct pw_factory_v0_events,info,__VA_ARGS__)
+
+#define PW_VERSION_CLIENT_V0 0
+
+#define PW_CLIENT_V0_EVENT_INFO 0
+#define PW_CLIENT_V0_EVENT_NUM 1
+
+/** Client events */
+struct pw_client_v0_events {
+#define PW_VERSION_CLIENT_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify client info
+ *
+ * \param info info about the client
+ */
+ void (*info) (void *data, struct pw_client_info *info);
+};
+
+#define pw_client_resource_v0_info(r,...) pw_resource_notify(r,struct pw_client_v0_events,info,__VA_ARGS__)
+
+
+#define PW_VERSION_LINK_V0 0
+
+#define PW_LINK_V0_EVENT_INFO 0
+#define PW_LINK_V0_EVENT_NUM 1
+
+/** Link events */
+struct pw_link_v0_events {
+#define PW_VERSION_LINK_V0_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify link info
+ *
+ * \param info info about the link
+ */
+ void (*info) (void *data, struct pw_link_info *info);
+};
+
+#define pw_link_resource_v0_info(r,...) pw_resource_notify(r,struct pw_link_v0_events,info,__VA_ARGS__)
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_INTERFACES_V0_H */
diff --git a/src/modules/module-protocol-native/v0/protocol-native.c b/src/modules/module-protocol-native/v0/protocol-native.c
new file mode 100644
index 0000000..d6173ac
--- /dev/null
+++ b/src/modules/module-protocol-native/v0/protocol-native.c
@@ -0,0 +1,1371 @@
+/* PipeWire
+ *
+ * Copyright © 2017 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+
+#include "spa/pod/parser.h"
+#include "spa/pod/builder.h"
+#include "spa/debug/types.h"
+#include "spa/utils/string.h"
+
+#include "pipewire/pipewire.h"
+#include "pipewire/private.h"
+#include "pipewire/protocol.h"
+#include "pipewire/resource.h"
+#include "pipewire/extensions/protocol-native.h"
+#include "pipewire/extensions/metadata.h"
+#include "pipewire/extensions/session-manager.h"
+#include "pipewire/extensions/client-node.h"
+
+#include "interfaces.h"
+#include "typemap.h"
+
+#include "../connection.h"
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define PW_PROTOCOL_NATIVE_FLAG_REMAP (1<<0)
+
+SPA_EXPORT
+uint32_t pw_protocol_native0_find_type(struct pw_impl_client *client, const char *type)
+{
+ uint32_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) {
+ if (spa_streq(type_map[i].type, type))
+ return i;
+ }
+ return SPA_ID_INVALID;
+}
+
+static void
+update_types_server(struct pw_resource *resource)
+{
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_UPDATE_TYPES, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", 0,
+ "i", SPA_N_ELEMENTS(type_map), NULL);
+
+ for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) {
+ spa_pod_builder_add(b, "s", type_map[i].type, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+
+static void core_marshal_info(void *data, const struct pw_core_info *info)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct protocol_compat_v2 *compat_v2 = client->compat_v2;
+ struct spa_pod_builder *b;
+ uint32_t i, n_items;
+ uint64_t change_mask = 0;
+ struct spa_pod_frame f;
+ struct pw_protocol_native_message *msg;
+
+#define PW_CORE_V0_CHANGE_MASK_USER_NAME (1 << 0)
+#define PW_CORE_V0_CHANGE_MASK_HOST_NAME (1 << 1)
+#define PW_CORE_V0_CHANGE_MASK_VERSION (1 << 2)
+#define PW_CORE_V0_CHANGE_MASK_NAME (1 << 3)
+#define PW_CORE_V0_CHANGE_MASK_COOKIE (1 << 4)
+#define PW_CORE_V0_CHANGE_MASK_PROPS (1 << 5)
+
+ if (compat_v2->send_types) {
+ update_types_server(resource);
+ change_mask |= PW_CORE_V0_CHANGE_MASK_USER_NAME |
+ PW_CORE_V0_CHANGE_MASK_HOST_NAME |
+ PW_CORE_V0_CHANGE_MASK_VERSION |
+ PW_CORE_V0_CHANGE_MASK_NAME |
+ PW_CORE_V0_CHANGE_MASK_COOKIE;
+ compat_v2->send_types = false;
+ }
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_INFO, &msg);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ if (info->change_mask & PW_CORE_CHANGE_MASK_PROPS)
+ change_mask |= PW_CORE_V0_CHANGE_MASK_PROPS;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", change_mask,
+ "s", info->user_name,
+ "s", info->host_name,
+ "s", info->version,
+ "s", info->name,
+ "i", info->cookie,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_marshal_done(void *data, uint32_t id, int seq)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_DONE, NULL);
+
+ spa_pod_builder_add_struct(b, "i", seq);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_marshal_error(void *data, uint32_t id, int seq, int res, const char *error)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_ERROR, NULL);
+
+ spa_pod_builder_add_struct(b,
+ "i", id,
+ "i", res,
+ "s", error);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void core_marshal_remove_id(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_REMOVE_ID, NULL);
+
+ spa_pod_builder_add_struct(b, "i", id);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int core_demarshal_client_update(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_dict props;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ "i", &props.n_items, NULL) < 0)
+ return -EINVAL;
+
+ props.items = alloca(props.n_items * sizeof(struct spa_dict_item));
+ for (i = 0; i < props.n_items; i++) {
+ if (spa_pod_parser_get(&prs,
+ "s", &props.items[i].key,
+ "s", &props.items[i].value,
+ NULL) < 0)
+ return -EINVAL;
+ }
+ pw_impl_client_update_properties(client, &props);
+ return 0;
+}
+
+static uint32_t parse_perms(const char *str)
+{
+ uint32_t perms = 0;
+
+ while (*str != '\0') {
+ switch (*str++) {
+ case 'r':
+ perms |= PW_PERM_R;
+ break;
+ case 'w':
+ perms |= PW_PERM_W;
+ break;
+ case 'x':
+ perms |= PW_PERM_X;
+ break;
+ }
+ }
+ return perms;
+}
+
+static int core_demarshal_permissions(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_dict props;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t i, n_permissions;
+ struct pw_permission *permissions, defperm = { 0, };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs, "i", &props.n_items, NULL) < 0)
+ return -EINVAL;
+
+ props.items = alloca(props.n_items * sizeof(struct spa_dict_item));
+
+ n_permissions = 0;
+ permissions = alloca(props.n_items * sizeof(struct pw_permission));
+
+ for (i = 0; i < props.n_items; i++) {
+ uint32_t id, perms;
+ const char *str;
+
+ if (spa_pod_parser_get(&prs,
+ "s", &props.items[i].key,
+ "s", &props.items[i].value,
+ NULL) < 0)
+ return -EINVAL;
+
+ str = props.items[i].value;
+ /* first set global permissions */
+ if (spa_streq(props.items[i].key, PW_CORE_PERMISSIONS_GLOBAL)) {
+ size_t len;
+
+ /* <global-id>:[r][w][x] */
+ len = strcspn(str, ":");
+ if (len == 0)
+ continue;
+ id = atoi(str);
+ perms = parse_perms(str + len);
+ permissions[n_permissions++] = PW_PERMISSION_INIT(id, perms);
+ } else if (spa_streq(props.items[i].key, PW_CORE_PERMISSIONS_DEFAULT)) {
+ perms = parse_perms(str);
+ defperm = PW_PERMISSION_INIT(PW_ID_ANY, perms);
+ }
+ }
+ /* add default permission if set */
+ if (defperm.id == PW_ID_ANY)
+ permissions[n_permissions++] = defperm;
+
+ for (i = 0; i < n_permissions; i++) {
+ pw_log_debug("%d: %d: %08x", i, permissions[i].id, permissions[i].permissions);
+ }
+
+ return pw_impl_client_update_permissions(resource->client,
+ n_permissions, permissions);
+}
+
+static int core_demarshal_hello(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ void *ptr;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "P", &ptr) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, hello, 0, 2);
+}
+
+static int core_demarshal_sync(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t seq;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs, "i", &seq) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, sync, 0, 0, seq);
+}
+
+static int core_demarshal_get_registry(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ int32_t version, new_id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "i", &version,
+ "i", &new_id) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, get_registry, 0, version, new_id);
+}
+
+SPA_EXPORT
+uint32_t pw_protocol_native0_type_from_v2(struct pw_impl_client *client, uint32_t type)
+{
+ void *t;
+ uint32_t index;
+ struct protocol_compat_v2 *compat_v2 = client->compat_v2;
+
+ if ((t = pw_map_lookup(&compat_v2->types, type)) == NULL)
+ return SPA_ID_INVALID;
+
+ index = PW_MAP_PTR_TO_ID(t);
+ if (index >= SPA_N_ELEMENTS(type_map))
+ return SPA_ID_INVALID;
+
+ return type_map[index].id;
+}
+
+SPA_EXPORT
+const char * pw_protocol_native0_name_from_v2(struct pw_impl_client *client, uint32_t type)
+{
+ void *t;
+ uint32_t index;
+ struct protocol_compat_v2 *compat_v2 = client->compat_v2;
+
+ if ((t = pw_map_lookup(&compat_v2->types, type)) == NULL)
+ return NULL;
+
+ index = PW_MAP_PTR_TO_ID(t);
+ if (index >= SPA_N_ELEMENTS(type_map))
+ return NULL;
+
+ return type_map[index].name;
+}
+
+SPA_EXPORT
+uint32_t pw_protocol_native0_name_to_v2(struct pw_impl_client *client, const char *name)
+{
+ uint32_t i;
+ /* match name to type table and return index */
+ for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) {
+ if (type_map[i].name != NULL && spa_streq(type_map[i].name, name))
+ return i;
+ }
+ return SPA_ID_INVALID;
+}
+
+SPA_EXPORT
+uint32_t pw_protocol_native0_type_to_v2(struct pw_impl_client *client,
+ const struct spa_type_info *info, uint32_t type)
+{
+ const char *name;
+
+ /** find full name of type in type_info */
+ if ((name = spa_debug_type_find_name(info, type)) == NULL)
+ return SPA_ID_INVALID;
+
+ return pw_protocol_native0_name_to_v2(client, name);
+}
+
+struct spa_pod_prop_body0 {
+ uint32_t key;
+#define SPA_POD_PROP0_RANGE_NONE 0 /**< no range */
+#define SPA_POD_PROP0_RANGE_MIN_MAX 1 /**< property has range */
+#define SPA_POD_PROP0_RANGE_STEP 2 /**< property has range with step */
+#define SPA_POD_PROP0_RANGE_ENUM 3 /**< property has enumeration */
+#define SPA_POD_PROP0_RANGE_FLAGS 4 /**< property has flags */
+#define SPA_POD_PROP0_RANGE_MASK 0xf /**< mask to select range type */
+#define SPA_POD_PROP0_FLAG_UNSET (1 << 4) /**< property value is unset */
+#define SPA_POD_PROP0_FLAG_OPTIONAL (1 << 5) /**< property value is optional */
+#define SPA_POD_PROP0_FLAG_READONLY (1 << 6) /**< property is readonly */
+#define SPA_POD_PROP0_FLAG_DEPRECATED (1 << 7) /**< property is deprecated */
+#define SPA_POD_PROP0_FLAG_INFO (1 << 8) /**< property is informational and is not
+ * used when filtering */
+ uint32_t flags;
+ struct spa_pod value;
+ /* array with elements of value.size follows,
+ * first element is value/default, rest are alternatives */
+};
+
+/* v2 iterates object as containing spa_pod */
+#define SPA_POD_OBJECT_BODY_FOREACH0(body, size, iter) \
+ for ((iter) = SPA_PTROFF((body), sizeof(struct spa_pod_object_body), struct spa_pod); \
+ spa_pod_is_inside(body, size, iter); \
+ (iter) = spa_pod_next(iter))
+
+#define SPA_POD_PROP_ALTERNATIVE_FOREACH0(body, _size, iter) \
+ for ((iter) = SPA_PTROFF((body), (body)->value.size + \
+ sizeof(struct spa_pod_prop_body0), __typeof__(*iter)); \
+ (iter) <= SPA_PTROFF((body), (_size)-(body)->value.size, __typeof__(*iter)); \
+ (iter) = SPA_PTROFF((iter), (body)->value.size, __typeof__(*iter)))
+
+#define SPA0_POD_PROP_N_VALUES(b,size) (((size) - sizeof(struct spa_pod_prop_body0)) / (b)->value.size)
+
+static int remap_from_v2(uint32_t type, void *body, uint32_t size, struct pw_impl_client *client,
+ struct spa_pod_builder *builder)
+{
+ int res;
+
+ switch (type) {
+ case SPA_TYPE_Id:
+ spa_pod_builder_id(builder, pw_protocol_native0_type_from_v2(client, *(int32_t*) body));
+ break;
+
+ /** choice was props in v2 */
+ case SPA_TYPE_Choice:
+ {
+ struct spa_pod_prop_body0 *b = body;
+ struct spa_pod_frame f;
+ void *alt;
+ uint32_t key = pw_protocol_native0_type_from_v2(client, b->key);
+ enum spa_choice_type type;
+
+ spa_pod_builder_prop(builder, key, 0);
+
+ switch (b->flags & SPA_POD_PROP0_RANGE_MASK) {
+ default:
+ case SPA_POD_PROP0_RANGE_NONE:
+ type = SPA_CHOICE_None;
+ break;
+ case SPA_POD_PROP0_RANGE_MIN_MAX:
+ type = SPA_CHOICE_Range;
+ break;
+ case SPA_POD_PROP0_RANGE_STEP:
+ type = SPA_CHOICE_Step;
+ break;
+ case SPA_POD_PROP0_RANGE_ENUM:
+ type = SPA_CHOICE_Enum;
+ break;
+ case SPA_POD_PROP0_RANGE_FLAGS:
+ type = SPA_CHOICE_Flags;
+ break;
+ }
+ if (!SPA_FLAG_IS_SET(b->flags, SPA_POD_PROP0_FLAG_UNSET) &&
+ SPA0_POD_PROP_N_VALUES(b, size) == 1)
+ type = SPA_CHOICE_None;
+
+ spa_pod_builder_push_choice(builder, &f, type, 0);
+
+ if (b->value.type == SPA_TYPE_Id) {
+ uint32_t id;
+ if ((res = spa_pod_get_id(&b->value, &id)) < 0)
+ return res;
+ spa_pod_builder_id(builder, pw_protocol_native0_type_from_v2(client, id));
+ SPA_POD_PROP_ALTERNATIVE_FOREACH0(b, size, alt)
+ if ((res = remap_from_v2(b->value.type, alt, b->value.size, client, builder)) < 0)
+ return res;
+ } else {
+ spa_pod_builder_raw(builder, &b->value, size - sizeof(struct spa_pod));
+ }
+
+ spa_pod_builder_pop(builder, &f);
+
+ break;
+ }
+ case SPA_TYPE_Object:
+ {
+ struct spa_pod_object_body *b = body;
+ struct spa_pod *p;
+ struct spa_pod_frame f;
+ uint32_t type, count = 0;
+
+ /* type and id are switched */
+ type = pw_protocol_native0_type_from_v2(client, b->id),
+ spa_pod_builder_push_object(builder, &f, type,
+ pw_protocol_native0_type_from_v2(client, b->type));
+
+ /* object contained pods in v2 */
+ SPA_POD_OBJECT_BODY_FOREACH0(b, size, p) {
+ if (type == SPA_TYPE_OBJECT_Format && count < 2) {
+ uint32_t id;
+ if (spa_pod_get_id(p, &id) < 0)
+ continue;
+ id = pw_protocol_native0_type_from_v2(client, id);
+
+ if (count == 0) {
+ spa_pod_builder_prop(builder, SPA_FORMAT_mediaType, 0);
+ spa_pod_builder_id(builder, id);
+ }
+ if (count == 1) {
+ spa_pod_builder_prop(builder, SPA_FORMAT_mediaSubtype, 0);
+ spa_pod_builder_id(builder, id);
+ }
+ count++;
+ continue;
+ }
+ if ((res = remap_from_v2(p->type,
+ SPA_POD_BODY(p),
+ p->size,
+ client, builder)) < 0)
+ return res;
+ }
+ spa_pod_builder_pop(builder, &f);
+ break;
+ }
+ case SPA_TYPE_Struct:
+ {
+ struct spa_pod *b = body, *p;
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(builder, &f);
+ SPA_POD_FOREACH(b, size, p)
+ if ((res = remap_from_v2(p->type, SPA_POD_BODY(p), p->size, client, builder)) < 0)
+ return res;
+ spa_pod_builder_pop(builder, &f);
+ break;
+ }
+ default:
+ break;
+ }
+ return 0;
+}
+
+static int remap_to_v2(struct pw_impl_client *client, const struct spa_type_info *info,
+ uint32_t type, void *body, uint32_t size,
+ struct spa_pod_builder *builder)
+{
+ int res;
+
+ switch (type) {
+ case SPA_TYPE_Id:
+ spa_pod_builder_id(builder, pw_protocol_native0_type_to_v2(client, info, *(int32_t*) body));
+ break;
+
+ case SPA_TYPE_Object:
+ {
+ struct spa_pod_object_body *b = body;
+ struct spa_pod_prop *p;
+ struct spa_pod_frame f[2];
+ uint32_t type;
+ const struct spa_type_info *ti, *ii;
+
+ ti = spa_debug_type_find(info, b->type);
+ ii = ti ? spa_debug_type_find(ti->values, 0) : NULL;
+
+ if (b->type == SPA_TYPE_COMMAND_Node ||
+ b->type == SPA_TYPE_EVENT_Node) {
+ spa_pod_builder_push_object(builder, &f[0], 0,
+ pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, b->id));
+ } else {
+ ii = ii ? spa_debug_type_find(ii->values, b->id) : NULL;
+ /* type and id are switched */
+ type = pw_protocol_native0_type_to_v2(client, info, b->type),
+ spa_pod_builder_push_object(builder, &f[0],
+ pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, b->id), type);
+ }
+
+
+ info = ti ? ti->values : info;
+
+ SPA_POD_OBJECT_BODY_FOREACH(b, size, p) {
+ uint32_t key, flags;
+ uint32_t n_vals, choice;
+ struct spa_pod *values;
+
+ ii = spa_debug_type_find(info, p->key);
+
+ values = spa_pod_get_values(&p->value, &n_vals, &choice);
+
+ if (b->type == SPA_TYPE_OBJECT_Format &&
+ (p->key == SPA_FORMAT_mediaType ||
+ p->key == SPA_FORMAT_mediaSubtype)) {
+ uint32_t val;
+
+ if (spa_pod_get_id(values, &val) < 0)
+ continue;
+ spa_pod_builder_id(builder,
+ pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, val));
+ continue;
+ }
+
+ flags = 0;
+ switch(choice) {
+ case SPA_CHOICE_None:
+ flags |= SPA_POD_PROP0_RANGE_NONE;
+ break;
+ case SPA_CHOICE_Range:
+ flags |= SPA_POD_PROP0_RANGE_MIN_MAX | SPA_POD_PROP0_FLAG_UNSET;
+ break;
+ case SPA_CHOICE_Step:
+ flags |= SPA_POD_PROP0_RANGE_STEP | SPA_POD_PROP0_FLAG_UNSET;
+ break;
+ case SPA_CHOICE_Enum:
+ flags |= SPA_POD_PROP0_RANGE_ENUM | SPA_POD_PROP0_FLAG_UNSET;
+ break;
+ case SPA_CHOICE_Flags:
+ flags |= SPA_POD_PROP0_RANGE_FLAGS | SPA_POD_PROP0_FLAG_UNSET;
+ break;
+ }
+
+ key = pw_protocol_native0_type_to_v2(client, info, p->key);
+
+ spa_pod_builder_push_choice(builder, &f[1], key, flags);
+
+ if (values->type == SPA_TYPE_Id) {
+ uint32_t i, *id = SPA_POD_BODY(values);
+
+ for (i = 0; i < n_vals; i++) {
+ spa_pod_builder_id(builder,
+ pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, id[i]));
+ }
+
+ } else {
+ spa_pod_builder_raw(builder, values, sizeof(struct spa_pod) + n_vals * values->size);
+ }
+ spa_pod_builder_pop(builder, &f[1]);
+ }
+ spa_pod_builder_pop(builder, &f[0]);
+ break;
+ }
+ case SPA_TYPE_Struct:
+ {
+ struct spa_pod *b = body, *p;
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(builder, &f);
+ SPA_POD_FOREACH(b, size, p)
+ if ((res = remap_to_v2(client, info, p->type, SPA_POD_BODY(p), p->size, builder)) < 0)
+ return res;
+ spa_pod_builder_pop(builder, &f);
+ break;
+ }
+ default:
+ break;
+ }
+ return 0;
+}
+
+
+
+
+SPA_EXPORT
+struct spa_pod * pw_protocol_native0_pod_from_v2(struct pw_impl_client *client, const struct spa_pod *pod)
+{
+ uint8_t buffer[4096];
+ struct spa_pod *copy;
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, 4096);
+ int res;
+
+ if (pod == NULL)
+ return NULL;
+
+ if ((res = remap_from_v2(SPA_POD_TYPE(pod),
+ SPA_POD_BODY(pod),
+ SPA_POD_BODY_SIZE(pod),
+ client, &b)) < 0) {
+ errno = -res;
+ return NULL;
+ }
+ copy = spa_pod_copy(b.data);
+ return copy;
+}
+
+SPA_EXPORT
+int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod,
+ struct spa_pod_builder *b)
+{
+ int res;
+
+ if (pod == NULL) {
+ spa_pod_builder_none(b);
+ return 0;
+ }
+
+ if ((res = remap_to_v2(client, pw_type_info(),
+ SPA_POD_TYPE(pod),
+ SPA_POD_BODY(pod),
+ SPA_POD_BODY_SIZE(pod),
+ b)) < 0) {
+ return -res;
+ }
+ return 0;
+}
+
+static int core_demarshal_create_object(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ uint32_t version, type, new_id, i;
+ const char *factory_name, *type_name;
+ struct spa_dict props;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ "s", &factory_name,
+ "I", &type,
+ "i", &version,
+ "i", &props.n_items, NULL) < 0)
+ return -EINVAL;
+
+ props.items = alloca(props.n_items * sizeof(struct spa_dict_item));
+ for (i = 0; i < props.n_items; i++) {
+ if (spa_pod_parser_get(&prs,
+ "s", &props.items[i].key,
+ "s", &props.items[i].value, NULL) < 0)
+ return -EINVAL;
+ }
+ if (spa_pod_parser_get(&prs, "i", &new_id, NULL) < 0)
+ return -EINVAL;
+
+ type_name = pw_protocol_native0_name_from_v2(client, type);
+ if (type_name == NULL)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_core_methods, create_object, 0, factory_name,
+ type_name, version,
+ &props, new_id);
+}
+
+static int core_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object, *r;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "i", &id, NULL) < 0)
+ return -EINVAL;
+
+ pw_log_debug("client %p: destroy resource %u", client, id);
+
+ if ((r = pw_impl_client_find_resource(client, id)) == NULL)
+ goto no_resource;
+
+ return pw_resource_notify(resource, struct pw_core_methods, destroy, 0, r);
+
+no_resource:
+ pw_log_error("client %p: unknown resource %u op:%u", client, id, msg->opcode);
+ pw_resource_errorf(resource, -ENOENT, "unknown resource %d op:%u", id, msg->opcode);
+ return 0;
+}
+
+static int core_demarshal_update_types_server(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct protocol_compat_v2 *compat_v2 = client->compat_v2;
+ struct spa_pod_parser prs;
+ uint32_t first_id, n_types;
+ struct spa_pod_frame f;
+ const char **types;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0 ||
+ spa_pod_parser_get(&prs,
+ "i", &first_id,
+ "i", &n_types,
+ NULL) < 0)
+ return -EINVAL;
+
+ if (first_id == 0)
+ compat_v2->send_types = true;
+
+ types = alloca(n_types * sizeof(char *));
+ for (i = 0; i < n_types; i++) {
+ if (spa_pod_parser_get(&prs, "s", &types[i], NULL) < 0)
+ return -EINVAL;
+ }
+
+ for (i = 0; i < n_types; i++, first_id++) {
+ uint32_t type_id = pw_protocol_native0_find_type(client, types[i]);
+ if (type_id == SPA_ID_INVALID)
+ continue;
+ if (pw_map_insert_at(&compat_v2->types, first_id, PW_MAP_ID_TO_PTR(type_id)) < 0)
+ pw_log_error("can't add type %d->%d for client", first_id, type_id);
+ }
+ return 0;
+}
+
+static void registry_marshal_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version, const struct spa_dict *props)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items, parent_id;
+ uint32_t type_id;
+ const char *str;
+
+ type_id = pw_protocol_native0_find_type(client, type);
+ if (type_id == SPA_ID_INVALID)
+ return;
+
+ b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_V0_EVENT_GLOBAL, NULL);
+
+ n_items = props ? props->n_items : 0;
+
+ parent_id = 0;
+ if (props) {
+ if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ if ((str = spa_dict_lookup(props, "node.id")) != NULL)
+ parent_id = atoi(str);
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ if ((str = spa_dict_lookup(props, "device.id")) != NULL)
+ parent_id = atoi(str);
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Client) ||
+ spa_streq(type, PW_TYPE_INTERFACE_Device) ||
+ spa_streq(type, PW_TYPE_INTERFACE_Factory)) {
+ if ((str = spa_dict_lookup(props, "module.id")) != NULL)
+ parent_id = atoi(str);
+ }
+ }
+
+ version = 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", id,
+ "i", parent_id,
+ "i", permissions,
+ "I", type_id,
+ "i", version,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", props->items[i].key,
+ "s", props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void registry_marshal_global_remove(void *data, uint32_t id)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_V0_EVENT_GLOBAL_REMOVE, NULL);
+
+ spa_pod_builder_add_struct(b, "i", id);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int registry_demarshal_bind(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ uint32_t id, version, type, new_id;
+ const char *type_name;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "i", &id,
+ "I", &type,
+ "i", &version,
+ "i", &new_id) < 0)
+ return -EINVAL;
+
+ type_name = pw_protocol_native0_name_from_v2(client, type);
+ if (type_name == NULL)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_registry_methods, bind, 0, id, type_name, version, new_id);
+}
+
+static void module_marshal_info(void *data, const struct pw_module_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_resource(resource, PW_MODULE_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "s", info->name,
+ "s", info->filename,
+ "s", info->args,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void factory_marshal_info(void *data, const struct pw_factory_info *info)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items, type, version;
+
+ type = pw_protocol_native0_find_type(client, info->type);
+ if (type == SPA_ID_INVALID)
+ return;
+
+ b = pw_protocol_native_begin_resource(resource, PW_FACTORY_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ version = 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "s", info->name,
+ "I", type,
+ "i", version,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void node_marshal_info(void *data, const struct pw_node_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_resource(resource, PW_NODE_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "s", "node.name",
+ "i", info->max_input_ports,
+ "i", info->n_input_ports,
+ "i", info->max_output_ports,
+ "i", info->n_output_ports,
+ "i", info->state,
+ "s", info->error,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void node_marshal_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_NODE_V0_EVENT_PARAM, NULL);
+
+ id = pw_protocol_native0_type_to_v2(client, spa_type_param, id),
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "I", id,
+ "i", index,
+ "i", next,
+ NULL);
+ pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)param, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int node_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "I", &id,
+ "i", &index,
+ "i", &num,
+ "P", &filter) < 0)
+ return -EINVAL;
+
+ id = pw_protocol_native0_type_from_v2(client, id);
+ filter = NULL;
+
+ return pw_resource_notify(resource, struct pw_node_methods, enum_params, 0,
+ 0, id, index, num, filter);
+}
+
+static void port_marshal_info(void *data, const struct pw_port_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+ uint64_t change_mask = 0;
+ const char *port_name;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PORT_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+#define PW_PORT_V0_CHANGE_MASK_NAME (1 << 0)
+#define PW_PORT_V0_CHANGE_MASK_PROPS (1 << 1)
+#define PW_PORT_V0_CHANGE_MASK_ENUM_PARAMS (1 << 2)
+
+ change_mask |= PW_PORT_V0_CHANGE_MASK_NAME;
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
+ change_mask |= PW_PORT_V0_CHANGE_MASK_PROPS;
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS)
+ change_mask |= PW_PORT_V0_CHANGE_MASK_ENUM_PARAMS;
+
+ port_name = NULL;
+ if (info->props != NULL)
+ port_name = spa_dict_lookup(info->props, "port.name");
+ if (port_name == NULL)
+ port_name = "port.name";
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", change_mask,
+ "s", port_name,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void port_marshal_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+
+ b = pw_protocol_native_begin_resource(resource, PW_PORT_V0_EVENT_PARAM, NULL);
+
+ id = pw_protocol_native0_type_to_v2(client, spa_type_param, id),
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "I", id,
+ "i", index,
+ "i", next,
+ NULL);
+ pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)param, b);
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int port_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ "I", &id,
+ "i", &index,
+ "i", &num,
+ "P", &filter) < 0)
+ return -EINVAL;
+
+ id = pw_protocol_native0_type_from_v2(client, id);
+ filter = NULL;
+
+ return pw_resource_notify(resource, struct pw_port_methods, enum_params, 0,
+ 0, id, index, num, filter);
+}
+
+static void client_marshal_info(void *data, const struct pw_client_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_resource(resource, PW_CLIENT_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void client_marshal_permissions(void *data, uint32_t index, uint32_t n_permissions,
+ const struct pw_permission *permissions)
+{
+}
+
+
+static void link_marshal_info(void *data, const struct pw_link_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i, n_items;
+
+ b = pw_protocol_native_begin_resource(resource, PW_LINK_V0_EVENT_INFO, NULL);
+
+ n_items = info->props ? info->props->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ "i", info->id,
+ "l", info->change_mask,
+ "i", info->output_node_id,
+ "i", info->output_port_id,
+ "i", info->input_node_id,
+ "i", info->input_port_id,
+ "P", info->format,
+ "i", n_items, NULL);
+
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ "s", info->props->items[i].key,
+ "s", info->props->items[i].value, NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_core_method_demarshal[PW_CORE_V0_METHOD_NUM] = {
+ [PW_CORE_V0_METHOD_HELLO] = { &core_demarshal_hello, 0, },
+ [PW_CORE_V0_METHOD_UPDATE_TYPES] = { &core_demarshal_update_types_server, 0, },
+ [PW_CORE_V0_METHOD_SYNC] = { &core_demarshal_sync, 0, },
+ [PW_CORE_V0_METHOD_GET_REGISTRY] = { &core_demarshal_get_registry, 0, },
+ [PW_CORE_V0_METHOD_CLIENT_UPDATE] = { &core_demarshal_client_update, 0, },
+ [PW_CORE_V0_METHOD_PERMISSIONS] = { &core_demarshal_permissions, 0, },
+ [PW_CORE_V0_METHOD_CREATE_OBJECT] = { &core_demarshal_create_object, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, },
+ [PW_CORE_V0_METHOD_DESTROY] = { &core_demarshal_destroy, 0, }
+};
+
+static const struct pw_core_events pw_protocol_native_core_event_marshal = {
+ PW_VERSION_CORE_EVENTS,
+ .info = &core_marshal_info,
+ .done = &core_marshal_done,
+ .error = &core_marshal_error,
+ .remove_id = &core_marshal_remove_id,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_core_marshal = {
+ PW_TYPE_INTERFACE_Core,
+ PW_VERSION_CORE_V0,
+ PW_CORE_V0_METHOD_NUM,
+ PW_CORE_EVENT_NUM,
+ 0,
+ NULL,
+ pw_protocol_native_core_method_demarshal,
+ &pw_protocol_native_core_event_marshal,
+ NULL
+};
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_registry_method_demarshal[] = {
+ [PW_REGISTRY_V0_METHOD_BIND] = { &registry_demarshal_bind, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, },
+};
+
+static const struct pw_registry_events pw_protocol_native_registry_event_marshal = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = &registry_marshal_global,
+ .global_remove = &registry_marshal_global_remove,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_registry_marshal = {
+ PW_TYPE_INTERFACE_Registry,
+ PW_VERSION_REGISTRY_V0,
+ PW_REGISTRY_V0_METHOD_NUM,
+ PW_REGISTRY_EVENT_NUM,
+ 0,
+ NULL,
+ pw_protocol_native_registry_method_demarshal,
+ &pw_protocol_native_registry_event_marshal,
+ NULL
+};
+
+static const struct pw_module_events pw_protocol_native_module_event_marshal = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = &module_marshal_info,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_module_marshal = {
+ PW_TYPE_INTERFACE_Module,
+ PW_VERSION_MODULE_V0,
+ 0,
+ PW_MODULE_EVENT_NUM,
+ 0,
+ NULL, NULL,
+ &pw_protocol_native_module_event_marshal,
+ NULL
+};
+
+static const struct pw_factory_events pw_protocol_native_factory_event_marshal = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = &factory_marshal_info,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_factory_marshal = {
+ PW_TYPE_INTERFACE_Factory,
+ PW_VERSION_FACTORY_V0,
+ 0,
+ PW_FACTORY_EVENT_NUM,
+ 0,
+ NULL, NULL,
+ &pw_protocol_native_factory_event_marshal,
+ NULL,
+};
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_node_method_demarshal[] = {
+ [PW_NODE_V0_METHOD_ENUM_PARAMS] = { &node_demarshal_enum_params, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, },
+};
+
+static const struct pw_node_events pw_protocol_native_node_event_marshal = {
+ PW_VERSION_NODE_EVENTS,
+ .info = &node_marshal_info,
+ .param = &node_marshal_param,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_node_marshal = {
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE_V0,
+ PW_NODE_V0_METHOD_NUM,
+ PW_NODE_EVENT_NUM,
+ 0,
+ NULL,
+ pw_protocol_native_node_method_demarshal,
+ &pw_protocol_native_node_event_marshal,
+ NULL
+};
+
+
+static const struct pw_protocol_native_demarshal pw_protocol_native_port_method_demarshal[] = {
+ [PW_PORT_V0_METHOD_ENUM_PARAMS] = { &port_demarshal_enum_params, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, },
+};
+
+static const struct pw_port_events pw_protocol_native_port_event_marshal = {
+ PW_VERSION_PORT_EVENTS,
+ .info = &port_marshal_info,
+ .param = &port_marshal_param,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_port_marshal = {
+ PW_TYPE_INTERFACE_Port,
+ PW_VERSION_PORT_V0,
+ PW_PORT_V0_METHOD_NUM,
+ PW_PORT_EVENT_NUM,
+ 0,
+ NULL,
+ pw_protocol_native_port_method_demarshal,
+ &pw_protocol_native_port_event_marshal,
+ NULL
+};
+
+static const struct pw_client_events pw_protocol_native_client_event_marshal = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = &client_marshal_info,
+ .permissions = &client_marshal_permissions,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_marshal = {
+ PW_TYPE_INTERFACE_Client,
+ PW_VERSION_CLIENT_V0,
+ 0,
+ PW_CLIENT_EVENT_NUM,
+ 0,
+ NULL, NULL,
+ &pw_protocol_native_client_event_marshal,
+ NULL,
+};
+
+static const struct pw_link_events pw_protocol_native_link_event_marshal = {
+ PW_VERSION_LINK_EVENTS,
+ .info = &link_marshal_info,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_link_marshal = {
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK_V0,
+ 0,
+ PW_LINK_EVENT_NUM,
+ 0,
+ NULL, NULL,
+ &pw_protocol_native_link_event_marshal,
+ NULL
+};
+
+void pw_protocol_native0_init(struct pw_protocol *protocol)
+{
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_core_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_registry_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_module_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_node_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_port_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_factory_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_link_marshal);
+}
diff --git a/src/modules/module-protocol-native/v0/typemap.h b/src/modules/module-protocol-native/v0/typemap.h
new file mode 100644
index 0000000..1d80ea2
--- /dev/null
+++ b/src/modules/module-protocol-native/v0/typemap.h
@@ -0,0 +1,282 @@
+static const struct type_info {
+ const char *type;
+ const char *name;
+ uint32_t id;
+} type_map[] = {
+ { "Spa:Interface:TypeMap", SPA_TYPE_INFO_INTERFACE_BASE, 0, },
+ { "Spa:Interface:Log", SPA_TYPE_INTERFACE_Log, 0, },
+ { "Spa:Interface:Loop", SPA_TYPE_INTERFACE_Loop, 0, },
+ { "Spa:Interface:LoopControl", SPA_TYPE_INTERFACE_LoopControl, 0, },
+ { "Spa:Interface:LoopUtils", SPA_TYPE_INTERFACE_LoopUtils, 0, },
+ { "PipeWire:Interface:Core", PW_TYPE_INTERFACE_Core, 0, },
+ { "PipeWire:Interface:Registry", PW_TYPE_INTERFACE_Registry, 0, },
+ { "PipeWire:Interface:Node", PW_TYPE_INTERFACE_Node, 0, },
+ { "PipeWire:Interface:Port", PW_TYPE_INTERFACE_Port,0, },
+ { "PipeWire:Interface:Factory", PW_TYPE_INTERFACE_Factory, 0, },
+ { "PipeWire:Interface:Link", PW_TYPE_INTERFACE_Link, 0, },
+ { "PipeWire:Interface:Client", PW_TYPE_INTERFACE_Client, 0, },
+ { "PipeWire:Interface:Module", PW_TYPE_INTERFACE_Module, 0, },
+ { "PipeWire:Interface:Device", PW_TYPE_INTERFACE_Device, 0, },
+
+ { "PipeWire:Interface:Metadata", PW_TYPE_INTERFACE_Metadata, 0, },
+ { "PipeWire:Interface:Session", PW_TYPE_INTERFACE_Session, 0, },
+ { "PipeWire:Interface:Endpoint", PW_TYPE_INTERFACE_Endpoint, 0, },
+ { "PipeWire:Interface:EndpointStream", PW_TYPE_INTERFACE_EndpointStream, 0, },
+ { "PipeWire:Interface:EndpointLink", PW_TYPE_INTERFACE_EndpointLink, 0, },
+
+ { "PipeWire:Interface:ClientNode", PW_TYPE_INTERFACE_ClientNode, 0, },
+ { "PipeWire:Interface:ClientSession", PW_TYPE_INTERFACE_ClientSession, 0, },
+ { "PipeWire:Interface:ClientEndpoint", PW_TYPE_INTERFACE_ClientEndpoint, 0, },
+
+ { "Spa:Interface:Node", SPA_TYPE_INTERFACE_Node, 0, },
+ { "Spa:Interface:Clock", },
+ { "Spa:Interface:Monitor", },
+ { "Spa:Interface:Device", SPA_TYPE_INTERFACE_Device, 0, },
+ { "Spa:POD:Object:Param:Format", SPA_TYPE_INFO_Format, SPA_TYPE_OBJECT_Format, },
+ { "Spa:POD:Object:Param:Props", SPA_TYPE_INFO_Props, SPA_TYPE_OBJECT_Props, },
+ { "Spa:Pointer:IO:Buffers", },
+ { "Spa:Pointer:IO:Control:Range", },
+ { "Spa:Pointer:IO:Prop", },
+ { "Spa:Enum:ParamId:List", },
+ { "Spa:POD:Object:Param:List", },
+ { "Spa:POD:Object:Param:List:id", },
+ { "Spa:Enum:ParamId:PropInfo", SPA_TYPE_INFO_PARAM_ID_BASE "PropInfo", SPA_PARAM_PropInfo, },
+ { "Spa:POD:Object:Param:PropInfo", SPA_TYPE_INFO_PROP_INFO_BASE, SPA_PROP_INFO_START, },
+ { "Spa:POD:Object:Param:PropInfo:id", SPA_TYPE_INFO_PROP_INFO_BASE "id", SPA_PROP_INFO_id, },
+ { "Spa:POD:Object:Param:PropInfo:name", SPA_TYPE_INFO_PROP_INFO_BASE "name", SPA_PROP_INFO_name, },
+ { "Spa:POD:Object:Param:PropInfo:type", SPA_TYPE_INFO_PROP_INFO_BASE "typ", SPA_PROP_INFO_type, },
+ { "Spa:POD:Object:Param:PropInfo:labels", SPA_TYPE_INFO_PROP_INFO_BASE "labels", SPA_PROP_INFO_labels, },
+ { "Spa:Enum:ParamId:Props", SPA_TYPE_INFO_PARAM_ID_BASE "Props", SPA_PARAM_Props, },
+ { "Spa:Enum:ParamId:EnumFormat", SPA_TYPE_INFO_PARAM_ID_BASE "EnumFormat", SPA_PARAM_EnumFormat,},
+ { "Spa:Enum:ParamId:Format", SPA_TYPE_INFO_PARAM_ID_BASE "Format", SPA_PARAM_Format, },
+ { "Spa:Enum:ParamId:Buffers", SPA_TYPE_INFO_PARAM_ID_BASE "Buffers", SPA_PARAM_Buffers },
+ { "Spa:Enum:ParamId:Meta", SPA_TYPE_INFO_PARAM_ID_BASE "Meta", SPA_PARAM_Meta, },
+ { "Spa:Pointer:Meta:Header", SPA_TYPE_INFO_META_BASE "Header", SPA_META_Header, },
+ { "Spa:Pointer:Meta:VideoCrop", SPA_TYPE_INFO_META_REGION_BASE "VideoCrop", SPA_META_VideoCrop, },
+ { "Spa:Pointer:Meta:VideoDamage", SPA_TYPE_INFO_META_ARRAY_REGION_BASE "VideoDamage", SPA_META_VideoDamage, },
+ { "Spa:Pointer:Meta:Bitmap", SPA_TYPE_INFO_META_BASE "Bitmap", SPA_META_Bitmap, },
+ { "Spa:Pointer:Meta:Cursor", SPA_TYPE_INFO_META_BASE "Cursor", SPA_META_Cursor, },
+ { "Spa:Enum:DataType:MemPtr", SPA_TYPE_INFO_DATA_BASE "MemPtr", SPA_DATA_MemPtr, },
+ { "Spa:Enum:DataType:Fd:MemFd", SPA_TYPE_INFO_DATA_FD_BASE "MemFd", SPA_DATA_MemFd, },
+ { "Spa:Enum:DataType:Fd:DmaBuf", SPA_TYPE_INFO_DATA_FD_BASE "DmaBuf", SPA_DATA_DmaBuf, },
+ { "Spa:POD:Object:Event:Node:Error", SPA_TYPE_INFO_NODE_EVENT_BASE "Error", SPA_NODE_EVENT_Error, },
+ { "Spa:POD:Object:Event:Node:Buffering", SPA_TYPE_INFO_NODE_EVENT_BASE "Buffering", SPA_NODE_EVENT_Buffering, },
+ { "Spa:POD:Object:Event:Node:RequestRefresh", SPA_TYPE_INFO_NODE_EVENT_BASE "RequestRefresh", SPA_NODE_EVENT_RequestRefresh, },
+ { "Spa:POD:Object:Event:Node:RequestClockUpdate", SPA_TYPE_INFO_NODE_EVENT_BASE "RequestClockUpdate", SPA_NODE0_EVENT_RequestClockUpdate, },
+ { "Spa:POD:Object:Command:Node", SPA_TYPE_INFO_COMMAND_BASE "Node", SPA_TYPE_COMMAND_Node,},
+ { "Spa:POD:Object:Command:Node:Suspend", SPA_TYPE_INFO_NODE_COMMAND_BASE "Suspend", SPA_NODE_COMMAND_Suspend,},
+ { "Spa:POD:Object:Command:Node:Pause", SPA_TYPE_INFO_NODE_COMMAND_BASE "Pause", SPA_NODE_COMMAND_Pause, },
+ { "Spa:POD:Object:Command:Node:Start", SPA_TYPE_INFO_NODE_COMMAND_BASE "Start", SPA_NODE_COMMAND_Start, },
+ { "Spa:POD:Object:Command:Node:Enable", SPA_TYPE_INFO_NODE_COMMAND_BASE "Enable", SPA_NODE_COMMAND_Enable, },
+ { "Spa:POD:Object:Command:Node:Disable", SPA_TYPE_INFO_NODE_COMMAND_BASE "Disable", SPA_NODE_COMMAND_Disable, },
+ { "Spa:POD:Object:Command:Node:Flush", SPA_TYPE_INFO_NODE_COMMAND_BASE "Flush", SPA_NODE_COMMAND_Flush, },
+ { "Spa:POD:Object:Command:Node:Drain", SPA_TYPE_INFO_NODE_COMMAND_BASE "Drain", SPA_NODE_COMMAND_Drain, },
+ { "Spa:POD:Object:Command:Node:Marker", SPA_TYPE_INFO_NODE_COMMAND_BASE "Marker", SPA_NODE_COMMAND_Marker, },
+ { "Spa:POD:Object:Command:Node:ClockUpdate", SPA_TYPE_INFO_NODE_COMMAND_BASE "ClockUpdate", SPA_NODE0_COMMAND_ClockUpdate, },
+ { "Spa:POD:Object:Event:Monitor:Added", },
+ { "Spa:POD:Object:Event:Monitor:Removed", },
+ { "Spa:POD:Object:Event:Monitor:Changed", },
+ { "Spa:POD:Object:MonitorItem", },
+ { "Spa:POD:Object:MonitorItem:id", },
+ { "Spa:POD:Object:MonitorItem:flags", },
+ { "Spa:POD:Object:MonitorItem:state", },
+ { "Spa:POD:Object:MonitorItem:name", },
+ { "Spa:POD:Object:MonitorItem:class", },
+ { "Spa:POD:Object:MonitorItem:info", },
+ { "Spa:POD:Object:MonitorItem:factory", },
+ { "Spa:POD:Object:Param:Buffers", SPA_TYPE_INFO_PARAM_Buffers, SPA_TYPE_OBJECT_ParamBuffers, },
+ { "Spa:POD:Object:Param:Buffers:size", SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "size", SPA_PARAM_BUFFERS_size, },
+ { "Spa:POD:Object:Param:Buffers:stride", SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "stride", SPA_PARAM_BUFFERS_stride, },
+ { "Spa:POD:Object:Param:Buffers:buffers", SPA_TYPE_INFO_PARAM_BUFFERS_BASE "buffers", SPA_PARAM_BUFFERS_buffers, },
+ { "Spa:POD:Object:Param:Buffers:align",SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "align", SPA_PARAM_BUFFERS_align, },
+ { "Spa:POD:Object:Param:Meta", SPA_TYPE_INFO_PARAM_Meta, SPA_TYPE_OBJECT_ParamMeta, },
+ { "Spa:POD:Object:Param:Meta:type", SPA_TYPE_INFO_PARAM_META_BASE "type", SPA_PARAM_META_type, },
+ { "Spa:POD:Object:Param:Meta:size", SPA_TYPE_INFO_PARAM_META_BASE "size", SPA_PARAM_META_size, },
+ { "Spa:POD:Object:Param:IO:id", },
+ { "Spa:POD:Object:Param:IO:size", },
+ { "Spa:Enum:ParamId:IO:Buffers", },
+ { "Spa:POD:Object:Param:IO:Buffers", },
+ { "Spa:Enum:ParamId:IO:Control", },
+ { "Spa:POD:Object:Param:IO:Control", },
+ { "Spa:Enum:ParamId:IO:Props:In", },
+ { "Spa:Enum:ParamId:IO:Props:Out", },
+ { "Spa:POD:Object:Param:IO:Prop", },
+ { "Spa:Interface:DBus", SPA_TYPE_INFO_INTERFACE_BASE "DBus", 0, },
+ { "Spa:Enum:MediaType:audio", SPA_TYPE_INFO_MEDIA_TYPE_BASE "audio", SPA_MEDIA_TYPE_audio, },
+ { "Spa:Enum:MediaType:video", SPA_TYPE_INFO_MEDIA_TYPE_BASE "video", SPA_MEDIA_TYPE_video, },
+ { "Spa:Enum:MediaType:image", SPA_TYPE_INFO_MEDIA_TYPE_BASE "image", SPA_MEDIA_TYPE_image, },
+ { "Spa:Enum:MediaType:binary", SPA_TYPE_INFO_MEDIA_TYPE_BASE "binary", SPA_MEDIA_TYPE_binary, },
+ { "Spa:Enum:MediaType:stream", SPA_TYPE_INFO_MEDIA_TYPE_BASE "stream", SPA_MEDIA_TYPE_stream, },
+ { "Spa:Enum:MediaType:application", SPA_TYPE_INFO_MEDIA_TYPE_BASE "application", SPA_MEDIA_TYPE_application, },
+ { "Spa:Enum:MediaSubtype:raw", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "raw", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:dsp", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dsp", SPA_MEDIA_SUBTYPE_dsp, },
+ { "Spa:Enum:MediaSubtype:control", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "control", SPA_MEDIA_SUBTYPE_control, },
+ { "Spa:Enum:MediaSubtype:h264", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h264", SPA_MEDIA_SUBTYPE_h264, },
+ { "Spa:Enum:MediaSubtype:mjpg", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mjpg", SPA_MEDIA_SUBTYPE_mjpg, },
+ { "Spa:Enum:MediaSubtype:dv", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dv", SPA_MEDIA_SUBTYPE_dv, },
+ { "Spa:Enum:MediaSubtype:mpegts", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpegts", SPA_MEDIA_SUBTYPE_mpegts, },
+ { "Spa:Enum:MediaSubtype:h263", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h263", SPA_MEDIA_SUBTYPE_h263, },
+ { "Spa:Enum:MediaSubtype:mpeg1", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg1", SPA_MEDIA_SUBTYPE_mpeg1, },
+ { "Spa:Enum:MediaSubtype:mpeg2", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg2", SPA_MEDIA_SUBTYPE_mpeg2, },
+ { "Spa:Enum:MediaSubtype:mpeg4", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg4", SPA_MEDIA_SUBTYPE_mpeg4, },
+ { "Spa:Enum:MediaSubtype:xvid", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "xvid", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:vc1", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vc1", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:vp8", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp8", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:vp9", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp9", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:jpeg", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "jpeg", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:bayer", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "bayer", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:mp3", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mp3", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:aac", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "aac", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:vorbis", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vorbis", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:wma", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "wma", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:ra", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "ra", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:sbc", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "sbc", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:adpcm", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "adpcm", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:g723", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g723", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:g726", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g726", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:g729", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g729", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:amr", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "amr", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:gsm", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "gsm", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:Enum:MediaSubtype:midi", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "midi", SPA_MEDIA_SUBTYPE_raw, },
+ { "Spa:POD:Object:Param:Format:Video:format", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "format", SPA_FORMAT_VIDEO_format,},
+ { "Spa:POD:Object:Param:Format:Video:size", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "size", SPA_FORMAT_VIDEO_size,},
+ { "Spa:POD:Object:Param:Format:Video:framerate", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "framerate", SPA_FORMAT_VIDEO_framerate},
+ { "Spa:POD:Object:Param:Format:Video:max-framerate", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "maxFramerate", SPA_FORMAT_VIDEO_maxFramerate},
+ { "Spa:POD:Object:Param:Format:Video:views", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "views", SPA_FORMAT_VIDEO_views},
+ { "Spa:POD:Object:Param:Format:Video:interlace-mode", },
+ { "Spa:POD:Object:Param:Format:Video:pixel-aspect-ratio", },
+ { "Spa:POD:Object:Param:Format:Video:multiview-mode", },
+ { "Spa:POD:Object:Param:Format:Video:multiview-flags", },
+ { "Spa:POD:Object:Param:Format:Video:chroma-site", },
+ { "Spa:POD:Object:Param:Format:Video:color-range", },
+ { "Spa:POD:Object:Param:Format:Video:color-matrix", },
+ { "Spa:POD:Object:Param:Format:Video:transfer-function", },
+ { "Spa:POD:Object:Param:Format:Video:color-primaries", },
+ { "Spa:POD:Object:Param:Format:Video:profile", },
+ { "Spa:POD:Object:Param:Format:Video:level", },
+ { "Spa:POD:Object:Param:Format:Video:stream-format", },
+ { "Spa:POD:Object:Param:Format:Video:alignment", },
+ { "Spa:POD:Object:Param:Format:Audio:format", SPA_TYPE_INFO_FORMAT_AUDIO_BASE "format", SPA_FORMAT_AUDIO_format,},
+ { "Spa:POD:Object:Param:Format:Audio:flags", },
+ { "Spa:POD:Object:Param:Format:Audio:layout", },
+ { "Spa:POD:Object:Param:Format:Audio:rate", },
+ { "Spa:POD:Object:Param:Format:Audio:channels", },
+ { "Spa:POD:Object:Param:Format:Audio:channel-mask", },
+ { "Spa:Enum:VideoFormat:encoded", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "encoded", SPA_VIDEO_FORMAT_ENCODED,},
+ { "Spa:Enum:VideoFormat:I420", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420", SPA_VIDEO_FORMAT_I420,},
+ { "Spa:Enum:VideoFormat:YV12", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YV12", SPA_VIDEO_FORMAT_YV12,},
+ { "Spa:Enum:VideoFormat:YUY2", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUY2", SPA_VIDEO_FORMAT_YUY2,},
+ { "Spa:Enum:VideoFormat:UYVY", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVY", SPA_VIDEO_FORMAT_UYVY,},
+ { "Spa:Enum:VideoFormat:AYUV", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV", SPA_VIDEO_FORMAT_AYUV,},
+ { "Spa:Enum:VideoFormat:RGBx", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx", SPA_VIDEO_FORMAT_RGBx,},
+ { "Spa:Enum:VideoFormat:BGRx", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx", SPA_VIDEO_FORMAT_BGRx,},
+ { "Spa:Enum:VideoFormat:xRGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB", SPA_VIDEO_FORMAT_xRGB,},
+ { "Spa:Enum:VideoFormat:xBGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR", SPA_VIDEO_FORMAT_xBGR,},
+ { "Spa:Enum:VideoFormat:RGBA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA", SPA_VIDEO_FORMAT_RGBA,},
+ { "Spa:Enum:VideoFormat:BGRA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA", SPA_VIDEO_FORMAT_BGRA,},
+ { "Spa:Enum:VideoFormat:ARGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB", SPA_VIDEO_FORMAT_ARGB,},
+ { "Spa:Enum:VideoFormat:ABGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR", SPA_VIDEO_FORMAT_ABGR,},
+ { "Spa:Enum:VideoFormat:RGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB", SPA_VIDEO_FORMAT_RGB,},
+ { "Spa:Enum:VideoFormat:BGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR", SPA_VIDEO_FORMAT_BGR,},
+ { "Spa:Enum:VideoFormat:Y41B", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y41B", SPA_VIDEO_FORMAT_Y41B,},
+ { "Spa:Enum:VideoFormat:Y42B", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y42B", SPA_VIDEO_FORMAT_Y42B,},
+ { "Spa:Enum:VideoFormat:YVYU", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVYU", SPA_VIDEO_FORMAT_YVYU,},
+ { "Spa:Enum:VideoFormat:Y444", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444", SPA_VIDEO_FORMAT_Y444,},
+ { "Spa:Enum:VideoFormat:v210", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v210", SPA_VIDEO_FORMAT_v210,},
+ { "Spa:Enum:VideoFormat:v216", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v216", SPA_VIDEO_FORMAT_v216,},
+ { "Spa:Enum:VideoFormat:NV12", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12", SPA_VIDEO_FORMAT_NV12,},
+ { "Spa:Enum:VideoFormat:NV21", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV21", SPA_VIDEO_FORMAT_NV21,},
+ { "Spa:Enum:VideoFormat:GRAY8", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY8", SPA_VIDEO_FORMAT_GRAY8,},
+ { "Spa:Enum:VideoFormat:GRAY16_BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_BE", SPA_VIDEO_FORMAT_GRAY16_BE,},
+ { "Spa:Enum:VideoFormat:GRAY16_LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_LE", SPA_VIDEO_FORMAT_GRAY16_LE,},
+ { "Spa:Enum:VideoFormat:v308", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v308", SPA_VIDEO_FORMAT_v308,},
+ { "Spa:Enum:VideoFormat:RGB16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB16", SPA_VIDEO_FORMAT_RGB16,},
+ { "Spa:Enum:VideoFormat:BGR16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR16", SPA_VIDEO_FORMAT_BGR16,},
+ { "Spa:Enum:VideoFormat:RGB15", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB15", SPA_VIDEO_FORMAT_RGB15,},
+ { "Spa:Enum:VideoFormat:BGR15", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR15", SPA_VIDEO_FORMAT_BGR15,},
+ { "Spa:Enum:VideoFormat:UYVP", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVP", SPA_VIDEO_FORMAT_UYVP,},
+ { "Spa:Enum:VideoFormat:A420", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420", SPA_VIDEO_FORMAT_A420,},
+ { "Spa:Enum:VideoFormat:RGB8P", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB8P", SPA_VIDEO_FORMAT_RGB8P,},
+ { "Spa:Enum:VideoFormat:YUV9", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUV9", SPA_VIDEO_FORMAT_YUV9,},
+ { "Spa:Enum:VideoFormat:YVU9", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVU9", SPA_VIDEO_FORMAT_YVU9,},
+ { "Spa:Enum:VideoFormat:IYU1", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU1", SPA_VIDEO_FORMAT_IYU1,},
+ { "Spa:Enum:VideoFormat:ARGB64", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB64", SPA_VIDEO_FORMAT_ARGB64,},
+ { "Spa:Enum:VideoFormat:AYUV64", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV64", SPA_VIDEO_FORMAT_AYUV64,},
+ { "Spa:Enum:VideoFormat:r210", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "r210", SPA_VIDEO_FORMAT_r210,},
+ { "Spa:Enum:VideoFormat:I420_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10BE", SPA_VIDEO_FORMAT_I420_10BE,},
+ { "Spa:Enum:VideoFormat:I420_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10LE", SPA_VIDEO_FORMAT_I420_10LE,},
+ { "Spa:Enum:VideoFormat:I422_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10BE", SPA_VIDEO_FORMAT_I422_10BE,},
+ { "Spa:Enum:VideoFormat:I422_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10LE", SPA_VIDEO_FORMAT_I422_10LE,},
+ { "Spa:Enum:VideoFormat:Y444_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10BE", SPA_VIDEO_FORMAT_Y444_10BE,},
+ { "Spa:Enum:VideoFormat:Y444_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10LE", SPA_VIDEO_FORMAT_Y444_10LE,},
+ { "Spa:Enum:VideoFormat:GBR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR", SPA_VIDEO_FORMAT_GBR,},
+ { "Spa:Enum:VideoFormat:GBR_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10BE", SPA_VIDEO_FORMAT_GBR_10BE,},
+ { "Spa:Enum:VideoFormat:GBR_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10LE", SPA_VIDEO_FORMAT_GBR_10LE,},
+ { "Spa:Enum:VideoFormat:NV16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV16", SPA_VIDEO_FORMAT_NV16,},
+ { "Spa:Enum:VideoFormat:NV24", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV24", SPA_VIDEO_FORMAT_NV24,},
+ { "Spa:Enum:VideoFormat:NV12_64Z32", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12_64Z32", SPA_VIDEO_FORMAT_NV12_64Z32,},
+ { "Spa:Enum:VideoFormat:A420_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10BE", SPA_VIDEO_FORMAT_A420_10BE,},
+ { "Spa:Enum:VideoFormat:A420_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10LE", SPA_VIDEO_FORMAT_A420_10LE,},
+ { "Spa:Enum:VideoFormat:A422_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10BE", SPA_VIDEO_FORMAT_A422_10BE,},
+ { "Spa:Enum:VideoFormat:A422_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10LE", SPA_VIDEO_FORMAT_A422_10LE,},
+ { "Spa:Enum:VideoFormat:A444_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10BE", SPA_VIDEO_FORMAT_A444_10BE,},
+ { "Spa:Enum:VideoFormat:A444_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10LE", SPA_VIDEO_FORMAT_A444_10LE,},
+ { "Spa:Enum:VideoFormat:NV61", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV61", SPA_VIDEO_FORMAT_NV61,},
+ { "Spa:Enum:VideoFormat:P010_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10BE", SPA_VIDEO_FORMAT_P010_10BE,},
+ { "Spa:Enum:VideoFormat:P010_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10LE", SPA_VIDEO_FORMAT_P010_10LE,},
+ { "Spa:Enum:VideoFormat:IYU2", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU2", SPA_VIDEO_FORMAT_IYU2,},
+ { "Spa:Enum:VideoFormat:VYUY", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "VYUY", SPA_VIDEO_FORMAT_VYUY,},
+ { "Spa:Enum:VideoFormat:GBRA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA", SPA_VIDEO_FORMAT_GBRA,},
+ { "Spa:Enum:VideoFormat:GBRA_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10BE", SPA_VIDEO_FORMAT_GBRA_10BE,},
+ { "Spa:Enum:VideoFormat:GBRA_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10LE", SPA_VIDEO_FORMAT_GBRA_10LE,},
+ { "Spa:Enum:VideoFormat:GBR_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12BE", SPA_VIDEO_FORMAT_GBR_12BE,},
+ { "Spa:Enum:VideoFormat:GBR_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12LE", SPA_VIDEO_FORMAT_GBR_12LE,},
+ { "Spa:Enum:VideoFormat:GBRA_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12BE", SPA_VIDEO_FORMAT_GBRA_12BE,},
+ { "Spa:Enum:VideoFormat:GBRA_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12LE", SPA_VIDEO_FORMAT_GBRA_12LE,},
+ { "Spa:Enum:VideoFormat:I420_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12BE", SPA_VIDEO_FORMAT_I420_12BE,},
+ { "Spa:Enum:VideoFormat:I420_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12LE", SPA_VIDEO_FORMAT_I420_12LE,},
+ { "Spa:Enum:VideoFormat:I422_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12BE", SPA_VIDEO_FORMAT_I422_12BE,},
+ { "Spa:Enum:VideoFormat:I422_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12LE", SPA_VIDEO_FORMAT_I422_12LE,},
+ { "Spa:Enum:VideoFormat:Y444_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12BE", SPA_VIDEO_FORMAT_Y444_12BE,},
+ { "Spa:Enum:VideoFormat:Y444_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12LE", SPA_VIDEO_FORMAT_Y444_12LE,},
+ { "Spa:Enum:VideoFormat:xRGB_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB_210LE", SPA_VIDEO_FORMAT_xRGB_210LE,},
+ { "Spa:Enum:VideoFormat:xBGR_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR_210LE", SPA_VIDEO_FORMAT_xBGR_210LE,},
+ { "Spa:Enum:VideoFormat:RGBx_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx_102LE", SPA_VIDEO_FORMAT_RGBx_102LE,},
+ { "Spa:Enum:VideoFormat:BGRx_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx_102LE", SPA_VIDEO_FORMAT_BGRx_102LE,},
+ { "Spa:Enum:VideoFormat:ARGB_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB_210LE", SPA_VIDEO_FORMAT_ARGB_210LE,},
+ { "Spa:Enum:VideoFormat:ABGR_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR_210LE", SPA_VIDEO_FORMAT_ABGR_210LE,},
+ { "Spa:Enum:VideoFormat:RGBA_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_102LE", SPA_VIDEO_FORMAT_RGBA_102LE,},
+ { "Spa:Enum:VideoFormat:BGRA_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA_102LE", SPA_VIDEO_FORMAT_BGRA_102LE,},
+ { "Spa:Enum:AudioFormat:ENCODED", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ENCODED", SPA_AUDIO_FORMAT_ENCODED,},
+ { "Spa:Enum:AudioFormat:S8", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S8", SPA_AUDIO_FORMAT_S8, },
+ { "Spa:Enum:AudioFormat:U8", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U8", SPA_AUDIO_FORMAT_U8, },
+ { "Spa:Enum:AudioFormat:S16LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16_LE", SPA_AUDIO_FORMAT_S16_LE, },
+ { "Spa:Enum:AudioFormat:U16LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16_LE", SPA_AUDIO_FORMAT_U16_LE, },
+ { "Spa:Enum:AudioFormat:S24_32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32_LE", SPA_AUDIO_FORMAT_S24_32_LE, },
+ { "Spa:Enum:AudioFormat:U24_32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32_LE", SPA_AUDIO_FORMAT_U24_32_LE, },
+ { "Spa:Enum:AudioFormat:S32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32_LE", SPA_AUDIO_FORMAT_S32_LE, },
+ { "Spa:Enum:AudioFormat:U32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32_LE", SPA_AUDIO_FORMAT_U32_LE, },
+ { "Spa:Enum:AudioFormat:S24LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_LE", SPA_AUDIO_FORMAT_S24_LE, },
+ { "Spa:Enum:AudioFormat:U24LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_LE", SPA_AUDIO_FORMAT_U24_LE, },
+ { "Spa:Enum:AudioFormat:S20LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20_LE", SPA_AUDIO_FORMAT_S20_LE, },
+ { "Spa:Enum:AudioFormat:U20LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20_LE", SPA_AUDIO_FORMAT_U20_LE, },
+ { "Spa:Enum:AudioFormat:S18LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18_LE", SPA_AUDIO_FORMAT_S18_LE, },
+ { "Spa:Enum:AudioFormat:U18LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18_LE", SPA_AUDIO_FORMAT_U18_LE, },
+ { "Spa:Enum:AudioFormat:F32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32_LE", SPA_AUDIO_FORMAT_F32_LE, },
+ { "Spa:Enum:AudioFormat:F64LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64_LE", SPA_AUDIO_FORMAT_F64_LE, },
+ { "Spa:Enum:AudioFormat:S16BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16_BE", SPA_AUDIO_FORMAT_S16_BE, },
+ { "Spa:Enum:AudioFormat:U16BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16_BE", SPA_AUDIO_FORMAT_U16_BE, },
+ { "Spa:Enum:AudioFormat:S24_32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32_BE", SPA_AUDIO_FORMAT_S24_32_BE, },
+ { "Spa:Enum:AudioFormat:U24_32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32_BE", SPA_AUDIO_FORMAT_U24_32_BE, },
+ { "Spa:Enum:AudioFormat:S32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32_BE", SPA_AUDIO_FORMAT_S32_BE, },
+ { "Spa:Enum:AudioFormat:U32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32_BE", SPA_AUDIO_FORMAT_U32_BE, },
+ { "Spa:Enum:AudioFormat:S24BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_BE", SPA_AUDIO_FORMAT_S24_BE, },
+ { "Spa:Enum:AudioFormat:U24BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_BE", SPA_AUDIO_FORMAT_U24_BE, },
+ { "Spa:Enum:AudioFormat:S20BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20_BE", SPA_AUDIO_FORMAT_S20_BE, },
+ { "Spa:Enum:AudioFormat:U20BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20_BE", SPA_AUDIO_FORMAT_U20_BE, },
+ { "Spa:Enum:AudioFormat:S18BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18_BE", SPA_AUDIO_FORMAT_S18_BE, },
+ { "Spa:Enum:AudioFormat:U18BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18_BE", SPA_AUDIO_FORMAT_U18_BE, },
+ { "Spa:Enum:AudioFormat:F32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32_BE", SPA_AUDIO_FORMAT_F32_BE, },
+ { "Spa:Enum:AudioFormat:F64BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64_BE", SPA_AUDIO_FORMAT_F64_BE, },
+ { "Spa:Enum:AudioFormat:F32P", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32P", SPA_AUDIO_FORMAT_F32P, },
+};
diff --git a/src/modules/module-protocol-pulse.c b/src/modules/module-protocol-pulse.c
new file mode 100644
index 0000000..eaf4c2f
--- /dev/null
+++ b/src/modules/module-protocol-pulse.c
@@ -0,0 +1,387 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+
+#include "module-protocol-pulse/pulse-server.h"
+
+/** \page page_module_protocol_pulse PipeWire Module: Protocol Pulse
+ *
+ * This module implements a complete PulseAudio server on top of
+ * PipeWire. This is only the server implementation, client are expected
+ * to use the original PulseAudio client library. This provides a
+ * high level of compatibility with existing applications; in fact,
+ * all usual PulseAudio tools such as pavucontrol, pactl, pamon, paplay
+ * should continue to work as they did before.
+ *
+ * This module is usually loaded as part of a standalone pipewire process,
+ * called pipewire-pulse, with the pipewire-pulse.conf config file.
+ *
+ * The pulse server implements a sample cache that is otherwise not
+ * available in PipeWire.
+ *
+ * ## Module Options
+ *
+ * The module arguments can be the contents of the pulse.properties but
+ * it is recommended to make a separate pulse.properties section in the
+ * config file so that overrides can be done.
+ *
+ * ## pulse.properties
+ *
+ * A config section with server properties can be given.
+ *
+ *\code{.unparsed}
+ * pulse.properties = {
+ * # the addresses this server listens on
+ * server.address = [
+ * "unix:native"
+ * #"unix:/tmp/something" # absolute paths may be used
+ * #"tcp:4713" # IPv4 and IPv6 on all addresses
+ * #"tcp:[::]:9999" # IPv6 on all addresses
+ * #"tcp:127.0.0.1:8888" # IPv4 on a single address
+ * #
+ * #{ address = "tcp:4713" # address
+ * # max-clients = 64 # maximum number of clients
+ * # listen-backlog = 32 # backlog in the server listen queue
+ * # client.access = "restricted" # permissions for clients
+ * #}
+ * ]
+ * #pulse.min.req = 256/48000 # 5ms
+ * #pulse.default.req = 960/48000 # 20 milliseconds
+ * #pulse.min.frag = 256/48000 # 5ms
+ * #pulse.default.frag = 96000/48000 # 2 seconds
+ * #pulse.default.tlength = 96000/48000 # 2 seconds
+ * #pulse.min.quantum = 256/48000 # 5ms
+ * #pulse.default.format = F32
+ * #pulse.default.position = [ FL FR ]
+ * # These overrides are only applied when running in a vm.
+ * vm.overrides = {
+ * pulse.min.quantum = 1024/48000 # 22ms
+ * }
+ * }
+ *\endcode
+ *
+ * ### Connection options
+ *
+ *\code{.unparsed}
+ * ...
+ * server.address = [
+ * "unix:native"
+ * # "tcp:4713"
+ * ]
+ * ...
+ *\endcode
+ *
+ * The addresses the server listens on when starting. Uncomment the `tcp:4713` entry to also
+ * make the server listen on a tcp socket. This is equivalent to loading `module-native-protocol-tcp`.
+ *
+ * There is also a slightly more verbose syntax with more options:
+ *
+ *\code{.unparsed}
+ * ....
+ * server.address = [
+ * { address = "tcp:4713" # address
+ * max-clients = 64 # maximum number of clients
+ * listen-backlog = 32 # backlog in the server listen queue
+ * client.access = "restricted" # permissions for clients
+ * }
+ * ....
+ *\endcode
+ *
+ * Use `client.access` to use one of the access methods to restrict the permissions given to
+ * clients connected via this address.
+ *
+ * By default network access is given the "restricted" permissions. The session manager is responsible
+ * for assigning permission to clients with restricted permissions (usually read-only permissions).
+ *
+ * ### Playback buffering options
+ *
+ *\code{.unparsed}
+ * pulse.min.req = 256/48000 # 5ms
+ *\endcode
+ *
+ * The minimum amount of data to request for clients. The client requested
+ * values will be clamped to this value. Lowering this value together with
+ * tlength can decrease latency if the client wants this, but increase CPU overhead.
+ *
+ *\code{.unparsed}
+ * pulse.default.req = 960/48000 # 20 milliseconds
+ *\endcode
+ *
+ * The default amount of data to request for clients. If the client does not
+ * specify any particular value, this default will be used. Lowering this value
+ * together with tlength can decrease latency but increase CPU overhead.
+ *
+ *\code{.unparsed}
+ * pulse.default.tlength = 96000/48000 # 2 seconds
+ *\endcode
+ *
+ * The target amount of data to buffer on the server side. If the client did not
+ * specify a value, this default will be used. Lower values can decrease the
+ * latency.
+ *
+ * ### Record buffering options
+ *
+ *\code{.unparsed}
+ * pulse.min.frag = 256/48000 # 5ms
+ *\endcode
+ *
+ * The minimum allowed size of the capture buffer before it is sent to a client.
+ * The requested value of the client will be clamped to this. Lowering this value
+ * can reduce latency at the expense of more CPU usage.
+ *
+ *\code{.unparsed}
+ * pulse.default.frag = 96000/48000 # 2 seconds
+ *\endcode
+ *
+ * The default size of the capture buffer before it is sent to a client. If the client
+ * did not specify any value, this default will be used. Lowering this value can
+ * reduce latency at the expense of more CPU usage.
+ *
+ * ### Scheduling options
+ *
+ *\code{.unparsed}
+ * pulse.min.quantum = 256/48000 # 5ms
+ *\endcode
+ *
+ * The minimum quantum (buffer size in samples) to use for pulseaudio clients.
+ * This value is calculated based on the frag and req/tlength for record and
+ * playback streams respectively and then clamped to this value to ensure no
+ * pulseaudio client asks for too small quantums. Lowering this value might
+ * decrease latency at the expense of more CPU usage.
+ *
+ * ### Format options
+ *
+ *\code{.unparsed}
+ * pulse.default.format = F32
+ *\endcode
+ *
+ * Some modules will default to this format when no other format was given. This
+ * is equivalent to the PulseAudio `default-sample-format` option in
+ * `/etc/pulse/daemon.conf`.
+ *
+ *\code{.unparsed}
+ * pulse.default.position = [ FL FR ]
+ *\endcode
+ *
+ * Some modules will default to this channelmap (with its number of channels).
+ * This is equivalent to the PulseAudio `default-sample-channels` and
+ * `default-channel-map` options in `/etc/pulse/daemon.conf`.
+ *
+ * ### VM options
+ *
+ *\code{.unparsed}
+ * vm.overrides = {
+ * pulse.min.quantum = 1024/48000 # 22ms
+ * }
+ *\endcode
+ *
+ * When running in a VM, the `vm.override` section will override the properties
+ * in pulse.properties with the given values. This might be interesting because
+ * VMs usually can't support the low latency settings that are possible on real
+ * hardware.
+ *
+ * ## Stream settings and rules
+ *
+ * Streams created by module-protocol-pulse will use the stream.properties
+ * section and stream.rules sections as usual.
+ *
+ * ## Application settings (Rules)
+ *
+ * The pulse protocol module supports generic config rules. It supports a pulse.rules
+ * section with a `quirks` and an `update-props` action.
+ *
+ *\code{.unparsed}
+ * pulse.rules = [
+ * {
+ * # skype does not want to use devices that don't have an S16 sample format.
+ * matches = [
+ * { application.process.binary = "teams" }
+ * { application.process.binary = "teams-insiders" }
+ * { application.process.binary = "skypeforlinux" }
+ * ]
+ * actions = { quirks = [ force-s16-info ] }
+ * }
+ * {
+ * # speech dispatcher asks for too small latency and then underruns.
+ * matches = [ { application.name = "~speech-dispatcher*" } ]
+ * actions = {
+ * update-props = {
+ * pulse.min.req = 1024/48000 # 21ms
+ * pulse.min.quantum = 1024/48000 # 21ms
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * ### Quirks
+ *
+ * The quirks action takes an array of quirks to apply for the client.
+ *
+ * * `force-s16-info` makes the sink and source introspect code pretend that the sample format
+ * is S16 (16 bits) samples. Some application refuse the sink/source if this
+ * is not the case.
+ * * `remove-capture-dont-move` Removes the DONT_MOVE flag on capture streams. Some applications
+ * set this flag so that the stream can't be moved anymore with tools such as
+ * pavucontrol.
+ *
+ * ### update-props
+ *
+ * Takes an object with the properties to update on the client. Common actions are to
+ * tweak the quantum values.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-protocol-pulse
+ * args = { }
+ * }
+ * ]
+ *
+ * pulse.properties = {
+ * server.address = [ "unix:native" ]
+ * }
+ *
+ * pulse.rules = [
+ * {
+ * # skype does not want to use devices that don't have an S16 sample format.
+ * matches = [
+ * { application.process.binary = "teams" }
+ * { application.process.binary = "teams-insiders" }
+ * { application.process.binary = "skypeforlinux" }
+ * ]
+ * actions = { quirks = [ force-s16-info ] }
+ * }
+ * {
+ * # speech dispatcher asks for too small latency and then underruns.
+ * matches = [ { application.name = "~speech-dispatcher*" } ]
+ * actions = {
+ * update-props = {
+ * pulse.min.req = 1024/48000 # 21ms
+ * pulse.min.quantum = 1024/48000 # 21ms
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "protocol-pulse"
+
+PW_LOG_TOPIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+PW_LOG_TOPIC(pulse_conn, "conn." NAME);
+PW_LOG_TOPIC(pulse_ext_dev_restore, "mod." NAME ".device-restore");
+PW_LOG_TOPIC(pulse_ext_stream_restore, "mod." NAME ".stream-restore");
+
+#define MODULE_USAGE PW_PROTOCOL_PULSE_USAGE
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Implement a PulseAudio server" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct spa_hook module_listener;
+
+ struct pw_protocol_pulse *pulse;
+};
+
+static void impl_free(struct impl *impl)
+{
+ spa_hook_remove(&impl->module_listener);
+ if (impl->pulse)
+ pw_protocol_pulse_destroy(impl->pulse);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ pw_log_debug("module %p: destroy", impl);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+ PW_LOG_TOPIC_INIT(pulse_conn);
+ /* it's easier to init these here than adding an init() call to the
+ * extensions */
+ PW_LOG_TOPIC_INIT(pulse_ext_dev_restore);
+ PW_LOG_TOPIC_INIT(pulse_ext_stream_restore);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = NULL;
+
+ impl->pulse = pw_protocol_pulse_new(context, props, 0);
+ if (impl->pulse == NULL) {
+ res = -errno;
+ free(impl);
+ return res;
+ }
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+}
diff --git a/src/modules/module-protocol-pulse/client.c b/src/modules/module-protocol-pulse/client.c
new file mode 100644
index 0000000..1e6d202
--- /dev/null
+++ b/src/modules/module-protocol-pulse/client.c
@@ -0,0 +1,390 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/list.h>
+#include <pipewire/core.h>
+#include <pipewire/log.h>
+#include <pipewire/loop.h>
+#include <pipewire/map.h>
+#include <pipewire/properties.h>
+
+#include "client.h"
+#include "commands.h"
+#include "defs.h"
+#include "internal.h"
+#include "log.h"
+#include "manager.h"
+#include "message.h"
+#include "operation.h"
+#include "pending-sample.h"
+#include "server.h"
+#include "stream.h"
+
+#define client_emit_disconnect(c) spa_hook_list_call(&(c)->listener_list, struct client_events, disconnect, 0)
+
+struct client *client_new(struct server *server)
+{
+ struct client *client = calloc(1, sizeof(*client));
+ if (client == NULL)
+ return NULL;
+
+ client->ref = 1;
+ client->server = server;
+ client->impl = server->impl;
+ client->connect_tag = SPA_ID_INVALID;
+
+ pw_map_init(&client->streams, 16, 16);
+ spa_list_init(&client->out_messages);
+ spa_list_init(&client->operations);
+ spa_list_init(&client->pending_samples);
+ spa_list_init(&client->pending_streams);
+ spa_hook_list_init(&client->listener_list);
+
+ spa_list_append(&server->clients, &client->link);
+ server->n_clients++;
+
+ return client;
+}
+
+static int client_free_stream(void *item, void *data)
+{
+ struct stream *s = item;
+
+ stream_free(s);
+ return 0;
+}
+
+/*
+ * tries to detach the client from the server,
+ * but it does not drop the server's reference
+ */
+bool client_detach(struct client *client)
+{
+ struct impl *impl = client->impl;
+ struct server *server = client->server;
+
+ if (server == NULL)
+ return false;
+
+ pw_log_debug("client %p: detaching from server %p", client, server);
+
+ /* remove from the `server->clients` list */
+ spa_list_remove(&client->link);
+ spa_list_append(&impl->cleanup_clients, &client->link);
+
+ server->n_clients--;
+ if (server->wait_clients > 0 && --server->wait_clients == 0) {
+ int mask = server->source->mask;
+ SPA_FLAG_SET(mask, SPA_IO_IN);
+ pw_loop_update_io(impl->loop, server->source, mask);
+ }
+
+ client->server = NULL;
+
+ return true;
+}
+
+void client_disconnect(struct client *client)
+{
+ struct impl *impl = client->impl;
+
+ if (client->disconnect)
+ return;
+
+ client_emit_disconnect(client);
+
+ /* the client must be detached from the server to disconnect */
+ spa_assert(client->server == NULL);
+
+ client->disconnect = true;
+
+ pw_map_for_each(&client->streams, client_free_stream, client);
+
+ if (client->source) {
+ pw_loop_destroy_source(impl->loop, client->source);
+ client->source = NULL;
+ }
+
+ if (client->manager) {
+ pw_manager_destroy(client->manager);
+ client->manager = NULL;
+ }
+}
+
+void client_free(struct client *client)
+{
+ struct impl *impl = client->impl;
+ struct pending_sample *p;
+ struct message *msg;
+ struct operation *o;
+
+ pw_log_debug("client %p: free", client);
+
+ client_detach(client);
+ client_disconnect(client);
+
+ /* remove from the `impl->cleanup_clients` list */
+ spa_list_remove(&client->link);
+
+ spa_list_consume(p, &client->pending_samples, link)
+ pending_sample_free(p);
+
+ if (client->message)
+ message_free(client->message, false, false);
+
+ spa_list_consume(msg, &client->out_messages, link)
+ message_free(msg, true, false);
+
+ spa_list_consume(o, &client->operations, link)
+ operation_free(o);
+
+ if (client->core)
+ pw_core_disconnect(client->core);
+
+ pw_map_clear(&client->streams);
+
+ pw_work_queue_cancel(impl->work_queue, client, SPA_ID_INVALID);
+
+ free(client->default_sink);
+ free(client->default_source);
+
+ free(client->temporary_default_sink);
+ free(client->temporary_default_source);
+
+ pw_properties_free(client->props);
+ pw_properties_free(client->routes);
+
+ spa_hook_list_clean(&client->listener_list);
+
+ free(client);
+}
+
+int client_queue_message(struct client *client, struct message *msg)
+{
+ struct impl *impl = client->impl;
+ int res;
+
+ if (msg == NULL)
+ return -EINVAL;
+
+ if (client->disconnect) {
+ res = -ENOTCONN;
+ goto error;
+ }
+
+ if (msg->length == 0) {
+ res = 0;
+ goto error;
+ } else if (msg->length > msg->allocated) {
+ res = -ENOMEM;
+ goto error;
+ }
+
+ msg->offset = 0;
+ spa_list_append(&client->out_messages, &msg->link);
+
+ uint32_t mask = client->source->mask;
+ if (!SPA_FLAG_IS_SET(mask, SPA_IO_OUT)) {
+ SPA_FLAG_SET(mask, SPA_IO_OUT);
+ pw_loop_update_io(impl->loop, client->source, mask);
+ }
+
+ client->new_msg_since_last_flush = true;
+
+ return 0;
+
+error:
+ message_free(msg, false, false);
+ return res;
+}
+
+static int client_try_flush_messages(struct client *client)
+{
+ pw_log_trace("client %p: flushing", client);
+
+ spa_assert(!client->disconnect);
+
+ while (!spa_list_is_empty(&client->out_messages)) {
+ struct message *m = spa_list_first(&client->out_messages, struct message, link);
+ struct descriptor desc;
+ const void *data;
+ size_t size;
+
+ if (client->out_index < sizeof(desc)) {
+ desc.length = htonl(m->length);
+ desc.channel = htonl(m->channel);
+ desc.offset_hi = 0;
+ desc.offset_lo = 0;
+ desc.flags = 0;
+
+ data = SPA_PTROFF(&desc, client->out_index, void);
+ size = sizeof(desc) - client->out_index;
+ } else if (client->out_index < m->length + sizeof(desc)) {
+ uint32_t idx = client->out_index - sizeof(desc);
+ data = m->data + idx;
+ size = m->length - idx;
+ } else {
+ if (debug_messages && m->channel == SPA_ID_INVALID)
+ message_dump(SPA_LOG_LEVEL_INFO, m);
+ message_free(m, true, false);
+ client->out_index = 0;
+ continue;
+ }
+
+ while (true) {
+ ssize_t sent = send(client->source->fd, data, size, MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (sent < 0) {
+ int res = -errno;
+ if (res == -EINTR)
+ continue;
+ return res;
+ }
+ client->out_index += sent;
+ break;
+ }
+ }
+ return 0;
+}
+
+int client_flush_messages(struct client *client)
+{
+ client->new_msg_since_last_flush = false;
+
+ int res = client_try_flush_messages(client);
+ if (res >= 0) {
+ uint32_t mask = client->source->mask;
+
+ if (SPA_FLAG_IS_SET(mask, SPA_IO_OUT)) {
+ SPA_FLAG_CLEAR(mask, SPA_IO_OUT);
+ pw_loop_update_io(client->impl->loop, client->source, mask);
+ }
+ } else {
+ if (res != -EAGAIN && res != -EWOULDBLOCK)
+ return res;
+ }
+ return 0;
+}
+
+static bool drop_from_out_queue(struct client *client, struct message *m)
+{
+ spa_assert(!spa_list_is_empty(&client->out_messages));
+
+ struct message *first = spa_list_first(&client->out_messages, struct message, link);
+ if (m == first && client->out_index > 0)
+ return false;
+
+ message_free(m, true, false);
+
+ return true;
+}
+
+/* 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)
+{
+ struct message *m, *t;
+
+ if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_NEW)
+ return false;
+
+ /* NOTE: reverse iteration */
+ spa_list_for_each_safe_reverse(m, t, &client->out_messages, link) {
+ if (m->extra[0] != COMMAND_SUBSCRIBE_EVENT)
+ continue;
+ if ((m->extra[1] ^ event) & SUBSCRIPTION_EVENT_FACILITY_MASK)
+ continue;
+ if (m->extra[2] != index)
+ continue;
+
+ if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_REMOVE) {
+ /* This object is being removed, hence there is
+ * point in keeping the old events regarding
+ * entry in the queue. */
+
+ bool is_new = (m->extra[1] & SUBSCRIPTION_EVENT_TYPE_MASK) == SUBSCRIPTION_EVENT_NEW;
+
+ if (drop_from_out_queue(client, m)) {
+ pw_log_debug("client %p: dropped redundant event due to remove event for object %u",
+ client, index);
+
+ /* if the NEW event for the current object could successfully be dropped,
+ there is no need to deliver the REMOVE event */
+ if (is_new)
+ goto drop;
+ }
+
+ /* stop if the NEW event for the current object is reached */
+ if (is_new)
+ break;
+ }
+
+ if ((event & SUBSCRIPTION_EVENT_TYPE_MASK) == 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;
+ }
+ }
+
+ return false;
+
+drop:
+ pw_log_debug("client %p: dropped redundant event for object %u", client, index);
+
+ return true;
+}
+
+int client_queue_subscribe_event(struct client *client, uint32_t mask, uint32_t event, uint32_t index)
+{
+ if (client->disconnect)
+ return -ENOTCONN;
+
+ if (!(client->subscribed & mask))
+ return 0;
+
+ pw_log_debug("client %p: SUBSCRIBE event:%08x index:%u", client, event, index);
+
+ if (client_prune_subscribe_events(client, event, index))
+ return 0;
+
+ struct message *reply = message_alloc(client->impl, -1, 0);
+ reply->extra[0] = COMMAND_SUBSCRIBE_EVENT;
+ reply->extra[1] = event;
+ reply->extra[2] = index;
+
+ message_put(reply,
+ TAG_U32, COMMAND_SUBSCRIBE_EVENT,
+ TAG_U32, -1,
+ TAG_U32, event,
+ TAG_U32, index,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
diff --git a/src/modules/module-protocol-pulse/client.h b/src/modules/module-protocol-pulse/client.h
new file mode 100644
index 0000000..ed0ee81
--- /dev/null
+++ b/src/modules/module-protocol-pulse/client.h
@@ -0,0 +1,136 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_CLIENT_H
+#define PULSER_SERVER_CLIENT_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+#include <pipewire/map.h>
+
+struct impl;
+struct server;
+struct message;
+struct spa_source;
+struct pw_properties;
+struct pw_core;
+struct pw_manager;
+struct pw_manager_object;
+struct pw_properties;
+
+struct descriptor {
+ uint32_t length;
+ uint32_t channel;
+ uint32_t offset_hi;
+ uint32_t offset_lo;
+ uint32_t flags;
+};
+
+struct client {
+ struct spa_list link;
+ struct impl *impl;
+ struct server *server;
+
+ int ref;
+ const char *name; /* owned by `client::props` */
+
+ struct spa_source *source;
+
+ uint32_t version;
+
+ struct pw_properties *props;
+
+ uint64_t quirks;
+
+ struct pw_core *core;
+ struct pw_manager *manager;
+ struct spa_hook manager_listener;
+
+ uint32_t subscribed;
+
+ struct pw_manager_object *metadata_default;
+ char *default_sink;
+ char *default_source;
+ char *temporary_default_sink; /**< pending value, for MOVE_* commands */
+ char *temporary_default_source; /**< pending value, for MOVE_* commands */
+ struct pw_manager_object *metadata_routes;
+ struct pw_properties *routes;
+
+ uint32_t connect_tag;
+
+ uint32_t in_index;
+ uint32_t out_index;
+ struct descriptor desc;
+ struct message *message;
+
+ struct pw_map streams;
+ struct spa_list out_messages;
+
+ struct spa_list operations;
+
+ struct spa_list pending_samples;
+
+ struct spa_list pending_streams;
+
+ unsigned int disconnect:1;
+ unsigned int new_msg_since_last_flush:1;
+ unsigned int authenticated:1;
+
+ struct pw_manager_object *prev_default_sink;
+ struct pw_manager_object *prev_default_source;
+
+ struct spa_hook_list listener_list;
+};
+
+struct client_events {
+#define VERSION_CLIENT_EVENTS 0
+ uint32_t version;
+
+ void (*disconnect) (void *data);
+};
+
+struct client *client_new(struct server *server);
+bool client_detach(struct client *client);
+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);
+
+static inline void client_unref(struct client *client)
+{
+ if (--client->ref == 0)
+ client_free(client);
+}
+
+static inline void client_add_listener(struct client *client, struct spa_hook *listener,
+ const struct client_events *events, void *data)
+{
+ spa_hook_list_append(&client->listener_list, listener, events, data);
+}
+
+#endif /* PULSER_SERVER_CLIENT_H */
diff --git a/src/modules/module-protocol-pulse/cmd.c b/src/modules/module-protocol-pulse/cmd.c
new file mode 100644
index 0000000..e8e6406
--- /dev/null
+++ b/src/modules/module-protocol-pulse/cmd.c
@@ -0,0 +1,136 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/json.h>
+
+#include <pipewire/utils.h>
+
+#include "module.h"
+#include "cmd.h"
+
+static const char WHITESPACE[] = " \t\n\r";
+
+static int do_load_module(struct impl *impl, char *args, const char *flags)
+{
+ int res, n;
+ struct module *module;
+ char *a[2] = { NULL };
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ pw_log_info("load-module expects module name");
+ return -EINVAL;
+ }
+
+ module = module_create(impl, a[0], a[1]);
+ if (module == NULL)
+ return -errno;
+ if ((res = module_load(module)) < 0)
+ return res;
+
+ return res;
+}
+
+static int do_cmd(struct impl *impl, const char *cmd, char *args, const char *flags)
+{
+ int res = 0;
+ if (spa_streq(cmd, "load-module")) {
+ res = do_load_module(impl, args, flags);
+ } else {
+ pw_log_warn("ignoring unknown command `%s` with args `%s`",
+ cmd, args);
+ }
+ if (res < 0) {
+ if (flags && strstr(flags, "nofail")) {
+ pw_log_info("nofail command %s %s: %s",
+ cmd, args, spa_strerror(res));
+ res = 0;
+ } else {
+ pw_log_error("can't run command %s %s: %s",
+ cmd, args, spa_strerror(res));
+ }
+ }
+ return res;
+}
+
+/*
+ * pulse.cmd = [
+ * { cmd = <command> [ args = "<arguments>" ] }
+ * ...
+ * ]
+ */
+static int parse_cmd(void *user_data, const char *location,
+ const char *section, const char *str, size_t len)
+{
+ struct impl *impl = user_data;
+ struct spa_json it[3];
+ char key[512], *s;
+ int res = 0;
+
+ s = strndup(str, len);
+ spa_json_init(&it[0], s, len);
+ if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+ pw_log_error("config file error: pulse.cmd is not an array");
+ res = -EINVAL;
+ goto exit;
+ }
+
+ while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+ char *cmd = NULL, *args = NULL, *flags = NULL;
+
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(&it[2], &val)) <= 0)
+ break;
+
+ if (spa_streq(key, "cmd")) {
+ cmd = (char*)val;
+ spa_json_parse_stringn(val, len, cmd, len+1);
+ } else if (spa_streq(key, "args")) {
+ args = (char*)val;
+ spa_json_parse_stringn(val, len, args, len+1);
+ } else if (spa_streq(key, "flags")) {
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(&it[2], val, len);
+ flags = (char*)val;
+ spa_json_parse_stringn(val, len, flags, len+1);
+ }
+ }
+ if (cmd != NULL)
+ res = do_cmd(impl, cmd, args, flags);
+ if (res < 0)
+ break;
+ }
+exit:
+ free(s);
+ return res;
+}
+
+int cmd_run(struct impl *impl)
+{
+ return pw_context_conf_section_for_each(impl->context, "pulse.cmd",
+ parse_cmd, impl);
+}
diff --git a/src/modules/module-protocol-pulse/cmd.h b/src/modules/module-protocol-pulse/cmd.h
new file mode 100644
index 0000000..81f528c
--- /dev/null
+++ b/src/modules/module-protocol-pulse/cmd.h
@@ -0,0 +1,32 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_CMD_H
+#define PULSER_SERVER_CMD_H
+
+#include "internal.h"
+
+int cmd_run(struct impl *impl);
+
+#endif /* PULSER_SERVER_CMD_H */
diff --git a/src/modules/module-protocol-pulse/collect.c b/src/modules/module-protocol-pulse/collect.c
new file mode 100644
index 0000000..ac6fd6e
--- /dev/null
+++ b/src/modules/module-protocol-pulse/collect.c
@@ -0,0 +1,549 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/props.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/string.h>
+#include <pipewire/pipewire.h>
+
+#include "collect.h"
+#include "defs.h"
+#include "log.h"
+#include "manager.h"
+
+void select_best(struct selector *s, struct pw_manager_object *o)
+{
+ int32_t prio = 0;
+
+ if (o->props &&
+ pw_properties_fetch_int32(o->props, PW_KEY_PRIORITY_SESSION, &prio) == 0) {
+ if (s->best == NULL || prio > s->score) {
+ s->best = o;
+ s->score = prio;
+ }
+ }
+}
+
+struct pw_manager_object *select_object(struct pw_manager *m, struct selector *s)
+{
+ struct pw_manager_object *o;
+ const char *str;
+
+ spa_list_for_each(o, &m->object_list, link) {
+ if (o->creating || o->removing)
+ continue;
+ if (s->type != NULL && !s->type(o))
+ continue;
+ if (o->id == s->id)
+ return o;
+ if (o->index == s->index)
+ return o;
+ if (s->accumulate)
+ s->accumulate(s, o);
+ if (o->props && s->key != NULL && s->value != NULL &&
+ (str = pw_properties_get(o->props, s->key)) != NULL &&
+ spa_streq(str, s->value))
+ return o;
+ if (s->value != NULL && (uint32_t)atoi(s->value) == o->index)
+ return o;
+ }
+ return s->best;
+}
+
+uint32_t id_to_index(struct pw_manager *m, uint32_t id)
+{
+ struct pw_manager_object *o;
+ spa_list_for_each(o, &m->object_list, link) {
+ if (o->id == id)
+ return o->index;
+ }
+ return SPA_ID_INVALID;
+}
+
+bool collect_is_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction)
+{
+ struct pw_manager_object *o;
+ uint32_t in_node, out_node;
+
+ spa_list_for_each(o, &m->object_list, link) {
+ if (o->props == NULL || !pw_manager_object_is_link(o))
+ continue;
+
+ if (pw_properties_fetch_uint32(o->props, PW_KEY_LINK_OUTPUT_NODE, &out_node) != 0 ||
+ pw_properties_fetch_uint32(o->props, PW_KEY_LINK_INPUT_NODE, &in_node) != 0)
+ continue;
+
+ if ((direction == PW_DIRECTION_OUTPUT && id == out_node) ||
+ (direction == PW_DIRECTION_INPUT && id == in_node))
+ return true;
+ }
+ return false;
+}
+
+struct pw_manager_object *find_peer_for_link(struct pw_manager *m,
+ struct pw_manager_object *o, uint32_t id, enum pw_direction direction)
+{
+ struct pw_manager_object *p;
+ uint32_t in_node, out_node;
+
+ if (o->props == NULL)
+ return NULL;
+
+ if (pw_properties_fetch_uint32(o->props, PW_KEY_LINK_OUTPUT_NODE, &out_node) != 0 ||
+ pw_properties_fetch_uint32(o->props, PW_KEY_LINK_INPUT_NODE, &in_node) != 0)
+ return NULL;
+
+ if (direction == PW_DIRECTION_OUTPUT && id == out_node) {
+ struct selector sel = { .id = in_node, .type = pw_manager_object_is_sink, };
+ if ((p = select_object(m, &sel)) != NULL)
+ return p;
+ }
+ if (direction == PW_DIRECTION_INPUT && id == in_node) {
+ struct selector sel = { .id = out_node, .type = pw_manager_object_is_recordable, };
+ if ((p = select_object(m, &sel)) != NULL)
+ return p;
+ }
+ return NULL;
+}
+
+struct pw_manager_object *find_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction)
+{
+ struct pw_manager_object *o, *p;
+
+ spa_list_for_each(o, &m->object_list, link) {
+ if (!pw_manager_object_is_link(o))
+ continue;
+ if ((p = find_peer_for_link(m, o, id, direction)) != NULL)
+ return p;
+ }
+ return NULL;
+}
+
+void collect_card_info(struct pw_manager_object *card, struct card_info *info)
+{
+ struct pw_manager_param *p;
+
+ spa_list_for_each(p, &card->param_list, link) {
+ switch (p->id) {
+ case SPA_PARAM_EnumProfile:
+ info->n_profiles++;
+ break;
+ case SPA_PARAM_Profile:
+ spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&info->active_profile));
+ break;
+ case SPA_PARAM_EnumRoute:
+ info->n_ports++;
+ break;
+ }
+ }
+}
+
+uint32_t collect_profile_info(struct pw_manager_object *card, struct card_info *card_info,
+ struct profile_info *profile_info)
+{
+ struct pw_manager_param *p;
+ struct profile_info *pi;
+ uint32_t n;
+
+ n = 0;
+ spa_list_for_each(p, &card->param_list, link) {
+ struct spa_pod *classes = NULL;
+
+ if (p->id != SPA_PARAM_EnumProfile)
+ continue;
+
+ pi = &profile_info[n];
+ spa_zero(*pi);
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&pi->index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(&pi->name),
+ SPA_PARAM_PROFILE_description, SPA_POD_OPT_String(&pi->description),
+ SPA_PARAM_PROFILE_priority, SPA_POD_OPT_Int(&pi->priority),
+ SPA_PARAM_PROFILE_available, SPA_POD_OPT_Id(&pi->available),
+ SPA_PARAM_PROFILE_classes, SPA_POD_OPT_Pod(&classes)) < 0) {
+ continue;
+ }
+ if (pi->description == NULL)
+ pi->description = pi->name;
+ if (pi->index == card_info->active_profile)
+ card_info->active_profile_name = pi->name;
+
+ if (classes != NULL) {
+ struct spa_pod *iter;
+
+ SPA_POD_STRUCT_FOREACH(classes, iter) {
+ struct spa_pod_parser prs;
+ char *class;
+ uint32_t count;
+
+ spa_pod_parser_pod(&prs, iter);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_String(&class),
+ SPA_POD_Int(&count)) < 0)
+ continue;
+
+ if (spa_streq(class, "Audio/Sink"))
+ pi->n_sinks += count;
+ else if (spa_streq(class, "Audio/Source"))
+ pi->n_sources += count;
+ }
+ }
+ n++;
+ }
+ if (card_info->active_profile_name == NULL && n > 0)
+ card_info->active_profile_name = profile_info[0].name;
+
+ return n;
+}
+
+uint32_t find_profile_index(struct pw_manager_object *card, const char *name)
+{
+ struct pw_manager_param *p;
+
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t index;
+ const char *test_name;
+
+ if (p->id != SPA_PARAM_EnumProfile)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(&test_name)) < 0)
+ continue;
+
+ if (spa_streq(test_name, name))
+ return index;
+
+ }
+ return SPA_ID_INVALID;
+}
+
+void collect_device_info(struct pw_manager_object *device, struct pw_manager_object *card,
+ struct device_info *dev_info, bool monitor, struct defs *defs)
+{
+ struct pw_manager_param *p;
+
+ if (card && !monitor) {
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t index, dev;
+ struct spa_pod *props;
+
+ if (p->id != SPA_PARAM_Route)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&index),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&dev),
+ SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props)) < 0)
+ continue;
+ if (dev != dev_info->device)
+ continue;
+ dev_info->active_port = index;
+ if (props) {
+ volume_parse_param(props, &dev_info->volume_info, monitor);
+ dev_info->have_volume = true;
+ }
+ }
+ }
+
+ spa_list_for_each(p, &device->param_list, link) {
+ switch (p->id) {
+ case SPA_PARAM_EnumFormat:
+ {
+ struct spa_pod *copy = spa_pod_copy(p->param);
+ spa_pod_fixate(copy);
+ format_parse_param(copy, true, &dev_info->ss, &dev_info->map,
+ &defs->sample_spec, &defs->channel_map);
+ free(copy);
+ break;
+ }
+ case SPA_PARAM_Format:
+ format_parse_param(p->param, true, &dev_info->ss, &dev_info->map,
+ NULL, NULL);
+ break;
+
+ case SPA_PARAM_Props:
+ if (!dev_info->have_volume) {
+ volume_parse_param(p->param, &dev_info->volume_info, monitor);
+ dev_info->have_volume = true;
+ }
+ dev_info->have_iec958codecs = spa_pod_find_prop(p->param,
+ NULL, SPA_PROP_iec958Codecs) != NULL;
+ break;
+ }
+ }
+ if (dev_info->ss.channels != dev_info->map.channels)
+ dev_info->ss.channels = dev_info->map.channels;
+ if (dev_info->volume_info.volume.channels != dev_info->map.channels)
+ dev_info->volume_info.volume.channels = dev_info->map.channels;
+}
+
+static bool array_contains(uint32_t *vals, uint32_t n_vals, uint32_t val)
+{
+ uint32_t n;
+ if (vals == NULL || n_vals == 0)
+ return false;
+ for (n = 0; n < n_vals; n++)
+ if (vals[n] == val)
+ return true;
+ return false;
+}
+
+uint32_t collect_port_info(struct pw_manager_object *card, struct card_info *card_info,
+ struct device_info *dev_info, struct port_info *port_info)
+{
+ struct pw_manager_param *p;
+ uint32_t n;
+
+ if (card == NULL)
+ return 0;
+
+ n = 0;
+ spa_list_for_each(p, &card->param_list, link) {
+ struct spa_pod *devices = NULL, *profiles = NULL;
+ struct port_info *pi;
+
+ if (p->id != SPA_PARAM_EnumRoute)
+ continue;
+
+ pi = &port_info[n];
+ spa_zero(*pi);
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&pi->index),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(&pi->direction),
+ SPA_PARAM_ROUTE_name, SPA_POD_String(&pi->name),
+ SPA_PARAM_ROUTE_description, SPA_POD_OPT_String(&pi->description),
+ SPA_PARAM_ROUTE_priority, SPA_POD_OPT_Int(&pi->priority),
+ SPA_PARAM_ROUTE_available, SPA_POD_OPT_Id(&pi->available),
+ SPA_PARAM_ROUTE_info, SPA_POD_OPT_Pod(&pi->info),
+ SPA_PARAM_ROUTE_devices, SPA_POD_OPT_Pod(&devices),
+ SPA_PARAM_ROUTE_profiles, SPA_POD_OPT_Pod(&profiles)) < 0)
+ continue;
+
+ if (pi->description == NULL)
+ pi->description = pi->name;
+ if (devices)
+ pi->devices = spa_pod_get_array(devices, &pi->n_devices);
+ if (profiles)
+ pi->profiles = spa_pod_get_array(profiles, &pi->n_profiles);
+
+ if (dev_info != NULL) {
+ if (pi->direction != dev_info->direction)
+ continue;
+ if (!array_contains(pi->profiles, pi->n_profiles, card_info->active_profile))
+ continue;
+ if (!array_contains(pi->devices, pi->n_devices, dev_info->device))
+ continue;
+ if (pi->index == dev_info->active_port)
+ dev_info->active_port_name = pi->name;
+ }
+
+ while (pi->info != NULL) {
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[1];
+ uint32_t n;
+ const char *key, *value;
+
+ spa_pod_parser_pod(&prs, pi->info);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get_int(&prs, (int32_t*)&pi->n_props) < 0)
+ break;
+
+ for (n = 0; n < pi->n_props; n++) {
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_String(&key),
+ SPA_POD_String(&value),
+ NULL) < 0)
+ break;
+ if (spa_streq(key, "port.availability-group"))
+ pi->availability_group = value;
+ else if (spa_streq(key, "port.type"))
+ pi->type = port_type_value(value);
+ }
+ spa_pod_parser_pop(&prs, &f[0]);
+ break;
+ }
+ n++;
+ }
+ if (dev_info != NULL && dev_info->active_port_name == NULL && n > 0)
+ dev_info->active_port_name = port_info[0].name;
+ return n;
+}
+
+uint32_t find_port_index(struct pw_manager_object *card, uint32_t direction, const char *port_name)
+{
+ struct pw_manager_param *p;
+
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t index, dir;
+ const char *name;
+
+ if (p->id != SPA_PARAM_EnumRoute)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&index),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(&dir),
+ SPA_PARAM_ROUTE_name, SPA_POD_String(&name)) < 0)
+ continue;
+ if (dir != direction)
+ continue;
+ if (spa_streq(name, port_name))
+ return index;
+
+ }
+ return SPA_ID_INVALID;
+}
+
+struct spa_dict *collect_props(struct spa_pod *info, struct spa_dict *dict)
+{
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f[1];
+ int32_t n, n_items;
+
+ spa_pod_parser_pod(&prs, info);
+ if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 ||
+ spa_pod_parser_get_int(&prs, &n_items) < 0)
+ return NULL;
+
+ for (n = 0; n < n_items; n++) {
+ if (spa_pod_parser_get(&prs,
+ SPA_POD_String(&dict->items[n].key),
+ SPA_POD_String(&dict->items[n].value),
+ NULL) < 0)
+ break;
+ }
+ spa_pod_parser_pop(&prs, &f[0]);
+ dict->n_items = n;
+ return dict;
+}
+
+uint32_t collect_transport_codec_info(struct pw_manager_object *card,
+ struct transport_codec_info *codecs, uint32_t max_codecs,
+ uint32_t *active)
+{
+ struct pw_manager_param *p;
+ uint32_t n_codecs = 0;
+
+ *active = SPA_ID_INVALID;
+
+ if (card == NULL)
+ return 0;
+
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t iid;
+ const struct spa_pod_choice *type;
+ const struct spa_pod_struct *labels;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ int32_t *id;
+ bool first;
+
+ if (p->id != SPA_PARAM_PropInfo)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_id, SPA_POD_Id(&iid),
+ SPA_PROP_INFO_type, SPA_POD_PodChoice(&type),
+ SPA_PROP_INFO_labels, SPA_POD_PodStruct(&labels)) < 0)
+ continue;
+
+ if (iid != SPA_PROP_bluetoothAudioCodec)
+ continue;
+
+ if (SPA_POD_CHOICE_TYPE(type) != SPA_CHOICE_Enum ||
+ SPA_POD_TYPE(SPA_POD_CHOICE_CHILD(type)) != SPA_TYPE_Int)
+ continue;
+
+ /*
+ * XXX: PropInfo currently uses Int, not Id, in type and labels.
+ */
+
+ /* Codec name list */
+ first = true;
+ SPA_POD_CHOICE_FOREACH(type, id) {
+ if (first) {
+ /* Skip default */
+ first = false;
+ continue;
+ }
+ if (n_codecs >= max_codecs)
+ break;
+ codecs[n_codecs++].id = *id;
+ }
+
+ /* Codec description list */
+ spa_pod_parser_pod(&prs, (struct spa_pod *)labels);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0)
+ continue;
+
+ while (1) {
+ int32_t id;
+ const char *desc;
+ uint32_t j;
+
+ if (spa_pod_parser_get_int(&prs, &id) < 0 ||
+ spa_pod_parser_get_string(&prs, &desc) < 0)
+ break;
+
+ for (j = 0; j < n_codecs; ++j) {
+ if (codecs[j].id == (uint32_t)id)
+ codecs[j].description = desc;
+ }
+ }
+ }
+
+ /* Active codec */
+ spa_list_for_each(p, &card->param_list, link) {
+ uint32_t j;
+ uint32_t id;
+
+ if (p->id != SPA_PARAM_Props)
+ continue;
+
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(&id)) < 0)
+ continue;
+
+ for (j = 0; j < n_codecs; ++j) {
+ if (codecs[j].id == id)
+ *active = j;
+ }
+ }
+
+ return n_codecs;
+}
diff --git a/src/modules/module-protocol-pulse/collect.h b/src/modules/module-protocol-pulse/collect.h
new file mode 100644
index 0000000..d4b85ab
--- /dev/null
+++ b/src/modules/module-protocol-pulse/collect.h
@@ -0,0 +1,166 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_COLLECT_H
+#define PULSE_SERVER_COLLECT_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/param/bluetooth/audio.h>
+#include <pipewire/pipewire.h>
+
+#include "internal.h"
+#include "format.h"
+#include "volume.h"
+
+struct pw_manager;
+struct pw_manager_object;
+
+/* ========================================================================== */
+
+struct selector {
+ bool (*type) (struct pw_manager_object *o);
+ uint32_t id;
+ uint32_t index;
+ const char *key;
+ const char *value;
+ void (*accumulate) (struct selector *sel, struct pw_manager_object *o);
+ int32_t score;
+ struct pw_manager_object *best;
+};
+
+struct pw_manager_object *select_object(struct pw_manager *m, struct selector *s);
+uint32_t id_to_index(struct pw_manager *m, uint32_t id);
+void select_best(struct selector *s, struct pw_manager_object *o);
+
+/* ========================================================================== */
+
+struct device_info {
+ uint32_t direction;
+
+ struct sample_spec ss;
+ struct channel_map map;
+ struct volume_info volume_info;
+ unsigned int have_volume:1;
+ unsigned int have_iec958codecs:1;
+
+ uint32_t device;
+ uint32_t active_port;
+ const char *active_port_name;
+};
+
+#define DEVICE_INFO_INIT(_dir) \
+ (struct device_info) { \
+ .direction = _dir, \
+ .ss = SAMPLE_SPEC_INIT, \
+ .map = CHANNEL_MAP_INIT, \
+ .volume_info = VOLUME_INFO_INIT, \
+ .device = SPA_ID_INVALID, \
+ .active_port = SPA_ID_INVALID, \
+ }
+
+void collect_device_info(struct pw_manager_object *device, struct pw_manager_object *card,
+ struct device_info *dev_info, bool monitor, struct defs *defs);
+
+/* ========================================================================== */
+
+struct card_info {
+ uint32_t n_profiles;
+ uint32_t active_profile;
+ const char *active_profile_name;
+
+ uint32_t n_ports;
+};
+
+#define CARD_INFO_INIT \
+ (struct card_info) { \
+ .active_profile = SPA_ID_INVALID, \
+ }
+
+void collect_card_info(struct pw_manager_object *card, struct card_info *info);
+
+/* ========================================================================== */
+
+struct profile_info {
+ uint32_t index;
+ const char *name;
+ const char *description;
+ uint32_t priority;
+ uint32_t available;
+ uint32_t n_sources;
+ uint32_t n_sinks;
+};
+
+uint32_t collect_profile_info(struct pw_manager_object *card, struct card_info *card_info,
+ struct profile_info *profile_info);
+
+/* ========================================================================== */
+
+struct port_info {
+ uint32_t index;
+ uint32_t direction;
+ const char *name;
+ const char *description;
+ uint32_t priority;
+ uint32_t available;
+
+ const char *availability_group;
+ uint32_t type;
+
+ uint32_t n_devices;
+ uint32_t *devices;
+ uint32_t n_profiles;
+ uint32_t *profiles;
+
+ uint32_t n_props;
+ struct spa_pod *info;
+};
+
+uint32_t collect_port_info(struct pw_manager_object *card, struct card_info *card_info,
+ struct device_info *dev_info, struct port_info *port_info);
+
+/* ========================================================================== */
+
+struct transport_codec_info {
+ enum spa_bluetooth_audio_codec id;
+ const char *description;
+};
+
+uint32_t collect_transport_codec_info(struct pw_manager_object *card,
+ struct transport_codec_info *codecs, uint32_t max_codecs,
+ uint32_t *active);
+
+/* ========================================================================== */
+
+struct spa_dict *collect_props(struct spa_pod *info, struct spa_dict *dict);
+uint32_t find_profile_index(struct pw_manager_object *card, const char *name);
+uint32_t find_port_index(struct pw_manager_object *card, uint32_t direction, const char *port_name);
+struct pw_manager_object *find_peer_for_link(struct pw_manager *m,
+ struct pw_manager_object *o, uint32_t id, enum pw_direction direction);
+struct pw_manager_object *find_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction);
+bool collect_is_linked(struct pw_manager *m, uint32_t id, enum pw_direction direction);
+
+#endif
diff --git a/src/modules/module-protocol-pulse/commands.h b/src/modules/module-protocol-pulse/commands.h
new file mode 100644
index 0000000..9dd1d74
--- /dev/null
+++ b/src/modules/module-protocol-pulse/commands.h
@@ -0,0 +1,208 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_COMMANDS_H
+#define PULSE_SERVER_COMMANDS_H
+
+#include <stdint.h>
+
+struct client;
+struct message;
+
+enum pulseaudio_command {
+ /* Generic commands */
+ COMMAND_ERROR,
+ COMMAND_TIMEOUT, /* pseudo command */
+ COMMAND_REPLY,
+
+ /* CLIENT->SERVER */
+ COMMAND_CREATE_PLAYBACK_STREAM, /* Payload changed in v9, v12 (0.9.0, 0.9.8) */
+ COMMAND_DELETE_PLAYBACK_STREAM,
+ COMMAND_CREATE_RECORD_STREAM, /* Payload changed in v9, v12 (0.9.0, 0.9.8) */
+ COMMAND_DELETE_RECORD_STREAM,
+ COMMAND_EXIT,
+ COMMAND_AUTH,
+ COMMAND_SET_CLIENT_NAME,
+ COMMAND_LOOKUP_SINK,
+ COMMAND_LOOKUP_SOURCE,
+ COMMAND_DRAIN_PLAYBACK_STREAM,
+ COMMAND_STAT,
+ COMMAND_GET_PLAYBACK_LATENCY,
+ COMMAND_CREATE_UPLOAD_STREAM,
+ COMMAND_DELETE_UPLOAD_STREAM,
+ COMMAND_FINISH_UPLOAD_STREAM,
+ COMMAND_PLAY_SAMPLE,
+ COMMAND_REMOVE_SAMPLE,
+
+ COMMAND_GET_SERVER_INFO,
+ COMMAND_GET_SINK_INFO,
+ COMMAND_GET_SINK_INFO_LIST,
+ COMMAND_GET_SOURCE_INFO,
+ COMMAND_GET_SOURCE_INFO_LIST,
+ COMMAND_GET_MODULE_INFO,
+ COMMAND_GET_MODULE_INFO_LIST,
+ COMMAND_GET_CLIENT_INFO,
+ COMMAND_GET_CLIENT_INFO_LIST,
+ COMMAND_GET_SINK_INPUT_INFO, /* Payload changed in v11 (0.9.7) */
+ COMMAND_GET_SINK_INPUT_INFO_LIST, /* Payload changed in v11 (0.9.7) */
+ COMMAND_GET_SOURCE_OUTPUT_INFO,
+ COMMAND_GET_SOURCE_OUTPUT_INFO_LIST,
+ COMMAND_GET_SAMPLE_INFO,
+ COMMAND_GET_SAMPLE_INFO_LIST,
+ COMMAND_SUBSCRIBE,
+
+ COMMAND_SET_SINK_VOLUME,
+ COMMAND_SET_SINK_INPUT_VOLUME,
+ COMMAND_SET_SOURCE_VOLUME,
+
+ COMMAND_SET_SINK_MUTE,
+ COMMAND_SET_SOURCE_MUTE,
+
+ COMMAND_CORK_PLAYBACK_STREAM,
+ COMMAND_FLUSH_PLAYBACK_STREAM,
+ COMMAND_TRIGGER_PLAYBACK_STREAM,
+
+ COMMAND_SET_DEFAULT_SINK,
+ COMMAND_SET_DEFAULT_SOURCE,
+
+ COMMAND_SET_PLAYBACK_STREAM_NAME,
+ COMMAND_SET_RECORD_STREAM_NAME,
+
+ COMMAND_KILL_CLIENT,
+ COMMAND_KILL_SINK_INPUT,
+ COMMAND_KILL_SOURCE_OUTPUT,
+
+ COMMAND_LOAD_MODULE,
+ COMMAND_UNLOAD_MODULE,
+
+ /* Obsolete */
+ COMMAND_ADD_AUTOLOAD___OBSOLETE,
+ COMMAND_REMOVE_AUTOLOAD___OBSOLETE,
+ COMMAND_GET_AUTOLOAD_INFO___OBSOLETE,
+ COMMAND_GET_AUTOLOAD_INFO_LIST___OBSOLETE,
+
+ COMMAND_GET_RECORD_LATENCY,
+ COMMAND_CORK_RECORD_STREAM,
+ COMMAND_FLUSH_RECORD_STREAM,
+ COMMAND_PREBUF_PLAYBACK_STREAM,
+
+ /* SERVER->CLIENT */
+ COMMAND_REQUEST,
+ COMMAND_OVERFLOW,
+ COMMAND_UNDERFLOW,
+ COMMAND_PLAYBACK_STREAM_KILLED,
+ COMMAND_RECORD_STREAM_KILLED,
+ COMMAND_SUBSCRIBE_EVENT,
+
+ /* A few more client->server commands */
+
+ /* Supported since protocol v10 (0.9.5) */
+ COMMAND_MOVE_SINK_INPUT,
+ COMMAND_MOVE_SOURCE_OUTPUT,
+
+ /* Supported since protocol v11 (0.9.7) */
+ COMMAND_SET_SINK_INPUT_MUTE,
+
+ COMMAND_SUSPEND_SINK,
+ COMMAND_SUSPEND_SOURCE,
+
+ /* Supported since protocol v12 (0.9.8) */
+ COMMAND_SET_PLAYBACK_STREAM_BUFFER_ATTR,
+ COMMAND_SET_RECORD_STREAM_BUFFER_ATTR,
+
+ COMMAND_UPDATE_PLAYBACK_STREAM_SAMPLE_RATE,
+ COMMAND_UPDATE_RECORD_STREAM_SAMPLE_RATE,
+
+ /* SERVER->CLIENT */
+ COMMAND_PLAYBACK_STREAM_SUSPENDED,
+ COMMAND_RECORD_STREAM_SUSPENDED,
+ COMMAND_PLAYBACK_STREAM_MOVED,
+ COMMAND_RECORD_STREAM_MOVED,
+
+ /* Supported since protocol v13 (0.9.11) */
+ COMMAND_UPDATE_RECORD_STREAM_PROPLIST,
+ COMMAND_UPDATE_PLAYBACK_STREAM_PROPLIST,
+ COMMAND_UPDATE_CLIENT_PROPLIST,
+ COMMAND_REMOVE_RECORD_STREAM_PROPLIST,
+ COMMAND_REMOVE_PLAYBACK_STREAM_PROPLIST,
+ COMMAND_REMOVE_CLIENT_PROPLIST,
+
+ /* SERVER->CLIENT */
+ COMMAND_STARTED,
+
+ /* Supported since protocol v14 (0.9.12) */
+ COMMAND_EXTENSION,
+ /* Supported since protocol v15 (0.9.15) */
+ COMMAND_GET_CARD_INFO,
+ COMMAND_GET_CARD_INFO_LIST,
+ COMMAND_SET_CARD_PROFILE,
+
+ COMMAND_CLIENT_EVENT,
+ COMMAND_PLAYBACK_STREAM_EVENT,
+ COMMAND_RECORD_STREAM_EVENT,
+
+ /* SERVER->CLIENT */
+ COMMAND_PLAYBACK_BUFFER_ATTR_CHANGED,
+ COMMAND_RECORD_BUFFER_ATTR_CHANGED,
+
+ /* Supported since protocol v16 (0.9.16) */
+ COMMAND_SET_SINK_PORT,
+ COMMAND_SET_SOURCE_PORT,
+
+ /* Supported since protocol v22 (1.0) */
+ COMMAND_SET_SOURCE_OUTPUT_VOLUME,
+ COMMAND_SET_SOURCE_OUTPUT_MUTE,
+
+ /* Supported since protocol v27 (3.0) */
+ COMMAND_SET_PORT_LATENCY_OFFSET,
+
+ /* Supported since protocol v30 (6.0) */
+ /* BOTH DIRECTIONS */
+ COMMAND_ENABLE_SRBCHANNEL,
+ COMMAND_DISABLE_SRBCHANNEL,
+
+ /* Supported since protocol v31 (9.0)
+ * BOTH DIRECTIONS */
+ COMMAND_REGISTER_MEMFD_SHMID,
+
+ /* Supported since protocol v35 (15.0) */
+ COMMAND_SEND_OBJECT_MESSAGE,
+
+ COMMAND_MAX
+};
+
+enum command_access_flag {
+ COMMAND_ACCESS_WITHOUT_AUTH = (1 << 0),
+ COMMAND_ACCESS_WITHOUT_MANAGER = (1 << 1),
+};
+
+struct command {
+ const char *name;
+ int (*run) (struct client *client, uint32_t command, uint32_t tag, struct message *msg);
+ uint32_t access;
+};
+
+extern const struct command commands[COMMAND_MAX];
+
+#endif /* PULSE_SERVER_COMMANDS_H */
diff --git a/src/modules/module-protocol-pulse/dbus-name.c b/src/modules/module-protocol-pulse/dbus-name.c
new file mode 100644
index 0000000..b82bc45
--- /dev/null
+++ b/src/modules/module-protocol-pulse/dbus-name.c
@@ -0,0 +1,87 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <pipewire/context.h>
+
+#include "log.h"
+#include "dbus-name.h"
+
+void *dbus_request_name(struct pw_context *context, const char *name)
+{
+ struct spa_dbus *dbus;
+ struct spa_dbus_connection *conn;
+ const struct spa_support *support;
+ uint32_t n_support;
+ DBusConnection *bus;
+ DBusError error;
+
+ support = pw_context_get_support(context, &n_support);
+
+ dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ if (dbus == NULL) {
+ errno = ENOTSUP;
+ return NULL;
+ }
+
+ conn = spa_dbus_get_connection(dbus, SPA_DBUS_TYPE_SESSION);
+ if (conn == NULL)
+ return NULL;
+
+ bus = spa_dbus_connection_get(conn);
+ if (bus == NULL) {
+ spa_dbus_connection_destroy(conn);
+ return NULL;
+ }
+
+ dbus_error_init(&error);
+
+ if (dbus_bus_request_name(bus, name,
+ DBUS_NAME_FLAG_DO_NOT_QUEUE,
+ &error) == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER)
+ return conn;
+
+ if (dbus_error_is_set(&error))
+ pw_log_error("Failed to acquire %s: %s: %s", name, error.name, error.message);
+ else
+ pw_log_error("D-Bus name %s already taken.", name);
+
+ dbus_error_free(&error);
+
+ spa_dbus_connection_destroy(conn);
+
+ errno = EEXIST;
+ return NULL;
+}
+
+void dbus_release_name(void *data)
+{
+ struct spa_dbus_connection *conn = data;
+ spa_dbus_connection_destroy(conn);
+}
diff --git a/src/modules/module-protocol-pulse/dbus-name.h b/src/modules/module-protocol-pulse/dbus-name.h
new file mode 100644
index 0000000..a15fb80
--- /dev/null
+++ b/src/modules/module-protocol-pulse/dbus-name.h
@@ -0,0 +1,33 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_DBUS_NAME_H
+#define PULSE_SERVER_DBUS_NAME_H
+
+struct pw_context;
+
+void *dbus_request_name(struct pw_context *context, const char *name);
+void dbus_release_name(void *data);
+
+#endif /* PULSE_SERVER_DBUS_NAME_H */
diff --git a/src/modules/module-protocol-pulse/defs.h b/src/modules/module-protocol-pulse/defs.h
new file mode 100644
index 0000000..2e9a43e
--- /dev/null
+++ b/src/modules/module-protocol-pulse/defs.h
@@ -0,0 +1,265 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_DEFS_H
+#define PULSE_SERVER_DEFS_H
+
+#include <pipewire/node.h>
+
+#define FLAG_SHMDATA 0x80000000LU
+#define FLAG_SHMDATA_MEMFD_BLOCK 0x20000000LU
+#define FLAG_SHMRELEASE 0x40000000LU
+#define FLAG_SHMREVOKE 0xC0000000LU
+#define FLAG_SHMMASK 0xFF000000LU
+#define FLAG_SEEKMASK 0x000000FFLU
+#define FLAG_SHMWRITABLE 0x00800000LU
+
+#define SEEK_RELATIVE 0
+#define SEEK_ABSOLUTE 1
+#define SEEK_RELATIVE_ON_READ 2
+#define SEEK_RELATIVE_END 3
+
+#define FRAME_SIZE_MAX_ALLOW (1024*1024*16)
+
+#define PROTOCOL_FLAG_MASK 0xffff0000u
+#define PROTOCOL_VERSION_MASK 0x0000ffffu
+#define PROTOCOL_VERSION 35
+
+#define NATIVE_COOKIE_LENGTH 256
+#define MAX_TAG_SIZE (64*1024)
+
+#define MIN_BUFFERS 1u
+#define MAX_BUFFERS 4u
+
+#define MAXLENGTH (4u*1024*1024) /* 4MB */
+
+#define SCACHE_ENTRY_SIZE_MAX (1024*1024*16)
+
+#define MODULE_INDEX_MASK 0xfffffffu
+#define MODULE_EXTENSION_FLAG (1u << 28)
+#define MODULE_FLAG (1u << 29)
+
+#define DEFAULT_SINK "@DEFAULT_SINK@"
+#define DEFAULT_SOURCE "@DEFAULT_SOURCE@"
+#define DEFAULT_MONITOR "@DEFAULT_MONITOR@"
+
+enum error_code {
+ ERR_OK = 0, /**< No error */
+ ERR_ACCESS, /**< Access failure */
+ ERR_COMMAND, /**< Unknown command */
+ ERR_INVALID, /**< Invalid argument */
+ ERR_EXIST, /**< Entity exists */
+ ERR_NOENTITY, /**< No such entity */
+ ERR_CONNECTIONREFUSED, /**< Connection refused */
+ ERR_PROTOCOL, /**< Protocol error */
+ ERR_TIMEOUT, /**< Timeout */
+ ERR_AUTHKEY, /**< No authentication key */
+ ERR_INTERNAL, /**< Internal error */
+ ERR_CONNECTIONTERMINATED, /**< Connection terminated */
+ ERR_KILLED, /**< Entity killed */
+ ERR_INVALIDSERVER, /**< Invalid server */
+ ERR_MODINITFAILED, /**< Module initialization failed */
+ ERR_BADSTATE, /**< Bad state */
+ ERR_NODATA, /**< No data */
+ ERR_VERSION, /**< Incompatible protocol version */
+ ERR_TOOLARGE, /**< Data too large */
+ ERR_NOTSUPPORTED, /**< Operation not supported \since 0.9.5 */
+ ERR_UNKNOWN, /**< The error code was unknown to the client */
+ ERR_NOEXTENSION, /**< Extension does not exist. \since 0.9.12 */
+ ERR_OBSOLETE, /**< Obsolete functionality. \since 0.9.15 */
+ ERR_NOTIMPLEMENTED, /**< Missing implementation. \since 0.9.15 */
+ ERR_FORKED, /**< The caller forked without calling execve() and tried to reuse the context. \since 0.9.15 */
+ ERR_IO, /**< An IO error happened. \since 0.9.16 */
+ ERR_BUSY, /**< Device or resource busy. \since 0.9.17 */
+ ERR_MAX /**< Not really an error but the first invalid error code */
+};
+
+static inline int res_to_err(int res)
+{
+ switch (res) {
+ case 0: return ERR_OK;
+ case -EACCES: case -EPERM: return ERR_ACCESS;
+ case -ENOTTY: return ERR_COMMAND;
+ case -EINVAL: return ERR_INVALID;
+ case -EEXIST: return ERR_EXIST;
+ case -ENOENT: case -ESRCH: case -ENXIO: case -ENODEV: return ERR_NOENTITY;
+ case -ECONNREFUSED:
+#ifdef ENONET
+ case -ENONET:
+#endif
+ case -EHOSTDOWN: case -ENETDOWN: return ERR_CONNECTIONREFUSED;
+ case -EPROTO: case -EBADMSG: return ERR_PROTOCOL;
+ case -ETIMEDOUT:
+#ifdef ETIME
+ case -ETIME:
+#endif
+ return ERR_TIMEOUT;
+#ifdef ENOKEY
+ case -ENOKEY: return ERR_AUTHKEY;
+#endif
+ case -ECONNRESET: case -EPIPE: return ERR_CONNECTIONTERMINATED;
+#ifdef EBADFD
+ case -EBADFD: return ERR_BADSTATE;
+#endif
+#ifdef ENODATA
+ case -ENODATA: return ERR_NODATA;
+#endif
+ case -EOVERFLOW: case -E2BIG: case -EFBIG:
+ case -ERANGE: case -ENAMETOOLONG: return ERR_TOOLARGE;
+ case -ENOTSUP: case -EPROTONOSUPPORT: case -ESOCKTNOSUPPORT: return ERR_NOTSUPPORTED;
+ case -ENOSYS: return ERR_NOTIMPLEMENTED;
+ case -EIO: return ERR_IO;
+ case -EBUSY: return ERR_BUSY;
+ case -ENFILE: case -EMFILE: return ERR_INTERNAL;
+ }
+ return ERR_UNKNOWN;
+}
+
+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,
+ SUBSCRIPTION_EVENT_SINK_INPUT = 0x0002U,
+ SUBSCRIPTION_EVENT_SOURCE_OUTPUT = 0x0003U,
+ SUBSCRIPTION_EVENT_MODULE = 0x0004U,
+ SUBSCRIPTION_EVENT_CLIENT = 0x0005U,
+ SUBSCRIPTION_EVENT_SAMPLE_CACHE = 0x0006U,
+ SUBSCRIPTION_EVENT_SERVER = 0x0007U,
+ SUBSCRIPTION_EVENT_AUTOLOAD = 0x0008U,
+ SUBSCRIPTION_EVENT_CARD = 0x0009U,
+ SUBSCRIPTION_EVENT_FACILITY_MASK = 0x000FU,
+
+ SUBSCRIPTION_EVENT_NEW = 0x0000U,
+ SUBSCRIPTION_EVENT_CHANGE = 0x0010U,
+ SUBSCRIPTION_EVENT_REMOVE = 0x0020U,
+ SUBSCRIPTION_EVENT_TYPE_MASK = 0x0030U
+};
+
+enum {
+ STATE_INVALID = -1,
+ STATE_RUNNING = 0,
+ STATE_IDLE = 1,
+ STATE_SUSPENDED = 2,
+ STATE_INIT = -2,
+ STATE_UNLINKED = -3
+};
+
+static inline int node_state(enum pw_node_state state)
+{
+ switch (state) {
+ case PW_NODE_STATE_ERROR:
+ return STATE_UNLINKED;
+ case PW_NODE_STATE_CREATING:
+ return STATE_INIT;
+ case PW_NODE_STATE_SUSPENDED:
+ return STATE_SUSPENDED;
+ case PW_NODE_STATE_IDLE:
+ return STATE_IDLE;
+ case PW_NODE_STATE_RUNNING:
+ return STATE_RUNNING;
+ }
+ return STATE_INVALID;
+}
+
+enum {
+ SINK_HW_VOLUME_CTRL = 0x0001U,
+ SINK_LATENCY = 0x0002U,
+ SINK_HARDWARE = 0x0004U,
+ SINK_NETWORK = 0x0008U,
+ SINK_HW_MUTE_CTRL = 0x0010U,
+ SINK_DECIBEL_VOLUME = 0x0020U,
+ SINK_FLAT_VOLUME = 0x0040U,
+ SINK_DYNAMIC_LATENCY = 0x0080U,
+ SINK_SET_FORMATS = 0x0100U,
+};
+
+enum {
+ SOURCE_HW_VOLUME_CTRL = 0x0001U,
+ SOURCE_LATENCY = 0x0002U,
+ SOURCE_HARDWARE = 0x0004U,
+ SOURCE_NETWORK = 0x0008U,
+ SOURCE_HW_MUTE_CTRL = 0x0010U,
+ SOURCE_DECIBEL_VOLUME = 0x0020U,
+ SOURCE_DYNAMIC_LATENCY = 0x0040U,
+ SOURCE_FLAT_VOLUME = 0x0080U,
+};
+
+static const char * const port_types[] = {
+ "unknown",
+ "aux",
+ "speaker",
+ "headphones",
+ "line",
+ "mic",
+ "headset",
+ "handset",
+ "earpiece",
+ "spdif",
+ "hdmi",
+ "tv",
+ "radio",
+ "video",
+ "usb",
+ "bluetooth",
+ "portable",
+ "handsfree",
+ "car",
+ "hifi",
+ "phone",
+ "network",
+ "analog",
+};
+
+static inline uint32_t port_type_value(const char *port_type)
+{
+ uint32_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(port_types); i++) {
+ if (strcmp(port_types[i], port_type) == 0)
+ return i;
+ }
+ return 0;
+}
+
+#define METADATA_DEFAULT_SINK "default.audio.sink"
+#define METADATA_DEFAULT_SOURCE "default.audio.source"
+#define METADATA_CONFIG_DEFAULT_SINK "default.configured.audio.sink"
+#define METADATA_CONFIG_DEFAULT_SOURCE "default.configured.audio.source"
+#define METADATA_TARGET_NODE "target.node"
+#define METADATA_TARGET_OBJECT "target.object"
+
+#endif /* PULSE_SERVER_DEFS_H */
diff --git a/src/modules/module-protocol-pulse/extension.c b/src/modules/module-protocol-pulse/extension.c
new file mode 100644
index 0000000..0ffa4c5
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extension.c
@@ -0,0 +1,45 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+
+#include "defs.h"
+#include "extension.h"
+#include "extensions/registry.h"
+
+static const struct extension extensions[] = {
+ { "module-stream-restore", 0 | MODULE_EXTENSION_FLAG, do_extension_stream_restore, },
+ { "module-device-restore", 1 | MODULE_EXTENSION_FLAG, do_extension_device_restore, },
+ { "module-device-manager", 2 | MODULE_EXTENSION_FLAG, do_extension_device_manager, },
+};
+
+const struct extension *extension_find(uint32_t index, const char *name)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(extensions, ext) {
+ if (index == ext->index || spa_streq(name, ext->name))
+ return ext;
+ }
+ return NULL;
+}
diff --git a/src/modules/module-protocol-pulse/extension.h b/src/modules/module-protocol-pulse/extension.h
new file mode 100644
index 0000000..3c1b059
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extension.h
@@ -0,0 +1,47 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_EXTENSION_H
+#define PULSE_SERVER_EXTENSION_H
+
+#include <stdint.h>
+
+struct client;
+struct message;
+
+struct extension_sub {
+ const char *name;
+ uint32_t command;
+ int (*process)(struct client *client, uint32_t command, uint32_t tag, struct message *m);
+};
+
+struct extension {
+ const char *name;
+ uint32_t index;
+ int (*process)(struct client *client, uint32_t tag, struct message *m);
+};
+
+const struct extension *extension_find(uint32_t index, const char *name);
+
+#endif /* PULSE_SERVER_EXTENSION_H */
diff --git a/src/modules/module-protocol-pulse/extensions/ext-device-manager.c b/src/modules/module-protocol-pulse/extensions/ext-device-manager.c
new file mode 100644
index 0000000..2ba080e
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extensions/ext-device-manager.c
@@ -0,0 +1,8 @@
+#include <errno.h>
+
+#include "registry.h"
+
+int do_extension_device_manager(struct client *client, uint32_t tag, struct message *m)
+{
+ return -ENOTSUP;
+}
diff --git a/src/modules/module-protocol-pulse/extensions/ext-device-restore.c b/src/modules/module-protocol-pulse/extensions/ext-device-restore.c
new file mode 100644
index 0000000..4fab654
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extensions/ext-device-restore.c
@@ -0,0 +1,340 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#define EXT_DEVICE_RESTORE_VERSION 1
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/dict.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <pipewire/log.h>
+#include <pipewire/properties.h>
+
+#include "../client.h"
+#include "../collect.h"
+#include "../defs.h"
+#include "../extension.h"
+#include "../format.h"
+#include "../manager.h"
+#include "../message.h"
+#include "../reply.h"
+#include "../volume.h"
+#include "registry.h"
+
+PW_LOG_TOPIC_EXTERN(pulse_ext_dev_restore);
+#undef PW_LOG_TOPIC_DEFAULT
+#define PW_LOG_TOPIC_DEFAULT pulse_ext_dev_restore
+
+#define DEVICE_TYPE_SINK 0
+#define DEVICE_TYPE_SOURCE 1
+
+static int do_extension_device_restore_test(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, EXT_DEVICE_RESTORE_VERSION,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int do_extension_device_restore_subscribe(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return reply_simple_ack(client, tag);
+}
+
+
+struct format_data {
+ struct client *client;
+ struct message *reply;
+};
+
+static int do_sink_read_format(void *data, struct pw_manager_object *o)
+{
+ struct format_data *d = data;
+ struct pw_manager_param *p;
+ struct format_info info[32];
+ uint32_t i, n_info = 0;
+
+ if (!pw_manager_object_is_sink(o))
+ return 0;
+
+ spa_list_for_each(p, &o->param_list, link) {
+ uint32_t index = 0;
+
+ if (p->id != SPA_PARAM_EnumFormat)
+ continue;
+
+ while (n_info < SPA_N_ELEMENTS(info)) {
+ spa_zero(info[n_info]);
+ if (format_info_from_param(&info[n_info], p->param, index++) < 0)
+ break;
+ if (info[n_info].encoding == ENCODING_ANY) {
+ format_info_clear(&info[n_info]);
+ continue;
+ }
+ n_info++;
+ }
+ }
+ message_put(d->reply,
+ TAG_U32, DEVICE_TYPE_SINK,
+ TAG_U32, o->index, /* sink index */
+ TAG_U8, n_info, /* n_formats */
+ TAG_INVALID);
+ for (i = 0; i < n_info; i++) {
+ message_put(d->reply,
+ TAG_FORMAT_INFO, &info[i],
+ TAG_INVALID);
+ format_info_clear(&info[i]);
+ }
+ return 0;
+}
+
+static int do_extension_device_restore_read_formats_all(struct client *client,
+ uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct format_data data;
+
+ spa_zero(data);
+ data.client = client;
+ data.reply = reply_new(client, tag);
+
+ pw_manager_for_each_object(manager, do_sink_read_format, &data);
+
+ return client_queue_message(client, data.reply);
+}
+
+static int do_extension_device_restore_read_formats(struct client *client,
+ uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct format_data data;
+ uint32_t type, sink_index;
+ struct selector sel;
+ struct pw_manager_object *o;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &type,
+ TAG_U32, &sink_index,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ if (type != DEVICE_TYPE_SINK) {
+ pw_log_info("Device format reading is only supported on sinks");
+ return -ENOTSUP;
+ }
+
+ spa_zero(sel);
+ sel.index = sink_index;
+ sel.type = pw_manager_object_is_sink;
+
+ o = select_object(manager, &sel);
+ if (o == NULL)
+ return -ENOENT;
+
+ spa_zero(data);
+ data.client = client;
+ data.reply = reply_new(client, tag);
+
+ do_sink_read_format(&data, o);
+
+ return client_queue_message(client, data.reply);
+}
+
+static int set_card_codecs(struct pw_manager_object *o, uint32_t port_index,
+ uint32_t device_id, uint32_t n_codecs, uint32_t *codecs)
+{
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[2];
+ struct spa_pod *param;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route);
+ spa_pod_builder_add(&b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(port_index),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(device_id),
+ 0);
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_props, 0);
+ spa_pod_builder_push_object(&b, &f[1],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ spa_pod_builder_add(&b,
+ SPA_PROP_iec958Codecs, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, n_codecs, codecs), 0);
+ spa_pod_builder_pop(&b, &f[1]);
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_save, 0);
+ spa_pod_builder_bool(&b, true);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_device_set_param((struct pw_device*)o->proxy,
+ SPA_PARAM_Route, 0, param);
+ return 0;
+}
+
+static int set_node_codecs(struct pw_manager_object *o, uint32_t n_codecs, uint32_t *codecs)
+{
+ char buf[1024];
+ struct spa_pod_builder b;
+ struct spa_pod *param;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ spa_pod_builder_init(&b, buf, sizeof(buf));
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+ SPA_PROP_iec958Codecs, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, n_codecs, codecs));
+
+ pw_node_set_param((struct pw_node*)o->proxy,
+ SPA_PARAM_Props, 0, param);
+
+ return 0;
+}
+
+
+static int do_extension_device_restore_save_formats(struct client *client,
+ uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct selector sel;
+ struct pw_manager_object *o, *card = NULL;
+ struct pw_node_info *info;
+ int res;
+ uint32_t type, sink_index, card_id = SPA_ID_INVALID;
+ uint8_t i, n_formats;
+ uint32_t n_codecs = 0, codec, iec958codecs[32];
+ struct device_info dev_info;
+ const char *str;
+
+ if ((res = message_get(m,
+ TAG_U32, &type,
+ TAG_U32, &sink_index,
+ TAG_U8, &n_formats,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+ if (n_formats < 1)
+ return -EPROTO;
+
+ if (type != DEVICE_TYPE_SINK)
+ return -ENOTSUP;
+
+ for (i = 0; i < n_formats; ++i) {
+ struct format_info format;
+ spa_zero(format);
+ if (message_get(m,
+ TAG_FORMAT_INFO, &format,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ codec = format_encoding2id(format.encoding);
+ if (codec != SPA_ID_INVALID && n_codecs < SPA_N_ELEMENTS(iec958codecs))
+ iec958codecs[n_codecs++] = codec;
+
+ format_info_clear(&format);
+ }
+ if (n_codecs == 0)
+ return -ENOTSUP;
+
+ spa_zero(sel);
+ sel.index = sink_index;
+ sel.type = pw_manager_object_is_sink;
+
+ o = select_object(manager, &sel);
+ if (o == NULL || (info = o->info) == NULL || info->props == NULL)
+ return -ENOENT;
+
+ dev_info = DEVICE_INFO_INIT(SPA_DIRECTION_INPUT);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ collect_device_info(o, card, &dev_info, false, &impl->defs);
+
+ if (card != NULL && dev_info.active_port != SPA_ID_INVALID) {
+ res = set_card_codecs(card, dev_info.active_port,
+ dev_info.device, n_codecs, iec958codecs);
+ } else {
+ res = set_node_codecs(o, n_codecs, iec958codecs);
+ }
+ if (res < 0)
+ return res;
+
+ return reply_simple_ack(client, tag);
+}
+
+static const struct extension_sub ext_device_restore[] = {
+ { "TEST", 0, do_extension_device_restore_test, },
+ { "SUBSCRIBE", 1, do_extension_device_restore_subscribe, },
+ { "EVENT", 2, },
+ { "READ_FORMATS_ALL", 3, do_extension_device_restore_read_formats_all, },
+ { "READ_FORMATS", 4, do_extension_device_restore_read_formats, },
+ { "SAVE_FORMATS", 5, do_extension_device_restore_save_formats, },
+};
+
+int do_extension_device_restore(struct client *client, uint32_t tag, struct message *m)
+{
+ uint32_t command;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &command,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ if (command >= SPA_N_ELEMENTS(ext_device_restore))
+ return -ENOTSUP;
+ if (ext_device_restore[command].process == NULL)
+ return -EPROTO;
+
+ pw_log_info("client %p [%s]: EXT_DEVICE_RESTORE_%s tag:%u",
+ client, client->name, ext_device_restore[command].name, tag);
+
+ return ext_device_restore[command].process(client, command, tag, m);
+}
diff --git a/src/modules/module-protocol-pulse/extensions/ext-stream-restore.c b/src/modules/module-protocol-pulse/extensions/ext-stream-restore.c
new file mode 100644
index 0000000..76c7332
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extensions/ext-stream-restore.c
@@ -0,0 +1,330 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#define EXT_STREAM_RESTORE_VERSION 1
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/dict.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <pipewire/log.h>
+#include <pipewire/properties.h>
+
+#include "../client.h"
+#include "../defs.h"
+#include "../extension.h"
+#include "../format.h"
+#include "../manager.h"
+#include "../message.h"
+#include "../remap.h"
+#include "../reply.h"
+#include "../volume.h"
+#include "registry.h"
+
+PW_LOG_TOPIC_EXTERN(pulse_ext_stream_restore);
+#undef PW_LOG_TOPIC_DEFAULT
+#define PW_LOG_TOPIC_DEFAULT pulse_ext_stream_restore
+
+static int do_extension_stream_restore_test(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, EXT_STREAM_RESTORE_VERSION,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int key_from_name(const char *name, char *key, size_t maxlen)
+{
+ const char *media_class, *select, *str;
+
+ if (spa_strstartswith(name, "sink-input-"))
+ media_class = "Output/Audio";
+ else if (spa_strstartswith(name, "source-output-"))
+ media_class = "Input/Audio";
+ else
+ return -1;
+
+ if ((str = strstr(name, "-by-media-role:")) != NULL) {
+ const struct str_map *map;
+ str += strlen("-by-media-role:");
+ map = str_map_find(media_role_map, NULL, str);
+ str = map ? map->pw_str : str;
+ select = "media.role";
+ }
+ else if ((str = strstr(name, "-by-application-id:")) != NULL) {
+ str += strlen("-by-application-id:");
+ select = "application.id";
+ }
+ else if ((str = strstr(name, "-by-application-name:")) != NULL) {
+ str += strlen("-by-application-name:");
+ select = "application.name";
+ }
+ else if ((str = strstr(name, "-by-media-name:")) != NULL) {
+ str += strlen("-by-media-name:");
+ select = "media.name";
+ } else
+ return -1;
+
+ snprintf(key, maxlen, "restore.stream.%s.%s:%s",
+ media_class, select, str);
+ return 0;
+}
+
+static int key_to_name(const char *key, char *name, size_t maxlen)
+{
+ const char *type, *select, *str;
+
+ if (spa_strstartswith(key, "restore.stream.Output/Audio."))
+ type = "sink-input";
+ else if (spa_strstartswith(key, "restore.stream.Input/Audio."))
+ type = "source-output";
+ else
+ type = "stream";
+
+ if ((str = strstr(key, ".media.role:")) != NULL) {
+ const struct str_map *map;
+ str += strlen(".media.role:");
+ map = str_map_find(media_role_map, str, NULL);
+ select = "media-role";
+ str = map ? map->pa_str : str;
+ }
+ else if ((str = strstr(key, ".application.id:")) != NULL) {
+ str += strlen(".application.id:");
+ select = "application-id";
+ }
+ else if ((str = strstr(key, ".application.name:")) != NULL) {
+ str += strlen(".application.name:");
+ select = "application-name";
+ }
+ else if ((str = strstr(key, ".media.name:")) != NULL) {
+ str += strlen(".media.name:");
+ select = "media-name";
+ }
+ else
+ return -1;
+
+ snprintf(name, maxlen, "%s-by-%s:%s", type, select, str);
+ return 0;
+
+}
+
+static int do_extension_stream_restore_read(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+ const struct spa_dict_item *item;
+
+ reply = reply_new(client, tag);
+
+ spa_dict_for_each(item, &client->routes->dict) {
+ struct spa_json it[3];
+ const char *value;
+ char name[1024], key[128];
+ char device_name[1024] = "\0";
+ bool mute = false;
+ struct volume vol = VOLUME_INIT;
+ struct channel_map map = CHANNEL_MAP_INIT;
+ float volume = 0.0f;
+
+ 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)
+ continue;
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_streq(key, "volume")) {
+ if (spa_json_get_float(&it[1], &volume) <= 0)
+ continue;
+ }
+ else if (spa_streq(key, "mute")) {
+ if (spa_json_get_bool(&it[1], &mute) <= 0)
+ continue;
+ }
+ else if (spa_streq(key, "volumes")) {
+ vol = VOLUME_INIT;
+ if (spa_json_enter_array(&it[1], &it[2]) <= 0)
+ continue;
+
+ for (vol.channels = 0; vol.channels < CHANNELS_MAX; vol.channels++) {
+ if (spa_json_get_float(&it[2], &vol.values[vol.channels]) <= 0)
+ break;
+ }
+ }
+ else if (spa_streq(key, "channels")) {
+ if (spa_json_enter_array(&it[1], &it[2]) <= 0)
+ continue;
+
+ for (map.channels = 0; map.channels < CHANNELS_MAX; map.channels++) {
+ char chname[16];
+ if (spa_json_get_string(&it[2], 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)
+ continue;
+ }
+ else if (spa_json_next(&it[1], &value) <= 0)
+ break;
+ }
+ message_put(reply,
+ TAG_STRING, name,
+ TAG_CHANNEL_MAP, &map,
+ TAG_CVOLUME, &vol,
+ TAG_STRING, device_name[0] ? device_name : NULL,
+ TAG_BOOLEAN, mute,
+ TAG_INVALID);
+ }
+
+ return client_queue_message(client, reply);
+}
+
+static int do_extension_stream_restore_write(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ int res;
+ uint32_t mode;
+ bool apply;
+
+ if ((res = message_get(m,
+ TAG_U32, &mode,
+ TAG_BOOLEAN, &apply,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ while (m->offset < m->length) {
+ const char *name, *device_name = NULL;
+ struct channel_map map;
+ struct volume vol;
+ bool mute = false;
+ uint32_t i;
+ FILE *f;
+ char *ptr;
+ size_t size;
+ char key[1024], buf[128];
+
+ spa_zero(map);
+ spa_zero(vol);
+
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_CHANNEL_MAP, &map,
+ TAG_CVOLUME, &vol,
+ TAG_STRING, &device_name,
+ TAG_BOOLEAN, &mute,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ if (name == NULL || name[0] == '\0')
+ return -EPROTO;
+
+ if ((f = open_memstream(&ptr, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ fprintf(f, " \"mute\": %s", mute ? "true" : "false");
+ if (vol.channels > 0) {
+ fprintf(f, ", \"volumes\": [");
+ for (i = 0; i < vol.channels; i++)
+ fprintf(f, "%s%s", (i == 0 ? " ":", "),
+ spa_json_format_float(buf, sizeof(buf), vol.values[i]));
+ fprintf(f, " ]");
+ }
+ if (map.channels > 0) {
+ fprintf(f, ", \"channels\": [");
+ for (i = 0; i < map.channels; i++)
+ fprintf(f, "%s\"%s\"", (i == 0 ? " ":", "), channel_id2name(map.map[i]));
+ fprintf(f, " ]");
+ }
+ if (device_name != NULL && device_name[0] &&
+ (client->default_source == NULL || !spa_streq(device_name, client->default_source)) &&
+ (client->default_sink == NULL || !spa_streq(device_name, client->default_sink)))
+ fprintf(f, ", \"target-node\": \"%s\"", device_name);
+ fprintf(f, " }");
+ fclose(f);
+ if (key_from_name(name, key, sizeof(key)) >= 0) {
+ pw_log_debug("%s -> %s: %s", name, key, ptr);
+ if ((res = pw_manager_set_metadata(client->manager,
+ client->metadata_routes,
+ PW_ID_CORE, key, "Spa:String:JSON", "%s", ptr)) < 0)
+ pw_log_warn("failed to set metadata %s = %s, %s", key, ptr, strerror(-res));
+ }
+ free(ptr);
+ }
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_extension_stream_restore_delete(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return reply_simple_ack(client, tag);
+}
+
+static int do_extension_stream_restore_subscribe(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return reply_simple_ack(client, tag);
+}
+
+static const struct extension_sub ext_stream_restore[] = {
+ { "TEST", 0, do_extension_stream_restore_test, },
+ { "READ", 1, do_extension_stream_restore_read, },
+ { "WRITE", 2, do_extension_stream_restore_write, },
+ { "DELETE", 3, do_extension_stream_restore_delete, },
+ { "SUBSCRIBE", 4, do_extension_stream_restore_subscribe, },
+ { "EVENT", 5, },
+};
+
+int do_extension_stream_restore(struct client *client, uint32_t tag, struct message *m)
+{
+ uint32_t command;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &command,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ if (command >= SPA_N_ELEMENTS(ext_stream_restore))
+ return -ENOTSUP;
+ if (ext_stream_restore[command].process == NULL)
+ return -EPROTO;
+
+ pw_log_info("client %p [%s]: EXT_STREAM_RESTORE_%s tag:%u",
+ client, client->name, ext_stream_restore[command].name, tag);
+
+ return ext_stream_restore[command].process(client, command, tag, m);
+}
diff --git a/src/modules/module-protocol-pulse/extensions/registry.h b/src/modules/module-protocol-pulse/extensions/registry.h
new file mode 100644
index 0000000..ab9faf7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/extensions/registry.h
@@ -0,0 +1,13 @@
+#ifndef PIPEWIRE_PULSE_EXTENSION_REGISTRY_H
+#define PIPEWIRE_PULSE_EXTENSION_REGISTRY_H
+
+#include <stdint.h>
+
+struct client;
+struct message;
+
+int do_extension_stream_restore(struct client *client, uint32_t tag, struct message *m);
+int do_extension_device_restore(struct client *client, uint32_t tag, struct message *m);
+int do_extension_device_manager(struct client *client, uint32_t tag, struct message *m);
+
+#endif /* PIPEWIRE_PULSE_EXTENSION_REGISTRY_H */
diff --git a/src/modules/module-protocol-pulse/format.c b/src/modules/module-protocol-pulse/format.c
new file mode 100644
index 0000000..ced75cf
--- /dev/null
+++ b/src/modules/module-protocol-pulse/format.c
@@ -0,0 +1,861 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/string.h>
+#include <spa/debug/types.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/utils/json.h>
+
+#include "format.h"
+
+static const struct format audio_formats[] = {
+ [SAMPLE_U8] = { SAMPLE_U8, SPA_AUDIO_FORMAT_U8, "u8", 1 },
+ [SAMPLE_ALAW] = { SAMPLE_ALAW, SPA_AUDIO_FORMAT_ALAW, "aLaw", 1 },
+ [SAMPLE_ULAW] = { SAMPLE_ULAW, SPA_AUDIO_FORMAT_ULAW, "uLaw", 1 },
+ [SAMPLE_S16LE] = { SAMPLE_S16LE, SPA_AUDIO_FORMAT_S16_LE, "s16le", 2 },
+ [SAMPLE_S16BE] = { SAMPLE_S16BE, SPA_AUDIO_FORMAT_S16_BE, "s16be", 2 },
+ [SAMPLE_FLOAT32LE] = { SAMPLE_FLOAT32LE, SPA_AUDIO_FORMAT_F32_LE, "float32le", 4 },
+ [SAMPLE_FLOAT32BE] = { SAMPLE_FLOAT32BE, SPA_AUDIO_FORMAT_F32_BE, "float32be", 4 },
+ [SAMPLE_S32LE] = { SAMPLE_S32LE, SPA_AUDIO_FORMAT_S32_LE, "s32le", 4 },
+ [SAMPLE_S32BE] = { SAMPLE_S32BE, SPA_AUDIO_FORMAT_S32_BE, "s32be", 4 },
+ [SAMPLE_S24LE] = { SAMPLE_S24LE, SPA_AUDIO_FORMAT_S24_LE, "s24le", 3 },
+ [SAMPLE_S24BE] = { SAMPLE_S24BE, SPA_AUDIO_FORMAT_S24_BE, "s24be", 3 },
+ [SAMPLE_S24_32LE] = { SAMPLE_S24_32LE, SPA_AUDIO_FORMAT_S24_32_LE, "s24-32le", 4 },
+ [SAMPLE_S24_32BE] = { SAMPLE_S24_32BE, SPA_AUDIO_FORMAT_S24_32_BE, "s24-32be", 4 },
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+ { SAMPLE_S16BE, SPA_AUDIO_FORMAT_S16_BE, "s16ne", 2 },
+ { SAMPLE_FLOAT32BE, SPA_AUDIO_FORMAT_F32_BE, "float32ne", 4 },
+ { SAMPLE_S32BE, SPA_AUDIO_FORMAT_S32_BE, "s32ne", 4 },
+ { SAMPLE_S24BE, SPA_AUDIO_FORMAT_S24_BE, "s24ne", 3 },
+ { SAMPLE_S24_32BE, SPA_AUDIO_FORMAT_S24_32_BE, "s24-32ne", 4 },
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+ { SAMPLE_S16LE, SPA_AUDIO_FORMAT_S16_LE, "s16ne", 2 },
+ { SAMPLE_FLOAT32LE, SPA_AUDIO_FORMAT_F32_LE, "float32ne", 4 },
+ { SAMPLE_S32LE, SPA_AUDIO_FORMAT_S32_LE, "s32ne", 4 },
+ { SAMPLE_S24LE, SPA_AUDIO_FORMAT_S24_LE, "s24ne", 3 },
+ { SAMPLE_S24_32LE, SPA_AUDIO_FORMAT_S24_32_LE, "s24-32ne", 4 },
+#endif
+ /* planar formats, we just report them as interleaved */
+ { SAMPLE_U8, SPA_AUDIO_FORMAT_U8P, "u8ne", 1 },
+ { SAMPLE_S16NE, SPA_AUDIO_FORMAT_S16P, "s16ne", 2 },
+ { SAMPLE_S24_32NE, SPA_AUDIO_FORMAT_S24_32P, "s24-32ne", 4 },
+ { SAMPLE_S32NE, SPA_AUDIO_FORMAT_S32P, "s32ne", 4 },
+ { SAMPLE_S24NE, SPA_AUDIO_FORMAT_S24P, "s24ne", 3 },
+ { SAMPLE_FLOAT32NE, SPA_AUDIO_FORMAT_F32P, "float32ne", 4 },
+};
+
+static const struct channel audio_channels[] = {
+ [CHANNEL_POSITION_MONO] = { SPA_AUDIO_CHANNEL_MONO, "mono", },
+
+ [CHANNEL_POSITION_FRONT_LEFT] = { SPA_AUDIO_CHANNEL_FL, "front-left", },
+ [CHANNEL_POSITION_FRONT_RIGHT] = { SPA_AUDIO_CHANNEL_FR, "front-right", },
+ [CHANNEL_POSITION_FRONT_CENTER] = { SPA_AUDIO_CHANNEL_FC, "front-center", },
+
+ [CHANNEL_POSITION_REAR_CENTER] = { SPA_AUDIO_CHANNEL_RC, "rear-center", },
+ [CHANNEL_POSITION_REAR_LEFT] = { SPA_AUDIO_CHANNEL_RL, "rear-left", },
+ [CHANNEL_POSITION_REAR_RIGHT] = { SPA_AUDIO_CHANNEL_RR, "rear-right", },
+
+ [CHANNEL_POSITION_LFE] = { SPA_AUDIO_CHANNEL_LFE, "lfe", },
+ [CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = { SPA_AUDIO_CHANNEL_FLC, "front-left-of-center", },
+ [CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = { SPA_AUDIO_CHANNEL_FRC, "front-right-of-center", },
+
+ [CHANNEL_POSITION_SIDE_LEFT] = { SPA_AUDIO_CHANNEL_SL, "side-left", },
+ [CHANNEL_POSITION_SIDE_RIGHT] = { SPA_AUDIO_CHANNEL_SR, "side-right", },
+
+ [CHANNEL_POSITION_AUX0] = { SPA_AUDIO_CHANNEL_AUX0, "aux0", },
+ [CHANNEL_POSITION_AUX1] = { SPA_AUDIO_CHANNEL_AUX1, "aux1", },
+ [CHANNEL_POSITION_AUX2] = { SPA_AUDIO_CHANNEL_AUX2, "aux2", },
+ [CHANNEL_POSITION_AUX3] = { SPA_AUDIO_CHANNEL_AUX3, "aux3", },
+ [CHANNEL_POSITION_AUX4] = { SPA_AUDIO_CHANNEL_AUX4, "aux4", },
+ [CHANNEL_POSITION_AUX5] = { SPA_AUDIO_CHANNEL_AUX5, "aux5", },
+ [CHANNEL_POSITION_AUX6] = { SPA_AUDIO_CHANNEL_AUX6, "aux6", },
+ [CHANNEL_POSITION_AUX7] = { SPA_AUDIO_CHANNEL_AUX7, "aux7", },
+ [CHANNEL_POSITION_AUX8] = { SPA_AUDIO_CHANNEL_AUX8, "aux8", },
+ [CHANNEL_POSITION_AUX9] = { SPA_AUDIO_CHANNEL_AUX9, "aux9", },
+ [CHANNEL_POSITION_AUX10] = { SPA_AUDIO_CHANNEL_AUX10, "aux10", },
+ [CHANNEL_POSITION_AUX11] = { SPA_AUDIO_CHANNEL_AUX11, "aux11", },
+ [CHANNEL_POSITION_AUX12] = { SPA_AUDIO_CHANNEL_AUX12, "aux12", },
+ [CHANNEL_POSITION_AUX13] = { SPA_AUDIO_CHANNEL_AUX13, "aux13", },
+ [CHANNEL_POSITION_AUX14] = { SPA_AUDIO_CHANNEL_AUX14, "aux14", },
+ [CHANNEL_POSITION_AUX15] = { SPA_AUDIO_CHANNEL_AUX15, "aux15", },
+ [CHANNEL_POSITION_AUX16] = { SPA_AUDIO_CHANNEL_AUX16, "aux16", },
+ [CHANNEL_POSITION_AUX17] = { SPA_AUDIO_CHANNEL_AUX17, "aux17", },
+ [CHANNEL_POSITION_AUX18] = { SPA_AUDIO_CHANNEL_AUX18, "aux18", },
+ [CHANNEL_POSITION_AUX19] = { SPA_AUDIO_CHANNEL_AUX19, "aux19", },
+ [CHANNEL_POSITION_AUX20] = { SPA_AUDIO_CHANNEL_AUX20, "aux20", },
+ [CHANNEL_POSITION_AUX21] = { SPA_AUDIO_CHANNEL_AUX21, "aux21", },
+ [CHANNEL_POSITION_AUX22] = { SPA_AUDIO_CHANNEL_AUX22, "aux22", },
+ [CHANNEL_POSITION_AUX23] = { SPA_AUDIO_CHANNEL_AUX23, "aux23", },
+ [CHANNEL_POSITION_AUX24] = { SPA_AUDIO_CHANNEL_AUX24, "aux24", },
+ [CHANNEL_POSITION_AUX25] = { SPA_AUDIO_CHANNEL_AUX25, "aux25", },
+ [CHANNEL_POSITION_AUX26] = { SPA_AUDIO_CHANNEL_AUX26, "aux26", },
+ [CHANNEL_POSITION_AUX27] = { SPA_AUDIO_CHANNEL_AUX27, "aux27", },
+ [CHANNEL_POSITION_AUX28] = { SPA_AUDIO_CHANNEL_AUX28, "aux28", },
+ [CHANNEL_POSITION_AUX29] = { SPA_AUDIO_CHANNEL_AUX29, "aux29", },
+ [CHANNEL_POSITION_AUX30] = { SPA_AUDIO_CHANNEL_AUX30, "aux30", },
+ [CHANNEL_POSITION_AUX31] = { SPA_AUDIO_CHANNEL_AUX31, "aux31", },
+
+ [CHANNEL_POSITION_TOP_CENTER] = { SPA_AUDIO_CHANNEL_TC, "top-center", },
+
+ [CHANNEL_POSITION_TOP_FRONT_LEFT] = { SPA_AUDIO_CHANNEL_TFL, "top-front-left", },
+ [CHANNEL_POSITION_TOP_FRONT_RIGHT] = { SPA_AUDIO_CHANNEL_TFR, "top-front-right", },
+ [CHANNEL_POSITION_TOP_FRONT_CENTER] = { SPA_AUDIO_CHANNEL_TFC, "top-front-center", },
+
+ [CHANNEL_POSITION_TOP_REAR_LEFT] = { SPA_AUDIO_CHANNEL_TRL, "top-rear-left", },
+ [CHANNEL_POSITION_TOP_REAR_RIGHT] = { SPA_AUDIO_CHANNEL_TRR, "top-rear-right", },
+ [CHANNEL_POSITION_TOP_REAR_CENTER] = { SPA_AUDIO_CHANNEL_TRC, "top-rear-center", },
+};
+
+uint32_t format_pa2id(enum sample_format format)
+{
+ if (format < 0 || format >= SAMPLE_MAX)
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+ return audio_formats[format].id;
+}
+
+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";
+}
+
+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_CHANNEL_UNKNOWN;
+}
+
+uint32_t format_paname2id(const char *name, size_t size)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+ if (f->name != NULL &&
+ strncmp(name, f->name, size) == 0)
+ return f->id;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+enum sample_format format_id2pa(uint32_t id)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+ if (id == f->id)
+ return f->pa;
+ }
+ return SAMPLE_INVALID;
+}
+
+const char *format_id2paname(uint32_t id)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_formats, f) {
+ if (id == f->id && f->name != NULL)
+ return f->name;
+ }
+ return "invalid";
+}
+
+uint32_t sample_spec_frame_size(const struct sample_spec *ss)
+{
+ switch (ss->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_U8P:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_S8P:
+ case SPA_AUDIO_FORMAT_ULAW:
+ case SPA_AUDIO_FORMAT_ALAW:
+ return ss->channels;
+ case SPA_AUDIO_FORMAT_S16_LE:
+ case SPA_AUDIO_FORMAT_S16_BE:
+ case SPA_AUDIO_FORMAT_S16P:
+ case SPA_AUDIO_FORMAT_U16_LE:
+ case SPA_AUDIO_FORMAT_U16_BE:
+ return 2 * ss->channels;
+ case SPA_AUDIO_FORMAT_S24_LE:
+ case SPA_AUDIO_FORMAT_S24_BE:
+ case SPA_AUDIO_FORMAT_S24P:
+ case SPA_AUDIO_FORMAT_U24_LE:
+ case SPA_AUDIO_FORMAT_U24_BE:
+ case SPA_AUDIO_FORMAT_S20_LE:
+ case SPA_AUDIO_FORMAT_S20_BE:
+ case SPA_AUDIO_FORMAT_U20_LE:
+ case SPA_AUDIO_FORMAT_U20_BE:
+ case SPA_AUDIO_FORMAT_S18_LE:
+ case SPA_AUDIO_FORMAT_S18_BE:
+ case SPA_AUDIO_FORMAT_U18_LE:
+ case SPA_AUDIO_FORMAT_U18_BE:
+ return 3 * ss->channels;
+ case SPA_AUDIO_FORMAT_F32_LE:
+ case SPA_AUDIO_FORMAT_F32_BE:
+ case SPA_AUDIO_FORMAT_F32P:
+ case SPA_AUDIO_FORMAT_S32_LE:
+ case SPA_AUDIO_FORMAT_S32_BE:
+ case SPA_AUDIO_FORMAT_S32P:
+ case SPA_AUDIO_FORMAT_U32_LE:
+ case SPA_AUDIO_FORMAT_U32_BE:
+ case SPA_AUDIO_FORMAT_S24_32_LE:
+ case SPA_AUDIO_FORMAT_S24_32_BE:
+ case SPA_AUDIO_FORMAT_S24_32P:
+ case SPA_AUDIO_FORMAT_U24_32_LE:
+ case SPA_AUDIO_FORMAT_U24_32_BE:
+ return 4 * ss->channels;
+ case SPA_AUDIO_FORMAT_F64_LE:
+ case SPA_AUDIO_FORMAT_F64_BE:
+ case SPA_AUDIO_FORMAT_F64P:
+ return 8 * ss->channels;
+ default:
+ return 0;
+ }
+}
+
+bool sample_spec_valid(const struct sample_spec *ss)
+{
+ return (sample_spec_frame_size(ss) > 0 &&
+ ss->rate > 0 && ss->rate <= RATE_MAX &&
+ ss->channels > 0 && ss->channels <= CHANNELS_MAX);
+}
+
+uint32_t channel_pa2id(enum channel_position channel)
+{
+ if (channel < 0 || (size_t)channel >= SPA_N_ELEMENTS(audio_channels))
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+
+ return audio_channels[channel].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";
+}
+
+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;
+}
+
+enum channel_position channel_id2pa(uint32_t id, uint32_t *aux)
+{
+ size_t i;
+ for (i = 0; i < SPA_N_ELEMENTS(audio_channels); i++) {
+ if (id == audio_channels[i].channel)
+ return i;
+ }
+ return CHANNEL_POSITION_AUX0 + ((*aux)++ & 31);
+}
+
+const char *channel_id2paname(uint32_t id, uint32_t *aux)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_channels, i) {
+ if (id == i->channel && i->name != NULL)
+ return i->name;
+ }
+ return audio_channels[CHANNEL_POSITION_AUX0 + ((*aux)++ & 31)].name;
+}
+
+uint32_t channel_paname2id(const char *name, size_t size)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(audio_channels, i) {
+ if (strncmp(name, i->name, size) == 0)
+ return i->channel;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+
+void channel_map_to_positions(const struct channel_map *map, uint32_t *pos)
+{
+ int i;
+ for (i = 0; i < map->channels; i++)
+ pos[i] = map->map[i];
+}
+
+void channel_map_parse(const char *str, struct channel_map *map)
+{
+ const char *p = str;
+ size_t len;
+
+ if (spa_streq(p, "stereo")) {
+ *map = (struct channel_map) {
+ .channels = 2,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ };
+ } else if (spa_streq(p, "surround-21")) {
+ *map = (struct channel_map) {
+ .channels = 3,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_LFE,
+ };
+ } else if (spa_streq(p, "surround-40")) {
+ *map = (struct channel_map) {
+ .channels = 4,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ };
+ } else if (spa_streq(p, "surround-41")) {
+ *map = (struct channel_map) {
+ .channels = 5,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ .map[4] = SPA_AUDIO_CHANNEL_LFE,
+ };
+ } else if (spa_streq(p, "surround-50")) {
+ *map = (struct channel_map) {
+ .channels = 5,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ .map[4] = SPA_AUDIO_CHANNEL_FC,
+ };
+ } else if (spa_streq(p, "surround-51")) {
+ *map = (struct channel_map) {
+ .channels = 6,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ .map[4] = SPA_AUDIO_CHANNEL_FC,
+ .map[5] = SPA_AUDIO_CHANNEL_LFE,
+ };
+ } else if (spa_streq(p, "surround-71")) {
+ *map = (struct channel_map) {
+ .channels = 8,
+ .map[0] = SPA_AUDIO_CHANNEL_FL,
+ .map[1] = SPA_AUDIO_CHANNEL_FR,
+ .map[2] = SPA_AUDIO_CHANNEL_RL,
+ .map[3] = SPA_AUDIO_CHANNEL_RR,
+ .map[4] = SPA_AUDIO_CHANNEL_FC,
+ .map[5] = SPA_AUDIO_CHANNEL_LFE,
+ .map[6] = SPA_AUDIO_CHANNEL_SL,
+ .map[7] = SPA_AUDIO_CHANNEL_SR,
+ };
+ } else {
+ map->channels = 0;
+ while (*p && map->channels < SPA_AUDIO_MAX_CHANNELS) {
+ if ((len = strcspn(p, ",")) == 0)
+ break;
+ map->map[map->channels++] = channel_paname2id(p, len);
+ p += len + strspn(p+len, ",");
+ }
+ }
+}
+
+bool channel_map_valid(const struct channel_map *map)
+{
+ uint8_t i;
+ uint32_t aux = 0;
+ if (map->channels == 0 || map->channels > CHANNELS_MAX)
+ return false;
+ for (i = 0; i < map->channels; i++)
+ if (channel_id2pa(map->map[i], &aux) >= CHANNEL_POSITION_MAX)
+ return false;
+ return true;
+}
+
+struct encoding_info {
+ const char *name;
+ uint32_t id;
+};
+
+static const struct encoding_info encoding_names[] = {
+ [ENCODING_ANY] = { "ANY", 0 },
+ [ENCODING_PCM] = { "PCM", SPA_AUDIO_IEC958_CODEC_PCM },
+ [ENCODING_AC3_IEC61937] = { "AC3-IEC61937", SPA_AUDIO_IEC958_CODEC_AC3 },
+ [ENCODING_EAC3_IEC61937] = { "EAC3-IEC61937", SPA_AUDIO_IEC958_CODEC_EAC3 },
+ [ENCODING_MPEG_IEC61937] = { "MPEG-IEC61937", SPA_AUDIO_IEC958_CODEC_MPEG },
+ [ENCODING_DTS_IEC61937] = { "DTS-IEC61937", SPA_AUDIO_IEC958_CODEC_DTS },
+ [ENCODING_MPEG2_AAC_IEC61937] = { "MPEG2-AAC-IEC61937", SPA_AUDIO_IEC958_CODEC_MPEG2_AAC },
+ [ENCODING_TRUEHD_IEC61937] = { "TRUEHD-IEC61937", SPA_AUDIO_IEC958_CODEC_TRUEHD },
+ [ENCODING_DTSHD_IEC61937] = { "DTSHD-IEC61937", SPA_AUDIO_IEC958_CODEC_DTSHD },
+};
+
+const char *format_encoding2name(enum encoding enc)
+{
+ if (enc >= 0 && enc < (int)SPA_N_ELEMENTS(encoding_names) &&
+ encoding_names[enc].name != NULL)
+ return encoding_names[enc].name;
+ return "INVALID";
+}
+uint32_t format_encoding2id(enum encoding enc)
+{
+ if (enc >= 0 && enc < (int)SPA_N_ELEMENTS(encoding_names) &&
+ encoding_names[enc].name != NULL)
+ return encoding_names[enc].id;
+ return SPA_ID_INVALID;
+}
+
+static enum encoding format_encoding_from_id(uint32_t id)
+{
+ int i;
+ for (i = 0; i < (int)SPA_N_ELEMENTS(encoding_names); i++) {
+ if (encoding_names[i].id == id)
+ return i;
+ }
+ return ENCODING_ANY;
+}
+
+int format_parse_param(const struct spa_pod *param, bool collect,
+ struct sample_spec *ss, struct channel_map *map,
+ const struct sample_spec *def_ss, const struct channel_map *def_map)
+{
+ struct spa_audio_info info = { 0 };
+ uint32_t i;
+
+ if (spa_format_parse(param, &info.media_type, &info.media_subtype) < 0)
+ return -ENOTSUP;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio)
+ return -ENOTSUP;
+
+ switch (info.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ if (spa_format_audio_raw_parse(param, &info.info.raw) < 0)
+ return -ENOTSUP;
+ if (def_ss != NULL) {
+ if (ss != NULL)
+ *ss = *def_ss;
+ } else {
+ if (info.info.raw.format == 0 ||
+ info.info.raw.rate == 0 ||
+ info.info.raw.channels == 0 ||
+ info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS)
+ return -ENOTSUP;
+ }
+ break;
+ case SPA_MEDIA_SUBTYPE_iec958:
+ {
+ struct spa_audio_info_iec958 iec;
+
+ if (collect)
+ break;
+
+ if (spa_format_audio_iec958_parse(param, &iec) < 0)
+ return -ENOTSUP;
+
+ info.info.raw.format = SPA_AUDIO_FORMAT_S16;
+ info.info.raw.rate = iec.rate;
+ info.info.raw.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.info.raw.position[1] = SPA_AUDIO_CHANNEL_FR;
+ switch (iec.codec) {
+ case SPA_AUDIO_IEC958_CODEC_TRUEHD:
+ case SPA_AUDIO_IEC958_CODEC_DTSHD:
+ info.info.raw.channels = 8;
+ info.info.raw.position[2] = SPA_AUDIO_CHANNEL_FC;
+ info.info.raw.position[3] = SPA_AUDIO_CHANNEL_LFE;
+ info.info.raw.position[4] = SPA_AUDIO_CHANNEL_SL;
+ info.info.raw.position[5] = SPA_AUDIO_CHANNEL_SR;
+ info.info.raw.position[6] = SPA_AUDIO_CHANNEL_RL;
+ info.info.raw.position[7] = SPA_AUDIO_CHANNEL_RR;
+ break;
+ default:
+ info.info.raw.channels = 2;
+ break;
+ }
+ break;
+ }
+ default:
+ return -ENOTSUP;
+ }
+ if (ss) {
+ if (info.info.raw.format)
+ ss->format = info.info.raw.format;
+ if (info.info.raw.rate)
+ ss->rate = info.info.raw.rate;
+ if (info.info.raw.channels)
+ ss->channels = info.info.raw.channels;
+ }
+ if (map) {
+ if (info.info.raw.channels) {
+ map->channels = info.info.raw.channels;
+ for (i = 0; i < map->channels; i++)
+ map->map[i] = info.info.raw.position[i];
+ }
+ }
+ return 0;
+}
+
+const struct spa_pod *format_build_param(struct spa_pod_builder *b, uint32_t id,
+ const struct sample_spec *spec, const struct channel_map *map)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_object(b, &f, 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),
+ 0);
+ if (spec->format != SPA_AUDIO_FORMAT_UNKNOWN)
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(spec->format), 0);
+ else {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(14,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F32,
+ SPA_AUDIO_FORMAT_F32_OE,
+ SPA_AUDIO_FORMAT_S32,
+ SPA_AUDIO_FORMAT_S32_OE,
+ SPA_AUDIO_FORMAT_S24_32,
+ SPA_AUDIO_FORMAT_S24_32_OE,
+ SPA_AUDIO_FORMAT_S24,
+ SPA_AUDIO_FORMAT_S24_OE,
+ SPA_AUDIO_FORMAT_S16,
+ SPA_AUDIO_FORMAT_S16_OE,
+ SPA_AUDIO_FORMAT_ULAW,
+ SPA_AUDIO_FORMAT_ALAW,
+ SPA_AUDIO_FORMAT_U8),
+ 0);
+
+ }
+
+ if (spec->rate != 0)
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_rate, SPA_POD_Int(spec->rate), 0);
+ if (spec->channels != 0) {
+ spa_pod_builder_add(b,
+ SPA_FORMAT_AUDIO_channels, SPA_POD_Int(spec->channels), 0);
+
+ if (map && map->channels == spec->channels) {
+ uint32_t positions[SPA_AUDIO_MAX_CHANNELS];
+ channel_map_to_positions(map, positions);
+ spa_pod_builder_add(b, SPA_FORMAT_AUDIO_position,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id,
+ spec->channels, positions), 0);
+ }
+ }
+ return spa_pod_builder_pop(b, &f);
+}
+
+int format_info_from_spec(struct format_info *info, const struct sample_spec *ss,
+ const struct channel_map *map)
+{
+ spa_zero(*info);
+ info->encoding = ENCODING_PCM;
+ if ((info->props = pw_properties_new(NULL, NULL)) == NULL)
+ return -errno;
+
+ pw_properties_setf(info->props, "format.sample_format", "\"%s\"",
+ format_id2paname(ss->format));
+ pw_properties_setf(info->props, "format.rate", "%d", ss->rate);
+ pw_properties_setf(info->props, "format.channels", "%d", ss->channels);
+ if (map && map->channels == ss->channels) {
+ char chmap[1024] = "";
+ int i, o, r;
+ uint32_t aux = 0;
+
+ for (i = 0, o = 0; i < map->channels; i++) {
+ r = snprintf(chmap+o, sizeof(chmap)-o, "%s%s", i == 0 ? "" : ",",
+ channel_id2paname(map->map[i], &aux));
+ if (r < 0 || o + r >= (int)sizeof(chmap))
+ return -ENOSPC;
+ o += r;
+ }
+ pw_properties_setf(info->props, "format.channel_map", "\"%s\"", chmap);
+ }
+ return 0;
+}
+
+static int add_int(struct format_info *info, const char *k, struct spa_pod *param,
+ uint32_t key)
+{
+ const struct spa_pod_prop *prop;
+ struct spa_pod *val;
+ uint32_t i, n_values, choice;
+ int32_t *values;
+
+ prop = spa_pod_find_prop(param, NULL, key);
+ if (prop == NULL)
+ return -ENOENT;
+
+ val = spa_pod_get_values(&prop->value, &n_values, &choice);
+ if (val->type != SPA_TYPE_Int)
+ return -ENOTSUP;
+
+ values = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ pw_properties_setf(info->props, k, "%d", values[0]);
+ break;
+ case SPA_CHOICE_Range:
+ pw_properties_setf(info->props, k, "{ \"min\": %d, \"max\": %d }",
+ values[1], values[2]);
+ break;
+ case SPA_CHOICE_Enum:
+ {
+ char *ptr;
+ size_t size;
+ FILE *f;
+
+ if ((f = open_memstream(&ptr, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "[");
+ for (i = 1; i < n_values; i++)
+ fprintf(f, "%s %d", i == 1 ? "" : ",", values[i]);
+ fprintf(f, " ]");
+ fclose(f);
+
+ pw_properties_set(info->props, k, ptr);
+ free(ptr);
+ break;
+ }
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static int format_info_pcm_from_param(struct format_info *info, struct spa_pod *param, uint32_t index)
+{
+ if (index > 0)
+ return -ENOENT;
+
+ info->encoding = ENCODING_PCM;
+ /* don't add params here yet, pulseaudio doesn't do that either */
+ return 0;
+}
+
+static int format_info_iec958_from_param(struct format_info *info, struct spa_pod *param, uint32_t index)
+{
+ const struct spa_pod_prop *prop;
+ struct spa_pod *val;
+ uint32_t n_values, *values, choice;
+
+ prop = spa_pod_find_prop(param, NULL, SPA_FORMAT_AUDIO_iec958Codec);
+ if (prop == NULL)
+ return -ENOENT;
+
+ val = spa_pod_get_values(&prop->value, &n_values, &choice);
+ if (val->type != SPA_TYPE_Id)
+ return -ENOTSUP;
+
+ if (index >= n_values)
+ return -ENOENT;
+
+ values = SPA_POD_BODY(val);
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ info->encoding = format_encoding_from_id(values[index]);
+ break;
+ case SPA_CHOICE_Enum:
+ info->encoding = format_encoding_from_id(values[index+1]);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ if ((info->props = pw_properties_new(NULL, NULL)) == NULL)
+ return -errno;
+
+ add_int(info, "format.rate", param, SPA_FORMAT_AUDIO_rate);
+
+ return 0;
+}
+
+int format_info_from_param(struct format_info *info, struct spa_pod *param, uint32_t index)
+{
+ uint32_t media_type, media_subtype;
+ int res;
+
+ if (spa_format_parse(param, &media_type, &media_subtype) < 0)
+ return -ENOTSUP;
+
+ if (media_type != SPA_MEDIA_TYPE_audio)
+ return -ENOTSUP;
+
+ switch(media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ res = format_info_pcm_from_param(info, param, index);
+ break;
+ case SPA_MEDIA_SUBTYPE_iec958:
+ res = format_info_iec958_from_param(info, param, index);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return res;
+}
+
+static uint32_t format_info_get_format(const struct format_info *info)
+{
+ const char *str, *val;
+ struct spa_json it[2];
+ int len;
+
+ 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)
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+
+ if (spa_json_is_string(val, len))
+ return format_paname2id(val+1, len-2);
+
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static int format_info_get_rate(const struct format_info *info)
+{
+ const char *str, *val;
+ struct spa_json it[2];
+ int len, v;
+
+ 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)
+ return -EINVAL;
+ if (spa_json_is_int(val, len)) {
+ if (spa_json_parse_int(val, len, &v) <= 0)
+ return -EINVAL;
+ return v;
+ }
+ return -ENOTSUP;
+}
+
+int format_info_to_spec(const struct format_info *info, struct sample_spec *ss,
+ struct channel_map *map)
+{
+ const char *str, *val;
+ struct spa_json it[2];
+ float f;
+ int res, len;
+
+ spa_zero(*ss);
+ spa_zero(*map);
+
+ if (info->encoding != ENCODING_PCM)
+ return -ENOTSUP;
+ if (info->props == NULL)
+ return -ENOENT;
+
+ if ((ss->format = format_info_get_format(info)) == SPA_AUDIO_FORMAT_UNKNOWN)
+ return -ENOTSUP;
+
+ if ((res = format_info_get_rate(info)) < 0)
+ return res;
+ ss->rate = res;
+
+ 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)
+ return -EINVAL;
+ if (spa_json_is_float(val, len)) {
+ if (spa_json_parse_float(val, len, &f) <= 0)
+ return -EINVAL;
+ ss->channels = f;
+ } else if (spa_json_is_array(val, len)) {
+ return -ENOTSUP;
+ } else if (spa_json_is_object(val, len)) {
+ return -ENOTSUP;
+ } else
+ 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)
+ return -EINVAL;
+ if (!spa_json_is_string(val, len))
+ return -EINVAL;
+ while ((*str == '\"' || *str == ',') &&
+ (len = strcspn(++str, "\",")) > 0) {
+ map->map[map->channels++] = channel_paname2id(str, len);
+ str += len;
+ }
+ }
+ return 0;
+}
+
+const struct spa_pod *format_info_build_param(struct spa_pod_builder *b, uint32_t id,
+ const struct format_info *info, uint32_t *rate)
+{
+ struct sample_spec ss;
+ struct channel_map map;
+ const struct spa_pod *param = NULL;
+ int res;
+
+ switch (info->encoding) {
+ case ENCODING_PCM:
+ if ((res = format_info_to_spec(info, &ss, &map)) < 0) {
+ errno = -res;
+ return NULL;
+ }
+ *rate = ss.rate;
+ param = format_build_param(b, id, &ss, &map);
+ break;
+ case ENCODING_AC3_IEC61937:
+ case ENCODING_EAC3_IEC61937:
+ case ENCODING_MPEG_IEC61937:
+ case ENCODING_DTS_IEC61937:
+ case ENCODING_MPEG2_AAC_IEC61937:
+ case ENCODING_TRUEHD_IEC61937:
+ case ENCODING_DTSHD_IEC61937:
+ {
+ struct spa_audio_info_iec958 i = { 0 };
+ i.codec = format_encoding2id(info->encoding);
+ if ((res = format_info_get_rate(info)) <= 0) {
+ errno = -res;
+ return NULL;
+ }
+ i.rate = res;
+ param = spa_format_audio_iec958_build(b, id, &i);
+ break;
+ }
+ default:
+ errno = ENOTSUP;
+ break;
+ }
+ return param;
+}
diff --git a/src/modules/module-protocol-pulse/format.h b/src/modules/module-protocol-pulse/format.h
new file mode 100644
index 0000000..4300fc0
--- /dev/null
+++ b/src/modules/module-protocol-pulse/format.h
@@ -0,0 +1,233 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_FORMAT_H
+#define PULSE_SERVER_FORMAT_H
+
+#include <spa/utils/defs.h>
+#include <pipewire/properties.h>
+
+struct spa_pod;
+struct spa_pod_builder;
+
+#define RATE_MAX (48000u*8u)
+#define CHANNELS_MAX (64u)
+
+enum sample_format {
+ SAMPLE_U8,
+ SAMPLE_ALAW,
+ SAMPLE_ULAW,
+ SAMPLE_S16LE,
+ SAMPLE_S16BE,
+ SAMPLE_FLOAT32LE,
+ SAMPLE_FLOAT32BE,
+ SAMPLE_S32LE,
+ SAMPLE_S32BE,
+ SAMPLE_S24LE,
+ SAMPLE_S24BE,
+ SAMPLE_S24_32LE,
+ SAMPLE_S24_32BE,
+ SAMPLE_MAX,
+ SAMPLE_INVALID = -1
+};
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define SAMPLE_S16NE SAMPLE_S16BE
+#define SAMPLE_FLOAT32NE SAMPLE_FLOAT32BE
+#define SAMPLE_S32NE SAMPLE_S32BE
+#define SAMPLE_S24NE SAMPLE_S24BE
+#define SAMPLE_S24_32NE SAMPLE_S24_32BE
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+#define SAMPLE_S16NE SAMPLE_S16LE
+#define SAMPLE_FLOAT32NE SAMPLE_FLOAT32LE
+#define SAMPLE_S32NE SAMPLE_S32LE
+#define SAMPLE_S24NE SAMPLE_S24LE
+#define SAMPLE_S24_32NE SAMPLE_S24_32LE
+#endif
+
+struct format {
+ uint32_t pa;
+ uint32_t id;
+ const char *name;
+ uint32_t size;
+};
+
+struct sample_spec {
+ uint32_t format;
+ uint32_t rate;
+ uint8_t channels;
+};
+#define SAMPLE_SPEC_INIT \
+ (struct sample_spec) { \
+ .format = SPA_AUDIO_FORMAT_UNKNOWN, \
+ .rate = 0, \
+ .channels = 0, \
+ }
+
+enum channel_position {
+ CHANNEL_POSITION_INVALID = -1,
+ CHANNEL_POSITION_MONO = 0,
+ CHANNEL_POSITION_FRONT_LEFT,
+ CHANNEL_POSITION_FRONT_RIGHT,
+ CHANNEL_POSITION_FRONT_CENTER,
+
+ CHANNEL_POSITION_REAR_CENTER,
+ CHANNEL_POSITION_REAR_LEFT,
+ CHANNEL_POSITION_REAR_RIGHT,
+
+ CHANNEL_POSITION_LFE,
+ CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
+ CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
+
+ CHANNEL_POSITION_SIDE_LEFT,
+ CHANNEL_POSITION_SIDE_RIGHT,
+ CHANNEL_POSITION_AUX0,
+ CHANNEL_POSITION_AUX1,
+ CHANNEL_POSITION_AUX2,
+ CHANNEL_POSITION_AUX3,
+ CHANNEL_POSITION_AUX4,
+ CHANNEL_POSITION_AUX5,
+ CHANNEL_POSITION_AUX6,
+ CHANNEL_POSITION_AUX7,
+ CHANNEL_POSITION_AUX8,
+ CHANNEL_POSITION_AUX9,
+ CHANNEL_POSITION_AUX10,
+ CHANNEL_POSITION_AUX11,
+ CHANNEL_POSITION_AUX12,
+ CHANNEL_POSITION_AUX13,
+ CHANNEL_POSITION_AUX14,
+ CHANNEL_POSITION_AUX15,
+ CHANNEL_POSITION_AUX16,
+ CHANNEL_POSITION_AUX17,
+ CHANNEL_POSITION_AUX18,
+ CHANNEL_POSITION_AUX19,
+ CHANNEL_POSITION_AUX20,
+ CHANNEL_POSITION_AUX21,
+ CHANNEL_POSITION_AUX22,
+ CHANNEL_POSITION_AUX23,
+ CHANNEL_POSITION_AUX24,
+ CHANNEL_POSITION_AUX25,
+ CHANNEL_POSITION_AUX26,
+ CHANNEL_POSITION_AUX27,
+ CHANNEL_POSITION_AUX28,
+ CHANNEL_POSITION_AUX29,
+ CHANNEL_POSITION_AUX30,
+ CHANNEL_POSITION_AUX31,
+
+ CHANNEL_POSITION_TOP_CENTER,
+
+ CHANNEL_POSITION_TOP_FRONT_LEFT,
+ CHANNEL_POSITION_TOP_FRONT_RIGHT,
+ CHANNEL_POSITION_TOP_FRONT_CENTER,
+
+ CHANNEL_POSITION_TOP_REAR_LEFT,
+ CHANNEL_POSITION_TOP_REAR_RIGHT,
+ CHANNEL_POSITION_TOP_REAR_CENTER,
+
+ CHANNEL_POSITION_MAX
+};
+
+struct channel {
+ uint32_t channel;
+ const char *name;
+};
+
+struct channel_map {
+ uint8_t channels;
+ uint32_t map[CHANNELS_MAX];
+};
+
+#define CHANNEL_MAP_INIT \
+ (struct channel_map) { \
+ .channels = 0, \
+ }
+
+enum encoding {
+ ENCODING_ANY,
+ ENCODING_PCM,
+ ENCODING_AC3_IEC61937,
+ ENCODING_EAC3_IEC61937,
+ ENCODING_MPEG_IEC61937,
+ ENCODING_DTS_IEC61937,
+ ENCODING_MPEG2_AAC_IEC61937,
+ ENCODING_TRUEHD_IEC61937,
+ ENCODING_DTSHD_IEC61937,
+ ENCODING_MAX,
+ ENCODING_INVALID = -1,
+};
+
+struct format_info {
+ enum encoding encoding;
+ struct pw_properties *props;
+};
+
+uint32_t format_pa2id(enum sample_format format);
+const char *format_id2name(uint32_t format);
+uint32_t format_name2id(const char *name);
+uint32_t format_paname2id(const char *name, size_t size);
+enum sample_format format_id2pa(uint32_t id);
+const char *format_id2paname(uint32_t id);
+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);
+bool sample_spec_valid(const struct sample_spec *ss);
+
+uint32_t channel_pa2id(enum channel_position channel);
+const char *channel_id2name(uint32_t channel);
+uint32_t channel_name2id(const char *name);
+enum channel_position channel_id2pa(uint32_t id, uint32_t *aux);
+const char *channel_id2paname(uint32_t id, uint32_t *aux);
+uint32_t channel_paname2id(const char *name, size_t size);
+
+void channel_map_to_positions(const struct channel_map *map, uint32_t *pos);
+void channel_map_parse(const char *str, struct channel_map *map);
+bool channel_map_valid(const struct channel_map *map);
+
+int format_parse_param(const struct spa_pod *param, bool collect, struct sample_spec *ss,
+ struct channel_map *map, const struct sample_spec *def_ss,
+ const struct channel_map *def_map);
+
+const struct spa_pod *format_build_param(struct spa_pod_builder *b, uint32_t id,
+ const struct sample_spec *spec, const struct channel_map *map);
+
+int format_info_from_spec(struct format_info *info, const struct sample_spec *ss,
+ const struct channel_map *map);
+int format_info_from_param(struct format_info *info, struct spa_pod *param, uint32_t index);
+
+const struct spa_pod *format_info_build_param(struct spa_pod_builder *b, uint32_t id,
+ const struct format_info *info, uint32_t *rate);
+
+int format_info_from_spec(struct format_info *info, const struct sample_spec *ss,
+ const struct channel_map *map);
+int format_info_to_spec(const struct format_info *info, struct sample_spec *ss,
+ struct channel_map *map);
+
+static inline void format_info_clear(struct format_info *info)
+{
+ pw_properties_free(info->props);
+ spa_zero(*info);
+}
+
+#endif
diff --git a/src/modules/module-protocol-pulse/internal.h b/src/modules/module-protocol-pulse/internal.h
new file mode 100644
index 0000000..1d5731c
--- /dev/null
+++ b/src/modules/module-protocol-pulse/internal.h
@@ -0,0 +1,108 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_INTERNAL_H
+#define PULSE_SERVER_INTERNAL_H
+
+#include "config.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/ringbuffer.h>
+#include <pipewire/map.h>
+#include <pipewire/private.h>
+
+#include "format.h"
+#include "server.h"
+
+struct pw_loop;
+struct pw_context;
+struct pw_work_queue;
+struct pw_properties;
+
+struct defs {
+ struct spa_fraction min_req;
+ struct spa_fraction default_req;
+ struct spa_fraction min_frag;
+ struct spa_fraction default_frag;
+ struct spa_fraction default_tlength;
+ struct spa_fraction min_quantum;
+ struct sample_spec sample_spec;
+ struct channel_map channel_map;
+ uint32_t quantum_limit;
+ uint32_t idle_timeout;
+};
+
+struct stats {
+ uint32_t n_allocated;
+ uint32_t allocated;
+ uint32_t n_accumulated;
+ uint32_t accumulated;
+ uint32_t sample_cache;
+};
+
+struct impl {
+ struct pw_loop *loop;
+ struct pw_context *context;
+ struct spa_hook context_listener;
+
+ struct pw_properties *props;
+ void *dbus_name;
+
+ struct ratelimit rate_limit;
+
+ struct spa_hook_list hooks;
+ struct spa_list servers;
+
+ struct pw_work_queue *work_queue;
+ struct spa_list cleanup_clients;
+
+ struct pw_map samples;
+ struct pw_map modules;
+
+ struct spa_list free_messages;
+ struct defs defs;
+ struct stats stat;
+};
+
+struct impl_events {
+#define VERSION_IMPL_EVENTS 0
+ uint32_t version;
+
+ void (*server_started) (void *data, struct server *server);
+
+ void (*server_stopped) (void *data, struct server *server);
+};
+
+void impl_add_listener(struct impl *impl,
+ struct spa_hook *listener,
+ const struct impl_events *events, void *data);
+
+extern bool debug_messages;
+
+void broadcast_subscribe_event(struct impl *impl, uint32_t mask, uint32_t event, uint32_t id);
+
+#endif
diff --git a/src/modules/module-protocol-pulse/log.h b/src/modules/module-protocol-pulse/log.h
new file mode 100644
index 0000000..e1f5ca7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/log.h
@@ -0,0 +1,34 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#ifndef PULSE_LOG_H
+#define PULSE_LOG_H
+
+#include <pipewire/log.h>
+
+PW_LOG_TOPIC_EXTERN(mod_topic);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#endif /* PULSE_LOG_H */
diff --git a/src/modules/module-protocol-pulse/manager.c b/src/modules/module-protocol-pulse/manager.c
new file mode 100644
index 0000000..7aa8d70
--- /dev/null
+++ b/src/modules/module-protocol-pulse/manager.c
@@ -0,0 +1,1028 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "manager.h"
+
+#include <spa/pod/iter.h>
+#include <spa/pod/parser.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <pipewire/extensions/metadata.h>
+
+#include "log.h"
+#include "module-protocol-pulse/server.h"
+
+#define manager_emit_sync(m) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, sync, 0)
+#define manager_emit_added(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, added, 0, o)
+#define manager_emit_updated(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, updated, 0, o)
+#define manager_emit_removed(m,o) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, removed, 0, o)
+#define manager_emit_metadata(m,o,s,k,t,v) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, metadata,0,o,s,k,t,v)
+#define manager_emit_disconnect(m) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, disconnect, 0)
+#define manager_emit_object_data_timeout(m,o,k) spa_hook_list_call(&(m)->hooks, struct pw_manager_events, object_data_timeout,0,o,k)
+
+struct object;
+
+struct manager {
+ struct pw_manager this;
+
+ struct pw_loop *loop;
+
+ struct spa_hook core_listener;
+ struct spa_hook registry_listener;
+ int sync_seq;
+
+ struct spa_hook_list hooks;
+};
+
+struct object_info {
+ const char *type;
+ uint32_t version;
+ const void *events;
+ void (*init) (struct object *object);
+ void (*destroy) (struct object *object);
+};
+
+struct object_data {
+ struct spa_list link;
+ struct object *object;
+ const char *key;
+ size_t size;
+ struct spa_source *timer;
+};
+
+struct object {
+ struct pw_manager_object this;
+
+ struct manager *manager;
+
+ const struct object_info *info;
+
+ struct spa_list pending_list;
+
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+
+ struct spa_list data_list;
+};
+
+static int core_sync(struct manager *m)
+{
+ m->sync_seq = pw_core_sync(m->this.core, PW_ID_CORE, m->sync_seq);
+ pw_log_debug("sync start %u", m->sync_seq);
+ return m->sync_seq;
+}
+
+static uint32_t clear_params(struct spa_list *param_list, uint32_t id)
+{
+ struct pw_manager_param *p, *t;
+ uint32_t count = 0;
+
+ spa_list_for_each_safe(p, t, param_list, link) {
+ if (id == SPA_ID_INVALID || p->id == id) {
+ spa_list_remove(&p->link);
+ free(p);
+ count++;
+ }
+ }
+ return count;
+}
+
+static struct pw_manager_param *add_param(struct spa_list *params,
+ int seq, uint32_t id, const struct spa_pod *param)
+{
+ struct pw_manager_param *p;
+
+ if (id == SPA_ID_INVALID) {
+ if (param == NULL || !spa_pod_is_object(param)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ id = SPA_POD_OBJECT_ID(param);
+ }
+
+ p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0));
+ if (p == NULL)
+ return NULL;
+
+ p->id = id;
+ p->seq = seq;
+ if (param != NULL) {
+ p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ } else {
+ clear_params(params, id);
+ p->param = NULL;
+ }
+ spa_list_append(params, &p->link);
+
+ return p;
+}
+
+static bool has_param(struct spa_list *param_list, struct pw_manager_param *p)
+{
+ struct pw_manager_param *t;
+ spa_list_for_each(t, param_list, link) {
+ if (p->id == t->id &&
+ SPA_POD_SIZE(p->param) == SPA_POD_SIZE(t->param) &&
+ memcmp(p->param, t->param, SPA_POD_SIZE(p->param)) == 0)
+ return true;
+ }
+ return false;
+}
+
+static struct object *find_object_by_id(struct manager *m, uint32_t id)
+{
+ struct object *o;
+ spa_list_for_each(o, &m->this.object_list, this.link) {
+ if (o->this.id == id)
+ return o;
+ }
+ return NULL;
+}
+
+static void object_update_params(struct object *o)
+{
+ struct pw_manager_param *p, *t;
+ uint32_t i;
+
+ for (i = 0; i < o->this.n_params; i++) {
+ spa_list_for_each_safe(p, t, &o->pending_list, link) {
+ if (p->id == o->this.params[i].id &&
+ p->seq != o->this.params[i].seq &&
+ p->param != NULL) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+ }
+
+ spa_list_consume(p, &o->pending_list, link) {
+ spa_list_remove(&p->link);
+ if (p->param == NULL) {
+ clear_params(&o->this.param_list, p->id);
+ free(p);
+ } else {
+ spa_list_append(&o->this.param_list, &p->link);
+ }
+ }
+}
+
+static void object_data_free(struct object_data *d)
+{
+ spa_list_remove(&d->link);
+ if (d->timer) {
+ pw_loop_destroy_source(d->object->manager->loop, d->timer);
+ d->timer = NULL;
+ }
+ free(d);
+}
+
+static void object_destroy(struct object *o)
+{
+ struct manager *m = o->manager;
+ struct object_data *d;
+ spa_list_remove(&o->this.link);
+ m->this.n_objects--;
+ if (o->this.proxy)
+ pw_proxy_destroy(o->this.proxy);
+ pw_properties_free(o->this.props);
+ if (o->this.message_object_path)
+ free(o->this.message_object_path);
+ clear_params(&o->this.param_list, SPA_ID_INVALID);
+ clear_params(&o->pending_list, SPA_ID_INVALID);
+ spa_list_consume(d, &o->data_list, link)
+ object_data_free(d);
+ free(o);
+}
+
+/* core */
+static const struct object_info core_info = {
+ .type = PW_TYPE_INTERFACE_Core,
+ .version = PW_VERSION_CORE,
+};
+
+/* client */
+static void client_event_info(void *data, const struct pw_client_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
+
+ info = o->this.info = pw_client_info_merge(o->this.info, info, o->this.changed == 0);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->this.changed += changed;
+ core_sync(o->manager);
+ }
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info,
+};
+
+static void client_destroy(struct object *o)
+{
+ if (o->this.info) {
+ pw_client_info_free(o->this.info);
+ o->this.info = NULL;
+ }
+}
+
+static const struct object_info client_info = {
+ .type = PW_TYPE_INTERFACE_Client,
+ .version = PW_VERSION_CLIENT,
+ .events = &client_events,
+ .destroy = client_destroy,
+};
+
+/* module */
+static void module_event_info(void *data, const struct pw_module_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
+
+ info = o->this.info = pw_module_info_merge(o->this.info, info, o->this.changed == 0);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_MODULE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->this.changed += changed;
+ core_sync(o->manager);
+ }
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info,
+};
+
+static void module_destroy(struct object *o)
+{
+ if (o->this.info) {
+ pw_module_info_free(o->this.info);
+ o->this.info = NULL;
+ }
+}
+
+static const struct object_info module_info = {
+ .type = PW_TYPE_INTERFACE_Module,
+ .version = PW_VERSION_MODULE,
+ .events = &module_events,
+ .destroy = module_destroy,
+};
+
+/* device */
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
+
+ info = o->this.info = pw_device_info_merge(o->this.info, info, o->this.changed == 0);
+ if (info == NULL)
+ return;
+
+ o->this.n_params = info->n_params;
+ o->this.params = info->params;
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+ int res;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ switch (id) {
+ case SPA_PARAM_EnumProfile:
+ case SPA_PARAM_Profile:
+ case SPA_PARAM_EnumRoute:
+ changed++;
+ break;
+ case SPA_PARAM_Route:
+ break;
+ }
+ add_param(&o->pending_list, info->params[i].seq, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_device_enum_params((struct pw_device*)o->this.proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->this.changed += changed;
+ core_sync(o->manager);
+ }
+}
+static struct object *find_device(struct manager *m, uint32_t card_id, uint32_t device)
+{
+ struct object *o;
+
+ spa_list_for_each(o, &m->this.object_list, this.link) {
+ struct pw_node_info *info;
+ const char *str;
+
+ if (!spa_streq(o->this.type, PW_TYPE_INTERFACE_Node))
+ continue;
+
+ if ((info = o->this.info) != NULL &&
+ (str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL &&
+ (uint32_t)atoi(str) == card_id &&
+ (str = spa_dict_lookup(info->props, "card.profile.device")) != NULL &&
+ (uint32_t)atoi(str) == device)
+ return o;
+ }
+ return NULL;
+}
+
+static void device_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data, *dev;
+ struct manager *m = o->manager;
+ struct pw_manager_param *p;
+
+ p = add_param(&o->pending_list, seq, id, param);
+ if (p == NULL)
+ return;
+
+ if (id == SPA_PARAM_Route && !has_param(&o->this.param_list, p)) {
+ uint32_t idx, device;
+ if (spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&device)) < 0)
+ return;
+
+ if ((dev = find_device(m, o->this.id, device)) != NULL) {
+ dev->this.changed++;
+ core_sync(o->manager);
+ }
+ }
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = device_event_param,
+};
+
+static void device_destroy(struct object *o)
+{
+ if (o->this.info) {
+ pw_device_info_free(o->this.info);
+ o->this.info = NULL;
+ }
+}
+
+static const struct object_info device_info = {
+ .type = PW_TYPE_INTERFACE_Device,
+ .version = PW_VERSION_DEVICE,
+ .events = &device_events,
+ .destroy = device_destroy,
+};
+
+/* node */
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->this.id, info->change_mask);
+
+ info = o->this.info = pw_node_info_merge(o->this.info, info, o->this.changed == 0);
+ if (info == NULL)
+ return;
+
+ o->this.n_params = info->n_params;
+ o->this.params = info->params;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_STATE)
+ changed++;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+ int res;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ changed++;
+ add_param(&o->pending_list, info->params[i].seq, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_node_enum_params((struct pw_node*)o->this.proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->this.changed += changed;
+ core_sync(o->manager);
+ }
+}
+
+static void node_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+ add_param(&o->pending_list, seq, id, param);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = node_event_param,
+};
+
+static void node_destroy(struct object *o)
+{
+ if (o->this.info) {
+ pw_node_info_free(o->this.info);
+ o->this.info = NULL;
+ }
+}
+
+static const struct object_info node_info = {
+ .type = PW_TYPE_INTERFACE_Node,
+ .version = PW_VERSION_NODE,
+ .events = &node_events,
+ .destroy = node_destroy,
+};
+
+/* link */
+static const struct object_info link_info = {
+ .type = PW_TYPE_INTERFACE_Link,
+ .version = PW_VERSION_LINK,
+};
+
+/* metadata */
+static int metadata_property(void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct object *o = data;
+ struct manager *m = o->manager;
+ manager_emit_metadata(m, &o->this, subject, key, type, value);
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property,
+};
+
+static void metadata_init(struct object *object)
+{
+ struct object *o = object;
+ struct manager *m = o->manager;
+ o->this.creating = false;
+ manager_emit_added(m, &o->this);
+}
+
+static const struct object_info metadata_info = {
+ .type = PW_TYPE_INTERFACE_Metadata,
+ .version = PW_VERSION_METADATA,
+ .events = &metadata_events,
+ .init = metadata_init,
+};
+
+static const struct object_info *objects[] =
+{
+ &core_info,
+ &module_info,
+ &client_info,
+ &device_info,
+ &node_info,
+ &link_info,
+ &metadata_info,
+};
+
+static const struct object_info *find_info(const char *type, uint32_t version)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(objects, i) {
+ if (spa_streq((*i)->type, type) &&
+ (*i)->version <= version)
+ return *i;
+ }
+ return NULL;
+}
+
+static void
+destroy_removed(void *data)
+{
+ struct object *o = data;
+ pw_proxy_destroy(o->this.proxy);
+}
+
+static void
+destroy_proxy(void *data)
+{
+ struct object *o = data;
+
+ spa_assert(o->info);
+
+ if (o->info->events)
+ spa_hook_remove(&o->object_listener);
+ spa_hook_remove(&o->proxy_listener);
+
+ if (o->info->destroy)
+ o->info->destroy(o);
+
+ o->this.proxy = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = destroy_removed,
+ .destroy = destroy_proxy,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct manager *m = data;
+ struct object *o;
+ const struct object_info *info;
+ const char *str;
+ struct pw_proxy *proxy;
+
+ info = find_info(type, version);
+ if (info == NULL)
+ return;
+
+ proxy = pw_registry_bind(m->this.registry,
+ id, type, info->version, 0);
+ if (proxy == NULL)
+ return;
+
+ o = calloc(1, sizeof(*o));
+ if (o == NULL) {
+ pw_log_error("can't alloc object for %u %s/%d: %m", id, type, version);
+ pw_proxy_destroy(proxy);
+ return;
+ }
+ str = props ? spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL) : NULL;
+ if (!spa_atou64(str, &o->this.serial, 0))
+ o->this.serial = SPA_ID_INVALID;
+
+ o->this.id = id;
+ o->this.permissions = permissions;
+ o->this.type = info->type;
+ o->this.version = version;
+ o->this.index = o->this.serial < (1ULL<<32) ? o->this.serial : SPA_ID_INVALID;
+ o->this.props = props ? pw_properties_new_dict(props) : NULL;
+ o->this.proxy = proxy;
+ o->this.creating = true;
+ spa_list_init(&o->this.param_list);
+ spa_list_init(&o->pending_list);
+ spa_list_init(&o->data_list);
+
+ o->manager = m;
+ o->info = info;
+ spa_list_append(&m->this.object_list, &o->this.link);
+ m->this.n_objects++;
+
+ if (info->events)
+ pw_proxy_add_object_listener(proxy,
+ &o->object_listener,
+ o->info->events, o);
+ pw_proxy_add_listener(proxy,
+ &o->proxy_listener,
+ &proxy_events, o);
+
+ if (info->init)
+ info->init(o);
+
+ core_sync(m);
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct manager *m = data;
+ struct object *o;
+
+ if ((o = find_object_by_id(m, id)) == NULL)
+ return;
+
+ o->this.removing = true;
+
+ if (!o->this.creating)
+ manager_emit_removed(m, &o->this);
+
+ object_destroy(o);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_info(void *data, const struct pw_core_info *info)
+{
+ struct manager *m = data;
+ m->this.info = pw_core_info_merge(m->this.info, info, true);
+}
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct manager *m = data;
+ struct object *o;
+
+ if (id == PW_ID_CORE) {
+ if (m->sync_seq != seq)
+ return;
+
+ pw_log_debug("sync end %u/%u", m->sync_seq, seq);
+
+ manager_emit_sync(m);
+
+ spa_list_for_each(o, &m->this.object_list, this.link)
+ object_update_params(o);
+
+ spa_list_for_each(o, &m->this.object_list, this.link) {
+ if (o->this.creating) {
+ o->this.creating = false;
+ manager_emit_added(m, &o->this);
+ o->this.changed = 0;
+ } else if (o->this.changed > 0) {
+ manager_emit_updated(m, &o->this);
+ o->this.changed = 0;
+ }
+ }
+ }
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct manager *m = data;
+
+ if (id == PW_ID_CORE && res == -EPIPE) {
+ pw_log_debug("connection error: %d, %s", res, message);
+ manager_emit_disconnect(m);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .info = on_core_info,
+ .error = on_core_error
+};
+
+struct pw_manager *pw_manager_new(struct pw_core *core)
+{
+ struct manager *m;
+ struct pw_context *context;
+
+ m = calloc(1, sizeof(*m));
+ if (m == NULL)
+ return NULL;
+
+ m->this.core = core;
+ m->this.registry = pw_core_get_registry(m->this.core,
+ PW_VERSION_REGISTRY, 0);
+ if (m->this.registry == NULL) {
+ free(m);
+ return NULL;
+ }
+
+ context = pw_core_get_context(core);
+ m->loop = pw_context_get_main_loop(context);
+
+ spa_hook_list_init(&m->hooks);
+
+ spa_list_init(&m->this.object_list);
+
+ pw_core_add_listener(m->this.core,
+ &m->core_listener,
+ &core_events, m);
+ pw_registry_add_listener(m->this.registry,
+ &m->registry_listener,
+ &registry_events, m);
+
+ return &m->this;
+}
+
+void pw_manager_add_listener(struct pw_manager *manager,
+ struct spa_hook *listener,
+ const struct pw_manager_events *events, void *data)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ spa_hook_list_append(&m->hooks, listener, events, data);
+ core_sync(m);
+}
+
+int pw_manager_set_metadata(struct pw_manager *manager,
+ struct pw_manager_object *metadata,
+ uint32_t subject, const char *key, const char *type,
+ const char *format, ...)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ struct object *s;
+ va_list args;
+ char buf[1024];
+ char *value;
+
+ if ((s = find_object_by_id(m, subject)) == NULL)
+ return -ENOENT;
+ if (!SPA_FLAG_IS_SET(s->this.permissions, PW_PERM_M))
+ return -EACCES;
+
+ if (metadata == NULL)
+ return -ENOTSUP;
+ if (!SPA_FLAG_IS_SET(metadata->permissions, PW_PERM_W|PW_PERM_X))
+ return -EACCES;
+ if (metadata->proxy == NULL)
+ return -ENOENT;
+
+ if (type != NULL) {
+ va_start(args, format);
+ vsnprintf(buf, sizeof(buf), format, args);
+ va_end(args);
+ value = buf;
+ } else {
+ spa_assert(format == NULL);
+ value = NULL;
+ }
+
+ pw_metadata_set_property(metadata->proxy,
+ subject, key, type, value);
+ return 0;
+}
+
+int pw_manager_for_each_object(struct pw_manager *manager,
+ int (*callback) (void *data, struct pw_manager_object *object),
+ void *data)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ struct object *o;
+ int res;
+
+ spa_list_for_each(o, &m->this.object_list, this.link) {
+ if (o->this.creating || o->this.removing)
+ continue;
+ if ((res = callback(data, &o->this)) != 0)
+ return res;
+ }
+ return 0;
+}
+
+void pw_manager_destroy(struct pw_manager *manager)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ struct object *o;
+
+ spa_hook_list_clean(&m->hooks);
+
+ spa_hook_remove(&m->core_listener);
+
+ spa_list_consume(o, &m->this.object_list, this.link)
+ object_destroy(o);
+
+ spa_hook_remove(&m->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)m->this.registry);
+
+ if (m->this.info)
+ pw_core_info_free(m->this.info);
+
+ free(m);
+}
+
+static struct object_data *object_find_data(struct object *o, const char *key)
+{
+ struct object_data *d;
+ spa_list_for_each(d, &o->data_list, link) {
+ if (spa_streq(d->key, key))
+ return d;
+ }
+ return NULL;
+}
+
+void *pw_manager_object_add_data(struct pw_manager_object *obj, const char *key, size_t size)
+{
+ struct object *o = SPA_CONTAINER_OF(obj, struct object, this);
+ struct object_data *d;
+
+ d = object_find_data(o, key);
+ if (d != NULL) {
+ if (d->size == size)
+ goto done;
+ object_data_free(d);
+ }
+
+ d = calloc(1, sizeof(struct object_data) + size);
+ if (d == NULL)
+ return NULL;
+
+ d->object = o;
+ d->key = key;
+ d->size = size;
+
+ spa_list_append(&o->data_list, &d->link);
+
+done:
+ return SPA_PTROFF(d, sizeof(struct object_data), void);
+}
+
+static void object_data_timeout(void *data, uint64_t count)
+{
+ struct object_data *d = data;
+ struct object *o = d->object;
+ struct manager *m = o->manager;
+
+ pw_log_debug("manager:%p object id:%d data '%s' lifetime ends",
+ m, o->this.id, d->key);
+
+ if (d->timer) {
+ pw_loop_destroy_source(m->loop, d->timer);
+ d->timer = NULL;
+ }
+
+ manager_emit_object_data_timeout(m, &o->this, d->key);
+}
+
+void *pw_manager_object_add_temporary_data(struct pw_manager_object *obj, const char *key,
+ size_t size, uint64_t lifetime_nsec)
+{
+ struct object *o = SPA_CONTAINER_OF(obj, struct object, this);
+ struct object_data *d;
+ void *data;
+ struct timespec timeout = {0}, interval = {0};
+
+ data = pw_manager_object_add_data(obj, key, size);
+ if (data == NULL)
+ return NULL;
+
+ d = SPA_PTROFF(data, -sizeof(struct object_data), void);
+
+ if (d->timer == NULL)
+ d->timer = pw_loop_add_timer(o->manager->loop, object_data_timeout, d);
+ if (d->timer == NULL)
+ return NULL;
+
+ timeout.tv_sec = lifetime_nsec / SPA_NSEC_PER_SEC;
+ timeout.tv_nsec = lifetime_nsec % SPA_NSEC_PER_SEC;
+ pw_loop_update_timer(o->manager->loop, d->timer, &timeout, &interval, false);
+
+ return data;
+}
+
+void *pw_manager_object_get_data(struct pw_manager_object *obj, const char *id)
+{
+ struct object *o = SPA_CONTAINER_OF(obj, struct object, this);
+ struct object_data *d = object_find_data(o, id);
+
+ return d ? SPA_PTROFF(d, sizeof(*d), void) : NULL;
+}
+
+int pw_manager_sync(struct pw_manager *manager)
+{
+ struct manager *m = SPA_CONTAINER_OF(manager, struct manager, this);
+ return core_sync(m);
+}
+
+bool pw_manager_object_is_client(struct pw_manager_object *o)
+{
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Client);
+}
+
+bool pw_manager_object_is_module(struct pw_manager_object *o)
+{
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Module);
+}
+
+bool pw_manager_object_is_card(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Device) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ spa_streq(str, "Audio/Device");
+}
+
+bool pw_manager_object_is_sink(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ (spa_streq(str, "Audio/Sink") || spa_streq(str, "Audio/Duplex"));
+}
+
+bool pw_manager_object_is_source(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ (spa_streq(str, "Audio/Source") ||
+ spa_streq(str, "Audio/Duplex") ||
+ spa_streq(str, "Audio/Source/Virtual"));
+}
+
+bool pw_manager_object_is_monitor(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ (spa_streq(str, "Audio/Sink"));
+}
+
+bool pw_manager_object_is_virtual(struct pw_manager_object *o)
+{
+ const char *str;
+ struct pw_node_info *info;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ (info = o->info) != NULL && info->props != NULL &&
+ (str = spa_dict_lookup(info->props, PW_KEY_NODE_VIRTUAL)) != NULL &&
+ pw_properties_parse_bool(str);
+}
+
+bool pw_manager_object_is_source_or_monitor(struct pw_manager_object *o)
+{
+ return pw_manager_object_is_source(o) || pw_manager_object_is_monitor(o);
+}
+
+bool pw_manager_object_is_sink_input(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ spa_streq(str, "Stream/Output/Audio");
+}
+
+bool pw_manager_object_is_source_output(struct pw_manager_object *o)
+{
+ const char *str;
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Node) &&
+ o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_MEDIA_CLASS)) != NULL &&
+ spa_streq(str, "Stream/Input/Audio");
+}
+
+bool pw_manager_object_is_recordable(struct pw_manager_object *o)
+{
+ return pw_manager_object_is_source(o) || pw_manager_object_is_sink(o) || pw_manager_object_is_sink_input(o);
+}
+
+bool pw_manager_object_is_link(struct pw_manager_object *o)
+{
+ return spa_streq(o->type, PW_TYPE_INTERFACE_Link);
+}
diff --git a/src/modules/module-protocol-pulse/manager.h b/src/modules/module-protocol-pulse/manager.h
new file mode 100644
index 0000000..56aea25
--- /dev/null
+++ b/src/modules/module-protocol-pulse/manager.h
@@ -0,0 +1,145 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_MANAGER_H
+#define PIPEWIRE_MANAGER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/pod/pod.h>
+
+#include <pipewire/pipewire.h>
+
+struct pw_manager_object;
+
+struct pw_manager_events {
+#define PW_VERSION_MANAGER_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*sync) (void *data);
+
+ void (*added) (void *data, struct pw_manager_object *object);
+
+ void (*updated) (void *data, struct pw_manager_object *object);
+
+ void (*removed) (void *data, struct pw_manager_object *object);
+
+ void (*metadata) (void *data, struct pw_manager_object *object,
+ uint32_t subject, const char *key,
+ const char *type, const char *value);
+
+ void (*disconnect) (void *data);
+
+ void (*object_data_timeout) (void *data, struct pw_manager_object *object,
+ const char *key);
+};
+
+struct pw_manager {
+ struct pw_core *core;
+ struct pw_registry *registry;
+
+ struct pw_core_info *info;
+
+ uint32_t n_objects;
+ struct spa_list object_list;
+};
+
+struct pw_manager_param {
+ uint32_t id;
+ int32_t seq;
+ struct spa_list link; /**< link in manager_object param_list */
+ struct spa_pod *param;
+};
+
+struct pw_manager_object {
+ struct spa_list link; /**< link in manager object_list */
+ uint64_t serial;
+ uint32_t id;
+ uint32_t permissions;
+ const char *type;
+ uint32_t version;
+ uint32_t index;
+ struct pw_properties *props;
+ struct pw_proxy *proxy;
+ char *message_object_path;
+ int (*message_handler)(struct pw_manager *m, struct pw_manager_object *o,
+ const char *message, const char *params, char **response);
+
+ int changed;
+ void *info;
+ struct spa_param_info *params;
+ uint32_t n_params;
+
+ struct spa_list param_list;
+ unsigned int creating:1;
+ unsigned int removing:1;
+};
+
+struct pw_manager *pw_manager_new(struct pw_core *core);
+
+void pw_manager_add_listener(struct pw_manager *manager,
+ struct spa_hook *listener,
+ const struct pw_manager_events *events, void *data);
+
+int pw_manager_sync(struct pw_manager *manager);
+
+void pw_manager_destroy(struct pw_manager *manager);
+
+int pw_manager_set_metadata(struct pw_manager *manager,
+ struct pw_manager_object *metadata,
+ uint32_t subject, const char *key, const char *type,
+ const char *format, ...) SPA_PRINTF_FUNC(6,7);
+
+int pw_manager_for_each_object(struct pw_manager *manager,
+ int (*callback) (void *data, struct pw_manager_object *object),
+ void *data);
+
+void *pw_manager_object_add_data(struct pw_manager_object *o, const char *key, size_t size);
+void *pw_manager_object_get_data(struct pw_manager_object *obj, const char *key);
+void *pw_manager_object_add_temporary_data(struct pw_manager_object *o, const char *key,
+ size_t size, uint64_t lifetime_nsec);
+
+bool pw_manager_object_is_client(struct pw_manager_object *o);
+bool pw_manager_object_is_module(struct pw_manager_object *o);
+bool pw_manager_object_is_card(struct pw_manager_object *o);
+bool pw_manager_object_is_sink(struct pw_manager_object *o);
+bool pw_manager_object_is_source(struct pw_manager_object *o);
+bool pw_manager_object_is_monitor(struct pw_manager_object *o);
+bool pw_manager_object_is_virtual(struct pw_manager_object *o);
+bool pw_manager_object_is_source_or_monitor(struct pw_manager_object *o);
+bool pw_manager_object_is_sink_input(struct pw_manager_object *o);
+bool pw_manager_object_is_source_output(struct pw_manager_object *o);
+bool pw_manager_object_is_recordable(struct pw_manager_object *o);
+bool pw_manager_object_is_link(struct pw_manager_object *o);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_MANAGER_H */
diff --git a/src/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c
new file mode 100644
index 0000000..8763ea7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/message-handler.c
@@ -0,0 +1,143 @@
+#include <stdint.h>
+
+#include <regex.h>
+
+#include <spa/param/props.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/pod.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/pipewire.h>
+
+#include "collect.h"
+#include "log.h"
+#include "manager.h"
+#include "message-handler.h"
+
+static int bluez_card_object_message_handler(struct pw_manager *m, struct pw_manager_object *o, const char *message, const char *params, char **response)
+{
+ struct transport_codec_info codecs[64];
+ uint32_t n_codecs, active;
+
+ pw_log_debug(": bluez-card %p object message:'%s' params:'%s'", o, message, params);
+
+ n_codecs = collect_transport_codec_info(o, codecs, SPA_N_ELEMENTS(codecs), &active);
+
+ if (n_codecs == 0)
+ return -EINVAL;
+
+ if (spa_streq(message, "switch-codec")) {
+ char codec[256];
+ struct spa_json it;
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[1];
+ struct spa_pod *param;
+ uint32_t codec_id = SPA_ID_INVALID;
+
+ /* Parse args */
+ if (params == NULL)
+ return -EINVAL;
+
+ spa_json_init(&it, params, strlen(params));
+ if (spa_json_get_string(&it, codec, sizeof(codec)) <= 0)
+ return -EINVAL;
+
+ codec_id = atoi(codec);
+
+ /* Switch codec */
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ spa_pod_builder_add(&b,
+ SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(codec_id), 0);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_device_set_param((struct pw_device *)o->proxy,
+ SPA_PARAM_Props, 0, param);
+ return 0;
+ } else if (spa_streq(message, "list-codecs")) {
+ uint32_t i;
+ FILE *r;
+ size_t size;
+ bool first = true;
+
+ r = open_memstream(response, &size);
+ if (r == NULL)
+ return -errno;
+
+ fputc('[', r);
+ for (i = 0; i < n_codecs; ++i) {
+ const char *desc = codecs[i].description;
+ fprintf(r, "%s{\"name\":\"%d\",\"description\":\"%s\"}",
+ first ? "" : ",",
+ (int)codecs[i].id, desc ? desc : "Unknown");
+ first = false;
+ }
+ fputc(']', r);
+
+ return fclose(r) ? -errno : 0;
+ } else if (spa_streq(message, "get-codec")) {
+ if (active == SPA_ID_INVALID)
+ *response = strdup("null");
+ else
+ *response = spa_aprintf("\"%d\"", (int)codecs[active].id);
+ return *response ? 0 : -ENOMEM;
+ }
+
+ return -ENOSYS;
+}
+
+static int core_object_message_handler(struct pw_manager *m, struct pw_manager_object *o, const char *message, const char *params, char **response)
+{
+ pw_log_debug(": core %p object message:'%s' params:'%s'", o, message, params);
+
+ if (spa_streq(message, "list-handlers")) {
+ FILE *r;
+ size_t size;
+ bool first = true;
+
+ r = open_memstream(response, &size);
+ if (r == NULL)
+ return -errno;
+
+ fputc('[', r);
+ spa_list_for_each(o, &m->object_list, link) {
+ if (o->message_object_path) {
+ fprintf(r, "%s{\"name\":\"%s\",\"description\":\"%s\"}",
+ first ? "" : ",",
+ o->message_object_path, o->type);
+ first = false;
+ }
+ }
+ fputc(']', r);
+ return fclose(r) ? -errno : 0;
+ }
+
+ return -ENOSYS;
+}
+
+void register_object_message_handlers(struct pw_manager_object *o)
+{
+ const char *str;
+
+ if (o->id == PW_ID_CORE) {
+ free(o->message_object_path);
+ o->message_object_path = strdup("/core");
+ o->message_handler = core_object_message_handler;
+ return;
+ }
+
+ if (pw_manager_object_is_card(o) && o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_DEVICE_API)) != NULL &&
+ spa_streq(str, "bluez5")) {
+ str = pw_properties_get(o->props, PW_KEY_DEVICE_NAME);
+ if (str) {
+ free(o->message_object_path);
+ o->message_object_path = spa_aprintf("/card/%s/bluez", str);
+ o->message_handler = bluez_card_object_message_handler;
+ }
+ return;
+ }
+}
diff --git a/src/modules/module-protocol-pulse/message-handler.h b/src/modules/module-protocol-pulse/message-handler.h
new file mode 100644
index 0000000..da2a7b0
--- /dev/null
+++ b/src/modules/module-protocol-pulse/message-handler.h
@@ -0,0 +1,8 @@
+#ifndef PULSE_SERVER_MESSAGE_HANDLER_H
+#define PULSE_SERVER_MESSAGE_HANDLER_H
+
+struct pw_manager_object;
+
+void register_object_message_handlers(struct pw_manager_object *o);
+
+#endif /* PULSE_SERVER_MESSAGE_HANDLER_H */
diff --git a/src/modules/module-protocol-pulse/message.c b/src/modules/module-protocol-pulse/message.c
new file mode 100644
index 0000000..daf4d2e
--- /dev/null
+++ b/src/modules/module-protocol-pulse/message.c
@@ -0,0 +1,879 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <arpa/inet.h>
+#include <math.h>
+
+#include <spa/debug/buffer.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/string.h>
+#include <pipewire/log.h>
+
+#include "defs.h"
+#include "format.h"
+#include "internal.h"
+#include "message.h"
+#include "remap.h"
+#include "volume.h"
+
+#define MAX_SIZE (256*1024)
+#define MAX_ALLOCATED (16*1024 *1024)
+
+#define VOLUME_MUTED ((uint32_t) 0U)
+#define VOLUME_NORM ((uint32_t) 0x10000U)
+#define VOLUME_MAX ((uint32_t) UINT32_MAX/2)
+
+#define PA_CHANNELS_MAX (32u)
+
+PW_LOG_TOPIC_EXTERN(pulse_conn);
+#define PW_LOG_TOPIC_DEFAULT pulse_conn
+
+static inline uint32_t volume_from_linear(float vol)
+{
+ uint32_t v;
+ if (vol <= 0.0f)
+ v = VOLUME_MUTED;
+ else
+ v = SPA_CLAMP((uint64_t) lround(cbrt(vol) * VOLUME_NORM),
+ VOLUME_MUTED, VOLUME_MAX);
+ return v;
+}
+
+static inline float volume_to_linear(uint32_t vol)
+{
+ float v = ((float)vol) / VOLUME_NORM;
+ return v * v * v;
+}
+
+static int read_u8(struct message *m, uint8_t *val)
+{
+ if (m->offset + 1 > m->length)
+ return -ENOSPC;
+ *val = m->data[m->offset];
+ m->offset++;
+ return 0;
+}
+
+static int read_u32(struct message *m, uint32_t *val)
+{
+ if (m->offset + 4 > m->length)
+ return -ENOSPC;
+ memcpy(val, &m->data[m->offset], 4);
+ *val = ntohl(*val);
+ m->offset += 4;
+ return 0;
+}
+static int read_u64(struct message *m, uint64_t *val)
+{
+ uint32_t tmp;
+ int res;
+ if ((res = read_u32(m, &tmp)) < 0)
+ return res;
+ *val = ((uint64_t)tmp) << 32;
+ if ((res = read_u32(m, &tmp)) < 0)
+ return res;
+ *val |= tmp;
+ return 0;
+}
+
+static int read_sample_spec(struct message *m, struct sample_spec *ss)
+{
+ int res;
+ uint8_t tmp;
+ if ((res = read_u8(m, &tmp)) < 0)
+ return res;
+ ss->format = format_pa2id(tmp);
+ if ((res = read_u8(m, &ss->channels)) < 0)
+ return res;
+ return read_u32(m, &ss->rate);
+}
+
+static int read_props(struct message *m, struct pw_properties *props, bool remap)
+{
+ int res;
+
+ while (true) {
+ const char *key;
+ const void *data;
+ uint32_t length;
+ size_t size;
+ const struct str_map *map;
+
+ if ((res = message_get(m,
+ TAG_STRING, &key,
+ TAG_INVALID)) < 0)
+ return res;
+
+ if (key == NULL)
+ break;
+
+ if ((res = message_get(m,
+ TAG_U32, &length,
+ TAG_INVALID)) < 0)
+ return res;
+ if (length > MAX_TAG_SIZE)
+ return -EINVAL;
+
+ if ((res = message_get(m,
+ TAG_ARBITRARY, &data, &size,
+ TAG_INVALID)) < 0)
+ return res;
+
+ if (remap && (map = str_map_find(props_key_map, NULL, key)) != NULL) {
+ key = map->pw_str;
+ if (map->child != NULL &&
+ (map = str_map_find(map->child, NULL, data)) != NULL)
+ data = map->pw_str;
+ }
+ pw_properties_set(props, key, data);
+ }
+ return 0;
+}
+
+static int read_arbitrary(struct message *m, const void **val, size_t *length)
+{
+ uint32_t len;
+ int res;
+ if ((res = read_u32(m, &len)) < 0)
+ return res;
+ if (m->offset + len > m->length)
+ return -ENOSPC;
+ *val = m->data + m->offset;
+ m->offset += len;
+ if (length)
+ *length = len;
+ return 0;
+}
+
+static int read_string(struct message *m, char **str)
+{
+ uint32_t n, maxlen;
+ if (m->offset + 1 > m->length)
+ return -ENOSPC;
+ maxlen = m->length - m->offset;
+ n = strnlen(SPA_PTROFF(m->data, m->offset, char), maxlen);
+ if (n == maxlen)
+ return -EINVAL;
+ *str = SPA_PTROFF(m->data, m->offset, char);
+ m->offset += n + 1;
+ return 0;
+}
+
+static int read_timeval(struct message *m, struct timeval *tv)
+{
+ int res;
+ uint32_t tmp;
+
+ if ((res = read_u32(m, &tmp)) < 0)
+ return res;
+ tv->tv_sec = tmp;
+ if ((res = read_u32(m, &tmp)) < 0)
+ return res;
+ tv->tv_usec = tmp;
+ return 0;
+}
+
+static int read_channel_map(struct message *m, struct channel_map *map)
+{
+ int res;
+ uint8_t i, tmp;
+
+ if ((res = read_u8(m, &map->channels)) < 0)
+ return res;
+ if (map->channels > CHANNELS_MAX)
+ return -EINVAL;
+ for (i = 0; i < map->channels; i ++) {
+ if ((res = read_u8(m, &tmp)) < 0)
+ return res;
+ map->map[i] = channel_pa2id(tmp);
+ }
+ return 0;
+}
+static int read_volume(struct message *m, float *vol)
+{
+ int res;
+ uint32_t v;
+ if ((res = read_u32(m, &v)) < 0)
+ return res;
+ *vol = volume_to_linear(v);
+ return 0;
+}
+
+static int read_cvolume(struct message *m, struct volume *vol)
+{
+ int res;
+ uint8_t i;
+
+ if ((res = read_u8(m, &vol->channels)) < 0)
+ return res;
+ if (vol->channels > CHANNELS_MAX)
+ return -EINVAL;
+ for (i = 0; i < vol->channels; i ++) {
+ if ((res = read_volume(m, &vol->values[i])) < 0)
+ return res;
+ }
+ return 0;
+}
+
+static int read_format_info(struct message *m, struct format_info *info)
+{
+ int res;
+ uint8_t tag, encoding;
+
+ spa_zero(*info);
+ if ((res = read_u8(m, &tag)) < 0)
+ return res;
+ if (tag != TAG_U8)
+ return -EPROTO;
+ if ((res = read_u8(m, &encoding)) < 0)
+ return res;
+ info->encoding = encoding;
+
+ if ((res = read_u8(m, &tag)) < 0)
+ return res;
+ if (tag != TAG_PROPLIST)
+ return -EPROTO;
+
+ info->props = pw_properties_new(NULL, NULL);
+ if (info->props == NULL)
+ return -errno;
+ if ((res = read_props(m, info->props, false)) < 0)
+ format_info_clear(info);
+ return res;
+}
+
+int message_get(struct message *m, ...)
+{
+ va_list va;
+ int res = 0;
+
+ va_start(va, m);
+
+ while (true) {
+ int tag = va_arg(va, int);
+ uint8_t dtag;
+ if (tag == TAG_INVALID)
+ break;
+
+ if ((res = read_u8(m, &dtag)) < 0)
+ goto done;
+
+ switch (dtag) {
+ case TAG_STRING:
+ if (tag != TAG_STRING)
+ goto invalid;
+ if ((res = read_string(m, va_arg(va, char**))) < 0)
+ goto done;
+ break;
+ case TAG_STRING_NULL:
+ if (tag != TAG_STRING)
+ goto invalid;
+ *va_arg(va, char**) = NULL;
+ break;
+ case TAG_U8:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_u8(m, va_arg(va, uint8_t*))) < 0)
+ goto done;
+ break;
+ case TAG_U32:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_u32(m, va_arg(va, uint32_t*))) < 0)
+ goto done;
+ break;
+ case TAG_S64:
+ case TAG_U64:
+ case TAG_USEC:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_u64(m, va_arg(va, uint64_t*))) < 0)
+ goto done;
+ break;
+ case TAG_SAMPLE_SPEC:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_sample_spec(m, va_arg(va, struct sample_spec*))) < 0)
+ goto done;
+ break;
+ case TAG_ARBITRARY:
+ {
+ const void **val = va_arg(va, const void**);
+ size_t *len = va_arg(va, size_t*);
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_arbitrary(m, val, len)) < 0)
+ goto done;
+ break;
+ }
+ case TAG_BOOLEAN_TRUE:
+ if (tag != TAG_BOOLEAN)
+ goto invalid;
+ *va_arg(va, bool*) = true;
+ break;
+ case TAG_BOOLEAN_FALSE:
+ if (tag != TAG_BOOLEAN)
+ goto invalid;
+ *va_arg(va, bool*) = false;
+ break;
+ case TAG_TIMEVAL:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_timeval(m, va_arg(va, struct timeval*))) < 0)
+ goto done;
+ break;
+ case TAG_CHANNEL_MAP:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_channel_map(m, va_arg(va, struct channel_map*))) < 0)
+ goto done;
+ break;
+ case TAG_CVOLUME:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_cvolume(m, va_arg(va, struct volume*))) < 0)
+ goto done;
+ break;
+ case TAG_PROPLIST:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_props(m, va_arg(va, struct pw_properties*), true)) < 0)
+ goto done;
+ break;
+ case TAG_VOLUME:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_volume(m, va_arg(va, float*))) < 0)
+ goto done;
+ break;
+ case TAG_FORMAT_INFO:
+ if (dtag != tag)
+ goto invalid;
+ if ((res = read_format_info(m, va_arg(va, struct format_info*))) < 0)
+ goto done;
+ break;
+ }
+ }
+ res = 0;
+ goto done;
+
+invalid:
+ res = -EINVAL;
+
+done:
+ va_end(va);
+
+ return res;
+}
+
+static int ensure_size(struct message *m, uint32_t size)
+{
+ uint32_t alloc, diff;
+ void *data;
+
+ if (m->length > m->allocated)
+ return -ENOMEM;
+
+ if (m->length + size <= m->allocated)
+ return size;
+
+ alloc = SPA_ROUND_UP_N(SPA_MAX(m->allocated + size, 4096u), 4096u);
+ diff = alloc - m->allocated;
+ if ((data = realloc(m->data, alloc)) == NULL) {
+ free(m->data);
+ m->data = NULL;
+ m->impl->stat.allocated -= m->allocated;
+ m->allocated = 0;
+ return -errno;
+ }
+ m->impl->stat.allocated += diff;
+ m->impl->stat.accumulated += diff;
+ m->data = data;
+ m->allocated = alloc;
+ return size;
+}
+
+static void write_8(struct message *m, uint8_t val)
+{
+ if (ensure_size(m, 1) > 0)
+ m->data[m->length] = val;
+ m->length++;
+}
+
+static void write_32(struct message *m, uint32_t val)
+{
+ val = htonl(val);
+ if (ensure_size(m, 4) > 0)
+ memcpy(m->data + m->length, &val, 4);
+ m->length += 4;
+}
+
+static void write_string(struct message *m, const char *s)
+{
+ write_8(m, s ? TAG_STRING : TAG_STRING_NULL);
+ if (s != NULL) {
+ int len = strlen(s) + 1;
+ if (ensure_size(m, len) > 0)
+ strcpy(SPA_PTROFF(m->data, m->length, char), s);
+ m->length += len;
+ }
+}
+static void write_u8(struct message *m, uint8_t val)
+{
+ write_8(m, TAG_U8);
+ write_8(m, val);
+}
+
+static void write_u32(struct message *m, uint32_t val)
+{
+ write_8(m, TAG_U32);
+ write_32(m, val);
+}
+
+static void write_64(struct message *m, uint8_t tag, uint64_t val)
+{
+ write_8(m, tag);
+ write_32(m, val >> 32);
+ write_32(m, val);
+}
+
+static void write_sample_spec(struct message *m, struct sample_spec *ss)
+{
+ uint32_t channels = SPA_MIN(ss->channels, PA_CHANNELS_MAX);
+ write_8(m, TAG_SAMPLE_SPEC);
+ write_8(m, format_id2pa(ss->format));
+ write_8(m, channels);
+ write_32(m, ss->rate);
+}
+
+static void write_arbitrary(struct message *m, const void *p, size_t length)
+{
+ write_8(m, TAG_ARBITRARY);
+ write_32(m, length);
+ if (ensure_size(m, length) > 0)
+ memcpy(m->data + m->length, p, length);
+ m->length += length;
+}
+
+static void write_boolean(struct message *m, bool val)
+{
+ write_8(m, val ? TAG_BOOLEAN_TRUE : TAG_BOOLEAN_FALSE);
+}
+
+static void write_timeval(struct message *m, struct timeval *tv)
+{
+ write_8(m, TAG_TIMEVAL);
+ write_32(m, tv->tv_sec);
+ write_32(m, tv->tv_usec);
+}
+
+static void write_channel_map(struct message *m, struct channel_map *map)
+{
+ uint8_t i;
+ uint32_t aux = 0, channels = SPA_MIN(map->channels, PA_CHANNELS_MAX);
+ write_8(m, TAG_CHANNEL_MAP);
+ write_8(m, channels);
+ for (i = 0; i < channels; i ++)
+ write_8(m, channel_id2pa(map->map[i], &aux));
+}
+
+static void write_volume(struct message *m, float vol)
+{
+ write_8(m, TAG_VOLUME);
+ write_32(m, volume_from_linear(vol));
+}
+
+static void write_cvolume(struct message *m, struct volume *vol)
+{
+ uint8_t i;
+ uint32_t channels = SPA_MIN(vol->channels, PA_CHANNELS_MAX);
+ write_8(m, TAG_CVOLUME);
+ write_8(m, channels);
+ for (i = 0; i < channels; i ++)
+ write_32(m, volume_from_linear(vol->values[i]));
+}
+
+static void add_stream_group(struct message *m, struct spa_dict *dict, const char *key,
+ const char *media_class, const char *media_role)
+{
+ const char *str, *id, *prefix;
+ char *b;
+ int l;
+
+ if (media_class == NULL)
+ return;
+ if (spa_streq(media_class, "Stream/Output/Audio"))
+ prefix = "sink-input";
+ else if (spa_streq(media_class, "Stream/Input/Audio"))
+ prefix = "source-output";
+ else
+ return;
+
+ if ((str = media_role) != NULL)
+ id = "media-role";
+ else if ((str = spa_dict_lookup(dict, PW_KEY_APP_ID)) != NULL)
+ id = "application-id";
+ else if ((str = spa_dict_lookup(dict, PW_KEY_APP_NAME)) != NULL)
+ id = "application-name";
+ else if ((str = spa_dict_lookup(dict, PW_KEY_MEDIA_NAME)) != NULL)
+ id = "media-name";
+ else
+ return;
+
+ write_string(m, key);
+ l = strlen(prefix) + strlen(id) + strlen(str) + 6; /* "-by-" , ":" and \0 */
+ b = alloca(l);
+ snprintf(b, l, "%s-by-%s:%s", prefix, id, str);
+ write_u32(m, l);
+ write_arbitrary(m, b, l);
+}
+
+static void write_dict(struct message *m, struct spa_dict *dict, bool remap)
+{
+ const struct spa_dict_item *it;
+
+ write_8(m, TAG_PROPLIST);
+ if (dict != NULL) {
+ const char *media_class = NULL, *media_role = NULL;
+ spa_dict_for_each(it, dict) {
+ const char *key = it->key;
+ const char *val = it->value;
+ int l;
+ const struct str_map *map;
+
+ if (remap && (map = str_map_find(props_key_map, key, NULL)) != NULL) {
+ key = map->pa_str;
+ if (map->child != NULL &&
+ (map = str_map_find(map->child, val, NULL)) != NULL)
+ val = map->pa_str;
+ }
+ if (spa_streq(key, "media.class"))
+ media_class = val;
+ if (spa_streq(key, "media.role"))
+ media_role = val;
+
+ write_string(m, key);
+ l = strlen(val) + 1;
+ write_u32(m, l);
+ write_arbitrary(m, val, l);
+
+ }
+ if (remap)
+ add_stream_group(m, dict, "module-stream-restore.id",
+ media_class, media_role);
+ }
+ write_string(m, NULL);
+}
+
+static void write_format_info(struct message *m, struct format_info *info)
+{
+ write_8(m, TAG_FORMAT_INFO);
+ write_u8(m, (uint8_t) info->encoding);
+ write_dict(m, info->props ? &info->props->dict : NULL, false);
+}
+
+int message_put(struct message *m, ...)
+{
+ va_list va;
+
+ if (m == NULL)
+ return -EINVAL;
+
+ va_start(va, m);
+
+ while (true) {
+ int tag = va_arg(va, int);
+ if (tag == TAG_INVALID)
+ break;
+
+ switch (tag) {
+ case TAG_STRING:
+ write_string(m, va_arg(va, const char *));
+ break;
+ case TAG_U8:
+ write_u8(m, (uint8_t)va_arg(va, int));
+ break;
+ case TAG_U32:
+ write_u32(m, (uint32_t)va_arg(va, uint32_t));
+ break;
+ case TAG_S64:
+ case TAG_U64:
+ case TAG_USEC:
+ write_64(m, tag, va_arg(va, uint64_t));
+ break;
+ case TAG_SAMPLE_SPEC:
+ write_sample_spec(m, va_arg(va, struct sample_spec*));
+ break;
+ case TAG_ARBITRARY:
+ {
+ const void *p = va_arg(va, const void*);
+ size_t length = va_arg(va, size_t);
+ write_arbitrary(m, p, length);
+ break;
+ }
+ case TAG_BOOLEAN:
+ write_boolean(m, va_arg(va, int));
+ break;
+ case TAG_TIMEVAL:
+ write_timeval(m, va_arg(va, struct timeval*));
+ break;
+ case TAG_CHANNEL_MAP:
+ write_channel_map(m, va_arg(va, struct channel_map*));
+ break;
+ case TAG_CVOLUME:
+ write_cvolume(m, va_arg(va, struct volume*));
+ break;
+ case TAG_PROPLIST:
+ write_dict(m, va_arg(va, struct spa_dict*), true);
+ break;
+ case TAG_VOLUME:
+ write_volume(m, va_arg(va, double));
+ break;
+ case TAG_FORMAT_INFO:
+ write_format_info(m, va_arg(va, struct format_info*));
+ break;
+ }
+ }
+ va_end(va);
+
+ if (m->length > m->allocated)
+ return -ENOMEM;
+
+ return 0;
+}
+
+int message_dump(enum spa_log_level level, struct message *m)
+{
+ int res;
+ uint32_t i, offset = m->offset, o;
+
+ pw_log(level, "message: len:%d alloc:%u", m->length, m->allocated);
+ while (true) {
+ uint8_t tag;
+
+ o = m->offset;
+ if (read_u8(m, &tag) < 0)
+ break;
+
+ switch (tag) {
+ case TAG_STRING:
+ {
+ char *val;
+ if ((res = read_string(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: string: '%s'", o, val);
+ break;
+ }
+ case TAG_STRING_NULL:
+ pw_log(level, "%u: string: NULL", o);
+ break;
+ case TAG_U8:
+ {
+ uint8_t val;
+ if ((res = read_u8(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: u8: %u", o, val);
+ break;
+ }
+ case TAG_U32:
+ {
+ uint32_t val;
+ if ((res = read_u32(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: u32: %u", o, val);
+ break;
+ }
+ case TAG_S64:
+ {
+ uint64_t val;
+ if ((res = read_u64(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: s64: %"PRIi64"", o, (int64_t)val);
+ break;
+ }
+ case TAG_U64:
+ {
+ uint64_t val;
+ if ((res = read_u64(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: u64: %"PRIu64"", o, val);
+ break;
+ }
+ case TAG_USEC:
+ {
+ uint64_t val;
+ if ((res = read_u64(m, &val)) < 0)
+ return res;
+ pw_log(level, "%u: u64: %"PRIu64"", o, val);
+ break;
+ }
+ case TAG_SAMPLE_SPEC:
+ {
+ struct sample_spec ss;
+ if ((res = read_sample_spec(m, &ss)) < 0)
+ return res;
+ pw_log(level, "%u: ss: format:%s rate:%d channels:%u", o,
+ format_id2name(ss.format), ss.rate,
+ ss.channels);
+ break;
+ }
+ case TAG_ARBITRARY:
+ {
+ const void *mem;
+ size_t len;
+ if ((res = read_arbitrary(m, &mem, &len)) < 0)
+ return res;
+ spa_debug_mem(0, mem, len);
+ break;
+ }
+ case TAG_BOOLEAN_TRUE:
+ pw_log(level, "%u: bool: true", o);
+ break;
+ case TAG_BOOLEAN_FALSE:
+ pw_log(level, "%u: bool: false", o);
+ break;
+ case TAG_TIMEVAL:
+ {
+ struct timeval tv;
+ if ((res = read_timeval(m, &tv)) < 0)
+ return res;
+ pw_log(level, "%u: timeval: %lu:%lu", o, tv.tv_sec, tv.tv_usec);
+ break;
+ }
+ case TAG_CHANNEL_MAP:
+ {
+ struct channel_map map;
+ if ((res = read_channel_map(m, &map)) < 0)
+ return res;
+ pw_log(level, "%u: channelmap: channels:%u", o, map.channels);
+ for (i = 0; i < map.channels; i++)
+ pw_log(level, " %d: %s", i, channel_id2name(map.map[i]));
+ break;
+ }
+ case TAG_CVOLUME:
+ {
+ struct volume vol;
+ if ((res = read_cvolume(m, &vol)) < 0)
+ return res;
+ pw_log(level, "%u: cvolume: channels:%u", o, vol.channels);
+ for (i = 0; i < vol.channels; i++)
+ pw_log(level, " %d: %f", i, vol.values[i]);
+ break;
+ }
+ case TAG_PROPLIST:
+ {
+ struct pw_properties *props = pw_properties_new(NULL, NULL);
+ const struct spa_dict_item *it;
+ res = read_props(m, props, false);
+ if (res >= 0) {
+ pw_log(level, "%u: props: n_items:%u", o, props->dict.n_items);
+ spa_dict_for_each(it, &props->dict)
+ pw_log(level, " '%s': '%s'", it->key, it->value);
+ }
+ pw_properties_free(props);
+ if (res < 0)
+ return res;
+ break;
+ }
+ case TAG_VOLUME:
+ {
+ float vol;
+ if ((res = read_volume(m, &vol)) < 0)
+ return res;
+ pw_log(level, "%u: volume: %f", o, vol);
+ break;
+ }
+ case TAG_FORMAT_INFO:
+ {
+ struct format_info info;
+ const struct spa_dict_item *it;
+ if ((res = read_format_info(m, &info)) < 0)
+ return res;
+ pw_log(level, "%u: format-info: enc:%s n_items:%u",
+ o, format_encoding2name(info.encoding),
+ info.props->dict.n_items);
+ spa_dict_for_each(it, &info.props->dict)
+ pw_log(level, " '%s': '%s'", it->key, it->value);
+ break;
+ }
+ }
+ }
+ m->offset = offset;
+
+ return 0;
+}
+
+struct message *message_alloc(struct impl *impl, uint32_t channel, uint32_t size)
+{
+ struct message *msg;
+
+ if (!spa_list_is_empty(&impl->free_messages)) {
+ msg = spa_list_first(&impl->free_messages, struct message, link);
+ spa_list_remove(&msg->link);
+ pw_log_trace("using recycled message %p size:%d", msg, size);
+
+ spa_assert(msg->impl == impl);
+ } else {
+ if ((msg = calloc(1, sizeof(*msg))) == NULL)
+ return NULL;
+
+ pw_log_trace("new message %p size:%d", msg, size);
+ msg->impl = impl;
+ msg->impl->stat.n_allocated++;
+ msg->impl->stat.n_accumulated++;
+ }
+
+ if (ensure_size(msg, size) < 0) {
+ message_free(msg, false, true);
+ return NULL;
+ }
+
+ spa_zero(msg->extra);
+ msg->channel = channel;
+ msg->offset = 0;
+ msg->length = size;
+
+ return msg;
+}
+
+void message_free(struct message *msg, bool dequeue, bool destroy)
+{
+ if (dequeue)
+ spa_list_remove(&msg->link);
+
+ if (msg->impl->stat.allocated > MAX_ALLOCATED || msg->allocated > MAX_SIZE)
+ destroy = true;
+
+ if (destroy) {
+ pw_log_trace("destroy message %p size:%d", msg, msg->allocated);
+ msg->impl->stat.n_allocated--;
+ msg->impl->stat.allocated -= msg->allocated;
+ free(msg->data);
+ free(msg);
+ } else {
+ pw_log_trace("recycle message %p size:%d/%d", msg, msg->length, msg->allocated);
+ spa_list_append(&msg->impl->free_messages, &msg->link);
+ msg->length = 0;
+ }
+}
diff --git a/src/modules/module-protocol-pulse/message.h b/src/modules/module-protocol-pulse/message.h
new file mode 100644
index 0000000..ad95292
--- /dev/null
+++ b/src/modules/module-protocol-pulse/message.h
@@ -0,0 +1,75 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_MESSAGE_H
+#define PULSE_SERVER_MESSAGE_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+#include <spa/support/log.h>
+
+struct impl;
+
+struct message {
+ struct spa_list link;
+ struct impl *impl;
+ uint32_t extra[4];
+ uint32_t channel;
+ uint32_t allocated;
+ uint32_t length;
+ uint32_t offset;
+ uint8_t *data;
+};
+
+enum {
+ TAG_INVALID = 0,
+ TAG_STRING = 't',
+ TAG_STRING_NULL = 'N',
+ TAG_U32 = 'L',
+ TAG_U8 = 'B',
+ TAG_U64 = 'R',
+ TAG_S64 = 'r',
+ TAG_SAMPLE_SPEC = 'a',
+ TAG_ARBITRARY = 'x',
+ TAG_BOOLEAN_TRUE = '1',
+ TAG_BOOLEAN_FALSE = '0',
+ TAG_BOOLEAN = TAG_BOOLEAN_TRUE,
+ TAG_TIMEVAL = 'T',
+ TAG_USEC = 'U' /* 64bit unsigned */,
+ TAG_CHANNEL_MAP = 'm',
+ TAG_CVOLUME = 'v',
+ TAG_PROPLIST = 'P',
+ TAG_VOLUME = 'V',
+ TAG_FORMAT_INFO = 'f',
+};
+
+struct message *message_alloc(struct impl *impl, uint32_t channel, uint32_t size);
+void message_free(struct message *msg, bool dequeue, bool destroy);
+int message_get(struct message *m, ...);
+int message_put(struct message *m, ...);
+int message_dump(enum spa_log_level level, struct message *m);
+
+#endif /* PULSE_SERVER_MESSAGE_H */
diff --git a/src/modules/module-protocol-pulse/module.c b/src/modules/module-protocol-pulse/module.c
new file mode 100644
index 0000000..ec10404
--- /dev/null
+++ b/src/modules/module-protocol-pulse/module.c
@@ -0,0 +1,333 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Georges Basile Stavracas Neto
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/string.h>
+#include <pipewire/log.h>
+#include <pipewire/map.h>
+#include <pipewire/properties.h>
+#include <pipewire/work-queue.h>
+
+#include "defs.h"
+#include "format.h"
+#include "internal.h"
+#include "log.h"
+#include "module.h"
+#include "remap.h"
+
+static void on_module_unload(void *obj, void *data, int res, uint32_t index)
+{
+ struct module *module = obj;
+ module_unload(module);
+}
+
+void module_schedule_unload(struct module *module)
+{
+ if (module->unloading)
+ return;
+
+ pw_work_queue_add(module->impl->work_queue, module, 0, on_module_unload, NULL);
+ module->unloading = true;
+}
+
+static struct module *module_new(struct impl *impl, const struct module_info *info)
+{
+ struct module *module;
+
+ module = calloc(1, sizeof(*module) + info->data_size);
+ if (module == NULL)
+ return NULL;
+
+ module->index = SPA_ID_INVALID;
+ module->impl = impl;
+ module->info = info;
+ spa_hook_list_init(&module->listener_list);
+ module->user_data = SPA_PTROFF(module, sizeof(*module), void);
+ module->loaded = false;
+
+ return module;
+}
+
+void module_add_listener(struct module *module,
+ struct spa_hook *listener,
+ const struct module_events *events, void *data)
+{
+ spa_hook_list_append(&module->listener_list, listener, events, data);
+}
+
+int module_load(struct module *module)
+{
+ pw_log_info("load module index:%u name:%s", module->index, module->info->name);
+ if (module->info->load == NULL)
+ return -ENOTSUP;
+ /* subscription event is sent when the module does a
+ * module_emit_loaded() */
+ return module->info->load(module);
+}
+
+void module_free(struct module *module)
+{
+ struct impl *impl = module->impl;
+
+ module_emit_destroy(module);
+
+ if (module->index != SPA_ID_INVALID)
+ pw_map_remove(&impl->modules, module->index & MODULE_INDEX_MASK);
+
+ if (module->unloading)
+ pw_work_queue_cancel(impl->work_queue, module, SPA_ID_INVALID);
+
+ spa_hook_list_clean(&module->listener_list);
+ pw_properties_free(module->props);
+
+ free((char*)module->args);
+
+ free(module);
+}
+
+int module_unload(struct module *module)
+{
+ struct impl *impl = module->impl;
+ int res = 0;
+
+ pw_log_info("unload module index:%u name:%s", module->index, module->info->name);
+
+ if (module->info->unload)
+ res = module->info->unload(module);
+
+ if (module->loaded)
+ broadcast_subscribe_event(impl,
+ SUBSCRIPTION_MASK_MODULE,
+ SUBSCRIPTION_EVENT_REMOVE | SUBSCRIPTION_EVENT_MODULE,
+ module->index);
+
+ module_free(module);
+
+ return res;
+}
+
+/** utils */
+void module_args_add_props(struct pw_properties *props, const char *str)
+{
+ char *s = strdup(str), *p = s, *e, f;
+ const char *k, *v;
+ const struct str_map *map;
+
+ while (*p) {
+ while (*p && isspace(*p))
+ p++;
+ e = strchr(p, '=');
+ if (e == NULL)
+ break;
+ *e = '\0';
+ k = p;
+ p = e+1;
+
+ if (*p == '\"') {
+ p++;
+ f = '\"';
+ } else if (*p == '\'') {
+ p++;
+ f = '\'';
+ } else {
+ f = ' ';
+ }
+ v = p;
+ for (e = p; *e ; e++) {
+ if (*e == f)
+ break;
+ if (*e == '\\')
+ e++;
+ }
+ p = e;
+ if (*e != '\0')
+ p++;
+ *e = '\0';
+
+ if ((map = str_map_find(props_key_map, NULL, k)) != NULL) {
+ k = map->pw_str;
+ if (map->child != NULL &&
+ (map = str_map_find(map->child, NULL, v)) != NULL)
+ v = map->pw_str;
+ }
+ pw_properties_set(props, k, v);
+ }
+ free(s);
+}
+
+int module_args_to_audioinfo(struct impl *impl, struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+ uint32_t i;
+
+ /* We don't use any incoming format setting and use our native format */
+ spa_zero(*info);
+ info->flags = SPA_AUDIO_FLAG_UNPOSITIONED;
+ info->format = SPA_AUDIO_FORMAT_F32P;
+
+ if ((str = pw_properties_get(props, "channels")) != NULL) {
+ info->channels = pw_properties_parse_int(str);
+ if (info->channels == 0 || info->channels > SPA_AUDIO_MAX_CHANNELS) {
+ pw_log_error("invalid channels '%s'", str);
+ return -EINVAL;
+ }
+ pw_properties_set(props, "channels", NULL);
+ }
+ if ((str = pw_properties_get(props, "channel_map")) != NULL) {
+ struct channel_map map;
+
+ channel_map_parse(str, &map);
+ if (map.channels == 0 || map.channels > SPA_AUDIO_MAX_CHANNELS) {
+ pw_log_error("invalid channel_map '%s'", str);
+ return -EINVAL;
+ }
+ if (info->channels == 0)
+ info->channels = map.channels;
+ if (info->channels != map.channels) {
+ pw_log_error("Mismatched channel map");
+ return -EINVAL;
+ }
+ channel_map_to_positions(&map, info->position);
+ info->flags &= ~SPA_AUDIO_FLAG_UNPOSITIONED;
+ pw_properties_set(props, "channel_map", NULL);
+ } else {
+ if (info->channels == 0)
+ info->channels = impl->defs.sample_spec.channels;
+
+ if (info->channels == impl->defs.channel_map.channels) {
+ channel_map_to_positions(&impl->defs.channel_map, info->position);
+ } else if (info->channels == 1) {
+ info->position[0] = SPA_AUDIO_CHANNEL_MONO;
+ } else if (info->channels == 2) {
+ info->position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->position[1] = SPA_AUDIO_CHANNEL_FR;
+ } else {
+ /* FIXME add more mappings */
+ for (i = 0; i < info->channels; i++)
+ info->position[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+ }
+ if (info->position[0] != SPA_AUDIO_CHANNEL_UNKNOWN)
+ info->flags &= ~SPA_AUDIO_FLAG_UNPOSITIONED;
+ }
+
+ if ((str = pw_properties_get(props, "rate")) != NULL) {
+ info->rate = pw_properties_parse_int(str);
+ pw_properties_set(props, "rate", NULL);
+ } else {
+ info->rate = 0;
+ }
+ return 0;
+}
+
+bool module_args_parse_bool(const char *v)
+{
+ if (spa_streq(v, "1") || !strcasecmp(v, "y") || !strcasecmp(v, "t") ||
+ !strcasecmp(v, "yes") || !strcasecmp(v, "true") || !strcasecmp(v, "on"))
+ return true;
+ return false;
+}
+
+static const struct module_info *find_module_info(const char *name)
+{
+ extern const struct module_info __start_pw_mod_pulse_modules[];
+ extern const struct module_info __stop_pw_mod_pulse_modules[];
+
+ const struct module_info *info = __start_pw_mod_pulse_modules;
+
+ for (; info < __stop_pw_mod_pulse_modules; info++) {
+ if (spa_streq(info->name, name))
+ return info;
+ }
+
+ spa_assert(info == __stop_pw_mod_pulse_modules);
+
+ return NULL;
+}
+
+static int find_module_by_name(void *item_data, void *data)
+{
+ const char *name = data;
+ const struct module *module = item_data;
+ return spa_streq(module->info->name, name) ? 1 : 0;
+}
+
+struct module *module_create(struct impl *impl, const char *name, const char *args)
+{
+ const struct module_info *info;
+ struct module *module;
+
+ info = find_module_info(name);
+ if (info == NULL) {
+ errno = ENOENT;
+ return NULL;
+ }
+
+ if (info->load_once) {
+ int exists;
+ exists = pw_map_for_each(&impl->modules, find_module_by_name,
+ (void *)name);
+ if (exists) {
+ errno = EEXIST;
+ return NULL;
+ }
+ }
+
+ module = module_new(impl, info);
+ if (module == NULL)
+ return NULL;
+
+ module->props = pw_properties_new(NULL, NULL);
+ if (module->props == NULL) {
+ module_free(module);
+ return NULL;
+ }
+
+ if (args)
+ module_args_add_props(module->props, args);
+
+ int res = module->info->prepare(module);
+ if (res < 0) {
+ module_free(module);
+ errno = -res;
+ return NULL;
+ }
+
+ module->index = pw_map_insert_new(&impl->modules, module);
+ if (module->index == SPA_ID_INVALID) {
+ module_unload(module);
+ return NULL;
+ }
+
+ module->args = args ? strdup(args) : NULL;
+ module->index |= MODULE_FLAG;
+
+ return module;
+}
diff --git a/src/modules/module-protocol-pulse/module.h b/src/modules/module-protocol-pulse/module.h
new file mode 100644
index 0000000..1a6ffb0
--- /dev/null
+++ b/src/modules/module-protocol-pulse/module.h
@@ -0,0 +1,94 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Georges Basile Stavracas Neto
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PULSE_MODULE_H
+#define PIPEWIRE_PULSE_MODULE_H
+
+#include <spa/param/audio/raw.h>
+#include <spa/utils/hook.h>
+
+#include "internal.h"
+
+struct module;
+struct pw_properties;
+
+struct module_info {
+ const char *name;
+
+ unsigned int load_once:1;
+
+ int (*prepare) (struct module *module);
+ int (*load) (struct module *module);
+ int (*unload) (struct module *module);
+
+ const struct spa_dict *properties;
+ size_t data_size;
+};
+
+#define DEFINE_MODULE_INFO(name) \
+ __attribute__((used)) \
+ __attribute__((retain)) \
+ __attribute__((section("pw_mod_pulse_modules"))) \
+ __attribute__((aligned(__alignof__(struct module_info)))) \
+ const struct module_info name
+
+struct module_events {
+#define VERSION_MODULE_EVENTS 0
+ uint32_t version;
+
+ void (*loaded) (void *data, int result);
+ void (*destroy) (void *data);
+};
+
+struct module {
+ uint32_t index;
+ const char *args;
+ struct pw_properties *props;
+ struct impl *impl;
+ const struct module_info *info;
+ struct spa_hook_list listener_list;
+ void *user_data;
+ unsigned int loaded:1;
+ unsigned int unloading:1;
+};
+
+#define module_emit_loaded(m,r) spa_hook_list_call(&m->listener_list, struct module_events, loaded, 0, r)
+#define module_emit_destroy(m) spa_hook_list_call(&(m)->listener_list, struct module_events, destroy, 0)
+
+struct module *module_create(struct impl *impl, const char *name, const char *args);
+void module_free(struct module *module);
+int module_load(struct module *module);
+int module_unload(struct module *module);
+void module_schedule_unload(struct module *module);
+
+void module_add_listener(struct module *module,
+ struct spa_hook *listener,
+ const struct module_events *events, void *data);
+
+void module_args_add_props(struct pw_properties *props, const char *str);
+int module_args_to_audioinfo(struct impl *impl, struct pw_properties *props, struct spa_audio_info_raw *info);
+bool module_args_parse_bool(const char *str);
+
+#endif
diff --git a/src/modules/module-protocol-pulse/modules/module-always-sink.c b/src/modules/module-protocol-pulse/modules/module-always-sink.c
new file mode 100644
index 0000000..7549c09
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-always-sink.c
@@ -0,0 +1,122 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "../module.h"
+
+#define NAME "always-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_always_sink_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_always_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_always_sink_load(struct module *module)
+{
+ struct module_always_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ const char *str;
+ size_t size;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if ((str = pw_properties_get(module->props, "sink_name")) != NULL)
+ fprintf(f, " sink.name = \"%s\"", str);
+ fprintf(f, " }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-fallback-sink",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+ return 0;
+}
+
+static int module_always_sink_unload(struct module *module)
+{
+ struct module_always_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item module_always_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Always keeps at least one sink loaded even if it's a null one" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name of sink>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_always_sink_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_always_sink_data * const data = module->user_data;
+ data->module = module;
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_always_sink) = {
+ .name = "module-always-sink",
+ .load_once = true,
+ .prepare = module_always_sink_prepare,
+ .load = module_always_sink_load,
+ .unload = module_always_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_always_sink_info),
+ .data_size = sizeof(struct module_always_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-combine-sink.c b/src/modules/module-protocol-pulse/modules/module-combine-sink.c
new file mode 100644
index 0000000..86536f7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-combine-sink.c
@@ -0,0 +1,341 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/utils.h>
+
+#include "../manager.h"
+#include "../module.h"
+
+#define NAME "combine-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MAX_SINKS 64 /* ... good enough for anyone */
+
+#define TIMEOUT_SINKS_MSEC 2000
+
+static const struct spa_dict_item module_combine_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Combine multiple sinks into a single sink" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name of the sink> "
+ "sink_properties=<properties for the sink> "
+ /* not a great name, but for backwards compatibility... */
+ "slaves=<sinks to combine> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "remix=<remix channels> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct module_combine_sink_data;
+
+struct module_combine_sink_data {
+ struct module *module;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct pw_manager *manager;
+ struct spa_hook manager_listener;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ char *sink_name;
+ char **sink_names;
+ struct pw_properties *combine_props;
+
+ struct spa_source *sinks_timeout;
+
+ struct spa_audio_info_raw info;
+
+ unsigned int sinks_pending;
+ unsigned int remix:1;
+ unsigned int load_emitted:1;
+ unsigned int start_error:1;
+};
+
+static void check_initialized(struct module_combine_sink_data *data)
+{
+ struct module *module = data->module;
+
+ if (data->load_emitted)
+ return;
+
+ if (data->start_error) {
+ pw_log_debug("module load error");
+ data->load_emitted = true;
+ module_emit_loaded(module, -EIO);
+ } else if (data->sinks_pending == 0) {
+ pw_log_debug("module loaded");
+ data->load_emitted = true;
+ module_emit_loaded(module, 0);
+ }
+}
+
+static void manager_added(void *d, struct pw_manager_object *o)
+{
+ struct module_combine_sink_data *data = d;
+ const char *str;
+ uint32_t val = 0;
+ struct pw_node_info *info;
+
+ if (!spa_streq(o->type, PW_TYPE_INTERFACE_Node) ||
+ (info = o->info) == NULL || info->props == NULL)
+ return;
+
+ str = spa_dict_lookup(info->props, "pulse.module.id");
+ if (str == NULL || !spa_atou32(str, &val, 0) || val != data->module->index)
+ return;
+
+ pw_log_info("found our %s, pending:%d",
+ pw_properties_get(o->props, PW_KEY_NODE_NAME),
+ data->sinks_pending);
+
+ if (!pw_manager_object_is_sink(o)) {
+ if (data->sinks_pending > 0)
+ data->sinks_pending--;
+ }
+ check_initialized(data);
+ return;
+}
+
+static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .added = manager_added,
+};
+
+static void on_sinks_timeout(void *d, uint64_t count)
+{
+ struct module_combine_sink_data *data = d;
+
+ if (data->load_emitted)
+ return;
+
+ data->start_error = true;
+ check_initialized(data);
+}
+
+static void module_destroy(void *data)
+{
+ struct module_combine_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_combine_sink_load(struct module *module)
+{
+ struct module_combine_sink_data *data = module->user_data;
+ uint32_t i;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ data->core = pw_context_connect(module->impl->context, NULL, 0);
+ if (data->core == NULL)
+ return -errno;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ fprintf(f, " node.name = %s", data->sink_name);
+ fprintf(f, " node.description = %s", data->sink_name);
+ if (data->info.rate != 0)
+ fprintf(f, " audio.rate = %u", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " audio.channels = %u", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " audio.position = [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ]");
+ }
+ }
+ fprintf(f, " combine.props = {");
+ fprintf(f, " pulse.module.id = %u", module->index);
+ pw_properties_serialize_dict(f, &data->combine_props->dict, 0);
+ fprintf(f, " } stream.props = {");
+ if (!data->remix)
+ fprintf(f, " "PW_KEY_STREAM_DONT_REMIX" = true");
+ fprintf(f, " pulse.module.id = %u", module->index);
+ fprintf(f, " } stream.rules = [");
+ if (data->sink_names == NULL) {
+ fprintf(f, " { matches = [ { media.class = \"Audio/Sink\" } ]");
+ fprintf(f, " actions = { create-stream = { } } }");
+ } else {
+ for (i = 0; data->sink_names[i] != NULL; i++) {
+ char name[1024];
+ spa_json_encode_string(name, sizeof(name)-1, data->sink_names[i]);
+ fprintf(f, " { matches = [ { media.class = \"Audio/Sink\" ");
+ fprintf(f, " node.name = %s } ]", name);
+ fprintf(f, " actions = { create-stream = { } } }");
+ }
+ }
+ fprintf(f, " ]");
+ fprintf(f, "}");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-combine-stream",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ data->manager = pw_manager_new(data->core);
+ if (data->manager == NULL)
+ return -errno;
+
+ pw_manager_add_listener(data->manager, &data->manager_listener,
+ &manager_events, data);
+
+ data->sinks_timeout = pw_loop_add_timer(module->impl->loop, on_sinks_timeout, data);
+ if (data->sinks_timeout) {
+ struct timespec timeout = {0};
+ timeout.tv_sec = TIMEOUT_SINKS_MSEC / 1000;
+ timeout.tv_nsec = (TIMEOUT_SINKS_MSEC % 1000) * SPA_NSEC_PER_MSEC;
+ pw_loop_update_timer(module->impl->loop, data->sinks_timeout, &timeout, NULL, false);
+ }
+ return data->load_emitted ? 0 : SPA_RESULT_RETURN_ASYNC(0);
+}
+
+static int module_combine_sink_unload(struct module *module)
+{
+ struct module_combine_sink_data *d = module->user_data;
+
+ if (d->sinks_timeout != NULL)
+ pw_loop_destroy_source(module->impl->loop, d->sinks_timeout);
+
+ if (d->mod != NULL) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ if (d->manager != NULL) {
+ spa_hook_remove(&d->manager_listener);
+ pw_manager_destroy(d->manager);
+ }
+ if (d->core != NULL) {
+ spa_hook_remove(&d->core_listener);
+ pw_core_disconnect(d->core);
+ }
+ pw_free_strv(d->sink_names);
+ free(d->sink_name);
+ pw_properties_free(d->combine_props);
+ return 0;
+}
+
+static int module_combine_sink_prepare(struct module * const module)
+{
+ struct module_combine_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *combine_props = NULL;
+ const char *str;
+ char *sink_name = NULL, **sink_names = NULL;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+ int num_sinks = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ combine_props = pw_properties_new(NULL, NULL);
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ sink_name = strdup(str);
+ pw_properties_set(props, "sink_name", NULL);
+ } else {
+ sink_name = strdup("combined");
+ }
+
+ if ((str = pw_properties_get(module->props, "sink_properties")) != NULL)
+ module_args_add_props(combine_props, str);
+
+ if ((str = pw_properties_get(props, "slaves")) != NULL) {
+ sink_names = pw_split_strv(str, ",", MAX_SINKS, &num_sinks);
+ pw_properties_set(props, "slaves", NULL);
+ }
+ d->remix = true;
+ if ((str = pw_properties_get(props, "remix")) != NULL) {
+ d->remix = pw_properties_parse_bool(str);
+ pw_properties_set(props, "remix", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "adjust_time")) != NULL) {
+ pw_log_info("The `adjust_time` modarg is ignored");
+ pw_properties_set(props, "adjust_time", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "resample_method")) != NULL) {
+ pw_log_info("The `resample_method` modarg is ignored");
+ pw_properties_set(props, "resample_method", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ d->module = module;
+ d->info = info;
+ d->sink_name = sink_name;
+ d->sink_names = sink_names;
+ d->sinks_pending = (sink_names == NULL) ? 0 : num_sinks;
+ d->combine_props = combine_props;
+
+ return 0;
+out:
+ free(sink_name);
+ pw_free_strv(sink_names);
+ pw_properties_free(combine_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_combine_sink) = {
+ .name = "module-combine-sink",
+ .prepare = module_combine_sink_prepare,
+ .load = module_combine_sink_load,
+ .unload = module_combine_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_combine_sink_info),
+ .data_size = sizeof(struct module_combine_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-echo-cancel.c b/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
new file mode 100644
index 0000000..c2ab8ad
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-echo-cancel.c
@@ -0,0 +1,276 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "echo-cancel"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_echo_cancel_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *props;
+ struct pw_properties *capture_props;
+ struct pw_properties *source_props;
+ struct pw_properties *sink_props;
+ struct pw_properties *playback_props;
+
+ struct spa_audio_info_raw info;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_echo_cancel_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_echo_cancel_load(struct module *module)
+{
+ struct module_echo_cancel_data *data = module->user_data;
+ FILE *f;
+ const char *str;
+ char *args;
+ size_t size;
+ uint32_t i;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ /* Can't just serialise this dict because the "null" method gets
+ * interpreted as a JSON null */
+ if ((str = pw_properties_get(data->props, "aec.method")))
+ fprintf(f, " aec.method = \"%s\"", str);
+ if ((str = pw_properties_get(data->props, "aec.args")))
+ fprintf(f, " aec.args = \"%s\"", str);
+ if (data->info.rate != 0)
+ fprintf(f, " audio.rate = %u", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " audio.channels = %u", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " audio.position = [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ]");
+ }
+ }
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } source.props = {");
+ pw_properties_serialize_dict(f, &data->source_props->dict, 0);
+ fprintf(f, " } sink.props = {");
+ pw_properties_serialize_dict(f, &data->sink_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-echo-cancel",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_echo_cancel_unload(struct module *module)
+{
+ struct module_echo_cancel_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->props);
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->source_props);
+ pw_properties_free(d->sink_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_echo_cancel_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Acoustic echo canceller" },
+ { PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
+ "source_properties=<properties for the source> "
+ "source_master=<name of source to filter> "
+ "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "sink_master=<name of sink to filter> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "aec_method=<implementation to use> "
+ "aec_args=<parameters for the AEC engine> "
+#if 0
+ /* These are not implemented because they don't
+ * really make sense in the PipeWire context */
+ "format=<sample format> "
+ "adjust_time=<how often to readjust rates in s> "
+ "adjust_threshold=<how much drift to readjust after in ms> "
+ "autoloaded=<set if this module is being loaded automatically> "
+ "save_aec=<save AEC data in /tmp> "
+ "use_volume_sharing=<yes or no> "
+ "use_master_format=<yes or no> "
+#endif
+ },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_echo_cancel_prepare(struct module * const module)
+{
+ struct module_echo_cancel_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *aec_props = NULL, *sink_props = NULL, *source_props = NULL;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ aec_props = pw_properties_new(NULL, NULL);
+ capture_props = pw_properties_new(NULL, NULL);
+ source_props = pw_properties_new(NULL, NULL);
+ sink_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!aec_props || !source_props || !sink_props || !capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(source_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ } else {
+ pw_properties_set(source_props, PW_KEY_NODE_NAME, "echo-cancel-source");
+ }
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(sink_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ } else {
+ pw_properties_set(sink_props, PW_KEY_NODE_NAME, "echo-cancel-sink");
+ }
+
+ if ((str = pw_properties_get(props, "source_master")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(capture_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(capture_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(capture_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ pw_properties_set(props, "source_master", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink_master")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "sink_master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(source_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(sink_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "aec_method")) != NULL) {
+ pw_properties_set(aec_props, "aec.method", str);
+ pw_properties_set(props, "aec_method", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "aec_args")) != NULL) {
+ pw_properties_set(aec_props, "aec.args", str);
+ pw_properties_set(props, "aec_args", NULL);
+ }
+
+ d->module = module;
+ d->props = aec_props;
+ d->capture_props = capture_props;
+ d->source_props = source_props;
+ d->sink_props = sink_props;
+ d->playback_props = playback_props;
+ d->info = info;
+
+ return 0;
+out:
+ pw_properties_free(aec_props);
+ pw_properties_free(playback_props);
+ pw_properties_free(sink_props);
+ pw_properties_free(source_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_echo_cancel) = {
+ .name = "module-echo-cancel",
+ .prepare = module_echo_cancel_prepare,
+ .load = module_echo_cancel_load,
+ .unload = module_echo_cancel_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_echo_cancel_info),
+ .data_size = sizeof(struct module_echo_cancel_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-gsettings.c b/src/modules/module-protocol-pulse/modules/module-gsettings.c
new file mode 100644
index 0000000..36e216b
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-gsettings.c
@@ -0,0 +1,298 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <gio/gio.h>
+#include <glib.h>
+
+#include <spa/debug/mem.h>
+#include <pipewire/pipewire.h>
+#include <pipewire/thread.h>
+
+#include "../module.h"
+
+#define NAME "gsettings"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define PA_GSETTINGS_MODULE_GROUP_SCHEMA "org.freedesktop.pulseaudio.module-group"
+#define PA_GSETTINGS_MODULE_GROUPS_SCHEMA "org.freedesktop.pulseaudio.module-groups"
+#define PA_GSETTINGS_MODULE_GROUPS_PATH "/org/freedesktop/pulseaudio/module-groups/"
+
+#define MAX_MODULES 10
+
+struct module_gsettings_data {
+ struct module *module;
+
+ GMainContext *context;
+ GMainLoop *loop;
+ struct spa_thread *thr;
+
+ GSettings *settings;
+ gchar **group_names;
+
+ struct spa_list groups;
+};
+
+struct group {
+ struct spa_list link;
+ char *name;
+ struct module *module;
+ struct spa_hook module_listener;
+};
+
+struct info {
+ bool enabled;
+ char *name;
+ char *module[MAX_MODULES];
+ char *args[MAX_MODULES];
+};
+
+static void clean_info(const struct info *info)
+{
+ int i;
+ for (i = 0; i < MAX_MODULES; i++) {
+ g_free(info->module[i]);
+ g_free(info->args[i]);
+ }
+ g_free(info->name);
+}
+
+static void unload_module(struct module_gsettings_data *d, struct group *g)
+{
+ spa_list_remove(&g->link);
+ g_free(g->name);
+ if (g->module)
+ module_unload(g->module);
+ free(g);
+}
+
+static void unload_group(struct module_gsettings_data *d, const char *name)
+{
+ struct group *g, *t;
+ spa_list_for_each_safe(g, t, &d->groups, link) {
+ if (spa_streq(g->name, name))
+ unload_module(d, g);
+ }
+}
+static void module_destroy(void *data)
+{
+ struct group *g = data;
+ if (g->module) {
+ spa_hook_remove(&g->module_listener);
+ g->module = NULL;
+ }
+}
+
+static const struct module_events module_gsettings_events = {
+ VERSION_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int load_group(struct module_gsettings_data *d, const struct info *info)
+{
+ struct group *g;
+ int i, res;
+
+ for (i = 0; i < MAX_MODULES; i++) {
+ if (info->module[i] == NULL || strlen(info->module[i]) <= 0)
+ break;
+
+ g = calloc(1, sizeof(struct group));
+ if (g == NULL)
+ return -errno;
+
+ g->name = strdup(info->name);
+ g->module = module_create(d->module->impl, info->module[i], info->args[i]);
+ if (g->module == NULL) {
+ pw_log_info("can't create module:%s args:%s: %m",
+ info->module[i], info->args[i]);
+ } else {
+ module_add_listener(g->module, &g->module_listener,
+ &module_gsettings_events, g);
+ if ((res = module_load(g->module)) < 0) {
+ pw_log_warn("can't load module:%s args:%s: %s",
+ info->module[i], info->args[i],
+ spa_strerror(res));
+ }
+ }
+ spa_list_append(&d->groups, &g->link);
+ }
+ return 0;
+}
+
+static int
+do_handle_info(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct module_gsettings_data *d = user_data;
+ const struct info *info = data;
+
+ unload_group(d, info->name);
+ if (info->enabled)
+ load_group(d, info);
+
+ clean_info(info);
+ return 0;
+}
+
+static void handle_module_group(struct module_gsettings_data *d, gchar *name)
+{
+ struct impl *impl = d->module->impl;
+ GSettings *settings;
+ gchar p[1024];
+ struct info info;
+ int i;
+
+ snprintf(p, sizeof(p), PA_GSETTINGS_MODULE_GROUPS_PATH"%s/", name);
+
+ settings = g_settings_new_with_path(PA_GSETTINGS_MODULE_GROUP_SCHEMA, p);
+ if (settings == NULL)
+ return;
+
+ spa_zero(info);
+ info.name = strdup(p);
+ info.enabled = g_settings_get_boolean(settings, "enabled");
+
+ for (i = 0; i < MAX_MODULES; i++) {
+ snprintf(p, sizeof(p), "name%d", i);
+ info.module[i] = g_settings_get_string(settings, p);
+
+ snprintf(p, sizeof(p), "args%i", i);
+ info.args[i] = g_settings_get_string(settings, p);
+ }
+ pw_loop_invoke(impl->loop, do_handle_info, 0,
+ &info, sizeof(info), false, d);
+
+ g_object_unref(G_OBJECT(settings));
+}
+
+static void module_group_callback(GSettings *settings, gchar *key, gpointer user_data)
+{
+ struct module_gsettings_data *d = g_object_get_data(G_OBJECT(settings), "module-data");
+ handle_module_group(d, user_data);
+}
+
+static void *do_loop(void *user_data)
+{
+ struct module_gsettings_data *d = user_data;
+
+ pw_log_info("enter");
+ g_main_context_push_thread_default(d->context);
+
+ d->loop = g_main_loop_new(d->context, FALSE);
+
+ g_main_loop_run(d->loop);
+
+ g_main_context_pop_thread_default(d->context);
+ g_main_loop_unref (d->loop);
+ d->loop = NULL;
+ pw_log_info("leave");
+
+ return NULL;
+}
+
+static int module_gsettings_load(struct module *module)
+{
+ struct module_gsettings_data *data = module->user_data;
+ gchar **name;
+
+ data->context = g_main_context_new();
+ g_main_context_push_thread_default(data->context);
+
+ data->settings = g_settings_new(PA_GSETTINGS_MODULE_GROUPS_SCHEMA);
+ if (data->settings == NULL)
+ return -EIO;
+
+ data->group_names = g_settings_list_children(data->settings);
+
+ for (name = data->group_names; *name; name++) {
+ GSettings *child = g_settings_get_child(data->settings, *name);
+ /* The child may have been removed between the
+ * g_settings_list_children() and g_settings_get_child() calls. */
+ if (child == NULL)
+ continue;
+
+ g_object_set_data(G_OBJECT(child), "module-data", data);
+ g_signal_connect(child, "changed", (GCallback) module_group_callback, *name);
+ handle_module_group(data, *name);
+ }
+ g_main_context_pop_thread_default(data->context);
+
+ data->thr = pw_thread_utils_create(NULL, do_loop, data);
+ return 0;
+}
+
+static gboolean
+do_stop(gpointer data)
+{
+ struct module_gsettings_data *d = data;
+ if (d->loop)
+ g_main_loop_quit(d->loop);
+ return FALSE;
+}
+
+static int module_gsettings_unload(struct module *module)
+{
+ struct module_gsettings_data *d = module->user_data;
+ struct group *g;
+
+ g_main_context_invoke(d->context, do_stop, d);
+ pw_thread_utils_join(d->thr, NULL);
+ g_main_context_unref(d->context);
+
+ spa_list_consume(g, &d->groups, link)
+ unload_module(d, g);
+
+ g_strfreev(d->group_names);
+ g_object_unref(G_OBJECT(d->settings));
+ return 0;
+}
+
+static int module_gsettings_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_gsettings_data * const data = module->user_data;
+ spa_list_init(&data->groups);
+ data->module = module;
+
+ return 0;
+}
+
+static const struct spa_dict_item module_gsettings_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "GSettings Adapter" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+DEFINE_MODULE_INFO(module_gsettings) = {
+ .name = "module-gsettings",
+ .load_once = true,
+ .prepare = module_gsettings_prepare,
+ .load = module_gsettings_load,
+ .unload = module_gsettings_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_gsettings_info),
+ .data_size = sizeof(struct module_gsettings_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c b/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
new file mode 100644
index 0000000..3b3b700
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-ladspa-sink.c
@@ -0,0 +1,257 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "ladspa-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_ladspa_sink_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_ladspa_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_ladspa_sink_load(struct module *module)
+{
+ struct module_ladspa_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ const char *str, *plugin, *label;
+ size_t size;
+
+ if ((plugin = pw_properties_get(module->props, "plugin")) == NULL)
+ return -EINVAL;
+ if ((label = pw_properties_get(module->props, "label")) == NULL)
+ return -EINVAL;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "ladspa-sink-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "ladspa-sink-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " filter.graph = {");
+ fprintf(f, " nodes = [ { ");
+ fprintf(f, " type = ladspa ");
+ fprintf(f, " plugin = \"%s\" ", plugin);
+ fprintf(f, " label = \"%s\" ", label);
+ if ((str = pw_properties_get(module->props, "control")) != NULL) {
+ size_t len;
+ const char *s, *state = NULL;
+ int count = 0;
+
+ fprintf(f, " control = {");
+ while ((s = pw_split_walk(str, ", ", &len, &state))) {
+ fprintf(f, " \"%d\" = %.*s", count, (int)len, s);
+ count++;
+ }
+ fprintf(f, " }");
+ }
+ fprintf(f, " } ]");
+ if ((str = pw_properties_get(module->props, "inputs")) != NULL)
+ fprintf(f, " inputs = [ %s ] ", str);
+ if ((str = pw_properties_get(module->props, "outputs")) != NULL)
+ fprintf(f, " outputs = [ %s ] ", str);
+ fprintf(f, " }");
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-filter-chain",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_ladspa_sink_unload(struct module *module)
+{
+ struct module_ladspa_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_ladspa_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Virtual LADSPA sink" },
+ { PW_KEY_MODULE_USAGE,
+ "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "sink_input_properties=<properties for the sink input> "
+ "master=<name of sink to filter> "
+ "sink_master=<name of sink to filter> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<input channel map> "
+ "plugin=<ladspa plugin name> "
+ "label=<ladspa plugin label> "
+ "control=<comma separated list of input control values> "
+ "input_ladspaport_map=<comma separated list of input LADSPA port names> "
+ "output_ladspaport_map=<comma separated list of output LADSPA port names> "},
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void position_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_ladspa_sink_prepare(struct module * const module)
+{
+ struct module_ladspa_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str;
+ struct spa_audio_info_raw capture_info = { 0 };
+ struct spa_audio_info_raw playback_info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(capture_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+ if (pw_properties_get(capture_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(capture_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ if (pw_properties_get(capture_props, PW_KEY_DEVICE_CLASS) == NULL)
+ pw_properties_set(capture_props, PW_KEY_DEVICE_CLASS, "filter");
+
+ if ((str = pw_properties_get(capture_props, PW_KEY_NODE_DESCRIPTION)) == NULL) {
+ str = pw_properties_get(capture_props, PW_KEY_NODE_NAME);
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s Sink", str);
+ } else {
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ }
+
+ if ((str = pw_properties_get(props, "master")) != NULL ||
+ (str = pw_properties_get(props, "sink_master")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &capture_info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ playback_info = capture_info;
+
+ position_to_props(&capture_info, capture_props);
+ position_to_props(&playback_info, playback_props);
+
+ if (pw_properties_get(playback_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_PASSIVE, "true");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_ladspa_sink) = {
+ .name = "module-ladspa-sink",
+ .prepare = module_ladspa_sink_prepare,
+ .load = module_ladspa_sink_load,
+ .unload = module_ladspa_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_ladspa_sink_info),
+ .data_size = sizeof(struct module_ladspa_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-ladspa-source.c b/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
new file mode 100644
index 0000000..9533350
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-ladspa-source.c
@@ -0,0 +1,265 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "ladspa-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_ladspa_source_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_ladspa_source_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_ladspa_source_load(struct module *module)
+{
+ struct module_ladspa_source_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ const char *str, *plugin, *label;
+ size_t size;
+
+ if ((plugin = pw_properties_get(module->props, "plugin")) == NULL)
+ return -EINVAL;
+ if ((label = pw_properties_get(module->props, "label")) == NULL)
+ return -EINVAL;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "ladspa-source-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "ladspa-source-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " filter.graph = {");
+ fprintf(f, " nodes = [ { ");
+ fprintf(f, " type = ladspa ");
+ fprintf(f, " plugin = \"%s\" ", plugin);
+ fprintf(f, " label = \"%s\" ", label);
+ if ((str = pw_properties_get(module->props, "control")) != NULL) {
+ size_t len;
+ const char *s, *state = NULL;
+ int count = 0;
+
+ fprintf(f, " control = {");
+ while ((s = pw_split_walk(str, ", ", &len, &state))) {
+ fprintf(f, " \"%d\" = %.*s", count, (int)len, s);
+ count++;
+ }
+ fprintf(f, " }");
+ }
+ fprintf(f, " } ]");
+ if ((str = pw_properties_get(module->props, "inputs")) != NULL)
+ fprintf(f, " inputs = [ %s ] ", str);
+ if ((str = pw_properties_get(module->props, "outputs")) != NULL)
+ fprintf(f, " outputs = [ %s ] ", str);
+ fprintf(f, " }");
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-filter-chain",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_ladspa_source_unload(struct module *module)
+{
+ struct module_ladspa_source_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_ladspa_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Virtual LADSPA source" },
+ { PW_KEY_MODULE_USAGE,
+ "source_name=<name for the source> "
+ "source_properties=<properties for the source> "
+ "source_output_properties=<properties for the source output> "
+ "master=<name of source to filter> "
+ "source_master=<name of source to filter> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<input channel map> "
+ "plugin=<ladspa plugin name> "
+ "label=<ladspa plugin label> "
+ "control=<comma separated list of input control values> "
+ "input_ladspaport_map=<comma separated list of input LADSPA port names> "
+ "output_ladspaport_map=<comma separated list of output LADSPA port names> "},
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void position_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_ladspa_source_prepare(struct module * const module)
+{
+ struct module_ladspa_source_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str;
+ struct spa_audio_info_raw capture_info = { 0 };
+ struct spa_audio_info_raw playback_info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(playback_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+ if (pw_properties_get(playback_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(playback_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ if (pw_properties_get(playback_props, PW_KEY_DEVICE_CLASS) == NULL)
+ pw_properties_set(playback_props, PW_KEY_DEVICE_CLASS, "filter");
+
+ if ((str = pw_properties_get(playback_props, PW_KEY_NODE_DESCRIPTION)) == NULL) {
+ str = pw_properties_get(playback_props, PW_KEY_NODE_NAME);
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s Source", str);
+ } else {
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ }
+
+ if ((str = pw_properties_get(props, "master")) != NULL ||
+ (str = pw_properties_get(props, "source_master")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(capture_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(capture_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(capture_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ pw_properties_set(props, "source_master", NULL);
+ pw_properties_set(props, "master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &playback_info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ capture_info = playback_info;
+
+ position_to_props(&capture_info, capture_props);
+ position_to_props(&playback_info, playback_props);
+
+ if (pw_properties_get(capture_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_PASSIVE, "true");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_ladspa_source) = {
+ .name = "module-ladspa-source",
+ .prepare = module_ladspa_source_prepare,
+ .load = module_ladspa_source_load,
+ .unload = module_ladspa_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_ladspa_source_info),
+ .data_size = sizeof(struct module_ladspa_source_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-loopback.c b/src/modules/module-protocol-pulse/modules/module-loopback.c
new file mode 100644
index 0000000..614ee50
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-loopback.c
@@ -0,0 +1,245 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Arun Raghavan <arun@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "loopback"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_loopback_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+
+ struct spa_audio_info_raw info;
+ uint32_t latency_msec;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_loopback_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_loopback_load(struct module *module)
+{
+ struct module_loopback_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size, i;
+ char val[256];
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "loopback-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "loopback-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if (data->info.channels != 0) {
+ fprintf(f, " audio.channels = %u", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " audio.position = [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ]");
+ }
+ }
+ if (data->latency_msec != 0)
+ fprintf(f, " target.delay.sec = %s",
+ spa_json_format_float(val, sizeof(val),
+ data->latency_msec / 1000.0f));
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-loopback",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_loopback_unload(struct module *module)
+{
+ struct module_loopback_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_loopback_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Arun Raghavan <arun@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Loopback from source to sink" },
+ { PW_KEY_MODULE_USAGE, "source=<source to connect to> "
+ "sink=<sink to connect to> "
+ "latency_msec=<latency in ms> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "sink_input_properties=<proplist> "
+ "source_output_properties=<proplist> "
+ "source_dont_move=<boolean> "
+ "sink_dont_move=<boolean> "
+ "remix=<remix channels?> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_loopback_prepare(struct module * const module)
+{
+ struct module_loopback_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ /* The following modargs are not implemented:
+ * adjust_time, max_latency_msec, fast_adjust_threshold_msec: these are just not relevant
+ */
+
+ if ((str = pw_properties_get(props, "source")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(capture_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(capture_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(capture_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ pw_properties_set(props, "source", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "sink", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_dont_move")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_DONT_RECONNECT, str);
+ pw_properties_set(props, "source_dont_move", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink_dont_move")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_DONT_RECONNECT, str);
+ pw_properties_set(props, "sink_dont_move", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "remix")) != NULL) {
+ /* Note that the boolean is inverted */
+ pw_properties_set(playback_props, PW_KEY_STREAM_DONT_REMIX,
+ module_args_parse_bool(str) ? "false" : "true");
+ pw_properties_set(props, "remix", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "latency_msec")) != NULL)
+ d->latency_msec = atoi(str);
+
+ if ((str = pw_properties_get(props, "sink_input_properties")) != NULL) {
+ module_args_add_props(playback_props, str);
+ pw_properties_set(props, "sink_input_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "source_output_properties")) != NULL) {
+ module_args_add_props(capture_props, str);
+ pw_properties_set(props, "source_output_properties", NULL);
+ }
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+ d->info = info;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_loopback) = {
+ .name = "module-loopback",
+ .prepare = module_loopback_prepare,
+ .load = module_loopback_load,
+ .unload = module_loopback_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_loopback_info),
+ .data_size = sizeof(struct module_loopback_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c b/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
new file mode 100644
index 0000000..e8f4c14
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c
@@ -0,0 +1,127 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "../module.h"
+#include "../pulse-server.h"
+#include "../server.h"
+
+#define NAME "protocol-tcp"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_native_protocol_tcp_data {
+ struct module *module;
+ struct pw_array servers;
+};
+
+static int module_native_protocol_tcp_load(struct module *module)
+{
+ struct module_native_protocol_tcp_data *data = module->user_data;
+ struct impl *impl = module->impl;
+ const char *address;
+ int res;
+
+ if ((address = pw_properties_get(module->props, "pulse.tcp")) == NULL)
+ return -EIO;
+
+ pw_array_init(&data->servers, sizeof(struct server *));
+
+ res = servers_create_and_start(impl, address, &data->servers);
+ if (res < 0)
+ return res;
+
+ return 0;
+}
+
+static int module_native_protocol_tcp_unload(struct module *module)
+{
+ struct module_native_protocol_tcp_data *d = module->user_data;
+ struct server **s;
+
+ pw_array_for_each (s, &d->servers)
+ server_free(*s);
+
+ pw_array_clear(&d->servers);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_native_protocol_tcp_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Native protocol (TCP sockets)" },
+ { PW_KEY_MODULE_USAGE, "port=<TCP port number> "
+ "listen=<address to listen on> "
+ "auth-anonymous=<don't check for cookies?>"},
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_native_protocol_tcp_prepare(struct module * const module)
+{
+ struct module_native_protocol_tcp_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ const char *port, *listen, *auth;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if ((port = pw_properties_get(props, "port")) == NULL)
+ port = SPA_STRINGIFY(PW_PROTOCOL_PULSE_DEFAULT_PORT);
+
+ listen = pw_properties_get(props, "listen");
+
+ auth = pw_properties_get(props, "auth-anonymous");
+
+ f = open_memstream(&args, &size);
+ if (f == NULL)
+ return -errno;
+
+ fprintf(f, "[ { ");
+ fprintf(f, " \"address\": \"tcp:%s%s%s\" ",
+ listen ? listen : "", listen ? ":" : "", port);
+ if (auth && module_args_parse_bool(auth))
+ fprintf(f, " \"client.access\": \"unrestricted\" ");
+ fprintf(f, "} ]");
+ fclose(f);
+
+ pw_properties_set(props, "pulse.tcp", args);
+ free(args);
+
+ d->module = module;
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_native_protocol_tcp) = {
+ .name = "module-native-protocol-tcp",
+ .prepare = module_native_protocol_tcp_prepare,
+ .load = module_native_protocol_tcp_load,
+ .unload = module_native_protocol_tcp_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_native_protocol_tcp_info),
+ .data_size = sizeof(struct module_native_protocol_tcp_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-null-sink.c b/src/modules/module-protocol-pulse/modules/module-null-sink.c
new file mode 100644
index 0000000..caae598
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-null-sink.c
@@ -0,0 +1,228 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Georges Basile Stavracas Neto
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "../manager.h"
+#include "../module.h"
+
+#define NAME "null-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_null_sink_data {
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+};
+
+static void module_null_sink_proxy_removed(void *data)
+{
+ struct module *module = data;
+ struct module_null_sink_data *d = module->user_data;
+ pw_proxy_destroy(d->proxy);
+}
+
+static void module_null_sink_proxy_destroy(void *data)
+{
+ struct module *module = data;
+ struct module_null_sink_data *d = module->user_data;
+
+ pw_log_info("proxy %p destroy", d->proxy);
+
+ spa_hook_remove(&d->proxy_listener);
+ d->proxy = NULL;
+
+ module_schedule_unload(module);
+}
+
+static void module_null_sink_proxy_bound(void *data, uint32_t global_id)
+{
+ struct module *module = data;
+ struct module_null_sink_data *d = module->user_data;
+
+ pw_log_info("proxy %p bound", d->proxy);
+
+ module_emit_loaded(module, 0);
+}
+
+static void module_null_sink_proxy_error(void *data, int seq, int res, const char *message)
+{
+ struct module *module = data;
+ struct module_null_sink_data *d = module->user_data;
+
+ pw_log_info("proxy %p error %d", d->proxy, res);
+
+ pw_proxy_destroy(d->proxy);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = module_null_sink_proxy_removed,
+ .bound = module_null_sink_proxy_bound,
+ .error = module_null_sink_proxy_error,
+ .destroy = module_null_sink_proxy_destroy,
+};
+
+static void module_null_sink_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct module *module = data;
+
+ pw_log_warn("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ module_schedule_unload(module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = module_null_sink_core_error,
+};
+
+static int module_null_sink_load(struct module *module)
+{
+ struct module_null_sink_data *d = module->user_data;
+
+ d->core = pw_context_connect(module->impl->context, NULL, 0);
+ if (d->core == NULL)
+ return -errno;
+
+ pw_core_add_listener(d->core, &d->core_listener, &core_events, module);
+
+ pw_properties_setf(module->props, "pulse.module.id", "%u", module->index);
+
+ d->proxy = pw_core_create_object(d->core,
+ "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,
+ module->props ? &module->props->dict : NULL, 0);
+ if (d->proxy == NULL)
+ return -errno;
+
+ pw_proxy_add_listener(d->proxy, &d->proxy_listener, &proxy_events, module);
+
+ return SPA_RESULT_RETURN_ASYNC(0);
+}
+
+static int module_null_sink_unload(struct module *module)
+{
+ struct module_null_sink_data *d = module->user_data;
+
+ if (d->proxy != NULL) {
+ spa_hook_remove(&d->proxy_listener);
+ pw_proxy_destroy(d->proxy);
+ d->proxy = NULL;
+ }
+
+ if (d->core != NULL) {
+ spa_hook_remove(&d->core_listener);
+ pw_core_disconnect(d->core);
+ d->core = NULL;
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item module_null_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "A NULL sink" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name of sink> "
+ "sink_properties=<properties for the sink> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_null_sink_prepare(struct module * const module)
+{
+ struct pw_properties * const props = module->props;
+ const char *str;
+ struct spa_audio_info_raw info = { 0 };
+ uint32_t i;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ else {
+ pw_properties_set(props, PW_KEY_NODE_NAME, "null-sink");
+ }
+
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0)
+ return -EINVAL;
+
+ if (info.rate)
+ pw_properties_setf(props, SPA_KEY_AUDIO_RATE, "%u", info.rate);
+ if (info.channels) {
+ char *s, *p;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info.channels);
+
+ p = s = alloca(info.channels * 8);
+ for (i = 0; i < info.channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info.position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+ }
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ if ((str = pw_properties_get(props, PW_KEY_NODE_DESCRIPTION)) == NULL) {
+ const char *name, *class;
+
+ name = pw_properties_get(props, PW_KEY_NODE_NAME);
+ class = pw_properties_get(props, PW_KEY_MEDIA_CLASS);
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s%s%s%ssink",
+ name, (name[0] == '\0') ? "" : " ",
+ class ? class : "", (class && class[0] != '\0') ? " " : "");
+ }
+ pw_properties_set(props, PW_KEY_FACTORY_NAME, "support.null-audio-sink");
+
+ if (pw_properties_get(props, "monitor.channel-volumes") == NULL)
+ pw_properties_set(props, "monitor.channel-volumes", "true");
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_null_sink) = {
+ .name = "module-null-sink",
+ .prepare = module_null_sink_prepare,
+ .load = module_null_sink_load,
+ .unload = module_null_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_null_sink_info),
+ .data_size = sizeof(struct module_null_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-sink.c b/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
new file mode 100644
index 0000000..2cc36db
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-pipe-sink.c
@@ -0,0 +1,210 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "pipe-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_pipesink_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *capture_props;
+ struct spa_audio_info_raw info;
+ char *filename;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_pipesink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_pipe_sink_load(struct module *module)
+{
+ struct module_pipesink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ uint32_t i;
+
+ pw_properties_setf(data->capture_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ fprintf(f, " \"tunnel.mode\" = \"sink\" ");
+ if (data->filename != NULL)
+ fprintf(f, " \"pipe.filename\": \"%s\"", data->filename);
+ if (data->info.format != 0)
+ fprintf(f, " \"audio.format\": \"%s\"", format_id2name(data->info.format));
+ if (data->info.rate != 0)
+ fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " \"audio.position\": [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ],");
+ }
+ }
+ fprintf(f, " \"stream.props\": {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-pipe-tunnel",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+ return 0;
+}
+
+static int module_pipe_sink_unload(struct module *module)
+{
+ struct module_pipesink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ pw_properties_free(d->capture_props);
+ free(d->filename);
+ return 0;
+}
+
+static const struct spa_dict_item module_pipe_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Pipe sink" },
+ { PW_KEY_MODULE_USAGE, "file=<name of the FIFO special file to use> "
+ "sink_name=<name for the sink> "
+ "sink_properties=<sink properties> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_pipe_sink_prepare(struct module * const module)
+{
+ struct module_pipesink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *capture_props = NULL;
+ struct spa_audio_info_raw info = { 0 };
+ const char *str;
+ char *filename = NULL;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ if (!capture_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ info.format = SPA_AUDIO_FORMAT_S16;
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ info.format = format_paname2id(str, strlen(str));
+ pw_properties_set(props, "format", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL)
+ module_args_add_props(capture_props, str);
+
+ if ((str = pw_properties_get(props, "file")) != NULL) {
+ filename = strdup(str);
+ pw_properties_set(props, "file", NULL);
+ }
+ if ((str = pw_properties_get(capture_props, PW_KEY_DEVICE_ICON_NAME)) == NULL)
+ pw_properties_set(capture_props, PW_KEY_DEVICE_ICON_NAME,
+ "audio-card");
+ if ((str = pw_properties_get(capture_props, PW_KEY_NODE_NAME)) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME,
+ "fifo_output");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->info = info;
+ d->filename = filename;
+
+ return 0;
+out:
+ pw_properties_free(capture_props);
+ free(filename);
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_pipe_sink) = {
+ .name = "module-pipe-sink",
+ .prepare = module_pipe_sink_prepare,
+ .load = module_pipe_sink_load,
+ .unload = module_pipe_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_pipe_sink_info),
+ .data_size = sizeof(struct module_pipesink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-pipe-source.c b/src/modules/module-protocol-pulse/modules/module-pipe-source.c
new file mode 100644
index 0000000..6153635
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-pipe-source.c
@@ -0,0 +1,210 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "pipe-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_pipesrc_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *playback_props;
+ struct spa_audio_info_raw info;
+ char *filename;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_pipesrc_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_pipe_source_load(struct module *module)
+{
+ struct module_pipesrc_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ uint32_t i;
+
+ pw_properties_setf(data->playback_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ fprintf(f, " \"tunnel.mode\" = \"source\" ");
+ if (data->filename != NULL)
+ fprintf(f, " \"pipe.filename\": \"%s\"", data->filename);
+ if (data->info.format != 0)
+ fprintf(f, " \"audio.format\": \"%s\"", format_id2name(data->info.format));
+ if (data->info.rate != 0)
+ fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " \"audio.position\": [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ],");
+ }
+ }
+ fprintf(f, " \"stream.props\": {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-pipe-tunnel",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+ return 0;
+}
+
+static int module_pipe_source_unload(struct module *module)
+{
+ struct module_pipesrc_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ pw_properties_free(d->playback_props);
+ free(d->filename);
+ return 0;
+}
+
+static const struct spa_dict_item module_pipe_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Pipe source" },
+ { PW_KEY_MODULE_USAGE, "file=<name of the FIFO special file to use> "
+ "source_name=<name for the source> "
+ "source_properties=<source properties> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_pipe_source_prepare(struct module * const module)
+{
+ struct module_pipesrc_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL;
+ struct spa_audio_info_raw info = { 0 };
+ const char *str;
+ char *filename = NULL;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!playback_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ info.format = SPA_AUDIO_FORMAT_S16;
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ info.format = format_paname2id(str, strlen(str));
+ pw_properties_set(props, "format", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL)
+ module_args_add_props(playback_props, str);
+
+ if ((str = pw_properties_get(props, "file")) != NULL) {
+ filename = strdup(str);
+ pw_properties_set(props, "file", NULL);
+ }
+ if ((str = pw_properties_get(playback_props, PW_KEY_DEVICE_ICON_NAME)) == NULL)
+ pw_properties_set(playback_props, PW_KEY_DEVICE_ICON_NAME,
+ "audio-input-microphone");
+ if ((str = pw_properties_get(playback_props, PW_KEY_NODE_NAME)) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME,
+ "fifo_input");
+
+ d->module = module;
+ d->playback_props = playback_props;
+ d->info = info;
+ d->filename = filename;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ free(filename);
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_pipe_source) = {
+ .name = "module-pipe-source",
+ .prepare = module_pipe_source_prepare,
+ .load = module_pipe_source_load,
+ .unload = module_pipe_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_pipe_source_info),
+ .data_size = sizeof(struct module_pipesrc_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-raop-discover.c b/src/modules/module-protocol-pulse/modules/module-raop-discover.c
new file mode 100644
index 0000000..e406189
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-raop-discover.c
@@ -0,0 +1,112 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "raop-discover"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+
+struct module_raop_discover_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_raop_discover_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_raop_discover_load(struct module *module)
+{
+ struct module_raop_discover_data *data = module->user_data;
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-raop-discover",
+ NULL, NULL);
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_raop_discover_unload(struct module *module)
+{
+ struct module_raop_discover_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item module_raop_discover_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.con>" },
+ { PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Discovery of RAOP devices" },
+ { PW_KEY_MODULE_USAGE, "" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_raop_discover_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_raop_discover_data * const data = module->user_data;
+ data->module = module;
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_raop_discover) = {
+ .name = "module-raop-discover",
+ .load_once = true,
+ .prepare = module_raop_discover_prepare,
+ .load = module_raop_discover_load,
+ .unload = module_raop_discover_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_raop_discover_info),
+ .data_size = sizeof(struct module_raop_discover_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-remap-sink.c b/src/modules/module-protocol-pulse/modules/module-remap-sink.c
new file mode 100644
index 0000000..f6b57f0
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-remap-sink.c
@@ -0,0 +1,253 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "remap-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_remap_sink_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_remap_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_remap_sink_load(struct module *module)
+{
+ struct module_remap_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "remap-sink-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "remap-sink-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-loopback",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_remap_sink_unload(struct module *module)
+{
+ struct module_remap_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_remap_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Remap sink channels" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "master=<name of sink to remap> "
+ "master_channel_map=<channel map> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "resample_method=<resampler> "
+ "remix=<remix channels?>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void position_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_remap_sink_prepare(struct module * const module)
+{
+ struct module_remap_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str, *master;
+ struct spa_audio_info_raw capture_info = { 0 };
+ struct spa_audio_info_raw playback_info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ master = pw_properties_get(props, "master");
+ if (pw_properties_get(props, "sink_name") == NULL) {
+ pw_properties_setf(props, "sink_name", "%s.remapped",
+ master ? master : "default");
+ }
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
+ pw_properties_setf(playback_props, PW_KEY_NODE_NAME, "output.%s", str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(capture_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+ if (pw_properties_get(capture_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(capture_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ if (pw_properties_get(capture_props, PW_KEY_DEVICE_CLASS) == NULL)
+ pw_properties_set(capture_props, PW_KEY_DEVICE_CLASS, "filter");
+
+ if ((str = pw_properties_get(capture_props, PW_KEY_MEDIA_NAME)) != NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, str);
+ if ((str = pw_properties_get(capture_props, PW_KEY_NODE_DESCRIPTION)) != NULL) {
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ } else {
+ str = pw_properties_get(capture_props, PW_KEY_NODE_NAME);
+ if (master != NULL || str == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "Remapped %s sink",
+ master ? master : "default");
+ } else {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s sink", str);
+ }
+ }
+ if ((str = pw_properties_get(props, "master")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &capture_info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ playback_info = capture_info;
+
+ if ((str = pw_properties_get(props, "master_channel_map")) != NULL) {
+ struct channel_map map;
+
+ channel_map_parse(str, &map);
+ if (map.channels == 0 || map.channels > SPA_AUDIO_MAX_CHANNELS) {
+ pw_log_error("invalid channel_map '%s'", str);
+ res = -EINVAL;
+ goto out;
+ }
+ channel_map_to_positions(&map, playback_info.position);
+ pw_properties_set(props, "master_channel_map", NULL);
+ }
+ position_to_props(&capture_info, capture_props);
+ position_to_props(&playback_info, playback_props);
+
+ if ((str = pw_properties_get(props, "remix")) != NULL) {
+ /* Note that the boolean is inverted */
+ pw_properties_set(playback_props, PW_KEY_STREAM_DONT_REMIX,
+ module_args_parse_bool(str) ? "false" : "true");
+ pw_properties_set(props, "remix", NULL);
+ }
+
+ if (pw_properties_get(playback_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_PASSIVE, "true");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_remap_sink) = {
+ .name = "module-remap-sink",
+ .prepare = module_remap_sink_prepare,
+ .load = module_remap_sink_load,
+ .unload = module_remap_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_remap_sink_info),
+ .data_size = sizeof(struct module_remap_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-remap-source.c b/src/modules/module-protocol-pulse/modules/module-remap-source.c
new file mode 100644
index 0000000..5ee6092
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-remap-source.c
@@ -0,0 +1,260 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "remap-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_remap_source_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_remap_source_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_remap_source_load(struct module *module)
+{
+ struct module_remap_source_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_GROUP, "remap-source-%u", module->index);
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_GROUP, "remap-source-%u", module->index);
+ pw_properties_setf(data->capture_props, "pulse.module.id", "%u", module->index);
+ pw_properties_setf(data->playback_props, "pulse.module.id", "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " capture.props = { ");
+ pw_properties_serialize_dict(f, &data->capture_props->dict, 0);
+ fprintf(f, " } playback.props = { ");
+ pw_properties_serialize_dict(f, &data->playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-loopback",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_remap_source_unload(struct module *module)
+{
+ struct module_remap_source_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->capture_props);
+ pw_properties_free(d->playback_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_remap_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Remap source channels" },
+ { PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
+ "source_properties=<properties for the source> "
+ "master=<name of source to filter> "
+ "master_channel_map=<channel map> "
+ "format=<sample format> "
+ "rate=<sample rate> "
+ "channels=<number of channels> "
+ "channel_map=<channel map> "
+ "resample_method=<resampler> "
+ "remix=<remix channels?>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void position_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_remap_source_prepare(struct module * const module)
+{
+ struct module_remap_source_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *playback_props = NULL, *capture_props = NULL;
+ const char *str, *master;
+ struct spa_audio_info_raw capture_info = { 0 };
+ struct spa_audio_info_raw playback_info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ capture_props = pw_properties_new(NULL, NULL);
+ playback_props = pw_properties_new(NULL, NULL);
+ if (!capture_props || !playback_props) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ master = pw_properties_get(props, "master");
+ if (pw_properties_get(props, "source_name") == NULL) {
+ pw_properties_setf(props, "source_name", "%s.remapped",
+ master ? master : "default");
+ }
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
+ pw_properties_setf(capture_props, PW_KEY_NODE_NAME, "input.%s", str);
+ pw_properties_set(props, "source_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(playback_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+ if (pw_properties_get(playback_props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(playback_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ if (pw_properties_get(playback_props, PW_KEY_DEVICE_CLASS) == NULL)
+ pw_properties_set(playback_props, PW_KEY_DEVICE_CLASS, "filter");
+
+ if ((str = pw_properties_get(playback_props, PW_KEY_MEDIA_NAME)) != NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, str);
+ if ((str = pw_properties_get(playback_props, PW_KEY_NODE_DESCRIPTION)) != NULL) {
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ } else {
+ str = pw_properties_get(playback_props, PW_KEY_NODE_NAME);
+ if (master != NULL || str == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "Remapped %s source",
+ master ? master : "default");
+ } else {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "%s source", str);
+ }
+ }
+ if ((str = pw_properties_get(props, "master")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(capture_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(capture_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(capture_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ pw_properties_set(props, "master", NULL);
+ }
+
+ if (module_args_to_audioinfo(module->impl, props, &playback_info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ capture_info = playback_info;
+
+ if ((str = pw_properties_get(props, "master_channel_map")) != NULL) {
+ struct channel_map map;
+
+ channel_map_parse(str, &map);
+ if (map.channels == 0 || map.channels > SPA_AUDIO_MAX_CHANNELS) {
+ pw_log_error("invalid channel_map '%s'", str);
+ res = -EINVAL;
+ goto out;
+ }
+ channel_map_to_positions(&map, capture_info.position);
+ pw_properties_set(props, "master_channel_map", NULL);
+ }
+ position_to_props(&playback_info, playback_props);
+ position_to_props(&capture_info, capture_props);
+
+ if ((str = pw_properties_get(props, "remix")) != NULL) {
+ /* Note that the boolean is inverted */
+ pw_properties_set(capture_props, PW_KEY_STREAM_DONT_REMIX,
+ module_args_parse_bool(str) ? "false" : "true");
+ pw_properties_set(props, "remix", NULL);
+ }
+
+ if (pw_properties_get(capture_props, PW_KEY_NODE_PASSIVE) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_PASSIVE, "true");
+
+ d->module = module;
+ d->capture_props = capture_props;
+ d->playback_props = playback_props;
+
+ return 0;
+out:
+ pw_properties_free(playback_props);
+ pw_properties_free(capture_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_remap_source) = {
+ .name = "module-remap-source",
+ .prepare = module_remap_source_prepare,
+ .load = module_remap_source_load,
+ .unload = module_remap_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_remap_source_info),
+ .data_size = sizeof(struct module_remap_source_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c b/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
new file mode 100644
index 0000000..3a672ac
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-roc-sink-input.c
@@ -0,0 +1,201 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "roc-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_sink_input_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *source_props;
+ struct pw_properties *roc_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_roc_sink_input_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_roc_sink_input_load(struct module *module)
+{
+ struct module_roc_sink_input_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->source_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->roc_props->dict, 0);
+ fprintf(f, " source.props = {");
+ pw_properties_serialize_dict(f, &data->source_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-roc-source",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_roc_sink_input_unload(struct module *module)
+{
+ struct module_roc_sink_input_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->roc_props);
+ pw_properties_free(d->source_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_sink_input_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc sink-input" },
+ { PW_KEY_MODULE_USAGE, "sink=<name for the sink> "
+ "sink_input_properties=<properties for the sink_input> "
+ "resampler_profile=<empty>|disable|high|medium|low "
+ "fec_code=<empty>|disable|rs8m|ldpc "
+ "sess_latency_msec=<target network latency in milliseconds> "
+ "local_ip=<local receiver ip> "
+ "local_source_port=<local receiver port for source packets> "
+ "local_repair_port=<local receiver port for repair packets> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_roc_sink_input_prepare(struct module * const module)
+{
+ struct module_roc_sink_input_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *source_props = NULL, *roc_props = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ source_props = pw_properties_new(NULL, NULL);
+ roc_props = pw_properties_new(NULL, NULL);
+ if (!source_props || !roc_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "sink")) != NULL) {
+ pw_properties_set(source_props, PW_KEY_TARGET_OBJECT, str);
+ pw_properties_set(props, "sink", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_input_properties")) != NULL) {
+ module_args_add_props(source_props, str);
+ pw_properties_set(props, "sink_input_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_ip")) != NULL) {
+ pw_properties_set(roc_props, "local.ip", str);
+ pw_properties_set(props, "local_ip", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_source_port")) != NULL) {
+ pw_properties_set(roc_props, "local.source.port", str);
+ pw_properties_set(props, "local_source_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_repair_port")) != NULL) {
+ pw_properties_set(roc_props, "local.repair.port", str);
+ pw_properties_set(props, "local_repair_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sess_latency_msec")) != NULL) {
+ pw_properties_set(roc_props, "sess.latency.msec", str);
+ pw_properties_set(props, "sess_latency_msec", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "resampler_profile")) != NULL) {
+ pw_properties_set(roc_props, "resampler.profile", str);
+ pw_properties_set(props, "resampler_profile", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "fec_code")) != NULL) {
+ pw_properties_set(roc_props, "fec.code", str);
+ pw_properties_set(props, "fec_code", NULL);
+ }
+
+ d->module = module;
+ d->source_props = source_props;
+ d->roc_props = roc_props;
+
+ return 0;
+out:
+ pw_properties_free(source_props);
+ pw_properties_free(roc_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_roc_sink_input) = {
+ .name = "module-roc-sink-input",
+ .prepare = module_roc_sink_input_prepare,
+ .load = module_roc_sink_input_load,
+ .unload = module_roc_sink_input_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_roc_sink_input_info),
+ .data_size = sizeof(struct module_roc_sink_input_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-sink.c b/src/modules/module-protocol-pulse/modules/module-roc-sink.c
new file mode 100644
index 0000000..2dd5bb8
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-roc-sink.c
@@ -0,0 +1,197 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "roc-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_sink_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *sink_props;
+ struct pw_properties *roc_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_roc_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_roc_sink_load(struct module *module)
+{
+ struct module_roc_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->sink_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->roc_props->dict, 0);
+ fprintf(f, " sink.props = {");
+ pw_properties_serialize_dict(f, &data->sink_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-roc-sink",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_roc_sink_unload(struct module *module)
+{
+ struct module_roc_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->roc_props);
+ pw_properties_free(d->sink_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc sink" },
+ { PW_KEY_MODULE_USAGE, "sink_name=<name for the sink> "
+ "sink_properties=<properties for the sink> "
+ "fec_code=<empty>|disable|rs8m|ldpc "
+ "remote_ip=<remote receiver ip> "
+ "remote_source_port=<remote receiver port for source packets> "
+ "remote_repair_port=<remote receiver port for repair packets> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_roc_sink_prepare(struct module * const module)
+{
+ struct module_roc_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *sink_props = NULL, *roc_props = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ sink_props = pw_properties_new(NULL, NULL);
+ roc_props = pw_properties_new(NULL, NULL);
+ if (!sink_props || !roc_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(sink_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(sink_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_MEDIA_CLASS)) == NULL) {
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ pw_properties_set(sink_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+ }
+
+ if ((str = pw_properties_get(props, "remote_ip")) != NULL) {
+ pw_properties_set(roc_props, "remote.ip", str);
+ pw_properties_set(props, "remote_ip", NULL);
+ } else {
+ pw_log_error("Remote IP not specified");
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "remote_source_port")) != NULL) {
+ pw_properties_set(roc_props, "remote.source.port", str);
+ pw_properties_set(props, "remote_source_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "remote_repair_port")) != NULL) {
+ pw_properties_set(roc_props, "remote.repair.port", str);
+ pw_properties_set(props, "remote_repair_port", NULL);
+ }
+ if ((str = pw_properties_get(props, "fec_code")) != NULL) {
+ pw_properties_set(roc_props, "fec.code", str);
+ pw_properties_set(props, "fec_code", NULL);
+ }
+
+ d->module = module;
+ d->sink_props = sink_props;
+ d->roc_props = roc_props;
+
+ return 0;
+out:
+ pw_properties_free(sink_props);
+ pw_properties_free(roc_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_roc_sink) = {
+ .name = "module-roc-sink",
+ .prepare = module_roc_sink_prepare,
+ .load = module_roc_sink_load,
+ .unload = module_roc_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_roc_sink_info),
+ .data_size = sizeof(struct module_roc_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-roc-source.c b/src/modules/module-protocol-pulse/modules/module-roc-source.c
new file mode 100644
index 0000000..681f27a
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-roc-source.c
@@ -0,0 +1,206 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "roc-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_source_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *source_props;
+ struct pw_properties *roc_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_roc_source_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_roc_source_load(struct module *module)
+{
+ struct module_roc_source_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->source_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->roc_props->dict, 0);
+ fprintf(f, " source.props = {");
+ pw_properties_serialize_dict(f, &data->source_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-roc-source",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_roc_source_unload(struct module *module)
+{
+ struct module_roc_source_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->roc_props);
+ pw_properties_free(d->source_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc source" },
+ { PW_KEY_MODULE_USAGE, "source_name=<name for the source> "
+ "source_properties=<properties for the source> "
+ "resampler_profile=<empty>|disable|high|medium|low "
+ "fec_code=<empty>|disable|rs8m|ldpc "
+ "sess_latency_msec=<target network latency in milliseconds> "
+ "local_ip=<local receiver ip> "
+ "local_source_port=<local receiver port for source packets> "
+ "local_repair_port=<local receiver port for repair packets> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_roc_source_prepare(struct module * const module)
+{
+ struct module_roc_source_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *source_props = NULL, *roc_props = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ source_props = pw_properties_new(NULL, NULL);
+ roc_props = pw_properties_new(NULL, NULL);
+ if (!source_props || !roc_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(source_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(source_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_MEDIA_CLASS)) == NULL) {
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ pw_properties_set(source_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+ }
+
+ if ((str = pw_properties_get(props, "local_ip")) != NULL) {
+ pw_properties_set(roc_props, "local.ip", str);
+ pw_properties_set(props, "local_ip", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_source_port")) != NULL) {
+ pw_properties_set(roc_props, "local.source.port", str);
+ pw_properties_set(props, "local_source_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "local_repair_port")) != NULL) {
+ pw_properties_set(roc_props, "local.repair.port", str);
+ pw_properties_set(props, "local_repair_port", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sess_latency_msec")) != NULL) {
+ pw_properties_set(roc_props, "sess.latency.msec", str);
+ pw_properties_set(props, "sess_latency_msec", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "resampler_profile")) != NULL) {
+ pw_properties_set(roc_props, "resampler.profile", str);
+ pw_properties_set(props, "resampler_profile", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "fec_code")) != NULL) {
+ pw_properties_set(roc_props, "fec.code", str);
+ pw_properties_set(props, "fec_code", NULL);
+ }
+
+ d->module = module;
+ d->source_props = source_props;
+ d->roc_props = roc_props;
+
+ return 0;
+out:
+ pw_properties_free(source_props);
+ pw_properties_free(roc_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_roc_source) = {
+ .name = "module-roc-source",
+ .prepare = module_roc_source_prepare,
+ .load = module_roc_source_load,
+ .unload = module_roc_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_roc_source_info),
+ .data_size = sizeof(struct module_roc_source_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
new file mode 100644
index 0000000..fdaecd7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c
@@ -0,0 +1,164 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "rtp-recv"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_rtp_recv_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *stream_props;
+ struct pw_properties *global_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_rtp_recv_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_rtp_recv_load(struct module *module)
+{
+ struct module_rtp_recv_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ pw_properties_setf(data->stream_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->global_props->dict, 0);
+ fprintf(f, " stream.props = {");
+ pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-rtp-source",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_rtp_recv_unload(struct module *module)
+{
+ struct module_rtp_recv_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->global_props);
+ pw_properties_free(d->stream_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_rtp_recv_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Receive data from a network via RTP/SAP/SDP" },
+ { PW_KEY_MODULE_USAGE, "sink=<name of the sink> "
+ "sap_address=<multicast address to listen on> "
+ "latency_msec=<latency in ms> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_rtp_recv_prepare(struct module * const module)
+{
+ struct module_rtp_recv_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *stream_props = NULL, *global_props = NULL;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ stream_props = pw_properties_new(NULL, NULL);
+ global_props = pw_properties_new(NULL, NULL);
+ if (!stream_props || !global_props) {
+ res = -errno;
+ goto out;
+ }
+ if ((str = pw_properties_get(props, "sink")) != NULL)
+ pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT, str);
+
+ if ((str = pw_properties_get(props, "sap_address")) != NULL)
+ pw_properties_set(global_props, "sap.ip", str);
+
+ if ((str = pw_properties_get(props, "latency_msec")) != NULL)
+ pw_properties_set(global_props, "sess.latency.msec", str);
+
+ d->module = module;
+ d->stream_props = stream_props;
+ d->global_props = global_props;
+
+ return 0;
+out:
+ pw_properties_free(stream_props);
+ pw_properties_free(global_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_rtp_recv) = {
+ .name = "module-rtp-recv",
+ .prepare = module_rtp_recv_prepare,
+ .load = module_rtp_recv_load,
+ .unload = module_rtp_recv_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_rtp_recv_info),
+ .data_size = sizeof(struct module_rtp_recv_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-send.c b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
new file mode 100644
index 0000000..b9aad05
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-rtp-send.c
@@ -0,0 +1,224 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "rtp-send"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_rtp_send_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ struct pw_properties *stream_props;
+ struct pw_properties *global_props;
+ struct spa_audio_info_raw info;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_rtp_send_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_rtp_send_load(struct module *module)
+{
+ struct module_rtp_send_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ uint32_t i;
+
+ pw_properties_setf(data->stream_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &data->global_props->dict, 0);
+ if (data->info.format != 0)
+ fprintf(f, " \"audio.format\": \"%s\"", format_id2name(data->info.format));
+ if (data->info.rate != 0)
+ fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " \"audio.position\": [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ],");
+ }
+ }
+ fprintf(f, " stream.props = {");
+ pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-rtp-sink",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_rtp_send_unload(struct module *module)
+{
+ struct module_rtp_send_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->global_props);
+ pw_properties_free(d->stream_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_rtp_send_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Read data from source and send it to the network via RTP/SAP/SDP" },
+ { PW_KEY_MODULE_USAGE, "source=<name of the source> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "rate=<sample rate> "
+ "destination_ip=<destination IP address> "
+ "source_ip=<source IP address> "
+ "port=<port number> "
+ "mtu=<maximum transfer unit> "
+ "loop=<loopback to local host?> "
+ "ttl=<ttl value> "
+ "inhibit_auto_suspend=<always|never|only_with_non_monitor_sources> "
+ "stream_name=<name of the stream> "
+ "enable_opus=<enable OPUS codec>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_rtp_send_prepare(struct module * const module)
+{
+ struct module_rtp_send_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *stream_props = NULL, *global_props = NULL;
+ struct spa_audio_info_raw info = { 0 };
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ stream_props = pw_properties_new(NULL, NULL);
+ global_props = pw_properties_new(NULL, NULL);
+ if (!stream_props || !global_props) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(stream_props, PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(stream_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT, str);
+ }
+ }
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ info.format = 0;
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ if ((info.format = format_paname2id(str, strlen(str))) ==
+ SPA_AUDIO_FORMAT_UNKNOWN) {
+ pw_log_error("unknown format %s", str);
+ res = -EINVAL;
+ goto out;
+ }
+ }
+
+ if ((str = pw_properties_get(props, "destination_ip")) != NULL)
+ pw_properties_set(global_props, "destination.ip", str);
+ if ((str = pw_properties_get(props, "source_ip")) != NULL)
+ pw_properties_set(global_props, "source.ip", str);
+ if ((str = pw_properties_get(props, "port")) != NULL)
+ pw_properties_set(global_props, "destination.port", str);
+ if ((str = pw_properties_get(props, "mtu")) != NULL)
+ pw_properties_set(global_props, "net.mtu", str);
+ if ((str = pw_properties_get(props, "loop")) != NULL)
+ pw_properties_set(global_props, "net.loop",
+ module_args_parse_bool(str) ? "true" : "false");
+ if ((str = pw_properties_get(props, "ttl")) != NULL)
+ pw_properties_set(global_props, "net.ttl", str);
+ if ((str = pw_properties_get(props, "stream_name")) != NULL)
+ pw_properties_set(global_props, "sess.name", str);
+
+ d->module = module;
+ d->stream_props = stream_props;
+ d->global_props = global_props;
+ d->info = info;
+
+ return 0;
+out:
+ pw_properties_free(stream_props);
+ pw_properties_free(global_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_rtp_send) = {
+ .name = "module-rtp-send",
+ .prepare = module_rtp_send_prepare,
+ .load = module_rtp_send_load,
+ .unload = module_rtp_send_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_rtp_send_info),
+ .data_size = sizeof(struct module_rtp_send_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c b/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
new file mode 100644
index 0000000..3d0f7d7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c
@@ -0,0 +1,210 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "simple-protocol-tcp"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_simple_protocol_tcp_data {
+ struct module *module;
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ struct pw_properties *module_props;
+
+ struct spa_audio_info_raw info;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_simple_protocol_tcp_data *d = data;
+
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_simple_protocol_tcp_load(struct module *module)
+{
+ struct module_simple_protocol_tcp_data *data = module->user_data;
+ struct impl *impl = module->impl;
+ char *args;
+ size_t size;
+ uint32_t i;
+ FILE *f;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if (data->info.rate != 0)
+ fprintf(f, " \"audio.rate\": %u,", data->info.rate);
+ if (data->info.channels != 0) {
+ fprintf(f, " \"audio.channels\": %u,", data->info.channels);
+ if (!(data->info.flags & SPA_AUDIO_FLAG_UNPOSITIONED)) {
+ fprintf(f, " \"audio.position\": [ ");
+ for (i = 0; i < data->info.channels; i++)
+ fprintf(f, "%s\"%s\"", i == 0 ? "" : ",",
+ channel_id2name(data->info.position[i]));
+ fprintf(f, " ],");
+ }
+ }
+ pw_properties_serialize_dict(f, &data->module_props->dict, 0);
+ fprintf(f, "}");
+ fclose(f);
+
+ data->mod = pw_context_load_module(impl->context,
+ "libpipewire-module-protocol-simple",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod, &data->mod_listener, &module_events, data);
+
+ return 0;
+}
+
+static int module_simple_protocol_tcp_unload(struct module *module)
+{
+ struct module_simple_protocol_tcp_data *d = module->user_data;
+
+ if (d->mod != NULL) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ }
+
+ pw_properties_free(d->module_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_simple_protocol_tcp_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Simple protocol (TCP sockets)" },
+ { PW_KEY_MODULE_USAGE, "rate=<sample rate> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "channel_map=<number of channels> "
+ "sink=<sink to connect to> "
+ "source=<source to connect to> "
+ "playback=<enable playback?> "
+ "record=<enable record?> "
+ "port=<TCP port number> "
+ "listen=<address to listen on>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_simple_protocol_tcp_prepare(struct module * const module)
+{
+ struct module_simple_protocol_tcp_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *module_props = NULL;
+ const char *str, *port, *listen;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ module_props = pw_properties_new(NULL, NULL);
+ if (module_props == NULL) {
+ res = -errno;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ pw_properties_set(module_props, "audio.format",
+ format_id2name(format_paname2id(str, strlen(str))));
+ pw_properties_set(props, "format", NULL);
+ }
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+ if ((str = pw_properties_get(props, "playback")) != NULL) {
+ pw_properties_set(module_props, "playback", str);
+ pw_properties_set(props, "playback", NULL);
+ }
+ if ((str = pw_properties_get(props, "record")) != NULL) {
+ pw_properties_set(module_props, "capture", str);
+ pw_properties_set(props, "record", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "source")) != NULL) {
+ if (spa_strendswith(str, ".monitor")) {
+ pw_properties_setf(module_props, "capture.node",
+ "%.*s", (int)strlen(str)-8, str);
+ pw_properties_set(module_props, PW_KEY_STREAM_CAPTURE_SINK,
+ "true");
+ } else {
+ pw_properties_set(module_props, "capture.node", str);
+ }
+ pw_properties_set(props, "source", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink")) != NULL) {
+ pw_properties_set(module_props, "playback.node", str);
+ pw_properties_set(props, "sink", NULL);
+ }
+
+ if ((port = pw_properties_get(props, "port")) == NULL)
+ port = "4711";
+ listen = pw_properties_get(props, "listen");
+
+ pw_properties_setf(module_props, "server.address", "[ \"tcp:%s%s%s\" ]",
+ listen ? listen : "", listen ? ":" : "", port);
+
+ d->module = module;
+ d->module_props = module_props;
+ d->info = info;
+
+ return 0;
+out:
+ pw_properties_free(module_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_simple_protocol_tcp) = {
+ .name = "module-simple-protocol-tcp",
+ .prepare = module_simple_protocol_tcp_prepare,
+ .load = module_simple_protocol_tcp_load,
+ .unload = module_simple_protocol_tcp_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_simple_protocol_tcp_info),
+ .data_size = sizeof(struct module_simple_protocol_tcp_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
new file mode 100644
index 0000000..6354193
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-switch-on-connect.c
@@ -0,0 +1,291 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Pauli Virtanen <pav@iki.fi>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+#include <regex.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#include "../manager.h"
+#include "../collect.h"
+
+#define NAME "switch-on-connect"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+/* Ignore HDMI by default */
+#define DEFAULT_BLOCKLIST "hdmi"
+
+struct module_switch_on_connect_data {
+ struct module *module;
+
+ struct pw_core *core;
+ struct pw_manager *manager;
+ struct spa_hook core_listener;
+ struct spa_hook manager_listener;
+ struct pw_manager_object *metadata_default;
+
+ regex_t blocklist;
+
+ int sync_seq;
+
+ unsigned int only_from_unavailable:1;
+ unsigned int ignore_virtual:1;
+ unsigned int started:1;
+};
+
+static void handle_metadata(struct module_switch_on_connect_data *d, struct pw_manager_object *old,
+ struct pw_manager_object *new, const char *name)
+{
+ if (spa_streq(name, "default")) {
+ if (d->metadata_default == old)
+ d->metadata_default = new;
+ }
+}
+
+static void manager_added(void *data, struct pw_manager_object *o)
+{
+ struct module_switch_on_connect_data *d = data;
+ struct pw_node_info *info = o->info;
+ struct pw_device_info *card_info = NULL;
+ uint32_t card_id = SPA_ID_INVALID;
+ struct pw_manager_object *card = NULL;
+ const char *str, *bus, *name;
+
+ if (spa_streq(o->type, PW_TYPE_INTERFACE_Metadata)) {
+ if (o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_METADATA_NAME)) != NULL)
+ handle_metadata(d, NULL, o, str);
+ }
+
+ if (!d->metadata_default || !d->started)
+ return;
+
+ if (!(pw_manager_object_is_sink(o) || pw_manager_object_is_source_or_monitor(o)))
+ return;
+
+ if (!info || !info->props)
+ return;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if (!name)
+ return;
+
+ /* Find card */
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(d->manager, &sel);
+ }
+ if (!card)
+ return;
+ card_info = card->info;
+ if (!card_info || !card_info->props)
+ return;
+
+ pw_log_debug("considering switching to %s", name);
+
+ /* If internal device, only consider hdmi sinks */
+ str = spa_dict_lookup(info->props, "api.alsa.path");
+ bus = spa_dict_lookup(card_info->props, PW_KEY_DEVICE_BUS);
+ if ((spa_streq(bus, "pci") || spa_streq(bus, "isa")) &&
+ !(pw_manager_object_is_sink(o) && spa_strstartswith(str, "hdmi"))) {
+ pw_log_debug("not switching to internal device");
+ return;
+ }
+
+ if (regexec(&d->blocklist, name, 0, NULL, 0) == 0) {
+ pw_log_debug("not switching to blocklisted device");
+ return;
+ }
+
+ if (d->ignore_virtual && spa_dict_lookup(info->props, PW_KEY_DEVICE_API) == NULL) {
+ pw_log_debug("not switching to virtual device");
+ return;
+ }
+
+ if (d->only_from_unavailable) {
+ /* XXX: not implemented */
+ }
+
+ /* Switch default */
+ pw_log_debug("switching to %s", name);
+
+ pw_manager_set_metadata(d->manager, d->metadata_default,
+ PW_ID_CORE,
+ pw_manager_object_is_sink(o) ? METADATA_CONFIG_DEFAULT_SINK
+ : METADATA_CONFIG_DEFAULT_SOURCE,
+ "Spa:String:JSON", "{ \"name\"\"%s\" }", name);
+}
+
+static void manager_sync(void *data)
+{
+ struct module_switch_on_connect_data *d = data;
+
+ /* Manager emits devices/etc next --- enable started flag after that */
+ if (!d->started)
+ d->sync_seq = pw_core_sync(d->core, PW_ID_CORE, d->sync_seq);
+}
+
+static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .added = manager_added,
+ .sync = manager_sync,
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct module_switch_on_connect_data *d = data;
+ if (seq == d->sync_seq) {
+ pw_log_debug("%p: started", d);
+ d->started = true;
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+};
+
+static int module_switch_on_connect_load(struct module *module)
+{
+ struct impl *impl = module->impl;
+ struct module_switch_on_connect_data *d = module->user_data;
+ int res;
+
+ d->core = pw_context_connect(impl->context, NULL, 0);
+ if (d->core == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ d->manager = pw_manager_new(d->core);
+ if (d->manager == NULL) {
+ res = -errno;
+ pw_core_disconnect(d->core);
+ d->core = NULL;
+ goto error;
+ }
+
+ pw_manager_add_listener(d->manager, &d->manager_listener, &manager_events, d);
+ pw_core_add_listener(d->core, &d->core_listener, &core_events, d);
+
+ /* Postpone setting started flag after initial nodes emitted */
+ pw_manager_sync(d->manager);
+
+ return 0;
+
+error:
+ pw_log_error("%p: failed to connect: %s", impl, spa_strerror(res));
+ return res;
+}
+
+static int module_switch_on_connect_unload(struct module *module)
+{
+ struct module_switch_on_connect_data *d = module->user_data;
+
+ if (d->manager) {
+ spa_hook_remove(&d->manager_listener);
+ pw_manager_destroy(d->manager);
+ d->manager = NULL;
+ }
+
+ if (d->core) {
+ spa_hook_remove(&d->core_listener);
+ pw_core_disconnect(d->core);
+ d->core = NULL;
+ }
+
+ regfree(&d->blocklist);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_switch_on_connect_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen <pav@iki.fi>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Switch to new devices on connect. "
+ "This module exists for Pulseaudio compatibility, and is useful only when some applications "
+ "try to manage the default sinks/sources themselves and interfere with PipeWire's builtin "
+ "default device switching." },
+ { PW_KEY_MODULE_USAGE, "only_from_unavailable=<boolean, only switch from unavailable ports (not implemented yet)> "
+ "ignore_virtual=<boolean, ignore new virtual sinks and sources, defaults to true> "
+ "blocklist=<regex, ignore matching devices, default=hdmi> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_switch_on_connect_prepare(struct module * const module)
+{
+ struct module_switch_on_connect_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ bool only_from_unavailable = false, ignore_virtual = true;
+ const char *str;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if ((str = pw_properties_get(props, "only_from_unavailable")) != NULL) {
+ only_from_unavailable = module_args_parse_bool(str);
+ pw_properties_set(props, "only_from_unavailable", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "ignore_virtual")) != NULL) {
+ ignore_virtual = module_args_parse_bool(str);
+ pw_properties_set(props, "ignore_virtual", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "blocklist")) == NULL)
+ str = DEFAULT_BLOCKLIST;
+
+ if (regcomp(&d->blocklist, str, REG_NOSUB | REG_EXTENDED) != 0)
+ return -EINVAL;
+
+ pw_properties_set(props, "blocklist", NULL);
+
+ d->module = module;
+ d->ignore_virtual = ignore_virtual;
+ d->only_from_unavailable = only_from_unavailable;
+
+ if (d->only_from_unavailable) {
+ /* XXX: not implemented */
+ pw_log_warn("only_from_unavailable is not implemented");
+ }
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_switch_on_connect) = {
+ .name = "module-switch-on-connect",
+ .load_once = true,
+ .prepare = module_switch_on_connect_prepare,
+ .load = module_switch_on_connect_load,
+ .unload = module_switch_on_connect_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_switch_on_connect_info),
+ .data_size = sizeof(struct module_switch_on_connect_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c b/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
new file mode 100644
index 0000000..55f9535
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-tunnel-sink.c
@@ -0,0 +1,230 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/i18n.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "tunnel-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_tunnel_sink_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ uint32_t latency_msec;
+
+ struct pw_properties *stream_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_tunnel_sink_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_tunnel_sink_load(struct module *module)
+{
+ struct module_tunnel_sink_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ const char *server;
+
+ server = pw_properties_get(module->props, "server");
+
+ pw_properties_setf(data->stream_props, "pulse.module.id",
+ "%u", module->index);
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " pulse.server.address = \"%s\" ", server);
+ fprintf(f, " tunnel.mode = sink ");
+ if (data->latency_msec > 0)
+ fprintf(f, " pulse.latency = %u ", data->latency_msec);
+ fprintf(f, " stream.props = {");
+ pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-pulse-tunnel",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_tunnel_sink_unload(struct module *module)
+{
+ struct module_tunnel_sink_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->stream_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_tunnel_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create a network sink which connects to a remote PulseAudio server" },
+ { PW_KEY_MODULE_USAGE,
+ "server=<address> "
+ "sink=<name of the remote sink> "
+ "sink_name=<name for the local sink> "
+ "sink_properties=<properties for the local sink> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "rate=<sample rate> "
+ "channel_map=<channel map> "
+ "latency_msec=<fixed latency in ms> "
+ "cookie=<cookie file path>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void audio_info_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_tunnel_sink_prepare(struct module * const module)
+{
+ struct module_tunnel_sink_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *stream_props = NULL;
+ const char *str, *server, *remote_sink_name;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ stream_props = pw_properties_new(NULL, NULL);
+ if (stream_props == NULL) {
+ res = -ENOMEM;
+ goto out;
+ }
+
+ remote_sink_name = pw_properties_get(props, "sink");
+ if (remote_sink_name)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, remote_sink_name);
+
+ if ((server = pw_properties_get(props, "server")) == NULL) {
+ pw_log_error("no server given");
+ res = -EINVAL;
+ goto out;
+ }
+
+ pw_properties_setf(stream_props, PW_KEY_NODE_DESCRIPTION,
+ _("Tunnel to %s/%s"), server,
+ remote_sink_name ? remote_sink_name : "");
+ pw_properties_set(stream_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ if ((str = pw_properties_get(props, "sink_name")) != NULL) {
+ pw_properties_set(stream_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink_name", NULL);
+ } else {
+ pw_properties_setf(stream_props, PW_KEY_NODE_NAME,
+ "tunnel-sink.%s", server);
+ }
+
+ if ((str = pw_properties_get(props, "sink_properties")) != NULL) {
+ module_args_add_props(stream_props, str);
+ pw_properties_set(props, "sink_properties", NULL);
+ }
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ audio_info_to_props(&info, stream_props);
+ if ((str = pw_properties_get(props, "format")) != NULL) {
+ uint32_t id = format_paname2id(str, strlen(str));
+ if (id == SPA_AUDIO_FORMAT_UNKNOWN) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ pw_properties_set(stream_props, PW_KEY_AUDIO_FORMAT, format_id2name(id));
+ }
+
+ d->module = module;
+ d->stream_props = stream_props;
+
+ pw_properties_fetch_uint32(props, "latency_msec", &d->latency_msec);
+
+ return 0;
+out:
+ pw_properties_free(stream_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_tunnel_sink) = {
+ .name = "module-tunnel-sink",
+ .prepare = module_tunnel_sink_prepare,
+ .load = module_tunnel_sink_load,
+ .unload = module_tunnel_sink_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_tunnel_sink_info),
+ .data_size = sizeof(struct module_tunnel_sink_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-tunnel-source.c b/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
new file mode 100644
index 0000000..fa17c16
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-tunnel-source.c
@@ -0,0 +1,220 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/i18n.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "tunnel-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_tunnel_source_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+
+ uint32_t latency_msec;
+
+ struct pw_properties *stream_props;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_tunnel_source_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_tunnel_source_load(struct module *module)
+{
+ struct module_tunnel_source_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+ const char *server;
+
+ pw_properties_setf(data->stream_props, "pulse.module.id",
+ "%u", module->index);
+
+ server = pw_properties_get(module->props, "server");
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &module->props->dict, 0);
+ fprintf(f, " pulse.server.address = \"%s\" ", server);
+ fprintf(f, " tunnel.mode = source ");
+ if (data->latency_msec > 0)
+ fprintf(f, " pulse.latency = %u ", data->latency_msec);
+ fprintf(f, " stream.props = {");
+ pw_properties_serialize_dict(f, &data->stream_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-pulse-tunnel",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_tunnel_source_unload(struct module *module)
+{
+ struct module_tunnel_source_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ pw_properties_free(d->stream_props);
+
+ return 0;
+}
+
+static const struct spa_dict_item module_tunnel_source_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create a network source which connects to a remote PulseAudio server" },
+ { PW_KEY_MODULE_USAGE,
+ "server=<address> "
+ "source=<name of the remote source> "
+ "source_name=<name for the local source> "
+ "source_properties=<properties for the local source> "
+ "format=<sample format> "
+ "channels=<number of channels> "
+ "rate=<sample rate> "
+ "channel_map=<channel map> "
+ "latency_msec=<fixed latency in ms> "
+ "cookie=<cookie file path>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static void audio_info_to_props(struct spa_audio_info_raw *info, struct pw_properties *props)
+{
+ char *s, *p;
+ uint32_t i;
+
+ pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels);
+ p = s = alloca(info->channels * 8);
+ for (i = 0; i < info->channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(info->position[i]));
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+}
+
+static int module_tunnel_source_prepare(struct module * const module)
+{
+ struct module_tunnel_source_data * const d = module->user_data;
+ struct pw_properties * const props = module->props;
+ struct pw_properties *stream_props = NULL;
+ const char *str, *server, *remote_source_name;
+ struct spa_audio_info_raw info = { 0 };
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ stream_props = pw_properties_new(NULL, NULL);
+ if (stream_props == NULL) {
+ res = -ENOMEM;
+ goto out;
+ }
+
+ remote_source_name = pw_properties_get(props, "source");
+ if (remote_source_name)
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, remote_source_name);
+
+ if ((server = pw_properties_get(props, "server")) == NULL) {
+ pw_log_error("no server given");
+ res = -EINVAL;
+ goto out;
+ }
+
+ pw_properties_setf(stream_props, PW_KEY_NODE_DESCRIPTION,
+ _("Tunnel to %s/%s"), server,
+ remote_source_name ? remote_source_name : "");
+ pw_properties_set(stream_props, PW_KEY_MEDIA_CLASS, "Audio/Source");
+
+ if ((str = pw_properties_get(props, "source_name")) != NULL) {
+ pw_properties_set(stream_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source_name", NULL);
+ } else {
+ pw_properties_setf(stream_props, PW_KEY_NODE_NAME,
+ "tunnel-source.%s", server);
+ }
+ if ((str = pw_properties_get(props, "source_properties")) != NULL) {
+ module_args_add_props(stream_props, str);
+ pw_properties_set(props, "source_properties", NULL);
+ }
+ if (module_args_to_audioinfo(module->impl, props, &info) < 0) {
+ res = -EINVAL;
+ goto out;
+ }
+
+ audio_info_to_props(&info, stream_props);
+
+ d->module = module;
+ d->stream_props = stream_props;
+
+ pw_properties_fetch_uint32(props, "latency_msec", &d->latency_msec);
+
+ return 0;
+out:
+ pw_properties_free(stream_props);
+
+ return res;
+}
+
+DEFINE_MODULE_INFO(module_tunnel_source) = {
+ .name = "module-tunnel-source",
+ .prepare = module_tunnel_source_prepare,
+ .load = module_tunnel_source_load,
+ .unload = module_tunnel_source_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_tunnel_source_info),
+ .data_size = sizeof(struct module_tunnel_source_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-x11-bell.c b/src/modules/module-protocol-pulse/modules/module-x11-bell.c
new file mode 100644
index 0000000..9d7e217
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-x11-bell.c
@@ -0,0 +1,130 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "../module.h"
+
+#define NAME "x11-bell"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_x11_bell_data {
+ struct module *module;
+
+ struct pw_impl_module *mod;
+ struct spa_hook mod_listener;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_x11_bell_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_x11_bell_load(struct module *module)
+{
+ struct module_x11_bell_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ const char *str;
+ size_t size;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if ((str = pw_properties_get(module->props, "sink")) != NULL)
+ fprintf(f, " sink.name = \"%s\"", str);
+ if ((str = pw_properties_get(module->props, "sample")) != NULL)
+ fprintf(f, " sample.name = \"%s\"", str);
+ if ((str = pw_properties_get(module->props, "display")) != NULL)
+ fprintf(f, " x11.display = \"%s\"", str);
+ if ((str = pw_properties_get(module->props, "xauthority")) != NULL)
+ fprintf(f, " x11.xauthority = \"%s\"", str);
+ fprintf(f, " }");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-x11-bell",
+ args, NULL);
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+ return 0;
+}
+
+static int module_x11_bell_unload(struct module *module)
+{
+ struct module_x11_bell_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+ return 0;
+}
+
+static int module_x11_bell_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_x11_bell_data * const data = module->user_data;
+ data->module = module;
+
+ return 0;
+}
+
+static const struct spa_dict_item module_x11_bell_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "X11 bell interceptor" },
+ { PW_KEY_MODULE_USAGE, "sink=<sink to connect to> "
+ "sample=<the sample to play> "
+ "display=<X11 display> "
+ "xauthority=<X11 Authority>" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+DEFINE_MODULE_INFO(module_x11_bell) = {
+ .name = "module-x11-bell",
+ .prepare = module_x11_bell_prepare,
+ .load = module_x11_bell_load,
+ .unload = module_x11_bell_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_x11_bell_info),
+ .data_size = sizeof(struct module_x11_bell_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
new file mode 100644
index 0000000..ccdaf27
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c
@@ -0,0 +1,133 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/hook.h>
+#include <pipewire/pipewire.h>
+
+#include "../defs.h"
+#include "../module.h"
+
+#define NAME "zeroconf-discover"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+
+struct module_zeroconf_discover_data {
+ struct module *module;
+
+ struct spa_hook mod_listener;
+ struct pw_impl_module *mod;
+
+ uint32_t latency_msec;
+};
+
+static void module_destroy(void *data)
+{
+ struct module_zeroconf_discover_data *d = data;
+ spa_hook_remove(&d->mod_listener);
+ d->mod = NULL;
+ module_schedule_unload(d->module);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+static int module_zeroconf_discover_load(struct module *module)
+{
+ struct module_zeroconf_discover_data *data = module->user_data;
+ FILE *f;
+ char *args;
+ size_t size;
+
+ if ((f = open_memstream(&args, &size)) == NULL)
+ return -errno;
+
+ fprintf(f, "{");
+ if (data->latency_msec > 0)
+ fprintf(f, " pulse.latency = %u ", data->latency_msec);
+ fprintf(f, "}");
+ fclose(f);
+
+ data->mod = pw_context_load_module(module->impl->context,
+ "libpipewire-module-zeroconf-discover",
+ args, NULL);
+
+ free(args);
+
+ if (data->mod == NULL)
+ return -errno;
+
+ pw_impl_module_add_listener(data->mod,
+ &data->mod_listener,
+ &module_events, data);
+
+ return 0;
+}
+
+static int module_zeroconf_discover_unload(struct module *module)
+{
+ struct module_zeroconf_discover_data *d = module->user_data;
+
+ if (d->mod) {
+ spa_hook_remove(&d->mod_listener);
+ pw_impl_module_destroy(d->mod);
+ d->mod = NULL;
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item module_zeroconf_discover_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.con>" },
+ { PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Discovery" },
+ { PW_KEY_MODULE_USAGE,
+ "latency_msec=<fixed latency in ms> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_zeroconf_discover_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct pw_properties * const props = module->props;
+ struct module_zeroconf_discover_data * const data = module->user_data;
+ data->module = module;
+
+ pw_properties_fetch_uint32(props, "latency_msec", &data->latency_msec);
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_zeroconf_discover) = {
+ .name = "module-zeroconf-discover",
+ .load_once = true,
+ .prepare = module_zeroconf_discover_prepare,
+ .load = module_zeroconf_discover_load,
+ .unload = module_zeroconf_discover_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_zeroconf_discover_info),
+ .data_size = sizeof(struct module_zeroconf_discover_data),
+};
diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
new file mode 100644
index 0000000..2f04867
--- /dev/null
+++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c
@@ -0,0 +1,746 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <sys/utsname.h>
+#include <arpa/inet.h>
+
+#include <pipewire/pipewire.h>
+
+#include "../collect.h"
+#include "../defs.h"
+#include "../manager.h"
+#include "../module.h"
+#include "../pulse-server.h"
+#include "../server.h"
+#include "../../module-zeroconf-discover/avahi-poll.h"
+
+#include <avahi-client/client.h>
+#include <avahi-client/publish.h>
+#include <avahi-common/alternative.h>
+#include <avahi-common/error.h>
+#include <avahi-common/domain.h>
+#include <avahi-common/malloc.h>
+
+#define NAME "zeroconf-publish"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define SERVICE_TYPE_SINK "_pulse-sink._tcp"
+#define SERVICE_TYPE_SOURCE "_pulse-source._tcp"
+#define SERVICE_TYPE_SERVER "_pulse-server._tcp"
+#define SERVICE_SUBTYPE_SINK_HARDWARE "_hardware._sub."SERVICE_TYPE_SINK
+#define SERVICE_SUBTYPE_SINK_VIRTUAL "_virtual._sub."SERVICE_TYPE_SINK
+#define SERVICE_SUBTYPE_SOURCE_HARDWARE "_hardware._sub."SERVICE_TYPE_SOURCE
+#define SERVICE_SUBTYPE_SOURCE_VIRTUAL "_virtual._sub."SERVICE_TYPE_SOURCE
+#define SERVICE_SUBTYPE_SOURCE_MONITOR "_monitor._sub."SERVICE_TYPE_SOURCE
+#define SERVICE_SUBTYPE_SOURCE_NON_MONITOR "_non-monitor._sub."SERVICE_TYPE_SOURCE
+
+#define SERVICE_DATA_ID "module-zeroconf-publish.service"
+
+enum service_subtype {
+ SUBTYPE_HARDWARE,
+ SUBTYPE_VIRTUAL,
+ SUBTYPE_MONITOR
+};
+
+struct service {
+ struct spa_list link;
+
+ struct module_zeroconf_publish_data *userdata;
+
+ AvahiEntryGroup *entry_group;
+ AvahiStringList *txt;
+ struct server *server;
+
+ const char *service_type;
+ enum service_subtype subtype;
+
+ char *name;
+ bool is_sink;
+
+ struct sample_spec ss;
+ struct channel_map cm;
+ struct pw_properties *props;
+
+ char service_name[AVAHI_LABEL_MAX];
+ unsigned published:1;
+};
+
+struct module_zeroconf_publish_data {
+ struct module *module;
+
+ struct pw_core *core;
+ struct pw_manager *manager;
+
+ struct spa_hook core_listener;
+ struct spa_hook manager_listener;
+ struct spa_hook impl_listener;
+
+ AvahiPoll *avahi_poll;
+ AvahiClient *client;
+
+ /* lists of services */
+ struct spa_list pending;
+ struct spa_list published;
+};
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct module_zeroconf_publish_data *d = data;
+ struct module *module = d->module;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ module_schedule_unload(module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void get_service_name(struct pw_manager_object *o, char *buf, size_t length)
+{
+ const char *hn, *un, *n;
+
+ hn = pw_get_host_name();
+ un = pw_get_user_name();
+ n = pw_properties_get(o->props, PW_KEY_NODE_DESCRIPTION);
+
+ snprintf(buf, length, "%s@%s: %s", un, hn, n);
+}
+
+static void service_free(struct service *s)
+{
+ pw_log_debug("service %p: free", s);
+
+ if (s->entry_group)
+ avahi_entry_group_free(s->entry_group);
+
+ if (s->name)
+ free(s->name);
+
+ pw_properties_free(s->props);
+ avahi_string_list_free(s->txt);
+ spa_list_remove(&s->link);
+}
+
+static void unpublish_service(struct service *s)
+{
+ spa_list_remove(&s->link);
+ spa_list_append(&s->userdata->pending, &s->link);
+ s->published = false;
+ s->server = NULL;
+}
+
+static void unpublish_all_services(struct module_zeroconf_publish_data *d)
+{
+ struct service *s;
+
+ spa_list_consume(s, &d->published, link)
+ unpublish_service(s);
+}
+
+static char* channel_map_snprint(char *s, size_t l, const struct channel_map *map)
+{
+ unsigned channel;
+ bool first = true;
+ char *e;
+ uint32_t aux = 0;
+
+ spa_assert(s);
+ spa_assert(l > 0);
+ spa_assert(map);
+
+ if (!channel_map_valid(map)) {
+ snprintf(s, l, "(invalid)");
+ return s;
+ }
+
+ *(e = s) = 0;
+
+ for (channel = 0; channel < map->channels && l > 1; channel++) {
+ l -= spa_scnprintf(e, l, "%s%s",
+ first ? "" : ",",
+ channel_id2paname(map->map[channel], &aux));
+
+ e = strchr(e, 0);
+ first = false;
+ }
+
+ return s;
+}
+
+static void fill_service_data(struct module_zeroconf_publish_data *d, struct service *s,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = d->module->impl;
+ bool is_sink = pw_manager_object_is_sink(o);
+ bool is_source = pw_manager_object_is_source(o);
+ struct pw_node_info *info = o->info;
+ const char *name, *desc, *str;
+ uint32_t card_id = SPA_ID_INVALID;
+ struct pw_manager *manager = d->manager;
+ struct pw_manager_object *card = NULL;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct device_info dev_info = is_sink ?
+ DEVICE_INFO_INIT(PW_DIRECTION_OUTPUT) : DEVICE_INFO_INIT(PW_DIRECTION_INPUT);
+ uint32_t flags = 0;
+
+ if (info == NULL || info->props == NULL)
+ return;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if ((desc = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)) == NULL)
+ desc = name ? name : "Unknown";
+ if (name == NULL)
+ name = "unknown";
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ if (card)
+ collect_card_info(card, &card_info);
+
+ collect_device_info(o, card, &dev_info, false, &impl->defs);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_API)) != NULL) {
+ if (is_sink)
+ flags |= SINK_HARDWARE;
+ else if (is_source)
+ flags |= SOURCE_HARDWARE;
+ }
+
+ s->ss = dev_info.ss;
+ s->cm = dev_info.map;
+ s->name = strdup(name);
+ s->props = pw_properties_copy(o->props);
+
+ if (is_sink) {
+ s->is_sink = true;
+ s->service_type = SERVICE_TYPE_SINK;
+ s->subtype = flags & SINK_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL;
+ } else if (is_source) {
+ s->is_sink = false;
+ s->service_type = SERVICE_TYPE_SOURCE;
+ s->subtype = flags & SOURCE_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL;
+ } else
+ spa_assert_not_reached();
+}
+
+static struct service *create_service(struct module_zeroconf_publish_data *d, struct pw_manager_object *o)
+{
+ struct service *s;
+
+ s = pw_manager_object_add_data(o, SERVICE_DATA_ID, sizeof(*s));
+ if (s == NULL)
+ return NULL;
+
+ s->userdata = d;
+ s->entry_group = NULL;
+ get_service_name(o, s->service_name, sizeof(s->service_name));
+ spa_list_append(&d->pending, &s->link);
+
+ fill_service_data(d, s, o);
+
+ pw_log_debug("service %p: created for object %p", s, o);
+
+ return s;
+}
+
+static AvahiStringList* txt_record_server_data(struct pw_core_info *info, AvahiStringList *l)
+{
+ const char *t;
+ struct utsname u;
+
+ spa_assert(info);
+
+ l = avahi_string_list_add_pair(l, "server-version", PACKAGE_NAME" "PACKAGE_VERSION);
+
+ t = pw_get_user_name();
+ l = avahi_string_list_add_pair(l, "user-name", t);
+
+ if (uname(&u) >= 0) {
+ char sysname[sizeof(u.sysname) + sizeof(u.machine) + sizeof(u.release)];
+
+ snprintf(sysname, sizeof(sysname), "%s %s %s", u.sysname, u.machine, u.release);
+ l = avahi_string_list_add_pair(l, "uname", sysname);
+ }
+
+ t = pw_get_host_name();
+ l = avahi_string_list_add_pair(l, "fqdn", t);
+ l = avahi_string_list_add_printf(l, "cookie=0x%08x", info->cookie);
+
+ return l;
+}
+
+static void clear_entry_group(struct service *s)
+{
+ if (s->entry_group == NULL)
+ return;
+
+ avahi_entry_group_free(s->entry_group);
+ s->entry_group = NULL;
+}
+
+static void publish_service(struct service *s);
+
+static void service_entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata)
+{
+ struct service *s = userdata;
+
+ spa_assert(s);
+ if (!s->published) {
+ pw_log_info("cancel unpublished service: %s", s->service_name);
+ clear_entry_group(s);
+ return;
+ }
+
+ switch (state) {
+ case AVAHI_ENTRY_GROUP_ESTABLISHED:
+ pw_log_info("established service: %s", s->service_name);
+ break;
+ case AVAHI_ENTRY_GROUP_COLLISION:
+ {
+ char *t;
+
+ t = avahi_alternative_service_name(s->service_name);
+ pw_log_info("service name collision: renaming '%s' to '%s'", s->service_name, t);
+ snprintf(s->service_name, sizeof(s->service_name), "%s", t);
+ avahi_free(t);
+
+ unpublish_service(s);
+ publish_service(s);
+ break;
+ }
+ case AVAHI_ENTRY_GROUP_FAILURE:
+ pw_log_error("failed to establish service '%s': %s",
+ s->service_name,
+ avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
+ unpublish_service(s);
+ clear_entry_group(s);
+ break;
+
+ case AVAHI_ENTRY_GROUP_UNCOMMITED:
+ case AVAHI_ENTRY_GROUP_REGISTERING:
+ break;
+ }
+}
+
+#define PA_CHANNEL_MAP_SNPRINT_MAX (CHANNELS_MAX * 32)
+
+static AvahiStringList *get_service_txt(const struct service *s)
+{
+ static const char * const subtype_text[] = {
+ [SUBTYPE_HARDWARE] = "hardware",
+ [SUBTYPE_VIRTUAL] = "virtual",
+ [SUBTYPE_MONITOR] = "monitor"
+ };
+
+ static const struct mapping {
+ const char *pw_key, *txt_key;
+ } mappings[] = {
+ { PW_KEY_NODE_DESCRIPTION, "description" },
+ { PW_KEY_DEVICE_VENDOR_NAME, "vendor-name" },
+ { PW_KEY_DEVICE_PRODUCT_NAME, "product-name" },
+ { PW_KEY_DEVICE_CLASS, "class" },
+ { PW_KEY_DEVICE_FORM_FACTOR, "form-factor" },
+ { PW_KEY_DEVICE_ICON_NAME, "icon-name" },
+ };
+
+ char cm[PA_CHANNEL_MAP_SNPRINT_MAX];
+ AvahiStringList *txt = NULL;
+
+ txt = txt_record_server_data(s->userdata->manager->info, txt);
+
+ txt = avahi_string_list_add_pair(txt, "device", s->name);
+ txt = avahi_string_list_add_printf(txt, "rate=%u", s->ss.rate);
+ txt = avahi_string_list_add_printf(txt, "channels=%u", s->ss.channels);
+ txt = avahi_string_list_add_pair(txt, "format", format_id2paname(s->ss.format));
+ txt = avahi_string_list_add_pair(txt, "channel_map", channel_map_snprint(cm, sizeof(cm), &s->cm));
+ txt = avahi_string_list_add_pair(txt, "subtype", subtype_text[s->subtype]);
+
+ SPA_FOR_EACH_ELEMENT_VAR(mappings, m) {
+ const char *value = pw_properties_get(s->props, m->pw_key);
+ if (value != NULL)
+ txt = avahi_string_list_add_pair(txt, m->txt_key, value);
+ }
+
+ return txt;
+}
+
+static struct server *find_server(struct service *s, int *proto, uint16_t *port)
+{
+ struct module_zeroconf_publish_data *d = s->userdata;
+ struct impl *impl = d->module->impl;
+ struct server *server;
+
+ spa_list_for_each(server, &impl->servers, link) {
+ if (server->addr.ss_family == AF_INET) {
+ *proto = AVAHI_PROTO_INET;
+ *port = ntohs(((struct sockaddr_in*) &server->addr)->sin_port);
+ return server;
+ } else if (server->addr.ss_family == AF_INET6) {
+ *proto = AVAHI_PROTO_INET6;
+ *port = ntohs(((struct sockaddr_in6*) &server->addr)->sin6_port);
+ return server;
+ }
+ }
+
+ return NULL;
+}
+
+static void publish_service(struct service *s)
+{
+ struct module_zeroconf_publish_data *d = s->userdata;
+ int proto;
+ uint16_t port;
+
+ struct server *server = find_server(s, &proto, &port);
+ if (!server)
+ return;
+
+ pw_log_debug("found server:%p proto:%d port:%d", server, proto, port);
+
+ if (!d->client || avahi_client_get_state(d->client) != AVAHI_CLIENT_S_RUNNING)
+ return;
+
+ s->published = true;
+ if (!s->entry_group) {
+ s->entry_group = avahi_entry_group_new(d->client, service_entry_group_callback, s);
+ if (s->entry_group == NULL) {
+ pw_log_error("avahi_entry_group_new(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+ } else {
+ avahi_entry_group_reset(s->entry_group);
+ }
+
+ if (s->txt == NULL)
+ s->txt = get_service_txt(s);
+
+ if (avahi_entry_group_add_service_strlst(
+ s->entry_group,
+ AVAHI_IF_UNSPEC, proto,
+ 0,
+ s->service_name,
+ s->service_type,
+ NULL,
+ NULL,
+ port,
+ s->txt) < 0) {
+ pw_log_error("avahi_entry_group_add_service_strlst(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+
+ if (avahi_entry_group_add_service_subtype(
+ s->entry_group,
+ AVAHI_IF_UNSPEC, proto,
+ 0,
+ s->service_name,
+ s->service_type,
+ NULL,
+ s->is_sink ? (s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SINK_HARDWARE : SERVICE_SUBTYPE_SINK_VIRTUAL) :
+ (s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SOURCE_HARDWARE : (s->subtype == SUBTYPE_VIRTUAL ? SERVICE_SUBTYPE_SOURCE_VIRTUAL : SERVICE_SUBTYPE_SOURCE_MONITOR))) < 0) {
+
+ pw_log_error("avahi_entry_group_add_service_subtype(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+
+ if (!s->is_sink && s->subtype != SUBTYPE_MONITOR) {
+ if (avahi_entry_group_add_service_subtype(
+ s->entry_group,
+ AVAHI_IF_UNSPEC, proto,
+ 0,
+ s->service_name,
+ SERVICE_TYPE_SOURCE,
+ NULL,
+ SERVICE_SUBTYPE_SOURCE_NON_MONITOR) < 0) {
+ pw_log_error("avahi_entry_group_add_service_subtype(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+ }
+
+ if (avahi_entry_group_commit(s->entry_group) < 0) {
+ pw_log_error("avahi_entry_group_commit(): %s",
+ avahi_strerror(avahi_client_errno(d->client)));
+ goto error;
+ }
+
+ spa_list_remove(&s->link);
+ spa_list_append(&d->published, &s->link);
+ s->server = server;
+
+ pw_log_info("created service: %s", s->service_name);
+ return;
+
+error:
+ s->published = false;
+ return;
+}
+
+static void publish_pending(struct module_zeroconf_publish_data *data)
+{
+ struct service *s, *next;
+
+ spa_list_for_each_safe(s, next, &data->pending, link)
+ publish_service(s);
+}
+
+static void clear_pending_entry_groups(struct module_zeroconf_publish_data *data)
+{
+ struct service *s;
+
+ spa_list_for_each(s, &data->pending, link)
+ clear_entry_group(s);
+}
+
+static void client_callback(AvahiClient *c, AvahiClientState state, void *d)
+{
+ struct module_zeroconf_publish_data *data = d;
+
+ spa_assert(c);
+ spa_assert(data);
+
+ data->client = c;
+
+ switch (state) {
+ case AVAHI_CLIENT_S_RUNNING:
+ pw_log_info("the avahi daemon is up and running");
+ publish_pending(data);
+ break;
+ case AVAHI_CLIENT_S_COLLISION:
+ pw_log_error("host name collision");
+ unpublish_all_services(d);
+ break;
+ case AVAHI_CLIENT_FAILURE:
+ {
+ int err = avahi_client_errno(data->client);
+
+ pw_log_error("avahi client failure: %s", avahi_strerror(err));
+
+ unpublish_all_services(data);
+ clear_pending_entry_groups(data);
+ avahi_client_free(data->client);
+ data->client = NULL;
+
+ if (err == AVAHI_ERR_DISCONNECTED) {
+ data->client = avahi_client_new(data->avahi_poll, AVAHI_CLIENT_NO_FAIL, client_callback, data, &err);
+ if (data->client == NULL)
+ pw_log_error("failed to create avahi client: %s", avahi_strerror(err));
+ }
+
+ if (data->client == NULL)
+ module_schedule_unload(data->module);
+
+ break;
+ }
+ case AVAHI_CLIENT_CONNECTING:
+ pw_log_info("connecting to the avahi daemon...");
+ break;
+ default:
+ break;
+ }
+}
+
+static void manager_removed(void *d, struct pw_manager_object *o)
+{
+ if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source(o))
+ return;
+
+ struct service *s = pw_manager_object_get_data(o, SERVICE_DATA_ID);
+ if (s == NULL)
+ return;
+
+ service_free(s);
+}
+
+static void manager_added(void *d, struct pw_manager_object *o)
+{
+ struct service *s;
+ struct pw_node_info *info;
+ const char *str;
+
+ if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source(o))
+ return;
+
+ info = o->info;
+ if (info == NULL || info->props == NULL)
+ return;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_NODE_NETWORK)) != NULL &&
+ spa_atob(str))
+ return;
+
+ s = create_service(d, o);
+ if (s == NULL)
+ return;
+
+ publish_service(s);
+}
+
+static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .added = manager_added,
+ .removed = manager_removed,
+};
+
+
+static void impl_server_started(void *data, struct server *server)
+{
+ struct module_zeroconf_publish_data *d = data;
+ pw_log_info("a new server is started, try publish");
+ publish_pending(d);
+}
+
+static void impl_server_stopped(void *data, struct server *server)
+{
+ struct module_zeroconf_publish_data *d = data;
+ pw_log_info("a server stopped, try republish");
+
+ struct service *s, *tmp;
+ spa_list_for_each_safe(s, tmp, &d->published, link) {
+ if (s->server == server)
+ unpublish_service(s);
+ }
+
+ publish_pending(d);
+}
+
+static const struct impl_events impl_events = {
+ VERSION_IMPL_EVENTS,
+ .server_started = impl_server_started,
+ .server_stopped = impl_server_stopped,
+};
+
+static int module_zeroconf_publish_load(struct module *module)
+{
+ struct module_zeroconf_publish_data *data = module->user_data;
+ struct pw_loop *loop;
+ int error;
+
+ data->core = pw_context_connect(module->impl->context, NULL, 0);
+ if (data->core == NULL) {
+ pw_log_error("failed to connect to pipewire: %m");
+ return -errno;
+ }
+
+ pw_core_add_listener(data->core,
+ &data->core_listener,
+ &core_events, data);
+
+ loop = pw_context_get_main_loop(module->impl->context);
+ data->avahi_poll = pw_avahi_poll_new(loop);
+
+ data->client = avahi_client_new(data->avahi_poll, AVAHI_CLIENT_NO_FAIL,
+ client_callback, data, &error);
+ if (!data->client) {
+ pw_log_error("failed to create avahi client: %s", avahi_strerror(error));
+ return -errno;
+ }
+
+ data->manager = pw_manager_new(data->core);
+ if (data->manager == NULL) {
+ pw_log_error("failed to create pipewire manager: %m");
+ return -errno;
+ }
+
+ pw_manager_add_listener(data->manager, &data->manager_listener,
+ &manager_events, data);
+
+ impl_add_listener(module->impl, &data->impl_listener, &impl_events, data);
+
+ return 0;
+}
+
+static int module_zeroconf_publish_unload(struct module *module)
+{
+ struct module_zeroconf_publish_data *d = module->user_data;
+ struct service *s;
+
+ spa_hook_remove(&d->impl_listener);
+
+ unpublish_all_services(d);
+
+ spa_list_consume(s, &d->pending, link)
+ service_free(s);
+
+ if (d->client)
+ avahi_client_free(d->client);
+
+ if (d->avahi_poll)
+ pw_avahi_poll_free(d->avahi_poll);
+
+ if (d->manager != NULL) {
+ spa_hook_remove(&d->manager_listener);
+ pw_manager_destroy(d->manager);
+ }
+
+ if (d->core != NULL) {
+ spa_hook_remove(&d->core_listener);
+ pw_core_disconnect(d->core);
+ }
+
+ return 0;
+}
+
+static const struct spa_dict_item module_zeroconf_publish_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io" },
+ { PW_KEY_MODULE_DESCRIPTION, "mDNS/DNS-SD Service Publish" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static int module_zeroconf_publish_prepare(struct module * const module)
+{
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ struct module_zeroconf_publish_data * const data = module->user_data;
+ data->module = module;
+ spa_list_init(&data->pending);
+ spa_list_init(&data->published);
+
+ return 0;
+}
+
+DEFINE_MODULE_INFO(module_zeroconf_publish) = {
+ .name = "module-zeroconf-publish",
+ .prepare = module_zeroconf_publish_prepare,
+ .load = module_zeroconf_publish_load,
+ .unload = module_zeroconf_publish_unload,
+ .properties = &SPA_DICT_INIT_ARRAY(module_zeroconf_publish_info),
+ .data_size = sizeof(struct module_zeroconf_publish_data),
+};
diff --git a/src/modules/module-protocol-pulse/operation.c b/src/modules/module-protocol-pulse/operation.c
new file mode 100644
index 0000000..1c9833d
--- /dev/null
+++ b/src/modules/module-protocol-pulse/operation.c
@@ -0,0 +1,92 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+
+#include <spa/utils/list.h>
+#include <pipewire/log.h>
+
+#include "client.h"
+#include "log.h"
+#include "manager.h"
+#include "operation.h"
+#include "reply.h"
+
+int operation_new_cb(struct client *client, uint32_t tag,
+ void (*callback)(void *data, struct client *client, uint32_t tag),
+ void *data)
+{
+ struct operation *o;
+
+ if ((o = calloc(1, sizeof(*o))) == NULL)
+ return -errno;
+
+ o->client = client;
+ o->tag = tag;
+ o->callback = callback;
+ o->data = data;
+
+ spa_list_append(&client->operations, &o->link);
+ pw_manager_sync(client->manager);
+
+ pw_log_debug("client %p [%s]: new operation tag:%u", client, client->name, tag);
+
+ return 0;
+}
+
+int operation_new(struct client *client, uint32_t tag)
+{
+ return operation_new_cb(client, tag, NULL, NULL);
+}
+
+void operation_free(struct operation *o)
+{
+ spa_list_remove(&o->link);
+ free(o);
+}
+
+struct operation *operation_find(struct client *client, uint32_t tag)
+{
+ struct operation *o;
+ spa_list_for_each(o, &client->operations, link) {
+ if (o->tag == tag)
+ return o;
+ }
+ return NULL;
+}
+
+void operation_complete(struct operation *o)
+{
+ struct client *client = o->client;
+
+ pw_log_info("[%s]: tag:%u complete", client->name, o->tag);
+
+ spa_list_remove(&o->link);
+
+ if (o->callback)
+ o->callback(o->data, client, o->tag);
+ else
+ reply_simple_ack(client, o->tag);
+ free(o);
+}
diff --git a/src/modules/module-protocol-pulse/operation.h b/src/modules/module-protocol-pulse/operation.h
new file mode 100644
index 0000000..1fa07cc
--- /dev/null
+++ b/src/modules/module-protocol-pulse/operation.h
@@ -0,0 +1,50 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_OPERATION_H
+#define PULSER_SERVER_OPERATION_H
+
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+
+struct client;
+
+struct operation {
+ struct spa_list link;
+ struct client *client;
+ uint32_t tag;
+ void (*callback) (void *data, struct client *client, uint32_t tag);
+ void *data;
+};
+
+int operation_new(struct client *client, uint32_t tag);
+int operation_new_cb(struct client *client, uint32_t tag,
+ void (*callback) (void *data, struct client *client, uint32_t tag),
+ void *data);
+struct operation *operation_find(struct client *client, uint32_t tag);
+void operation_free(struct operation *o);
+void operation_complete(struct operation *o);
+
+#endif /* PULSER_SERVER_OPERATION_H */
diff --git a/src/modules/module-protocol-pulse/pending-sample.c b/src/modules/module-protocol-pulse/pending-sample.c
new file mode 100644
index 0000000..399fc3b
--- /dev/null
+++ b/src/modules/module-protocol-pulse/pending-sample.c
@@ -0,0 +1,50 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+#include <pipewire/work-queue.h>
+
+#include "client.h"
+#include "internal.h"
+#include "log.h"
+#include "operation.h"
+#include "pending-sample.h"
+#include "sample-play.h"
+
+void pending_sample_free(struct pending_sample *ps)
+{
+ struct client * const client = ps->client;
+ struct impl * const impl = client->impl;
+ struct operation *o;
+
+ spa_list_remove(&ps->link);
+ spa_hook_remove(&ps->listener);
+ pw_work_queue_cancel(impl->work_queue, ps, SPA_ID_INVALID);
+
+ if ((o = operation_find(client, ps->tag)) != NULL)
+ operation_free(o);
+
+ sample_play_destroy(ps->play);
+}
diff --git a/src/modules/module-protocol-pulse/pending-sample.h b/src/modules/module-protocol-pulse/pending-sample.h
new file mode 100644
index 0000000..f467ca7
--- /dev/null
+++ b/src/modules/module-protocol-pulse/pending-sample.h
@@ -0,0 +1,48 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_PENDING_SAMPLE_H
+#define PULSE_SERVER_PENDING_SAMPLE_H
+
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+
+struct client;
+struct sample_play;
+
+struct pending_sample {
+ struct spa_list link;
+ struct client *client;
+ struct sample_play *play;
+ struct spa_hook listener;
+ uint32_t tag;
+ unsigned ready:1;
+ unsigned done:1;
+};
+
+void pending_sample_free(struct pending_sample *ps);
+
+#endif /* PULSE_SERVER_PENDING_SAMPLE_H */
diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c
new file mode 100644
index 0000000..41a814a
--- /dev/null
+++ b/src/modules/module-protocol-pulse/pulse-server.c
@@ -0,0 +1,5689 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <sys/time.h>
+
+#include <pipewire/log.h>
+
+#include "log.h"
+
+#include <spa/support/cpu.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/debug/dict.h>
+#include <spa/debug/mem.h>
+#include <spa/debug/types.h>
+#include <spa/param/audio/raw.h>
+#include <spa/pod/pod.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/props.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/metadata.h>
+
+#include "pulse-server.h"
+#include "client.h"
+#include "collect.h"
+#include "commands.h"
+#include "cmd.h"
+#include "dbus-name.h"
+#include "defs.h"
+#include "extension.h"
+#include "format.h"
+#include "internal.h"
+#include "manager.h"
+#include "message.h"
+#include "message-handler.h"
+#include "module.h"
+#include "operation.h"
+#include "pending-sample.h"
+#include "quirks.h"
+#include "reply.h"
+#include "sample.h"
+#include "sample-play.h"
+#include "server.h"
+#include "stream.h"
+#include "utils.h"
+#include "volume.h"
+
+#define DEFAULT_MIN_REQ "256/48000"
+#define DEFAULT_DEFAULT_REQ "960/48000"
+#define DEFAULT_MIN_FRAG "256/48000"
+#define DEFAULT_DEFAULT_FRAG "96000/48000"
+#define DEFAULT_DEFAULT_TLENGTH "96000/48000"
+#define DEFAULT_MIN_QUANTUM "256/48000"
+#define DEFAULT_FORMAT "F32"
+#define DEFAULT_POSITION "[ FL FR ]"
+#define DEFAULT_IDLE_TIMEOUT "0"
+
+#define MAX_FORMATS 32
+/* The max amount of data we send in one block when capturing. In PulseAudio this
+ * size is derived from the mempool PA_MEMPOOL_SLOT_SIZE */
+#define MAX_BLOCK (64*1024)
+
+#define TEMPORARY_MOVE_TIMEOUT (SPA_NSEC_PER_SEC)
+
+PW_LOG_TOPIC_EXTERN(pulse_conn);
+
+bool debug_messages = false;
+
+struct latency_offset_data {
+ int64_t prev_latency_offset;
+ uint8_t initialized:1;
+};
+
+struct temporary_move_data {
+ uint32_t peer_index;
+ uint8_t used:1;
+};
+
+static struct sample *find_sample(struct impl *impl, uint32_t index, const char *name)
+{
+ union pw_map_item *item;
+
+ if (index != SPA_ID_INVALID)
+ return pw_map_lookup(&impl->samples, index);
+
+ pw_array_for_each(item, &impl->samples.items) {
+ struct sample *s = item->data;
+ if (!pw_map_item_is_free(item) &&
+ spa_streq(s->name, name))
+ return s;
+ }
+ return NULL;
+}
+
+void broadcast_subscribe_event(struct impl *impl, uint32_t mask, uint32_t event, 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);
+ }
+}
+
+static int do_command_auth(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+ uint32_t version;
+ const void *cookie;
+ size_t len;
+
+ if (message_get(m,
+ TAG_U32, &version,
+ TAG_ARBITRARY, &cookie, &len,
+ TAG_INVALID) < 0) {
+ return -EPROTO;
+ }
+ if (version < 8)
+ return -EPROTO;
+ if (len != NATIVE_COOKIE_LENGTH)
+ return -EINVAL;
+
+ if ((version & PROTOCOL_VERSION_MASK) >= 13)
+ version &= PROTOCOL_VERSION_MASK;
+
+ client->version = version;
+ client->authenticated = true;
+
+ pw_log_info("client:%p AUTH tag:%u version:%d", client, tag, version);
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, PROTOCOL_VERSION,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int reply_set_client_name(struct client *client, uint32_t tag)
+{
+ struct pw_manager *manager = client->manager;
+ struct message *reply;
+ struct pw_client *c;
+ uint32_t id, index;
+
+ c = pw_core_get_client(client->core);
+ if (c == NULL)
+ return -ENOENT;
+
+ id = pw_proxy_get_bound_id((struct pw_proxy*)c);
+ index = id_to_index(manager, id);
+
+ pw_log_info("[%s] reply tag:%u id:%u index:%u", client->name, tag, id, index);
+
+ reply = reply_new(client, tag);
+
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_U32, index, /* client index */
+ TAG_INVALID);
+ }
+ return client_queue_message(client, reply);
+}
+
+static void manager_sync(void *data)
+{
+ struct client *client = data;
+ struct operation *o;
+
+ pw_log_debug("%p: manager sync", client);
+
+ if (client->connect_tag != SPA_ID_INVALID) {
+ reply_set_client_name(client, client->connect_tag);
+ client->connect_tag = SPA_ID_INVALID;
+ }
+
+ client->ref++;
+ spa_list_consume(o, &client->operations, link)
+ operation_complete(o);
+ client_unref(client);
+}
+
+static struct stream *find_stream(struct client *client, uint32_t index)
+{
+ union pw_map_item *item;
+ pw_array_for_each(item, &client->streams.items) {
+ struct stream *s = item->data;
+ if (!pw_map_item_is_free(item) &&
+ s->index == index)
+ return s;
+ }
+ return NULL;
+}
+
+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;
+
+ if (pw_manager_object_is_sink(o)) {
+ client_queue_subscribe_event(client,
+ SUBSCRIPTION_MASK_SINK,
+ SUBSCRIPTION_EVENT_SINK | type,
+ res_index);
+ }
+ if (pw_manager_object_is_source_or_monitor(o)) {
+ mask = SUBSCRIPTION_MASK_SOURCE;
+ event = SUBSCRIPTION_EVENT_SOURCE;
+ }
+ else if (pw_manager_object_is_sink_input(o)) {
+ mask = SUBSCRIPTION_MASK_SINK_INPUT;
+ event = SUBSCRIPTION_EVENT_SINK_INPUT;
+ }
+ else if (pw_manager_object_is_source_output(o)) {
+ mask = SUBSCRIPTION_MASK_SOURCE_OUTPUT;
+ event = SUBSCRIPTION_EVENT_SOURCE_OUTPUT;
+ }
+ else if (pw_manager_object_is_module(o)) {
+ mask = SUBSCRIPTION_MASK_MODULE;
+ event = SUBSCRIPTION_EVENT_MODULE;
+ }
+ else if (pw_manager_object_is_client(o)) {
+ mask = SUBSCRIPTION_MASK_CLIENT;
+ event = SUBSCRIPTION_EVENT_CLIENT;
+ }
+ else if (pw_manager_object_is_card(o)) {
+ mask = SUBSCRIPTION_MASK_CARD;
+ event = SUBSCRIPTION_EVENT_CARD;
+ } else
+ event = SPA_ID_INVALID;
+
+ if (event != SPA_ID_INVALID)
+ client_queue_subscribe_event(client,
+ mask,
+ event | type,
+ res_index);
+ return 0;
+}
+
+static uint32_t get_temporary_move_target(struct client *client, struct pw_manager_object *o)
+{
+ struct temporary_move_data *d;
+
+ d = pw_manager_object_get_data(o, "temporary_move_data");
+ if (d == NULL || d->peer_index == SPA_ID_INVALID)
+ return SPA_ID_INVALID;
+
+ pw_log_debug("[%s] using temporary move target for index:%d -> index:%d",
+ client->name, o->index, d->peer_index);
+ d->used = true;
+ return d->peer_index;
+}
+
+static void set_temporary_move_target(struct client *client, struct pw_manager_object *o, uint32_t index)
+{
+ struct temporary_move_data *d;
+
+ if (!pw_manager_object_is_sink_input(o) && !pw_manager_object_is_source_output(o))
+ return;
+
+ if (index == SPA_ID_INVALID) {
+ d = pw_manager_object_get_data(o, "temporary_move_data");
+ if (d == NULL)
+ return;
+ if (d->peer_index != SPA_ID_INVALID)
+ pw_log_debug("cleared temporary move target for index:%d", o->index);
+ d->peer_index = SPA_ID_INVALID;
+ d->used = false;
+ return;
+ }
+
+ d = pw_manager_object_add_temporary_data(o, "temporary_move_data",
+ sizeof(struct temporary_move_data),
+ TEMPORARY_MOVE_TIMEOUT);
+ if (d == NULL)
+ return;
+
+ pw_log_debug("[%s] set temporary move target for index:%d to index:%d",
+ client->name, o->index, index);
+ d->peer_index = index;
+ d->used = false;
+}
+
+static void temporary_move_target_timeout(struct client *client, struct pw_manager_object *o)
+{
+ struct temporary_move_data *d = pw_manager_object_get_data(o, "temporary_move_data");
+ struct pw_manager_object *peer;
+
+ /*
+ * Send change event if the temporary data was used, and the peer
+ * is not what we claimed.
+ */
+
+ if (d == NULL || d->peer_index == SPA_ID_INVALID || !d->used)
+ goto done;
+
+ peer = find_linked(client->manager, o->id, pw_manager_object_is_sink_input(o) ?
+ PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT);
+ if (peer == NULL || peer->index != d->peer_index) {
+ pw_log_debug("[%s] temporary move timeout for index:%d, send change event",
+ client->name, o->index);
+ send_object_event(client, o, SUBSCRIPTION_EVENT_CHANGE);
+ }
+
+done:
+ set_temporary_move_target(client, o, SPA_ID_INVALID);
+}
+
+static struct pw_manager_object *find_device(struct client *client,
+ uint32_t index, const char *name, bool sink, bool *is_monitor);
+
+static int64_t get_node_latency_offset(struct pw_manager_object *o)
+{
+ int64_t latency_offset = 0LL;
+ struct pw_manager_param *p;
+
+ spa_list_for_each(p, &o->param_list, link) {
+ if (p->id != SPA_PARAM_Props)
+ continue;
+ if (spa_pod_parse_object(p->param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(&latency_offset)) == 1)
+ break;
+ }
+ return latency_offset;
+}
+
+static void send_latency_offset_subscribe_event(struct client *client, struct pw_manager_object *o)
+{
+ struct pw_manager *manager = client->manager;
+ struct latency_offset_data *d;
+ struct pw_node_info *info;
+ const char *str;
+ uint32_t card_id = SPA_ID_INVALID;
+ int64_t latency_offset = 0LL;
+ bool changed = false;
+
+ if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source_or_monitor(o))
+ return;
+
+ /*
+ * Pulseaudio sends card change events on latency offset change.
+ */
+ if ((info = o->info) == NULL || info->props == NULL)
+ return;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if (card_id == SPA_ID_INVALID)
+ return;
+
+ d = pw_manager_object_add_data(o, "latency_offset_data", sizeof(struct latency_offset_data));
+ if (d == NULL)
+ return;
+
+ latency_offset = get_node_latency_offset(o);
+ changed = (!d->initialized || latency_offset != d->prev_latency_offset);
+
+ d->prev_latency_offset = latency_offset;
+ d->initialized = true;
+
+ if (changed)
+ client_queue_subscribe_event(client,
+ SUBSCRIPTION_MASK_CARD,
+ SUBSCRIPTION_EVENT_CARD | SUBSCRIPTION_EVENT_CHANGE,
+ id_to_index(manager, card_id));
+}
+
+static void send_default_change_subscribe_event(struct client *client, bool sink, bool source)
+{
+ struct pw_manager_object *def;
+ bool changed = false;
+
+ if (sink) {
+ def = find_device(client, SPA_ID_INVALID, NULL, true, NULL);
+ if (client->prev_default_sink != def) {
+ client->prev_default_sink = def;
+ changed = true;
+ }
+ }
+
+ if (source) {
+ def = find_device(client, SPA_ID_INVALID, NULL, false, NULL);
+ if (client->prev_default_source != def) {
+ client->prev_default_source = def;
+ changed = true;
+ }
+ }
+
+ if (changed)
+ client_queue_subscribe_event(client,
+ SUBSCRIPTION_MASK_SERVER,
+ SUBSCRIPTION_EVENT_CHANGE |
+ SUBSCRIPTION_EVENT_SERVER,
+ -1);
+}
+
+static void handle_metadata(struct client *client, struct pw_manager_object *old,
+ struct pw_manager_object *new, const char *name)
+{
+ if (spa_streq(name, "default")) {
+ if (client->metadata_default == old)
+ client->metadata_default = new;
+ }
+ else if (spa_streq(name, "route-settings")) {
+ if (client->metadata_routes == old)
+ client->metadata_routes = new;
+ }
+}
+
+static uint32_t frac_to_bytes_round_up(struct spa_fraction val, const struct sample_spec *ss)
+{
+ uint64_t u;
+ u = (uint64_t) (val.num * 1000000UL * (uint64_t) ss->rate) / val.denom;
+ u = (u + 1000000UL - 1) / 1000000UL;
+ u *= sample_spec_frame_size(ss);
+ return (uint32_t) u;
+}
+
+static void clamp_latency(struct stream *s, struct spa_fraction *lat)
+{
+ if (lat->num * s->min_quantum.denom / lat->denom < s->min_quantum.num)
+ lat->num = (s->min_quantum.num * lat->denom +
+ (s->min_quantum.denom -1)) / s->min_quantum.denom;
+}
+
+static uint64_t fix_playback_buffer_attr(struct stream *s, struct buffer_attr *attr,
+ uint32_t rate, struct spa_fraction *lat)
+{
+ uint32_t frame_size, max_prebuf, minreq, latency, max_latency, maxlength;
+ struct defs *defs = &s->impl->defs;
+
+ if ((frame_size = s->frame_size) == 0)
+ frame_size = sample_spec_frame_size(&s->ss);
+ if (frame_size == 0)
+ frame_size = 4;
+
+ maxlength = SPA_ROUND_DOWN(MAXLENGTH, frame_size);
+
+ pw_log_info("[%s] maxlength:%u tlength:%u minreq:%u prebuf:%u max:%u",
+ s->client->name, attr->maxlength, attr->tlength,
+ attr->minreq, attr->prebuf, maxlength);
+
+ minreq = frac_to_bytes_round_up(s->min_req, &s->ss);
+ max_latency = defs->quantum_limit * frame_size;
+
+ if (attr->maxlength == (uint32_t) -1 || attr->maxlength > maxlength)
+ attr->maxlength = maxlength;
+ else
+ attr->maxlength = SPA_ROUND_DOWN(attr->maxlength, frame_size);
+
+ minreq = SPA_MIN(minreq, attr->maxlength);
+
+ if (attr->tlength == (uint32_t) -1)
+ attr->tlength = frac_to_bytes_round_up(s->default_tlength, &s->ss);
+ attr->tlength = SPA_CLAMP(attr->tlength, minreq, attr->maxlength);
+ attr->tlength = SPA_ROUND_UP(attr->tlength, frame_size);
+
+ if (attr->minreq == (uint32_t) -1) {
+ uint32_t process = frac_to_bytes_round_up(s->default_req, &s->ss);
+ /* With low-latency, tlength/4 gives a decent default in all of traditional,
+ * adjust latency and early request modes. */
+ uint32_t m = attr->tlength / 4;
+ m = SPA_ROUND_DOWN(m, frame_size);
+ attr->minreq = SPA_MIN(process, m);
+ }
+ attr->minreq = SPA_MAX(attr->minreq, minreq);
+
+ if (attr->tlength < attr->minreq+frame_size)
+ attr->tlength = SPA_MIN(attr->minreq + frame_size, attr->maxlength);
+
+ if (s->early_requests) {
+ latency = attr->minreq;
+ } else if (s->adjust_latency) {
+ if (attr->tlength > attr->minreq * 2)
+ latency = SPA_MIN(max_latency, (attr->tlength - attr->minreq * 2) / 2);
+ else
+ latency = attr->minreq;
+
+ latency = SPA_ROUND_DOWN(latency, frame_size);
+
+ if (attr->tlength >= latency)
+ attr->tlength -= latency;
+ } else {
+ if (attr->tlength > attr->minreq * 2)
+ latency = SPA_MIN(max_latency, attr->tlength - attr->minreq * 2);
+ else
+ latency = attr->minreq;
+ }
+
+ if (attr->tlength < latency + 2 * attr->minreq)
+ attr->tlength = SPA_MIN(latency + 2 * attr->minreq, attr->maxlength);
+
+ attr->minreq = SPA_ROUND_DOWN(attr->minreq, frame_size);
+ if (attr->minreq <= 0) {
+ attr->minreq = frame_size;
+ attr->tlength += frame_size*2;
+ }
+ if (attr->tlength <= attr->minreq)
+ attr->tlength = SPA_MIN(attr->minreq*2 + frame_size, attr->maxlength);
+
+ max_prebuf = attr->tlength + frame_size - attr->minreq;
+ if (attr->prebuf == (uint32_t) -1 || attr->prebuf > max_prebuf)
+ attr->prebuf = max_prebuf;
+ attr->prebuf = SPA_ROUND_DOWN(attr->prebuf, frame_size);
+
+ attr->fragsize = 0;
+
+ lat->num = latency / frame_size;
+ lat->denom = rate;
+ clamp_latency(s, lat);
+
+ pw_log_info("[%s] maxlength:%u tlength:%u minreq:%u/%u prebuf:%u latency:%u/%u %u",
+ s->client->name, attr->maxlength, attr->tlength,
+ attr->minreq, minreq, attr->prebuf, lat->num, lat->denom, frame_size);
+
+ return lat->num * SPA_USEC_PER_SEC / lat->denom;
+}
+
+static uint64_t set_playback_buffer_attr(struct stream *s, struct buffer_attr *attr)
+{
+ struct spa_fraction lat;
+ uint64_t lat_usec;
+ struct spa_dict_item items[6];
+ char latency[32], rate[32];
+ char attr_maxlength[32];
+ char attr_tlength[32];
+ char attr_prebuf[32];
+ char attr_minreq[32];
+
+ lat_usec = fix_playback_buffer_attr(s, attr, s->ss.rate, &lat);
+
+ s->attr = *attr;
+
+ snprintf(latency, sizeof(latency), "%u/%u", lat.num, lat.denom);
+ snprintf(rate, sizeof(rate), "1/%u", lat.denom);
+ snprintf(attr_maxlength, sizeof(attr_maxlength), "%u", s->attr.maxlength);
+ snprintf(attr_tlength, sizeof(attr_tlength), "%u", s->attr.tlength);
+ snprintf(attr_prebuf, sizeof(attr_prebuf), "%u", s->attr.prebuf);
+ snprintf(attr_minreq, sizeof(attr_minreq), "%u", s->attr.minreq);
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latency);
+ items[1] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_RATE, rate);
+ items[2] = SPA_DICT_ITEM_INIT("pulse.attr.maxlength", attr_maxlength);
+ items[3] = SPA_DICT_ITEM_INIT("pulse.attr.tlength", attr_tlength);
+ items[4] = SPA_DICT_ITEM_INIT("pulse.attr.prebuf", attr_prebuf);
+ items[5] = SPA_DICT_ITEM_INIT("pulse.attr.minreq", attr_minreq);
+ pw_stream_update_properties(s->stream, &SPA_DICT_INIT(items, 6));
+
+ if (s->attr.prebuf > 0)
+ s->in_prebuf = true;
+
+ return lat_usec;
+}
+
+static int reply_create_playback_stream(struct stream *stream, struct pw_manager_object *peer)
+{
+ struct client *client = stream->client;
+ struct pw_manager *manager = client->manager;
+ struct message *reply;
+ uint32_t missing, peer_index;
+ const char *peer_name;
+ uint64_t lat_usec;
+
+ stream->buffer = calloc(1, MAXLENGTH);
+ if (stream->buffer == NULL)
+ return -errno;
+
+ lat_usec = set_playback_buffer_attr(stream, &stream->attr);
+
+ missing = stream_pop_missing(stream);
+ stream->index = id_to_index(manager, stream->id);
+ stream->lat_usec = lat_usec;
+
+ pw_log_info("[%s] reply CREATE_PLAYBACK_STREAM tag:%u index:%u missing:%u lat:%"PRIu64,
+ client->name, stream->create_tag, stream->index, missing, lat_usec);
+
+ reply = reply_new(client, stream->create_tag);
+ message_put(reply,
+ TAG_U32, stream->channel, /* stream index/channel */
+ TAG_U32, stream->index, /* sink_input/stream index */
+ TAG_U32, missing, /* missing/requested bytes */
+ TAG_INVALID);
+
+ if (peer && pw_manager_object_is_sink(peer)) {
+ peer_index = peer->index;
+ peer_name = pw_properties_get(peer->props, PW_KEY_NODE_NAME);
+ } else {
+ peer_index = SPA_ID_INVALID;
+ peer_name = NULL;
+ }
+
+ if (client->version >= 9) {
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.tlength,
+ TAG_U32, stream->attr.prebuf,
+ TAG_U32, stream->attr.minreq,
+ TAG_INVALID);
+ }
+ if (client->version >= 12) {
+ message_put(reply,
+ TAG_SAMPLE_SPEC, &stream->ss,
+ TAG_CHANNEL_MAP, &stream->map,
+ TAG_U32, peer_index, /* sink index */
+ TAG_STRING, peer_name, /* sink name */
+ TAG_BOOLEAN, false, /* sink suspended state */
+ TAG_INVALID);
+ }
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_USEC, lat_usec, /* sink configured latency */
+ TAG_INVALID);
+ }
+ if (client->version >= 21) {
+ struct format_info info;
+ spa_zero(info);
+ info.encoding = ENCODING_PCM;
+ message_put(reply,
+ TAG_FORMAT_INFO, &info, /* sink_input format */
+ TAG_INVALID);
+ }
+
+ stream->create_tag = SPA_ID_INVALID;
+
+ return client_queue_message(client, reply);
+}
+
+static uint64_t fix_record_buffer_attr(struct stream *s, struct buffer_attr *attr,
+ uint32_t rate, struct spa_fraction *lat)
+{
+ uint32_t frame_size, minfrag, latency, maxlength;
+
+ if ((frame_size = s->frame_size) == 0)
+ frame_size = sample_spec_frame_size(&s->ss);
+ if (frame_size == 0)
+ frame_size = 4;
+
+ maxlength = SPA_ROUND_DOWN(MAXLENGTH, frame_size);
+
+ pw_log_info("[%s] maxlength:%u fragsize:%u framesize:%u",
+ s->client->name, attr->maxlength, attr->fragsize,
+ frame_size);
+
+ if (attr->maxlength == (uint32_t) -1 || attr->maxlength > maxlength)
+ attr->maxlength = maxlength;
+ else
+ attr->maxlength = SPA_ROUND_DOWN(attr->maxlength, frame_size);
+ attr->maxlength = SPA_MAX(attr->maxlength, frame_size);
+
+ minfrag = frac_to_bytes_round_up(s->min_frag, &s->ss);
+
+ if (attr->fragsize == (uint32_t) -1 || attr->fragsize == 0)
+ attr->fragsize = frac_to_bytes_round_up(s->default_frag, &s->ss);
+ attr->fragsize = SPA_CLAMP(attr->fragsize, minfrag, attr->maxlength);
+ attr->fragsize = SPA_ROUND_UP(attr->fragsize, frame_size);
+
+ attr->tlength = attr->minreq = attr->prebuf = 0;
+
+ /* make sure we can queue at least to fragsize without overruns */
+ if (attr->maxlength < attr->fragsize * 4) {
+ attr->maxlength = attr->fragsize * 4;
+ if (attr->maxlength > maxlength) {
+ attr->maxlength = maxlength;
+ attr->fragsize = SPA_ROUND_DOWN(maxlength / 4, frame_size);
+ }
+ }
+
+ latency = attr->fragsize;
+
+ lat->num = latency / frame_size;
+ lat->denom = rate;
+ clamp_latency(s, lat);
+
+ pw_log_info("[%s] maxlength:%u fragsize:%u minfrag:%u latency:%u/%u",
+ s->client->name, attr->maxlength, attr->fragsize, minfrag,
+ lat->num, lat->denom);
+
+ return lat->num * SPA_USEC_PER_SEC / lat->denom;
+}
+
+static uint64_t set_record_buffer_attr(struct stream *s, struct buffer_attr *attr)
+{
+ struct spa_dict_item items[4];
+ char latency[32], rate[32];
+ char attr_maxlength[32];
+ char attr_fragsize[32];
+ struct spa_fraction lat;
+ uint64_t lat_usec;
+
+ lat_usec = fix_record_buffer_attr(s, attr, s->ss.rate, &lat);
+
+ s->attr = *attr;
+
+ snprintf(latency, sizeof(latency), "%u/%u", lat.num, lat.denom);
+ snprintf(rate, sizeof(rate), "1/%u", lat.denom);
+
+ snprintf(attr_maxlength, sizeof(attr_maxlength), "%u", s->attr.maxlength);
+ snprintf(attr_fragsize, sizeof(attr_fragsize), "%u", s->attr.fragsize);
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latency);
+ items[1] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_RATE, rate);
+ items[2] = SPA_DICT_ITEM_INIT("pulse.attr.maxlength", attr_maxlength);
+ items[3] = SPA_DICT_ITEM_INIT("pulse.attr.fragsize", attr_fragsize);
+ pw_stream_update_properties(s->stream, &SPA_DICT_INIT(items, 4));
+
+ return lat_usec;
+}
+
+static int reply_create_record_stream(struct stream *stream, struct pw_manager_object *peer)
+{
+ struct client *client = stream->client;
+ struct pw_manager *manager = client->manager;
+ char *tmp;
+ struct message *reply;
+ const char *peer_name, *name;
+ uint32_t peer_index;
+ uint64_t lat_usec;
+
+ stream->buffer = calloc(1, MAXLENGTH);
+ if (stream->buffer == NULL)
+ return -errno;
+
+ lat_usec = set_record_buffer_attr(stream, &stream->attr);
+
+ stream->index = id_to_index(manager, stream->id);
+ stream->lat_usec = lat_usec;
+
+ pw_log_info("[%s] reply CREATE_RECORD_STREAM tag:%u index:%u latency:%"PRIu64,
+ client->name, stream->create_tag, stream->index, lat_usec);
+
+ reply = reply_new(client, stream->create_tag);
+ message_put(reply,
+ TAG_U32, stream->channel, /* stream index/channel */
+ TAG_U32, stream->index, /* source_output/stream index */
+ TAG_INVALID);
+
+ if (peer && pw_manager_object_is_sink_input(peer))
+ peer = find_linked(manager, peer->id, PW_DIRECTION_OUTPUT);
+ if (peer && pw_manager_object_is_source_or_monitor(peer)) {
+ name = pw_properties_get(peer->props, PW_KEY_NODE_NAME);
+ peer_index = peer->index;
+ if (!pw_manager_object_is_source(peer)) {
+ size_t len = (name ? strlen(name) : 5) + 10;
+ peer_name = tmp = alloca(len);
+ snprintf(tmp, len, "%s.monitor", name ? name : "sink");
+ } else {
+ peer_name = name;
+ }
+ } else {
+ peer_index = SPA_ID_INVALID;
+ peer_name = NULL;
+ }
+
+ if (client->version >= 9) {
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.fragsize,
+ TAG_INVALID);
+ }
+ if (client->version >= 12) {
+ message_put(reply,
+ TAG_SAMPLE_SPEC, &stream->ss,
+ TAG_CHANNEL_MAP, &stream->map,
+ TAG_U32, peer_index, /* source index */
+ TAG_STRING, peer_name, /* source name */
+ TAG_BOOLEAN, false, /* source suspended state */
+ TAG_INVALID);
+ }
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_USEC, lat_usec, /* source configured latency */
+ TAG_INVALID);
+ }
+ if (client->version >= 22) {
+ struct format_info info;
+ spa_zero(info);
+ info.encoding = ENCODING_PCM;
+ message_put(reply,
+ TAG_FORMAT_INFO, &info, /* source_output format */
+ TAG_INVALID);
+ }
+
+ stream->create_tag = SPA_ID_INVALID;
+
+ return client_queue_message(client, reply);
+}
+
+static int reply_create_stream(struct stream *stream, struct pw_manager_object *peer)
+{
+ stream->peer_index = peer->index;
+ return stream->direction == PW_DIRECTION_OUTPUT ?
+ reply_create_playback_stream(stream, peer) :
+ reply_create_record_stream(stream, peer);
+}
+
+static void manager_added(void *data, struct pw_manager_object *o)
+{
+ struct client *client = data;
+ struct pw_manager *manager = client->manager;
+ const char *str;
+
+ register_object_message_handlers(o);
+
+ if (strcmp(o->type, PW_TYPE_INTERFACE_Core) == 0 && manager->info != NULL) {
+ struct pw_core_info *info = manager->info;
+ if (info->props) {
+ if ((str = spa_dict_lookup(info->props, "default.clock.rate")) != NULL)
+ client->impl->defs.sample_spec.rate = atoi(str);
+ if ((str = spa_dict_lookup(info->props, "default.clock.quantum-limit")) != NULL)
+ client->impl->defs.quantum_limit = atoi(str);
+ }
+ }
+
+ if (spa_streq(o->type, PW_TYPE_INTERFACE_Metadata)) {
+ if (o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_METADATA_NAME)) != NULL)
+ handle_metadata(client, NULL, o, str);
+ }
+
+ if (spa_streq(o->type, PW_TYPE_INTERFACE_Link)) {
+ struct stream *s, *t;
+ struct pw_manager_object *peer = NULL;
+ union pw_map_item *item;
+ pw_array_for_each(item, &client->streams.items) {
+ struct stream *s = item->data;
+ const char *peer_name;
+
+ if (pw_map_item_is_free(item) || s->pending)
+ continue;
+ if (s->peer_index == SPA_ID_INVALID)
+ continue;
+
+ peer = find_peer_for_link(manager, o, s->id, s->direction);
+ if (peer == NULL || peer->props == NULL ||
+ peer->index == s->peer_index)
+ continue;
+
+ s->peer_index = peer->index;
+
+ peer_name = pw_properties_get(peer->props, PW_KEY_NODE_NAME);
+ if (peer_name && s->direction == PW_DIRECTION_INPUT &&
+ pw_manager_object_is_monitor(peer)) {
+ int len = strlen(peer_name) + 10;
+ char *tmp = alloca(len);
+ snprintf(tmp, len, "%s.monitor", peer_name);
+ peer_name = tmp;
+ }
+ if (peer_name != NULL)
+ stream_send_moved(s, peer->index, peer_name);
+ }
+ spa_list_for_each_safe(s, t, &client->pending_streams, link) {
+ peer = find_peer_for_link(manager, o, s->id, s->direction);
+ if (peer) {
+ reply_create_stream(s, peer);
+ spa_list_remove(&s->link);
+ s->pending = false;
+ }
+ }
+ }
+
+ send_object_event(client, o, SUBSCRIPTION_EVENT_NEW);
+
+ /* Adding sinks etc. may also change defaults */
+ send_default_change_subscribe_event(client, pw_manager_object_is_sink(o), pw_manager_object_is_source_or_monitor(o));
+}
+
+static void manager_updated(void *data, struct pw_manager_object *o)
+{
+ struct client *client = data;
+
+ send_object_event(client, o, SUBSCRIPTION_EVENT_CHANGE);
+
+ set_temporary_move_target(client, o, SPA_ID_INVALID);
+
+ send_latency_offset_subscribe_event(client, o);
+ send_default_change_subscribe_event(client, pw_manager_object_is_sink(o), pw_manager_object_is_source_or_monitor(o));
+}
+
+static void manager_removed(void *data, struct pw_manager_object *o)
+{
+ struct client *client = data;
+ const char *str;
+
+ send_object_event(client, o, SUBSCRIPTION_EVENT_REMOVE);
+
+ send_default_change_subscribe_event(client, pw_manager_object_is_sink(o), pw_manager_object_is_source_or_monitor(o));
+
+ if (spa_streq(o->type, PW_TYPE_INTERFACE_Metadata)) {
+ if (o->props != NULL &&
+ (str = pw_properties_get(o->props, PW_KEY_METADATA_NAME)) != NULL)
+ handle_metadata(client, o, NULL, str);
+ }
+}
+
+static void manager_object_data_timeout(void *data, struct pw_manager_object *o, const char *key)
+{
+ struct client *client = data;
+
+ if (spa_streq(key, "temporary_move_data"))
+ 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)
+{
+ struct client *client = data;
+ bool changed = false;
+
+ pw_log_debug("meta id:%d subject:%d key:%s type:%s value:%s",
+ o->id, subject, key, type, value);
+
+ if (subject == PW_ID_CORE && o == client->metadata_default) {
+ char name[1024];
+
+ if (key == NULL || spa_streq(key, "default.audio.sink")) {
+ if (value != NULL) {
+ if (json_object_find(value,
+ "name", name, sizeof(name)) < 0)
+ value = NULL;
+ else
+ value = name;
+ }
+ if ((changed = !spa_streq(client->default_sink, value))) {
+ free(client->default_sink);
+ client->default_sink = value ? strdup(value) : NULL;
+ }
+ free(client->temporary_default_sink);
+ client->temporary_default_sink = NULL;
+ }
+ if (key == NULL || spa_streq(key, "default.audio.source")) {
+ if (value != NULL) {
+ if (json_object_find(value,
+ "name", name, sizeof(name)) < 0)
+ value = NULL;
+ else
+ value = name;
+ }
+ if ((changed = !spa_streq(client->default_source, value))) {
+ free(client->default_source);
+ client->default_source = value ? strdup(value) : NULL;
+ }
+ free(client->temporary_default_source);
+ client->temporary_default_source = NULL;
+ }
+ if (changed)
+ send_default_change_subscribe_event(client, true, true);
+ }
+ if (subject == PW_ID_CORE && o == client->metadata_routes) {
+ if (key == NULL)
+ pw_properties_clear(client->routes);
+ else
+ pw_properties_set(client->routes, key, value);
+ }
+}
+
+
+static void do_free_client(void *obj, void *data, int res, uint32_t id)
+{
+ struct client *client = obj;
+ client_free(client);
+}
+
+static void manager_disconnect(void *data)
+{
+ struct client *client = data;
+ pw_log_debug("manager_disconnect()");
+ pw_work_queue_add(client->impl->work_queue, client, 0,
+ do_free_client, NULL);
+}
+
+static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .sync = manager_sync,
+ .added = manager_added,
+ .updated = manager_updated,
+ .removed = manager_removed,
+ .metadata = manager_metadata,
+ .disconnect = manager_disconnect,
+ .object_data_timeout = manager_object_data_timeout,
+};
+
+static int do_set_client_name(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ const char *name = NULL;
+ int res = 0, changed = 0;
+
+ if (client->version < 13) {
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ if (name)
+ changed += pw_properties_set(client->props,
+ PW_KEY_APP_NAME, name);
+ } else {
+ if (message_get(m,
+ TAG_PROPLIST, client->props,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ changed++;
+ }
+
+ client_update_quirks(client);
+
+ client->name = pw_properties_get(client->props, PW_KEY_APP_NAME);
+ pw_log_info("[%s] %s tag:%d", client->name,
+ commands[command].name, tag);
+
+ if (client->core == NULL) {
+ client->core = pw_context_connect(impl->context,
+ pw_properties_copy(client->props), 0);
+ if (client->core == NULL) {
+ res = -errno;
+ goto error;
+ }
+ client->manager = pw_manager_new(client->core);
+ if (client->manager == NULL) {
+ res = -errno;
+ goto error;
+ }
+ client->connect_tag = tag;
+ pw_manager_add_listener(client->manager, &client->manager_listener,
+ &manager_events, client);
+ } else {
+ if (changed)
+ pw_core_update_properties(client->core, &client->props->dict);
+
+ if (client->connect_tag == SPA_ID_INVALID)
+ res = reply_set_client_name(client, tag);
+ }
+
+ return res;
+error:
+ pw_log_error("%p: failed to connect client: %s", impl, spa_strerror(res));
+ return res;
+
+}
+
+static int do_subscribe(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t mask;
+
+ if (message_get(m,
+ TAG_U32, &mask,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] SUBSCRIBE tag:%u mask:%08x",
+ client->name, tag, mask);
+
+ if (mask & ~SUBSCRIPTION_MASK_ALL)
+ return -EINVAL;
+
+ client->subscribed = mask;
+
+ return reply_simple_ack(client, tag);
+}
+
+static void stream_control_info(void *data, uint32_t id,
+ const struct pw_stream_control *control)
+{
+ struct stream *stream = data;
+
+ switch (id) {
+ case SPA_PROP_channelVolumes:
+ stream->volume.channels = control->n_values;
+ memcpy(stream->volume.values, control->values, control->n_values * sizeof(float));
+ pw_log_info("stream %p: volume changed %f", stream, stream->volume.values[0]);
+ break;
+ case SPA_PROP_mute:
+ stream->muted = control->values[0] >= 0.5;
+ pw_log_info("stream %p: mute changed %d", stream, stream->muted);
+ break;
+ }
+}
+
+static void do_destroy_stream(void *obj, void *data, int res, uint32_t id)
+{
+ struct stream *stream = obj;
+
+ stream_free(stream);
+}
+
+static void stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct stream *stream = data;
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ bool destroy_stream = false;
+
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ reply_error(client, -1, stream->create_tag, -EIO);
+ destroy_stream = true;
+ break;
+ case PW_STREAM_STATE_UNCONNECTED:
+ if (stream->create_tag != SPA_ID_INVALID)
+ reply_error(client, -1, stream->create_tag, -ENOENT);
+ else
+ stream->killed = true;
+ destroy_stream = true;
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ stream->id = pw_stream_get_node_id(stream->stream);
+ break;
+ case PW_STREAM_STATE_CONNECTING:
+ case PW_STREAM_STATE_STREAMING:
+ break;
+ }
+
+ if (destroy_stream) {
+ pw_work_queue_add(impl->work_queue, stream, 0,
+ do_destroy_stream, NULL);
+ }
+}
+
+static const struct spa_pod *get_buffers_param(struct stream *s,
+ struct buffer_attr *attr, struct spa_pod_builder *b)
+{
+ const struct spa_pod *param;
+ uint32_t blocks, buffers, size, maxsize, stride;
+ struct defs *defs = &s->impl->defs;
+
+ blocks = 1;
+ stride = s->frame_size;
+
+ maxsize = defs->quantum_limit * 32 * s->frame_size;
+ if (s->direction == PW_DIRECTION_OUTPUT) {
+ size = attr->minreq;
+ } else {
+ size = attr->fragsize;
+ }
+ buffers = SPA_CLAMP(maxsize / size, MIN_BUFFERS, MAX_BUFFERS);
+
+ pw_log_info("[%s] stride %d maxsize %d size %u buffers %d", s->client->name,
+ stride, maxsize, size, buffers);
+
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers,
+ MIN_BUFFERS, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ size, size, maxsize),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride));
+ return param;
+}
+
+static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct stream *stream = data;
+ const struct spa_pod *params[4];
+ uint32_t n_params = 0;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ int res;
+
+ if (id != SPA_PARAM_Format || param == NULL)
+ return;
+
+ if ((res = format_parse_param(param, false, &stream->ss, &stream->map, NULL, NULL)) < 0) {
+ pw_stream_set_error(stream->stream, res, "format not supported");
+ return;
+ }
+
+ pw_log_info("[%s] got format:%s rate:%u channels:%u", stream->client->name,
+ format_id2name(stream->ss.format),
+ stream->ss.rate, stream->ss.channels);
+
+ stream->frame_size = sample_spec_frame_size(&stream->ss);
+ if (stream->frame_size == 0) {
+ pw_stream_set_error(stream->stream, res, "format not supported");
+ return;
+ }
+ stream->rate = stream->ss.rate;
+
+ if (stream->create_tag != SPA_ID_INVALID) {
+ struct pw_manager_object *peer;
+
+ if (stream->volume_set) {
+ pw_stream_set_control(stream->stream,
+ SPA_PROP_channelVolumes, stream->volume.channels, stream->volume.values, 0);
+ }
+ if (stream->muted_set) {
+ float val = stream->muted ? 1.0f : 0.0f;
+ pw_stream_set_control(stream->stream,
+ SPA_PROP_mute, 1, &val, 0);
+ }
+ if (stream->corked)
+ stream_set_paused(stream, true, "cork after create");
+
+ /* if peer exists, reply immediately, otherwise reply when the link is created */
+ peer = find_linked(stream->client->manager, stream->id, stream->direction);
+ if (peer) {
+ reply_create_stream(stream, peer);
+ } else {
+ spa_list_append(&stream->client->pending_streams, &stream->link);
+ stream->pending = true;
+ }
+ }
+
+ params[n_params++] = get_buffers_param(stream, &stream->attr, &b);
+ pw_stream_update_params(stream->stream, params, n_params);
+}
+
+static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
+{
+ struct stream *stream = data;
+ switch (id) {
+ case SPA_IO_Position:
+ stream->position = area;
+ break;
+ }
+}
+
+struct process_data {
+ struct pw_time pwt;
+ uint32_t read_inc;
+ uint32_t write_inc;
+ uint32_t underrun_for;
+ uint32_t playing_for;
+ uint32_t minreq;
+ uint32_t quantum;
+ unsigned int underrun:1;
+ unsigned int idle:1;
+};
+
+static int
+do_process_done(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *stream = user_data;
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ const struct process_data *pd = data;
+ uint32_t index, towrite;
+ int32_t avail;
+
+ stream->timestamp = pd->pwt.now;
+ stream->delay = pd->pwt.buffered * SPA_USEC_PER_SEC / stream->ss.rate;
+ if (pd->pwt.rate.denom > 0)
+ stream->delay += pd->pwt.delay * SPA_USEC_PER_SEC * pd->pwt.rate.num / pd->pwt.rate.denom;
+
+ if (stream->direction == PW_DIRECTION_OUTPUT) {
+ if (pd->quantum != stream->last_quantum)
+ stream_update_minreq(stream, pd->minreq);
+ stream->last_quantum = pd->quantum;
+
+ stream->read_index += pd->read_inc;
+ if (stream->corked) {
+ if (stream->underrun_for != (uint64_t)-1)
+ stream->underrun_for += pd->underrun_for;
+ stream->playing_for = 0;
+ return 0;
+ }
+ if (pd->underrun != stream->is_underrun) {
+ stream->is_underrun = pd->underrun;
+ stream->underrun_for = 0;
+ stream->playing_for = 0;
+ if (pd->underrun)
+ stream_send_underflow(stream, stream->read_index);
+ else
+ stream_send_started(stream);
+ }
+ if (pd->idle) {
+ if (!stream->is_idle) {
+ stream->idle_time = stream->timestamp;
+ } else if (!stream->is_paused &&
+ stream->idle_timeout_sec > 0 &&
+ stream->timestamp - stream->idle_time >
+ (stream->idle_timeout_sec * SPA_NSEC_PER_SEC)) {
+ stream_set_paused(stream, true, "long underrun");
+ }
+ }
+ stream->is_idle = pd->idle;
+ stream->playing_for += pd->playing_for;
+ if (stream->underrun_for != (uint64_t)-1)
+ stream->underrun_for += pd->underrun_for;
+
+ stream_send_request(stream);
+ } else {
+ struct message *msg;
+ stream->write_index += pd->write_inc;
+
+ avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+ if (!spa_list_is_empty(&client->out_messages)) {
+ pw_log_debug("%p: [%s] pending read:%u avail:%d",
+ stream, client->name, index, avail);
+ return 0;
+ }
+
+ if (avail <= 0) {
+ /* underrun, can't really happen but if it does we
+ * do nothing and wait for more data */
+ pw_log_warn("%p: [%s] underrun read:%u avail:%d",
+ stream, client->name, index, avail);
+ } else {
+ if ((uint32_t)avail > stream->attr.maxlength) {
+ uint32_t skip = avail - stream->attr.fragsize;
+ /* overrun, catch up to latest fragment and send it */
+ pw_log_warn("%p: [%s] overrun recover read:%u avail:%d max:%u skip:%u",
+ stream, client->name, index, avail, stream->attr.maxlength, skip);
+ index += skip;
+ stream->read_index += skip;
+ avail = stream->attr.fragsize;
+ }
+ pw_log_trace("avail:%d index:%u", avail, index);
+
+ while ((uint32_t)avail >= stream->attr.fragsize) {
+ towrite = SPA_MIN(avail, MAX_BLOCK);
+ towrite = SPA_MIN(towrite, stream->attr.fragsize);
+ towrite = SPA_ROUND_DOWN(towrite, stream->frame_size);
+
+ msg = message_alloc(impl, stream->channel, towrite);
+ if (msg == NULL)
+ return -errno;
+
+ spa_ringbuffer_read_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ msg->data, towrite);
+
+ client_queue_message(client, msg);
+
+ index += towrite;
+ avail -= towrite;
+ stream->read_index += towrite;
+ }
+ spa_ringbuffer_read_update(&stream->ring, index);
+ }
+ }
+ return 0;
+}
+
+
+static void stream_process(void *data)
+{
+ struct stream *stream = data;
+ struct client *client = stream->client;
+ struct impl *impl = stream->impl;
+ void *p;
+ struct pw_buffer *buffer;
+ struct spa_buffer *buf;
+ struct spa_data *d;
+ uint32_t offs, size, minreq = 0, index;
+ struct process_data pd;
+ bool do_flush = false;
+
+ if (stream->create_tag != SPA_ID_INVALID)
+ return;
+
+ pw_log_trace_fp("%p: process", stream);
+ buffer = pw_stream_dequeue_buffer(stream->stream);
+ if (buffer == NULL)
+ return;
+
+ buf = buffer->buffer;
+ d = &buf->datas[0];
+ if ((p = d->data) == NULL)
+ return;
+
+ spa_zero(pd);
+
+ if (stream->direction == PW_DIRECTION_OUTPUT) {
+ int32_t avail = spa_ringbuffer_get_read_index(&stream->ring, &index);
+
+ minreq = buffer->requested * stream->frame_size;
+ if (minreq == 0)
+ minreq = stream->attr.minreq;
+
+ pd.minreq = minreq;
+ pd.quantum = stream->position ? stream->position->clock.duration : minreq;
+
+ if (avail < (int32_t)minreq || stream->corked) {
+ /* underrun, produce a silence buffer */
+ size = SPA_MIN(d->maxsize, minreq);
+ memset(p, 0, size);
+
+ if (stream->draining && !stream->corked) {
+ stream->draining = false;
+ do_flush = true;
+ } else {
+ pd.underrun_for = size;
+ pd.underrun = true;
+ }
+ if ((stream->attr.prebuf == 0 || do_flush) && !stream->corked) {
+ if (avail > 0) {
+ avail = SPA_MIN((uint32_t)avail, size);
+ spa_ringbuffer_read_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ p, avail);
+ }
+ index += size;
+ pd.read_inc = size;
+ spa_ringbuffer_read_update(&stream->ring, index);
+
+ pd.playing_for = size;
+ }
+ pd.idle = true;
+ pw_log_debug("%p: [%s] underrun read:%u avail:%d max:%u",
+ stream, client->name, index, avail, minreq);
+ } else {
+ if (avail > (int32_t)stream->attr.maxlength) {
+ uint32_t skip = avail - stream->attr.maxlength;
+ /* overrun, reported by other side, here we skip
+ * ahead to the oldest data. */
+ pw_log_debug("%p: [%s] overrun read:%u avail:%d max:%u skip:%u",
+ stream, client->name, index, avail,
+ stream->attr.maxlength, skip);
+ index += skip;
+ pd.read_inc = skip;
+ avail = stream->attr.maxlength;
+ }
+ size = SPA_MIN(d->maxsize, (uint32_t)avail);
+ size = SPA_MIN(size, minreq);
+
+ spa_ringbuffer_read_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ p, size);
+
+ index += size;
+ pd.read_inc += size;
+ spa_ringbuffer_read_update(&stream->ring, index);
+
+ pd.playing_for = size;
+ pd.underrun = false;
+ }
+ d->chunk->offset = 0;
+ d->chunk->stride = stream->frame_size;
+ d->chunk->size = size;
+ buffer->size = size / stream->frame_size;
+ } else {
+ int32_t filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offs);
+
+ if (filled < 0) {
+ /* underrun, can't really happen because we never read more
+ * than what's available on the other side */
+ pw_log_warn("%p: [%s] underrun write:%u filled:%d",
+ stream, client->name, index, filled);
+ } else if ((uint32_t)filled + size > stream->attr.maxlength) {
+ /* overrun, can happen when the other side is not
+ * reading fast enough. We still write our data into the
+ * ringbuffer and expect the other side to warn and catch up. */
+ pw_log_debug("%p: [%s] overrun write:%u filled:%d size:%u max:%u",
+ stream, client->name, index, filled,
+ size, stream->attr.maxlength);
+ }
+
+ spa_ringbuffer_write_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ SPA_PTROFF(p, offs, void),
+ SPA_MIN(size, MAXLENGTH));
+
+ index += size;
+ pd.write_inc = size;
+ spa_ringbuffer_write_update(&stream->ring, index);
+ }
+ pw_stream_queue_buffer(stream->stream, buffer);
+
+ if (do_flush)
+ pw_stream_flush(stream->stream, true);
+
+ pw_stream_get_time_n(stream->stream, &pd.pwt, sizeof(pd.pwt));
+
+ pw_loop_invoke(impl->loop,
+ do_process_done, 1, &pd, sizeof(pd), false, stream);
+}
+
+static void stream_drained(void *data)
+{
+ struct stream *stream = data;
+ if (stream->drain_tag != 0) {
+ pw_log_info("[%s] drained channel:%u tag:%d",
+ stream->client->name, stream->channel,
+ stream->drain_tag);
+ reply_simple_ack(stream->client, stream->drain_tag);
+ stream->drain_tag = 0;
+
+ pw_stream_set_active(stream->stream, !stream->is_paused);
+ }
+}
+
+static const struct pw_stream_events stream_events =
+{
+ PW_VERSION_STREAM_EVENTS,
+ .control_info = stream_control_info,
+ .state_changed = stream_state_changed,
+ .param_changed = stream_param_changed,
+ .io_changed = stream_io_changed,
+ .process = stream_process,
+ .drained = stream_drained,
+};
+
+static void log_format_info(struct impl *impl, enum spa_log_level level, struct format_info *format)
+{
+ const struct spa_dict_item *it;
+ pw_logt(level, mod_topic, "%p: format %s",
+ impl, format_encoding2name(format->encoding));
+ spa_dict_for_each(it, &format->props->dict)
+ pw_logt(level, mod_topic, "%p: '%s': '%s'",
+ impl, it->key, it->value);
+}
+
+static int do_create_playback_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ const char *name = NULL;
+ int res;
+ struct sample_spec ss;
+ struct channel_map map;
+ uint32_t sink_index, syncid, rate = 0;
+ const char *sink_name;
+ struct buffer_attr attr = { 0 };
+ bool corked = false,
+ no_remap = false,
+ no_remix = false,
+ fix_format = false,
+ fix_rate = false,
+ fix_channels = false,
+ no_move = false,
+ variable_rate = false,
+ muted = false,
+ adjust_latency = false,
+ early_requests = false,
+ dont_inhibit_auto_suspend = false,
+ volume_set = true,
+ muted_set = false,
+ fail_on_suspend = false,
+ relative_volume = false,
+ passthrough = false;
+ struct volume volume;
+ struct pw_properties *props = NULL;
+ uint8_t n_formats = 0;
+ struct stream *stream = NULL;
+ uint32_t n_params = 0, n_valid_formats = 0, flags;
+ const struct spa_pod *params[MAX_FORMATS];
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ props = pw_properties_copy(client->props);
+ if (props == NULL)
+ goto error_errno;
+
+ if (client->version < 13) {
+ if ((res = message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID)) < 0)
+ goto error_protocol;
+ if (name == NULL)
+ goto error_protocol;
+ }
+ if (message_get(m,
+ TAG_SAMPLE_SPEC, &ss,
+ TAG_CHANNEL_MAP, &map,
+ TAG_U32, &sink_index,
+ TAG_STRING, &sink_name,
+ TAG_U32, &attr.maxlength,
+ TAG_BOOLEAN, &corked,
+ TAG_U32, &attr.tlength,
+ TAG_U32, &attr.prebuf,
+ TAG_U32, &attr.minreq,
+ TAG_U32, &syncid,
+ TAG_CVOLUME, &volume,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ pw_log_info("[%s] CREATE_PLAYBACK_STREAM tag:%u corked:%u sink-name:%s sink-index:%u",
+ client->name, tag, corked, sink_name, sink_index);
+
+ if (sink_index != SPA_ID_INVALID && sink_name != NULL)
+ goto error_invalid;
+
+ if (client->version >= 12) {
+ if (message_get(m,
+ TAG_BOOLEAN, &no_remap,
+ TAG_BOOLEAN, &no_remix,
+ TAG_BOOLEAN, &fix_format,
+ TAG_BOOLEAN, &fix_rate,
+ TAG_BOOLEAN, &fix_channels,
+ TAG_BOOLEAN, &no_move,
+ TAG_BOOLEAN, &variable_rate,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 13) {
+ if (message_get(m,
+ TAG_BOOLEAN, &muted,
+ TAG_BOOLEAN, &adjust_latency,
+ TAG_PROPLIST, props,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 14) {
+ if (message_get(m,
+ TAG_BOOLEAN, &volume_set,
+ TAG_BOOLEAN, &early_requests,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 15) {
+ if (message_get(m,
+ TAG_BOOLEAN, &muted_set,
+ TAG_BOOLEAN, &dont_inhibit_auto_suspend,
+ TAG_BOOLEAN, &fail_on_suspend,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 17) {
+ if (message_get(m,
+ TAG_BOOLEAN, &relative_volume,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 18) {
+ if (message_get(m,
+ TAG_BOOLEAN, &passthrough,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+
+ if (client->version >= 21) {
+ if (message_get(m,
+ TAG_U8, &n_formats,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (n_formats) {
+ uint8_t i;
+ for (i = 0; i < n_formats; i++) {
+ struct format_info format;
+ uint32_t r;
+
+ if (message_get(m,
+ TAG_FORMAT_INFO, &format,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_info_build_param(&b,
+ SPA_PARAM_EnumFormat, &format, &r)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ if (r > rate)
+ rate = r;
+ } else {
+ log_format_info(impl, SPA_LOG_LEVEL_WARN, &format);
+ }
+ format_info_clear(&format);
+ }
+ }
+ }
+ if (sample_spec_valid(&ss)) {
+ if (fix_format || fix_rate || fix_channels) {
+ struct sample_spec sfix = ss;
+ if (fix_format)
+ sfix.format = SPA_AUDIO_FORMAT_UNKNOWN;
+ if (fix_rate)
+ sfix.rate = 0;
+ if (fix_channels)
+ sfix.channels = 0;
+ if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_build_param(&b,
+ SPA_PARAM_EnumFormat, &sfix,
+ sfix.channels > 0 ? &map : NULL)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ }
+ }
+ else if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_build_param(&b,
+ SPA_PARAM_EnumFormat, &ss,
+ ss.channels > 0 ? &map : NULL)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ } else {
+ pw_log_warn("%p: unsupported format:%s rate:%d channels:%u",
+ impl, format_id2name(ss.format), ss.rate,
+ ss.channels);
+ }
+ rate = ss.rate;
+ }
+
+ if (m->offset != m->length)
+ goto error_protocol;
+
+ if (n_valid_formats == 0)
+ goto error_no_formats;
+
+ stream = stream_new(client, STREAM_TYPE_PLAYBACK, tag, &ss, &map, &attr);
+ if (stream == NULL)
+ goto error_errno;
+
+ stream->corked = corked;
+ stream->adjust_latency = adjust_latency;
+ stream->early_requests = early_requests;
+ stream->volume = volume;
+ stream->volume_set = volume_set;
+ stream->muted = muted;
+ stream->muted_set = muted_set;
+ stream->is_underrun = true;
+ stream->underrun_for = -1;
+
+ if (rate != 0) {
+ struct spa_fraction lat;
+ fix_playback_buffer_attr(stream, &attr, rate, &lat);
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", rate);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u",
+ lat.num, lat.denom);
+ }
+ if (no_remix)
+ pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "true");
+ flags = 0;
+ if (no_move)
+ flags |= PW_STREAM_FLAG_DONT_RECONNECT;
+
+ if (sink_name != NULL) {
+ pw_properties_set(props,
+ PW_KEY_TARGET_OBJECT, sink_name);
+ } else if (sink_index != SPA_ID_INVALID && sink_index != 0) {
+ pw_properties_setf(props,
+ PW_KEY_TARGET_OBJECT, "%u", sink_index);
+ }
+
+ stream->stream = pw_stream_new(client->core, name, props);
+ props = NULL;
+ if (stream->stream == NULL)
+ goto error_errno;
+
+ pw_log_debug("%p: new stream %p channel:%d passthrough:%d",
+ impl, stream, stream->channel, passthrough);
+
+ pw_stream_add_listener(stream->stream,
+ &stream->stream_listener,
+ &stream_events, stream);
+
+ pw_stream_connect(stream->stream,
+ PW_DIRECTION_OUTPUT,
+ SPA_ID_INVALID,
+ flags |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, n_params);
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ goto error;
+error_protocol:
+ res = -EPROTO;
+ goto error;
+error_no_formats:
+ res = -ENOTSUP;
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error:
+ pw_properties_free(props);
+ if (stream)
+ stream_free(stream);
+ return res;
+}
+
+static int do_create_record_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ const char *name = NULL;
+ int res;
+ struct sample_spec ss;
+ struct channel_map map;
+ uint32_t source_index;
+ const char *source_name;
+ struct buffer_attr attr = { 0 };
+ bool corked = false,
+ no_remap = false,
+ no_remix = false,
+ fix_format = false,
+ fix_rate = false,
+ fix_channels = false,
+ no_move = false,
+ variable_rate = false,
+ peak_detect = false,
+ adjust_latency = false,
+ early_requests = false,
+ dont_inhibit_auto_suspend = false,
+ volume_set = true,
+ muted = false,
+ muted_set = false,
+ fail_on_suspend = false,
+ relative_volume = false,
+ passthrough = false;
+ uint32_t direct_on_input_idx = SPA_ID_INVALID;
+ struct volume volume = VOLUME_INIT;
+ struct pw_properties *props = NULL;
+ uint8_t n_formats = 0;
+ struct stream *stream = NULL;
+ uint32_t n_params = 0, n_valid_formats = 0, flags, id, rate = 0;
+ const struct spa_pod *params[MAX_FORMATS];
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ props = pw_properties_copy(client->props);
+ if (props == NULL)
+ goto error_errno;
+
+ if (client->version < 13) {
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ if (name == NULL)
+ goto error_protocol;
+ }
+ if (message_get(m,
+ TAG_SAMPLE_SPEC, &ss,
+ TAG_CHANNEL_MAP, &map,
+ TAG_U32, &source_index,
+ TAG_STRING, &source_name,
+ TAG_U32, &attr.maxlength,
+ TAG_BOOLEAN, &corked,
+ TAG_U32, &attr.fragsize,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ pw_log_info("[%s] CREATE_RECORD_STREAM tag:%u corked:%u source-name:%s source-index:%u",
+ client->name, tag, corked, source_name, source_index);
+
+ if (source_index != SPA_ID_INVALID && source_name != NULL)
+ goto error_invalid;
+
+ if (client->version >= 12) {
+ if (message_get(m,
+ TAG_BOOLEAN, &no_remap,
+ TAG_BOOLEAN, &no_remix,
+ TAG_BOOLEAN, &fix_format,
+ TAG_BOOLEAN, &fix_rate,
+ TAG_BOOLEAN, &fix_channels,
+ TAG_BOOLEAN, &no_move,
+ TAG_BOOLEAN, &variable_rate,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 13) {
+ if (message_get(m,
+ TAG_BOOLEAN, &peak_detect,
+ TAG_BOOLEAN, &adjust_latency,
+ TAG_PROPLIST, props,
+ TAG_U32, &direct_on_input_idx,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 14) {
+ if (message_get(m,
+ TAG_BOOLEAN, &early_requests,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 15) {
+ if (message_get(m,
+ TAG_BOOLEAN, &dont_inhibit_auto_suspend,
+ TAG_BOOLEAN, &fail_on_suspend,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (client->version >= 22) {
+ if (message_get(m,
+ TAG_U8, &n_formats,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (n_formats) {
+ uint8_t i;
+ for (i = 0; i < n_formats; i++) {
+ struct format_info format;
+ uint32_t r;
+
+ if (message_get(m,
+ TAG_FORMAT_INFO, &format,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_info_build_param(&b,
+ SPA_PARAM_EnumFormat, &format, &r)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ if (r > rate)
+ rate = r;
+ } else {
+ log_format_info(impl, SPA_LOG_LEVEL_WARN, &format);
+ }
+ format_info_clear(&format);
+ }
+ }
+ if (message_get(m,
+ TAG_CVOLUME, &volume,
+ TAG_BOOLEAN, &muted,
+ TAG_BOOLEAN, &volume_set,
+ TAG_BOOLEAN, &muted_set,
+ TAG_BOOLEAN, &relative_volume,
+ TAG_BOOLEAN, &passthrough,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ } else {
+ volume_set = false;
+ }
+ if (sample_spec_valid(&ss)) {
+ if (fix_format || fix_rate || fix_channels) {
+ struct sample_spec sfix = ss;
+ if (fix_format)
+ sfix.format = SPA_AUDIO_FORMAT_UNKNOWN;
+ if (fix_rate)
+ sfix.rate = 0;
+ if (fix_channels)
+ sfix.channels = 0;
+ if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_build_param(&b,
+ SPA_PARAM_EnumFormat, &sfix,
+ sfix.channels > 0 ? &map : NULL)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ }
+ }
+ else if (n_params < MAX_FORMATS &&
+ (params[n_params] = format_build_param(&b,
+ SPA_PARAM_EnumFormat, &ss,
+ ss.channels > 0 ? &map : NULL)) != NULL) {
+ n_params++;
+ n_valid_formats++;
+ } else {
+ pw_log_warn("%p: unsupported format:%s rate:%d channels:%u",
+ impl, format_id2name(ss.format), ss.rate,
+ ss.channels);
+ }
+ rate = ss.rate;
+ }
+ if (m->offset != m->length)
+ goto error_protocol;
+
+ if (n_valid_formats == 0)
+ goto error_no_formats;
+
+ stream = stream_new(client, STREAM_TYPE_RECORD, tag, &ss, &map, &attr);
+ if (stream == NULL)
+ goto error_errno;
+
+ stream->corked = corked;
+ stream->adjust_latency = adjust_latency;
+ stream->early_requests = early_requests;
+ stream->volume = volume;
+ stream->volume_set = volume_set;
+ stream->muted = muted;
+ stream->muted_set = muted_set;
+
+ if (client->quirks & QUIRK_REMOVE_CAPTURE_DONT_MOVE)
+ no_move = false;
+
+ if (rate != 0) {
+ struct spa_fraction lat;
+ fix_record_buffer_attr(stream, &attr, rate, &lat);
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", rate);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u",
+ lat.num, lat.denom);
+ }
+ if (peak_detect)
+ pw_properties_set(props, PW_KEY_STREAM_MONITOR, "true");
+ if (no_remix)
+ pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "true");
+ flags = 0;
+ if (no_move)
+ flags |= PW_STREAM_FLAG_DONT_RECONNECT;
+
+ if (direct_on_input_idx != SPA_ID_INVALID) {
+ source_index = direct_on_input_idx;
+ } else if (source_name != NULL) {
+ if ((id = atoi(source_name)) != 0)
+ source_index = id;
+ }
+ if (source_index != SPA_ID_INVALID && source_index != 0) {
+ pw_properties_setf(props,
+ PW_KEY_TARGET_OBJECT, "%u", source_index);
+ } else if (source_name != NULL) {
+ if (spa_strendswith(source_name, ".monitor")) {
+ pw_properties_setf(props,
+ PW_KEY_TARGET_OBJECT,
+ "%.*s", (int)strlen(source_name)-8, source_name);
+ pw_properties_set(props,
+ PW_KEY_STREAM_CAPTURE_SINK, "true");
+ } else {
+ pw_properties_set(props,
+ PW_KEY_TARGET_OBJECT, source_name);
+ }
+ }
+
+ stream->stream = pw_stream_new(client->core, name, props);
+ props = NULL;
+ if (stream->stream == NULL)
+ goto error_errno;
+
+ pw_stream_add_listener(stream->stream,
+ &stream->stream_listener,
+ &stream_events, stream);
+
+ pw_stream_connect(stream->stream,
+ PW_DIRECTION_INPUT,
+ SPA_ID_INVALID,
+ flags |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, n_params);
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ goto error;
+error_protocol:
+ res = -EPROTO;
+ goto error;
+error_no_formats:
+ res = -ENOTSUP;
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error:
+ pw_properties_free(props);
+ if (stream)
+ stream_free(stream);
+ return res;
+}
+
+static int do_delete_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] DELETE_STREAM tag:%u channel:%u",
+ client->name, tag, channel);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL)
+ return -ENOENT;
+ if (command == COMMAND_DELETE_PLAYBACK_STREAM &&
+ stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+ if (command == COMMAND_DELETE_RECORD_STREAM &&
+ stream->type != STREAM_TYPE_RECORD)
+ return -ENOENT;
+ if (command == COMMAND_DELETE_UPLOAD_STREAM &&
+ stream->type != STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ stream_free(stream);
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_get_playback_latency(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t channel;
+ struct timeval tv, now;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_TIMEVAL, &tv,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_debug("%p: %s tag:%u channel:%u", impl, commands[command].name, tag, channel);
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+
+ pw_log_debug("read:0x%"PRIx64" write:0x%"PRIx64" queued:%"PRIi64" delay:%"PRIi64
+ " playing:%"PRIu64,
+ stream->read_index, stream->write_index,
+ stream->write_index - stream->read_index, stream->delay,
+ stream->playing_for);
+
+ gettimeofday(&now, NULL);
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_USEC, stream->delay, /* sink latency + queued samples */
+ TAG_USEC, 0LL, /* always 0 */
+ TAG_BOOLEAN, stream->playing_for > 0 &&
+ !stream->corked, /* playing state */
+ TAG_TIMEVAL, &tv,
+ TAG_TIMEVAL, &now,
+ TAG_S64, stream->write_index,
+ TAG_S64, stream->read_index,
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_U64, stream->underrun_for,
+ TAG_U64, stream->playing_for,
+ TAG_INVALID);
+ }
+ return client_queue_message(client, reply);
+}
+
+static int do_get_record_latency(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t channel;
+ struct timeval tv, now;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_TIMEVAL, &tv,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_debug("%p: %s channel:%u", impl, commands[command].name, channel);
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type != STREAM_TYPE_RECORD)
+ return -ENOENT;
+
+ pw_log_debug("read:0x%"PRIx64" write:0x%"PRIx64" queued:%"PRIi64" delay:%"PRIi64,
+ stream->read_index, stream->write_index,
+ stream->write_index - stream->read_index, stream->delay);
+
+
+ gettimeofday(&now, NULL);
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_USEC, 0LL, /* monitor latency */
+ TAG_USEC, stream->delay, /* source latency + queued */
+ TAG_BOOLEAN, !stream->corked, /* playing state */
+ TAG_TIMEVAL, &tv,
+ TAG_TIMEVAL, &now,
+ TAG_S64, stream->write_index,
+ TAG_S64, stream->read_index,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int do_create_upload_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ const char *name;
+ struct sample_spec ss;
+ struct channel_map map;
+ struct pw_properties *props = NULL;
+ uint32_t length;
+ struct stream *stream = NULL;
+ struct message *reply;
+ int res;
+
+ if ((props = pw_properties_copy(client->props)) == NULL)
+ goto error_errno;
+
+ if ((res = message_get(m,
+ TAG_STRING, &name,
+ TAG_SAMPLE_SPEC, &ss,
+ TAG_CHANNEL_MAP, &map,
+ TAG_U32, &length,
+ TAG_INVALID)) < 0)
+ goto error_proto;
+
+ if (client->version >= 13) {
+ if ((res = message_get(m,
+ TAG_PROPLIST, props,
+ TAG_INVALID)) < 0)
+ goto error_proto;
+
+ } else {
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, name);
+ }
+ if (name == NULL)
+ name = pw_properties_get(props, "event.id");
+ if (name == NULL)
+ name = pw_properties_get(props, PW_KEY_MEDIA_NAME);
+
+ if (name == NULL ||
+ !sample_spec_valid(&ss) ||
+ !channel_map_valid(&map) ||
+ ss.channels != map.channels ||
+ length == 0 || (length % sample_spec_frame_size(&ss) != 0))
+ goto error_invalid;
+ if (length >= SCACHE_ENTRY_SIZE_MAX)
+ goto error_toolarge;
+
+ pw_log_info("[%s] %s tag:%u name:%s length:%d",
+ client->name, commands[command].name, tag,
+ name, length);
+
+ stream = stream_new(client, STREAM_TYPE_UPLOAD, tag, &ss, &map, &(struct buffer_attr) {
+ .maxlength = length,
+ });
+ if (stream == NULL)
+ goto error_errno;
+
+ stream->props = props;
+
+ stream->buffer = calloc(1, MAXLENGTH);
+ if (stream->buffer == NULL)
+ goto error_errno;
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, stream->channel,
+ TAG_U32, length,
+ TAG_INVALID);
+ return client_queue_message(client, reply);
+
+error_errno:
+ res = -errno;
+ goto error;
+error_proto:
+ res = -EPROTO;
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error_toolarge:
+ res = -EOVERFLOW;
+ goto error;
+error:
+ pw_properties_free(props);
+ if (stream)
+ stream_free(stream);
+ return res;
+}
+
+static int do_finish_upload_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ uint32_t channel, event;
+ struct stream *stream;
+ struct sample *sample;
+ const char *name;
+ int res;
+
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type != STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ name = pw_properties_get(stream->props, "event.id");
+ if (name == NULL)
+ name = pw_properties_get(stream->props, PW_KEY_MEDIA_NAME);
+ if (name == NULL)
+ goto error_invalid;
+
+ pw_log_info("[%s] %s tag:%u channel:%u name:%s",
+ client->name, commands[command].name, tag,
+ channel, name);
+
+ struct sample *old = find_sample(impl, SPA_ID_INVALID, name);
+ if (old == NULL || old->ref > 1) {
+ sample = calloc(1, sizeof(*sample));
+ if (sample == NULL)
+ goto error_errno;
+
+ if (old != NULL) {
+ sample->index = old->index;
+ spa_assert_se(pw_map_insert_at(&impl->samples, sample->index, sample) == 0);
+
+ old->index = SPA_ID_INVALID;
+ sample_unref(old);
+ } else {
+ sample->index = pw_map_insert_new(&impl->samples, sample);
+ if (sample->index == SPA_ID_INVALID)
+ goto error_errno;
+ }
+ } else {
+ pw_properties_free(old->props);
+ free(old->buffer);
+ impl->stat.sample_cache -= old->length;
+
+ sample = old;
+ }
+
+ if (old != NULL)
+ event = SUBSCRIPTION_EVENT_CHANGE;
+ else
+ event = SUBSCRIPTION_EVENT_NEW;
+
+ sample->ref = 1;
+ sample->impl = impl;
+ sample->name = name;
+ sample->props = stream->props;
+ sample->ss = stream->ss;
+ sample->map = stream->map;
+ sample->buffer = stream->buffer;
+ sample->length = stream->attr.maxlength;
+
+ impl->stat.sample_cache += sample->length;
+
+ stream->props = NULL;
+ stream->buffer = NULL;
+ stream_free(stream);
+
+ broadcast_subscribe_event(impl,
+ SUBSCRIPTION_MASK_SAMPLE_CACHE,
+ event | SUBSCRIPTION_EVENT_SAMPLE_CACHE,
+ sample->index);
+
+ return reply_simple_ack(client, tag);
+
+error_errno:
+ res = -errno;
+ free(sample);
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error:
+ stream_free(stream);
+ return res;
+}
+
+static const char *get_default(struct client *client, bool sink)
+{
+ struct selector sel;
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o;
+ const char *def, *str, *mon;
+
+ spa_zero(sel);
+ if (sink) {
+ sel.type = pw_manager_object_is_sink;
+ sel.key = PW_KEY_NODE_NAME;
+ sel.value = client->default_sink;
+ def = DEFAULT_SINK;
+ } else {
+ sel.type = pw_manager_object_is_source_or_monitor;
+ sel.key = PW_KEY_NODE_NAME;
+ sel.value = client->default_source;
+ def = DEFAULT_SOURCE;
+ }
+ sel.accumulate = select_best;
+
+ o = select_object(manager, &sel);
+ if (o == NULL || o->props == NULL)
+ return def;
+ str = pw_properties_get(o->props, PW_KEY_NODE_NAME);
+
+ if (!sink && pw_manager_object_is_monitor(o)) {
+ def = DEFAULT_MONITOR;
+ if (str != NULL &&
+ (mon = pw_properties_get(o->props, PW_KEY_NODE_NAME".monitor")) == NULL) {
+ pw_properties_setf(o->props,
+ PW_KEY_NODE_NAME".monitor",
+ "%s.monitor", str);
+ }
+ str = pw_properties_get(o->props, PW_KEY_NODE_NAME".monitor");
+ }
+ if (str == NULL)
+ str = def;
+ return str;
+}
+
+static struct pw_manager_object *find_device(struct client *client,
+ uint32_t index, const char *name, bool sink, bool *is_monitor)
+{
+ struct selector sel;
+ bool monitor = false, find_default = false;
+ struct pw_manager_object *o;
+
+ if (name != NULL) {
+ if (spa_streq(name, DEFAULT_MONITOR)) {
+ if (sink)
+ return NULL;
+ sink = true;
+ find_default = true;
+ } else if (spa_streq(name, DEFAULT_SOURCE)) {
+ if (sink)
+ return NULL;
+ find_default = true;
+ } else if (spa_streq(name, DEFAULT_SINK)) {
+ if (!sink)
+ return NULL;
+ find_default = true;
+ } else if (spa_atou32(name, &index, 0)) {
+ name = NULL;
+ }
+ }
+ if (name == NULL && (index == SPA_ID_INVALID || index == 0))
+ find_default = true;
+
+ if (find_default) {
+ name = get_default(client, sink);
+ index = SPA_ID_INVALID;
+ }
+
+ if (name != NULL) {
+ if (spa_strendswith(name, ".monitor")) {
+ name = strndupa(name, strlen(name)-8);
+ monitor = true;
+ }
+ } else if (index == SPA_ID_INVALID)
+ return NULL;
+
+
+ spa_zero(sel);
+ sel.type = sink ?
+ pw_manager_object_is_sink :
+ pw_manager_object_is_source_or_monitor;
+ sel.index = index;
+ sel.key = PW_KEY_NODE_NAME;
+ sel.value = name;
+
+ o = select_object(client->manager, &sel);
+ if (o != NULL) {
+ if (!sink && pw_manager_object_is_monitor(o))
+ monitor = true;
+ }
+ if (is_monitor)
+ *is_monitor = monitor;
+
+ return o;
+}
+
+static void sample_play_finish(struct pending_sample *ps)
+{
+ struct client *client = ps->client;
+ pending_sample_free(ps);
+ client_unref(client);
+}
+
+static void sample_play_ready_reply(void *data, struct client *client, uint32_t tag)
+{
+ struct pending_sample *ps = data;
+ struct message *reply;
+ uint32_t index = id_to_index(client->manager, ps->play->id);
+
+ pw_log_info("[%s] PLAY_SAMPLE tag:%u index:%u",
+ client->name, ps->tag, index);
+
+ ps->ready = true;
+
+ reply = reply_new(client, ps->tag);
+ if (client->version >= 13)
+ message_put(reply,
+ TAG_U32, index,
+ TAG_INVALID);
+
+ client_queue_message(client, reply);
+
+ if (ps->done)
+ sample_play_finish(ps);
+}
+
+static void sample_play_ready(void *data, uint32_t id)
+{
+ struct pending_sample *ps = data;
+ struct client *client = ps->client;
+ operation_new_cb(client, ps->tag, sample_play_ready_reply, ps);
+}
+
+static void on_sample_done(void *obj, void *data, int res, uint32_t id)
+{
+ struct pending_sample *ps = obj;
+ ps->done = true;
+ if (ps->ready)
+ sample_play_finish(ps);
+}
+
+static void sample_play_done(void *data, int res)
+{
+ struct pending_sample *ps = data;
+ struct client *client = ps->client;
+ struct impl *impl = client->impl;
+
+ if (res < 0)
+ reply_error(client, COMMAND_PLAY_SAMPLE, ps->tag, res);
+ else
+ pw_log_info("[%s] PLAY_SAMPLE done tag:%u", client->name, ps->tag);
+
+ pw_work_queue_add(impl->work_queue, ps, 0,
+ on_sample_done, client);
+}
+
+static const struct sample_play_events sample_play_events = {
+ VERSION_SAMPLE_PLAY_EVENTS,
+ .ready = sample_play_ready,
+ .done = sample_play_done,
+};
+
+static int do_play_sample(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ uint32_t sink_index, volume;
+ struct sample *sample;
+ struct sample_play *play;
+ const char *sink_name, *name;
+ struct pw_properties *props = NULL;
+ struct pending_sample *ps;
+ struct pw_manager_object *o;
+ int res;
+
+ if ((props = pw_properties_new(NULL, NULL)) == NULL)
+ goto error_errno;
+
+ if ((res = message_get(m,
+ TAG_U32, &sink_index,
+ TAG_STRING, &sink_name,
+ TAG_U32, &volume,
+ TAG_STRING, &name,
+ TAG_INVALID)) < 0)
+ goto error_proto;
+
+ if (client->version >= 13) {
+ if ((res = message_get(m,
+ TAG_PROPLIST, props,
+ TAG_INVALID)) < 0)
+ goto error_proto;
+
+ }
+ pw_log_info("[%s] %s tag:%u sink_index:%u sink_name:%s name:%s",
+ client->name, commands[command].name, tag,
+ sink_index, sink_name, name);
+
+ pw_properties_update(props, &client->props->dict);
+
+ if (sink_index != SPA_ID_INVALID && sink_name != NULL)
+ goto error_inval;
+
+ o = find_device(client, sink_index, sink_name, PW_DIRECTION_OUTPUT, NULL);
+ if (o == NULL)
+ goto error_noent;
+
+ sample = find_sample(impl, SPA_ID_INVALID, name);
+ if (sample == NULL)
+ goto error_noent;
+
+ pw_properties_setf(props, PW_KEY_TARGET_OBJECT, "%"PRIu64, o->serial);
+
+ play = sample_play_new(client->core, sample, props, sizeof(struct pending_sample));
+ props = NULL;
+ if (play == NULL)
+ goto error_errno;
+
+ ps = play->user_data;
+ ps->client = client;
+ ps->play = play;
+ ps->tag = tag;
+ sample_play_add_listener(play, &ps->listener, &sample_play_events, ps);
+ spa_list_append(&client->pending_samples, &ps->link);
+ client->ref++;
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ goto error;
+error_proto:
+ res = -EPROTO;
+ goto error;
+error_inval:
+ res = -EINVAL;
+ goto error;
+error_noent:
+ res = -ENOENT;
+ goto error;
+error:
+ pw_properties_free(props);
+ return res;
+}
+
+static int do_remove_sample(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ const char *name;
+ struct sample *sample;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u name:%s",
+ client->name, commands[command].name, tag,
+ name);
+ if (name == NULL)
+ return -EINVAL;
+ if ((sample = find_sample(impl, SPA_ID_INVALID, name)) == NULL)
+ return -ENOENT;
+
+ broadcast_subscribe_event(impl,
+ SUBSCRIPTION_MASK_SAMPLE_CACHE,
+ SUBSCRIPTION_EVENT_REMOVE |
+ SUBSCRIPTION_EVENT_SAMPLE_CACHE,
+ sample->index);
+
+ pw_map_remove(&impl->samples, sample->index);
+ sample->index = SPA_ID_INVALID;
+
+ sample_unref(sample);
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_cork_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ bool cork;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_BOOLEAN, &cork,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u channel:%u cork:%s",
+ client->name, commands[command].name, tag,
+ channel, cork ? "yes" : "no");
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ stream->corked = cork;
+ stream_set_paused(stream, cork, "cork request");
+ if (cork) {
+ stream->is_underrun = true;
+ } else {
+ stream->playing_for = 0;
+ stream->underrun_for = -1;
+ stream_send_request(stream);
+ }
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_flush_trigger_prebuf_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u channel:%u",
+ client->name, commands[command].name, tag, channel);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ switch (command) {
+ case COMMAND_FLUSH_PLAYBACK_STREAM:
+ case COMMAND_FLUSH_RECORD_STREAM:
+ stream_flush(stream);
+ break;
+ case COMMAND_TRIGGER_PLAYBACK_STREAM:
+ case COMMAND_PREBUF_PLAYBACK_STREAM:
+ if (stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+ if (command == COMMAND_TRIGGER_PLAYBACK_STREAM)
+ stream->in_prebuf = false;
+ else if (stream->attr.prebuf > 0)
+ stream->in_prebuf = true;
+ stream_send_request(stream);
+ break;
+ default:
+ return -EINVAL;
+ }
+ return reply_simple_ack(client, tag);
+}
+
+static int set_node_volume_mute(struct pw_manager_object *o,
+ struct volume *vol, bool *mute, bool is_monitor)
+{
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[1];
+ struct spa_pod *param;
+ uint32_t volprop, muteprop;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ if (is_monitor) {
+ volprop = SPA_PROP_monitorVolumes;
+ muteprop = SPA_PROP_monitorMute;
+ } else {
+ volprop = SPA_PROP_channelVolumes;
+ muteprop = SPA_PROP_mute;
+ }
+
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ if (vol)
+ spa_pod_builder_add(&b,
+ volprop, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float,
+ vol->channels,
+ vol->values), 0);
+ if (mute)
+ spa_pod_builder_add(&b,
+ muteprop, SPA_POD_Bool(*mute), 0);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_node_set_param((struct pw_node*)o->proxy,
+ SPA_PARAM_Props, 0, param);
+ return 0;
+}
+
+static int set_card_volume_mute_delay(struct pw_manager_object *o, uint32_t port_index,
+ uint32_t device_id, struct volume *vol, bool *mute, int64_t *latency_offset)
+{
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[2];
+ struct spa_pod *param;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route);
+ spa_pod_builder_add(&b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(port_index),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(device_id),
+ 0);
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_props, 0);
+ spa_pod_builder_push_object(&b, &f[1],
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ if (vol)
+ spa_pod_builder_add(&b,
+ SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float,
+ vol->channels,
+ vol->values), 0);
+ if (mute)
+ spa_pod_builder_add(&b,
+ SPA_PROP_mute, SPA_POD_Bool(*mute), 0);
+ if (latency_offset)
+ spa_pod_builder_add(&b,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(*latency_offset), 0);
+ spa_pod_builder_pop(&b, &f[1]);
+ spa_pod_builder_prop(&b, SPA_PARAM_ROUTE_save, 0);
+ spa_pod_builder_bool(&b, true);
+ param = spa_pod_builder_pop(&b, &f[0]);
+
+ pw_device_set_param((struct pw_device*)o->proxy,
+ SPA_PARAM_Route, 0, param);
+ return 0;
+}
+
+static int set_card_port(struct pw_manager_object *o, uint32_t device_id,
+ uint32_t port_index)
+{
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ pw_device_set_param((struct pw_device*)o->proxy,
+ SPA_PARAM_Route, 0,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(port_index),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(device_id),
+ SPA_PARAM_ROUTE_save, SPA_POD_Bool(true)));
+
+ return 0;
+}
+
+static int do_set_stream_volume(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ uint32_t index;
+ struct stream *stream;
+ struct volume volume;
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_CVOLUME, &volume,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u",
+ client->name, commands[command].name, tag, index);
+
+ stream = find_stream(client, index);
+ if (stream != NULL) {
+
+ if (volume_compare(&stream->volume, &volume) == 0)
+ goto done;
+
+ pw_stream_set_control(stream->stream,
+ SPA_PROP_channelVolumes, volume.channels, volume.values,
+ 0);
+ } else {
+ struct selector sel;
+ struct pw_manager_object *o;
+
+ spa_zero(sel);
+ sel.index = index;
+ if (command == COMMAND_SET_SINK_INPUT_VOLUME)
+ sel.type = pw_manager_object_is_sink_input;
+ else
+ sel.type = pw_manager_object_is_source_output;
+
+ o = select_object(manager, &sel);
+ if (o == NULL)
+ return -ENOENT;
+
+ if ((res = set_node_volume_mute(o, &volume, NULL, false)) < 0)
+ return res;
+ }
+done:
+ return operation_new(client, tag);
+}
+
+static int do_set_stream_mute(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ uint32_t index;
+ struct stream *stream;
+ int res;
+ bool mute;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_BOOLEAN, &mute,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] DO_SET_STREAM_MUTE tag:%u index:%u mute:%u",
+ client->name, tag, index, mute);
+
+ stream = find_stream(client, index);
+ if (stream != NULL) {
+ float val;
+
+ if (stream->muted == mute)
+ goto done;
+
+ val = mute ? 1.0f : 0.0f;
+ pw_stream_set_control(stream->stream,
+ SPA_PROP_mute, 1, &val,
+ 0);
+ } else {
+ struct selector sel;
+ struct pw_manager_object *o;
+
+ spa_zero(sel);
+ sel.index = index;
+ if (command == COMMAND_SET_SINK_INPUT_MUTE)
+ sel.type = pw_manager_object_is_sink_input;
+ else
+ sel.type = pw_manager_object_is_source_output;
+
+ o = select_object(manager, &sel);
+ if (o == NULL)
+ return -ENOENT;
+
+ if ((res = set_node_volume_mute(o, NULL, &mute, false)) < 0)
+ return res;
+ }
+done:
+ return operation_new(client, tag);
+}
+
+static int do_set_volume(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct pw_node_info *info;
+ uint32_t index, card_id = SPA_ID_INVALID;
+ const char *name, *str;
+ struct volume volume;
+ struct pw_manager_object *o, *card = NULL;
+ int res;
+ struct device_info dev_info;
+ enum pw_direction direction;
+ bool is_monitor;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_CVOLUME, &volume,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s",
+ client->name, commands[command].name, tag, index, name);
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ if (command == COMMAND_SET_SINK_VOLUME)
+ direction = PW_DIRECTION_OUTPUT;
+ else
+ direction = PW_DIRECTION_INPUT;
+
+ o = find_device(client, index, name, direction == PW_DIRECTION_OUTPUT, &is_monitor);
+ if (o == NULL || (info = o->info) == NULL || info->props == NULL)
+ return -ENOENT;
+
+ dev_info = DEVICE_INFO_INIT(direction);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ collect_device_info(o, card, &dev_info, is_monitor, &impl->defs);
+
+ if (dev_info.have_volume &&
+ volume_compare(&dev_info.volume_info.volume, &volume) == 0)
+ goto done;
+
+ if (card != NULL && !is_monitor && dev_info.active_port != SPA_ID_INVALID)
+ res = set_card_volume_mute_delay(card, dev_info.active_port,
+ dev_info.device, &volume, NULL, NULL);
+ else
+ res = set_node_volume_mute(o, &volume, NULL, is_monitor);
+
+ if (res < 0)
+ return res;
+
+done:
+ return operation_new(client, tag);
+}
+
+static int do_set_mute(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct pw_node_info *info;
+ uint32_t index, card_id = SPA_ID_INVALID;
+ const char *name, *str;
+ bool mute;
+ struct pw_manager_object *o, *card = NULL;
+ int res;
+ struct device_info dev_info;
+ enum pw_direction direction;
+ bool is_monitor;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_BOOLEAN, &mute,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s mute:%d",
+ client->name, commands[command].name, tag, index, name, mute);
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ if (command == COMMAND_SET_SINK_MUTE)
+ direction = PW_DIRECTION_OUTPUT;
+ else
+ direction = PW_DIRECTION_INPUT;
+
+ o = find_device(client, index, name, direction == PW_DIRECTION_OUTPUT, &is_monitor);
+ if (o == NULL || (info = o->info) == NULL || info->props == NULL)
+ return -ENOENT;
+
+ dev_info = DEVICE_INFO_INIT(direction);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ collect_device_info(o, card, &dev_info, is_monitor, &impl->defs);
+
+ if (dev_info.have_volume &&
+ dev_info.volume_info.mute == mute)
+ goto done;
+
+ if (card != NULL && !is_monitor && dev_info.active_port != SPA_ID_INVALID)
+ res = set_card_volume_mute_delay(card, dev_info.active_port,
+ dev_info.device, NULL, &mute, NULL);
+ else
+ res = set_node_volume_mute(o, NULL, &mute, is_monitor);
+
+ if (res < 0)
+ return res;
+done:
+ return operation_new(client, tag);
+}
+
+static int do_set_port(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_node_info *info;
+ uint32_t index, card_id = SPA_ID_INVALID, device_id = SPA_ID_INVALID;
+ uint32_t port_index = SPA_ID_INVALID;
+ const char *name, *str, *port_name;
+ struct pw_manager_object *o, *card = NULL;
+ int res;
+ enum pw_direction direction;
+
+ if ((res = message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_STRING, &port_name,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s port:%s",
+ client->name, commands[command].name, tag, index, name, port_name);
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ if (command == COMMAND_SET_SINK_PORT)
+ direction = PW_DIRECTION_OUTPUT;
+ else
+ direction = PW_DIRECTION_INPUT;
+
+ o = find_device(client, index, name, direction == PW_DIRECTION_OUTPUT, NULL);
+ if (o == NULL || (info = o->info) == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ device_id = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ if (card == NULL || device_id == SPA_ID_INVALID)
+ return -ENOENT;
+
+ port_index = find_port_index(card, direction, port_name);
+ if (port_index == SPA_ID_INVALID)
+ return -ENOENT;
+
+ if ((res = set_card_port(card, device_id, port_index)) < 0)
+ return res;
+
+ return operation_new(client, tag);
+}
+
+static int do_set_port_latency_offset(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ const char *port_name = NULL;
+ struct pw_manager_object *card;
+ struct selector sel;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct port_info *port_info;
+ int64_t offset;
+ int64_t value;
+ int res;
+ uint32_t n_ports;
+ size_t i;
+
+ spa_zero(sel);
+ sel.key = PW_KEY_DEVICE_NAME;
+ sel.type = pw_manager_object_is_card;
+
+ if ((res = message_get(m,
+ TAG_U32, &sel.index,
+ TAG_STRING, &sel.value,
+ TAG_STRING, &port_name,
+ TAG_S64, &offset,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u card_name:%s port_name:%s offset:%"PRIi64,
+ client->name, commands[command].name, tag, sel.index, sel.value, port_name, offset);
+
+ if ((sel.index == SPA_ID_INVALID && sel.value == NULL) ||
+ (sel.index != SPA_ID_INVALID && sel.value != NULL))
+ return -EINVAL;
+ if (port_name == NULL)
+ return -EINVAL;
+
+ value = offset * 1000; /* to nsec */
+
+ if ((card = select_object(manager, &sel)) == NULL)
+ return -ENOENT;
+
+ collect_card_info(card, &card_info);
+ port_info = alloca(card_info.n_ports * sizeof(*port_info));
+ card_info.active_profile = SPA_ID_INVALID;
+ n_ports = collect_port_info(card, &card_info, NULL, port_info);
+
+ /* Set offset on all devices of the port */
+ res = -ENOENT;
+ for (i = 0; i < n_ports; i++) {
+ struct port_info *pi = &port_info[i];
+ size_t j;
+
+ if (!spa_streq(pi->name, port_name))
+ continue;
+
+ res = 0;
+ for (j = 0; j < pi->n_devices; ++j) {
+ res = set_card_volume_mute_delay(card, pi->index, pi->devices[j], NULL, NULL, &value);
+ if (res < 0)
+ break;
+ }
+
+ if (res < 0)
+ break;
+
+ return operation_new(client, tag);
+ }
+
+ return res;
+}
+
+static int do_set_stream_name(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+ const char *name = NULL;
+ struct spa_dict_item items[1];
+ int res;
+
+ if ((res = message_get(m,
+ TAG_U32, &channel,
+ TAG_STRING, &name,
+ TAG_INVALID)) < 0)
+ return -EPROTO;
+
+ if (name == NULL)
+ return -EINVAL;
+
+ pw_log_info("[%s] SET_STREAM_NAME tag:%u channel:%d name:%s",
+ client->name, tag, channel, name);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, name);
+ pw_stream_update_properties(stream->stream,
+ &SPA_DICT_INIT(items, 1));
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_update_proplist(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel, mode;
+ struct stream *stream;
+ struct pw_properties *props;
+ int res;
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ return -errno;
+
+ if (command != COMMAND_UPDATE_CLIENT_PROPLIST) {
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ } else {
+ channel = SPA_ID_INVALID;
+ }
+
+ pw_log_info("[%s] %s tag:%u channel:%d",
+ client->name, commands[command].name, tag, channel);
+
+ if (message_get(m,
+ TAG_U32, &mode,
+ TAG_PROPLIST, props,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ if (command != COMMAND_UPDATE_CLIENT_PROPLIST) {
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ goto error_noentity;
+
+ pw_stream_update_properties(stream->stream, &props->dict);
+ } else {
+ if (pw_properties_update(client->props, &props->dict) > 0) {
+ client_update_quirks(client);
+ client->name = pw_properties_get(client->props, PW_KEY_APP_NAME);
+ pw_core_update_properties(client->core, &client->props->dict);
+ }
+ }
+ res = reply_simple_ack(client, tag);
+exit:
+ pw_properties_free(props);
+ return res;
+
+error_protocol:
+ res = -EPROTO;
+ goto exit;
+error_noentity:
+ res = -ENOENT;
+ goto exit;
+}
+
+static int do_remove_proplist(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t i, channel;
+ struct stream *stream;
+ struct pw_properties *props;
+ struct spa_dict dict;
+ struct spa_dict_item *items;
+ int res;
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ return -errno;
+
+ if (command != COMMAND_REMOVE_CLIENT_PROPLIST) {
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ } else {
+ channel = SPA_ID_INVALID;
+ }
+
+ pw_log_info("[%s] %s tag:%u channel:%d",
+ client->name, commands[command].name, tag, channel);
+
+ while (true) {
+ const char *key;
+
+ if (message_get(m,
+ TAG_STRING, &key,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ if (key == NULL)
+ break;
+ pw_properties_set(props, key, key);
+ }
+
+ dict.n_items = props->dict.n_items;
+ dict.items = items = alloca(sizeof(struct spa_dict_item) * dict.n_items);
+ for (i = 0; i < dict.n_items; i++) {
+ items[i].key = props->dict.items[i].key;
+ items[i].value = NULL;
+ }
+
+ if (command != COMMAND_UPDATE_CLIENT_PROPLIST) {
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ goto error_noentity;
+
+ pw_stream_update_properties(stream->stream, &dict);
+ } else {
+ pw_core_update_properties(client->core, &dict);
+ }
+ res = reply_simple_ack(client, tag);
+exit:
+ pw_properties_free(props);
+ return res;
+
+error_protocol:
+ res = -EPROTO;
+ goto exit;
+error_noentity:
+ res = -ENOENT;
+ goto exit;
+}
+
+
+static int do_get_server_info(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct pw_core_info *info = manager ? manager->info : NULL;
+ char name[256];
+ struct message *reply;
+
+ pw_log_info("[%s] GET_SERVER_INFO tag:%u", client->name, tag);
+
+ snprintf(name, sizeof(name), "PulseAudio (on PipeWire %s)", pw_get_library_version());
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_STRING, name,
+ TAG_STRING, "15.0.0",
+ TAG_STRING, pw_get_user_name(),
+ TAG_STRING, pw_get_host_name(),
+ TAG_SAMPLE_SPEC, &impl->defs.sample_spec,
+ TAG_STRING, manager ? get_default(client, true) : "", /* default sink name */
+ TAG_STRING, manager ? get_default(client, false) : "", /* default source name */
+ TAG_U32, info ? info->cookie : 0, /* cookie */
+ TAG_INVALID);
+
+ if (client->version >= 15) {
+ message_put(reply,
+ TAG_CHANNEL_MAP, &impl->defs.channel_map,
+ TAG_INVALID);
+ }
+ return client_queue_message(client, reply);
+}
+
+static int do_stat(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+
+ pw_log_info("[%s] STAT tag:%u", client->name, tag);
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, impl->stat.n_allocated, /* n_allocated */
+ TAG_U32, impl->stat.allocated, /* allocated size */
+ TAG_U32, impl->stat.n_accumulated, /* n_accumulated */
+ TAG_U32, impl->stat.accumulated, /* accumulated_size */
+ TAG_U32, impl->stat.sample_cache, /* sample cache size */
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int do_lookup(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct message *reply;
+ struct pw_manager_object *o;
+ const char *name;
+ bool is_sink = command == COMMAND_LOOKUP_SINK;
+ bool is_monitor;
+
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] LOOKUP tag:%u name:'%s'", client->name, tag, name);
+
+ if ((o = find_device(client, SPA_ID_INVALID, name, is_sink, &is_monitor)) == NULL)
+ return -ENOENT;
+
+ reply = reply_new(client, tag);
+ message_put(reply,
+ TAG_U32, o->index,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+static int do_drain_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] DRAIN tag:%u channel:%d", client->name, tag, channel);
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+
+ stream->drain_tag = tag;
+ stream->draining = true;
+ stream_set_paused(stream, false, "drain start");
+
+ return 0;
+}
+
+static int fill_client_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct pw_client_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ const char *str;
+ uint32_t module_id = SPA_ID_INVALID;
+
+ if (!pw_manager_object_is_client(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = (uint32_t)atoi(str);
+
+ message_put(m,
+ TAG_U32, o->index, /* client index */
+ TAG_STRING, pw_properties_get(o->props, PW_KEY_APP_NAME),
+ TAG_U32, id_to_index(manager, module_id), /* module index */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_INVALID);
+ if (client->version >= 13) {
+ message_put(m,
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static int fill_module_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct pw_module_info *info = o->info;
+
+ if (!pw_manager_object_is_module(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ message_put(m,
+ TAG_U32, o->index, /* module index */
+ TAG_STRING, info->name,
+ TAG_STRING, info->args,
+ TAG_U32, -1, /* n_used */
+ TAG_INVALID);
+
+ if (client->version < 15) {
+ message_put(m,
+ TAG_BOOLEAN, false, /* auto unload deprecated */
+ TAG_INVALID);
+ }
+ if (client->version >= 15) {
+ message_put(m,
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static int fill_ext_module_info(struct client *client, struct message *m,
+ struct module *module)
+{
+ message_put(m,
+ TAG_U32, module->index, /* module index */
+ TAG_STRING, module->info->name,
+ TAG_STRING, module->args,
+ TAG_U32, -1, /* n_used */
+ TAG_INVALID);
+
+ if (client->version < 15) {
+ message_put(m,
+ TAG_BOOLEAN, false, /* auto unload deprecated */
+ TAG_INVALID);
+ }
+ if (client->version >= 15) {
+ message_put(m,
+ TAG_PROPLIST, module->info->properties,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static int64_t get_port_latency_offset(struct client *client, struct pw_manager_object *card, struct port_info *pi)
+{
+ struct pw_manager *m = client->manager;
+ struct pw_manager_object *o;
+ size_t j;
+
+ /*
+ * The latency offset is a property of nodes in PipeWire, so we look it up on the
+ * nodes. We'll return the latency offset of the first node in the port.
+ *
+ * This is also because we need to be consistent with
+ * send_latency_offset_subscribe_event, which sends events on node changes. The
+ * route data might not be updated yet when these events arrive.
+ */
+ for (j = 0; j < pi->n_devices; ++j) {
+ spa_list_for_each(o, &m->object_list, link) {
+ const char *str;
+ uint32_t card_id = SPA_ID_INVALID;
+ uint32_t device_id = SPA_ID_INVALID;
+ struct pw_node_info *info;
+
+ if (o->creating || o->removing)
+ continue;
+ if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source_or_monitor(o))
+ continue;
+ if ((info = o->info) == NULL || info->props == NULL)
+ continue;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if (card_id != card->id)
+ continue;
+
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ device_id = (uint32_t)atoi(str);
+
+ if (device_id == pi->devices[j])
+ return get_node_latency_offset(o);
+ }
+ }
+
+ return 0LL;
+}
+
+static int fill_card_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_device_info *info = o->info;
+ const char *str, *drv_name;
+ uint32_t module_id = SPA_ID_INVALID, n_profiles, n;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct profile_info *profile_info;
+
+ if (!pw_manager_object_is_card(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = (uint32_t)atoi(str);
+
+ drv_name = spa_dict_lookup(info->props, PW_KEY_DEVICE_API);
+ if (drv_name && spa_streq("bluez5", drv_name))
+ drv_name = "module-bluez5-device.c"; /* blueman needs this */
+
+ message_put(m,
+ TAG_U32, o->index, /* card index */
+ TAG_STRING, spa_dict_lookup(info->props, PW_KEY_DEVICE_NAME),
+ TAG_U32, id_to_index(manager, module_id),
+ TAG_STRING, drv_name,
+ TAG_INVALID);
+
+ collect_card_info(o, &card_info);
+
+ message_put(m,
+ TAG_U32, card_info.n_profiles, /* n_profiles */
+ TAG_INVALID);
+
+ profile_info = alloca(card_info.n_profiles * sizeof(*profile_info));
+ n_profiles = collect_profile_info(o, &card_info, profile_info);
+
+ for (n = 0; n < n_profiles; n++) {
+ struct profile_info *pi = &profile_info[n];
+
+ message_put(m,
+ TAG_STRING, pi->name, /* profile name */
+ TAG_STRING, pi->description, /* profile description */
+ TAG_U32, pi->n_sinks, /* n_sinks */
+ TAG_U32, pi->n_sources, /* n_sources */
+ TAG_U32, pi->priority, /* priority */
+ TAG_INVALID);
+
+ if (client->version >= 29) {
+ message_put(m,
+ TAG_U32, pi->available != SPA_PARAM_AVAILABILITY_no, /* available */
+ TAG_INVALID);
+ }
+ }
+ message_put(m,
+ TAG_STRING, card_info.active_profile_name, /* active profile name */
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+
+ if (client->version >= 26) {
+ uint32_t n_ports;
+ struct port_info *port_info, *pi;
+
+ port_info = alloca(card_info.n_ports * sizeof(*port_info));
+ card_info.active_profile = SPA_ID_INVALID;
+ n_ports = collect_port_info(o, &card_info, NULL, port_info);
+
+ message_put(m,
+ TAG_U32, n_ports, /* n_ports */
+ TAG_INVALID);
+
+ for (n = 0; n < n_ports; n++) {
+ struct spa_dict_item *items;
+ struct spa_dict *pdict = NULL, dict;
+ uint32_t i, pi_n_profiles;
+
+ pi = &port_info[n];
+
+ if (pi->info && pi->n_props > 0) {
+ items = alloca(pi->n_props * sizeof(*items));
+ dict.items = items;
+ pdict = collect_props(pi->info, &dict);
+ }
+
+ message_put(m,
+ TAG_STRING, pi->name, /* port name */
+ TAG_STRING, pi->description, /* port description */
+ TAG_U32, pi->priority, /* port priority */
+ TAG_U32, pi->available, /* port available */
+ TAG_U8, pi->direction == SPA_DIRECTION_INPUT ? 2 : 1, /* port direction */
+ TAG_PROPLIST, pdict, /* port proplist */
+ TAG_INVALID);
+
+ pi_n_profiles = SPA_MIN(pi->n_profiles, n_profiles);
+ if (pi->n_profiles != pi_n_profiles) {
+ /* libpulse assumes port profile array size <= n_profiles */
+ pw_log_error("%p: card %d port %d profiles inconsistent (%d < %d)",
+ client->impl, o->id, n, n_profiles, pi->n_profiles);
+ }
+
+ message_put(m,
+ TAG_U32, pi_n_profiles, /* n_profiles */
+ TAG_INVALID);
+
+ for (i = 0; i < pi_n_profiles; i++) {
+ uint32_t j;
+ const char *name = "off";
+
+ for (j = 0; j < n_profiles; ++j) {
+ if (profile_info[j].index == pi->profiles[i]) {
+ name = profile_info[j].name;
+ break;
+ }
+ }
+
+ message_put(m,
+ TAG_STRING, name, /* profile name */
+ TAG_INVALID);
+ }
+ if (client->version >= 27) {
+ int64_t latency_offset = get_port_latency_offset(client, o, pi);
+ message_put(m,
+ TAG_S64, latency_offset / 1000, /* port latency offset */
+ TAG_INVALID);
+ }
+ if (client->version >= 34) {
+ message_put(m,
+ TAG_STRING, pi->availability_group, /* available group */
+ TAG_U32, pi->type, /* port type */
+ TAG_INVALID);
+ }
+ }
+ }
+ return 0;
+}
+
+static int fill_sink_info_proplist(struct message *m, const struct spa_dict *sink_props,
+ const struct pw_manager_object *card)
+{
+ struct pw_device_info *card_info = card ? card->info : NULL;
+ struct pw_properties *props = NULL;
+
+ if (card_info && card_info->props) {
+ props = pw_properties_new_dict(sink_props);
+ if (props == NULL)
+ return -ENOMEM;
+
+ pw_properties_add(props, card_info->props);
+ sink_props = &props->dict;
+ }
+ message_put(m, TAG_PROPLIST, sink_props, TAG_INVALID);
+
+ pw_properties_free(props);
+
+ return 0;
+}
+
+static bool validate_device_info(struct device_info *dev_info)
+{
+ return sample_spec_valid(&dev_info->ss) &&
+ channel_map_valid(&dev_info->map) &&
+ volume_valid(&dev_info->volume_info.volume);
+}
+
+static int fill_sink_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = client->impl;
+ struct pw_node_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ const char *name, *desc, *str;
+ char *monitor_name = NULL;
+ uint32_t module_id = SPA_ID_INVALID;
+ uint32_t card_id = SPA_ID_INVALID;
+ struct pw_manager_object *card = NULL;
+ uint32_t flags;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct device_info dev_info = DEVICE_INFO_INIT(PW_DIRECTION_OUTPUT);
+ size_t size;
+
+ if (!pw_manager_object_is_sink(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if ((desc = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)) == NULL)
+ desc = name ? name : "Unknown";
+ if (name == NULL)
+ name = "unknown";
+
+ size = strlen(name) + 10;
+ monitor_name = alloca(size);
+ if (pw_manager_object_is_source(o))
+ snprintf(monitor_name, size, "%s", name);
+ else
+ snprintf(monitor_name, size, "%s.monitor", name);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = id_to_index(manager, (uint32_t)atoi(str));
+ if (module_id == SPA_ID_INVALID &&
+ (str = spa_dict_lookup(info->props, "pulse.module.id")) != NULL)
+ module_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ if (card)
+ collect_card_info(card, &card_info);
+
+ collect_device_info(o, card, &dev_info, false, &impl->defs);
+
+ if (!validate_device_info(&dev_info)) {
+ pw_log_warn("%d: sink not ready: sample:%d map:%d volume:%d",
+ o->id, sample_spec_valid(&dev_info.ss),
+ channel_map_valid(&dev_info.map),
+ volume_valid(&dev_info.volume_info.volume));
+ return -ENOENT;
+ }
+
+ flags = SINK_LATENCY | SINK_DYNAMIC_LATENCY | SINK_DECIBEL_VOLUME;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_API)) != NULL)
+ flags |= SINK_HARDWARE;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_NODE_NETWORK)) != NULL)
+ flags |= SINK_NETWORK;
+ if (SPA_FLAG_IS_SET(dev_info.volume_info.flags, VOLUME_HW_VOLUME))
+ flags |= SINK_HW_VOLUME_CTRL;
+ if (SPA_FLAG_IS_SET(dev_info.volume_info.flags, VOLUME_HW_MUTE))
+ flags |= SINK_HW_MUTE_CTRL;
+ if (dev_info.have_iec958codecs)
+ flags |= SINK_SET_FORMATS;
+
+ if (client->quirks & QUIRK_FORCE_S16_FORMAT)
+ dev_info.ss.format = SPA_AUDIO_FORMAT_S16;
+
+ message_put(m,
+ TAG_U32, o->index, /* sink index */
+ TAG_STRING, name,
+ TAG_STRING, desc,
+ TAG_SAMPLE_SPEC, &dev_info.ss,
+ TAG_CHANNEL_MAP, &dev_info.map,
+ TAG_U32, module_id, /* module index */
+ TAG_CVOLUME, &dev_info.volume_info.volume,
+ TAG_BOOLEAN, dev_info.volume_info.mute,
+ TAG_U32, o->index, /* monitor source index */
+ TAG_STRING, monitor_name, /* monitor source name */
+ TAG_USEC, 0LL, /* latency */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_U32, flags, /* flags */
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ int res;
+ if ((res = fill_sink_info_proplist(m, info->props, card)) < 0)
+ return res;
+ message_put(m,
+ TAG_USEC, 0LL, /* requested latency */
+ TAG_INVALID);
+ }
+ if (client->version >= 15) {
+ bool is_linked = collect_is_linked(manager, o->id, SPA_DIRECTION_INPUT);
+ int state = node_state(info->state);
+
+ /* running with nothing linked is probably the monitor that is
+ * keeping this sink busy */
+ if (state == STATE_RUNNING && !is_linked)
+ state = STATE_IDLE;
+
+ message_put(m,
+ TAG_VOLUME, dev_info.volume_info.base, /* base volume */
+ TAG_U32, state, /* state */
+ TAG_U32, dev_info.volume_info.steps, /* n_volume_steps */
+ TAG_U32, card ? card->index : SPA_ID_INVALID, /* card index */
+ TAG_INVALID);
+ }
+ if (client->version >= 16) {
+ uint32_t n_ports, n;
+ struct port_info *port_info, *pi;
+
+ port_info = alloca(card_info.n_ports * sizeof(*port_info));
+ n_ports = collect_port_info(card, &card_info, &dev_info, port_info);
+
+ message_put(m,
+ TAG_U32, n_ports, /* n_ports */
+ TAG_INVALID);
+ for (n = 0; n < n_ports; n++) {
+ pi = &port_info[n];
+ message_put(m,
+ TAG_STRING, pi->name, /* name */
+ TAG_STRING, pi->description, /* description */
+ TAG_U32, pi->priority, /* priority */
+ TAG_INVALID);
+ if (client->version >= 24) {
+ message_put(m,
+ TAG_U32, pi->available, /* available */
+ TAG_INVALID);
+ }
+ if (client->version >= 34) {
+ message_put(m,
+ TAG_STRING, pi->availability_group, /* availability_group */
+ TAG_U32, pi->type, /* type */
+ TAG_INVALID);
+ }
+ }
+ message_put(m,
+ TAG_STRING, dev_info.active_port_name, /* active port name */
+ TAG_INVALID);
+ }
+ if (client->version >= 21) {
+ struct pw_manager_param *p;
+ struct format_info info[32];
+ uint32_t i, n_info = 0;
+
+ spa_list_for_each(p, &o->param_list, link) {
+ uint32_t index = 0;
+
+ if (p->id != SPA_PARAM_EnumFormat)
+ continue;
+
+ while (n_info < SPA_N_ELEMENTS(info)) {
+ spa_zero(info[n_info]);
+ if (format_info_from_param(&info[n_info], p->param, index++) < 0)
+ break;
+ if (info[n_info].encoding == ENCODING_ANY ||
+ (info[n_info].encoding == ENCODING_PCM && info[n_info].props != NULL)) {
+ format_info_clear(&info[n_info]);
+ continue;
+ }
+ n_info++;
+ }
+ }
+ message_put(m,
+ TAG_U8, n_info, /* n_formats */
+ TAG_INVALID);
+ for (i = 0; i < n_info; i++) {
+ message_put(m,
+ TAG_FORMAT_INFO, &info[i],
+ TAG_INVALID);
+ format_info_clear(&info[i]);
+ }
+ }
+ return 0;
+}
+
+static int fill_source_info_proplist(struct message *m, const struct spa_dict *source_props,
+ const struct pw_manager_object *card, const bool is_monitor)
+{
+ struct pw_device_info *card_info = card ? card->info : NULL;
+ struct pw_properties *props = NULL;
+
+ if ((card_info && card_info->props) || is_monitor) {
+ props = pw_properties_new_dict(source_props);
+ if (props == NULL)
+ return -ENOMEM;
+
+ if (card_info && card_info->props)
+ pw_properties_add(props, card_info->props);
+
+ if (is_monitor)
+ pw_properties_set(props, PW_KEY_DEVICE_CLASS, "monitor");
+
+ source_props = &props->dict;
+ }
+ message_put(m, TAG_PROPLIST, source_props, TAG_INVALID);
+
+ pw_properties_free(props);
+
+ return 0;
+}
+
+static int fill_source_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = client->impl;
+ struct pw_node_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ bool is_monitor;
+ const char *name, *desc, *str;
+ char *monitor_name = NULL;
+ char *monitor_desc = NULL;
+ uint32_t module_id = SPA_ID_INVALID;
+ uint32_t card_id = SPA_ID_INVALID;
+ struct pw_manager_object *card = NULL;
+ uint32_t flags;
+ struct card_info card_info = CARD_INFO_INIT;
+ struct device_info dev_info = DEVICE_INFO_INIT(PW_DIRECTION_INPUT);
+ size_t size;
+
+ is_monitor = pw_manager_object_is_monitor(o);
+ if ((!pw_manager_object_is_source(o) && !is_monitor) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ if ((desc = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)) == NULL)
+ desc = name ? name : "Unknown";
+ if (name == NULL)
+ name = "unknown";
+
+ size = strlen(name) + 10;
+ monitor_name = alloca(size);
+ snprintf(monitor_name, size, "%s.monitor", name);
+
+ size = strlen(desc) + 20;
+ monitor_desc = alloca(size);
+ snprintf(monitor_desc, size, "Monitor of %s", desc);
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = id_to_index(manager, (uint32_t)atoi(str));
+ if (module_id == SPA_ID_INVALID &&
+ (str = spa_dict_lookup(info->props, "pulse.module.id")) != NULL)
+ module_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_ID)) != NULL)
+ card_id = (uint32_t)atoi(str);
+ if ((str = spa_dict_lookup(info->props, "card.profile.device")) != NULL)
+ dev_info.device = (uint32_t)atoi(str);
+
+ if (card_id != SPA_ID_INVALID) {
+ struct selector sel = { .id = card_id, .type = pw_manager_object_is_card, };
+ card = select_object(manager, &sel);
+ }
+ if (card)
+ collect_card_info(card, &card_info);
+
+ collect_device_info(o, card, &dev_info, is_monitor, &impl->defs);
+
+ if (!validate_device_info(&dev_info)) {
+ pw_log_warn("%d: source not ready: sample:%d map:%d volume:%d",
+ o->id, sample_spec_valid(&dev_info.ss),
+ channel_map_valid(&dev_info.map),
+ volume_valid(&dev_info.volume_info.volume));
+ return -ENOENT;
+ }
+
+ flags = SOURCE_LATENCY | SOURCE_DYNAMIC_LATENCY | SOURCE_DECIBEL_VOLUME;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_DEVICE_API)) != NULL)
+ flags |= SOURCE_HARDWARE;
+ if ((str = spa_dict_lookup(info->props, PW_KEY_NODE_NETWORK)) != NULL)
+ flags |= SOURCE_NETWORK;
+ if (SPA_FLAG_IS_SET(dev_info.volume_info.flags, VOLUME_HW_VOLUME))
+ flags |= SOURCE_HW_VOLUME_CTRL;
+ if (SPA_FLAG_IS_SET(dev_info.volume_info.flags, VOLUME_HW_MUTE))
+ flags |= SOURCE_HW_MUTE_CTRL;
+
+ if (client->quirks & QUIRK_FORCE_S16_FORMAT)
+ dev_info.ss.format = SPA_AUDIO_FORMAT_S16;
+
+ message_put(m,
+ TAG_U32, o->index, /* source index */
+ TAG_STRING, is_monitor ? monitor_name : name,
+ TAG_STRING, is_monitor ? monitor_desc : desc,
+ TAG_SAMPLE_SPEC, &dev_info.ss,
+ TAG_CHANNEL_MAP, &dev_info.map,
+ TAG_U32, module_id, /* module index */
+ TAG_CVOLUME, &dev_info.volume_info.volume,
+ TAG_BOOLEAN, dev_info.volume_info.mute,
+ TAG_U32, is_monitor ? o->index : SPA_ID_INVALID,/* monitor of sink */
+ TAG_STRING, is_monitor ? name : NULL, /* monitor of sink name */
+ TAG_USEC, 0LL, /* latency */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_U32, flags, /* flags */
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ int res;
+ if ((res = fill_source_info_proplist(m, info->props, card, is_monitor)) < 0)
+ return res;
+ message_put(m,
+ TAG_USEC, 0LL, /* requested latency */
+ TAG_INVALID);
+ }
+ if (client->version >= 15) {
+ bool is_linked = collect_is_linked(manager, o->id, SPA_DIRECTION_OUTPUT);
+ int state = node_state(info->state);
+
+ /* running with nothing linked is probably the sink that is
+ * keeping this source busy */
+ if (state == STATE_RUNNING && !is_linked)
+ state = STATE_IDLE;
+
+ message_put(m,
+ TAG_VOLUME, dev_info.volume_info.base, /* base volume */
+ TAG_U32, state, /* state */
+ TAG_U32, dev_info.volume_info.steps, /* n_volume_steps */
+ TAG_U32, card ? card->index : SPA_ID_INVALID, /* card index */
+ TAG_INVALID);
+ }
+ if (client->version >= 16) {
+ uint32_t n_ports, n;
+ struct port_info *port_info, *pi;
+
+ port_info = alloca(card_info.n_ports * sizeof(*port_info));
+ n_ports = collect_port_info(card, &card_info, &dev_info, port_info);
+
+ message_put(m,
+ TAG_U32, n_ports, /* n_ports */
+ TAG_INVALID);
+ for (n = 0; n < n_ports; n++) {
+ pi = &port_info[n];
+ message_put(m,
+ TAG_STRING, pi->name, /* name */
+ TAG_STRING, pi->description, /* description */
+ TAG_U32, pi->priority, /* priority */
+ TAG_INVALID);
+ if (client->version >= 24) {
+ message_put(m,
+ TAG_U32, pi->available, /* available */
+ TAG_INVALID);
+ }
+ if (client->version >= 34) {
+ message_put(m,
+ TAG_STRING, pi->availability_group, /* availability_group */
+ TAG_U32, pi->type, /* type */
+ TAG_INVALID);
+ }
+ }
+ message_put(m,
+ TAG_STRING, dev_info.active_port_name, /* active port name */
+ TAG_INVALID);
+ }
+ if (client->version >= 21) {
+ struct format_info info;
+ spa_zero(info);
+ info.encoding = ENCODING_PCM;
+ message_put(m,
+ TAG_U8, 1, /* n_formats */
+ TAG_FORMAT_INFO, &info,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static const char *get_media_name(struct pw_node_info *info)
+{
+ const char *media_name;
+ media_name = spa_dict_lookup(info->props, PW_KEY_MEDIA_NAME);
+ if (media_name == NULL)
+ media_name = "";
+ return media_name;
+}
+
+static int fill_sink_input_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = client->impl;
+ struct pw_node_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ const char *str;
+ uint32_t module_id = SPA_ID_INVALID, client_id = SPA_ID_INVALID;
+ uint32_t peer_index;
+ struct device_info dev_info = DEVICE_INFO_INIT(PW_DIRECTION_OUTPUT);
+
+ if (!pw_manager_object_is_sink_input(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = id_to_index(manager, (uint32_t)atoi(str));
+ if (module_id == SPA_ID_INVALID &&
+ (str = spa_dict_lookup(info->props, "pulse.module.id")) != NULL)
+ module_id = (uint32_t)atoi(str);
+
+ if (!pw_manager_object_is_virtual(o) &&
+ (str = spa_dict_lookup(info->props, PW_KEY_CLIENT_ID)) != NULL)
+ client_id = (uint32_t)atoi(str);
+
+ collect_device_info(o, NULL, &dev_info, false, &impl->defs);
+
+ if (!validate_device_info(&dev_info))
+ return -ENOENT;
+
+ peer_index = get_temporary_move_target(client, o);
+ if (peer_index == SPA_ID_INVALID) {
+ struct pw_manager_object *peer;
+ peer = find_linked(manager, o->id, PW_DIRECTION_OUTPUT);
+ if (peer && pw_manager_object_is_sink(peer))
+ peer_index = peer->index;
+ else
+ peer_index = SPA_ID_INVALID;
+ }
+
+ message_put(m,
+ TAG_U32, o->index, /* sink_input index */
+ TAG_STRING, get_media_name(info),
+ TAG_U32, module_id, /* module index */
+ TAG_U32, id_to_index(manager, client_id), /* client index */
+ TAG_U32, peer_index, /* sink index */
+ TAG_SAMPLE_SPEC, &dev_info.ss,
+ TAG_CHANNEL_MAP, &dev_info.map,
+ TAG_CVOLUME, &dev_info.volume_info.volume,
+ TAG_USEC, 0LL, /* latency */
+ TAG_USEC, 0LL, /* sink latency */
+ TAG_STRING, "PipeWire", /* resample method */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_INVALID);
+ if (client->version >= 11)
+ message_put(m,
+ TAG_BOOLEAN, dev_info.volume_info.mute, /* muted */
+ TAG_INVALID);
+ if (client->version >= 13)
+ message_put(m,
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+ if (client->version >= 19)
+ message_put(m,
+ TAG_BOOLEAN, info->state != PW_NODE_STATE_RUNNING, /* corked */
+ TAG_INVALID);
+ if (client->version >= 20)
+ message_put(m,
+ TAG_BOOLEAN, true, /* has_volume */
+ TAG_BOOLEAN, true, /* volume writable */
+ TAG_INVALID);
+ if (client->version >= 21) {
+ struct format_info fi;
+ format_info_from_spec(&fi, &dev_info.ss, &dev_info.map);
+ message_put(m,
+ TAG_FORMAT_INFO, &fi,
+ TAG_INVALID);
+ format_info_clear(&fi);
+ }
+ return 0;
+}
+
+static int fill_source_output_info(struct client *client, struct message *m,
+ struct pw_manager_object *o)
+{
+ struct impl *impl = client->impl;
+ struct pw_node_info *info = o->info;
+ struct pw_manager *manager = client->manager;
+ const char *str;
+ uint32_t module_id = SPA_ID_INVALID, client_id = SPA_ID_INVALID;
+ uint32_t peer_index;
+ struct device_info dev_info = DEVICE_INFO_INIT(PW_DIRECTION_INPUT);
+
+ if (!pw_manager_object_is_source_output(o) || info == NULL || info->props == NULL)
+ return -ENOENT;
+
+ if ((str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID)) != NULL)
+ module_id = id_to_index(manager, (uint32_t)atoi(str));
+ if (module_id == SPA_ID_INVALID &&
+ (str = spa_dict_lookup(info->props, "pulse.module.id")) != NULL)
+ module_id = (uint32_t)atoi(str);
+
+ if (!pw_manager_object_is_virtual(o) &&
+ (str = spa_dict_lookup(info->props, PW_KEY_CLIENT_ID)) != NULL)
+ client_id = (uint32_t)atoi(str);
+
+ collect_device_info(o, NULL, &dev_info, false, &impl->defs);
+
+ if (!validate_device_info(&dev_info))
+ return -ENOENT;
+
+ peer_index = get_temporary_move_target(client, o);
+ if (peer_index == SPA_ID_INVALID) {
+ struct pw_manager_object *peer;
+ peer = find_linked(manager, o->id, PW_DIRECTION_INPUT);
+ if (peer && pw_manager_object_is_source_or_monitor(peer))
+ peer_index = peer->index;
+ else
+ peer_index = SPA_ID_INVALID;
+ }
+
+ message_put(m,
+ TAG_U32, o->index, /* source_output index */
+ TAG_STRING, get_media_name(info),
+ TAG_U32, module_id, /* module index */
+ TAG_U32, id_to_index(manager, client_id), /* client index */
+ TAG_U32, peer_index, /* source index */
+ TAG_SAMPLE_SPEC, &dev_info.ss,
+ TAG_CHANNEL_MAP, &dev_info.map,
+ TAG_USEC, 0LL, /* latency */
+ TAG_USEC, 0LL, /* source latency */
+ TAG_STRING, "PipeWire", /* resample method */
+ TAG_STRING, "PipeWire", /* driver */
+ TAG_INVALID);
+ if (client->version >= 13)
+ message_put(m,
+ TAG_PROPLIST, info->props,
+ TAG_INVALID);
+ if (client->version >= 19)
+ message_put(m,
+ TAG_BOOLEAN, info->state != PW_NODE_STATE_RUNNING, /* corked */
+ TAG_INVALID);
+ if (client->version >= 22) {
+ struct format_info fi;
+ format_info_from_spec(&fi, &dev_info.ss, &dev_info.map);
+ message_put(m,
+ TAG_CVOLUME, &dev_info.volume_info.volume,
+ TAG_BOOLEAN, dev_info.volume_info.mute, /* muted */
+ TAG_BOOLEAN, true, /* has_volume */
+ TAG_BOOLEAN, true, /* volume writable */
+ TAG_FORMAT_INFO, &fi,
+ TAG_INVALID);
+ format_info_clear(&fi);
+ }
+ return 0;
+}
+
+static int do_get_info(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct message *reply = NULL;
+ int res;
+ struct pw_manager_object *o;
+ struct selector sel;
+ int (*fill_func) (struct client *client, struct message *m, struct pw_manager_object *o) = NULL;
+
+ spa_zero(sel);
+
+ if (message_get(m,
+ TAG_U32, &sel.index,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+
+ reply = reply_new(client, tag);
+
+ if (command == COMMAND_GET_MODULE_INFO && (sel.index & MODULE_FLAG) != 0) {
+ struct module *module;
+ module = pw_map_lookup(&impl->modules, sel.index & MODULE_INDEX_MASK);
+ if (module == NULL)
+ goto error_noentity;
+ fill_ext_module_info(client, reply, module);
+ return client_queue_message(client, reply);
+ }
+
+ switch (command) {
+ case COMMAND_GET_CLIENT_INFO:
+ sel.type = pw_manager_object_is_client;
+ fill_func = fill_client_info;
+ break;
+ case COMMAND_GET_MODULE_INFO:
+ sel.type = pw_manager_object_is_module;
+ fill_func = fill_module_info;
+ break;
+ case COMMAND_GET_CARD_INFO:
+ sel.type = pw_manager_object_is_card;
+ sel.key = PW_KEY_DEVICE_NAME;
+ fill_func = fill_card_info;
+ break;
+ case COMMAND_GET_SINK_INFO:
+ sel.type = pw_manager_object_is_sink;
+ sel.key = PW_KEY_NODE_NAME;
+ fill_func = fill_sink_info;
+ break;
+ case COMMAND_GET_SOURCE_INFO:
+ sel.type = pw_manager_object_is_source_or_monitor;
+ sel.key = PW_KEY_NODE_NAME;
+ fill_func = fill_source_info;
+ break;
+ case COMMAND_GET_SINK_INPUT_INFO:
+ sel.type = pw_manager_object_is_sink_input;
+ fill_func = fill_sink_input_info;
+ break;
+ case COMMAND_GET_SOURCE_OUTPUT_INFO:
+ sel.type = pw_manager_object_is_source_output;
+ fill_func = fill_source_output_info;
+ break;
+ }
+ if (sel.key) {
+ if (message_get(m,
+ TAG_STRING, &sel.value,
+ TAG_INVALID) < 0)
+ goto error_protocol;
+ }
+ if (fill_func == NULL)
+ goto error_invalid;
+
+ if (sel.index != SPA_ID_INVALID && sel.value != NULL)
+ goto error_invalid;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s", client->name,
+ commands[command].name, tag, sel.index, sel.value);
+
+ if (command == COMMAND_GET_SINK_INFO || command == COMMAND_GET_SOURCE_INFO) {
+ o = find_device(client, sel.index, sel.value,
+ command == COMMAND_GET_SINK_INFO, NULL);
+ } else {
+ if (sel.value == NULL && sel.index == SPA_ID_INVALID)
+ goto error_invalid;
+ o = select_object(manager, &sel);
+ }
+ if (o == NULL)
+ goto error_noentity;
+
+ if ((res = fill_func(client, reply, o)) < 0)
+ goto error;
+
+ return client_queue_message(client, reply);
+
+error_protocol:
+ res = -EPROTO;
+ goto error;
+error_noentity:
+ res = -ENOENT;
+ goto error;
+error_invalid:
+ res = -EINVAL;
+ goto error;
+error:
+ if (reply)
+ message_free(reply, false, false);
+ return res;
+}
+
+static uint64_t bytes_to_usec(uint64_t length, const struct sample_spec *ss)
+{
+ uint64_t u;
+ uint64_t frame_size = sample_spec_frame_size(ss);
+ if (frame_size == 0)
+ return 0;
+ u = length / frame_size;
+ u *= SPA_USEC_PER_SEC;
+ u /= ss->rate;
+ return u;
+}
+
+static int fill_sample_info(struct client *client, struct message *m,
+ struct sample *sample)
+{
+ struct volume vol;
+
+ volume_make(&vol, sample->ss.channels);
+
+ message_put(m,
+ TAG_U32, sample->index,
+ TAG_STRING, sample->name,
+ TAG_CVOLUME, &vol,
+ TAG_USEC, bytes_to_usec(sample->length, &sample->ss),
+ TAG_SAMPLE_SPEC, &sample->ss,
+ TAG_CHANNEL_MAP, &sample->map,
+ TAG_U32, sample->length,
+ TAG_BOOLEAN, false, /* lazy */
+ TAG_STRING, NULL, /* filename */
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ message_put(m,
+ TAG_PROPLIST, &sample->props->dict,
+ TAG_INVALID);
+ }
+ return 0;
+}
+
+static int do_get_sample_info(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply = NULL;
+ uint32_t index;
+ const char *name;
+ struct sample *sample;
+ int res;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s", client->name,
+ commands[command].name, tag, index, name);
+
+ if ((sample = find_sample(impl, index, name)) == NULL)
+ return -ENOENT;
+
+ reply = reply_new(client, tag);
+ if ((res = fill_sample_info(client, reply, sample)) < 0)
+ goto error;
+
+ return client_queue_message(client, reply);
+
+error:
+ if (reply)
+ message_free(reply, false, false);
+ return res;
+}
+
+static int do_get_sample_info_list(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+ union pw_map_item *item;
+
+ pw_log_info("[%s] %s tag:%u", client->name,
+ commands[command].name, tag);
+
+ reply = reply_new(client, tag);
+ pw_array_for_each(item, &impl->samples.items) {
+ struct sample *s = item->data;
+ if (pw_map_item_is_free(item))
+ continue;
+ fill_sample_info(client, reply, s);
+ }
+ return client_queue_message(client, reply);
+}
+
+struct info_list_data {
+ struct client *client;
+ struct message *reply;
+ int (*fill_func) (struct client *client, struct message *m, struct pw_manager_object *o);
+};
+
+static int do_list_info(void *data, struct pw_manager_object *object)
+{
+ struct info_list_data *info = data;
+ info->fill_func(info->client, info->reply, object);
+ return 0;
+}
+
+static int do_info_list_module(void *item, void *data)
+{
+ struct module *m = item;
+ struct info_list_data *info = data;
+ fill_ext_module_info(info->client, info->reply, m);
+ return 0;
+}
+
+static int do_get_info_list(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ struct info_list_data info;
+
+ pw_log_info("[%s] %s tag:%u", client->name,
+ commands[command].name, tag);
+
+ spa_zero(info);
+ info.client = client;
+
+ switch (command) {
+ case COMMAND_GET_CLIENT_INFO_LIST:
+ info.fill_func = fill_client_info;
+ break;
+ case COMMAND_GET_MODULE_INFO_LIST:
+ info.fill_func = fill_module_info;
+ break;
+ case COMMAND_GET_CARD_INFO_LIST:
+ info.fill_func = fill_card_info;
+ break;
+ case COMMAND_GET_SINK_INFO_LIST:
+ info.fill_func = fill_sink_info;
+ break;
+ case COMMAND_GET_SOURCE_INFO_LIST:
+ info.fill_func = fill_source_info;
+ break;
+ case COMMAND_GET_SINK_INPUT_INFO_LIST:
+ info.fill_func = fill_sink_input_info;
+ break;
+ case COMMAND_GET_SOURCE_OUTPUT_INFO_LIST:
+ info.fill_func = fill_source_output_info;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ info.reply = reply_new(client, tag);
+ if (info.fill_func)
+ pw_manager_for_each_object(manager, do_list_info, &info);
+
+ if (command == COMMAND_GET_MODULE_INFO_LIST)
+ pw_map_for_each(&impl->modules, do_info_list_module, &info);
+
+ return client_queue_message(client, info.reply);
+}
+
+static int do_set_stream_buffer_attr(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel;
+ struct stream *stream;
+ struct message *reply;
+ struct buffer_attr attr;
+ bool adjust_latency = false, early_requests = false;
+
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u channel:%u", client->name,
+ commands[command].name, tag, channel);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL)
+ return -ENOENT;
+
+ if (command == COMMAND_SET_PLAYBACK_STREAM_BUFFER_ATTR) {
+ if (stream->type != STREAM_TYPE_PLAYBACK)
+ return -ENOENT;
+
+ if (message_get(m,
+ TAG_U32, &attr.maxlength,
+ TAG_U32, &attr.tlength,
+ TAG_U32, &attr.prebuf,
+ TAG_U32, &attr.minreq,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ } else {
+ if (stream->type != STREAM_TYPE_RECORD)
+ return -ENOENT;
+
+ if (message_get(m,
+ TAG_U32, &attr.maxlength,
+ TAG_U32, &attr.fragsize,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ }
+ if (client->version >= 13) {
+ if (message_get(m,
+ TAG_BOOLEAN, &adjust_latency,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ }
+ if (client->version >= 14) {
+ if (message_get(m,
+ TAG_BOOLEAN, &early_requests,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+ }
+
+ reply = reply_new(client, tag);
+
+ stream->adjust_latency = adjust_latency;
+ stream->early_requests = early_requests;
+
+ if (command == COMMAND_SET_PLAYBACK_STREAM_BUFFER_ATTR) {
+ stream->lat_usec = set_playback_buffer_attr(stream, &attr);
+
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.tlength,
+ TAG_U32, stream->attr.prebuf,
+ TAG_U32, stream->attr.minreq,
+ TAG_INVALID);
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_USEC, stream->lat_usec, /* configured_sink_latency */
+ TAG_INVALID);
+ }
+ } else {
+ stream->lat_usec = set_record_buffer_attr(stream, &attr);
+
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.fragsize,
+ TAG_INVALID);
+ if (client->version >= 13) {
+ message_put(reply,
+ TAG_USEC, stream->lat_usec, /* configured_source_latency */
+ TAG_INVALID);
+ }
+ }
+ return client_queue_message(client, reply);
+}
+
+static int do_update_stream_sample_rate(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t channel, rate;
+ struct stream *stream;
+ float corr;
+
+ if (message_get(m,
+ TAG_U32, &channel,
+ TAG_U32, &rate,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u channel:%u rate:%u", client->name,
+ commands[command].name, tag, channel, rate);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_UPLOAD)
+ return -ENOENT;
+
+ stream->rate = rate;
+
+ corr = (double)rate/(double)stream->ss.rate;
+ pw_stream_set_control(stream->stream, SPA_PROP_rate, 1, &corr, NULL);
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_extension(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ uint32_t index;
+ const char *name;
+ const struct extension *ext;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s", client->name,
+ commands[command].name, tag, index, name);
+
+ if ((index == SPA_ID_INVALID && name == NULL) ||
+ (index != SPA_ID_INVALID && name != NULL))
+ return -EINVAL;
+
+ ext = extension_find(index, name);
+ if (ext == NULL)
+ return -ENOENT;
+
+ return ext->process(client, tag, m);
+}
+
+static int do_set_profile(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o;
+ const char *profile_name;
+ uint32_t profile_index = SPA_ID_INVALID;
+ struct selector sel;
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+
+ spa_zero(sel);
+ sel.key = PW_KEY_DEVICE_NAME;
+ sel.type = pw_manager_object_is_card;
+
+ if (message_get(m,
+ TAG_U32, &sel.index,
+ TAG_STRING, &sel.value,
+ TAG_STRING, &profile_name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s profile:%s", client->name,
+ commands[command].name, tag, sel.index, sel.value, profile_name);
+
+ if ((sel.index == SPA_ID_INVALID && sel.value == NULL) ||
+ (sel.index != SPA_ID_INVALID && sel.value != NULL))
+ return -EINVAL;
+ if (profile_name == NULL)
+ return -EINVAL;
+
+ if ((o = select_object(manager, &sel)) == NULL)
+ return -ENOENT;
+
+ if ((profile_index = find_profile_index(o, profile_name)) == SPA_ID_INVALID)
+ return -ENOENT;
+
+ if (!SPA_FLAG_IS_SET(o->permissions, PW_PERM_W | PW_PERM_X))
+ return -EACCES;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ pw_device_set_param((struct pw_device*)o->proxy,
+ SPA_PARAM_Profile, 0,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(profile_index),
+ SPA_PARAM_PROFILE_save, SPA_POD_Bool(true)));
+
+ return operation_new(client, tag);
+}
+
+static int do_set_default(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o;
+ const char *name, *str;
+ int res;
+ bool sink = command == COMMAND_SET_DEFAULT_SINK;
+
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u name:%s", client->name,
+ commands[command].name, tag, name);
+
+ if (name != NULL && (o = find_device(client, SPA_ID_INVALID, name, sink, NULL)) == NULL)
+ return -ENOENT;
+
+ if (name != NULL) {
+ if (o->props && (str = pw_properties_get(o->props, PW_KEY_NODE_NAME)) != NULL)
+ name = str;
+ else if (spa_strendswith(name, ".monitor"))
+ name = strndupa(name, strlen(name)-8);
+
+ res = pw_manager_set_metadata(manager, client->metadata_default,
+ PW_ID_CORE,
+ sink ? METADATA_CONFIG_DEFAULT_SINK : METADATA_CONFIG_DEFAULT_SOURCE,
+ "Spa:String:JSON", "{ \"name\": \"%s\" }", name);
+ } else {
+ res = pw_manager_set_metadata(manager, client->metadata_default,
+ PW_ID_CORE,
+ sink ? METADATA_CONFIG_DEFAULT_SINK : METADATA_CONFIG_DEFAULT_SOURCE,
+ NULL, NULL);
+ }
+ if (res < 0)
+ return res;
+
+ /*
+ * The metadata is not necessarily updated within one server sync.
+ * Correct functioning of MOVE_* commands requires knowing the current
+ * default target, so we need to stash temporary values here in case
+ * the client emits them before metadata gets updated.
+ */
+ if (sink) {
+ free(client->temporary_default_sink);
+ client->temporary_default_sink = name ? strdup(name) : NULL;
+ } else {
+ free(client->temporary_default_source);
+ client->temporary_default_source = name ? strdup(name) : NULL;
+ }
+
+ return operation_new(client, tag);
+}
+
+static int do_suspend(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager_object *o;
+ const char *name;
+ uint32_t index, cmd;
+ bool sink = command == COMMAND_SUSPEND_SINK, suspend;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_STRING, &name,
+ TAG_BOOLEAN, &suspend,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u name:%s", client->name,
+ commands[command].name, tag, index, name);
+
+ if ((o = find_device(client, index, name, sink, NULL)) == NULL)
+ return -ENOENT;
+
+ if (o->proxy == NULL)
+ return -ENOENT;
+
+ if (suspend) {
+ cmd = SPA_NODE_COMMAND_Suspend;
+ pw_node_send_command((struct pw_node*)o->proxy, &SPA_NODE_COMMAND_INIT(cmd));
+ }
+ return operation_new(client, tag);
+}
+
+static int do_move_stream(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o, *dev, *dev_default;
+ uint32_t index, index_device;
+ int target_id;
+ int64_t target_serial;
+ const char *name_device;
+ const char *name;
+ struct pw_node_info *info;
+ struct selector sel;
+ int res;
+ bool sink = command == COMMAND_MOVE_SINK_INPUT;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_U32, &index_device,
+ TAG_STRING, &name_device,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ if ((index_device == SPA_ID_INVALID && name_device == NULL) ||
+ (index_device != SPA_ID_INVALID && name_device != NULL))
+ return -EINVAL;
+
+ pw_log_info("[%s] %s tag:%u index:%u device:%d name:%s", client->name,
+ commands[command].name, tag, index, index_device, name_device);
+
+ spa_zero(sel);
+ sel.index = index;
+ sel.type = sink ? pw_manager_object_is_sink_input: pw_manager_object_is_source_output;
+
+ o = select_object(manager, &sel);
+ if (o == NULL)
+ return -ENOENT;
+
+ info = o->info;
+ if (info == NULL || info->props == NULL)
+ return -EINVAL;
+ if (spa_atob(spa_dict_lookup(info->props, PW_KEY_NODE_DONT_RECONNECT)))
+ return -EINVAL;
+
+ if ((dev = find_device(client, index_device, name_device, sink, NULL)) == NULL)
+ return -ENOENT;
+
+ /*
+ * The client metadata is not necessarily yet updated after SET_DEFAULT command,
+ * so use the temporary values if they are still set.
+ */
+ name = sink ? client->temporary_default_sink : client->temporary_default_source;
+ dev_default = find_device(client, SPA_ID_INVALID, name, sink, NULL);
+
+ if (dev == dev_default) {
+ /*
+ * When moving streams to a node that is equal to the default,
+ * Pulseaudio understands this to mean '... and unset preferred sink/source',
+ * forgetting target.node. Follow that behavior here.
+ */
+ target_id = -1;
+ target_serial = -1;
+ } else {
+ target_id = dev->id;
+ target_serial = dev->serial;
+ }
+
+ if ((res = pw_manager_set_metadata(manager, client->metadata_default,
+ o->id,
+ METADATA_TARGET_NODE,
+ SPA_TYPE_INFO_BASE"Id", "%d", target_id)) < 0)
+ return res;
+
+ if ((res = pw_manager_set_metadata(manager, client->metadata_default,
+ o->id,
+ METADATA_TARGET_OBJECT,
+ SPA_TYPE_INFO_BASE"Id", "%"PRIi64, target_serial)) < 0)
+ return res;
+
+ name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME);
+ pw_log_debug("[%s] %s done tag:%u index:%u name:%s target:%d target-serial:%"PRIi64, client->name,
+ commands[command].name, tag, index, name ? name : "<null>",
+ target_id, target_serial);
+
+ /* We will temporarily claim the stream was already moved */
+ set_temporary_move_target(client, o, dev->index);
+ send_object_event(client, o, SUBSCRIPTION_EVENT_CHANGE);
+
+ return reply_simple_ack(client, tag);
+}
+
+static int do_kill(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct pw_manager *manager = client->manager;
+ struct pw_manager_object *o;
+ uint32_t index;
+ struct selector sel;
+
+ if (message_get(m,
+ TAG_U32, &index,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u", client->name,
+ commands[command].name, tag, index);
+
+ spa_zero(sel);
+ sel.index = index;
+ switch (command) {
+ case COMMAND_KILL_CLIENT:
+ sel.type = pw_manager_object_is_client;
+ break;
+ case COMMAND_KILL_SINK_INPUT:
+ sel.type = pw_manager_object_is_sink_input;
+ break;
+ case COMMAND_KILL_SOURCE_OUTPUT:
+ sel.type = pw_manager_object_is_source_output;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if ((o = select_object(manager, &sel)) == NULL)
+ return -ENOENT;
+
+ pw_registry_destroy(manager->registry, o->id);
+
+ return reply_simple_ack(client, tag);
+}
+
+static void handle_module_loaded(struct module *module, struct client *client, uint32_t tag, int result)
+{
+ const char *client_name = client != NULL ? client->name : "?";
+ struct impl *impl = module->impl;
+
+ spa_assert(!SPA_RESULT_IS_ASYNC(result));
+
+ if (SPA_RESULT_IS_OK(result)) {
+ pw_log_info("[%s] loaded module index:%u name:%s tag:%d",
+ client_name, module->index, module->info->name, tag);
+
+ module->loaded = true;
+
+ broadcast_subscribe_event(impl,
+ SUBSCRIPTION_MASK_MODULE,
+ SUBSCRIPTION_EVENT_NEW | SUBSCRIPTION_EVENT_MODULE,
+ module->index);
+
+ if (client != NULL) {
+ struct message *reply = reply_new(client, tag);
+
+ message_put(reply,
+ TAG_U32, module->index,
+ TAG_INVALID);
+ client_queue_message(client, reply);
+ }
+ }
+ else {
+ pw_log_warn("%p: [%s] failed to load module index:%u name:%s tag:%d result:%d (%s)",
+ impl, client_name,
+ module->index, module->info->name, tag,
+ result, spa_strerror(result));
+
+ module_schedule_unload(module);
+
+ if (client != NULL)
+ reply_error(client, COMMAND_LOAD_MODULE, tag, result);
+ }
+}
+
+struct pending_module {
+ struct client *client;
+ struct spa_hook client_listener;
+
+ struct module *module;
+ struct spa_hook module_listener;
+
+ struct spa_hook manager_listener;
+
+ uint32_t tag;
+
+ int result;
+ bool wait_sync;
+};
+
+static void finish_pending_module(struct pending_module *pm)
+{
+ spa_hook_remove(&pm->module_listener);
+
+ if (pm->client != NULL) {
+ spa_hook_remove(&pm->client_listener);
+ spa_hook_remove(&pm->manager_listener);
+ }
+
+ handle_module_loaded(pm->module, pm->client, pm->tag, pm->result);
+ free(pm);
+}
+
+static void on_load_module_manager_sync(void *data)
+{
+ struct pending_module *pm = data;
+
+ pw_log_debug("pending module %p: manager sync wait_sync:%d tag:%d",
+ pm, pm->wait_sync, pm->tag);
+
+ if (!pm->wait_sync)
+ return;
+
+ finish_pending_module(pm);
+}
+
+static void on_module_loaded(void *data, int result)
+{
+ struct pending_module *pm = data;
+
+ pw_log_debug("pending module %p: loaded, result:%d tag:%d",
+ pm, result, pm->tag);
+
+ pm->result = result;
+
+ /*
+ * Do manager sync first: the module may have its own core, so
+ * although things are completed on the server, our client
+ * might not yet see them.
+ */
+
+ if (pm->client == NULL) {
+ finish_pending_module(pm);
+ } else {
+ pw_log_debug("pending module %p: wait manager sync tag:%d", pm, pm->tag);
+ pm->wait_sync = true;
+ pw_manager_sync(pm->client->manager);
+ }
+}
+
+static void on_module_destroy(void *data)
+{
+ struct pending_module *pm = data;
+
+ pw_log_debug("pending module %p: destroyed, tag:%d",
+ pm, pm->tag);
+
+ pm->result = -ECANCELED;
+ finish_pending_module(pm);
+}
+
+static void on_client_disconnect(void *data)
+{
+ struct pending_module *pm = data;
+
+ pw_log_debug("pending module %p: client disconnect tag:%d", pm, pm->tag);
+
+ spa_hook_remove(&pm->client_listener);
+ spa_hook_remove(&pm->manager_listener);
+ pm->client = NULL;
+
+ if (pm->wait_sync)
+ finish_pending_module(pm);
+}
+
+static void on_load_module_manager_disconnect(void *data)
+{
+ on_client_disconnect(data);
+}
+
+static int do_load_module(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ static const struct module_events module_events = {
+ VERSION_MODULE_EVENTS,
+ .loaded = on_module_loaded,
+ .destroy = on_module_destroy,
+ };
+ static const struct client_events client_events = {
+ VERSION_CLIENT_EVENTS,
+ .disconnect = on_client_disconnect,
+ };
+ static const struct pw_manager_events manager_events = {
+ PW_VERSION_MANAGER_EVENTS,
+ .disconnect = on_load_module_manager_disconnect,
+ .sync = on_load_module_manager_sync,
+ };
+
+ struct impl *impl = client->impl;
+ const char *name, *argument;
+ struct module *module;
+ struct pending_module *pm;
+ int r;
+
+ if (message_get(m,
+ TAG_STRING, &name,
+ TAG_STRING, &argument,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s name:%s argument:%s",
+ client->name, commands[command].name, name, argument);
+
+ module = module_create(impl, name, argument);
+ if (module == NULL)
+ return -errno;
+
+ pm = calloc(1, sizeof(*pm));
+ if (pm == NULL)
+ return -errno;
+
+ pm->tag = tag;
+ pm->client = client;
+ pm->module = module;
+
+ pw_log_debug("pending module %p: start tag:%d", pm, tag);
+
+ r = module_load(module);
+
+ module_add_listener(module, &pm->module_listener, &module_events, pm);
+ client_add_listener(client, &pm->client_listener, &client_events, pm);
+ pw_manager_add_listener(client->manager, &pm->manager_listener, &manager_events, pm);
+
+ if (!SPA_RESULT_IS_ASYNC(r))
+ on_module_loaded(pm, r);
+
+ /*
+ * return 0 to prevent `handle_packet()` from sending a reply
+ * because we want `handle_module_loaded()` to send the reply
+ */
+ return 0;
+}
+
+static int do_unload_module(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct module *module;
+ uint32_t module_index;
+
+ if (message_get(m,
+ TAG_U32, &module_index,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u index:%u", client->name,
+ commands[command].name, tag, module_index);
+
+ if (module_index == SPA_ID_INVALID)
+ return -EINVAL;
+ if ((module_index & MODULE_FLAG) == 0)
+ return -EPERM;
+
+ module = pw_map_lookup(&impl->modules, module_index & MODULE_INDEX_MASK);
+ if (module == NULL)
+ return -ENOENT;
+
+ module_unload(module);
+
+ return operation_new(client, tag);
+}
+
+static int do_send_object_message(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ struct impl *impl = client->impl;
+ struct pw_manager *manager = client->manager;
+ const char *object_path = NULL;
+ const char *message = NULL;
+ const char *params = NULL;
+ char *response = NULL;
+ char *path = NULL;
+ struct message *reply;
+ struct pw_manager_object *o;
+ int len = 0;
+ int res;
+
+ if (message_get(m,
+ TAG_STRING, &object_path,
+ TAG_STRING, &message,
+ TAG_STRING, &params,
+ TAG_INVALID) < 0)
+ return -EPROTO;
+
+ pw_log_info("[%s] %s tag:%u object_path:'%s' message:'%s' params:'%s'",
+ client->name, commands[command].name, tag, object_path,
+ message, params ? params : "<null>");
+
+ if (object_path == NULL || message == NULL)
+ return -EINVAL;
+
+ len = strlen(object_path);
+ if (len > 0 && object_path[len - 1] == '/')
+ --len;
+ path = strndup(object_path, len);
+ if (path == NULL)
+ return -ENOMEM;
+
+ res = -ENOENT;
+
+ spa_list_for_each(o, &manager->object_list, link) {
+ if (o->message_object_path && spa_streq(o->message_object_path, path)) {
+ if (o->message_handler)
+ res = o->message_handler(manager, o, message, params, &response);
+ else
+ res = -ENOSYS;
+ break;
+ }
+ }
+
+ free(path);
+ if (res < 0)
+ return res;
+
+ pw_log_debug("%p: object message response:'%s'", impl, response ? response : "<null>");
+
+ reply = reply_new(client, tag);
+ message_put(reply, TAG_STRING, response, TAG_INVALID);
+ free(response);
+ return client_queue_message(client, reply);
+}
+
+static int do_error_access(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return -EACCES;
+}
+
+static SPA_UNUSED int do_error_not_implemented(struct client *client, uint32_t command, uint32_t tag, struct message *m)
+{
+ return -ENOSYS;
+}
+
+#define COMMAND(name, ...) [COMMAND_ ## name] = { #name, __VA_ARGS__ }
+const struct command commands[COMMAND_MAX] =
+{
+ COMMAND(ERROR),
+ COMMAND(TIMEOUT), /* pseudo command */
+ COMMAND(REPLY),
+
+ /* CLIENT->SERVER */
+ COMMAND(CREATE_PLAYBACK_STREAM, do_create_playback_stream),
+ COMMAND(DELETE_PLAYBACK_STREAM, do_delete_stream),
+ COMMAND(CREATE_RECORD_STREAM, do_create_record_stream),
+ COMMAND(DELETE_RECORD_STREAM, do_delete_stream),
+ COMMAND(EXIT, do_error_access),
+ COMMAND(AUTH, do_command_auth, COMMAND_ACCESS_WITHOUT_AUTH | COMMAND_ACCESS_WITHOUT_MANAGER),
+ COMMAND(SET_CLIENT_NAME, do_set_client_name, COMMAND_ACCESS_WITHOUT_MANAGER),
+ COMMAND(LOOKUP_SINK, do_lookup),
+ COMMAND(LOOKUP_SOURCE, do_lookup),
+ COMMAND(DRAIN_PLAYBACK_STREAM, do_drain_stream),
+ COMMAND(STAT, do_stat, COMMAND_ACCESS_WITHOUT_MANAGER),
+ COMMAND(GET_PLAYBACK_LATENCY, do_get_playback_latency),
+ COMMAND(CREATE_UPLOAD_STREAM, do_create_upload_stream),
+ COMMAND(DELETE_UPLOAD_STREAM, do_delete_stream),
+ COMMAND(FINISH_UPLOAD_STREAM, do_finish_upload_stream),
+ COMMAND(PLAY_SAMPLE, do_play_sample),
+ COMMAND(REMOVE_SAMPLE, do_remove_sample),
+
+ COMMAND(GET_SERVER_INFO, do_get_server_info, COMMAND_ACCESS_WITHOUT_MANAGER),
+ COMMAND(GET_SINK_INFO, do_get_info),
+ COMMAND(GET_SOURCE_INFO, do_get_info),
+ COMMAND(GET_MODULE_INFO, do_get_info),
+ COMMAND(GET_CLIENT_INFO, do_get_info),
+ COMMAND(GET_SINK_INPUT_INFO, do_get_info),
+ COMMAND(GET_SOURCE_OUTPUT_INFO, do_get_info),
+ COMMAND(GET_SAMPLE_INFO, do_get_sample_info),
+ COMMAND(GET_CARD_INFO, do_get_info),
+ COMMAND(SUBSCRIBE, do_subscribe),
+
+ COMMAND(GET_SINK_INFO_LIST, do_get_info_list),
+ COMMAND(GET_SOURCE_INFO_LIST, do_get_info_list),
+ COMMAND(GET_MODULE_INFO_LIST, do_get_info_list),
+ COMMAND(GET_CLIENT_INFO_LIST, do_get_info_list),
+ COMMAND(GET_SINK_INPUT_INFO_LIST, do_get_info_list),
+ COMMAND(GET_SOURCE_OUTPUT_INFO_LIST, do_get_info_list),
+ COMMAND(GET_SAMPLE_INFO_LIST, do_get_sample_info_list),
+ COMMAND(GET_CARD_INFO_LIST, do_get_info_list),
+
+ COMMAND(SET_SINK_VOLUME, do_set_volume),
+ COMMAND(SET_SINK_INPUT_VOLUME, do_set_stream_volume),
+ COMMAND(SET_SOURCE_VOLUME, do_set_volume),
+
+ COMMAND(SET_SINK_MUTE, do_set_mute),
+ COMMAND(SET_SOURCE_MUTE, do_set_mute),
+
+ COMMAND(CORK_PLAYBACK_STREAM, do_cork_stream),
+ COMMAND(FLUSH_PLAYBACK_STREAM, do_flush_trigger_prebuf_stream),
+ COMMAND(TRIGGER_PLAYBACK_STREAM, do_flush_trigger_prebuf_stream),
+ COMMAND(PREBUF_PLAYBACK_STREAM, do_flush_trigger_prebuf_stream),
+
+ COMMAND(SET_DEFAULT_SINK, do_set_default),
+ COMMAND(SET_DEFAULT_SOURCE, do_set_default),
+
+ COMMAND(SET_PLAYBACK_STREAM_NAME, do_set_stream_name),
+ COMMAND(SET_RECORD_STREAM_NAME, do_set_stream_name),
+
+ COMMAND(KILL_CLIENT, do_kill),
+ COMMAND(KILL_SINK_INPUT, do_kill),
+ COMMAND(KILL_SOURCE_OUTPUT, do_kill),
+
+ COMMAND(LOAD_MODULE, do_load_module),
+ COMMAND(UNLOAD_MODULE, do_unload_module),
+
+ /* Obsolete */
+ COMMAND(ADD_AUTOLOAD___OBSOLETE, do_error_access),
+ COMMAND(REMOVE_AUTOLOAD___OBSOLETE, do_error_access),
+ COMMAND(GET_AUTOLOAD_INFO___OBSOLETE, do_error_access),
+ COMMAND(GET_AUTOLOAD_INFO_LIST___OBSOLETE, do_error_access),
+
+ COMMAND(GET_RECORD_LATENCY, do_get_record_latency),
+ COMMAND(CORK_RECORD_STREAM, do_cork_stream),
+ COMMAND(FLUSH_RECORD_STREAM, do_flush_trigger_prebuf_stream),
+
+ /* SERVER->CLIENT */
+ COMMAND(REQUEST),
+ COMMAND(OVERFLOW),
+ COMMAND(UNDERFLOW),
+ COMMAND(PLAYBACK_STREAM_KILLED),
+ COMMAND(RECORD_STREAM_KILLED),
+ COMMAND(SUBSCRIBE_EVENT),
+
+ /* A few more client->server commands */
+
+ /* Supported since protocol v10 (0.9.5) */
+ COMMAND(MOVE_SINK_INPUT, do_move_stream),
+ COMMAND(MOVE_SOURCE_OUTPUT, do_move_stream),
+
+ /* Supported since protocol v11 (0.9.7) */
+ COMMAND(SET_SINK_INPUT_MUTE, do_set_stream_mute),
+
+ COMMAND(SUSPEND_SINK, do_suspend),
+ COMMAND(SUSPEND_SOURCE, do_suspend),
+
+ /* Supported since protocol v12 (0.9.8) */
+ COMMAND(SET_PLAYBACK_STREAM_BUFFER_ATTR, do_set_stream_buffer_attr),
+ COMMAND(SET_RECORD_STREAM_BUFFER_ATTR, do_set_stream_buffer_attr),
+
+ COMMAND(UPDATE_PLAYBACK_STREAM_SAMPLE_RATE, do_update_stream_sample_rate),
+ COMMAND(UPDATE_RECORD_STREAM_SAMPLE_RATE, do_update_stream_sample_rate),
+
+ /* SERVER->CLIENT */
+ COMMAND(PLAYBACK_STREAM_SUSPENDED),
+ COMMAND(RECORD_STREAM_SUSPENDED),
+ COMMAND(PLAYBACK_STREAM_MOVED),
+ COMMAND(RECORD_STREAM_MOVED),
+
+ /* Supported since protocol v13 (0.9.11) */
+ COMMAND(UPDATE_RECORD_STREAM_PROPLIST, do_update_proplist),
+ COMMAND(UPDATE_PLAYBACK_STREAM_PROPLIST, do_update_proplist),
+ COMMAND(UPDATE_CLIENT_PROPLIST, do_update_proplist),
+
+ COMMAND(REMOVE_RECORD_STREAM_PROPLIST, do_remove_proplist),
+ COMMAND(REMOVE_PLAYBACK_STREAM_PROPLIST, do_remove_proplist),
+ COMMAND(REMOVE_CLIENT_PROPLIST, do_remove_proplist),
+
+ /* SERVER->CLIENT */
+ COMMAND(STARTED),
+
+ /* Supported since protocol v14 (0.9.12) */
+ COMMAND(EXTENSION, do_extension),
+ /* Supported since protocol v15 (0.9.15) */
+ COMMAND(SET_CARD_PROFILE, do_set_profile),
+
+ /* SERVER->CLIENT */
+ COMMAND(CLIENT_EVENT),
+ COMMAND(PLAYBACK_STREAM_EVENT),
+ COMMAND(RECORD_STREAM_EVENT),
+
+ /* SERVER->CLIENT */
+ COMMAND(PLAYBACK_BUFFER_ATTR_CHANGED),
+ COMMAND(RECORD_BUFFER_ATTR_CHANGED),
+
+ /* Supported since protocol v16 (0.9.16) */
+ COMMAND(SET_SINK_PORT, do_set_port),
+ COMMAND(SET_SOURCE_PORT, do_set_port),
+
+ /* Supported since protocol v22 (1.0) */
+ COMMAND(SET_SOURCE_OUTPUT_VOLUME, do_set_stream_volume),
+ COMMAND(SET_SOURCE_OUTPUT_MUTE, do_set_stream_mute),
+
+ /* Supported since protocol v27 (3.0) */
+ COMMAND(SET_PORT_LATENCY_OFFSET, do_set_port_latency_offset),
+
+ /* Supported since protocol v30 (6.0) */
+ /* BOTH DIRECTIONS */
+ COMMAND(ENABLE_SRBCHANNEL, do_error_access),
+ COMMAND(DISABLE_SRBCHANNEL, do_error_access),
+
+ /* Supported since protocol v31 (9.0)
+ * BOTH DIRECTIONS */
+ COMMAND(REGISTER_MEMFD_SHMID, do_error_access),
+
+ /* Supported since protocol v35 (15.0) */
+ COMMAND(SEND_OBJECT_MESSAGE, do_send_object_message),
+};
+#undef COMMAND
+
+static int impl_free_sample(void *item, void *data)
+{
+ struct sample *s = item;
+
+ spa_assert(s->ref == 1);
+ sample_unref(s);
+
+ return 0;
+}
+
+static int impl_unload_module(void *item, void *data)
+{
+ struct module *m = item;
+ module_unload(m);
+ return 0;
+}
+
+static void impl_clear(struct impl *impl)
+{
+ struct message *msg;
+ struct server *s;
+ struct client *c;
+
+ pw_map_for_each(&impl->modules, impl_unload_module, impl);
+ pw_map_clear(&impl->modules);
+
+ spa_list_consume(s, &impl->servers, link)
+ server_free(s);
+
+ spa_list_consume(c, &impl->cleanup_clients, link)
+ client_free(c);
+
+ spa_list_consume(msg, &impl->free_messages, link)
+ message_free(msg, true, true);
+
+ pw_map_for_each(&impl->samples, impl_free_sample, impl);
+ pw_map_clear(&impl->samples);
+
+ spa_hook_list_clean(&impl->hooks);
+
+#ifdef HAVE_DBUS
+ if (impl->dbus_name) {
+ dbus_release_name(impl->dbus_name);
+ impl->dbus_name = NULL;
+ }
+#endif
+
+ if (impl->context) {
+ spa_hook_remove(&impl->context_listener);
+ impl->context = NULL;
+ }
+
+ pw_properties_free(impl->props);
+ impl->props = NULL;
+}
+
+static void impl_free(struct impl *impl)
+{
+ impl_clear(impl);
+ free(impl);
+}
+
+static void context_destroy(void *data)
+{
+ impl_clear(data);
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .destroy = context_destroy,
+};
+
+static int parse_frac(struct pw_properties *props, const char *key, const char *def,
+ struct spa_fraction *res)
+{
+ const char *str;
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL)
+ str = def;
+ if (sscanf(str, "%u/%u", &res->num, &res->denom) != 2 || res->denom == 0) {
+ pw_log_warn(": invalid fraction %s, default to %s", str, def);
+ sscanf(def, "%u/%u", &res->num, &res->denom);
+ }
+ pw_log_info(": defaults: %s = %u/%u", key, res->num, res->denom);
+ return 0;
+}
+
+static int parse_position(struct pw_properties *props, const char *key, const char *def,
+ struct channel_map *res)
+{
+ const char *str;
+ struct spa_json it[2];
+ char v[256];
+
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL)
+ str = def;
+
+ 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));
+
+ res->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ res->channels < SPA_AUDIO_MAX_CHANNELS) {
+ res->map[res->channels++] = channel_name2id(v);
+ }
+ pw_log_info(": defaults: %s = %s", key, str);
+ return 0;
+}
+static int parse_format(struct pw_properties *props, const char *key, const char *def,
+ struct sample_spec *res)
+{
+ const char *str;
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL)
+ str = def;
+ res->format = format_name2id(str);
+ if (res->format == SPA_AUDIO_FORMAT_UNKNOWN) {
+ pw_log_warn(": unknown format %s, default to %s", str, def);
+ res->format = format_name2id(def);
+ }
+ pw_log_info(": defaults: %s = %s", key, format_id2name(res->format));
+ return 0;
+}
+static int parse_uint32(struct pw_properties *props, const char *key, const char *def,
+ uint32_t *res)
+{
+ const char *str;
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL)
+ str = def;
+ if (!spa_atou32(str, res, 0)) {
+ pw_log_warn(": invalid uint32_t %s, default to %s", str, def);
+ spa_atou32(def, res, 0);
+ }
+ pw_log_info(": defaults: %s = %u", key, *res);
+ return 0;
+}
+
+static void load_defaults(struct defs *def, struct pw_properties *props)
+{
+ parse_frac(props, "pulse.min.req", DEFAULT_MIN_REQ, &def->min_req);
+ parse_frac(props, "pulse.default.req", DEFAULT_DEFAULT_REQ, &def->default_req);
+ parse_frac(props, "pulse.min.frag", DEFAULT_MIN_FRAG, &def->min_frag);
+ parse_frac(props, "pulse.default.frag", DEFAULT_DEFAULT_FRAG, &def->default_frag);
+ parse_frac(props, "pulse.default.tlength", DEFAULT_DEFAULT_TLENGTH, &def->default_tlength);
+ parse_frac(props, "pulse.min.quantum", DEFAULT_MIN_QUANTUM, &def->min_quantum);
+ parse_format(props, "pulse.default.format", DEFAULT_FORMAT, &def->sample_spec);
+ parse_position(props, "pulse.default.position", DEFAULT_POSITION, &def->channel_map);
+ parse_uint32(props, "pulse.idle.timeout", DEFAULT_IDLE_TIMEOUT, &def->idle_timeout);
+ def->sample_spec.channels = def->channel_map.channels;
+ def->quantum_limit = 8192;
+}
+
+struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context,
+ struct pw_properties *props, size_t user_data_size)
+{
+ const struct spa_support *support;
+ struct spa_cpu *cpu;
+ uint32_t n_support;
+ struct impl *impl;
+ const char *str;
+ int res = 0;
+
+ impl = calloc(1, sizeof(*impl) + user_data_size);
+ if (impl == NULL)
+ goto error_exit;
+
+ if (props == NULL)
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ goto error_free;
+
+ support = pw_context_get_support(context, &n_support);
+ cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU);
+
+ pw_context_conf_update_props(context, "pulse.properties", props);
+
+ if ((str = pw_properties_get(props, "vm.overrides")) != NULL) {
+ if (cpu != NULL && spa_cpu_get_vm_type(cpu) != SPA_CPU_VM_NONE)
+ pw_properties_update_string(props, str, strlen(str));
+ pw_properties_set(props, "vm.overrides", NULL);
+ }
+
+ load_defaults(&impl->defs, props);
+
+ debug_messages = pw_log_topic_enabled(SPA_LOG_LEVEL_INFO, pulse_conn);
+
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+ impl->props = props;
+
+ impl->work_queue = pw_context_get_work_queue(context);
+
+ spa_hook_list_init(&impl->hooks);
+ spa_list_init(&impl->servers);
+ impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+ impl->rate_limit.burst = 1;
+ pw_map_init(&impl->samples, 16, 16);
+ pw_map_init(&impl->modules, 16, 16);
+ spa_list_init(&impl->cleanup_clients);
+ spa_list_init(&impl->free_messages);
+
+ str = pw_properties_get(props, "server.address");
+ if (str == NULL) {
+ pw_properties_setf(props, "server.address",
+ "[ \"%s-%s\" ]",
+ PW_PROTOCOL_PULSE_DEFAULT_SERVER,
+ get_server_name(context));
+ str = pw_properties_get(props, "server.address");
+ }
+
+ if (str == NULL)
+ goto error_free;
+
+ if ((res = servers_create_and_start(impl, str, NULL)) < 0) {
+ pw_log_error("%p: no servers could be started: %s",
+ impl, spa_strerror(res));
+ goto error_free;
+ }
+
+ if ((res = create_pid_file()) < 0) {
+ pw_log_warn("%p: can't create pid file: %s",
+ impl, spa_strerror(res));
+ }
+ pw_context_add_listener(context, &impl->context_listener,
+ &context_events, impl);
+
+#ifdef HAVE_DBUS
+ impl->dbus_name = dbus_request_name(context, "org.pulseaudio.Server");
+#endif
+ cmd_run(impl);
+
+ return (struct pw_protocol_pulse *) impl;
+
+error_free:
+ free(impl);
+
+error_exit:
+ pw_properties_free(props);
+
+ if (res < 0)
+ errno = -res;
+
+ return NULL;
+}
+
+void impl_add_listener(struct impl *impl,
+ struct spa_hook *listener,
+ const struct impl_events *events, void *data)
+{
+ spa_hook_list_append(&impl->hooks, listener, events, data);
+}
+
+void *pw_protocol_pulse_get_user_data(struct pw_protocol_pulse *pulse)
+{
+ return SPA_PTROFF(pulse, sizeof(struct impl), void);
+}
+
+void pw_protocol_pulse_destroy(struct pw_protocol_pulse *pulse)
+{
+ struct impl *impl = (struct impl*)pulse;
+ impl_free(impl);
+}
diff --git a/src/modules/module-protocol-pulse/pulse-server.h b/src/modules/module-protocol-pulse/pulse-server.h
new file mode 100644
index 0000000..3e8e6ee
--- /dev/null
+++ b/src/modules/module-protocol-pulse/pulse-server.h
@@ -0,0 +1,56 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PROTOCOL_PULSE_H
+#define PIPEWIRE_PROTOCOL_PULSE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#define PW_PROTOCOL_PULSE_DEFAULT_PORT 4713
+#define PW_PROTOCOL_PULSE_DEFAULT_SOCKET "native"
+
+#define PW_PROTOCOL_PULSE_DEFAULT_SERVER "unix:"PW_PROTOCOL_PULSE_DEFAULT_SOCKET
+
+#define PW_PROTOCOL_PULSE_USAGE "[ server.address=(tcp:[<ip>:]<port>|unix:<path>)[,...] ] " \
+
+struct pw_context;
+struct pw_properties;
+struct pw_protocol_pulse;
+struct pw_protocol_pulse_server;
+
+struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context,
+ struct pw_properties *props, size_t user_data_size);
+void *pw_protocol_pulse_get_user_data(struct pw_protocol_pulse *pulse);
+void pw_protocol_pulse_destroy(struct pw_protocol_pulse *pulse);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_PROTOCOL_PULSE_H */
diff --git a/src/modules/module-protocol-pulse/quirks.c b/src/modules/module-protocol-pulse/quirks.c
new file mode 100644
index 0000000..eb22438
--- /dev/null
+++ b/src/modules/module-protocol-pulse/quirks.c
@@ -0,0 +1,75 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <regex.h>
+
+#include <spa/utils/json.h>
+
+#include <pipewire/properties.h>
+
+#include "log.h"
+#include "quirks.h"
+#include "internal.h"
+
+static uint64_t parse_quirks(const char *str)
+{
+ static const struct { const char *key; uint64_t value; } quirk_keys[] = {
+ { "force-s16-info", QUIRK_FORCE_S16_FORMAT },
+ { "remove-capture-dont-move", QUIRK_REMOVE_CAPTURE_DONT_MOVE },
+ };
+ SPA_FOR_EACH_ELEMENT_VAR(quirk_keys, i) {
+ if (spa_streq(str, i->key))
+ return i->value;
+ }
+ return 0;
+}
+
+static int apply_match(void *data, const char *location, const char *action,
+ const char *val, size_t len)
+{
+ struct client *client = data;
+
+ if (spa_streq(action, "update-props")) {
+ pw_properties_update_string(client->props, val, len);
+ } else if (spa_streq(action, "quirks")) {
+ struct spa_json quirks = SPA_JSON_INIT(val, len), it[1];
+ uint64_t quirks_cur = 0;
+ char v[128];
+
+ if (spa_json_enter_array(&quirks, &it[0]) > 0) {
+ while (spa_json_get_string(&it[0], v, sizeof(v)) > 0)
+ quirks_cur |= parse_quirks(v);
+ }
+ client->quirks = quirks_cur;
+ }
+ return 0;
+}
+
+int client_update_quirks(struct client *client)
+{
+ struct impl *impl = client->impl;
+ struct pw_context *context = impl->context;
+ return pw_context_conf_section_match_rules(context, "pulse.rules",
+ &client->props->dict, apply_match, client);
+}
diff --git a/src/modules/module-protocol-pulse/quirks.h b/src/modules/module-protocol-pulse/quirks.h
new file mode 100644
index 0000000..0229089
--- /dev/null
+++ b/src/modules/module-protocol-pulse/quirks.h
@@ -0,0 +1,36 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_QUIRKS_H
+#define PULSER_SERVER_QUIRKS_H
+
+#include "client.h"
+
+#define QUIRK_FORCE_S16_FORMAT (1ull<<0) /** forces S16 sample format in sink and source
+ * info */
+#define QUIRK_REMOVE_CAPTURE_DONT_MOVE (1ull<<1) /** removes the capture stream DONT_MOVE flag */
+
+int client_update_quirks(struct client *client);
+
+#endif /* PULSER_SERVER_QUIRKS_H */
diff --git a/src/modules/module-protocol-pulse/remap.c b/src/modules/module-protocol-pulse/remap.c
new file mode 100644
index 0000000..1442dee
--- /dev/null
+++ b/src/modules/module-protocol-pulse/remap.c
@@ -0,0 +1,58 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include <pipewire/keys.h>
+
+#include "remap.h"
+
+const struct str_map media_role_map[] = {
+ { "Movie", "video", },
+ { "Music", "music", },
+ { "Game", "game", },
+ { "Notification", "event", },
+ { "Communication", "phone", },
+ { "Movie", "animation", },
+ { "Production", "production", },
+ { "Accessibility", "a11y", },
+ { "Test", "test", },
+ { NULL, NULL },
+};
+
+const struct str_map props_key_map[] = {
+ { PW_KEY_DEVICE_BUS_PATH, "device.bus_path" },
+ { PW_KEY_DEVICE_SYSFS_PATH, "sysfs.path" },
+ { PW_KEY_DEVICE_FORM_FACTOR, "device.form_factor" },
+ { PW_KEY_DEVICE_ICON_NAME, "device.icon_name" },
+ { PW_KEY_DEVICE_INTENDED_ROLES, "device.intended_roles" },
+ { PW_KEY_NODE_DESCRIPTION, "device.description" },
+ { PW_KEY_MEDIA_ICON_NAME, "media.icon_name" },
+ { PW_KEY_APP_ICON_NAME, "application.icon_name" },
+ { PW_KEY_APP_PROCESS_MACHINE_ID, "application.process.machine_id" },
+ { PW_KEY_APP_PROCESS_SESSION_ID, "application.process.session_id" },
+ { PW_KEY_MEDIA_ROLE, "media.role", media_role_map },
+ { "pipe.filename", "device.string" },
+ { NULL, NULL },
+};
diff --git a/src/modules/module-protocol-pulse/remap.h b/src/modules/module-protocol-pulse/remap.h
new file mode 100644
index 0000000..49cbdf2
--- /dev/null
+++ b/src/modules/module-protocol-pulse/remap.h
@@ -0,0 +1,52 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_REMAP_H
+#define PULSE_SERVER_REMAP_H
+
+#include <stddef.h>
+
+#include <spa/utils/string.h>
+
+struct str_map {
+ const char *pw_str;
+ const char *pa_str;
+ const struct str_map *child;
+};
+
+extern const struct str_map media_role_map[];
+
+extern const struct str_map props_key_map[];
+
+static inline const struct str_map *str_map_find(const struct str_map *map, const char *pw, const char *pa)
+{
+ size_t i;
+ for (i = 0; map[i].pw_str; i++)
+ if ((pw && spa_streq(map[i].pw_str, pw)) ||
+ (pa && spa_streq(map[i].pa_str, pa)))
+ return &map[i];
+ return NULL;
+}
+
+#endif /* PULSE_SERVER_REMAP_H */
diff --git a/src/modules/module-protocol-pulse/reply.c b/src/modules/module-protocol-pulse/reply.c
new file mode 100644
index 0000000..9444e5b
--- /dev/null
+++ b/src/modules/module-protocol-pulse/reply.c
@@ -0,0 +1,84 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+
+#include <spa/utils/result.h>
+#include <pipewire/log.h>
+
+#include "defs.h"
+#include "client.h"
+#include "commands.h"
+#include "message.h"
+#include "log.h"
+
+struct message *reply_new(const struct client *client, uint32_t tag)
+{
+ struct message *reply = message_alloc(client->impl, -1, 0);
+
+ pw_log_debug("client %p: new reply tag:%u", client, tag);
+
+ message_put(reply,
+ TAG_U32, COMMAND_REPLY,
+ TAG_U32, tag,
+ TAG_INVALID);
+
+ return reply;
+}
+
+int reply_error(struct client *client, uint32_t command, uint32_t tag, int res)
+{
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t error = res_to_err(res);
+ const char *name;
+ enum spa_log_level level;
+
+ if (command < COMMAND_MAX)
+ name = commands[command].name;
+ else
+ name = "invalid";
+
+ switch (res) {
+ case -ENOENT:
+ case -ENOTSUP:
+ level = SPA_LOG_LEVEL_INFO;
+ break;
+ default:
+ level = SPA_LOG_LEVEL_WARN;
+ break;
+ }
+
+ pw_log(level, "client %p [%s]: ERROR command:%d (%s) tag:%u error:%u (%s)",
+ client, client->name, command, name, tag, error, spa_strerror(res));
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, COMMAND_ERROR,
+ TAG_U32, tag,
+ TAG_U32, error,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
diff --git a/src/modules/module-protocol-pulse/reply.h b/src/modules/module-protocol-pulse/reply.h
new file mode 100644
index 0000000..1ca9ad1
--- /dev/null
+++ b/src/modules/module-protocol-pulse/reply.h
@@ -0,0 +1,42 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_REPLY_H
+#define PULSE_SERVER_REPLY_H
+
+#include <stdint.h>
+
+#include "client.h"
+
+struct message;
+
+struct message *reply_new(const struct client *client, uint32_t tag);
+int reply_error(struct client *client, uint32_t command, uint32_t tag, int res);
+
+static inline int reply_simple_ack(struct client *client, uint32_t tag)
+{
+ return client_queue_message(client, reply_new(client, tag));
+}
+
+#endif /* PULSE_SERVER_REPLY_H */
diff --git a/src/modules/module-protocol-pulse/sample-play.c b/src/modules/module-protocol-pulse/sample-play.c
new file mode 100644
index 0000000..37c68a3
--- /dev/null
+++ b/src/modules/module-protocol-pulse/sample-play.c
@@ -0,0 +1,211 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <spa/node/io.h>
+#include <spa/param/audio/raw.h>
+#include <spa/pod/builder.h>
+#include <spa/utils/hook.h>
+#include <pipewire/context.h>
+#include <pipewire/core.h>
+#include <pipewire/log.h>
+#include <pipewire/properties.h>
+#include <pipewire/stream.h>
+
+#include "format.h"
+#include "log.h"
+#include "sample.h"
+#include "sample-play.h"
+
+static void sample_play_stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct sample_play *p = data;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ case PW_STREAM_STATE_ERROR:
+ sample_play_emit_done(p, -EIO);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ p->id = pw_stream_get_node_id(p->stream);
+ sample_play_emit_ready(p, p->id);
+ break;
+ default:
+ break;
+ }
+}
+
+static void sample_play_stream_destroy(void *data)
+{
+ struct sample_play *p = data;
+
+ pw_log_info("destroy %s", p->sample->name);
+
+ spa_hook_remove(&p->listener);
+ p->stream = NULL;
+
+ sample_unref(p->sample);
+ p->sample = NULL;
+}
+
+static void sample_play_stream_process(void *data)
+{
+ struct sample_play *p = data;
+ struct sample *s = p->sample;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ uint32_t size;
+ uint8_t *d;
+
+ if (p->offset >= s->length) {
+ pw_stream_flush(p->stream, true);
+ return;
+ }
+
+ size = s->length - p->offset;
+
+ if ((b = pw_stream_dequeue_buffer(p->stream)) == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((d = buf->datas[0].data) == NULL)
+ return;
+
+ size = SPA_MIN(size, buf->datas[0].maxsize);
+ if (b->requested)
+ size = SPA_MIN(size, b->requested * p->stride);
+
+ memcpy(d, s->buffer + p->offset, size);
+
+ p->offset += size;
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = p->stride;
+ buf->datas[0].chunk->size = size;
+
+ pw_stream_queue_buffer(p->stream, b);
+}
+
+static void sample_play_stream_drained(void *data)
+{
+ struct sample_play *p = data;
+
+ sample_play_emit_done(p, 0);
+}
+
+static const struct pw_stream_events sample_play_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = sample_play_stream_state_changed,
+ .destroy = sample_play_stream_destroy,
+ .process = sample_play_stream_process,
+ .drained = sample_play_stream_drained,
+};
+
+struct sample_play *sample_play_new(struct pw_core *core,
+ struct sample *sample, struct pw_properties *props,
+ size_t user_data_size)
+{
+ struct sample_play *p;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_pod *params[1];
+ uint32_t n_params = 0;
+ int res;
+
+ p = calloc(1, sizeof(*p) + user_data_size);
+ if (p == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ p->context = pw_core_get_context(core);
+ p->main_loop = pw_context_get_main_loop(p->context);
+ spa_hook_list_init(&p->hooks);
+ p->user_data = SPA_PTROFF(p, sizeof(struct sample_play), void);
+
+ pw_properties_update(props, &sample->props->dict);
+
+ p->stream = pw_stream_new(core, sample->name, props);
+ props = NULL;
+ if (p->stream == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ /* safe to increment the reference count here because it will be decreased
+ by the stream's 'destroy' event handler, which will be called
+ (even if `pw_stream_connect()` fails) */
+ p->sample = sample_ref(sample);
+ p->stride = sample_spec_frame_size(&sample->ss);
+
+ pw_stream_add_listener(p->stream,
+ &p->listener,
+ &sample_play_stream_events, p);
+
+ params[n_params++] = format_build_param(&b, SPA_PARAM_EnumFormat,
+ &sample->ss, &sample->map);
+
+ res = pw_stream_connect(p->stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params);
+ if (res < 0)
+ goto error_cleanup;
+
+ return p;
+
+error_cleanup:
+ pw_stream_destroy(p->stream);
+error_free:
+ pw_properties_free(props);
+ free(p);
+ errno = -res;
+ return NULL;
+}
+
+void sample_play_destroy(struct sample_play *p)
+{
+ if (p->stream)
+ pw_stream_destroy(p->stream);
+
+ spa_hook_list_clean(&p->hooks);
+
+ free(p);
+}
+
+void sample_play_add_listener(struct sample_play *p, struct spa_hook *listener,
+ const struct sample_play_events *events, void *data)
+{
+ spa_hook_list_append(&p->hooks, listener, events, data);
+}
diff --git a/src/modules/module-protocol-pulse/sample-play.h b/src/modules/module-protocol-pulse/sample-play.h
new file mode 100644
index 0000000..5738935
--- /dev/null
+++ b/src/modules/module-protocol-pulse/sample-play.h
@@ -0,0 +1,76 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_SAMPLE_PLAY_H
+#define PULSER_SERVER_SAMPLE_PLAY_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+
+struct sample;
+struct pw_core;
+struct pw_loop;
+struct pw_stream;
+struct pw_context;
+struct pw_properties;
+
+struct sample_play_events {
+#define VERSION_SAMPLE_PLAY_EVENTS 0
+ uint32_t version;
+
+ void (*ready) (void *data, uint32_t id);
+
+ void (*done) (void *data, int err);
+};
+
+#define sample_play_emit_ready(p,i) spa_hook_list_call(&p->hooks, struct sample_play_events, ready, 0, i)
+#define sample_play_emit_done(p,r) spa_hook_list_call(&p->hooks, struct sample_play_events, done, 0, r)
+
+struct sample_play {
+ struct spa_list link;
+ struct sample *sample;
+ struct pw_stream *stream;
+ uint32_t id;
+ struct spa_hook listener;
+ struct pw_context *context;
+ struct pw_loop *main_loop;
+ uint32_t offset;
+ uint32_t stride;
+ struct spa_hook_list hooks;
+ void *user_data;
+};
+
+struct sample_play *sample_play_new(struct pw_core *core,
+ struct sample *sample, struct pw_properties *props,
+ size_t user_data_size);
+
+void sample_play_destroy(struct sample_play *p);
+
+void sample_play_add_listener(struct sample_play *p, struct spa_hook *listener,
+ const struct sample_play_events *events, void *data);
+
+#endif /* PULSER_SERVER_SAMPLE_PLAY_H */
diff --git a/src/modules/module-protocol-pulse/sample.c b/src/modules/module-protocol-pulse/sample.c
new file mode 100644
index 0000000..a2d8de9
--- /dev/null
+++ b/src/modules/module-protocol-pulse/sample.c
@@ -0,0 +1,50 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+
+#include <pipewire/log.h>
+#include <pipewire/map.h>
+#include <pipewire/properties.h>
+
+#include "internal.h"
+#include "log.h"
+#include "sample.h"
+
+void sample_free(struct sample *sample)
+{
+ struct impl * const impl = sample->impl;
+
+ pw_log_info("free sample id:%u name:%s", sample->index, sample->name);
+
+ impl->stat.sample_cache -= sample->length;
+
+ if (sample->index != SPA_ID_INVALID)
+ pw_map_remove(&impl->samples, sample->index);
+
+ pw_properties_free(sample->props);
+
+ free(sample->buffer);
+ free(sample);
+}
diff --git a/src/modules/module-protocol-pulse/sample.h b/src/modules/module-protocol-pulse/sample.h
new file mode 100644
index 0000000..db347eb
--- /dev/null
+++ b/src/modules/module-protocol-pulse/sample.h
@@ -0,0 +1,61 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_SAMPLE_H
+#define PULSE_SERVER_SAMPLE_H
+
+#include <stdint.h>
+
+#include "format.h"
+
+struct impl;
+struct pw_properties;
+
+struct sample {
+ int ref;
+ uint32_t index;
+ struct impl *impl;
+ const char *name;
+ struct sample_spec ss;
+ struct channel_map map;
+ struct pw_properties *props;
+ uint32_t length;
+ uint8_t *buffer;
+};
+
+void sample_free(struct sample *sample);
+
+static inline struct sample *sample_ref(struct sample *sample)
+{
+ sample->ref++;
+ return sample;
+}
+
+static inline void sample_unref(struct sample *sample)
+{
+ if (--sample->ref == 0)
+ sample_free(sample);
+}
+
+#endif /* PULSE_SERVER_SAMPLE_H */
diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c
new file mode 100644
index 0000000..927e253
--- /dev/null
+++ b/src/modules/module-protocol-pulse/server.c
@@ -0,0 +1,1087 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <netinet/ip.h>
+#include <unistd.h>
+
+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-daemon.h>
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/json.h>
+#include <spa/utils/result.h>
+#include <pipewire/pipewire.h>
+
+#include "client.h"
+#include "commands.h"
+#include "defs.h"
+#include "internal.h"
+#include "log.h"
+#include "message.h"
+#include "reply.h"
+#include "server.h"
+#include "stream.h"
+#include "utils.h"
+#include "flatpak-utils.h"
+
+#define LISTEN_BACKLOG 32
+#define MAX_CLIENTS 64
+
+static int handle_packet(struct client *client, struct message *msg)
+{
+ uint32_t command, tag;
+ int res = 0;
+
+ if (message_get(msg,
+ TAG_U32, &command,
+ TAG_U32, &tag,
+ TAG_INVALID) < 0) {
+ res = -EPROTO;
+ goto finish;
+ }
+
+ pw_log_debug("client %p: received packet command:%u tag:%u",
+ client, command, tag);
+
+ if (command >= COMMAND_MAX) {
+ res = -EINVAL;
+ goto finish;
+ }
+
+ if (debug_messages) {
+ pw_log_debug("client %p: command:%s", client, commands[command].name);
+ message_dump(SPA_LOG_LEVEL_INFO, msg);
+ }
+
+ const struct command *cmd = &commands[command];
+ if (cmd->run == NULL) {
+ res = -ENOTSUP;
+ goto finish;
+ }
+
+ if (!client->authenticated && !SPA_FLAG_IS_SET(cmd->access, COMMAND_ACCESS_WITHOUT_AUTH)) {
+ res = -EACCES;
+ goto finish;
+ }
+
+ if (client->manager == NULL && !SPA_FLAG_IS_SET(cmd->access, COMMAND_ACCESS_WITHOUT_MANAGER)) {
+ res = -EACCES;
+ goto finish;
+ }
+
+ res = cmd->run(client, command, tag, msg);
+
+finish:
+ message_free(msg, false, false);
+ if (res < 0)
+ reply_error(client, command, tag, res);
+
+ return 0;
+}
+
+static int handle_memblock(struct client *client, struct message *msg)
+{
+ struct stream *stream;
+ uint32_t channel, flags, index;
+ int64_t offset, diff;
+ int32_t filled;
+ int res = 0;
+
+ channel = ntohl(client->desc.channel);
+ offset = (int64_t) (
+ (((uint64_t) ntohl(client->desc.offset_hi)) << 32) |
+ (((uint64_t) ntohl(client->desc.offset_lo))));
+ flags = ntohl(client->desc.flags);
+
+ pw_log_debug("client %p: received memblock channel:%d offset:%" PRIi64 " flags:%08x size:%u",
+ client, channel, offset, flags, msg->length);
+
+ stream = pw_map_lookup(&client->streams, channel);
+ if (stream == NULL || stream->type == STREAM_TYPE_RECORD) {
+ pw_log_info("client %p [%s]: received memblock for unknown channel %d",
+ client, client->name, channel);
+ goto finish;
+ }
+
+ filled = spa_ringbuffer_get_write_index(&stream->ring, &index);
+ pw_log_debug("new block %p %p/%u filled:%d index:%d flags:%02x offset:%" PRIu64,
+ msg, msg->data, msg->length, filled, index, flags, offset);
+
+ switch (flags & FLAG_SEEKMASK) {
+ case SEEK_RELATIVE:
+ diff = offset;
+ break;
+ case SEEK_ABSOLUTE:
+ diff = offset - (int64_t)stream->write_index;
+ break;
+ case SEEK_RELATIVE_ON_READ:
+ case SEEK_RELATIVE_END:
+ diff = offset - (int64_t)filled;
+ break;
+ default:
+ pw_log_warn("client %p [%s]: received memblock frame with invalid seek mode: %" PRIu32,
+ client, client->name, (uint32_t)(flags & FLAG_SEEKMASK));
+ res = -EPROTO;
+ goto finish;
+ }
+
+ index += diff;
+ filled += diff;
+ stream->write_index += diff;
+ if ((flags & FLAG_SEEKMASK) == SEEK_RELATIVE)
+ stream->requested -= diff;
+
+ if (filled < 0) {
+ /* underrun, reported on reader side */
+ } else if (filled + msg->length > stream->attr.maxlength) {
+ /* overrun */
+ stream_send_overflow(stream);
+ }
+
+ /* always write data to ringbuffer, we expect the other side
+ * to recover */
+ spa_ringbuffer_write_data(&stream->ring,
+ stream->buffer, MAXLENGTH,
+ index % MAXLENGTH,
+ msg->data,
+ SPA_MIN(msg->length, MAXLENGTH));
+ index += msg->length;
+ spa_ringbuffer_write_update(&stream->ring, index);
+
+ stream->write_index += msg->length;
+ stream->requested -= msg->length;
+
+ stream_send_request(stream);
+
+ if (stream->is_paused && !stream->corked)
+ stream_set_paused(stream, false, "new data");
+
+finish:
+ message_free(msg, false, false);
+ return res;
+}
+
+static int do_read(struct client *client)
+{
+ struct impl * const impl = client->impl;
+ size_t size;
+ int res = 0;
+ void *data;
+
+ if (client->in_index < sizeof(client->desc)) {
+ data = SPA_PTROFF(&client->desc, client->in_index, void);
+ size = sizeof(client->desc) - client->in_index;
+ } else {
+ uint32_t idx = client->in_index - sizeof(client->desc);
+
+ if (client->message == NULL || client->message->length < idx) {
+ res = -EPROTO;
+ goto exit;
+ }
+
+ data = SPA_PTROFF(client->message->data, idx, void);
+ size = client->message->length - idx;
+ }
+
+ while (true) {
+ ssize_t r = recv(client->source->fd, data, size, MSG_DONTWAIT);
+
+ if (r == 0 && size != 0) {
+ res = -EPIPE;
+ goto exit;
+ } else if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ res = -errno;
+ if (res != -EAGAIN && res != -EWOULDBLOCK &&
+ res != -EPIPE && res != -ECONNRESET)
+ pw_log_warn("recv client:%p res %zd: %m", client, r);
+ goto exit;
+ }
+
+ client->in_index += r;
+ break;
+ }
+
+ if (client->in_index == sizeof(client->desc)) {
+ uint32_t flags, length, channel;
+
+ flags = ntohl(client->desc.flags);
+ if ((flags & FLAG_SHMMASK) != 0) {
+ res = -EPROTO;
+ goto exit;
+ }
+
+ length = ntohl(client->desc.length);
+ if (length > FRAME_SIZE_MAX_ALLOW || length <= 0) {
+ pw_log_warn("client %p: received invalid frame size: %u",
+ client, length);
+ res = -EPROTO;
+ goto exit;
+ }
+
+ channel = ntohl(client->desc.channel);
+ if (channel == (uint32_t) -1) {
+ if (flags != 0) {
+ pw_log_warn("client %p: received packet frame with invalid flags",
+ client);
+ res = -EPROTO;
+ goto exit;
+ }
+ }
+
+ if (client->message)
+ message_free(client->message, false, false);
+
+ client->message = message_alloc(impl, channel, length);
+ } else if (client->message &&
+ client->in_index >= client->message->length + sizeof(client->desc)) {
+ struct message * const msg = client->message;
+
+ client->message = NULL;
+ client->in_index = 0;
+
+ if (msg->channel == (uint32_t)-1)
+ res = handle_packet(client, msg);
+ else
+ res = handle_memblock(client, msg);
+ }
+
+exit:
+ return res;
+}
+
+static void
+on_client_data(void *data, int fd, uint32_t mask)
+{
+ struct client * const client = data;
+ int res;
+
+ client->ref++;
+
+ if (mask & SPA_IO_HUP) {
+ res = -EPIPE;
+ goto error;
+ }
+
+ if (mask & SPA_IO_ERR) {
+ res = -EIO;
+ goto error;
+ }
+
+ if (mask & SPA_IO_IN) {
+ pw_log_trace("client %p: can read", client);
+ while (true) {
+ res = do_read(client);
+ if (res < 0) {
+ if (res != -EAGAIN && res != -EWOULDBLOCK)
+ goto error;
+ break;
+ }
+ }
+ }
+
+ if (mask & SPA_IO_OUT || client->new_msg_since_last_flush) {
+ res = client_flush_messages(client);
+ if (res < 0)
+ goto error;
+ }
+
+done:
+ /* drop the reference that was acquired at the beginning of the function */
+ client_unref(client);
+ return;
+
+error:
+ switch (res) {
+ case -EPIPE:
+ case -ECONNRESET:
+ pw_log_info("server %p: client %p [%s] disconnected",
+ client->server, client, client->name);
+ SPA_FALLTHROUGH;
+ case -EPROTO:
+ /*
+ * drop the server's reference to the client
+ * (if it hasn't been dropped already),
+ * it is guaranteed that this will not call `client_free()`
+ * since at the beginning of this function an extra reference
+ * has been acquired which will keep the client alive
+ */
+ if (client_detach(client))
+ client_unref(client);
+
+ /* then disconnect the client */
+ client_disconnect(client);
+ break;
+ default:
+ pw_log_error("server %p: client %p [%s] error %d (%s)",
+ client->server, client, client->name, res, spa_strerror(res));
+ break;
+ }
+
+ goto done;
+}
+
+static void
+on_connect(void *data, int fd, uint32_t mask)
+{
+ struct server * const server = data;
+ struct impl * const impl = server->impl;
+ struct sockaddr_storage name;
+ socklen_t length;
+ int client_fd, val;
+ struct client *client = NULL;
+ const char *client_access = NULL;
+ pid_t pid;
+
+ length = sizeof(name);
+ client_fd = accept4(fd, (struct sockaddr *) &name, &length, SOCK_CLOEXEC);
+ if (client_fd < 0) {
+ if (errno == EMFILE || errno == ENFILE) {
+ if (server->n_clients > 0) {
+ int m = server->source->mask;
+ SPA_FLAG_CLEAR(m, SPA_IO_IN);
+ pw_loop_update_io(impl->loop, server->source, m);
+ server->wait_clients++;
+ }
+ }
+ goto error;
+ }
+
+ if (server->n_clients >= server->max_clients) {
+ close(client_fd);
+ errno = ECONNREFUSED;
+ goto error;
+ }
+
+ client = client_new(server);
+ if (client == NULL)
+ goto error;
+
+ pw_log_debug("server %p: new client %p fd:%d", server, client, client_fd);
+
+ client->source = pw_loop_add_io(impl->loop,
+ client_fd,
+ SPA_IO_ERR | SPA_IO_HUP | SPA_IO_IN,
+ true, on_client_data, client);
+ if (client->source == NULL)
+ goto error;
+
+ client->props = pw_properties_new(
+ PW_KEY_CLIENT_API, "pipewire-pulse",
+ "config.ext", pw_properties_get(impl->props, "config.ext"),
+ NULL);
+ if (client->props == NULL)
+ goto error;
+
+ pw_properties_setf(client->props,
+ "pulse.server.type", "%s",
+ server->addr.ss_family == AF_UNIX ? "unix" : "tcp");
+
+ client->routes = pw_properties_new(NULL, NULL);
+ if (client->routes == NULL)
+ goto error;
+
+ if (server->client_access[0] != '\0')
+ client_access = server->client_access;
+
+ if (server->addr.ss_family == AF_UNIX) {
+ char *app_id = NULL, *devices = NULL;
+
+#ifdef SO_PRIORITY
+ val = 6;
+ if (setsockopt(client_fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(SO_PRIORITY) failed: %m");
+#endif
+ pid = get_client_pid(client, client_fd);
+ if (pid != 0 && pw_check_flatpak(pid, &app_id, &devices) == 1) {
+ /*
+ * XXX: we should really use Portal client access here
+ *
+ * However, session managers currently support only camera
+ * permissions, and the XDG Portal doesn't have a "Sound Manager"
+ * permission defined. So for now, use access=flatpak, and determine
+ * extra permissions here.
+ *
+ * The application has access to the Pulseaudio socket,
+ * and with real PA it would always then have full sound access.
+ * We'll restrict the full access here behind devices=all;
+ * if the application can access all devices it can then
+ * also sound and camera devices directly, so granting also the
+ * Manager permissions here is reasonable.
+ *
+ * The "Manager" permission in any case is also currently not safe
+ * as the session manager does not check any permission store
+ * for it.
+ */
+ client_access = "flatpak";
+ pw_properties_set(client->props, "pipewire.access.portal.app_id",
+ app_id);
+
+ if (devices && (spa_streq(devices, "all") ||
+ spa_strstartswith(devices, "all;") ||
+ strstr(devices, ";all;")))
+ pw_properties_set(client->props, PW_KEY_MEDIA_CATEGORY, "Manager");
+ else
+ pw_properties_set(client->props, PW_KEY_MEDIA_CATEGORY, NULL);
+ }
+ free(devices);
+ free(app_id);
+ }
+ else if (server->addr.ss_family == AF_INET || server->addr.ss_family == AF_INET6) {
+
+ val = 1;
+ if (setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(TCP_NODELAY) failed: %m");
+
+ if (server->addr.ss_family == AF_INET) {
+ val = IPTOS_LOWDELAY;
+ if (setsockopt(client_fd, IPPROTO_IP, IP_TOS, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(IP_TOS) failed: %m");
+ }
+ if (client_access == NULL)
+ client_access = "restricted";
+ }
+ pw_properties_set(client->props, PW_KEY_CLIENT_ACCESS, client_access);
+
+ return;
+
+error:
+ pw_log_error("server %p: failed to create client: %m", server);
+ if (client)
+ client_free(client);
+}
+
+static int parse_unix_address(const char *address, struct sockaddr_storage *addrs, int len)
+{
+ struct sockaddr_un addr = {0};
+ int res;
+
+ if (address[0] != '/') {
+ char runtime_dir[PATH_MAX];
+
+ if ((res = get_runtime_dir(runtime_dir, sizeof(runtime_dir))) < 0)
+ return res;
+
+ res = snprintf(addr.sun_path, sizeof(addr.sun_path),
+ "%s/%s", runtime_dir, address);
+ }
+ else {
+ res = snprintf(addr.sun_path, sizeof(addr.sun_path),
+ "%s", address);
+ }
+
+ if (res < 0)
+ return -EINVAL;
+
+ if ((size_t) res >= sizeof(addr.sun_path)) {
+ pw_log_warn("'%s...' too long", addr.sun_path);
+ return -ENAMETOOLONG;
+ }
+
+ if (len < 1)
+ return -ENOSPC;
+
+ addr.sun_family = AF_UNIX;
+
+ memcpy(&addrs[0], &addr, sizeof(addr));
+ return 1;
+}
+
+#ifndef SUN_LEN
+#define SUN_LEN(addr_un) \
+ (offsetof(struct sockaddr_un, sun_path) + strlen((addr_un)->sun_path))
+#endif
+
+static bool is_stale_socket(int fd, const struct sockaddr_un *addr_un)
+{
+ if (connect(fd, (const struct sockaddr *) addr_un, SUN_LEN(addr_un)) < 0) {
+ if (errno == ECONNREFUSED)
+ return true;
+ }
+
+ return false;
+}
+
+#ifdef HAVE_SYSTEMD
+static int check_systemd_activation(const char *path)
+{
+ const int n = sd_listen_fds(0);
+
+ for (int i = 0; i < n; i++) {
+ const int fd = SD_LISTEN_FDS_START + i;
+
+ if (sd_is_socket_unix(fd, SOCK_STREAM, 1, path, 0) > 0)
+ return fd;
+ }
+
+ return -1;
+}
+#else
+static inline int check_systemd_activation(SPA_UNUSED const char *path)
+{
+ return -1;
+}
+#endif
+
+static int start_unix_server(struct server *server, const struct sockaddr_storage *addr)
+{
+ const struct sockaddr_un * const addr_un = (const struct sockaddr_un *) addr;
+ struct stat socket_stat;
+ int fd, res;
+
+ spa_assert(addr_un->sun_family == AF_UNIX);
+
+ fd = check_systemd_activation(addr_un->sun_path);
+ if (fd >= 0) {
+ server->activated = true;
+ pw_log_info("server %p: found systemd socket activation socket for '%s'",
+ server, addr_un->sun_path);
+ goto done;
+ }
+ else {
+ server->activated = false;
+ }
+
+ fd = socket(addr_un->sun_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ if (fd < 0) {
+ res = -errno;
+ pw_log_info("server %p: socket() failed: %m", server);
+ goto error;
+ }
+
+ if (stat(addr_un->sun_path, &socket_stat) < 0) {
+ if (errno != ENOENT) {
+ res = -errno;
+ pw_log_warn("server %p: stat('%s') failed: %m",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+ }
+ else if (socket_stat.st_mode & S_IWUSR || socket_stat.st_mode & S_IWGRP) {
+ if (!S_ISSOCK(socket_stat.st_mode)) {
+ res = -EEXIST;
+ pw_log_warn("server %p: '%s' exists and is not a socket",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+
+ /* socket is there, check if it's stale */
+ if (!is_stale_socket(fd, addr_un)) {
+ res = -EADDRINUSE;
+ pw_log_warn("server %p: socket '%s' is in use",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+
+ pw_log_warn("server %p: unlinking stale socket '%s'",
+ server, addr_un->sun_path);
+
+ if (unlink(addr_un->sun_path) < 0)
+ pw_log_warn("server %p: unlink('%s') failed: %m",
+ server, addr_un->sun_path);
+ }
+ if (bind(fd, (const struct sockaddr *) addr_un, SUN_LEN(addr_un)) < 0) {
+ res = -errno;
+ pw_log_warn("server %p: bind() to '%s' failed: %m",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+
+ if (chmod(addr_un->sun_path, 0777) < 0)
+ pw_log_warn("server %p: chmod('%s') failed: %m",
+ server, addr_un->sun_path);
+
+ if (listen(fd, server->listen_backlog) < 0) {
+ res = -errno;
+ pw_log_warn("server %p: listen() on '%s' failed: %m",
+ server, addr_un->sun_path);
+ goto error_close;
+ }
+
+ pw_log_info("server %p: listening on unix:%s", server, addr_un->sun_path);
+
+done:
+ server->addr = *addr;
+
+ return fd;
+
+error_close:
+ close(fd);
+error:
+ return res;
+}
+
+static int parse_port(const char *port)
+{
+ const char *end;
+ long p;
+
+ if (port[0] == ':')
+ port += 1;
+
+ errno = 0;
+ p = strtol(port, (char **) &end, 0);
+
+ if (errno != 0)
+ return -errno;
+
+ if (end == port || *end != '\0')
+ return -EINVAL;
+
+ if (!(1 <= p && p <= 65535))
+ return -EINVAL;
+
+ return p;
+}
+
+static int parse_ipv6_address(const char *address, struct sockaddr_in6 *out)
+{
+ char addr_str[INET6_ADDRSTRLEN];
+ struct sockaddr_in6 addr = {0};
+ const char *end;
+ size_t len;
+ int res;
+
+ if (address[0] != '[')
+ return -EINVAL;
+
+ address += 1;
+
+ end = strchr(address, ']');
+ if (end == NULL)
+ return -EINVAL;
+
+ len = end - address;
+ if (len >= sizeof(addr_str))
+ return -ENAMETOOLONG;
+
+ memcpy(addr_str, address, len);
+ addr_str[len] = '\0';
+
+ res = inet_pton(AF_INET6, addr_str, &addr.sin6_addr.s6_addr);
+ if (res < 0)
+ return -errno;
+ if (res == 0)
+ return -EINVAL;
+
+ res = parse_port(end + 1);
+ if (res < 0)
+ return res;
+
+ addr.sin6_port = htons(res);
+ addr.sin6_family = AF_INET6;
+
+ *out = addr;
+
+ return 0;
+}
+
+static int parse_ipv4_address(const char *address, struct sockaddr_in *out)
+{
+ char addr_str[INET_ADDRSTRLEN];
+ struct sockaddr_in addr = {0};
+ size_t len;
+ int res;
+
+ len = strspn(address, "0123456789.");
+ if (len == 0)
+ return -EINVAL;
+ if (len >= sizeof(addr_str))
+ return -ENAMETOOLONG;
+
+ memcpy(addr_str, address, len);
+ addr_str[len] = '\0';
+
+ res = inet_pton(AF_INET, addr_str, &addr.sin_addr.s_addr);
+ if (res < 0)
+ return -errno;
+ if (res == 0)
+ return -EINVAL;
+
+ res = parse_port(address + len);
+ if (res < 0)
+ return res;
+
+ addr.sin_port = htons(res);
+ addr.sin_family = AF_INET;
+
+ *out = addr;
+
+ return 0;
+}
+
+#define FORMATTED_IP_ADDR_STRLEN (INET6_ADDRSTRLEN + 2 + 1 + 5)
+
+static int format_ip_address(const struct sockaddr_storage *addr, char *buffer, size_t buflen)
+{
+ char ip[INET6_ADDRSTRLEN];
+ const void *src;
+ bool is_ipv6 = false;
+ int port;
+
+ switch (addr->ss_family) {
+ case AF_INET:
+ src = &((struct sockaddr_in *) addr)->sin_addr.s_addr;
+ port = ntohs(((struct sockaddr_in *) addr)->sin_port);
+ break;
+ case AF_INET6:
+ is_ipv6 = true;
+ src = &((struct sockaddr_in6 *) addr)->sin6_addr.s6_addr;
+ port = ntohs(((struct sockaddr_in6 *) addr)->sin6_port);
+ break;
+ default:
+ return -EAFNOSUPPORT;
+ }
+
+ if (inet_ntop(addr->ss_family, src, ip, sizeof(ip)) == NULL)
+ return -errno;
+
+ return snprintf(buffer, buflen, "%s%s%s:%d",
+ is_ipv6 ? "[" : "",
+ ip,
+ is_ipv6 ? "]" : "",
+ port);
+}
+
+static int get_ip_address_length(const struct sockaddr_storage *addr)
+{
+ switch (addr->ss_family) {
+ case AF_INET:
+ return sizeof(struct sockaddr_in);
+ case AF_INET6:
+ return sizeof(struct sockaddr_in6);
+ default:
+ return -EAFNOSUPPORT;
+ }
+}
+
+static int parse_ip_address(const char *address, struct sockaddr_storage *addrs, int len)
+{
+ char ip[FORMATTED_IP_ADDR_STRLEN];
+ struct sockaddr_storage addr;
+ int res;
+
+ res = parse_ipv6_address(address, (struct sockaddr_in6 *) &addr);
+ if (res == 0) {
+ if (len < 1)
+ return -ENOSPC;
+ addrs[0] = addr;
+ return 1;
+ }
+
+ res = parse_ipv4_address(address, (struct sockaddr_in *) &addr);
+ if (res == 0) {
+ if (len < 1)
+ return -ENOSPC;
+ addrs[0] = addr;
+ return 1;
+ }
+
+ res = parse_port(address);
+ if (res < 0)
+ return res;
+
+ if (len < 2)
+ return -ENOSPC;
+
+ snprintf(ip, sizeof(ip), "0.0.0.0:%d", res);
+ spa_assert_se(parse_ipv4_address(ip, (struct sockaddr_in *) &addr) == 0);
+ addrs[0] = addr;
+
+ snprintf(ip, sizeof(ip), "[::]:%d", res);
+ spa_assert_se(parse_ipv6_address(ip, (struct sockaddr_in6 *) &addr) == 0);
+ addrs[1] = addr;
+
+ return 2;
+}
+
+static int start_ip_server(struct server *server, const struct sockaddr_storage *addr)
+{
+ char ip[FORMATTED_IP_ADDR_STRLEN];
+ int fd, res;
+
+ spa_assert(addr->ss_family == AF_INET || addr->ss_family == AF_INET6);
+
+ fd = socket(addr->ss_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_TCP);
+ if (fd < 0) {
+ res = -errno;
+ pw_log_warn("server %p: socket() failed: %m", server);
+ goto error;
+ }
+
+ {
+ int on = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
+ pw_log_warn("server %p: setsockopt(SO_REUSEADDR) failed: %m", server);
+ }
+
+ if (addr->ss_family == AF_INET6) {
+ int on = 1;
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) < 0)
+ pw_log_warn("server %p: setsockopt(IPV6_V6ONLY) failed: %m", server);
+ }
+
+ if (bind(fd, (const struct sockaddr *) addr, get_ip_address_length(addr)) < 0) {
+ res = -errno;
+ pw_log_warn("server %p: bind() failed: %m", server);
+ goto error_close;
+ }
+
+ if (listen(fd, server->listen_backlog) < 0) {
+ res = -errno;
+ pw_log_warn("server %p: listen() failed: %m", server);
+ goto error_close;
+ }
+
+ spa_assert_se(format_ip_address(addr, ip, sizeof(ip)) >= 0);
+ pw_log_info("server %p: listening on tcp:%s", server, ip);
+
+ server->addr = *addr;
+
+ return fd;
+
+error_close:
+ close(fd);
+error:
+ return res;
+}
+
+static struct server *server_new(struct impl *impl)
+{
+ struct server * const server = calloc(1, sizeof(*server));
+ if (server == NULL)
+ return NULL;
+
+ server->impl = impl;
+ server->addr.ss_family = AF_UNSPEC;
+ spa_list_init(&server->clients);
+ spa_list_append(&impl->servers, &server->link);
+
+ pw_log_debug("server %p: new", server);
+
+ return server;
+}
+
+static int server_start(struct server *server, const struct sockaddr_storage *addr)
+{
+ struct impl * const impl = server->impl;
+ int res = 0, fd;
+
+ switch (addr->ss_family) {
+ case AF_INET:
+ case AF_INET6:
+ fd = start_ip_server(server, addr);
+ break;
+ case AF_UNIX:
+ fd = start_unix_server(server, addr);
+ break;
+ default:
+ /* shouldn't happen */
+ fd = -EAFNOSUPPORT;
+ break;
+ }
+
+ if (fd < 0)
+ return fd;
+
+ server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_connect, server);
+ if (server->source == NULL) {
+ res = -errno;
+ pw_log_error("server %p: can't create server source: %m", impl);
+ }
+ if (res >= 0)
+ spa_hook_list_call(&impl->hooks, struct impl_events, server_started, 0, server);
+
+ return res;
+}
+
+static int parse_address(const char *address, struct sockaddr_storage *addrs, int len)
+{
+ if (spa_strstartswith(address, "tcp:"))
+ return parse_ip_address(address + strlen("tcp:"), addrs, len);
+
+ if (spa_strstartswith(address, "unix:"))
+ return parse_unix_address(address + strlen("unix:"), addrs, len);
+
+ return -EAFNOSUPPORT;
+}
+
+#define SUN_PATH_SIZE (sizeof(((struct sockaddr_un *) NULL)->sun_path))
+#define FORMATTED_UNIX_ADDR_STRLEN (SUN_PATH_SIZE + 5)
+#define FORMATTED_TCP_ADDR_STRLEN (FORMATTED_IP_ADDR_STRLEN + 4)
+#define FORMATTED_SOCKET_ADDR_STRLEN \
+ (FORMATTED_UNIX_ADDR_STRLEN > FORMATTED_TCP_ADDR_STRLEN ? \
+ FORMATTED_UNIX_ADDR_STRLEN : \
+ FORMATTED_TCP_ADDR_STRLEN)
+
+static int format_socket_address(const struct sockaddr_storage *addr, char *buffer, size_t buflen)
+{
+ if (addr->ss_family == AF_INET || addr->ss_family == AF_INET6) {
+ char ip[FORMATTED_IP_ADDR_STRLEN];
+
+ spa_assert_se(format_ip_address(addr, ip, sizeof(ip)) >= 0);
+
+ return snprintf(buffer, buflen, "tcp:%s", ip);
+ }
+ else if (addr->ss_family == AF_UNIX) {
+ const struct sockaddr_un *addr_un = (const struct sockaddr_un *) addr;
+
+ return snprintf(buffer, buflen, "unix:%s", addr_un->sun_path);
+ }
+
+ return -EAFNOSUPPORT;
+}
+
+int servers_create_and_start(struct impl *impl, const char *addresses, struct pw_array *servers)
+{
+ 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];
+
+ /* 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)
+ return -EINVAL;
+
+ /* a server-spec is either an address or an object */
+ while ((len = spa_json_next(&it[1], &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;
+
+ if (spa_streq(key, "address")) {
+ spa_json_parse_stringn(v, len, addr_str, sizeof(addr_str));
+ } else if (spa_streq(key, "max-clients")) {
+ spa_json_parse_int(v, len, &max_clients);
+ } else if (spa_streq(key, "listen-backlog")) {
+ spa_json_parse_int(v, len, &listen_backlog);
+ } else if (spa_streq(key, "client.access")) {
+ spa_json_parse_stringn(v, len, client_access, sizeof(client_access));
+ }
+ }
+ } else {
+ spa_json_parse_stringn(v, len, addr_str, sizeof(addr_str));
+ }
+
+ n_addr = parse_address(addr_str, addrs, SPA_N_ELEMENTS(addrs));
+ if (n_addr < 0) {
+ pw_log_warn("pulse-server %p: failed to parse address '%s': %s",
+ impl, addr_str, spa_strerror(n_addr));
+ UPDATE_ERR(n_addr);
+ continue;
+ }
+
+ /* try to create sockets for each address in the list */
+ for (i = 0; i < n_addr; i++) {
+ const struct sockaddr_storage *addr = &addrs[i];
+ struct server * const server = server_new(impl);
+
+ if (server == NULL) {
+ UPDATE_ERR(-errno);
+ continue;
+ }
+
+ server->max_clients = max_clients;
+ server->listen_backlog = listen_backlog;
+ memcpy(server->client_access, client_access, sizeof(client_access));
+
+ res = server_start(server, addr);
+ if (res < 0) {
+ spa_assert_se(format_socket_address(addr, addr_str, sizeof(addr_str)) >= 0);
+ pw_log_warn("pulse-server %p: failed to start server on '%s': %s",
+ impl, addr_str, spa_strerror(res));
+ UPDATE_ERR(res);
+ server_free(server);
+ continue;
+ }
+
+ if (servers != NULL)
+ pw_array_add_ptr(servers, server);
+
+ count += 1;
+ }
+ }
+ if (count == 0) {
+ UPDATE_ERR(-EINVAL);
+ return err;
+ }
+ return count;
+
+#undef UPDATE_ERR
+}
+
+void server_free(struct server *server)
+{
+ struct impl * const impl = server->impl;
+ struct client *c, *t;
+
+ pw_log_debug("server %p: free", server);
+
+ spa_list_remove(&server->link);
+
+ spa_list_for_each_safe(c, t, &server->clients, link) {
+ spa_assert_se(client_detach(c));
+ client_unref(c);
+ }
+
+ spa_hook_list_call(&impl->hooks, struct impl_events, server_stopped, 0, server);
+
+ if (server->source)
+ pw_loop_destroy_source(impl->loop, server->source);
+
+ if (server->addr.ss_family == AF_UNIX && !server->activated)
+ unlink(((const struct sockaddr_un *) &server->addr)->sun_path);
+
+ free(server);
+}
diff --git a/src/modules/module-protocol-pulse/server.h b/src/modules/module-protocol-pulse/server.h
new file mode 100644
index 0000000..9474715
--- /dev/null
+++ b/src/modules/module-protocol-pulse/server.h
@@ -0,0 +1,60 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_SERVER_H
+#define PULSER_SERVER_SERVER_H
+
+#include <stdint.h>
+
+#include <sys/socket.h>
+
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+
+struct impl;
+struct pw_array;
+struct spa_source;
+
+struct server {
+ struct spa_list link;
+ struct impl *impl;
+
+ struct sockaddr_storage addr;
+
+ struct spa_source *source;
+ struct spa_list clients;
+
+ uint32_t max_clients;
+ uint32_t listen_backlog;
+ char client_access[64];
+
+ uint32_t n_clients;
+ uint32_t wait_clients;
+ unsigned int activated:1;
+};
+
+int servers_create_and_start(struct impl *impl, const char *addresses, struct pw_array *servers);
+void server_free(struct server *server);
+
+#endif /* PULSER_SERVER_SERVER_H */
diff --git a/src/modules/module-protocol-pulse/stream.c b/src/modules/module-protocol-pulse/stream.c
new file mode 100644
index 0000000..59fb8a3
--- /dev/null
+++ b/src/modules/module-protocol-pulse/stream.c
@@ -0,0 +1,428 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <spa/utils/hook.h>
+#include <spa/utils/ringbuffer.h>
+#include <pipewire/log.h>
+#include <pipewire/loop.h>
+#include <pipewire/map.h>
+#include <pipewire/properties.h>
+#include <pipewire/stream.h>
+#include <pipewire/work-queue.h>
+
+#include "client.h"
+#include "commands.h"
+#include "defs.h"
+#include "internal.h"
+#include "log.h"
+#include "message.h"
+#include "reply.h"
+#include "stream.h"
+
+static int parse_frac(struct pw_properties *props, const char *key,
+ const struct spa_fraction *def, struct spa_fraction *res)
+{
+ const char *str;
+ if (props == NULL ||
+ (str = pw_properties_get(props, key)) == NULL ||
+ sscanf(str, "%u/%u", &res->num, &res->denom) != 2 ||
+ res->denom == 0) {
+ *res = *def;
+ }
+ return 0;
+}
+
+struct stream *stream_new(struct client *client, enum stream_type type, uint32_t create_tag,
+ const struct sample_spec *ss, const struct channel_map *map,
+ const struct buffer_attr *attr)
+{
+ int res;
+ struct defs *defs = &client->impl->defs;
+ const char *str;
+
+ struct stream *stream = calloc(1, sizeof(*stream));
+ if (stream == NULL)
+ return NULL;
+
+ stream->channel = pw_map_insert_new(&client->streams, stream);
+ if (stream->channel == SPA_ID_INVALID)
+ goto error_errno;
+
+ stream->impl = client->impl;
+ stream->client = client;
+ stream->type = type;
+ stream->create_tag = create_tag;
+ stream->ss = *ss;
+ stream->map = *map;
+ stream->attr = *attr;
+ spa_ringbuffer_init(&stream->ring);
+
+ stream->peer_index = SPA_ID_INVALID;
+
+ parse_frac(client->props, "pulse.min.req", &defs->min_req, &stream->min_req);
+ parse_frac(client->props, "pulse.min.frag", &defs->min_frag, &stream->min_frag);
+ parse_frac(client->props, "pulse.min.quantum", &defs->min_quantum, &stream->min_quantum);
+ parse_frac(client->props, "pulse.default.req", &defs->default_req, &stream->default_req);
+ parse_frac(client->props, "pulse.default.frag", &defs->default_frag, &stream->default_frag);
+ parse_frac(client->props, "pulse.default.tlength", &defs->default_tlength, &stream->default_tlength);
+ stream->idle_timeout_sec = defs->idle_timeout;
+ if ((str = pw_properties_get(client->props, "pulse.idle.timeout")) != NULL)
+ spa_atou32(str, &stream->idle_timeout_sec, 0);
+
+ switch (type) {
+ case STREAM_TYPE_RECORD:
+ stream->direction = PW_DIRECTION_INPUT;
+ break;
+ case STREAM_TYPE_PLAYBACK:
+ case STREAM_TYPE_UPLOAD:
+ stream->direction = PW_DIRECTION_OUTPUT;
+ break;
+ default:
+ spa_assert_not_reached();
+ }
+
+ return stream;
+
+error_errno:
+ res = errno;
+ free(stream);
+ errno = res;
+
+ return NULL;
+}
+
+void stream_free(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+
+ pw_log_debug("client %p: stream %p channel:%d", client, stream, stream->channel);
+
+ if (stream->pending)
+ spa_list_remove(&stream->link);
+
+ if (stream->drain_tag)
+ reply_error(client, -1, stream->drain_tag, -ENOENT);
+
+ if (stream->killed)
+ stream_send_killed(stream);
+
+ if (stream->stream) {
+ spa_hook_remove(&stream->stream_listener);
+ pw_stream_disconnect(stream->stream);
+
+ /* force processing of all pending messages before we destroy
+ * the stream */
+ pw_loop_invoke(impl->loop, NULL, 0, NULL, 0, false, client);
+
+ pw_stream_destroy(stream->stream);
+ }
+ if (stream->channel != SPA_ID_INVALID)
+ pw_map_remove(&client->streams, stream->channel);
+
+ pw_work_queue_cancel(impl->work_queue, stream, SPA_ID_INVALID);
+
+ if (stream->buffer)
+ free(stream->buffer);
+
+ pw_properties_free(stream->props);
+
+ free(stream);
+}
+
+void stream_flush(struct stream *stream)
+{
+ pw_stream_flush(stream->stream, false);
+
+ if (stream->type == STREAM_TYPE_PLAYBACK) {
+ stream->ring.writeindex = stream->ring.readindex;
+ stream->write_index = stream->read_index;
+
+ if (stream->attr.prebuf > 0)
+ stream->in_prebuf = true;
+
+ stream->playing_for = 0;
+ stream->underrun_for = -1;
+ stream->is_underrun = true;
+
+ stream_send_request(stream);
+ } else {
+ stream->ring.readindex = stream->ring.writeindex;
+ stream->read_index = stream->write_index;
+ }
+}
+
+static bool stream_prebuf_active(struct stream *stream, int32_t avail)
+{
+ if (stream->in_prebuf) {
+ if (avail >= (int32_t) stream->attr.prebuf)
+ stream->in_prebuf = false;
+ } else {
+ if (stream->attr.prebuf > 0 && avail <= 0)
+ stream->in_prebuf = true;
+ }
+ return stream->in_prebuf;
+}
+
+uint32_t stream_pop_missing(struct stream *stream)
+{
+ int64_t missing, avail;
+
+ avail = stream->write_index - stream->read_index;
+
+ missing = stream->attr.tlength;
+ missing -= stream->requested;
+ missing -= avail;
+
+ if (missing <= 0)
+ return 0;
+
+ if (missing < stream->attr.minreq && !stream_prebuf_active(stream, avail))
+ return 0;
+
+ stream->requested += missing;
+
+ return missing;
+}
+
+void stream_set_paused(struct stream *stream, bool paused, const char *reason)
+{
+ if (stream->is_paused == paused)
+ return;
+
+ if (reason && stream->client)
+ pw_log_info("%p: [%s] %s because of %s",
+ stream, stream->client->name,
+ paused ? "paused" : "resumed", reason);
+
+ stream->is_paused = paused;
+ pw_stream_set_active(stream->stream, !paused);
+}
+
+int stream_send_underflow(struct stream *stream, int64_t offset)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+
+ if (ratelimit_test(&impl->rate_limit, stream->timestamp, SPA_LOG_LEVEL_INFO)) {
+ pw_log_info("[%s]: UNDERFLOW channel:%u offset:%" PRIi64,
+ client->name, stream->channel, offset);
+ }
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, COMMAND_UNDERFLOW,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_INVALID);
+
+ if (client->version >= 23) {
+ message_put(reply,
+ TAG_S64, offset,
+ TAG_INVALID);
+ }
+
+ return client_queue_message(client, reply);
+}
+
+int stream_send_overflow(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+
+ pw_log_warn("client %p [%s]: stream %p OVERFLOW channel:%u",
+ client, client->name, stream, stream->channel);
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, COMMAND_OVERFLOW,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+int stream_send_killed(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t command;
+
+ command = stream->direction == PW_DIRECTION_OUTPUT ?
+ COMMAND_PLAYBACK_STREAM_KILLED :
+ COMMAND_RECORD_STREAM_KILLED;
+
+ pw_log_info("[%s]: %s channel:%u",
+ client->name, commands[command].name, stream->channel);
+
+ if (client->version < 23)
+ return 0;
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, command,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+int stream_send_started(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+
+ pw_log_debug("client %p [%s]: stream %p STARTED channel:%u",
+ client, client->name, stream, stream->channel);
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, COMMAND_STARTED,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_INVALID);
+
+ return client_queue_message(client, reply);
+}
+
+int stream_send_request(struct stream *stream)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *msg;
+ uint32_t size;
+
+ size = stream_pop_missing(stream);
+ pw_log_debug("stream %p: REQUEST channel:%d %u", stream, stream->channel, size);
+
+ if (size == 0)
+ return 0;
+
+ msg = message_alloc(impl, -1, 0);
+ message_put(msg,
+ TAG_U32, COMMAND_REQUEST,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_U32, size,
+ TAG_INVALID);
+
+ return client_queue_message(client, msg);
+}
+
+int stream_update_minreq(struct stream *stream, uint32_t minreq)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ uint32_t old_tlength = stream->attr.tlength;
+ uint32_t new_tlength = minreq + 2 * stream->attr.minreq;
+ uint64_t lat_usec;
+
+ if (new_tlength <= old_tlength)
+ return 0;
+
+ if (new_tlength > MAXLENGTH)
+ new_tlength = MAXLENGTH;
+
+ stream->attr.tlength = new_tlength;
+ if (stream->attr.tlength > stream->attr.maxlength)
+ stream->attr.maxlength = stream->attr.tlength;
+
+ if (client->version >= 15) {
+ struct message *msg;
+
+ lat_usec = minreq * SPA_USEC_PER_SEC / stream->ss.rate;
+
+ msg = message_alloc(impl, -1, 0);
+ message_put(msg,
+ TAG_U32, COMMAND_PLAYBACK_BUFFER_ATTR_CHANGED,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.tlength,
+ TAG_U32, stream->attr.prebuf,
+ TAG_U32, stream->attr.minreq,
+ TAG_USEC, lat_usec,
+ TAG_INVALID);
+ return client_queue_message(client, msg);
+ }
+ return 0;
+}
+
+int stream_send_moved(struct stream *stream, uint32_t peer_index, const char *peer_name)
+{
+ struct client *client = stream->client;
+ struct impl *impl = client->impl;
+ struct message *reply;
+ uint32_t command;
+
+ command = stream->direction == PW_DIRECTION_OUTPUT ?
+ COMMAND_PLAYBACK_STREAM_MOVED :
+ COMMAND_RECORD_STREAM_MOVED;
+
+ pw_log_info("client %p [%s]: stream %p %s channel:%u",
+ client, client->name, stream, commands[command].name,
+ stream->channel);
+
+ if (client->version < 12)
+ return 0;
+
+ reply = message_alloc(impl, -1, 0);
+ message_put(reply,
+ TAG_U32, command,
+ TAG_U32, -1,
+ TAG_U32, stream->channel,
+ TAG_U32, peer_index,
+ TAG_STRING, peer_name,
+ TAG_BOOLEAN, false, /* suspended */
+ TAG_INVALID);
+
+ if (client->version >= 13) {
+ if (command == COMMAND_PLAYBACK_STREAM_MOVED) {
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.tlength,
+ TAG_U32, stream->attr.prebuf,
+ TAG_U32, stream->attr.minreq,
+ TAG_USEC, stream->lat_usec,
+ TAG_INVALID);
+ } else {
+ message_put(reply,
+ TAG_U32, stream->attr.maxlength,
+ TAG_U32, stream->attr.fragsize,
+ TAG_USEC, stream->lat_usec,
+ TAG_INVALID);
+ }
+ }
+ return client_queue_message(client, reply);
+}
diff --git a/src/modules/module-protocol-pulse/stream.h b/src/modules/module-protocol-pulse/stream.h
new file mode 100644
index 0000000..424b2a1
--- /dev/null
+++ b/src/modules/module-protocol-pulse/stream.h
@@ -0,0 +1,141 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSER_SERVER_STREAM_H
+#define PULSER_SERVER_STREAM_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <spa/utils/hook.h>
+#include <spa/utils/ringbuffer.h>
+#include <pipewire/pipewire.h>
+
+#include "format.h"
+#include "volume.h"
+
+struct impl;
+struct client;
+struct spa_io_rate_match;
+
+struct buffer_attr {
+ uint32_t maxlength;
+ uint32_t tlength;
+ uint32_t prebuf;
+ uint32_t minreq;
+ uint32_t fragsize;
+};
+
+enum stream_type {
+ STREAM_TYPE_RECORD,
+ STREAM_TYPE_PLAYBACK,
+ STREAM_TYPE_UPLOAD,
+};
+
+struct stream {
+ struct spa_list link;
+ uint32_t create_tag;
+ uint32_t channel; /* index in map */
+ uint32_t id; /* id of global */
+ uint32_t index; /* index */
+
+ uint32_t peer_index;
+
+ struct impl *impl;
+ struct client *client;
+ enum stream_type type;
+ enum pw_direction direction;
+
+ struct pw_properties *props;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_io_position *position;
+ struct spa_ringbuffer ring;
+ void *buffer;
+
+ int64_t read_index;
+ int64_t write_index;
+ uint64_t underrun_for;
+ uint64_t playing_for;
+ uint64_t ticks_base;
+ uint64_t timestamp;
+ uint64_t idle_time;
+ int64_t delay;
+
+ uint32_t last_quantum;
+ int64_t requested;
+
+ struct spa_fraction min_req;
+ struct spa_fraction default_req;
+ struct spa_fraction min_frag;
+ struct spa_fraction default_frag;
+ struct spa_fraction default_tlength;
+ struct spa_fraction min_quantum;
+ uint32_t idle_timeout_sec;
+
+ struct sample_spec ss;
+ struct channel_map map;
+ struct buffer_attr attr;
+ uint32_t frame_size;
+ uint32_t rate;
+ uint64_t lat_usec;
+
+ struct volume volume;
+ bool muted;
+
+ uint32_t drain_tag;
+ unsigned int corked:1;
+ unsigned int draining:1;
+ unsigned int volume_set:1;
+ unsigned int muted_set:1;
+ unsigned int early_requests:1;
+ unsigned int adjust_latency:1;
+ unsigned int is_underrun:1;
+ unsigned int in_prebuf:1;
+ unsigned int killed:1;
+ unsigned int pending:1;
+ unsigned int is_idle:1;
+ unsigned int is_paused:1;
+};
+
+struct stream *stream_new(struct client *client, enum stream_type type, uint32_t create_tag,
+ const struct sample_spec *ss, const struct channel_map *map,
+ const struct buffer_attr *attr);
+void stream_free(struct stream *stream);
+void stream_flush(struct stream *stream);
+uint32_t stream_pop_missing(struct stream *stream);
+
+void stream_set_paused(struct stream *stream, bool paused, const char *reason);
+
+int stream_send_underflow(struct stream *stream, int64_t offset);
+int stream_send_overflow(struct stream *stream);
+int stream_send_killed(struct stream *stream);
+int stream_send_started(struct stream *stream);
+int stream_send_request(struct stream *stream);
+int stream_update_minreq(struct stream *stream, uint32_t minreq);
+int stream_send_moved(struct stream *stream, uint32_t peer_index, const char *peer_name);
+
+#endif /* PULSER_SERVER_STREAM_H */
diff --git a/src/modules/module-protocol-pulse/utils.c b/src/modules/module-protocol-pulse/utils.c
new file mode 100644
index 0000000..7626449
--- /dev/null
+++ b/src/modules/module-protocol-pulse/utils.c
@@ -0,0 +1,207 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <limits.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#ifdef HAVE_SYS_VFS_H
+#include <sys/vfs.h>
+#endif
+#ifdef HAVE_PWD_H
+#include <pwd.h>
+#endif
+
+#include <spa/utils/result.h>
+#include <pipewire/context.h>
+#include <pipewire/log.h>
+#include <pipewire/keys.h>
+
+#include "log.h"
+#include "utils.h"
+
+int get_runtime_dir(char *buf, size_t buflen)
+{
+ const char *runtime_dir, *dir = NULL;
+ struct stat stat_buf;
+ int res, size;
+
+ runtime_dir = getenv("PULSE_RUNTIME_PATH");
+ if (runtime_dir == NULL) {
+ runtime_dir = getenv("XDG_RUNTIME_DIR");
+ dir = "pulse";
+ }
+ if (runtime_dir == NULL) {
+ pw_log_error("could not find a suitable runtime directory in"
+ "$PULSE_RUNTIME_PATH and $XDG_RUNTIME_DIR");
+ return -ENOENT;
+ }
+
+ size = snprintf(buf, buflen, "%s%s%s", runtime_dir,
+ dir ? "/" : "", dir ? dir : "");
+ if (size < 0)
+ return -errno;
+ if ((size_t) size >= buflen) {
+ pw_log_error("path %s%s%s too long", runtime_dir,
+ dir ? "/" : "", dir ? dir : "");
+ return -ENAMETOOLONG;
+ }
+
+ if (stat(buf, &stat_buf) < 0) {
+ res = -errno;
+ if (res != -ENOENT) {
+ pw_log_error("stat() %s failed: %m", buf);
+ return res;
+ }
+ if (mkdir(buf, 0700) < 0) {
+ res = -errno;
+ pw_log_error("mkdir() %s failed: %m", buf);
+ return res;
+ }
+ pw_log_info("created %s", buf);
+ } else if (!S_ISDIR(stat_buf.st_mode)) {
+ pw_log_error("%s is not a directory", buf);
+ return -ENOTDIR;
+ }
+ return 0;
+}
+
+int check_flatpak(struct client *client, pid_t pid)
+{
+ char root_path[2048];
+ int root_fd, info_fd, res;
+ struct stat stat_buf;
+
+ sprintf(root_path, "/proc/%ld/root", (long) pid);
+ root_fd = openat(AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
+ if (root_fd == -1) {
+ res = -errno;
+ if (res == -EACCES) {
+ struct statfs buf;
+ /* Access to the root dir isn't allowed. This can happen if the root is on a fuse
+ * filesystem, such as in a toolbox container. We will never have a fuse rootfs
+ * in the flatpak case, so in that case its safe to ignore this and
+ * continue to detect other types of apps. */
+ if (statfs(root_path, &buf) == 0 &&
+ buf.f_type == 0x65735546) /* FUSE_SUPER_MAGIC */
+ return 0;
+ }
+ /* Not able to open the root dir shouldn't happen. Probably the app died and
+ * we're failing due to /proc/$pid not existing. In that case fail instead
+ * of treating this as privileged. */
+ pw_log_info("failed to open \"%s\"%s", root_path, spa_strerror(res));
+ return res;
+ }
+ info_fd = openat(root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+ close(root_fd);
+ if (info_fd == -1) {
+ if (errno == ENOENT) {
+ pw_log_debug("no .flatpak-info, client on the host");
+ /* No file => on the host */
+ return 0;
+ }
+ res = -errno;
+ pw_log_error("error opening .flatpak-info: %m");
+ return res;
+ }
+ if (fstat(info_fd, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)) {
+ /* Some weird fd => failure, assume sandboxed */
+ pw_log_error("error fstat .flatpak-info: %m");
+ }
+ close(info_fd);
+ return 1;
+}
+
+pid_t get_client_pid(struct client *client, int client_fd)
+{
+ socklen_t len;
+#if defined(__linux__)
+ struct ucred ucred;
+ len = sizeof(ucred);
+ if (getsockopt(client_fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) < 0) {
+ pw_log_warn("client %p: no peercred: %m", client);
+ } else
+ return ucred.pid;
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
+ struct xucred xucred;
+ len = sizeof(xucred);
+ if (getsockopt(client_fd, 0, LOCAL_PEERCRED, &xucred, &len) < 0) {
+ pw_log_warn("client %p: no peercred: %m", client);
+ } else {
+#if __FreeBSD__ >= 13
+ return xucred.cr_pid;
+#endif
+ }
+#endif
+ return 0;
+}
+
+const char *get_server_name(struct pw_context *context)
+{
+ const char *name = NULL;
+ 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 || name[0] == '\0')
+ name = PW_DEFAULT_REMOTE;
+ return name;
+}
+
+int create_pid_file(void) {
+ char pid_file[PATH_MAX];
+ FILE *f;
+ int res;
+
+ if ((res = get_runtime_dir(pid_file, sizeof(pid_file))) < 0)
+ return res;
+
+ if (strlen(pid_file) > PATH_MAX - sizeof("/pid")) {
+ pw_log_error("path too long: %s/pid", pid_file);
+ return -ENAMETOOLONG;
+ }
+
+ strcat(pid_file, "/pid");
+
+ if ((f = fopen(pid_file, "we")) == NULL) {
+ res = -errno;
+ pw_log_error("failed to open pid file: %m");
+ return res;
+ }
+
+ fprintf(f, "%lu\n", (unsigned long) getpid());
+ fclose(f);
+
+ return 0;
+}
diff --git a/src/modules/module-protocol-pulse/utils.h b/src/modules/module-protocol-pulse/utils.h
new file mode 100644
index 0000000..fafccf3
--- /dev/null
+++ b/src/modules/module-protocol-pulse/utils.h
@@ -0,0 +1,40 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_UTILS_H
+#define PULSE_SERVER_UTILS_H
+
+#include <stddef.h>
+#include <sys/types.h>
+
+struct client;
+struct pw_context;
+
+int get_runtime_dir(char *buf, size_t buflen);
+int check_flatpak(struct client *client, pid_t pid);
+pid_t get_client_pid(struct client *client, int client_fd);
+const char *get_server_name(struct pw_context *context);
+int create_pid_file(void);
+
+#endif /* PULSE_SERVER_UTILS_H */
diff --git a/src/modules/module-protocol-pulse/volume.c b/src/modules/module-protocol-pulse/volume.c
new file mode 100644
index 0000000..31f45a5
--- /dev/null
+++ b/src/modules/module-protocol-pulse/volume.c
@@ -0,0 +1,114 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/param/props.h>
+#include <spa/param/audio/raw.h>
+#include <spa/pod/iter.h>
+#include <spa/utils/defs.h>
+#include <pipewire/log.h>
+
+#include "log.h"
+#include "volume.h"
+
+int volume_compare(struct volume *vol, struct volume *other)
+{
+ uint8_t i;
+ if (vol->channels != other->channels) {
+ pw_log_info("channels %d<>%d", vol->channels, other->channels);
+ return -1;
+ }
+ for (i = 0; i < vol->channels; i++) {
+ if (vol->values[i] != other->values[i]) {
+ pw_log_info("%d: val %f<>%f", i, vol->values[i], other->values[i]);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bool monitor)
+{
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ struct spa_pod_prop *prop;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &info->level) < 0)
+ continue;
+ SPA_FLAG_UPDATE(info->flags, VOLUME_HW_VOLUME,
+ prop->flags & SPA_POD_PROP_FLAG_HARDWARE);
+
+ break;
+ case SPA_PROP_mute:
+ if (monitor)
+ continue;
+ if (spa_pod_get_bool(&prop->value, &info->mute) < 0)
+ continue;
+ SPA_FLAG_UPDATE(info->flags, VOLUME_HW_MUTE,
+ prop->flags & SPA_POD_PROP_FLAG_HARDWARE);
+ break;
+ case SPA_PROP_channelVolumes:
+ if (monitor)
+ continue;
+ info->volume.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ info->volume.values, SPA_AUDIO_MAX_CHANNELS);
+ SPA_FLAG_UPDATE(info->flags, VOLUME_HW_VOLUME,
+ prop->flags & SPA_POD_PROP_FLAG_HARDWARE);
+ break;
+ case SPA_PROP_monitorMute:
+ if (!monitor)
+ continue;
+ if (spa_pod_get_bool(&prop->value, &info->mute) < 0)
+ continue;
+ SPA_FLAG_CLEAR(info->flags, VOLUME_HW_MUTE);
+ break;
+ case SPA_PROP_monitorVolumes:
+ if (!monitor)
+ continue;
+ info->volume.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ info->volume.values, SPA_AUDIO_MAX_CHANNELS);
+ SPA_FLAG_CLEAR(info->flags, VOLUME_HW_VOLUME);
+ break;
+ case SPA_PROP_volumeBase:
+ if (spa_pod_get_float(&prop->value, &info->base) < 0)
+ continue;
+ break;
+ case SPA_PROP_volumeStep:
+ {
+ float step;
+ if (spa_pod_get_float(&prop->value, &step) >= 0)
+ info->steps = 0x10000u * step;
+ break;
+ }
+ case SPA_PROP_channelMap:
+ info->map.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ info->map.map, SPA_AUDIO_MAX_CHANNELS);
+ break;
+ default:
+ break;
+ }
+ }
+ return 0;
+}
diff --git a/src/modules/module-protocol-pulse/volume.h b/src/modules/module-protocol-pulse/volume.h
new file mode 100644
index 0000000..11ec51a
--- /dev/null
+++ b/src/modules/module-protocol-pulse/volume.h
@@ -0,0 +1,85 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PULSE_SERVER_VOLUME_H
+#define PULSE_SERVER_VOLUME_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "format.h"
+
+struct spa_pod;
+
+struct volume {
+ uint8_t channels;
+ float values[CHANNELS_MAX];
+};
+
+#define VOLUME_INIT \
+ (struct volume) { \
+ .channels = 0, \
+ }
+
+struct volume_info {
+ struct volume volume;
+ struct channel_map map;
+ bool mute;
+ float level;
+ float base;
+ uint32_t steps;
+#define VOLUME_HW_VOLUME (1<<0)
+#define VOLUME_HW_MUTE (1<<1)
+ uint32_t flags;
+};
+
+#define VOLUME_INFO_INIT \
+ (struct volume_info) { \
+ .volume = VOLUME_INIT, \
+ .mute = false, \
+ .level = 1.0, \
+ .base = 1.0, \
+ .steps = 256, \
+ }
+
+static inline bool volume_valid(const struct volume *vol)
+{
+ if (vol->channels == 0 || vol->channels > CHANNELS_MAX)
+ return false;
+ return true;
+}
+
+static inline void volume_make(struct volume *vol, uint8_t channels)
+{
+ uint8_t i;
+ for (i = 0; i < channels; i++)
+ vol->values[i] = 1.0f;
+ vol->channels = channels;
+}
+
+int volume_compare(struct volume *vol, struct volume *other);
+int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bool monitor);
+
+#endif
diff --git a/src/modules/module-protocol-simple.c b/src/modules/module-protocol-simple.c
new file mode 100644
index 0000000..1e17836
--- /dev/null
+++ b/src/modules/module-protocol-simple.c
@@ -0,0 +1,911 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <netinet/ip.h>
+#include <sys/un.h>
+#include <arpa/inet.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/pod/pod.h>
+#include <spa/pod/builder.h>
+#include <spa/debug/types.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/impl.h>
+
+/** \page page_module_protocol_simple PipeWire Module: Protocol Simple
+ *
+ * The simple protocol provides a bidirectional audio stream on a network
+ * socket.
+ *
+ * It is meant to be used with the `simple protocol player` app, available on
+ * Android to play and record a stream.
+ *
+ * Each client that connects will create a capture and/or playback stream,
+ * depending on the configuration options.
+ *
+ * ## Module Options
+ *
+ * - `capture`: boolean if capture is enabled. This will create a capture stream
+ * for each connected client.
+ * - `playback`: boolean if playback is enabled. This will create a playback
+ * stream for each connected client.
+ * - `capture.node`: an optional node serial or name to use for capture.
+ * - `playback.node`: an optional node serial or name to use for playback.
+ * - `server.address = []`: an array of server addresses to listen on as
+ * tcp:<ip>:<port>.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_RATE
+ * - \ref PW_KEY_STREAM_CAPTURE_SINK
+ *
+ * By default the server will work with stereo 16 bits samples at 44.1KHz.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-protocol-simple
+ * args = {
+ * # Provide capture stream, clients can capture data from PipeWire
+ * capture = true
+ * #
+ * # Provide playback stream, client can send data to PipeWire for playback
+ * playback = true
+ * #
+ * # The node name or id to use for capture.
+ * #capture.node = null
+ * #
+ * # To make the capture stream capture the monitor ports
+ * #stream.capture.sink = false
+ * #
+ * # The node name or id to use for playback.
+ * #playback.node = null
+ * #
+ * #audio.rate = 44100
+ * #audio.format = S16
+ * #audio.channels = 2
+ * #audio.position = [ FL FR ]
+ * #
+ * # The addresses this server listens on for new
+ * # client connections
+ * server.address = [
+ * "tcp:4711"
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "protocol-simple"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_PORT 4711
+#define DEFAULT_SERVER "[ \"tcp:"SPA_STRINGIFY(DEFAULT_PORT)"\" ]"
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+#define DEFAULT_LATENCY "1024/44100"
+
+#define MAX_CLIENTS 10
+
+#define MODULE_USAGE "[ capture=<bool> ] " \
+ "[ playback=<bool> ] " \
+ "[ remote.name=<remote> ] " \
+ "[ node.latency=<num/denom, default:"DEFAULT_LATENCY"> ] " \
+ "[ node.rate=<1/rate, default:1/"SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ capture.node=<source-target> [ stream.capture.sink=true ]] " \
+ "[ playback.node=<sink-target> ] " \
+ "[ audio.rate=<sample-rate, default:"SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ audio.format=<format, default:"DEFAULT_FORMAT"> ] " \
+ "[ audio.channels=<channels, default: "SPA_STRINGIFY(DEFAULT_CHANNELS)"> ] " \
+ "[ audio.position=<position, default:"DEFAULT_POSITION"> ] " \
+ "[ server.address=<[ tcp:[<ip>:]<port>[,...] ], default:"DEFAULT_SERVER">" \
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Implements a simple protocol" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_loop *loop;
+ struct pw_context *context;
+
+ struct pw_properties *props;
+ struct spa_hook module_listener;
+ struct spa_list server_list;
+
+ struct pw_work_queue *work_queue;
+
+ bool capture;
+ bool playback;
+
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+};
+
+struct client {
+ struct spa_list link;
+ struct impl *impl;
+ struct server *server;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+
+ struct spa_source *source;
+ char name[128];
+
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+
+ unsigned int disconnect:1;
+ unsigned int disconnecting:1;
+ unsigned int cleanup:1;
+};
+
+struct server {
+ struct spa_list link;
+ struct impl *impl;
+
+#define SERVER_TYPE_INVALID 0
+#define SERVER_TYPE_UNIX 1
+#define SERVER_TYPE_INET 2
+ uint32_t type;
+ struct sockaddr_un addr;
+ struct spa_source *source;
+
+ struct spa_list client_list;
+ uint32_t n_clients;
+};
+
+static void client_disconnect(struct client *client)
+{
+ struct impl *impl = client->impl;
+
+ if (client->disconnect)
+ return;
+
+ client->disconnect = true;
+
+ if (client->source)
+ pw_loop_destroy_source(impl->loop, client->source);
+}
+
+static void client_free(struct client *client)
+{
+ struct impl *impl = client->impl;
+
+ pw_log_info("%p: client:%p [%s] free", impl, client, client->name);
+
+ client_disconnect(client);
+
+ pw_work_queue_cancel(impl->work_queue, client, SPA_ID_INVALID);
+
+ spa_list_remove(&client->link);
+ client->server->n_clients--;
+
+ if (client->capture)
+ pw_stream_destroy(client->capture);
+ if (client->playback)
+ pw_stream_destroy(client->playback);
+ if (client->core) {
+ client->disconnecting = true;
+ spa_hook_remove(&client->core_proxy_listener);
+ pw_core_disconnect(client->core);
+ }
+ free(client);
+}
+
+
+static void on_client_cleanup(void *obj, void *data, int res, uint32_t id)
+{
+ struct client *c = obj;
+ client_free(c);
+}
+
+static void client_cleanup(struct client *client)
+{
+ struct impl *impl = client->impl;
+ if (!client->cleanup) {
+ client->cleanup = true;
+ pw_work_queue_add(impl->work_queue, client, 0, on_client_cleanup, impl);
+ }
+}
+
+static void
+on_client_data(void *data, int fd, uint32_t mask)
+{
+ struct client *client = data;
+ struct impl *impl = client->impl;
+ int res;
+
+ if (mask & SPA_IO_HUP) {
+ res = -EPIPE;
+ goto error;
+ }
+ if (mask & SPA_IO_ERR) {
+ res = -EIO;
+ goto error;
+ }
+ return;
+
+error:
+ if (res == -EPIPE)
+ pw_log_info("%p: client:%p [%s] disconnected", impl, client, client->name);
+ else {
+ pw_log_error("%p: client:%p [%s] error %d (%s)", impl,
+ client, client->name, res, spa_strerror(res));
+ }
+ client_cleanup(client);
+}
+
+static void capture_process(void *data)
+{
+ struct client *client = data;
+ struct impl *impl = client->impl;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t size, offset;
+ int res;
+
+ if ((buf = pw_stream_dequeue_buffer(client->capture)) == NULL) {
+ pw_log_debug("%p: client:%p [%s] out of capture buffers: %m", impl,
+ client, client->name);
+ return;
+ }
+ d = &buf->buffer->datas[0];
+
+ offset = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offset);
+
+ while (size > 0) {
+ res = send(client->source->fd,
+ SPA_PTROFF(d->data, offset, void),
+ size,
+ MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (res < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno != EAGAIN && errno != EWOULDBLOCK)
+ pw_log_warn("%p: client:%p [%s] send error %d: %m", impl,
+ client, client->name, res);
+ client_cleanup(client);
+ break;
+ }
+ offset += res;
+ size -= res;
+ }
+ pw_stream_queue_buffer(client->capture, buf);
+}
+
+static void playback_process(void *data)
+{
+ struct client *client = data;
+ struct impl *impl = client->impl;
+ struct pw_buffer *buf;
+ uint32_t size, offset;
+ struct spa_data *d;
+ int res;
+
+ if ((buf = pw_stream_dequeue_buffer(client->playback)) == NULL) {
+ pw_log_debug("%p: client:%p [%s] out of playback buffers: %m", impl,
+ client, client->name);
+ return;
+ }
+ d = &buf->buffer->datas[0];
+
+ size = d->maxsize;
+ if (buf->requested)
+ size = SPA_MIN(size, buf->requested * impl->frame_size);
+
+ offset = 0;
+ while (size > 0) {
+ res = recv(client->source->fd,
+ SPA_PTROFF(d->data, offset, void),
+ size,
+ MSG_DONTWAIT);
+ if (res == 0) {
+ pw_log_info("%p: client:%p [%s] disconnect", impl,
+ client, client->name);
+ client_cleanup(client);
+ break;
+ }
+ if (res < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno != EAGAIN && errno != EWOULDBLOCK)
+ pw_log_warn("%p: client:%p [%s] recv error %d: %m",
+ impl, client, client->name, res);
+ break;
+ }
+ offset += res;
+ size -= res;
+ }
+ d->chunk->offset = 0;
+ d->chunk->size = offset;
+ d->chunk->stride = impl->frame_size;
+
+ pw_stream_queue_buffer(client->playback, buf);
+}
+
+static void capture_destroy(void *data)
+{
+ struct client *client = data;
+ spa_hook_remove(&client->capture_listener);
+ client->capture = NULL;
+}
+
+static void on_stream_state_changed(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct client *client = data;
+ struct impl *impl = client->impl;
+
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ if (!client->disconnect) {
+ pw_log_info("%p: client:%p [%s] stream error %s",
+ impl, client, client->name,
+ pw_stream_state_as_string(state));
+ client_cleanup(client);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static void playback_destroy(void *data)
+{
+ struct client *client = data;
+ spa_hook_remove(&client->playback_listener);
+ client->playback = NULL;
+}
+
+static const struct pw_stream_events capture_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = capture_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = capture_process
+};
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = playback_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = playback_process
+};
+
+static int create_streams(struct impl *impl, struct client *client)
+{
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ struct pw_properties *props;
+ const char *latency;
+ int res;
+
+ if ((latency = pw_properties_get(impl->props, PW_KEY_NODE_LATENCY)) == NULL)
+ latency = DEFAULT_LATENCY;
+
+ if (impl->capture) {
+ props = pw_properties_new(
+ PW_KEY_NODE_LATENCY, latency,
+ PW_KEY_NODE_RATE, pw_properties_get(impl->props, PW_KEY_NODE_RATE),
+ PW_KEY_TARGET_OBJECT, pw_properties_get(impl->props, "capture.node"),
+ PW_KEY_STREAM_CAPTURE_SINK, pw_properties_get(impl->props,
+ PW_KEY_STREAM_CAPTURE_SINK),
+ PW_KEY_NODE_NETWORK, "true",
+ NULL);
+ if (props == NULL)
+ return -errno;
+
+ pw_properties_setf(props,
+ PW_KEY_MEDIA_NAME, "%s capture", client->name);
+ client->capture = pw_stream_new(client->core,
+ pw_properties_get(props, PW_KEY_MEDIA_NAME),
+ props);
+ if (client->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(client->capture, &client->capture_listener,
+ &capture_stream_events, client);
+ }
+ if (impl->playback) {
+ props = pw_properties_new(
+ PW_KEY_NODE_LATENCY, latency,
+ PW_KEY_NODE_RATE, pw_properties_get(impl->props, PW_KEY_NODE_RATE),
+ PW_KEY_TARGET_OBJECT, pw_properties_get(impl->props, "playback.node"),
+ PW_KEY_NODE_NETWORK, "true",
+ NULL);
+ if (props == NULL)
+ return -errno;
+
+ pw_properties_setf(props,
+ PW_KEY_MEDIA_NAME, "%s playback", client->name);
+
+ client->playback = pw_stream_new(client->core,
+ pw_properties_get(props, PW_KEY_MEDIA_NAME),
+ props);
+ if (client->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(client->playback, &client->playback_listener,
+ &playback_stream_events, client);
+ }
+
+ n_params = 0;
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &impl->info);
+
+ if (impl->capture) {
+ if ((res = pw_stream_connect(client->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+ }
+ if (impl->playback) {
+ if ((res = pw_stream_connect(client->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+ }
+ return 0;
+}
+
+static void on_core_proxy_destroy(void *data)
+{
+ struct client *client = data;
+ spa_hook_remove(&client->core_proxy_listener);
+ client->core = NULL;
+ client_cleanup(client);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ PW_VERSION_CORE_EVENTS,
+ .destroy = on_core_proxy_destroy,
+};
+
+static void
+on_connect(void *data, int fd, uint32_t mask)
+{
+ struct server *server = data;
+ struct impl *impl = server->impl;
+ struct sockaddr_in addr;
+ socklen_t addrlen;
+ int client_fd, val;
+ struct client *client = NULL;
+ struct pw_properties *props = NULL;
+
+ addrlen = sizeof(addr);
+ client_fd = accept4(fd, &addr, &addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
+ if (client_fd < 0)
+ goto error;
+
+ if (server->n_clients >= MAX_CLIENTS) {
+ close(client_fd);
+ errno = ECONNREFUSED;
+ goto error;
+ }
+
+ client = calloc(1, sizeof(struct client));
+ if (client == NULL)
+ goto error;
+
+ client->impl = impl;
+ client->server = server;
+ spa_list_append(&server->client_list, &client->link);
+ server->n_clients++;
+
+ if (inet_ntop(addr.sin_family, &addr.sin_addr.s_addr, client->name, sizeof(client->name)) == NULL)
+ snprintf(client->name, sizeof(client->name), "client %d", client_fd);
+
+ client->source = pw_loop_add_io(impl->loop,
+ client_fd,
+ SPA_IO_ERR | SPA_IO_HUP,
+ true, on_client_data, client);
+ if (client->source == NULL)
+ goto error;
+
+ pw_log_info("%p: client:%p [%s] connected", impl, client, client->name);
+
+ props = pw_properties_new(
+ PW_KEY_CLIENT_API, "protocol-simple",
+ PW_KEY_REMOTE_NAME,
+ pw_properties_get(impl->props, PW_KEY_REMOTE_NAME),
+ NULL);
+ if (props == NULL)
+ goto error;
+
+ pw_properties_setf(props,
+ "protocol.server.type", "%s",
+ server->type == SERVER_TYPE_INET ? "tcp" : "unix");
+
+ if (server->type == SERVER_TYPE_UNIX) {
+ goto error;
+ } else if (server->type == SERVER_TYPE_INET) {
+ val = 1;
+ if (setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY,
+ (const void *) &val, sizeof(val)) < 0)
+ pw_log_warn("TCP_NODELAY failed: %m");
+
+ val = IPTOS_LOWDELAY;
+ if (setsockopt(client_fd, IPPROTO_IP, IP_TOS,
+ (const void *) &val, sizeof(val)) < 0)
+ pw_log_warn("IP_TOS failed: %m");
+
+ pw_properties_set(props, PW_KEY_CLIENT_ACCESS, "restricted");
+ }
+
+ client->core = pw_context_connect(impl->context, props, 0);
+ props = NULL;
+ if (client->core == NULL)
+ goto error;
+
+ pw_proxy_add_listener((struct pw_proxy*)client->core,
+ &client->core_proxy_listener, &core_proxy_events,
+ client);
+
+ create_streams(impl, client);
+
+ return;
+error:
+ pw_log_error("%p: failed to create client: %m", impl);
+ pw_properties_free(props);
+ if (client != NULL)
+ client_free(client);
+ return;
+}
+
+static int make_inet_socket(struct server *server, const char *name)
+{
+ struct sockaddr_in addr;
+ int res, fd, on;
+ uint32_t address = INADDR_ANY;
+ uint16_t port;
+ char *col;
+
+ col = strchr(name, ':');
+ if (col) {
+ struct in_addr ipv4;
+ char *n;
+ port = atoi(col+1);
+ n = strndupa(name, col - name);
+ if (inet_pton(AF_INET, n, &ipv4) > 0)
+ address = ntohl(ipv4.s_addr);
+ else
+ address = INADDR_ANY;
+ } else {
+ address = INADDR_ANY;
+ port = atoi(name);
+ }
+ if (port == 0)
+ port = DEFAULT_PORT;
+
+ if ((fd = socket(PF_INET, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ res = -errno;
+ pw_log_error("%p: socket() failed: %m", server);
+ goto error;
+ }
+
+ on = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void *) &on, sizeof(on)) < 0)
+ pw_log_warn("%p: setsockopt(): %m", server);
+
+ spa_zero(addr);
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+ addr.sin_addr.s_addr = htonl(address);
+
+ if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
+ res = -errno;
+ pw_log_error("%p: bind() failed: %m", server);
+ goto error_close;
+ }
+ if (listen(fd, 5) < 0) {
+ res = -errno;
+ pw_log_error("%p: listen() failed: %m", server);
+ goto error_close;
+ }
+ server->type = SERVER_TYPE_INET;
+ pw_log_info("listening on tcp:%08x:%u", address, port);
+
+ return fd;
+
+error_close:
+ close(fd);
+error:
+ return res;
+}
+
+static void server_free(struct server *server)
+{
+ struct impl *impl = server->impl;
+ struct client *c;
+
+ pw_log_debug("%p: free server %p", impl, server);
+
+ spa_list_remove(&server->link);
+ spa_list_consume(c, &server->client_list, link)
+ client_free(c);
+ if (server->source)
+ pw_loop_destroy_source(impl->loop, server->source);
+ free(server);
+}
+
+static struct server *create_server(struct impl *impl, const char *address)
+{
+ int fd, res;
+ struct server *server;
+
+ server = calloc(1, sizeof(struct server));
+ if (server == NULL)
+ return NULL;
+
+ server->impl = impl;
+ spa_list_init(&server->client_list);
+ spa_list_append(&impl->server_list, &server->link);
+
+ if (spa_strstartswith(address, "tcp:")) {
+ fd = make_inet_socket(server, address+4);
+ } else {
+ fd = -EINVAL;
+ }
+ if (fd < 0) {
+ res = fd;
+ goto error;
+ }
+ server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_connect, server);
+ if (server->source == NULL) {
+ res = -errno;
+ pw_log_error("%p: can't create server source: %m", impl);
+ goto error_close;
+ }
+ return server;
+
+error_close:
+ close(fd);
+error:
+ server_free(server);
+ errno = -res;
+ return NULL;
+}
+
+static void impl_free(struct impl *impl)
+{
+ struct server *s;
+
+ spa_hook_remove(&impl->module_listener);
+ spa_list_consume(s, &impl->server_list, link)
+ server_free(s);
+ pw_properties_free(impl->props);
+ 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 inline uint32_t parse_position(uint32_t *pos, const char *val, size_t len)
+{
+ uint32_t channels = 0;
+ 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);
+
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ channels < SPA_AUDIO_MAX_CHANNELS) {
+ pos[channels++] = channel_from_name(v);
+ }
+ return channels;
+}
+
+static int parse_params(struct impl *impl)
+{
+ const char *str;
+ struct spa_json it[2];
+ char value[512];
+
+ pw_properties_fetch_bool(impl->props, "capture", &impl->capture);
+ pw_properties_fetch_bool(impl->props, "playback", &impl->playback);
+ if (!impl->playback && !impl->capture) {
+ pw_log_error("missing capture or playback param");
+ return -EINVAL;
+ }
+
+ if ((str = pw_properties_get(impl->props, "audio.format")) == NULL)
+ str = DEFAULT_FORMAT;
+ impl->info.format = format_from_name(str, strlen(str));
+ if (impl->info.format == SPA_AUDIO_FORMAT_UNKNOWN) {
+ pw_log_error("invalid format '%s'", str);
+ return -EINVAL;
+ }
+ impl->info.rate = pw_properties_get_uint32(impl->props, "audio.rate", DEFAULT_RATE);
+ if (impl->info.rate == 0) {
+ pw_log_error("invalid rate '%s'", str);
+ return -EINVAL;
+ }
+ impl->info.channels = pw_properties_get_uint32(impl->props, "audio.channels", DEFAULT_CHANNELS);
+ if (impl->info.channels == 0) {
+ pw_log_error("invalid channels '%s'", str);
+ return -EINVAL;
+ }
+ if ((str = pw_properties_get(impl->props, "audio.position")) == NULL)
+ str = DEFAULT_POSITION;
+ if (parse_position(impl->info.position, str, strlen(str)) != impl->info.channels) {
+ pw_log_error("invalid position '%s'", str);
+ return -EINVAL;
+ }
+
+ switch (impl->info.format) {
+ case SPA_AUDIO_FORMAT_U8:
+ impl->frame_size = 1;
+ break;
+ case SPA_AUDIO_FORMAT_S16_LE:
+ case SPA_AUDIO_FORMAT_S16_BE:
+ case SPA_AUDIO_FORMAT_S16P:
+ impl->frame_size = 2;
+ break;
+ case SPA_AUDIO_FORMAT_S24_LE:
+ case SPA_AUDIO_FORMAT_S24_BE:
+ case SPA_AUDIO_FORMAT_S24P:
+ impl->frame_size = 3;
+ break;
+ default:
+ impl->frame_size = 4;
+ break;
+ }
+ impl->frame_size *= impl->info.channels;
+
+ 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 (create_server(impl, value) == NULL) {
+ pw_log_warn("%p: can't create server for %s: %m",
+ impl, value);
+ }
+ }
+ }
+ return 0;
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ pw_log_debug("module %p: destroy", impl);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args)
+ props = pw_properties_new_string(args);
+ else
+ props = pw_properties_new(NULL, NULL);
+
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+ impl->props = props;
+ spa_list_init(&impl->server_list);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ impl->work_queue = pw_context_get_work_queue(context);
+
+ if ((res = parse_params(impl)) < 0)
+ goto error_free;
+
+ return 0;
+
+error_free:
+ impl_free(impl);
+ return res;
+}
diff --git a/src/modules/module-pulse-tunnel.c b/src/modules/module-pulse-tunnel.c
new file mode 100644
index 0000000..e2fe8b6
--- /dev/null
+++ b/src/modules/module-pulse-tunnel.c
@@ -0,0 +1,1080 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.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/utils/ringbuffer.h>
+#include <spa/utils/dll.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/raw.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+#include <pipewire/private.h>
+
+#include <pulse/pulseaudio.h>
+#include "module-protocol-pulse/format.h"
+
+/** \page page_module_pulse_tunnel PipeWire Module: Pulse Tunnel
+ *
+ * The pulse-tunnel module provides a source or sink that tunnels all audio to
+ * a remote PulseAudio connection.
+ *
+ * It is usually used with the PulseAudio or module-protocol-pulse on the remote
+ * end to accept the connection.
+ *
+ * This module is usually used together with module-zeroconf-discover that will
+ * automatically load the tunnel with the right parameters based on zeroconf
+ * information.
+ *
+ * ## Module Options
+ *
+ * - `tunnel.mode`: the desired tunnel to create, must be `source` or `sink`.
+ * (Default `sink`)
+ * - `pulse.server.address`: the address of the PulseAudio server to tunnel to.
+ * - `pulse.latency`: the latency to end-to-end latency in milliseconds to
+ * maintain (Default 200).
+ * - `stream.props`: Extra properties for the local stream.
+ *
+ * ## General options
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ * - \ref PW_KEY_TARGET_OBJECT to specify the remote node.name or serial.id to link to
+ *
+ * ## Example configuration of a virtual sink
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-pulse-tunnel
+ * args = {
+ * tunnel.mode = sink
+ * # Set the remote address to tunnel to
+ * pulse.server.address = "tcp:192.168.1.126"
+ * #pulse.latency = 200
+ * #audio.rate=<sample rate>
+ * #audio.channels=<number of channels>
+ * #audio.position=<channel map>
+ * #target.object=<remote target name>
+ * stream.props = {
+ * # extra sink properties
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "pulse-tunnel"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define MODULE_USAGE "[ remote.name=<remote> ] " \
+ "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ node.target=<remote node target name or serial> ] " \
+ "[ audio.format=<sample format> ] " \
+ "[ audio.rate=<sample rate> ] " \
+ "[ audio.channels=<number of channels> ] " \
+ "[ audio.position=<channel map> ] " \
+ "pulse.server.address=<address> " \
+ "pulse.latency=<latency in msec> " \
+ "[ tunnel.mode=source|sink " \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Create a PulseAudio tunnel" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#define RINGBUFFER_SIZE (1u << 22)
+#define RINGBUFFER_MASK (RINGBUFFER_SIZE-1)
+
+#define DEFAULT_LATENCY_MSEC (200)
+
+struct impl {
+ struct pw_context *context;
+
+#define MODE_SINK 0
+#define MODE_SOURCE 1
+ uint32_t mode;
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+
+ struct spa_hook module_listener;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ uint32_t latency_msec;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ struct spa_ringbuffer ring;
+ void *buffer;
+ uint8_t empty[8192];
+
+ pa_threaded_mainloop *pa_mainloop;
+ pa_context *pa_context;
+ pa_stream *pa_stream;
+
+ struct ratelimit rate_limit;
+
+ 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;
+ unsigned resync:1;
+
+ unsigned int do_disconnect:1;
+};
+
+static void cork_stream(struct impl *impl, bool cork)
+{
+ pa_operation *operation;
+
+ pa_threaded_mainloop_lock(impl->pa_mainloop);
+
+ pw_log_debug("corking: %d", cork);
+ if (cork && impl->mode == MODE_SINK) {
+ /* When the sink becomes suspended (which is the only case where we
+ * cork the stream), we don't want to keep any old data around, because
+ * the old data is most likely unrelated to the audio that will be
+ * played at the time when the sink starts running again. */
+ if ((operation = pa_stream_flush(impl->pa_stream, NULL, NULL)))
+ pa_operation_unref(operation);
+
+ spa_ringbuffer_init(&impl->ring);
+ memset(impl->buffer, 0, RINGBUFFER_SIZE);
+ }
+ if (!cork)
+ impl->resync = true;
+
+ if ((operation = pa_stream_cork(impl->pa_stream, cork, NULL, NULL)))
+ pa_operation_unref(operation);
+
+ pa_threaded_mainloop_unlock(impl->pa_mainloop);
+}
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ cork_stream(impl, true);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ cork_stream(impl, false);
+ break;
+ default:
+ break;
+ }
+}
+
+static void update_rate(struct impl *impl, bool playback)
+{
+ float error, corr;
+
+ if (impl->rate_match == NULL)
+ return;
+
+ if (playback)
+ error = (float)impl->target_latency - (float)impl->current_latency;
+ else
+ error = (float)impl->current_latency - (float)impl->target_latency;
+ error = SPA_CLAMP(error, -impl->max_error, impl->max_error);
+
+ corr = spa_dll_update(&impl->dll, error);
+ pw_log_debug("error:%f corr:%f current:%u target:%u",
+ error, corr,
+ impl->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;
+}
+
+static void playback_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ int32_t filled;
+ uint32_t write_index, offs, size;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
+ size = SPA_MIN(size, RINGBUFFER_SIZE);
+
+ filled = spa_ringbuffer_get_write_index(&impl->ring, &write_index);
+
+ if (filled < 0) {
+ pw_log_warn("%p: underrun write:%u filled:%d",
+ impl, write_index, filled);
+ } else if ((uint32_t)filled + size > RINGBUFFER_SIZE) {
+ pw_log_warn("%p: overrun write:%u filled:%d + size:%u > max:%u",
+ impl, write_index, filled,
+ size, RINGBUFFER_SIZE);
+ impl->resync = true;
+ } else {
+ update_rate(impl, true);
+ }
+ spa_ringbuffer_write_data(&impl->ring,
+ impl->buffer, RINGBUFFER_SIZE,
+ write_index & RINGBUFFER_MASK,
+ SPA_PTROFF(bd->data, offs, void),
+ size);
+ write_index += size;
+ spa_ringbuffer_write_update(&impl->ring, write_index);
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static void capture_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ int32_t avail;
+ uint32_t size, req, index;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+
+ if ((req = buf->requested * impl->frame_size) == 0)
+ req = 4096 * impl->frame_size;
+
+ size = SPA_MIN(bd->maxsize, req);
+
+ avail = spa_ringbuffer_get_read_index(&impl->ring, &index);
+ if (avail < (int32_t)size) {
+ memset(bd->data, 0, size);
+ } else {
+ if (avail > (int32_t)RINGBUFFER_SIZE) {
+ avail = impl->target_buffer;
+ index += avail - impl->target_buffer;
+ } else {
+ update_rate(impl, false);
+ }
+ spa_ringbuffer_read_data(&impl->ring,
+ impl->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK,
+ bd->data, size);
+
+ index += size;
+ spa_ringbuffer_read_update(&impl->ring, index);
+ }
+ bd->chunk->offset = 0;
+ bd->chunk->size = size;
+ bd->chunk->stride = impl->frame_size;
+
+ 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,
+ .process = playback_stream_process
+};
+
+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,
+ .process = capture_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ struct spa_latency_info latency;
+
+ impl->stream = pw_stream_new(impl->core, "pulse", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ if (impl->mode == MODE_SOURCE) {
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &capture_stream_events, impl);
+ } else {
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &playback_stream_events, impl);
+ }
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ spa_zero(latency);
+ latency.direction = impl->mode == MODE_SOURCE ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
+ latency.min_ns = latency.max_ns = impl->latency_msec * SPA_NSEC_PER_MSEC;
+
+ params[n_params++] = spa_latency_build(&b,
+ SPA_PARAM_Latency, &latency);
+
+ if ((res = pw_stream_connect(impl->stream,
+ impl->mode == MODE_SOURCE ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static void context_state_cb(pa_context *c, void *userdata)
+{
+ struct impl *impl = userdata;
+ bool do_destroy = false;
+ switch (pa_context_get_state(c)) {
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_FAILED:
+ do_destroy = true;
+ SPA_FALLTHROUGH;
+ case PA_CONTEXT_READY:
+ pa_threaded_mainloop_signal(impl->pa_mainloop, 0);
+ break;
+ case PA_CONTEXT_UNCONNECTED:
+ do_destroy = true;
+ break;
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ break;
+ }
+ if (do_destroy)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static void stream_state_cb(pa_stream *s, void * userdata)
+{
+ struct impl *impl = userdata;
+ bool do_destroy = false;
+ switch (pa_stream_get_state(s)) {
+ case PA_STREAM_FAILED:
+ case PA_STREAM_TERMINATED:
+ do_destroy = true;
+ SPA_FALLTHROUGH;
+ case PA_STREAM_READY:
+ pa_threaded_mainloop_signal(impl->pa_mainloop, 0);
+ break;
+ case PA_STREAM_UNCONNECTED:
+ do_destroy = true;
+ break;
+ case PA_STREAM_CREATING:
+ break;
+ }
+ if (do_destroy)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static void stream_read_request_cb(pa_stream *s, size_t length, void *userdata)
+{
+ struct impl *impl = userdata;
+ int32_t filled;
+ uint32_t index;
+ pa_usec_t latency;
+ int negative;
+
+ filled = spa_ringbuffer_get_write_index(&impl->ring, &index);
+
+ if (filled < 0) {
+ pw_log_warn("%p: underrun write:%u filled:%d",
+ impl, index, filled);
+ } else if (filled + length > RINGBUFFER_SIZE) {
+ pw_log_warn("%p: overrun write:%u filled:%d",
+ impl, index, filled);
+ }
+ while (length > 0) {
+ const void *p;
+ size_t nbytes = 0;
+
+ if (SPA_UNLIKELY(pa_stream_peek(impl->pa_stream, &p, &nbytes) != 0)) {
+ pw_log_error("pa_stream_peek() failed: %s",
+ pa_strerror(pa_context_errno(impl->pa_context)));
+ return;
+ }
+ pw_log_debug("read %zd nbytes:%zd", length, nbytes);
+
+ if (length < nbytes)
+ break;
+
+ while (nbytes > 0) {
+ uint32_t to_write = SPA_MIN(nbytes, sizeof(impl->empty));
+
+ spa_ringbuffer_write_data(&impl->ring,
+ impl->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK,
+ p ? p : impl->empty, to_write);
+
+ index += to_write;
+ p = p ? SPA_PTROFF(p, to_write, void) : NULL;
+ nbytes -= to_write;
+ length -= to_write;
+ filled += to_write;
+ }
+ pa_stream_drop(impl->pa_stream);
+ }
+
+ pa_stream_get_latency(impl->pa_stream, &latency, &negative);
+ impl->current_latency = latency * impl->info.rate / SPA_USEC_PER_SEC;
+ impl->current_latency += filled / impl->frame_size;
+
+ spa_ringbuffer_write_update(&impl->ring, index);
+}
+
+static void stream_write_request_cb(pa_stream *s, size_t length, void *userdata)
+{
+ struct impl *impl = userdata;
+ int32_t avail;
+ uint32_t index;
+ size_t size;
+ pa_usec_t latency;
+ int negative, res;
+
+ if (impl->resync) {
+ impl->resync = false;
+ avail = length + impl->target_buffer;
+ spa_ringbuffer_get_write_index(&impl->ring, &index);
+ index -= avail;
+ } else {
+ avail = spa_ringbuffer_get_read_index(&impl->ring, &index);
+ }
+
+ pa_stream_get_latency(impl->pa_stream, &latency, &negative);
+ impl->current_latency = latency * impl->info.rate / SPA_USEC_PER_SEC;
+ impl->current_latency += avail / impl->frame_size;
+
+ while (avail < (int32_t)length) {
+ uint32_t maxsize = SPA_ROUND_DOWN(sizeof(impl->empty), impl->frame_size);
+ /* send silence for the data we don't have */
+ size = SPA_MIN(length - avail, maxsize);
+ if ((res = pa_stream_write(impl->pa_stream,
+ impl->empty, size,
+ NULL, 0, PA_SEEK_RELATIVE)) != 0)
+ pw_log_warn("error writing stream: %s", pa_strerror(res));
+ length -= size;
+ }
+ while (length > 0 && avail >= (int32_t)length) {
+ void *data;
+
+ size = length;
+ pa_stream_begin_write(impl->pa_stream, &data, &size);
+
+ spa_ringbuffer_read_data(&impl->ring,
+ impl->buffer, RINGBUFFER_SIZE,
+ index & RINGBUFFER_MASK,
+ data, size);
+
+ if ((res = pa_stream_write(impl->pa_stream,
+ data, size, NULL, 0, PA_SEEK_RELATIVE)) != 0)
+ pw_log_warn("error writing stream: %zd %s", size,
+ pa_strerror(res));
+
+ index += size;
+ length -= size;
+ avail -= size;
+ spa_ringbuffer_read_update(&impl->ring, index);
+ }
+}
+static void stream_underflow_cb(pa_stream *s, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ if (ratelimit_test(&impl->rate_limit, SPA_TIMESPEC_TO_NSEC(&ts), SPA_LOG_LEVEL_WARN))
+ pw_log_warn("underflow");
+ impl->resync = true;
+}
+static void stream_overflow_cb(pa_stream *s, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ if (ratelimit_test(&impl->rate_limit, SPA_TIMESPEC_TO_NSEC(&ts), SPA_LOG_LEVEL_WARN))
+ pw_log_warn("overflow");
+ impl->resync = true;
+}
+
+static void stream_latency_update_cb(pa_stream *s, void *userdata)
+{
+ struct impl *impl = userdata;
+ pa_usec_t usec;
+ int negative;
+
+ pa_stream_get_latency(s, &usec, &negative);
+
+ pw_log_debug("latency %ld negative %d", usec, negative);
+ pa_threaded_mainloop_signal(impl->pa_mainloop, 0);
+}
+
+static pa_proplist* tunnel_new_proplist(struct impl *impl)
+{
+ pa_proplist *proplist = pa_proplist_new();
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, "PipeWire");
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "org.pipewire.PipeWire");
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION);
+ return proplist;
+}
+
+static int create_pulse_stream(struct impl *impl)
+{
+ pa_sample_spec ss;
+ pa_channel_map map;
+ const char *server_address, *remote_node_target;
+ pa_proplist *props = NULL;
+ pa_mainloop_api *api;
+ char stream_name[1024];
+ pa_buffer_attr bufferattr;
+ int res = -EIO;
+ uint32_t latency_bytes, i, aux = 0;
+
+ if ((impl->pa_mainloop = pa_threaded_mainloop_new()) == NULL)
+ goto error;
+
+ api = pa_threaded_mainloop_get_api(impl->pa_mainloop);
+
+ props = tunnel_new_proplist(impl);
+ impl->pa_context = pa_context_new_with_proplist(api, "PipeWire", props);
+ pa_proplist_free(props);
+
+ if (impl->pa_context == NULL)
+ goto error;
+
+ pa_context_set_state_callback(impl->pa_context, context_state_cb, impl);
+
+ server_address = pw_properties_get(impl->props, "pulse.server.address");
+
+ if (pa_context_connect(impl->pa_context, server_address, 0, NULL) < 0) {
+ res = pa_context_errno(impl->pa_context);
+ goto error;
+ }
+
+ pa_threaded_mainloop_lock(impl->pa_mainloop);
+
+ if (pa_threaded_mainloop_start(impl->pa_mainloop) < 0)
+ goto error_unlock;
+
+ for (;;) {
+ pa_context_state_t state;
+
+ state = pa_context_get_state(impl->pa_context);
+ if (state == PA_CONTEXT_READY)
+ break;
+
+ if (!PA_CONTEXT_IS_GOOD(state)) {
+ res = pa_context_errno(impl->pa_context);
+ goto error_unlock;
+ }
+ /* Wait until the context is ready */
+ pa_threaded_mainloop_wait(impl->pa_mainloop);
+ }
+
+ ss.format = (pa_sample_format_t) format_id2pa(impl->info.format);
+ ss.channels = impl->info.channels;
+ ss.rate = impl->info.rate;
+
+ map.channels = impl->info.channels;
+ for (i = 0; i < map.channels; i++)
+ map.map[i] = (pa_channel_position_t)channel_id2pa(impl->info.position[i], &aux);
+
+ snprintf(stream_name, sizeof(stream_name), _("Tunnel for %s@%s"),
+ pw_get_user_name(), pw_get_host_name());
+
+ if (!(impl->pa_stream = pa_stream_new(impl->pa_context, stream_name, &ss, &map))) {
+ res = pa_context_errno(impl->pa_context);
+ goto error_unlock;
+ }
+
+ pa_stream_set_state_callback(impl->pa_stream, stream_state_cb, impl);
+ pa_stream_set_read_callback(impl->pa_stream, stream_read_request_cb, impl);
+ pa_stream_set_write_callback(impl->pa_stream, stream_write_request_cb, impl);
+ pa_stream_set_underflow_callback(impl->pa_stream, stream_underflow_cb, impl);
+ pa_stream_set_overflow_callback(impl->pa_stream, stream_overflow_cb, impl);
+ pa_stream_set_latency_update_callback(impl->pa_stream, stream_latency_update_cb, impl);
+
+ remote_node_target = pw_properties_get(impl->props, PW_KEY_TARGET_OBJECT);
+
+ bufferattr.fragsize = (uint32_t) -1;
+ bufferattr.minreq = (uint32_t) -1;
+ bufferattr.maxlength = (uint32_t) -1;
+ bufferattr.prebuf = (uint32_t) -1;
+
+ latency_bytes = pa_usec_to_bytes(impl->latency_msec * SPA_USEC_PER_MSEC, &ss);
+
+ impl->target_latency = latency_bytes / impl->frame_size;
+
+ /* half in our buffer, half in the network + remote */
+ impl->target_buffer = latency_bytes / 2;
+
+ if (impl->mode == MODE_SOURCE) {
+ bufferattr.fragsize = latency_bytes / 2;
+
+ res = pa_stream_connect_record(impl->pa_stream,
+ remote_node_target, &bufferattr,
+ PA_STREAM_DONT_MOVE |
+ PA_STREAM_INTERPOLATE_TIMING |
+ PA_STREAM_ADJUST_LATENCY |
+ PA_STREAM_AUTO_TIMING_UPDATE);
+ } else {
+ bufferattr.tlength = latency_bytes / 2;
+ bufferattr.minreq = bufferattr.tlength / 4;
+ bufferattr.prebuf = bufferattr.tlength;
+
+ res = pa_stream_connect_playback(impl->pa_stream,
+ remote_node_target, &bufferattr,
+ PA_STREAM_DONT_MOVE |
+ PA_STREAM_INTERPOLATE_TIMING |
+ PA_STREAM_ADJUST_LATENCY |
+ PA_STREAM_AUTO_TIMING_UPDATE,
+ NULL, NULL);
+ }
+
+ if (res < 0) {
+ res = pa_context_errno(impl->pa_context);
+ goto error_unlock;
+ }
+
+ for (;;) {
+ pa_stream_state_t state;
+
+ state = pa_stream_get_state(impl->pa_stream);
+ if (state == PA_STREAM_READY)
+ break;
+
+ if (!PA_STREAM_IS_GOOD(state)) {
+ res = pa_context_errno(impl->pa_context);
+ goto error_unlock;
+ }
+
+ /* Wait until the stream is ready */
+ pa_threaded_mainloop_wait(impl->pa_mainloop);
+ }
+
+ pa_threaded_mainloop_unlock(impl->pa_mainloop);
+
+ return 0;
+
+error_unlock:
+ pa_threaded_mainloop_unlock(impl->pa_mainloop);
+error:
+ pw_log_error("failed to connect: %s", pa_strerror(res));
+ return -res;
+}
+
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+
+ if (impl->pa_mainloop)
+ pa_threaded_mainloop_stop(impl->pa_mainloop);
+ if (impl->pa_stream)
+ pa_stream_unref(impl->pa_stream);
+ if (impl->pa_context) {
+ pa_context_disconnect(impl->pa_context);
+ pa_context_unref(impl->pa_context);
+ }
+ if (impl->pa_mainloop)
+ pa_threaded_mainloop_free(impl->pa_mainloop);
+
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl->buffer);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static 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));
+}
+
+static int calc_frame_size(struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ struct impl *impl;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+
+ spa_ringbuffer_init(&impl->ring);
+ impl->buffer = calloc(1, RINGBUFFER_SIZE);
+ spa_dll_init(&impl->dll);
+ impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+ impl->rate_limit.burst = 1;
+
+ if ((str = pw_properties_get(props, "tunnel.mode")) != NULL) {
+ if (spa_streq(str, "source")) {
+ impl->mode = MODE_SOURCE;
+ } else if (spa_streq(str, "sink")) {
+ impl->mode = MODE_SINK;
+ } else {
+ pw_log_error("invalid tunnel.mode '%s'", str);
+ res = -EINVAL;
+ goto error;
+ }
+ }
+
+ impl->latency_msec = pw_properties_get_uint32(props, "pulse.latency", DEFAULT_LATENCY_MSEC);
+
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_NETWORK, "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS,
+ impl->mode == MODE_SINK ?
+ "Audio/Sink" : "Audio/Source");
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_NODE_NETWORK);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ pw_log_error("unsupported audio format:%d channels:%d",
+ impl->info.format, impl->info.channels);
+ res = -EINVAL;
+ goto error;
+ }
+ spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MIN, 128, impl->info.rate);
+ impl->max_error = 256.0f;
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_pulse_stream(impl)) < 0)
+ goto error;
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c
new file mode 100644
index 0000000..0c62011
--- /dev/null
+++ b/src/modules/module-raop-discover.c
@@ -0,0 +1,547 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include <avahi-client/lookup.h>
+#include <avahi-common/error.h>
+#include <avahi-common/malloc.h>
+
+#include "module-protocol-pulse/format.h"
+#include "module-zeroconf-discover/avahi-poll.h"
+
+/** \page page_module_raop_discover PipeWire Module: RAOP Discover
+ *
+ * Automatically creates RAOP (Airplay) sink devices based on zeroconf
+ * information.
+ *
+ * This module will load module-raop-sink for each discovered sink
+ * with the right parameters.
+ *
+ * ## Module Options
+ *
+ * This module has no options.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-raop-discover
+ * args = { }
+ * }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * \ref page_module_raop_sink
+ */
+
+#define NAME "raop-discover"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE " "
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Discover remote streams" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#define SERVICE_TYPE_SINK "_raop._tcp"
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_properties *properties;
+
+ AvahiPoll *avahi_poll;
+ AvahiClient *client;
+ AvahiServiceBrowser *sink_browser;
+
+ struct spa_list tunnel_list;
+};
+
+struct tunnel_info {
+ AvahiIfIndex interface;
+ AvahiProtocol protocol;
+ const char *name;
+ const char *type;
+ const char *domain;
+};
+
+#define TUNNEL_INFO(...) ((struct tunnel_info){ __VA_ARGS__ })
+
+struct tunnel {
+ struct spa_list link;
+ struct tunnel_info info;
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+};
+
+static int start_client(struct impl *impl);
+
+static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *info)
+{
+ struct tunnel *t;
+
+ t = calloc(1, sizeof(*t));
+ if (t == NULL)
+ return NULL;
+
+ t->info.interface = info->interface;
+ t->info.protocol = info->protocol;
+ t->info.name = strdup(info->name);
+ t->info.type = strdup(info->type);
+ t->info.domain = strdup(info->domain);
+ spa_list_append(&impl->tunnel_list, &t->link);
+
+ return t;
+}
+
+static struct tunnel *find_tunnel(struct impl *impl, const struct tunnel_info *info)
+{
+ struct tunnel *t;
+ spa_list_for_each(t, &impl->tunnel_list, link) {
+ if (t->info.interface == info->interface &&
+ t->info.protocol == info->protocol &&
+ spa_streq(t->info.name, info->name) &&
+ spa_streq(t->info.type, info->type) &&
+ spa_streq(t->info.domain, info->domain))
+ return t;
+ }
+ return NULL;
+}
+
+static void free_tunnel(struct tunnel *t)
+{
+ pw_impl_module_destroy(t->module);
+}
+
+static void impl_free(struct impl *impl)
+{
+ struct tunnel *t;
+
+ spa_list_consume(t, &impl->tunnel_list, link)
+ free_tunnel(t);
+
+ if (impl->sink_browser)
+ avahi_service_browser_free(impl->sink_browser);
+ if (impl->client)
+ avahi_client_free(impl->client);
+ if (impl->avahi_poll)
+ pw_avahi_poll_free(impl->avahi_poll);
+ pw_properties_free(impl->properties);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static bool str_in_list(const char *haystack, const char *delimiters, const char *needle)
+{
+ const char *s, *state = NULL;
+ size_t len;
+ while ((s = pw_split_walk(haystack, delimiters, &len, &state))) {
+ if (spa_strneq(needle, s, len))
+ return true;
+ }
+ return false;
+}
+
+static void pw_properties_from_avahi_string(const char *key, const char *value,
+ struct pw_properties *props)
+{
+ if (spa_streq(key, "device")) {
+ pw_properties_set(props, "raop.device", value);
+ }
+ else if (spa_streq(key, "tp")) {
+ /* transport protocol, "UDP", "TCP", "UDP,TCP" */
+ if (str_in_list(value, ",", "UDP"))
+ value = "udp";
+ else if (str_in_list(value, ",", "TCP"))
+ value = "tcp";
+ pw_properties_set(props, "raop.transport", value);
+ } else if (spa_streq(key, "et")) {
+ /* Supported encryption types:
+ * 0 = none,
+ * 1 = RSA,
+ * 2 = FairPlay,
+ * 3 = MFiSAP,
+ * 4 = FairPlay SAPv2.5. */
+ if (str_in_list(value, ",", "1"))
+ value = "RSA";
+ else if (str_in_list(value, ",", "4"))
+ value = "auth_setup";
+ else
+ value = "none";
+ pw_properties_set(props, "raop.encryption.type", value);
+ } else if (spa_streq(key, "cn")) {
+ /* Suported audio codecs:
+ * 0 = PCM,
+ * 1 = ALAC,
+ * 2 = AAC,
+ * 3 = AAC ELD. */
+ if (str_in_list(value, ",", "0"))
+ value = "PCM";
+ else if (str_in_list(value, ",", "1"))
+ value = "ALAC";
+ else if (str_in_list(value, ",", "2"))
+ value = "AAC";
+ else if (str_in_list(value, ",", "2"))
+ value = "AAC-ELD";
+ else
+ value = "unknown";
+ pw_properties_set(props, "raop.audio.codec", value);
+ } else if (spa_streq(key, "ch")) {
+ /* Number of channels */
+ pw_properties_set(props, PW_KEY_AUDIO_CHANNELS, value);
+ } else if (spa_streq(key, "ss")) {
+ /* Sample size */
+ if (spa_streq(value, "16"))
+ value = "S16";
+ else if (spa_streq(value, "24"))
+ value = "S24";
+ else if (spa_streq(value, "32"))
+ value = "S32";
+ else
+ value = "UNKNOWN";
+ pw_properties_set(props, PW_KEY_AUDIO_FORMAT, value);
+ } else if (spa_streq(key, "sr")) {
+ /* Sample rate */
+ pw_properties_set(props, PW_KEY_AUDIO_RATE, value);
+ } else if (spa_streq(key, "am")) {
+ /* Device model */
+ pw_properties_set(props, "device.model", value);
+ }
+}
+
+static void submodule_destroy(void *data)
+{
+ struct tunnel *t = data;
+
+ spa_list_remove(&t->link);
+ spa_hook_remove(&t->module_listener);
+
+ free((char *) t->info.name);
+ free((char *) t->info.type);
+ free((char *) t->info.domain);
+
+ free(t);
+}
+
+static const struct pw_impl_module_events submodule_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = submodule_destroy,
+};
+
+static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiResolverEvent event, const char *name, const char *type, const char *domain,
+ const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt,
+ AvahiLookupResultFlags flags, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct tunnel *t;
+ struct tunnel_info tinfo;
+ const char *str;
+ AvahiStringList *l;
+ FILE *f;
+ char *args;
+ size_t size;
+ struct pw_impl_module *mod;
+ struct pw_properties *props = NULL;
+ char at[AVAHI_ADDRESS_STR_MAX];
+
+ if (event != AVAHI_RESOLVER_FOUND) {
+ pw_log_error("Resolving of '%s' failed: %s", name,
+ avahi_strerror(avahi_client_errno(impl->client)));
+ goto done;
+ }
+ tinfo = TUNNEL_INFO(.interface = interface,
+ .protocol = protocol,
+ .name = name,
+ .type = type,
+ .domain = domain);
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ pw_log_error("Can't allocate properties: %m");
+ goto done;
+ }
+
+ avahi_address_snprint(at, sizeof(at), a);
+
+ pw_properties_setf(props, "raop.hostname", "%s", at);
+ pw_properties_setf(props, "raop.port", "%u", port);
+
+ if ((str = strstr(name, "@"))) {
+ str++;
+ if (strlen(str) > 0)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, str);
+ else
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ "RAOP on %s", host_name);
+ }
+
+ for (l = txt; l; l = l->next) {
+ char *key, *value;
+
+ if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0)
+ break;
+
+ pw_properties_from_avahi_string(key, value, props);
+ avahi_free(key);
+ avahi_free(value);
+ }
+
+
+ if ((f = open_memstream(&args, &size)) == NULL) {
+ pw_log_error("Can't open memstream: %m");
+ goto done;
+ }
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &props->dict, 0);
+ fprintf(f, " stream.props = {");
+ fprintf(f, " }");
+ fprintf(f, "}");
+ fclose(f);
+
+ pw_properties_free(props);
+
+ pw_log_info("loading module args:'%s'", args);
+ mod = pw_context_load_module(impl->context,
+ "libpipewire-module-raop-sink",
+ args, NULL);
+ free(args);
+
+ if (mod == NULL) {
+ pw_log_error("Can't load module: %m");
+ goto done;
+ }
+
+ t = make_tunnel(impl, &tinfo);
+ if (t == NULL) {
+ pw_log_error("Can't make tunnel: %m");
+ pw_impl_module_destroy(mod);
+ goto done;
+ }
+
+ pw_impl_module_add_listener(mod, &t->module_listener, &submodule_events, t);
+
+ t->module = mod;
+
+done:
+ avahi_service_resolver_free(r);
+}
+
+
+static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiBrowserEvent event, const char *name, const char *type, const char *domain,
+ AvahiLookupResultFlags flags, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct tunnel_info info;
+ struct tunnel *t;
+
+ if (flags & AVAHI_LOOKUP_RESULT_LOCAL)
+ return;
+
+ info = TUNNEL_INFO(.interface = interface,
+ .protocol = protocol,
+ .name = name,
+ .type = type,
+ .domain = domain);
+
+ t = find_tunnel(impl, &info);
+
+ switch (event) {
+ case AVAHI_BROWSER_NEW:
+ if (t != NULL)
+ return;
+ if (!(avahi_service_resolver_new(impl->client,
+ interface, protocol,
+ name, type, domain,
+ AVAHI_PROTO_UNSPEC, 0,
+ resolver_cb, impl)))
+ pw_log_error("can't make service resolver: %s",
+ avahi_strerror(avahi_client_errno(impl->client)));
+ break;
+ case AVAHI_BROWSER_REMOVE:
+ if (t == NULL)
+ return;
+ free_tunnel(t);
+ break;
+ default:
+ break;
+ }
+}
+
+
+static struct AvahiServiceBrowser *make_browser(struct impl *impl, const char *service_type)
+{
+ struct AvahiServiceBrowser *s;
+
+ s = avahi_service_browser_new(impl->client,
+ AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+ service_type, NULL, 0,
+ browser_cb, impl);
+ if (s == NULL) {
+ pw_log_error("can't make browser for %s: %s", service_type,
+ avahi_strerror(avahi_client_errno(impl->client)));
+ }
+ return s;
+}
+
+static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata)
+{
+ struct impl *impl = userdata;
+
+ impl->client = c;
+
+ switch (state) {
+ case AVAHI_CLIENT_S_REGISTERING:
+ case AVAHI_CLIENT_S_RUNNING:
+ case AVAHI_CLIENT_S_COLLISION:
+ if (impl->sink_browser == NULL)
+ impl->sink_browser = make_browser(impl, SERVICE_TYPE_SINK);
+ if (impl->sink_browser == NULL)
+ goto error;
+ break;
+ case AVAHI_CLIENT_FAILURE:
+ if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED)
+ start_client(impl);
+
+ SPA_FALLTHROUGH;
+ case AVAHI_CLIENT_CONNECTING:
+ if (impl->sink_browser) {
+ avahi_service_browser_free(impl->sink_browser);
+ impl->sink_browser = NULL;
+ }
+ break;
+ default:
+ break;
+ }
+ return;
+error:
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static int start_client(struct impl *impl)
+{
+ int res;
+ if ((impl->client = avahi_client_new(impl->avahi_poll,
+ AVAHI_CLIENT_NO_FAIL,
+ client_callback, impl,
+ &res)) == NULL) {
+ pw_log_error("can't create client: %s", avahi_strerror(res));
+ pw_impl_module_schedule_destroy(impl->module);
+ return -EIO;
+ }
+ return 0;
+}
+
+static int start_avahi(struct impl *impl)
+{
+ struct pw_loop *loop;
+
+ loop = pw_context_get_main_loop(impl->context);
+ impl->avahi_poll = pw_avahi_poll_new(loop);
+
+ return start_client(impl);
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL)
+ goto error_errno;
+
+ spa_list_init(&impl->tunnel_list);
+
+ impl->module = module;
+ impl->context = context;
+ impl->properties = props;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ start_avahi(impl);
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ if (impl)
+ impl_free(impl);
+ return res;
+}
diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c
new file mode 100644
index 0000000..303cb42
--- /dev/null
+++ b/src/modules/module-raop-sink.c
@@ -0,0 +1,1848 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <limits.h>
+#include <math.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <openssl/err.h>
+#include <openssl/rand.h>
+#include <openssl/rsa.h>
+#include <openssl/engine.h>
+#include <openssl/aes.h>
+#include <openssl/md5.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/debug/types.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/latency-utils.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include "module-raop/rtsp-client.h"
+
+/** \page page_module_raop_sink PipeWire Module: AirPlay Sink
+ *
+ * Creates a new Sink to stream to an Airplay device.
+ *
+ * Normally this sink is automatically created with \ref page_module_raop_discover
+ * with the right parameters but it is possible to manually create a RAOP sink
+ * as well.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `raop.hostname`: The hostname of the remote end.
+ * - `raop.port`: The port of the remote end.
+ * - `raop.transport`: The data transport to use, one of "udp" or "tcp". Defaults
+ * to "udp".
+ * - `raop.encryption.type`: The encryption type to use. One of "none", "RSA" or
+ * "auth_setup". Default is "none".
+ * - `raop.audio.codec`: The audio codec to use. Needs to be "PCM". Defaults to "PCM".
+ * - `raop.password`: The password to use.
+ * - `stream.props = {}`: properties to be passed to the sink stream
+ *
+ * Options with well-known behavior.
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-raop-sink
+ * args = {
+ * # Set the remote address to tunnel to
+ * raop.hostname = "my-raop-device"
+ * raop.port = 8190
+ * #raop.transport = "udp"
+ * raop.encryption.type = "RSA"
+ * #raop.audio.codec = "PCM"
+ * #raop.password = "****"
+ * #audio.format = "S16"
+ * #audio.rate = 44100
+ * #audio.channels = 2
+ * #audio.position = [ FL FR ]
+ * stream.props = {
+ * # extra sink properties
+ * }
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * ## See also
+ *
+ * \ref page_module_raop_discover
+ */
+
+#define NAME "raop-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FRAMES_PER_TCP_PACKET 4096
+#define FRAMES_PER_UDP_PACKET 352
+
+#define DEFAULT_TCP_AUDIO_PORT 6000
+#define DEFAULT_UDP_AUDIO_PORT 6000
+#define DEFAULT_UDP_CONTROL_PORT 6001
+#define DEFAULT_UDP_TIMING_PORT 6002
+
+#define AES_CHUNK_SIZE 16
+#ifndef MD5_DIGEST_LENGTH
+#define MD5_DIGEST_LENGTH 16
+#endif
+#define MD5_HASH_LENGTH (2*MD5_DIGEST_LENGTH)
+
+#define DEFAULT_USER_AGENT "iTunes/11.0.4 (Windows; N)"
+#define DEFAULT_USER_NAME "iTunes"
+
+#define MAX_PORT_RETRY 128
+
+#define DEFAULT_FORMAT "S16"
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define DEFAULT_LATENCY 22050
+
+#define MODULE_USAGE "[ raop.hostname=<name of host> ] " \
+ "[ raop.port=<remote port> ] " \
+ "[ raop.transport=<transport, default:udp> ] " \
+ "[ raop.encryption.type=<encryption, default:none> ] " \
+ "[ raop.audio.codec=PCM ] " \
+ "[ raop.password=<password for auth> ] " \
+ "[ node.latency=<latency as fraction> ] " \
+ "[ node.name=<name of the nodes> ] " \
+ "[ node.description=<description of the nodes> ] " \
+ "[ audio.format=<format, default:"DEFAULT_FORMAT"> ] " \
+ "[ audio.rate=<sample rate, default: "SPA_STRINGIFY(DEFAULT_RATE)"> ] " \
+ "[ audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS)"> ] " \
+ "[ audio.position=<channel map, default:"DEFAULT_POSITION"> ] " \
+ "[ stream.props=<properties> ] "
+
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "An RAOP audio sink" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+enum {
+ PROTO_TCP,
+ PROTO_UDP,
+};
+enum {
+ CRYPTO_NONE,
+ CRYPTO_RSA,
+ CRYPTO_AUTH_SETUP,
+};
+enum {
+ CODEC_PCM,
+ CODEC_ALAC,
+ CODEC_AAC,
+ CODEC_AAC_ELD,
+};
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_properties *props;
+
+ struct pw_impl_module *module;
+ struct pw_loop *loop;
+
+ struct spa_hook module_listener;
+
+ int protocol;
+ int encryption;
+ int codec;
+
+ struct pw_core *core;
+ struct spa_hook core_proxy_listener;
+ struct spa_hook core_listener;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+ struct spa_audio_info_raw info;
+ uint32_t frame_size;
+
+ struct pw_rtsp_client *rtsp;
+ struct spa_hook rtsp_listener;
+ struct pw_properties *headers;
+
+ char session_id[32];
+ char *password;
+
+ unsigned int do_disconnect:1;
+
+ uint8_t key[AES_CHUNK_SIZE]; /* Key for aes-cbc */
+ uint8_t iv[AES_CHUNK_SIZE]; /* Initialization vector for cbc */
+ AES_KEY aes; /* AES encryption */
+
+ uint16_t control_port;
+ int control_fd;
+ struct spa_source *control_source;
+
+ uint16_t timing_port;
+ int timing_fd;
+ struct spa_source *timing_source;
+
+ uint16_t server_port;
+ int server_fd;
+ struct spa_source *server_source;
+
+ uint32_t block_size;
+ uint32_t delay;
+ uint32_t latency;
+
+ uint16_t seq;
+ uint32_t rtptime;
+ uint32_t ssrc;
+ uint32_t sync;
+ uint32_t sync_period;
+ unsigned int first:1;
+ unsigned int connected:1;
+ unsigned int ready:1;
+ unsigned int recording:1;
+
+ uint8_t buffer[FRAMES_PER_TCP_PACKET * 4];
+ uint32_t filled;
+};
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static inline void bit_writer(uint8_t **p, int *pos, uint8_t data, int len)
+{
+ int rb = 8 - *pos - len;
+ if (rb >= 0) {
+ **p = (*pos ? **p : 0) | (data << rb);
+ *pos += len;
+ } else {
+ *(*p)++ |= (data >> -rb);
+ **p = data << (8+rb);
+ *pos = -rb;
+ }
+}
+
+static int aes_encrypt(struct impl *impl, uint8_t *data, int len)
+{
+ uint8_t nv[AES_CHUNK_SIZE];
+ uint8_t *buffer;
+ int i, j;
+
+ memcpy(nv, impl->iv, AES_CHUNK_SIZE);
+ for (i = 0; i + AES_CHUNK_SIZE <= len; i += AES_CHUNK_SIZE) {
+ buffer = data + i;
+ for (j = 0; j < AES_CHUNK_SIZE; j++)
+ buffer[j] ^= nv[j];
+
+ AES_encrypt(buffer, buffer, &impl->aes);
+
+ memcpy(nv, buffer, AES_CHUNK_SIZE);
+ }
+ return i;
+}
+
+static inline uint64_t timespec_to_ntp(struct timespec *ts)
+{
+ uint64_t ntp = (uint64_t) ts->tv_nsec * UINT32_MAX / SPA_NSEC_PER_SEC;
+ return ntp | (uint64_t) (ts->tv_sec + 0x83aa7e80) << 32;
+}
+
+static inline uint64_t ntp_now(int clockid)
+{
+ struct timespec now;
+ clock_gettime(clockid, &now);
+ return timespec_to_ntp(&now);
+}
+
+static int send_udp_sync_packet(struct impl *impl,
+ struct sockaddr *dest_addr, socklen_t addrlen)
+{
+ uint32_t pkt[5];
+ uint32_t rtptime = impl->rtptime;
+ uint32_t delay = impl->delay;
+ uint64_t transmitted;
+
+ pkt[0] = htonl(0x80d40007);
+ if (impl->first)
+ pkt[0] |= htonl(0x10000000);
+ rtptime -= delay;
+ pkt[1] = htonl(rtptime);
+ transmitted = ntp_now(CLOCK_MONOTONIC);
+ pkt[2] = htonl(transmitted >> 32);
+ pkt[3] = htonl(transmitted & 0xffffffff);
+ rtptime += delay;
+ pkt[4] = htonl(rtptime);
+
+ pw_log_debug("sync: delayed:%u now:%"PRIu64" rtptime:%u",
+ rtptime - delay, transmitted, rtptime);
+
+ return sendto(impl->control_fd, pkt, sizeof(pkt), 0, dest_addr, addrlen);
+}
+
+static int send_udp_timing_packet(struct impl *impl, uint64_t remote, uint64_t received,
+ struct sockaddr *dest_addr, socklen_t addrlen)
+{
+ uint32_t pkt[8];
+ uint64_t transmitted;
+
+ pkt[0] = htonl(0x80d30007);
+ pkt[1] = 0x00000000;
+ pkt[2] = htonl(remote >> 32);
+ pkt[3] = htonl(remote & 0xffffffff);
+ pkt[4] = htonl(received >> 32);
+ pkt[5] = htonl(received & 0xffffffff);
+ transmitted = ntp_now(CLOCK_MONOTONIC);
+ pkt[6] = htonl(transmitted >> 32);
+ pkt[7] = htonl(transmitted & 0xffffffff);
+
+ pw_log_debug("sync: remote:%"PRIu64" received:%"PRIu64" transmitted:%"PRIu64,
+ remote, received, transmitted);
+
+ return sendto(impl->timing_fd, pkt, sizeof(pkt), 0, dest_addr, addrlen);
+}
+
+static int write_codec_pcm(void *dst, void *frames, uint32_t n_frames)
+{
+ uint8_t *bp, *b, *d = frames;
+ int bpos = 0;
+ uint32_t i;
+
+ b = bp = dst;
+
+ bit_writer(&bp, &bpos, 1, 3); /* channel=1, stereo */
+ bit_writer(&bp, &bpos, 0, 4); /* Unknown */
+ bit_writer(&bp, &bpos, 0, 8); /* Unknown */
+ bit_writer(&bp, &bpos, 0, 4); /* Unknown */
+ bit_writer(&bp, &bpos, 1, 1); /* Hassize */
+ bit_writer(&bp, &bpos, 0, 2); /* Unused */
+ bit_writer(&bp, &bpos, 1, 1); /* Is-not-compressed */
+ bit_writer(&bp, &bpos, (n_frames >> 24) & 0xff, 8);
+ bit_writer(&bp, &bpos, (n_frames >> 16) & 0xff, 8);
+ bit_writer(&bp, &bpos, (n_frames >> 8) & 0xff, 8);
+ bit_writer(&bp, &bpos, (n_frames) & 0xff, 8);
+
+ for (i = 0; i < n_frames; i++) {
+ bit_writer(&bp, &bpos, *(d + 1), 8);
+ bit_writer(&bp, &bpos, *(d + 0), 8);
+ bit_writer(&bp, &bpos, *(d + 3), 8);
+ bit_writer(&bp, &bpos, *(d + 2), 8);
+ d += 4;
+ }
+ return bp - b + 1;
+}
+
+static int flush_to_udp_packet(struct impl *impl)
+{
+ const size_t max = 12 + 8 + impl->block_size;
+ uint32_t pkt[max], len, n_frames;
+ uint8_t *dst;
+ int res;
+
+ if (!impl->recording)
+ return 0;
+
+ impl->sync++;
+ if (impl->first || impl->sync == impl->sync_period) {
+ impl->sync = 0;
+ send_udp_sync_packet(impl, NULL, 0);
+ }
+ pkt[0] = htonl(0x80600000);
+ if (impl->first)
+ pkt[0] |= htonl((uint32_t)0x80 << 16);
+ pkt[0] |= htonl((uint32_t)impl->seq);
+ pkt[1] = htonl(impl->rtptime);
+ pkt[2] = htonl(impl->ssrc);
+
+ n_frames = impl->filled / impl->frame_size;
+ dst = (uint8_t*)&pkt[3];
+
+ switch (impl->codec) {
+ case CODEC_PCM:
+ case CODEC_ALAC:
+ len = write_codec_pcm(dst, impl->buffer, n_frames);
+ break;
+ default:
+ len = 8 + impl->block_size;
+ memset(dst, 0, len);
+ break;
+ }
+ if (impl->encryption == CRYPTO_RSA)
+ aes_encrypt(impl, dst, len);
+
+ impl->rtptime += n_frames;
+ impl->seq = (impl->seq + 1) & 0xffff;
+
+ pw_log_debug("send %u", len + 12);
+ res = send(impl->server_fd, pkt, len + 12, 0);
+
+ impl->first = false;
+
+ return res;
+}
+
+static int flush_to_tcp_packet(struct impl *impl)
+{
+ const size_t max = 16 + 8 + impl->block_size;
+ uint32_t pkt[max], len, n_frames;
+ uint8_t *dst;
+ int res;
+
+ if (!impl->recording)
+ return 0;
+
+ pkt[0] = htonl(0x24000000);
+ pkt[1] = htonl(0x80e00000);
+ pkt[1] |= htonl((uint32_t)impl->seq);
+ pkt[2] = htonl(impl->rtptime);
+ pkt[3] = htonl(impl->ssrc);
+
+ n_frames = impl->filled / impl->frame_size;
+ dst = (uint8_t*)&pkt[4];
+
+ switch (impl->codec) {
+ case CODEC_PCM:
+ case CODEC_ALAC:
+ len = write_codec_pcm(dst, impl->buffer, n_frames);
+ break;
+ default:
+ len = 8 + impl->block_size;
+ memset(dst, 0, len);
+ break;
+ }
+ if (impl->encryption == CRYPTO_RSA)
+ aes_encrypt(impl, dst, len);
+
+ pkt[0] |= htonl((uint32_t) len + 12);
+
+ impl->rtptime += n_frames;
+ impl->seq = (impl->seq + 1) & 0xffff;
+
+ pw_log_debug("send %u", len + 16);
+ res = send(impl->server_fd, pkt, len + 16, 0);
+
+ impl->first = false;
+
+ return res;
+}
+
+static void playback_stream_process(void *d)
+{
+ struct impl *impl = d;
+ struct pw_buffer *buf;
+ struct spa_data *bd;
+ uint8_t *data;
+ uint32_t offs, size;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("out of buffers: %m");
+ return;
+ }
+
+ bd = &buf->buffer->datas[0];
+
+ offs = SPA_MIN(bd->chunk->offset, bd->maxsize);
+ size = SPA_MIN(bd->chunk->size, bd->maxsize - offs);
+ data = SPA_PTROFF(bd->data, offs, uint8_t);
+
+ while (size > 0 && impl->block_size > 0) {
+ uint32_t avail, to_fill;
+
+ avail = impl->block_size - impl->filled;
+ to_fill = SPA_MIN(avail, size);
+
+ memcpy(&impl->buffer[impl->filled], data, to_fill);
+
+ impl->filled += to_fill;
+ avail -= to_fill;
+ size -= to_fill;
+ data += to_fill;
+
+ if (avail == 0) {
+ switch (impl->protocol) {
+ case PROTO_UDP:
+ flush_to_udp_packet(impl);
+ break;
+ case PROTO_TCP:
+ flush_to_tcp_packet(impl);
+ break;
+ }
+ impl->filled = 0;
+ }
+ }
+
+ pw_stream_queue_buffer(impl->stream, buf);
+}
+
+static int create_udp_socket(struct impl *impl, uint16_t *port)
+{
+ int res, ip_version, fd, val, i, af;
+ struct sockaddr_in sa4;
+ struct sockaddr_in6 sa6;
+
+ if ((res = pw_rtsp_client_get_local_ip(impl->rtsp,
+ &ip_version, NULL, 0)) < 0)
+ return res;
+
+ if (ip_version == 4) {
+ sa4.sin_family = af = AF_INET;
+ sa4.sin_addr.s_addr = INADDR_ANY;
+ } else {
+ sa6.sin6_family = af = AF_INET6;
+ sa6.sin6_addr = in6addr_any;
+ }
+
+ if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ pw_log_error("socket failed: %m");
+ return -errno;
+ }
+
+#ifdef SO_TIMESTAMP
+ val = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &val, sizeof(val)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt failed: %m");
+ goto error;
+ }
+#endif
+ val = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt failed: %m");
+ goto error;
+ }
+
+ for (i = 0; i < MAX_PORT_RETRY; i++) {
+ int ret;
+
+ if (ip_version == 4) {
+ sa4.sin_port = htons(*port);
+ ret = bind(fd, (struct sockaddr*)&sa4, sizeof(sa4));
+ } else {
+ sa6.sin6_port = htons(*port);
+ ret = bind(fd, (struct sockaddr*)&sa6, sizeof(sa6));
+ }
+ if (ret == 0)
+ break;
+ if (ret < 0 && errno != EADDRINUSE) {
+ res = -errno;
+ pw_log_error("bind failed: %m");
+ goto error;
+ }
+ (*port)++;
+ }
+ return fd;
+error:
+ close(fd);
+ return res;
+}
+
+static int connect_socket(struct impl *impl, int type, int fd, uint16_t port)
+{
+ const char *host;
+ struct sockaddr_in sa4;
+ struct sockaddr_in6 sa6;
+ struct sockaddr *sa;
+ size_t salen;
+ int res, af;
+
+ host = pw_properties_get(impl->props, "raop.hostname");
+ if (host == NULL)
+ return -EINVAL;
+
+ if (inet_pton(AF_INET, host, &sa4.sin_addr) > 0) {
+ sa4.sin_family = af = AF_INET;
+ sa4.sin_port = htons(port);
+ sa = (struct sockaddr *) &sa4;
+ salen = sizeof(sa4);
+ } else if (inet_pton(AF_INET6, host, &sa6.sin6_addr) > 0) {
+ sa6.sin6_family = af = AF_INET6;
+ sa6.sin6_port = htons(port);
+ sa = (struct sockaddr *) &sa6;
+ salen = sizeof(sa6);
+ } else {
+ pw_log_error("Invalid host '%s'", host);
+ return -EINVAL;
+ }
+
+ if (fd < 0 &&
+ (fd = socket(af, type | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ pw_log_error("socket failed: %m");
+ return -errno;
+ }
+
+ res = connect(fd, sa, salen);
+ if (res < 0 && errno != EINPROGRESS) {
+ res = -errno;
+ pw_log_error("connect failed: %m");
+ goto error;
+ }
+ pw_log_info("Connected to host:%s port:%d", host, port);
+ return fd;
+
+error:
+ if (fd >= 0)
+ close(fd);
+ return res;
+}
+
+static void
+on_timing_source_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+ uint32_t packet[8];
+ ssize_t bytes;
+
+ if (mask & SPA_IO_IN) {
+ uint64_t remote, received;
+ struct sockaddr_storage sender;
+ socklen_t sender_size = sizeof(sender);
+
+ received = ntp_now(CLOCK_MONOTONIC);
+ bytes = recvfrom(impl->timing_fd, packet, sizeof(packet), 0,
+ (struct sockaddr*)&sender, &sender_size);
+ if (bytes < 0) {
+ pw_log_debug("error reading timing packet: %m");
+ return;
+ }
+ if (bytes != sizeof(packet)) {
+ pw_log_warn("discarding short (%zd < %zd) timing packet",
+ bytes, sizeof(bytes));
+ return;
+ }
+ if (packet[0] != ntohl(0x80d20007))
+ return;
+
+ remote = ((uint64_t)ntohl(packet[6])) << 32 | ntohl(packet[7]);
+ if (send_udp_timing_packet(impl, remote, received,
+ (struct sockaddr *)&sender, sender_size) < 0) {
+ pw_log_warn("error sending timing packet");
+ return;
+ }
+ }
+}
+
+
+static void
+on_control_source_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+ uint32_t packet[2];
+ ssize_t bytes;
+
+ if (mask & SPA_IO_IN) {
+ uint32_t hdr;
+ uint16_t seq, num;
+
+ bytes = read(impl->control_fd, packet, sizeof(packet));
+ if (bytes < 0) {
+ pw_log_debug("error reading control packet: %m");
+ return;
+ }
+ if (bytes != sizeof(packet)) {
+ pw_log_warn("discarding short (%zd < %zd) control packet",
+ bytes, sizeof(bytes));
+ return;
+ }
+ hdr = ntohl(packet[0]);
+ if ((hdr & 0xff000000) != 0x80000000)
+ return;
+
+ seq = ntohl(packet[1]) >> 16;
+ num = ntohl(packet[1]) & 0xffff;
+ if (num == 0)
+ return;
+
+ switch (hdr >> 16 & 0xff) {
+ case 0xd5:
+ pw_log_debug("retransmit request seq:%u num:%u", seq, num);
+ /* retransmit request */
+ break;
+ }
+ }
+}
+
+static int rtsp_flush_reply(void *data, int status, const struct spa_dict *headers)
+{
+ pw_log_info("reply %d", status);
+ return 0;
+}
+
+static int rtsp_do_flush(struct impl *impl)
+{
+ int res;
+
+ if (!impl->recording)
+ return 0;
+
+ pw_properties_set(impl->headers, "Range", "npt=0-");
+ pw_properties_setf(impl->headers, "RTP-Info",
+ "seq=%u;rtptime=%u", impl->seq, impl->rtptime);
+
+ impl->recording = false;
+
+ res = pw_rtsp_client_send(impl->rtsp, "FLUSH", &impl->headers->dict,
+ NULL, NULL, rtsp_flush_reply, impl);
+
+ pw_properties_set(impl->headers, "Range", NULL);
+ pw_properties_set(impl->headers, "RTP-Info", NULL);
+
+ return res;
+}
+
+static int rtsp_record_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ const char *str;
+ uint32_t n_params;
+ const struct spa_pod *params[2];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+ struct spa_latency_info latency;
+ char progress[128];
+
+ pw_log_info("reply %d", status);
+
+ if ((str = spa_dict_lookup(headers, "Audio-Latency")) != NULL) {
+ if (!spa_atou32(str, &impl->latency, 0))
+ impl->latency = DEFAULT_LATENCY;
+ } else {
+ impl->latency = DEFAULT_LATENCY;
+ }
+
+ spa_zero(latency);
+ latency.direction = PW_DIRECTION_INPUT;
+ latency.min_rate = latency.max_rate = impl->latency;
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+
+ pw_stream_update_params(impl->stream, params, n_params);
+
+ impl->first = true;
+ impl->sync = 0;
+ impl->sync_period = impl->info.rate / (impl->block_size / impl->frame_size);
+ impl->recording = true;
+
+ snprintf(progress, sizeof(progress), "progress: %s/%s/%s\r\n", "0", "0", "0");
+ return pw_rtsp_client_send(impl->rtsp, "SET_PARAMETER", NULL,
+ "text/parameters", progress, NULL, NULL);
+}
+
+static int rtsp_do_record(struct impl *impl)
+{
+ int res;
+
+ if (!impl->ready || impl->recording)
+ return 0;
+
+ pw_properties_set(impl->headers, "Range", "npt=0-");
+ pw_properties_setf(impl->headers, "RTP-Info",
+ "seq=%u;rtptime=%u", impl->seq, impl->rtptime);
+
+ res = pw_rtsp_client_send(impl->rtsp, "RECORD", &impl->headers->dict,
+ NULL, NULL, rtsp_record_reply, impl);
+
+ pw_properties_set(impl->headers, "Range", NULL);
+ pw_properties_set(impl->headers, "RTP-Info", NULL);
+
+ return res;
+}
+
+static void
+on_server_source_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+
+ if (mask & (SPA_IO_ERR | SPA_IO_HUP))
+ goto error;
+ if (mask & SPA_IO_OUT) {
+ int res;
+ socklen_t len;
+
+ pw_loop_update_io(impl->loop, impl->server_source,
+ impl->server_source->mask & ~SPA_IO_OUT);
+
+ len = sizeof(res);
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) {
+ pw_log_error("getsockopt: %m");
+ goto error;
+ }
+ if (res != 0)
+ goto error;
+
+ impl->ready = true;
+ if (pw_stream_get_state(impl->stream, NULL) == PW_STREAM_STATE_STREAMING)
+ rtsp_do_record(impl);
+ }
+ return;
+error:
+ pw_loop_update_io(impl->loop, impl->server_source, 0);
+}
+
+static int rtsp_setup_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ const char *str, *state = NULL, *s;
+ size_t len;
+ uint64_t ntp;
+ uint16_t control_port, timing_port;
+
+ pw_log_info("reply %d", status);
+
+ if ((str = spa_dict_lookup(headers, "Session")) == NULL) {
+ pw_log_error("missing Session header");
+ return 0;
+ }
+ pw_properties_set(impl->headers, "Session", str);
+
+ if ((str = spa_dict_lookup(headers, "Transport")) == NULL) {
+ pw_log_error("missing Transport header");
+ return 0;
+ }
+
+ impl->server_port = control_port = timing_port = 0;
+ while ((s = pw_split_walk(str, ";", &len, &state)) != NULL) {
+ if (spa_strstartswith(s, "server_port=")) {
+ impl->server_port = atoi(s + 12);
+ }
+ else if (spa_strstartswith(s, "control_port=")) {
+ control_port = atoi(s + 13);
+ }
+ else if (spa_strstartswith(s, "timing_port=")) {
+ timing_port = atoi(s + 12);
+ }
+ }
+ if (impl->server_port == 0) {
+ pw_log_error("missing server port in Transport");
+ return 0;
+ }
+
+ if (pw_getrandom(&impl->seq, sizeof(impl->seq), 0) < 0 ||
+ pw_getrandom(&impl->rtptime, sizeof(impl->rtptime), 0) < 0) {
+ pw_log_error("error generating random seq and rtptime: %m");
+ return 0;
+ }
+
+ pw_log_info("server port:%u", impl->server_port);
+
+ switch (impl->protocol) {
+ case PROTO_TCP:
+ if ((impl->server_fd = connect_socket(impl, SOCK_STREAM, -1, impl->server_port)) < 0)
+ return impl->server_fd;
+
+ impl->server_source = pw_loop_add_io(impl->loop, impl->server_fd,
+ SPA_IO_OUT, false, on_server_source_io, impl);
+ break;
+
+ case PROTO_UDP:
+ if (control_port == 0 || timing_port == 0) {
+ pw_log_error("missing UDP ports in Transport");
+ return 0;
+ }
+ pw_log_info("control:%u timing:%u", control_port, timing_port);
+
+ if ((impl->server_fd = connect_socket(impl, SOCK_DGRAM, -1, impl->server_port)) < 0)
+ return impl->server_fd;
+ if ((impl->control_fd = connect_socket(impl, SOCK_DGRAM, impl->control_fd, control_port)) < 0)
+ return impl->control_fd;
+ if ((impl->timing_fd = connect_socket(impl, SOCK_DGRAM, impl->timing_fd, timing_port)) < 0)
+ return impl->timing_fd;
+
+ ntp = ntp_now(CLOCK_MONOTONIC);
+ send_udp_timing_packet(impl, ntp, ntp, NULL, 0);
+
+ impl->control_source = pw_loop_add_io(impl->loop, impl->control_fd,
+ SPA_IO_IN, false, on_control_source_io, impl);
+
+ impl->ready = true;
+ if (pw_stream_get_state(impl->stream, NULL) == PW_STREAM_STATE_STREAMING)
+ rtsp_do_record(impl);
+ break;
+ default:
+ return 0;
+ }
+ return 0;
+}
+
+static int rtsp_do_setup(struct impl *impl)
+{
+ int res;
+
+ switch (impl->protocol) {
+ case PROTO_TCP:
+ pw_properties_set(impl->headers, "Transport",
+ "RTP/AVP/TCP;unicast;interleaved=0-1;mode=record");
+ break;
+
+ case PROTO_UDP:
+ impl->control_port = DEFAULT_UDP_CONTROL_PORT;
+ impl->timing_port = DEFAULT_UDP_TIMING_PORT;
+
+ impl->control_fd = create_udp_socket(impl, &impl->control_port);
+ impl->timing_fd = create_udp_socket(impl, &impl->timing_port);
+ if (impl->control_fd < 0 || impl->timing_fd < 0)
+ goto error;
+
+ impl->timing_source = pw_loop_add_io(impl->loop, impl->timing_fd,
+ SPA_IO_IN, false, on_timing_source_io, impl);
+
+ pw_properties_setf(impl->headers, "Transport",
+ "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;"
+ "control_port=%u;timing_port=%u",
+ impl->control_port, impl->timing_port);
+ break;
+
+ default:
+ return -ENOTSUP;
+ }
+
+ res = pw_rtsp_client_send(impl->rtsp, "SETUP", &impl->headers->dict,
+ NULL, NULL, rtsp_setup_reply, impl);
+
+ pw_properties_set(impl->headers, "Transport", NULL);
+
+ return res;
+error:
+ if (impl->control_fd > 0)
+ close(impl->control_fd);
+ impl->control_fd = -1;
+ if (impl->timing_fd > 0)
+ close(impl->timing_fd);
+ impl->timing_fd = -1;
+ return -EIO;
+}
+
+static int rtsp_announce_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+
+ pw_log_info("reply %d", status);
+
+ pw_properties_set(impl->headers, "Apple-Challenge", NULL);
+
+ return rtsp_do_setup(impl);
+}
+
+static void base64_encode(const uint8_t *data, size_t len, char *enc, char pad)
+{
+ static const char tab[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ size_t i;
+ for (i = 0; i < len; i += 3) {
+ uint32_t v;
+ v = data[i+0] << 16;
+ v |= (i+1 < len ? data[i+1] : 0) << 8;
+ v |= (i+2 < len ? data[i+2] : 0);
+ *enc++ = tab[(v >> (3*6)) & 0x3f];
+ *enc++ = tab[(v >> (2*6)) & 0x3f];
+ *enc++ = i+1 < len ? tab[(v >> (1*6)) & 0x3f] : pad;
+ *enc++ = i+2 < len ? tab[(v >> (0*6)) & 0x3f] : pad;
+ }
+ *enc = '\0';
+}
+
+static size_t base64_decode(const char *data, size_t len, uint8_t *dec)
+{
+ uint8_t tab[] = {
+ 62, -1, -1, -1, 63, 52, 53, 54, 55, 56,
+ 57, 58, 59, 60, 61, -1, -1, -1, -1, -1,
+ -1, -1, 0, 1, 2, 3, 4, 5, 6, 7,
+ 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
+ 18, 19, 20, 21, 22, 23, 24, 25, -1, -1,
+ -1, -1, -1, -1, 26, 27, 28, 29, 30, 31,
+ 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
+ 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 };
+ size_t i, j;
+ for (i = 0, j = 0; i < len; i += 4) {
+ uint32_t v;
+ v = tab[data[i+0]-43] << (3*6);
+ v |= tab[data[i+1]-43] << (2*6);
+ v |= (data[i+2] == '=' ? 0 : tab[data[i+2]-43]) << (1*6);
+ v |= (data[i+3] == '=' ? 0 : tab[data[i+3]-43]);
+ dec[j++] = (v >> 16) & 0xff;
+ if (data[i+2] != '=') dec[j++] = (v >> 8) & 0xff;
+ if (data[i+3] != '=') dec[j++] = v & 0xff;
+ }
+ return j;
+}
+
+static int rsa_encrypt(uint8_t *data, int len, uint8_t *res)
+{
+ RSA *rsa;
+ uint8_t modulus[256];
+ uint8_t exponent[8];
+ size_t size;
+ BIGNUM *n_bn = NULL;
+ BIGNUM *e_bn = NULL;
+ char n[] =
+ "59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUtwC"
+ "5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDR"
+ "KSKv6kDqnw4UwPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuB"
+ "OitnZ/bDzPHrTOZz0Dew0uowxf/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJ"
+ "Q+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/UAaHqn9JdsBWLUEpVviYnh"
+ "imNVvYFZeCXg/IdTQ+x4IRdiXNv5hEew==";
+ char e[] = "AQAB";
+
+ rsa = RSA_new();
+
+ size = base64_decode(n, strlen(n), modulus);
+ n_bn = BN_bin2bn(modulus, size, NULL);
+
+ size = base64_decode(e, strlen(e), exponent);
+ e_bn = BN_bin2bn(exponent, size, NULL);
+
+ RSA_set0_key(rsa, n_bn, e_bn, NULL);
+
+ size = RSA_public_encrypt(len, data, res, rsa, RSA_PKCS1_OAEP_PADDING);
+ RSA_free(rsa);
+ return size;
+}
+
+static int rtsp_do_announce(struct impl *impl)
+{
+ const char *host;
+ uint8_t rsakey[512];
+ char key[512*2];
+ char iv[16*2];
+ int res, frames, i, ip_version;
+ char *sdp;
+ char local_ip[256];
+ int min_latency;
+ min_latency = DEFAULT_LATENCY;
+ host = pw_properties_get(impl->props, "raop.hostname");
+
+ if (impl->protocol == PROTO_TCP)
+ frames = FRAMES_PER_TCP_PACKET;
+ else
+ frames = FRAMES_PER_UDP_PACKET;
+
+ impl->block_size = frames * impl->frame_size;
+
+ pw_rtsp_client_get_local_ip(impl->rtsp, &ip_version,
+ local_ip, sizeof(local_ip));
+
+ switch (impl->encryption) {
+ case CRYPTO_NONE:
+ asprintf(&sdp, "v=0\r\n"
+ "o=iTunes %s 0 IN IP%d %s\r\n"
+ "s=iTunes\r\n"
+ "c=IN IP%d %s\r\n"
+ "t=0 0\r\n"
+ "m=audio 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 AppleLossless\r\n"
+ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n",
+ impl->session_id, ip_version, local_ip,
+ ip_version, host, frames);
+ break;
+
+ case CRYPTO_AUTH_SETUP:
+ asprintf(&sdp, "v=0\r\n"
+ "o=iTunes %s 0 IN IP%d %s\r\n"
+ "s=iTunes\r\n"
+ "c=IN IP%d %s\r\n"
+ "t=0 0\r\n"
+ "m=audio 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 AppleLossless\r\n"
+ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
+ "a=min-latency:%d",
+ impl->session_id, ip_version, local_ip,
+ ip_version, host, frames, min_latency);
+ break;
+
+ case CRYPTO_RSA:
+ if (pw_getrandom(impl->key, sizeof(impl->key), 0) < 0 ||
+ pw_getrandom(impl->iv, sizeof(impl->iv), 0) < 0)
+ return -errno;
+
+ AES_set_encrypt_key(impl->key, 128, &impl->aes);
+
+ i = rsa_encrypt(impl->key, 16, rsakey);
+ base64_encode(rsakey, i, key, '=');
+ base64_encode(impl->iv, 16, iv, '=');
+
+ asprintf(&sdp, "v=0\r\n"
+ "o=iTunes %s 0 IN IP%d %s\r\n"
+ "s=iTunes\r\n"
+ "c=IN IP%d %s\r\n"
+ "t=0 0\r\n"
+ "m=audio 0 RTP/AVP 96\r\n"
+ "a=rtpmap:96 AppleLossless\r\n"
+ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
+ "a=rsaaeskey:%s\r\n"
+ "a=aesiv:%s\r\n",
+ impl->session_id, ip_version, local_ip,
+ ip_version, host, frames, key, iv);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ res = pw_rtsp_client_send(impl->rtsp, "ANNOUNCE", &impl->headers->dict,
+ "application/sdp", sdp, rtsp_announce_reply, impl);
+ free(sdp);
+
+ return res;
+}
+
+static int rtsp_auth_setup_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+
+ pw_log_info("reply %d", status);
+
+ return rtsp_do_announce(impl);
+}
+
+static int rtsp_do_auth_setup(struct impl *impl)
+{
+ static const unsigned char content[33] =
+ "\x01"
+ "\x59\x02\xed\xe9\x0d\x4e\xf2\xbd\x4c\xb6\x8a\x63\x30\x03\x82\x07"
+ "\xa9\x4d\xbd\x50\xd8\xaa\x46\x5b\x5d\x8c\x01\x2a\x0c\x7e\x1d\x4e";
+
+ return pw_rtsp_client_url_send(impl->rtsp, "/auth-setup", "POST", &impl->headers->dict,
+ "application/octet-stream", content, sizeof(content),
+ rtsp_auth_setup_reply, impl);
+}
+
+static const char *find_attr(char **tokens, const char *key)
+{
+ int i;
+ char *p, *s;
+ for (i = 0; tokens[i]; i++) {
+ if (!spa_strstartswith(tokens[i], key))
+ continue;
+ p = tokens[i] + strlen(key);
+ if ((s = rindex(p, '"')) == NULL)
+ continue;
+ *s = '\0';
+ if ((s = index(p, '"')) == NULL)
+ continue;
+ return s+1;
+ }
+ return NULL;
+}
+
+static int rtsp_auth_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ int res = 0;
+
+ pw_log_info("auth %d", status);
+
+ switch (status) {
+ case 200:
+ if (impl->encryption == CRYPTO_AUTH_SETUP)
+ res = rtsp_do_auth_setup(impl);
+ else
+ res = rtsp_do_announce(impl);
+ break;
+ }
+ return res;
+}
+
+SPA_PRINTF_FUNC(2,3)
+static int MD5_hash(char hash[MD5_HASH_LENGTH+1], const char *fmt, ...)
+{
+ unsigned char d[MD5_DIGEST_LENGTH];
+ int i;
+ va_list args;
+ char buffer[1024];
+
+ va_start(args, fmt);
+ vsnprintf(buffer, sizeof(buffer), fmt, args);
+ va_end(args);
+
+ MD5((unsigned char*) buffer, strlen(buffer), d);
+ for (i = 0; i < MD5_DIGEST_LENGTH; i++)
+ sprintf(&hash[2*i], "%02x", (uint8_t) d[i]);
+ hash[MD5_HASH_LENGTH] = '\0';
+ return 0;
+}
+
+static int rtsp_do_auth(struct impl *impl, const struct spa_dict *headers)
+{
+ const char *str;
+ char **tokens;
+ int n_tokens;
+ char auth[1024];
+
+ if (impl->password == NULL)
+ return -ENOTSUP;
+
+ if ((str = spa_dict_lookup(headers, "WWW-Authenticate")) == NULL)
+ return -ENOENT;
+
+ pw_log_info("Auth: %s", str);
+
+ tokens = pw_split_strv(str, " ", INT_MAX, &n_tokens);
+ if (tokens == NULL || tokens[0] == NULL)
+ goto error;
+
+ if (spa_streq(tokens[0], "Basic")) {
+ char buf[256];
+ char enc[512];
+ spa_scnprintf(buf, sizeof(buf), "%s:%s", DEFAULT_USER_NAME, impl->password);
+ base64_encode((uint8_t*)buf, strlen(buf), enc, '=');
+ spa_scnprintf(auth, sizeof(auth), "Basic %s", enc);
+ }
+ else if (spa_streq(tokens[0], "Digest")) {
+ const char *realm, *nonce, *url;
+ char h1[MD5_HASH_LENGTH+1];
+ char h2[MD5_HASH_LENGTH+1];
+ char resp[MD5_HASH_LENGTH+1];
+
+ realm = find_attr(tokens, "realm");
+ nonce = find_attr(tokens, "nonce");
+ if (realm == NULL || nonce == NULL)
+ goto error;
+
+ url = pw_rtsp_client_get_url(impl->rtsp);
+
+ MD5_hash(h1, "%s:%s:%s", DEFAULT_USER_NAME, realm, impl->password);
+ MD5_hash(h2, "OPTIONS:%s", url);
+ MD5_hash(resp, "%s:%s:%s", h1, nonce, h2);
+
+ spa_scnprintf(auth, sizeof(auth),
+ "username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"",
+ DEFAULT_USER_NAME, realm, nonce, url, resp);
+ }
+ else
+ goto error;
+
+ pw_properties_setf(impl->headers, "Authorization", "%s %s",
+ tokens[0], auth);
+ pw_free_strv(tokens);
+
+ pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict,
+ NULL, NULL, rtsp_auth_reply, impl);
+
+ return 0;
+error:
+ pw_free_strv(tokens);
+ return -EINVAL;
+}
+
+static int rtsp_options_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ int res = 0;
+
+ pw_log_info("options %d", status);
+
+ switch (status) {
+ case 401:
+ res = rtsp_do_auth(impl, headers);
+ break;
+ case 200:
+ if (impl->encryption == CRYPTO_AUTH_SETUP)
+ res = rtsp_do_auth_setup(impl);
+ else
+ res = rtsp_do_announce(impl);
+ break;
+ }
+ return res;
+}
+
+static void rtsp_connected(void *data)
+{
+ struct impl *impl = data;
+ uint32_t sci[2];
+ uint8_t rac[16];
+ char sac[16*4];
+
+ pw_log_info("connected");
+
+ impl->connected = true;
+
+ if (pw_getrandom(sci, sizeof(sci), 0) < 0 ||
+ pw_getrandom(rac, sizeof(rac), 0) < 0) {
+ pw_log_error("error generating random data: %m");
+ return;
+ }
+
+ pw_properties_setf(impl->headers, "Client-Instance",
+ "%08x%08x", sci[0], sci[1]);
+
+ base64_encode(rac, sizeof(rac), sac, '\0');
+ pw_properties_set(impl->headers, "Apple-Challenge", sac);
+
+ pw_properties_set(impl->headers, "User-Agent", DEFAULT_USER_AGENT);
+
+ pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict,
+ NULL, NULL, rtsp_options_reply, impl);
+}
+
+static void connection_cleanup(struct impl *impl)
+{
+ impl->ready = false;
+ if (impl->server_source != NULL) {
+ pw_loop_destroy_source(impl->loop, impl->server_source);
+ impl->server_source = NULL;
+ }
+ if (impl->server_fd >= 0) {
+ close(impl->server_fd);
+ impl->server_fd = -1;
+ }
+ if (impl->control_source != NULL) {
+ pw_loop_destroy_source(impl->loop, impl->control_source);
+ impl->control_source = NULL;
+ }
+ if (impl->control_fd >= 0) {
+ close(impl->control_fd);
+ impl->control_fd = -1;
+ }
+ if (impl->timing_source != NULL) {
+ pw_loop_destroy_source(impl->loop, impl->timing_source);
+ impl->timing_source = NULL;
+ }
+ if (impl->timing_fd >= 0) {
+ close(impl->timing_fd);
+ impl->timing_fd = -1;
+ }
+}
+
+static void rtsp_disconnected(void *data)
+{
+ struct impl *impl = data;
+ pw_log_info("disconnected");
+ impl->connected = false;
+ connection_cleanup(impl);
+}
+
+static void rtsp_error(void *data, int res)
+{
+ pw_log_error("error %d", res);
+}
+
+static void rtsp_message(void *data, int status,
+ const struct spa_dict *headers)
+{
+ const struct spa_dict_item *it;
+ pw_log_info("message %d", status);
+ spa_dict_for_each(it, headers)
+ pw_log_info(" %s: %s", it->key, it->value);
+
+}
+
+static const struct pw_rtsp_client_events rtsp_events = {
+ PW_VERSION_RTSP_CLIENT_EVENTS,
+ .connected = rtsp_connected,
+ .error = rtsp_error,
+ .disconnected = rtsp_disconnected,
+ .message = rtsp_message,
+};
+
+static void stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ rtsp_do_flush(impl);
+ break;
+ case PW_STREAM_STATE_STREAMING:
+ rtsp_do_record(impl);
+ break;
+ default:
+ break;
+ }
+}
+
+static int rtsp_do_connect(struct impl *impl)
+{
+ const char *hostname, *port;
+ uint32_t session_id;
+
+ if (impl->connected) {
+ if (!impl->ready)
+ return rtsp_do_announce(impl);
+ return 0;
+ }
+
+ hostname = pw_properties_get(impl->props, "raop.hostname");
+ port = pw_properties_get(impl->props, "raop.port");
+ if (hostname == NULL || port == NULL)
+ return -EINVAL;
+
+ if (pw_getrandom(&session_id, sizeof(session_id), 0) < 0)
+ return -errno;
+
+ spa_scnprintf(impl->session_id, sizeof(impl->session_id), "%u", session_id);
+
+ return pw_rtsp_client_connect(impl->rtsp, hostname, atoi(port), impl->session_id);
+}
+
+static int rtsp_teardown_reply(void *data, int status, const struct spa_dict *headers)
+{
+ struct impl *impl = data;
+ const char *str;
+
+ pw_log_info("reply");
+
+ connection_cleanup(impl);
+
+ if ((str = spa_dict_lookup(headers, "Connection")) != NULL) {
+ if (spa_streq(str, "close"))
+ pw_rtsp_client_disconnect(impl->rtsp);
+ }
+ return 0;
+}
+
+static int rtsp_do_teardown(struct impl *impl)
+{
+ if (!impl->ready)
+ return 0;
+
+ return pw_rtsp_client_send(impl->rtsp, "TEARDOWN", NULL,
+ NULL, NULL, rtsp_teardown_reply, impl);
+}
+
+static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param)
+{
+ struct impl *impl = data;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ if (param == NULL)
+ rtsp_do_teardown(impl);
+ else
+ rtsp_do_connect(impl);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events playback_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = stream_state_changed,
+ .param_changed = stream_param_changed,
+ .process = playback_stream_process
+};
+
+static int create_stream(struct impl *impl)
+{
+ int res;
+ uint32_t n_params;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b;
+
+ impl->stream = pw_stream_new(impl->core, "RAOP sink", impl->stream_props);
+ impl->stream_props = NULL;
+
+ if (impl->stream == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &playback_stream_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b,
+ SPA_PARAM_EnumFormat, &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ impl->headers = pw_properties_new(NULL, NULL);
+
+ impl->rtsp = pw_rtsp_client_new(impl->loop, NULL, 0);
+ if (impl->rtsp == NULL)
+ return -errno;
+
+ pw_rtsp_client_add_listener(impl->rtsp, &impl->rtsp_listener,
+ &rtsp_events, impl);
+
+ return 0;
+}
+
+static void core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_error,
+};
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ if (impl->rtsp)
+ pw_rtsp_client_destroy(impl->rtsp);
+
+ pw_properties_free(impl->headers);
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+ free(impl->password);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static int calc_frame_size(struct spa_audio_info_raw *info)
+{
+ int res = info->channels;
+ switch (info->format) {
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_ALAW:
+ case SPA_AUDIO_FORMAT_ULAW:
+ return res;
+ case SPA_AUDIO_FORMAT_S16:
+ case SPA_AUDIO_FORMAT_S16_OE:
+ case SPA_AUDIO_FORMAT_U16:
+ return res * 2;
+ case SPA_AUDIO_FORMAT_S24:
+ case SPA_AUDIO_FORMAT_S24_OE:
+ case SPA_AUDIO_FORMAT_U24:
+ return res * 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S24_32_OE:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_S32_OE:
+ case SPA_AUDIO_FORMAT_U32:
+ case SPA_AUDIO_FORMAT_U32_OE:
+ case SPA_AUDIO_FORMAT_F32:
+ case SPA_AUDIO_FORMAT_F32_OE:
+ return res * 4;
+ case SPA_AUDIO_FORMAT_F64:
+ case SPA_AUDIO_FORMAT_F64_OE:
+ return res * 8;
+ default:
+ return 0;
+ }
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props = NULL;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid();
+ struct impl *impl;
+ const char *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+ impl->server_fd = -1;
+ impl->control_fd = -1;
+ impl->timing_fd = -1;
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+ impl->props = props;
+
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto error;
+ }
+
+ impl->module = module;
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+
+ if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "raop-sink-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+ pw_properties_get(props, PW_KEY_NODE_NAME));
+ if (pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_LATENCY, "352/44100");
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->frame_size = calc_frame_size(&impl->info);
+ if (impl->frame_size == 0) {
+ pw_log_error("unsupported audio format:%d channels:%d",
+ impl->info.format, impl->info.channels);
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((str = pw_properties_get(props, "raop.transport")) == NULL)
+ str = "udp";
+ if (spa_streq(str, "udp"))
+ impl->protocol = PROTO_UDP;
+ else if (spa_streq(str, "tcp"))
+ impl->protocol = PROTO_TCP;
+ else {
+ pw_log_error( "can't handle transport %s", str);
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((str = pw_properties_get(props, "raop.encryption.type")) == NULL)
+ str = "none";
+ if (spa_streq(str, "none"))
+ impl->encryption = CRYPTO_NONE;
+ else if (spa_streq(str, "RSA"))
+ impl->encryption = CRYPTO_RSA;
+ else if (spa_streq(str, "auth_setup"))
+ impl->encryption = CRYPTO_AUTH_SETUP;
+ else {
+ pw_log_error( "can't handle encryption type %s", str);
+ res = -EINVAL;
+ goto error;
+ }
+
+ if ((str = pw_properties_get(props, "raop.audio.codec")) == NULL)
+ str = "PCM";
+ if (spa_streq(str, "PCM"))
+ impl->codec = CODEC_PCM;
+ else if (spa_streq(str, "ALAC"))
+ impl->codec = CODEC_ALAC;
+ else {
+ pw_log_error( "can't handle codec type %s", str);
+ res = -EINVAL;
+ goto error;
+ }
+ str = pw_properties_get(props, "raop.password");
+ impl->password = str ? strdup(str) : NULL;
+
+ 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);
+ impl->core = pw_context_connect(impl->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto error;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = create_stream(impl)) < 0)
+ goto error;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-raop/rtsp-client.c b/src/modules/module-raop/rtsp-client.c
new file mode 100644
index 0000000..4ac9a31
--- /dev/null
+++ b/src/modules/module-raop/rtsp-client.c
@@ -0,0 +1,631 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+
+#include <spa/utils/result.h>
+
+#include "rtsp-client.h"
+
+#define pw_rtsp_client_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_rtsp_client_events, m, v, ##__VA_ARGS__)
+#define pw_rtsp_client_emit_destroy(c) pw_rtsp_client_emit(c, destroy, 0)
+#define pw_rtsp_client_emit_connected(c) pw_rtsp_client_emit(c, connected, 0)
+#define pw_rtsp_client_emit_disconnected(c) pw_rtsp_client_emit(c, disconnected, 0)
+#define pw_rtsp_client_emit_error(c,r) pw_rtsp_client_emit(c, error, 0, r)
+#define pw_rtsp_client_emit_message(c,...) pw_rtsp_client_emit(c, message, 0, __VA_ARGS__)
+
+struct message {
+ struct spa_list link;
+ void *data;
+ size_t len;
+ size_t offset;
+ uint32_t cseq;
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers);
+ void *user_data;
+};
+
+enum client_recv_state {
+ CLIENT_RECV_NONE,
+ CLIENT_RECV_STATUS,
+ CLIENT_RECV_HEADERS,
+ CLIENT_RECV_CONTENT,
+};
+
+struct pw_rtsp_client {
+ struct pw_loop *loop;
+ struct pw_properties *props;
+
+ struct spa_hook_list listener_list;
+
+ char *session_id;
+ char *url;
+
+ union {
+ struct sockaddr sa;
+ struct sockaddr_in in;
+ struct sockaddr_in6 in6;
+ } local_addr;
+
+ struct spa_source *source;
+ unsigned int connecting:1;
+ unsigned int need_flush:1;
+
+ enum client_recv_state recv_state;
+ int status;
+ char line_buf[1024];
+ size_t line_pos;
+ struct pw_properties *headers;
+ size_t content_length;
+
+ uint32_t cseq;
+
+ struct spa_list messages;
+ struct spa_list pending;
+
+ void *user_data;
+};
+
+struct pw_rtsp_client *pw_rtsp_client_new(struct pw_loop *main_loop,
+ struct pw_properties *props,
+ size_t user_data_size)
+{
+ struct pw_rtsp_client *client;
+
+ client = calloc(1, sizeof(*client) + user_data_size);
+ if (client == NULL)
+ return NULL;
+
+ client->loop = main_loop;
+ client->props = props;
+ if (user_data_size > 0)
+ client->user_data = SPA_PTROFF(client, sizeof(*client), void);
+
+ spa_list_init(&client->messages);
+ spa_list_init(&client->pending);
+ spa_hook_list_init(&client->listener_list);
+ client->headers = pw_properties_new(NULL, NULL);
+ client->recv_state = CLIENT_RECV_NONE;
+
+ pw_log_info("new client %p", client);
+
+ return client;
+}
+
+void pw_rtsp_client_destroy(struct pw_rtsp_client *client)
+{
+ pw_log_info("destroy client %p", client);
+ pw_rtsp_client_emit_destroy(client);
+
+ pw_rtsp_client_disconnect(client);
+ pw_properties_free(client->headers);
+ pw_properties_free(client->props);
+ spa_hook_list_clean(&client->listener_list);
+ free(client);
+}
+
+void *pw_rtsp_client_get_user_data(struct pw_rtsp_client *client)
+{
+ return client->user_data;
+}
+
+const char *pw_rtsp_client_get_url(struct pw_rtsp_client *client)
+{
+ return client->url;
+}
+
+void pw_rtsp_client_add_listener(struct pw_rtsp_client *client,
+ struct spa_hook *listener,
+ const struct pw_rtsp_client_events *events, void *data)
+{
+ spa_hook_list_append(&client->listener_list, listener, events, data);
+}
+
+const struct pw_properties *pw_rtsp_client_get_properties(struct pw_rtsp_client *client)
+{
+ return client->props;
+}
+
+int pw_rtsp_client_get_local_ip(struct pw_rtsp_client *client,
+ int *version, char *ip, size_t len)
+{
+ if (client->local_addr.sa.sa_family == AF_INET) {
+ *version = 4;
+ if (ip)
+ inet_ntop(client->local_addr.sa.sa_family,
+ &client->local_addr.in.sin_addr, ip, len);
+ } else if (client->local_addr.sa.sa_family == AF_INET6) {
+ *version = 6;
+ if (ip)
+ inet_ntop(client->local_addr.sa.sa_family,
+ &client->local_addr.in6.sin6_addr,
+ ip, len);
+ } else
+ return -EIO;
+ return 0;
+}
+
+static int handle_connect(struct pw_rtsp_client *client, int fd)
+{
+ int res, ip_version;
+ socklen_t len;
+ char local_ip[INET6_ADDRSTRLEN];
+
+ len = sizeof(res);
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) {
+ pw_log_error("getsockopt: %m");
+ return -errno;
+ }
+ if (res != 0)
+ return -res;
+
+ len = sizeof(client->local_addr.sa);
+ if (getsockname(fd, &client->local_addr.sa, &len) < 0)
+ return -errno;
+
+ if ((res = pw_rtsp_client_get_local_ip(client, &ip_version,
+ local_ip, sizeof(local_ip))) < 0)
+ return res;
+
+ if (ip_version == 4)
+ asprintf(&client->url, "rtsp://%s/%s", local_ip, client->session_id);
+ else
+ asprintf(&client->url, "rtsp://[%s]/%s", local_ip, client->session_id);
+
+ pw_log_info("connected local ip %s", local_ip);
+
+ client->connecting = false;
+
+ client->recv_state = CLIENT_RECV_STATUS;
+ pw_properties_clear(client->headers);
+ client->status = 0;
+ client->line_pos = 0;
+ client->content_length = 0;
+
+ pw_rtsp_client_emit_connected(client);
+
+ return 0;
+}
+
+static int read_line(struct pw_rtsp_client *client, char **buf)
+{
+ int res;
+
+ while (true) {
+ uint8_t c;
+
+ res = read(client->source->fd, &c, 1);
+ if (res == 0)
+ return -EPIPE;
+ if (res < 0) {
+ res = -errno;
+ if (res == -EINTR)
+ continue;
+ if (res != -EAGAIN && res != -EWOULDBLOCK)
+ return res;
+ return 0;
+ }
+ if (c == '\n') {
+ client->line_buf[client->line_pos] = '\0';
+ client->line_pos = 0;
+ if (buf)
+ *buf = client->line_buf;
+ return 1;
+ }
+ if (c == '\r')
+ continue;
+ if (client->line_pos < sizeof(client->line_buf) - 1)
+ client->line_buf[client->line_pos++] = c;
+ client->line_buf[client->line_pos] = '\0';
+ }
+ return 0;
+}
+
+static struct message *find_pending(struct pw_rtsp_client *client, uint32_t cseq)
+{
+ struct message *msg;
+ spa_list_for_each(msg, &client->pending, link) {
+ if (msg->cseq == cseq)
+ return msg;
+ }
+ return NULL;
+}
+
+static int process_status(struct pw_rtsp_client *client, char *buf)
+{
+ const char *state = NULL, *s;
+ size_t len;
+
+ pw_log_info("status: %s", buf);
+
+ s = pw_split_walk(buf, " ", &len, &state);
+ if (!spa_strstartswith(s, "RTSP/"))
+ return -EPROTO;
+
+ s = pw_split_walk(buf, " ", &len, &state);
+ if (s == NULL)
+ return -EPROTO;
+
+ client->status = atoi(s);
+ if (client->status == 0)
+ return -EPROTO;
+
+ s = pw_split_walk(buf, " ", &len, &state);
+ if (s == NULL)
+ return -EPROTO;
+
+ pw_properties_clear(client->headers);
+ client->recv_state = CLIENT_RECV_HEADERS;
+
+ return 0;
+}
+
+static void dispatch_handler(struct pw_rtsp_client *client)
+{
+ uint32_t cseq;
+ int res;
+ struct message *msg;
+
+ if (pw_properties_fetch_uint32(client->headers, "CSeq", &cseq) < 0)
+ return;
+
+ pw_log_info("received reply to request with cseq:%" PRIu32, cseq);
+
+ msg = find_pending(client, cseq);
+ if (msg) {
+ res = msg->reply(msg->user_data, client->status, &client->headers->dict);
+ spa_list_remove(&msg->link);
+ free(msg);
+
+ if (res < 0)
+ pw_log_warn("client %p: handle reply cseq:%u error: %s",
+ client, cseq, spa_strerror(res));
+ }
+ else {
+ pw_rtsp_client_emit_message(client, client->status, &client->headers->dict);
+ }
+}
+
+static void process_received_message(struct pw_rtsp_client *client)
+{
+ client->recv_state = CLIENT_RECV_STATUS;
+ dispatch_handler(client);
+}
+
+static int process_header(struct pw_rtsp_client *client, char *buf)
+{
+ if (strlen(buf) > 0) {
+ char *key = buf, *value;
+
+ value = strstr(buf, ":");
+ if (value == NULL)
+ return -EPROTO;
+
+ *value++ = '\0';
+
+ value = pw_strip(value, " ");
+
+ pw_properties_set(client->headers, key, value);
+ }
+ else {
+ const struct spa_dict_item *it;
+ spa_dict_for_each(it, &client->headers->dict)
+ pw_log_info(" %s: %s", it->key, it->value);
+
+ client->content_length = pw_properties_get_uint32(client->headers, "Content-Length", 0);
+ if (client->content_length > 0)
+ client->recv_state = CLIENT_RECV_CONTENT;
+ else
+ process_received_message(client);
+ }
+
+ return 0;
+}
+
+static int process_content(struct pw_rtsp_client *client)
+{
+ char buf[1024];
+
+ while (client->content_length > 0) {
+ const size_t max_recv = SPA_MIN(sizeof(buf), client->content_length);
+
+ ssize_t res = read(client->source->fd, buf, max_recv);
+ if (res == 0)
+ return -EPIPE;
+
+ if (res < 0) {
+ res = -errno;
+ if (res == -EAGAIN || res == -EWOULDBLOCK)
+ return 0;
+
+ return res;
+ }
+
+ spa_assert((size_t) res <= client->content_length);
+ client->content_length -= res;
+ }
+
+ if (client->content_length == 0)
+ process_received_message(client);
+
+ return 0;
+}
+
+static int process_input(struct pw_rtsp_client *client)
+{
+ if (client->recv_state == CLIENT_RECV_STATUS || client->recv_state == CLIENT_RECV_HEADERS) {
+ char *buf = NULL;
+ int res;
+
+ if ((res = read_line(client, &buf)) <= 0)
+ return res;
+
+ pw_log_debug("received line: %s", buf);
+
+ switch (client->recv_state) {
+ case CLIENT_RECV_STATUS:
+ return process_status(client, buf);
+ case CLIENT_RECV_HEADERS:
+ return process_header(client, buf);
+ default:
+ spa_assert_not_reached();
+ }
+ }
+ else if (client->recv_state == CLIENT_RECV_CONTENT) {
+ return process_content(client);
+ }
+ else {
+ spa_assert_not_reached();
+ }
+}
+
+static int flush_output(struct pw_rtsp_client *client)
+{
+ int res;
+
+ client->need_flush = false;
+
+ while (true) {
+ struct message *msg;
+ void *data;
+ size_t size;
+
+ if (spa_list_is_empty(&client->messages))
+ break;
+
+ msg = spa_list_first(&client->messages, struct message, link);
+
+ if (msg->offset < msg->len) {
+ data = SPA_PTROFF(msg->data, msg->offset, void);
+ size = msg->len - msg->offset;
+ } else {
+ pw_log_info("sent: %s", (char *)msg->data);
+ spa_list_remove(&msg->link);
+ if (msg->reply != NULL)
+ spa_list_append(&client->pending, &msg->link);
+ else
+ free(msg);
+ continue;
+ }
+
+ while (true) {
+ res = send(client->source->fd, data, size, MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (res < 0) {
+ res = -errno;
+ if (res == -EINTR)
+ continue;
+ if (res != -EAGAIN && res != -EWOULDBLOCK)
+ pw_log_warn("client %p: send %zu, error %d: %m",
+ client, size, res);
+ return res;
+ }
+ msg->offset += res;
+ break;
+ }
+ }
+ return 0;
+}
+
+static void
+on_source_io(void *data, int fd, uint32_t mask)
+{
+ struct pw_rtsp_client *client = data;
+ int res;
+
+ if (mask & (SPA_IO_ERR | SPA_IO_HUP)) {
+ res = -EPIPE;
+ goto error;
+ }
+ if (mask & SPA_IO_IN) {
+ if ((res = process_input(client)) < 0)
+ goto error;
+ }
+ if (mask & SPA_IO_OUT || client->need_flush) {
+ if (client->connecting) {
+ if ((res = handle_connect(client, fd)) < 0)
+ goto error;
+ }
+ res = flush_output(client);
+ if (res >= 0) {
+ pw_loop_update_io(client->loop, client->source,
+ client->source->mask & ~SPA_IO_OUT);
+ } else if (res != -EAGAIN)
+ goto error;
+ }
+done:
+ return;
+error:
+ pw_log_error("%p: got connection error %d (%s)", client, res, spa_strerror(res));
+ pw_rtsp_client_emit_error(client, res);
+ pw_rtsp_client_disconnect(client);
+ goto done;
+}
+
+int pw_rtsp_client_connect(struct pw_rtsp_client *client,
+ const char *hostname, uint16_t port, const char *session_id)
+{
+ struct addrinfo hints;
+ struct addrinfo *result, *rp;
+ int res, fd;
+ char port_str[12];
+
+ if (client->source != NULL)
+ pw_rtsp_client_disconnect(client);
+
+ pw_log_info("%p: connect %s:%u", client, hostname, port);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = 0;
+ hints.ai_protocol = 0;
+
+ spa_scnprintf(port_str, sizeof(port_str), "%u", port);
+
+ if ((res = getaddrinfo(hostname, port_str, &hints, &result)) != 0) {
+ pw_log_error("getaddrinfo: %s", gai_strerror(res));
+ return -EINVAL;
+ }
+ for (rp = result; rp != NULL; rp = rp->ai_next) {
+ fd = socket(rp->ai_family,
+ rp->ai_socktype | SOCK_CLOEXEC | SOCK_NONBLOCK,
+ rp->ai_protocol);
+ if (fd == -1)
+ continue;
+
+ res = connect(fd, rp->ai_addr, rp->ai_addrlen);
+ if (res == 0 || (res < 0 && errno == EINPROGRESS))
+ break;
+
+ close(fd);
+ }
+ freeaddrinfo(result);
+
+ if (rp == NULL) {
+ pw_log_error("Could not connect to %s:%u", hostname, port);
+ return -EINVAL;
+ }
+
+ client->source = pw_loop_add_io(client->loop, fd,
+ SPA_IO_IN | SPA_IO_OUT | SPA_IO_HUP | SPA_IO_ERR,
+ true, on_source_io, client);
+
+ if (client->source == NULL) {
+ res = -errno;
+ pw_log_error("%p: source create failed: %m", client);
+ close(fd);
+ return res;
+ }
+ client->connecting = true;
+ free(client->session_id);
+ client->session_id = strdup(session_id);
+ pw_log_info("%p: connecting", client);
+
+ return 0;
+}
+
+int pw_rtsp_client_disconnect(struct pw_rtsp_client *client)
+{
+ if (client->source == NULL)
+ return 0;
+
+ pw_loop_destroy_source(client->loop, client->source);
+ client->source = NULL;
+ free(client->url);
+ client->url = NULL;
+ free(client->session_id);
+ client->session_id = NULL;
+ pw_rtsp_client_emit_disconnected(client);
+ return 0;
+}
+
+int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
+ const char *cmd, const struct spa_dict *headers,
+ const char *content_type, const void *content, size_t content_length,
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers),
+ void *user_data)
+{
+ FILE *f;
+ size_t len;
+ const struct spa_dict_item *it;
+ struct message *msg;
+ uint32_t cseq;
+
+ if ((f = open_memstream((char**)&msg, &len)) == NULL)
+ return -errno;
+
+ fseek(f, sizeof(*msg), SEEK_SET);
+
+ cseq = ++client->cseq;
+
+ fprintf(f, "%s %s RTSP/1.0\r\n", cmd, url);
+ fprintf(f, "CSeq: %" PRIu32 "\r\n", cseq);
+
+ if (headers != NULL) {
+ spa_dict_for_each(it, headers)
+ fprintf(f, "%s: %s\r\n", it->key, it->value);
+ }
+ if (content_type != NULL && content != NULL) {
+ fprintf(f, "Content-Type: %s\r\nContent-Length: %zu\r\n",
+ content_type, content_length);
+ }
+ fprintf(f, "\r\n");
+
+ if (content_type && content)
+ fwrite(content, 1, content_length, f);
+
+ fclose(f);
+
+ msg->data = SPA_PTROFF(msg, sizeof(*msg), void);
+ msg->len = len - sizeof(*msg);
+ msg->offset = 0;
+ msg->reply = reply;
+ msg->user_data = user_data;
+ msg->cseq = cseq;
+
+ spa_list_append(&client->messages, &msg->link);
+
+ client->need_flush = true;
+ if (client->source && !(client->source->mask & SPA_IO_OUT)) {
+ pw_loop_update_io(client->loop, client->source,
+ client->source->mask | SPA_IO_OUT);
+ }
+ return 0;
+}
+
+int pw_rtsp_client_send(struct pw_rtsp_client *client,
+ const char *cmd, const struct spa_dict *headers,
+ const char *content_type, const char *content,
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers),
+ void *user_data)
+{
+ const size_t content_length = content ? strlen(content) : 0;
+
+ return pw_rtsp_client_url_send(client, client->url, cmd, headers,
+ content_type, content, content_length,
+ reply, user_data);
+}
diff --git a/src/modules/module-raop/rtsp-client.h b/src/modules/module-raop/rtsp-client.h
new file mode 100644
index 0000000..014468b
--- /dev/null
+++ b/src/modules/module-raop/rtsp-client.h
@@ -0,0 +1,92 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_RTSP_CLIENT_H
+#define PIPEWIRE_RTSP_CLIENT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+
+#include <pipewire/pipewire.h>
+
+struct pw_rtsp_client;
+
+struct pw_rtsp_client_events {
+#define PW_VERSION_RTSP_CLIENT_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*connected) (void *data);
+ void (*error) (void *data, int res);
+ void (*disconnected) (void *data);
+
+ void (*message) (void *data, int status,
+ const struct spa_dict *headers);
+
+};
+
+struct pw_rtsp_client * pw_rtsp_client_new(struct pw_loop *main_loop,
+ struct pw_properties *props,
+ size_t user_data_size);
+
+void pw_rtsp_client_destroy(struct pw_rtsp_client *client);
+
+void *pw_rtsp_client_get_user_data(struct pw_rtsp_client *client);
+const char *pw_rtsp_client_get_url(struct pw_rtsp_client *client);
+
+void pw_rtsp_client_add_listener(struct pw_rtsp_client *client,
+ struct spa_hook *listener,
+ const struct pw_rtsp_client_events *events, void *data);
+
+const struct pw_properties *pw_rtsp_client_get_properties(struct pw_rtsp_client *client);
+
+int pw_rtsp_client_connect(struct pw_rtsp_client *client,
+ const char *hostname, uint16_t port, const char *session_id);
+int pw_rtsp_client_disconnect(struct pw_rtsp_client *client);
+
+int pw_rtsp_client_get_local_ip(struct pw_rtsp_client *client,
+ int *version, char *ip, size_t len);
+
+int pw_rtsp_client_url_send(struct pw_rtsp_client *client, const char *url,
+ const char *cmd, const struct spa_dict *headers,
+ const char *content_type, const void *content, size_t content_length,
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers),
+ void *user_data);
+
+int pw_rtsp_client_send(struct pw_rtsp_client *client,
+ const char *cmd, const struct spa_dict *headers,
+ const char *content_type, const char *content,
+ int (*reply) (void *user_data, int status, const struct spa_dict *headers),
+ void *user_data);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_RTSP_CLIENT_H */
diff --git a/src/modules/module-roc-sink.c b/src/modules/module-roc-sink.c
new file mode 100644
index 0000000..86bd983
--- /dev/null
+++ b/src/modules/module-roc-sink.c
@@ -0,0 +1,514 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <roc/config.h>
+#include <roc/log.h>
+#include <roc/context.h>
+#include <roc/log.h>
+#include <roc/sender.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#include "module-roc/common.h"
+
+/** \page page_module_roc_sink PipeWire Module: ROC sink
+ *
+ * The `roc-sink` module creates a PipeWire sink that sends samples to
+ * a preconfigured receiver address. One can then connect an audio stream
+ * of any running application to that sink or make it the default sink.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `sink.props = {}`: properties to be passed to the sink stream
+ * - `sink.name = <str>`: node.name of the sink
+ * - `remote.ip = <str>`: remote receiver ip
+ * - `remote.source.port = <str>`: remote receiver TCP/UDP port for source packets
+ * - `remote.repair.port = <str>`: remote receiver TCP/UDP port for receiver packets
+ * - `fec.code = <str>`: Possible values: `disable`, `rs8m`, `ldpc`
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-roc-sink
+ * args = {
+ * local.ip = 0.0.0.0
+ * fec.code = disable
+ * remote.ip = 192.168.0.244
+ * remote.source.port = 10001
+ * remote.repair.port = 10002
+ * sink.name = "ROC Sink"
+ * sink.props = {
+ * node.name = "roc-sink"
+ * }
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ */
+
+#define NAME "roc-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_sink_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_properties *props;
+ struct pw_context *module_context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+
+ struct pw_stream *capture;
+ struct spa_hook capture_listener;
+ struct pw_properties *capture_props;
+
+ unsigned int do_disconnect:1;
+
+ roc_endpoint *remote_source_addr;
+ roc_endpoint *remote_repair_addr;
+ roc_context *context;
+ roc_sender *sender;
+
+ roc_fec_encoding fec_code;
+ uint32_t rate;
+ char *remote_ip;
+ int remote_source_port;
+ int remote_repair_port;
+};
+
+static void stream_destroy(void *d)
+{
+ struct module_roc_sink_data *data = d;
+ spa_hook_remove(&data->capture_listener);
+ data->capture = NULL;
+}
+
+static void capture_process(void *data)
+{
+ struct module_roc_sink_data *impl = data;
+ struct pw_buffer *in;
+ struct spa_data *d;
+ roc_frame frame;
+ uint32_t i, size, offset;
+
+ if ((in = pw_stream_dequeue_buffer(impl->capture)) == NULL) {
+ pw_log_debug("Out of capture buffers: %m");
+ return;
+ }
+
+ for (i = 0; i < in->buffer->n_datas; i++) {
+ d = &in->buffer->datas[i];
+
+ offset = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->maxsize - offset, d->chunk->size);
+
+ while (size > 0) {
+ spa_zero(frame);
+
+ frame.samples = SPA_MEMBER(d->data, offset, void);
+ frame.samples_size = size;
+
+ if (roc_sender_write(impl->sender, &frame) != 0) {
+ pw_log_warn("Failed to write to roc sink");
+ break;
+ }
+
+ offset += frame.samples_size;
+ size -= frame.samples_size;
+ }
+ }
+ pw_stream_queue_buffer(impl->capture, in);
+}
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+ struct module_roc_sink_data *data = d;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(data->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct module_roc_sink_data *data = d;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("stream disconnected, unloading");
+ pw_impl_module_schedule_destroy(data->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_error("stream error: %s", error);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events in_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = capture_process
+};
+
+static void core_destroy(void *d)
+{
+ struct module_roc_sink_data *data = d;
+ spa_hook_remove(&data->core_listener);
+ data->core = NULL;
+ pw_impl_module_schedule_destroy(data->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct module_roc_sink_data *data)
+{
+ if (data->capture)
+ pw_stream_destroy(data->capture);
+ if (data->core && data->do_disconnect)
+ pw_core_disconnect(data->core);
+
+ pw_properties_free(data->capture_props);
+ pw_properties_free(data->props);
+
+ if (data->sender)
+ roc_sender_close(data->sender);
+ if (data->context)
+ roc_context_close(data->context);
+
+ if (data->remote_source_addr)
+ (void) roc_endpoint_deallocate(data->remote_source_addr);
+ if (data->remote_repair_addr)
+ (void) roc_endpoint_deallocate(data->remote_repair_addr);
+
+ free(data->remote_ip);
+ free(data);
+}
+
+static void module_destroy(void *d)
+{
+ struct module_roc_sink_data *data = d;
+ spa_hook_remove(&data->module_listener);
+ impl_destroy(data);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static int roc_sink_setup(struct module_roc_sink_data *data)
+{
+ roc_context_config context_config;
+ roc_sender_config sender_config;
+ struct spa_audio_info_raw info = { 0 };
+ const struct spa_pod *params[1];
+ struct spa_pod_builder b;
+ uint32_t n_params;
+ uint8_t buffer[1024];
+ int res;
+ roc_protocol audio_proto, repair_proto;
+
+ memset(&context_config, 0, sizeof(context_config));
+
+ res = roc_context_open(&context_config, &data->context);
+ if (res) {
+ pw_log_error("failed to create roc context: %d", res);
+ return -EINVAL;
+ }
+
+ memset(&sender_config, 0, sizeof(sender_config));
+
+ sender_config.frame_sample_rate = data->rate;
+ sender_config.frame_channels = ROC_CHANNEL_SET_STEREO;
+ sender_config.frame_encoding = ROC_FRAME_ENCODING_PCM_FLOAT;
+ sender_config.fec_encoding = data->fec_code;
+
+ info.rate = data->rate;
+
+ /* Fixed to be the same as ROC sender config above */
+ info.channels = 2;
+ info.format = SPA_AUDIO_FORMAT_F32;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+
+ pw_properties_setf(data->capture_props, PW_KEY_NODE_RATE, "1/%d", info.rate);
+
+ res = roc_sender_open(data->context, &sender_config, &data->sender);
+ if (res) {
+ pw_log_error("failed to create roc sender: %d", res);
+ return -EINVAL;
+ }
+
+ switch (data->fec_code) {
+ case ROC_FEC_ENCODING_DEFAULT:
+ case ROC_FEC_ENCODING_RS8M:
+ audio_proto = ROC_PROTO_RTP_RS8M_SOURCE;
+ repair_proto = ROC_PROTO_RS8M_REPAIR;
+ break;
+ case ROC_FEC_ENCODING_LDPC_STAIRCASE:
+ audio_proto = ROC_PROTO_RTP_LDPC_SOURCE;
+ repair_proto = ROC_PROTO_LDPC_REPAIR;
+ break;
+ default:
+ audio_proto = ROC_PROTO_RTP;
+ repair_proto = 0;
+ break;
+ }
+
+ res = pw_roc_create_endpoint(&data->remote_source_addr, audio_proto, data->remote_ip, data->remote_source_port);
+ if (res < 0) {
+ pw_log_warn("failed to create source endpoint: %s", spa_strerror(res));
+ return res;
+ }
+
+ if (roc_sender_connect(data->sender, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_SOURCE,
+ data->remote_source_addr) != 0) {
+ pw_log_error("can't connect roc sender to remote source address");
+ return -EINVAL;
+ }
+
+ if (repair_proto != 0) {
+ res = pw_roc_create_endpoint(&data->remote_repair_addr, repair_proto, data->remote_ip, data->remote_repair_port);
+ if (res < 0) {
+ pw_log_error("failed to create repair endpoint: %s", spa_strerror(res));
+ return res;
+ }
+
+ if (roc_sender_connect(data->sender, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_REPAIR,
+ data->remote_repair_addr) != 0) {
+ pw_log_error("can't connect roc sender to remote repair address");
+ return -EINVAL;
+ }
+ }
+
+ data->capture = pw_stream_new(data->core,
+ "roc-sink capture", data->capture_props);
+ data->capture_props = NULL;
+ if (data->capture == NULL)
+ return -errno;
+
+ pw_stream_add_listener(data->capture,
+ &data->capture_listener,
+ &in_stream_events, data);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &info);
+
+ if ((res = pw_stream_connect(data->capture,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+static const struct spa_dict_item module_roc_sink_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { PW_KEY_MODULE_DESCRIPTION, "roc sink" },
+ { PW_KEY_MODULE_USAGE, "sink.name=<name for the sink> "
+ "local.ip=<local sender ip> "
+ "fec.code=<empty>|disable|rs8m|ldpc "
+ "remote.ip=<remote receiver ip> "
+ "remote.source.port=<remote receiver port for source packets> "
+ "remote.repair.port=<remote receiver port for repair packets> "
+ "sink.props= { key=val ... } " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct module_roc_sink_data *data;
+ struct pw_properties *props = NULL, *capture_props = NULL;
+ const char *str;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ data = calloc(1, sizeof(struct module_roc_sink_data));
+ if (data == NULL)
+ return -errno;
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ data->props = props;
+
+ capture_props = pw_properties_new(NULL, NULL);
+ if (capture_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ data->capture_props = capture_props;
+
+ data->module = module;
+ data->module_context = context;
+
+ if ((str = pw_properties_get(props, "sink.name")) != NULL) {
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "sink.name", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "sink.props")) != NULL)
+ pw_properties_update_string(capture_props, str, strlen(str));
+
+ if (pw_properties_get(capture_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_NAME, "roc-sink");
+ if (pw_properties_get(capture_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_DESCRIPTION, "ROC Sink");
+ if (pw_properties_get(capture_props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(capture_props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(capture_props, PW_KEY_NODE_NETWORK, "true");
+ if ((str = pw_properties_get(capture_props, PW_KEY_MEDIA_CLASS)) == NULL)
+ pw_properties_set(capture_props, PW_KEY_MEDIA_CLASS, "Audio/Sink");
+
+ data->rate = pw_properties_get_uint32(capture_props, PW_KEY_AUDIO_RATE, data->rate);
+ if (data->rate == 0)
+ data->rate = PW_ROC_DEFAULT_RATE;
+
+ if ((str = pw_properties_get(props, "remote.ip")) != NULL) {
+ data->remote_ip = strdup(str);
+ pw_properties_set(props, "remote.ip", NULL);
+ } else {
+ pw_log_error("Remote IP not specified");
+ res = -EINVAL;
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "remote.source.port")) != NULL) {
+ data->remote_source_port = pw_properties_parse_int(str);
+ pw_properties_set(props, "remote.source.port", NULL);
+ } else {
+ data->remote_source_port = PW_ROC_DEFAULT_SOURCE_PORT;
+ }
+
+ if ((str = pw_properties_get(props, "remote.repair.port")) != NULL) {
+ data->remote_repair_port = pw_properties_parse_int(str);
+ pw_properties_set(props, "remote.repair.port", NULL);
+ } else {
+ data->remote_repair_port = PW_ROC_DEFAULT_REPAIR_PORT;
+ }
+ if ((str = pw_properties_get(props, "fec.code")) != NULL) {
+ if (pw_roc_parse_fec_encoding(&data->fec_code, str)) {
+ pw_log_error("Invalid fec code %s, using default", str);
+ data->fec_code = ROC_FEC_ENCODING_DEFAULT;
+ }
+ pw_log_info("using fec.code %s %d", str, data->fec_code);
+ pw_properties_set(props, "fec.code", NULL);
+ } else {
+ data->fec_code = ROC_FEC_ENCODING_DEFAULT;
+ }
+
+
+ data->core = pw_context_get_object(data->module_context, PW_TYPE_INTERFACE_Core);
+ if (data->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ data->core = pw_context_connect(data->module_context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ data->do_disconnect = true;
+ }
+ if (data->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto out;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)data->core,
+ &data->core_proxy_listener,
+ &core_proxy_events, data);
+ pw_core_add_listener(data->core,
+ &data->core_listener,
+ &core_events, data);
+
+ if ((res = roc_sink_setup(data)) < 0)
+ goto out;
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_roc_sink_info));
+
+ pw_log_info("Successfully loaded module-roc-sink");
+
+ return 0;
+
+out:
+ impl_destroy(data);
+ return res;
+}
diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c
new file mode 100644
index 0000000..91fe60b
--- /dev/null
+++ b/src/modules/module-roc-source.c
@@ -0,0 +1,546 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ * Copyright © 2021 Sanchayan Maity <sanchayan@asymptotic.io>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <roc/config.h>
+#include <roc/log.h>
+#include <roc/context.h>
+#include <roc/log.h>
+#include <roc/receiver.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#include "module-roc/common.h"
+
+/** \page page_module_roc_source PipeWire Module: ROC source
+ *
+ * The `roc-source` module creates a PipeWire source that receives samples
+ * from ROC sender and passes them to the sink it is connected to. One can
+ * then connect it to any audio device.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `source.props = {}`: properties to be passed to the source stream
+ * - `source.name = <str>`: node.name of the source
+ * - `local.ip = <str>`: local sender ip
+ * - `local.source.port = <str>`: local receiver TCP/UDP port for source packets
+ * - `local.repair.port = <str>`: local receiver TCP/UDP port for receiver packets
+ * - `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`
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-roc-source
+ * args = {
+ * local.ip = 0.0.0.0
+ * resampler.profile = medium
+ * fec.code = disable
+ * sess.latency.msec = 5000
+ * local.source.port = 10001
+ * local.repair.port = 10002
+ * source.name = "ROC Source"
+ * source.props = {
+ * node.name = "roc-source"
+ * }
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ */
+
+#define NAME "roc-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct module_roc_source_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_properties *props;
+ struct pw_context *module_context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+
+ struct pw_stream *playback;
+ struct spa_hook playback_listener;
+ struct pw_properties *playback_props;
+
+ unsigned int do_disconnect:1;
+ uint32_t stride;
+
+ roc_endpoint *local_source_addr;
+ roc_endpoint *local_repair_addr;
+ roc_context *context;
+ roc_receiver *receiver;
+
+ roc_resampler_profile resampler_profile;
+ roc_fec_encoding fec_code;
+ uint32_t rate;
+ char *local_ip;
+ int local_source_port;
+ int local_repair_port;
+ int sess_latency_msec;
+};
+
+static void stream_destroy(void *d)
+{
+ struct module_roc_source_data *data = d;
+ spa_hook_remove(&data->playback_listener);
+ data->playback = NULL;
+}
+
+static void playback_process(void *data)
+{
+ struct module_roc_source_data *impl = data;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ roc_frame frame;
+ uint8_t *dst;
+
+ if ((b = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
+ pw_log_debug("Out of playback buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((dst = buf->datas[0].data) == NULL)
+ return;
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = impl->stride;
+ buf->datas[0].chunk->size = 0;
+
+ spa_zero(frame);
+ frame.samples = dst;
+ frame.samples_size = SPA_MIN(b->requested * impl->stride, buf->datas[0].maxsize);
+
+ if (roc_receiver_read(impl->receiver, &frame) != 0) {
+ /* Handle EOF and error */
+ pw_log_error("Failed to read from roc source");
+ pw_impl_module_schedule_destroy(impl->module);
+ frame.samples_size = 0;
+ }
+
+ buf->datas[0].chunk->size = frame.samples_size;
+ b->size = frame.samples_size / impl->stride;
+
+ pw_stream_queue_buffer(impl->playback, b);
+}
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+ struct module_roc_source_data *data = d;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(data->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct module_roc_source_data *data = d;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("stream disconnected, unloading");
+ pw_impl_module_schedule_destroy(data->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_error("stream error: %s", error);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events out_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = playback_process
+};
+
+static void core_destroy(void *d)
+{
+ struct module_roc_source_data *data = d;
+ spa_hook_remove(&data->core_listener);
+ data->core = NULL;
+ pw_impl_module_schedule_destroy(data->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct module_roc_source_data *data)
+{
+ if (data->playback)
+ pw_stream_destroy(data->playback);
+ if (data->core && data->do_disconnect)
+ pw_core_disconnect(data->core);
+
+ pw_properties_free(data->playback_props);
+ pw_properties_free(data->props);
+
+ if (data->receiver)
+ roc_receiver_close(data->receiver);
+ if (data->context)
+ roc_context_close(data->context);
+
+ if (data->local_source_addr)
+ (void) roc_endpoint_deallocate(data->local_source_addr);
+ if (data->local_repair_addr)
+ (void) roc_endpoint_deallocate(data->local_repair_addr);
+
+ free(data->local_ip);
+ free(data);
+}
+
+static void module_destroy(void *d)
+{
+ struct module_roc_source_data *data = d;
+ spa_hook_remove(&data->module_listener);
+ impl_destroy(data);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static int roc_source_setup(struct module_roc_source_data *data)
+{
+ roc_context_config context_config;
+ roc_receiver_config receiver_config;
+ struct spa_audio_info_raw info = { 0 };
+ const struct spa_pod *params[1];
+ struct spa_pod_builder b;
+ uint32_t n_params;
+ uint8_t buffer[1024];
+ int res;
+ roc_protocol audio_proto, repair_proto;
+
+ spa_zero(context_config);
+ res = roc_context_open(&context_config, &data->context);
+ if (res) {
+ pw_log_error("failed to create roc context: %d", res);
+ return -EINVAL;
+ }
+
+ spa_zero(receiver_config);
+ receiver_config.frame_sample_rate = data->rate;
+ receiver_config.frame_channels = ROC_CHANNEL_SET_STEREO;
+ receiver_config.frame_encoding = ROC_FRAME_ENCODING_PCM_FLOAT;
+ receiver_config.resampler_profile = data->resampler_profile;
+
+ info.rate = data->rate;
+
+ /* Fixed to be the same as ROC receiver config above */
+ info.channels = 2;
+ info.format = SPA_AUDIO_FORMAT_F32;
+ info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ data->stride = info.channels * sizeof(float);
+
+ pw_properties_setf(data->playback_props, PW_KEY_NODE_RATE, "1/%d", info.rate);
+
+ /*
+ * Note that target latency is in nano seconds.
+ *
+ * The session will not start playing until it accumulates the
+ * requested latency. Then if resampler is enabled, the session will
+ * adjust it's clock to keep actual latency as close as possible to
+ * the target latency. If zero, default value will be used.
+ *
+ * See API reference:
+ * https://roc-streaming.org/toolkit/docs/api/reference.html
+ */
+ receiver_config.target_latency = data->sess_latency_msec * 1000000;
+
+ res = roc_receiver_open(data->context, &receiver_config, &data->receiver);
+ if (res) {
+ pw_log_error("failed to create roc receiver: %d", res);
+ return -EINVAL;
+ }
+
+ switch (data->fec_code) {
+ case ROC_FEC_ENCODING_DEFAULT:
+ case ROC_FEC_ENCODING_RS8M:
+ audio_proto = ROC_PROTO_RTP_RS8M_SOURCE;
+ repair_proto = ROC_PROTO_RS8M_REPAIR;
+ break;
+ case ROC_FEC_ENCODING_LDPC_STAIRCASE:
+ audio_proto = ROC_PROTO_RTP_LDPC_SOURCE;
+ repair_proto = ROC_PROTO_LDPC_REPAIR;
+ break;
+ default:
+ audio_proto = ROC_PROTO_RTP;
+ repair_proto = 0;
+ break;
+ }
+
+ res = pw_roc_create_endpoint(&data->local_source_addr, audio_proto, data->local_ip, data->local_source_port);
+ if (res < 0) {
+ pw_log_error("failed to create source endpoint: %s", spa_strerror(res));
+ return res;
+ }
+
+ if (roc_receiver_bind(data->receiver, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_SOURCE,
+ data->local_source_addr) != 0) {
+ pw_log_error("can't connect roc receiver to local source address");
+ return -EINVAL;
+ }
+
+ if (repair_proto != 0) {
+ res = pw_roc_create_endpoint(&data->local_repair_addr, repair_proto, data->local_ip, data->local_repair_port);
+ if (res < 0) {
+ pw_log_error("failed to create repair endpoint: %s", spa_strerror(res));
+ return res;
+ }
+
+ if (roc_receiver_bind(data->receiver, ROC_SLOT_DEFAULT, ROC_INTERFACE_AUDIO_REPAIR,
+ data->local_repair_addr) != 0) {
+ pw_log_error("can't connect roc receiver to local repair address");
+ return -EINVAL;
+ }
+ }
+
+ data->playback = pw_stream_new(data->core,
+ "roc-source playback", data->playback_props);
+ data->playback_props = NULL;
+ if (data->playback == NULL)
+ return -errno;
+
+ pw_stream_add_listener(data->playback,
+ &data->playback_listener,
+ &out_stream_events, data);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &info);
+
+ if ((res = pw_stream_connect(data->playback,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+ return 0;
+}
+
+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 "
+ "fec.code=<empty>|disable|rs8m|ldpc "
+ "sess.latency.msec=<target network latency in milliseconds> "
+ "local.ip=<local receiver ip> "
+ "local.source.port=<local receiver port for source packets> "
+ "local.repair.port=<local receiver port for repair packets> "
+ "source.props= { key=value ... }" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct module_roc_source_data *data;
+ struct pw_properties *props = NULL, *playback_props = NULL;
+ const char *str;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ data = calloc(1, sizeof(struct module_roc_source_data));
+ if (data == NULL)
+ return -errno;
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ data->props = props;
+
+ playback_props = pw_properties_new(NULL, NULL);
+ if (playback_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ data->playback_props = playback_props;
+
+ data->module = module;
+ data->module_context = context;
+
+ if ((str = pw_properties_get(props, "source.name")) != NULL) {
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, str);
+ pw_properties_set(props, "source.name", NULL);
+ }
+
+ if ((str = pw_properties_get(props, "source.props")) != NULL)
+ pw_properties_update_string(playback_props, str, strlen(str));
+
+ if (pw_properties_get(playback_props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_NAME, "roc-source");
+ if (pw_properties_get(playback_props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_DESCRIPTION, "ROC Source");
+ if (pw_properties_get(playback_props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(playback_props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(playback_props, PW_KEY_NODE_NETWORK, "true");
+
+ data->rate = pw_properties_get_uint32(playback_props, PW_KEY_AUDIO_RATE, data->rate);
+ if (data->rate == 0)
+ data->rate = PW_ROC_DEFAULT_RATE;
+
+ if ((str = pw_properties_get(props, "local.ip")) != NULL) {
+ data->local_ip = strdup(str);
+ pw_properties_set(props, "local.ip", NULL);
+ } else {
+ data->local_ip = strdup(PW_ROC_DEFAULT_IP);
+ }
+
+ if ((str = pw_properties_get(props, "local.source.port")) != NULL) {
+ data->local_source_port = pw_properties_parse_int(str);
+ pw_properties_set(props, "local.source.port", NULL);
+ } else {
+ data->local_source_port = PW_ROC_DEFAULT_SOURCE_PORT;
+ }
+
+ if ((str = pw_properties_get(props, "local.repair.port")) != NULL) {
+ data->local_repair_port = pw_properties_parse_int(str);
+ pw_properties_set(props, "local.repair.port", NULL);
+ } else {
+ data->local_repair_port = PW_ROC_DEFAULT_REPAIR_PORT;
+ }
+
+ if ((str = pw_properties_get(props, "sess.latency.msec")) != NULL) {
+ data->sess_latency_msec = pw_properties_parse_int(str);
+ pw_properties_set(props, "sess.latency.msec", NULL);
+ } else {
+ data->sess_latency_msec = PW_ROC_DEFAULT_SESS_LATENCY;
+ }
+
+ if ((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;
+ }
+ pw_properties_set(props, "resampler.profile", NULL);
+ } else {
+ data->resampler_profile = ROC_RESAMPLER_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);
+ data->fec_code = ROC_FEC_ENCODING_DEFAULT;
+ }
+ pw_properties_set(props, "fec.code", NULL);
+ } else {
+ data->fec_code = ROC_FEC_ENCODING_DEFAULT;
+ }
+
+ data->core = pw_context_get_object(data->module_context, PW_TYPE_INTERFACE_Core);
+ if (data->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ data->core = pw_context_connect(data->module_context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ data->do_disconnect = true;
+ }
+ if (data->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto out;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)data->core,
+ &data->core_proxy_listener,
+ &core_proxy_events, data);
+ pw_core_add_listener(data->core,
+ &data->core_listener,
+ &core_events, data);
+
+ if ((res = roc_source_setup(data)) < 0)
+ goto out;
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_roc_source_info));
+
+ pw_log_info("Successfully loaded module-roc-source");
+
+ return 0;
+out:
+ impl_destroy(data);
+ return res;
+}
diff --git a/src/modules/module-roc/common.h b/src/modules/module-roc/common.h
new file mode 100644
index 0000000..248c66e
--- /dev/null
+++ b/src/modules/module-roc/common.h
@@ -0,0 +1,71 @@
+#ifndef MODULE_ROC_COMMON_H
+#define MODULE_ROC_COMMON_H
+
+#include <roc/config.h>
+#include <roc/endpoint.h>
+
+#include <spa/utils/string.h>
+
+#define PW_ROC_DEFAULT_IP "0.0.0.0"
+#define PW_ROC_DEFAULT_SOURCE_PORT 10001
+#define PW_ROC_DEFAULT_REPAIR_PORT 10002
+#define PW_ROC_DEFAULT_SESS_LATENCY 200
+#define PW_ROC_DEFAULT_RATE 44100
+
+static inline int pw_roc_parse_fec_encoding(roc_fec_encoding *out, const char *str)
+{
+ if (!str || !*str)
+ *out = ROC_FEC_ENCODING_DEFAULT;
+ else if (spa_streq(str, "disable"))
+ *out = ROC_FEC_ENCODING_DISABLE;
+ else if (spa_streq(str, "rs8m"))
+ *out = ROC_FEC_ENCODING_RS8M;
+ else if (spa_streq(str, "ldpc"))
+ *out = ROC_FEC_ENCODING_LDPC_STAIRCASE;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static inline int pw_roc_parse_resampler_profile(roc_resampler_profile *out, const char *str)
+{
+ if (!str || !*str)
+ *out = ROC_RESAMPLER_PROFILE_DEFAULT;
+ else if (spa_streq(str, "disable"))
+ *out = ROC_RESAMPLER_PROFILE_DISABLE;
+ else if (spa_streq(str, "high"))
+ *out = ROC_RESAMPLER_PROFILE_HIGH;
+ else if (spa_streq(str, "medium"))
+ *out = ROC_RESAMPLER_PROFILE_MEDIUM;
+ else if (spa_streq(str, "low"))
+ *out = ROC_RESAMPLER_PROFILE_LOW;
+ 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;
+
+ if (roc_endpoint_allocate(&endpoint))
+ return -ENOMEM;
+
+ if (roc_endpoint_set_protocol(endpoint, protocol))
+ goto out_error_free_ep;
+
+ if (roc_endpoint_set_host(endpoint, ip))
+ goto out_error_free_ep;
+
+ if (roc_endpoint_set_port(endpoint, port))
+ goto out_error_free_ep;
+
+ *result = endpoint;
+ return 0;
+
+out_error_free_ep:
+ (void) roc_endpoint_deallocate(endpoint);
+ return -EINVAL;
+}
+
+#endif /* MODULE_ROC_COMMON_H */
diff --git a/src/modules/module-rt.c b/src/modules/module-rt.c
new file mode 100644
index 0000000..f6b4ade
--- /dev/null
+++ b/src/modules/module-rt.c
@@ -0,0 +1,1095 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+/***
+ Copyright 2009 Lennart Poettering
+ Copyright 2010 David Henningsson <diwic@ubuntu.com>
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+***/
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#include <sys/thr.h>
+#endif
+#include <fcntl.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <sys/resource.h>
+#include <sys/syscall.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/thread.h>
+
+#ifdef HAVE_DBUS
+#include <spa/support/dbus.h>
+#include <dbus/dbus.h>
+#endif
+
+/** \page page_module_rt PipeWire Module: RT
+ *
+ * The `rt` modules can give real-time priorities to processing threads.
+ *
+ * It uses the operating system's scheduler to enable realtime scheduling
+ * for certain threads to assist with low latency audio processing.
+ * This requires `RLIMIT_RTPRIO` to be set to a value that's equal to this
+ * module's `rt.prio` parameter or higher. Most distros will come with some
+ * package that configures this for certain groups or users. If this is not set
+ * up and DBus is available, then this module will fall back to using RTKit.
+ *
+ * ## Module Options
+ *
+ * - `nice.level`: The nice value set for the application thread. It improves
+ * performance of the communication with the pipewire daemon.
+ * - `rt.prio`: The realtime priority of the data thread. Higher values are
+ * higher priority.
+ * - `rt.time.soft`, `rt.time.hard`: The amount of CPU time an RT thread can
+ * consume without doing any blocking calls before the kernel kills
+ * the thread. This is a safety measure to avoid lockups of the complete
+ * system when some thread consumes 100%.
+
+ * The nice level is by default set to an invalid value so that clients don't
+ * automatically have the nice level raised.
+ *
+ * The PipeWire server processes are explicitly configured with a valid nice level.
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-rt
+ * args = {
+ * #nice.level = 20
+ * #rt.prio = 88
+ * #rt.time.soft = -1
+ * #rt.time.hard = -1
+ * }
+ * flags = [ ifexists nofail ]
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "rt"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define REALTIME_POLICY SCHED_FIFO
+#ifdef SCHED_RESET_ON_FORK
+#define PW_SCHED_RESET_ON_FORK SCHED_RESET_ON_FORK
+#else
+/* FreeBSD compat */
+#define PW_SCHED_RESET_ON_FORK 0
+#endif
+
+#define IS_VALID_NICE_LEVEL(l) ((l)>=-20 && (l)<=19)
+
+#define DEFAULT_NICE_LEVEL 20
+#define DEFAULT_RT_PRIO_MIN 11
+#define DEFAULT_RT_PRIO 88
+#define DEFAULT_RT_TIME_SOFT -1
+#define DEFAULT_RT_TIME_HARD -1
+
+#define MODULE_USAGE "[nice.level=<priority: default "SPA_STRINGIFY(DEFAULT_NICE_LEVEL)"(don't change)>] " \
+ "[rt.prio=<priority: default "SPA_STRINGIFY(DEFAULT_RT_PRIO)">] " \
+ "[rt.time.soft=<in usec: default "SPA_STRINGIFY(DEFAULT_RT_TIME_SOFT)"] " \
+ "[rt.time.hard=<in usec: default "SPA_STRINGIFY(DEFAULT_RT_TIME_HARD)"] "
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Use realtime thread scheduling, falling back to RTKit" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#ifdef HAVE_DBUS
+#define RTKIT_SERVICE_NAME "org.freedesktop.RealtimeKit1"
+#define RTKIT_OBJECT_PATH "/org/freedesktop/RealtimeKit1"
+#define RTKIT_INTERFACE "org.freedesktop.RealtimeKit1"
+
+#define XDG_PORTAL_SERVICE_NAME "org.freedesktop.portal.Desktop"
+#define XDG_PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop"
+#define XDG_PORTAL_INTERFACE "org.freedesktop.portal.Realtime"
+
+/** \cond */
+struct pw_rtkit_bus {
+ DBusConnection *bus;
+};
+/** \endcond */
+
+struct thread {
+ struct impl *impl;
+ struct spa_list link;
+ pthread_t thread;
+ pid_t pid;
+ void *(*start)(void*);
+ void *arg;
+};
+#endif /* HAVE_DBUS */
+
+struct impl {
+ struct pw_context *context;
+
+ struct spa_thread_utils thread_utils;
+
+ int nice_level;
+ int rt_prio;
+ rlim_t rt_time_soft;
+ rlim_t rt_time_hard;
+
+ struct spa_hook module_listener;
+
+#ifdef HAVE_DBUS
+ bool use_rtkit;
+ /* For D-Bus. These are const static. */
+ const char* service_name;
+ const char* object_path;
+ const char* interface;
+ struct pw_rtkit_bus *rtkit_bus;
+
+ /* These are only for the RTKit implementation to fill in the `thread`
+ * struct. Since there's barely any overhead here we'll do this
+ * regardless of which backend is used. */
+ pthread_mutex_t lock;
+ pthread_cond_t cond;
+ struct spa_list threads_list;
+#endif
+};
+
+#ifndef RLIMIT_RTTIME
+#define RLIMIT_RTTIME 15
+#endif
+
+static pid_t _gettid(void)
+{
+#if defined(HAVE_GETTID)
+ return (pid_t) gettid();
+#elif defined(__linux__)
+ return syscall(SYS_gettid);
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
+ long pid;
+ thr_self(&pid);
+ return (pid_t)pid;
+#else
+#error "No gettid impl"
+#endif
+}
+
+#ifdef HAVE_DBUS
+struct pw_rtkit_bus *pw_rtkit_bus_get(DBusBusType bus_type)
+{
+ struct pw_rtkit_bus *bus;
+ DBusError error;
+
+ if (getenv("DISABLE_RTKIT")) {
+ errno = ENOTSUP;
+ return NULL;
+ }
+
+ dbus_error_init(&error);
+
+ bus = calloc(1, sizeof(struct pw_rtkit_bus));
+ if (bus == NULL)
+ return NULL;
+
+ bus->bus = dbus_bus_get_private(bus_type, &error);
+ if (bus->bus == NULL)
+ goto error;
+
+ dbus_connection_set_exit_on_disconnect(bus->bus, false);
+
+ return bus;
+
+error:
+ free(bus);
+ pw_log_error("Failed to connect to %s bus: %s",
+ bus_type == DBUS_BUS_SYSTEM ? "system" : "session", error.message);
+ dbus_error_free(&error);
+ errno = ECONNREFUSED;
+ return NULL;
+}
+
+struct pw_rtkit_bus *pw_rtkit_bus_get_system(void)
+{
+ return pw_rtkit_bus_get(DBUS_BUS_SYSTEM);
+}
+
+struct pw_rtkit_bus *pw_rtkit_bus_get_session(void)
+{
+ return pw_rtkit_bus_get(DBUS_BUS_SESSION);
+}
+
+bool pw_rtkit_check_xdg_portal(struct pw_rtkit_bus *system_bus)
+{
+ if (!dbus_bus_name_has_owner(system_bus->bus, XDG_PORTAL_SERVICE_NAME, NULL)) {
+ pw_log_warn("Can't find %s. Is xdg-desktop-portal running?", XDG_PORTAL_SERVICE_NAME);
+ return false;
+ }
+
+ return true;
+}
+
+void pw_rtkit_bus_free(struct pw_rtkit_bus *system_bus)
+{
+ dbus_connection_close(system_bus->bus);
+ dbus_connection_unref(system_bus->bus);
+ free(system_bus);
+}
+
+static int translate_error(const char *name)
+{
+ pw_log_warn("RTKit error: %s", name);
+
+ if (spa_streq(name, DBUS_ERROR_NO_MEMORY))
+ return -ENOMEM;
+ if (spa_streq(name, DBUS_ERROR_SERVICE_UNKNOWN) ||
+ spa_streq(name, DBUS_ERROR_NAME_HAS_NO_OWNER))
+ return -ENOENT;
+ if (spa_streq(name, DBUS_ERROR_ACCESS_DENIED) ||
+ spa_streq(name, DBUS_ERROR_AUTH_FAILED))
+ return -EACCES;
+
+ return -EIO;
+}
+
+static long long rtkit_get_int_property(struct impl *impl, const char *propname,
+ long long *propval)
+{
+ DBusMessage *m = NULL, *r = NULL;
+ DBusMessageIter iter, subiter;
+ dbus_int64_t i64;
+ dbus_int32_t i32;
+ DBusError error;
+ int current_type;
+ long long ret;
+ struct pw_rtkit_bus *connection = impl->rtkit_bus;
+
+ dbus_error_init(&error);
+
+ if (!(m = dbus_message_new_method_call(impl->service_name,
+ impl->object_path,
+ "org.freedesktop.DBus.Properties", "Get"))) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ if (!dbus_message_append_args(m,
+ DBUS_TYPE_STRING, &impl->interface,
+ DBUS_TYPE_STRING, &propname, DBUS_TYPE_INVALID)) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ if (!(r = dbus_connection_send_with_reply_and_block(connection->bus, m, -1, &error))) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+ if (dbus_set_error_from_message(&error, r)) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+ ret = -EBADMSG;
+ dbus_message_iter_init(r, &iter);
+ while ((current_type = dbus_message_iter_get_arg_type(&iter)) != DBUS_TYPE_INVALID) {
+
+ if (current_type == DBUS_TYPE_VARIANT) {
+ dbus_message_iter_recurse(&iter, &subiter);
+
+ while ((current_type =
+ dbus_message_iter_get_arg_type(&subiter)) != DBUS_TYPE_INVALID) {
+
+ if (current_type == DBUS_TYPE_INT32) {
+ dbus_message_iter_get_basic(&subiter, &i32);
+ *propval = i32;
+ ret = 0;
+ }
+
+ if (current_type == DBUS_TYPE_INT64) {
+ dbus_message_iter_get_basic(&subiter, &i64);
+ *propval = i64;
+ ret = 0;
+ }
+
+ dbus_message_iter_next(&subiter);
+ }
+ }
+ dbus_message_iter_next(&iter);
+ }
+
+finish:
+
+ if (m)
+ dbus_message_unref(m);
+
+ if (r)
+ dbus_message_unref(r);
+
+ dbus_error_free(&error);
+
+ return ret;
+}
+
+int pw_rtkit_get_max_realtime_priority(struct impl *impl)
+{
+ long long retval;
+ int err;
+
+ err = rtkit_get_int_property(impl, "MaxRealtimePriority", &retval);
+ return err < 0 ? err : retval;
+}
+
+int pw_rtkit_get_min_nice_level(struct impl *impl, int *min_nice_level)
+{
+ long long retval;
+ int err;
+
+ err = rtkit_get_int_property(impl, "MinNiceLevel", &retval);
+ if (err >= 0)
+ *min_nice_level = retval;
+ return err;
+}
+
+long long pw_rtkit_get_rttime_usec_max(struct impl *impl)
+{
+ long long retval;
+ int err;
+
+ err = rtkit_get_int_property(impl, "RTTimeUSecMax", &retval);
+ return err < 0 ? err : retval;
+}
+
+int pw_rtkit_make_realtime(struct impl *impl, pid_t thread, int priority)
+{
+ DBusMessage *m = NULL, *r = NULL;
+ dbus_uint64_t pid;
+ dbus_uint64_t u64;
+ dbus_uint32_t u32;
+ DBusError error;
+ int ret;
+ struct pw_rtkit_bus *connection = impl->rtkit_bus;
+
+ dbus_error_init(&error);
+
+ if (thread == 0)
+ thread = _gettid();
+
+ if (!(m = dbus_message_new_method_call(impl->service_name,
+ impl->object_path, impl->interface,
+ "MakeThreadRealtimeWithPID"))) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ pid = (dbus_uint64_t) getpid();
+ u64 = (dbus_uint64_t) thread;
+ u32 = (dbus_uint32_t) priority;
+
+ if (!dbus_message_append_args(m,
+ DBUS_TYPE_UINT64, &pid,
+ DBUS_TYPE_UINT64, &u64,
+ DBUS_TYPE_UINT32, &u32, DBUS_TYPE_INVALID)) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ if (!(r = dbus_connection_send_with_reply_and_block(connection->bus, m, -1, &error))) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+
+ if (dbus_set_error_from_message(&error, r)) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+ ret = 0;
+
+finish:
+
+ if (m)
+ dbus_message_unref(m);
+
+ if (r)
+ dbus_message_unref(r);
+
+ dbus_error_free(&error);
+
+ return ret;
+}
+
+int pw_rtkit_make_high_priority(struct impl *impl, pid_t thread, int nice_level)
+{
+ DBusMessage *m = NULL, *r = NULL;
+ dbus_uint64_t pid;
+ dbus_uint64_t u64;
+ dbus_int32_t s32;
+ DBusError error;
+ int ret;
+ struct pw_rtkit_bus *connection = impl->rtkit_bus;
+
+ dbus_error_init(&error);
+
+ if (thread == 0)
+ thread = _gettid();
+
+ if (!(m = dbus_message_new_method_call(impl->service_name,
+ impl->object_path, impl->interface,
+ "MakeThreadHighPriorityWithPID"))) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+ pid = (dbus_uint64_t) getpid();
+ u64 = (dbus_uint64_t) thread;
+ s32 = (dbus_int32_t) nice_level;
+
+ if (!dbus_message_append_args(m,
+ DBUS_TYPE_UINT64, &pid,
+ DBUS_TYPE_UINT64, &u64,
+ DBUS_TYPE_INT32, &s32, DBUS_TYPE_INVALID)) {
+ ret = -ENOMEM;
+ goto finish;
+ }
+
+
+
+ if (!(r = dbus_connection_send_with_reply_and_block(connection->bus, m, -1, &error))) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+
+ if (dbus_set_error_from_message(&error, r)) {
+ ret = translate_error(error.name);
+ goto finish;
+ }
+
+ ret = 0;
+
+finish:
+
+ if (m)
+ dbus_message_unref(m);
+
+ if (r)
+ dbus_message_unref(r);
+
+ dbus_error_free(&error);
+
+ return ret;
+}
+#endif /* HAVE_DBUS */
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ pw_context_set_object(impl->context, SPA_TYPE_INTERFACE_ThreadUtils, NULL);
+ spa_hook_remove(&impl->module_listener);
+
+#ifdef HAVE_DBUS
+ if (impl->rtkit_bus)
+ pw_rtkit_bus_free(impl->rtkit_bus);
+#endif
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static int get_rt_priority_range(int *out_min, int *out_max)
+{
+ int min, max;
+
+ if ((min = sched_get_priority_min(REALTIME_POLICY)) < 0)
+ return -errno;
+ if ((max = sched_get_priority_max(REALTIME_POLICY)) < 0)
+ return -errno;
+
+ if (out_min)
+ *out_min = min;
+ if (out_max)
+ *out_max = max;
+
+ return 0;
+}
+/**
+ * Check if the current user has permissions to use realtime scheduling at the
+ * specified priority.
+ */
+static bool check_realtime_privileges(struct impl *impl)
+{
+ rlim_t priority = impl->rt_prio;
+ int err, old_policy, new_policy, min, max;
+ struct sched_param old_sched_params;
+ struct sched_param new_sched_params;
+ int try = 0;
+
+ while (try++ < 2) {
+ /* We could check `RLIMIT_RTPRIO`, but the BSDs generally don't have
+ * that available, and there are also other ways to use realtime
+ * scheduling without that rlimit being set such as `CAP_SYS_NICE` or
+ * running as root. Instead of checking a bunch of preconditions, we
+ * just try if setting realtime scheduling works or not. */
+ if ((err = pthread_getschedparam(pthread_self(), &old_policy, &old_sched_params)) != 0) {
+ pw_log_warn("Failed to check RLIMIT_RTPRIO: %s", strerror(err));
+ return false;
+ }
+ if ((err = get_rt_priority_range(&min, &max)) < 0) {
+ pw_log_warn("Failed to get priority range: %s", strerror(err));
+ return false;
+ }
+ if (try == 2) {
+ struct rlimit rlim;
+ /* second try, try to clamp to RLIMIT_RTPRIO */
+ if (getrlimit(RLIMIT_RTPRIO, &rlim) == 0 && max > (int)rlim.rlim_max) {
+ pw_log_info("Clamp rtprio %d to %d", (int)priority, (int)rlim.rlim_max);
+ max = (int)rlim.rlim_max;
+ }
+ else
+ break;
+ }
+ if (max < DEFAULT_RT_PRIO_MIN) {
+ pw_log_info("Priority max (%d) must be at least %d", max, DEFAULT_RT_PRIO_MIN);
+ return false;
+ }
+
+ /* If the current scheduling policy has `SCHED_RESET_ON_FORK` set, then
+ * this also needs to be set here or `pthread_setschedparam()` will return
+ * an error code. Similarly, if it is not set, then we don't want to set
+ * it here as it would irreversible change the current thread's
+ * scheduling policy. */
+ spa_zero(new_sched_params);
+ new_sched_params.sched_priority = SPA_CLAMP((int)priority, min, max);
+ new_policy = REALTIME_POLICY;
+ if ((old_policy & PW_SCHED_RESET_ON_FORK) != 0)
+ new_policy |= PW_SCHED_RESET_ON_FORK;
+
+ if (pthread_setschedparam(pthread_self(), new_policy, &new_sched_params) == 0) {
+ impl->rt_prio = new_sched_params.sched_priority;
+ pthread_setschedparam(pthread_self(), old_policy, &old_sched_params);
+ return true;
+ }
+ }
+ pw_log_info("Can't set rt prio to %d: %m (try increasing rlimits)", (int)priority);
+ return false;
+}
+
+static int sched_set_nice(int nice_level)
+{
+ if (setpriority(PRIO_PROCESS, _gettid(), nice_level) == 0)
+ return 0;
+ else
+ return -errno;
+}
+
+static int set_nice(struct impl *impl, int nice_level, bool warn)
+{
+ int res = 0;
+
+#ifdef HAVE_DBUS
+ if (impl->use_rtkit)
+ res = pw_rtkit_make_high_priority(impl, 0, nice_level);
+ else
+ res = sched_set_nice(nice_level);
+#else
+ res = sched_set_nice(nice_level);
+#endif
+
+ if (res < 0) {
+ if (warn)
+ pw_log_warn("could not set nice-level to %d: %s",
+ nice_level, spa_strerror(res));
+ } else {
+ pw_log_info("main thread nice level set to %d",
+ nice_level);
+ }
+ return res;
+}
+
+static int set_rlimit(struct impl *impl)
+{
+ struct rlimit rl;
+ int res = 0;
+
+ spa_zero(rl);
+ rl.rlim_cur = impl->rt_time_soft;
+ rl.rlim_max = impl->rt_time_hard;
+
+#ifdef HAVE_DBUS
+ if (impl->use_rtkit) {
+ long long rttime;
+ rttime = pw_rtkit_get_rttime_usec_max(impl);
+ if (rttime >= 0) {
+ if ((rlim_t)rttime < rl.rlim_cur) {
+ pw_log_debug("clamping rt.time.soft from %llu to %lld because of RTKit",
+ (long long)rl.rlim_cur, rttime);
+ }
+
+ rl.rlim_cur = SPA_MIN(rl.rlim_cur, (rlim_t)rttime);
+ rl.rlim_max = SPA_MIN(rl.rlim_max, (rlim_t)rttime);
+ }
+ }
+#endif
+
+ if (setrlimit(RLIMIT_RTTIME, &rl) < 0)
+ res = -errno;
+
+ if (res < 0)
+ pw_log_debug("setrlimit() failed: %s", spa_strerror(res));
+ else
+ pw_log_debug("rt.time.soft:%"PRIi64" rt.time.hard:%"PRIi64,
+ (int64_t)rl.rlim_cur, (int64_t)rl.rlim_max);
+
+ return res;
+}
+
+static int acquire_rt_sched(struct spa_thread *thread, int priority)
+{
+ int err, min, max;
+ struct sched_param sp;
+ pthread_t pt = (pthread_t)thread;
+
+ if ((err = get_rt_priority_range(&min, &max)) < 0)
+ return err;
+
+ if (priority < min || priority > max) {
+ pw_log_info("clamping priority %d to range %d - %d for policy %d",
+ priority, min, max, REALTIME_POLICY);
+ priority = SPA_CLAMP(priority, min, max);
+ }
+
+ spa_zero(sp);
+ sp.sched_priority = priority;
+ if ((err = pthread_setschedparam(pt, REALTIME_POLICY | PW_SCHED_RESET_ON_FORK, &sp)) != 0) {
+ pw_log_warn("could not make thread %p realtime: %s", thread, strerror(err));
+ return -err;
+ }
+
+ pw_log_info("acquired realtime priority %d for thread %p", priority, thread);
+ return 0;
+}
+
+static int impl_drop_rt_generic(void *object, struct spa_thread *thread)
+{
+ struct sched_param sp;
+ pthread_t pt = (pthread_t)thread;
+ int err;
+
+ spa_zero(sp);
+ if ((err = pthread_setschedparam(pt, SCHED_OTHER | PW_SCHED_RESET_ON_FORK, &sp)) != 0) {
+ pw_log_debug("thread %p: SCHED_OTHER|SCHED_RESET_ON_FORK failed: %s",
+ thread, strerror(err));
+ return -err;
+ }
+ pw_log_info("thread %p dropped realtime priority", thread);
+ return 0;
+}
+
+#ifdef HAVE_DBUS
+static struct thread *find_thread_by_pt(struct impl *impl, pthread_t pt)
+{
+ struct thread *t;
+
+ spa_list_for_each(t, &impl->threads_list, link) {
+ if (pthread_equal(t->thread, pt))
+ return t;
+ }
+ return NULL;
+}
+
+static void *custom_start(void *data)
+{
+ struct thread *this = data;
+ struct impl *impl = this->impl;
+
+ pthread_mutex_lock(&impl->lock);
+ this->pid = _gettid();
+ pthread_cond_broadcast(&impl->cond);
+ pthread_mutex_unlock(&impl->lock);
+
+ return this->start(this->arg);
+}
+
+static struct spa_thread *impl_create(void *object, const struct spa_dict *props,
+ void *(*start_routine)(void*), void *arg)
+{
+ struct impl *impl = object;
+ struct thread *this;
+ struct spa_thread *thread;
+
+ this = calloc(1, sizeof(*this));
+ this->impl = impl;
+ this->start = start_routine;
+ this->arg = arg;
+
+ /* This thread list is only used for the RTKit implementation */
+ pthread_mutex_lock(&impl->lock);
+ thread = pw_thread_utils_create(props, custom_start, this);
+ if (thread == NULL)
+ goto exit;
+
+ this->thread = (pthread_t)thread;
+ pthread_cond_wait(&impl->cond, &impl->lock);
+
+ spa_list_append(&impl->threads_list, &this->link);
+exit:
+ pthread_mutex_unlock(&impl->lock);
+
+ if (thread == NULL) {
+ free(this);
+ return NULL;
+ }
+ return thread;
+}
+
+static int impl_join(void *object, struct spa_thread *thread, void **retval)
+{
+ struct impl *impl = object;
+ pthread_t pt = (pthread_t)thread;
+ struct thread *thr;
+
+ pthread_mutex_lock(&impl->lock);
+ if ((thr = find_thread_by_pt(impl, pt)) != NULL) {
+ spa_list_remove(&thr->link);
+ free(thr);
+ }
+ pthread_mutex_unlock(&impl->lock);
+
+ return pthread_join(pt, retval);
+}
+
+
+static int get_rtkit_priority_range(struct impl *impl, int *min, int *max)
+{
+ if (min)
+ *min = 1;
+ if (max) {
+ if ((*max = pw_rtkit_get_max_realtime_priority(impl)) < 0)
+ return *max;
+ if (*max < 1)
+ *max = 1;
+ }
+ return 0;
+}
+
+static int impl_get_rt_range(void *object, const struct spa_dict *props,
+ int *min, int *max)
+{
+ struct impl *impl = object;
+ int res;
+ if (impl->use_rtkit)
+ res = get_rtkit_priority_range(impl, min, max);
+ else
+ res = get_rt_priority_range(min, max);
+ return res;
+}
+
+static pid_t impl_gettid(struct impl *impl, pthread_t pt)
+{
+ struct thread *thr;
+ pid_t pid;
+
+ pthread_mutex_lock(&impl->lock);
+ if ((thr = find_thread_by_pt(impl, pt)) != NULL)
+ pid = thr->pid;
+ else
+ pid = _gettid();
+ pthread_mutex_unlock(&impl->lock);
+
+ return pid;
+}
+
+static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority)
+{
+ struct impl *impl = object;
+ struct sched_param sp;
+ int err;
+ pthread_t pt = (pthread_t)thread;
+ pid_t pid;
+
+ /* See the docstring on `spa_thread_utils_methods::acquire_rt` */
+ if (priority == -1) {
+ priority = impl->rt_prio;
+ }
+
+ if (impl->use_rtkit) {
+ int min, max;
+
+ if ((err = get_rtkit_priority_range(impl, &min, &max)) < 0)
+ return err;
+
+ pid = impl_gettid(impl, pt);
+
+ if (priority < min || priority > max) {
+ pw_log_info("clamping requested priority %d for thread %d "
+ "between %d and %d", priority, pid, min, max);
+ priority = SPA_CLAMP(priority, min, max);
+ }
+
+ spa_zero(sp);
+ sp.sched_priority = priority;
+
+ if (pthread_setschedparam(pt, SCHED_OTHER | PW_SCHED_RESET_ON_FORK, &sp) == 0) {
+ pw_log_debug("SCHED_OTHER|SCHED_RESET_ON_FORK worked.");
+ }
+
+ if ((err = pw_rtkit_make_realtime(impl, pid, priority)) < 0) {
+ pw_log_warn("could not make thread %d realtime using RTKit: %s", pid, spa_strerror(err));
+ return err;
+ }
+
+ pw_log_info("acquired realtime priority %d for thread %d using RTKit", priority, pid);
+ return 0;
+ } else {
+ return acquire_rt_sched(thread, priority);
+ }
+}
+
+static const struct spa_thread_utils_methods impl_thread_utils = {
+ SPA_VERSION_THREAD_UTILS_METHODS,
+ .create = impl_create,
+ .join = impl_join,
+ .get_rt_range = impl_get_rt_range,
+ .acquire_rt = impl_acquire_rt,
+ .drop_rt = impl_drop_rt_generic,
+};
+
+#else /* HAVE_DBUS */
+
+static struct spa_thread *impl_create(void *object, const struct spa_dict *props,
+ void *(*start_routine)(void*), void *arg)
+{
+ return pw_thread_utils_create(props, start_routine, arg);
+}
+
+static int impl_join(void *object, struct spa_thread *thread, void **retval)
+{
+ return pw_thread_utils_join(thread, retval);
+}
+
+static int impl_get_rt_range(void *object, const struct spa_dict *props,
+ int *min, int *max)
+{
+ return get_rt_priority_range(min, max);
+}
+
+static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority)
+{
+ struct impl *impl = object;
+
+ /* See the docstring on `spa_thread_utils_methods::acquire_rt` */
+ if (priority == -1)
+ priority = impl->rt_prio;
+
+ return acquire_rt_sched(thread, priority);
+}
+
+static const struct spa_thread_utils_methods impl_thread_utils = {
+ SPA_VERSION_THREAD_UTILS_METHODS,
+ .create = impl_create,
+ .join = impl_join,
+ .get_rt_range = impl_get_rt_range,
+ .acquire_rt = impl_acquire_rt,
+ .drop_rt = impl_drop_rt_generic,
+};
+#endif /* HAVE_DBUS */
+
+
+#ifdef HAVE_DBUS
+static int check_rtkit(struct impl *impl, struct pw_context *context, bool *can_use_rtkit)
+{
+ const struct pw_properties *context_props;
+ const char *str;
+
+ *can_use_rtkit = true;
+
+ if ((context_props = pw_context_get_properties(context)) != NULL &&
+ (str = pw_properties_get(context_props, "support.dbus")) != NULL &&
+ !pw_properties_parse_bool(str))
+ *can_use_rtkit = false;
+
+ return 0;
+}
+#endif /* HAVE_DBUS */
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ struct pw_properties *props;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -ENOMEM;
+
+ pw_log_debug("module %p: new", impl);
+
+ props = args ? pw_properties_new_string(args) : pw_properties_new(NULL, NULL);
+ if (!props) {
+ res = -errno;
+ goto error;
+ }
+
+ impl->context = context;
+ impl->nice_level = pw_properties_get_int32(props, "nice.level", DEFAULT_NICE_LEVEL);
+ impl->rt_prio = pw_properties_get_int32(props, "rt.prio", DEFAULT_RT_PRIO);
+ impl->rt_time_soft = pw_properties_get_int32(props, "rt.time.soft", DEFAULT_RT_TIME_SOFT);
+ impl->rt_time_hard = pw_properties_get_int32(props, "rt.time.hard", DEFAULT_RT_TIME_HARD);
+
+ bool can_use_rtkit = false, use_rtkit = false;
+
+#ifdef HAVE_DBUS
+ spa_list_init(&impl->threads_list);
+ pthread_mutex_init(&impl->lock, NULL);
+ pthread_cond_init(&impl->cond, NULL);
+
+ if ((res = check_rtkit(impl, context, &can_use_rtkit)) < 0)
+ goto error;
+#endif
+ /* If the user has permissions to use regular realtime scheduling, as well as
+ * the nice level we want, then we'll use that instead of RTKit */
+ if (!check_realtime_privileges(impl)) {
+ if (!can_use_rtkit) {
+ res = -ENOTSUP;
+ pw_log_warn("regular realtime scheduling not available (RTKit fallback disabled)");
+ goto error;
+ }
+ use_rtkit = true;
+ }
+
+ if (IS_VALID_NICE_LEVEL(impl->nice_level)) {
+ if (set_nice(impl, impl->nice_level, !can_use_rtkit) < 0)
+ use_rtkit = can_use_rtkit;
+ }
+
+#ifdef HAVE_DBUS
+ impl->use_rtkit = use_rtkit;
+ if (impl->use_rtkit) {
+ /* Checking xdg-desktop-portal. It works fine in all situations. */
+ impl->rtkit_bus = pw_rtkit_bus_get_session();
+ if (impl->rtkit_bus != NULL) {
+ if (pw_rtkit_check_xdg_portal(impl->rtkit_bus)) {
+ impl->service_name = XDG_PORTAL_SERVICE_NAME;
+ impl->object_path = XDG_PORTAL_OBJECT_PATH;
+ impl->interface = XDG_PORTAL_INTERFACE;
+ } else {
+ pw_log_warn("found session bus but no portal");
+ pw_rtkit_bus_free(impl->rtkit_bus);
+ impl->rtkit_bus = NULL;
+ }
+ }
+ /* Failed to get xdg-desktop-portal, try to use rtkit. */
+ if (impl->rtkit_bus == NULL) {
+ impl->rtkit_bus = pw_rtkit_bus_get_system();
+ if (impl->rtkit_bus != NULL) {
+ impl->service_name = RTKIT_SERVICE_NAME;
+ impl->object_path = RTKIT_OBJECT_PATH;
+ impl->interface = RTKIT_INTERFACE;
+ } else {
+ res = -errno;
+ pw_log_warn("could not get system bus: %m");
+ goto error;
+ }
+ }
+ /* Retry set_nice with rtkit */
+ if (IS_VALID_NICE_LEVEL(impl->nice_level))
+ set_nice(impl, impl->nice_level, true);
+ }
+#endif
+ set_rlimit(impl);
+
+ impl->thread_utils.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_ThreadUtils,
+ SPA_VERSION_THREAD_UTILS,
+ &impl_thread_utils, impl);
+
+ pw_context_set_object(context, SPA_TYPE_INTERFACE_ThreadUtils,
+ &impl->thread_utils);
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+ pw_impl_module_update_properties(module, &props->dict);
+
+#ifdef HAVE_DBUS
+ if (impl->use_rtkit) {
+ pw_log_debug("initialized using RTKit");
+ } else {
+ pw_log_debug("initialized using regular realtime scheduling");
+ }
+#else
+ pw_log_debug("initialized using regular realtime scheduling");
+#endif
+
+ goto done;
+
+error:
+#ifdef HAVE_DBUS
+ if (impl->rtkit_bus)
+ pw_rtkit_bus_free(impl->rtkit_bus);
+#endif
+ free(impl);
+done:
+ pw_properties_free(props);
+
+ return res;
+}
diff --git a/src/modules/module-rtp-sink.c b/src/modules/module-rtp-sink.c
new file mode 100644
index 0000000..b0c622d
--- /dev/null
+++ b/src/modules/module-rtp-sink.c
@@ -0,0 +1,975 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <limits.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+#include <netinet/ip.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <ctype.h>
+
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#include <module-rtp/sap.h>
+#include <module-rtp/rtp.h>
+
+
+/** \page page_module_rtp_sink PipeWire Module: RTP sink
+ *
+ * The `rtp-sink` module creates a PipeWire sink that sends audio
+ * RTP packets.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `sap.ip = <str>`: IP address of the SAP messages, default "224.0.0.56"
+ * - `sap.port = <int>`: port of the SAP messages, default 9875
+ * - `source.ip =<str>`: source IP address, default "0.0.0.0"
+ * - `destination.ip =<str>`: destination IP address, default "224.0.0.56"
+ * - `destination.port =<int>`: destination port, default random beteen 46000 and 47024
+ * - `local.ifname = <str>`: interface name to use
+ * - `net.mtu = <int>`: MTU to use, default 1280
+ * - `net.ttl = <int>`: TTL to use, default 1
+ * - `net.loop = <bool>`: loopback multicast, default false
+ * - `sess.min-ptime = <int>`: minimum packet time in milliseconds, default 2
+ * - `sess.max-ptime = <int>`: maximum packet time in milliseconds, default 20
+ * - `sess.name = <str>`: a session name
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_REMOTE_NAME
+ * - \ref PW_KEY_AUDIO_FORMAT
+ * - \ref PW_KEY_AUDIO_RATE
+ * - \ref PW_KEY_AUDIO_CHANNELS
+ * - \ref SPA_KEY_AUDIO_POSITION
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_NODE_GROUP
+ * - \ref PW_KEY_NODE_LATENCY
+ * - \ref PW_KEY_NODE_VIRTUAL
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-rtp-sink
+ * args = {
+ * #sap.ip = "224.0.0.56"
+ * #sap.port = 9875
+ * #source.ip = "0.0.0.0"
+ * #destination.ip = "224.0.0.56"
+ * #destination.port = 46000
+ * #local.ifname = "eth0"
+ * #net.mtu = 1280
+ * #net.ttl = 1
+ * #net.loop = false
+ * #sess.min-ptime = 2
+ * #sess.max-ptime = 20
+ * #sess.name = "PipeWire RTP stream"
+ * #audio.format = "S16BE"
+ * #audio.rate = 48000
+ * #audio.channels = 2
+ * #audio.position = [ FL FR ]
+ * stream.props = {
+ * node.name = "rtp-sink"
+ * }
+ * }
+ *}
+ *]
+ *\endcode
+ *
+ * \since 0.3.60
+ */
+
+#define NAME "rtp-sink"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define SAP_INTERVAL_SEC 5
+#define SAP_MIME_TYPE "application/sdp"
+
+#define BUFFER_SIZE (1u<<20)
+#define BUFFER_MASK (BUFFER_SIZE-1)
+
+#define DEFAULT_SAP_IP "224.0.0.56"
+#define DEFAULT_SAP_PORT 9875
+
+#define DEFAULT_FORMAT "S16BE"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_POSITION "[ FL FR ]"
+
+#define DEFAULT_PORT 46000
+#define DEFAULT_SOURCE_IP "0.0.0.0"
+#define DEFAULT_DESTINATION_IP "224.0.0.56"
+#define DEFAULT_TTL 1
+#define DEFAULT_MTU 1280
+#define DEFAULT_LOOP false
+
+#define DEFAULT_MIN_PTIME 2
+#define DEFAULT_MAX_PTIME 20
+
+#define USAGE "sap.ip=<SAP IP address to send announce, default:"DEFAULT_SAP_IP"> " \
+ "sap.port=<SAP port to send on, default:"SPA_STRINGIFY(DEFAULT_SAP_PORT)"> " \
+ "source.ip=<source IP address, default:"DEFAULT_SOURCE_IP"> " \
+ "destination.ip=<destination IP address, default:"DEFAULT_DESTINATION_IP"> " \
+ "local.ifname=<local interface name to use> " \
+ "net.mtu=<desired MTU, default:"SPA_STRINGIFY(DEFAULT_MTU)"> " \
+ "net.ttl=<desired TTL, default:"SPA_STRINGIFY(DEFAULT_TTL)"> " \
+ "net.loop=<desired loopback, default:"SPA_STRINGIFY(DEFAULT_LOOP)"> " \
+ "sess.name=<a name for the session> " \
+ "sess.min-ptime=<minimum packet time in milliseconds, default:2> " \
+ "sess.max-ptime=<maximum packet time in milliseconds, default:20> " \
+ "audio.format=<format, default:"DEFAULT_FORMAT"> " \
+ "audio.rate=<sample rate, default:"SPA_STRINGIFY(DEFAULT_RATE)"> " \
+ "audio.channels=<number of channels, default:"SPA_STRINGIFY(DEFAULT_CHANNELS)"> "\
+ "audio.position=<channel map, default:"DEFAULT_POSITION"> " \
+ "stream.props= { key=value ... }"
+
+static const struct spa_dict_item module_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "RTP Sink" },
+ { PW_KEY_MODULE_USAGE, USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+static const struct format_info {
+ uint32_t format;
+ uint32_t size;
+ const char *mime;
+} format_info[] = {
+ { SPA_AUDIO_FORMAT_U8, 1, "L8" },
+ { SPA_AUDIO_FORMAT_ALAW, 1, "PCMA" },
+ { SPA_AUDIO_FORMAT_ULAW, 1, "PCMU" },
+ { SPA_AUDIO_FORMAT_S16_BE, 2, "L16" },
+ { SPA_AUDIO_FORMAT_S24_BE, 3, "L24" },
+};
+
+static const struct format_info *find_format_info(uint32_t format)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, f)
+ if (f->format == format)
+ return f;
+ return NULL;
+}
+
+struct impl {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_properties *props;
+ struct pw_context *module_context;
+
+ struct pw_loop *loop;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+
+ struct spa_source *timer;
+
+ struct pw_properties *stream_props;
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ unsigned int do_disconnect:1;
+
+ char *ifname;
+ char *session_name;
+ int sess_latency_msec;
+ int mtu;
+ bool ttl;
+ bool mcast_loop;
+ uint32_t min_ptime;
+ uint32_t max_ptime;
+ uint32_t pbytes;
+
+ struct sockaddr_storage src_addr;
+ socklen_t src_len;
+
+ uint16_t port;
+ struct sockaddr_storage dst_addr;
+ socklen_t dst_len;
+
+ uint16_t sap_port;
+ struct sockaddr_storage sap_addr;
+ socklen_t sap_len;
+
+ uint16_t msg_id_hash;
+ uint32_t ntp;
+
+ struct spa_audio_info_raw info;
+ const struct format_info *format_info;
+ uint32_t frame_size;
+ int payload;
+ uint16_t seq;
+ uint32_t timestamp;
+ uint32_t ssrc;
+
+ struct spa_ringbuffer ring;
+ uint8_t buffer[BUFFER_SIZE];
+
+ int rtp_fd;
+ int sap_fd;
+};
+
+
+static void stream_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->stream_listener);
+ impl->stream = NULL;
+}
+
+static inline void
+set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size,
+ uint32_t offset, struct iovec *iov, uint32_t len)
+{
+ iov[0].iov_len = SPA_MIN(len, size - offset);
+ iov[0].iov_base = SPA_PTROFF(buffer, offset, void);
+ iov[1].iov_len = len - iov[0].iov_len;
+ iov[1].iov_base = buffer;
+}
+
+static void flush_packets(struct impl *impl)
+{
+ int32_t avail;
+ uint32_t index;
+ struct iovec iov[3];
+ struct msghdr msg;
+ ssize_t n;
+ struct rtp_header header;
+ int32_t tosend;
+
+ avail = spa_ringbuffer_get_read_index(&impl->ring, &index);
+
+ tosend = impl->pbytes;
+
+ if (avail < tosend)
+ return;
+
+ spa_zero(header);
+ header.v = 2;
+ header.pt = impl->payload;
+ header.ssrc = htonl(impl->ssrc);
+
+ iov[0].iov_base = &header;
+ iov[0].iov_len = sizeof(header);
+
+ msg.msg_name = NULL;
+ msg.msg_namelen = 0;
+ msg.msg_iov = iov;
+ msg.msg_iovlen = 3;
+ msg.msg_control = NULL;
+ msg.msg_controllen = 0;
+ msg.msg_flags = 0;
+
+ while (avail >= tosend) {
+ header.sequence_number = htons(impl->seq);
+ header.timestamp = htonl(impl->timestamp);
+
+ set_iovec(&impl->ring,
+ impl->buffer, BUFFER_SIZE,
+ index & BUFFER_MASK,
+ &iov[1], tosend);
+
+ n = sendmsg(impl->rtp_fd, &msg, MSG_NOSIGNAL);
+ if (n < 0) {
+ switch (errno) {
+ case ECONNREFUSED:
+ case ECONNRESET:
+ pw_log_debug("remote end not listening");
+ break;
+ default:
+ pw_log_warn("sendmsg() failed: %m");
+ break;
+ }
+ }
+
+ impl->seq++;
+ impl->timestamp += tosend / impl->frame_size;
+
+ index += tosend;
+ avail -= tosend;
+ }
+ spa_ringbuffer_read_update(&impl->ring, index);
+}
+
+static void stream_process(void *data)
+{
+ struct impl *impl = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t index;
+ int32_t filled, wanted;
+
+ if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) {
+ pw_log_debug("Out of stream buffers: %m");
+ return;
+ }
+ d = buf->buffer->datas;
+
+ wanted = d[0].chunk->size;
+
+ filled = spa_ringbuffer_get_write_index(&impl->ring, &index);
+
+ if (filled + wanted > (int32_t)BUFFER_SIZE) {
+ pw_log_warn("overrun %u + %u > %u", filled, wanted, BUFFER_SIZE);
+ } else {
+ spa_ringbuffer_write_data(&impl->ring,
+ impl->buffer,
+ BUFFER_SIZE,
+ index & BUFFER_MASK,
+ d[0].data, wanted);
+
+ index += wanted;
+ spa_ringbuffer_write_update(&impl->ring, index);
+ }
+ pw_stream_queue_buffer(impl->stream, buf);
+
+ flush_packets(impl);
+}
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct impl *impl = d;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("stream disconnected, unloading");
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_error("stream error: %s", error);
+ break;
+ default:
+ break;
+ }
+}
+
+static const struct pw_stream_events in_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .process = stream_process
+};
+
+static int parse_address(const char *address, uint16_t port,
+ struct sockaddr_storage *addr, socklen_t *len)
+{
+ struct sockaddr_in *sa4 = (struct sockaddr_in*)addr;
+ struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)addr;
+
+ if (inet_pton(AF_INET, address, &sa4->sin_addr) > 0) {
+ sa4->sin_family = AF_INET;
+ sa4->sin_port = htons(port);
+ *len = sizeof(*sa4);
+ } else if (inet_pton(AF_INET6, address, &sa6->sin6_addr) > 0) {
+ sa6->sin6_family = AF_INET6;
+ sa6->sin6_port = htons(port);
+ *len = sizeof(*sa6);
+ } else
+ return -EINVAL;
+
+ return 0;
+}
+
+static bool is_multicast(struct sockaddr *sa, socklen_t salen)
+{
+ if (sa->sa_family == AF_INET) {
+ static const uint32_t ipv4_mcast_mask = 0xe0000000;
+ struct sockaddr_in *sa4 = (struct sockaddr_in*)sa;
+ return (ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask;
+ } else if (sa->sa_family == AF_INET6) {
+ struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
+ return sa6->sin6_addr.s6_addr[0] == 0xff;
+ }
+ return false;
+}
+
+static int make_socket(struct sockaddr_storage *src, socklen_t src_len,
+ struct sockaddr_storage *dst, socklen_t dst_len,
+ bool loop, int ttl)
+{
+ int af, fd, val, res;
+
+ af = src->ss_family;
+ if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ pw_log_error("socket failed: %m");
+ return -errno;
+ }
+ if (bind(fd, (struct sockaddr*)src, src_len) < 0) {
+ res = -errno;
+ pw_log_error("bind() failed: %m");
+ goto error;
+ }
+ if (connect(fd, (struct sockaddr*)dst, dst_len) < 0) {
+ res = -errno;
+ pw_log_error("connect() failed: %m");
+ goto error;
+ }
+ if (is_multicast((struct sockaddr*)dst, dst_len)) {
+ val = loop;
+ if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(IP_MULTICAST_LOOP) failed: %m");
+
+ val = ttl;
+ if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(IP_MULTICAST_TTL) failed: %m");
+ }
+#ifdef SO_PRIORITY
+ val = 6;
+ if (setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(SO_PRIORITY) failed: %m");
+#endif
+ val = IPTOS_LOWDELAY;
+ if (setsockopt(fd, IPPROTO_IP, IP_TOS, &val, sizeof(val)) < 0)
+ pw_log_warn("setsockopt(IP_TOS) failed: %m");
+
+
+ return fd;
+error:
+ close(fd);
+ return res;
+}
+
+static int setup_stream(struct impl *impl)
+{
+ const struct spa_pod *params[1];
+ struct spa_pod_builder b;
+ uint32_t n_params;
+ uint8_t buffer[1024];
+ struct pw_properties *props;
+ int res, fd;
+
+ props = pw_properties_copy(impl->stream_props);
+ if (props == NULL)
+ return -errno;
+
+ if (pw_properties_get(props, PW_KEY_NODE_LATENCY) == NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY,
+ "%d/%d", impl->pbytes / impl->frame_size,
+ impl->info.rate);
+ }
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", impl->info.rate);
+
+ impl->stream = pw_stream_new(impl->core,
+ "rtp-sink capture", props);
+ if (impl->stream == NULL)
+ return -errno;
+
+ pw_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &in_stream_events, impl);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &impl->info);
+
+ if ((res = pw_stream_connect(impl->stream,
+ PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0)
+ return res;
+
+
+ if ((fd = make_socket(&impl->src_addr, impl->src_len,
+ &impl->dst_addr, impl->dst_len,
+ impl->mcast_loop, impl->ttl)) < 0)
+ return fd;
+
+ impl->rtp_fd = fd;
+
+ return 0;
+}
+
+static int get_ip(const struct sockaddr_storage *sa, char *ip, size_t len)
+{
+ if (sa->ss_family == AF_INET) {
+ struct sockaddr_in *in = (struct sockaddr_in*)sa;
+ inet_ntop(sa->ss_family, &in->sin_addr, ip, len);
+ } else if (sa->ss_family == AF_INET6) {
+ struct sockaddr_in6 *in = (struct sockaddr_in6*)sa;
+ inet_ntop(sa->ss_family, &in->sin6_addr, ip, len);
+ } else
+ return -EIO;
+ return 0;
+}
+static void send_sap(struct impl *impl, bool bye)
+{
+ char buffer[2048], src_addr[64], dst_addr[64], dst_ttl[8];
+ const char *user_name, *af;
+ struct sockaddr *sa = (struct sockaddr*)&impl->src_addr;
+ struct sap_header header;
+ struct iovec iov[4];
+ struct msghdr msg;
+
+ spa_zero(header);
+ header.v = 1;
+ header.t = bye;
+ header.msg_id_hash = impl->msg_id_hash;
+
+ iov[0].iov_base = &header;
+ iov[0].iov_len = sizeof(header);
+
+ if (sa->sa_family == AF_INET) {
+ iov[1].iov_base = &((struct sockaddr_in*) sa)->sin_addr;
+ iov[1].iov_len = 4U;
+ af = "IP4";
+ } else {
+ iov[1].iov_base = &((struct sockaddr_in6*) sa)->sin6_addr;
+ iov[1].iov_len = 16U;
+ header.a = 1;
+ af = "IP6";
+ }
+ iov[2].iov_base = SAP_MIME_TYPE;
+ iov[2].iov_len = sizeof(SAP_MIME_TYPE);
+
+ get_ip(&impl->src_addr, src_addr, sizeof(src_addr));
+ get_ip(&impl->dst_addr, dst_addr, sizeof(dst_addr));
+
+ if ((user_name = pw_get_user_name()) == NULL)
+ user_name = "-";
+
+ spa_zero(dst_ttl);
+ if (is_multicast((struct sockaddr*)&impl->dst_addr, impl->dst_len))
+ snprintf(dst_ttl, sizeof(dst_ttl), "/%d", impl->ttl);
+
+ snprintf(buffer, sizeof(buffer),
+ "v=0\n"
+ "o=%s %u 0 IN %s %s\n"
+ "s=%s\n"
+ "c=IN %s %s%s\n"
+ "t=%u 0\n"
+ "a=recvonly\n"
+ "a=tool:PipeWire %s\n"
+ "m=audio %u RTP/AVP %i\n"
+ "a=rtpmap:%i %s/%u/%u\n"
+ "a=type:broadcast\n"
+ "a=ptime:%d\n",
+ user_name, impl->ntp, af, src_addr,
+ impl->session_name,
+ af, dst_addr, dst_ttl,
+ impl->ntp,
+ pw_get_library_version(),
+ impl->port, impl->payload,
+ impl->payload, impl->format_info->mime,
+ impl->info.rate, impl->info.channels,
+ (impl->pbytes / impl->frame_size) * 1000 / impl->info.rate);
+
+ iov[3].iov_base = buffer;
+ iov[3].iov_len = strlen(buffer);
+
+ msg.msg_name = NULL;
+ msg.msg_namelen = 0;
+ msg.msg_iov = iov;
+ msg.msg_iovlen = 4;
+ msg.msg_control = NULL;
+ msg.msg_controllen = 0;
+ msg.msg_flags = 0;
+
+ sendmsg(impl->sap_fd, &msg, MSG_NOSIGNAL);
+}
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+ struct impl *impl = data;
+ send_sap(impl, 0);
+}
+
+static int start_sap_announce(struct impl *impl)
+{
+ int fd, res;
+ struct timespec value, interval;
+
+ if ((fd = make_socket(&impl->src_addr, impl->src_len,
+ &impl->sap_addr, impl->sap_len,
+ impl->mcast_loop, impl->ttl)) < 0)
+ return fd;
+
+ impl->sap_fd = fd;
+
+ pw_log_info("starting SAP timer");
+ impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl);
+ if (impl->timer == NULL) {
+ res = -errno;
+ pw_log_error("can't create timer source: %m");
+ goto error;
+ }
+ value.tv_sec = 0;
+ value.tv_nsec = 1;
+ interval.tv_sec = SAP_INTERVAL_SEC;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false);
+
+ return 0;
+error:
+ close(fd);
+ return res;
+
+}
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ send_sap(impl, 1);
+
+ if (impl->stream)
+ pw_stream_destroy(impl->stream);
+
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ if (impl->timer)
+ pw_loop_destroy_source(impl->loop, impl->timer);
+
+ if (impl->rtp_fd != -1)
+ close(impl->rtp_fd);
+ if (impl->sap_fd != -1)
+ close(impl->sap_fd);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl->ifname);
+ free(impl->session_name);
+ free(impl);
+}
+
+static void module_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = d;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static inline uint32_t format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static uint32_t channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static void parse_position(struct spa_audio_info_raw *info, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ info->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ info->channels < SPA_AUDIO_MAX_CHANNELS) {
+ info->position[info->channels++] = channel_from_name(v);
+ }
+}
+
+static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info)
+{
+ const char *str;
+
+ spa_zero(*info);
+ if ((str = pw_properties_get(props, PW_KEY_AUDIO_FORMAT)) == NULL)
+ str = DEFAULT_FORMAT;
+ info->format = format_from_name(str, strlen(str));
+
+ info->rate = pw_properties_get_uint32(props, PW_KEY_AUDIO_RATE, info->rate);
+ if (info->rate == 0)
+ info->rate = DEFAULT_RATE;
+
+ info->channels = pw_properties_get_uint32(props, PW_KEY_AUDIO_CHANNELS, info->channels);
+ info->channels = SPA_MIN(info->channels, SPA_AUDIO_MAX_CHANNELS);
+ if ((str = pw_properties_get(props, SPA_KEY_AUDIO_POSITION)) != NULL)
+ parse_position(info, str, strlen(str));
+ if (info->channels == 0)
+ parse_position(info, DEFAULT_POSITION, strlen(DEFAULT_POSITION));
+}
+
+static void copy_props(struct impl *impl, struct pw_properties *props, const char *key)
+{
+ const char *str;
+ if ((str = pw_properties_get(props, key)) != NULL) {
+ if (pw_properties_get(impl->stream_props, key) == NULL)
+ pw_properties_set(impl->stream_props, key, str);
+ }
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ struct pw_properties *props = NULL, *stream_props = NULL;
+ uint32_t id = pw_global_get_id(pw_impl_module_get_global(module));
+ uint32_t pid = getpid(), port, min_bytes, max_bytes;
+ char addr[64];
+ const char *str;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ impl->rtp_fd = -1;
+ impl->sap_fd = -1;
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ impl->props = props;
+
+ stream_props = pw_properties_new(NULL, NULL);
+ if (stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+ impl->stream_props = stream_props;
+
+ impl->module = module;
+ impl->module_context = context;
+ impl->loop = pw_context_get_main_loop(context);
+
+ if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(stream_props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(stream_props, PW_KEY_NODE_NETWORK, "true");
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "rtp-sink-%u-%u", pid, id);
+ if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL)
+ pw_properties_set(props, PW_KEY_NODE_DESCRIPTION,
+ pw_properties_get(props, PW_KEY_NODE_NAME));
+ if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL)
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, "RTP Sender Stream");
+
+ if ((str = pw_properties_get(props, "stream.props")) != NULL)
+ pw_properties_update_string(stream_props, str, strlen(str));
+
+ copy_props(impl, props, PW_KEY_AUDIO_FORMAT);
+ copy_props(impl, props, PW_KEY_AUDIO_RATE);
+ copy_props(impl, props, PW_KEY_AUDIO_CHANNELS);
+ copy_props(impl, props, SPA_KEY_AUDIO_POSITION);
+ copy_props(impl, props, PW_KEY_NODE_NAME);
+ copy_props(impl, props, PW_KEY_NODE_DESCRIPTION);
+ copy_props(impl, props, PW_KEY_NODE_GROUP);
+ copy_props(impl, props, PW_KEY_NODE_LATENCY);
+ copy_props(impl, props, PW_KEY_NODE_VIRTUAL);
+ copy_props(impl, props, PW_KEY_MEDIA_NAME);
+ copy_props(impl, props, PW_KEY_MEDIA_CLASS);
+
+ parse_audio_info(impl->stream_props, &impl->info);
+
+ impl->format_info = find_format_info(impl->info.format);
+ if (impl->format_info == NULL) {
+ pw_log_error("unsupported audio format:%d channels:%d",
+ impl->info.format, impl->info.channels);
+ res = -EINVAL;
+ goto out;
+ }
+ impl->frame_size = impl->format_info->size * impl->info.channels;
+ impl->msg_id_hash = rand();
+ impl->ntp = (uint32_t) time(NULL) + 2208988800U;
+
+ impl->payload = 127;
+ impl->seq = rand();
+ impl->timestamp = rand();
+ impl->ssrc = rand();
+
+ str = pw_properties_get(props, "local.ifname");
+ impl->ifname = str ? strdup(str) : NULL;
+
+ if ((str = pw_properties_get(props, "sap.ip")) == NULL)
+ str = DEFAULT_SAP_IP;
+ port = pw_properties_get_uint32(props, "sap.port", DEFAULT_SAP_PORT);
+ if ((res = parse_address(str, port, &impl->sap_addr, &impl->sap_len)) < 0) {
+ pw_log_error("invalid sap.ip %s: %s", str, spa_strerror(res));
+ goto out;
+ }
+
+ if ((str = pw_properties_get(props, "source.ip")) == NULL)
+ str = DEFAULT_SOURCE_IP;
+ if ((res = parse_address(str, 0, &impl->src_addr, &impl->src_len)) < 0) {
+ pw_log_error("invalid source.ip %s: %s", str, spa_strerror(res));
+ goto out;
+ }
+
+ impl->port = DEFAULT_PORT + ((uint32_t) (rand() % 512) << 1);
+ impl->port = pw_properties_get_uint32(props, "destination.port", impl->port);
+ if ((str = pw_properties_get(props, "destination.ip")) == NULL)
+ str = DEFAULT_DESTINATION_IP;
+ if ((res = parse_address(str, impl->port, &impl->dst_addr, &impl->dst_len)) < 0) {
+ pw_log_error("invalid destination.ip %s: %s", str, spa_strerror(res));
+ goto out;
+ }
+
+ impl->mtu = pw_properties_get_uint32(props, "net.mtu", DEFAULT_MTU);
+ impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL);
+ impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP);
+
+ impl->min_ptime = pw_properties_get_uint32(props, "sess.min-ptime", DEFAULT_MIN_PTIME);
+ impl->max_ptime = pw_properties_get_uint32(props, "sess.max-ptime", DEFAULT_MAX_PTIME);
+
+ min_bytes = (impl->min_ptime * impl->info.rate / 1000) * impl->frame_size;
+ max_bytes = (impl->max_ptime * impl->info.rate / 1000) * impl->frame_size;
+
+ impl->pbytes = SPA_ROUND_DOWN(impl->mtu, impl->frame_size);
+ impl->pbytes = SPA_CLAMP(impl->pbytes, min_bytes, max_bytes);
+
+ if ((str = pw_properties_get(props, "sess.name")) == NULL)
+ pw_properties_setf(props, "sess.name", "PipeWire RTP Stream on %s",
+ pw_get_host_name());
+ str = pw_properties_get(props, "sess.name");
+ impl->session_name = str ? strdup(str) : NULL;
+
+ pw_properties_set(stream_props, "rtp.session", impl->session_name);
+ get_ip(&impl->src_addr, addr, sizeof(addr));
+ pw_properties_set(stream_props, "rtp.source.ip", addr);
+ get_ip(&impl->dst_addr, addr, sizeof(addr));
+ pw_properties_set(stream_props, "rtp.destination.ip", addr);
+ pw_properties_setf(stream_props, "rtp.destination.port", "%u", impl->port);
+ pw_properties_setf(stream_props, "rtp.mtu", "%u", impl->mtu);
+ pw_properties_setf(stream_props, "rtp.ttl", "%u", impl->ttl);
+ pw_properties_setf(stream_props, "rtp.ptime", "%u",
+ (impl->pbytes / impl->frame_size) * 1000 / impl->info.rate);
+
+ impl->core = pw_context_get_object(impl->module_context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->module_context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto out;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ if ((res = setup_stream(impl)) < 0)
+ goto out;
+
+ if ((res = start_sap_announce(impl)) < 0)
+ goto out;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info));
+
+ pw_log_info("Successfully loaded module-rtp-sink");
+
+ return 0;
+out:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c
new file mode 100644
index 0000000..5353532
--- /dev/null
+++ b/src/modules/module-rtp-source.c
@@ -0,0 +1,1200 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <limits.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <ctype.h>
+
+#include <spa/utils/hook.h>
+#include <spa/utils/result.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/dll.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/debug/mem.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#include <module-rtp/sap.h>
+#include <module-rtp/rtp.h>
+
+#ifdef __FreeBSD__
+#define ifr_ifindex ifr_index
+#endif
+
+/** \page page_module_rtp_source PipeWire Module: RTP source
+ *
+ * The `rtp-source` module creates a PipeWire source that receives audio
+ * RTP packets.
+ *
+ * ## Module Options
+ *
+ * Options specific to the behavior of this module
+ *
+ * - `sap.ip = <str>`: IP address of the SAP messages, default "224.0.0.56"
+ * - `sap.port = <str>`: port of the SAP messages, default 9875
+ * - `local.ifname = <str>`: interface name to use
+ * - `sess.latency.msec = <str>`: target network latency in milliseconds, default 100
+ * - `stream.props = {}`: properties to be passed to the stream
+ *
+ * ## General options
+ *
+ * Options with well-known behavior:
+ *
+ * - \ref PW_KEY_NODE_NAME
+ * - \ref PW_KEY_NODE_DESCRIPTION
+ * - \ref PW_KEY_MEDIA_NAME
+ * - \ref PW_KEY_MEDIA_CLASS
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-rtp-source
+ * args = {
+ * #sap.ip = 224.0.0.56
+ * #sap.port = 9875
+ * #local.ifname = eth0
+ * sess.latency.msec = 100
+ * stream.props = {
+ * #media.class = "Audio/Source"
+ * #node.name = "rtp-source"
+ * }
+ * stream.rules = [
+ * { matches = [
+ * # any of the items in matches needs to match, if one does,
+ * # actions are emited.
+ * { # all keys must match the value. ~ in value starts regex.
+ * #rtp.origin = "wim 3883629975 0 IN IP4 0.0.0.0"
+ * #rtp.payload = "127"
+ * #rtp.fmt = "L16/48000/2"
+ * #rtp.session = "PipeWire RTP Stream on fedora"
+ * }
+ * ]
+ * actions = {
+ * create-stream = {
+ * #sess.latency.msec = 100
+ * #target.object = ""
+ * }
+ * }
+ * }
+ * ]
+ * }
+ * }
+ * ]
+ *\endcode
+ *
+ * \since 0.3.60
+ */
+
+#define NAME "rtp-source"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define SAP_MIME_TYPE "application/sdp"
+
+#define ERROR_MSEC 2
+#define MAX_SESSIONS 16
+
+#define DEFAULT_CLEANUP_INTERVAL_SEC 90
+#define DEFAULT_SAP_IP "224.0.0.56"
+#define DEFAULT_SAP_PORT 9875
+#define DEFAULT_SESS_LATENCY 100
+
+#define BUFFER_SIZE (1u<<22)
+#define BUFFER_MASK (BUFFER_SIZE-1)
+
+#define USAGE "sap.ip=<SAP IP address to listen on, default "DEFAULT_SAP_IP"> " \
+ "sap.port=<SAP port to listen on, default "SPA_STRINGIFY(DEFAULT_SAP_PORT)"> " \
+ "local.ifname=<local interface name to use> " \
+ "sess.latency.msec=<target network latency, default "SPA_STRINGIFY(DEFAULT_SESS_LATENCY)"> " \
+ "stream.props= { key=value ... } " \
+ "stream.rules=<rules> "
+
+static const struct spa_dict_item module_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "RTP Source" },
+ { PW_KEY_MODULE_USAGE, USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct impl {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+ struct pw_properties *props;
+ struct pw_context *module_context;
+
+ struct pw_loop *loop;
+ struct pw_loop *data_loop;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook core_proxy_listener;
+
+ struct spa_source *timer;
+ struct spa_source *sap_source;
+
+ struct pw_properties *stream_props;
+
+ unsigned int do_disconnect:1;
+
+ char *ifname;
+ char *sap_ip;
+ int sap_port;
+ int sess_latency_msec;
+ uint32_t cleanup_interval;
+
+ struct spa_list sessions;
+ uint32_t n_sessions;
+};
+
+static const struct format_info {
+ uint32_t format;
+ uint32_t size;
+ const char *mime;
+} format_info[] = {
+ { SPA_AUDIO_FORMAT_U8, 1, "L8" },
+ { SPA_AUDIO_FORMAT_ALAW, 1, "PCMA" },
+ { SPA_AUDIO_FORMAT_ULAW, 1, "PCMU" },
+ { SPA_AUDIO_FORMAT_S16_BE, 2, "L16" },
+ { SPA_AUDIO_FORMAT_S24_BE, 3, "L24" },
+};
+
+static const struct format_info *find_format_info(const char *mime)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, f)
+ if (spa_streq(f->mime, mime))
+ return f;
+ return NULL;
+}
+
+struct sdp_info {
+ uint16_t hash;
+
+ char origin[128];
+ char session[256];
+
+ struct sockaddr_storage sa;
+ socklen_t salen;
+
+ uint16_t port;
+ uint8_t payload;
+
+ const struct format_info *format_info;
+ struct spa_audio_info_raw info;
+ uint32_t stride;
+};
+
+struct session {
+ struct impl *impl;
+ struct spa_list link;
+
+ uint64_t timestamp;
+
+ struct sdp_info info;
+
+ struct spa_source *source;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ uint32_t expected_ssrc;
+ uint16_t expected_seq;
+ unsigned have_ssrc:1;
+ unsigned have_seq:1;
+ unsigned have_sync:1;
+
+ struct spa_ringbuffer ring;
+ uint8_t buffer[BUFFER_SIZE];
+
+ struct spa_io_rate_match *rate_match;
+ struct spa_dll dll;
+ uint32_t target_buffer;
+ uint32_t last_packet_size;
+ float max_error;
+ unsigned buffering:1;
+ unsigned first:1;
+};
+
+static void stream_destroy(void *d)
+{
+ struct session *sess = d;
+ spa_hook_remove(&sess->stream_listener);
+ sess->stream = NULL;
+}
+
+static void stream_process(void *data)
+{
+ struct session *sess = data;
+ struct pw_buffer *buf;
+ struct spa_data *d;
+ uint32_t index, target_buffer;
+ int32_t avail, wanted;
+
+ if ((buf = pw_stream_dequeue_buffer(sess->stream)) == NULL) {
+ pw_log_debug("Out of stream buffers: %m");
+ return;
+ }
+ d = buf->buffer->datas;
+
+ wanted = buf->requested ?
+ SPA_MIN(buf->requested * sess->info.stride, d[0].maxsize)
+ : d[0].maxsize;
+
+ avail = spa_ringbuffer_get_read_index(&sess->ring, &index);
+
+ target_buffer = sess->target_buffer + sess->last_packet_size / 2;
+
+ if (avail < wanted || sess->buffering) {
+ memset(d[0].data, 0, wanted);
+ if (!sess->buffering && sess->have_sync) {
+ pw_log_debug("underrun %u/%u < %u, buffering...",
+ avail, target_buffer, wanted);
+ sess->buffering = true;
+ }
+ } else {
+ float error, corr;
+ if (avail > (int32_t)SPA_MIN(target_buffer * 8, BUFFER_SIZE)) {
+ pw_log_warn("overrun %u > %u", avail, target_buffer * 8);
+ index += avail - target_buffer;
+ avail = target_buffer;
+ } else {
+ if (sess->first) {
+ if ((uint32_t)avail > target_buffer) {
+ uint32_t skip = avail - target_buffer;
+ pw_log_debug("first: avail:%d skip:%u target:%u",
+ avail, skip, target_buffer);
+ index += skip;
+ avail = target_buffer;
+ }
+ sess->first = false;
+ }
+ error = (float)target_buffer - (float)avail;
+ error = SPA_CLAMP(error, -sess->max_error, sess->max_error);
+
+ corr = spa_dll_update(&sess->dll, error);
+
+ pw_log_debug("avail:%u target:%u error:%f corr:%f", avail,
+ target_buffer, error, corr);
+
+ if (sess->rate_match) {
+ SPA_FLAG_SET(sess->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE);
+ sess->rate_match->rate = 1.0f / corr;
+ }
+ }
+ spa_ringbuffer_read_data(&sess->ring,
+ sess->buffer,
+ BUFFER_SIZE,
+ index & BUFFER_MASK,
+ d[0].data, wanted);
+
+ index += wanted;
+ spa_ringbuffer_read_update(&sess->ring, index);
+ }
+ d[0].chunk->size = wanted;
+ d[0].chunk->stride = sess->info.stride;
+ d[0].chunk->offset = 0;
+ buf->size = wanted / sess->info.stride;
+
+ pw_stream_queue_buffer(sess->stream, buf);
+}
+
+static void on_stream_state_changed(void *d, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct session *sess = d;
+ struct impl *impl = sess->impl;
+
+ switch (state) {
+ case PW_STREAM_STATE_UNCONNECTED:
+ pw_log_info("stream disconnected, unloading");
+ pw_impl_module_schedule_destroy(impl->module);
+ break;
+ case PW_STREAM_STATE_ERROR:
+ pw_log_error("stream error: %s", error);
+ break;
+ default:
+ break;
+ }
+}
+
+static void stream_io_changed(void *data, uint32_t id, void *area, uint32_t size)
+{
+ struct session *sess = data;
+ switch (id) {
+ case SPA_IO_RateMatch:
+ sess->rate_match = area;
+ break;
+ }
+}
+
+static const struct pw_stream_events out_stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy,
+ .state_changed = on_stream_state_changed,
+ .io_changed = stream_io_changed,
+ .process = stream_process
+};
+
+static void
+on_rtp_io(void *data, int fd, uint32_t mask)
+{
+ struct session *sess = data;
+ struct rtp_header *hdr;
+ ssize_t len, hlen;
+ uint8_t buffer[2048], *payload;
+
+ if (mask & SPA_IO_IN) {
+ uint32_t index, expected_index, timestamp;
+ uint16_t seq;
+ int32_t filled;
+
+ if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0)
+ goto receive_error;
+
+ if (len < 12)
+ goto short_packet;
+
+ hdr = (struct rtp_header*)buffer;
+ if (hdr->v != 2)
+ goto invalid_version;
+
+ hlen = 12 + hdr->cc * 4;
+ if (hlen > len)
+ goto invalid_len;
+
+ if (sess->have_ssrc && sess->expected_ssrc != hdr->ssrc)
+ goto unexpected_ssrc;
+ sess->expected_ssrc = hdr->ssrc;
+ sess->have_ssrc = true;
+
+ seq = ntohs(hdr->sequence_number);
+ if (sess->have_seq && sess->expected_seq != seq) {
+ pw_log_warn("unexpected seq (%d != %d)", seq, sess->expected_seq);
+ }
+ sess->expected_seq = seq + 1;
+ sess->have_seq = true;
+
+ len = SPA_ROUND_DOWN(len - hlen, sess->info.stride);
+ payload = &buffer[hlen];
+
+ filled = spa_ringbuffer_get_write_index(&sess->ring, &index);
+
+ timestamp = ntohl(hdr->timestamp);
+ expected_index = timestamp * sess->info.stride;
+
+ if (!sess->have_sync) {
+ pw_log_trace("got rtp, no sync");
+ sess->ring.readindex = sess->ring.writeindex =
+ index = expected_index;
+ filled = 0;
+ sess->have_sync = true;
+ sess->buffering = true;
+ pw_log_debug("sync to timestamp %u", index);
+
+ spa_dll_init(&sess->dll);
+ spa_dll_set_bw(&sess->dll, SPA_DLL_BW_MIN, 128, sess->info.info.rate);
+
+ } else if (expected_index != index) {
+ pw_log_trace("got rtp, wrong timestamp");
+ pw_log_debug("unexpected timestamp (%u != %u)",
+ index / sess->info.stride,
+ expected_index / sess->info.stride);
+ index = expected_index;
+ filled = 0;
+ }
+
+ if (filled + len > BUFFER_SIZE) {
+ pw_log_debug("got rtp, capture overrun %u %zd", filled, len);
+ sess->have_sync = false;
+ } else {
+ uint32_t target_buffer;
+
+ pw_log_trace("got rtp packet len:%zd", len);
+ spa_ringbuffer_write_data(&sess->ring,
+ sess->buffer,
+ BUFFER_SIZE,
+ index & BUFFER_MASK,
+ payload, len);
+ index += len;
+ filled += len;
+ spa_ringbuffer_write_update(&sess->ring, index);
+
+ sess->last_packet_size = len;
+ target_buffer = sess->target_buffer + len/2;
+
+ if (sess->buffering && (uint32_t)filled > target_buffer) {
+ sess->buffering = false;
+ pw_log_debug("buffering done %u > %u",
+ filled, target_buffer);
+ }
+ }
+ }
+ return;
+
+receive_error:
+ pw_log_warn("recv error: %m");
+ return;
+short_packet:
+ pw_log_warn("short packet received");
+ return;
+invalid_version:
+ pw_log_warn("invalid RTP version");
+ return;
+invalid_len:
+ pw_log_warn("invalid RTP length");
+ return;
+unexpected_ssrc:
+ pw_log_warn("unexpected SSRC (expected %u != %u)",
+ sess->expected_ssrc, hdr->ssrc);
+ return;
+}
+
+static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname)
+{
+ int af, fd, val, res;
+ struct ifreq req;
+
+ af = sa->sa_family;
+ if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
+ pw_log_error("socket failed: %m");
+ return -errno;
+ }
+#ifdef SO_TIMESTAMP
+ val = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &val, sizeof(val)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt failed: %m");
+ goto error;
+ }
+#endif
+ val = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
+ res = -errno;
+ pw_log_error("setsockopt failed: %m");
+ goto error;
+ }
+
+ spa_zero(req);
+ if (ifname) {
+ snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", ifname);
+ res = ioctl(fd, SIOCGIFINDEX, &req);
+ if (res < 0)
+ pw_log_warn("SIOCGIFINDEX %s failed: %m", ifname);
+ }
+ res = 0;
+ if (af == AF_INET) {
+ static const uint32_t ipv4_mcast_mask = 0xe0000000;
+ struct sockaddr_in *sa4 = (struct sockaddr_in*)sa;
+ if ((ntohl(sa4->sin_addr.s_addr) & ipv4_mcast_mask) == ipv4_mcast_mask) {
+ struct ip_mreqn mr4;
+ memset(&mr4, 0, sizeof(mr4));
+ mr4.imr_multiaddr = sa4->sin_addr;
+ mr4.imr_ifindex = req.ifr_ifindex;
+ res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mr4, sizeof(mr4));
+ } else {
+ sa4->sin_addr.s_addr = INADDR_ANY;
+ }
+ } else if (af == AF_INET6) {
+ struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa;
+ if (sa6->sin6_addr.s6_addr[0] == 0xff) {
+ struct ipv6_mreq mr6;
+ memset(&mr6, 0, sizeof(mr6));
+ mr6.ipv6mr_multiaddr = sa6->sin6_addr;
+ mr6.ipv6mr_interface = req.ifr_ifindex;
+ res = setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mr6, sizeof(mr6));
+ } else {
+ sa6->sin6_addr = in6addr_any;
+ }
+ } else {
+ res = -EINVAL;
+ goto error;
+ }
+
+ if (res < 0) {
+ res = -errno;
+ pw_log_error("join mcast failed: %m");
+ goto error;
+ }
+
+ if (bind(fd, sa, salen) < 0) {
+ res = -errno;
+ pw_log_error("bind() failed: %m");
+ goto error;
+ }
+ return fd;
+error:
+ return res;
+}
+
+static uint32_t msec_to_bytes(struct sdp_info *info, uint32_t msec)
+{
+ return msec * info->stride * info->info.rate / 1000;
+}
+
+static void session_touch(struct session *sess)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ sess->timestamp = SPA_TIMESPEC_TO_NSEC(&ts);
+}
+
+static void session_free(struct session *sess)
+{
+ if (sess->impl) {
+ pw_log_info("free session %s %s", sess->info.origin, sess->info.session);
+ sess->impl->n_sessions--;
+ spa_list_remove(&sess->link);
+ }
+ if (sess->stream)
+ pw_stream_destroy(sess->stream);
+ if (sess->source)
+ pw_loop_destroy_source(sess->impl->data_loop, sess->source);
+ free(sess);
+}
+
+struct session_info {
+ struct session *session;
+ 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 session_info *i = data;
+ int res = 0;
+
+ i->matched = true;
+ if (spa_streq(action, "create-stream")) {
+ pw_properties_update_string(i->props, str, len);
+ }
+ return res;
+}
+
+static int session_new(struct impl *impl, struct sdp_info *info)
+{
+ struct session *session;
+ const struct spa_pod *params[1];
+ struct spa_pod_builder b;
+ uint32_t n_params;
+ uint8_t buffer[1024];
+ struct pw_properties *props;
+ int res, fd, sess_latency_msec;
+ const char *str;
+
+ if (impl->n_sessions >= MAX_SESSIONS) {
+ pw_log_warn("too many sessions (%u >= %u)", impl->n_sessions, MAX_SESSIONS);
+ return -EMFILE;
+ }
+
+ session = calloc(1, sizeof(struct session));
+ if (session == NULL)
+ return -errno;
+
+ session->info = *info;
+ session->first = true;
+
+ props = pw_properties_copy(impl->stream_props);
+ if (props == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ pw_properties_set(props, "rtp.origin", info->origin);
+ pw_properties_setf(props, "rtp.payload", "%u", info->payload);
+ pw_properties_setf(props, "rtp.fmt", "%s/%u/%u", info->format_info->mime,
+ info->info.rate, info->info.channels);
+ if (info->session[0]) {
+ pw_properties_set(props, "rtp.session", info->session);
+ pw_properties_setf(props, PW_KEY_MEDIA_NAME, "RTP Stream (%s)",
+ info->session);
+ pw_properties_setf(props, PW_KEY_NODE_NAME, "%s",
+ info->session);
+ } else {
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, "RTP Stream");
+ }
+
+ if ((str = pw_properties_get(impl->props, "stream.rules")) != NULL) {
+ struct session_info sinfo = {
+ .session = session,
+ .props = props,
+ };
+ pw_conf_match_rules(str, strlen(str), NAME, &props->dict,
+ rule_matched, &sinfo);
+
+ if (!sinfo.matched) {
+ res = 0;
+ pw_log_info("session '%s' was not matched", info->session);
+ goto error;
+ }
+ }
+
+ pw_log_info("new session %s %s", info->origin, info->session);
+
+ sess_latency_msec = pw_properties_get_uint32(props,
+ "sess.latency.msec", impl->sess_latency_msec);
+
+ session->target_buffer = msec_to_bytes(info, sess_latency_msec);
+ session->max_error = msec_to_bytes(info, ERROR_MSEC);
+
+ pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", info->info.rate);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d",
+ session->target_buffer / (2 * info->stride), info->info.rate);
+
+ spa_dll_init(&session->dll);
+ spa_dll_set_bw(&session->dll, SPA_DLL_BW_MIN, 128, session->info.info.rate);
+
+ session->stream = pw_stream_new(impl->core,
+ "rtp-source playback", props);
+ if (session->stream == NULL) {
+ res = -errno;
+ pw_log_error("can't create stream: %m");
+ goto error;
+ }
+
+ pw_stream_add_listener(session->stream,
+ &session->stream_listener,
+ &out_stream_events, session);
+
+ n_params = 0;
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &info->info);
+
+ if ((res = pw_stream_connect(session->stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, n_params)) < 0) {
+ pw_log_error("can't connect stream: %s", spa_strerror(res));
+ goto error;
+ }
+
+ if ((fd = make_socket((const struct sockaddr *)&info->sa,
+ info->salen, impl->ifname)) < 0) {
+ res = fd;
+ goto error;
+ }
+
+ session->source = pw_loop_add_io(impl->data_loop, fd,
+ SPA_IO_IN, true, on_rtp_io, session);
+ if (session->source == NULL) {
+ res = -errno;
+ pw_log_error("can't create io source: %m");
+ goto error;
+ }
+
+ pw_log_info("starting RTP listener");
+ session_touch(session);
+
+ session->impl = impl;
+ spa_list_append(&impl->sessions, &session->link);
+ impl->n_sessions++;
+
+ return 0;
+error:
+ session_free(session);
+ return res;
+}
+
+static struct session *session_find(struct impl *impl, struct sdp_info *info)
+{
+ struct session *sess;
+ spa_list_for_each(sess, &impl->sessions, link) {
+ if (info->hash == sess->info.hash &&
+ spa_streq(info->origin, sess->info.origin))
+ return sess;
+ }
+ return NULL;
+}
+
+static int parse_sdp_c(struct impl *impl, char *c, struct sdp_info *info)
+{
+ int res;
+
+ c[strcspn(c, "/")] = 0;
+ if (spa_strstartswith(c, "c=IN IP4 ")) {
+ struct sockaddr_in *sa = (struct sockaddr_in*) &info->sa;
+
+ c += strlen("c=IN IP4 ");
+ if (inet_pton(AF_INET, c, &sa->sin_addr) <= 0) {
+ res = -errno;
+ pw_log_warn("inet_pton(%s) failed: %m", c);
+ goto error;
+ }
+ sa->sin_family = AF_INET;
+ info->salen = sizeof(struct sockaddr_in);
+ }
+ else if (spa_strstartswith(c, "c=IN IP6 ")) {
+ struct sockaddr_in6 *sa = (struct sockaddr_in6*) &info->sa;
+
+ c += strlen("c=IN IP6 ");
+ if (inet_pton(AF_INET6, c, &sa->sin6_addr) <= 0) {
+ res = -errno;
+ pw_log_warn("inet_pton(%s) failed: %m", c);
+ goto error;
+ }
+
+ sa->sin6_family = AF_INET6;
+ info->salen = sizeof(struct sockaddr_in6);
+ } else
+ return -EINVAL;
+
+
+ res= 0;
+error:
+ return res;
+}
+
+static int parse_sdp_m(struct impl *impl, char *c, struct sdp_info *info)
+{
+ int port, payload;
+
+ if (!spa_strstartswith(c, "m=audio "))
+ return -EINVAL;
+
+ c += strlen("m=audio ");
+ if (sscanf(c, "%i RTP/AVP %i", &port, &payload) != 2)
+ return -EINVAL;
+
+ if (port <= 0 || port > 0xFFFF)
+ return -EINVAL;
+
+ if (payload < 0 || payload > 127)
+ return -EINVAL;
+
+ info->port = (uint16_t) port;
+ info->payload = (uint8_t) payload;
+
+ return 0;
+}
+
+static int parse_sdp_a(struct impl *impl, char *c, struct sdp_info *info)
+{
+ int payload, len, rate, channels;
+
+ if (!spa_strstartswith(c, "a=rtpmap:"))
+ return 0;
+
+ c += strlen("a=rtpmap:");
+
+ if (sscanf(c, "%i %n", &payload, &len) != 1)
+ return -EINVAL;
+
+ if (payload < 0 || payload > 127)
+ return -EINVAL;
+
+ if (payload != info->payload)
+ return 0;
+
+ c += len;
+ c[strcspn(c, "/")] = 0;
+
+ info->format_info = find_format_info(c);
+ if (info->format_info == NULL)
+ return -EINVAL;
+
+ info->info.format = info->format_info->format;
+ info->stride = info->format_info->size;
+
+ c += strlen(c) + 1;
+ if (sscanf(c, "%u/%u", &rate, &channels) == 2) {
+ info->info.rate = rate;
+ info->info.channels = channels;
+ if (channels == 2) {
+ info->info.position[0] = SPA_AUDIO_CHANNEL_FL;
+ info->info.position[1] = SPA_AUDIO_CHANNEL_FR;
+ }
+ } else if (sscanf(c, "%u", &rate) == 1) {
+ info->info.rate = rate;
+ info->info.channels = 1;
+ } else
+ return -EINVAL;
+
+ info->stride *= info->info.channels;
+
+ return 0;
+}
+
+static int parse_sdp(struct impl *impl, char *sdp, struct sdp_info *info)
+{
+ char *s = sdp;
+ int count = 0, res = 0;
+ size_t l;
+
+ while (*s) {
+ if ((l = strcspn(s, "\r\n")) < 2)
+ goto too_short;
+
+ s[l] = 0;
+ pw_log_debug("%d: %s", count, s);
+
+ if (count++ == 0 && strcmp(s, "v=0") != 0)
+ goto invalid_version;
+
+ if (spa_strstartswith(s, "o="))
+ snprintf(info->origin, sizeof(info->origin), "%s", &s[2]);
+ else if (spa_strstartswith(s, "s="))
+ snprintf(info->session, sizeof(info->session), "%s", &s[2]);
+ else if (spa_strstartswith(s, "c="))
+ res = parse_sdp_c(impl, s, info);
+ else if (spa_strstartswith(s, "m="))
+ res = parse_sdp_m(impl, s, info);
+ else if (spa_strstartswith(s, "a="))
+ res = parse_sdp_a(impl, s, info);
+
+ if (res < 0)
+ goto error;
+ s += l + 1;
+ while (isspace(*s))
+ s++;
+ }
+ if (((struct sockaddr*) &info->sa)->sa_family == AF_INET)
+ ((struct sockaddr_in*) &info->sa)->sin_port = htons(info->port);
+ else
+ ((struct sockaddr_in6*) &info->sa)->sin6_port = htons(info->port);
+
+ return 0;
+too_short:
+ pw_log_warn("SDP: line starting with `%.6s...' too short", s);
+ return -EINVAL;
+invalid_version:
+ pw_log_warn("SDP: invalid first version line `%*s'", (int)l, s);
+ return -EINVAL;
+error:
+ pw_log_warn("SDP: error: %s", spa_strerror(res));
+ return res;
+}
+
+static int parse_sap(struct impl *impl, void *data, size_t len)
+{
+ struct sap_header *header;
+ char *mime, *sdp;
+ struct sdp_info info;
+ struct session *sess;
+ int res;
+ size_t offs;
+ bool bye;
+
+ if (len < 8)
+ return -EINVAL;
+
+ header = (struct sap_header*) data;
+ if (header->v != 1)
+ return -EINVAL;
+
+ if (header->e)
+ return -ENOTSUP;
+ if (header->c)
+ return -ENOTSUP;
+
+ offs = header->a ? 12 : 8;
+ offs += header->auth_len * 4;
+ if (len <= offs)
+ return -EINVAL;
+
+ mime = SPA_PTROFF(data, offs, char);
+ if (spa_strstartswith(mime, "v=0")) {
+ sdp = mime;
+ mime = SAP_MIME_TYPE;
+ } else if (spa_streq(mime, SAP_MIME_TYPE))
+ sdp = SPA_PTROFF(mime, strlen(mime)+1, char);
+ else
+ return -EINVAL;
+
+ pw_log_debug("got sap: %s %s", mime, sdp);
+
+ spa_zero(info);
+ if ((res = parse_sdp(impl, sdp, &info)) < 0)
+ return res;
+
+ bye = header->t;
+
+ sess = session_find(impl, &info);
+ if (sess == NULL) {
+ if (!bye)
+ session_new(impl, &info);
+ } else {
+ if (bye)
+ session_free(sess);
+ else
+ session_touch(sess);
+ }
+ return res;
+}
+
+static void
+on_sap_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+
+ if (mask & SPA_IO_IN) {
+ uint8_t buffer[2048];
+ ssize_t len;
+
+ if ((len = recv(fd, buffer, sizeof(buffer), 0)) < 0) {
+ pw_log_warn("recv error: %m");
+ return;
+ }
+ if ((size_t)len >= sizeof(buffer))
+ return;
+
+ buffer[len] = 0;
+ parse_sap(impl, buffer, len);
+ }
+}
+
+static int start_sap_listener(struct impl *impl)
+{
+ struct sockaddr_in sa4;
+ struct sockaddr_in6 sa6;
+ struct sockaddr *sa;
+ socklen_t salen;
+ int fd, res;
+
+ if (inet_pton(AF_INET, impl->sap_ip, &sa4.sin_addr) > 0) {
+ sa4.sin_family = AF_INET;
+ sa4.sin_port = htons(impl->sap_port);
+ sa = (struct sockaddr*) &sa4;
+ salen = sizeof(sa4);
+ } else if (inet_pton(AF_INET6, impl->sap_ip, &sa6.sin6_addr) > 0) {
+ sa6.sin6_family = AF_INET6;
+ sa6.sin6_port = htons(impl->sap_port);
+ sa = (struct sockaddr*) &sa6;
+ salen = sizeof(sa6);
+ } else
+ return -EINVAL;
+
+ if ((fd = make_socket(sa, salen, impl->ifname)) < 0)
+ return fd;
+
+ pw_log_info("starting SAP listener");
+ impl->sap_source = pw_loop_add_io(impl->loop, fd,
+ SPA_IO_IN, true, on_sap_io, impl);
+ if (impl->sap_source == NULL) {
+ res = -errno;
+ goto error;
+ }
+ return 0;
+error:
+ close(fd);
+ return res;
+
+}
+
+static void on_timer_event(void *data, uint64_t expirations)
+{
+ struct impl *impl = data;
+ struct timespec now;
+ struct session *sess, *tmp;
+ uint64_t timestamp, interval;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ timestamp = SPA_TIMESPEC_TO_NSEC(&now);
+ interval = impl->cleanup_interval * SPA_NSEC_PER_SEC;
+
+ spa_list_for_each_safe(sess, tmp, &impl->sessions, link) {
+ if (sess->timestamp + interval < timestamp) {
+ pw_log_debug("More than %lu elapsed from last advertisement at %lu", interval, sess->timestamp);
+ pw_log_info("No advertisement packets found for timeout, closing RTP source");
+ session_free(sess);
+ }
+ }
+}
+
+static void core_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->core_listener);
+ impl->core = NULL;
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_proxy_events core_proxy_events = {
+ .destroy = core_destroy,
+};
+
+static void impl_destroy(struct impl *impl)
+{
+ struct session *sess;
+ spa_list_consume(sess, &impl->sessions, link)
+ session_free(sess);
+
+ if (impl->core && impl->do_disconnect)
+ pw_core_disconnect(impl->core);
+
+ if (impl->sap_source)
+ pw_loop_destroy_source(impl->loop, impl->sap_source);
+ if (impl->timer)
+ pw_loop_destroy_source(impl->loop, impl->timer);
+
+ pw_properties_free(impl->stream_props);
+ pw_properties_free(impl->props);
+
+ free(impl->ifname);
+ free(impl->sap_ip);
+ free(impl);
+}
+
+static void module_destroy(void *d)
+{
+ struct impl *impl = d;
+ spa_hook_remove(&impl->module_listener);
+ impl_destroy(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message)
+{
+ struct impl *impl = d;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ const char *str;
+ struct timespec value, interval;
+ int res = 0;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -errno;
+
+ spa_list_init(&impl->sessions);
+
+ if (args == NULL)
+ args = "";
+
+ impl->props = pw_properties_new_string(args);
+ impl->stream_props = pw_properties_new(NULL, NULL);
+ if (impl->props == NULL || impl->stream_props == NULL) {
+ res = -errno;
+ pw_log_error( "can't create properties: %m");
+ goto out;
+ }
+
+ impl->module = module;
+ impl->module_context = context;
+ impl->loop = pw_context_get_main_loop(context);
+ impl->data_loop = pw_data_loop_get_loop(pw_context_get_data_loop(context));
+
+ if (pw_properties_get(impl->stream_props, PW_KEY_NODE_VIRTUAL) == NULL)
+ pw_properties_set(impl->stream_props, PW_KEY_NODE_VIRTUAL, "true");
+ if (pw_properties_get(impl->stream_props, PW_KEY_NODE_NETWORK) == NULL)
+ pw_properties_set(impl->stream_props, PW_KEY_NODE_NETWORK, "true");
+
+ if ((str = pw_properties_get(impl->props, "stream.props")) != NULL)
+ pw_properties_update_string(impl->stream_props, str, strlen(str));
+
+ str = pw_properties_get(impl->props, "local.ifname");
+ impl->ifname = str ? strdup(str) : NULL;
+
+ str = pw_properties_get(impl->props, "sap.ip");
+ impl->sap_ip = strdup(str ? str : DEFAULT_SAP_IP);
+ impl->sap_port = pw_properties_get_uint32(impl->props,
+ "sap.port", DEFAULT_SAP_PORT);
+ impl->sess_latency_msec = pw_properties_get_uint32(impl->props,
+ "sess.latency.msec", DEFAULT_SESS_LATENCY);
+ impl->cleanup_interval = pw_properties_get_uint32(impl->props,
+ "sap.interval.sec", DEFAULT_CLEANUP_INTERVAL_SEC);
+
+ impl->core = pw_context_get_object(impl->module_context, PW_TYPE_INTERFACE_Core);
+ if (impl->core == NULL) {
+ str = pw_properties_get(impl->props, PW_KEY_REMOTE_NAME);
+ impl->core = pw_context_connect(impl->module_context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, str,
+ NULL),
+ 0);
+ impl->do_disconnect = true;
+ }
+ if (impl->core == NULL) {
+ res = -errno;
+ pw_log_error("can't connect: %m");
+ goto out;
+ }
+
+ pw_proxy_add_listener((struct pw_proxy*)impl->core,
+ &impl->core_proxy_listener,
+ &core_proxy_events, impl);
+ pw_core_add_listener(impl->core,
+ &impl->core_listener,
+ &core_events, impl);
+
+ impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl);
+ if (impl->timer == NULL) {
+ res = -errno;
+ pw_log_error("can't create timer source: %m");
+ goto out;
+ }
+ value.tv_sec = 0;
+ value.tv_nsec = 1;
+ interval.tv_sec = impl->cleanup_interval;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false);
+
+ if ((res = start_sap_listener(impl)) < 0)
+ goto out;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info));
+
+ pw_log_info("Successfully loaded module-rtp-source");
+
+ return 0;
+out:
+ impl_destroy(impl);
+ return res;
+}
diff --git a/src/modules/module-rtp/rtp.h b/src/modules/module-rtp/rtp.h
new file mode 100644
index 0000000..92ff364
--- /dev/null
+++ b/src/modules/module-rtp/rtp.h
@@ -0,0 +1,78 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_RTP_H
+#define PIPEWIRE_RTP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rtp_header {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned cc:4;
+ unsigned x:1;
+ unsigned p:1;
+ unsigned v:2;
+
+ unsigned pt:7;
+ unsigned m:1;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+ unsigned v:2;
+ unsigned p:1;
+ unsigned x:1;
+ unsigned cc:4;
+
+ unsigned m:1;
+ unsigned pt:7;
+#else
+#error "Unknown byte order"
+#endif
+ uint16_t sequence_number;
+ uint32_t timestamp;
+ uint32_t ssrc;
+ uint32_t csrc[0];
+} __attribute__ ((packed));
+
+struct rtp_payload {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned frame_count:4;
+ unsigned rfa0:1;
+ unsigned is_last_fragment:1;
+ unsigned is_first_fragment:1;
+ unsigned is_fragmented:1;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+ unsigned is_fragmented:1;
+ unsigned is_first_fragment:1;
+ unsigned is_last_fragment:1;
+ unsigned rfa0:1;
+ unsigned frame_count:4;
+#endif
+} __attribute__ ((packed));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_RTP_H */
diff --git a/src/modules/module-rtp/sap.h b/src/modules/module-rtp/sap.h
new file mode 100644
index 0000000..b9a0a7a
--- /dev/null
+++ b/src/modules/module-rtp/sap.h
@@ -0,0 +1,58 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_SAP_H
+#define PIPEWIRE_SAP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct sap_header {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+ unsigned c:1;
+ unsigned e:1;
+ unsigned t:1;
+ unsigned r:1;
+ unsigned a:1;
+ unsigned v:3;
+#elif __BYTE_ORDER == __BIG_ENDIAN
+ unsigned v:3;
+ unsigned a:1;
+ unsigned r:1;
+ unsigned t:1;
+ unsigned e:1;
+ unsigned c:1;
+#else
+#error "Unknown byte order"
+#endif
+ uint8_t auth_len;
+ uint16_t msg_id_hash;
+} __attribute__ ((packed));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_SAP_H */
diff --git a/src/modules/module-session-manager.c b/src/modules/module-session-manager.c
new file mode 100644
index 0000000..85879f7
--- /dev/null
+++ b/src/modules/module-session-manager.c
@@ -0,0 +1,74 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <pipewire/impl.h>
+
+/** \page page_module_session_manager PipeWire Module: Session Manager
+ *
+ * This module implements some usefull objects for implementing a session
+ * manager. It is not yet actively used.
+ */
+
+/* client-endpoint.c */
+int client_endpoint_factory_init(struct pw_impl_module *module);
+/* client-session.c */
+int client_session_factory_init(struct pw_impl_module *module);
+
+int session_factory_init(struct pw_impl_module *module);
+int endpoint_factory_init(struct pw_impl_module *module);
+int endpoint_stream_factory_init(struct pw_impl_module *module);
+int endpoint_link_factory_init(struct pw_impl_module *module);
+
+/* protocol-native.c */
+int pw_protocol_native_ext_session_manager_init(struct pw_context *context);
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "George Kiagiadakis <george.kiagiadakis@collabora.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Implements objects for session management" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ int res;
+
+ if ((res = pw_protocol_native_ext_session_manager_init(context)) < 0)
+ return res;
+
+ client_endpoint_factory_init(module);
+ client_session_factory_init(module);
+ session_factory_init(module);
+ endpoint_factory_init(module);
+ endpoint_stream_factory_init(module);
+ endpoint_link_factory_init(module);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+}
diff --git a/src/modules/module-session-manager/client-endpoint/client-endpoint.c b/src/modules/module-session-manager/client-endpoint/client-endpoint.c
new file mode 100644
index 0000000..b2f2d98
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/client-endpoint.c
@@ -0,0 +1,296 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <spa/utils/result.h>
+#include <pipewire/impl.h>
+
+#include <pipewire/extensions/session-manager.h>
+
+#include "client-endpoint.h"
+#include "endpoint.h"
+#include "endpoint-stream.h"
+
+#define NAME "client-endpoint"
+
+struct factory_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+};
+
+static struct endpoint_stream *find_stream(struct client_endpoint *this, uint32_t id)
+{
+ struct endpoint_stream *s;
+ spa_list_for_each(s, &this->streams, link) {
+ if (s->id == id)
+ return s;
+ }
+ return NULL;
+}
+
+static int client_endpoint_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info)
+{
+ struct client_endpoint *this = object;
+ struct endpoint *endpoint = &this->endpoint;
+
+ return endpoint_update(endpoint, change_mask, n_params, params, info);
+}
+
+static int client_endpoint_stream_update(void *object,
+ uint32_t stream_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct client_endpoint *this = object;
+ struct endpoint *endpoint = &this->endpoint;
+ struct endpoint_stream *stream = find_stream(this, stream_id);
+ struct pw_properties *props = NULL;
+
+ if (!stream) {
+ static const char * const keys[] = {
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_ENDPOINT_ID,
+ PW_KEY_PRIORITY_SESSION,
+ PW_KEY_ENDPOINT_MONITOR,
+ PW_KEY_ENDPOINT_STREAM_NAME,
+ PW_KEY_ENDPOINT_STREAM_DESCRIPTION,
+ NULL
+ };
+
+ struct pw_context *context = pw_global_get_context(endpoint->global);
+
+ stream = calloc(1, sizeof(struct endpoint_stream));
+ if (!stream)
+ goto no_mem;
+
+ props = pw_properties_new(NULL, NULL);
+ if (!props)
+ goto no_mem;
+
+ pw_properties_update_keys(props, &endpoint->props->dict, keys);
+ if (info && info->props)
+ pw_properties_update_keys(props, info->props, keys);
+
+ if (endpoint_stream_init(stream, stream_id, endpoint->info.id,
+ this, context, props) < 0)
+ goto no_mem;
+
+ spa_list_append(&this->streams, &stream->link);
+ }
+ else if (change_mask & PW_CLIENT_ENDPOINT_STREAM_UPDATE_DESTROYED) {
+ endpoint_stream_clear(stream);
+ spa_list_remove(&stream->link);
+ free(stream);
+ stream = NULL;
+ }
+
+ return stream ?
+ endpoint_stream_update(stream, change_mask, n_params, params, info)
+ : 0;
+
+ no_mem:
+ pw_properties_free(props);
+ free(stream);
+ pw_log_error(NAME" %p: cannot update stream: no memory", this);
+ pw_resource_errorf(this->resource, -ENOMEM,
+ NAME" %p: cannot update stream: no memory", this);
+ return -ENOMEM;
+}
+
+static const struct pw_client_endpoint_methods methods = {
+ PW_VERSION_CLIENT_ENDPOINT_METHODS,
+ .update = client_endpoint_update,
+ .stream_update = client_endpoint_stream_update,
+};
+
+static void client_endpoint_destroy(void *data)
+{
+ struct client_endpoint *this = data;
+ struct endpoint_stream *s;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ spa_list_consume(s, &this->streams, link) {
+ endpoint_stream_clear(s);
+ spa_list_remove(&s->link);
+ free(s);
+ }
+ endpoint_clear(&this->endpoint);
+ spa_hook_remove(&this->resource_listener);
+
+ free(this);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_endpoint_destroy,
+};
+
+static void *create_object(void *data,
+ struct pw_resource *owner_resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_impl_factory *factory = d->factory;
+ struct client_endpoint *this;
+ struct pw_impl_client *owner = pw_resource_get_client(owner_resource);
+ struct pw_context *context = pw_impl_client_get_context(owner);
+
+ this = calloc(1, sizeof(struct client_endpoint));
+ if (this == NULL)
+ goto no_mem;
+
+ spa_list_init(&this->streams);
+
+ pw_log_debug(NAME" %p: new", this);
+
+ if (!properties)
+ properties = pw_properties_new(NULL, NULL);
+ if (!properties)
+ goto no_mem;
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(owner)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(factory)->id);
+
+ this->resource = pw_resource_new(owner, new_id, PW_PERM_ALL, type, version, 0);
+ if (this->resource == NULL)
+ goto no_mem;
+
+ if (endpoint_init(&this->endpoint, this, context, properties) < 0)
+ goto no_mem;
+
+ pw_resource_add_listener(this->resource, &this->resource_listener,
+ &resource_events, this);
+ pw_resource_add_object_listener(this->resource, &this->object_listener,
+ &methods, this);
+
+ return this;
+
+ no_mem:
+ pw_properties_free(properties);
+ if (this && this->resource)
+ pw_resource_destroy(this->resource);
+ free(this);
+ pw_log_error("can't create client endpoint: no memory");
+ pw_resource_error(owner_resource, -ENOMEM,
+ "can't create client endpoint: no memory");
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int client_endpoint_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ factory = pw_context_create_factory(context,
+ "client-endpoint",
+ PW_TYPE_INTERFACE_ClientEndpoint,
+ PW_VERSION_CLIENT_ENDPOINT,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -ENOMEM;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/module-session-manager/client-endpoint/client-endpoint.h b/src/modules/module-session-manager/client-endpoint/client-endpoint.h
new file mode 100644
index 0000000..bc5630d
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/client-endpoint.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_CLIENT_ENDPOINT_H
+#define MODULE_SESSION_MANAGER_CLIENT_ENDPOINT_H
+
+#include "endpoint.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_endpoint {
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+ struct endpoint endpoint;
+ struct spa_list streams;
+};
+
+#define pw_client_endpoint_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_client_endpoint_events,m,v,__VA_ARGS__)
+#define pw_client_endpoint_resource_set_id(r,...) \
+ pw_client_endpoint_resource(r,set_id,0,__VA_ARGS__)
+#define pw_client_endpoint_resource_set_session_id(r,...) \
+ pw_client_endpoint_resource(r,set_session_id,0,__VA_ARGS__)
+#define pw_client_endpoint_resource_set_param(r,...) \
+ pw_client_endpoint_resource(r,set_param,0,__VA_ARGS__)
+#define pw_client_endpoint_resource_stream_set_param(r,...) \
+ pw_client_endpoint_resource(r,stream_set_param,0,__VA_ARGS__)
+#define pw_client_endpoint_resource_create_link(r,...) \
+ pw_client_endpoint_resource(r,create_link,0,__VA_ARGS__)
+
+int client_endpoint_factory_init(struct pw_impl_module *module);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_CLIENT_ENDPOINT_H */
diff --git a/src/modules/module-session-manager/client-endpoint/endpoint-stream.c b/src/modules/module-session-manager/client-endpoint/endpoint-stream.c
new file mode 100644
index 0000000..8dde6f7
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/endpoint-stream.c
@@ -0,0 +1,352 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/pod/filter.h>
+
+#include "endpoint-stream.h"
+#include "client-endpoint.h"
+
+#define NAME "endpoint-stream"
+
+struct resource_data {
+ struct endpoint_stream *stream;
+ struct spa_hook object_listener;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+#define pw_endpoint_stream_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_stream_events,m,v,__VA_ARGS__)
+#define pw_endpoint_stream_resource_info(r,...) \
+ pw_endpoint_stream_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_stream_resource_param(r,...) \
+ pw_endpoint_stream_resource(r,param,0,__VA_ARGS__)
+
+static int endpoint_stream_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_stream *this = data->stream;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ while (true) {
+ index = next++;
+ if (index >= this->n_params)
+ break;
+
+ param = this->params[index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", this, seq, index);
+
+ pw_endpoint_stream_resource_param(resource, seq, id, index, next, result);
+
+ if (++count == num)
+ break;
+ }
+ return 0;
+}
+
+static int endpoint_stream_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ data->stream, pw_resource_get_id(resource), ids[i]);
+ endpoint_stream_enum_params(resource, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int endpoint_stream_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_stream *this = data->stream;
+
+ pw_client_endpoint_resource_set_param(this->client_ep->resource,
+ id, flags, param);
+
+ return 0;
+}
+
+static const struct pw_endpoint_stream_methods methods = {
+ PW_VERSION_ENDPOINT_STREAM_METHODS,
+ .subscribe_params = endpoint_stream_subscribe_params,
+ .enum_params = endpoint_stream_enum_params,
+ .set_param = endpoint_stream_set_param,
+};
+
+struct emit_param_data {
+ struct endpoint_stream *this;
+ struct spa_pod *param;
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct emit_param_data *d = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == d->id) {
+ pw_endpoint_stream_resource_param(resource, 1,
+ d->id, d->index, d->next, d->param);
+ }
+ }
+ return 0;
+}
+
+static void endpoint_stream_notify_subscribed(struct endpoint_stream *this,
+ uint32_t index, uint32_t next)
+{
+ struct pw_global *global = this->global;
+ struct emit_param_data data;
+ struct spa_pod *param = this->params[index];
+
+ if (!param || !spa_pod_is_object (param))
+ return;
+
+ data.this = this;
+ data.param = param;
+ data.id = SPA_POD_OBJECT_ID (param);
+ data.index = index;
+ data.next = next;
+
+ pw_global_for_each_resource(global, emit_param, &data);
+}
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ struct endpoint_stream *this = data;
+ pw_endpoint_stream_resource_info(resource, &this->info);
+ return 0;
+}
+
+int endpoint_stream_update(struct endpoint_stream *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info)
+{
+ if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_PARAMS) {
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, n_params, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ free(this->params);
+ this->params = NULL;
+ this->n_params = 0;
+ goto no_mem;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++) {
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ endpoint_stream_notify_subscribed(this, i, i+1);
+ }
+ }
+
+ if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_INFO) {
+ if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_LINK_PARAMS) {
+ free(this->info.link_params);
+ this->info.link_params = spa_pod_copy(info->link_params);
+ }
+
+ if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS)
+ pw_properties_update(this->props, info->props);
+
+ if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS) {
+ this->info.n_params = info->n_params;
+ if (info->n_params == 0) {
+ free(this->info.params);
+ this->info.params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->info.params, info->n_params, sizeof(struct spa_param_info));
+ if (p == NULL) {
+ free(this->info.params);
+ this->info.params = NULL;
+ this->info.n_params = 0;
+ goto no_mem;
+ }
+ this->info.params = p;
+ memcpy(this->info.params, info->params, info->n_params * sizeof(struct spa_param_info));
+ }
+ }
+
+ if (!this->info.name)
+ this->info.name = info->name ? strdup(info->name) : NULL;
+
+ this->info.change_mask = info->change_mask;
+ pw_global_for_each_resource(this->global, emit_info, this);
+ this->info.change_mask = 0;
+ }
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't update: no memory");
+ pw_resource_error(this->client_ep->resource, -ENOMEM,
+ NAME" can't update: no memory");
+ return -ENOMEM;
+}
+
+static int endpoint_stream_bind(void *_data, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct endpoint_stream *this = _data;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ pw_global_get_type(global), version, sizeof(*data));
+ if (resource == NULL)
+ goto no_mem;
+
+ data = pw_resource_get_user_data(resource);
+ data->stream = this;
+ pw_resource_add_object_listener(resource, &data->object_listener,
+ &methods, resource);
+
+ pw_log_debug(NAME" %p: bound to %d", this, pw_resource_get_id(resource));
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_ALL;
+ pw_endpoint_stream_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't create resource: no memory");
+ pw_resource_error(this->client_ep->resource, -ENOMEM,
+ NAME" can't create resource: no memory");
+ return -ENOMEM;
+}
+
+int endpoint_stream_init(struct endpoint_stream *this,
+ uint32_t id, uint32_t endpoint_id,
+ struct client_endpoint *client_ep,
+ struct pw_context *context,
+ struct pw_properties *properties)
+{
+ pw_log_debug(NAME" %p: new", this);
+
+ this->client_ep = client_ep;
+ this->id = id;
+ this->props = properties;
+
+ pw_properties_setf(properties, PW_KEY_ENDPOINT_ID, "%u", endpoint_id);
+
+ properties = pw_properties_copy(properties);
+ if (!properties)
+ goto no_mem;
+
+ this->global = pw_global_new (context,
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ properties, endpoint_stream_bind, this);
+ if (!this->global)
+ goto no_mem;
+
+ pw_properties_setf(this->props, PW_KEY_OBJECT_ID, "%u",
+ pw_global_get_id(this->global));
+ pw_properties_setf(this->props, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+
+ this->info.version = PW_VERSION_ENDPOINT_STREAM_INFO;
+ this->info.id = pw_global_get_id(this->global);
+ this->info.endpoint_id = endpoint_id;
+ this->info.props = &this->props->dict;
+
+ return pw_global_register(this->global);
+
+ no_mem:
+ pw_log_error(NAME" - can't create - out of memory");
+ return -ENOMEM;
+}
+
+void endpoint_stream_clear(struct endpoint_stream *this)
+{
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ pw_global_destroy(this->global);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ free(this->params);
+
+ free(this->info.name);
+ free(this->info.link_params);
+ free(this->info.params);
+
+ pw_properties_free(this->props);
+}
diff --git a/src/modules/module-session-manager/client-endpoint/endpoint-stream.h b/src/modules/module-session-manager/client-endpoint/endpoint-stream.h
new file mode 100644
index 0000000..7172287
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/endpoint-stream.h
@@ -0,0 +1,64 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_ENDPOINT_STREAM_H
+#define MODULE_SESSION_MANAGER_ENDPOINT_STREAM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_endpoint;
+
+struct endpoint_stream {
+ struct spa_list link;
+ struct client_endpoint *client_ep;
+ struct pw_global *global;
+ uint32_t id; /* endpoint-local stream id */
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct pw_endpoint_stream_info info;
+ struct pw_properties *props; /* wrapper of info.props */
+};
+
+int endpoint_stream_init(struct endpoint_stream *this,
+ uint32_t id, uint32_t endpoint_id,
+ struct client_endpoint *client_ep,
+ struct pw_context *context,
+ struct pw_properties *properties);
+
+void endpoint_stream_clear(struct endpoint_stream *this);
+
+int endpoint_stream_update(struct endpoint_stream *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_ENDPOINT_STREAM_H */
diff --git a/src/modules/module-session-manager/client-endpoint/endpoint.c b/src/modules/module-session-manager/client-endpoint/endpoint.c
new file mode 100644
index 0000000..aa13989
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/endpoint.c
@@ -0,0 +1,385 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/pod/filter.h>
+
+#include "endpoint.h"
+#include "client-endpoint.h"
+
+#define NAME "endpoint"
+
+struct resource_data {
+ struct endpoint *endpoint;
+ struct spa_hook object_listener;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+#define pw_endpoint_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_events,m,v,__VA_ARGS__)
+#define pw_endpoint_resource_info(r,...) \
+ pw_endpoint_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_resource_param(r,...) \
+ pw_endpoint_resource(r,param,0,__VA_ARGS__)
+
+static int endpoint_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint *this = data->endpoint;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", this, id, start, num);
+
+ while (true) {
+ index = next++;
+ if (index >= this->n_params)
+ break;
+
+ param = this->params[index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", this, seq, index);
+
+ pw_endpoint_resource_param(resource, seq, id, index, next, result);
+
+ if (++count == num)
+ break;
+ }
+ return 0;
+}
+
+static int endpoint_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ data->endpoint, pw_resource_get_id(resource), ids[i]);
+ endpoint_enum_params(resource, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int endpoint_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint *this = data->endpoint;
+
+ pw_log_debug("%p", this);
+ pw_client_endpoint_resource_set_param(this->client_ep->resource,
+ id, flags, param);
+
+ return 0;
+}
+
+static int endpoint_create_link(void *object, const struct spa_dict *props)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint *this = data->endpoint;
+
+ pw_log_debug("%p", this);
+ pw_client_endpoint_resource_create_link(this->client_ep->resource,
+ props);
+
+ return 0;
+}
+
+static const struct pw_endpoint_methods methods = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .subscribe_params = endpoint_subscribe_params,
+ .enum_params = endpoint_enum_params,
+ .set_param = endpoint_set_param,
+ .create_link = endpoint_create_link,
+};
+
+struct emit_param_data {
+ struct endpoint *this;
+ struct spa_pod *param;
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct emit_param_data *d = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == d->id) {
+ pw_endpoint_resource_param(resource, 1,
+ d->id, d->index, d->next, d->param);
+ }
+ }
+ return 0;
+}
+
+static void endpoint_notify_subscribed(struct endpoint *this,
+ uint32_t index, uint32_t next)
+{
+ struct pw_global *global = this->global;
+ struct emit_param_data data;
+ struct spa_pod *param = this->params[index];
+
+ if (!param || !spa_pod_is_object (param))
+ return;
+
+ data.this = this;
+ data.param = param;
+ data.id = SPA_POD_OBJECT_ID (param);
+ data.index = index;
+ data.next = next;
+
+ pw_global_for_each_resource(global, emit_param, &data);
+}
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ struct endpoint *this = data;
+ pw_endpoint_resource_info(resource, &this->info);
+ return 0;
+}
+
+int endpoint_update(struct endpoint *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info)
+{
+ if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_PARAMS) {
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, n_params, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ free(this->params);
+ this->params = NULL;
+ this->n_params = 0;
+ goto no_mem;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++) {
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ endpoint_notify_subscribed(this, i, i+1);
+ }
+ }
+
+ if (change_mask & PW_CLIENT_ENDPOINT_UPDATE_INFO) {
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_STREAMS)
+ this->info.n_streams = info->n_streams;
+
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_SESSION)
+ this->info.session_id = info->session_id;
+
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS)
+ pw_properties_update(this->props, info->props);
+
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ this->info.n_params = info->n_params;
+ if (info->n_params == 0) {
+ free(this->info.params);
+ this->info.params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->info.params, info->n_params, sizeof(struct spa_param_info));
+ if (p == NULL) {
+ free(this->info.params);
+ this->info.params = NULL;
+ this->info.n_params = 0;
+ goto no_mem;
+ }
+ this->info.params = p;
+ memcpy(this->info.params, info->params, info->n_params * sizeof(struct spa_param_info));
+ }
+ }
+
+ if (!this->info.name) {
+ this->info.name = info->name ? strdup(info->name) : NULL;
+ this->info.media_class = info->media_class ? strdup(info->media_class) : NULL;
+ this->info.direction = info->direction;
+ this->info.flags = info->flags;
+ }
+
+ this->info.change_mask = info->change_mask;
+ pw_global_for_each_resource(this->global, emit_info, this);
+ this->info.change_mask = 0;
+ }
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't update: no memory");
+ pw_resource_error(this->client_ep->resource, -ENOMEM,
+ NAME" can't update: no memory");
+ return -ENOMEM;
+}
+
+static int endpoint_bind(void *_data, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct endpoint *this = _data;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ pw_global_get_type(global), version, sizeof(*data));
+ if (resource == NULL)
+ goto no_mem;
+
+ data = pw_resource_get_user_data(resource);
+ data->endpoint = this;
+ pw_resource_add_object_listener(resource, &data->object_listener,
+ &methods, resource);
+
+ pw_log_debug(NAME" %p: bound to %d", this, pw_resource_get_id(resource));
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_ENDPOINT_CHANGE_MASK_ALL;
+ pw_endpoint_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't create resource: no memory");
+ pw_resource_error(this->client_ep->resource, -ENOMEM,
+ NAME" can't create resource: no memory");
+ return -ENOMEM;
+}
+
+int endpoint_init(struct endpoint *this,
+ struct client_endpoint *client_ep,
+ struct pw_context *context,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_DEVICE_ID,
+ PW_KEY_NODE_ID,
+ PW_KEY_MEDIA_CLASS,
+ PW_KEY_SESSION_ID,
+ PW_KEY_PRIORITY_SESSION,
+ PW_KEY_ENDPOINT_NAME,
+ PW_KEY_ENDPOINT_CLIENT_ID,
+ PW_KEY_ENDPOINT_ICON_NAME,
+ PW_KEY_ENDPOINT_MONITOR,
+ NULL
+ };
+
+ pw_log_debug(NAME" %p: new", this);
+
+ this->client_ep = client_ep;
+ this->props = properties;
+
+ this->global = pw_global_new (context,
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ NULL, endpoint_bind, this);
+ if (!this->global)
+ goto no_mem;
+
+ pw_properties_setf(this->props, PW_KEY_OBJECT_ID, "%u",
+ pw_global_get_id(this->global));
+ pw_properties_setf(this->props, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+
+ this->info.version = PW_VERSION_ENDPOINT_INFO;
+ this->info.id = pw_global_get_id(this->global);
+ this->info.props = &this->props->dict;
+
+ pw_global_update_keys(this->global, &this->props->dict, keys);
+
+ pw_resource_set_bound_id(client_ep->resource, this->info.id);
+
+ return pw_global_register(this->global);
+
+ no_mem:
+ pw_log_error(NAME" - can't create - out of memory");
+ return -ENOMEM;
+}
+
+void endpoint_clear(struct endpoint *this)
+{
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ pw_global_destroy(this->global);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ free(this->params);
+
+ free(this->info.name);
+ free(this->info.media_class);
+ free(this->info.params);
+
+ pw_properties_free(this->props);
+}
diff --git a/src/modules/module-session-manager/client-endpoint/endpoint.h b/src/modules/module-session-manager/client-endpoint/endpoint.h
new file mode 100644
index 0000000..5ceff39
--- /dev/null
+++ b/src/modules/module-session-manager/client-endpoint/endpoint.h
@@ -0,0 +1,61 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_ENDPOINT_H
+#define MODULE_SESSION_MANAGER_ENDPOINT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_endpoint;
+
+struct endpoint {
+ struct client_endpoint *client_ep;
+ struct pw_global *global;
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct pw_endpoint_info info;
+ struct pw_properties *props; /* wrapper of info.props */
+};
+
+int endpoint_init(struct endpoint *this,
+ struct client_endpoint *client_ep,
+ struct pw_context *context,
+ struct pw_properties *properties);
+
+void endpoint_clear(struct endpoint *this);
+
+int endpoint_update(struct endpoint *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_ENDPOINT_H */
diff --git a/src/modules/module-session-manager/client-session/client-session.c b/src/modules/module-session-manager/client-session/client-session.c
new file mode 100644
index 0000000..89997c9
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/client-session.c
@@ -0,0 +1,295 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <spa/utils/result.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include "client-session.h"
+#include "session.h"
+#include "endpoint-link.h"
+
+#define NAME "client-session"
+
+struct factory_data {
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+};
+
+static struct endpoint_link *find_link(struct client_session *this, uint32_t id)
+{
+ struct endpoint_link *l;
+ spa_list_for_each(l, &this->links, link) {
+ if (l->id == id)
+ return l;
+ }
+ return NULL;
+}
+
+static int client_session_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info)
+{
+ struct client_session *this = object;
+ struct session *session = &this->session;
+
+ return session_update(session, change_mask, n_params, params, info);
+}
+
+static int client_session_link_update(void *object,
+ uint32_t link_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info)
+{
+ struct client_session *this = object;
+ struct session *session = &this->session;
+ struct endpoint_link *link = find_link(this, link_id);
+ struct pw_properties *props = NULL;
+
+ if (!link) {
+ static const char * const keys[] = {
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_SESSION_ID,
+ PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT,
+ PW_KEY_ENDPOINT_LINK_OUTPUT_STREAM,
+ PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT,
+ PW_KEY_ENDPOINT_LINK_INPUT_STREAM,
+ NULL
+ };
+
+ struct pw_context *context = pw_global_get_context(session->global);
+
+ link = calloc(1, sizeof(struct endpoint_link));
+ if (!link)
+ goto no_mem;
+
+ props = pw_properties_new(NULL, NULL);
+ if (!props)
+ goto no_mem;
+ pw_properties_update_keys(props, &session->props->dict, keys);
+ if (info && info->props)
+ pw_properties_update_keys(props, info->props, keys);
+
+ if (endpoint_link_init(link, link_id, session->info.id,
+ this, context, props) < 0)
+ goto no_mem;
+
+ spa_list_append(&this->links, &link->link);
+ }
+ else if (change_mask & PW_CLIENT_SESSION_LINK_UPDATE_DESTROYED) {
+ endpoint_link_clear(link);
+ spa_list_remove(&link->link);
+ free(link);
+ link = NULL;
+ }
+
+ return link ?
+ endpoint_link_update(link, change_mask, n_params, params, info)
+ : 0;
+
+ no_mem:
+ pw_properties_free(props);
+ free(link);
+ pw_log_error(NAME" %p: cannot update link: no memory", this);
+ pw_resource_error(this->resource, -ENOMEM,
+ "cannot update link: no memory");
+ return -ENOMEM;
+}
+
+static const struct pw_client_session_methods methods = {
+ PW_VERSION_CLIENT_SESSION_METHODS,
+ .update = client_session_update,
+ .link_update = client_session_link_update,
+};
+
+static void client_session_destroy(void *data)
+{
+ struct client_session *this = data;
+ struct endpoint_link *l;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ spa_list_consume(l, &this->links, link) {
+ endpoint_link_clear(l);
+ spa_list_remove(&l->link);
+ free(l);
+ }
+ session_clear(&this->session);
+ spa_hook_remove(&this->resource_listener);
+
+ free(this);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_session_destroy,
+};
+
+static void *create_object(void *data,
+ struct pw_resource *owner_resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_impl_factory *factory = d->factory;
+ struct client_session *this;
+ struct pw_impl_client *owner = pw_resource_get_client(owner_resource);
+ struct pw_context *context = pw_impl_client_get_context(owner);
+
+ this = calloc(1, sizeof(struct client_session));
+ if (this == NULL)
+ goto no_mem;
+
+ spa_list_init(&this->links);
+
+ pw_log_debug(NAME" %p: new", this);
+
+ if (!properties)
+ properties = pw_properties_new(NULL, NULL);
+ if (!properties)
+ goto no_mem;
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(owner)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(factory)->id);
+
+ this->resource = pw_resource_new(owner, new_id, PW_PERM_ALL, type, version, 0);
+ if (this->resource == NULL)
+ goto no_mem;
+
+ if (session_init(&this->session, this, context, properties) < 0)
+ goto no_mem;
+
+ pw_resource_add_listener(this->resource, &this->resource_listener,
+ &resource_events, this);
+ pw_resource_add_object_listener(this->resource, &this->object_listener,
+ &methods, this);
+
+ return this;
+
+ no_mem:
+ pw_properties_free(properties);
+ if (this && this->resource)
+ pw_resource_destroy(this->resource);
+ free(this);
+ pw_log_error("can't create client session: no memory");
+ pw_resource_error(owner_resource, -ENOMEM,
+ "can't create client session: no memory");
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int client_session_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ factory = pw_context_create_factory(context,
+ "client-session",
+ PW_TYPE_INTERFACE_ClientSession,
+ PW_VERSION_CLIENT_SESSION,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -ENOMEM;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/module-session-manager/client-session/client-session.h b/src/modules/module-session-manager/client-session/client-session.h
new file mode 100644
index 0000000..fc6124c
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/client-session.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_CLIENT_SESSION_H
+#define MODULE_SESSION_MANAGER_CLIENT_SESSION_H
+
+#include "session.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_session {
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+ struct session session;
+ struct spa_list links;
+};
+
+#define pw_client_session_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_client_session_events,m,v,__VA_ARGS__)
+#define pw_client_session_resource_set_id(r,...) \
+ pw_client_session_resource(r,set_id,0,__VA_ARGS__)
+#define pw_client_session_resource_set_param(r,...) \
+ pw_client_session_resource(r,set_param,0,__VA_ARGS__)
+#define pw_client_session_resource_link_set_param(r,...) \
+ pw_client_session_resource(r,link_set_param,0,__VA_ARGS__)
+#define pw_client_session_resource_create_link(r,...) \
+ pw_client_session_resource(r,create_link,0,__VA_ARGS__)
+#define pw_client_session_resource_destroy_link(r,...) \
+ pw_client_session_resource(r,destroy_link,0,__VA_ARGS__)
+#define pw_client_session_resource_link_request_state(r,...) \
+ pw_client_session_resource(r,link_request_state,0,__VA_ARGS__)
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_CLIENT_SESSION_H */
diff --git a/src/modules/module-session-manager/client-session/endpoint-link.c b/src/modules/module-session-manager/client-session/endpoint-link.c
new file mode 100644
index 0000000..0bdbfc9
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/endpoint-link.c
@@ -0,0 +1,369 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/pod/filter.h>
+
+#include "endpoint-link.h"
+#include "client-session.h"
+
+#define NAME "endpoint-link"
+
+struct resource_data {
+ struct endpoint_link *link;
+ struct spa_hook object_listener;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+#define pw_endpoint_link_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_link_events,m,v,__VA_ARGS__)
+#define pw_endpoint_link_resource_info(r,...) \
+ pw_endpoint_link_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_link_resource_param(r,...) \
+ pw_endpoint_link_resource(r,param,0,__VA_ARGS__)
+
+static int endpoint_link_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_link *this = data->link;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ while (true) {
+ index = next++;
+ if (index >= this->n_params)
+ break;
+
+ param = this->params[index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", this, seq, index);
+
+ pw_endpoint_link_resource_param(resource, seq, id, index, next, result);
+
+ if (++count == num)
+ break;
+ }
+ return 0;
+}
+
+static int endpoint_link_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ data->link, pw_resource_get_id(resource), ids[i]);
+ endpoint_link_enum_params(resource, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int endpoint_link_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_link *this = data->link;
+
+ pw_client_session_resource_set_param(this->client_sess->resource,
+ id, flags, param);
+
+ return 0;
+}
+
+static int endpoint_link_request_state(void *object, enum pw_endpoint_link_state state)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct endpoint_link *this = data->link;
+
+ pw_client_session_resource_link_request_state(this->client_sess->resource,
+ this->id, state);
+
+ return 0;
+}
+
+static const struct pw_endpoint_link_methods methods = {
+ PW_VERSION_ENDPOINT_LINK_METHODS,
+ .subscribe_params = endpoint_link_subscribe_params,
+ .enum_params = endpoint_link_enum_params,
+ .set_param = endpoint_link_set_param,
+ .request_state = endpoint_link_request_state,
+};
+
+struct emit_param_data {
+ struct endpoint_link *this;
+ struct spa_pod *param;
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct emit_param_data *d = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == d->id) {
+ pw_endpoint_link_resource_param(resource, 1,
+ d->id, d->index, d->next, d->param);
+ }
+ }
+ return 0;
+}
+
+static void endpoint_link_notify_subscribed(struct endpoint_link *this,
+ uint32_t index, uint32_t next)
+{
+ struct pw_global *global = this->global;
+ struct emit_param_data data;
+ struct spa_pod *param = this->params[index];
+
+ if (!param || !spa_pod_is_object (param))
+ return;
+
+ data.this = this;
+ data.param = param;
+ data.id = SPA_POD_OBJECT_ID (param);
+ data.index = index;
+ data.next = next;
+
+ pw_global_for_each_resource(global, emit_param, &data);
+}
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ struct endpoint_link *this = data;
+ pw_endpoint_link_resource_info(resource, &this->info);
+ return 0;
+}
+
+int endpoint_link_update(struct endpoint_link *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info)
+{
+ if (change_mask & PW_CLIENT_SESSION_UPDATE_PARAMS) {
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, n_params, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ free(this->params);
+ this->params = NULL;
+ this->n_params = 0;
+ goto no_mem;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++) {
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ endpoint_link_notify_subscribed(this, i, i+1);
+ }
+ }
+
+ if (change_mask & PW_CLIENT_SESSION_UPDATE_INFO) {
+ if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_STATE) {
+ this->info.state = info->state;
+ free(this->info.error);
+ this->info.error = info->error ? strdup(info->error) : NULL;
+ }
+
+ if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PROPS)
+ pw_properties_update(this->props, info->props);
+
+ if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS) {
+ this->info.n_params = info->n_params;
+ if (info->n_params == 0) {
+ free(this->info.params);
+ this->info.params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->info.params, info->n_params, sizeof(struct spa_param_info));
+ if (p == NULL) {
+ free(this->info.params);
+ this->info.params = NULL;
+ this->info.n_params = 0;
+ goto no_mem;
+ }
+ this->info.params = p;
+ memcpy(this->info.params, info->params, info->n_params * sizeof(struct spa_param_info));
+ }
+ }
+
+ if (!this->info.output_endpoint_id) {
+ this->info.output_endpoint_id = info->output_endpoint_id;
+ this->info.output_stream_id = info->output_stream_id;
+ this->info.input_endpoint_id = info->input_endpoint_id;
+ this->info.input_stream_id = info->input_stream_id;
+ }
+
+ this->info.change_mask = info->change_mask;
+ pw_global_for_each_resource(this->global, emit_info, this);
+ this->info.change_mask = 0;
+ }
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" %p: can't update: no memory", this);
+ pw_resource_error(this->client_sess->resource, -ENOMEM,
+ "can't update: no memory");
+ return -ENOMEM;
+}
+
+static int endpoint_link_bind(void *_data, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct endpoint_link *this = _data;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ pw_global_get_type(global), version, sizeof(*data));
+ if (resource == NULL)
+ goto no_mem;
+
+ data = pw_resource_get_user_data(resource);
+ data->link = this;
+ pw_resource_add_object_listener(resource, &data->object_listener,
+ &methods, resource);
+
+ pw_log_debug(NAME" %p: bound to %d", this, pw_resource_get_id(resource));
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_ALL;
+ pw_endpoint_link_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" %p: can't create resource: no memory", this);
+ pw_resource_error(this->client_sess->resource, -ENOMEM,
+ "can't create resource: no memory");
+ return -ENOMEM;
+}
+
+int endpoint_link_init(struct endpoint_link *this,
+ uint32_t id, uint32_t session_id,
+ struct client_session *client_sess,
+ struct pw_context *context,
+ struct pw_properties *properties)
+{
+ pw_log_debug(NAME" %p: new", this);
+
+ this->client_sess = client_sess;
+ this->id = id;
+ this->props = properties;
+
+ pw_properties_setf(properties, PW_KEY_SESSION_ID, "%u", session_id);
+
+ properties = pw_properties_copy(properties);
+ if (!properties)
+ goto no_mem;
+
+ this->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ properties, endpoint_link_bind, this);
+ if (!this->global)
+ goto no_mem;
+
+ pw_properties_setf(this->props, PW_KEY_OBJECT_ID, "%u",
+ pw_global_get_id(this->global));
+ pw_properties_setf(this->props, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+
+ this->info.version = PW_VERSION_ENDPOINT_LINK_INFO;
+ this->info.id = pw_global_get_id(this->global);
+ this->info.session_id = session_id;
+ this->info.props = &this->props->dict;
+
+ return pw_global_register(this->global);
+
+ no_mem:
+ pw_log_error(NAME" - can't create - out of memory");
+ return -ENOMEM;
+}
+
+void endpoint_link_clear(struct endpoint_link *this)
+{
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ pw_global_destroy(this->global);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ free(this->params);
+
+ free(this->info.error);
+ free(this->info.params);
+
+ pw_properties_free(this->props);
+}
diff --git a/src/modules/module-session-manager/client-session/endpoint-link.h b/src/modules/module-session-manager/client-session/endpoint-link.h
new file mode 100644
index 0000000..c75f975
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/endpoint-link.h
@@ -0,0 +1,65 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_ENDPOINT_LINK_H
+#define MODULE_SESSION_MANAGER_ENDPOINT_LINK_H
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_session;
+
+struct endpoint_link {
+ struct spa_list link;
+ struct client_session *client_sess;
+ struct pw_global *global;
+ uint32_t id; /* session-local link id */
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct pw_endpoint_link_info info;
+ struct pw_properties *props; /* wrapper of info.props */
+};
+
+int endpoint_link_init(struct endpoint_link *this,
+ uint32_t id, uint32_t session_id,
+ struct client_session *client_sess,
+ struct pw_context *context,
+ struct pw_properties *properties);
+
+void endpoint_link_clear(struct endpoint_link *this);
+
+int endpoint_link_update(struct endpoint_link *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_ENDPOINT_LINK_H */
diff --git a/src/modules/module-session-manager/client-session/session.c b/src/modules/module-session-manager/client-session/session.c
new file mode 100644
index 0000000..87c1b96
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/session.c
@@ -0,0 +1,344 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/pod/filter.h>
+
+#include "session.h"
+#include "client-session.h"
+
+#define NAME "session"
+
+struct resource_data {
+ struct session *session;
+ struct spa_hook object_listener;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+#define pw_session_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_session_events,m,v,__VA_ARGS__)
+#define pw_session_resource_info(r,...) \
+ pw_session_resource(r,info,0,__VA_ARGS__)
+#define pw_session_resource_param(r,...) \
+ pw_session_resource(r,param,0,__VA_ARGS__)
+
+static int session_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct session *this = data->session;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ while (true) {
+ index = next++;
+ if (index >= this->n_params)
+ break;
+
+ param = this->params[index];
+
+ if (param == NULL || !spa_pod_is_object_id(param, id))
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", this, seq, index);
+
+ pw_session_resource_param(resource, seq, id, index, next, result);
+
+ if (++count == num)
+ break;
+ }
+ return 0;
+}
+
+static int session_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ data->session, pw_resource_get_id(resource), ids[i]);
+ session_enum_params(resource, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int session_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ struct session *this = data->session;
+
+ pw_client_session_resource_set_param(this->client_sess->resource,
+ id, flags, param);
+
+ return 0;
+}
+
+static const struct pw_session_methods methods = {
+ PW_VERSION_SESSION_METHODS,
+ .subscribe_params = session_subscribe_params,
+ .enum_params = session_enum_params,
+ .set_param = session_set_param,
+};
+
+struct emit_param_data {
+ struct session *this;
+ struct spa_pod *param;
+ uint32_t id;
+ uint32_t index;
+ uint32_t next;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct emit_param_data *d = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == d->id) {
+ pw_session_resource_param(resource, 1,
+ d->id, d->index, d->next, d->param);
+ }
+ }
+ return 0;
+}
+
+static void session_notify_subscribed(struct session *this,
+ uint32_t index, uint32_t next)
+{
+ struct pw_global *global = this->global;
+ struct emit_param_data data;
+ struct spa_pod *param = this->params[index];
+
+ if (!param || !spa_pod_is_object (param))
+ return;
+
+ data.this = this;
+ data.param = param;
+ data.id = SPA_POD_OBJECT_ID (param);
+ data.index = index;
+ data.next = next;
+
+ pw_global_for_each_resource(global, emit_param, &data);
+}
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ struct session *this = data;
+ pw_session_resource_info(resource, &this->info);
+ return 0;
+}
+
+int session_update(struct session *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info)
+{
+ if (change_mask & PW_CLIENT_SESSION_UPDATE_PARAMS) {
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: update %d params", this, n_params);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ this->n_params = n_params;
+ if (this->n_params == 0) {
+ free(this->params);
+ this->params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->params, n_params, sizeof(struct spa_pod*));
+ if (p == NULL) {
+ free(this->params);
+ this->params = NULL;
+ this->n_params = 0;
+ goto no_mem;
+ }
+ this->params = p;
+ }
+ for (i = 0; i < this->n_params; i++) {
+ this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL;
+ session_notify_subscribed(this, i, i+1);
+ }
+ }
+
+ if (change_mask & PW_CLIENT_SESSION_UPDATE_INFO) {
+ if (info->change_mask & PW_SESSION_CHANGE_MASK_PROPS)
+ pw_properties_update(this->props, info->props);
+
+ if (info->change_mask & PW_SESSION_CHANGE_MASK_PARAMS) {
+ this->info.n_params = info->n_params;
+ if (info->n_params == 0) {
+ free(this->info.params);
+ this->info.params = NULL;
+ } else {
+ void *p;
+ p = pw_reallocarray(this->info.params, info->n_params, sizeof(struct spa_param_info));
+ if (p == NULL) {
+ free(this->info.params);
+ this->info.params = NULL;
+ this->info.n_params = 0;
+ goto no_mem;
+ }
+ this->info.params = p;
+ memcpy(this->info.params, info->params, info->n_params * sizeof(struct spa_param_info));
+ }
+ }
+ this->info.change_mask = info->change_mask;
+ pw_global_for_each_resource(this->global, emit_info, this);
+ this->info.change_mask = 0;
+ }
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't update: no memory");
+ pw_resource_error(this->client_sess->resource, -ENOMEM,
+ NAME" can't update: no memory");
+ return -ENOMEM;
+}
+
+static int session_bind(void *_data, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct session *this = _data;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ pw_global_get_type(global), version, sizeof(*data));
+ if (resource == NULL)
+ goto no_mem;
+
+ data = pw_resource_get_user_data(resource);
+ data->session = this;
+
+ pw_resource_add_object_listener(resource, &data->object_listener,
+ &methods, resource);
+
+ pw_log_debug(NAME" %p: bound to %d", this, pw_resource_get_id(resource));
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_SESSION_CHANGE_MASK_ALL;
+ pw_session_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+ no_mem:
+ pw_log_error(NAME" can't create resource: no memory");
+ pw_resource_error(this->client_sess->resource, -ENOMEM,
+ NAME" can't create resource: no memory");
+ return -ENOMEM;
+}
+
+int session_init(struct session *this,
+ struct client_session *client_sess,
+ struct pw_context *context,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ NULL
+ };
+
+ pw_log_debug(NAME" %p: new", this);
+
+ this->client_sess = client_sess;
+ this->props = properties;
+
+ this->global = pw_global_new (context,
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ NULL, session_bind, this);
+ if (!this->global)
+ goto no_mem;
+
+ pw_properties_setf(this->props, PW_KEY_OBJECT_ID, "%u",
+ pw_global_get_id(this->global));
+ pw_properties_setf(this->props, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+
+ this->info.version = PW_VERSION_SESSION_INFO;
+ this->info.id = pw_global_get_id(this->global);
+ this->info.props = &this->props->dict;
+
+ pw_global_update_keys(this->global, &this->props->dict, keys);
+
+ pw_resource_set_bound_id(client_sess->resource, this->info.id);
+
+ return pw_global_register(this->global);
+
+ no_mem:
+ pw_log_error(NAME" - can't create - out of memory");
+ return -ENOMEM;
+}
+
+void session_clear(struct session *this)
+{
+ uint32_t i;
+
+ pw_log_debug(NAME" %p: destroy", this);
+
+ pw_global_destroy(this->global);
+
+ for (i = 0; i < this->n_params; i++)
+ free(this->params[i]);
+ free(this->params);
+
+ free(this->info.params);
+
+ pw_properties_free(this->props);
+}
diff --git a/src/modules/module-session-manager/client-session/session.h b/src/modules/module-session-manager/client-session/session.h
new file mode 100644
index 0000000..a94b18f
--- /dev/null
+++ b/src/modules/module-session-manager/client-session/session.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef MODULE_SESSION_MANAGER_SESSION_H
+#define MODULE_SESSION_MANAGER_SESSION_H
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct client_session;
+
+struct session {
+ struct client_session *client_sess;
+ struct pw_global *global;
+ uint32_t n_params;
+ struct spa_pod **params;
+ struct pw_session_info info;
+ struct pw_properties *props; /* wrapper of info.props */
+};
+
+int session_init(struct session *this,
+ struct client_session *client_sess,
+ struct pw_context *context,
+ struct pw_properties *properties);
+
+void session_clear(struct session *this);
+
+int session_update(struct session *this,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* MODULE_SESSION_MANAGER_SESSION_H */
diff --git a/src/modules/module-session-manager/endpoint-link.c b/src/modules/module-session-manager/endpoint-link.c
new file mode 100644
index 0000000..55ff580
--- /dev/null
+++ b/src/modules/module-session-manager/endpoint-link.c
@@ -0,0 +1,590 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/filter.h>
+
+#define MAX_PARAMS 32
+
+#define NAME "endpoint-link"
+
+struct pw_proxy *pw_core_endpoint_link_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct impl
+{
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ union {
+ struct pw_endpoint_link *link;
+ struct pw_resource *resource;
+ };
+ struct spa_hook resource_listener;
+ struct spa_hook link_listener;
+
+ struct pw_endpoint_link_info *cached_info;
+ struct spa_list cached_params;
+
+ int ping_seq;
+ bool registered;
+};
+
+struct param_data
+{
+ struct spa_list link;
+ uint32_t id;
+ struct pw_array params;
+};
+
+struct resource_data
+{
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook object_listener;
+
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+struct factory_data
+{
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_export_type export;
+};
+
+#define pw_endpoint_link_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_link_events,m,v,__VA_ARGS__)
+
+#define pw_endpoint_link_resource_info(r,...) \
+ pw_endpoint_link_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_link_resource_param(r,...) \
+ pw_endpoint_link_resource(r,param,0,__VA_ARGS__)
+
+static int method_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct param_data *pdata;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", impl, id, start, num);
+
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ while (true) {
+ index = next++;
+ if (index >= pw_array_get_len(&pdata->params, void*))
+ return 0;
+
+ param = *pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", impl, seq, index);
+
+ pw_endpoint_link_resource_param(d->resource, seq, id, index, next, result);
+
+ if (++count == num)
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int method_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(d->subscribe_ids));
+ d->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ d->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ impl, pw_resource_get_id(d->resource), ids[i]);
+ method_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int method_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ /* store only on the implementation; our cache will be updated
+ by the param event, since we are subscribed */
+ pw_endpoint_link_set_param(impl->link, id, flags, param);
+ return 0;
+}
+
+static int method_request_state(void *object, enum pw_endpoint_link_state state)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ pw_endpoint_link_request_state(impl->link, state);
+ return 0;
+}
+
+static const struct pw_endpoint_link_methods link_methods = {
+ PW_VERSION_ENDPOINT_LINK_METHODS,
+ .subscribe_params = method_subscribe_params,
+ .enum_params = method_enum_params,
+ .set_param = method_set_param,
+ .request_state = method_request_state,
+};
+
+static int global_bind(void *object, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_EndpointLink,
+ version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &link_methods, data);
+
+ impl->cached_info->change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_ALL;
+ pw_endpoint_link_resource_info(resource, impl->cached_info);
+ impl->cached_info->change_mask = 0;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+ free(impl);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static void impl_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct param_data *pdata, *tmp;
+
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->link_listener);
+ impl->resource = NULL;
+
+ /* clear cache */
+ if (impl->cached_info)
+ pw_endpoint_link_info_free(impl->cached_info);
+ spa_list_for_each_safe(pdata, tmp, &impl->cached_params, link) {
+ struct spa_pod **pod;
+ pw_array_for_each(pod, &pdata->params)
+ free(*pod);
+ pw_array_clear(&pdata->params);
+ spa_list_remove(&pdata->link);
+ free(pdata);
+ }
+
+ if (impl->global)
+ pw_global_destroy(impl->global);
+}
+
+static void register_global(struct impl *impl)
+{
+ impl->cached_info->id = pw_global_get_id (impl->global);
+ pw_resource_set_bound_id(impl->resource, impl->cached_info->id);
+ pw_global_register(impl->global);
+ impl->registered = true;
+}
+
+static void impl_resource_pong (void *data, int seq)
+{
+ struct impl *impl = data;
+
+ /* complete registration, if this was the initial sync */
+ if (!impl->registered && seq == impl->ping_seq) {
+ register_global(impl);
+ }
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ const struct pw_endpoint_link_info *info = data;
+ pw_endpoint_link_resource_info(resource, info);
+ return 0;
+}
+
+static void event_info(void *data, const struct pw_endpoint_link_info *info)
+{
+ struct impl *impl = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ uint32_t i;
+
+ /* figure out changes to params */
+ if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if ((!impl->cached_info ||
+ info->params[i].flags != impl->cached_info->params[i].flags)
+ && info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = info->params[i].id;
+ }
+ }
+
+ /* cache for new clients */
+ impl->cached_info = pw_endpoint_link_info_update (impl->cached_info, info);
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_info, (void*) info);
+
+ /* cache params & register */
+ if (n_changed_ids > 0) {
+ /* prepare params storage */
+ for (i = 0; i < n_changed_ids; i++) {
+ struct param_data *pdata = calloc(1, sizeof(struct param_data));
+ pdata->id = changed_ids[i];
+ pw_array_init(&pdata->params, sizeof(void*));
+ spa_list_append(&impl->cached_params, &pdata->link);
+ }
+
+ /* subscribe to impl */
+ pw_endpoint_link_subscribe_params(impl->link, changed_ids, n_changed_ids);
+
+ /* register asynchronously on the pong event */
+ impl->ping_seq = pw_resource_ping(impl->resource, 0);
+ }
+ else if (!impl->registered) {
+ register_global(impl);
+ }
+}
+
+struct param_event_args
+{
+ uint32_t id, index, next;
+ const struct spa_pod *param;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct param_event_args *args = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == args->id) {
+ pw_endpoint_link_resource_param(resource, 1,
+ args->id, args->index, args->next, args->param);
+ }
+ }
+ return 0;
+}
+
+static void event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct param_data *pdata;
+ struct spa_pod **pod;
+ struct param_event_args args = { id, index, next, param };
+
+ /* cache for new requests */
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ if (!pw_array_check_index(&pdata->params, index, void*)) {
+ while (pw_array_get_len(&pdata->params, void*) <= index)
+ pw_array_add_ptr(&pdata->params, NULL);
+ }
+
+ pod = pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+ free(*pod);
+ *pod = spa_pod_copy(param);
+ }
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_param, &args);
+}
+
+static const struct pw_endpoint_link_events link_events = {
+ PW_VERSION_ENDPOINT_LINK_EVENTS,
+ .info = event_info,
+ .param = event_param,
+};
+
+static void *link_new(struct pw_context *context,
+ struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ spa_list_init(&impl->cached_params);
+
+ /* handle destroy events */
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &impl_resource_events, impl);
+
+ /* handle implementation events -> cache + client resources */
+ pw_endpoint_link_add_listener(impl->link,
+ &impl->link_listener,
+ &link_events, impl);
+
+ /* global is not registered here on purpose;
+ we first cache info + params and then expose the global */
+
+ return impl;
+}
+
+static void *create_object(void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_resource *impl_resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ void *result;
+ int res;
+
+ impl_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (impl_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_resource_install_marshal(impl_resource, true);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -ENOMEM;
+ goto error_link;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ result = link_new(pw_impl_client_get_context(client), impl_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_link;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_link:
+ pw_log_error("can't create endpoint link: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create endpoint link: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(impl_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export.link);
+ d->module = NULL;
+
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int endpoint_link_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ factory = pw_context_create_factory(context,
+ "endpoint-link",
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+
+ data->export.type = PW_TYPE_INTERFACE_EndpointLink;
+ data->export.func = pw_core_endpoint_link_export;
+ if ((res = pw_context_register_export_type(context, &data->export)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-session-manager/endpoint-stream.c b/src/modules/module-session-manager/endpoint-stream.c
new file mode 100644
index 0000000..ce2abdb
--- /dev/null
+++ b/src/modules/module-session-manager/endpoint-stream.c
@@ -0,0 +1,581 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/filter.h>
+
+#define MAX_PARAMS 32
+
+#define NAME "endpoint-stream"
+
+struct pw_proxy *pw_core_endpoint_stream_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct impl
+{
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ union {
+ struct pw_endpoint_stream *stream;
+ struct pw_resource *resource;
+ };
+ struct spa_hook resource_listener;
+ struct spa_hook stream_listener;
+
+ struct pw_endpoint_stream_info *cached_info;
+ struct spa_list cached_params;
+
+ int ping_seq;
+ bool registered;
+};
+
+struct param_data
+{
+ struct spa_list link;
+ uint32_t id;
+ struct pw_array params;
+};
+
+struct resource_data
+{
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook object_listener;
+
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+struct factory_data
+{
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_export_type export;
+};
+
+#define pw_endpoint_stream_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_stream_events,m,v,__VA_ARGS__)
+
+#define pw_endpoint_stream_resource_info(r,...) \
+ pw_endpoint_stream_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_stream_resource_param(r,...) \
+ pw_endpoint_stream_resource(r,param,0,__VA_ARGS__)
+
+static int method_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct param_data *pdata;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", impl, id, start, num);
+
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ while (true) {
+ index = next++;
+ if (index >= pw_array_get_len(&pdata->params, void*))
+ return 0;
+
+ param = *pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", impl, seq, index);
+
+ pw_endpoint_stream_resource_param(d->resource, seq, id, index, next, result);
+
+ if (++count == num)
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int method_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(d->subscribe_ids));
+ d->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ d->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ impl, pw_resource_get_id(d->resource), ids[i]);
+ method_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int method_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ /* store only on the implementation; our cache will be updated
+ by the param event, since we are subscribed */
+ pw_endpoint_stream_set_param(impl->stream, id, flags, param);
+ return 0;
+}
+
+static const struct pw_endpoint_stream_methods stream_methods = {
+ PW_VERSION_ENDPOINT_STREAM_METHODS,
+ .subscribe_params = method_subscribe_params,
+ .enum_params = method_enum_params,
+ .set_param = method_set_param,
+};
+
+static int global_bind(void *object, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_EndpointStream,
+ version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &stream_methods, data);
+
+ impl->cached_info->change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_ALL;
+ pw_endpoint_stream_resource_info(resource, impl->cached_info);
+ impl->cached_info->change_mask = 0;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+ free(impl);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static void impl_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct param_data *pdata, *tmp;
+
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->stream_listener);
+ impl->resource = NULL;
+
+ /* clear cache */
+ if (impl->cached_info)
+ pw_endpoint_stream_info_free(impl->cached_info);
+ spa_list_for_each_safe(pdata, tmp, &impl->cached_params, link) {
+ struct spa_pod **pod;
+ pw_array_for_each(pod, &pdata->params)
+ free(*pod);
+ pw_array_clear(&pdata->params);
+ spa_list_remove(&pdata->link);
+ free(pdata);
+ }
+
+ if (impl->global)
+ pw_global_destroy(impl->global);
+}
+
+static void register_global(struct impl *impl)
+{
+ impl->cached_info->id = pw_global_get_id (impl->global);
+ pw_resource_set_bound_id(impl->resource, impl->cached_info->id);
+ pw_global_register(impl->global);
+ impl->registered = true;
+}
+
+static void impl_resource_pong (void *data, int seq)
+{
+ struct impl *impl = data;
+
+ /* complete registration, if this was the initial sync */
+ if (!impl->registered && seq == impl->ping_seq) {
+ register_global(impl);
+ }
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ const struct pw_endpoint_stream_info *info = data;
+ pw_endpoint_stream_resource_info(resource, info);
+ return 0;
+}
+
+static void event_info(void *data, const struct pw_endpoint_stream_info *info)
+{
+ struct impl *impl = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ uint32_t i;
+
+ /* figure out changes to params */
+ if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if ((!impl->cached_info ||
+ info->params[i].flags != impl->cached_info->params[i].flags)
+ && info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = info->params[i].id;
+ }
+ }
+
+ /* cache for new clients */
+ impl->cached_info = pw_endpoint_stream_info_update (impl->cached_info, info);
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_info, (void*) info);
+
+ /* cache params & register */
+ if (n_changed_ids > 0) {
+ /* prepare params storage */
+ for (i = 0; i < n_changed_ids; i++) {
+ struct param_data *pdata = calloc(1, sizeof(struct param_data));
+ pdata->id = changed_ids[i];
+ pw_array_init(&pdata->params, sizeof(void*));
+ spa_list_append(&impl->cached_params, &pdata->link);
+ }
+
+ /* subscribe to impl */
+ pw_endpoint_stream_subscribe_params(impl->stream, changed_ids, n_changed_ids);
+
+ /* register asynchronously on the pong event */
+ impl->ping_seq = pw_resource_ping(impl->resource, 0);
+ }
+ else if (!impl->registered) {
+ register_global(impl);
+ }
+}
+
+struct param_event_args
+{
+ uint32_t id, index, next;
+ const struct spa_pod *param;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct param_event_args *args = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == args->id) {
+ pw_endpoint_stream_resource_param(resource, 1,
+ args->id, args->index, args->next, args->param);
+ }
+ }
+ return 0;
+}
+
+static void event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct param_data *pdata;
+ struct spa_pod **pod;
+ struct param_event_args args = { id, index, next, param };
+
+ /* cache for new requests */
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ if (!pw_array_check_index(&pdata->params, index, void*)) {
+ while (pw_array_get_len(&pdata->params, void*) <= index)
+ pw_array_add_ptr(&pdata->params, NULL);
+ }
+
+ pod = pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+ free(*pod);
+ *pod = spa_pod_copy(param);
+ }
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_param, &args);
+}
+
+static const struct pw_endpoint_stream_events stream_events = {
+ PW_VERSION_ENDPOINT_STREAM_EVENTS,
+ .info = event_info,
+ .param = event_param,
+};
+
+static void *stream_new(struct pw_context *context,
+ struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ spa_list_init(&impl->cached_params);
+
+ /* handle destroy events */
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &impl_resource_events, impl);
+
+ /* handle implementation events -> cache + client resources */
+ pw_endpoint_stream_add_listener(impl->stream,
+ &impl->stream_listener,
+ &stream_events, impl);
+
+ /* global is not registered here on purpose;
+ we first cache info + params and then expose the global */
+
+ return impl;
+}
+
+static void *create_object(void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_resource *impl_resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ void *result;
+ int res;
+
+ impl_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (impl_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_resource_install_marshal(impl_resource, true);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -ENOMEM;
+ goto error_stream;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ result = stream_new(pw_impl_client_get_context(client), impl_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_stream;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_stream:
+ pw_log_error("can't create endpoint stream: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create endpoint stream: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(impl_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export.link);
+ d->module = NULL;
+
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int endpoint_stream_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ factory = pw_context_create_factory(context,
+ "endpoint-stream",
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+
+ data->export.type = PW_TYPE_INTERFACE_EndpointStream;
+ data->export.func = pw_core_endpoint_stream_export;
+ if ((res = pw_context_register_export_type(context, &data->export)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-session-manager/endpoint.c b/src/modules/module-session-manager/endpoint.c
new file mode 100644
index 0000000..752a529
--- /dev/null
+++ b/src/modules/module-session-manager/endpoint.c
@@ -0,0 +1,590 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/filter.h>
+
+#define MAX_PARAMS 32
+
+#define NAME "endpoint"
+
+struct pw_proxy *pw_core_endpoint_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct impl
+{
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ union {
+ struct pw_endpoint *endpoint;
+ struct pw_resource *resource;
+ };
+ struct spa_hook resource_listener;
+ struct spa_hook endpoint_listener;
+
+ struct pw_endpoint_info *cached_info;
+ struct spa_list cached_params;
+
+ int ping_seq;
+ bool registered;
+};
+
+struct param_data
+{
+ struct spa_list link;
+ uint32_t id;
+ struct pw_array params;
+};
+
+struct resource_data
+{
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook object_listener;
+
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+struct factory_data
+{
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_export_type export;
+};
+
+#define pw_endpoint_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_endpoint_events,m,v,__VA_ARGS__)
+
+#define pw_endpoint_resource_info(r,...) \
+ pw_endpoint_resource(r,info,0,__VA_ARGS__)
+#define pw_endpoint_resource_param(r,...) \
+ pw_endpoint_resource(r,param,0,__VA_ARGS__)
+
+static int method_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct param_data *pdata;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", impl, id, start, num);
+
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ while (true) {
+ index = next++;
+ if (index >= pw_array_get_len(&pdata->params, void*))
+ return 0;
+
+ param = *pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", impl, seq, index);
+
+ pw_endpoint_resource_param(d->resource, seq, id, index, next, result);
+
+ if (++count == num)
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int method_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(d->subscribe_ids));
+ d->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ d->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ impl, pw_resource_get_id(d->resource), ids[i]);
+ method_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int method_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ /* store only on the implementation; our cache will be updated
+ by the param event, since we are subscribed */
+ pw_endpoint_set_param(impl->endpoint, id, flags, param);
+ return 0;
+}
+
+static int method_create_link(void *object, const struct spa_dict *props)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ pw_endpoint_create_link(impl->endpoint, props);
+ return 0;
+}
+
+static const struct pw_endpoint_methods endpoint_methods = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .subscribe_params = method_subscribe_params,
+ .enum_params = method_enum_params,
+ .set_param = method_set_param,
+ .create_link = method_create_link,
+};
+
+static int global_bind(void *object, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_Endpoint,
+ version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &endpoint_methods, data);
+
+ impl->cached_info->change_mask = PW_ENDPOINT_CHANGE_MASK_ALL;
+ pw_endpoint_resource_info(resource, impl->cached_info);
+ impl->cached_info->change_mask = 0;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+ free(impl);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static void impl_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct param_data *pdata, *tmp;
+
+ spa_hook_remove(&impl->resource_listener);
+ spa_hook_remove(&impl->endpoint_listener);
+ impl->resource = NULL;
+
+ /* clear cache */
+ if (impl->cached_info)
+ pw_endpoint_info_free(impl->cached_info);
+ spa_list_for_each_safe(pdata, tmp, &impl->cached_params, link) {
+ struct spa_pod **pod;
+ pw_array_for_each(pod, &pdata->params)
+ free(*pod);
+ pw_array_clear(&pdata->params);
+ spa_list_remove(&pdata->link);
+ free(pdata);
+ }
+
+ if (impl->global)
+ pw_global_destroy(impl->global);
+}
+
+static void register_global(struct impl *impl)
+{
+ impl->cached_info->id = pw_global_get_id (impl->global);
+ pw_resource_set_bound_id(impl->resource, impl->cached_info->id);
+ pw_global_register(impl->global);
+ impl->registered = true;
+}
+
+static void impl_resource_pong (void *data, int seq)
+{
+ struct impl *impl = data;
+
+ /* complete registration, if this was the initial sync */
+ if (!impl->registered && seq == impl->ping_seq) {
+ register_global(impl);
+ }
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ const struct pw_endpoint_info *info = data;
+ pw_endpoint_resource_info(resource, info);
+ return 0;
+}
+
+static void event_info(void *data, const struct pw_endpoint_info *info)
+{
+ struct impl *impl = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ uint32_t i;
+
+ /* figure out changes to params */
+ if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if ((!impl->cached_info ||
+ info->params[i].flags != impl->cached_info->params[i].flags)
+ && info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = info->params[i].id;
+ }
+ }
+
+ /* cache for new clients */
+ impl->cached_info = pw_endpoint_info_update (impl->cached_info, info);
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_info, (void*) info);
+
+ /* cache params & register */
+ if (n_changed_ids > 0) {
+ /* prepare params storage */
+ for (i = 0; i < n_changed_ids; i++) {
+ struct param_data *pdata = calloc(1, sizeof(struct param_data));
+ pdata->id = changed_ids[i];
+ pw_array_init(&pdata->params, sizeof(void*));
+ spa_list_append(&impl->cached_params, &pdata->link);
+ }
+
+ /* subscribe to impl */
+ pw_endpoint_subscribe_params(impl->endpoint, changed_ids, n_changed_ids);
+
+ /* register asynchronously on the pong event */
+ impl->ping_seq = pw_resource_ping(impl->resource, 0);
+ }
+ else if (!impl->registered) {
+ register_global(impl);
+ }
+}
+
+struct param_event_args
+{
+ uint32_t id, index, next;
+ const struct spa_pod *param;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct param_event_args *args = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == args->id) {
+ pw_endpoint_resource_param(resource, 1,
+ args->id, args->index, args->next, args->param);
+ }
+ }
+ return 0;
+}
+
+static void event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct param_data *pdata;
+ struct spa_pod **pod;
+ struct param_event_args args = { id, index, next, param };
+
+ /* cache for new requests */
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ if (!pw_array_check_index(&pdata->params, index, void*)) {
+ while (pw_array_get_len(&pdata->params, void*) <= index)
+ pw_array_add_ptr(&pdata->params, NULL);
+ }
+
+ pod = pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+ free(*pod);
+ *pod = spa_pod_copy(param);
+ }
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_param, &args);
+}
+
+static const struct pw_endpoint_events endpoint_events = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = event_info,
+ .param = event_param,
+};
+
+static void *endpoint_new(struct pw_context *context,
+ struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ spa_list_init(&impl->cached_params);
+
+ /* handle destroy events */
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &impl_resource_events, impl);
+
+ /* handle implementation events -> cache + client resources */
+ pw_endpoint_add_listener(impl->endpoint,
+ &impl->endpoint_listener,
+ &endpoint_events, impl);
+
+ /* global is not registered here on purpose;
+ we first cache info + params and then expose the global */
+
+ return impl;
+}
+
+static void *create_object(void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_resource *impl_resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ void *result;
+ int res;
+
+ impl_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (impl_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_resource_install_marshal(impl_resource, true);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -ENOMEM;
+ goto error_endpoint;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ result = endpoint_new(pw_impl_client_get_context(client), impl_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_endpoint;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_endpoint:
+ pw_log_error("can't create endpoint: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create endpoint: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(impl_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export.link);
+
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int endpoint_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ factory = pw_context_create_factory(context,
+ "endpoint",
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+
+ data->export.type = PW_TYPE_INTERFACE_Endpoint;
+ data->export.func = pw_core_endpoint_export;
+ if ((res = pw_context_register_export_type(context, &data->export)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-session-manager/protocol-native.c b/src/modules/module-session-manager/protocol-native.c
new file mode 100644
index 0000000..6981252
--- /dev/null
+++ b/src/modules/module-session-manager/protocol-native.c
@@ -0,0 +1,3083 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/protocol-native.h>
+
+#define MAX_DICT 1024
+#define MAX_PARAMS 4096
+#define MAX_PARAM_INFO 128
+
+static void push_dict(struct spa_pod_builder *b, const struct spa_dict *dict)
+{
+ struct spa_pod_frame f;
+ uint32_t n_items;
+ uint32_t i;
+
+ n_items = dict ? dict->n_items : 0;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b, SPA_POD_Int(n_items), NULL);
+ for (i = 0; i < n_items; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_String(dict->items[i].key),
+ SPA_POD_String(dict->items[i].value),
+ NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define parse_dict(p, f, dict) \
+do { \
+ uint32_t i; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, SPA_POD_Int(&(dict)->n_items), NULL) < 0) \
+ return -EINVAL; \
+ \
+ if ((dict)->n_items > 0) { \
+ if ((dict)->n_items > MAX_DICT) \
+ return -ENOSPC; \
+ (dict)->items = alloca((dict)->n_items * sizeof(struct spa_dict_item)); \
+ for (i = 0; i < (dict)->n_items; i++) { \
+ if (spa_pod_parser_get(p, \
+ SPA_POD_String(&(dict)->items[i].key), \
+ SPA_POD_String(&(dict)->items[i].value), \
+ NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+static void push_param_infos(struct spa_pod_builder *b, uint32_t n_params,
+ const struct spa_param_info *params)
+{
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b, SPA_POD_Int(n_params), NULL);
+ for (i = 0; i < n_params; i++) {
+ spa_pod_builder_add(b,
+ SPA_POD_Id(params[i].id),
+ SPA_POD_Int(params[i].flags),
+ NULL);
+ }
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define parse_param_infos(p, f, n_params_p, params_p) \
+do { \
+ uint32_t i; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, SPA_POD_Int(n_params_p), NULL) < 0) \
+ return -EINVAL; \
+ \
+ if (*(n_params_p) > 0) { \
+ if (*(n_params_p) > MAX_PARAM_INFO) \
+ return -ENOSPC; \
+ *(params_p) = alloca(*(n_params_p) * sizeof(struct spa_param_info)); \
+ for (i = 0; i < *(n_params_p); i++) { \
+ if (spa_pod_parser_get(p, \
+ SPA_POD_Id(&(*(params_p))[i].id), \
+ SPA_POD_Int(&(*(params_p))[i].flags), \
+ NULL) < 0) \
+ return -EINVAL; \
+ } \
+ } \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+/***********************************************
+ * INFO STRUCTURES
+ ***********************************************/
+
+static void
+marshal_pw_session_info(struct spa_pod_builder *b,
+ const struct pw_session_info *info)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->version),
+ SPA_POD_Int(info->id),
+ SPA_POD_Long(info->change_mask),
+ NULL);
+ push_dict(b, info->props);
+ push_param_infos(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define demarshal_pw_session_info(p, f, info) \
+do { \
+ struct spa_pod_frame sub_f; \
+ uint32_t version; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, \
+ SPA_POD_Int(&version), \
+ SPA_POD_Int(&(info)->id), \
+ SPA_POD_Long(&(info)->change_mask), \
+ NULL) < 0) \
+ return -EINVAL; \
+ \
+ (info)->change_mask &= PW_SESSION_CHANGE_MASK_ALL; \
+ \
+ parse_dict(p, &sub_f, (info)->props); \
+ parse_param_infos(p, &sub_f, &(info)->n_params, &(info)->params); \
+ \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+static void
+marshal_pw_endpoint_info(struct spa_pod_builder *b,
+ const struct pw_endpoint_info *info)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->version),
+ SPA_POD_Int(info->id),
+ SPA_POD_String(info->name),
+ SPA_POD_String(info->media_class),
+ SPA_POD_Int(info->direction),
+ SPA_POD_Int(info->flags),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Int(info->n_streams),
+ SPA_POD_Int(info->session_id),
+ NULL);
+ push_dict(b, info->props);
+ push_param_infos(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define demarshal_pw_endpoint_info(p, f, info) \
+do { \
+ struct spa_pod_frame sub_f; \
+ uint32_t version; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, \
+ SPA_POD_Int(&version), \
+ SPA_POD_Int(&(info)->id), \
+ SPA_POD_String(&(info)->name), \
+ SPA_POD_String(&(info)->media_class), \
+ SPA_POD_Int(&(info)->direction), \
+ SPA_POD_Int(&(info)->flags), \
+ SPA_POD_Long(&(info)->change_mask), \
+ SPA_POD_Int(&(info)->n_streams), \
+ SPA_POD_Int(&(info)->session_id), \
+ NULL) < 0) \
+ return -EINVAL; \
+ \
+ (info)->change_mask &= PW_ENDPOINT_CHANGE_MASK_ALL; \
+ \
+ parse_dict(p, &sub_f, (info)->props); \
+ parse_param_infos(p, &sub_f, &(info)->n_params, &(info)->params); \
+ \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+static void
+marshal_pw_endpoint_stream_info(struct spa_pod_builder *b,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->version),
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->endpoint_id),
+ SPA_POD_String(info->name),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Pod(info->link_params),
+ NULL);
+ push_dict(b, info->props);
+ push_param_infos(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define demarshal_pw_endpoint_stream_info(p, f, info) \
+do { \
+ struct spa_pod_frame sub_f; \
+ uint32_t version; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, \
+ SPA_POD_Int(&version), \
+ SPA_POD_Int(&(info)->id), \
+ SPA_POD_Int(&(info)->endpoint_id), \
+ SPA_POD_String(&(info)->name), \
+ SPA_POD_Long(&(info)->change_mask), \
+ SPA_POD_Pod(&(info)->link_params), \
+ NULL) < 0) \
+ return -EINVAL; \
+ \
+ (info)->change_mask &= PW_ENDPOINT_STREAM_CHANGE_MASK_ALL; \
+ \
+ parse_dict(p, &sub_f, (info)->props); \
+ parse_param_infos(p, &sub_f, &(info)->n_params, &(info)->params); \
+ \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+static void
+marshal_pw_endpoint_link_info(struct spa_pod_builder *b,
+ const struct pw_endpoint_link_info *info)
+{
+ struct spa_pod_frame f;
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(info->version),
+ SPA_POD_Int(info->id),
+ SPA_POD_Int(info->session_id),
+ SPA_POD_Int(info->output_endpoint_id),
+ SPA_POD_Int(info->output_stream_id),
+ SPA_POD_Int(info->input_endpoint_id),
+ SPA_POD_Int(info->input_stream_id),
+ SPA_POD_Long(info->change_mask),
+ SPA_POD_Int(info->state),
+ SPA_POD_String(info->error),
+ NULL);
+ push_dict(b, info->props);
+ push_param_infos(b, info->n_params, info->params);
+ spa_pod_builder_pop(b, &f);
+}
+
+/* macro because of alloca() */
+#define demarshal_pw_endpoint_link_info(p, f, info) \
+do { \
+ struct spa_pod_frame sub_f; \
+ uint32_t version; \
+ \
+ if (spa_pod_parser_push_struct(p, f) < 0 || \
+ spa_pod_parser_get(p, \
+ SPA_POD_Int(&version), \
+ SPA_POD_Int(&(info)->id), \
+ SPA_POD_Int(&(info)->session_id), \
+ SPA_POD_Int(&(info)->output_endpoint_id), \
+ SPA_POD_Int(&(info)->output_stream_id), \
+ SPA_POD_Int(&(info)->input_endpoint_id), \
+ SPA_POD_Int(&(info)->input_stream_id), \
+ SPA_POD_Long(&(info)->change_mask), \
+ SPA_POD_Int(&(info)->state), \
+ SPA_POD_String(&(info)->error), \
+ NULL) < 0) \
+ return -EINVAL; \
+ \
+ (info)->change_mask &= PW_ENDPOINT_LINK_CHANGE_MASK_ALL; \
+ \
+ parse_dict(p, &sub_f, (info)->props); \
+ parse_param_infos(p, &sub_f, &(info)->n_params, &(info)->params); \
+ \
+ spa_pod_parser_pop(p, f); \
+} while(0)
+
+
+/***********************************************
+ * COMMON
+ ***********************************************/
+
+static int demarshal_add_listener_enotsup(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ return -ENOTSUP;
+}
+
+/***********************************************
+ * CLIENT ENDPOINT
+ ***********************************************/
+
+static int client_endpoint_marshal_set_session_id (void *object, uint32_t id)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_ENDPOINT_EVENT_SET_SESSION_ID, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(id));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_endpoint_marshal_set_param (void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_ENDPOINT_EVENT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_endpoint_marshal_stream_set_param (void *object,
+ uint32_t stream_id, uint32_t id,
+ uint32_t flags, const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_ENDPOINT_EVENT_STREAM_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(stream_id),
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_endpoint_marshal_create_link (void *object,
+ const struct spa_dict *props)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_ENDPOINT_EVENT_CREATE_LINK, NULL);
+
+ push_dict(b, props);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_endpoint_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_client_endpoint_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int client_endpoint_marshal_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_CLIENT_ENDPOINT_METHOD_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params),
+ NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info)
+ marshal_pw_endpoint_info(b, info);
+ else
+ spa_pod_builder_add(b, SPA_POD_Pod(NULL), NULL);
+
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_endpoint_marshal_stream_update(void *object,
+ uint32_t stream_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_CLIENT_ENDPOINT_METHOD_STREAM_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(stream_id),
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params),
+ NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info)
+ marshal_pw_endpoint_stream_info(b, info);
+ else
+ spa_pod_builder_add(b, SPA_POD_Pod(NULL), NULL);
+
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_endpoint_demarshal_set_session_id(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&id)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_endpoint_events,
+ set_session_id, 0, id);
+}
+
+static int client_endpoint_demarshal_set_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_endpoint_events,
+ set_param, 0, id, flags, param);
+}
+
+static int client_endpoint_demarshal_stream_set_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t stream_id, id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&stream_id),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_endpoint_events,
+ stream_set_param, 0, stream_id, id, flags, param);
+}
+
+static int client_endpoint_demarshal_create_link(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ parse_dict(&prs, &f, &props);
+
+ return pw_proxy_notify(proxy, struct pw_client_endpoint_events,
+ create_link, 0, &props);
+}
+
+static int client_endpoint_demarshal_update(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs[2];
+ struct spa_pod_frame f[2];
+ uint32_t change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_info info = { .props = &props }, *infop = NULL;
+ struct spa_pod *ipod;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs[0], msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs[0], &f[0]) < 0 ||
+ spa_pod_parser_get(&prs[0],
+ SPA_POD_Int(&change_mask),
+ SPA_POD_Int(&n_params), NULL) < 0)
+ return -EINVAL;
+
+ if (n_params > MAX_PARAMS)
+ return -ENOSPC;
+ if (n_params > 0)
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs[0],
+ SPA_POD_PodObject(&params[i]), NULL) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs[0], SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+ if (ipod) {
+ infop = &info;
+ spa_pod_parser_pod(&prs[1], ipod);
+ demarshal_pw_endpoint_info(&prs[1], &f[1], infop);
+ }
+
+ return pw_resource_notify(resource, struct pw_client_endpoint_methods,
+ update, 0, change_mask, n_params, params, infop);
+}
+
+static int client_endpoint_demarshal_stream_update(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs[2];
+ struct spa_pod_frame f[2];
+ uint32_t stream_id, change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_stream_info info = { .props = &props }, *infop = NULL;
+ struct spa_pod *ipod;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs[0], msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs[0], &f[0]) < 0 ||
+ spa_pod_parser_get(&prs[0],
+ SPA_POD_Int(&stream_id),
+ SPA_POD_Int(&change_mask),
+ SPA_POD_Int(&n_params), NULL) < 0)
+ return -EINVAL;
+
+ if (n_params > MAX_PARAMS)
+ return -ENOSPC;
+ if (n_params > 0)
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs[0],
+ SPA_POD_PodObject(&params[i]), NULL) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs[0], SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+ if (ipod) {
+ infop = &info;
+ spa_pod_parser_pod(&prs[1], ipod);
+ demarshal_pw_endpoint_stream_info(&prs[1], &f[1], infop);
+ }
+
+ return pw_resource_notify(resource, struct pw_client_endpoint_methods,
+ stream_update, 0, stream_id, change_mask, n_params, params, infop);
+}
+
+static const struct pw_client_endpoint_events pw_protocol_native_client_endpoint_event_marshal = {
+ PW_VERSION_CLIENT_ENDPOINT_EVENTS,
+ .set_session_id = client_endpoint_marshal_set_session_id,
+ .set_param = client_endpoint_marshal_set_param,
+ .stream_set_param = client_endpoint_marshal_stream_set_param,
+ .create_link = client_endpoint_marshal_create_link,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_endpoint_event_demarshal[PW_CLIENT_ENDPOINT_EVENT_NUM] =
+{
+ [PW_CLIENT_ENDPOINT_EVENT_SET_SESSION_ID] = { client_endpoint_demarshal_set_session_id, 0 },
+ [PW_CLIENT_ENDPOINT_EVENT_SET_PARAM] = { client_endpoint_demarshal_set_param, 0 },
+ [PW_CLIENT_ENDPOINT_EVENT_STREAM_SET_PARAM] = { client_endpoint_demarshal_stream_set_param, 0 },
+ [PW_CLIENT_ENDPOINT_EVENT_CREATE_LINK] = { client_endpoint_demarshal_create_link, 0 },
+};
+
+static const struct pw_client_endpoint_methods pw_protocol_native_client_endpoint_method_marshal = {
+ PW_VERSION_CLIENT_ENDPOINT_METHODS,
+ .add_listener = client_endpoint_marshal_add_listener,
+ .update = client_endpoint_marshal_update,
+ .stream_update = client_endpoint_marshal_stream_update,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_endpoint_method_demarshal[PW_CLIENT_ENDPOINT_METHOD_NUM] =
+{
+ [PW_CLIENT_ENDPOINT_METHOD_ADD_LISTENER] = { NULL, 0 },
+ [PW_CLIENT_ENDPOINT_METHOD_UPDATE] = { client_endpoint_demarshal_update, 0 },
+ [PW_CLIENT_ENDPOINT_METHOD_STREAM_UPDATE] = { client_endpoint_demarshal_stream_update, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_endpoint_marshal = {
+ PW_TYPE_INTERFACE_ClientEndpoint,
+ PW_VERSION_CLIENT_ENDPOINT,
+ 0,
+ PW_CLIENT_ENDPOINT_METHOD_NUM,
+ PW_CLIENT_ENDPOINT_EVENT_NUM,
+ &pw_protocol_native_client_endpoint_method_marshal,
+ &pw_protocol_native_client_endpoint_method_demarshal,
+ &pw_protocol_native_client_endpoint_event_marshal,
+ &pw_protocol_native_client_endpoint_event_demarshal,
+};
+
+/***********************************************
+ * CLIENT SESSION
+ ***********************************************/
+
+static int client_session_marshal_set_param (void *data,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_SESSION_EVENT_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_session_marshal_link_set_param (void *data,
+ uint32_t link_id, uint32_t id,
+ uint32_t flags, const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_SESSION_EVENT_LINK_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(link_id),
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_session_marshal_link_request_state (void *data,
+ uint32_t link_id, uint32_t state)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_CLIENT_SESSION_EVENT_LINK_REQUEST_STATE, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(link_id),
+ SPA_POD_Int(state));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int client_session_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_client_session_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int client_session_marshal_update(void *object,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_CLIENT_SESSION_METHOD_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params),
+ NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info)
+ marshal_pw_session_info(b, info);
+ else
+ spa_pod_builder_add(b, SPA_POD_Pod(NULL), NULL);
+
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_session_marshal_link_update(void *object,
+ uint32_t link_id,
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+ struct spa_pod_frame f;
+ uint32_t i;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_CLIENT_SESSION_METHOD_LINK_UPDATE, NULL);
+
+ spa_pod_builder_push_struct(b, &f);
+ spa_pod_builder_add(b,
+ SPA_POD_Int(link_id),
+ SPA_POD_Int(change_mask),
+ SPA_POD_Int(n_params),
+ NULL);
+
+ for (i = 0; i < n_params; i++)
+ spa_pod_builder_add(b, SPA_POD_Pod(params[i]), NULL);
+
+ if (info)
+ marshal_pw_endpoint_link_info(b, info);
+ else
+ spa_pod_builder_add(b, SPA_POD_Pod(NULL), NULL);
+
+ spa_pod_builder_pop(b, &f);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int client_session_demarshal_set_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_session_events,
+ set_param, 0, id, flags, param);
+}
+
+static int client_session_demarshal_link_set_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t link_id, id, flags;
+ const struct spa_pod *param = NULL;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&link_id),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_PodObject(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_session_events,
+ link_set_param, 0, link_id, id, flags, param);
+}
+
+static int client_session_demarshal_link_request_state(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t link_id, state;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&link_id),
+ SPA_POD_Int(&state)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_client_session_events,
+ link_request_state, 0, link_id, state);
+}
+
+static int client_session_demarshal_update(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs[2];
+ struct spa_pod_frame f[2];
+ uint32_t change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_session_info info = { .props = &props }, *infop = NULL;
+ struct spa_pod *ipod;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs[0], msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs[0], &f[0]) < 0 ||
+ spa_pod_parser_get(&prs[0],
+ SPA_POD_Int(&change_mask),
+ SPA_POD_Int(&n_params), NULL) < 0)
+ return -EINVAL;
+
+ if (n_params > MAX_PARAMS)
+ return -ENOSPC;
+ if (n_params > 0)
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs[0],
+ SPA_POD_PodObject(&params[i]), NULL) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs[0], SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+ if (ipod) {
+ infop = &info;
+ spa_pod_parser_pod(&prs[1], ipod);
+ demarshal_pw_session_info(&prs[1], &f[1], infop);
+ }
+
+ return pw_resource_notify(resource, struct pw_client_session_methods,
+ update, 0, change_mask, n_params, params, infop);
+}
+
+static int client_session_demarshal_link_update(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs[2];
+ struct spa_pod_frame f[2];
+ uint32_t link_id, change_mask, n_params;
+ const struct spa_pod **params = NULL;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_link_info info = { .props = &props }, *infop = NULL;
+ struct spa_pod *ipod;
+ uint32_t i;
+
+ spa_pod_parser_init(&prs[0], msg->data, msg->size);
+ if (spa_pod_parser_push_struct(&prs[0], &f[0]) < 0 ||
+ spa_pod_parser_get(&prs[0],
+ SPA_POD_Int(&link_id),
+ SPA_POD_Int(&change_mask),
+ SPA_POD_Int(&n_params), NULL) < 0)
+ return -EINVAL;
+
+ if (n_params > MAX_PARAMS)
+ return -ENOSPC;
+ if (n_params > 0)
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0; i < n_params; i++)
+ if (spa_pod_parser_get(&prs[0],
+ SPA_POD_PodObject(&params[i]), NULL) < 0)
+ return -EINVAL;
+
+ if (spa_pod_parser_get(&prs[0], SPA_POD_PodStruct(&ipod), NULL) < 0)
+ return -EINVAL;
+ if (ipod) {
+ infop = &info;
+ spa_pod_parser_pod(&prs[1], ipod);
+ demarshal_pw_endpoint_link_info(&prs[1], &f[1], infop);
+ }
+
+ return pw_resource_notify(resource, struct pw_client_session_methods,
+ link_update, 0, link_id, change_mask, n_params, params, infop);
+}
+
+static const struct pw_client_session_events pw_protocol_native_client_session_event_marshal = {
+ PW_VERSION_CLIENT_SESSION_EVENTS,
+ .set_param = client_session_marshal_set_param,
+ .link_set_param = client_session_marshal_link_set_param,
+ .link_request_state = client_session_marshal_link_request_state,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_session_event_demarshal[PW_CLIENT_SESSION_EVENT_NUM] =
+{
+ [PW_CLIENT_SESSION_EVENT_SET_PARAM] = { client_session_demarshal_set_param, 0 },
+ [PW_CLIENT_SESSION_EVENT_LINK_SET_PARAM] = { client_session_demarshal_link_set_param, 0 },
+ [PW_CLIENT_SESSION_EVENT_LINK_REQUEST_STATE] = { client_session_demarshal_link_request_state, 0 },
+};
+
+static const struct pw_client_session_methods pw_protocol_native_client_session_method_marshal = {
+ PW_VERSION_CLIENT_SESSION_METHODS,
+ .add_listener = client_session_marshal_add_listener,
+ .update = client_session_marshal_update,
+ .link_update = client_session_marshal_link_update,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_client_session_method_demarshal[PW_CLIENT_SESSION_METHOD_NUM] =
+{
+ [PW_CLIENT_SESSION_METHOD_ADD_LISTENER] = { NULL, 0 },
+ [PW_CLIENT_SESSION_METHOD_UPDATE] = { client_session_demarshal_update, 0 },
+ [PW_CLIENT_SESSION_METHOD_LINK_UPDATE] = { client_session_demarshal_link_update, 0 },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_client_session_marshal = {
+ PW_TYPE_INTERFACE_ClientSession,
+ PW_VERSION_CLIENT_SESSION,
+ 0,
+ PW_CLIENT_SESSION_METHOD_NUM,
+ PW_CLIENT_SESSION_EVENT_NUM,
+ &pw_protocol_native_client_session_method_marshal,
+ &pw_protocol_native_client_session_method_demarshal,
+ &pw_protocol_native_client_session_event_marshal,
+ &pw_protocol_native_client_session_event_demarshal,
+};
+
+/***********************************************
+ * ENDPOINT LINK
+ ***********************************************/
+
+static void endpoint_link_proxy_marshal_info (void *data,
+ const struct pw_endpoint_link_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_link_info(b, info);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_link_resource_marshal_info (void *data,
+ const struct pw_endpoint_link_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_link_info(b, info);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void endpoint_link_proxy_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+static void endpoint_link_resource_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_link_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int endpoint_link_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_link_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int endpoint_link_proxy_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_link_resource_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_link_resource_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_link_resource_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_marshal_request_state(void *object,
+ enum pw_endpoint_link_state state)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_LINK_METHOD_REQUEST_STATE, NULL);
+
+ spa_pod_builder_add_struct(b, SPA_POD_Int(state));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_link_resource_marshal_request_state(void *object,
+ enum pw_endpoint_link_state state)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_LINK_METHOD_REQUEST_STATE, NULL);
+
+ spa_pod_builder_add_struct(b, SPA_POD_Int(state));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_link_proxy_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_link_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_link_info(&prs, &f, &info);
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_events,
+ info, 0, &info);
+}
+
+static int endpoint_link_resource_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_link_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_link_info(&prs, &f, &info);
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_events,
+ info, 0, &info);
+}
+
+static int endpoint_link_proxy_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_link_resource_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_link_proxy_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_link_resource_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_link_proxy_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_link_resource_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_link_proxy_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_link_resource_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_link_proxy_demarshal_request_state(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ enum pw_endpoint_link_state state;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&state)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_link_methods,
+ request_state, 0, state);
+}
+
+static int endpoint_link_resource_demarshal_request_state(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ enum pw_endpoint_link_state state;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&state)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_link_methods,
+ request_state, 0, state);
+}
+
+static const struct pw_endpoint_link_events pw_protocol_native_endpoint_link_client_event_marshal = {
+ PW_VERSION_ENDPOINT_LINK_EVENTS,
+ .info = endpoint_link_proxy_marshal_info,
+ .param = endpoint_link_proxy_marshal_param,
+};
+
+static const struct pw_endpoint_link_events pw_protocol_native_endpoint_link_server_event_marshal = {
+ PW_VERSION_ENDPOINT_LINK_EVENTS,
+ .info = endpoint_link_resource_marshal_info,
+ .param = endpoint_link_resource_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_link_client_event_demarshal[PW_ENDPOINT_LINK_EVENT_NUM] =
+{
+ [PW_ENDPOINT_LINK_EVENT_INFO] = { endpoint_link_proxy_demarshal_info, 0 },
+ [PW_ENDPOINT_LINK_EVENT_PARAM] = { endpoint_link_proxy_demarshal_param, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_link_server_event_demarshal[PW_ENDPOINT_LINK_EVENT_NUM] =
+{
+ [PW_ENDPOINT_LINK_EVENT_INFO] = { endpoint_link_resource_demarshal_info, 0 },
+ [PW_ENDPOINT_LINK_EVENT_PARAM] = { endpoint_link_resource_demarshal_param, 0 },
+};
+
+static const struct pw_endpoint_link_methods pw_protocol_native_endpoint_link_client_method_marshal = {
+ PW_VERSION_ENDPOINT_LINK_METHODS,
+ .add_listener = endpoint_link_proxy_marshal_add_listener,
+ .subscribe_params = endpoint_link_proxy_marshal_subscribe_params,
+ .enum_params = endpoint_link_proxy_marshal_enum_params,
+ .set_param = endpoint_link_proxy_marshal_set_param,
+ .request_state = endpoint_link_proxy_marshal_request_state,
+};
+
+static const struct pw_endpoint_link_methods pw_protocol_native_endpoint_link_server_method_marshal = {
+ PW_VERSION_ENDPOINT_LINK_METHODS,
+ .add_listener = endpoint_link_resource_marshal_add_listener,
+ .subscribe_params = endpoint_link_resource_marshal_subscribe_params,
+ .enum_params = endpoint_link_resource_marshal_enum_params,
+ .set_param = endpoint_link_resource_marshal_set_param,
+ .request_state = endpoint_link_resource_marshal_request_state,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_link_client_method_demarshal[PW_ENDPOINT_LINK_METHOD_NUM] =
+{
+ [PW_ENDPOINT_LINK_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS] = { endpoint_link_proxy_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS] = { endpoint_link_proxy_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_LINK_METHOD_SET_PARAM] = { endpoint_link_proxy_demarshal_set_param, PW_PERM_W },
+ [PW_ENDPOINT_LINK_METHOD_REQUEST_STATE] = { endpoint_link_proxy_demarshal_request_state, PW_PERM_W },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_link_server_method_demarshal[PW_ENDPOINT_LINK_METHOD_NUM] =
+{
+ [PW_ENDPOINT_LINK_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS] = { endpoint_link_resource_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS] = { endpoint_link_resource_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_LINK_METHOD_SET_PARAM] = { endpoint_link_resource_demarshal_set_param, PW_PERM_W },
+ [PW_ENDPOINT_LINK_METHOD_REQUEST_STATE] = { endpoint_link_resource_demarshal_request_state, PW_PERM_W },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_link_marshal = {
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ 0,
+ PW_ENDPOINT_LINK_METHOD_NUM,
+ PW_ENDPOINT_LINK_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_link_client_method_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_link_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_link_server_event_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_link_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_link_impl_marshal = {
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_ENDPOINT_LINK_EVENT_NUM,
+ PW_ENDPOINT_LINK_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_link_client_event_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_link_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_link_server_method_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_link_client_method_demarshal,
+};
+
+/***********************************************
+ * ENDPOINT STREAM
+ ***********************************************/
+
+static void endpoint_stream_proxy_marshal_info (void *data,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_stream_info(b, info);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_stream_resource_marshal_info (void *data,
+ const struct pw_endpoint_stream_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_stream_info(b, info);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void endpoint_stream_proxy_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_stream_resource_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_stream_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_stream_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int endpoint_stream_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_stream_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int endpoint_stream_proxy_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_stream_resource_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_stream_proxy_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_stream_resource_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_stream_proxy_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_STREAM_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_stream_resource_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_STREAM_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_stream_proxy_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_stream_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_stream_info(&prs, &f, &info);
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_events,
+ info, 0, &info);
+}
+
+static int endpoint_stream_resource_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_stream_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_stream_info(&prs, &f, &info);
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_events,
+ info, 0, &info);
+}
+
+static int endpoint_stream_proxy_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_stream_resource_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_stream_proxy_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_stream_resource_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_stream_proxy_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_stream_resource_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_stream_proxy_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_stream_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_stream_resource_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_stream_methods,
+ set_param, 0, id, flags, param);
+}
+
+static const struct pw_endpoint_stream_events pw_protocol_native_endpoint_stream_client_event_marshal = {
+ PW_VERSION_ENDPOINT_STREAM_EVENTS,
+ .info = endpoint_stream_proxy_marshal_info,
+ .param = endpoint_stream_proxy_marshal_param,
+};
+
+static const struct pw_endpoint_stream_events pw_protocol_native_endpoint_stream_server_event_marshal = {
+ PW_VERSION_ENDPOINT_STREAM_EVENTS,
+ .info = endpoint_stream_resource_marshal_info,
+ .param = endpoint_stream_resource_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_stream_client_event_demarshal[PW_ENDPOINT_STREAM_EVENT_NUM] =
+{
+ [PW_ENDPOINT_STREAM_EVENT_INFO] = { endpoint_stream_proxy_demarshal_info, 0 },
+ [PW_ENDPOINT_STREAM_EVENT_PARAM] = { endpoint_stream_proxy_demarshal_param, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_stream_server_event_demarshal[PW_ENDPOINT_STREAM_EVENT_NUM] =
+{
+ [PW_ENDPOINT_STREAM_EVENT_INFO] = { endpoint_stream_resource_demarshal_info, 0 },
+ [PW_ENDPOINT_STREAM_EVENT_PARAM] = { endpoint_stream_resource_demarshal_param, 0 },
+};
+
+static const struct pw_endpoint_stream_methods pw_protocol_native_endpoint_stream_client_method_marshal = {
+ PW_VERSION_ENDPOINT_STREAM_METHODS,
+ .add_listener = endpoint_stream_proxy_marshal_add_listener,
+ .subscribe_params = endpoint_stream_proxy_marshal_subscribe_params,
+ .enum_params = endpoint_stream_proxy_marshal_enum_params,
+ .set_param = endpoint_stream_proxy_marshal_set_param,
+};
+
+static const struct pw_endpoint_stream_methods pw_protocol_native_endpoint_stream_server_method_marshal = {
+ PW_VERSION_ENDPOINT_STREAM_METHODS,
+ .add_listener = endpoint_stream_resource_marshal_add_listener,
+ .subscribe_params = endpoint_stream_resource_marshal_subscribe_params,
+ .enum_params = endpoint_stream_resource_marshal_enum_params,
+ .set_param = endpoint_stream_resource_marshal_set_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_stream_client_method_demarshal[PW_ENDPOINT_STREAM_METHOD_NUM] =
+{
+ [PW_ENDPOINT_STREAM_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS] = { endpoint_stream_proxy_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS] = { endpoint_stream_proxy_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_SET_PARAM] = { endpoint_stream_proxy_demarshal_set_param, PW_PERM_W },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_stream_server_method_demarshal[PW_ENDPOINT_STREAM_METHOD_NUM] =
+{
+ [PW_ENDPOINT_STREAM_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS] = { endpoint_stream_resource_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS] = { endpoint_stream_resource_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_STREAM_METHOD_SET_PARAM] = { endpoint_stream_resource_demarshal_set_param, PW_PERM_W },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_stream_marshal = {
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ 0,
+ PW_ENDPOINT_STREAM_METHOD_NUM,
+ PW_ENDPOINT_STREAM_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_stream_client_method_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_stream_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_stream_server_event_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_stream_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_stream_impl_marshal = {
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_ENDPOINT_STREAM_EVENT_NUM,
+ PW_ENDPOINT_STREAM_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_stream_client_event_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_stream_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_stream_server_method_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_stream_client_method_demarshal,
+};
+
+/***********************************************
+ * ENDPOINT
+ ***********************************************/
+
+static void endpoint_proxy_marshal_info (void *data,
+ const struct pw_endpoint_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_info(b, info);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_resource_marshal_info (void *data,
+ const struct pw_endpoint_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_EVENT_INFO, NULL);
+
+ marshal_pw_endpoint_info(b, info);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void endpoint_proxy_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void endpoint_resource_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int endpoint_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int endpoint_proxy_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_resource_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_resource_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_resource_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_marshal_create_link(void *object,
+ const struct spa_dict *props)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_ENDPOINT_METHOD_CREATE_LINK, NULL);
+
+ push_dict(b, props);
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int endpoint_resource_marshal_create_link(void *object,
+ const struct spa_dict *props)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_ENDPOINT_METHOD_CREATE_LINK, NULL);
+
+ push_dict(b, props);
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int endpoint_proxy_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_info(&prs, &f, &info);
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_events,
+ info, 0, &info);
+}
+
+static int endpoint_resource_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_endpoint_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_endpoint_info(&prs, &f, &info);
+
+ return pw_resource_notify(resource, struct pw_endpoint_events,
+ info, 0, &info);
+}
+
+static int endpoint_proxy_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_resource_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int endpoint_proxy_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_resource_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int endpoint_proxy_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_resource_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int endpoint_proxy_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_resource_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_endpoint_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int endpoint_proxy_demarshal_create_link(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ parse_dict(&prs, &f, &props);
+
+ return pw_proxy_notify(proxy, struct pw_endpoint_methods,
+ create_link, 0, &props);
+}
+
+static int endpoint_resource_demarshal_create_link(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ parse_dict(&prs, &f, &props);
+
+ return pw_resource_notify(resource, struct pw_endpoint_methods,
+ create_link, 0, &props);
+}
+
+static const struct pw_endpoint_events pw_protocol_native_endpoint_client_event_marshal = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = endpoint_proxy_marshal_info,
+ .param = endpoint_proxy_marshal_param,
+};
+
+static const struct pw_endpoint_events pw_protocol_native_endpoint_server_event_marshal = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = endpoint_resource_marshal_info,
+ .param = endpoint_resource_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_client_event_demarshal[PW_ENDPOINT_EVENT_NUM] =
+{
+ [PW_ENDPOINT_EVENT_INFO] = { endpoint_proxy_demarshal_info, 0 },
+ [PW_ENDPOINT_EVENT_PARAM] = { endpoint_proxy_demarshal_param, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_server_event_demarshal[PW_ENDPOINT_EVENT_NUM] =
+{
+ [PW_ENDPOINT_EVENT_INFO] = { endpoint_resource_demarshal_info, 0 },
+ [PW_ENDPOINT_EVENT_PARAM] = { endpoint_resource_demarshal_param, 0 },
+};
+
+static const struct pw_endpoint_methods pw_protocol_native_endpoint_client_method_marshal = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .add_listener = endpoint_proxy_marshal_add_listener,
+ .subscribe_params = endpoint_proxy_marshal_subscribe_params,
+ .enum_params = endpoint_proxy_marshal_enum_params,
+ .set_param = endpoint_proxy_marshal_set_param,
+ .create_link = endpoint_proxy_marshal_create_link,
+};
+
+static const struct pw_endpoint_methods pw_protocol_native_endpoint_server_method_marshal = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .add_listener = endpoint_resource_marshal_add_listener,
+ .subscribe_params = endpoint_resource_marshal_subscribe_params,
+ .enum_params = endpoint_resource_marshal_enum_params,
+ .set_param = endpoint_resource_marshal_set_param,
+ .create_link = endpoint_resource_marshal_create_link,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_client_method_demarshal[PW_ENDPOINT_METHOD_NUM] =
+{
+ [PW_ENDPOINT_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS] = { endpoint_proxy_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_METHOD_ENUM_PARAMS] = { endpoint_proxy_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_METHOD_SET_PARAM] = { endpoint_proxy_demarshal_set_param, PW_PERM_W },
+ [PW_ENDPOINT_METHOD_CREATE_LINK] = { endpoint_proxy_demarshal_create_link, PW_PERM_X },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_endpoint_server_method_demarshal[PW_ENDPOINT_METHOD_NUM] =
+{
+ [PW_ENDPOINT_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS] = { endpoint_resource_demarshal_subscribe_params, 0 },
+ [PW_ENDPOINT_METHOD_ENUM_PARAMS] = { endpoint_resource_demarshal_enum_params, 0 },
+ [PW_ENDPOINT_METHOD_SET_PARAM] = { endpoint_resource_demarshal_set_param, PW_PERM_W },
+ [PW_ENDPOINT_METHOD_CREATE_LINK] = { endpoint_resource_demarshal_create_link, PW_PERM_X },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_marshal = {
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ 0,
+ PW_ENDPOINT_METHOD_NUM,
+ PW_ENDPOINT_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_client_method_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_server_event_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_endpoint_impl_marshal = {
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_ENDPOINT_EVENT_NUM,
+ PW_ENDPOINT_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_endpoint_client_event_marshal,
+ .server_demarshal = pw_protocol_native_endpoint_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_endpoint_server_method_marshal,
+ .client_demarshal = pw_protocol_native_endpoint_client_method_demarshal,
+};
+
+/***********************************************
+ * SESSION
+ ***********************************************/
+
+static void session_proxy_marshal_info (void *data,
+ const struct pw_session_info *info)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_EVENT_INFO, NULL);
+
+ marshal_pw_session_info(b, info);
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void session_resource_marshal_info (void *data,
+ const struct pw_session_info *info)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_EVENT_INFO, NULL);
+
+ marshal_pw_session_info(b, info);
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static void session_proxy_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_proxy(proxy, b);
+}
+
+static void session_resource_marshal_param (void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_EVENT_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(seq),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(next),
+ SPA_POD_Pod(param));
+
+ pw_protocol_native_end_resource(resource, b);
+}
+
+static int session_proxy_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_session_events *events,
+ void *data)
+{
+ struct pw_proxy *proxy = object;
+ pw_proxy_add_object_listener(proxy, listener, events, data);
+ return 0;
+}
+
+static int session_resource_marshal_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_session_events *events,
+ void *data)
+{
+ struct pw_resource *resource = object;
+ pw_resource_add_object_listener(resource, listener, events, data);
+ return 0;
+}
+
+static int session_proxy_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int session_resource_marshal_subscribe_params(void *object,
+ uint32_t *ids, uint32_t n_ids)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_METHOD_SUBSCRIBE_PARAMS, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, n_ids, ids));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int session_proxy_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int session_resource_marshal_enum_params(void *object,
+ int seq, uint32_t id,
+ uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct pw_protocol_native_message *msg;
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_METHOD_ENUM_PARAMS, &msg);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Int(SPA_RESULT_RETURN_ASYNC(msg->seq)),
+ SPA_POD_Id(id),
+ SPA_POD_Int(index),
+ SPA_POD_Int(num),
+ SPA_POD_Pod(filter));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int session_proxy_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_proxy(proxy,
+ PW_SESSION_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_proxy(proxy, b);
+}
+
+static int session_resource_marshal_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_builder *b;
+
+ b = pw_protocol_native_begin_resource(resource,
+ PW_SESSION_METHOD_SET_PARAM, NULL);
+
+ spa_pod_builder_add_struct(b,
+ SPA_POD_Id(id),
+ SPA_POD_Int(flags),
+ SPA_POD_Pod(param));
+
+ return pw_protocol_native_end_resource(resource, b);
+}
+
+static int session_proxy_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_session_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_session_info(&prs, &f, &info);
+
+ return pw_proxy_notify(proxy, struct pw_session_events,
+ info, 0, &info);
+}
+
+static int session_resource_demarshal_info(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ struct spa_dict props = SPA_DICT_INIT(NULL, 0);
+ struct pw_session_info info = { .props = &props };
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+
+ demarshal_pw_session_info(&prs, &f, &info);
+
+ return pw_resource_notify(resource, struct pw_session_events,
+ info, 0, &info);
+}
+
+static int session_proxy_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_session_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int session_resource_demarshal_param(void *data,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = data;
+ struct spa_pod_parser prs;
+ uint32_t id, index, next;
+ int seq;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&next),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_session_events,
+ param, 0, seq, id, index, next, param);
+}
+
+static int session_proxy_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_session_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int session_resource_demarshal_subscribe_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t csize, ctype, n_ids;
+ uint32_t *ids;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0)
+ return -EINVAL;
+
+ if (ctype != SPA_TYPE_Id)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_session_methods,
+ subscribe_params, 0, ids, n_ids);
+}
+
+static int session_proxy_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_session_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int session_resource_demarshal_enum_params(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, index, num;
+ int seq;
+ struct spa_pod *filter;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Int(&seq),
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&index),
+ SPA_POD_Int(&num),
+ SPA_POD_Pod(&filter)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_session_methods,
+ enum_params, 0, seq, id, index, num, filter);
+}
+
+static int session_proxy_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_proxy *proxy = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_proxy_notify(proxy, struct pw_session_methods,
+ set_param, 0, id, flags, param);
+}
+
+static int session_resource_demarshal_set_param(void *object,
+ const struct pw_protocol_native_message *msg)
+{
+ struct pw_resource *resource = object;
+ struct spa_pod_parser prs;
+ uint32_t id, flags;
+ struct spa_pod *param;
+
+ spa_pod_parser_init(&prs, msg->data, msg->size);
+ if (spa_pod_parser_get_struct(&prs,
+ SPA_POD_Id(&id),
+ SPA_POD_Int(&flags),
+ SPA_POD_Pod(&param)) < 0)
+ return -EINVAL;
+
+ return pw_resource_notify(resource, struct pw_session_methods,
+ set_param, 0, id, flags, param);
+}
+
+static const struct pw_session_events pw_protocol_native_session_client_event_marshal = {
+ PW_VERSION_SESSION_EVENTS,
+ .info = session_proxy_marshal_info,
+ .param = session_proxy_marshal_param,
+};
+
+static const struct pw_session_events pw_protocol_native_session_server_event_marshal = {
+ PW_VERSION_SESSION_EVENTS,
+ .info = session_resource_marshal_info,
+ .param = session_resource_marshal_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_session_client_event_demarshal[PW_SESSION_EVENT_NUM] =
+{
+ [PW_SESSION_EVENT_INFO] = { session_proxy_demarshal_info, 0 },
+ [PW_SESSION_EVENT_PARAM] = { session_proxy_demarshal_param, 0 },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_session_server_event_demarshal[PW_SESSION_EVENT_NUM] =
+{
+ [PW_SESSION_EVENT_INFO] = { session_resource_demarshal_info, 0 },
+ [PW_SESSION_EVENT_PARAM] = { session_resource_demarshal_param, 0 },
+};
+
+static const struct pw_session_methods pw_protocol_native_session_client_method_marshal = {
+ PW_VERSION_SESSION_METHODS,
+ .add_listener = session_proxy_marshal_add_listener,
+ .subscribe_params = session_proxy_marshal_subscribe_params,
+ .enum_params = session_proxy_marshal_enum_params,
+ .set_param = session_proxy_marshal_set_param,
+};
+
+static const struct pw_session_methods pw_protocol_native_session_server_method_marshal = {
+ PW_VERSION_SESSION_METHODS,
+ .add_listener = session_resource_marshal_add_listener,
+ .subscribe_params = session_resource_marshal_subscribe_params,
+ .enum_params = session_resource_marshal_enum_params,
+ .set_param = session_resource_marshal_set_param,
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_session_client_method_demarshal[PW_SESSION_METHOD_NUM] =
+{
+ [PW_SESSION_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_SESSION_METHOD_SUBSCRIBE_PARAMS] = { session_proxy_demarshal_subscribe_params, 0 },
+ [PW_SESSION_METHOD_ENUM_PARAMS] = { session_proxy_demarshal_enum_params, 0 },
+ [PW_SESSION_METHOD_SET_PARAM] = { session_proxy_demarshal_set_param, PW_PERM_W },
+};
+
+static const struct pw_protocol_native_demarshal
+pw_protocol_native_session_server_method_demarshal[PW_SESSION_METHOD_NUM] =
+{
+ [PW_SESSION_METHOD_ADD_LISTENER] = { demarshal_add_listener_enotsup, 0 },
+ [PW_SESSION_METHOD_SUBSCRIBE_PARAMS] = { session_resource_demarshal_subscribe_params, 0 },
+ [PW_SESSION_METHOD_ENUM_PARAMS] = { session_resource_demarshal_enum_params, 0 },
+ [PW_SESSION_METHOD_SET_PARAM] = { session_resource_demarshal_set_param, PW_PERM_W },
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_session_marshal = {
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ 0,
+ PW_SESSION_METHOD_NUM,
+ PW_SESSION_EVENT_NUM,
+ .client_marshal = &pw_protocol_native_session_client_method_marshal,
+ .server_demarshal = pw_protocol_native_session_server_method_demarshal,
+ .server_marshal = &pw_protocol_native_session_server_event_marshal,
+ .client_demarshal = pw_protocol_native_session_client_event_demarshal,
+};
+
+static const struct pw_protocol_marshal pw_protocol_native_session_impl_marshal = {
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ PW_PROTOCOL_MARSHAL_FLAG_IMPL,
+ PW_SESSION_EVENT_NUM,
+ PW_SESSION_METHOD_NUM,
+ .client_marshal = &pw_protocol_native_session_client_event_marshal,
+ .server_demarshal = pw_protocol_native_session_server_event_demarshal,
+ .server_marshal = &pw_protocol_native_session_server_method_marshal,
+ .client_demarshal = pw_protocol_native_session_client_method_demarshal,
+};
+
+int pw_protocol_native_ext_session_manager_init(struct pw_context *context)
+{
+ struct pw_protocol *protocol;
+
+ protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native);
+ if (protocol == NULL)
+ return -EPROTO;
+
+ /* deprecated */
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_endpoint_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_client_session_marshal);
+
+ /* client <-> server */
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_link_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_stream_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_session_marshal);
+
+ /* impl <-> server */
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_link_impl_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_stream_impl_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_endpoint_impl_marshal);
+ pw_protocol_add_marshal(protocol, &pw_protocol_native_session_impl_marshal);
+
+ return 0;
+}
diff --git a/src/modules/module-session-manager/proxy-session-manager.c b/src/modules/module-session-manager/proxy-session-manager.c
new file mode 100644
index 0000000..cd418e1
--- /dev/null
+++ b/src/modules/module-session-manager/proxy-session-manager.c
@@ -0,0 +1,188 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pipewire/pipewire.h"
+#include "pipewire/extensions/session-manager.h"
+
+struct object_data {
+ struct spa_hook object_listener;
+ struct spa_hook object_methods;
+ struct spa_hook proxy_listener;
+};
+
+static void proxy_object_destroy(void *_data)
+{
+ struct object_data *data = _data;
+ spa_hook_remove(&data->object_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = proxy_object_destroy,
+};
+
+struct pw_proxy *pw_core_endpoint_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_endpoint *endpoint = object;
+ struct spa_interface *remote_iface, *local_iface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "endpoint",
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+
+ remote_iface = (struct spa_interface*)proxy;
+ local_iface = (struct spa_interface*)endpoint;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ local_iface->cb.funcs, local_iface->cb.data);
+ pw_endpoint_add_listener(endpoint, &data->object_listener,
+ remote_iface->cb.funcs, remote_iface->cb.data);
+
+ return proxy;
+}
+
+struct pw_proxy *pw_core_endpoint_stream_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_endpoint_stream *endpoint_stream = object;
+ struct spa_interface *remote_iface, *local_iface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "endpoint-stream",
+ PW_TYPE_INTERFACE_EndpointStream,
+ PW_VERSION_ENDPOINT_STREAM,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+
+ remote_iface = (struct spa_interface*)proxy;
+ local_iface = (struct spa_interface*)endpoint_stream;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ local_iface->cb.funcs, local_iface->cb.data);
+ pw_endpoint_stream_add_listener(endpoint_stream, &data->object_listener,
+ remote_iface->cb.funcs, remote_iface->cb.data);
+
+ return proxy;
+}
+
+struct pw_proxy *pw_core_endpoint_link_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_endpoint_link *endpoint_link = object;
+ struct spa_interface *remote_iface, *local_iface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "endpoint-link",
+ PW_TYPE_INTERFACE_EndpointLink,
+ PW_VERSION_ENDPOINT_LINK,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+
+ remote_iface = (struct spa_interface*)proxy;
+ local_iface = (struct spa_interface*)endpoint_link;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ local_iface->cb.funcs, local_iface->cb.data);
+ pw_endpoint_link_add_listener(endpoint_link, &data->object_listener,
+ remote_iface->cb.funcs, remote_iface->cb.data);
+
+ return proxy;
+}
+
+struct pw_proxy *pw_core_session_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_session *session = object;
+ struct spa_interface *remote_iface, *local_iface;
+ struct pw_proxy *proxy;
+ struct object_data *data;
+
+ proxy = pw_core_create_object(core,
+ "session",
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ props,
+ user_data_size + sizeof(struct object_data));
+ if (proxy == NULL)
+ return NULL;
+
+ data = pw_proxy_get_user_data(proxy);
+ data = SPA_PTROFF(data, user_data_size, struct object_data);
+
+ remote_iface = (struct spa_interface*)proxy;
+ local_iface = (struct spa_interface*)session;
+
+ pw_proxy_install_marshal(proxy, true);
+
+ pw_proxy_add_listener(proxy, &data->proxy_listener, &proxy_events, data);
+
+ pw_proxy_add_object_listener(proxy, &data->object_methods,
+ local_iface->cb.funcs, local_iface->cb.data);
+ pw_session_add_listener(session, &data->object_listener,
+ remote_iface->cb.funcs, remote_iface->cb.data);
+
+ return proxy;
+}
diff --git a/src/modules/module-session-manager/session.c b/src/modules/module-session-manager/session.c
new file mode 100644
index 0000000..e3d7210
--- /dev/null
+++ b/src/modules/module-session-manager/session.c
@@ -0,0 +1,578 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/session-manager.h>
+#include <pipewire/extensions/session-manager/introspect-funcs.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/filter.h>
+
+#define MAX_PARAMS 32
+
+#define NAME "session"
+
+struct pw_proxy *pw_core_session_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+
+struct impl
+{
+ struct pw_global *global;
+ struct spa_hook global_listener;
+
+ union {
+ struct pw_session *session;
+ struct pw_resource *resource;
+ };
+ struct spa_hook resource_listener;
+ struct spa_hook session_listener;
+
+ struct pw_session_info *cached_info;
+ struct spa_list cached_params;
+
+ int ping_seq;
+ bool registered;
+};
+
+struct param_data
+{
+ struct spa_list link;
+ uint32_t id;
+ struct pw_array params;
+};
+
+struct resource_data
+{
+ struct impl *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook object_listener;
+
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[32];
+};
+
+struct factory_data
+{
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct pw_export_type export;
+};
+
+#define pw_session_resource(r,m,v,...) \
+ pw_resource_call(r,struct pw_session_events,m,v,__VA_ARGS__)
+
+#define pw_session_resource_info(r,...) \
+ pw_session_resource(r,info,0,__VA_ARGS__)
+#define pw_session_resource_param(r,...) \
+ pw_session_resource(r,param,0,__VA_ARGS__)
+
+static int method_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ struct param_data *pdata;
+ struct spa_pod *result;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ uint32_t index;
+ uint32_t next = start;
+ uint32_t count = 0;
+
+ pw_log_debug(NAME" %p: param %u %d/%d", impl, id, start, num);
+
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ while (true) {
+ index = next++;
+ if (index >= pw_array_get_len(&pdata->params, void*))
+ return 0;
+
+ param = *pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_pod_filter(&b, &result, param, filter) != 0)
+ continue;
+
+ pw_log_debug(NAME" %p: %d param %u", impl, seq, index);
+
+ pw_session_resource_param(d->resource, seq, id, index, next, result);
+
+ if (++count == num)
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static int method_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(d->subscribe_ids));
+ d->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ d->subscribe_ids[i] = ids[i];
+ pw_log_debug(NAME" %p: resource %d subscribe param %u",
+ impl, pw_resource_get_id(d->resource), ids[i]);
+ method_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int method_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *d = object;
+ struct impl *impl = d->impl;
+ /* store only on the implementation; our cache will be updated
+ by the param event, since we are subscribed */
+ pw_session_set_param(impl->session, id, flags, param);
+ return 0;
+}
+
+static const struct pw_session_methods session_methods = {
+ PW_VERSION_SESSION_METHODS,
+ .subscribe_params = method_subscribe_params,
+ .enum_params = method_enum_params,
+ .set_param = method_set_param,
+};
+
+static int global_bind(void *object, struct pw_impl_client *client,
+ uint32_t permissions, uint32_t version, uint32_t id)
+{
+ struct impl *impl = object;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions,
+ PW_TYPE_INTERFACE_Session,
+ version, sizeof(*data));
+ if (resource == NULL)
+ return -errno;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = impl;
+ data->resource = resource;
+
+ pw_global_add_resource(impl->global, resource);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &session_methods, data);
+
+ impl->cached_info->change_mask = PW_SESSION_CHANGE_MASK_ALL;
+ pw_session_resource_info(resource, impl->cached_info);
+ impl->cached_info->change_mask = 0;
+
+ return 0;
+}
+
+static void global_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->global_listener);
+ impl->global = NULL;
+ if (impl->resource)
+ pw_resource_destroy(impl->resource);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static void impl_resource_destroy(void *data)
+{
+ struct impl *impl = data;
+ struct param_data *pdata, *tmp;
+
+ spa_hook_remove(&impl->resource_listener);
+ impl->resource = NULL;
+
+ /* clear cache */
+ if (impl->cached_info)
+ pw_session_info_free(impl->cached_info);
+ spa_list_for_each_safe(pdata, tmp, &impl->cached_params, link) {
+ struct spa_pod **pod;
+ pw_array_for_each(pod, &pdata->params)
+ free(*pod);
+ pw_array_clear(&pdata->params);
+ spa_list_remove(&pdata->link);
+ free(pdata);
+ }
+
+ if (impl->global)
+ pw_global_destroy(impl->global);
+}
+
+static void register_global(struct impl *impl)
+{
+ impl->cached_info->id = pw_global_get_id (impl->global);
+ pw_resource_set_bound_id(impl->resource, impl->cached_info->id);
+ pw_global_register(impl->global);
+ impl->registered = true;
+}
+
+static void impl_resource_pong (void *data, int seq)
+{
+ struct impl *impl = data;
+
+ /* complete registration, if this was the initial sync */
+ if (!impl->registered && seq == impl->ping_seq) {
+ register_global(impl);
+ }
+}
+
+static const struct pw_resource_events impl_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = impl_resource_destroy,
+ .pong = impl_resource_pong,
+};
+
+static int emit_info(void *data, struct pw_resource *resource)
+{
+ const struct pw_session_info *info = data;
+ pw_session_resource_info(resource, info);
+ return 0;
+}
+
+static void event_info(void *data, const struct pw_session_info *info)
+{
+ struct impl *impl = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ uint32_t i;
+
+ /* figure out changes to params */
+ if (info->change_mask & PW_SESSION_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if ((!impl->cached_info ||
+ info->params[i].flags != impl->cached_info->params[i].flags)
+ && info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = info->params[i].id;
+ }
+ }
+
+ /* cache for new clients */
+ impl->cached_info = pw_session_info_update (impl->cached_info, info);
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_info, (void*) info);
+
+ /* cache params & register */
+ if (n_changed_ids > 0) {
+ /* prepare params storage */
+ for (i = 0; i < n_changed_ids; i++) {
+ struct param_data *pdata = calloc(1, sizeof(struct param_data));
+ pdata->id = changed_ids[i];
+ pw_array_init(&pdata->params, sizeof(void*));
+ spa_list_append(&impl->cached_params, &pdata->link);
+ }
+
+ /* subscribe to impl */
+ pw_session_subscribe_params(impl->session, changed_ids, n_changed_ids);
+
+ /* register asynchronously on the pong event */
+ impl->ping_seq = pw_resource_ping(impl->resource, 0);
+ }
+ else if (!impl->registered) {
+ register_global(impl);
+ }
+}
+
+struct param_event_args
+{
+ uint32_t id, index, next;
+ const struct spa_pod *param;
+};
+
+static int emit_param(void *_data, struct pw_resource *resource)
+{
+ struct param_event_args *args = _data;
+ struct resource_data *data;
+ uint32_t i;
+
+ data = pw_resource_get_user_data(resource);
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == args->id) {
+ pw_session_resource_param(resource, 1,
+ args->id, args->index, args->next, args->param);
+ }
+ }
+ return 0;
+}
+
+static void event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct impl *impl = data;
+ struct param_data *pdata;
+ struct spa_pod **pod;
+ struct param_event_args args = { id, index, next, param };
+
+ /* cache for new requests */
+ spa_list_for_each(pdata, &impl->cached_params, link) {
+ if (pdata->id != id)
+ continue;
+
+ if (!pw_array_check_index(&pdata->params, index, void*)) {
+ while (pw_array_get_len(&pdata->params, void*) <= index)
+ pw_array_add_ptr(&pdata->params, NULL);
+ }
+
+ pod = pw_array_get_unchecked(&pdata->params, index, struct spa_pod*);
+ free(*pod);
+ *pod = spa_pod_copy(param);
+ }
+
+ /* notify existing clients */
+ pw_global_for_each_resource(impl->global, emit_param, &args);
+}
+
+static const struct pw_session_events session_events = {
+ PW_VERSION_SESSION_EVENTS,
+ .info = event_info,
+ .param = event_param,
+};
+
+static void *session_new(struct pw_context *context,
+ struct pw_resource *resource,
+ struct pw_properties *properties)
+{
+ struct impl *impl;
+ char serial_str[32];
+ struct spa_dict_item items[1] = {
+ SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_SERIAL, serial_str),
+ };
+ struct spa_dict extra_props = SPA_DICT_INIT_ARRAY(items);
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ NULL
+ };
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL) {
+ pw_properties_free(properties);
+ return NULL;
+ }
+
+ impl->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ properties,
+ global_bind, impl);
+ if (impl->global == NULL) {
+ free(impl);
+ return NULL;
+ }
+ impl->resource = resource;
+
+ spa_scnprintf(serial_str, sizeof(serial_str), "%"PRIu64,
+ pw_global_get_serial(impl->global));
+ pw_global_update_keys(impl->global, &extra_props, keys);
+
+ spa_list_init(&impl->cached_params);
+
+ /* handle destroy events */
+ pw_global_add_listener(impl->global,
+ &impl->global_listener,
+ &global_events, impl);
+ pw_resource_add_listener(impl->resource,
+ &impl->resource_listener,
+ &impl_resource_events, impl);
+
+ /* handle implementation events -> cache + client resources */
+ pw_session_add_listener(impl->session,
+ &impl->session_listener,
+ &session_events, impl);
+
+ /* global is not registered here on purpose;
+ we first cache info + params and then expose the global */
+
+ return impl;
+}
+
+static void *create_object(void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *d = data;
+ struct pw_resource *impl_resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ void *result;
+ int res;
+
+ impl_resource = pw_resource_new(client, new_id, PW_PERM_ALL, type, version, 0);
+ if (impl_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ pw_resource_install_marshal(impl_resource, true);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -ENOMEM;
+ goto error_session;
+ }
+
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_impl_client_get_info(client)->id);
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_impl_factory_get_info(d->factory)->id);
+
+ result = session_new(pw_impl_client_get_context(client), impl_resource, properties);
+ if (result == NULL) {
+ res = -errno;
+ goto error_session;
+ }
+ return result;
+
+error_resource:
+ pw_log_error("can't create resource: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create resource: %s", spa_strerror(res));
+ goto error_exit;
+error_session:
+ pw_log_error("can't create session: %s", spa_strerror(res));
+ pw_resource_errorf_id(resource, new_id, res, "can't create session: %s", spa_strerror(res));
+ goto error_exit_free;
+
+error_exit_free:
+ pw_resource_remove(impl_resource);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation impl_factory = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->factory_listener);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ spa_list_remove(&d->export.link);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_impl_module_get_info(module)->id);
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error(NAME" %p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+int session_factory_init(struct pw_impl_module *module)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+ int res;
+
+ factory = pw_context_create_factory(context,
+ "session",
+ PW_TYPE_INTERFACE_Session,
+ PW_VERSION_SESSION,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+
+ pw_impl_factory_set_implementation(factory, &impl_factory, data);
+
+ data->export.type = PW_TYPE_INTERFACE_Session;
+ data->export.func = pw_core_session_export;
+ if ((res = pw_context_register_export_type(context, &data->export)) < 0)
+ goto error;
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+error:
+ pw_impl_factory_destroy(data->factory);
+ return res;
+}
diff --git a/src/modules/module-x11-bell.c b/src/modules/module-x11-bell.c
new file mode 100644
index 0000000..908f397
--- /dev/null
+++ b/src/modules/module-x11-bell.c
@@ -0,0 +1,364 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/string.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xlib-xcb.h>
+#include <X11/XKBlib.h>
+
+#ifdef HAVE_XFIXES_6
+#include <X11/extensions/Xfixes.h>
+#endif
+
+#include <canberra.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+/** \page page_module_x11_bell PipeWire Module: X11 Bell
+ *
+ * The `x11-bell` module intercept the X11 bell events and uses libcanberra to
+ * play a sound.
+ *
+ * ## Module Options
+ *
+ * - `sink.name = <str>`: node.name of the sink to connect to
+ * - `sample.name = <str>`: the name of the sample to play, default 'bell-window-system'
+ * - `x11.display = <str>`: the X11 display to use
+ * - `x11.xauthority = <str>`: the X11 XAuthority string placed in XAUTHORITY env
+ *
+ * ## General options
+ *
+ * There are no general options for this module.
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-x11-bell }
+ * args = {
+ * #sink.name = @DEFAULT_SINK@
+ * sample.name = "bell-window-system"
+ * #x11.display = ":1"
+ * #x11.xauthority = "test"
+ * ]
+ *\endcode
+ *
+ */
+
+#define NAME "x11-bell"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+struct impl {
+ struct pw_context *context;
+ struct pw_thread_loop *thread_loop;
+ struct pw_loop *thread_loop_loop;
+ struct pw_loop *loop;
+ struct spa_source *source;
+
+ struct pw_properties *properties;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ Display *display;
+};
+
+static int play_sample(struct impl *impl)
+{
+ const char *sample = NULL;
+ ca_context *ca;
+ int res;
+
+ if (impl->properties)
+ sample = pw_properties_get(impl->properties, "sample.name");
+ if (sample == NULL)
+ sample = "bell-window-system";
+
+ pw_log_info("play sample %s", sample);
+
+ if ((res = ca_context_create(&ca)) < 0) {
+ pw_log_error("canberra context create error: %s", ca_strerror(res));
+ res = -EIO;
+ goto exit;
+ }
+ if ((res = ca_context_open(ca)) < 0) {
+ pw_log_error("canberra context open error: %s", ca_strerror(res));
+ res = -EIO;
+ goto exit_destroy;
+ }
+ if ((res = ca_context_play(ca, 0,
+ CA_PROP_EVENT_ID, sample,
+ CA_PROP_MEDIA_NAME, "X11 bell event",
+ CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
+ NULL)) < 0) {
+ pw_log_warn("can't play sample (%s): %s", sample, ca_strerror(res));
+ res = -EIO;
+ goto exit_destroy;
+ }
+
+exit_destroy:
+ ca_context_destroy(ca);
+exit:
+ return res;
+}
+
+static int do_play_sample(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ play_sample(user_data);
+ return 0;
+}
+
+static void display_io(void *data, int fd, uint32_t mask)
+{
+ struct impl *impl = data;
+ XEvent e;
+
+ while (XPending(impl->display)) {
+ XNextEvent(impl->display, &e);
+
+ if (((XkbEvent*) &e)->any.xkb_type != XkbBellNotify)
+ continue;
+
+ pw_loop_invoke(impl->thread_loop_loop, do_play_sample, 0, NULL, 0, false, impl);
+ }
+}
+
+#ifdef HAVE_XSETIOERROREXITHANDLER
+static void x11_io_error_exit_handler(Display *display, void *data)
+{
+ struct impl *impl = data;
+
+ spa_assert(display == impl->display);
+
+ pw_log_warn("X11 display (%s) has encountered a fatal I/O error", DisplayString(display));
+
+ pw_loop_destroy_source(impl->loop, impl->source);
+ impl->source = NULL;
+
+ pw_impl_module_schedule_destroy(impl->module);
+}
+#endif
+
+static int x11_connect(struct impl *impl, const char *name)
+{
+ int major, minor;
+ unsigned int auto_ctrls, auto_values;
+
+ if (!(impl->display = XOpenDisplay(name))) {
+ pw_log_error("XOpenDisplay() failed");
+ return -EIO;
+ }
+
+ impl->source = pw_loop_add_io(impl->loop,
+ ConnectionNumber(impl->display),
+ SPA_IO_IN, false, display_io, impl);
+ if (!impl->source)
+ return -errno;
+
+#ifdef HAVE_XSETIOERROREXITHANDLER
+ XSetIOErrorExitHandler(impl->display, x11_io_error_exit_handler, impl);
+#endif
+
+#ifdef HAVE_XFIXES_6
+ XFixesSetClientDisconnectMode(impl->display, XFixesClientDisconnectFlagTerminate);
+#endif
+
+ major = XkbMajorVersion;
+ minor = XkbMinorVersion;
+
+ if (!XkbLibraryVersion(&major, &minor)) {
+ pw_log_error("XkbLibraryVersion() failed");
+ return -EIO;
+ }
+
+ major = XkbMajorVersion;
+ minor = XkbMinorVersion;
+
+ if (!XkbQueryExtension(impl->display, NULL, NULL, NULL, &major, &minor)) {
+ pw_log_error("XkbQueryExtension() failed");
+ return -EIO;
+ }
+
+ XkbSelectEvents(impl->display, XkbUseCoreKbd, XkbBellNotifyMask, XkbBellNotifyMask);
+ auto_ctrls = auto_values = XkbAudibleBellMask;
+ XkbSetAutoResetControls(impl->display, XkbAudibleBellMask, &auto_ctrls, &auto_values);
+ XkbChangeEnabledControls(impl->display, XkbUseCoreKbd, XkbAudibleBellMask, 0);
+
+ return 0;
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+
+ if (impl->module)
+ spa_hook_remove(&impl->module_listener);
+
+ if (impl->source)
+ pw_loop_destroy_source(impl->loop, impl->source);
+
+ if (impl->display)
+ XCloseDisplay(impl->display);
+
+ if (impl->thread_loop)
+ pw_thread_loop_destroy(impl->thread_loop);
+
+ pw_properties_free(impl->properties);
+
+ free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static const struct spa_dict_item module_x11_bell_info[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "X11 Bell interceptor" },
+ { PW_KEY_MODULE_USAGE, "sink.name=<name for the sink> "
+ "sample.name=<the sample name> "
+ "x11.display=<the X11 display> "
+ "x11.xauthority=<the X11 XAuthority> " },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct impl *impl;
+ const char *name = NULL, *str;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ return -ENOMEM;
+
+ pw_log_debug("module %p: new", impl);
+
+ impl->context = context;
+ impl->loop = pw_context_get_main_loop(context);
+
+ impl->thread_loop = pw_thread_loop_new("X11 Bell", NULL);
+ if (impl->thread_loop == NULL) {
+ res = -errno;
+ pw_log_error("can't create thread loop: %m");
+ goto error;
+ }
+ impl->thread_loop_loop = pw_thread_loop_get_loop(impl->thread_loop);
+ impl->properties = args ? pw_properties_new_string(args) : NULL;
+
+ impl->module = module;
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_x11_bell_info));
+
+ if (impl->properties) {
+ if ((str = pw_properties_get(impl->properties, "x11.xauthority")) != NULL) {
+ if (setenv("XAUTHORITY", str, 1)) {
+ res = -errno;
+ pw_log_error("XAUTHORITY setenv failed: %m");
+ goto error;
+ }
+ }
+ name = pw_properties_get(impl->properties, "x11.display");
+ }
+
+ /* we need to use a thread loop because this module will connect
+ * to pipewire eventually and will then block the mainloop. */
+ pw_thread_loop_start(impl->thread_loop);
+
+ res = x11_connect(impl, name);
+ if (res < 0)
+ goto error;
+
+ return 0;
+error:
+ module_destroy(impl);
+ return res;
+
+}
+
+static int x11_error_handler(Display *display, XErrorEvent *error)
+{
+ pw_log_warn("X11 error handler called on display %s with error %d",
+ DisplayString(display), error->error_code);
+ return 0;
+}
+
+static int x11_io_error_handler(Display *display)
+{
+ pw_log_warn("X11 I/O error handler called on display %s", DisplayString(display));
+ return 0;
+}
+
+__attribute__((constructor))
+static void set_x11_handlers(void)
+{
+ {
+ XErrorHandler prev = XSetErrorHandler(NULL);
+ XErrorHandler def = XSetErrorHandler(x11_error_handler);
+
+ if (prev != def)
+ XSetErrorHandler(prev);
+ }
+
+ {
+ XIOErrorHandler prev = XSetIOErrorHandler(NULL);
+ XIOErrorHandler def = XSetIOErrorHandler(x11_io_error_handler);
+
+ if (prev != def)
+ XSetIOErrorHandler(prev);
+ }
+}
+
+__attribute__((destructor))
+static void restore_x11_handlers(void)
+{
+ {
+ XErrorHandler prev = XSetErrorHandler(NULL);
+ if (prev != x11_error_handler)
+ XSetErrorHandler(prev);
+ }
+
+ {
+ XIOErrorHandler prev = XSetIOErrorHandler(NULL);
+ if (prev != x11_io_error_handler)
+ XSetIOErrorHandler(prev);
+ }
+}
diff --git a/src/modules/module-zeroconf-discover.c b/src/modules/module-zeroconf-discover.c
new file mode 100644
index 0000000..5e7436f
--- /dev/null
+++ b/src/modules/module-zeroconf-discover.c
@@ -0,0 +1,565 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include <avahi-client/lookup.h>
+#include <avahi-common/error.h>
+#include <avahi-common/malloc.h>
+
+#include "module-protocol-pulse/format.h"
+#include "module-zeroconf-discover/avahi-poll.h"
+
+/** \page page_module_zeroconf_discover PipeWire Module: Zeroconf Discover
+ *
+ * Use zeroconf to detect and load module-pulse-tunnel with the right
+ * parameters. This will automatically create sinks and sources to stream
+ * audio to/from remote PulseAudio servers. It also works with
+ * module-protocol-pulse.
+ *
+ * ## Module Options
+ *
+ * - `pulse.latency`: the latency to end-to-end latency in milliseconds to
+ * maintain (Default 200ms).
+ *
+ * ## Example configuration
+ *
+ *\code{.unparsed}
+ * context.modules = [
+ * { name = libpipewire-module-zeroconf-discover
+ * args = { }
+ * }
+ * ]
+ *\endcode
+ */
+
+#define NAME "zeroconf-discover"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "pulse.latency=<latency in msec> "
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Discover remote streams" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+#define SERVICE_TYPE_SINK "_pulse-sink._tcp"
+#define SERVICE_TYPE_SOURCE "_non-monitor._sub._pulse-source._tcp"
+
+
+struct impl {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_properties *properties;
+
+ AvahiPoll *avahi_poll;
+ AvahiClient *client;
+ AvahiServiceBrowser *sink_browser;
+ AvahiServiceBrowser *source_browser;
+
+ struct spa_list tunnel_list;
+};
+
+struct tunnel_info {
+ AvahiIfIndex interface;
+ AvahiProtocol protocol;
+ const char *name;
+ const char *type;
+ const char *domain;
+};
+
+#define TUNNEL_INFO(...) ((struct tunnel_info){ __VA_ARGS__ })
+
+struct tunnel {
+ struct spa_list link;
+ struct tunnel_info info;
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+};
+
+static int start_client(struct impl *impl);
+
+static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *info)
+{
+ struct tunnel *t;
+
+ t = calloc(1, sizeof(*t));
+ if (t == NULL)
+ return NULL;
+
+ t->info.interface = info->interface;
+ t->info.protocol = info->protocol;
+ t->info.name = strdup(info->name);
+ t->info.type = strdup(info->type);
+ t->info.domain = strdup(info->domain);
+ spa_list_append(&impl->tunnel_list, &t->link);
+
+ return t;
+}
+
+static struct tunnel *find_tunnel(struct impl *impl, const struct tunnel_info *info)
+{
+ struct tunnel *t;
+ spa_list_for_each(t, &impl->tunnel_list, link) {
+ if (t->info.interface == info->interface &&
+ t->info.protocol == info->protocol &&
+ spa_streq(t->info.name, info->name) &&
+ spa_streq(t->info.type, info->type) &&
+ spa_streq(t->info.domain, info->domain))
+ return t;
+ }
+ return NULL;
+}
+
+static void free_tunnel(struct tunnel *t)
+{
+ pw_impl_module_destroy(t->module);
+}
+
+static void impl_free(struct impl *impl)
+{
+ struct tunnel *t;
+
+ spa_list_consume(t, &impl->tunnel_list, link)
+ free_tunnel(t);
+
+ if (impl->sink_browser)
+ avahi_service_browser_free(impl->sink_browser);
+ if (impl->source_browser)
+ avahi_service_browser_free(impl->source_browser);
+ if (impl->client)
+ avahi_client_free(impl->client);
+ if (impl->avahi_poll)
+ pw_avahi_poll_free(impl->avahi_poll);
+ pw_properties_free(impl->properties);
+ free(impl);
+}
+
+static void module_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->module_listener);
+ impl_free(impl);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+static void pw_properties_from_avahi_string(const char *key, const char *value,
+ struct pw_properties *props)
+{
+ if (spa_streq(key, "device")) {
+ pw_properties_set(props, PW_KEY_TARGET_OBJECT, value);
+ }
+ else if (spa_streq(key, "rate")) {
+ pw_properties_set(props, PW_KEY_AUDIO_RATE, value);
+ }
+ else if (spa_streq(key, "channels")) {
+ pw_properties_set(props, PW_KEY_AUDIO_CHANNELS, value);
+ }
+ else if (spa_streq(key, "channel_map")) {
+ struct channel_map channel_map;
+ uint32_t i, pos[CHANNELS_MAX];
+ char *p, *s;
+
+ spa_zero(channel_map);
+ channel_map_parse(value, &channel_map);
+ channel_map_to_positions(&channel_map, pos);
+
+ p = s = alloca(4 + channel_map.channels * 8);
+ p += spa_scnprintf(p, 2, "[");
+ for (i = 0; i < channel_map.channels; i++)
+ p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",",
+ channel_id2name(pos[i]));
+ p += spa_scnprintf(p, 2, "]");
+ pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s);
+ }
+ else if (spa_streq(key, "format")) {
+ uint32_t fmt = format_paname2id(value, strlen(value));
+ if (fmt != SPA_AUDIO_FORMAT_UNKNOWN)
+ pw_properties_set(props, PW_KEY_AUDIO_FORMAT, format_id2name(fmt));
+ }
+ else if (spa_streq(key, "icon-name")) {
+ pw_properties_set(props, PW_KEY_DEVICE_ICON_NAME, value);
+ }
+ else if (spa_streq(key, "product-name")) {
+ pw_properties_set(props, PW_KEY_DEVICE_PRODUCT_NAME, value);
+ }
+ else if (spa_streq(key, "description")) {
+ pw_properties_set(props, "tunnel.remote.description", value);
+ }
+ else if (spa_streq(key, "fqdn")) {
+ pw_properties_set(props, "tunnel.remote.fqdn", value);
+ }
+ else if (spa_streq(key, "user-name")) {
+ pw_properties_set(props, "tunnel.remote.user", value);
+ }
+}
+
+static void submodule_destroy(void *data)
+{
+ struct tunnel *t = data;
+
+ spa_list_remove(&t->link);
+ spa_hook_remove(&t->module_listener);
+
+ free((char *) t->info.name);
+ free((char *) t->info.type);
+ free((char *) t->info.domain);
+
+ free(t);
+}
+
+static const struct pw_impl_module_events submodule_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = submodule_destroy,
+};
+
+static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiResolverEvent event, const char *name, const char *type, const char *domain,
+ const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt,
+ AvahiLookupResultFlags flags, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct tunnel *t;
+ struct tunnel_info tinfo;
+ const char *str, *device, *desc, *fqdn, *user;
+ char if_suffix[16] = "";
+ char at[AVAHI_ADDRESS_STR_MAX];
+ AvahiStringList *l;
+ FILE *f;
+ char *args;
+ size_t size;
+ struct pw_impl_module *mod;
+ struct pw_properties *props = NULL;
+
+ if (event != AVAHI_RESOLVER_FOUND) {
+ pw_log_error("Resolving of '%s' failed: %s", name,
+ avahi_strerror(avahi_client_errno(impl->client)));
+ goto done;
+ }
+ tinfo = TUNNEL_INFO(.interface = interface,
+ .protocol = protocol,
+ .name = name,
+ .type = type,
+ .domain = domain);
+
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL) {
+ pw_log_error("Can't allocate properties: %m");
+ goto done;
+ }
+
+ for (l = txt; l; l = l->next) {
+ char *key, *value;
+
+ if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0)
+ break;
+
+ pw_properties_from_avahi_string(key, value, props);
+ avahi_free(key);
+ avahi_free(value);
+ }
+
+ if ((device = pw_properties_get(props, PW_KEY_TARGET_OBJECT)) != NULL)
+ pw_properties_setf(props, PW_KEY_NODE_NAME,
+ "tunnel.%s.%s", host_name, device);
+ else
+ pw_properties_setf(props, PW_KEY_NODE_NAME,
+ "tunnel.%s", host_name);
+
+ str = strstr(type, "sink") ? "sink" : "source";
+ pw_properties_set(props, "tunnel.mode", str);
+
+ if (a->proto == AVAHI_PROTO_INET6 &&
+ a->data.ipv6.address[0] == 0xfe &&
+ (a->data.ipv6.address[1] & 0xc0) == 0x80)
+ snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface);
+
+ pw_properties_setf(props, "pulse.server.address", " [%s%s]:%u",
+ avahi_address_snprint(at, sizeof(at), a),
+ if_suffix, port);
+
+ desc = pw_properties_get(props, "tunnel.remote.description");
+ if (desc == NULL)
+ desc = pw_properties_get(props, PW_KEY_DEVICE_PRODUCT_NAME);
+ if (desc == NULL)
+ desc = pw_properties_get(props, PW_KEY_TARGET_OBJECT);
+ if (desc == NULL)
+ desc = _("Unknown device");
+
+ fqdn = pw_properties_get(props, "tunnel.remote.fqdn");
+ if (fqdn == NULL)
+ fqdn = pw_properties_get(props, "pulse.server.address");
+ if (fqdn == NULL)
+ fqdn = host_name;
+
+ user = pw_properties_get(props, "tunnel.remote.user");
+
+ if (desc != NULL && user != NULL && fqdn != NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ _("%s on %s@%s"), desc, user, fqdn);
+ }
+ else if (desc != NULL && fqdn != NULL) {
+ pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION,
+ _("%s on %s"), desc, fqdn);
+ }
+
+ if ((str = pw_properties_get(impl->properties, "pulse.latency")) != NULL)
+ pw_properties_set(props, "pulse.latency", str);
+
+ if ((f = open_memstream(&args, &size)) == NULL) {
+ pw_log_error("Can't open memstream: %m");
+ goto done;
+ }
+
+ fprintf(f, "{");
+ pw_properties_serialize_dict(f, &props->dict, 0);
+ fprintf(f, " stream.props = {");
+ fprintf(f, " }");
+ fprintf(f, "}");
+ fclose(f);
+
+ pw_properties_free(props);
+
+ pw_log_info("loading module args:'%s'", args);
+ mod = pw_context_load_module(impl->context,
+ "libpipewire-module-pulse-tunnel",
+ args, NULL);
+ free(args);
+
+ if (mod == NULL) {
+ pw_log_error("Can't load module: %m");
+ goto done;
+ }
+
+ t = make_tunnel(impl, &tinfo);
+ if (t == NULL) {
+ pw_log_error("Can't make tunnel: %m");
+ pw_impl_module_destroy(mod);
+ goto done;
+ }
+
+ pw_impl_module_add_listener(mod, &t->module_listener, &submodule_events, t);
+
+ t->module = mod;
+
+done:
+ avahi_service_resolver_free(r);
+}
+
+
+static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
+ AvahiBrowserEvent event, const char *name, const char *type, const char *domain,
+ AvahiLookupResultFlags flags, void *userdata)
+{
+ struct impl *impl = userdata;
+ struct tunnel_info info;
+ struct tunnel *t;
+
+ if (flags & AVAHI_LOOKUP_RESULT_LOCAL)
+ return;
+
+ info = TUNNEL_INFO(.interface = interface,
+ .protocol = protocol,
+ .name = name,
+ .type = type,
+ .domain = domain);
+
+ t = find_tunnel(impl, &info);
+
+ switch (event) {
+ case AVAHI_BROWSER_NEW:
+ if (t != NULL)
+ return;
+ if (!(avahi_service_resolver_new(impl->client,
+ interface, protocol,
+ name, type, domain,
+ AVAHI_PROTO_UNSPEC, 0,
+ resolver_cb, impl)))
+ pw_log_error("can't make service resolver: %s",
+ avahi_strerror(avahi_client_errno(impl->client)));
+ break;
+ case AVAHI_BROWSER_REMOVE:
+ if (t == NULL)
+ return;
+ free_tunnel(t);
+ break;
+ default:
+ break;
+ }
+}
+
+
+static struct AvahiServiceBrowser *make_browser(struct impl *impl, const char *service_type)
+{
+ struct AvahiServiceBrowser *s;
+
+ s = avahi_service_browser_new(impl->client,
+ AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
+ service_type, NULL, 0,
+ browser_cb, impl);
+ if (s == NULL) {
+ pw_log_error("can't make browser for %s: %s", service_type,
+ avahi_strerror(avahi_client_errno(impl->client)));
+ }
+ return s;
+}
+
+static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata)
+{
+ struct impl *impl = userdata;
+
+ impl->client = c;
+
+ switch (state) {
+ case AVAHI_CLIENT_S_REGISTERING:
+ case AVAHI_CLIENT_S_RUNNING:
+ case AVAHI_CLIENT_S_COLLISION:
+ if (impl->sink_browser == NULL)
+ impl->sink_browser = make_browser(impl, SERVICE_TYPE_SINK);
+ if (impl->sink_browser == NULL)
+ goto error;
+
+ if (impl->source_browser == NULL)
+ impl->source_browser = make_browser(impl, SERVICE_TYPE_SOURCE);
+ if (impl->source_browser == NULL)
+ goto error;
+
+ break;
+ case AVAHI_CLIENT_FAILURE:
+ if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED)
+ start_client(impl);
+
+ SPA_FALLTHROUGH;
+ case AVAHI_CLIENT_CONNECTING:
+ if (impl->sink_browser) {
+ avahi_service_browser_free(impl->sink_browser);
+ impl->sink_browser = NULL;
+ }
+ if (impl->source_browser) {
+ avahi_service_browser_free(impl->source_browser);
+ impl->source_browser = NULL;
+ }
+ break;
+ default:
+ break;
+ }
+ return;
+error:
+ pw_impl_module_schedule_destroy(impl->module);
+}
+
+static int start_client(struct impl *impl)
+{
+ int res;
+ if ((impl->client = avahi_client_new(impl->avahi_poll,
+ AVAHI_CLIENT_NO_FAIL,
+ client_callback, impl,
+ &res)) == NULL) {
+ pw_log_error("can't create client: %s", avahi_strerror(res));
+ pw_impl_module_schedule_destroy(impl->module);
+ return -EIO;
+ }
+ return 0;
+}
+
+static int start_avahi(struct impl *impl)
+{
+ struct pw_loop *loop;
+
+ loop = pw_context_get_main_loop(impl->context);
+ impl->avahi_poll = pw_avahi_poll_new(loop);
+
+ return start_client(impl);
+}
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_properties *props;
+ struct impl *impl;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_errno;
+
+ pw_log_debug("module %p: new %s", impl, args);
+
+ if (args == NULL)
+ args = "";
+
+ props = pw_properties_new_string(args);
+ if (props == NULL)
+ goto error_errno;
+
+ spa_list_init(&impl->tunnel_list);
+
+ impl->module = module;
+ impl->context = context;
+ impl->properties = props;
+
+ pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ start_avahi(impl);
+
+ return 0;
+
+error_errno:
+ res = -errno;
+ if (impl)
+ impl_free(impl);
+ return res;
+}
diff --git a/src/modules/module-zeroconf-discover/avahi-poll.c b/src/modules/module-zeroconf-discover/avahi-poll.c
new file mode 100644
index 0000000..e098646
--- /dev/null
+++ b/src/modules/module-zeroconf-discover/avahi-poll.c
@@ -0,0 +1,201 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#include "avahi-poll.h"
+
+struct impl {
+ AvahiPoll api;
+ struct pw_loop *loop;
+};
+
+struct AvahiWatch {
+ struct impl *impl;
+ struct spa_source *source;
+ AvahiWatchEvent events;
+ AvahiWatchCallback callback;
+ void *userdata;
+ unsigned int dispatching;
+};
+
+struct AvahiTimeout {
+ struct impl *impl;
+ struct spa_source *source;
+ AvahiTimeoutCallback callback;
+ void *userdata;
+};
+
+static AvahiWatchEvent from_pw_events(uint32_t mask)
+{
+ return (mask & SPA_IO_IN ? AVAHI_WATCH_IN : 0) |
+ (mask & SPA_IO_OUT ? AVAHI_WATCH_OUT : 0) |
+ (mask & SPA_IO_ERR ? AVAHI_WATCH_ERR : 0) |
+ (mask & SPA_IO_HUP ? AVAHI_WATCH_HUP : 0);
+}
+
+static uint32_t to_pw_events(AvahiWatchEvent e) {
+ return (e & AVAHI_WATCH_IN ? SPA_IO_IN : 0) |
+ (e & AVAHI_WATCH_OUT ? SPA_IO_OUT : 0) |
+ (e & AVAHI_WATCH_ERR ? SPA_IO_ERR : 0) |
+ (e & AVAHI_WATCH_HUP ? SPA_IO_HUP : 0);
+}
+
+static void watch_callback(void *data, int fd, uint32_t mask)
+{
+ AvahiWatch *w = data;
+
+ w->dispatching += 1;
+
+ w->events = from_pw_events(mask);
+ w->callback(w, fd, w->events, w->userdata);
+ w->events = 0;
+
+ if (--w->dispatching == 0 && !w->source)
+ free(w);
+}
+
+static AvahiWatch* watch_new(const AvahiPoll *api, int fd, AvahiWatchEvent event,
+ AvahiWatchCallback callback, void *userdata)
+{
+ struct impl *impl = api->userdata;
+ AvahiWatch *w;
+
+ w = calloc(1, sizeof(*w));
+ if (w == NULL)
+ return NULL;
+
+ w->impl = impl;
+ w->events = 0;
+ w->callback = callback;
+ w->userdata = userdata;
+ w->source = pw_loop_add_io(impl->loop, fd, to_pw_events(event),
+ false, watch_callback, w);
+
+ return w;
+}
+
+static void watch_update(AvahiWatch *w, AvahiWatchEvent event)
+{
+ struct impl *impl = w->impl;
+ pw_loop_update_io(impl->loop, w->source, to_pw_events(event));
+}
+
+static AvahiWatchEvent watch_get_events(AvahiWatch *w)
+{
+ return w->events;
+}
+
+static void watch_free(AvahiWatch *w)
+{
+ pw_loop_destroy_source(w->impl->loop, w->source);
+ w->source = NULL;
+
+ if (!w->dispatching)
+ free(w);
+}
+
+static void timeout_callback(void *data, uint64_t expirations)
+{
+ AvahiTimeout *w = data;
+ w->callback(w, w->userdata);
+}
+
+static AvahiTimeout* timeout_new(const AvahiPoll *api, const struct timeval *tv,
+ AvahiTimeoutCallback callback, void *userdata)
+{
+ struct impl *impl = api->userdata;
+ struct timespec value;
+ AvahiTimeout *w;
+
+ w = calloc(1, sizeof(*w));
+ if (w == NULL)
+ return NULL;
+
+ w->impl = impl;
+ w->callback = callback;
+ w->userdata = userdata;
+ w->source = pw_loop_add_timer(impl->loop, timeout_callback, w);
+
+ if (tv != NULL) {
+ value.tv_sec = tv->tv_sec;
+ value.tv_nsec = tv->tv_usec * 1000UL;
+ pw_loop_update_timer(impl->loop, w->source, &value, NULL, true);
+ }
+ return w;
+}
+
+static void timeout_update(AvahiTimeout *t, const struct timeval *tv)
+{
+ struct impl *impl = t->impl;
+ struct timespec value, *v = NULL;
+
+ if (tv != NULL) {
+ value.tv_sec = tv->tv_sec;
+ value.tv_nsec = tv->tv_usec * 1000UL;
+ if (value.tv_sec == 0 && value.tv_nsec == 0)
+ value.tv_nsec = 1;
+ v = &value;
+ }
+ pw_loop_update_timer(impl->loop, t->source, v, NULL, true);
+}
+
+static void timeout_free(AvahiTimeout *t)
+{
+ struct impl *impl = t->impl;
+ pw_loop_destroy_source(impl->loop, t->source);
+ free(t);
+}
+
+static const AvahiPoll avahi_poll_api = {
+ .watch_new = watch_new,
+ .watch_update = watch_update,
+ .watch_get_events = watch_get_events,
+ .watch_free = watch_free,
+ .timeout_new = timeout_new,
+ .timeout_update = timeout_update,
+ .timeout_free = timeout_free,
+};
+
+AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop)
+{
+ struct impl *impl;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL)
+ return NULL;
+
+ impl->loop = loop;
+ impl->api = avahi_poll_api;
+ impl->api.userdata = impl;
+
+ return &impl->api;
+}
+
+void pw_avahi_poll_free(AvahiPoll *p)
+{
+ struct impl *impl = SPA_CONTAINER_OF(p, struct impl, api);
+
+ free(impl);
+}
diff --git a/src/modules/module-zeroconf-discover/avahi-poll.h b/src/modules/module-zeroconf-discover/avahi-poll.h
new file mode 100644
index 0000000..04b785d
--- /dev/null
+++ b/src/modules/module-zeroconf-discover/avahi-poll.h
@@ -0,0 +1,31 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <avahi-client/client.h>
+
+#include <pipewire/loop.h>
+
+AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop);
+
+void pw_avahi_poll_free(AvahiPoll *p);
diff --git a/src/modules/spa/meson.build b/src/modules/spa/meson.build
new file mode 100644
index 0000000..8332910
--- /dev/null
+++ b/src/modules/spa/meson.build
@@ -0,0 +1,31 @@
+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/module-device-factory.c b/src/modules/spa/module-device-factory.c
new file mode 100644
index 0000000..fd712a2
--- /dev/null
+++ b/src/modules/spa/module-device-factory.c
@@ -0,0 +1,281 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+
+#include "pipewire/impl.h"
+
+#include "spa-device.h"
+
+#define NAME "spa-device-factory"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FACTORY_USAGE SPA_KEY_FACTORY_NAME"=<factory-name> " \
+ "["SPA_KEY_LIBRARY_NAME"=<library-name>]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Provide a factory to make SPA devices" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct factory_data {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct spa_list device_list;
+};
+
+struct device_data {
+ struct spa_list link;
+ struct pw_impl_device *device;
+ struct spa_hook device_listener;
+ struct spa_hook resource_listener;
+};
+
+static void resource_destroy(void *data)
+{
+ struct device_data *nd = data;
+ pw_log_debug("device %p", nd);
+ spa_hook_remove(&nd->resource_listener);
+ if (nd->device)
+ pw_impl_device_destroy(nd->device);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy
+};
+
+static void device_destroy(void *data)
+{
+ struct device_data *nd = data;
+ spa_list_remove(&nd->link);
+ spa_hook_remove(&nd->device_listener);
+ nd->device = NULL;
+}
+
+static const struct pw_impl_device_events device_events = {
+ PW_VERSION_IMPL_DEVICE_EVENTS,
+ .destroy = device_destroy,
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *data = _data;
+ struct pw_context *context = data->context;
+ struct pw_impl_device *device;
+ const char *str;
+ char *factory_name = NULL;
+ struct device_data *nd;
+ struct pw_impl_client *client;
+ int res;
+
+ if (properties == NULL)
+ goto error_properties;
+
+ if ((str = pw_properties_get(properties, SPA_KEY_FACTORY_NAME)) == NULL)
+ goto error_properties;
+
+ if ((factory_name = strdup(str)) == NULL)
+ goto error_properties;
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_global_get_id(pw_impl_factory_get_global(data->factory)));
+
+ client = resource ? pw_resource_get_client(resource) : NULL;
+
+ if (client) {
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_global_get_id(pw_impl_client_get_global(client)));
+ }
+
+ device = pw_spa_device_load(context,
+ factory_name,
+ 0,
+ properties,
+ sizeof(struct device_data));
+ if (device == NULL) {
+ res = -errno;
+ goto error_device;
+ }
+
+ nd = pw_spa_device_get_user_data(device);
+ nd->device = device;
+ spa_list_append(&data->device_list, &nd->link);
+
+ pw_impl_device_add_listener(device, &nd->device_listener, &device_events, nd);
+
+ if (client) {
+ struct pw_resource *bound_resource;
+
+ res = pw_global_bind(pw_impl_device_get_global(device),
+ client,
+ PW_PERM_ALL, version,
+ new_id);
+ if (res < 0)
+ goto error_bind;
+
+ if ((bound_resource = pw_impl_client_find_resource(client, new_id)) == NULL)
+ goto error_bind;
+
+ pw_resource_add_listener(bound_resource, &nd->resource_listener, &resource_events, nd);
+ }
+ free(factory_name);
+ return device;
+
+error_properties:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, "usage: "FACTORY_USAGE);
+ goto error_exit_cleanup;
+error_device:
+ pw_resource_errorf_id(resource, new_id, res,
+ "can't create device %s: %s", factory_name,
+ spa_strerror(res));
+ goto error_exit;
+error_bind:
+ pw_resource_errorf_id(resource, new_id, res, "can't bind device");
+ pw_impl_device_destroy(device);
+ goto error_exit;
+
+error_exit_cleanup:
+ pw_properties_free(properties);
+error_exit:
+ free(factory_name);
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation factory_impl = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ struct device_data *nd;
+
+ spa_hook_remove(&d->factory_listener);
+
+ spa_list_consume(nd, &d->device_list, link)
+ pw_impl_device_destroy(nd->device);
+
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "spa-device-factory",
+ PW_TYPE_INTERFACE_Device,
+ PW_VERSION_DEVICE,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->module = module;
+ data->context = context;
+ spa_list_init(&data->device_list);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_factory_set_implementation(factory, &factory_impl, data);
+
+ pw_log_debug("module %p: new", module);
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ return 0;
+}
diff --git a/src/modules/spa/module-device.c b/src/modules/spa/module-device.c
new file mode 100644
index 0000000..7bd6821
--- /dev/null
+++ b/src/modules/spa/module-device.c
@@ -0,0 +1,127 @@
+/* PipeWire
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <getopt.h>
+#include <limits.h>
+
+#include <pipewire/impl.h>
+
+#include "spa-device.h"
+
+#define NAME "spa-device"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "<factory> [key=value ...]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Load and manage an SPA device" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct device_data {
+ struct pw_impl_device *this;
+ struct pw_context *context;
+
+ struct spa_hook module_listener;
+};
+
+static void module_destroy(void *_data)
+{
+ struct device_data *data = _data;
+
+ spa_hook_remove(&data->module_listener);
+
+ pw_impl_device_destroy(data->this);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_properties *props = NULL;
+ char **argv = NULL;
+ int n_tokens;
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_device *device;
+ struct device_data *data;
+ int res;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if (args == NULL)
+ goto error_arguments;
+
+ argv = pw_split_strv(args, " \t", 2, &n_tokens);
+ if (n_tokens < 1)
+ goto error_arguments;
+
+ if (n_tokens == 2) {
+ props = pw_properties_new_string(argv[1]);
+ if (props == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+ }
+
+ device = pw_spa_device_load(context,
+ argv[0],
+ 0,
+ props,
+ sizeof(struct device_data));
+ if (device == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+
+ pw_free_strv(argv);
+
+ data = pw_spa_device_get_user_data(device);
+ data->this = device;
+ data->context = context;
+
+ pw_log_debug("module %p: new", module);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error_arguments:
+ res = -EINVAL;
+ pw_log_error("usage: module-spa-device " MODULE_USAGE);
+ goto error_exit_cleanup;
+error_exit_cleanup:
+ pw_free_strv(argv);
+ return res;
+}
diff --git a/src/modules/spa/module-node-factory.c b/src/modules/spa/module-node-factory.c
new file mode 100644
index 0000000..7fb2a42
--- /dev/null
+++ b/src/modules/spa/module-node-factory.c
@@ -0,0 +1,280 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include <spa/utils/result.h>
+
+#include "config.h"
+
+#include "pipewire/impl.h"
+
+#include "spa-node.h"
+
+#define NAME "spa-node-factory"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define FACTORY_USAGE SPA_KEY_FACTORY_NAME"=<factory-name> " \
+ "["SPA_KEY_LIBRARY_NAME"=<library-name>]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Provide a factory to make SPA nodes" },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct factory_data {
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ struct pw_impl_factory *factory;
+ struct spa_hook factory_listener;
+
+ struct spa_list node_list;
+};
+
+struct node_data {
+ struct factory_data *data;
+ struct spa_list link;
+ struct pw_impl_node *node;
+ struct spa_hook node_listener;
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ unsigned int linger:1;
+};
+
+static void resource_destroy(void *data)
+{
+ struct node_data *nd = data;
+ pw_log_debug("node %p", nd);
+ spa_hook_remove(&nd->resource_listener);
+ nd->resource = NULL;
+ if (nd->node && !nd->linger)
+ pw_impl_node_destroy(nd->node);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy
+};
+
+static void node_destroy(void *data)
+{
+ struct node_data *nd = data;
+ pw_log_debug("node %p", nd);
+ spa_list_remove(&nd->link);
+ spa_hook_remove(&nd->node_listener);
+ nd->node = NULL;
+
+ if (nd->resource) {
+ spa_hook_remove(&nd->resource_listener);
+ nd->resource = NULL;
+ }
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .destroy = node_destroy,
+};
+
+static void *create_object(void *_data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ struct factory_data *data = _data;
+ struct pw_context *context = data->context;
+ struct pw_impl_node *node;
+ const char *factory_name;
+ struct node_data *nd;
+ int res;
+ struct pw_impl_client *client;
+ bool linger;
+
+ if (properties == NULL)
+ goto error_properties;
+
+ factory_name = pw_properties_get(properties, SPA_KEY_FACTORY_NAME);
+ if (factory_name == NULL)
+ goto error_properties;
+
+ pw_properties_setf(properties, PW_KEY_FACTORY_ID, "%d",
+ pw_global_get_id(pw_impl_factory_get_global(data->factory)));
+
+ linger = pw_properties_get_bool(properties, PW_KEY_OBJECT_LINGER, false);
+
+ client = resource ? pw_resource_get_client(resource) : NULL;
+ if (client && !linger) {
+ pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d",
+ pw_global_get_id(pw_impl_client_get_global(client)));
+ }
+ node = pw_spa_node_load(context,
+ factory_name,
+ PW_SPA_NODE_FLAG_ACTIVATE,
+ properties,
+ sizeof(struct node_data));
+ if (node == NULL)
+ goto error_create_node;
+
+ nd = pw_spa_node_get_user_data(node);
+ nd->data = data;
+ nd->node = node;
+ nd->linger = linger;
+ spa_list_append(&data->node_list, &nd->link);
+
+ pw_impl_node_add_listener(node, &nd->node_listener, &node_events, nd);
+
+ if (client) {
+ res = pw_global_bind(pw_impl_node_get_global(node),
+ client, PW_PERM_ALL, version, new_id);
+ if (res < 0)
+ goto error_bind;
+
+ if ((nd->resource = pw_impl_client_find_resource(client, new_id)) == NULL)
+ goto error_bind;
+
+ pw_resource_add_listener(nd->resource, &nd->resource_listener, &resource_events, nd);
+ }
+ return node;
+
+error_properties:
+ res = -EINVAL;
+ pw_resource_errorf_id(resource, new_id, res, "usage: "FACTORY_USAGE);
+ goto error_exit_cleanup;
+error_create_node:
+ res = -errno;
+ pw_resource_errorf_id(resource, new_id, res,
+ "can't create node: %s", spa_strerror(res));
+ goto error_exit;
+error_bind:
+ pw_resource_errorf_id(resource, new_id, res, "can't bind node");
+ pw_impl_node_destroy(node);
+ goto error_exit;
+
+error_exit_cleanup:
+ pw_properties_free(properties);
+error_exit:
+ errno = -res;
+ return NULL;
+}
+
+static const struct pw_impl_factory_implementation factory_impl = {
+ PW_VERSION_IMPL_FACTORY_IMPLEMENTATION,
+ .create_object = create_object,
+};
+
+static void factory_destroy(void *data)
+{
+ struct factory_data *d = data;
+ struct node_data *nd;
+
+ spa_hook_remove(&d->factory_listener);
+ spa_list_consume(nd, &d->node_list, link)
+ pw_impl_node_destroy(nd->node);
+ d->factory = NULL;
+ if (d->module)
+ pw_impl_module_destroy(d->module);
+}
+
+static const struct pw_impl_factory_events factory_events = {
+ PW_VERSION_IMPL_FACTORY_EVENTS,
+ .destroy = factory_destroy,
+};
+
+static void module_destroy(void *data)
+{
+ struct factory_data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ if (d->factory)
+ pw_impl_factory_destroy(d->factory);
+}
+
+static void module_registered(void *data)
+{
+ struct factory_data *d = data;
+ struct pw_impl_module *module = d->module;
+ struct pw_impl_factory *factory = d->factory;
+ struct spa_dict_item items[1];
+ char id[16];
+ int res;
+
+ snprintf(id, sizeof(id), "%d", pw_global_get_id(pw_impl_module_get_global(module)));
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MODULE_ID, id);
+ pw_impl_factory_update_properties(factory, &SPA_DICT_INIT(items, 1));
+
+ if ((res = pw_impl_factory_register(factory, NULL)) < 0) {
+ pw_log_error("%p: can't register factory: %s", factory, spa_strerror(res));
+ }
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+ .registered = module_registered,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_factory *factory;
+ struct factory_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ factory = pw_context_create_factory(context,
+ "spa-node-factory",
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ NULL,
+ sizeof(*data));
+ if (factory == NULL)
+ return -errno;
+
+ data = pw_impl_factory_get_user_data(factory);
+ data->factory = factory;
+ data->context = context;
+ data->module = module;
+ spa_list_init(&data->node_list);
+
+ pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data);
+ pw_impl_factory_set_implementation(factory, &factory_impl, data);
+
+ pw_log_debug("module %p: new", module);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+}
diff --git a/src/modules/spa/module-node.c b/src/modules/spa/module-node.c
new file mode 100644
index 0000000..9f66d2b
--- /dev/null
+++ b/src/modules/spa/module-node.c
@@ -0,0 +1,129 @@
+/* PipeWire
+ * Copyright © 2016 Axis Communications <dev-gstreamer@axis.com>
+ * @author Linus Svensson <linus.svensson@axis.com>
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <getopt.h>
+#include <limits.h>
+
+#include <pipewire/impl.h>
+
+#include "spa-node.h"
+
+#define NAME "spa-node"
+
+PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
+#define PW_LOG_TOPIC_DEFAULT mod_topic
+
+#define MODULE_USAGE "<factory> [key=value ...]"
+
+static const struct spa_dict_item module_props[] = {
+ { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { PW_KEY_MODULE_DESCRIPTION, "Load and manage an SPA node" },
+ { PW_KEY_MODULE_USAGE, MODULE_USAGE },
+ { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
+};
+
+struct node_data {
+ struct pw_impl_node *this;
+ struct pw_context *context;
+ struct pw_properties *properties;
+
+ struct spa_hook module_listener;
+};
+
+static void module_destroy(void *_data)
+{
+ struct node_data *data = _data;
+ spa_hook_remove(&data->module_listener);
+ pw_impl_node_destroy(data->this);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy,
+};
+
+SPA_EXPORT
+int pipewire__module_init(struct pw_impl_module *module, const char *args)
+{
+ struct pw_properties *props = NULL;
+ char **argv = NULL;
+ int n_tokens, res;
+ struct pw_context *context = pw_impl_module_get_context(module);
+ struct pw_impl_node *node;
+ struct node_data *data;
+
+ PW_LOG_TOPIC_INIT(mod_topic);
+
+ if (args == NULL)
+ goto error_arguments;
+
+ argv = pw_split_strv(args, " \t", 2, &n_tokens);
+ if (n_tokens < 1)
+ goto error_arguments;
+
+ if (n_tokens == 2) {
+ props = pw_properties_new_string(argv[1]);
+ if (props == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+ }
+
+ node = pw_spa_node_load(context,
+ argv[0],
+ PW_SPA_NODE_FLAG_ACTIVATE,
+ props,
+ sizeof(struct node_data));
+
+ if (node == NULL) {
+ res = -errno;
+ goto error_exit_cleanup;
+ }
+
+ pw_free_strv(argv);
+
+ data = pw_spa_node_get_user_data(node);
+ data->this = node;
+ data->context = context;
+ data->properties = props;
+
+ pw_log_debug("module %p: new", module);
+ pw_impl_module_add_listener(module, &data->module_listener, &module_events, data);
+
+ pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
+
+ return 0;
+
+error_arguments:
+ res = -EINVAL;
+ pw_log_error("usage: module-spa-node " MODULE_USAGE);
+ goto error_exit_cleanup;
+error_exit_cleanup:
+ pw_free_strv(argv);
+ return res;
+}
diff --git a/src/modules/spa/spa-device.c b/src/modules/spa/spa-device.c
new file mode 100644
index 0000000..936793b
--- /dev/null
+++ b/src/modules/spa/spa-device.c
@@ -0,0 +1,161 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+
+#include <spa/utils/result.h>
+#include <spa/param/props.h>
+#include <spa/pod/iter.h>
+#include <spa/debug/types.h>
+
+#include "spa-device.h"
+
+struct impl {
+ struct pw_impl_device *this;
+
+ enum pw_spa_device_flags flags;
+
+ void *unload;
+ struct spa_handle *handle;
+ struct spa_device *device;
+
+ struct spa_hook device_listener;
+
+ void *user_data;
+};
+
+static void device_free(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_device *device = impl->this;
+
+ pw_log_debug("spa-device %p: free", device);
+
+ spa_hook_remove(&impl->device_listener);
+ if (impl->handle)
+ pw_unload_spa_handle(impl->handle);
+}
+
+static const struct pw_impl_device_events device_events = {
+ PW_VERSION_IMPL_DEVICE_EVENTS,
+ .free = device_free,
+};
+
+struct pw_impl_device *
+pw_spa_device_new(struct pw_context *context,
+ enum pw_spa_device_flags flags,
+ struct spa_device *device,
+ struct spa_handle *handle,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_device *this;
+ struct impl *impl;
+ int res;
+
+ this = pw_context_create_device(context, properties, sizeof(struct impl) + user_data_size);
+ if (this == NULL)
+ return NULL;
+
+ impl = pw_impl_device_get_user_data(this);
+ impl->this = this;
+ impl->device = device;
+ impl->handle = handle;
+ impl->flags = flags;
+
+ if (user_data_size > 0)
+ impl->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ pw_impl_device_add_listener(this, &impl->device_listener, &device_events, impl);
+ pw_impl_device_set_implementation(this, impl->device);
+
+ if (!SPA_FLAG_IS_SET(impl->flags, PW_SPA_DEVICE_FLAG_NO_REGISTER)) {
+ if ((res = pw_impl_device_register(this, NULL)) < 0)
+ goto error_register;
+ }
+ return this;
+
+error_register:
+ pw_impl_device_destroy(this);
+ errno = -res;
+ return NULL;
+}
+
+void *pw_spa_device_get_user_data(struct pw_impl_device *device)
+{
+ struct impl *impl = pw_impl_device_get_user_data(device);
+ return impl->user_data;
+}
+
+struct pw_impl_device *pw_spa_device_load(struct pw_context *context,
+ const char *factory_name,
+ enum pw_spa_device_flags flags,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_device *this;
+ struct spa_handle *handle;
+ void *iface;
+ int res;
+
+ handle = pw_context_load_spa_handle(context, factory_name,
+ properties ? &properties->dict : NULL);
+ if (handle == NULL)
+ goto error_load;
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Device, &iface)) < 0)
+ goto error_interface;
+
+ this = pw_spa_device_new(context, flags,
+ iface, handle, properties, user_data_size);
+ if (this == NULL)
+ goto error_device;
+
+ return this;
+
+error_load:
+ res = -errno;
+ pw_log_debug("can't load device handle %s: %m", factory_name);
+ goto error_exit;
+error_interface:
+ pw_log_debug("can't get device interface %s: %s", factory_name,
+ spa_strerror(res));
+ goto error_exit_unload;
+error_device:
+ properties = NULL;
+ res = -errno;
+ pw_log_debug("can't create device %s: %m", factory_name);
+ goto error_exit_unload;
+
+error_exit_unload:
+ pw_unload_spa_handle(handle);
+error_exit:
+ errno = -res;
+ pw_properties_free(properties);
+ return NULL;
+}
diff --git a/src/modules/spa/spa-device.h b/src/modules/spa/spa-device.h
new file mode 100644
index 0000000..2d23b6f
--- /dev/null
+++ b/src/modules/spa/spa-device.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_SPA_DEVICE_H
+#define PIPEWIRE_SPA_DEVICE_H
+
+#include <spa/monitor/device.h>
+
+#include <pipewire/impl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum pw_spa_device_flags {
+ PW_SPA_DEVICE_FLAG_DISABLE = (1 << 0),
+ PW_SPA_DEVICE_FLAG_NO_REGISTER = (1 << 1),
+};
+
+struct pw_impl_device *
+pw_spa_device_new(struct pw_context *context,
+ enum pw_spa_device_flags flags,
+ struct spa_device *device,
+ struct spa_handle *handle,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+struct pw_impl_device *
+pw_spa_device_load(struct pw_context *context,
+ const char *factory_name,
+ enum pw_spa_device_flags flags,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+void *pw_spa_device_get_user_data(struct pw_impl_device *device);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_SPA_DEVICE_H */
diff --git a/src/modules/spa/spa-node.c b/src/modules/spa/spa-node.c
new file mode 100644
index 0000000..1f8615b
--- /dev/null
+++ b/src/modules/spa/spa-node.c
@@ -0,0 +1,292 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/utils/result.h>
+#include <spa/param/props.h>
+#include <spa/pod/iter.h>
+#include <spa/debug/types.h>
+
+#include "spa-node.h"
+
+struct impl {
+ struct pw_impl_node *this;
+
+ enum pw_spa_node_flags flags;
+
+ struct spa_handle *handle;
+ struct spa_node *node; /**< handle to SPA node */
+
+ struct spa_hook node_listener;
+ int init_pending;
+
+ void *user_data;
+
+ unsigned int async_init:1;
+};
+
+static void spa_node_free(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_node *node = impl->this;
+
+ pw_log_debug("spa-node %p: free", node);
+
+ spa_hook_remove(&impl->node_listener);
+ if (impl->handle)
+ pw_unload_spa_handle(impl->handle);
+}
+
+static void complete_init(struct impl *impl)
+{
+ struct pw_impl_node *this = impl->this;
+
+ impl->init_pending = SPA_ID_INVALID;
+
+ if (SPA_FLAG_IS_SET(impl->flags, PW_SPA_NODE_FLAG_ACTIVATE))
+ pw_impl_node_set_active(this, true);
+
+ if (!SPA_FLAG_IS_SET(impl->flags, PW_SPA_NODE_FLAG_NO_REGISTER))
+ pw_impl_node_register(this, NULL);
+ else
+ pw_impl_node_initialized(this);
+}
+
+static void spa_node_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct impl *impl = data;
+ struct pw_impl_node *node = impl->this;
+
+ if (seq == impl->init_pending) {
+ pw_log_debug("spa-node %p: init complete event %d %d", node, seq, res);
+ complete_init(impl);
+ }
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .free = spa_node_free,
+ .result = spa_node_result,
+};
+
+struct pw_impl_node *
+pw_spa_node_new(struct pw_context *context,
+ enum pw_spa_node_flags flags,
+ struct spa_node *node,
+ struct spa_handle *handle,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_node *this;
+ struct impl *impl;
+ int res;
+
+ this = pw_context_create_node(context, properties, sizeof(struct impl) + user_data_size);
+ if (this == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+
+ impl = pw_impl_node_get_user_data(this);
+ impl->this = this;
+ impl->node = node;
+ impl->handle = handle;
+ impl->flags = flags;
+
+ if (user_data_size > 0)
+ impl->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ pw_impl_node_add_listener(this, &impl->node_listener, &node_events, impl);
+ if ((res = pw_impl_node_set_implementation(this, impl->node)) < 0)
+ goto error_exit_clean_node;
+
+ if (flags & PW_SPA_NODE_FLAG_ASYNC) {
+ impl->init_pending = spa_node_sync(impl->node, res);
+ } else {
+ complete_init(impl);
+ }
+ return this;
+
+error_exit_clean_node:
+ pw_impl_node_destroy(this);
+ handle = NULL;
+error_exit:
+ if (handle)
+ pw_unload_spa_handle(handle);
+ errno = -res;
+ return NULL;
+
+}
+
+void *pw_spa_node_get_user_data(struct pw_impl_node *node)
+{
+ struct impl *impl = pw_impl_node_get_user_data(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 pw_impl_node *pw_spa_node_load(struct pw_context *context,
+ const char *factory_name,
+ enum pw_spa_node_flags flags,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_node *this;
+ struct spa_node *spa_node;
+ int res;
+ struct spa_handle *handle;
+ void *iface;
+ const struct pw_properties *p;
+
+ if (properties) {
+ p = pw_context_get_properties(context);
+ pw_properties_set(properties, "clock.quantum-limit",
+ pw_properties_get(p, "default.clock.quantum-limit"));
+ }
+
+ handle = pw_context_load_spa_handle(context,
+ factory_name,
+ properties ? &properties->dict : NULL);
+ if (handle == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+
+ if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) {
+ pw_log_error("can't get node interface %d", res);
+ goto error_exit_unload;
+ }
+ if (SPA_RESULT_IS_ASYNC(res))
+ flags |= PW_SPA_NODE_FLAG_ASYNC;
+
+ spa_node = iface;
+
+ if (properties != NULL) {
+ 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, properties, user_data_size);
+ if (this == NULL) {
+ res = -errno;
+ properties = NULL;
+ goto error_exit_unload;
+ }
+
+ return this;
+
+error_exit_unload:
+ pw_unload_spa_handle(handle);
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
diff --git a/src/modules/spa/spa-node.h b/src/modules/spa/spa-node.h
new file mode 100644
index 0000000..5289a17
--- /dev/null
+++ b/src/modules/spa/spa-node.h
@@ -0,0 +1,63 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_SPA_NODE_H
+#define PIPEWIRE_SPA_NODE_H
+
+#include <spa/node/node.h>
+
+#include <pipewire/impl.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum pw_spa_node_flags {
+ PW_SPA_NODE_FLAG_ACTIVATE = (1 << 0),
+ PW_SPA_NODE_FLAG_NO_REGISTER = (1 << 1),
+ PW_SPA_NODE_FLAG_ASYNC = (1 << 2),
+};
+
+struct pw_impl_node *
+pw_spa_node_new(struct pw_context *context,
+ enum pw_spa_node_flags flags,
+ struct spa_node *node,
+ struct spa_handle *handle,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+struct pw_impl_node *
+pw_spa_node_load(struct pw_context *context,
+ const char *factory_name,
+ enum pw_spa_node_flags flags,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+void *pw_spa_node_get_user_data(struct pw_impl_node *node);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_SPA_NODE_H */
diff --git a/src/pipewire/array.h b/src/pipewire/array.h
new file mode 100644
index 0000000..44f31a8
--- /dev/null
+++ b/src/pipewire/array.h
@@ -0,0 +1,176 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_ARRAY_H
+#define PIPEWIRE_ARRAY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+
+/** \defgroup pw_array Array
+ *
+ * \brief An array object
+ *
+ * The array is a dynamically resizable data structure that can
+ * hold items of the same size.
+ */
+
+/**
+ * \addtogroup pw_array
+ * \{
+ */
+struct pw_array {
+ void *data; /**< pointer to array data */
+ size_t size; /**< length of array in bytes */
+ size_t alloc; /**< number of allocated memory in \a data */
+ size_t extend; /**< number of bytes to extend with */
+};
+
+#define PW_ARRAY_INIT(extend) ((struct pw_array) { NULL, 0, 0, (extend) })
+
+#define pw_array_get_len_s(a,s) ((a)->size / (s))
+#define pw_array_get_unchecked_s(a,idx,s,t) SPA_PTROFF((a)->data,(idx)*(s),t)
+#define pw_array_check_index_s(a,idx,s) ((idx) < pw_array_get_len_s(a,s))
+
+/** Get the number of items of type \a t in array */
+#define pw_array_get_len(a,t) pw_array_get_len_s(a,sizeof(t))
+/** Get the item with index \a idx and type \a t from array */
+#define pw_array_get_unchecked(a,idx,t) pw_array_get_unchecked_s(a,idx,sizeof(t),t)
+/** Check if an item with index \a idx and type \a t exist in array */
+#define pw_array_check_index(a,idx,t) pw_array_check_index_s(a,idx,sizeof(t))
+
+#define pw_array_first(a) ((a)->data)
+#define pw_array_end(a) SPA_PTROFF((a)->data, (a)->size, void)
+#define pw_array_check(a,p) (SPA_PTROFF(p,sizeof(*(p)),void) <= pw_array_end(a))
+
+#define pw_array_for_each(pos, array) \
+ for ((pos) = (__typeof__(pos)) pw_array_first(array); \
+ pw_array_check(array, pos); \
+ (pos)++)
+
+#define pw_array_consume(pos, array) \
+ for ((pos) = (__typeof__(pos)) pw_array_first(array); \
+ pw_array_check(array, pos); \
+ (pos) = (__typeof__(pos)) pw_array_first(array))
+
+#define pw_array_remove(a,p) \
+({ \
+ (a)->size -= sizeof(*(p)); \
+ memmove(p, SPA_PTROFF((p), sizeof(*(p)), void), \
+ SPA_PTRDIFF(pw_array_end(a),(p))); \
+})
+
+/** Initialize the array with given extend */
+static inline void pw_array_init(struct pw_array *arr, size_t extend)
+{
+ arr->data = NULL;
+ arr->size = arr->alloc = 0;
+ arr->extend = extend;
+}
+
+/** Clear the array */
+static inline void pw_array_clear(struct pw_array *arr)
+{
+ free(arr->data);
+ pw_array_init(arr, arr->extend);
+}
+
+/** Reset the array */
+static inline 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)
+{
+ size_t alloc, need;
+
+ alloc = arr->alloc;
+ need = arr->size + size;
+
+ if (SPA_UNLIKELY(alloc < need)) {
+ void *data;
+ alloc = SPA_MAX(alloc, arr->extend);
+ spa_assert(alloc != 0); /* forgot pw_array_init */
+ while (alloc < need)
+ alloc *= 2;
+ if (SPA_UNLIKELY((data = realloc(arr->data, alloc)) == NULL))
+ return -errno;
+ arr->data = data;
+ arr->alloc = alloc;
+ }
+ return 0;
+}
+
+/** Add \a ref size bytes to \a arr. A pointer to memory that can
+ * hold at least \a size bytes is returned */
+static inline void *pw_array_add(struct pw_array *arr, size_t size)
+{
+ void *p;
+
+ if (pw_array_ensure_size(arr, size) < 0)
+ return NULL;
+
+ p = SPA_PTROFF(arr->data, arr->size, void);
+ arr->size += size;
+
+ return p;
+}
+
+/** Add \a ref size bytes to \a arr. When there is not enough memory to
+ * hold \a size bytes, NULL is returned */
+static inline void *pw_array_add_fixed(struct pw_array *arr, size_t size)
+{
+ void *p;
+
+ if (SPA_UNLIKELY(arr->alloc < arr->size + size)) {
+ errno = ENOSPC;
+ return NULL;
+ }
+
+ p = SPA_PTROFF(arr->data, arr->size, void);
+ arr->size += size;
+
+ return p;
+}
+
+/** Add a pointer to array */
+#define pw_array_add_ptr(a,p) \
+ *((void**) pw_array_add(a, sizeof(void*))) = (p)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_ARRAY_H */
diff --git a/src/pipewire/buffers.c b/src/pipewire/buffers.c
new file mode 100644
index 0000000..c3cd7d8
--- /dev/null
+++ b/src/pipewire/buffers.c
@@ -0,0 +1,368 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/node/utils.h>
+#include <spa/pod/parser.h>
+#include <spa/param/param.h>
+#include <spa/buffer/alloc.h>
+
+#include "pipewire/keys.h"
+#include "pipewire/private.h"
+
+#include "buffers.h"
+
+PW_LOG_TOPIC_EXTERN(log_buffers);
+#define PW_LOG_TOPIC_DEFAULT log_buffers
+
+#define MAX_ALIGN 32
+#define MAX_BLOCKS 64u
+
+struct port {
+ struct spa_node *node;
+ enum spa_direction direction;
+ uint32_t port_id;
+};
+
+/* Allocate an array of buffers that can be shared */
+static int alloc_buffers(struct pw_mempool *pool,
+ uint32_t n_buffers,
+ uint32_t n_params,
+ struct spa_pod **params,
+ uint32_t n_datas,
+ uint32_t *data_sizes,
+ int32_t *data_strides,
+ uint32_t *data_aligns,
+ uint32_t *data_types,
+ uint32_t flags,
+ struct pw_buffers *allocation)
+{
+ struct spa_buffer **buffers;
+ void *skel, *data;
+ uint32_t i;
+ uint32_t n_metas;
+ struct spa_meta *metas;
+ struct spa_data *datas;
+ struct pw_memblock *m;
+ struct spa_buffer_alloc_info info = { 0, };
+
+ if (!SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_SHARED))
+ SPA_FLAG_SET(info.flags, SPA_BUFFER_ALLOC_FLAG_INLINE_ALL);
+
+ n_metas = 0;
+
+ metas = alloca(sizeof(struct spa_meta) * n_params);
+ datas = alloca(sizeof(struct spa_data) * n_datas);
+
+ /* collect metadata */
+ for (i = 0; i < n_params; i++) {
+ if (spa_pod_is_object_type (params[i], SPA_TYPE_OBJECT_ParamMeta)) {
+ uint32_t type, size;
+
+ if (spa_pod_parse_object(params[i],
+ SPA_TYPE_OBJECT_ParamMeta, NULL,
+ SPA_PARAM_META_type, SPA_POD_Id(&type),
+ SPA_PARAM_META_size, SPA_POD_Int(&size)) < 0)
+ continue;
+
+ pw_log_debug("%p: enable meta %d %d", allocation, type, size);
+
+ metas[n_metas].type = type;
+ metas[n_metas].size = size;
+ n_metas++;
+ }
+ }
+
+ for (i = 0; i < n_datas; i++) {
+ struct spa_data *d = &datas[i];
+
+ spa_zero(*d);
+ if (data_sizes[i] > 0) {
+ /* we allocate memory */
+ d->type = SPA_DATA_MemPtr;
+ d->maxsize = data_sizes[i];
+ SPA_FLAG_SET(d->flags, SPA_DATA_FLAG_READWRITE);
+ } else {
+ /* client allocates memory. Set the mask of possible
+ * types in the type field */
+ d->type = data_types[i];
+ d->maxsize = 0;
+ }
+ if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_DYNAMIC))
+ SPA_FLAG_SET(d->flags, SPA_DATA_FLAG_DYNAMIC);
+ }
+
+ spa_buffer_alloc_fill_info(&info, n_metas, metas, n_datas, datas, data_aligns);
+
+ buffers = calloc(1, info.max_align + n_buffers * (sizeof(struct spa_buffer *) + info.skel_size));
+ if (buffers == NULL)
+ return -errno;
+
+ skel = SPA_PTROFF(buffers, n_buffers * sizeof(struct spa_buffer *), void);
+ skel = SPA_PTR_ALIGN(skel, info.max_align, void);
+
+ if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_SHARED)) {
+ /* pointer to buffer structures */
+ m = pw_mempool_alloc(pool,
+ PW_MEMBLOCK_FLAG_READWRITE |
+ PW_MEMBLOCK_FLAG_SEAL |
+ PW_MEMBLOCK_FLAG_MAP,
+ SPA_DATA_MemFd,
+ n_buffers * info.mem_size);
+ if (m == NULL) {
+ free(buffers);
+ return -errno;
+ }
+
+ data = m->map->ptr;
+ } else {
+ m = NULL;
+ data = NULL;
+ }
+
+ pw_log_debug("%p: layout buffers skel:%p data:%p buffers:%p",
+ allocation, skel, data, buffers);
+ spa_buffer_alloc_layout_array(&info, n_buffers, buffers, skel, data);
+
+ allocation->mem = m;
+ allocation->n_buffers = n_buffers;
+ allocation->buffers = buffers;
+ allocation->flags = flags;
+
+ return 0;
+}
+
+static int
+param_filter(struct pw_buffers *this,
+ struct port *in_port,
+ struct port *out_port,
+ uint32_t id,
+ struct spa_pod_builder *result)
+{
+ uint8_t ibuf[4096];
+ struct spa_pod_builder ib = { 0 };
+ struct spa_pod *oparam, *iparam;
+ uint32_t iidx, oidx;
+ int in_res = -EIO, out_res = -EIO, num = 0;
+
+ for (iidx = 0;;) {
+ spa_pod_builder_init(&ib, ibuf, sizeof(ibuf));
+ pw_log_debug("%p: input param %d id:%d", this, iidx, id);
+ in_res = spa_node_port_enum_params_sync(in_port->node,
+ in_port->direction, in_port->port_id,
+ id, &iidx, NULL, &iparam, &ib);
+
+ if (in_res < 1) {
+ /* in_res == -ENOENT : unknown parameter, assume NULL and we will
+ * exit the loop below.
+ * in_res < 1 : some error or no data, exit now
+ */
+ if (in_res == -ENOENT)
+ iparam = NULL;
+ else
+ break;
+ }
+
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, iparam);
+
+ for (oidx = 0;;) {
+ pw_log_debug("%p: output param %d id:%d", this, oidx, id);
+ out_res = spa_node_port_enum_params_sync(out_port->node,
+ out_port->direction, out_port->port_id,
+ id, &oidx, iparam, &oparam, result);
+
+ /* out_res < 1 : no value or error, exit now */
+ if (out_res < 1)
+ break;
+
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, oparam);
+ num++;
+ }
+ if (out_res == -ENOENT && iparam) {
+ /* no output param known but we have an input param,
+ * use that one */
+ spa_pod_builder_raw_padded(result, iparam, SPA_POD_SIZE(iparam));
+ num++;
+ }
+ /* no more input values, exit */
+ if (in_res < 1)
+ break;
+ }
+ if (num == 0) {
+ if (out_res == -ENOENT && in_res == -ENOENT)
+ return 0;
+ if (in_res < 0)
+ return in_res;
+ if (out_res < 0)
+ return out_res;
+ return -EINVAL;
+ }
+ return num;
+}
+
+static struct spa_pod *find_param(struct spa_pod **params, uint32_t n_params, uint32_t type)
+{
+ uint32_t i;
+
+ for (i = 0; i < n_params; i++) {
+ if (spa_pod_is_object_type(params[i], type))
+ return params[i];
+ }
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_buffers_negotiate(struct pw_context *context, uint32_t flags,
+ struct spa_node *outnode, uint32_t out_port_id,
+ struct spa_node *innode, uint32_t in_port_id,
+ struct pw_buffers *result)
+{
+ struct spa_pod **params, *param;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ uint32_t i, offset, n_params;
+ uint32_t max_buffers, blocks;
+ size_t minsize, stride, align;
+ uint32_t *data_sizes;
+ int32_t *data_strides;
+ uint32_t *data_aligns;
+ uint32_t types, *data_types;
+ struct port output = { outnode, SPA_DIRECTION_OUTPUT, out_port_id };
+ struct port input = { innode, SPA_DIRECTION_INPUT, in_port_id };
+ int res;
+
+ if (flags & PW_BUFFERS_FLAG_IN_PRIORITY) {
+ struct port tmp = output;
+ output = input;
+ input = tmp;
+ }
+
+ res = param_filter(result, &input, &output, SPA_PARAM_Buffers, &b);
+ if (res < 0) {
+ pw_context_debug_port_params(context, input.node, input.direction,
+ input.port_id, SPA_PARAM_Buffers, res,
+ "input param");
+ pw_context_debug_port_params(context, output.node, output.direction,
+ output.port_id, SPA_PARAM_Buffers, res,
+ "output param");
+ return res;
+ }
+ n_params = res;
+ if ((res = param_filter(result, &input, &output, SPA_PARAM_Meta, &b)) > 0)
+ n_params += res;
+
+ params = alloca(n_params * sizeof(struct spa_pod *));
+ for (i = 0, offset = 0; i < n_params; i++) {
+ params[i] = SPA_PTROFF(buffer, offset, struct spa_pod);
+ spa_pod_fixate(params[i]);
+ pw_log_debug("%p: fixated param %d:", result, i);
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, params[i]);
+ offset += SPA_ROUND_UP_N(SPA_POD_SIZE(params[i]), 8);
+ }
+
+ max_buffers = context->settings.link_max_buffers;
+
+ align = pw_properties_get_uint32(context->properties, PW_KEY_CPU_MAX_ALIGN, MAX_ALIGN);
+
+ minsize = stride = 0;
+ types = SPA_ID_INVALID; /* bitmask of allowed types */
+ blocks = 1;
+
+ param = find_param(params, n_params, SPA_TYPE_OBJECT_ParamBuffers);
+ if (param) {
+ uint32_t qmax_buffers = max_buffers,
+ qminsize = minsize, qstride = stride, qalign = align;
+ uint32_t qtypes = types, qblocks = blocks;
+
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamBuffers, NULL,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_OPT_Int(&qmax_buffers),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_OPT_Int(&qblocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_OPT_Int(&qminsize),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_OPT_Int(&qstride),
+ SPA_PARAM_BUFFERS_align, SPA_POD_OPT_Int(&qalign),
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_OPT_Int(&qtypes));
+
+ max_buffers =
+ qmax_buffers == 0 ? max_buffers : SPA_MIN(qmax_buffers,
+ max_buffers);
+ blocks = SPA_CLAMP(qblocks, blocks, MAX_BLOCKS);
+ minsize = SPA_MAX(minsize, qminsize);
+ stride = SPA_MAX(stride, qstride);
+ align = SPA_MAX(align, qalign);
+ types = qtypes;
+
+ pw_log_debug("%p: %d %d %d %d %d %d -> %d %zd %zd %d %zd %d", result,
+ qblocks, qminsize, qstride, qmax_buffers, qalign, qtypes,
+ blocks, minsize, stride, max_buffers, align, types);
+ } else {
+ pw_log_warn("%p: no buffers param", result);
+ minsize = 8192;
+ max_buffers = 2;
+ }
+ if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_SHARED_MEM)) {
+ if (types != SPA_ID_INVALID)
+ SPA_FLAG_CLEAR(types, 1<<SPA_DATA_MemPtr);
+ if (types == 0 || types == SPA_ID_INVALID)
+ types = 1<<SPA_DATA_MemFd;
+ }
+
+ if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_NO_MEM))
+ minsize = 0;
+
+ data_sizes = alloca(sizeof(uint32_t) * blocks);
+ data_strides = alloca(sizeof(int32_t) * blocks);
+ data_aligns = alloca(sizeof(uint32_t) * blocks);
+ data_types = alloca(sizeof(uint32_t) * blocks);
+
+ for (i = 0; i < blocks; i++) {
+ data_sizes[i] = minsize;
+ data_strides[i] = stride;
+ data_aligns[i] = align;
+ data_types[i] = types;
+ }
+
+ if ((res = alloc_buffers(context->pool,
+ max_buffers,
+ n_params,
+ params,
+ blocks,
+ data_sizes, data_strides,
+ data_aligns, data_types,
+ flags,
+ result)) < 0) {
+ pw_log_error("%p: can't alloc buffers: %s", result, spa_strerror(res));
+ }
+
+ return res;
+}
+
+SPA_EXPORT
+void pw_buffers_clear(struct pw_buffers *buffers)
+{
+ pw_log_debug("%p: clear %d buffers:%p", buffers, buffers->n_buffers, buffers->buffers);
+ if (buffers->mem)
+ pw_memblock_unref(buffers->mem);
+ free(buffers->buffers);
+ spa_zero(*buffers);
+}
diff --git a/src/pipewire/buffers.h b/src/pipewire/buffers.h
new file mode 100644
index 0000000..abef392
--- /dev/null
+++ b/src/pipewire/buffers.h
@@ -0,0 +1,75 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_BUFFERS_H
+#define PIPEWIRE_BUFFERS_H
+
+#include <spa/node/node.h>
+
+#include <pipewire/context.h>
+#include <pipewire/mem.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_buffers Buffers
+ * Buffer handling
+ */
+
+/**
+ * \addtogroup pw_buffers
+ * \{
+ */
+
+#define PW_BUFFERS_FLAG_NONE 0
+#define PW_BUFFERS_FLAG_NO_MEM (1<<0) /**< don't allocate buffer memory */
+#define PW_BUFFERS_FLAG_SHARED (1<<1) /**< buffers can be shared */
+#define PW_BUFFERS_FLAG_DYNAMIC (1<<2) /**< buffers have dynamic data */
+#define PW_BUFFERS_FLAG_SHARED_MEM (1<<3) /**< buffers need shared memory */
+#define PW_BUFFERS_FLAG_IN_PRIORITY (1<<4) /**< input parameters have priority */
+
+struct pw_buffers {
+ struct pw_memblock *mem; /**< allocated buffer memory */
+ struct spa_buffer **buffers; /**< port buffers */
+ uint32_t n_buffers; /**< number of port buffers */
+ uint32_t flags; /**< flags */
+};
+
+int pw_buffers_negotiate(struct pw_context *context, uint32_t flags,
+ struct spa_node *outnode, uint32_t out_port_id,
+ struct spa_node *innode, uint32_t in_port_id,
+ struct pw_buffers *result);
+
+void pw_buffers_clear(struct pw_buffers *buffers);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_BUFFERS_H */
diff --git a/src/pipewire/client.h b/src/pipewire/client.h
new file mode 100644
index 0000000..2ed4c5b
--- /dev/null
+++ b/src/pipewire/client.h
@@ -0,0 +1,186 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CLIENT_H
+#define PIPEWIRE_CLIENT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/param/param.h>
+
+#include <pipewire/proxy.h>
+#include <pipewire/permission.h>
+
+/** \defgroup pw_client Client
+ * Client interface
+ */
+
+/**
+ * \addtogroup pw_client
+ * \{
+ */
+#define PW_TYPE_INTERFACE_Client PW_TYPE_INFO_INTERFACE_BASE "Client"
+
+#define PW_VERSION_CLIENT 3
+struct pw_client;
+
+/* default ID of the current client after connect */
+#define PW_ID_CLIENT 1
+
+/** The client information. Extra information can be added in later versions */
+struct pw_client_info {
+ uint32_t id; /**< id of the global */
+#define PW_CLIENT_CHANGE_MASK_PROPS (1 << 0)
+#define PW_CLIENT_CHANGE_MASK_ALL ((1 << 1)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ struct spa_dict *props; /**< extra properties */
+};
+
+/** Update an existing \ref pw_client_info with \a update with reset */
+struct pw_client_info *
+pw_client_info_update(struct pw_client_info *info,
+ const struct pw_client_info *update);
+/** Merge an existing \ref pw_client_info with \a update */
+struct pw_client_info *
+pw_client_info_merge(struct pw_client_info *info,
+ const struct pw_client_info *update, bool reset);
+/** Free a \ref pw_client_info */
+void pw_client_info_free(struct pw_client_info *info);
+
+
+#define PW_CLIENT_EVENT_INFO 0
+#define PW_CLIENT_EVENT_PERMISSIONS 1
+#define PW_CLIENT_EVENT_NUM 2
+
+/** Client events */
+struct pw_client_events {
+#define PW_VERSION_CLIENT_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify client info
+ *
+ * \param info info about the client
+ */
+ void (*info) (void *data, const struct pw_client_info *info);
+ /**
+ * Notify a client permission
+ *
+ * Event emitted as a result of the get_permissions method.
+ *
+ * \param default_permissions the default permissions
+ * \param index the index of the first permission entry
+ * \param n_permissions the number of permissions
+ * \param permissions the permissions
+ */
+ void (*permissions) (void *data,
+ uint32_t index,
+ uint32_t n_permissions,
+ const struct pw_permission *permissions);
+};
+
+
+#define PW_CLIENT_METHOD_ADD_LISTENER 0
+#define PW_CLIENT_METHOD_ERROR 1
+#define PW_CLIENT_METHOD_UPDATE_PROPERTIES 2
+#define PW_CLIENT_METHOD_GET_PERMISSIONS 3
+#define PW_CLIENT_METHOD_UPDATE_PERMISSIONS 4
+#define PW_CLIENT_METHOD_NUM 5
+
+/** Client methods */
+struct pw_client_methods {
+#define PW_VERSION_CLIENT_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_client_events *events,
+ void *data);
+ /**
+ * Send an error to a client
+ *
+ * \param id the global id to report the error on
+ * \param res an errno style error code
+ * \param message an error string
+ */
+ int (*error) (void *object, uint32_t id, int res, const char *message);
+ /**
+ * Update client properties
+ *
+ * \param props new properties
+ */
+ int (*update_properties) (void *object, const struct spa_dict *props);
+
+ /**
+ * Get client permissions
+ *
+ * A permissions event will be emitted with the permissions.
+ *
+ * \param index the first index to query, 0 for first
+ * \param num the maximum number of items to get
+ */
+ int (*get_permissions) (void *object, uint32_t index, uint32_t num);
+ /**
+ * Manage the permissions of the global objects for this
+ * client
+ *
+ * Update the permissions of the global objects using the
+ * provided array with permissions
+ *
+ * Globals can use the default permissions or can have specific
+ * permissions assigned to them.
+ *
+ * \param n_permissions number of permissions
+ * \param permissions array of permissions
+ */
+ int (*update_permissions) (void *object, uint32_t n_permissions,
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_CLIENT_H */
diff --git a/src/pipewire/conf.c b/src/pipewire/conf.c
new file mode 100644
index 0000000..700d8ee
--- /dev/null
+++ b/src/pipewire/conf.c
@@ -0,0 +1,1186 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/wait.h>
+#include <dirent.h>
+#include <regex.h>
+#ifdef HAVE_PWD_H
+#include <pwd.h>
+#endif
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#ifndef O_PATH
+#define O_PATH 0
+#endif
+#endif
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/private.h>
+
+PW_LOG_TOPIC_EXTERN(log_conf);
+#define PW_LOG_TOPIC_DEFAULT log_conf
+
+static int make_path(char *path, size_t size, const char *paths[])
+{
+ int i, len;
+ char *p = path;
+ for (i = 0; paths[i] != NULL; i++) {
+ len = snprintf(p, size, "%s%s", i == 0 ? "" : "/", paths[i]);
+ if (len < 0)
+ return -errno;
+ if ((size_t)len >= size)
+ return -ENOSPC;
+ p += len;
+ size -= len;
+ }
+ return 0;
+}
+
+static int get_abs_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ if (prefix[0] == '/') {
+ const char *paths[] = { prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int get_envconf_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ const char *dir;
+
+ dir = getenv("PIPEWIRE_CONFIG_DIR");
+ if (dir != NULL) {
+ const char *paths[] = { dir, prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int get_homeconf_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ char buffer[4096];
+ const char *dir;
+
+ dir = getenv("XDG_CONFIG_HOME");
+ if (dir != NULL) {
+ const char *paths[] = { dir, "pipewire", prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ }
+ dir = getenv("HOME");
+ if (dir == NULL) {
+ struct passwd pwd, *result = NULL;
+ if (getpwuid_r(getuid(), &pwd, buffer, sizeof(buffer), &result) == 0)
+ dir = result ? result->pw_dir : NULL;
+ }
+ if (dir != NULL) {
+ const char *paths[] = { dir, ".config", "pipewire", prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ }
+ return 0;
+}
+
+static int get_configdir_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ const char *dir;
+ dir = PIPEWIRE_CONFIG_DIR;
+ if (dir != NULL) {
+ const char *paths[] = { dir, prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ }
+ return 0;
+}
+
+static int get_confdata_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ const char *dir;
+ dir = PIPEWIRE_CONFDATADIR;
+ if (dir != NULL) {
+ const char *paths[] = { dir, prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ }
+ return 0;
+}
+
+static int get_config_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ int res;
+
+ if (prefix == NULL) {
+ prefix = name;
+ name = NULL;
+ }
+ if ((res = get_abs_path(path, size, prefix, name)) != 0)
+ return res;
+
+ if (pw_check_option("no-config", "true"))
+ goto no_config;
+
+ if ((res = get_envconf_path(path, size, prefix, name)) != 0)
+ return res;
+
+ if ((res = get_homeconf_path(path, size, prefix, name)) != 0)
+ return res;
+
+ if ((res = get_configdir_path(path, size, prefix, name)) != 0)
+ return res;
+no_config:
+ if ((res = get_confdata_path(path, size, prefix, name)) != 0)
+ return res;
+ return 0;
+}
+
+static int get_config_dir(char *path, size_t size, const char *prefix, const char *name, int *level)
+{
+ int res;
+
+ if (prefix == NULL) {
+ prefix = name;
+ name = NULL;
+ }
+ if ((res = get_abs_path(path, size, prefix, name)) != 0) {
+ if ((*level)++ == 0)
+ return res;
+ return -ENOENT;
+ }
+
+ if (pw_check_option("no-config", "true"))
+ goto no_config;
+
+ if ((res = get_envconf_path(path, size, prefix, name)) != 0) {
+ if ((*level)++ == 0)
+ return res;
+ return -ENOENT;
+ }
+
+ if (*level == 0) {
+ (*level)++;
+ if ((res = get_homeconf_path(path, size, prefix, name)) != 0)
+ return res;
+ }
+ if (*level == 1) {
+ (*level)++;
+ if ((res = get_configdir_path(path, size, prefix, name)) != 0)
+ return res;
+ }
+ if (*level == 2) {
+no_config:
+ (*level)++;
+ if ((res = get_confdata_path(path, size, prefix, name)) != 0)
+ return res;
+ }
+ return 0;
+}
+
+static int get_envstate_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ const char *dir;
+ dir = getenv("PIPEWIRE_STATE_DIR");
+ if (dir != NULL) {
+ const char *paths[] = { dir, prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int get_homestate_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ const char *dir;
+ char buffer[4096];
+
+ dir = getenv("XDG_STATE_HOME");
+ if (dir != NULL) {
+ const char *paths[] = { dir, "pipewire", prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ }
+ dir = getenv("HOME");
+ if (dir == NULL) {
+ struct passwd pwd, *result = NULL;
+ if (getpwuid_r(getuid(), &pwd, buffer, sizeof(buffer), &result) == 0)
+ dir = result ? result->pw_dir : NULL;
+ }
+ if (dir != NULL) {
+ const char *paths[] = { dir, ".local", "state", "pipewire", prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ }
+ if (dir != NULL) {
+ /* fallback for old XDG_CONFIG_HOME */
+ const char *paths[] = { dir, ".config", "pipewire", prefix, name, NULL };
+ if (make_path(path, size, paths) == 0 &&
+ access(path, R_OK) == 0)
+ return 1;
+ }
+ return 0;
+}
+
+static int get_state_path(char *path, size_t size, const char *prefix, const char *name)
+{
+ int res;
+
+ if (prefix == NULL) {
+ prefix = name;
+ name = NULL;
+ }
+ if ((res = get_abs_path(path, size, prefix, name)) != 0)
+ return res;
+
+ if ((res = get_envstate_path(path, size, prefix, name)) != 0)
+ return res;
+
+ if ((res = get_homestate_path(path, size, prefix, name)) != 0)
+ return res;
+
+ return 0;
+}
+
+static int ensure_path(char *path, int size, const char *paths[])
+{
+ int i, len, mode;
+ char *p = path;
+
+ for (i = 0; paths[i] != NULL; i++) {
+ len = snprintf(p, size, "%s/", paths[i]);
+ if (len < 0)
+ return -errno;
+ if (len >= size)
+ return -ENOSPC;
+
+ p += len;
+ size -= len;
+
+ mode = X_OK;
+ if (paths[i+1] == NULL)
+ mode |= R_OK | W_OK;
+
+ if (access(path, mode) < 0) {
+ if (errno != ENOENT)
+ return -errno;
+ if (mkdir(path, 0700) < 0) {
+ pw_log_info("Can't create directory %s: %m", path);
+ return -errno;
+ }
+ if (access(path, mode) < 0)
+ return -errno;
+
+ pw_log_info("created directory %s", path);
+ }
+ }
+ return 0;
+}
+
+static int open_write_dir(char *path, int size, const char *prefix)
+{
+ const char *dir;
+ char buffer[4096];
+ int res;
+
+ if (prefix != NULL && prefix[0] == '/') {
+ const char *paths[] = { prefix, NULL };
+ if (ensure_path(path, size, paths) == 0)
+ goto found;
+ }
+ dir = getenv("XDG_STATE_HOME");
+ if (dir != NULL) {
+ const char *paths[] = { dir, "pipewire", prefix, NULL };
+ if (ensure_path(path, size, paths) == 0)
+ goto found;
+ }
+ dir = getenv("HOME");
+ if (dir == NULL) {
+ struct passwd pwd, *result = NULL;
+ if (getpwuid_r(getuid(), &pwd, buffer, sizeof(buffer), &result) == 0)
+ dir = result ? result->pw_dir : NULL;
+ }
+ if (dir != NULL) {
+ const char *paths[] = { dir, ".local", "state", "pipewire", prefix, NULL };
+ if (ensure_path(path, size, paths) == 0)
+ goto found;
+ }
+ return -ENOENT;
+found:
+ if ((res = open(path, O_CLOEXEC | O_DIRECTORY | O_PATH)) < 0) {
+ pw_log_error("Can't open state directory %s: %m", path);
+ return -errno;
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_conf_save_state(const char *prefix, const char *name, const struct pw_properties *conf)
+{
+ char path[PATH_MAX];
+ char *tmp_name;
+ int res, sfd, fd, count = 0;
+ FILE *f;
+
+ if ((sfd = open_write_dir(path, sizeof(path), prefix)) < 0)
+ return sfd;
+
+ tmp_name = alloca(strlen(name)+5);
+ sprintf(tmp_name, "%s.tmp", name);
+ if ((fd = openat(sfd, tmp_name, O_CLOEXEC | O_CREAT | O_WRONLY | O_TRUNC, 0600)) < 0) {
+ pw_log_error("can't open file '%s': %m", tmp_name);
+ res = -errno;
+ goto error;
+ }
+
+ f = fdopen(fd, "w");
+ fprintf(f, "{");
+ count += pw_properties_serialize_dict(f, &conf->dict, PW_PROPERTIES_FLAG_NL);
+ fprintf(f, "%s}", count == 0 ? " " : "\n");
+ fclose(f);
+
+ if (renameat(sfd, tmp_name, sfd, name) < 0) {
+ pw_log_error("can't rename temp file '%s': %m", tmp_name);
+ res = -errno;
+ goto error;
+ }
+ res = 0;
+ pw_log_info("%p: saved state '%s%s'", conf, path, name);
+error:
+ close(sfd);
+ return res;
+}
+
+static int conf_load(const char *path, struct pw_properties *conf)
+{
+ char *data;
+ struct stat sbuf;
+ int fd, count;
+
+ if ((fd = open(path, O_CLOEXEC | O_RDONLY)) < 0)
+ goto error;
+
+ if (fstat(fd, &sbuf) < 0)
+ goto error_close;
+
+ if (sbuf.st_size > 0) {
+ if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
+ goto error_close;
+
+ count = pw_properties_update_string(conf, data, sbuf.st_size);
+ munmap(data, sbuf.st_size);
+ } else {
+ count = 0;
+ }
+ close(fd);
+
+ pw_log_info("%p: loaded config '%s' with %d items", conf, path, count);
+
+ return 0;
+
+error_close:
+ close(fd);
+error:
+ pw_log_warn("%p: error loading config '%s': %m", conf, path);
+ return -errno;
+}
+
+static bool check_override(struct pw_properties *conf, const char *name, int level)
+{
+ const struct spa_dict_item *it;
+
+ spa_dict_for_each(it, &conf->dict) {
+ int lev, idx;
+
+ if (!spa_streq(name, it->value))
+ continue;
+ if (sscanf(it->key, "override.%d.%d.config.name", &lev, &idx) != 2)
+ continue;
+ if (lev < level)
+ return false;
+ }
+ return true;
+}
+
+static void add_override(struct pw_properties *conf, struct pw_properties *override,
+ const char *path, const char *name, int level, int index)
+{
+ const struct spa_dict_item *it;
+ char key[1024];
+
+ snprintf(key, sizeof(key), "override.%d.%d.config.path", level, index);
+ pw_properties_set(conf, key, path);
+ snprintf(key, sizeof(key), "override.%d.%d.config.name", level, index);
+ pw_properties_set(conf, key, name);
+ spa_dict_for_each(it, &override->dict) {
+ snprintf(key, sizeof(key), "override.%d.%d.%s", level, index, it->key);
+ pw_properties_set(conf, key, it->value);
+ }
+}
+
+static int conf_filter(const struct dirent *entry)
+{
+ return spa_strendswith(entry->d_name, ".conf");
+}
+
+SPA_EXPORT
+int pw_conf_load_conf(const char *prefix, const char *name, struct pw_properties *conf)
+{
+ char path[PATH_MAX];
+ char fname[PATH_MAX + 256];
+ int i, res, level = 0;
+ struct pw_properties *override = NULL;
+ const char *dname;
+
+ if (name == NULL) {
+ pw_log_debug("%p: config name must not be NULL", conf);
+ return -EINVAL;
+ }
+
+ if (get_config_path(path, sizeof(path), prefix, name) == 0) {
+ pw_log_debug("%p: can't load config '%s': %m", conf, path);
+ return -ENOENT;
+ }
+ pw_properties_set(conf, "config.prefix", prefix);
+ pw_properties_set(conf, "config.name", name);
+ pw_properties_set(conf, "config.path", path);
+
+ if ((res = conf_load(path, conf)) < 0)
+ return res;
+
+ pw_properties_setf(conf, "config.name.d", "%s.d", name);
+ dname = pw_properties_get(conf, "config.name.d");
+
+ while (true) {
+ struct dirent **entries = NULL;
+ int n;
+
+ if (get_config_dir(path, sizeof(path), prefix, dname, &level) <= 0)
+ break;
+
+ n = scandir(path, &entries, conf_filter, alphasort);
+ if (n == 0)
+ continue;
+ if (n < 0) {
+ pw_log_warn("scandir %s failed: %m", path);
+ continue;
+ }
+ if (override == NULL &&
+ (override = pw_properties_new(NULL, NULL)) == NULL)
+ return -errno;
+
+ for (i = 0; i < n; i++) {
+ const char *name = entries[i]->d_name;
+
+ snprintf(fname, sizeof(fname), "%s/%s", path, name);
+ if (check_override(conf, name, level)) {
+ if (conf_load(fname, override) >= 0)
+ add_override(conf, override, fname, name, level, i);
+ pw_properties_clear(override);
+ } else {
+ pw_log_info("skip override %s with lower priority", fname);
+ }
+ free(entries[i]);
+ }
+ free(entries);
+ }
+ pw_properties_free(override);
+ return 0;
+}
+
+SPA_EXPORT
+int pw_conf_load_state(const char *prefix, const char *name, struct pw_properties *conf)
+{
+ char path[PATH_MAX];
+
+ if (name == NULL) {
+ pw_log_debug("%p: config name must not be NULL", conf);
+ return -EINVAL;
+ }
+
+ if (get_state_path(path, sizeof(path), prefix, name) == 0) {
+ pw_log_debug("%p: can't load config '%s': %m", conf, path);
+ return -ENOENT;
+ }
+ return conf_load(path, conf);
+}
+
+struct data {
+ struct pw_context *context;
+ struct pw_properties *props;
+ int count;
+};
+
+/* context.spa-libs = {
+ * <factory-name regex> = <library-name>
+ * }
+ */
+static int parse_spa_libs(void *user_data, const char *location,
+ const char *section, const char *str, size_t len)
+{
+ struct data *d = user_data;
+ struct pw_context *context = d->context;
+ struct spa_json it[2];
+ char key[512], value[512];
+
+ spa_json_init(&it[0], str, len);
+ if (spa_json_enter_object(&it[0], &it[1]) < 0) {
+ pw_log_error("config file error: context.spa-libs is not an object");
+ return -EINVAL;
+ }
+
+ while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
+ if (spa_json_get_string(&it[1], value, sizeof(value)) > 0) {
+ pw_context_add_spa_lib(context, key, value);
+ d->count++;
+ }
+ }
+ return 0;
+}
+
+static int load_module(struct pw_context *context, const char *key, const char *args, const char *flags)
+{
+ if (pw_context_load_module(context, key, args, NULL) == NULL) {
+ if (errno == ENOENT && flags && strstr(flags, "ifexists") != NULL) {
+ pw_log_info("%p: skipping unavailable module %s",
+ context, key);
+ } else if (flags == NULL || strstr(flags, "nofail") == NULL) {
+ pw_log_error("%p: could not load mandatory module \"%s\": %m",
+ context, key);
+ return -errno;
+ } else {
+ pw_log_info("%p: could not load optional module \"%s\": %m",
+ context, key);
+ }
+ } else {
+ pw_log_info("%p: loaded module %s", context, key);
+ }
+ return 0;
+}
+
+/*
+ * context.modules = [
+ * { name = <module-name>
+ * [ args = { <key> = <value> ... } ]
+ * [ flags = [ [ ifexists ] [ nofail ] ]
+ * }
+ * ]
+ */
+static int parse_modules(void *user_data, const char *location,
+ const char *section, const char *str, size_t len)
+{
+ struct data *d = user_data;
+ struct pw_context *context = d->context;
+ struct spa_json it[3];
+ char key[512], *s;
+ int res = 0;
+
+ s = strndup(str, len);
+ spa_json_init(&it[0], s, len);
+ if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+ pw_log_error("config file error: context.modules is not an array");
+ res = -EINVAL;
+ goto exit;
+ }
+
+ while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+ char *name = NULL, *args = NULL, *flags = NULL;
+
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(&it[2], &val)) <= 0)
+ break;
+
+ if (spa_streq(key, "name")) {
+ name = (char*)val;
+ spa_json_parse_stringn(val, len, name, len+1);
+ } else if (spa_streq(key, "args")) {
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(&it[2], val, len);
+
+ args = (char*)val;
+ spa_json_parse_stringn(val, len, args, len+1);
+ } else if (spa_streq(key, "flags")) {
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(&it[2], val, len);
+ flags = (char*)val;
+ spa_json_parse_stringn(val, len, flags, len+1);
+ }
+ }
+ if (name != NULL)
+ res = load_module(context, name, args, flags);
+
+ if (res < 0)
+ break;
+
+ d->count++;
+ }
+exit:
+ free(s);
+ return res;
+}
+
+static int create_object(struct pw_context *context, const char *key, const char *args, const char *flags)
+{
+ struct pw_impl_factory *factory;
+ void *obj;
+
+ pw_log_debug("find factory %s", key);
+ factory = pw_context_find_factory(context, key);
+ if (factory == NULL) {
+ if (flags && strstr(flags, "nofail") != NULL)
+ return 0;
+ pw_log_error("can't find factory %s", key);
+ return -ENOENT;
+ }
+ pw_log_debug("create object with args %s", args);
+ obj = pw_impl_factory_create_object(factory,
+ NULL, NULL, 0,
+ args ? pw_properties_new_string(args) : NULL,
+ SPA_ID_INVALID);
+ if (obj == NULL) {
+ if (flags && strstr(flags, "nofail") != NULL)
+ return 0;
+ pw_log_error("can't create object from factory %s: %m", key);
+ return -errno;
+ }
+ return 0;
+}
+
+/*
+ * context.objects = [
+ * { factory = <factory-name>
+ * [ args = { <key> = <value> ... } ]
+ * [ flags = [ [ nofail ] ] ]
+ * }
+ * ]
+ */
+static int parse_objects(void *user_data, const char *location,
+ const char *section, const char *str, size_t len)
+{
+ struct data *d = user_data;
+ struct pw_context *context = d->context;
+ struct spa_json it[3];
+ char key[512], *s;
+ int res = 0;
+
+ s = strndup(str, len);
+ spa_json_init(&it[0], s, len);
+ if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+ pw_log_error("config file error: context.objects is not an array");
+ res = -EINVAL;
+ goto exit;
+ }
+
+ while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+ char *factory = NULL, *args = NULL, *flags = NULL;
+
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(&it[2], &val)) <= 0)
+ break;
+
+ if (spa_streq(key, "factory")) {
+ factory = (char*)val;
+ spa_json_parse_stringn(val, len, factory, len+1);
+ } else if (spa_streq(key, "args")) {
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(&it[2], val, len);
+
+ args = (char*)val;
+ spa_json_parse_stringn(val, len, args, len+1);
+ } else if (spa_streq(key, "flags")) {
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(&it[2], val, len);
+
+ flags = (char*)val;
+ spa_json_parse_stringn(val, len, flags, len+1);
+ }
+ }
+ if (factory != NULL)
+ res = create_object(context, factory, args, flags);
+
+ if (res < 0)
+ break;
+ d->count++;
+ }
+exit:
+ free(s);
+ return res;
+}
+
+static int do_exec(struct pw_context *context, const char *key, const char *args)
+{
+ int pid, res, n_args;
+
+ pid = fork();
+
+ if (pid == 0) {
+ char *cmd, **argv;
+
+ /* Double fork to avoid zombies; we don't want to set SIGCHLD handler */
+ pid = fork();
+
+ if (pid < 0) {
+ pw_log_error("fork error: %m");
+ exit(1);
+ } else if (pid != 0) {
+ exit(0);
+ }
+
+ cmd = spa_aprintf("%s %s", key, args ? args : "");
+ argv = pw_split_strv(cmd, " \t", INT_MAX, &n_args);
+ free(cmd);
+
+ pw_log_info("exec %s '%s'", key, args);
+ res = execvp(key, argv);
+ pw_free_strv(argv);
+
+ if (res == -1) {
+ res = -errno;
+ pw_log_error("execvp error '%s': %m", key);
+ }
+
+ exit(1);
+ } else if (pid < 0) {
+ pw_log_error("fork error: %m");
+ } else {
+ int status = 0;
+ do {
+ errno = 0;
+ res = waitpid(pid, &status, 0);
+ } while (res < 0 && errno == EINTR);
+ pw_log_debug("exec got pid %d res:%d status:%d", (int)pid, res, status);
+ }
+ return 0;
+}
+
+/*
+ * context.exec = [
+ * { path = <program-name>
+ * [ args = "<arguments>" ]
+ * }
+ * ]
+ */
+static int parse_exec(void *user_data, const char *location,
+ const char *section, const char *str, size_t len)
+{
+ struct data *d = user_data;
+ struct pw_context *context = d->context;
+ struct spa_json it[3];
+ char key[512], *s;
+ int res = 0;
+
+ s = strndup(str, len);
+ spa_json_init(&it[0], s, len);
+ if (spa_json_enter_array(&it[0], &it[1]) < 0) {
+ pw_log_error("config file error: context.exec is not an array");
+ res = -EINVAL;
+ goto exit;
+ }
+
+ while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+ char *path = NULL, *args = NULL;
+
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ const char *val;
+ int len;
+
+ if ((len = spa_json_next(&it[2], &val)) <= 0)
+ break;
+
+ if (spa_streq(key, "path")) {
+ path = (char*)val;
+ spa_json_parse_stringn(val, len, path, len+1);
+ } else if (spa_streq(key, "args")) {
+ args = (char*)val;
+ spa_json_parse_stringn(val, len, args, len+1);
+ }
+ }
+ if (path != NULL)
+ res = do_exec(context, path, args);
+
+ if (res < 0)
+ break;
+
+ d->count++;
+ }
+exit:
+ free(s);
+ return res;
+}
+
+
+SPA_EXPORT
+int pw_context_conf_section_for_each(struct pw_context *context, const char *section,
+ int (*callback) (void *data, const char *location, const char *section,
+ const char *str, size_t len),
+ void *data)
+{
+ struct pw_properties *conf = context->conf;
+ const char *path = NULL;
+ const struct spa_dict_item *it;
+ int res = 0;
+
+ spa_dict_for_each(it, &conf->dict) {
+ if (spa_strendswith(it->key, "config.path")) {
+ path = it->value;
+ continue;
+
+ } else if (spa_streq(it->key, section)) {
+ pw_log_info("handle config '%s' section '%s'", path, section);
+ } else if (spa_strstartswith(it->key, "override.") &&
+ spa_strendswith(it->key, section)) {
+ pw_log_info("handle override '%s' section '%s'", path, section);
+ } else
+ continue;
+
+ res = callback(data, path, section, it->value, strlen(it->value));
+ if (res != 0)
+ break;
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_context_parse_conf_section(struct pw_context *context,
+ struct pw_properties *conf, const char *section)
+{
+ struct data data = { .context = context };
+ int res;
+
+ if (spa_streq(section, "context.spa-libs"))
+ res = pw_context_conf_section_for_each(context, section,
+ parse_spa_libs, &data);
+ else if (spa_streq(section, "context.modules"))
+ res = pw_context_conf_section_for_each(context, section,
+ parse_modules, &data);
+ else if (spa_streq(section, "context.objects"))
+ res = pw_context_conf_section_for_each(context, section,
+ parse_objects, &data);
+ else if (spa_streq(section, "context.exec"))
+ res = pw_context_conf_section_for_each(context, section,
+ parse_exec, &data);
+ else
+ res = -EINVAL;
+
+ return res == 0 ? data.count : res;
+}
+
+static int update_props(void *user_data, const char *location, const char *key,
+ const char *val, size_t len)
+{
+ struct data *data = user_data;
+ data->count += pw_properties_update_string(data->props, val, len);
+ return 0;
+}
+
+static int try_load_conf(const char *conf_prefix, const char *conf_name,
+ struct pw_properties *conf)
+{
+ int res;
+
+ if (conf_name == NULL)
+ return -EINVAL;
+ if (spa_streq(conf_name, "null"))
+ return 0;
+ if ((res = pw_conf_load_conf(conf_prefix, conf_name, conf)) < 0) {
+ bool skip_prefix = conf_prefix == NULL || conf_name[0] == '/';
+ pw_log_warn("can't load config %s%s%s: %s",
+ skip_prefix ? "" : conf_prefix,
+ skip_prefix ? "" : "/",
+ conf_name, spa_strerror(res));
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_conf_load_conf_for_context(struct pw_properties *props, struct pw_properties *conf)
+{
+ const char *conf_prefix, *conf_name;
+ int res;
+
+ conf_prefix = getenv("PIPEWIRE_CONFIG_PREFIX");
+ if (conf_prefix == NULL)
+ conf_prefix = pw_properties_get(props, PW_KEY_CONFIG_PREFIX);
+
+ conf_name = getenv("PIPEWIRE_CONFIG_NAME");
+ if ((res = try_load_conf(conf_prefix, conf_name, conf)) < 0) {
+ conf_name = pw_properties_get(props, PW_KEY_CONFIG_NAME);
+ if ((res = try_load_conf(conf_prefix, conf_name, conf)) < 0) {
+ conf_name = "client.conf";
+ if ((res = try_load_conf(conf_prefix, conf_name, conf)) < 0) {
+ pw_log_error("can't load default config %s: %s",
+ conf_name, spa_strerror(res));
+ return res;
+ }
+ }
+ }
+
+ conf_name = pw_properties_get(props, PW_KEY_CONFIG_OVERRIDE_NAME);
+ if (conf_name != NULL) {
+ struct pw_properties *override;
+ const char *path, *name;
+
+ override = pw_properties_new(NULL, NULL);
+ if (override == NULL) {
+ res = -errno;
+ return res;
+ }
+
+ conf_prefix = pw_properties_get(props, PW_KEY_CONFIG_OVERRIDE_PREFIX);
+ if ((res = try_load_conf(conf_prefix, conf_name, override)) < 0) {
+ pw_log_error("can't load default override config %s: %s",
+ conf_name, spa_strerror(res));
+ pw_properties_free (override);
+ return res;
+ }
+ path = pw_properties_get(override, "config.path");
+ name = pw_properties_get(override, "config.name");
+ add_override(conf, override, path, name, 0, 1);
+ pw_properties_free(override);
+ }
+
+ return res;
+}
+
+SPA_EXPORT
+int pw_context_conf_update_props(struct pw_context *context,
+ const char *section, struct pw_properties *props)
+{
+ struct data data = { .context = context, .props = props };
+ int res;
+ const char *str = pw_properties_get(props, "config.ext");
+
+ res = pw_context_conf_section_for_each(context, section,
+ update_props, &data);
+ if (res == 0 && str != NULL) {
+ char key[128];
+ snprintf(key, sizeof(key), "%s.%s", section, str);
+ res = pw_context_conf_section_for_each(context, key,
+ update_props, &data);
+ }
+ return res == 0 ? data.count : res;
+}
+
+
+/*
+ * {
+ * # all keys must match the value. ~ in value starts regex.
+ * <key> = <value>
+ * ...
+ * }
+ */
+static bool find_match(struct spa_json *arr, const struct spa_dict *props)
+{
+ struct spa_json it[1];
+
+ while (spa_json_enter_object(arr, &it[0]) > 0) {
+ char key[256], val[1024];
+ const char *str, *value;
+ int match = 0, fail = 0;
+ int len;
+
+ while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) {
+ bool success = false;
+
+ if ((len = spa_json_next(&it[0], &value)) <= 0)
+ break;
+
+ str = spa_dict_lookup(props, key);
+
+ if (spa_json_is_null(value, len)) {
+ success = str == NULL;
+ } else {
+ if (spa_json_parse_stringn(value, len, val, sizeof(val)) < 0)
+ continue;
+ value = val;
+ len = strlen(val);
+ }
+ if (str != NULL) {
+ if (value[0] == '~') {
+ regex_t preg;
+ if (regcomp(&preg, value+1, REG_EXTENDED | REG_NOSUB) == 0) {
+ if (regexec(&preg, str, 0, NULL, 0) == 0)
+ success = true;
+ regfree(&preg);
+ }
+ } else if (strncmp(str, value, len) == 0 &&
+ strlen(str) == (size_t)len) {
+ success = true;
+ }
+ }
+ if (success) {
+ match++;
+ pw_log_debug("'%s' match '%s' < > '%.*s'", key, str, len, value);
+ }
+ else
+ fail++;
+ }
+ if (match > 0 && fail == 0)
+ return true;
+ }
+ return false;
+}
+
+/**
+ * [
+ * {
+ * matches = [
+ * # any of the items in matches needs to match, if one does,
+ * # actions are emited.
+ * {
+ * # all keys must match the value. ~ in value starts regex.
+ * <key> = <value>
+ * ...
+ * }
+ * ...
+ * ]
+ * actions = {
+ * <action> = <value>
+ * ...
+ * }
+ * }
+ * ]
+ */
+SPA_EXPORT
+int pw_conf_match_rules(const char *str, size_t len, const char *location,
+ const struct spa_dict *props,
+ int (*callback) (void *data, const char *location, const char *action,
+ const char *str, size_t len),
+ void *data)
+{
+ const char *val;
+ struct spa_json it[4], actions;
+
+ spa_json_init(&it[0], str, len);
+ if (spa_json_enter_array(&it[0], &it[1]) < 0)
+ return 0;
+
+ while (spa_json_enter_object(&it[1], &it[2]) > 0) {
+ char key[64];
+ bool have_match = false, have_actions = false;
+
+ while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
+ if (spa_streq(key, "matches")) {
+ if (spa_json_enter_array(&it[2], &it[3]) < 0)
+ break;
+
+ have_match = find_match(&it[3], props);
+ }
+ else if (spa_streq(key, "actions")) {
+ if (spa_json_enter_object(&it[2], &actions) > 0)
+ have_actions = true;
+ }
+ else if (spa_json_next(&it[2], &val) <= 0)
+ break;
+ }
+ if (!have_match || !have_actions)
+ continue;
+
+ while (spa_json_get_string(&actions, key, sizeof(key)) > 0) {
+ int res, len;
+ pw_log_debug("action %s", key);
+
+ if ((len = spa_json_next(&actions, &val)) <= 0)
+ break;
+
+ if (spa_json_is_container(val, len))
+ len = spa_json_container_len(&actions, val, len);
+
+ if ((res = callback(data, location, key, val, len)) < 0)
+ return res;
+ }
+ }
+ return 0;
+}
+
+struct match {
+ const struct spa_dict *props;
+ int (*matched) (void *data, const char *location, const char *action,
+ const char *val, size_t len);
+ void *data;
+};
+
+static int match_rules(void *data, const char *location, const char *section,
+ const char *str, size_t len)
+{
+ struct match *match = data;
+ return pw_conf_match_rules(str, len, location,
+ match->props, match->matched, match->data);
+}
+
+SPA_EXPORT
+int pw_context_conf_section_match_rules(struct pw_context *context, const char *section,
+ const struct spa_dict *props,
+ int (*callback) (void *data, const char *location, const char *action,
+ const char *str, size_t len),
+ void *data)
+{
+ struct match match = {
+ .props = props,
+ .matched = callback,
+ .data = data };
+ int res;
+ const char *str = spa_dict_lookup(props, "config.ext");
+
+ res = pw_context_conf_section_for_each(context, section,
+ match_rules, &match);
+ if (res == 0 && str != NULL) {
+ char key[128];
+ snprintf(key, sizeof(key), "%s.%s", section, str);
+ res = pw_context_conf_section_for_each(context, key,
+ match_rules, &match);
+ }
+ return res;
+}
diff --git a/src/pipewire/conf.h b/src/pipewire/conf.h
new file mode 100644
index 0000000..8ada200
--- /dev/null
+++ b/src/pipewire/conf.h
@@ -0,0 +1,49 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/context.h>
+
+/** \defgroup pw_conf Configuration
+ * Loading/saving properties from/to configuration files.
+ */
+
+/**
+ * \addtogroup pw_conf
+ * \{
+ */
+
+int pw_conf_load_conf_for_context(struct pw_properties *props, struct pw_properties *conf);
+int pw_conf_load_conf(const char *prefix, const char *name, struct pw_properties *conf);
+int pw_conf_load_state(const char *prefix, const char *name, struct pw_properties *conf);
+int pw_conf_save_state(const char *prefix, const char *name, const struct pw_properties *conf);
+
+int pw_conf_match_rules(const char *str, size_t len, const char *location,
+ const struct spa_dict *props,
+ int (*callback) (void *data, const char *location, const char *action,
+ const char *str, size_t len),
+ void *data);
+
+/**
+ * \}
+ */
diff --git a/src/pipewire/context.c b/src/pipewire/context.c
new file mode 100644
index 0000000..6ba6488
--- /dev/null
+++ b/src/pipewire/context.c
@@ -0,0 +1,1461 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#include <errno.h>
+#include <unistd.h>
+#include <time.h>
+#include <stdio.h>
+#include <regex.h>
+#include <limits.h>
+#include <sys/mman.h>
+
+#include <pipewire/log.h>
+
+#include <spa/support/cpu.h>
+#include <spa/support/dbus.h>
+#include <spa/support/plugin.h>
+#include <spa/support/plugin-loader.h>
+#include <spa/node/utils.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/private.h>
+#include <pipewire/thread.h>
+#include <pipewire/conf.h>
+
+#include <pipewire/extensions/protocol-native.h>
+
+PW_LOG_TOPIC_EXTERN(log_context);
+#define PW_LOG_TOPIC_DEFAULT log_context
+
+/** \cond */
+struct impl {
+ struct pw_context this;
+ struct spa_handle *dbus_handle;
+ struct spa_plugin_loader plugin_loader;
+ unsigned int recalc:1;
+ unsigned int recalc_pending:1;
+};
+
+
+struct factory_entry {
+ regex_t regex;
+ char *lib;
+};
+/** \endcond */
+
+static void fill_properties(struct pw_context *context)
+{
+ struct pw_properties *properties = context->properties;
+
+ if (!pw_properties_get(properties, PW_KEY_APP_NAME))
+ pw_properties_set(properties, PW_KEY_APP_NAME, pw_get_client_name());
+
+ if (!pw_properties_get(properties, PW_KEY_APP_PROCESS_BINARY))
+ pw_properties_set(properties, PW_KEY_APP_PROCESS_BINARY, pw_get_prgname());
+
+ if (!pw_properties_get(properties, PW_KEY_APP_LANGUAGE)) {
+ pw_properties_set(properties, PW_KEY_APP_LANGUAGE, getenv("LANG"));
+ }
+ if (!pw_properties_get(properties, PW_KEY_APP_PROCESS_ID)) {
+ pw_properties_setf(properties, PW_KEY_APP_PROCESS_ID, "%zd", (size_t) getpid());
+ }
+ if (!pw_properties_get(properties, PW_KEY_APP_PROCESS_USER))
+ pw_properties_set(properties, PW_KEY_APP_PROCESS_USER, pw_get_user_name());
+
+ if (!pw_properties_get(properties, PW_KEY_APP_PROCESS_HOST))
+ pw_properties_set(properties, PW_KEY_APP_PROCESS_HOST, pw_get_host_name());
+
+ if (!pw_properties_get(properties, PW_KEY_APP_PROCESS_SESSION_ID)) {
+ pw_properties_set(properties, PW_KEY_APP_PROCESS_SESSION_ID,
+ getenv("XDG_SESSION_ID"));
+ }
+ if (!pw_properties_get(properties, PW_KEY_WINDOW_X11_DISPLAY)) {
+ pw_properties_set(properties, PW_KEY_WINDOW_X11_DISPLAY,
+ getenv("DISPLAY"));
+ }
+ pw_properties_set(properties, PW_KEY_CORE_VERSION, context->core->info.version);
+ pw_properties_set(properties, PW_KEY_CORE_NAME, context->core->info.name);
+}
+
+static int context_set_freewheel(struct pw_context *context, bool freewheel)
+{
+ struct spa_thread *thr;
+ int res = 0;
+
+ if ((thr = pw_data_loop_get_thread(context->data_loop_impl)) == NULL)
+ return -EIO;
+
+ if (freewheel) {
+ pw_log_info("%p: enter freewheel", context);
+ if (context->thread_utils)
+ res = spa_thread_utils_drop_rt(context->thread_utils, thr);
+ } else {
+ pw_log_info("%p: exit freewheel", context);
+ /* Use the priority as configured within the realtime module */
+ if (context->thread_utils)
+ res = spa_thread_utils_acquire_rt(context->thread_utils, thr, -1);
+ }
+ if (res < 0)
+ pw_log_info("%p: freewheel error:%s", context, spa_strerror(res));
+
+ context->freewheeling = freewheel;
+
+ return res;
+}
+
+static struct spa_handle *impl_plugin_loader_load(void *object, const char *factory_name, const struct spa_dict *info)
+{
+ struct impl *impl = object;
+
+ if (impl == NULL || factory_name == NULL) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ return pw_context_load_spa_handle(&impl->this, factory_name, info);
+}
+
+static int impl_plugin_loader_unload(void *object, struct spa_handle *handle)
+{
+ spa_return_val_if_fail(object != NULL, -EINVAL);
+ return pw_unload_spa_handle(handle);
+}
+
+static const struct spa_plugin_loader_methods impl_plugin_loader = {
+ SPA_VERSION_PLUGIN_LOADER_METHODS,
+ .load = impl_plugin_loader_load,
+ .unload = impl_plugin_loader_unload,
+};
+
+static void init_plugin_loader(struct impl *impl)
+{
+ impl->plugin_loader.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_PluginLoader,
+ SPA_VERSION_PLUGIN_LOADER,
+ &impl_plugin_loader,
+ impl);
+}
+
+static int do_data_loop_setup(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *data, size_t size, void *user_data)
+{
+ struct pw_context *this = user_data;
+ const char *str;
+ struct spa_cpu *cpu;
+
+ cpu = spa_support_find(this->support, this->n_support, SPA_TYPE_INTERFACE_CPU);
+
+ if ((str = pw_properties_get(this->properties, SPA_KEY_CPU_ZERO_DENORMALS)) != NULL &&
+ cpu != NULL) {
+ pw_log_info("setting zero denormals: %s", str);
+ spa_cpu_zero_denormals(cpu, spa_atob(str));
+ }
+ return 0;
+}
+
+/** Create a new context object
+ *
+ * \param main_loop the main loop to use
+ * \param properties extra properties for the context, ownership it taken
+ *
+ * \return a newly allocated context object
+ */
+SPA_EXPORT
+struct pw_context *pw_context_new(struct pw_loop *main_loop,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct impl *impl;
+ struct pw_context *this;
+ const char *lib, *str;
+ void *dbus_iface = NULL;
+ uint32_t n_support;
+ struct pw_properties *pr, *conf;
+ struct spa_cpu *cpu;
+ int res = 0;
+
+ impl = calloc(1, sizeof(struct impl) + user_data_size);
+ if (impl == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ this = &impl->this;
+
+ pw_log_debug("%p: new", this);
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ pw_array_init(&this->factory_lib, 32);
+ pw_array_init(&this->objects, 32);
+ pw_map_init(&this->globals, 128, 32);
+
+ spa_list_init(&this->core_impl_list);
+ spa_list_init(&this->protocol_list);
+ spa_list_init(&this->core_list);
+ spa_list_init(&this->registry_resource_list);
+ spa_list_init(&this->global_list);
+ spa_list_init(&this->module_list);
+ spa_list_init(&this->device_list);
+ spa_list_init(&this->client_list);
+ spa_list_init(&this->node_list);
+ spa_list_init(&this->factory_list);
+ spa_list_init(&this->metadata_list);
+ spa_list_init(&this->link_list);
+ spa_list_init(&this->control_list[0]);
+ spa_list_init(&this->control_list[1]);
+ spa_list_init(&this->export_list);
+ spa_list_init(&this->driver_list);
+ spa_hook_list_init(&this->listener_list);
+ spa_hook_list_init(&this->driver_listener_list);
+
+ this->sc_pagesize = sysconf(_SC_PAGESIZE);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+ this->properties = properties;
+
+ conf = pw_properties_new(NULL, NULL);
+ if (conf == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+ this->conf = conf;
+ if ((res = pw_conf_load_conf_for_context (properties, conf)) < 0)
+ goto error_free;
+
+ n_support = pw_get_support(this->support, SPA_N_ELEMENTS(this->support) - 6);
+ cpu = spa_support_find(this->support, n_support, SPA_TYPE_INTERFACE_CPU);
+
+ res = pw_context_conf_update_props(this, "context.properties", properties);
+ pw_log_info("%p: parsed %d context.properties items", this, res);
+
+ if ((str = getenv("PIPEWIRE_CORE"))) {
+ pw_log_info("using core.name from environment: %s", str);
+ pw_properties_set(properties, PW_KEY_CORE_NAME, str);
+ }
+
+ if ((str = pw_properties_get(properties, "vm.overrides")) != NULL) {
+ if (cpu != NULL && spa_cpu_get_vm_type(cpu) != SPA_CPU_VM_NONE)
+ pw_properties_update_string(properties, str, strlen(str));
+ pw_properties_set(properties, "vm.overrides", NULL);
+ }
+ if (cpu != NULL) {
+ if (pw_properties_get(properties, PW_KEY_CPU_MAX_ALIGN) == NULL)
+ pw_properties_setf(properties, PW_KEY_CPU_MAX_ALIGN,
+ "%u", spa_cpu_get_max_align(cpu));
+ }
+
+ if (getenv("PIPEWIRE_DEBUG") == NULL &&
+ (str = pw_properties_get(properties, "log.level")) != NULL)
+ pw_log_set_level(atoi(str));
+
+ if (pw_properties_get_bool(properties, "mem.mlock-all", false)) {
+ if (mlockall(MCL_CURRENT | MCL_FUTURE) < 0)
+ pw_log_warn("%p: could not mlockall; %m", impl);
+ else
+ pw_log_info("%p: mlockall succeeded", impl);
+ }
+
+ pw_settings_init(this);
+ this->settings = this->defaults;
+
+ pr = pw_properties_copy(properties);
+ if ((str = pw_properties_get(pr, "context.data-loop." PW_KEY_LIBRARY_NAME_SYSTEM)))
+ pw_properties_set(pr, PW_KEY_LIBRARY_NAME_SYSTEM, str);
+
+ this->data_loop_impl = pw_data_loop_new(&pr->dict);
+ pw_properties_free(pr);
+ if (this->data_loop_impl == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ this->pool = pw_mempool_new(NULL);
+ if (this->pool == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ this->data_loop = pw_data_loop_get_loop(this->data_loop_impl);
+ this->data_system = this->data_loop->system;
+ this->main_loop = main_loop;
+
+ this->work_queue = pw_work_queue_new(this->main_loop);
+ if (this->work_queue == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ init_plugin_loader(impl);
+
+ this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, this->main_loop->system);
+ this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, this->main_loop->loop);
+ this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_LoopUtils, this->main_loop->utils);
+ this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataSystem, this->data_system);
+ this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, this->data_loop->loop);
+ this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_PluginLoader, &impl->plugin_loader);
+
+ if ((str = pw_properties_get(properties, "support.dbus")) == NULL ||
+ pw_properties_parse_bool(str)) {
+ lib = pw_properties_get(properties, PW_KEY_LIBRARY_NAME_DBUS);
+ if (lib == NULL)
+ lib = "support/libspa-dbus";
+
+ impl->dbus_handle = pw_load_spa_handle(lib,
+ SPA_NAME_SUPPORT_DBUS, NULL,
+ n_support, this->support);
+
+ if (impl->dbus_handle == NULL) {
+ pw_log_warn("%p: can't load dbus library: %s", this, lib);
+ } else if ((res = spa_handle_get_interface(impl->dbus_handle,
+ SPA_TYPE_INTERFACE_DBus, &dbus_iface)) < 0) {
+ pw_log_warn("%p: can't load dbus interface: %s", this, spa_strerror(res));
+ } else {
+ this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DBus, dbus_iface);
+ }
+ }
+ this->n_support = n_support;
+ spa_assert(n_support <= SPA_N_ELEMENTS(this->support));
+
+ this->core = pw_context_create_core(this, pw_properties_copy(properties), 0);
+ if (this->core == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+ pw_impl_core_register(this->core, NULL);
+
+ fill_properties(this);
+
+ if ((res = pw_context_parse_conf_section(this, conf, "context.spa-libs")) < 0)
+ goto error_free;
+ pw_log_info("%p: parsed %d context.spa-libs items", this, res);
+ if ((res = pw_context_parse_conf_section(this, conf, "context.modules")) < 0)
+ goto error_free;
+ if (res > 0)
+ pw_log_info("%p: parsed %d context.modules items", this, res);
+ else
+ pw_log_warn("%p: no modules loaded from context.modules", this);
+ if ((res = pw_context_parse_conf_section(this, conf, "context.objects")) < 0)
+ goto error_free;
+ pw_log_info("%p: parsed %d context.objects items", this, res);
+ if ((res = pw_context_parse_conf_section(this, conf, "context.exec")) < 0)
+ goto error_free;
+ pw_log_info("%p: parsed %d context.exec items", this, res);
+
+ if ((res = pw_data_loop_start(this->data_loop_impl)) < 0)
+ goto error_free;
+
+ pw_data_loop_invoke(this->data_loop_impl,
+ do_data_loop_setup, 0, NULL, 0, false, this);
+
+ pw_settings_expose(this);
+
+ pw_log_debug("%p: created", this);
+
+ return this;
+
+error_free:
+ pw_context_destroy(this);
+error_cleanup:
+ errno = -res;
+ return NULL;
+}
+
+/** Destroy a context object
+ *
+ * \param context a context to destroy
+ */
+SPA_EXPORT
+void pw_context_destroy(struct pw_context *context)
+{
+ struct impl *impl = SPA_CONTAINER_OF(context, struct impl, this);
+ struct pw_global *global;
+ struct pw_impl_client *client;
+ struct pw_impl_module *module;
+ struct pw_impl_device *device;
+ struct pw_core *core;
+ struct pw_resource *resource;
+ struct pw_impl_node *node;
+ struct factory_entry *entry;
+ struct pw_impl_metadata *metadata;
+ struct pw_impl_core *core_impl;
+
+ pw_log_debug("%p: destroy", context);
+ pw_context_emit_destroy(context);
+
+ spa_list_consume(core, &context->core_list, link)
+ pw_core_disconnect(core);
+
+ spa_list_consume(client, &context->client_list, link)
+ pw_impl_client_destroy(client);
+
+ spa_list_consume(node, &context->node_list, link)
+ pw_impl_node_destroy(node);
+
+ spa_list_consume(device, &context->device_list, link)
+ pw_impl_device_destroy(device);
+
+ spa_list_consume(resource, &context->registry_resource_list, link)
+ pw_resource_destroy(resource);
+
+ if (context->data_loop_impl)
+ pw_data_loop_stop(context->data_loop_impl);
+
+ spa_list_consume(module, &context->module_list, link)
+ pw_impl_module_destroy(module);
+
+ spa_list_consume(global, &context->global_list, link)
+ pw_global_destroy(global);
+
+ spa_list_consume(metadata, &context->metadata_list, link)
+ pw_impl_metadata_destroy(metadata);
+
+ spa_list_consume(core_impl, &context->core_impl_list, link)
+ pw_impl_core_destroy(core_impl);
+
+ pw_log_debug("%p: free", context);
+ pw_context_emit_free(context);
+
+ if (context->data_loop_impl)
+ pw_data_loop_destroy(context->data_loop_impl);
+
+ if (context->pool)
+ pw_mempool_destroy(context->pool);
+
+ if (context->work_queue)
+ pw_work_queue_destroy(context->work_queue);
+
+ pw_properties_free(context->properties);
+ pw_properties_free(context->conf);
+
+ pw_settings_clean(context);
+
+ if (impl->dbus_handle)
+ pw_unload_spa_handle(impl->dbus_handle);
+
+ pw_array_for_each(entry, &context->factory_lib) {
+ regfree(&entry->regex);
+ free(entry->lib);
+ }
+ pw_array_clear(&context->factory_lib);
+
+ pw_array_clear(&context->objects);
+
+ pw_map_clear(&context->globals);
+
+ spa_hook_list_clean(&context->listener_list);
+ spa_hook_list_clean(&context->driver_listener_list);
+
+ free(context);
+}
+
+SPA_EXPORT
+void *pw_context_get_user_data(struct pw_context *context)
+{
+ return context->user_data;
+}
+
+SPA_EXPORT
+void pw_context_add_listener(struct pw_context *context,
+ struct spa_hook *listener,
+ const struct pw_context_events *events,
+ void *data)
+{
+ spa_hook_list_append(&context->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+const struct spa_support *pw_context_get_support(struct pw_context *context, uint32_t *n_support)
+{
+ *n_support = context->n_support;
+ return context->support;
+}
+
+SPA_EXPORT
+struct pw_loop *pw_context_get_main_loop(struct pw_context *context)
+{
+ return context->main_loop;
+}
+
+SPA_EXPORT
+struct pw_data_loop *pw_context_get_data_loop(struct pw_context *context)
+{
+ return context->data_loop_impl;
+}
+
+SPA_EXPORT
+struct pw_work_queue *pw_context_get_work_queue(struct pw_context *context)
+{
+ return context->work_queue;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_context_get_properties(struct pw_context *context)
+{
+ return context->properties;
+}
+
+SPA_EXPORT
+const char *pw_context_get_conf_section(struct pw_context *context, const char *section)
+{
+ return pw_properties_get(context->conf, section);
+}
+
+/** Update context properties
+ *
+ * \param context a context
+ * \param dict properties to update
+ *
+ * Update the context object with the given properties
+ */
+SPA_EXPORT
+int pw_context_update_properties(struct pw_context *context, const struct spa_dict *dict)
+{
+ int changed;
+
+ changed = pw_properties_update(context->properties, dict);
+ pw_log_debug("%p: updated %d properties", context, changed);
+
+ return changed;
+}
+
+static bool global_can_read(struct pw_context *context, struct pw_global *global)
+{
+ if (context->current_client &&
+ !PW_PERM_IS_R(pw_global_get_permissions(global, context->current_client)))
+ return false;
+ return true;
+}
+
+SPA_EXPORT
+int pw_context_for_each_global(struct pw_context *context,
+ int (*callback) (void *data, struct pw_global *global),
+ void *data)
+{
+ struct pw_global *g, *t;
+ int res;
+
+ spa_list_for_each_safe(g, t, &context->global_list, link) {
+ if (!global_can_read(context, g))
+ continue;
+ if ((res = callback(data, g)) != 0)
+ return res;
+ }
+ return 0;
+}
+
+SPA_EXPORT
+struct pw_global *pw_context_find_global(struct pw_context *context, uint32_t id)
+{
+ struct pw_global *global;
+
+ global = pw_map_lookup(&context->globals, id);
+ if (global == NULL || !global->registered) {
+ errno = ENOENT;
+ return NULL;
+ }
+
+ if (!global_can_read(context, global)) {
+ errno = EACCES;
+ return NULL;
+ }
+ return global;
+}
+
+SPA_PRINTF_FUNC(7, 8) int pw_context_debug_port_params(struct pw_context *this,
+ struct spa_node *node, enum spa_direction direction,
+ uint32_t port_id, uint32_t id, int err, const char *debug, ...)
+{
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ uint32_t state;
+ struct spa_pod *param;
+ int res;
+ va_list args;
+
+ va_start(args, debug);
+ vsnprintf((char*)buffer, sizeof(buffer), debug, args);
+ va_end(args);
+
+ pw_log_error("params %s: %d:%d %s (%s)",
+ spa_debug_type_find_name(spa_type_param, id),
+ direction, port_id, spa_strerror(err), buffer);
+
+ if (err == -EBUSY)
+ return 0;
+
+ state = 0;
+ while (true) {
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ res = spa_node_port_enum_params_sync(node,
+ direction, port_id,
+ id, &state,
+ NULL, &param, &b);
+ if (res != 1) {
+ if (res < 0)
+ pw_log_error(" error: %s", spa_strerror(res));
+ break;
+ }
+ pw_log_pod(SPA_LOG_LEVEL_ERROR, param);
+ }
+ return 0;
+}
+
+/** Find a common format between two ports
+ *
+ * \param context a context object
+ * \param output an output port
+ * \param input an input port
+ * \param props extra properties
+ * \param n_format_filters number of format filters
+ * \param format_filters array of format filters
+ * \param[out] format the common format between the ports
+ * \param builder builder to use for processing
+ * \param[out] error an error when something is wrong
+ * \return a common format of NULL on error
+ *
+ * Find a common format between the given ports. The format will
+ * be restricted to a subset given with the format filters.
+ */
+int pw_context_find_format(struct pw_context *context,
+ struct pw_impl_port *output,
+ struct pw_impl_port *input,
+ struct pw_properties *props,
+ uint32_t n_format_filters,
+ struct spa_pod **format_filters,
+ struct spa_pod **format,
+ struct spa_pod_builder *builder,
+ char **error)
+{
+ uint32_t out_state, in_state;
+ int res;
+ uint32_t iidx = 0, oidx = 0;
+ struct spa_pod_builder fb = { 0 };
+ uint8_t fbuf[4096];
+ struct spa_pod *filter;
+
+ out_state = output->state;
+ in_state = input->state;
+
+ pw_log_debug("%p: finding best format %d %d", context, out_state, in_state);
+
+ /* when a port is configured but the node is idle, we can reconfigure with a different format */
+ if (out_state > PW_IMPL_PORT_STATE_CONFIGURE && output->node->info.state == PW_NODE_STATE_IDLE)
+ out_state = PW_IMPL_PORT_STATE_CONFIGURE;
+ if (in_state > PW_IMPL_PORT_STATE_CONFIGURE && input->node->info.state == PW_NODE_STATE_IDLE)
+ in_state = PW_IMPL_PORT_STATE_CONFIGURE;
+
+ pw_log_debug("%p: states %d %d", context, out_state, in_state);
+
+ if (in_state == PW_IMPL_PORT_STATE_CONFIGURE && out_state > PW_IMPL_PORT_STATE_CONFIGURE) {
+ /* only input needs format */
+ spa_pod_builder_init(&fb, fbuf, sizeof(fbuf));
+ if ((res = spa_node_port_enum_params_sync(output->node->node,
+ output->direction, output->port_id,
+ SPA_PARAM_Format, &oidx,
+ NULL, &filter, &fb)) != 1) {
+ if (res < 0)
+ *error = spa_aprintf("error get output format: %s", spa_strerror(res));
+ else
+ *error = spa_aprintf("no output formats");
+ goto error;
+ }
+ pw_log_debug("%p: Got output format:", context);
+ pw_log_format(SPA_LOG_LEVEL_DEBUG, filter);
+
+ if ((res = spa_node_port_enum_params_sync(input->node->node,
+ input->direction, input->port_id,
+ SPA_PARAM_EnumFormat, &iidx,
+ filter, format, builder)) <= 0) {
+ if (res == -ENOENT || res == 0) {
+ pw_log_debug("%p: no input format filter, using output format: %s",
+ context, spa_strerror(res));
+ *format = filter;
+ } else {
+ *error = spa_aprintf("error input enum formats: %s", spa_strerror(res));
+ goto error;
+ }
+ }
+ } else if (out_state >= PW_IMPL_PORT_STATE_CONFIGURE && in_state > PW_IMPL_PORT_STATE_CONFIGURE) {
+ /* only output needs format */
+ spa_pod_builder_init(&fb, fbuf, sizeof(fbuf));
+ if ((res = spa_node_port_enum_params_sync(input->node->node,
+ input->direction, input->port_id,
+ SPA_PARAM_Format, &iidx,
+ NULL, &filter, &fb)) != 1) {
+ if (res < 0)
+ *error = spa_aprintf("error get input format: %s", spa_strerror(res));
+ else
+ *error = spa_aprintf("no input format");
+ goto error;
+ }
+ pw_log_debug("%p: Got input format:", context);
+ pw_log_format(SPA_LOG_LEVEL_DEBUG, filter);
+
+ if ((res = spa_node_port_enum_params_sync(output->node->node,
+ output->direction, output->port_id,
+ SPA_PARAM_EnumFormat, &oidx,
+ filter, format, builder)) <= 0) {
+ if (res == -ENOENT || res == 0) {
+ pw_log_debug("%p: no output format filter, using input format: %s",
+ context, spa_strerror(res));
+ *format = filter;
+ } else {
+ *error = spa_aprintf("error output enum formats: %s", spa_strerror(res));
+ goto error;
+ }
+ }
+ } else if (in_state == PW_IMPL_PORT_STATE_CONFIGURE && out_state == PW_IMPL_PORT_STATE_CONFIGURE) {
+ again:
+ /* both ports need a format */
+ pw_log_debug("%p: do enum input %d", context, iidx);
+ spa_pod_builder_init(&fb, fbuf, sizeof(fbuf));
+ if ((res = spa_node_port_enum_params_sync(input->node->node,
+ input->direction, input->port_id,
+ SPA_PARAM_EnumFormat, &iidx,
+ NULL, &filter, &fb)) != 1) {
+ if (res == -ENOENT) {
+ pw_log_debug("%p: no input filter", context);
+ filter = NULL;
+ } else {
+ if (res < 0)
+ *error = spa_aprintf("error input enum formats: %s", spa_strerror(res));
+ else
+ *error = spa_aprintf("no more input formats");
+ goto error;
+ }
+ }
+ pw_log_debug("%p: enum output %d with filter: %p", context, oidx, filter);
+ pw_log_format(SPA_LOG_LEVEL_DEBUG, filter);
+
+ if ((res = spa_node_port_enum_params_sync(output->node->node,
+ output->direction, output->port_id,
+ SPA_PARAM_EnumFormat, &oidx,
+ filter, format, builder)) != 1) {
+ if (res == 0 && filter != NULL) {
+ oidx = 0;
+ goto again;
+ }
+ *error = spa_aprintf("error output enum formats: %s", spa_strerror(res));
+ goto error;
+ }
+
+ pw_log_debug("%p: Got filtered:", context);
+ pw_log_format(SPA_LOG_LEVEL_DEBUG, *format);
+ } else {
+ res = -EBADF;
+ *error = spa_aprintf("error bad node state");
+ goto error;
+ }
+ return res;
+error:
+ if (res == 0)
+ res = -EINVAL;
+ return res;
+}
+
+static int ensure_state(struct pw_impl_node *node, bool running)
+{
+ enum pw_node_state state = node->info.state;
+ if (node->active && !SPA_FLAG_IS_SET(node->spa_flags, SPA_NODE_FLAG_NEED_CONFIGURE) && running)
+ state = PW_NODE_STATE_RUNNING;
+ else if (state > PW_NODE_STATE_IDLE)
+ state = PW_NODE_STATE_IDLE;
+ return pw_impl_node_set_state(node, state);
+}
+
+static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, struct spa_list *collect)
+{
+ struct spa_list queue;
+ struct pw_impl_node *n, *t;
+ struct pw_impl_port *p;
+ struct pw_impl_link *l;
+
+ pw_log_debug("node %p: '%s'", node, node->name);
+
+ /* start with node in the queue */
+ spa_list_init(&queue);
+ spa_list_append(&queue, &node->sort_link);
+ node->visited = true;
+
+ /* now follow all the links from the nodes in the queue
+ * and add the peers to the queue. */
+ spa_list_consume(n, &queue, sort_link) {
+ pw_log_debug(" next node %p: '%s'", n, n->name);
+
+ spa_list_remove(&n->sort_link);
+ spa_list_append(collect, &n->sort_link);
+ n->passive = true;
+
+ if (!n->active)
+ continue;
+
+ spa_list_for_each(p, &n->input_ports, link) {
+ spa_list_for_each(l, &p->links, input_link) {
+ t = l->output->node;
+
+ if (!t->active)
+ continue;
+
+ pw_impl_link_prepare(l);
+
+ if (!l->prepared)
+ continue;
+
+ if (!l->passive)
+ node->passive = n->passive = false;
+
+ if (!t->visited) {
+ t->visited = true;
+ spa_list_append(&queue, &t->sort_link);
+ }
+ }
+ }
+ spa_list_for_each(p, &n->output_ports, link) {
+ spa_list_for_each(l, &p->links, output_link) {
+ t = l->input->node;
+
+ if (!t->active)
+ continue;
+
+ pw_impl_link_prepare(l);
+
+ if (!l->prepared)
+ continue;
+
+ if (!l->passive)
+ node->passive = n->passive = false;
+
+ if (!t->visited) {
+ t->visited = true;
+ spa_list_append(&queue, &t->sort_link);
+ }
+ }
+ }
+ /* now go through all the nodes that have the same group and
+ * that are not yet visited */
+ if (n->group[0] == '\0')
+ continue;
+
+ spa_list_for_each(t, &context->node_list, link) {
+ if (t->exported || t == n || !t->active || t->visited)
+ continue;
+ if (!spa_streq(t->group, n->group))
+ continue;
+ pw_log_debug("%p join group %s: '%s'", t, t->group, n->group);
+ t->visited = true;
+ spa_list_append(&queue, &t->sort_link);
+ }
+ }
+ return 0;
+}
+
+static void move_to_driver(struct pw_context *context, struct spa_list *nodes,
+ struct pw_impl_node *driver)
+{
+ struct pw_impl_node *n;
+ pw_log_debug("driver: %p %s", driver, driver->name);
+ spa_list_consume(n, nodes, sort_link) {
+ spa_list_remove(&n->sort_link);
+ pw_log_debug(" follower: %p %s", n, n->name);
+ pw_impl_node_set_driver(n, driver);
+ }
+}
+static void remove_from_driver(struct pw_context *context, struct spa_list *nodes)
+{
+ struct pw_impl_node *n;
+ spa_list_consume(n, nodes, sort_link) {
+ spa_list_remove(&n->sort_link);
+ pw_impl_node_set_driver(n, NULL);
+ ensure_state(n, false);
+ }
+}
+
+static inline void get_quantums(struct pw_context *context, uint32_t *def,
+ uint32_t *min, uint32_t *max, uint32_t *limit, uint32_t *rate)
+{
+ struct settings *s = &context->settings;
+ if (s->clock_force_quantum != 0) {
+ *def = *min = *max = s->clock_force_quantum;
+ *rate = 0;
+ } else {
+ *def = s->clock_quantum;
+ *min = s->clock_min_quantum;
+ *max = s->clock_max_quantum;
+ *rate = s->clock_rate;
+ }
+ *limit = s->clock_quantum_limit;
+}
+
+static inline const uint32_t *get_rates(struct pw_context *context, uint32_t *def, uint32_t *n_rates,
+ bool *force)
+{
+ struct settings *s = &context->settings;
+ if (s->clock_force_rate != 0) {
+ *force = true;
+ *n_rates = 1;
+ *def = s->clock_force_rate;
+ return &s->clock_force_rate;
+ } else {
+ *force = false;
+ *n_rates = s->n_clock_rates;
+ *def = s->clock_rate;
+ return s->clock_rates;
+ }
+}
+static void reconfigure_driver(struct pw_context *context, struct pw_impl_node *n)
+{
+ struct pw_impl_node *s;
+
+ spa_list_for_each(s, &n->follower_list, follower_link) {
+ if (s == n)
+ continue;
+ pw_log_debug("%p: follower %p: '%s' suspend",
+ context, s, s->name);
+ pw_impl_node_set_state(s, PW_NODE_STATE_SUSPENDED);
+ }
+ pw_log_debug("%p: driver %p: '%s' suspend",
+ context, n, n->name);
+
+ if (n->info.state >= PW_NODE_STATE_IDLE)
+ n->reconfigure = true;
+ pw_impl_node_set_state(n, PW_NODE_STATE_SUSPENDED);
+}
+
+/* find smaller power of 2 */
+static uint32_t flp2(uint32_t x)
+{
+ x = x | (x >> 1);
+ x = x | (x >> 2);
+ x = x | (x >> 4);
+ x = x | (x >> 8);
+ x = x | (x >> 16);
+ return x - (x >> 1);
+}
+
+/* cmp fractions, avoiding overflows */
+static int fraction_compare(const struct spa_fraction *a, const struct spa_fraction *b)
+{
+ uint64_t fa = (uint64_t)a->num * (uint64_t)b->denom;
+ uint64_t fb = (uint64_t)b->num * (uint64_t)a->denom;
+ return fa < fb ? -1 : (fa > fb ? 1 : 0);
+}
+
+static uint32_t find_best_rate(const uint32_t *rates, uint32_t n_rates, uint32_t rate, uint32_t best)
+{
+ uint32_t i;
+ for (i = 0; i < n_rates; i++) {
+ if (SPA_ABS((int32_t)rate - (int32_t)rates[i]) <
+ SPA_ABS((int32_t)rate - (int32_t)best))
+ best = rates[i];
+ }
+ return best;
+}
+
+/* here we evaluate the complete state of the graph.
+ *
+ * It roughly operates in 3 stages:
+ *
+ * 1. go over all drivers and collect the nodes that need to be scheduled with the
+ * driver. This include all nodes that have an active link with the driver or
+ * with a node already scheduled with the driver.
+ *
+ * 2. go over all nodes that are not assigned to a driver. The ones that require
+ * a driver are moved to some random active driver found in step 1.
+ *
+ * 3. go over all drivers again, collect the quantum/rate of all followers, select
+ * the desired final value and activate the followers and then the driver.
+ *
+ * A complete graph evaluation is performed for each change that is made to the
+ * graph, such as making/destroying links, adding/removing nodes, property changes such
+ * as quantum/rate changes or metadata changes.
+ */
+int pw_context_recalc_graph(struct pw_context *context, const char *reason)
+{
+ struct impl *impl = SPA_CONTAINER_OF(context, struct impl, this);
+ struct settings *settings = &context->settings;
+ struct pw_impl_node *n, *s, *target, *fallback;
+ const uint32_t *rates;
+ uint32_t max_quantum, min_quantum, def_quantum, lim_quantum, rate_quantum;
+ uint32_t n_rates, def_rate;
+ bool freewheel = false, global_force_rate, global_force_quantum;
+ struct spa_list collect;
+
+ pw_log_info("%p: busy:%d reason:%s", context, impl->recalc, reason);
+
+ if (impl->recalc) {
+ impl->recalc_pending = true;
+ return -EBUSY;
+ }
+
+again:
+ impl->recalc = true;
+
+ get_quantums(context, &def_quantum, &min_quantum, &max_quantum, &lim_quantum, &rate_quantum);
+ rates = get_rates(context, &def_rate, &n_rates, &global_force_rate);
+
+ global_force_quantum = rate_quantum == 0;
+
+ /* start from all drivers and group all nodes that are linked
+ * to it. Some nodes are not (yet) linked to anything and they
+ * will end up 'unassigned' to a driver. Other nodes are drivers
+ * and if they have active followers, we can use them to schedule
+ * the unassigned nodes. */
+ target = fallback = NULL;
+ spa_list_for_each(n, &context->driver_list, driver_link) {
+ if (n->exported)
+ continue;
+
+ if (!n->visited) {
+ spa_list_init(&collect);
+ collect_nodes(context, n, &collect);
+ move_to_driver(context, &collect, n);
+ }
+ /* from now on we are only interested in active driving nodes.
+ * We're going to see if there are active followers. */
+ if (!n->driving || !n->active)
+ continue;
+
+ /* first active driving node is fallback */
+ if (fallback == NULL)
+ fallback = n;
+
+ if (n->passive)
+ continue;
+
+ spa_list_for_each(s, &n->follower_list, follower_link) {
+ pw_log_debug("%p: driver %p: follower %p %s: active:%d",
+ context, n, s, s->name, s->active);
+ if (s != n && s->active) {
+ /* if the driving node has active followers, it
+ * is a target for our unassigned nodes */
+ if (target == NULL)
+ target = n;
+ if (n->freewheel)
+ freewheel = true;
+ break;
+ }
+ }
+ }
+ /* no active node, use fallback driving node */
+ if (target == NULL)
+ target = fallback;
+
+ /* update the freewheel status */
+ if (context->freewheeling != freewheel)
+ context_set_freewheel(context, freewheel);
+
+ /* now go through all available nodes. The ones we didn't visit
+ * in collect_nodes() are not linked to any driver. We assign them
+ * to either an active driver or the first driver if they are in a
+ * group that needs a driver. Else we remove them from a driver
+ * and stop them. */
+ spa_list_for_each(n, &context->node_list, link) {
+ struct pw_impl_node *t, *driver;
+
+ if (n->exported || n->visited)
+ continue;
+
+ pw_log_debug("%p: unassigned node %p: '%s' active:%d want_driver:%d target:%p",
+ context, n, n->name, n->active, n->want_driver, target);
+
+ /* collect all nodes in this group */
+ spa_list_init(&collect);
+ collect_nodes(context, n, &collect);
+
+ driver = NULL;
+ spa_list_for_each(t, &collect, sort_link) {
+ /* is any active and want a driver or it want process */
+ if ((t->want_driver && t->active && !n->passive) ||
+ t->always_process)
+ driver = target;
+ }
+ if (driver != NULL) {
+ /* driver needed for this group */
+ driver->passive = false;
+ move_to_driver(context, &collect, driver);
+ } else {
+ /* no driver, make sure the nodes stops */
+ remove_from_driver(context, &collect);
+ }
+ }
+ /* clean up the visited flag now */
+ spa_list_for_each(n, &context->node_list, link)
+ n->visited = false;
+
+ /* assign final quantum and set state for followers and drivers */
+ spa_list_for_each(n, &context->driver_list, driver_link) {
+ bool running = false, lock_quantum = false, lock_rate = false;
+ struct spa_fraction latency = SPA_FRACTION(0, 0);
+ struct spa_fraction max_latency = SPA_FRACTION(0, 0);
+ struct spa_fraction rate = SPA_FRACTION(0, 0);
+ uint32_t quantum, target_rate, current_rate;
+ uint64_t quantum_stamp = 0, rate_stamp = 0;
+ bool force_rate, force_quantum;
+ const uint32_t *node_rates;
+ uint32_t node_n_rates, node_def_rate;
+ uint32_t node_max_quantum, node_min_quantum, node_def_quantum, node_rate_quantum;
+
+ if (!n->driving || n->exported)
+ continue;
+
+ node_def_quantum = def_quantum;
+ node_min_quantum = min_quantum;
+ node_max_quantum = max_quantum;
+ node_rate_quantum = rate_quantum;
+ force_quantum = global_force_quantum;
+
+ node_def_rate = def_rate;
+ node_n_rates = n_rates;
+ node_rates = rates;
+ force_rate = global_force_rate;
+
+ /* collect quantum and rate */
+ spa_list_for_each(s, &n->follower_list, follower_link) {
+
+ if (!s->moved) {
+ /* We only try to enforce the lock flags for nodes that
+ * are not recently moved between drivers. The nodes that
+ * are moved should try to enforce their quantum on the
+ * new driver. */
+ lock_quantum |= s->lock_quantum;
+ lock_rate |= s->lock_rate;
+ }
+ if (!global_force_quantum && s->force_quantum > 0 &&
+ s->stamp > quantum_stamp) {
+ node_def_quantum = node_min_quantum = node_max_quantum = s->force_quantum;
+ node_rate_quantum = 0;
+ quantum_stamp = s->stamp;
+ force_quantum = true;
+ }
+ if (!global_force_rate && s->force_rate > 0 &&
+ s->stamp > rate_stamp) {
+ node_def_rate = s->force_rate;
+ node_n_rates = 1;
+ node_rates = &s->force_rate;
+ force_rate = true;
+ rate_stamp = s->stamp;
+ }
+
+ /* smallest latencies */
+ if (latency.denom == 0 ||
+ (s->latency.denom > 0 &&
+ fraction_compare(&s->latency, &latency) < 0))
+ latency = s->latency;
+ if (max_latency.denom == 0 ||
+ (s->max_latency.denom > 0 &&
+ fraction_compare(&s->max_latency, &max_latency) < 0))
+ max_latency = s->max_latency;
+
+ /* largest rate */
+ if (rate.denom == 0 ||
+ (s->rate.denom > 0 &&
+ fraction_compare(&s->rate, &rate) > 0))
+ rate = s->rate;
+
+ if (s->active)
+ running = !n->passive;
+
+ pw_log_debug("%p: follower %p running:%d passive:%d rate:%u/%u latency %u/%u '%s'",
+ context, s, running, s->passive, rate.num, rate.denom,
+ latency.num, latency.denom, s->name);
+
+ s->moved = false;
+ }
+
+ if (force_quantum)
+ lock_quantum = false;
+ if (force_rate)
+ lock_rate = false;
+
+ if (n->reconfigure)
+ running = true;
+
+ current_rate = n->current_rate.denom;
+ if (lock_rate || n->reconfigure ||
+ (!force_rate &&
+ (n->info.state > PW_NODE_STATE_IDLE)))
+ /* when someone wants us to lock the rate of this driver or
+ * when the driver is busy and we don't need to force a rate,
+ * keep the current rate */
+ target_rate = current_rate;
+ else {
+ /* 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);
+ }
+
+ if (target_rate != current_rate) {
+ bool do_reconfigure = false;
+ /* we doing a rate switch */
+ pw_log_info("(%s-%u) state:%s new rate:%u->%u",
+ n->name, n->info.id,
+ pw_node_state_as_string(n->info.state),
+ n->current_rate.denom,
+ target_rate);
+
+ if (force_rate) {
+ if (settings->clock_rate_update_mode == CLOCK_RATE_UPDATE_MODE_HARD)
+ do_reconfigure = true;
+ } else {
+ if (n->info.state >= PW_NODE_STATE_SUSPENDED)
+ do_reconfigure = true;
+ }
+ if (do_reconfigure)
+ reconfigure_driver(context, n);
+
+ /* we're setting the pending rate. This will become the new
+ * current rate in the next iteration of the graph. */
+ n->current_rate = SPA_FRACTION(1, target_rate);
+ n->current_pending = true;
+ current_rate = target_rate;
+ /* we might be suspended now and the links need to be prepared again */
+ if (do_reconfigure)
+ goto 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;
+ }
+
+ /* calculate desired quantum */
+ if (max_latency.denom != 0) {
+ uint32_t tmp = (max_latency.num * current_rate / max_latency.denom);
+ if (tmp < node_max_quantum)
+ node_max_quantum = tmp;
+ }
+
+ quantum = node_def_quantum;
+ if (latency.denom != 0)
+ quantum = (latency.num * current_rate / latency.denom);
+ quantum = SPA_CLAMP(quantum, node_min_quantum, node_max_quantum);
+ quantum = SPA_MIN(quantum, lim_quantum);
+
+ if (settings->clock_power_of_two_quantum)
+ quantum = flp2(quantum);
+
+ if (running && quantum != n->current_quantum && !lock_quantum) {
+ pw_log_info("(%s-%u) new quantum:%"PRIu64"->%u",
+ n->name, n->info.id,
+ n->current_quantum,
+ quantum);
+ /* this is the new pending quantum */
+ n->current_quantum = quantum;
+ n->current_pending = true;
+ }
+
+ if (n->info.state < PW_NODE_STATE_RUNNING && n->current_pending) {
+ /* the driver node is not actually running and we have a
+ * pending change. Apply the change to the position now so
+ * that we have the right values when we change the node
+ * states of the driver and followers to RUNNING below */
+ pw_log_debug("%p: apply duration:%"PRIu64" rate:%u/%u", context,
+ n->current_quantum, n->current_rate.num,
+ n->current_rate.denom);
+ n->rt.position->clock.duration = n->current_quantum;
+ n->rt.position->clock.rate = n->current_rate;
+ n->current_pending = false;
+ }
+
+ pw_log_debug("%p: driver %p running:%d passive:%d quantum:%u '%s'",
+ context, n, running, n->passive, quantum, n->name);
+
+ /* first change the node states of the followers to the new target */
+ spa_list_for_each(s, &n->follower_list, follower_link) {
+ if (s == n)
+ continue;
+ pw_log_debug("%p: follower %p: active:%d '%s'",
+ context, s, s->active, s->name);
+ ensure_state(s, running);
+ }
+ /* now that all the followers are ready, start the driver */
+ ensure_state(n, running);
+ }
+ impl->recalc = false;
+ if (impl->recalc_pending) {
+ impl->recalc_pending = false;
+ goto again;
+ }
+
+ return 0;
+}
+
+SPA_EXPORT
+int pw_context_add_spa_lib(struct pw_context *context,
+ const char *factory_regexp, const char *lib)
+{
+ struct factory_entry *entry;
+ int err;
+
+ entry = pw_array_add(&context->factory_lib, sizeof(*entry));
+ if (entry == NULL)
+ return -errno;
+
+ if ((err = regcomp(&entry->regex, factory_regexp, REG_EXTENDED | REG_NOSUB)) != 0) {
+ char errbuf[1024];
+ regerror(err, &entry->regex, errbuf, sizeof(errbuf));
+ pw_log_error("%p: can compile regex: %s", context, errbuf);
+ pw_array_remove(&context->factory_lib, entry);
+ return -EINVAL;
+ }
+
+ entry->lib = strdup(lib);
+ pw_log_debug("%p: map factory regex '%s' to '%s", context,
+ factory_regexp, lib);
+ return 0;
+}
+
+SPA_EXPORT
+const char *pw_context_find_spa_lib(struct pw_context *context, const char *factory_name)
+{
+ struct factory_entry *entry;
+
+ pw_array_for_each(entry, &context->factory_lib) {
+ if (regexec(&entry->regex, factory_name, 0, NULL, 0) == 0)
+ return entry->lib;
+ }
+ return NULL;
+}
+
+SPA_EXPORT
+struct spa_handle *pw_context_load_spa_handle(struct pw_context *context,
+ const char *factory_name,
+ const struct spa_dict *info)
+{
+ const char *lib;
+ const struct spa_support *support;
+ uint32_t n_support;
+ struct spa_handle *handle;
+
+ pw_log_debug("%p: load factory %s", context, factory_name);
+
+ lib = pw_context_find_spa_lib(context, factory_name);
+ if (lib == NULL && info != NULL)
+ lib = spa_dict_lookup(info, SPA_KEY_LIBRARY_NAME);
+ if (lib == NULL) {
+ errno = ENOENT;
+ pw_log_warn("%p: no library for %s: %m",
+ context, factory_name);
+ return NULL;
+ }
+
+ support = pw_context_get_support(context, &n_support);
+
+ handle = pw_load_spa_handle(lib, factory_name,
+ info, n_support, support);
+
+ return handle;
+}
+
+SPA_EXPORT
+int pw_context_register_export_type(struct pw_context *context, struct pw_export_type *type)
+{
+ if (pw_context_find_export_type(context, type->type)) {
+ pw_log_warn("context %p: duplicate export type %s", context, type->type);
+ return -EEXIST;
+ }
+ pw_log_debug("context %p: Add export type %s to context", context, type->type);
+ spa_list_append(&context->export_list, &type->link);
+ return 0;
+}
+
+SPA_EXPORT
+const struct pw_export_type *pw_context_find_export_type(struct pw_context *context, const char *type)
+{
+ const struct pw_export_type *t;
+ spa_list_for_each(t, &context->export_list, link) {
+ if (spa_streq(t->type, type))
+ return t;
+ }
+ return NULL;
+}
+
+struct object_entry {
+ const char *type;
+ void *value;
+};
+
+static struct object_entry *find_object(struct pw_context *context, const char *type)
+{
+ struct object_entry *entry;
+ pw_array_for_each(entry, &context->objects) {
+ if (spa_streq(entry->type, type))
+ return entry;
+ }
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_context_set_object(struct pw_context *context, const char *type, void *value)
+{
+ struct object_entry *entry;
+
+ entry = find_object(context, type);
+
+ if (value == NULL) {
+ if (entry)
+ pw_array_remove(&context->objects, entry);
+ } else {
+ if (entry == NULL) {
+ entry = pw_array_add(&context->objects, sizeof(*entry));
+ if (entry == NULL)
+ return -errno;
+ entry->type = type;
+ }
+ entry->value = value;
+ }
+ if (spa_streq(type, SPA_TYPE_INTERFACE_ThreadUtils)) {
+ context->thread_utils = value;
+ if (context->data_loop_impl)
+ pw_data_loop_set_thread_utils(context->data_loop_impl,
+ context->thread_utils);
+ }
+ return 0;
+}
+
+SPA_EXPORT
+void *pw_context_get_object(struct pw_context *context, const char *type)
+{
+ struct object_entry *entry;
+
+ if ((entry = find_object(context, type)) != NULL)
+ return entry->value;
+
+ return NULL;
+}
diff --git a/src/pipewire/context.h b/src/pipewire/context.h
new file mode 100644
index 0000000..fe658a6
--- /dev/null
+++ b/src/pipewire/context.h
@@ -0,0 +1,195 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CONTEXT_H
+#define PIPEWIRE_CONTEXT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+/** \defgroup pw_context Context
+ *
+ * \brief The PipeWire context object manages all locally available
+ * resources. It is used by both clients and servers.
+ *
+ * The context is used to:
+ *
+ * - Load modules and extend the functionality. This includes
+ * extending the protocol with new object types or creating
+ * any of the available objects.
+ *
+ * - Create implementations of various objects like nodes,
+ * devices, factories, modules, etc.. This will usually also
+ * create pw_global objects that can then be shared with
+ * clients.
+ *
+ * - Connect to another PipeWire instance (the main daemon, for
+ * example) and interact with it (See \ref page_core_api).
+ *
+ * - Export a local implementation of an object to another
+ * instance.
+ */
+
+/**
+ * \addtogroup pw_context
+ * @{
+ */
+struct pw_context;
+
+struct pw_global;
+struct pw_impl_client;
+
+#include <pipewire/core.h>
+#include <pipewire/loop.h>
+#include <pipewire/properties.h>
+
+/** context events emitted by the context object added with \ref pw_context_add_listener */
+struct pw_context_events {
+#define PW_VERSION_CONTEXT_EVENTS 0
+ uint32_t version;
+
+ /** The context is being destroyed */
+ void (*destroy) (void *data);
+ /** The context is being freed */
+ void (*free) (void *data);
+ /** a new client object is added */
+ void (*check_access) (void *data, struct pw_impl_client *client);
+ /** a new global object was added */
+ void (*global_added) (void *data, struct pw_global *global);
+ /** a global object was removed */
+ void (*global_removed) (void *data, struct pw_global *global);
+};
+
+/** Make a new context object for a given main_loop. Ownership of the properties is taken */
+struct pw_context * pw_context_new(struct pw_loop *main_loop, /**< a main loop to run in */
+ struct pw_properties *props, /**< extra properties */
+ size_t user_data_size /**< extra user data size */);
+
+/** destroy a context object, all resources except the main_loop will be destroyed */
+void pw_context_destroy(struct pw_context *context);
+
+/** Get the context user data */
+void *pw_context_get_user_data(struct pw_context *context);
+
+/** Add a new event listener to a context */
+void pw_context_add_listener(struct pw_context *context,
+ struct spa_hook *listener,
+ const struct pw_context_events *events,
+ void *data);
+
+/** Get the context properties */
+const struct pw_properties *pw_context_get_properties(struct pw_context *context);
+
+/** Update the context properties */
+int pw_context_update_properties(struct pw_context *context, const struct spa_dict *dict);
+
+/** Get a config section for this context. Since 0.3.22, deprecated,
+ * use pw_context_conf_section_for_each(). */
+const char *pw_context_get_conf_section(struct pw_context *context, const char *section);
+/** Parse a standard config section for this context. Since 0.3.22 */
+int pw_context_parse_conf_section(struct pw_context *context,
+ struct pw_properties *conf, const char *section);
+
+/** update properties from a section into props. Since 0.3.45 */
+int pw_context_conf_update_props(struct pw_context *context, const char *section,
+ struct pw_properties *props);
+/** emit callback for all config sections. Since 0.3.45 */
+int pw_context_conf_section_for_each(struct pw_context *context, const char *section,
+ int (*callback) (void *data, const char *location, const char *section,
+ const char *str, size_t len),
+ void *data);
+/** emit callback for all matched properties. Since 0.3.46 */
+int pw_context_conf_section_match_rules(struct pw_context *context, const char *section,
+ const struct spa_dict *props,
+ int (*callback) (void *data, const char *location, const char *action,
+ const char *str, size_t len),
+ void *data);
+
+/** Get the context support objects */
+const struct spa_support *pw_context_get_support(struct pw_context *context, uint32_t *n_support);
+
+/** get the context main loop */
+struct pw_loop *pw_context_get_main_loop(struct pw_context *context);
+
+/** get the context data loop. Since 0.3.56 */
+struct pw_data_loop *pw_context_get_data_loop(struct pw_context *context);
+
+/** Get the work queue from the context: Since 0.3.26 */
+struct pw_work_queue *pw_context_get_work_queue(struct pw_context *context);
+
+/** Iterate the globals of the context. The callback should return
+ * 0 to fetch the next item, any other value stops the iteration and returns
+ * the value. When all callbacks return 0, this function returns 0 when all
+ * globals are iterated. */
+int pw_context_for_each_global(struct pw_context *context,
+ int (*callback) (void *data, struct pw_global *global),
+ void *data);
+
+/** Find a context global by id */
+struct pw_global *pw_context_find_global(struct pw_context *context, /**< the context */
+ uint32_t id /**< the global id */);
+
+/** add a spa library for the given factory_name regex */
+int pw_context_add_spa_lib(struct pw_context *context, const char *factory_regex, const char *lib);
+
+/** find the library name for a spa factory */
+const char * pw_context_find_spa_lib(struct pw_context *context, const char *factory_name);
+
+struct spa_handle *pw_context_load_spa_handle(struct pw_context *context,
+ const char *factory_name,
+ const struct spa_dict *info);
+
+
+/** data for registering export functions */
+struct pw_export_type {
+ struct spa_list link;
+ const char *type;
+ struct pw_proxy * (*func) (struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size);
+};
+
+/** register a type that can be exported on a context_proxy. This is usually used by
+ * extension modules */
+int pw_context_register_export_type(struct pw_context *context, struct pw_export_type *type);
+/** find information about registered export type */
+const struct pw_export_type *pw_context_find_export_type(struct pw_context *context, const char *type);
+
+/** add an object to the context */
+int pw_context_set_object(struct pw_context *context, const char *type, void *value);
+/** get an object from the context */
+void *pw_context_get_object(struct pw_context *context, const char *type);
+
+/**
+ * \}
+ */
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CONTEXT_H */
diff --git a/src/pipewire/control.c b/src/pipewire/control.c
new file mode 100644
index 0000000..0e3afd7
--- /dev/null
+++ b/src/pipewire/control.c
@@ -0,0 +1,267 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/control.h>
+#include <pipewire/private.h>
+
+#define NAME "control"
+
+struct impl {
+ struct pw_control this;
+
+ struct pw_memblock *mem;
+};
+
+struct pw_control *
+pw_control_new(struct pw_context *context,
+ struct pw_impl_port *port,
+ uint32_t id, uint32_t size,
+ size_t user_data_size)
+{
+ struct impl *impl;
+ struct pw_control *this;
+ enum spa_direction direction;
+
+ switch (id) {
+ case SPA_IO_Control:
+ direction = SPA_DIRECTION_INPUT;
+ break;
+ case SPA_IO_Notify:
+ direction = SPA_DIRECTION_OUTPUT;
+ break;
+ default:
+ errno = ENOTSUP;
+ goto error_exit;
+ }
+
+ impl = calloc(1, sizeof(struct impl) + user_data_size);
+ if (impl == NULL)
+ goto error_exit;
+
+ this = &impl->this;
+ this->id = id;
+ this->size = size;
+
+ pw_log_debug(NAME" %p: new %s %d", this,
+ spa_debug_type_find_name(spa_type_io, this->id), direction);
+
+ this->context = context;
+ this->port = port;
+ this->direction = direction;
+
+ spa_list_init(&this->links);
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ spa_hook_list_init(&this->listener_list);
+
+ spa_list_append(&context->control_list[direction], &this->link);
+ if (port) {
+ spa_list_append(&port->control_list[direction], &this->port_link);
+ pw_impl_port_emit_control_added(port, this);
+ }
+ return this;
+
+error_exit:
+ return NULL;
+}
+
+void pw_control_destroy(struct pw_control *control)
+{
+ struct impl *impl = SPA_CONTAINER_OF(control, struct impl, this);
+ struct pw_control_link *link;
+
+ pw_log_debug(NAME" %p: destroy", control);
+
+ pw_control_emit_destroy(control);
+
+ if (control->direction == SPA_DIRECTION_OUTPUT) {
+ spa_list_consume(link, &control->links, out_link)
+ pw_control_remove_link(link);
+ }
+ else {
+ spa_list_consume(link, &control->links, in_link)
+ pw_control_remove_link(link);
+ }
+
+ spa_list_remove(&control->link);
+
+ if (control->port) {
+ spa_list_remove(&control->port_link);
+ pw_impl_port_emit_control_removed(control->port, control);
+ }
+
+ pw_log_debug(NAME" %p: free", control);
+ pw_control_emit_free(control);
+
+ spa_hook_list_clean(&control->listener_list);
+
+ if (control->direction == SPA_DIRECTION_OUTPUT) {
+ if (impl->mem)
+ pw_memblock_unref(impl->mem);
+ }
+ free(control);
+}
+
+SPA_EXPORT
+struct pw_impl_port *pw_control_get_port(struct pw_control *control)
+{
+ return control->port;
+}
+
+SPA_EXPORT
+void pw_control_add_listener(struct pw_control *control,
+ struct spa_hook *listener,
+ const struct pw_control_events *events,
+ void *data)
+{
+ spa_hook_list_append(&control->listener_list, listener, events, data);
+}
+
+static int port_set_io(struct pw_impl_port *port, uint32_t mix, uint32_t id, void *data, uint32_t size)
+{
+ int res;
+
+ if (port->mix) {
+ res = spa_node_port_set_io(port->mix, port->direction, mix, id, data, size);
+ if (SPA_RESULT_IS_OK(res))
+ return res;
+ }
+
+ if ((res = spa_node_port_set_io(port->node->node,
+ port->direction, port->port_id,
+ id, data, size)) < 0) {
+ pw_log_warn("port %p: set io failed %d %s", port,
+ res, spa_strerror(res));
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_control_add_link(struct pw_control *control, uint32_t cmix,
+ struct pw_control *other, uint32_t omix,
+ struct pw_control_link *link)
+{
+ int res = 0;
+ struct impl *impl;
+ uint32_t size;
+
+ if (control->direction == SPA_DIRECTION_INPUT) {
+ SPA_SWAP(control, other);
+ SPA_SWAP(cmix, omix);
+ }
+ if (control->direction != SPA_DIRECTION_OUTPUT ||
+ other->direction != SPA_DIRECTION_INPUT)
+ return -EINVAL;
+
+ impl = SPA_CONTAINER_OF(control, struct impl, this);
+
+ pw_log_debug(NAME" %p: link to %p %s", control, other,
+ spa_debug_type_find_name(spa_type_io, control->id));
+
+ size = SPA_MAX(control->size, other->size);
+
+ if (impl->mem == NULL) {
+ impl->mem = pw_mempool_alloc(control->context->pool,
+ PW_MEMBLOCK_FLAG_READWRITE |
+ PW_MEMBLOCK_FLAG_SEAL |
+ PW_MEMBLOCK_FLAG_MAP,
+ SPA_DATA_MemFd, size);
+ if (impl->mem == NULL) {
+ res = -errno;
+ goto exit;
+ }
+ }
+
+ if (spa_list_is_empty(&control->links)) {
+ if (control->port) {
+ if ((res = port_set_io(control->port, cmix,
+ control->id,
+ impl->mem->map->ptr, size)) < 0) {
+ pw_log_warn(NAME" %p: set io failed %d %s", control,
+ res, spa_strerror(res));
+ goto exit;
+ }
+ }
+ }
+
+ if (other->port) {
+ if ((res = port_set_io(other->port, omix,
+ other->id, impl->mem->map->ptr, size)) < 0) {
+ pw_log_warn(NAME" %p: set io failed %d %s", control,
+ res, spa_strerror(res));
+ goto exit;
+ }
+ }
+
+ link->output = control;
+ link->input = other;
+ link->out_port = cmix;
+ link->in_port = omix;
+ link->valid = true;
+ spa_list_append(&control->links, &link->out_link);
+ spa_list_append(&other->links, &link->in_link);
+
+ pw_control_emit_linked(control, other);
+ pw_control_emit_linked(other, control);
+exit:
+ return res;
+}
+
+SPA_EXPORT
+int pw_control_remove_link(struct pw_control_link *link)
+{
+ int res = 0;
+ struct pw_control *output = link->output;
+ struct pw_control *input = link->input;
+
+ pw_log_debug(NAME" %p: unlink from %p", output, input);
+
+ spa_list_remove(&link->in_link);
+ spa_list_remove(&link->out_link);
+ link->valid = false;
+
+ if (spa_list_is_empty(&output->links)) {
+ if ((res = port_set_io(output->port, link->out_port,
+ output->id, NULL, 0)) < 0) {
+ pw_log_warn(NAME" %p: can't unset port control io", output);
+ }
+ }
+
+ if (input->port) {
+ if ((res = port_set_io(input->port, link->in_port,
+ input->id, NULL, 0)) < 0) {
+ pw_log_warn(NAME" %p: can't unset port control io", output);
+ }
+ }
+
+ pw_control_emit_unlinked(output, input);
+ pw_control_emit_unlinked(input, output);
+
+ return res;
+}
diff --git a/src/pipewire/control.h b/src/pipewire/control.h
new file mode 100644
index 0000000..92d89a1
--- /dev/null
+++ b/src/pipewire/control.h
@@ -0,0 +1,82 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CONTROL_H
+#define PIPEWIRE_CONTROL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+
+/** \defgroup pw_control Control
+ *
+ * \brief A control can be used to control a port property.
+ */
+
+/**
+ * \addtogroup pw_control
+ * \{
+ */
+struct pw_control;
+
+#include <pipewire/impl.h>
+
+/** Port events, use \ref pw_control_add_listener */
+struct pw_control_events {
+#define PW_VERSION_CONTROL_EVENTS 0
+ uint32_t version;
+
+ /** The control is destroyed */
+ void (*destroy) (void *data);
+
+ /** The control is freed */
+ void (*free) (void *data);
+
+ /** control is linked to another control */
+ void (*linked) (void *data, struct pw_control *other);
+ /** control is unlinked from another control */
+ void (*unlinked) (void *data, struct pw_control *other);
+
+};
+
+/** Get the control parent port or NULL when not set */
+struct pw_impl_port *pw_control_get_port(struct pw_control *control);
+
+/** Add an event listener on the control */
+void pw_control_add_listener(struct pw_control *control,
+ struct spa_hook *listener,
+ const struct pw_control_events *events,
+ void *data);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CONTROL_H */
diff --git a/src/pipewire/core.c b/src/pipewire/core.c
new file mode 100644
index 0000000..dd7e05e
--- /dev/null
+++ b/src/pipewire/core.c
@@ -0,0 +1,495 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <errno.h>
+#include <sys/mman.h>
+
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+
+#include "pipewire/pipewire.h"
+#include "pipewire/private.h"
+
+#include "pipewire/extensions/protocol-native.h"
+
+PW_LOG_TOPIC_EXTERN(log_core);
+#define PW_LOG_TOPIC_DEFAULT log_core
+
+static void core_event_ping(void *data, uint32_t id, int seq)
+{
+ struct pw_core *this = data;
+ pw_log_debug("%p: object %u ping %u", this, id, seq);
+ pw_core_pong(this->core, id, seq);
+}
+
+static void core_event_done(void *data, uint32_t id, int seq)
+{
+ struct pw_core *this = data;
+ struct pw_proxy *proxy;
+
+ pw_log_trace("%p: object %u done %d", this, id, seq);
+
+ proxy = pw_map_lookup(&this->objects, id);
+ if (proxy)
+ pw_proxy_emit_done(proxy, seq);
+}
+
+static void core_event_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct pw_core *this = data;
+ struct pw_proxy *proxy;
+
+ proxy = pw_map_lookup(&this->objects, id);
+
+ pw_log_debug("%p: proxy %p id:%u: bound:%d seq:%d res:%d (%s) msg:\"%s\"",
+ this, proxy, id, proxy ? proxy->bound_id : SPA_ID_INVALID,
+ seq, res, spa_strerror(res), message);
+ if (proxy)
+ pw_proxy_emit_error(proxy, seq, res, message);
+}
+
+static void core_event_remove_id(void *data, uint32_t id)
+{
+ struct pw_core *this = data;
+ struct pw_proxy *proxy;
+
+ pw_log_debug("%p: object remove %u", this, id);
+ if ((proxy = pw_map_lookup(&this->objects, id)) != NULL)
+ pw_proxy_remove(proxy);
+}
+
+static void core_event_bound_id(void *data, uint32_t id, uint32_t global_id)
+{
+ struct pw_core *this = data;
+ struct pw_proxy *proxy;
+
+ pw_log_debug("%p: proxy id %u bound %u", this, id, global_id);
+ if ((proxy = pw_map_lookup(&this->objects, id)) != NULL) {
+ pw_proxy_set_bound_id(proxy, global_id);
+ }
+}
+
+static void core_event_add_mem(void *data, uint32_t id, uint32_t type, int fd, uint32_t flags)
+{
+ struct pw_core *this = data;
+ struct pw_memblock *m;
+
+ pw_log_debug("%p: add mem %u type:%u fd:%d flags:%u", this, id, type, fd, flags);
+
+ m = pw_mempool_import(this->pool, flags, type, fd);
+ if (m->id != id) {
+ pw_log_error("%p: invalid mem id %u, fd:%d expected %u",
+ this, id, fd, m->id);
+ pw_proxy_errorf(&this->proxy, -EINVAL, "invalid mem id %u, expected %u", id, m->id);
+ pw_memblock_unref(m);
+ }
+}
+
+static void core_event_remove_mem(void *data, uint32_t id)
+{
+ struct pw_core *this = data;
+ pw_log_debug("%p: remove mem %u", this, id);
+ pw_mempool_remove_id(this->pool, id);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = core_event_error,
+ .ping = core_event_ping,
+ .done = core_event_done,
+ .remove_id = core_event_remove_id,
+ .bound_id = core_event_bound_id,
+ .add_mem = core_event_add_mem,
+ .remove_mem = core_event_remove_mem,
+};
+
+SPA_EXPORT
+struct pw_context *pw_core_get_context(struct pw_core *core)
+{
+ return core->context;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_core_get_properties(struct pw_core *core)
+{
+ return core->properties;
+}
+
+SPA_EXPORT
+int pw_core_update_properties(struct pw_core *core, const struct spa_dict *dict)
+{
+ int changed;
+
+ changed = pw_properties_update(core->properties, dict);
+
+ pw_log_debug("%p: updated %d properties", core, changed);
+
+ if (!changed)
+ return 0;
+
+ if (core->client)
+ pw_client_update_properties(core->client, &core->properties->dict);
+
+ return changed;
+}
+
+SPA_EXPORT
+void *pw_core_get_user_data(struct pw_core *core)
+{
+ return core->user_data;
+}
+
+static int remove_proxy(void *object, void *data)
+{
+ struct pw_core *core = data;
+ struct pw_proxy *p = object;
+
+ if (object == NULL)
+ return 0;
+
+ if (object != core)
+ pw_proxy_remove(p);
+
+ return 0;
+}
+
+static int destroy_proxy(void *object, void *data)
+{
+ struct pw_core *core = data;
+ struct pw_proxy *p = object;
+
+ if (object == NULL)
+ return 0;
+
+ if (object != core) {
+ pw_log_warn("%p: leaked proxy %p id:%d", core, p, p->id);
+ p->core = NULL;
+ }
+ return 0;
+}
+
+static void proxy_core_removed(void *data)
+{
+ struct pw_core *core = data;
+ struct pw_stream *stream, *s2;
+ struct pw_filter *filter, *f2;
+
+ if (core->removed)
+ return;
+
+ core->removed = true;
+
+ pw_log_debug("%p: core proxy removed", core);
+ spa_list_remove(&core->link);
+
+ spa_list_for_each_safe(stream, s2, &core->stream_list, link)
+ pw_stream_disconnect(stream);
+ spa_list_for_each_safe(filter, f2, &core->filter_list, link)
+ pw_filter_disconnect(filter);
+
+ pw_map_for_each(&core->objects, remove_proxy, core);
+}
+
+static void proxy_core_destroy(void *data)
+{
+ struct pw_core *core = data;
+ struct pw_stream *stream;
+ struct pw_filter *filter;
+
+ if (core->destroyed)
+ return;
+
+ core->destroyed = true;
+
+ pw_log_debug("%p: core proxy destroy", core);
+
+ spa_list_consume(stream, &core->stream_list, link)
+ pw_stream_destroy(stream);
+ spa_list_consume(filter, &core->filter_list, link)
+ pw_filter_destroy(filter);
+
+ pw_proxy_destroy((struct pw_proxy*)core->client);
+
+ pw_map_for_each(&core->objects, destroy_proxy, core);
+ pw_map_reset(&core->objects);
+
+ pw_protocol_client_disconnect(core->conn);
+
+ pw_mempool_destroy(core->pool);
+
+ pw_protocol_client_destroy(core->conn);
+
+ pw_map_clear(&core->objects);
+
+ pw_log_debug("%p: free", core);
+ pw_properties_free(core->properties);
+
+ spa_hook_remove(&core->core_listener);
+ spa_hook_remove(&core->proxy_core_listener);
+}
+
+static const struct pw_proxy_events proxy_core_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = proxy_core_removed,
+ .destroy = proxy_core_destroy,
+};
+
+SPA_EXPORT
+struct pw_client * pw_core_get_client(struct pw_core *core)
+{
+ return core->client;
+}
+
+SPA_EXPORT
+struct pw_proxy *pw_core_find_proxy(struct pw_core *core, uint32_t id)
+{
+ return pw_map_lookup(&core->objects, id);
+}
+
+SPA_EXPORT
+struct pw_proxy *pw_core_export(struct pw_core *core,
+ const char *type, const struct spa_dict *props, void *object,
+ size_t user_data_size)
+{
+ struct pw_proxy *proxy;
+ const struct pw_export_type *t;
+ int res;
+
+ t = pw_context_find_export_type(core->context, type);
+ if (t == NULL) {
+ res = -EPROTO;
+ goto error_export_type;
+ }
+
+ proxy = t->func(core, t->type, props, object, user_data_size);
+ if (proxy == NULL) {
+ res = -errno;
+ goto error_proxy_failed;
+ }
+ pw_log_debug("%p: export:%s proxy:%p", core, type, proxy);
+ return proxy;
+
+error_export_type:
+ pw_log_error("%p: can't export type %s: %s", core, type, spa_strerror(res));
+ goto exit;
+error_proxy_failed:
+ pw_log_error("%p: failed to create proxy: %s", core, spa_strerror(res));
+ goto exit;
+exit:
+ errno = -res;
+ return NULL;
+}
+
+static struct pw_core *core_new(struct pw_context *context,
+ struct pw_properties *properties, size_t user_data_size)
+{
+ struct pw_core *p;
+ struct pw_protocol *protocol;
+ const char *protocol_name;
+ int res;
+
+ p = calloc(1, sizeof(struct pw_core) + user_data_size);
+ if (p == NULL) {
+ res = -errno;
+ goto exit_cleanup;
+ }
+ pw_log_debug("%p: new", p);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ goto error_properties;
+
+ pw_properties_add(properties, &context->properties->dict);
+
+ p->proxy.core = p;
+ p->context = context;
+ p->properties = properties;
+ p->pool = pw_mempool_new(NULL);
+ p->core = p;
+ if (user_data_size > 0)
+ p->user_data = SPA_PTROFF(p, sizeof(struct pw_core), void);
+ p->proxy.user_data = p->user_data;
+
+ pw_map_init(&p->objects, 64, 32);
+ spa_list_init(&p->stream_list);
+ spa_list_init(&p->filter_list);
+
+ if ((protocol_name = pw_properties_get(properties, PW_KEY_PROTOCOL)) == NULL &&
+ (protocol_name = pw_properties_get(context->properties, PW_KEY_PROTOCOL)) == NULL)
+ protocol_name = PW_TYPE_INFO_PROTOCOL_Native;
+
+ protocol = pw_context_find_protocol(context, protocol_name);
+ if (protocol == NULL) {
+ res = -ENOTSUP;
+ goto error_protocol;
+ }
+
+ p->conn = pw_protocol_new_client(protocol, p, &properties->dict);
+ if (p->conn == NULL)
+ goto error_connection;
+
+ if ((res = pw_proxy_init(&p->proxy, PW_TYPE_INTERFACE_Core, PW_VERSION_CORE)) < 0)
+ goto error_proxy;
+
+ p->client = (struct pw_client*)pw_proxy_new(&p->proxy,
+ PW_TYPE_INTERFACE_Client, PW_VERSION_CLIENT, 0);
+ if (p->client == NULL) {
+ res = -errno;
+ goto error_proxy;
+ }
+
+ pw_core_add_listener(p, &p->core_listener, &core_events, p);
+ pw_proxy_add_listener(&p->proxy, &p->proxy_core_listener, &proxy_core_events, p);
+
+ pw_core_hello(p, PW_VERSION_CORE);
+ pw_client_update_properties(p->client, &p->properties->dict);
+
+ spa_list_append(&context->core_list, &p->link);
+
+ return p;
+
+error_properties:
+ res = -errno;
+ pw_log_error("%p: can't create properties: %m", p);
+ goto exit_free;
+error_protocol:
+ pw_log_error("%p: can't find protocol '%s': %s", p, protocol_name, spa_strerror(res));
+ goto exit_free;
+error_connection:
+ res = -errno;
+ pw_log_error("%p: can't create new native protocol connection: %m", p);
+ goto exit_free;
+error_proxy:
+ pw_log_error("%p: can't initialize proxy: %s", p, spa_strerror(res));
+ goto exit_free;
+
+exit_free:
+ free(p);
+exit_cleanup:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_core *
+pw_context_connect(struct pw_context *context, struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_core *core;
+ int res;
+
+ core = core_new(context, properties, user_data_size);
+ if (core == NULL)
+ return NULL;
+
+ pw_log_debug("%p: connect", core);
+
+ if ((res = pw_protocol_client_connect(core->conn,
+ &core->properties->dict,
+ NULL, NULL)) < 0)
+ goto error_free;
+
+ return core;
+
+error_free:
+ pw_core_disconnect(core);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_core *
+pw_context_connect_fd(struct pw_context *context, int fd, struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_core *core;
+ int res;
+
+ core = core_new(context, properties, user_data_size);
+ if (core == NULL)
+ return NULL;
+
+ pw_log_debug("%p: connect fd:%d", core, fd);
+
+ if ((res = pw_protocol_client_connect_fd(core->conn, fd, true)) < 0)
+ goto error_free;
+
+ return core;
+
+error_free:
+ pw_core_disconnect(core);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_core *
+pw_context_connect_self(struct pw_context *context, struct pw_properties *properties,
+ size_t user_data_size)
+{
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ pw_properties_set(properties, PW_KEY_REMOTE_NAME, "internal");
+
+ return pw_context_connect(context, properties, user_data_size);
+}
+
+SPA_EXPORT
+int pw_core_steal_fd(struct pw_core *core)
+{
+ int fd = pw_protocol_client_steal_fd(core->conn);
+ pw_log_debug("%p: fd:%d", core, fd);
+ return fd;
+}
+
+SPA_EXPORT
+int pw_core_set_paused(struct pw_core *core, bool paused)
+{
+ pw_log_debug("%p: state:%s", core, paused ? "pause" : "resume");
+ return pw_protocol_client_set_paused(core->conn, paused);
+}
+
+SPA_EXPORT
+struct pw_mempool * pw_core_get_mempool(struct pw_core *core)
+{
+ return core->pool;
+}
+
+SPA_EXPORT
+int pw_core_disconnect(struct pw_core *core)
+{
+ pw_log_debug("%p: disconnect", core);
+ pw_proxy_remove(&core->proxy);
+ pw_proxy_destroy(&core->proxy);
+ return 0;
+}
diff --git a/src/pipewire/core.h b/src/pipewire/core.h
new file mode 100644
index 0000000..604e7cc
--- /dev/null
+++ b/src/pipewire/core.h
@@ -0,0 +1,629 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_CORE_H
+#define PIPEWIRE_CORE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+#include <errno.h>
+
+#include <spa/utils/hook.h>
+
+/** \defgroup pw_core Core
+ *
+ * \brief The core global object.
+ *
+ * This is a special singleton object. It is used for internal PipeWire
+ * protocol features. Connecting to a PipeWire instance returns one core
+ * object, the caller should then register event listeners
+ * using \ref pw_core_add_listener.
+ *
+ * Updates to the core object are then provided through the \ref
+ * pw_core_events interface. See \ref page_tutorial2 for an example.
+ */
+
+/**
+ * \addtogroup pw_core
+ * \{
+ */
+#define PW_TYPE_INTERFACE_Core PW_TYPE_INFO_INTERFACE_BASE "Core"
+#define PW_TYPE_INTERFACE_Registry PW_TYPE_INFO_INTERFACE_BASE "Registry"
+
+#define PW_VERSION_CORE 3
+struct pw_core;
+#define PW_VERSION_REGISTRY 3
+struct pw_registry;
+
+/** The default remote name to connect to */
+#define PW_DEFAULT_REMOTE "pipewire-0"
+
+/** default ID for the core object after connect */
+#define PW_ID_CORE 0
+
+/* invalid ID that matches any object when used for permissions */
+#define PW_ID_ANY (uint32_t)(0xffffffff)
+
+/** The core information. Extra information may be added in later versions,
+ * clients must not assume a constant struct size */
+struct pw_core_info {
+ uint32_t id; /**< id of the global */
+ uint32_t cookie; /**< a random cookie for identifying this instance of PipeWire */
+ const char *user_name; /**< name of the user that started the core */
+ const char *host_name; /**< name of the machine the core is running on */
+ const char *version; /**< version of the core */
+ const char *name; /**< name of the core */
+#define PW_CORE_CHANGE_MASK_PROPS (1 << 0)
+#define PW_CORE_CHANGE_MASK_ALL ((1 << 1)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ struct spa_dict *props; /**< extra properties */
+};
+
+#include <pipewire/context.h>
+#include <pipewire/properties.h>
+#include <pipewire/proxy.h>
+
+/** Update an existing \ref pw_core_info with \a update with reset */
+struct pw_core_info *
+pw_core_info_update(struct pw_core_info *info,
+ const struct pw_core_info *update);
+/** Update an existing \ref pw_core_info with \a update */
+struct pw_core_info *
+pw_core_info_merge(struct pw_core_info *info,
+ const struct pw_core_info *update, bool reset);
+/** Free a \ref pw_core_info */
+void pw_core_info_free(struct pw_core_info *info);
+
+/** Core */
+
+#define PW_CORE_EVENT_INFO 0
+#define PW_CORE_EVENT_DONE 1
+#define PW_CORE_EVENT_PING 2
+#define PW_CORE_EVENT_ERROR 3
+#define PW_CORE_EVENT_REMOVE_ID 4
+#define PW_CORE_EVENT_BOUND_ID 5
+#define PW_CORE_EVENT_ADD_MEM 6
+#define PW_CORE_EVENT_REMOVE_MEM 7
+#define PW_CORE_EVENT_NUM 8
+
+/** \struct pw_core_events
+ * \brief Core events
+ */
+struct pw_core_events {
+#define PW_VERSION_CORE_EVENTS 0
+ uint32_t version;
+
+ /**
+ * Notify new core info
+ *
+ * This event is emitted when first bound to the core or when the
+ * hello method is called.
+ *
+ * \param info new core info
+ */
+ void (*info) (void *data, const struct pw_core_info *info);
+ /**
+ * Emit a done event
+ *
+ * The done event is emitted as a result of a sync method with the
+ * same seq number.
+ *
+ * \param seq the seq number passed to the sync method call
+ */
+ void (*done) (void *data, uint32_t id, int seq);
+
+ /** Emit a ping event
+ *
+ * The client should reply with a pong reply with the same seq
+ * number.
+ */
+ void (*ping) (void *data, uint32_t id, int seq);
+
+ /**
+ * Fatal error event
+ *
+ * The error event is sent out when a fatal (non-recoverable)
+ * error has occurred. The id argument is the proxy object where
+ * the error occurred, most often in response to a request to that
+ * object. The message is a brief description of the error,
+ * for (debugging) convenience.
+ *
+ * This event is usually also emitted on the proxy object with
+ * \a id.
+ *
+ * \param id object where the error occurred
+ * \param seq the sequence number that generated the error
+ * \param res error code
+ * \param message error description
+ */
+ void (*error) (void *data, uint32_t id, int seq, int res, const char *message);
+ /**
+ * Remove an object ID
+ *
+ * This event is used internally by the object ID management
+ * logic. When a client deletes an object, the server will send
+ * this event to acknowledge that it has seen the delete request.
+ * When the client receives this event, it will know that it can
+ * safely reuse the object ID.
+ *
+ * \param id deleted object ID
+ */
+ void (*remove_id) (void *data, uint32_t id);
+
+ /**
+ * Notify an object binding
+ *
+ * This event is emitted when a local object ID is bound to a
+ * global ID. It is emitted before the global becomes visible in the
+ * registry.
+ *
+ * \param id bound object ID
+ * \param global_id the global id bound to
+ */
+ void (*bound_id) (void *data, uint32_t id, uint32_t global_id);
+
+ /**
+ * Add memory for a client
+ *
+ * Memory is given to a client as \a fd of a certain
+ * memory \a type.
+ *
+ * Further references to this fd will be made with the per memory
+ * unique identifier \a id.
+ *
+ * \param id the unique id of the memory
+ * \param type the memory type, one of enum spa_data_type
+ * \param fd the file descriptor
+ * \param flags extra flags
+ */
+ void (*add_mem) (void *data, uint32_t id, uint32_t type, int fd, uint32_t flags);
+
+ /**
+ * Remove memory for a client
+ *
+ * \param id the memory id to remove
+ */
+ void (*remove_mem) (void *data, uint32_t id);
+};
+
+#define PW_CORE_METHOD_ADD_LISTENER 0
+#define PW_CORE_METHOD_HELLO 1
+#define PW_CORE_METHOD_SYNC 2
+#define PW_CORE_METHOD_PONG 3
+#define PW_CORE_METHOD_ERROR 4
+#define PW_CORE_METHOD_GET_REGISTRY 5
+#define PW_CORE_METHOD_CREATE_OBJECT 6
+#define PW_CORE_METHOD_DESTROY 7
+#define PW_CORE_METHOD_NUM 8
+
+/**
+ * \struct pw_core_methods
+ * \brief Core methods
+ *
+ * The core global object. This is a singleton object used for
+ * creating new objects in the remote PipeWire instance. It is
+ * also used for internal features.
+ */
+struct pw_core_methods {
+#define PW_VERSION_CORE_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_core_events *events,
+ void *data);
+ /**
+ * Start a conversation with the server. This will send
+ * the core info and will destroy all resources for the client
+ * (except the core and client resource).
+ */
+ int (*hello) (void *object, uint32_t version);
+ /**
+ * Do server roundtrip
+ *
+ * Ask the server to emit the 'done' event with \a seq.
+ *
+ * Since methods are handled in-order and events are delivered
+ * in-order, this can be used as a barrier to ensure all previous
+ * methods and the resulting events have been handled.
+ *
+ * \param seq the seq number passed to the done event
+ */
+ int (*sync) (void *object, uint32_t id, int seq);
+ /**
+ * Reply to a server ping event.
+ *
+ * Reply to the server ping event with the same seq.
+ *
+ * \param seq the seq number received in the ping event
+ */
+ int (*pong) (void *object, uint32_t id, int seq);
+ /**
+ * Fatal error event
+ *
+ * The error method is sent out when a fatal (non-recoverable)
+ * error has occurred. The id argument is the proxy object where
+ * the error occurred, most often in response to an event on that
+ * object. The message is a brief description of the error,
+ * for (debugging) convenience.
+ *
+ * This method is usually also emitted on the resource object with
+ * \a id.
+ *
+ * \param id object where the error occurred
+ * \param res error code
+ * \param message error description
+ */
+ int (*error) (void *object, uint32_t id, int seq, int res, const char *message);
+ /**
+ * Get the registry object
+ *
+ * Create a registry object that allows the client to list and bind
+ * the global objects available from the PipeWire server
+ * \param version the client version
+ * \param user_data_size extra size
+ */
+ struct pw_registry * (*get_registry) (void *object, uint32_t version,
+ size_t user_data_size);
+
+ /**
+ * Create a new object on the PipeWire server from a factory.
+ *
+ * \param factory_name the factory name to use
+ * \param type the interface to bind to
+ * \param version the version of the interface
+ * \param props extra properties
+ * \param user_data_size extra size
+ */
+ void * (*create_object) (void *object,
+ const char *factory_name,
+ const char *type,
+ uint32_t version,
+ const struct spa_dict *props,
+ size_t user_data_size);
+ /**
+ * Destroy an resource
+ *
+ * Destroy the server resource for the given proxy.
+ *
+ * \param obj the proxy to destroy
+ */
+ 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
+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)
+{
+ char buffer[1024];
+ vsnprintf(buffer, sizeof(buffer), message, args);
+ buffer[1023] = '\0';
+ return pw_core_error(core, id, seq, res, buffer);
+}
+
+static inline
+SPA_PRINTF_FUNC(5, 6) int
+pw_core_errorf(struct pw_core *core, uint32_t id, int seq,
+ int res, const char *message, ...)
+{
+ va_list args;
+ int r;
+ va_start(args, message);
+ r = pw_core_errorv(core, id, seq, res, message, args);
+ va_end(args);
+ return r;
+}
+
+static inline 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;
+}
+
+static inline void *
+pw_core_create_object(struct pw_core *core,
+ const char *factory_name,
+ const char *type,
+ uint32_t version,
+ 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;
+}
+
+#define pw_core_destroy(c,...) pw_core_method(c,destroy,0,__VA_ARGS__)
+
+/**
+ * \}
+ */
+
+/** \defgroup pw_registry Registry
+ *
+ * The registry object is a singleton object that keeps track of
+ * global objects on the PipeWire instance. See also \ref pw_global.
+ *
+ * Global objects typically represent an actual object in PipeWire
+ * (for example, a module or node) or they are singleton
+ * objects such as the core.
+ *
+ * When a client creates a registry object, the registry object
+ * will emit a global event for each global currently in the
+ * registry. Globals come and go as a result of device hotplugs or
+ * reconfiguration or other events, and the registry will send out
+ * global and global_remove events to keep the client up to date
+ * with the changes. To mark the end of the initial burst of
+ * events, the client can use the pw_core.sync methosd immediately
+ * after calling pw_core.get_registry.
+ *
+ * A client can bind to a global object by using the bind
+ * request. This creates a client-side proxy that lets the object
+ * emit events to the client and lets the client invoke methods on
+ * the object. See \ref page_proxy
+ *
+ * Clients can also change the permissions of the global objects that
+ * it can see. This is interesting when you want to configure a
+ * pipewire session before handing it to another application. You
+ * can, for example, hide certain existing or new objects or limit
+ * the access permissions on an object.
+ */
+
+/**
+ * \addtogroup pw_registry
+ * \{
+ */
+
+#define PW_REGISTRY_EVENT_GLOBAL 0
+#define PW_REGISTRY_EVENT_GLOBAL_REMOVE 1
+#define PW_REGISTRY_EVENT_NUM 2
+
+/** Registry events */
+struct pw_registry_events {
+#define PW_VERSION_REGISTRY_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify of a new global object
+ *
+ * The registry emits this event when a new global object is
+ * available.
+ *
+ * \param id the global object id
+ * \param permissions the permissions of the object
+ * \param type the type of the interface
+ * \param version the version of the interface
+ * \param props extra properties of the global
+ */
+ void (*global) (void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props);
+ /**
+ * Notify of a global object removal
+ *
+ * Emitted when a global object was removed from the registry.
+ * If the client has any bindings to the global, it should destroy
+ * those.
+ *
+ * \param id the id of the global that was removed
+ */
+ void (*global_remove) (void *data, uint32_t id);
+};
+
+#define PW_REGISTRY_METHOD_ADD_LISTENER 0
+#define PW_REGISTRY_METHOD_BIND 1
+#define PW_REGISTRY_METHOD_DESTROY 2
+#define PW_REGISTRY_METHOD_NUM 3
+
+/** Registry methods */
+struct pw_registry_methods {
+#define PW_VERSION_REGISTRY_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_registry_events *events,
+ void *data);
+ /**
+ * Bind to a global object
+ *
+ * Bind to the global object with \a id and use the client proxy
+ * with new_id as the proxy. After this call, methods can be
+ * send to the remote global object and events can be received
+ *
+ * \param id the global id to bind to
+ * \param type the interface type to bind to
+ * \param version the interface version to use
+ * \returns the new object
+ */
+ void * (*bind) (void *object, uint32_t id, const char *type, uint32_t version,
+ size_t use_data_size);
+
+ /**
+ * Attempt to destroy a global object
+ *
+ * Try to destroy the global object.
+ *
+ * \param id the global id to destroy
+ */
+ 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 *
+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;
+}
+
+#define pw_registry_destroy(p,...) pw_registry_method(p,destroy,0,__VA_ARGS__)
+
+/**
+ * \}
+ */
+
+/**
+ * \addtogroup pw_core
+ * \{
+ */
+
+/** Connect to a PipeWire instance
+ *
+ * \param context a \ref pw_context
+ * \param properties optional properties, ownership of the properties is
+ * taken.
+ * \param user_data_size extra user data size
+ *
+ * \return a \ref pw_core on success or NULL with errno set on error. The core
+ * will have an id of \ref PW_ID_CORE (0)
+ */
+struct pw_core *
+pw_context_connect(struct pw_context *context,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+/** Connect to a PipeWire instance on the given socket
+ *
+ * \param context a \ref pw_context
+ * \param fd the connected socket to use, the socket will be closed
+ * automatically on disconnect or error.
+ * \param properties optional properties, ownership of the properties is
+ * taken.
+ * \param user_data_size extra user data size
+ *
+ * \return a \ref pw_core on success or NULL with errno set on error */
+struct pw_core *
+pw_context_connect_fd(struct pw_context *context,
+ int fd,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+/** Connect to a given PipeWire instance
+ *
+ * \param context a \ref pw_context to connect to
+ * \param properties optional properties, ownership of the properties is
+ * taken.
+ * \param user_data_size extra user data size
+ *
+ * \return a \ref pw_core on success or NULL with errno set on error */
+struct pw_core *
+pw_context_connect_self(struct pw_context *context,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+/** Steal the fd of the core connection or < 0 on error. The core
+ * will be disconnected after this call. */
+int pw_core_steal_fd(struct pw_core *core);
+
+/** Pause or resume the core. When the core is paused, no new events
+ * will be dispatched until the core is resumed again. */
+int pw_core_set_paused(struct pw_core *core, bool paused);
+
+/** disconnect and destroy a core */
+int pw_core_disconnect(struct pw_core *core);
+
+/** Get the user_data. It is of the size specified when this object was
+ * constructed */
+void *pw_core_get_user_data(struct pw_core *core);
+
+/** Get the client proxy of the connected core. This will have the id
+ * of PW_ID_CLIENT (1) */
+struct pw_client * pw_core_get_client(struct pw_core *core);
+
+/** Get the context object used to created this core */
+struct pw_context * pw_core_get_context(struct pw_core *core);
+
+/** Get properties from the core */
+const struct pw_properties *pw_core_get_properties(struct pw_core *core);
+
+/** Update the core properties. This updates the properties
+ * of the associated client.
+ * \return the number of properties that were updated */
+int pw_core_update_properties(struct pw_core *core, const struct spa_dict *dict);
+
+/** Get the core mempool object */
+struct pw_mempool * pw_core_get_mempool(struct pw_core *core);
+
+/** Get the proxy with the given id */
+struct pw_proxy *pw_core_find_proxy(struct pw_core *core, uint32_t id);
+
+/** Export an object into the PipeWire instance associated with core */
+struct pw_proxy *pw_core_export(struct pw_core *core, /**< the core */
+ const char *type, /**< the type of object */
+ const struct spa_dict *props, /**< extra properties */
+ void *object, /**< object to export */
+ size_t user_data_size /**< extra user data */);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_CORE_H */
diff --git a/src/pipewire/data-loop.c b/src/pipewire/data-loop.c
new file mode 100644
index 0000000..4f4a2bc
--- /dev/null
+++ b/src/pipewire/data-loop.c
@@ -0,0 +1,292 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pthread.h>
+#include <errno.h>
+#include <sys/resource.h>
+
+#include "pipewire/log.h"
+#include "pipewire/data-loop.h"
+#include "pipewire/private.h"
+#include "pipewire/thread.h"
+
+PW_LOG_TOPIC_EXTERN(log_data_loop);
+#define PW_LOG_TOPIC_DEFAULT log_data_loop
+
+SPA_EXPORT
+int pw_data_loop_wait(struct pw_data_loop *this, int timeout)
+{
+ int res;
+
+ while (true) {
+ if (SPA_UNLIKELY(!this->running)) {
+ res = -ECANCELED;
+ break;
+ }
+ if (SPA_UNLIKELY((res = pw_loop_iterate(this->loop, timeout)) < 0)) {
+ if (res == -EINTR)
+ continue;
+ }
+ break;
+ }
+ return res;
+}
+
+SPA_EXPORT
+void pw_data_loop_exit(struct pw_data_loop *this)
+{
+ this->running = false;
+}
+
+static void thread_cleanup(void *arg)
+{
+ struct pw_data_loop *this = arg;
+ pw_log_debug("%p: leave thread", this);
+ this->running = false;
+ pw_loop_leave(this->loop);
+}
+
+static void *do_loop(void *user_data)
+{
+ struct pw_data_loop *this = user_data;
+ int res;
+
+ pw_log_debug("%p: enter thread", this);
+ pw_loop_enter(this->loop);
+
+ pthread_cleanup_push(thread_cleanup, this);
+
+ while (SPA_LIKELY(this->running)) {
+ if (SPA_UNLIKELY((res = pw_loop_iterate(this->loop, -1)) < 0)) {
+ if (res == -EINTR)
+ continue;
+ pw_log_error("%p: iterate error %d (%s)",
+ this, res, spa_strerror(res));
+ }
+ }
+ pthread_cleanup_pop(1);
+
+ return NULL;
+}
+
+static int do_stop(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *data, size_t size, void *user_data)
+{
+ struct pw_data_loop *this = user_data;
+ pw_log_debug("%p: stopping", this);
+ this->running = false;
+ return 0;
+}
+
+static struct pw_data_loop *loop_new(struct pw_loop *loop, const struct spa_dict *props)
+{
+ struct pw_data_loop *this;
+ const char *str;
+ int res;
+
+ this = calloc(1, sizeof(struct pw_data_loop));
+ if (this == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ pw_log_debug("%p: new", this);
+
+ if (loop == NULL) {
+ loop = pw_loop_new(props);
+ this->created = true;
+ }
+ if (loop == NULL) {
+ res = -errno;
+ pw_log_error("%p: can't create loop: %m", this);
+ goto error_free;
+ }
+ this->loop = loop;
+
+ if (props != NULL &&
+ (str = spa_dict_lookup(props, "loop.cancel")) != NULL)
+ this->cancel = pw_properties_parse_bool(str);
+
+ spa_hook_list_init(&this->listener_list);
+
+ return this;
+
+error_free:
+ free(this);
+error_cleanup:
+ errno = -res;
+ return NULL;
+}
+
+/** Create a new \ref pw_data_loop.
+ * \return a newly allocated data loop
+ *
+ */
+SPA_EXPORT
+struct pw_data_loop *pw_data_loop_new(const struct spa_dict *props)
+{
+ return loop_new(NULL, props);
+}
+
+
+/** Destroy a data loop
+ * \param loop the data loop to destroy
+ */
+SPA_EXPORT
+void pw_data_loop_destroy(struct pw_data_loop *loop)
+{
+ pw_log_debug("%p: destroy", loop);
+
+ pw_data_loop_emit_destroy(loop);
+
+ pw_data_loop_stop(loop);
+
+ if (loop->created)
+ pw_loop_destroy(loop->loop);
+
+ spa_hook_list_clean(&loop->listener_list);
+
+ free(loop);
+}
+
+SPA_EXPORT
+void pw_data_loop_add_listener(struct pw_data_loop *loop,
+ struct spa_hook *listener,
+ const struct pw_data_loop_events *events,
+ void *data)
+{
+ spa_hook_list_append(&loop->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+struct pw_loop *
+pw_data_loop_get_loop(struct pw_data_loop *loop)
+{
+ return loop->loop;
+}
+
+/** Start a data loop
+ * \param loop the data loop to start
+ * \return 0 if ok, -1 on error
+ *
+ * This will start the realtime thread that manages the loop.
+ *
+ */
+SPA_EXPORT
+int pw_data_loop_start(struct pw_data_loop *loop)
+{
+ if (!loop->running) {
+ struct spa_thread_utils *utils;
+ struct spa_thread *thr;
+
+ loop->running = true;
+
+ if ((utils = loop->thread_utils) == NULL)
+ utils = pw_thread_utils_get();
+ thr = spa_thread_utils_create(utils, NULL, do_loop, loop);
+ loop->thread = (pthread_t)thr;
+ if (thr == NULL) {
+ pw_log_error("%p: can't create thread: %m", loop);
+ loop->running = false;
+ return -errno;
+ }
+ spa_thread_utils_acquire_rt(utils, thr, -1);
+ }
+ return 0;
+}
+
+/** Stop a data loop
+ * \param loop the data loop to Stop
+ * \return 0
+ *
+ * This will stop and join the realtime thread that manages the loop.
+ *
+ */
+SPA_EXPORT
+int pw_data_loop_stop(struct pw_data_loop *loop)
+{
+ pw_log_debug("%p stopping", loop);
+ if (loop->running) {
+ struct spa_thread_utils *utils;
+ if (loop->cancel) {
+ pw_log_debug("%p cancel", loop);
+ pthread_cancel(loop->thread);
+ } else {
+ pw_log_debug("%p signal", loop);
+ pw_loop_invoke(loop->loop, do_stop, 1, NULL, 0, false, loop);
+ }
+ pw_log_debug("%p join", loop);
+ if ((utils = loop->thread_utils) == NULL)
+ utils = pw_thread_utils_get();
+ spa_thread_utils_join(utils, (struct spa_thread*)loop->thread, NULL);
+ pw_log_debug("%p joined", loop);
+ }
+ pw_log_debug("%p stopped", loop);
+ return 0;
+}
+
+/** Check if we are inside the data loop
+ * \param loop the data loop to check
+ * \return true is the current thread is the data loop thread
+ *
+ */
+SPA_EXPORT
+bool pw_data_loop_in_thread(struct pw_data_loop * loop)
+{
+ return loop->running && pthread_equal(loop->thread, pthread_self());
+}
+
+/** Get the thread object.
+ * \param loop the data loop to get the thread of
+ * \return the thread object or NULL when the thread is not running
+ *
+ * On posix based systems this returns a pthread_t
+ */
+SPA_EXPORT
+struct spa_thread *pw_data_loop_get_thread(struct pw_data_loop * loop)
+{
+ return loop->running ? (struct spa_thread*)loop->thread : NULL;
+}
+
+SPA_EXPORT
+int pw_data_loop_invoke(struct pw_data_loop *loop,
+ spa_invoke_func_t func, uint32_t seq, const void *data, size_t size,
+ bool block, void *user_data)
+{
+ return pw_loop_invoke(loop->loop, func, seq, data, size, block, user_data);
+}
+
+/** Set a thread utils implementation.
+ * \param loop the data loop to set the thread utils on
+ * \param impl the thread utils implementation
+ *
+ * This configures a custom spa_thread_utils implementation for this data
+ * loop. Use NULL to restore the system default implementation.
+ */
+SPA_EXPORT
+void pw_data_loop_set_thread_utils(struct pw_data_loop *loop,
+ struct spa_thread_utils *impl)
+{
+ loop->thread_utils = impl;
+}
diff --git a/src/pipewire/data-loop.h b/src/pipewire/data-loop.h
new file mode 100644
index 0000000..a459c19
--- /dev/null
+++ b/src/pipewire/data-loop.h
@@ -0,0 +1,113 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_DATA_LOOP_H
+#define PIPEWIRE_DATA_LOOP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+#include <spa/support/thread.h>
+
+/** \defgroup pw_data_loop Data Loop
+ *
+ * \brief PipeWire rt-loop object
+ *
+ * This loop starts a new real-time thread that
+ * is designed to run the processing graph.
+ */
+
+/**
+ * \addtogroup pw_data_loop
+ * \{
+ */
+struct pw_data_loop;
+
+#include <pipewire/loop.h>
+#include <pipewire/properties.h>
+
+/** Loop events, use \ref pw_data_loop_add_listener to add a listener */
+struct pw_data_loop_events {
+#define PW_VERSION_DATA_LOOP_EVENTS 0
+ uint32_t version;
+ /** The loop is destroyed */
+ void (*destroy) (void *data);
+};
+
+/** Make a new loop. */
+struct pw_data_loop *
+pw_data_loop_new(const struct spa_dict *props);
+
+/** Add an event listener to loop */
+void pw_data_loop_add_listener(struct pw_data_loop *loop,
+ struct spa_hook *listener,
+ const struct pw_data_loop_events *events,
+ void *data);
+
+/** wait for activity on the loop up to \a timeout milliseconds.
+ * Should be called from the loop function */
+int pw_data_loop_wait(struct pw_data_loop *loop, int timeout);
+
+/** make sure the thread will exit. Can be called from a loop callback */
+void pw_data_loop_exit(struct pw_data_loop *loop);
+
+/** Get the loop implementation of this data loop */
+struct pw_loop *
+pw_data_loop_get_loop(struct pw_data_loop *loop);
+
+/** Destroy the loop */
+void pw_data_loop_destroy(struct pw_data_loop *loop);
+
+/** Start the processing thread */
+int pw_data_loop_start(struct pw_data_loop *loop);
+
+/** Stop the processing thread */
+int pw_data_loop_stop(struct pw_data_loop *loop);
+
+/** Check if the current thread is the processing thread */
+bool pw_data_loop_in_thread(struct pw_data_loop *loop);
+/** Get the thread object */
+struct spa_thread *pw_data_loop_get_thread(struct pw_data_loop *loop);
+
+/** invoke func in the context of the thread or in the caller thread when
+ * the loop is not running. Since 0.3.3 */
+int pw_data_loop_invoke(struct pw_data_loop *loop,
+ spa_invoke_func_t func, uint32_t seq, const void *data, size_t size,
+ bool block, void *user_data);
+
+/** Set a custom spa_thread_utils for this loop. Setting NULL restores the
+ * system default implementation. Since 0.3.50 */
+void pw_data_loop_set_thread_utils(struct pw_data_loop *loop,
+ struct spa_thread_utils *impl);
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_DATA_LOOP_H */
diff --git a/src/pipewire/device.h b/src/pipewire/device.h
new file mode 100644
index 0000000..5bcaf80
--- /dev/null
+++ b/src/pipewire/device.h
@@ -0,0 +1,178 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_DEVICE_H
+#define PIPEWIRE_DEVICE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include <pipewire/proxy.h>
+
+/** \defgroup pw_device Device
+ * Device interface
+ */
+
+/**
+ * \addtogroup pw_device
+ * \{
+ */
+
+#define PW_TYPE_INTERFACE_Device PW_TYPE_INFO_INTERFACE_BASE "Device"
+
+#define PW_VERSION_DEVICE 3
+struct pw_device;
+
+/** The device information. Extra information can be added in later versions */
+struct pw_device_info {
+ uint32_t id; /**< id of the global */
+#define PW_DEVICE_CHANGE_MASK_PROPS (1 << 0)
+#define PW_DEVICE_CHANGE_MASK_PARAMS (1 << 1)
+#define PW_DEVICE_CHANGE_MASK_ALL ((1 << 2)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ struct spa_dict *props; /**< extra properties */
+ struct spa_param_info *params; /**< parameters */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+/** Update and existing \ref pw_device_info with \a update and reset */
+struct pw_device_info *
+pw_device_info_update(struct pw_device_info *info,
+ const struct pw_device_info *update);
+/** Merge and existing \ref pw_device_info with \a update */
+struct pw_device_info *
+pw_device_info_merge(struct pw_device_info *info,
+ const struct pw_device_info *update, bool reset);
+/** Free a \ref pw_device_info */
+void pw_device_info_free(struct pw_device_info *info);
+
+#define PW_DEVICE_EVENT_INFO 0
+#define PW_DEVICE_EVENT_PARAM 1
+#define PW_DEVICE_EVENT_NUM 2
+
+/** Device events */
+struct pw_device_events {
+#define PW_VERSION_DEVICE_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify device info
+ *
+ * \param info info about the device
+ */
+ void (*info) (void *data, const struct pw_device_info *info);
+ /**
+ * Notify a device param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param seq the sequence number of the request
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+
+#define PW_DEVICE_METHOD_ADD_LISTENER 0
+#define PW_DEVICE_METHOD_SUBSCRIBE_PARAMS 1
+#define PW_DEVICE_METHOD_ENUM_PARAMS 2
+#define PW_DEVICE_METHOD_SET_PARAM 3
+#define PW_DEVICE_METHOD_NUM 4
+
+/** Device methods */
+struct pw_device_methods {
+#define PW_VERSION_DEVICE_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_device_events *events,
+ void *data);
+ /**
+ * Subscribe to parameter changes
+ *
+ * Automatically emit param events for the given ids when
+ * they are changed.
+ *
+ * \param ids an array of param ids
+ * \param n_ids the number of ids in \a ids
+ */
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+
+ /**
+ * Enumerate device parameters
+ *
+ * Start enumeration of device parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param seq a sequence number to place in the reply
+ * \param id the parameter id to enum or PW_ID_ANY for all
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ int (*enum_params) (void *object, int seq, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+ /**
+ * Set a parameter on the device
+ *
+ * \param id the parameter id to set
+ * \param flags extra parameter flags
+ * \param param the parameter to set
+ */
+ int (*set_param) (void *object, uint32_t id, uint32_t flags,
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_DEVICE_H */
diff --git a/src/pipewire/extensions/client-node.h b/src/pipewire/extensions/client-node.h
new file mode 100644
index 0000000..15dbae3
--- /dev/null
+++ b/src/pipewire/extensions/client-node.h
@@ -0,0 +1,357 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_CLIENT_NODE_H
+#define PIPEWIRE_EXT_CLIENT_NODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/param/param.h>
+
+/** \defgroup pw_client_node Client Node
+ * Client node interface
+ */
+
+/**
+ * \addtogroup pw_client_node
+ * \{
+ */
+#define PW_TYPE_INTERFACE_ClientNode PW_TYPE_INFO_INTERFACE_BASE "ClientNode"
+
+#define PW_VERSION_CLIENT_NODE 4
+struct pw_client_node;
+
+#define PW_EXTENSION_MODULE_CLIENT_NODE PIPEWIRE_MODULE_PREFIX "module-client-node"
+
+/** information about a buffer */
+struct pw_client_node_buffer {
+ uint32_t mem_id; /**< the memory id for the metadata */
+ uint32_t offset; /**< offset in memory */
+ uint32_t size; /**< size in memory */
+ struct spa_buffer *buffer; /**< buffer describing metadata and buffer memory */
+};
+
+#define PW_CLIENT_NODE_EVENT_TRANSPORT 0
+#define PW_CLIENT_NODE_EVENT_SET_PARAM 1
+#define PW_CLIENT_NODE_EVENT_SET_IO 2
+#define PW_CLIENT_NODE_EVENT_EVENT 3
+#define PW_CLIENT_NODE_EVENT_COMMAND 4
+#define PW_CLIENT_NODE_EVENT_ADD_PORT 5
+#define PW_CLIENT_NODE_EVENT_REMOVE_PORT 6
+#define PW_CLIENT_NODE_EVENT_PORT_SET_PARAM 7
+#define PW_CLIENT_NODE_EVENT_PORT_USE_BUFFERS 8
+#define PW_CLIENT_NODE_EVENT_PORT_SET_IO 9
+#define PW_CLIENT_NODE_EVENT_SET_ACTIVATION 10
+#define PW_CLIENT_NODE_EVENT_PORT_SET_MIX_INFO 11
+#define PW_CLIENT_NODE_EVENT_NUM 12
+
+/** \ref pw_client_node events */
+struct pw_client_node_events {
+#define PW_VERSION_CLIENT_NODE_EVENTS 1
+ uint32_t version;
+ /**
+ * Notify of a new transport area
+ *
+ * The transport area is used to signal the client and the server.
+ *
+ * \param readfd fd for signal data can be read
+ * \param writefd fd for signal data can be written
+ * \param mem_id id for activation memory
+ * \param offset offset of activation memory
+ * \param size size of activation memory
+ */
+ int (*transport) (void *data,
+ int readfd,
+ int writefd,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size);
+ /**
+ * Notify of a property change
+ *
+ * When the server configures the properties on the node
+ * this event is sent
+ *
+ * \param id the id of the parameter
+ * \param flags parameter flags
+ * \param param the param to set
+ */
+ int (*set_param) (void *data,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ /**
+ * Configure an IO area for the client
+ *
+ * IO areas are identified with an id and are used to
+ * exchange state between client and server
+ *
+ * \param id the id of the io area
+ * \param mem_id the id of the memory to use
+ * \param offset offset of io area in memory
+ * \param size size of the io area
+ */
+ int (*set_io) (void *data,
+ uint32_t id,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size);
+ /**
+ * Receive an event from the client node
+ * \param event the received event */
+ int (*event) (void *data, const struct spa_event *event);
+ /**
+ * Notify of a new node command
+ *
+ * \param command the command
+ */
+ int (*command) (void *data, const struct spa_command *command);
+ /**
+ * A new port was added to the node
+ *
+ * The server can at any time add a port to the node when there
+ * are free ports available.
+ *
+ * \param direction the direction of the port
+ * \param port_id the new port id
+ * \param props extra properties
+ */
+ int (*add_port) (void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ const struct spa_dict *props);
+ /**
+ * A port was removed from the node
+ *
+ * \param direction a port direction
+ * \param port_id the remove port id
+ */
+ int (*remove_port) (void *data,
+ enum spa_direction direction,
+ uint32_t port_id);
+ /**
+ * A parameter was configured on the port
+ *
+ * \param direction a port direction
+ * \param port_id the port id
+ * \param id the id of the parameter
+ * \param flags flags used when setting the param
+ * \param param the new param
+ */
+ int (*port_set_param) (void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ /**
+ * Notify the port of buffers
+ *
+ * \param direction a port direction
+ * \param port_id the port id
+ * \param mix_id the mixer port id
+ * \param n_buffer the number of buffers
+ * \param buffers and array of buffer descriptions
+ */
+ int (*port_use_buffers) (void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t flags,
+ uint32_t n_buffers,
+ struct pw_client_node_buffer *buffers);
+ /**
+ * Configure the io area with \a id of \a port_id.
+ *
+ * \param direction the direction of the port
+ * \param port_id the port id
+ * \param mix_id the mixer port id
+ * \param id the id of the io area to set
+ * \param mem_id the id of the memory to use
+ * \param offset offset of io area in memory
+ * \param size size of the io area
+ */
+ int (*port_set_io) (void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t id,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size);
+
+ /**
+ * Notify the activation record of the next
+ * node to trigger
+ *
+ * \param node_id the peer node id
+ * \param signalfd the fd to wake up the peer
+ * \param mem_id the mem id of the memory
+ * \param the offset in \a mem_id to map
+ * \param the size of \a mem_id to map
+ */
+ int (*set_activation) (void *data,
+ uint32_t node_id,
+ int signalfd,
+ uint32_t mem_id,
+ uint32_t offset,
+ uint32_t size);
+
+ /**
+ * Notify about the peer of mix_id
+ *
+ * \param direction the direction of the port
+ * \param port_id the port id
+ * \param mix_id the mix id
+ * \param peer_id the id of the peer port
+ * \param props extra properties
+ *
+ * Since version 4:1
+ */
+ int (*port_set_mix_info) (void *data,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t peer_id,
+ const struct spa_dict *props);
+};
+
+#define PW_CLIENT_NODE_METHOD_ADD_LISTENER 0
+#define PW_CLIENT_NODE_METHOD_GET_NODE 1
+#define PW_CLIENT_NODE_METHOD_UPDATE 2
+#define PW_CLIENT_NODE_METHOD_PORT_UPDATE 3
+#define PW_CLIENT_NODE_METHOD_SET_ACTIVE 4
+#define PW_CLIENT_NODE_METHOD_EVENT 5
+#define PW_CLIENT_NODE_METHOD_PORT_BUFFERS 6
+#define PW_CLIENT_NODE_METHOD_NUM 7
+
+/** \ref pw_client_node methods */
+struct pw_client_node_methods {
+#define PW_VERSION_CLIENT_NODE_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_client_node_events *events,
+ void *data);
+ /** get the node object
+ */
+ struct pw_node * (*get_node) (void *object, uint32_t version, size_t user_data_size);
+ /**
+ * Update the node ports and properties
+ *
+ * Update the maximum number of ports and the params of the
+ * client node.
+ * \param change_mask bitfield with changed parameters
+ * \param max_input_ports new max input ports
+ * \param max_output_ports new max output ports
+ * \param params new params
+ */
+ int (*update) (void *object,
+#define PW_CLIENT_NODE_UPDATE_PARAMS (1 << 0)
+#define PW_CLIENT_NODE_UPDATE_INFO (1 << 1)
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_node_info *info);
+
+ /**
+ * Update a node port
+ *
+ * Update the information of one port of a node.
+ * \param direction the direction of the port
+ * \param port_id the port id to update
+ * \param change_mask a bitfield of changed items
+ * \param n_params number of port parameters
+ * \param params array of port parameters
+ * \param info port information
+ */
+ int (*port_update) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+#define PW_CLIENT_NODE_PORT_UPDATE_PARAMS (1 << 0)
+#define PW_CLIENT_NODE_PORT_UPDATE_INFO (1 << 1)
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct spa_port_info *info);
+ /**
+ * Activate or deactivate the node
+ */
+ int (*set_active) (void *object, bool active);
+ /**
+ * Send an event to the node
+ * \param event the event to send
+ */
+ int (*event) (void *object, const struct spa_event *event);
+
+ /**
+ * Send allocated buffers
+ */
+ int (*port_buffers) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t mix_id,
+ uint32_t n_buffers,
+ 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_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,
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_EXT_CLIENT_NODE_H */
diff --git a/src/pipewire/extensions/meson.build b/src/pipewire/extensions/meson.build
new file mode 100644
index 0000000..983809c
--- /dev/null
+++ b/src/pipewire/extensions/meson.build
@@ -0,0 +1,21 @@
+pipewire_ext_sm_headers = [
+ 'session-manager/impl-interfaces.h',
+ 'session-manager/interfaces.h',
+ 'session-manager/introspect.h',
+ 'session-manager/introspect-funcs.h',
+ 'session-manager/keys.h',
+]
+
+pipewire_ext_headers = [
+ 'client-node.h',
+ 'metadata.h',
+ 'profiler.h',
+ 'protocol-native.h',
+ 'session-manager.h',
+]
+
+install_headers(pipewire_ext_sm_headers,
+ subdir : pipewire_headers_dir / 'extensions' / 'session-manager')
+
+install_headers(pipewire_ext_headers,
+ subdir : pipewire_headers_dir / 'extensions')
diff --git a/src/pipewire/extensions/metadata.h b/src/pipewire/extensions/metadata.h
new file mode 100644
index 0000000..9541e2a
--- /dev/null
+++ b/src/pipewire/extensions/metadata.h
@@ -0,0 +1,112 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_METADATA_H
+#define PIPEWIRE_EXT_METADATA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+
+/** \defgroup pw_metadata Metadata
+ * Metadata interface
+ */
+
+/**
+ * \addtogroup pw_metadata
+ * \{
+ */
+#define PW_TYPE_INTERFACE_Metadata PW_TYPE_INFO_INTERFACE_BASE "Metadata"
+
+#define PW_VERSION_METADATA 3
+struct pw_metadata;
+
+#define PW_EXTENSION_MODULE_METADATA PIPEWIRE_MODULE_PREFIX "module-metadata"
+
+#define PW_METADATA_EVENT_PROPERTY 0
+#define PW_METADATA_EVENT_NUM 1
+
+/** \ref pw_metadata events */
+struct pw_metadata_events {
+#define PW_VERSION_METADATA_EVENTS 0
+ uint32_t version;
+
+ int (*property) (void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value);
+};
+
+#define PW_METADATA_METHOD_ADD_LISTENER 0
+#define PW_METADATA_METHOD_SET_PROPERTY 1
+#define PW_METADATA_METHOD_CLEAR 2
+#define PW_METADATA_METHOD_NUM 3
+
+/** \ref pw_metadata methods */
+struct pw_metadata_methods {
+#define PW_VERSION_METADATA_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_metadata_events *events,
+ void *data);
+
+ int (*set_property) (void *object,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value);
+
+ 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)
+
+#define PW_KEY_METADATA_NAME "metadata.name"
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_EXT_METADATA_H */
diff --git a/src/pipewire/extensions/profiler.h b/src/pipewire/extensions/profiler.h
new file mode 100644
index 0000000..a5940a1
--- /dev/null
+++ b/src/pipewire/extensions/profiler.h
@@ -0,0 +1,95 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_PROFILER_H
+#define PIPEWIRE_EXT_PROFILER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+
+/** \defgroup pw_profiler Profiler
+ * Profiler interface
+ */
+
+/**
+ * \addtogroup pw_profiler
+ * \{
+ */
+#define PW_TYPE_INTERFACE_Profiler PW_TYPE_INFO_INTERFACE_BASE "Profiler"
+
+#define PW_VERSION_PROFILER 3
+struct pw_profiler;
+
+#define PW_EXTENSION_MODULE_PROFILER PIPEWIRE_MODULE_PREFIX "module-profiler"
+
+#define PW_PROFILER_EVENT_PROFILE 0
+#define PW_PROFILER_EVENT_NUM 1
+
+/** \ref pw_profiler events */
+struct pw_profiler_events {
+#define PW_VERSION_PROFILER_EVENTS 0
+ uint32_t version;
+
+ void (*profile) (void *data, const struct spa_pod *pod);
+};
+
+#define PW_PROFILER_METHOD_ADD_LISTENER 0
+#define PW_PROFILER_METHOD_NUM 1
+
+/** \ref pw_profiler methods */
+struct pw_profiler_methods {
+#define PW_VERSION_PROFILER_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_profiler_events *events,
+ 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__)
+
+#define PW_KEY_PROFILER_NAME "profiler.name"
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_EXT_PROFILER_H */
diff --git a/src/pipewire/extensions/protocol-native.h b/src/pipewire/extensions/protocol-native.h
new file mode 100644
index 0000000..4fe1905
--- /dev/null
+++ b/src/pipewire/extensions/protocol-native.h
@@ -0,0 +1,105 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_PROTOCOL_NATIVE_H
+#define PIPEWIRE_EXT_PROTOCOL_NATIVE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+
+#include <pipewire/proxy.h>
+#include <pipewire/resource.h>
+
+/** \defgroup pw_protocol_native Native Protocol
+ * PipeWire native protocol interface
+ */
+
+/**
+ * \addtogroup pw_protocol_native
+ * \{
+ */
+#define PW_TYPE_INFO_PROTOCOL_Native PW_TYPE_INFO_PROTOCOL_BASE "Native"
+
+struct pw_protocol_native_message {
+ uint32_t id;
+ uint32_t opcode;
+ void *data;
+ uint32_t size;
+ uint32_t n_fds;
+ int *fds;
+ int seq;
+};
+
+struct pw_protocol_native_demarshal {
+ int (*func) (void *object, const struct pw_protocol_native_message *msg);
+ uint32_t permissions;
+ uint32_t flags;
+};
+
+/** \ref pw_protocol_native_ext methods */
+struct pw_protocol_native_ext {
+#define PW_VERSION_PROTOCOL_NATIVE_EXT 0
+ uint32_t version;
+
+ struct spa_pod_builder * (*begin_proxy) (struct pw_proxy *proxy,
+ uint8_t opcode, struct pw_protocol_native_message **msg);
+
+ uint32_t (*add_proxy_fd) (struct pw_proxy *proxy, int fd);
+ int (*get_proxy_fd) (struct pw_proxy *proxy, uint32_t index);
+
+ int (*end_proxy) (struct pw_proxy *proxy,
+ struct spa_pod_builder *builder);
+
+ struct spa_pod_builder * (*begin_resource) (struct pw_resource *resource,
+ uint8_t opcode, struct pw_protocol_native_message **msg);
+
+ uint32_t (*add_resource_fd) (struct pw_resource *resource, int fd);
+ int (*get_resource_fd) (struct pw_resource *resource, uint32_t index);
+
+ int (*end_resource) (struct pw_resource *resource,
+ struct spa_pod_builder *builder);
+};
+
+#define pw_protocol_native_begin_proxy(p,...) pw_protocol_ext(pw_proxy_get_protocol(p),struct pw_protocol_native_ext,begin_proxy,p,__VA_ARGS__)
+#define pw_protocol_native_add_proxy_fd(p,...) pw_protocol_ext(pw_proxy_get_protocol(p),struct pw_protocol_native_ext,add_proxy_fd,p,__VA_ARGS__)
+#define pw_protocol_native_get_proxy_fd(p,...) pw_protocol_ext(pw_proxy_get_protocol(p),struct pw_protocol_native_ext,get_proxy_fd,p,__VA_ARGS__)
+#define pw_protocol_native_end_proxy(p,...) pw_protocol_ext(pw_proxy_get_protocol(p),struct pw_protocol_native_ext,end_proxy,p,__VA_ARGS__)
+
+#define pw_protocol_native_begin_resource(r,...) pw_protocol_ext(pw_resource_get_protocol(r),struct pw_protocol_native_ext,begin_resource,r,__VA_ARGS__)
+#define pw_protocol_native_add_resource_fd(r,...) pw_protocol_ext(pw_resource_get_protocol(r),struct pw_protocol_native_ext,add_resource_fd,r,__VA_ARGS__)
+#define pw_protocol_native_get_resource_fd(r,...) pw_protocol_ext(pw_resource_get_protocol(r),struct pw_protocol_native_ext,get_resource_fd,r,__VA_ARGS__)
+#define pw_protocol_native_end_resource(r,...) pw_protocol_ext(pw_resource_get_protocol(r),struct pw_protocol_native_ext,end_resource,r,__VA_ARGS__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_EXT_PROTOCOL_NATIVE_H */
diff --git a/src/pipewire/extensions/session-manager.h b/src/pipewire/extensions/session-manager.h
new file mode 100644
index 0000000..1226d23
--- /dev/null
+++ b/src/pipewire/extensions/session-manager.h
@@ -0,0 +1,47 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_SESSION_MANAGER_H
+#define PIPEWIRE_EXT_SESSION_MANAGER_H
+
+/** \defgroup pw_session_manager Session Manager
+ * Session manager interface
+ */
+
+/**
+ * \addtogroup pw_session_manager
+ * \{
+ */
+
+#include "session-manager/introspect.h"
+#include "session-manager/interfaces.h"
+#include "session-manager/impl-interfaces.h"
+#include "session-manager/keys.h"
+
+/**
+ * \}
+ */
+
+#endif /* PIPEWIRE_EXT_SESSION_MANAGER_H */
diff --git a/src/pipewire/extensions/session-manager/impl-interfaces.h b/src/pipewire/extensions/session-manager/impl-interfaces.h
new file mode 100644
index 0000000..20dbc11
--- /dev/null
+++ b/src/pipewire/extensions/session-manager/impl-interfaces.h
@@ -0,0 +1,298 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_SESSION_MANAGER_IMPL_INTERFACES_H
+#define PIPEWIRE_EXT_SESSION_MANAGER_IMPL_INTERFACES_H
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <errno.h>
+
+#include "introspect.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup pw_session_manager
+ * \{
+ */
+
+#define PW_TYPE_INTERFACE_ClientEndpoint PW_TYPE_INFO_INTERFACE_BASE "ClientEndpoint"
+
+#define PW_VERSION_CLIENT_ENDPOINT 0
+struct pw_client_endpoint;
+
+#define PW_CLIENT_ENDPOINT_EVENT_SET_SESSION_ID 0
+#define PW_CLIENT_ENDPOINT_EVENT_SET_PARAM 1
+#define PW_CLIENT_ENDPOINT_EVENT_STREAM_SET_PARAM 2
+#define PW_CLIENT_ENDPOINT_EVENT_CREATE_LINK 3
+#define PW_CLIENT_ENDPOINT_EVENT_NUM 4
+
+struct pw_client_endpoint_events {
+#define PW_VERSION_CLIENT_ENDPOINT_EVENTS 0
+ uint32_t version; /**< version of this structure */
+
+ /**
+ * Sets the session id of the \a endpoint.
+ *
+ * On endpoints that are not session masters, this method notifies
+ * the implementation that it has been associated with a session.
+ * The implementation is obliged to set this id in the
+ * #struct pw_endpoint_info \a session_id field.
+ *
+ * \param endpoint a #pw_endpoint
+ * \param id the session id associated with this endpoint
+ *
+ * \return 0 on success
+ * -EINVAL when the session id has already been set
+ * -ENOTSUP when the endpoint is a session master
+ */
+ int (*set_session_id) (void *data, uint32_t session_id);
+
+ /**
+ * Set the configurable parameter in \a endpoint.
+ *
+ * Usually, \a param will be obtained from enum_params and then
+ * modified but it is also possible to set another spa_pod
+ * as long as its keys and types match a supported object.
+ *
+ * Objects with property keys that are not known are ignored.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param endpoint a #struct pw_endpoint
+ * \param id the parameter id to configure
+ * \param flags additional flags
+ * \param param the parameter to configure
+ *
+ * \return 0 on success
+ * -EINVAL when \a endpoint is NULL
+ * -ENOTSUP when there are no parameters implemented on \a endpoint
+ * -ENOENT the parameter is unknown
+ */
+ int (*set_param) (void *data,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ /**
+ * Set a parameter on \a stream_id of \a endpoint.
+ *
+ * When \a param is NULL, the parameter will be unset.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param endpoint a #struct pw_endpoint
+ * \param stream_id the stream to configure
+ * \param id the parameter id to set
+ * \param flags optional flags
+ * \param param a #struct spa_pod with the parameter to set
+ * \return 0 on success
+ * 1 on success, the value of \a param might have been
+ * changed depending on \a flags and the final value can
+ * be found by doing stream_enum_params.
+ * -EINVAL when \a endpoint is NULL or invalid arguments are given
+ * -ESRCH when the type or size of a property is not correct.
+ * -ENOENT when the param id is not found
+ */
+ int (*stream_set_param) (void *data, uint32_t stream_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ int (*create_link) (void *data, const struct spa_dict *props);
+};
+
+#define PW_CLIENT_ENDPOINT_METHOD_ADD_LISTENER 0
+#define PW_CLIENT_ENDPOINT_METHOD_UPDATE 1
+#define PW_CLIENT_ENDPOINT_METHOD_STREAM_UPDATE 2
+#define PW_CLIENT_ENDPOINT_METHOD_NUM 3
+
+struct pw_client_endpoint_methods {
+#define PW_VERSION_CLIENT_ENDPOINT_METHODS 0
+ uint32_t version; /**< version of this structure */
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_client_endpoint_events *events,
+ void *data);
+
+ /** Update endpoint information */
+ int (*update) (void *object,
+#define PW_CLIENT_ENDPOINT_UPDATE_PARAMS (1 << 0)
+#define PW_CLIENT_ENDPOINT_UPDATE_INFO (1 << 1)
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_info *info);
+
+ /** Update stream information */
+ int (*stream_update) (void *object,
+ uint32_t stream_id,
+#define PW_CLIENT_ENDPOINT_STREAM_UPDATE_PARAMS (1 << 0)
+#define PW_CLIENT_ENDPOINT_STREAM_UPDATE_INFO (1 << 1)
+#define PW_CLIENT_ENDPOINT_STREAM_UPDATE_DESTROYED (1 << 2)
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_stream_info *info);
+};
+
+#define pw_client_endpoint_method(o,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_interface_call_res((struct spa_interface*)o, \
+ struct pw_client_endpoint_methods, _res, \
+ method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+#define pw_client_endpoint_add_listener(o,...) pw_client_endpoint_method(o,add_listener,0,__VA_ARGS__)
+#define pw_client_endpoint_update(o,...) pw_client_endpoint_method(o,update,0,__VA_ARGS__)
+#define pw_client_endpoint_stream_update(o,...) pw_client_endpoint_method(o,stream_update,0,__VA_ARGS__)
+
+#define PW_TYPE_INTERFACE_ClientSession PW_TYPE_INFO_INTERFACE_BASE "ClientSession"
+
+#define PW_VERSION_CLIENT_SESSION 0
+struct pw_client_session;
+
+#define PW_CLIENT_SESSION_EVENT_SET_PARAM 0
+#define PW_CLIENT_SESSION_EVENT_LINK_SET_PARAM 1
+#define PW_CLIENT_SESSION_EVENT_LINK_REQUEST_STATE 2
+#define PW_CLIENT_SESSION_EVENT_NUM 3
+
+struct pw_client_session_events {
+#define PW_VERSION_CLIENT_SESSION_EVENTS 0
+ uint32_t version; /**< version of this structure */
+
+ /**
+ * Set the configurable parameter in \a session.
+ *
+ * Usually, \a param will be obtained from enum_params and then
+ * modified but it is also possible to set another spa_pod
+ * as long as its keys and types match a supported object.
+ *
+ * Objects with property keys that are not known are ignored.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param session a #struct pw_session
+ * \param id the parameter id to configure
+ * \param flags additional flags
+ * \param param the parameter to configure
+ *
+ * \return 0 on success
+ * -EINVAL when \a session is NULL
+ * -ENOTSUP when there are no parameters implemented on \a session
+ * -ENOENT the parameter is unknown
+ */
+ int (*set_param) (void *data,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ /**
+ * Set a parameter on \a link_id of \a session.
+ *
+ * When \a param is NULL, the parameter will be unset.
+ *
+ * This function must be called from the main thread.
+ *
+ * \param session a #struct pw_session
+ * \param link_id the link to configure
+ * \param id the parameter id to set
+ * \param flags optional flags
+ * \param param a #struct spa_pod with the parameter to set
+ * \return 0 on success
+ * 1 on success, the value of \a param might have been
+ * changed depending on \a flags and the final value can
+ * be found by doing link_enum_params.
+ * -EINVAL when \a session is NULL or invalid arguments are given
+ * -ESRCH when the type or size of a property is not correct.
+ * -ENOENT when the param id is not found
+ */
+ int (*link_set_param) (void *data, uint32_t link_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ int (*link_request_state) (void *data, uint32_t link_id, uint32_t state);
+};
+
+#define PW_CLIENT_SESSION_METHOD_ADD_LISTENER 0
+#define PW_CLIENT_SESSION_METHOD_UPDATE 1
+#define PW_CLIENT_SESSION_METHOD_LINK_UPDATE 2
+#define PW_CLIENT_SESSION_METHOD_NUM 3
+
+struct pw_client_session_methods {
+#define PW_VERSION_CLIENT_SESSION_METHODS 0
+ uint32_t version; /**< version of this structure */
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_client_session_events *events,
+ void *data);
+
+ /** Update session information */
+ int (*update) (void *object,
+#define PW_CLIENT_SESSION_UPDATE_PARAMS (1 << 0)
+#define PW_CLIENT_SESSION_UPDATE_INFO (1 << 1)
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_session_info *info);
+
+ /** Update link information */
+ int (*link_update) (void *object,
+ uint32_t link_id,
+#define PW_CLIENT_SESSION_LINK_UPDATE_PARAMS (1 << 0)
+#define PW_CLIENT_SESSION_LINK_UPDATE_INFO (1 << 1)
+#define PW_CLIENT_SESSION_LINK_UPDATE_DESTROYED (1 << 2)
+ uint32_t change_mask,
+ uint32_t n_params,
+ const struct spa_pod **params,
+ const struct pw_endpoint_link_info *info);
+};
+
+#define pw_client_session_method(o,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_interface_call_res((struct spa_interface*)o, \
+ struct pw_client_session_methods, _res, \
+ method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+#define pw_client_session_add_listener(o,...) pw_client_session_method(o,add_listener,0,__VA_ARGS__)
+#define pw_client_session_update(o,...) pw_client_session_method(o,update,0,__VA_ARGS__)
+#define pw_client_session_link_update(o,...) pw_client_session_method(o,link_update,0,__VA_ARGS__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_EXT_SESSION_MANAGER_IMPL_INTERFACES_H */
diff --git a/src/pipewire/extensions/session-manager/interfaces.h b/src/pipewire/extensions/session-manager/interfaces.h
new file mode 100644
index 0000000..0c11e1d
--- /dev/null
+++ b/src/pipewire/extensions/session-manager/interfaces.h
@@ -0,0 +1,479 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_SESSION_MANAGER_INTERFACES_H
+#define PIPEWIRE_EXT_SESSION_MANAGER_INTERFACES_H
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include "introspect.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup pw_session_manager
+ * \{
+ */
+
+#define PW_TYPE_INTERFACE_Session PW_TYPE_INFO_INTERFACE_BASE "Session"
+#define PW_VERSION_SESSION 0
+struct pw_session;
+
+#define PW_TYPE_INTERFACE_Endpoint PW_TYPE_INFO_INTERFACE_BASE "Endpoint"
+#define PW_VERSION_ENDPOINT 0
+struct pw_endpoint;
+
+#define PW_TYPE_INTERFACE_EndpointStream PW_TYPE_INFO_INTERFACE_BASE "EndpointStream"
+#define PW_VERSION_ENDPOINT_STREAM 0
+struct pw_endpoint_stream;
+
+#define PW_TYPE_INTERFACE_EndpointLink PW_TYPE_INFO_INTERFACE_BASE "EndpointLink"
+#define PW_VERSION_ENDPOINT_LINK 0
+struct pw_endpoint_link;
+
+/* Session */
+
+#define PW_SESSION_EVENT_INFO 0
+#define PW_SESSION_EVENT_PARAM 1
+#define PW_SESSION_EVENT_NUM 2
+
+struct pw_session_events {
+#define PW_VERSION_SESSION_EVENTS 0
+ uint32_t version; /**< version of this structure */
+
+ /**
+ * Notify session info
+ *
+ * \param info info about the session
+ */
+ void (*info) (void *data, const struct pw_session_info *info);
+
+ /**
+ * Notify a session param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param seq the sequence number of the request
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define PW_SESSION_METHOD_ADD_LISTENER 0
+#define PW_SESSION_METHOD_SUBSCRIBE_PARAMS 1
+#define PW_SESSION_METHOD_ENUM_PARAMS 2
+#define PW_SESSION_METHOD_SET_PARAM 3
+#define PW_SESSION_METHOD_CREATE_LINK 4
+#define PW_SESSION_METHOD_NUM 5
+
+struct pw_session_methods {
+#define PW_VERSION_SESSION_METHODS 0
+ uint32_t version; /**< version of this structure */
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_session_events *events,
+ void *data);
+
+ /**
+ * Subscribe to parameter changes
+ *
+ * Automatically emit param events for the given ids when
+ * they are changed.
+ *
+ * \param ids an array of param ids
+ * \param n_ids the number of ids in \a ids
+ */
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+
+ /**
+ * Enumerate session parameters
+ *
+ * Start enumeration of session parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param seq a sequence number returned in the reply
+ * \param id the parameter id to enumerate
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ int (*enum_params) (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+
+ /**
+ * Set a parameter on the session
+ *
+ * \param id the parameter id to set
+ * \param flags extra parameter flags
+ * \param param the parameter to set
+ */
+ int (*set_param) (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+};
+
+#define pw_session_method(o,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_interface_call_res((struct spa_interface*)o, \
+ struct pw_session_methods, _res, \
+ method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+#define pw_session_add_listener(c,...) pw_session_method(c,add_listener,0,__VA_ARGS__)
+#define pw_session_subscribe_params(c,...) pw_session_method(c,subscribe_params,0,__VA_ARGS__)
+#define pw_session_enum_params(c,...) pw_session_method(c,enum_params,0,__VA_ARGS__)
+#define pw_session_set_param(c,...) pw_session_method(c,set_param,0,__VA_ARGS__)
+
+
+/* Endpoint */
+
+#define PW_ENDPOINT_EVENT_INFO 0
+#define PW_ENDPOINT_EVENT_PARAM 1
+#define PW_ENDPOINT_EVENT_NUM 2
+
+struct pw_endpoint_events {
+#define PW_VERSION_ENDPOINT_EVENTS 0
+ uint32_t version; /**< version of this structure */
+
+ /**
+ * Notify endpoint info
+ *
+ * \param info info about the endpoint
+ */
+ void (*info) (void *data, const struct pw_endpoint_info *info);
+
+ /**
+ * Notify a endpoint param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param seq the sequence number of the request
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define PW_ENDPOINT_METHOD_ADD_LISTENER 0
+#define PW_ENDPOINT_METHOD_SUBSCRIBE_PARAMS 1
+#define PW_ENDPOINT_METHOD_ENUM_PARAMS 2
+#define PW_ENDPOINT_METHOD_SET_PARAM 3
+#define PW_ENDPOINT_METHOD_CREATE_LINK 4
+#define PW_ENDPOINT_METHOD_NUM 5
+
+struct pw_endpoint_methods {
+#define PW_VERSION_ENDPOINT_METHODS 0
+ uint32_t version; /**< version of this structure */
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_events *events,
+ void *data);
+
+ /**
+ * Subscribe to parameter changes
+ *
+ * Automatically emit param events for the given ids when
+ * they are changed.
+ *
+ * \param ids an array of param ids
+ * \param n_ids the number of ids in \a ids
+ */
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+
+ /**
+ * Enumerate endpoint parameters
+ *
+ * Start enumeration of endpoint parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param seq a sequence number returned in the reply
+ * \param id the parameter id to enumerate
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ int (*enum_params) (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+
+ /**
+ * Set a parameter on the endpoint
+ *
+ * \param id the parameter id to set
+ * \param flags extra parameter flags
+ * \param param the parameter to set
+ */
+ int (*set_param) (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ int (*create_link) (void *object, const struct spa_dict *props);
+};
+
+#define pw_endpoint_method(o,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_interface_call_res((struct spa_interface*)o, \
+ struct pw_endpoint_methods, _res, \
+ method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+#define pw_endpoint_add_listener(c,...) pw_endpoint_method(c,add_listener,0,__VA_ARGS__)
+#define pw_endpoint_subscribe_params(c,...) pw_endpoint_method(c,subscribe_params,0,__VA_ARGS__)
+#define pw_endpoint_enum_params(c,...) pw_endpoint_method(c,enum_params,0,__VA_ARGS__)
+#define pw_endpoint_set_param(c,...) pw_endpoint_method(c,set_param,0,__VA_ARGS__)
+#define pw_endpoint_create_link(c,...) pw_endpoint_method(c,create_link,0,__VA_ARGS__)
+
+/* Endpoint Stream */
+
+#define PW_ENDPOINT_STREAM_EVENT_INFO 0
+#define PW_ENDPOINT_STREAM_EVENT_PARAM 1
+#define PW_ENDPOINT_STREAM_EVENT_NUM 2
+
+struct pw_endpoint_stream_events {
+#define PW_VERSION_ENDPOINT_STREAM_EVENTS 0
+ uint32_t version; /**< version of this structure */
+
+ /**
+ * Notify endpoint stream info
+ *
+ * \param info info about the endpoint stream
+ */
+ void (*info) (void *data, const struct pw_endpoint_stream_info *info);
+
+ /**
+ * Notify a endpoint stream param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param seq the sequence number of the request
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define PW_ENDPOINT_STREAM_METHOD_ADD_LISTENER 0
+#define PW_ENDPOINT_STREAM_METHOD_SUBSCRIBE_PARAMS 1
+#define PW_ENDPOINT_STREAM_METHOD_ENUM_PARAMS 2
+#define PW_ENDPOINT_STREAM_METHOD_SET_PARAM 3
+#define PW_ENDPOINT_STREAM_METHOD_NUM 4
+
+struct pw_endpoint_stream_methods {
+#define PW_VERSION_ENDPOINT_STREAM_METHODS 0
+ uint32_t version; /**< version of this structure */
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_stream_events *events,
+ void *data);
+
+ /**
+ * Subscribe to parameter changes
+ *
+ * Automatically emit param events for the given ids when
+ * they are changed.
+ *
+ * \param ids an array of param ids
+ * \param n_ids the number of ids in \a ids
+ */
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+
+ /**
+ * Enumerate stream parameters
+ *
+ * Start enumeration of stream parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param seq a sequence number returned in the reply
+ * \param id the parameter id to enumerate
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ int (*enum_params) (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+
+ /**
+ * Set a parameter on the stream
+ *
+ * \param id the parameter id to set
+ * \param flags extra parameter flags
+ * \param param the parameter to set
+ */
+ int (*set_param) (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+};
+
+#define pw_endpoint_stream_method(o,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_interface_call_res((struct spa_interface*)o, \
+ struct pw_endpoint_stream_methods, _res, \
+ method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+#define pw_endpoint_stream_add_listener(c,...) pw_endpoint_stream_method(c,add_listener,0,__VA_ARGS__)
+#define pw_endpoint_stream_subscribe_params(c,...) pw_endpoint_stream_method(c,subscribe_params,0,__VA_ARGS__)
+#define pw_endpoint_stream_enum_params(c,...) pw_endpoint_stream_method(c,enum_params,0,__VA_ARGS__)
+#define pw_endpoint_stream_set_param(c,...) pw_endpoint_stream_method(c,set_param,0,__VA_ARGS__)
+
+/* Endpoint Link */
+
+#define PW_ENDPOINT_LINK_EVENT_INFO 0
+#define PW_ENDPOINT_LINK_EVENT_PARAM 1
+#define PW_ENDPOINT_LINK_EVENT_NUM 2
+
+struct pw_endpoint_link_events {
+#define PW_VERSION_ENDPOINT_LINK_EVENTS 0
+ uint32_t version; /**< version of this structure */
+
+ /**
+ * Notify endpoint link info
+ *
+ * \param info info about the endpoint link
+ */
+ void (*info) (void *data, const struct pw_endpoint_link_info *info);
+
+ /**
+ * Notify a endpoint link param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param seq the sequence number of the request
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define PW_ENDPOINT_LINK_METHOD_ADD_LISTENER 0
+#define PW_ENDPOINT_LINK_METHOD_SUBSCRIBE_PARAMS 1
+#define PW_ENDPOINT_LINK_METHOD_ENUM_PARAMS 2
+#define PW_ENDPOINT_LINK_METHOD_SET_PARAM 3
+#define PW_ENDPOINT_LINK_METHOD_REQUEST_STATE 4
+#define PW_ENDPOINT_LINK_METHOD_DESTROY 5
+#define PW_ENDPOINT_LINK_METHOD_NUM 6
+
+struct pw_endpoint_link_methods {
+#define PW_VERSION_ENDPOINT_LINK_METHODS 0
+ uint32_t version; /**< version of this structure */
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_link_events *events,
+ void *data);
+
+ /**
+ * Subscribe to parameter changes
+ *
+ * Automatically emit param events for the given ids when
+ * they are changed.
+ *
+ * \param ids an array of param ids
+ * \param n_ids the number of ids in \a ids
+ */
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+
+ /**
+ * Enumerate link parameters
+ *
+ * Start enumeration of link parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param seq a sequence number returned in the reply
+ * \param id the parameter id to enumerate
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ int (*enum_params) (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+
+ /**
+ * Set a parameter on the link
+ *
+ * \param id the parameter id to set
+ * \param flags extra parameter flags
+ * \param param the parameter to set
+ */
+ int (*set_param) (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ int (*request_state) (void *object, enum pw_endpoint_link_state state);
+};
+
+#define pw_endpoint_link_method(o,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_interface_call_res((struct spa_interface*)o, \
+ struct pw_endpoint_link_methods, _res, \
+ method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+#define pw_endpoint_link_add_listener(c,...) pw_endpoint_link_method(c,add_listener,0,__VA_ARGS__)
+#define pw_endpoint_link_subscribe_params(c,...) pw_endpoint_link_method(c,subscribe_params,0,__VA_ARGS__)
+#define pw_endpoint_link_enum_params(c,...) pw_endpoint_link_method(c,enum_params,0,__VA_ARGS__)
+#define pw_endpoint_link_set_param(c,...) pw_endpoint_link_method(c,set_param,0,__VA_ARGS__)
+#define pw_endpoint_link_request_state(c,...) pw_endpoint_link_method(c,request_state,0,__VA_ARGS__)
+
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_EXT_SESSION_MANAGER_INTERFACES_H */
diff --git a/src/pipewire/extensions/session-manager/introspect-funcs.h b/src/pipewire/extensions/session-manager/introspect-funcs.h
new file mode 100644
index 0000000..24df45d
--- /dev/null
+++ b/src/pipewire/extensions/session-manager/introspect-funcs.h
@@ -0,0 +1,323 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_SESSION_MANAGER_INTROSPECT_FUNCS_H
+#define PIPEWIRE_EXT_SESSION_MANAGER_INTROSPECT_FUNCS_H
+
+#include "introspect.h"
+#include <spa/pod/builder.h>
+#include <pipewire/pipewire.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup pw_session_manager
+ * \{
+ */
+
+static inline struct pw_session_info *
+pw_session_info_update (struct pw_session_info *info,
+ const struct pw_session_info *update)
+{
+ struct extended_info {
+ struct pw_properties *props_storage;
+ struct pw_session_info info;
+ } *ext;
+
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ ext = (struct extended_info *) calloc(1, sizeof(*ext));
+ if (ext == NULL)
+ return NULL;
+
+ info = &ext->info;
+ info->id = update->id;
+ } else {
+ ext = SPA_CONTAINER_OF(info, struct extended_info, info);
+ }
+
+ info->change_mask = update->change_mask;
+
+ if (update->change_mask & PW_SESSION_CHANGE_MASK_PROPS) {
+ if (!ext->props_storage) {
+ ext->props_storage = pw_properties_new(NULL, NULL);
+ info->props = &ext->props_storage->dict;
+ }
+ pw_properties_clear(ext->props_storage);
+ pw_properties_update(ext->props_storage, update->props);
+ }
+ if (update->change_mask & PW_SESSION_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free((void *) info->params);
+ if (update->params) {
+ size_t size = info->n_params * sizeof(struct spa_param_info);
+ info->params = (struct spa_param_info *) malloc(size);
+ memcpy(info->params, update->params, size);
+ }
+ else
+ info->params = NULL;
+ }
+ return info;
+}
+
+static inline void
+pw_session_info_free (struct pw_session_info *info)
+{
+ struct extended_info {
+ struct pw_properties *props_storage;
+ struct pw_session_info info;
+ } *ext = SPA_CONTAINER_OF(info, struct extended_info, info);
+
+ pw_properties_free(ext->props_storage);
+ free((void *) info->params);
+ free(ext);
+}
+
+static inline struct pw_endpoint_info *
+pw_endpoint_info_update (struct pw_endpoint_info *info,
+ const struct pw_endpoint_info *update)
+{
+ struct extended_info {
+ struct pw_properties *props_storage;
+ struct pw_endpoint_info info;
+ } *ext;
+
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ ext = (struct extended_info *) calloc(1, sizeof(*ext));
+ if (ext == NULL)
+ return NULL;
+
+ info = &ext->info;
+ info->id = update->id;
+ info->name = strdup(update->name);
+ info->media_class = strdup(update->media_class);
+ info->direction = update->direction;
+ info->flags = update->flags;
+ } else {
+ ext = SPA_CONTAINER_OF(info, struct extended_info, info);
+ }
+
+ info->change_mask = update->change_mask;
+
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_STREAMS)
+ info->n_streams = update->n_streams;
+
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_SESSION)
+ info->session_id = update->session_id;
+
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) {
+ if (!ext->props_storage) {
+ ext->props_storage = pw_properties_new(NULL, NULL);
+ info->props = &ext->props_storage->dict;
+ }
+ pw_properties_clear(ext->props_storage);
+ pw_properties_update(ext->props_storage, update->props);
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free((void *) info->params);
+ if (update->params) {
+ size_t size = info->n_params * sizeof(struct spa_param_info);
+ info->params = (struct spa_param_info *) malloc(size);
+ memcpy(info->params, update->params, size);
+ }
+ else
+ info->params = NULL;
+ }
+ return info;
+}
+
+static inline void
+pw_endpoint_info_free (struct pw_endpoint_info *info)
+{
+ struct extended_info {
+ struct pw_properties *props_storage;
+ struct pw_endpoint_info info;
+ } *ext = SPA_CONTAINER_OF(info, struct extended_info, info);
+
+ pw_properties_free(ext->props_storage);
+ free(info->name);
+ free(info->media_class);
+ free((void *) info->params);
+ free(ext);
+}
+
+static inline struct pw_endpoint_stream_info *
+pw_endpoint_stream_info_update (struct pw_endpoint_stream_info *info,
+ const struct pw_endpoint_stream_info *update)
+{
+ struct extended_info {
+ struct pw_properties *props_storage;
+ struct pw_endpoint_stream_info info;
+ } *ext;
+
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ ext = (struct extended_info *) calloc(1, sizeof(*ext));
+ if (ext == NULL)
+ return NULL;
+
+ info = &ext->info;
+ info->id = update->id;
+ info->endpoint_id = update->endpoint_id;
+ info->name = strdup(update->name);
+ } else {
+ ext = SPA_CONTAINER_OF(info, struct extended_info, info);
+ }
+
+ info->change_mask = update->change_mask;
+
+ if (update->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_LINK_PARAMS) {
+ free(info->link_params);
+ info->link_params = update->link_params ?
+ spa_pod_copy(update->link_params) : NULL;
+ }
+ if (update->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS) {
+ if (!ext->props_storage) {
+ ext->props_storage = pw_properties_new(NULL, NULL);
+ info->props = &ext->props_storage->dict;
+ }
+ pw_properties_clear(ext->props_storage);
+ pw_properties_update(ext->props_storage, update->props);
+ }
+ if (update->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free((void *) info->params);
+ if (update->params) {
+ size_t size = info->n_params * sizeof(struct spa_param_info);
+ info->params = (struct spa_param_info *) malloc(size);
+ memcpy(info->params, update->params, size);
+ }
+ else
+ info->params = NULL;
+ }
+ return info;
+}
+
+static inline void
+pw_endpoint_stream_info_free (struct pw_endpoint_stream_info *info)
+{
+ struct extended_info {
+ struct pw_properties *props_storage;
+ struct pw_endpoint_stream_info info;
+ } *ext = SPA_CONTAINER_OF(info, struct extended_info, info);
+
+ pw_properties_free(ext->props_storage);
+ free(info->name);
+ free(info->link_params);
+ free((void *) info->params);
+ free(ext);
+}
+
+
+static inline struct pw_endpoint_link_info *
+pw_endpoint_link_info_update (struct pw_endpoint_link_info *info,
+ const struct pw_endpoint_link_info *update)
+{
+ struct extended_info {
+ struct pw_properties *props_storage;
+ struct pw_endpoint_link_info info;
+ } *ext;
+
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ ext = (struct extended_info *) calloc(1, sizeof(*ext));
+ if (ext == NULL)
+ return NULL;
+
+ info = &ext->info;
+ info->id = update->id;
+ info->session_id = update->session_id;
+ info->output_endpoint_id = update->output_endpoint_id;
+ info->output_stream_id = update->output_stream_id;
+ info->input_endpoint_id = update->input_endpoint_id;
+ info->input_stream_id = update->input_stream_id;
+ } else {
+ ext = SPA_CONTAINER_OF(info, struct extended_info, info);
+ }
+
+ info->change_mask = update->change_mask;
+
+ if (update->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_STATE) {
+ info->state = update->state;
+ free(info->error);
+ info->error = update->error ? strdup(update->error) : NULL;
+ }
+ if (update->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PROPS) {
+ if (!ext->props_storage) {
+ ext->props_storage = pw_properties_new(NULL, NULL);
+ info->props = &ext->props_storage->dict;
+ }
+ pw_properties_clear(ext->props_storage);
+ pw_properties_update(ext->props_storage, update->props);
+ }
+ if (update->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free((void *) info->params);
+ if (update->params) {
+ size_t size = info->n_params * sizeof(struct spa_param_info);
+ info->params = (struct spa_param_info *) malloc(size);
+ memcpy(info->params, update->params, size);
+ }
+ else
+ info->params = NULL;
+ }
+ return info;
+}
+
+static inline void
+pw_endpoint_link_info_free (struct pw_endpoint_link_info *info)
+{
+ struct extended_info {
+ struct pw_properties *props_storage;
+ struct pw_endpoint_link_info info;
+ } *ext = SPA_CONTAINER_OF(info, struct extended_info, info);
+
+ pw_properties_free(ext->props_storage);
+ free(info->error);
+ free((void *) info->params);
+ free(ext);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_EXT_SESSION_MANAGER_INTROSPECT_FUNCS_H */
diff --git a/src/pipewire/extensions/session-manager/introspect.h b/src/pipewire/extensions/session-manager/introspect.h
new file mode 100644
index 0000000..a35a70b
--- /dev/null
+++ b/src/pipewire/extensions/session-manager/introspect.h
@@ -0,0 +1,130 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_SESSION_MANAGER_INTROSPECT_H
+#define PIPEWIRE_EXT_SESSION_MANAGER_INTROSPECT_H
+
+#include <spa/utils/defs.h>
+#include <spa/utils/dict.h>
+#include <spa/param/param.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup pw_session_manager
+ * \{
+ */
+
+enum pw_endpoint_link_state {
+ PW_ENDPOINT_LINK_STATE_ERROR = -1,
+ PW_ENDPOINT_LINK_STATE_PREPARING,
+ PW_ENDPOINT_LINK_STATE_INACTIVE,
+ PW_ENDPOINT_LINK_STATE_ACTIVE,
+};
+
+struct pw_session_info {
+#define PW_VERSION_SESSION_INFO 0
+ uint32_t version; /**< version of this structure */
+ uint32_t id; /**< the session id (global) */
+#define PW_SESSION_CHANGE_MASK_PROPS (1 << 0)
+#define PW_SESSION_CHANGE_MASK_PARAMS (1 << 1)
+#define PW_SESSION_CHANGE_MASK_ALL ((1 << 2)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ struct spa_dict *props; /**< extra properties */
+ struct spa_param_info *params; /**< parameters */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+struct pw_endpoint_info {
+#define PW_VERSION_ENDPOINT_INFO 0
+ uint32_t version; /**< version of this structure */
+ uint32_t id; /**< the endpoint id (global) */
+ char *name; /**< name of the endpoint */
+ char *media_class; /**< media class of the endpoint */
+ enum pw_direction direction; /**< direction of the endpoint */
+#define PW_ENDPOINT_FLAG_PROVIDES_SESSION (1 << 0)
+ uint32_t flags; /**< additional flags */
+#define PW_ENDPOINT_CHANGE_MASK_STREAMS (1 << 0)
+#define PW_ENDPOINT_CHANGE_MASK_SESSION (1 << 1)
+#define PW_ENDPOINT_CHANGE_MASK_PROPS (1 << 2)
+#define PW_ENDPOINT_CHANGE_MASK_PARAMS (1 << 3)
+#define PW_ENDPOINT_CHANGE_MASK_ALL ((1 << 4)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ uint32_t n_streams; /**< number of streams available */
+ uint32_t session_id; /**< the id of the controlling session */
+ struct spa_dict *props; /**< extra properties */
+ struct spa_param_info *params; /**< parameters */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+struct pw_endpoint_stream_info {
+#define PW_VERSION_ENDPOINT_STREAM_INFO 0
+ uint32_t version; /**< version of this structure */
+ uint32_t id; /**< the stream id (local or global) */
+ uint32_t endpoint_id; /**< the endpoint id (global) */
+ char *name; /**< name of the stream */
+#define PW_ENDPOINT_STREAM_CHANGE_MASK_LINK_PARAMS (1 << 0)
+#define PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS (1 << 1)
+#define PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS (1 << 2)
+#define PW_ENDPOINT_STREAM_CHANGE_MASK_ALL ((1 << 3)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ struct spa_pod *link_params; /**< information for linking this stream */
+ struct spa_dict *props; /**< extra properties */
+ struct spa_param_info *params; /**< parameters */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+struct pw_endpoint_link_info {
+#define PW_VERSION_ENDPOINT_LINK_INFO 0
+ uint32_t version; /**< version of this structure */
+ uint32_t id; /**< the link id (global) */
+ uint32_t session_id; /**< the session id (global) */
+ uint32_t output_endpoint_id; /**< the output endpoint id (global) */
+ uint32_t output_stream_id; /**< the output stream id (local or global) */
+ uint32_t input_endpoint_id; /**< the input endpoint id (global) */
+ uint32_t input_stream_id; /**< the input stream id (local or global) */
+#define PW_ENDPOINT_LINK_CHANGE_MASK_STATE (1 << 0)
+#define PW_ENDPOINT_LINK_CHANGE_MASK_PROPS (1 << 1)
+#define PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS (1 << 2)
+#define PW_ENDPOINT_LINK_CHANGE_MASK_ALL ((1 << 3)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ enum pw_endpoint_link_state state; /**< the state of the link */
+ char *error; /**< error string if state == ERROR */
+ struct spa_dict *props; /**< extra properties */
+ struct spa_param_info *params; /**< parameters */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_EXT_SESSION_MANAGER_INTROSPECT_H */
diff --git a/src/pipewire/extensions/session-manager/keys.h b/src/pipewire/extensions/session-manager/keys.h
new file mode 100644
index 0000000..9ff89f9
--- /dev/null
+++ b/src/pipewire/extensions/session-manager/keys.h
@@ -0,0 +1,67 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_EXT_SESSION_MANAGER_KEYS_H
+#define PIPEWIRE_EXT_SESSION_MANAGER_KEYS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup pw_session_manager
+ * \{
+ */
+
+#define PW_KEY_SESSION_ID "session.id" /**< id of a session manager */
+
+#define PW_KEY_ENDPOINT_ID "endpoint.id" /**< id of an endpoint */
+#define PW_KEY_ENDPOINT_NAME "endpoint.name" /**< the name of an endpoint */
+#define PW_KEY_ENDPOINT_MONITOR "endpoint.monitor" /**< endpoint is monitor of given endpoint */
+#define PW_KEY_ENDPOINT_CLIENT_ID "endpoint.client.id" /**< client of the endpoint */
+#define PW_KEY_ENDPOINT_ICON_NAME "endpoint.icon-name" /**< an XDG icon name for the device.
+ * Ex. "sound-card-speakers-usb" */
+#define PW_KEY_ENDPOINT_AUTOCONNECT "endpoint.autoconnect" /**< try to automatically connect this
+ * endpoint. */
+#define PW_KEY_ENDPOINT_TARGET "endpoint.target" /**< the suggested target to connect to */
+
+#define PW_KEY_ENDPOINT_STREAM_ID "endpoint-stream.id" /**< id of a stream */
+#define PW_KEY_ENDPOINT_STREAM_NAME "endpoint-stream.name" /**< unique name of a stream */
+#define PW_KEY_ENDPOINT_STREAM_DESCRIPTION "endpoint-stream.description" /**< description of a stream */
+
+#define PW_KEY_ENDPOINT_LINK_OUTPUT_ENDPOINT "endpoint-link.output.endpoint" /**< output endpoint of link */
+#define PW_KEY_ENDPOINT_LINK_OUTPUT_STREAM "endpoint-link.output.stream" /**< output stream of link */
+#define PW_KEY_ENDPOINT_LINK_INPUT_ENDPOINT "endpoint-link.input.endpoint" /**< input endpoint of link */
+#define PW_KEY_ENDPOINT_LINK_INPUT_STREAM "endpoint-link.input.stream" /**< input stream of link */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_EXT_SESSION_MANAGER_KEYS_H */
diff --git a/src/pipewire/factory.h b/src/pipewire/factory.h
new file mode 100644
index 0000000..c747dd9
--- /dev/null
+++ b/src/pipewire/factory.h
@@ -0,0 +1,123 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_FACTORY_H
+#define PIPEWIRE_FACTORY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include <pipewire/proxy.h>
+
+/** \defgroup pw_factory Factory
+ * Factory interface
+ */
+
+/**
+ * \addtogroup pw_factory
+ * \{
+ */
+#define PW_TYPE_INTERFACE_Factory PW_TYPE_INFO_INTERFACE_BASE "Factory"
+
+#define PW_VERSION_FACTORY 3
+struct pw_factory;
+
+/** The factory information. Extra information can be added in later versions */
+struct pw_factory_info {
+ uint32_t id; /**< id of the global */
+ const char *name; /**< name the factory */
+ const char *type; /**< type of the objects created by this factory */
+ uint32_t version; /**< version of the objects */
+#define PW_FACTORY_CHANGE_MASK_PROPS (1 << 0)
+#define PW_FACTORY_CHANGE_MASK_ALL ((1 << 1)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ struct spa_dict *props; /**< the properties of the factory */
+};
+
+struct pw_factory_info *
+pw_factory_info_update(struct pw_factory_info *info,
+ const struct pw_factory_info *update);
+struct pw_factory_info *
+pw_factory_info_merge(struct pw_factory_info *info,
+ const struct pw_factory_info *update, bool reset);
+void
+pw_factory_info_free(struct pw_factory_info *info);
+
+
+#define PW_FACTORY_EVENT_INFO 0
+#define PW_FACTORY_EVENT_NUM 1
+
+/** Factory events */
+struct pw_factory_events {
+#define PW_VERSION_FACTORY_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify factory info
+ *
+ * \param info info about the factory
+ */
+ void (*info) (void *data, const struct pw_factory_info *info);
+};
+
+#define PW_FACTORY_METHOD_ADD_LISTENER 0
+#define PW_FACTORY_METHOD_NUM 1
+
+/** Factory methods */
+struct pw_factory_methods {
+#define PW_VERSION_FACTORY_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_factory_events *events,
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_FACTORY_H */
diff --git a/src/pipewire/filter.c b/src/pipewire/filter.c
new file mode 100644
index 0000000..4ddf081
--- /dev/null
+++ b/src/pipewire/filter.c
@@ -0,0 +1,1962 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <math.h>
+#include <sys/mman.h>
+#include <time.h>
+
+#include <spa/buffer/alloc.h>
+#include <spa/param/props.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/string.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/debug/types.h>
+
+#include "pipewire/pipewire.h"
+#include "pipewire/filter.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_filter);
+#define PW_LOG_TOPIC_DEFAULT log_filter
+
+#define MAX_SAMPLES 8192
+#define MAX_BUFFERS 64
+
+#define MASK_BUFFERS (MAX_BUFFERS-1)
+
+static bool mlock_warned = false;
+
+static uint32_t mappable_dataTypes = (1<<SPA_DATA_MemFd);
+
+struct buffer {
+ struct pw_buffer this;
+ uint32_t id;
+#define BUFFER_FLAG_MAPPED (1 << 0)
+#define BUFFER_FLAG_QUEUED (1 << 1)
+#define BUFFER_FLAG_ADDED (1 << 2)
+ uint32_t flags;
+};
+
+struct queue {
+ uint32_t ids[MAX_BUFFERS];
+ struct spa_ringbuffer ring;
+ uint64_t incount;
+ uint64_t outcount;
+};
+
+struct data {
+ struct pw_context *context;
+ struct spa_hook filter_listener;
+};
+
+struct param {
+ uint32_t id;
+#define PARAM_FLAG_LOCKED (1 << 0)
+ uint32_t flags;
+ struct spa_list link;
+ struct spa_pod *param;
+};
+
+struct port {
+ struct spa_list link;
+
+ struct filter *filter;
+
+ enum spa_direction direction;
+ uint32_t id;
+ uint32_t flags;
+ struct pw_port *port;
+
+ struct pw_properties *props;
+
+ uint32_t change_mask_all;
+ struct spa_port_info info;
+ struct spa_list param_list;
+#define IDX_EnumFormat 0
+#define IDX_Meta 1
+#define IDX_IO 2
+#define IDX_Format 3
+#define IDX_Buffers 4
+#define IDX_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct spa_io_buffers *io;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct queue dequeued;
+ struct queue queued;
+
+ struct spa_latency_info latency[2];
+
+ /* from here is what the caller gets as user_data */
+ uint8_t user_data[0];
+};
+
+struct filter {
+ struct pw_filter this;
+
+ const char *path;
+
+ struct pw_context *context;
+
+ enum pw_filter_flags flags;
+
+ struct spa_node impl_node;
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+ struct spa_io_position *position;
+
+ struct {
+ struct spa_io_position *position;
+ } rt;
+
+ struct spa_list port_list;
+ struct pw_map ports[2];
+
+ uint32_t change_mask_all;
+ struct spa_node_info info;
+ struct spa_list param_list;
+#define IDX_PropInfo 0
+#define IDX_Props 1
+#define IDX_ProcessLatency 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+
+ struct spa_process_latency_info process_latency;
+
+ struct data data;
+ uintptr_t seq;
+ struct pw_time time;
+ uint64_t base_pos;
+ uint32_t clock_id;
+
+ struct spa_callbacks rt_callbacks;
+
+ unsigned int disconnecting:1;
+ unsigned int disconnect_core:1;
+ unsigned int subscribe:1;
+ unsigned int draining:1;
+ unsigned int allow_mlock:1;
+ unsigned int warn_mlock:1;
+ unsigned int process_rt:1;
+};
+
+static int get_param_index(uint32_t id)
+{
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ return IDX_PropInfo;
+ case SPA_PARAM_Props:
+ return IDX_Props;
+ case SPA_PARAM_ProcessLatency:
+ return IDX_ProcessLatency;
+ default:
+ return -1;
+ }
+}
+
+static int get_port_param_index(uint32_t id)
+{
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ return IDX_EnumFormat;
+ case SPA_PARAM_Meta:
+ return IDX_Meta;
+ case SPA_PARAM_IO:
+ return IDX_IO;
+ case SPA_PARAM_Format:
+ return IDX_Format;
+ case SPA_PARAM_Buffers:
+ return IDX_Buffers;
+ case SPA_PARAM_Latency:
+ return IDX_Latency;
+ default:
+ return -1;
+ }
+}
+
+static void fix_datatype(const struct spa_pod *param)
+{
+ const struct spa_pod_prop *pod_param;
+ const struct spa_pod *vals;
+ uint32_t dataType, n_vals, choice;
+
+ pod_param = spa_pod_find_prop(param, NULL, SPA_PARAM_BUFFERS_dataType);
+ if (pod_param == NULL)
+ return;
+
+ vals = spa_pod_get_values(&pod_param->value, &n_vals, &choice);
+ if (n_vals == 0)
+ return;
+
+ if (spa_pod_get_int(&vals[0], (int32_t*)&dataType) < 0)
+ return;
+
+ pw_log_debug("dataType: %u", dataType);
+ if (dataType & (1u << SPA_DATA_MemPtr)) {
+ SPA_POD_VALUE(struct spa_pod_int, &vals[0]) =
+ dataType | mappable_dataTypes;
+ pw_log_debug("Change dataType: %u -> %u", dataType,
+ SPA_POD_VALUE(struct spa_pod_int, &vals[0]));
+ }
+}
+
+static struct param *add_param(struct filter *impl, struct port *port,
+ uint32_t id, uint32_t flags, const struct spa_pod *param)
+{
+ struct param *p;
+ int idx;
+
+ if (param == NULL || !spa_pod_is_object(param)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ if (id == SPA_ID_INVALID)
+ id = SPA_POD_OBJECT_ID(param);
+
+ p = malloc(sizeof(struct param) + SPA_POD_SIZE(param));
+ if (p == NULL)
+ return NULL;
+
+ if (id == SPA_PARAM_Buffers && port != NULL &&
+ SPA_FLAG_IS_SET(port->flags, PW_FILTER_PORT_FLAG_MAP_BUFFERS) &&
+ port->direction == SPA_DIRECTION_INPUT)
+ fix_datatype(param);
+
+ if (id == SPA_PARAM_ProcessLatency && port == NULL)
+ spa_process_latency_parse(param, &impl->process_latency);
+
+ p->id = id;
+ p->flags = flags;
+ p->param = SPA_PTROFF(p, sizeof(struct param), struct spa_pod);
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ SPA_POD_OBJECT_ID(p->param) = id;
+
+ pw_log_debug("%p: port %p param id %d (%s)", impl, p, id,
+ spa_debug_type_find_name(spa_type_param, id));
+
+ if (port) {
+ idx = get_port_param_index(id);
+ spa_list_append(&port->param_list, &p->link);
+ if (idx != -1) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[idx].flags |= SPA_PARAM_INFO_READ;
+ port->params[idx].user++;
+ }
+ } else {
+ idx = get_param_index(id);
+ spa_list_append(&impl->param_list, &p->link);
+ if (idx != -1) {
+ impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ impl->params[idx].flags |= SPA_PARAM_INFO_READ;
+ impl->params[idx].user++;
+ }
+ }
+ return p;
+}
+
+static void clear_params(struct filter *impl, struct port *port, uint32_t id)
+{
+ struct param *p, *t;
+ struct spa_list *param_list;
+
+ if (port)
+ param_list = &port->param_list;
+ else
+ param_list = &impl->param_list;
+
+ spa_list_for_each_safe(p, t, param_list, link) {
+ if (id == SPA_ID_INVALID ||
+ (p->id == id && !(p->flags & PARAM_FLAG_LOCKED))) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+}
+
+static struct port *alloc_port(struct filter *filter,
+ enum spa_direction direction, uint32_t user_data_size)
+{
+ struct port *p;
+
+ p = calloc(1, sizeof(struct port) + user_data_size);
+ p->filter = filter;
+ p->direction = direction;
+ p->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ p->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ spa_list_init(&p->param_list);
+ spa_ringbuffer_init(&p->dequeued.ring);
+ spa_ringbuffer_init(&p->queued.ring);
+ p->id = pw_map_insert_new(&filter->ports[direction], p);
+ spa_list_append(&filter->port_list, &p->link);
+
+ return p;
+}
+
+static inline struct port *get_port(struct filter *filter, enum spa_direction direction, uint32_t port_id)
+{
+ if ((direction != SPA_DIRECTION_INPUT && direction != SPA_DIRECTION_OUTPUT))
+ return NULL;
+ return pw_map_lookup(&filter->ports[direction], port_id);
+}
+
+static inline int push_queue(struct port *port, struct queue *queue, struct buffer *buffer)
+{
+ uint32_t index;
+
+ if (SPA_FLAG_IS_SET(buffer->flags, BUFFER_FLAG_QUEUED))
+ return -EINVAL;
+
+ SPA_FLAG_SET(buffer->flags, BUFFER_FLAG_QUEUED);
+ queue->incount += buffer->this.size;
+
+ spa_ringbuffer_get_write_index(&queue->ring, &index);
+ queue->ids[index & MASK_BUFFERS] = buffer->id;
+ spa_ringbuffer_write_update(&queue->ring, index + 1);
+
+ return 0;
+}
+
+static inline struct buffer *pop_queue(struct port *port, struct queue *queue)
+{
+ uint32_t index, id;
+ struct buffer *buffer;
+
+ if (spa_ringbuffer_get_read_index(&queue->ring, &index) < 1) {
+ errno = EPIPE;
+ return NULL;
+ }
+
+ id = queue->ids[index & MASK_BUFFERS];
+ spa_ringbuffer_read_update(&queue->ring, index + 1);
+
+ buffer = &port->buffers[id];
+ queue->outcount += buffer->this.size;
+ SPA_FLAG_CLEAR(buffer->flags, BUFFER_FLAG_QUEUED);
+
+ return buffer;
+}
+
+static inline void clear_queue(struct port *port, struct queue *queue)
+{
+ spa_ringbuffer_init(&queue->ring);
+ queue->incount = queue->outcount;
+}
+
+static bool filter_set_state(struct pw_filter *filter, enum pw_filter_state state, const char *error)
+{
+ enum pw_filter_state old = filter->state;
+ bool res = old != state;
+
+ if (res) {
+ free(filter->error);
+ filter->error = error ? strdup(error) : NULL;
+
+ pw_log_debug("%p: update state from %s -> %s (%s)", filter,
+ pw_filter_state_as_string(old),
+ pw_filter_state_as_string(state), filter->error);
+
+ if (state == PW_FILTER_STATE_ERROR)
+ pw_log_error("%p: error %s", filter, error);
+
+ filter->state = state;
+ pw_filter_emit_state_changed(filter, old, state, error);
+ }
+ return res;
+}
+
+static int enum_params(struct filter *d, struct spa_list *param_list, int seq,
+ uint32_t id, uint32_t start, uint32_t num, const struct spa_pod *filter)
+{
+ struct spa_result_node_params result;
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ uint32_t count = 0;
+ struct param *p;
+ bool found = false;
+
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = 0;
+
+ pw_log_debug("%p: %p param id %d (%s) start:%d num:%d", d, param_list, id,
+ spa_debug_type_find_name(spa_type_param, id),
+ start, num);
+
+ spa_list_for_each(p, param_list, link) {
+ struct spa_pod *param;
+
+ param = p->param;
+ if (param == NULL || p->id != id)
+ continue;
+
+ found = true;
+
+ result.index = result.next++;
+ if (result.index < start)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ if (spa_pod_filter(&b.b, &result.param, param, filter) == 0) {
+ spa_node_emit_result(&d->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+static int impl_enum_params(void *object, int seq, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct filter *impl = object;
+ return enum_params(impl, &impl->param_list, seq, id, start, num, filter);
+}
+
+static int impl_set_param(void *object, uint32_t id, uint32_t flags, const struct spa_pod *param)
+{
+ struct filter *impl = object;
+ struct pw_filter *filter = &impl->this;
+
+ if (id != SPA_PARAM_Props)
+ return -ENOTSUP;
+
+ pw_filter_emit_param_changed(filter, NULL, id, param);
+ return 0;
+}
+
+static int
+do_set_position(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct filter *impl = user_data;
+ impl->rt.position = impl->position;
+ return 0;
+}
+
+static int impl_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct filter *impl = object;
+
+ pw_log_debug("%p: io %d %p/%zd", impl, id, data, size);
+
+ switch(id) {
+ case SPA_IO_Position:
+ if (data && size >= sizeof(struct spa_io_position))
+ impl->position = data;
+ else
+ impl->position = NULL;
+ pw_loop_invoke(impl->context->data_loop,
+ do_set_position, 1, NULL, 0, true, impl);
+ break;
+ }
+ pw_filter_emit_io_changed(&impl->this, NULL, id, data, size);
+
+ return 0;
+}
+
+static int impl_send_command(void *object, const struct spa_command *command)
+{
+ struct filter *impl = object;
+ struct pw_filter *filter = &impl->this;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Flush:
+ case SPA_NODE_COMMAND_Pause:
+ pw_loop_invoke(impl->context->main_loop,
+ NULL, 0, NULL, 0, false, impl);
+ if (filter->state == PW_FILTER_STATE_STREAMING) {
+ pw_log_debug("%p: pause", filter);
+ filter_set_state(filter, PW_FILTER_STATE_PAUSED, NULL);
+ }
+ break;
+ case SPA_NODE_COMMAND_Start:
+ if (filter->state == PW_FILTER_STATE_PAUSED) {
+ pw_log_debug("%p: start", filter);
+ filter_set_state(filter, PW_FILTER_STATE_STREAMING, NULL);
+ }
+ break;
+ default:
+ break;
+ }
+ pw_filter_emit_command(filter, command);
+ return 0;
+}
+
+static void emit_node_info(struct filter *d, bool full)
+{
+ uint32_t i;
+ uint64_t old = full ? d->info.change_mask : 0;
+ if (full)
+ d->info.change_mask = d->change_mask_all;
+ if (d->info.change_mask != 0) {
+ if (d->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < d->info.n_params; i++) {
+ if (d->params[i].user > 0) {
+ d->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ d->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_info(&d->hooks, &d->info);
+ }
+ d->info.change_mask = old;
+}
+
+static void emit_port_info(struct filter *d, struct port *p, bool full)
+{
+ uint32_t i;
+ uint64_t old = full ? p->info.change_mask : 0;
+ if (full)
+ p->info.change_mask = p->change_mask_all;
+ if (p->info.change_mask != 0) {
+ if (p->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < p->info.n_params; i++) {
+ if (p->params[i].user > 0) {
+ p->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ p->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_port_info(&d->hooks, p->direction, p->id, &p->info);
+ }
+ p->info.change_mask = old;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct filter *d = object;
+ struct spa_hook_list save;
+ struct port *p;
+
+ spa_hook_list_isolate(&d->hooks, &save, listener, events, data);
+
+ emit_node_info(d, true);
+
+ spa_list_for_each(p, &d->port_list, link)
+ emit_port_info(d, p, true);
+
+ spa_hook_list_join(&d->hooks, &save);
+
+ return 0;
+}
+
+static int impl_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks, void *data)
+{
+ struct filter *d = object;
+
+ d->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_port_set_io(void *object, enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct filter *impl = object;
+ struct port *port;
+
+ pw_log_debug("%p: id:%d (%s) %p %zd", impl, id,
+ spa_debug_type_find_name(spa_type_io, id), data, size);
+
+ if ((port = get_port(impl, direction, port_id)) == NULL)
+ return -EINVAL;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ if (data && size >= sizeof(struct spa_io_buffers))
+ port->io = data;
+ else
+ port->io = NULL;
+ break;
+ }
+
+ pw_filter_emit_io_changed(&impl->this, port->user_data, id, data, size);
+
+ return 0;
+}
+
+static int impl_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 filter *d = object;
+ struct port *port;
+
+ if ((port = get_port(d, direction, port_id)) == NULL)
+ return -EINVAL;
+
+ return enum_params(d, &port->param_list, seq, id, start, num, filter);
+}
+
+static int update_params(struct filter *impl, struct port *port, uint32_t id,
+ const struct spa_pod **params, uint32_t n_params)
+{
+ uint32_t i;
+ int res = 0;
+ bool update_latency = false;
+
+ if (id != SPA_ID_INVALID) {
+ clear_params(impl, port, id);
+ } else {
+ for (i = 0; i < n_params; i++) {
+ if (params[i] == NULL || !spa_pod_is_object(params[i]))
+ continue;
+ clear_params(impl, port, SPA_POD_OBJECT_ID(params[i]));
+ }
+ }
+ for (i = 0; i < n_params; i++) {
+ if (params[i] == NULL)
+ continue;
+
+ if (port != NULL &&
+ spa_pod_is_object(params[i]) &&
+ SPA_POD_OBJECT_ID(params[i]) == SPA_PARAM_Latency) {
+ struct spa_latency_info info;
+ if (spa_latency_parse(params[i], &info) >= 0) {
+ port->latency[info.direction] = info;
+ pw_log_debug("port %p: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, port,
+ 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);
+ update_latency = true;
+ }
+ continue;
+ }
+ if (add_param(impl, port, id, 0, params[i]) == NULL) {
+ res = -errno;
+ break;
+ }
+ }
+ if (port != NULL && update_latency) {
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ add_param(impl, port, SPA_PARAM_Latency, 0,
+ spa_latency_build(&b, SPA_PARAM_Latency, &port->latency[0]));
+ add_param(impl, port, SPA_PARAM_Latency, 0,
+ spa_latency_build(&b, SPA_PARAM_Latency, &port->latency[1]));
+ }
+ return res;
+}
+
+static int map_data(struct filter *impl, struct spa_data *data, int prot)
+{
+ void *ptr;
+ struct pw_map_range range;
+
+ pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize);
+
+ ptr = mmap(NULL, range.size, prot, MAP_SHARED, data->fd, range.offset);
+ if (ptr == MAP_FAILED) {
+ pw_log_error("%p: failed to mmap buffer mem: %m", impl);
+ return -errno;
+ }
+ data->data = SPA_PTROFF(ptr, range.start, void);
+ pw_log_debug("%p: fd %"PRIi64" mapped %d %d %p", impl, data->fd,
+ range.offset, range.size, data->data);
+
+ if (impl->allow_mlock && mlock(data->data, data->maxsize) < 0) {
+ if (errno != ENOMEM || !mlock_warned) {
+ pw_log(impl->warn_mlock ? SPA_LOG_LEVEL_WARN : SPA_LOG_LEVEL_DEBUG,
+ "%p: Failed to mlock memory %p %u: %s", impl,
+ data->data, data->maxsize,
+ errno == ENOMEM ?
+ "This is not a problem but for best performance, "
+ "consider increasing RLIMIT_MEMLOCK" : strerror(errno));
+ mlock_warned |= errno == ENOMEM;
+ }
+ }
+ return 0;
+}
+
+static int unmap_data(struct filter *impl, struct spa_data *data)
+{
+ struct pw_map_range range;
+
+ pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize);
+
+ if (munmap(SPA_PTROFF(data->data, -range.start, void), range.size) < 0)
+ pw_log_warn("%p: failed to unmap: %m", impl);
+
+ pw_log_debug("%p: fd %"PRIi64" unmapped", impl, data->fd);
+ return 0;
+}
+
+static void clear_buffers(struct port *port)
+{
+ uint32_t i, j;
+ struct filter *impl = port->filter;
+
+ pw_log_debug("%p: clear buffers %d", impl, port->n_buffers);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_ADDED))
+ pw_filter_emit_remove_buffer(&impl->this, port->user_data, &b->this);
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) {
+ for (j = 0; j < b->this.buffer->n_datas; j++) {
+ struct spa_data *d = &b->this.buffer->datas[j];
+ pw_log_debug("%p: clear buffer %d mem",
+ impl, b->id);
+ unmap_data(impl, d);
+ }
+ }
+ }
+ port->n_buffers = 0;
+ clear_queue(port, &port->dequeued);
+ clear_queue(port, &port->queued);
+}
+
+
+static int default_latency(struct filter *impl, struct port *port, enum spa_direction direction)
+{
+ struct pw_filter *filter = &impl->this;
+ struct spa_latency_info info;
+ struct port *p;
+
+ spa_latency_info_combine_start(&info, direction);
+ spa_list_for_each(p, &impl->port_list, link) {
+ if (p->direction == direction)
+ continue;
+ spa_latency_info_combine(&info, &p->latency[direction]);
+ }
+ spa_latency_info_combine_finish(&info);
+
+ spa_process_latency_info_add(&impl->process_latency, &info);
+
+ spa_list_for_each(p, &impl->port_list, link) {
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ const struct spa_pod *params[1];
+
+ if (p->direction != direction)
+ continue;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &info);
+ pw_filter_update_params(filter, p->user_data, params, 1);
+ }
+ return 0;
+}
+
+static int handle_latency(struct filter *impl, struct port *port, const struct spa_pod *param)
+{
+ struct pw_filter *filter = &impl->this;
+ struct spa_latency_info info;
+ int res;
+
+ if (param == NULL)
+ return 0;
+
+ if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+
+ pw_log_info("port %p: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, port,
+ 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 (info.direction == port->direction)
+ return 0;
+
+ if (SPA_FLAG_IS_SET(impl->flags, PW_FILTER_FLAG_CUSTOM_LATENCY)) {
+ pw_filter_emit_param_changed(filter, port->user_data,
+ SPA_PARAM_Latency, param);
+ } else {
+ default_latency(impl, port, info.direction);
+ }
+ return 0;
+}
+
+static int impl_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 filter *impl = object;
+ struct pw_filter *filter = &impl->this;
+ struct port *port;
+ int res;
+ bool emit = true;
+ const struct spa_pod *params[1];
+ uint32_t n_params = 0;
+
+ pw_log_debug("%p: port:%d.%d id:%d (%s) param:%p disconnecting:%d", impl,
+ direction, port_id, id,
+ spa_debug_type_find_name(spa_type_param, id), param,
+ impl->disconnecting);
+
+ if (impl->disconnecting && param != NULL)
+ return -EIO;
+
+ if ((port = get_port(impl, direction, port_id)) == NULL)
+ return -EINVAL;
+
+ if (param)
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, param);
+
+ params[0] = param;
+ n_params = param ? 1 : 0;
+
+ if ((res = update_params(impl, port, id, params, n_params)) < 0)
+ return res;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ clear_buffers(port);
+ break;
+ case SPA_PARAM_Latency:
+ handle_latency(impl, port, param);
+ emit = false;
+ break;
+ }
+
+ if (emit)
+ pw_filter_emit_param_changed(filter, port->user_data, id, param);
+
+ if (filter->state == PW_FILTER_STATE_ERROR)
+ return -EIO;
+
+ emit_port_info(impl, port, false);
+
+ return res;
+}
+
+static int impl_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 filter *impl = object;
+ struct port *port;
+ struct pw_filter *filter = &impl->this;
+ uint32_t i, j, impl_flags;
+ int prot, res;
+ int size = 0;
+
+ pw_log_debug("%p: port:%d.%d buffers:%u disconnecting:%d", impl,
+ direction, port_id, n_buffers, impl->disconnecting);
+
+ if ((port = get_port(impl, direction, port_id)) == NULL)
+ return -EINVAL;
+
+ if (impl->disconnecting && n_buffers > 0)
+ return -EIO;
+
+ clear_buffers(port);
+
+ impl_flags = port->flags;
+ prot = PROT_READ | (direction == SPA_DIRECTION_OUTPUT ? PROT_WRITE : 0);
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ int buf_size = 0;
+ struct buffer *b = &port->buffers[i];
+
+ b->flags = 0;
+ b->id = i;
+
+ if (SPA_FLAG_IS_SET(impl_flags, PW_FILTER_PORT_FLAG_MAP_BUFFERS)) {
+ for (j = 0; j < buffers[i]->n_datas; j++) {
+ struct spa_data *d = &buffers[i]->datas[j];
+ if ((mappable_dataTypes & (1<<d->type)) > 0) {
+ if ((res = map_data(impl, d, prot)) < 0)
+ return res;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED);
+ }
+ else if (d->type == SPA_DATA_MemPtr && d->data == NULL) {
+ pw_log_error("%p: invalid buffer mem", filter);
+ return -EINVAL;
+ }
+ buf_size += d->maxsize;
+ }
+
+ if (size > 0 && buf_size != size) {
+ pw_log_error("%p: invalid buffer size %d", filter, buf_size);
+ return -EINVAL;
+ } else
+ size = buf_size;
+ }
+ pw_log_debug("%p: got buffer %d %d datas, mapped size %d", filter, i,
+ buffers[i]->n_datas, size);
+ }
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+
+ b->this.buffer = buffers[i];
+
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ pw_log_trace("%p: recycle buffer %d", filter, b->id);
+ push_queue(port, &port->dequeued, b);
+ }
+
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_ADDED);
+ pw_filter_emit_add_buffer(filter, port->user_data, &b->this);
+ }
+
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct filter *impl = object;
+ struct port *port;
+
+ if ((port = get_port(impl, SPA_DIRECTION_OUTPUT, port_id)) == NULL)
+ return -EINVAL;
+
+ pw_log_trace("%p: recycle buffer %d", impl, buffer_id);
+ if (buffer_id < port->n_buffers)
+ push_queue(port, &port->queued, &port->buffers[buffer_id]);
+
+ return 0;
+}
+
+static inline void copy_position(struct filter *impl)
+{
+ struct spa_io_position *p = impl->rt.position;
+ if (SPA_UNLIKELY(p != NULL)) {
+ SEQ_WRITE(impl->seq);
+ impl->time.now = p->clock.nsec;
+ impl->time.rate = p->clock.rate;
+ if (SPA_UNLIKELY(impl->clock_id != p->clock.id)) {
+ impl->base_pos = p->clock.position - impl->time.ticks;
+ impl->clock_id = p->clock.id;
+ }
+ impl->time.ticks = p->clock.position - impl->base_pos;
+ impl->time.delay = 0;
+ SEQ_WRITE(impl->seq);
+ }
+}
+
+static int
+do_call_process(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct filter *impl = user_data;
+ struct pw_filter *filter = &impl->this;
+ pw_log_trace("%p: do process", filter);
+ pw_filter_emit_process(filter, impl->position);
+ return 0;
+}
+
+static void call_process(struct filter *impl)
+{
+ pw_log_trace("%p: call process", impl);
+ if (SPA_FLAG_IS_SET(impl->flags, PW_FILTER_FLAG_RT_PROCESS)) {
+ spa_callbacks_call(&impl->rt_callbacks, struct pw_filter_events,
+ process, 0, impl->rt.position);
+ }
+ else {
+ pw_loop_invoke(impl->context->main_loop,
+ do_call_process, 1, NULL, 0, false, impl);
+ }
+}
+
+static int
+do_call_drained(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct filter *impl = user_data;
+ struct pw_filter *filter = &impl->this;
+ pw_log_trace("%p: drained", filter);
+ pw_filter_emit_drained(filter);
+ impl->draining = false;
+ return 0;
+}
+
+static void call_drained(struct filter *impl)
+{
+ pw_loop_invoke(impl->context->main_loop,
+ do_call_drained, 1, NULL, 0, false, impl);
+}
+
+static int impl_node_process(void *object)
+{
+ struct filter *impl = object;
+ struct port *p;
+ struct buffer *b;
+ bool drained = true;
+
+ pw_log_trace("%p: do process %p", impl, impl->rt.position);
+
+ /** first dequeue and recycle buffers */
+ spa_list_for_each(p, &impl->port_list, link) {
+ struct spa_io_buffers *io = p->io;
+
+ if (io == NULL ||
+ io->buffer_id >= p->n_buffers)
+ continue;
+
+ if (p->direction == SPA_DIRECTION_INPUT) {
+ if (io->status != SPA_STATUS_HAVE_DATA)
+ continue;
+
+ /* push new buffer */
+ b = &p->buffers[io->buffer_id];
+ pw_log_trace("%p: dequeue buffer %d", impl, b->id);
+ push_queue(p, &p->dequeued, b);
+ drained = false;
+ } else {
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ continue;
+
+ /* recycle old buffer */
+ b = &p->buffers[io->buffer_id];
+ pw_log_trace("%p: recycle buffer %d", impl, b->id);
+ push_queue(p, &p->dequeued, b);
+ }
+ }
+
+ copy_position(impl);
+ call_process(impl);
+
+ /** recycle/push queued buffers */
+ spa_list_for_each(p, &impl->port_list, link) {
+ struct spa_io_buffers *io = p->io;
+
+ if (io == NULL)
+ continue;
+
+ if (p->direction == SPA_DIRECTION_INPUT) {
+ if (io->status != SPA_STATUS_HAVE_DATA)
+ continue;
+
+ /* pop buffer to recycle */
+ if ((b = pop_queue(p, &p->queued)) != NULL) {
+ pw_log_trace("%p: recycle buffer %d", impl, b->id);
+ io->buffer_id = b->id;
+ } else {
+ io->buffer_id = SPA_ID_INVALID;
+ }
+ io->status = SPA_STATUS_NEED_DATA;
+ } else {
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ continue;
+
+ if ((b = pop_queue(p, &p->queued)) != NULL) {
+ pw_log_trace("%p: pop %d %p", impl, b->id, io);
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ drained = false;
+ } else {
+ io->buffer_id = SPA_ID_INVALID;
+ io->status = SPA_STATUS_NEED_DATA;
+ }
+ }
+ }
+ if (drained && impl->draining)
+ call_drained(impl);
+
+ return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_add_listener,
+ .set_callbacks = impl_set_callbacks,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+ .set_io = impl_set_io,
+ .send_command = impl_send_command,
+ .port_set_io = impl_port_set_io,
+ .port_enum_params = impl_port_enum_params,
+ .port_set_param = impl_port_set_param,
+ .port_use_buffers = impl_port_use_buffers,
+ .port_reuse_buffer = impl_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static void proxy_removed(void *_data)
+{
+ struct pw_filter *filter = _data;
+ pw_log_debug("%p: removed", filter);
+ spa_hook_remove(&filter->proxy_listener);
+ filter->node_id = SPA_ID_INVALID;
+ filter_set_state(filter, PW_FILTER_STATE_UNCONNECTED, NULL);
+}
+
+static void proxy_destroy(void *_data)
+{
+ struct pw_filter *filter = _data;
+ pw_log_debug("%p: destroy", filter);
+ proxy_removed(_data);
+}
+
+static void proxy_error(void *_data, int seq, int res, const char *message)
+{
+ struct pw_filter *filter = _data;
+ /* 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() */
+ pw_filter_emit_state_changed(filter, filter->state,
+ PW_FILTER_STATE_ERROR, message);
+}
+
+static void proxy_bound(void *_data, uint32_t global_id)
+{
+ struct pw_filter *filter = _data;
+ filter->node_id = global_id;
+ filter_set_state(filter, PW_FILTER_STATE_PAUSED, NULL);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = proxy_removed,
+ .destroy = proxy_destroy,
+ .error = proxy_error,
+ .bound = proxy_bound,
+};
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct pw_filter *filter = _data;
+
+ pw_log_debug("%p: error id:%u seq:%d res:%d (%s): %s", filter,
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE) {
+ filter_set_state(filter, PW_FILTER_STATE_UNCONNECTED, message);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+struct match {
+ struct pw_filter *filter;
+ int count;
+};
+#define MATCH_INIT(f) ((struct match){ .filter = (f) })
+
+static int execute_match(void *data, const char *location, const char *action,
+ const char *val, size_t len)
+{
+ struct match *match = data;
+ struct pw_filter *this = match->filter;
+ if (spa_streq(action, "update-props"))
+ match->count += pw_properties_update_string(this->properties, val, len);
+ return 1;
+}
+
+static struct filter *
+filter_new(struct pw_context *context, const char *name,
+ struct pw_properties *props, const struct pw_properties *extra)
+{
+ struct filter *impl;
+ struct pw_filter *this;
+ const char *str;
+ struct match match;
+ int res;
+
+ impl = calloc(1, sizeof(struct filter));
+ if (impl == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ this = &impl->this;
+ pw_log_debug("%p: new", impl);
+
+ if (props == NULL) {
+ props = pw_properties_new(PW_KEY_MEDIA_NAME, name, NULL);
+ } else if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL) {
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, name);
+ }
+ if (props == NULL) {
+ res = -errno;
+ goto error_properties;
+ }
+ spa_hook_list_init(&impl->hooks);
+ this->properties = props;
+
+ pw_context_conf_update_props(context, "filter.properties", props);
+
+ match = MATCH_INIT(this);
+ pw_context_conf_section_match_rules(context, "filter.rules",
+ &this->properties->dict, execute_match, &match);
+
+ if ((str = getenv("PIPEWIRE_PROPS")) != NULL)
+ pw_properties_update_string(props, str, strlen(str));
+ if ((str = getenv("PIPEWIRE_QUANTUM")) != NULL) {
+ struct spa_fraction q;
+ if (sscanf(str, "%u/%u", &q.num, &q.denom) == 2 && q.denom != 0) {
+ pw_properties_setf(props, PW_KEY_NODE_RATE,
+ "1/%u", q.denom);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY,
+ "%u/%u", q.num, q.denom);
+ }
+ }
+ if ((str = getenv("PIPEWIRE_LATENCY")) != NULL)
+ pw_properties_set(props, PW_KEY_NODE_LATENCY, str);
+ if ((str = getenv("PIPEWIRE_RATE")) != NULL)
+ pw_properties_set(props, PW_KEY_NODE_RATE, str);
+
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL && extra) {
+ str = pw_properties_get(extra, PW_KEY_APP_NAME);
+ if (str == NULL)
+ str = pw_properties_get(extra, PW_KEY_APP_PROCESS_BINARY);
+ if (str == NULL)
+ str = name;
+ pw_properties_set(props, PW_KEY_NODE_NAME, str);
+ }
+
+ this->name = name ? strdup(name) : NULL;
+ this->node_id = SPA_ID_INVALID;
+
+ spa_list_init(&impl->param_list);
+ spa_list_init(&impl->port_list);
+ pw_map_init(&impl->ports[SPA_DIRECTION_INPUT], 32, 32);
+ pw_map_init(&impl->ports[SPA_DIRECTION_OUTPUT], 32, 32);
+
+ spa_hook_list_init(&this->listener_list);
+ spa_list_init(&this->controls);
+
+ this->state = PW_FILTER_STATE_UNCONNECTED;
+
+ impl->context = context;
+ impl->allow_mlock = context->settings.mem_allow_mlock;
+ impl->warn_mlock = context->settings.mem_warn_mlock;
+
+ return impl;
+
+error_properties:
+ free(impl);
+error_cleanup:
+ pw_properties_free(props);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_filter * pw_filter_new(struct pw_core *core, const char *name,
+ struct pw_properties *props)
+{
+ struct filter *impl;
+ struct pw_filter *this;
+ struct pw_context *context = core->context;
+
+ impl = filter_new(context, name, props, core->properties);
+ if (impl == NULL)
+ return NULL;
+
+ this = &impl->this;
+ this->core = core;
+ spa_list_append(&this->core->filter_list, &this->link);
+ pw_core_add_listener(core,
+ &this->core_listener, &core_events, this);
+
+ return this;
+}
+
+SPA_EXPORT
+struct pw_filter *
+pw_filter_new_simple(struct pw_loop *loop,
+ const char *name,
+ struct pw_properties *props,
+ const struct pw_filter_events *events,
+ void *data)
+{
+ struct pw_filter *this;
+ struct filter *impl;
+ struct pw_context *context;
+ int res;
+
+ if (props == NULL)
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ return NULL;
+
+ context = pw_context_new(loop, NULL, 0);
+ if (context == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ impl = filter_new(context, name, props, props);
+ if (impl == NULL) {
+ props = NULL;
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ this = &impl->this;
+
+ impl->data.context = context;
+ pw_filter_add_listener(this, &impl->data.filter_listener, events, data);
+
+ return this;
+
+error_cleanup:
+ if (context)
+ pw_context_destroy(context);
+ pw_properties_free(props);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+const char *pw_filter_state_as_string(enum pw_filter_state state)
+{
+ switch (state) {
+ case PW_FILTER_STATE_ERROR:
+ return "error";
+ case PW_FILTER_STATE_UNCONNECTED:
+ return "unconnected";
+ case PW_FILTER_STATE_CONNECTING:
+ return "connecting";
+ case PW_FILTER_STATE_PAUSED:
+ return "paused";
+ case PW_FILTER_STATE_STREAMING:
+ return "streaming";
+ }
+ return "invalid-state";
+}
+
+SPA_EXPORT
+void pw_filter_destroy(struct pw_filter *filter)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+ struct port *p;
+
+ pw_log_debug("%p: destroy", filter);
+
+ pw_filter_emit_destroy(filter);
+
+ if (!impl->disconnecting)
+ pw_filter_disconnect(filter);
+
+ spa_list_consume(p, &impl->port_list, link)
+ pw_filter_remove_port(p->user_data);
+
+ if (filter->core) {
+ spa_hook_remove(&filter->core_listener);
+ spa_list_remove(&filter->link);
+ }
+
+ clear_params(impl, NULL, SPA_ID_INVALID);
+
+ pw_log_debug("%p: free", filter);
+ free(filter->error);
+
+ pw_properties_free(filter->properties);
+
+ spa_hook_list_clean(&impl->hooks);
+ spa_hook_list_clean(&filter->listener_list);
+
+ pw_map_clear(&impl->ports[SPA_DIRECTION_INPUT]);
+ pw_map_clear(&impl->ports[SPA_DIRECTION_OUTPUT]);
+
+ free(filter->name);
+
+ if (impl->data.context)
+ pw_context_destroy(impl->data.context);
+
+ free(impl);
+}
+
+static void hook_removed(struct spa_hook *hook)
+{
+ struct filter *impl = hook->priv;
+ spa_zero(impl->rt_callbacks);
+ hook->priv = NULL;
+ hook->removed = NULL;
+}
+
+SPA_EXPORT
+void pw_filter_add_listener(struct pw_filter *filter,
+ struct spa_hook *listener,
+ const struct pw_filter_events *events,
+ void *data)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+ spa_hook_list_append(&filter->listener_list, listener, events, data);
+ if (events->process && impl->rt_callbacks.funcs == NULL) {
+ impl->rt_callbacks = SPA_CALLBACKS_INIT(events, data);
+ listener->removed = hook_removed;
+ listener->priv = impl;
+ }
+}
+
+SPA_EXPORT
+enum pw_filter_state pw_filter_get_state(struct pw_filter *filter, const char **error)
+{
+ if (error)
+ *error = filter->error;
+ return filter->state;
+}
+
+SPA_EXPORT
+struct pw_core *pw_filter_get_core(struct pw_filter *filter)
+{
+ return filter->core;
+}
+
+SPA_EXPORT
+const char *pw_filter_get_name(struct pw_filter *filter)
+{
+ return filter->name;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_filter_get_properties(struct pw_filter *filter, void *port_data)
+{
+ struct port *port = SPA_CONTAINER_OF(port_data, struct port, user_data);
+
+ if (port_data) {
+ return port->props;
+ } else {
+ return filter->properties;
+ }
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_filter_update_properties(struct pw_filter *filter, void *port_data, const struct spa_dict *dict)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+ struct port *port = SPA_CONTAINER_OF(port_data, struct port, user_data);
+ int changed = 0;
+
+ if (port_data) {
+ changed = pw_properties_update(port->props, dict);
+ port->info.props = &port->props->dict;
+ if (changed > 0) {
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PROPS;
+ emit_port_info(impl, port, false);
+ }
+ } else {
+ struct match match;
+
+ changed = pw_properties_update(filter->properties, dict);
+
+ match = MATCH_INIT(filter);
+ pw_context_conf_section_match_rules(impl->context, "filter.rules",
+ &filter->properties->dict, execute_match, &match);
+
+ impl->info.props = &filter->properties->dict;
+ if (changed > 0 || match.count > 0) {
+ impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ emit_node_info(impl, false);
+ }
+ }
+ return changed;
+}
+
+SPA_EXPORT
+int
+pw_filter_connect(struct pw_filter *filter,
+ enum pw_filter_flags flags,
+ const struct spa_pod **params,
+ uint32_t n_params)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+ int res;
+ uint32_t i;
+ struct spa_dict_item items[1];
+
+ pw_log_debug("%p: connect", filter);
+ impl->flags = flags;
+
+ impl->process_rt = SPA_FLAG_IS_SET(flags, PW_FILTER_FLAG_RT_PROCESS);
+
+ impl->warn_mlock = pw_properties_get_bool(filter->properties, "mem.warn-mlock", impl->warn_mlock);
+
+ impl->impl_node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, impl);
+
+ impl->change_mask_all =
+ SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+
+ impl->info = SPA_NODE_INFO_INIT();
+ impl->info.max_input_ports = UINT32_MAX;
+ impl->info.max_output_ports = UINT32_MAX;
+ impl->info.flags = impl->process_rt ? SPA_NODE_FLAG_RT : 0;
+ impl->info.props = &filter->properties->dict;
+ impl->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, 0);
+ impl->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_WRITE);
+ impl->params[IDX_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, 0);
+ impl->info.params = impl->params;
+ impl->info.n_params = N_NODE_PARAMS;
+ impl->info.change_mask = impl->change_mask_all;
+
+ clear_params(impl, NULL, SPA_ID_INVALID);
+ for (i = 0; i < n_params; i++) {
+ add_param(impl, NULL, SPA_ID_INVALID, 0, params[i]);
+ }
+
+ impl->disconnecting = false;
+ filter_set_state(filter, PW_FILTER_STATE_CONNECTING, NULL);
+
+ if (flags & PW_FILTER_FLAG_DRIVER)
+ pw_properties_set(filter->properties, PW_KEY_NODE_DRIVER, "true");
+ if ((pw_properties_get(filter->properties, PW_KEY_NODE_WANT_DRIVER) == NULL))
+ pw_properties_set(filter->properties, PW_KEY_NODE_WANT_DRIVER, "true");
+
+ if (filter->core == NULL) {
+ filter->core = pw_context_connect(impl->context,
+ pw_properties_copy(filter->properties), 0);
+ if (filter->core == NULL) {
+ res = -errno;
+ goto error_connect;
+ }
+ spa_list_append(&filter->core->filter_list, &filter->link);
+ pw_core_add_listener(filter->core,
+ &filter->core_listener, &core_events, filter);
+ impl->disconnect_core = true;
+ }
+
+ pw_log_debug("%p: export node %p", filter, &impl->impl_node);
+
+ items[0] = SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_REGISTER, "false");
+ filter->proxy = pw_core_export(filter->core,
+ SPA_TYPE_INTERFACE_Node, &SPA_DICT_INIT_ARRAY(items),
+ &impl->impl_node, 0);
+ if (filter->proxy == NULL) {
+ res = -errno;
+ goto error_proxy;
+ }
+
+ pw_proxy_add_listener(filter->proxy, &filter->proxy_listener, &proxy_events, filter);
+
+ return 0;
+
+error_connect:
+ pw_log_error("%p: can't connect: %s", filter, spa_strerror(res));
+ return res;
+error_proxy:
+ pw_log_error("%p: can't make proxy: %s", filter, spa_strerror(res));
+ return res;
+}
+
+SPA_EXPORT
+uint32_t pw_filter_get_node_id(struct pw_filter *filter)
+{
+ return filter->node_id;
+}
+
+SPA_EXPORT
+int pw_filter_disconnect(struct pw_filter *filter)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+
+ pw_log_debug("%p: disconnect", filter);
+ impl->disconnecting = true;
+
+ if (filter->proxy) {
+ pw_proxy_destroy(filter->proxy);
+ filter->proxy = NULL;
+ }
+ if (impl->disconnect_core) {
+ impl->disconnect_core = false;
+ spa_hook_remove(&filter->core_listener);
+ spa_list_remove(&filter->link);
+ pw_core_disconnect(filter->core);
+ filter->core = NULL;
+ }
+ return 0;
+}
+
+static void add_port_params(struct filter *impl, struct port *port)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ add_param(impl, port, SPA_PARAM_IO, PARAM_FLAG_LOCKED,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, SPA_PARAM_IO,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))));
+}
+
+static void add_audio_dsp_port_params(struct filter *impl, struct port *port)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ 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_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32)));
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ add_param(impl, port, SPA_PARAM_Buffers, PARAM_FLAG_LOCKED,
+ 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),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_STEP_Int(
+ MAX_SAMPLES * sizeof(float),
+ sizeof(float),
+ MAX_SAMPLES * sizeof(float),
+ sizeof(float)),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(4)));
+}
+
+static void add_video_dsp_port_params(struct filter *impl, struct port *port)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ 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_video),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
+ 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)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ 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_EXPORT
+void *pw_filter_add_port(struct pw_filter *filter,
+ enum pw_direction direction,
+ enum pw_filter_port_flags flags,
+ size_t port_data_size,
+ struct pw_properties *props,
+ const struct spa_pod **params, uint32_t n_params)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+ struct port *p;
+ const char *str;
+
+ if (props == NULL)
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ return NULL;
+
+ if ((p = alloc_port(impl, direction, port_data_size)) == NULL)
+ goto error_cleanup;
+
+ p->props = props;
+ p->flags = flags;
+
+ p->change_mask_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PROPS;
+ p->info = SPA_PORT_INFO_INIT();
+ p->info.flags = 0;
+ if (SPA_FLAG_IS_SET(flags, PW_FILTER_PORT_FLAG_ALLOC_BUFFERS))
+ p->info.flags |= SPA_PORT_FLAG_CAN_ALLOC_BUFFERS;
+ p->info.props = &p->props->dict;
+ p->change_mask_all |= SPA_PORT_CHANGE_MASK_PARAMS;
+ p->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, 0);
+ p->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, 0);
+ p->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, 0);
+ p->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ p->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ p->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_WRITE);
+ p->info.params = p->params;
+ p->info.n_params = N_PORT_PARAMS;
+
+ /* first configure default params */
+ add_port_params(impl, p);
+ if ((str = pw_properties_get(props, PW_KEY_FORMAT_DSP)) != NULL) {
+ if (spa_streq(str, "32 bit float mono audio"))
+ 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);
+ }
+ /* then override with user provided if any */
+ if (update_params(impl, p, SPA_ID_INVALID, params, n_params) < 0)
+ goto error_free;
+
+ emit_port_info(impl, p, true);
+
+ return p->user_data;
+
+
+error_free:
+ clear_params(impl, p, SPA_ID_INVALID);
+ free(p);
+error_cleanup:
+ pw_properties_free(props);
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_filter_remove_port(void *port_data)
+{
+ struct port *port = SPA_CONTAINER_OF(port_data, struct port, user_data);
+ struct filter *impl = port->filter;
+
+ spa_node_emit_port_info(&impl->hooks, port->direction, port->id, NULL);
+
+ spa_list_remove(&port->link);
+ pw_map_remove(&impl->ports[port->direction], port->id);
+
+ clear_buffers(port);
+ clear_params(impl, port, SPA_ID_INVALID);
+ pw_properties_free(port->props);
+ free(port);
+
+ return 0;
+}
+
+SPA_EXPORT
+int pw_filter_set_error(struct pw_filter *filter,
+ int res, const char *error, ...)
+{
+ if (res < 0) {
+ va_list args;
+ char *value;
+ int r;
+
+ va_start(args, error);
+ r = vasprintf(&value, error, args);
+ va_end(args);
+ if (r < 0)
+ return -errno;
+
+ if (filter->proxy)
+ pw_proxy_error(filter->proxy, res, value);
+ filter_set_state(filter, PW_FILTER_STATE_ERROR, value);
+
+ free(value);
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_filter_update_params(struct pw_filter *filter,
+ void *port_data,
+ const struct spa_pod **params,
+ uint32_t n_params)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+ struct port *port;
+ int res;
+
+ pw_log_debug("%p: update params", filter);
+
+ port = port_data ? SPA_CONTAINER_OF(port_data, struct port, user_data) : NULL;
+
+ res = update_params(impl, port, SPA_ID_INVALID, params, n_params);
+ if (res < 0)
+ return res;
+
+ if (port)
+ emit_port_info(impl, port, false);
+ else
+ emit_node_info(impl, false);
+
+ return res;
+}
+
+SPA_EXPORT
+int pw_filter_set_active(struct pw_filter *filter, bool active)
+{
+ pw_log_debug("%p: active:%d", filter, active);
+ return 0;
+}
+
+SPA_EXPORT
+int pw_filter_get_time(struct pw_filter *filter, struct pw_time *time)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+ uintptr_t seq1, seq2;
+
+ do {
+ seq1 = SEQ_READ(impl->seq);
+ *time = impl->time;
+ seq2 = SEQ_READ(impl->seq);
+ } while (!SEQ_READ_SUCCESS(seq1, seq2));
+
+ pw_log_trace("%p: %"PRIi64" %"PRIi64" %"PRIu64" %d/%d ", filter,
+ time->now, time->delay, time->ticks,
+ time->rate.num, time->rate.denom);
+
+ return 0;
+}
+
+static int
+do_process(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct filter *impl = user_data;
+ int res = impl_node_process(impl);
+ return spa_node_call_ready(&impl->callbacks, res);
+}
+
+static inline int call_trigger(struct filter *impl)
+{
+ int res = 0;
+ if (SPA_FLAG_IS_SET(impl->flags, PW_FILTER_FLAG_DRIVER)) {
+ res = pw_loop_invoke(impl->context->data_loop,
+ do_process, 1, NULL, 0, false, impl);
+ }
+ return res;
+}
+
+SPA_EXPORT
+struct pw_buffer *pw_filter_dequeue_buffer(void *port_data)
+{
+ struct port *p = SPA_CONTAINER_OF(port_data, struct port, user_data);
+ struct filter *impl = p->filter;
+ struct buffer *b;
+ int res;
+
+ if ((b = pop_queue(p, &p->dequeued)) == NULL) {
+ res = -errno;
+ pw_log_trace("%p: no more buffers: %m", impl);
+ errno = -res;
+ return NULL;
+ }
+ pw_log_trace("%p: dequeue buffer %d", impl, b->id);
+
+ return &b->this;
+}
+
+SPA_EXPORT
+int pw_filter_queue_buffer(void *port_data, struct pw_buffer *buffer)
+{
+ struct port *p = SPA_CONTAINER_OF(port_data, struct port, user_data);
+ struct filter *impl = p->filter;
+ struct buffer *b = SPA_CONTAINER_OF(buffer, struct buffer, this);
+ int res;
+
+ pw_log_trace("%p: queue buffer %d", impl, b->id);
+ if ((res = push_queue(p, &p->queued, b)) < 0)
+ return res;
+
+ return call_trigger(impl);
+}
+
+SPA_EXPORT
+void *pw_filter_get_dsp_buffer(void *port_data, uint32_t n_samples)
+{
+ struct port *p = SPA_CONTAINER_OF(port_data, struct port, user_data);
+ struct pw_buffer *buf;
+ struct spa_data *d;
+
+ if ((buf = pw_filter_dequeue_buffer(port_data)) == NULL)
+ return NULL;
+
+ d = &buf->buffer->datas[0];
+
+ if (p->direction == SPA_DIRECTION_OUTPUT) {
+ d->chunk->offset = 0;
+ d->chunk->size = n_samples * sizeof(float);
+ d->chunk->stride = sizeof(float);
+ d->chunk->flags = 0;
+ }
+
+ pw_filter_queue_buffer(port_data, buf);
+
+ return d->data;
+}
+
+static int
+do_flush(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+#if 0
+ struct filter *impl = user_data;
+ struct buffer *b;
+
+ pw_log_trace("%p: flush", impl);
+ do {
+ b = pop_queue(impl, &impl->queued);
+ if (b != NULL)
+ push_queue(impl, &impl->dequeued, b);
+ }
+ while (b);
+
+ impl->time.queued = impl->queued.outcount = impl->dequeued.incount =
+ impl->dequeued.outcount = impl->queued.incount;
+
+#endif
+ return 0;
+}
+static int
+do_drain(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct filter *impl = user_data;
+ impl->draining = true;
+ return 0;
+}
+
+SPA_EXPORT
+int pw_filter_flush(struct pw_filter *filter, bool drain)
+{
+ struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this);
+ pw_loop_invoke(impl->context->data_loop,
+ drain ? do_drain : do_flush, 1, NULL, 0, true, impl);
+ return 0;
+}
diff --git a/src/pipewire/filter.h b/src/pipewire/filter.h
new file mode 100644
index 0000000..255608a
--- /dev/null
+++ b/src/pipewire/filter.h
@@ -0,0 +1,250 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_FILTER_H
+#define PIPEWIRE_FILTER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_filter Filter
+ *
+ * \brief PipeWire filter object class
+ *
+ * The filter object provides a convenient way to implement
+ * processing filters.
+ *
+ * See also \ref api_pw_core
+ */
+
+/**
+ * \addtogroup pw_filter
+ * \{
+ */
+struct pw_filter;
+
+#include <spa/buffer/buffer.h>
+#include <spa/node/io.h>
+#include <spa/param/param.h>
+#include <spa/pod/command.h>
+
+#include <pipewire/core.h>
+#include <pipewire/stream.h>
+
+/** \enum pw_filter_state The state of a filter */
+enum pw_filter_state {
+ PW_FILTER_STATE_ERROR = -1, /**< the stream is in error */
+ PW_FILTER_STATE_UNCONNECTED = 0, /**< unconnected */
+ PW_FILTER_STATE_CONNECTING = 1, /**< connection is in progress */
+ PW_FILTER_STATE_PAUSED = 2, /**< filter is connected and paused */
+ PW_FILTER_STATE_STREAMING = 3 /**< filter is streaming */
+};
+
+#if 0
+struct pw_buffer {
+ struct spa_buffer *buffer; /**< the spa buffer */
+ void *user_data; /**< user data attached to the buffer */
+ uint64_t size; /**< For input ports, this field is set by pw_filter
+ * with the duration of the buffer in ticks.
+ * For output ports, this field is set by the user.
+ * This field is added for all queued buffers and
+ * returned in the time info. */
+};
+#endif
+
+/** Events for a filter. These events are always called from the mainloop
+ * unless explicitly documented otherwise. */
+struct pw_filter_events {
+#define PW_VERSION_FILTER_EVENTS 1
+ uint32_t version;
+
+ void (*destroy) (void *data);
+ /** when the filter state changes */
+ void (*state_changed) (void *data, enum pw_filter_state old,
+ enum pw_filter_state state, const char *error);
+
+ /** when io changed on a port of the filter (when port_data is NULL). */
+ void (*io_changed) (void *data, void *port_data,
+ uint32_t id, void *area, uint32_t size);
+ /** when a parameter changed on a port of the filter (when port_data is NULL). */
+ void (*param_changed) (void *data, void *port_data,
+ uint32_t id, const struct spa_pod *param);
+
+ /** when a new buffer was created for a port */
+ void (*add_buffer) (void *data, void *port_data, struct pw_buffer *buffer);
+ /** when a buffer was destroyed for a port */
+ void (*remove_buffer) (void *data, void *port_data, struct pw_buffer *buffer);
+
+ /** 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. */
+ void (*process) (void *data, struct spa_io_position *position);
+
+ /** The filter is drained */
+ void (*drained) (void *data);
+
+ /** A command notify, Since 0.3.39:1 */
+ void (*command) (void *data, const struct spa_command *command);
+};
+
+/** Convert a filter state to a readable string */
+const char * pw_filter_state_as_string(enum pw_filter_state state);
+
+/** \enum pw_filter_flags Extra flags that can be used in \ref pw_filter_connect() */
+enum pw_filter_flags {
+ PW_FILTER_FLAG_NONE = 0, /**< no flags */
+ PW_FILTER_FLAG_INACTIVE = (1 << 0), /**< start the filter inactive,
+ * pw_filter_set_active() needs to be
+ * called explicitly */
+ PW_FILTER_FLAG_DRIVER = (1 << 1), /**< be a driver */
+ PW_FILTER_FLAG_RT_PROCESS = (1 << 2), /**< call process from the realtime
+ * thread */
+ 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. */
+};
+
+enum pw_filter_port_flags {
+ PW_FILTER_PORT_FLAG_NONE = 0, /**< no flags */
+ PW_FILTER_PORT_FLAG_MAP_BUFFERS = (1 << 0), /**< mmap the buffers except DmaBuf */
+ PW_FILTER_PORT_FLAG_ALLOC_BUFFERS = (1 << 1), /**< the application will allocate buffer
+ * memory. In the add_buffer event, the
+ * data of the buffer should be set */
+};
+
+/** Create a new unconneced \ref pw_filter
+ * \return a newly allocated \ref pw_filter */
+struct pw_filter *
+pw_filter_new(struct pw_core *core, /**< a \ref pw_core */
+ const char *name, /**< a filter media name */
+ struct pw_properties *props /**< filter properties, ownership is taken */);
+
+struct pw_filter *
+pw_filter_new_simple(struct pw_loop *loop, /**< a \ref pw_loop to use */
+ const char *name, /**< a filter media name */
+ struct pw_properties *props, /**< filter properties, ownership is taken */
+ const struct pw_filter_events *events, /**< filter events */
+ void *data /**< data passed to events */);
+
+/** Destroy a filter */
+void pw_filter_destroy(struct pw_filter *filter);
+
+void pw_filter_add_listener(struct pw_filter *filter,
+ struct spa_hook *listener,
+ const struct pw_filter_events *events,
+ void *data);
+
+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);
+
+struct pw_core *pw_filter_get_core(struct pw_filter *filter);
+
+/** Connect a filter for processing.
+ * \return 0 on success < 0 on error.
+ *
+ * You should connect to the process event and use pw_filter_dequeue_buffer()
+ * to get the latest metadata and data. */
+int
+pw_filter_connect(struct pw_filter *filter, /**< a \ref pw_filter */
+ enum pw_filter_flags flags, /**< filter flags */
+ const struct spa_pod **params, /**< an array with params. */
+ uint32_t n_params /**< number of items in \a params */);
+
+/** Get the node ID of the filter.
+ * \return node ID. */
+uint32_t
+pw_filter_get_node_id(struct pw_filter *filter);
+
+/** Disconnect \a filter */
+int pw_filter_disconnect(struct pw_filter *filter);
+
+/** add a port to the filter, returns user data of port_data_size. */
+void *pw_filter_add_port(struct pw_filter *filter, /**< a \ref pw_filter */
+ enum pw_direction direction, /**< port direction */
+ enum pw_filter_port_flags flags, /**< port flags */
+ size_t port_data_size, /**< allocated and given to the user as port_data */
+ struct pw_properties *props, /**< port properties, ownership is taken */
+ const struct spa_pod **params, /**< an array of params. The params should
+ * ideally contain the supported formats */
+ uint32_t n_params /**< number of elements in \a params */);
+
+/** remove a port from the filter */
+int pw_filter_remove_port(void *port_data /**< data associated with port */);
+
+/** get properties, port_data of NULL will give global properties */
+const struct pw_properties *pw_filter_get_properties(struct pw_filter *filter,
+ void *port_data);
+
+/** Update properties, use NULL port_data for global filter properties */
+int pw_filter_update_properties(struct pw_filter *filter,
+ void *port_data, const struct spa_dict *dict);
+
+/** Set the filter in error state */
+int pw_filter_set_error(struct pw_filter *filter, /**< a \ref pw_filter */
+ int res, /**< a result code */
+ const char *error, /**< an error message */
+ ...
+ ) SPA_PRINTF_FUNC(3, 4);
+
+/** Update params, use NULL port_data for global filter params */
+int
+pw_filter_update_params(struct pw_filter *filter, /**< a \ref pw_filter */
+ void *port_data, /**< data associated with port */
+ const struct spa_pod **params, /**< an array of params. */
+ uint32_t n_params /**< number of elements in \a params */);
+
+
+/** Query the time on the filter, deprecated, use the spa_io_position in the
+ * process() method for timing information. */
+SPA_DEPRECATED
+int pw_filter_get_time(struct pw_filter *filter, struct pw_time *time);
+
+/** Get a buffer that can be filled for output ports or consumed
+ * for input ports. */
+struct pw_buffer *pw_filter_dequeue_buffer(void *port_data);
+
+/** Submit a buffer for playback or recycle a buffer for capture. */
+int pw_filter_queue_buffer(void *port_data, struct pw_buffer *buffer);
+
+/** Get a data pointer to the buffer data */
+void *pw_filter_get_dsp_buffer(void *port_data, uint32_t n_samples);
+
+/** Activate or deactivate the filter */
+int pw_filter_set_active(struct pw_filter *filter, bool active);
+
+/** Flush a filter. When \a drain is true, the drained callback will
+ * be called when all data is played or recorded */
+int pw_filter_flush(struct pw_filter *filter, bool drain);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_FILTER_H */
diff --git a/src/pipewire/global.c b/src/pipewire/global.c
new file mode 100644
index 0000000..d069ac8
--- /dev/null
+++ b/src/pipewire/global.c
@@ -0,0 +1,430 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <time.h>
+#include <stdio.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/private.h>
+
+#include <spa/debug/types.h>
+#include <spa/utils/string.h>
+
+PW_LOG_TOPIC_EXTERN(log_global);
+#define PW_LOG_TOPIC_DEFAULT log_global
+
+/** \cond */
+struct impl {
+ struct pw_global this;
+};
+/** \endcond */
+
+SPA_EXPORT
+uint32_t pw_global_get_permissions(struct pw_global *global, struct pw_impl_client *client)
+{
+ if (client->permission_func == NULL)
+ return PW_PERM_ALL;
+
+ return client->permission_func(global, client, client->permission_data);
+}
+
+/** Create a new global
+ *
+ * \param context a context object
+ * \param type the type of the global
+ * \param version the version of the type
+ * \param properties extra properties
+ * \param func a function to bind to this global
+ * \param object the associated object
+ * \return a result global
+ *
+ */
+SPA_EXPORT
+struct pw_global *
+pw_global_new(struct pw_context *context,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ pw_global_bind_func_t func,
+ void *object)
+{
+ struct impl *impl;
+ struct pw_global *this;
+ int res;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ this = &impl->this;
+
+ this->context = context;
+ this->type = type;
+ this->version = version;
+ this->func = func;
+ this->object = object;
+ this->properties = properties;
+ this->id = pw_map_insert_new(&context->globals, this);
+ if (this->id == SPA_ID_INVALID) {
+ res = -errno;
+ pw_log_error("%p: can't allocate new id: %m", this);
+ goto error_free;
+ }
+ this->serial = SPA_ID_INVALID;
+
+ spa_list_init(&this->resource_list);
+ spa_hook_list_init(&this->listener_list);
+
+ pw_log_debug("%p: new %s %d", this, this->type, this->id);
+
+ return this;
+
+error_free:
+ free(impl);
+error_cleanup:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+uint64_t pw_global_get_serial(struct pw_global *global)
+{
+ struct pw_context *context = global->context;
+ if (global->serial == SPA_ID_INVALID)
+ global->serial = context->serial++;
+ if ((uint32_t)context->serial == SPA_ID_INVALID)
+ context->serial++;
+ return global->serial;
+}
+
+/** register a global to the context registry
+ *
+ * \param global a global to add
+ * \return 0 on success < 0 errno value on failure
+ *
+ */
+SPA_EXPORT
+int pw_global_register(struct pw_global *global)
+{
+ struct pw_resource *registry;
+ struct pw_context *context = global->context;
+ struct pw_impl_client *client;
+
+ if (global->registered)
+ return -EEXIST;
+
+ spa_list_append(&context->global_list, &global->link);
+ global->registered = true;
+
+ global->generation = ++context->generation;
+
+ spa_list_for_each(registry, &context->registry_resource_list, link) {
+ uint32_t permissions = pw_global_get_permissions(global, registry->client);
+ pw_log_debug("registry %p: global %d %08x serial:%"PRIu64" generation:%"PRIu64,
+ registry, global->id, permissions, global->serial, global->generation);
+ if (PW_PERM_IS_R(permissions))
+ pw_registry_resource_global(registry,
+ global->id,
+ permissions,
+ global->type,
+ global->version,
+ &global->properties->dict);
+ }
+
+ /* Ensure a message is sent also to clients without registries, to force
+ * generation number update. */
+ spa_list_for_each(client, &context->client_list, link) {
+ uint32_t permissions;
+
+ if (client->sent_generation >= context->generation)
+ continue;
+ if (!client->core_resource)
+ continue;
+
+ permissions = pw_global_get_permissions(global, client);
+ if (PW_PERM_IS_R(permissions)) {
+ pw_log_debug("impl-client %p: (no registry) global %d %08x serial:%"PRIu64
+ " generation:%"PRIu64, client, global->id, permissions, global->serial,
+ global->generation);
+ pw_core_resource_done(client->core_resource, SPA_ID_INVALID, 0);
+ }
+ }
+
+ pw_log_debug("%p: registered %u", global, global->id);
+ pw_context_emit_global_added(context, global);
+
+ return 0;
+}
+
+static int global_unregister(struct pw_global *global)
+{
+ struct pw_context *context = global->context;
+ struct pw_resource *resource;
+
+ if (!global->registered)
+ return 0;
+
+ spa_list_for_each(resource, &context->registry_resource_list, link) {
+ uint32_t permissions = pw_global_get_permissions(global, resource->client);
+ pw_log_debug("registry %p: global %d %08x", resource, global->id, permissions);
+ if (PW_PERM_IS_R(permissions))
+ pw_registry_resource_global_remove(resource, global->id);
+ }
+
+ spa_list_remove(&global->link);
+ global->registered = false;
+ global->serial = SPA_ID_INVALID;
+
+ pw_log_debug("%p: unregistered %u", global, global->id);
+ pw_context_emit_global_removed(context, global);
+
+ return 0;
+}
+
+SPA_EXPORT
+struct pw_context *pw_global_get_context(struct pw_global *global)
+{
+ return global->context;
+}
+
+SPA_EXPORT
+const char * pw_global_get_type(struct pw_global *global)
+{
+ return global->type;
+}
+
+SPA_EXPORT
+bool pw_global_is_type(struct pw_global *global, const char *type)
+{
+ return spa_streq(global->type, type);
+}
+
+SPA_EXPORT
+uint32_t pw_global_get_version(struct pw_global *global)
+{
+ return global->version;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_global_get_properties(struct pw_global *global)
+{
+ return global->properties;
+}
+
+SPA_EXPORT
+int pw_global_update_keys(struct pw_global *global,
+ const struct spa_dict *dict, const char * const keys[])
+{
+ if (global->registered)
+ return -EINVAL;
+ return pw_properties_update_keys(global->properties, dict, keys);
+}
+
+SPA_EXPORT
+void * pw_global_get_object(struct pw_global *global)
+{
+ return global->object;
+}
+
+SPA_EXPORT
+uint32_t pw_global_get_id(struct pw_global *global)
+{
+ return global->id;
+}
+
+SPA_EXPORT
+int pw_global_add_resource(struct pw_global *global, struct pw_resource *resource)
+{
+ resource->global = global;
+ pw_log_debug("%p: resource %p id:%d global:%d", global, resource,
+ resource->id, global->id);
+ spa_list_append(&global->resource_list, &resource->link);
+ pw_resource_set_bound_id(resource, global->id);
+ return 0;
+}
+
+SPA_EXPORT
+int pw_global_for_each_resource(struct pw_global *global,
+ int (*callback) (void *data, struct pw_resource *resource),
+ void *data)
+{
+ struct pw_resource *resource, *t;
+ int res;
+
+ spa_list_for_each_safe(resource, t, &global->resource_list, link)
+ if ((res = callback(data, resource)) != 0)
+ return res;
+ return 0;
+}
+
+SPA_EXPORT
+void pw_global_add_listener(struct pw_global *global,
+ struct spa_hook *listener,
+ const struct pw_global_events *events,
+ void *data)
+{
+ spa_hook_list_append(&global->listener_list, listener, events, data);
+}
+
+/** Bind to a global
+ *
+ * \param global the global to bind to
+ * \param client the client that binds
+ * \param permissions the \ref pw_permission
+ * \param version the version
+ * \param id the id of the resource
+ *
+ * Let \a client bind to \a global with the given version and id.
+ * After binding, the client and the global object will be able to
+ * exchange messages on the proxy/resource with \a id.
+ *
+ */
+SPA_EXPORT int
+pw_global_bind(struct pw_global *global, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ int res;
+
+ if (global->version < version)
+ goto error_version;
+
+ if ((res = global->func(global->object, client, permissions, version, id)) < 0)
+ goto error_bind;
+
+ return res;
+
+error_version:
+ res = -EPROTO;
+ if (client->core_resource)
+ pw_core_resource_errorf(client->core_resource, id, client->recv_seq,
+ res, "id %d: interface version %d < %d",
+ id, global->version, version);
+ goto error_exit;
+error_bind:
+ if (client->core_resource)
+ pw_core_resource_errorf(client->core_resource, id, client->recv_seq,
+ res, "can't bind global %u/%u: %d (%s)", id, version,
+ res, spa_strerror(res));
+ goto error_exit;
+
+error_exit:
+ pw_log_error("%p: can't bind global %u/%u: %d (%s)", global, id,
+ version, res, spa_strerror(res));
+ pw_map_insert_at(&client->objects, id, NULL);
+ if (client->core_resource)
+ pw_core_resource_remove_id(client->core_resource, id);
+ return res;
+}
+
+SPA_EXPORT
+int pw_global_update_permissions(struct pw_global *global, struct pw_impl_client *client,
+ uint32_t old_permissions, uint32_t new_permissions)
+{
+ struct pw_context *context = global->context;
+ struct pw_resource *resource, *t;
+ bool do_hide, do_show;
+
+ do_hide = PW_PERM_IS_R(old_permissions) && !PW_PERM_IS_R(new_permissions);
+ do_show = !PW_PERM_IS_R(old_permissions) && PW_PERM_IS_R(new_permissions);
+
+ pw_log_debug("%p: client %p permissions changed %d %08x -> %08x",
+ global, client, global->id, old_permissions, new_permissions);
+
+ pw_global_emit_permissions_changed(global, client, old_permissions, new_permissions);
+
+ spa_list_for_each(resource, &context->registry_resource_list, link) {
+ if (resource->client != client)
+ continue;
+
+ if (do_hide) {
+ pw_log_debug("client %p: resource %p hide global %d",
+ client, resource, global->id);
+ pw_registry_resource_global_remove(resource, global->id);
+ }
+ else if (do_show) {
+ pw_log_debug("client %p: resource %p show global %d serial:%"PRIu64,
+ client, resource, global->id, global->serial);
+ pw_registry_resource_global(resource,
+ global->id,
+ new_permissions,
+ global->type,
+ global->version,
+ &global->properties->dict);
+ }
+ }
+
+ spa_list_for_each_safe(resource, t, &global->resource_list, link) {
+ if (resource->client != client)
+ continue;
+
+ /* don't ever destroy the core resource */
+ if (!PW_PERM_IS_R(new_permissions) && global->id != PW_ID_CORE)
+ pw_resource_destroy(resource);
+ else
+ resource->permissions = new_permissions;
+ }
+ return 0;
+}
+
+/** Destroy a global
+ *
+ * \param global a global to destroy
+ *
+ */
+SPA_EXPORT
+void pw_global_destroy(struct pw_global *global)
+{
+ struct pw_resource *resource;
+ struct pw_context *context = global->context;
+
+ global->destroyed = true;
+
+ pw_log_debug("%p: destroy %u", global, global->id);
+ pw_global_emit_destroy(global);
+
+ spa_list_consume(resource, &global->resource_list, link)
+ pw_resource_destroy(resource);
+
+ global_unregister(global);
+
+ pw_log_debug("%p: free", global);
+ pw_global_emit_free(global);
+
+ pw_map_remove(&context->globals, global->id);
+ spa_hook_list_clean(&global->listener_list);
+
+ pw_properties_free(global->properties);
+
+ free(global);
+}
diff --git a/src/pipewire/global.h b/src/pipewire/global.h
new file mode 100644
index 0000000..0cf2a7c
--- /dev/null
+++ b/src/pipewire/global.h
@@ -0,0 +1,165 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_GLOBAL_H
+#define PIPEWIRE_GLOBAL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_global Global
+ *
+ * \brief A global object visible to remote clients
+ *
+ * A global object is visible to remote clients and represents a resource
+ * that can be used or inspected.
+ *
+ * Global objects represent resources that are available on the PipeWire
+ * context and are accessible to remote clients.
+ * Globals come and go when devices or other resources become available for
+ * clients.
+ *
+ * Remote clients receives a list of globals when it binds to the registry
+ * object. See \ref pw_registry.
+ *
+ * A client can bind to a global to send methods or receive events from
+ * the global.
+ *
+ * See \ref page_proxy
+ */
+
+/**
+ * \addtogroup pw_global
+ * \{
+ */
+struct pw_global;
+
+#include <pipewire/impl.h>
+
+typedef int (*pw_global_bind_func_t) (void *object, /**< global object, see \ref pw_global_new */
+ struct pw_impl_client *client, /**< client that binds */
+ uint32_t permissions, /**< permissions for the bind */
+ uint32_t version, /**< client interface version */
+ uint32_t id /**< client proxy id */);
+
+/** Global events, use \ref pw_global_add_listener */
+struct pw_global_events {
+#define PW_VERSION_GLOBAL_EVENTS 0
+ uint32_t version;
+
+ /** The global is destroyed */
+ void (*destroy) (void *data);
+ /** The global is freed */
+ void (*free) (void *data);
+ /** The permissions changed for a client */
+ void (*permissions_changed) (void *data,
+ struct pw_impl_client *client,
+ uint32_t old_permissions,
+ uint32_t new_permissions);
+};
+
+/** Create a new global object */
+struct pw_global *
+pw_global_new(struct pw_context *context, /**< the context */
+ const char *type, /**< the interface type of the global */
+ uint32_t version, /**< the interface version of the global */
+ struct pw_properties *properties, /**< extra properties */
+ pw_global_bind_func_t func, /**< function to bind */
+ void *object /**< global object */);
+
+/** Register a global object to the context registry */
+int pw_global_register(struct pw_global *global);
+
+/** Add an event listener on the global */
+void pw_global_add_listener(struct pw_global *global,
+ struct spa_hook *listener,
+ const struct pw_global_events *events,
+ void *data);
+
+/** Get the permissions of the global for a given client */
+uint32_t pw_global_get_permissions(struct pw_global *global, struct pw_impl_client *client);
+
+/** Get the context object of this global */
+struct pw_context *pw_global_get_context(struct pw_global *global);
+
+/** Get the global type */
+const char *pw_global_get_type(struct pw_global *global);
+
+/** Check a global type */
+bool pw_global_is_type(struct pw_global *global, const char *type);
+
+/** Get the global version */
+uint32_t pw_global_get_version(struct pw_global *global);
+
+/** Get the global properties */
+const struct pw_properties *pw_global_get_properties(struct pw_global *global);
+
+/** Update the global properties, must be done when unregistered */
+int pw_global_update_keys(struct pw_global *global,
+ const struct spa_dict *dict, const char * const keys[]);
+
+/** Get the object associated with the global. This depends on the type of the
+ * global */
+void *pw_global_get_object(struct pw_global *global);
+
+/** Get the unique id of the global */
+uint32_t pw_global_get_id(struct pw_global *global);
+
+/** Get the serial number of the global */
+uint64_t pw_global_get_serial(struct pw_global *global);
+
+/** Add a resource to a global */
+int pw_global_add_resource(struct pw_global *global, struct pw_resource *resource);
+
+/** Iterate all resources added to the global The callback should return
+ * 0 to fetch the next item, any other value stops the iteration and returns
+ * the value. When all callbacks return 0, this function returns 0 when all
+ * items are iterated. */
+int pw_global_for_each_resource(struct pw_global *global,
+ int (*callback) (void *data, struct pw_resource *resource),
+ void *data);
+
+/** Let a client bind to a global */
+int pw_global_bind(struct pw_global *global,
+ struct pw_impl_client *client,
+ uint32_t permissions,
+ uint32_t version,
+ uint32_t id);
+
+int pw_global_update_permissions(struct pw_global *global, struct pw_impl_client *client,
+ uint32_t old_permissions, uint32_t new_permissions);
+
+/** Destroy a global */
+void pw_global_destroy(struct pw_global *global);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_GLOBAL_H */
diff --git a/src/pipewire/i18n.h b/src/pipewire/i18n.h
new file mode 100644
index 0000000..aa7b0b3
--- /dev/null
+++ b/src/pipewire/i18n.h
@@ -0,0 +1,56 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_I18N_H
+#define PIPEWIRE_I18N_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_gettext Internationalization
+ * Gettext interface
+ */
+
+/**
+ * \addtogroup pw_gettext
+ * \{
+ */
+#include <spa/support/i18n.h>
+
+SPA_FORMAT_ARG_FUNC(1) const char *pw_gettext(const char *msgid);
+SPA_FORMAT_ARG_FUNC(1) const char *pw_ngettext(const char *msgid, const char *msgid_plural, unsigned long int n);
+
+#define _(String) (pw_gettext(String))
+#define N_(String) (String)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_I18N_H */
diff --git a/src/pipewire/impl-client.c b/src/pipewire/impl-client.c
new file mode 100644
index 0000000..f45e382
--- /dev/null
+++ b/src/pipewire/impl-client.c
@@ -0,0 +1,780 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <assert.h>
+
+#include <spa/utils/string.h>
+
+#include "pipewire/impl.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_client);
+#define PW_LOG_TOPIC_DEFAULT log_client
+
+/** \cond */
+struct impl {
+ struct pw_impl_client this;
+ struct spa_hook context_listener;
+ struct pw_array permissions;
+ struct spa_hook pool_listener;
+ unsigned int registered:1;
+};
+
+#define pw_client_resource(r,m,v,...) pw_resource_call(r,struct pw_client_events,m,v,__VA_ARGS__)
+#define pw_client_resource_info(r,...) pw_client_resource(r,info,0,__VA_ARGS__)
+#define pw_client_resource_permissions(r,...) pw_client_resource(r,permissions,0,__VA_ARGS__)
+
+struct resource_data {
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+ struct pw_impl_client *client;
+};
+
+/** find a specific permission for a global or the default when there is none */
+static struct pw_permission *
+find_permission(struct pw_impl_client *client, uint32_t id)
+{
+ struct impl *impl = SPA_CONTAINER_OF(client, struct impl, this);
+ struct pw_permission *p;
+ uint32_t idx = id + 1;
+
+ if (id == PW_ID_ANY)
+ goto do_default;
+
+ if (!pw_array_check_index(&impl->permissions, idx, struct pw_permission))
+ goto do_default;
+
+ p = pw_array_get_unchecked(&impl->permissions, idx, struct pw_permission);
+ if (p->permissions == PW_PERM_INVALID)
+ goto do_default;
+
+ return p;
+
+do_default:
+ return pw_array_get_unchecked(&impl->permissions, 0, struct pw_permission);
+}
+
+static struct pw_permission *ensure_permissions(struct pw_impl_client *client, uint32_t id)
+{
+ struct impl *impl = SPA_CONTAINER_OF(client, struct impl, this);
+ struct pw_permission *p;
+ uint32_t idx = id + 1;
+ size_t len, i;
+
+ len = pw_array_get_len(&impl->permissions, struct pw_permission);
+ if (len <= idx) {
+ size_t diff = idx - len + 1;
+
+ p = pw_array_add(&impl->permissions, diff * sizeof(struct pw_permission));
+ if (p == NULL)
+ return NULL;
+
+ for (i = 0; i < diff; i++) {
+ p[i] = PW_PERMISSION_INIT(len + i - 1, PW_PERM_INVALID);
+ }
+ }
+ p = pw_array_get_unchecked(&impl->permissions, idx, struct pw_permission);
+ return p;
+}
+
+/** \endcond */
+
+static uint32_t
+client_permission_func(struct pw_global *global,
+ struct pw_impl_client *client, void *data)
+{
+ struct pw_permission *p = find_permission(client, global->id);
+ return p->permissions;
+}
+
+struct error_data {
+ uint32_t id;
+ int res;
+ const char *error;
+};
+
+static int error_resource(void *object, void *data)
+{
+ struct pw_resource *r = object;
+ struct error_data *d = data;
+ if (r && r->bound_id == d->id)
+ pw_resource_error(r, d->res, d->error);
+ return 0;
+}
+
+static int client_error(void *object, uint32_t id, int res, const char *error)
+{
+ struct resource_data *data = object;
+ struct pw_impl_client *client = data->client;
+ struct error_data d = { id, res, error };
+
+ pw_log_debug("%p: error for global %d", client, id);
+ pw_map_for_each(&client->objects, error_resource, &d);
+ return 0;
+}
+
+static bool has_key(const char * const keys[], const char *key)
+{
+ int i;
+ for (i = 0; keys[i]; i++) {
+ if (spa_streq(keys[i], key))
+ return true;
+ }
+ return false;
+}
+
+static int update_properties(struct pw_impl_client *client, const struct spa_dict *dict, bool filter)
+{
+ static const char * const ignored[] = {
+ PW_KEY_OBJECT_ID,
+ NULL
+ };
+
+ struct pw_resource *resource;
+ int changed = 0;
+ uint32_t i;
+ const char *old;
+
+ for (i = 0; i < dict->n_items; i++) {
+ if (filter) {
+ if (spa_strstartswith(dict->items[i].key, "pipewire.") &&
+ (old = pw_properties_get(client->properties, dict->items[i].key)) != NULL &&
+ (dict->items[i].value == NULL || !spa_streq(old, dict->items[i].value))) {
+ pw_log_warn("%p: refuse property update '%s' from '%s' to '%s'",
+ client, dict->items[i].key, old,
+ dict->items[i].value);
+ continue;
+
+ }
+ if (has_key(ignored, dict->items[i].key))
+ continue;
+ }
+ changed += pw_properties_set(client->properties, dict->items[i].key, dict->items[i].value);
+ }
+ client->info.props = &client->properties->dict;
+
+ pw_log_debug("%p: updated %d properties", client, changed);
+
+ if (!changed)
+ return 0;
+
+ client->info.change_mask |= PW_CLIENT_CHANGE_MASK_PROPS;
+
+ pw_impl_client_emit_info_changed(client, &client->info);
+
+ if (client->global)
+ spa_list_for_each(resource, &client->global->resource_list, link)
+ pw_client_resource_info(resource, &client->info);
+
+ client->info.change_mask = 0;
+
+ return changed;
+}
+
+static void update_busy(struct pw_impl_client *client)
+{
+ struct pw_permission *def;
+ def = find_permission(client, PW_ID_CORE);
+ pw_impl_client_set_busy(client, (def->permissions & PW_PERM_R) ? false : true);
+}
+
+static int finish_register(struct pw_impl_client *client)
+{
+ static const char * const keys[] = {
+ PW_KEY_ACCESS,
+ PW_KEY_CLIENT_ACCESS,
+ PW_KEY_APP_NAME,
+ NULL
+ };
+
+ struct impl *impl = SPA_CONTAINER_OF(client, struct impl, this);
+ struct pw_impl_client *current;
+
+ if (impl->registered)
+ return 0;
+
+ impl->registered = true;
+
+ current = client->context->current_client;
+ client->context->current_client = NULL;
+ pw_context_emit_check_access(client->context, client);
+ client->context->current_client = current;
+
+ update_busy(client);
+
+ pw_global_update_keys(client->global, client->info.props, keys);
+ pw_global_register(client->global);
+
+#ifdef OLD_MEDIA_SESSION_WORKAROUND
+ /*
+ * XXX: temporary workaround for pipewire-media-session, see #2159
+ */
+ if (spa_streq(spa_dict_lookup(client->info.props, PW_KEY_APP_NAME),
+ "pipewire-media-session")) {
+ client->recv_generation = UINT64_MAX;
+ pw_log_info("impl-client %p: enable old pipewire-media-session workaround",
+ client);
+ }
+#endif
+
+ return 0;
+}
+
+static int client_update_properties(void *object, const struct spa_dict *props)
+{
+ struct resource_data *data = object;
+ struct pw_impl_client *client = data->client;
+ int res = update_properties(client, props, true);
+ finish_register(client);
+ return res;
+}
+
+static int client_get_permissions(void *object, uint32_t index, uint32_t num)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ struct pw_impl_client *client = data->client;
+ struct impl *impl = SPA_CONTAINER_OF(client, struct impl, this);
+ size_t len;
+
+ len = pw_array_get_len(&impl->permissions, struct pw_permission);
+ if ((size_t)index >= len)
+ num = 0;
+ else if ((size_t)index + (size_t)num >= len)
+ num = len - index;
+
+ pw_client_resource_permissions(resource, index,
+ num, pw_array_get_unchecked(&impl->permissions, index, struct pw_permission));
+ return 0;
+}
+
+static int client_update_permissions(void *object,
+ uint32_t n_permissions, const struct pw_permission *permissions)
+{
+ struct resource_data *data = object;
+ struct pw_impl_client *client = data->client;
+ return pw_impl_client_update_permissions(client, n_permissions, permissions);
+}
+
+static const struct pw_client_methods client_methods = {
+ PW_VERSION_CLIENT_METHODS,
+ .error = client_error,
+ .update_properties = client_update_properties,
+ .get_permissions = client_get_permissions,
+ .update_permissions = client_update_permissions
+};
+
+static void client_unbind_func(void *data)
+{
+ struct resource_data *d = data;
+ struct pw_resource *resource = d->resource;
+ spa_hook_remove(&d->resource_listener);
+ spa_hook_remove(&d->object_listener);
+ if (resource->id == 1)
+ resource->client->client_resource = NULL;
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = client_unbind_func,
+};
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct pw_impl_client *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, sizeof(*data));
+ if (resource == NULL)
+ goto error_resource;
+
+ data = pw_resource_get_user_data(resource);
+ data->resource = resource;
+ data->client = this;
+ pw_resource_add_listener(resource,
+ &data->resource_listener,
+ &resource_events, data);
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &client_methods, data);
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+ pw_global_add_resource(global, resource);
+
+ if (resource->id == 1)
+ client->client_resource = resource;
+
+ this->info.change_mask = PW_CLIENT_CHANGE_MASK_ALL;
+ pw_client_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+error_resource:
+ pw_log_error("%p: can't create client resource: %m", this);
+ return -errno;
+}
+
+static void pool_added(void *data, struct pw_memblock *block)
+{
+ struct impl *impl = data;
+ struct pw_impl_client *client = &impl->this;
+
+ pw_log_debug("%p: added block %d", client, block->id);
+ if (client->core_resource) {
+ pw_core_resource_add_mem(client->core_resource,
+ block->id, block->type, block->fd,
+ block->flags & PW_MEMBLOCK_FLAG_READWRITE);
+ }
+}
+
+static void pool_removed(void *data, struct pw_memblock *block)
+{
+ struct impl *impl = data;
+ struct pw_impl_client *client = &impl->this;
+ pw_log_debug("%p: removed block %d", client, block->id);
+ if (client->core_resource)
+ pw_core_resource_remove_mem(client->core_resource, block->id);
+}
+
+static const struct pw_mempool_events pool_events = {
+ PW_VERSION_MEMPOOL_EVENTS,
+ .added = pool_added,
+ .removed = pool_removed,
+};
+
+static void
+context_global_removed(void *data, struct pw_global *global)
+{
+ struct impl *impl = data;
+ struct pw_impl_client *client = &impl->this;
+ struct pw_permission *p;
+
+ p = find_permission(client, global->id);
+ pw_log_debug("%p: global %d removed, %p", client, global->id, p);
+ if (p->id != PW_ID_ANY)
+ p->permissions = PW_PERM_INVALID;
+}
+
+static const struct pw_context_events context_events = {
+ PW_VERSION_CONTEXT_EVENTS,
+ .global_removed = context_global_removed,
+};
+
+/** Make a new client object
+ *
+ * \param core a \ref pw_context object to register the client with
+ * \param properties optional client properties, ownership is taken
+ * \return a newly allocated client object
+ *
+ */
+SPA_EXPORT
+struct pw_impl_client *pw_context_create_client(struct pw_impl_core *core,
+ struct pw_protocol *protocol,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_client *this;
+ struct impl *impl;
+ struct pw_permission *p;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl) + user_data_size);
+ if (impl == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ this = &impl->this;
+ pw_log_debug("%p: new", this);
+
+ this->refcount = 1;
+ this->context = core->context;
+ this->core = core;
+ this->protocol = protocol;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ pw_array_init(&impl->permissions, 1024);
+ p = pw_array_add(&impl->permissions, sizeof(struct pw_permission));
+ if (p == NULL) {
+ res = -errno;
+ goto error_clear_array;
+ }
+ p->id = PW_ID_ANY;
+ p->permissions = 0;
+
+ this->pool = pw_mempool_new(NULL);
+ if (this->pool == NULL) {
+ res = -errno;
+ goto error_clear_array;
+ }
+ pw_mempool_add_listener(this->pool, &impl->pool_listener, &pool_events, impl);
+
+ this->properties = properties;
+ this->permission_func = client_permission_func;
+ this->permission_data = impl;
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ spa_hook_list_init(&this->listener_list);
+
+ pw_map_init(&this->objects, 0, 32);
+
+ pw_context_add_listener(this->context, &impl->context_listener, &context_events, impl);
+
+ this->info.props = &this->properties->dict;
+
+ return this;
+
+error_clear_array:
+ pw_array_clear(&impl->permissions);
+error_free:
+ free(impl);
+error_cleanup:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_client *client = data;
+ spa_hook_remove(&client->global_listener);
+ client->global = NULL;
+ pw_impl_client_destroy(client);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+SPA_EXPORT
+int pw_impl_client_register(struct pw_impl_client *client,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_MODULE_ID,
+ PW_KEY_PROTOCOL,
+ PW_KEY_SEC_PID,
+ PW_KEY_SEC_UID,
+ PW_KEY_SEC_GID,
+ PW_KEY_SEC_LABEL,
+ NULL
+ };
+
+ struct pw_context *context = client->context;
+
+ if (client->registered)
+ goto error_existed;
+
+ pw_log_debug("%p: register", client);
+
+ client->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Client,
+ PW_VERSION_CLIENT,
+ properties,
+ global_bind,
+ client);
+ if (client->global == NULL)
+ return -errno;
+
+ spa_list_append(&context->client_list, &client->link);
+ client->registered = true;
+
+ client->info.id = client->global->id;
+ pw_properties_setf(client->properties, PW_KEY_OBJECT_ID, "%d", client->info.id);
+ pw_properties_setf(client->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(client->global));
+ client->info.props = &client->properties->dict;
+ pw_global_add_listener(client->global, &client->global_listener, &global_events, client);
+
+ pw_global_update_keys(client->global, client->info.props, keys);
+
+ pw_impl_client_emit_initialized(client);
+
+ return 0;
+
+error_existed:
+ pw_properties_free(properties);
+ return -EEXIST;
+}
+
+SPA_EXPORT
+struct pw_context *pw_impl_client_get_context(struct pw_impl_client *client)
+{
+ return client->core->context;
+}
+
+SPA_EXPORT
+struct pw_protocol *pw_impl_client_get_protocol(struct pw_impl_client *client)
+{
+ return client->protocol;
+}
+
+SPA_EXPORT
+struct pw_resource *pw_impl_client_get_core_resource(struct pw_impl_client *client)
+{
+ return client->core_resource;
+}
+
+SPA_EXPORT
+struct pw_resource *pw_impl_client_find_resource(struct pw_impl_client *client, uint32_t id)
+{
+ return pw_map_lookup(&client->objects, id);
+}
+
+SPA_EXPORT
+struct pw_global *pw_impl_client_get_global(struct pw_impl_client *client)
+{
+ return client->global;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_impl_client_get_properties(struct pw_impl_client *client)
+{
+ return client->properties;
+}
+
+SPA_EXPORT
+void *pw_impl_client_get_user_data(struct pw_impl_client *client)
+{
+ return client->user_data;
+}
+
+static int destroy_resource(void *object, void *data)
+{
+ if (object)
+ pw_resource_destroy(object);
+ return 0;
+}
+
+
+SPA_EXPORT
+void pw_impl_client_unref(struct pw_impl_client *client)
+{
+ struct impl *impl = SPA_CONTAINER_OF(client, struct impl, this);
+
+ assert(client->refcount > 0);
+ if (--client->refcount > 0)
+ return;
+
+ pw_log_debug("%p: free", impl);
+ assert(client->destroyed);
+
+ pw_impl_client_emit_free(client);
+
+ spa_hook_list_clean(&client->listener_list);
+
+ pw_map_clear(&client->objects);
+ pw_array_clear(&impl->permissions);
+
+ spa_hook_remove(&impl->pool_listener);
+ pw_mempool_destroy(client->pool);
+
+ pw_properties_free(client->properties);
+
+ free(impl);
+}
+
+/** Destroy a client object
+ *
+ * \param client the client to destroy
+ *
+ */
+SPA_EXPORT
+void pw_impl_client_destroy(struct pw_impl_client *client)
+{
+ struct impl *impl = SPA_CONTAINER_OF(client, struct impl, this);
+
+ pw_log_debug("%p: destroy", client);
+
+ assert(!client->destroyed);
+ client->destroyed = true;
+
+ pw_impl_client_emit_destroy(client);
+
+ spa_hook_remove(&impl->context_listener);
+
+ if (client->registered)
+ spa_list_remove(&client->link);
+
+ pw_map_for_each(&client->objects, destroy_resource, client);
+
+ if (client->global) {
+ spa_hook_remove(&client->global_listener);
+ pw_global_destroy(client->global);
+ }
+
+ pw_impl_client_unref(client);
+}
+
+SPA_EXPORT
+void pw_impl_client_add_listener(struct pw_impl_client *client,
+ struct spa_hook *listener,
+ const struct pw_impl_client_events *events,
+ void *data)
+{
+ spa_hook_list_append(&client->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+const struct pw_client_info *pw_impl_client_get_info(struct pw_impl_client *client)
+{
+ return &client->info;
+}
+
+/** Update client properties
+ *
+ * \param client the client
+ * \param dict a struct spa_dict with properties
+ *
+ * Add all properties in \a dict to the client properties. Existing
+ * properties are overwritten. Items can be removed by setting the value
+ * to NULL.
+ *
+ */
+SPA_EXPORT
+int pw_impl_client_update_properties(struct pw_impl_client *client, const struct spa_dict *dict)
+{
+ int res = update_properties(client, dict, false);
+ finish_register(client);
+ return res;
+}
+
+SPA_EXPORT
+int pw_impl_client_update_permissions(struct pw_impl_client *client,
+ uint32_t n_permissions, const struct pw_permission *permissions)
+{
+ struct pw_impl_core *core = client->core;
+ struct pw_context *context = core->context;
+ struct pw_permission *def;
+ uint32_t i;
+
+ if ((def = find_permission(client, PW_ID_ANY)) == NULL)
+ return -EIO;
+
+ for (i = 0; i < n_permissions; i++) {
+ struct pw_permission *p;
+ uint32_t old_perm, new_perm;
+ struct pw_global *global;
+
+ if (permissions[i].id == PW_ID_ANY) {
+ old_perm = def->permissions;
+ new_perm = permissions[i].permissions;
+
+ if (context->current_client == client)
+ new_perm &= old_perm;
+
+ pw_log_debug("%p: set default permissions %08x -> %08x",
+ client, old_perm, new_perm);
+
+ def->permissions = new_perm;
+
+ spa_list_for_each(global, &context->global_list, link) {
+ if (global->id == client->info.id)
+ continue;
+ p = find_permission(client, global->id);
+ if (p->id != PW_ID_ANY)
+ continue;
+ pw_global_update_permissions(global, client, old_perm, new_perm);
+ }
+ }
+ else {
+ struct pw_global *global;
+
+ global = pw_context_find_global(context, permissions[i].id);
+ if (global == NULL || global->id != permissions[i].id) {
+ pw_log_warn("%p: invalid global %d", client, permissions[i].id);
+ continue;
+ }
+ p = ensure_permissions(client, permissions[i].id);
+ if (p == NULL) {
+ pw_log_warn("%p: can't ensure permission: %m", client);
+ return -errno;
+ }
+ if ((def = find_permission(client, PW_ID_ANY)) == NULL)
+ return -EIO;
+ old_perm = p->permissions == PW_PERM_INVALID ? def->permissions : p->permissions;
+ new_perm = permissions[i].permissions;
+
+ if (context->current_client == client)
+ new_perm &= old_perm;
+
+ pw_log_debug("%p: set global %d permissions %08x -> %08x",
+ client, global->id, old_perm, new_perm);
+
+ p->permissions = new_perm;
+ pw_global_update_permissions(global, client, old_perm, new_perm);
+ }
+ }
+ update_busy(client);
+ return 0;
+}
+
+SPA_EXPORT
+void pw_impl_client_set_busy(struct pw_impl_client *client, bool busy)
+{
+ if (client->busy != busy) {
+ pw_log_debug("%p: busy %d", client, busy);
+ client->busy = busy;
+ pw_impl_client_emit_busy_changed(client, busy);
+ }
+}
+
+SPA_EXPORT
+int pw_impl_client_check_permissions(struct pw_impl_client *client,
+ uint32_t global_id, uint32_t permissions)
+{
+ struct pw_context *context = client->context;
+ struct pw_global *global;
+ uint32_t perms;
+
+ if ((global = pw_context_find_global(context, global_id)) == NULL)
+ return -ENOENT;
+
+ if (client->recv_generation != 0 && global->generation > client->recv_generation)
+ return -ESTALE;
+
+ perms = pw_global_get_permissions(global, client);
+ if ((perms & permissions) != permissions)
+ return -EPERM;
+
+ return 0;
+}
diff --git a/src/pipewire/impl-client.h b/src/pipewire/impl-client.h
new file mode 100644
index 0000000..3d20ae6
--- /dev/null
+++ b/src/pipewire/impl-client.h
@@ -0,0 +1,185 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_CLIENT_H
+#define PIPEWIRE_IMPL_CLIENT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+
+/** \page page_client_impl Client Implementation
+ *
+ * \section sec_page_client_impl_overview Overview
+ *
+ * The \ref pw_impl_client object is created by a protocol implementation when
+ * a new client connects.
+ *
+ * The client is used to keep track of all resources belonging to one
+ * connection with the PipeWire server.
+ *
+ * \section sec_page_client_impl_credentials Credentials
+ *
+ * The client object will have its credentials filled in by the protocol.
+ * This information is used to check if a resource or action is available
+ * for this client.
+ *
+ * \section sec_page_client_impl_types Types
+ *
+ * The client and server maintain a mapping between the client and server
+ * types. All type ids that are in messages exchanged between the client
+ * and server will automatically be remapped.
+ *
+ * \section sec_page_client_impl_resources Resources
+ *
+ * When a client binds to context global object, a resource is made for this
+ * binding and a unique id is assigned to the resources. The client and
+ * server will use this id as the destination when exchanging messages.
+ * See also \ref pw_resource
+ */
+
+/** \defgroup pw_impl_client Client Impl
+ *
+ * \brief PipeWire client object class
+ *
+ * The client object represents a client connection with the PipeWire
+ * server.
+ *
+ * Each client has its own list of resources it is bound to along with
+ * a mapping between the client types and server types.
+ *
+ * See: \ref page_client_impl
+ */
+
+/**
+ * \addtogroup pw_impl_client
+ * \{
+ */
+struct pw_impl_client;
+
+#include <pipewire/context.h>
+#include <pipewire/global.h>
+#include <pipewire/properties.h>
+#include <pipewire/resource.h>
+#include <pipewire/permission.h>
+
+/** The events that a client can emit */
+struct pw_impl_client_events {
+#define PW_VERSION_IMPL_CLIENT_EVENTS 0
+ uint32_t version;
+
+ /** emitted when the client is destroyed */
+ void (*destroy) (void *data);
+
+ /** emitted right before the client is freed */
+ void (*free) (void *data);
+
+ /** the client is initialized */
+ void (*initialized) (void *data);
+
+ /** emitted when the client info changed */
+ void (*info_changed) (void *data, const struct pw_client_info *info);
+
+ /** emitted when a new resource is added for client */
+ void (*resource_added) (void *data, struct pw_resource *resource);
+
+ /** emitted when a resource is removed */
+ void (*resource_removed) (void *data, struct pw_resource *resource);
+
+ /** emitted when the client becomes busy processing an asynchronous
+ * message. In the busy state no messages should be processed.
+ * Processing should resume when the client becomes not busy */
+ void (*busy_changed) (void *data, bool busy);
+};
+
+/** Create a new client. This is mainly used by protocols. */
+struct pw_impl_client *
+pw_context_create_client(struct pw_impl_core *core, /**< the core object */
+ struct pw_protocol *protocol, /**< the client protocol */
+ struct pw_properties *properties, /**< client properties */
+ size_t user_data_size /**< extra user data size */);
+
+/** Destroy a previously created client */
+void pw_impl_client_destroy(struct pw_impl_client *client);
+
+/** Finish configuration and register a client */
+int pw_impl_client_register(struct pw_impl_client *client, /**< the client to register */
+ struct pw_properties *properties/**< extra properties */);
+
+/** Get the client user data */
+void *pw_impl_client_get_user_data(struct pw_impl_client *client);
+
+/** Get the client information */
+const struct pw_client_info *pw_impl_client_get_info(struct pw_impl_client *client);
+
+/** Update the client properties */
+int pw_impl_client_update_properties(struct pw_impl_client *client, const struct spa_dict *dict);
+
+/** Update the client permissions */
+int pw_impl_client_update_permissions(struct pw_impl_client *client, uint32_t n_permissions,
+ const struct pw_permission *permissions);
+
+/** check if a client has permissions for global_id, Since 0.3.9 */
+int pw_impl_client_check_permissions(struct pw_impl_client *client,
+ uint32_t global_id, uint32_t permissions);
+
+/** Get the client properties */
+const struct pw_properties *pw_impl_client_get_properties(struct pw_impl_client *client);
+
+/** Get the context used to create this client */
+struct pw_context *pw_impl_client_get_context(struct pw_impl_client *client);
+/** Get the protocol used to create this client */
+struct pw_protocol *pw_impl_client_get_protocol(struct pw_impl_client *client);
+
+/** Get the client core resource */
+struct pw_resource *pw_impl_client_get_core_resource(struct pw_impl_client *client);
+
+/** Get a resource with the given id */
+struct pw_resource *pw_impl_client_find_resource(struct pw_impl_client *client, uint32_t id);
+
+/** Get the global associated with this client */
+struct pw_global *pw_impl_client_get_global(struct pw_impl_client *client);
+
+/** listen to events from this client */
+void pw_impl_client_add_listener(struct pw_impl_client *client,
+ struct spa_hook *listener,
+ const struct pw_impl_client_events *events,
+ void *data);
+
+
+/** Mark the client busy. This can be used when an asynchronous operation is
+ * started and no further processing is allowed to happen for the client */
+void pw_impl_client_set_busy(struct pw_impl_client *client, bool busy);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_CLIENT_H */
diff --git a/src/pipewire/impl-core.c b/src/pipewire/impl-core.c
new file mode 100644
index 0000000..776f030
--- /dev/null
+++ b/src/pipewire/impl-core.c
@@ -0,0 +1,673 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <unistd.h>
+#ifndef ENODATA
+#define ENODATA 9919
+#endif
+
+#include <spa/debug/types.h>
+#include <spa/utils/string.h>
+
+#include "pipewire/impl.h"
+#include "pipewire/private.h"
+
+#include "pipewire/extensions/protocol-native.h"
+
+PW_LOG_TOPIC_EXTERN(log_core);
+#define PW_LOG_TOPIC_DEFAULT log_core
+
+struct resource_data {
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+};
+
+static void * registry_bind(void *object, uint32_t id,
+ const char *type, uint32_t version, size_t user_data_size)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ struct pw_impl_client *client = resource->client;
+ struct pw_context *context = resource->context;
+ struct pw_global *global;
+ uint32_t permissions, new_id = user_data_size;
+
+ if ((global = pw_context_find_global(context, id)) == NULL)
+ goto error_no_id;
+
+ permissions = pw_global_get_permissions(global, client);
+
+ if (!PW_PERM_IS_R(permissions))
+ goto error_no_id;
+
+ if (resource->client->recv_generation != 0 && global->generation > resource->client->recv_generation)
+ goto error_stale_id;
+
+ if (!spa_streq(global->type, type))
+ goto error_wrong_interface;
+
+ pw_log_debug("global %p: bind global id %d, iface %s/%d to %d", global, id,
+ type, version, new_id);
+
+ if (pw_global_bind(global, client, permissions, version, new_id) < 0)
+ goto error_exit_clean;
+
+ return NULL;
+
+error_stale_id:
+ pw_log_debug("registry %p: not binding stale global "
+ "id %u to %u, generation:%"PRIu64" recv-generation:%"PRIu64,
+ resource, id, new_id, global->generation, resource->client->recv_generation);
+ pw_resource_errorf_id(resource, new_id, -ESTALE, "no global %u any more", id);
+ goto error_exit_clean;
+error_no_id:
+ pw_log_debug("registry %p: no global with id %u to bind to %u", resource, id, new_id);
+ pw_resource_errorf_id(resource, new_id, -ENOENT, "no global %u", id);
+ goto error_exit_clean;
+error_wrong_interface:
+ pw_log_debug("registry %p: global with id %u has no interface %s", resource, id, type);
+ pw_resource_errorf_id(resource, new_id, -ENOSYS, "no interface %s", type);
+ goto error_exit_clean;
+error_exit_clean:
+ /* unmark the new_id the map, the client does not yet know about the failed
+ * bind and will choose the next id, which we would refuse when we don't mark
+ * new_id as 'used and freed' */
+ pw_map_insert_at(&client->objects, new_id, NULL);
+ pw_core_resource_remove_id(client->core_resource, new_id);
+ return NULL;
+}
+
+static int registry_destroy(void *object, uint32_t id)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ struct pw_impl_client *client = resource->client;
+ struct pw_context *context = resource->context;
+ struct pw_global *global;
+ uint32_t permissions;
+ int res;
+
+ if ((global = pw_context_find_global(context, id)) == NULL)
+ goto error_no_id;
+
+ permissions = pw_global_get_permissions(global, client);
+
+ if (!PW_PERM_IS_R(permissions))
+ goto error_no_id;
+
+ if (resource->client->recv_generation != 0 && global->generation > resource->client->recv_generation)
+ goto error_stale_id;
+
+ if (id == PW_ID_CORE || !PW_PERM_IS_X(permissions))
+ goto error_not_allowed;
+
+ pw_log_debug("global %p: destroy global id %d", global, id);
+
+ pw_global_destroy(global);
+ return 0;
+
+error_stale_id:
+ pw_log_debug("registry %p: not destroying stale global "
+ "id %u, generation:%"PRIu64" recv-generation:%"PRIu64,
+ resource, id, global->generation, resource->client->recv_generation);
+ pw_resource_errorf(resource, -ESTALE, "no global %u any more", id);
+ res = -ESTALE;
+ goto error_exit;
+error_no_id:
+ pw_log_debug("registry %p: no global with id %u to destroy", resource, id);
+ pw_resource_errorf(resource, -ENOENT, "no global %u", id);
+ res = -ENOENT;
+ goto error_exit;
+error_not_allowed:
+ pw_log_debug("registry %p: destroy of id %u not allowed", resource, id);
+ pw_resource_errorf(resource, -EPERM, "no permission to destroy %u", id);
+ res = -EPERM;
+ goto error_exit;
+error_exit:
+ return res;
+}
+
+static const struct pw_registry_methods registry_methods = {
+ PW_VERSION_REGISTRY_METHODS,
+ .bind = registry_bind,
+ .destroy = registry_destroy
+};
+
+static void destroy_registry_resource(void *_data)
+{
+ struct resource_data *data = _data;
+ struct pw_resource *resource = data->resource;
+ spa_list_remove(&resource->link);
+ spa_hook_remove(&data->resource_listener);
+ spa_hook_remove(&data->object_listener);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = destroy_registry_resource
+};
+
+static int destroy_resource(void *object, void *data)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client;
+
+ if (resource &&
+ (client = resource->client) != NULL &&
+ resource != client->core_resource) {
+ pw_resource_remove(resource);
+ }
+ return 0;
+}
+
+static int core_hello(void *object, uint32_t version)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = resource->client;
+ struct pw_context *context = client->context;
+ struct pw_impl_core *this = client->core;
+ int res;
+
+ pw_log_debug("%p: hello %d from resource %p", context, version, resource);
+ pw_map_for_each(&client->objects, destroy_resource, client);
+
+ pw_mempool_clear(client->pool);
+
+ this->info.change_mask = PW_CORE_CHANGE_MASK_ALL;
+ pw_core_resource_info(resource, &this->info);
+
+ if (version >= 3) {
+ if ((res = pw_global_bind(client->global, client,
+ PW_PERM_ALL, PW_VERSION_CLIENT, PW_ID_CLIENT)) < 0)
+ return res;
+ }
+ return 0;
+}
+
+static int core_sync(void *object, uint32_t id, int seq)
+{
+ struct pw_resource *resource = object;
+ pw_log_trace("%p: sync %d for resource %d", resource->context, seq, id);
+ pw_core_resource_done(resource, id, seq);
+ return 0;
+}
+
+static int core_pong(void *object, uint32_t id, int seq)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = resource->client;
+ struct pw_resource *r;
+
+ pw_log_debug("%p: pong %d for resource %d", resource->context, seq, id);
+
+ if ((r = pw_impl_client_find_resource(client, id)) == NULL)
+ return -EINVAL;
+
+ pw_resource_emit_pong(r, seq);
+ return 0;
+}
+
+static int core_error(void *object, uint32_t id, int seq, int res, const char *message)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = resource->client;
+ struct pw_resource *r;
+
+ pw_log_error("%p: error %d for resource %d: %s", resource->context, res, id, message);
+
+ if ((r = pw_impl_client_find_resource(client, id)) == NULL)
+ return -EINVAL;
+
+ pw_resource_emit_error(r, seq, res, message);
+ return 0;
+}
+
+static struct pw_registry *core_get_registry(void *object, uint32_t version, size_t user_data_size)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = resource->client;
+ struct pw_context *context = client->context;
+ struct pw_global *global;
+ struct pw_resource *registry_resource;
+ struct resource_data *data;
+ uint32_t new_id = user_data_size;
+ int res;
+
+ registry_resource = pw_resource_new(client,
+ new_id,
+ PW_PERM_ALL,
+ PW_TYPE_INTERFACE_Registry,
+ version,
+ sizeof(*data));
+ if (registry_resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ data = pw_resource_get_user_data(registry_resource);
+ data->resource = registry_resource;
+ pw_resource_add_listener(registry_resource,
+ &data->resource_listener,
+ &resource_events,
+ data);
+ pw_resource_add_object_listener(registry_resource,
+ &data->object_listener,
+ &registry_methods,
+ data);
+
+ spa_list_append(&context->registry_resource_list, &registry_resource->link);
+
+ spa_list_for_each(global, &context->global_list, link) {
+ uint32_t permissions = pw_global_get_permissions(global, client);
+ if (PW_PERM_IS_R(permissions)) {
+ pw_registry_resource_global(registry_resource,
+ global->id,
+ permissions,
+ global->type,
+ global->version,
+ &global->properties->dict);
+ }
+ }
+
+ return (struct pw_registry *)registry_resource;
+
+error_resource:
+ pw_core_resource_errorf(client->core_resource, new_id,
+ client->recv_seq, res,
+ "can't create registry resource: %d (%s)",
+ res, spa_strerror(res));
+ pw_map_insert_at(&client->objects, new_id, NULL);
+ pw_core_resource_remove_id(client->core_resource, new_id);
+ errno = -res;
+ return NULL;
+}
+
+static void *
+core_create_object(void *object,
+ const char *factory_name,
+ const char *type,
+ uint32_t version,
+ const struct spa_dict *props,
+ size_t user_data_size)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = resource->client;
+ struct pw_impl_factory *factory;
+ void *obj;
+ struct pw_properties *properties;
+ struct pw_context *context = client->context;
+ uint32_t new_id = user_data_size;
+ int res;
+
+ factory = pw_context_find_factory(context, factory_name);
+ if (factory == NULL || factory->global == NULL)
+ goto error_no_factory;
+
+ if (!PW_PERM_IS_R(pw_global_get_permissions(factory->global, client)))
+ goto error_no_factory;
+
+ if (!spa_streq(factory->info.type, type))
+ goto error_type;
+
+ if (factory->info.version < version) {
+ pw_log_info("%p: version %d < %d", context,
+ factory->info.version, version);
+ }
+
+ if (props) {
+ properties = pw_properties_new_dict(props);
+ if (properties == NULL)
+ goto error_properties;
+ } else
+ properties = NULL;
+
+ /* error will be posted */
+ obj = pw_impl_factory_create_object(factory, resource, type,
+ version, properties, new_id);
+ if (obj == NULL)
+ goto error_create_failed;
+
+ return 0;
+
+error_no_factory:
+ res = -ENOENT;
+ pw_log_debug("%p: can't find factory '%s'", context, factory_name);
+ pw_resource_errorf_id(resource, new_id, res, "unknown factory name %s", factory_name);
+ goto error_exit;
+error_type:
+ res = -EPROTO;
+ pw_log_debug("%p: invalid resource type/version", context);
+ pw_resource_errorf_id(resource, new_id, res, "wrong resource type/version");
+ goto error_exit;
+error_properties:
+ res = -errno;
+ pw_log_debug("%p: can't create properties: %m", context);
+ pw_resource_errorf_id(resource, new_id, res, "can't create properties: %s", spa_strerror(res));
+ goto error_exit;
+error_create_failed:
+ res = -errno;
+ goto error_exit;
+error_exit:
+ pw_map_insert_at(&client->objects, new_id, NULL);
+ pw_core_resource_remove_id(client->core_resource, new_id);
+ errno = -res;
+ return NULL;
+}
+
+static int core_destroy(void *object, void *proxy)
+{
+ struct pw_resource *resource = object;
+ struct pw_impl_client *client = resource->client;
+ struct pw_impl_core *this = client->core;
+ struct pw_resource *r = proxy;
+ pw_log_debug("%p: destroy resource %p from client %p", this, r, client);
+ pw_resource_destroy(r);
+ return 0;
+}
+
+static const struct pw_core_methods core_methods = {
+ PW_VERSION_CORE_METHODS,
+ .hello = core_hello,
+ .sync = core_sync,
+ .pong = core_pong,
+ .error = core_error,
+ .get_registry = core_get_registry,
+ .create_object = core_create_object,
+ .destroy = core_destroy,
+};
+
+SPA_EXPORT
+struct pw_impl_core *pw_context_create_core(struct pw_context *context,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_core *this;
+ const char *name;
+ int res;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ this = calloc(1, sizeof(*this) + user_data_size);
+ if (this == NULL) {
+ res = -errno;
+ goto error_exit;
+ };
+
+ this->context = context;
+ this->properties = properties;
+
+ if ((name = pw_properties_get(properties, PW_KEY_CORE_NAME)) == NULL) {
+ pw_properties_setf(properties,
+ PW_KEY_CORE_NAME, "pipewire-%s-%d",
+ pw_get_user_name(), getpid());
+ name = pw_properties_get(properties, PW_KEY_CORE_NAME);
+ }
+
+ this->info.user_name = pw_get_user_name();
+ this->info.host_name = pw_get_host_name();
+ this->info.version = pw_get_library_version();
+ do {
+ res = pw_getrandom(&this->info.cookie,
+ sizeof(this->info.cookie), 0);
+ } while ((res == -1) && (errno == EINTR));
+ if (res == -1) {
+ res = -errno;
+ goto error_exit;
+ } else if (res != sizeof(this->info.cookie)) {
+ res = -ENODATA;
+ goto error_exit;
+ }
+ this->info.name = name;
+ spa_hook_list_init(&this->listener_list);
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(this, sizeof(*this), void);
+
+ pw_log_debug("%p: new %s", this, name);
+
+ return this;
+
+error_exit:
+ pw_properties_free(properties);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_impl_core *pw_context_get_default_core(struct pw_context *context)
+{
+ return context->core;
+}
+
+SPA_EXPORT
+void pw_impl_core_destroy(struct pw_impl_core *core)
+{
+ pw_log_debug("%p: destroy", core);
+ pw_impl_core_emit_destroy(core);
+
+ if (core->registered)
+ spa_list_remove(&core->link);
+
+ if (core->global) {
+ spa_hook_remove(&core->global_listener);
+ pw_global_destroy(core->global);
+ }
+
+ pw_impl_core_emit_free(core);
+ pw_log_debug("%p: free", core);
+
+ spa_hook_list_clean(&core->listener_list);
+
+ pw_properties_free(core->properties);
+
+ free(core);
+}
+
+static void core_unbind_func(void *data)
+{
+ struct resource_data *d = data;
+ struct pw_resource *resource = d->resource;
+ spa_hook_remove(&d->resource_listener);
+ spa_hook_remove(&d->object_listener);
+ if (resource->id == 0)
+ resource->client->core_resource = NULL;
+}
+
+static const struct pw_resource_events core_resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = core_unbind_func,
+};
+
+static int
+global_bind(void *object,
+ struct pw_impl_client *client,
+ uint32_t permissions,
+ uint32_t version,
+ uint32_t id)
+{
+ struct pw_impl_core *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+ int res;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, sizeof(*data));
+ if (resource == NULL) {
+ res = -errno;
+ goto error;
+ }
+
+ data = pw_resource_get_user_data(resource);
+ data->resource = resource;
+
+ pw_resource_add_listener(resource,
+ &data->resource_listener,
+ &core_resource_events, data);
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &core_methods, resource);
+
+ pw_global_add_resource(global, resource);
+
+ if (resource->id == 0) {
+ client->core_resource = resource;
+ }
+ else {
+ this->info.change_mask = PW_CORE_CHANGE_MASK_ALL;
+ pw_core_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+ }
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+
+ return 0;
+
+error:
+ pw_log_error("%p: can't create resource: %m", this);
+ return res;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_core *core = data;
+ spa_hook_remove(&core->global_listener);
+ core->global = NULL;
+ pw_impl_core_destroy(core);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+SPA_EXPORT
+const struct pw_properties *pw_impl_core_get_properties(struct pw_impl_core *core)
+{
+ return core->properties;
+}
+
+SPA_EXPORT
+int pw_impl_core_update_properties(struct pw_impl_core *core, const struct spa_dict *dict)
+{
+ struct pw_resource *resource;
+ int changed;
+
+ changed = pw_properties_update(core->properties, dict);
+ core->info.props = &core->properties->dict;
+
+ pw_log_debug("%p: updated %d properties", core, changed);
+
+ if (!changed)
+ return 0;
+
+ core->info.change_mask |= PW_CORE_CHANGE_MASK_PROPS;
+ if (core->global)
+ spa_list_for_each(resource, &core->global->resource_list, link)
+ pw_core_resource_info(resource, &core->info);
+ core->info.change_mask = 0;
+
+ return changed;
+}
+
+SPA_EXPORT
+int pw_impl_core_register(struct pw_impl_core *core,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_USER_NAME,
+ PW_KEY_HOST_NAME,
+ PW_KEY_CORE_NAME,
+ PW_KEY_CORE_VERSION,
+ NULL
+ };
+
+ struct pw_context *context = core->context;
+ int res;
+
+ if (core->registered)
+ goto error_existed;
+
+ core->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Core,
+ PW_VERSION_CORE,
+ properties,
+ global_bind,
+ core);
+ if (core->global == NULL)
+ return -errno;
+
+ spa_list_append(&context->core_impl_list, &core->link);
+ core->registered = true;
+
+ core->info.id = core->global->id;
+ pw_properties_setf(core->properties, PW_KEY_OBJECT_ID, "%d", core->info.id);
+ pw_properties_setf(core->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(core->global));
+ core->info.props = &core->properties->dict;
+
+ pw_global_update_keys(core->global, core->info.props, keys);
+
+ pw_impl_core_emit_initialized(core);
+
+ pw_global_add_listener(core->global, &core->global_listener, &global_events, core);
+ pw_global_register(core->global);
+
+ return 0;
+
+error_existed:
+ res = -EEXIST;
+ goto error_exit;
+error_exit:
+ pw_properties_free(properties);
+ return res;
+}
+
+SPA_EXPORT
+void *pw_impl_core_get_user_data(struct pw_impl_core *core)
+{
+ return core->user_data;
+}
+
+SPA_EXPORT
+struct pw_global *pw_impl_core_get_global(struct pw_impl_core *core)
+{
+ return core->global;
+}
+
+SPA_EXPORT
+void pw_impl_core_add_listener(struct pw_impl_core *core,
+ struct spa_hook *listener,
+ const struct pw_impl_core_events *events,
+ void *data)
+{
+ spa_hook_list_append(&core->listener_list, listener, events, data);
+}
diff --git a/src/pipewire/impl-core.h b/src/pipewire/impl-core.h
new file mode 100644
index 0000000..ec4107e
--- /dev/null
+++ b/src/pipewire/impl-core.h
@@ -0,0 +1,104 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_CORE_H
+#define PIPEWIRE_IMPL_CORE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_impl_core Core Impl
+ *
+ * \brief PipeWire core interface.
+ *
+ * The core is used to make objects on demand.
+ */
+
+/**
+ * \addtogroup pw_impl_core
+ * \{
+ */
+
+struct pw_impl_core;
+
+#include <pipewire/context.h>
+#include <pipewire/global.h>
+#include <pipewire/properties.h>
+#include <pipewire/resource.h>
+
+/** Factory events, listen to them with \ref pw_impl_core_add_listener */
+struct pw_impl_core_events {
+#define PW_VERSION_IMPL_CORE_EVENTS 0
+ uint32_t version;
+
+ /** the core is destroyed */
+ void (*destroy) (void *data);
+ /** the core is freed */
+ void (*free) (void *data);
+ /** the core is initialized */
+ void (*initialized) (void *data);
+};
+
+struct pw_impl_core *pw_context_create_core(struct pw_context *context,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+/* get the default core in a context */
+struct pw_impl_core *pw_context_get_default_core(struct pw_context *context);
+
+/** Get the core properties */
+const struct pw_properties *pw_impl_core_get_properties(struct pw_impl_core *core);
+
+/** Get the core information */
+const struct pw_core_info *pw_impl_core_get_info(struct pw_impl_core *core);
+
+/** Update the core properties */
+int pw_impl_core_update_properties(struct pw_impl_core *core, const struct spa_dict *dict);
+
+int pw_impl_core_register(struct pw_impl_core *core,
+ struct pw_properties *properties);
+
+void pw_impl_core_destroy(struct pw_impl_core *core);
+
+void *pw_impl_core_get_user_data(struct pw_impl_core *core);
+
+/** Get the global of this core */
+struct pw_global *pw_impl_core_get_global(struct pw_impl_core *core);
+
+/** Add an event listener */
+void pw_impl_core_add_listener(struct pw_impl_core *core,
+ struct spa_hook *listener,
+ const struct pw_impl_core_events *events,
+ void *data);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_CORE_H */
diff --git a/src/pipewire/impl-device.c b/src/pipewire/impl-device.c
new file mode 100644
index 0000000..855b046
--- /dev/null
+++ b/src/pipewire/impl-device.c
@@ -0,0 +1,935 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/debug/types.h>
+#include <spa/monitor/utils.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/utils/string.h>
+
+#include "pipewire/impl.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_device);
+#define PW_LOG_TOPIC_DEFAULT log_device
+
+struct impl {
+ struct pw_impl_device this;
+
+ struct spa_list param_list;
+ struct spa_list pending_list;
+
+ unsigned int cache_params:1;
+};
+
+#define pw_device_resource(r,m,v,...) pw_resource_call(r,struct pw_device_events,m,v,__VA_ARGS__)
+#define pw_device_resource_info(r,...) pw_device_resource(r,info,0,__VA_ARGS__)
+#define pw_device_resource_param(r,...) pw_device_resource(r,param,0,__VA_ARGS__)
+
+struct result_device_params_data {
+ struct impl *impl;
+ void *data;
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param);
+ int seq;
+ uint32_t count;
+ unsigned int cache:1;
+};
+
+struct resource_data {
+ struct pw_impl_device *device;
+ struct pw_resource *resource;
+
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ uint32_t subscribe_ids[MAX_PARAMS];
+ uint32_t n_subscribe_ids;
+
+ /* for async replies */
+ int seq;
+ int orig_seq;
+ int end;
+ struct spa_param_info *pi;
+ struct result_device_params_data data;
+ struct spa_hook listener;
+};
+
+struct object_data {
+ struct spa_list link;
+ uint32_t id;
+#define OBJECT_NODE 0
+#define OBJECT_DEVICE 1
+ uint32_t type;
+ struct spa_handle *handle;
+ void *object;
+ struct spa_hook listener;
+};
+
+static void object_destroy(struct object_data *od)
+{
+ switch (od->type) {
+ case OBJECT_NODE:
+ pw_impl_node_destroy(od->object);
+ break;
+ case OBJECT_DEVICE:
+ pw_impl_device_destroy(od->object);
+ break;
+ }
+}
+
+static void object_update(struct object_data *od, const struct spa_dict *props)
+{
+ switch (od->type) {
+ case OBJECT_NODE:
+ pw_impl_node_update_properties(od->object, props);
+ break;
+ case OBJECT_DEVICE:
+ pw_impl_device_update_properties(od->object, props);
+ break;
+ }
+}
+
+static void object_register(struct object_data *od)
+{
+ switch (od->type) {
+ case OBJECT_NODE:
+ pw_impl_node_register(od->object, NULL);
+ pw_impl_node_set_active(od->object, true);
+ break;
+ case OBJECT_DEVICE:
+ pw_impl_device_register(od->object, NULL);
+ break;
+ }
+}
+
+static void check_properties(struct pw_impl_device *device)
+{
+ const char *str;
+
+ if ((str = pw_properties_get(device->properties, PW_KEY_DEVICE_NAME)) &&
+ (device->name == NULL || !spa_streq(str, device->name))) {
+ free(device->name);
+ device->name = strdup(str);
+ pw_log_debug("%p: name '%s'", device, device->name);
+ }
+}
+
+SPA_EXPORT
+struct pw_impl_device *pw_context_create_device(struct pw_context *context,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct impl *impl;
+ struct pw_impl_device *this;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl) + user_data_size);
+ if (impl == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+ spa_list_init(&impl->param_list);
+ spa_list_init(&impl->pending_list);
+ impl->cache_params = true;
+
+ this = &impl->this;
+ this->name = strdup("device");
+ pw_log_debug("%p: new", this);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ this->context = context;
+ this->properties = properties;
+
+ this->info.props = &properties->dict;
+ this->info.params = this->params;
+ spa_hook_list_init(&this->listener_list);
+
+ spa_list_init(&this->object_list);
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(this, sizeof(struct impl), void);
+
+ check_properties(this);
+
+ return this;
+
+error_free:
+ free(impl);
+error_cleanup:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+void pw_impl_device_destroy(struct pw_impl_device *device)
+{
+ struct impl *impl = SPA_CONTAINER_OF(device, struct impl, this);
+ struct object_data *od;
+
+ pw_log_debug("%p: destroy", device);
+ pw_impl_device_emit_destroy(device);
+
+ spa_list_consume(od, &device->object_list, link)
+ object_destroy(od);
+
+ if (device->registered)
+ spa_list_remove(&device->link);
+
+ if (device->device)
+ spa_hook_remove(&device->listener);
+
+ if (device->global) {
+ spa_hook_remove(&device->global_listener);
+ pw_global_destroy(device->global);
+ }
+ pw_log_debug("%p: free", device);
+ pw_impl_device_emit_free(device);
+
+ pw_param_clear(&impl->param_list, SPA_ID_INVALID);
+ pw_param_clear(&impl->pending_list, SPA_ID_INVALID);
+
+ spa_hook_list_clean(&device->listener_list);
+
+ pw_properties_free(device->properties);
+ free(device->name);
+
+ free(device);
+}
+
+static void remove_busy_resource(struct resource_data *d)
+{
+ struct pw_impl_device *device = d->device;
+ struct impl *impl = SPA_CONTAINER_OF(device, struct impl, this);
+
+ if (d->end != -1) {
+ if (d->pi && d->data.cache) {
+ pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL);
+ d->pi->user = 1;
+ d->pi = NULL;
+ }
+ spa_hook_remove(&d->listener);
+ d->end = -1;
+ pw_impl_client_set_busy(d->resource->client, false);
+ }
+}
+
+static void resource_destroy(void *data)
+{
+ struct resource_data *d = data;
+ remove_busy_resource(d);
+ spa_hook_remove(&d->resource_listener);
+ spa_hook_remove(&d->object_listener);
+}
+
+static void resource_pong(void *data, int seq)
+{
+ struct resource_data *d = data;
+ struct pw_resource *resource = d->resource;
+ pw_log_debug("%p: resource %p: got pong %d", d->device,
+ resource, seq);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy,
+ .pong = resource_pong,
+};
+
+static void result_device_params(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct result_device_params_data *d = data;
+ struct impl *impl = d->impl;
+ pw_log_debug("%p: type %d", impl, type);
+
+ switch (type) {
+ case SPA_RESULT_TYPE_DEVICE_PARAMS:
+ {
+ const struct spa_result_device_params *r = result;
+ d->callback(d->data, seq, r->id, r->index, r->next, r->param);
+ if (d->cache) {
+ pw_log_debug("%p: add param %d", impl, r->id);
+ if (d->count++ == 0)
+ pw_param_add(&impl->pending_list, seq, r->id, NULL);
+ pw_param_add(&impl->pending_list, seq, r->id, r->param);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+SPA_EXPORT
+int pw_impl_device_for_each_param(struct pw_impl_device *device,
+ int seq, uint32_t param_id,
+ uint32_t index, uint32_t max,
+ const struct spa_pod *filter,
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param),
+ void *data)
+{
+ int res;
+ struct impl *impl = SPA_CONTAINER_OF(device, struct impl, this);
+ struct result_device_params_data user_data = { impl, data, callback, seq, 0, false };
+ struct spa_hook listener;
+ struct spa_param_info *pi;
+ static const struct spa_device_events device_events = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .result = result_device_params,
+ };
+
+ pi = pw_param_info_find(device->info.params, device->info.n_params, param_id);
+ if (pi == NULL)
+ return -ENOENT;
+
+ if (max == 0)
+ max = UINT32_MAX;
+
+ pw_log_debug("%p: params id:%d (%s) index:%u max:%u cached:%d", device, param_id,
+ spa_debug_type_find_name(spa_type_param, param_id),
+ index, max, pi->user);
+
+ if (pi->user == 1) {
+ struct pw_param *p;
+ uint8_t buffer[4096];
+ struct spa_pod_dynamic_builder b;
+ struct spa_result_device_params result;
+ uint32_t count = 0;
+
+ result.id = param_id;
+ result.next = 0;
+
+ spa_list_for_each(p, &impl->param_list, link) {
+ if (p->id != param_id)
+ continue;
+
+ result.index = result.next++;
+ if (result.index < index)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ if (spa_pod_filter(&b.b, &result.param, p->param, filter) == 0) {
+ pw_log_debug("%p: %d param %u", device, seq, result.index);
+ result_device_params(&user_data, seq, 0, SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == max)
+ break;
+ }
+ res = 0;
+ } else {
+ user_data.cache = impl->cache_params &&
+ (filter == NULL && index == 0 && max == UINT32_MAX);
+
+ spa_zero(listener);
+ spa_device_add_listener(device->device, &listener,
+ &device_events, &user_data);
+ res = spa_device_enum_params(device->device, seq,
+ param_id, index, max, filter);
+ spa_hook_remove(&listener);
+
+ if (!SPA_RESULT_IS_ASYNC(res) && user_data.cache) {
+ pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL);
+ pi->user = 1;
+ }
+ }
+
+ return res;
+}
+
+static int reply_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct resource_data *d = data;
+ pw_device_resource_param(d->resource, seq, id, index, next, param);
+ return 0;
+}
+
+static void result_device_params_async(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct resource_data *d = data;
+
+ pw_log_debug("%p: async result %d %d (%d/%d)", d->device,
+ res, seq, d->seq, d->end);
+
+ if (seq == d->seq)
+ result_device_params(&d->data, d->orig_seq, res, type, result);
+ if (seq == d->end)
+ remove_busy_resource(d);
+}
+
+static int device_enum_params(void *object, int seq, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ struct pw_impl_device *device = data->device;
+ struct impl *impl = SPA_CONTAINER_OF(device, struct impl, this);
+ struct pw_impl_client *client = resource->client;
+ int res;
+ static const struct spa_device_events device_events = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .result = result_device_params_async,
+ };
+
+ res = pw_impl_device_for_each_param(device, seq, id, start, num,
+ filter, reply_param, data);
+
+ if (res < 0) {
+ pw_resource_errorf(resource, res,
+ "enum params id:%d (%s) failed", id,
+ spa_debug_type_find_name(spa_type_param, id));
+ } else if (SPA_RESULT_IS_ASYNC(res)) {
+ pw_impl_client_set_busy(client, true);
+ data->data.impl = impl;
+ data->data.data = data;
+ data->data.callback = reply_param;
+ data->data.count = 0;
+ data->data.cache = impl->cache_params &&
+ (filter == NULL && start == 0);
+ if (data->end == -1)
+ spa_device_add_listener(device->device, &data->listener,
+ &device_events, data);
+ data->pi = pw_param_info_find(device->info.params,
+ device->info.n_params, id);
+ data->orig_seq = seq;
+ data->seq = res;
+ data->end = spa_device_sync(device->device, res);
+ }
+
+ return res;
+}
+
+static int device_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug("%p: resource %p subscribe param id:%d (%s)",
+ data->device, resource, ids[i],
+ spa_debug_type_find_name(spa_type_param, ids[i]));
+ device_enum_params(data, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static void result_device_done(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct resource_data *d = data;
+
+ pw_log_debug("%p: async result %d %d (%d/%d)", d->device,
+ res, seq, d->seq, d->end);
+
+ if (seq == d->end)
+ remove_busy_resource(d);
+}
+
+static int device_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ struct pw_impl_device *device = data->device;
+ struct pw_impl_client *client = resource->client;
+ int res;
+ static const struct spa_device_events device_events = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .result = result_device_done,
+ };
+
+ if ((res = spa_device_set_param(device->device, id, flags, param)) < 0) {
+ pw_resource_errorf(resource, res,
+ "set param id:%d (%s) flags:%08x failed", id,
+ spa_debug_type_find_name(spa_type_param, id), flags);
+ } else if (SPA_RESULT_IS_ASYNC(res)) {
+ pw_impl_client_set_busy(client, true);
+ data->data.data = data;
+ if (data->end == -1)
+ spa_device_add_listener(device->device, &data->listener,
+ &device_events, data);
+ data->seq = res;
+ data->end = spa_device_sync(device->device, res);
+ }
+ return res;
+}
+
+static const struct pw_device_methods device_methods = {
+ PW_VERSION_DEVICE_METHODS,
+ .subscribe_params = device_subscribe_params,
+ .enum_params = device_enum_params,
+ .set_param = device_set_param
+};
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct pw_impl_device *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, sizeof(*data));
+ if (resource == NULL)
+ goto error_resource;
+
+ data = pw_resource_get_user_data(resource);
+ data->device = this;
+ data->resource = resource;
+ data->end = -1;
+
+ pw_resource_add_listener(resource,
+ &data->resource_listener,
+ &resource_events, data);
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &device_methods, data);
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_DEVICE_CHANGE_MASK_ALL;
+ pw_device_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+error_resource:
+ pw_log_error("%p: can't create device resource: %m", this);
+ return -errno;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_device *device = data;
+ spa_hook_remove(&device->global_listener);
+ device->global = NULL;
+ pw_impl_device_destroy(device);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+SPA_EXPORT
+int pw_impl_device_register(struct pw_impl_device *device,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_OBJECT_PATH,
+ PW_KEY_MODULE_ID,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_DEVICE_API,
+ PW_KEY_DEVICE_DESCRIPTION,
+ PW_KEY_DEVICE_NAME,
+ PW_KEY_DEVICE_NICK,
+ PW_KEY_MEDIA_CLASS,
+ NULL
+ };
+
+ struct pw_context *context = device->context;
+ struct object_data *od;
+
+ if (device->registered)
+ goto error_existed;
+
+ device->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Device,
+ PW_VERSION_DEVICE,
+ properties,
+ global_bind,
+ device);
+ if (device->global == NULL)
+ return -errno;
+
+ spa_list_append(&context->device_list, &device->link);
+ device->registered = true;
+
+ device->info.id = device->global->id;
+ pw_properties_setf(device->properties, PW_KEY_OBJECT_ID, "%d", device->info.id);
+ pw_properties_setf(device->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(device->global));
+ device->info.props = &device->properties->dict;
+
+ pw_global_update_keys(device->global, device->info.props, keys);
+
+ pw_impl_device_emit_initialized(device);
+
+ pw_global_add_listener(device->global, &device->global_listener, &global_events, device);
+ pw_global_register(device->global);
+
+ spa_list_for_each(od, &device->object_list, link)
+ object_register(od);
+
+ return 0;
+
+error_existed:
+ pw_properties_free(properties);
+ return -EEXIST;
+}
+
+static void on_object_destroy(void *data)
+{
+ struct object_data *od = data;
+ spa_list_remove(&od->link);
+}
+
+static void on_object_free(void *data)
+{
+ struct object_data *od = data;
+ pw_unload_spa_handle(od->handle);
+}
+
+static const struct pw_impl_node_events node_object_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .destroy = on_object_destroy,
+ .free = on_object_free,
+};
+
+static const struct pw_impl_device_events device_object_events = {
+ PW_VERSION_IMPL_DEVICE_EVENTS,
+ .destroy = on_object_destroy,
+ .free = on_object_free,
+};
+
+static void emit_info_changed(struct pw_impl_device *device)
+{
+ struct pw_resource *resource;
+
+ pw_impl_device_emit_info_changed(device, &device->info);
+
+ if (device->global)
+ spa_list_for_each(resource, &device->global->resource_list, link)
+ pw_device_resource_info(resource, &device->info);
+
+ device->info.change_mask = 0;
+}
+
+static int update_properties(struct pw_impl_device *device, const struct spa_dict *dict, bool filter)
+{
+ static const char * const ignored[] = {
+ PW_KEY_OBJECT_ID,
+ PW_KEY_MODULE_ID,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ NULL
+ };
+
+ int changed;
+
+ changed = pw_properties_update_ignore(device->properties, dict, filter ? ignored : NULL);
+ device->info.props = &device->properties->dict;
+
+ pw_log_debug("%p: updated %d properties", device, changed);
+
+ if (!changed)
+ return 0;
+
+ device->info.change_mask |= PW_DEVICE_CHANGE_MASK_PROPS;
+
+ return changed;
+}
+
+static int resource_is_subscribed(struct pw_resource *resource, uint32_t id)
+{
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == id)
+ return 1;
+ }
+ return 0;
+}
+
+static int notify_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct pw_impl_device *device = data;
+ struct pw_resource *resource;
+
+ spa_list_for_each(resource, &device->global->resource_list, link) {
+ if (!resource_is_subscribed(resource, id))
+ continue;
+
+ pw_log_debug("%p: resource %p notify param %d", device, resource, id);
+ pw_device_resource_param(resource, seq, id, index, next, param);
+ }
+ return 0;
+}
+
+static void emit_params(struct pw_impl_device *device, uint32_t *changed_ids, uint32_t n_changed_ids)
+{
+ uint32_t i;
+ int res;
+
+ if (device->global == NULL)
+ return;
+
+ pw_log_debug("%p: emit %d params", device, n_changed_ids);
+
+ for (i = 0; i < n_changed_ids; i++) {
+ struct pw_resource *resource;
+ int subscribed = 0;
+
+ /* first check if anyone is subscribed */
+ spa_list_for_each(resource, &device->global->resource_list, link) {
+ if ((subscribed = resource_is_subscribed(resource, changed_ids[i])))
+ break;
+ }
+ if (!subscribed)
+ continue;
+
+ if ((res = pw_impl_device_for_each_param(device, 1, changed_ids[i], 0, UINT32_MAX,
+ NULL, notify_param, device)) < 0) {
+ pw_log_error("%p: error %d (%s)", device, res, spa_strerror(res));
+ }
+ }
+}
+
+static void device_info(void *data, const struct spa_device_info *info)
+{
+ struct pw_impl_device *device = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+
+ pw_log_debug("%p: flags:%08"PRIx64" change_mask:%08"PRIx64,
+ device, info->flags, info->change_mask);
+
+ if (info->change_mask & SPA_DEVICE_CHANGE_MASK_PROPS) {
+ update_properties(device, info->props, true);
+ }
+ if (info->change_mask & SPA_DEVICE_CHANGE_MASK_PARAMS) {
+ uint32_t i;
+
+ device->info.change_mask |= PW_DEVICE_CHANGE_MASK_PARAMS;
+ device->info.n_params = SPA_MIN(info->n_params, SPA_N_ELEMENTS(device->params));
+
+ for (i = 0; i < device->info.n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ pw_log_debug("%p: param %d id:%d (%s) %08x:%08x", device, i,
+ id, spa_debug_type_find_name(spa_type_param, id),
+ device->info.params[i].flags, info->params[i].flags);
+
+ device->info.params[i].id = device->params[i].id;
+ if (device->info.params[i].flags == info->params[i].flags)
+ continue;
+
+ pw_log_debug("%p: update param %d", device, id);
+ device->info.params[i] = info->params[i];
+ device->info.params[i].user = 0;
+
+ if (info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = id;
+ }
+ }
+ emit_info_changed(device);
+
+ if (n_changed_ids > 0)
+ emit_params(device, changed_ids, n_changed_ids);
+}
+
+static void device_add_object(struct pw_impl_device *device, uint32_t id,
+ const struct spa_device_object_info *info)
+{
+ struct pw_context *context = device->context;
+ struct spa_handle *handle;
+ struct pw_properties *props;
+ int res;
+ void *iface;
+ struct object_data *od = NULL;
+
+ if (info->factory_name == NULL) {
+ pw_log_debug("%p: missing factory name", device);
+ return;
+ }
+
+ handle = pw_context_load_spa_handle(context, info->factory_name, info->props);
+ if (handle == NULL) {
+ pw_log_warn("%p: can't load handle %s: %m",
+ device, info->factory_name);
+ return;
+ }
+
+ if ((res = spa_handle_get_interface(handle, info->type, &iface)) < 0) {
+ pw_log_error("%p: can't get %s interface: %s", device, info->type,
+ spa_strerror(res));
+ return;
+ }
+
+ props = pw_properties_copy(device->properties);
+ if (info->props && props)
+ pw_properties_update(props, info->props);
+
+ if (spa_streq(info->type, SPA_TYPE_INTERFACE_Node)) {
+ struct pw_impl_node *node;
+ node = pw_context_create_node(context, props, sizeof(struct object_data));
+
+ od = pw_impl_node_get_user_data(node);
+ od->object = node;
+ od->type = OBJECT_NODE;
+ pw_impl_node_add_listener(node, &od->listener, &node_object_events, od);
+ pw_impl_node_set_implementation(node, iface);
+ } else if (spa_streq(info->type, SPA_TYPE_INTERFACE_Device)) {
+ struct pw_impl_device *dev;
+ dev = pw_context_create_device(context, props, sizeof(struct object_data));
+
+ od = pw_impl_device_get_user_data(dev);
+ od->object = dev;
+ od->type = OBJECT_DEVICE;
+ pw_impl_device_add_listener(dev, &od->listener, &device_object_events, od);
+ pw_impl_device_set_implementation(dev, iface);
+ } else {
+ pw_log_warn("%p: unknown type %s", device, info->type);
+ pw_properties_free(props);
+ }
+
+ if (od) {
+ od->id = id;
+ od->handle = handle;
+ spa_list_append(&device->object_list, &od->link);
+ if (device->global)
+ object_register(od);
+ }
+ return;
+}
+
+static struct object_data *find_object(struct pw_impl_device *device, uint32_t id)
+{
+ struct object_data *od;
+ spa_list_for_each(od, &device->object_list, link) {
+ if (od->id == id)
+ return od;
+ }
+ return NULL;
+}
+
+static void device_object_info(void *data, uint32_t id,
+ const struct spa_device_object_info *info)
+{
+ struct pw_impl_device *device = data;
+ struct object_data *od;
+
+ od = find_object(device, id);
+
+ if (info == NULL) {
+ pw_log_debug("%p: remove node %d (%p)", device, id, od);
+ if (od)
+ object_destroy(od);
+ }
+ else if (od != NULL) {
+ if (info->change_mask & SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS)
+ object_update(od, info->props);
+ }
+ else {
+ device_add_object(device, id, info);
+ }
+}
+
+static const struct spa_device_events device_events = {
+ SPA_VERSION_DEVICE_EVENTS,
+ .info = device_info,
+ .object_info = device_object_info,
+};
+
+SPA_EXPORT
+int pw_impl_device_set_implementation(struct pw_impl_device *device, struct spa_device *spa_device)
+{
+ pw_log_debug("%p: implementation %p", device, spa_device);
+
+ if (device->device) {
+ pw_log_error("%p: implementation existed %p",
+ device, device->device);
+ return -EEXIST;
+ }
+ device->device = spa_device;
+ spa_device_add_listener(device->device,
+ &device->listener, &device_events, device);
+
+ return 0;
+}
+
+SPA_EXPORT
+struct spa_device *pw_impl_device_get_implementation(struct pw_impl_device *device)
+{
+ return device->device;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_impl_device_get_properties(struct pw_impl_device *device)
+{
+ return device->properties;
+}
+
+SPA_EXPORT
+int pw_impl_device_update_properties(struct pw_impl_device *device, const struct spa_dict *dict)
+{
+ int changed = update_properties(device, dict, false);
+ emit_info_changed(device);
+ return changed;
+}
+
+SPA_EXPORT
+void *pw_impl_device_get_user_data(struct pw_impl_device *device)
+{
+ return device->user_data;
+}
+
+SPA_EXPORT
+struct pw_global *pw_impl_device_get_global(struct pw_impl_device *device)
+{
+ return device->global;
+}
+
+SPA_EXPORT
+void pw_impl_device_add_listener(struct pw_impl_device *device,
+ struct spa_hook *listener,
+ const struct pw_impl_device_events *events,
+ void *data)
+{
+ spa_hook_list_append(&device->listener_list, listener, events, data);
+}
diff --git a/src/pipewire/impl-device.h b/src/pipewire/impl-device.h
new file mode 100644
index 0000000..016d6dc
--- /dev/null
+++ b/src/pipewire/impl-device.h
@@ -0,0 +1,116 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_DEVICE_H
+#define PIPEWIRE_IMPL_DEVICE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_impl_device Device Impl
+ *
+ * The device is an object that manages nodes. It typically
+ * corresponds to a physical hardware device but it does not
+ * have to be.
+ *
+ * The purpose of the device is to provide an interface to
+ * dynamically create/remove/configure the nodes it manages.
+ */
+
+/**
+ * \addtogroup pw_impl_device
+ * \{
+ */
+struct pw_impl_device;
+
+#include <spa/monitor/device.h>
+
+#include <pipewire/context.h>
+#include <pipewire/global.h>
+#include <pipewire/properties.h>
+#include <pipewire/resource.h>
+
+/** Device events, listen to them with \ref pw_impl_device_add_listener */
+struct pw_impl_device_events {
+#define PW_VERSION_IMPL_DEVICE_EVENTS 0
+ uint32_t version;
+
+ /** the device is destroyed */
+ void (*destroy) (void *data);
+ /** the device is freed */
+ void (*free) (void *data);
+ /** the device is initialized */
+ void (*initialized) (void *data);
+
+ /** the device info changed */
+ void (*info_changed) (void *data, const struct pw_device_info *info);
+};
+
+struct pw_impl_device *pw_context_create_device(struct pw_context *context,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+int pw_impl_device_register(struct pw_impl_device *device,
+ struct pw_properties *properties);
+
+void pw_impl_device_destroy(struct pw_impl_device *device);
+
+void *pw_impl_device_get_user_data(struct pw_impl_device *device);
+
+/** Set the device implementation */
+int pw_impl_device_set_implementation(struct pw_impl_device *device, struct spa_device *spa_device);
+/** Get the device implementation */
+struct spa_device *pw_impl_device_get_implementation(struct pw_impl_device *device);
+
+/** Get the global of this device */
+struct pw_global *pw_impl_device_get_global(struct pw_impl_device *device);
+
+/** Add an event listener */
+void pw_impl_device_add_listener(struct pw_impl_device *device,
+ struct spa_hook *listener,
+ const struct pw_impl_device_events *events,
+ void *data);
+
+int pw_impl_device_update_properties(struct pw_impl_device *device, const struct spa_dict *dict);
+
+const struct pw_properties *pw_impl_device_get_properties(struct pw_impl_device *device);
+
+int pw_impl_device_for_each_param(struct pw_impl_device *device,
+ int seq, uint32_t param_id,
+ uint32_t index, uint32_t max,
+ const struct spa_pod *filter,
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param),
+ void *data);
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_DEVICE_H */
diff --git a/src/pipewire/impl-factory.c b/src/pipewire/impl-factory.c
new file mode 100644
index 0000000..07572c3
--- /dev/null
+++ b/src/pipewire/impl-factory.c
@@ -0,0 +1,301 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/debug/types.h>
+#include <spa/utils/string.h>
+
+#include "pipewire/impl.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_factory);
+#define PW_LOG_TOPIC_DEFAULT log_factory
+
+#define pw_factory_resource_info(r,...) pw_resource_call(r,struct pw_factory_events,info,0,__VA_ARGS__)
+
+SPA_EXPORT
+struct pw_impl_factory *pw_context_create_factory(struct pw_context *context,
+ const char *name,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct pw_impl_factory *this;
+ int res;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ this = calloc(1, sizeof(*this) + user_data_size);
+ if (this == NULL) {
+ res = -errno;
+ goto error_exit;
+ };
+
+ this->context = context;
+ this->properties = properties;
+
+ this->info.name = strdup(name);
+ this->info.type = type;
+ this->info.version = version;
+ this->info.props = &properties->dict;
+ spa_hook_list_init(&this->listener_list);
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(this, sizeof(*this), void);
+
+ pw_log_debug("%p: new %s", this, name);
+
+ return this;
+
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+void pw_impl_factory_destroy(struct pw_impl_factory *factory)
+{
+ pw_log_debug("%p: destroy", factory);
+ pw_impl_factory_emit_destroy(factory);
+
+ if (factory->registered)
+ spa_list_remove(&factory->link);
+
+ if (factory->global) {
+ spa_hook_remove(&factory->global_listener);
+ pw_global_destroy(factory->global);
+ }
+
+ pw_impl_factory_emit_free(factory);
+ pw_log_debug("%p: free", factory);
+
+ spa_hook_list_clean(&factory->listener_list);
+
+ free((char *)factory->info.name);
+
+ pw_properties_free(factory->properties);
+
+ free(factory);
+}
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct pw_impl_factory *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, 0);
+ if (resource == NULL)
+ goto error_resource;
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_FACTORY_CHANGE_MASK_ALL;
+ pw_factory_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+error_resource:
+ pw_log_error("%p: can't create factory resource: %m", this);
+ return -errno;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_factory *factory = data;
+ spa_hook_remove(&factory->global_listener);
+ factory->global = NULL;
+ pw_impl_factory_destroy(factory);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+SPA_EXPORT
+const struct pw_properties *pw_impl_factory_get_properties(struct pw_impl_factory *factory)
+{
+ return factory->properties;
+}
+
+SPA_EXPORT
+int pw_impl_factory_update_properties(struct pw_impl_factory *factory, const struct spa_dict *dict)
+{
+ struct pw_resource *resource;
+ int changed;
+
+ changed = pw_properties_update(factory->properties, dict);
+ factory->info.props = &factory->properties->dict;
+
+ pw_log_debug("%p: updated %d properties", factory, changed);
+
+ if (!changed)
+ return 0;
+
+ factory->info.change_mask |= PW_FACTORY_CHANGE_MASK_PROPS;
+ if (factory->global)
+ spa_list_for_each(resource, &factory->global->resource_list, link)
+ pw_factory_resource_info(resource, &factory->info);
+ factory->info.change_mask = 0;
+
+ return changed;
+}
+
+SPA_EXPORT
+int pw_impl_factory_register(struct pw_impl_factory *factory,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_MODULE_ID,
+ PW_KEY_FACTORY_NAME,
+ PW_KEY_FACTORY_TYPE_NAME,
+ PW_KEY_FACTORY_TYPE_VERSION,
+ NULL
+ };
+
+ struct pw_context *context = factory->context;
+
+ if (factory->registered)
+ goto error_existed;
+
+ factory->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Factory,
+ PW_VERSION_FACTORY,
+ properties,
+ global_bind,
+ factory);
+ if (factory->global == NULL)
+ return -errno;
+
+ spa_list_append(&context->factory_list, &factory->link);
+ factory->registered = true;
+
+ factory->info.id = factory->global->id;
+ pw_properties_setf(factory->properties, PW_KEY_OBJECT_ID, "%d", factory->info.id);
+ pw_properties_setf(factory->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(factory->global));
+ pw_properties_set(factory->properties, PW_KEY_FACTORY_NAME, factory->info.name);
+ pw_properties_setf(factory->properties, PW_KEY_FACTORY_TYPE_NAME, "%s", factory->info.type);
+ pw_properties_setf(factory->properties, PW_KEY_FACTORY_TYPE_VERSION, "%d", factory->info.version);
+ factory->info.props = &factory->properties->dict;
+
+ pw_global_update_keys(factory->global, factory->info.props, keys);
+
+ pw_impl_factory_emit_initialized(factory);
+
+ pw_global_add_listener(factory->global, &factory->global_listener, &global_events, factory);
+ pw_global_register(factory->global);
+
+ return 0;
+
+error_existed:
+ pw_properties_free(properties);
+ return -EEXIST;
+}
+
+SPA_EXPORT
+void *pw_impl_factory_get_user_data(struct pw_impl_factory *factory)
+{
+ return factory->user_data;
+}
+
+SPA_EXPORT
+const struct pw_factory_info *pw_impl_factory_get_info(struct pw_impl_factory *factory)
+{
+ return &factory->info;
+}
+
+SPA_EXPORT
+struct pw_global *pw_impl_factory_get_global(struct pw_impl_factory *factory)
+{
+ return factory->global;
+}
+
+SPA_EXPORT
+void pw_impl_factory_add_listener(struct pw_impl_factory *factory,
+ struct spa_hook *listener,
+ const struct pw_impl_factory_events *events,
+ void *data)
+{
+ spa_hook_list_append(&factory->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+void pw_impl_factory_set_implementation(struct pw_impl_factory *factory,
+ const struct pw_impl_factory_implementation *implementation,
+ void *data)
+{
+ factory->impl = SPA_CALLBACKS_INIT(implementation, data);
+}
+
+SPA_EXPORT
+void *pw_impl_factory_create_object(struct pw_impl_factory *factory,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id)
+{
+ void *res = NULL;
+ spa_callbacks_call_res(&factory->impl,
+ struct pw_impl_factory_implementation,
+ res, create_object, 0,
+ resource, type, version, properties, new_id);
+ return res;
+}
+
+/** Find a factory by name
+ *
+ * \param context the context object
+ * \param name the name of the factory to find
+ *
+ * Find in the list of factories registered in \a context for one with
+ * the given \a name.
+ *
+ * \ingroup pw_context
+ */
+SPA_EXPORT
+struct pw_impl_factory *pw_context_find_factory(struct pw_context *context,
+ const char *name)
+{
+ struct pw_impl_factory *factory;
+
+ spa_list_for_each(factory, &context->factory_list, link) {
+ if (spa_streq(factory->info.name, name))
+ return factory;
+ }
+ return NULL;
+}
diff --git a/src/pipewire/impl-factory.h b/src/pipewire/impl-factory.h
new file mode 100644
index 0000000..f3cc546
--- /dev/null
+++ b/src/pipewire/impl-factory.h
@@ -0,0 +1,131 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_FACTORY_H
+#define PIPEWIRE_IMPL_FACTORY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_impl_factory Factory Impl
+ *
+ * The factory is used to make objects on demand.
+ */
+
+/**
+ * \addtogroup pw_impl_factory
+ * \{
+ */
+struct pw_impl_factory;
+
+#include <pipewire/context.h>
+#include <pipewire/impl-client.h>
+#include <pipewire/global.h>
+#include <pipewire/properties.h>
+#include <pipewire/resource.h>
+
+/** Factory events, listen to them with \ref pw_impl_factory_add_listener */
+struct pw_impl_factory_events {
+#define PW_VERSION_IMPL_FACTORY_EVENTS 0
+ uint32_t version;
+
+ /** the factory is destroyed */
+ void (*destroy) (void *data);
+ /** the factory is freed */
+ void (*free) (void *data);
+ /** the factory is initialized */
+ void (*initialized) (void *data);
+};
+
+struct pw_impl_factory_implementation {
+#define PW_VERSION_IMPL_FACTORY_IMPLEMENTATION 0
+ uint32_t version;
+
+ /** The function to create an object from this factory */
+ void *(*create_object) (void *data,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id);
+};
+
+struct pw_impl_factory *pw_context_create_factory(struct pw_context *context,
+ const char *name,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ size_t user_data_size);
+
+/** Get the factory properties */
+const struct pw_properties *pw_impl_factory_get_properties(struct pw_impl_factory *factory);
+
+/** Get the factory info */
+const struct pw_factory_info *pw_impl_factory_get_info(struct pw_impl_factory *factory);
+
+/** Update the factory properties */
+int pw_impl_factory_update_properties(struct pw_impl_factory *factory, const struct spa_dict *dict);
+
+int pw_impl_factory_register(struct pw_impl_factory *factory,
+ struct pw_properties *properties);
+
+void pw_impl_factory_destroy(struct pw_impl_factory *factory);
+
+void *pw_impl_factory_get_user_data(struct pw_impl_factory *factory);
+
+/** Get the global of this factory */
+struct pw_global *pw_impl_factory_get_global(struct pw_impl_factory *factory);
+
+/** Add an event listener */
+void pw_impl_factory_add_listener(struct pw_impl_factory *factory,
+ struct spa_hook *listener,
+ const struct pw_impl_factory_events *events,
+ void *data);
+
+void pw_impl_factory_set_implementation(struct pw_impl_factory *factory,
+ const struct pw_impl_factory_implementation *implementation,
+ void *data);
+
+void *pw_impl_factory_create_object(struct pw_impl_factory *factory,
+ struct pw_resource *resource,
+ const char *type,
+ uint32_t version,
+ struct pw_properties *properties,
+ uint32_t new_id);
+
+/** Find a factory by name */
+struct pw_impl_factory *
+pw_context_find_factory(struct pw_context *context /**< the context */,
+ const char *name /**< the factory name */);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_FACTORY_H */
diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c
new file mode 100644
index 0000000..f773728
--- /dev/null
+++ b/src/pipewire/impl-link.c
@@ -0,0 +1,1508 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <time.h>
+
+#include <spa/node/utils.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/compare.h>
+#include <spa/param/param.h>
+#include <spa/debug/types.h>
+
+#include "pipewire/impl-link.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_link);
+#define PW_LOG_TOPIC_DEFAULT log_link
+
+#define MAX_HOPS 32
+
+#define pw_link_resource_info(r,...) pw_resource_call(r,struct pw_link_events,info,0,__VA_ARGS__)
+
+/** \cond */
+struct impl {
+ struct pw_impl_link this;
+
+ unsigned int io_set:1;
+ unsigned int activated:1;
+
+ struct pw_work_queue *work;
+
+ uint32_t output_busy_id;
+ uint32_t input_busy_id;
+
+ struct spa_pod *format_filter;
+ struct pw_properties *properties;
+
+ struct spa_hook input_port_listener;
+ struct spa_hook input_node_listener;
+ struct spa_hook input_global_listener;
+ struct spa_hook output_port_listener;
+ struct spa_hook output_node_listener;
+ struct spa_hook output_global_listener;
+
+ struct spa_io_buffers io;
+
+ struct pw_impl_node *inode, *onode;
+};
+
+/** \endcond */
+
+static void info_changed(struct pw_impl_link *link)
+{
+ struct pw_resource *resource;
+
+ if (link->info.change_mask == 0)
+ return;
+
+ pw_impl_link_emit_info_changed(link, &link->info);
+
+ if (link->global)
+ spa_list_for_each(resource, &link->global->resource_list, link)
+ pw_link_resource_info(resource, &link->info);
+
+ link->info.change_mask = 0;
+}
+
+static void link_update_state(struct pw_impl_link *link, enum pw_link_state state, int res, char *error)
+{
+ struct impl *impl = SPA_CONTAINER_OF(link, struct impl, this);
+ enum pw_link_state old = link->info.state;
+
+ link->info.state = state;
+ free((char*)link->info.error);
+ link->info.error = error;
+
+ if (state == old)
+ return;
+
+ pw_log_debug("%p: %s -> %s (%s)", link,
+ pw_link_state_as_string(old),
+ pw_link_state_as_string(state), error);
+
+ if (state == PW_LINK_STATE_ERROR) {
+ pw_log_error("(%s) %s -> error (%s)", link->name,
+ pw_link_state_as_string(old), error);
+ } else {
+ pw_log_info("(%s) %s -> %s", link->name,
+ pw_link_state_as_string(old),
+ pw_link_state_as_string(state));
+ }
+
+ pw_impl_link_emit_state_changed(link, old, state, error);
+
+ link->info.change_mask |= PW_LINK_CHANGE_MASK_STATE;
+ if (state == PW_LINK_STATE_ERROR ||
+ state == PW_LINK_STATE_PAUSED ||
+ state == PW_LINK_STATE_ACTIVE)
+ info_changed(link);
+
+ if (state == PW_LINK_STATE_ERROR && link->global) {
+ struct pw_resource *resource;
+ spa_list_for_each(resource, &link->global->resource_list, link)
+ pw_resource_error(resource, res, error);
+ }
+
+ if (old < PW_LINK_STATE_PAUSED && state == PW_LINK_STATE_PAUSED) {
+ link->prepared = true;
+ link->preparing = false;
+ pw_context_recalc_graph(link->context, "link prepared");
+ } else if (old == PW_LINK_STATE_PAUSED && state < PW_LINK_STATE_PAUSED) {
+ link->prepared = false;
+ link->preparing = false;
+ pw_context_recalc_graph(link->context, "link unprepared");
+ } else if (state == PW_LINK_STATE_INIT) {
+ link->prepared = false;
+ link->preparing = false;
+ if (impl->output_busy_id != SPA_ID_INVALID) {
+ impl->output_busy_id = SPA_ID_INVALID;
+ link->output->busy_count--;
+ }
+ pw_work_queue_cancel(impl->work, &link->output_link, SPA_ID_INVALID);
+ if (impl->input_busy_id != SPA_ID_INVALID) {
+ impl->input_busy_id = SPA_ID_INVALID;
+ link->input->busy_count--;
+ }
+ pw_work_queue_cancel(impl->work, &link->input_link, SPA_ID_INVALID);
+ }
+}
+
+static void complete_ready(void *obj, void *data, int res, uint32_t id)
+{
+ struct pw_impl_port *port;
+ struct pw_impl_link *this = data;
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+
+ if (obj == &this->input_link)
+ port = this->input;
+ else
+ port = this->output;
+
+ if (id == impl->input_busy_id) {
+ impl->input_busy_id = SPA_ID_INVALID;
+ port->busy_count--;
+ } else if (id == impl->output_busy_id) {
+ impl->output_busy_id = SPA_ID_INVALID;
+ port->busy_count--;
+ } else if (id != SPA_ID_INVALID)
+ return;
+
+ pw_log_debug("%p: obj:%p port %p complete state:%d: %s", this, obj, port,
+ port->state, spa_strerror(res));
+
+ if (SPA_RESULT_IS_OK(res)) {
+ if (port->state < PW_IMPL_PORT_STATE_READY)
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_READY,
+ 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)));
+ }
+ 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)
+{
+ struct pw_impl_port *port;
+ struct pw_impl_link *this = data;
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ struct pw_impl_port_mix *mix;
+
+ if (obj == &this->input_link) {
+ port = this->input;
+ mix = &this->rt.in_mix;
+ } else {
+ port = this->output;
+ mix = &this->rt.out_mix;
+ }
+
+ if (id == impl->input_busy_id) {
+ impl->input_busy_id = SPA_ID_INVALID;
+ port->busy_count--;
+ } else if (id == impl->output_busy_id) {
+ impl->output_busy_id = SPA_ID_INVALID;
+ port->busy_count--;
+ } else if (id != SPA_ID_INVALID)
+ return;
+
+ pw_log_debug("%p: obj:%p port %p complete state:%d: %s", this, obj, port,
+ port->state, spa_strerror(res));
+
+ if (SPA_RESULT_IS_OK(res)) {
+ if (port->state < PW_IMPL_PORT_STATE_PAUSED)
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_PAUSED,
+ 0, NULL);
+ mix->have_buffers = true;
+ } 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;
+ }
+ 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)
+{
+ struct pw_impl_port *port;
+ struct pw_impl_link *this = data;
+
+ if (obj == &this->input_link)
+ port = this->input;
+ else
+ port = this->output;
+
+ pw_log_debug("%p: obj:%p port %p complete state:%d: %s", this, obj, port,
+ port->state, spa_strerror(res));
+}
+
+static int do_negotiate(struct pw_impl_link *this)
+{
+ struct pw_context *context = this->context;
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ int res = -EIO, res2;
+ struct spa_pod *format = NULL, *current;
+ char *error = NULL;
+ bool changed = true;
+ struct pw_impl_port *input, *output;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ uint32_t index;
+ uint32_t in_state, out_state;
+
+ if (this->info.state >= PW_LINK_STATE_NEGOTIATING)
+ return 0;
+
+ input = this->input;
+ output = this->output;
+
+ in_state = input->state;
+ out_state = output->state;
+
+ pw_log_debug("%p: in_state:%d out_state:%d", this, in_state, out_state);
+
+ if (in_state != PW_IMPL_PORT_STATE_CONFIGURE && out_state != PW_IMPL_PORT_STATE_CONFIGURE)
+ return 0;
+
+ link_update_state(this, PW_LINK_STATE_NEGOTIATING, 0, NULL);
+
+ input = this->input;
+ output = this->output;
+
+ /* find a common format for the ports */
+ if ((res = pw_context_find_format(context,
+ output, input, NULL, 0, NULL,
+ &format, &b, &error)) < 0) {
+ format = NULL;
+ goto error;
+ }
+
+ format = spa_pod_copy(format);
+ spa_pod_fixate(format);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ /* if output port had format and is idle, check if it changed. If so, renegotiate */
+ if (out_state > PW_IMPL_PORT_STATE_CONFIGURE && output->node->info.state == PW_NODE_STATE_IDLE) {
+ index = 0;
+ res = spa_node_port_enum_params_sync(output->node->node,
+ output->direction, output->port_id,
+ SPA_PARAM_Format, &index,
+ NULL, &current, &b);
+ switch (res) {
+ case -EIO:
+ current = NULL;
+ res = 0;
+ SPA_FALLTHROUGH
+ case 1:
+ break;
+ case 0:
+ res = -EBADF;
+ SPA_FALLTHROUGH
+ default:
+ error = spa_aprintf("error get output format: %s", spa_strerror(res));
+ goto error;
+ }
+ if (current == NULL || spa_pod_compare(current, format) != 0) {
+ pw_log_debug("%p: output format change, renegotiate", this);
+ if (current)
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, current);
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, format);
+ pw_impl_node_set_state(output->node, PW_NODE_STATE_SUSPENDED);
+ out_state = PW_IMPL_PORT_STATE_CONFIGURE;
+ }
+ else {
+ pw_log_debug("%p: format was already set", this);
+ changed = false;
+ }
+ }
+ /* if input port had format and is idle, check if it changed. If so, renegotiate */
+ if (in_state > PW_IMPL_PORT_STATE_CONFIGURE && input->node->info.state == PW_NODE_STATE_IDLE) {
+ index = 0;
+ res = spa_node_port_enum_params_sync(input->node->node,
+ input->direction, input->port_id,
+ SPA_PARAM_Format, &index,
+ NULL, &current, &b);
+ switch (res) {
+ case -EIO:
+ current = NULL;
+ res = 0;
+ SPA_FALLTHROUGH
+ case 1:
+ break;
+ case 0:
+ res = -EBADF;
+ SPA_FALLTHROUGH
+ default:
+ error = spa_aprintf("error get input format: %s", spa_strerror(res));
+ goto error;
+ }
+ if (current == NULL || spa_pod_compare(current, format) != 0) {
+ pw_log_debug("%p: input format change, renegotiate", this);
+ if (current)
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, current);
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, format);
+ pw_impl_node_set_state(input->node, PW_NODE_STATE_SUSPENDED);
+ in_state = PW_IMPL_PORT_STATE_CONFIGURE;
+ }
+ else {
+ pw_log_debug("%p: format was already set", this);
+ changed = false;
+ }
+ }
+
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, format);
+
+ SPA_POD_OBJECT_ID(format) = SPA_PARAM_Format;
+ pw_log_debug("%p: doing set format %p fixated:%d", this,
+ format, spa_pod_is_fixated(format));
+
+ if (out_state == PW_IMPL_PORT_STATE_CONFIGURE) {
+ pw_log_debug("%p: doing set format on output", this);
+ if ((res = pw_impl_port_set_param(output,
+ SPA_PARAM_Format, 0,
+ format)) < 0) {
+ error = spa_aprintf("error set output format: %d (%s)", res, spa_strerror(res));
+ pw_log_error("tried to set output format:");
+ pw_log_pod(SPA_LOG_LEVEL_ERROR, format);
+ goto error;
+ }
+ if (SPA_RESULT_IS_ASYNC(res)) {
+ output->busy_count++;
+ res = spa_node_sync(output->node->node, res);
+ impl->output_busy_id = pw_work_queue_add(impl->work, &this->output_link, res,
+ complete_ready, this);
+ } else {
+ complete_ready(&this->output_link, this, res, SPA_ID_INVALID);
+ }
+ }
+ if (in_state == PW_IMPL_PORT_STATE_CONFIGURE) {
+ pw_log_debug("%p: doing set format on input", this);
+ if ((res2 = pw_impl_port_set_param(input,
+ SPA_PARAM_Format, 0,
+ format)) < 0) {
+ error = spa_aprintf("error set input format: %d (%s)", res2, spa_strerror(res2));
+ pw_log_error("tried to set input format:");
+ pw_log_pod(SPA_LOG_LEVEL_ERROR, format);
+ goto error;
+ }
+ if (SPA_RESULT_IS_ASYNC(res2)) {
+ input->busy_count++;
+ res2 = spa_node_sync(input->node->node, res2);
+ impl->input_busy_id = pw_work_queue_add(impl->work, &this->input_link, res2,
+ complete_ready, this);
+ if (res == 0)
+ res = res2;
+ } else {
+ complete_ready(&this->input_link, this, res2, SPA_ID_INVALID);
+ }
+ }
+
+ free(this->info.format);
+ this->info.format = format;
+
+ if (changed)
+ this->info.change_mask |= PW_LINK_CHANGE_MASK_FORMAT;
+
+ pw_log_debug("%p: result %d", this, res);
+ return res;
+
+error:
+ pw_context_debug_port_params(context, input->node->node, input->direction,
+ input->port_id, SPA_PARAM_EnumFormat, res,
+ "input format (%s)", error);
+ pw_context_debug_port_params(context, output->node->node, output->direction,
+ output->port_id, SPA_PARAM_EnumFormat, res,
+ "output format (%s)", error);
+ link_update_state(this, PW_LINK_STATE_ERROR, res, error);
+ free(format);
+ return res;
+}
+
+static int port_set_io(struct pw_impl_link *this, struct pw_impl_port *port, uint32_t id,
+ void *data, size_t size, struct pw_impl_port_mix *mix)
+{
+ int res = 0;
+
+ mix->io = data;
+ pw_log_debug("%p: %s port %p %d.%d set io: %d %p %zd", this,
+ pw_direction_as_string(port->direction),
+ port, port->port_id, mix->port.port_id, id, data, size);
+
+ if ((res = spa_node_port_set_io(port->mix,
+ mix->port.direction,
+ mix->port.port_id,
+ id, data, size)) < 0) {
+ if (res == -ENOTSUP)
+ res = 0;
+ else
+ pw_log_warn("%p: port %p can't set io:%d (%s): %s",
+ this, port, id,
+ spa_debug_type_find_name(spa_type_io, id),
+ spa_strerror(res));
+ }
+ return res;
+}
+
+static void select_io(struct pw_impl_link *this)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ struct spa_io_buffers *io;
+
+ io = this->rt.in_mix.io;
+ if (io == NULL)
+ io = this->rt.out_mix.io;
+ if (io == NULL)
+ io = &impl->io;
+
+ this->io = io;
+ *this->io = SPA_IO_BUFFERS_INIT;
+}
+
+static int do_allocation(struct pw_impl_link *this)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ int res;
+ uint32_t in_flags, out_flags;
+ char *error = NULL;
+ struct pw_impl_port *input, *output;
+
+ if (this->info.state > PW_LINK_STATE_ALLOCATING)
+ return 0;
+
+ output = this->output;
+ input = this->input;
+
+ pw_log_debug("%p: out-state:%d in-state:%d", this, output->state, input->state);
+
+ if (input->state < PW_IMPL_PORT_STATE_READY || output->state < PW_IMPL_PORT_STATE_READY)
+ return 0;
+
+ link_update_state(this, PW_LINK_STATE_ALLOCATING, 0, NULL);
+
+ out_flags = output->spa_flags;
+ in_flags = input->spa_flags;
+
+ pw_log_debug("%p: out-node:%p in-node:%p: out-flags:%08x in-flags:%08x",
+ this, output->node, input->node, out_flags, in_flags);
+
+ this->rt.in_mix.have_buffers = false;
+ this->rt.out_mix.have_buffers = false;
+
+ if (out_flags & SPA_PORT_FLAG_LIVE) {
+ pw_log_debug("%p: setting link as live", this);
+ output->node->live = true;
+ input->node->live = true;
+ }
+
+ if (output->buffers.n_buffers) {
+ pw_log_debug("%p: reusing %d output buffers %p", this,
+ output->buffers.n_buffers, output->buffers.buffers);
+ this->rt.out_mix.have_buffers = true;
+ } else {
+ uint32_t flags, alloc_flags;
+
+ flags = 0;
+ /* always shared buffers for the link */
+ alloc_flags = PW_BUFFERS_FLAG_SHARED;
+ if (output->node->remote || input->node->remote)
+ alloc_flags |= PW_BUFFERS_FLAG_SHARED_MEM;
+
+ if (output->node->driver)
+ alloc_flags |= PW_BUFFERS_FLAG_IN_PRIORITY;
+
+ /* if output port can alloc buffers, alloc skeleton buffers */
+ if (SPA_FLAG_IS_SET(out_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS)) {
+ SPA_FLAG_SET(alloc_flags, PW_BUFFERS_FLAG_NO_MEM);
+ flags |= SPA_NODE_BUFFERS_FLAG_ALLOC;
+ }
+
+ if ((res = pw_buffers_negotiate(this->context, alloc_flags,
+ output->node->node, output->port_id,
+ input->node->node, input->port_id,
+ &output->buffers)) < 0) {
+ error = spa_aprintf("error alloc buffers: %s", spa_strerror(res));
+ goto error;
+ }
+
+ pw_log_debug("%p: allocating %d buffers %p", this,
+ output->buffers.n_buffers, output->buffers.buffers);
+
+ if ((res = pw_impl_port_use_buffers(output, &this->rt.out_mix, flags,
+ output->buffers.buffers,
+ output->buffers.n_buffers)) < 0) {
+ error = spa_aprintf("error use output buffers: %d (%s)", res,
+ spa_strerror(res));
+ goto error_clear;
+ }
+ if (SPA_RESULT_IS_ASYNC(res)) {
+ output->busy_count++;
+ res = spa_node_sync(output->node->node, res);
+ impl->output_busy_id = pw_work_queue_add(impl->work, &this->output_link, res,
+ complete_paused, this);
+ if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC)
+ return 0;
+ } else {
+ complete_paused(&this->output_link, this, res, SPA_ID_INVALID);
+ }
+ }
+
+ pw_log_debug("%p: using %d buffers %p on input port", this,
+ output->buffers.n_buffers, output->buffers.buffers);
+
+ if ((res = pw_impl_port_use_buffers(input, &this->rt.in_mix, 0,
+ output->buffers.buffers,
+ output->buffers.n_buffers)) < 0) {
+ error = spa_aprintf("error use input buffers: %d (%s)", res,
+ spa_strerror(res));
+ goto error;
+ }
+
+ if (SPA_RESULT_IS_ASYNC(res)) {
+ input->busy_count++;
+ res = spa_node_sync(input->node->node, res);
+ impl->input_busy_id = pw_work_queue_add(impl->work, &this->input_link, res,
+ complete_paused, this);
+ } else {
+ complete_paused(&this->input_link, this, res, SPA_ID_INVALID);
+ }
+ return 0;
+
+error_clear:
+ pw_buffers_clear(&output->buffers);
+error:
+ link_update_state(this, PW_LINK_STATE_ERROR, res, error);
+ return res;
+}
+
+static int
+do_activate_link(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct pw_impl_link *this = user_data;
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+
+ pw_log_trace("%p: activate", this);
+
+ spa_list_append(&this->output->rt.mix_list, &this->rt.out_mix.rt_link);
+ spa_list_append(&this->input->rt.mix_list, &this->rt.in_mix.rt_link);
+
+ if (impl->inode != impl->onode) {
+ struct pw_node_activation_state *state;
+
+ this->rt.target.activation = impl->inode->rt.activation;
+ spa_list_append(&impl->onode->rt.target_list, &this->rt.target.link);
+
+ state = &this->rt.target.activation->state[0];
+ if (!this->rt.target.active && impl->onode->rt.driver_target.node != NULL) {
+ state->required++;
+ this->rt.target.active = true;
+ }
+
+ pw_log_trace("%p: node:%p state:%p pending:%d/%d", this, impl->inode,
+ state, state->pending, state->required);
+ }
+ return 0;
+}
+
+int pw_impl_link_activate(struct pw_impl_link *this)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ int res;
+
+ pw_log_debug("%p: activate activated:%d state:%s", this, impl->activated,
+ pw_link_state_as_string(this->info.state));
+
+ if (impl->activated || !this->prepared ||
+ !impl->inode->active || !impl->onode->active)
+ return 0;
+
+ if (!impl->io_set) {
+ if ((res = port_set_io(this, this->output, SPA_IO_Buffers, this->io,
+ sizeof(struct spa_io_buffers), &this->rt.out_mix)) < 0)
+ return res;
+
+ if ((res = port_set_io(this, this->input, SPA_IO_Buffers, this->io,
+ sizeof(struct spa_io_buffers), &this->rt.in_mix)) < 0)
+ return res;
+ impl->io_set = true;
+ }
+ pw_loop_invoke(this->output->node->data_loop,
+ do_activate_link, SPA_ID_INVALID, NULL, 0, false, this);
+
+ impl->activated = true;
+ pw_log_info("(%s) activated", this->name);
+ link_update_state(this, PW_LINK_STATE_ACTIVE, 0, NULL);
+
+ return 0;
+}
+static void check_states(void *obj, void *user_data, int res, uint32_t id)
+{
+ struct pw_impl_link *this = obj;
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ int in_state, out_state;
+ struct pw_impl_port *input, *output;
+
+ if (this->info.state == PW_LINK_STATE_ERROR)
+ return;
+
+ if (this->info.state >= PW_LINK_STATE_PAUSED)
+ return;
+
+ output = this->output;
+ input = this->input;
+
+ if (output == NULL || input == NULL) {
+ link_update_state(this, PW_LINK_STATE_ERROR, -EIO,
+ strdup("link without input or output port"));
+ return;
+ }
+
+ if (output->node->info.state == PW_NODE_STATE_ERROR ||
+ input->node->info.state == PW_NODE_STATE_ERROR) {
+ pw_log_warn("%p: one of the nodes is in error out:%s in:%s", this,
+ pw_node_state_as_string(output->node->info.state),
+ pw_node_state_as_string(input->node->info.state));
+ return;
+ }
+
+ out_state = output->state;
+ in_state = input->state;
+
+ pw_log_debug("%p: output state %d, input state %d", this, out_state, in_state);
+
+ if (out_state == PW_IMPL_PORT_STATE_ERROR || in_state == PW_IMPL_PORT_STATE_ERROR) {
+ link_update_state(this, PW_LINK_STATE_ERROR, -EIO, strdup("ports are in error"));
+ return;
+ }
+
+ if (PW_IMPL_PORT_IS_CONTROL(output) && PW_IMPL_PORT_IS_CONTROL(input)) {
+ pw_impl_port_update_state(output, PW_IMPL_PORT_STATE_PAUSED, 0, NULL);
+ pw_impl_port_update_state(input, PW_IMPL_PORT_STATE_PAUSED, 0, NULL);
+ link_update_state(this, PW_LINK_STATE_PAUSED, 0, NULL);
+ }
+
+ if (output->busy_count > 0) {
+ pw_log_debug("%p: output port %p was busy", this, output);
+ res = spa_node_sync(output->node->node, 0);
+ pw_work_queue_add(impl->work, &this->output_link, res, complete_sync, this);
+ goto exit;
+ }
+ else if (input->busy_count > 0) {
+ pw_log_debug("%p: input port %p was busy", this, input);
+ res = spa_node_sync(input->node->node, 0);
+ pw_work_queue_add(impl->work, &this->input_link, res, complete_sync, this);
+ goto exit;
+ }
+
+ if ((res = do_negotiate(this)) != 0)
+ goto exit;
+
+ if ((res = do_allocation(this)) != 0)
+ goto exit;
+
+exit:
+ if (SPA_RESULT_IS_ERROR(res)) {
+ pw_log_debug("%p: got error result %d (%s)", this, res, spa_strerror(res));
+ return;
+ }
+
+ pw_work_queue_add(impl->work,
+ this, -EBUSY, (pw_work_func_t) check_states, this);
+}
+
+static void input_remove(struct pw_impl_link *this, struct pw_impl_port *port)
+{
+ struct impl *impl = (struct impl *) this;
+ struct pw_impl_port_mix *mix = &this->rt.in_mix;
+ int res;
+
+ pw_log_debug("%p: remove input port %p", this, port);
+
+ if (impl->input_busy_id != SPA_ID_INVALID) {
+ impl->input_busy_id = SPA_ID_INVALID;
+ port->busy_count--;
+ }
+ spa_hook_remove(&impl->input_port_listener);
+ spa_hook_remove(&impl->input_node_listener);
+ spa_hook_remove(&impl->input_global_listener);
+
+ spa_list_remove(&this->input_link);
+ pw_impl_port_emit_link_removed(this->input, this);
+
+ pw_impl_port_recalc_latency(this->input);
+
+ if ((res = pw_impl_port_use_buffers(port, mix, 0, NULL, 0)) < 0) {
+ pw_log_warn("%p: port %p clear error %s", this, port, spa_strerror(res));
+ }
+ pw_impl_port_release_mix(port, mix);
+
+ pw_work_queue_cancel(impl->work, &this->input_link, SPA_ID_INVALID);
+ this->input = NULL;
+}
+
+static void output_remove(struct pw_impl_link *this, struct pw_impl_port *port)
+{
+ struct impl *impl = (struct impl *) this;
+ struct pw_impl_port_mix *mix = &this->rt.out_mix;
+
+ pw_log_debug("%p: remove output port %p", this, port);
+
+ if (impl->output_busy_id != SPA_ID_INVALID) {
+ impl->output_busy_id = SPA_ID_INVALID;
+ port->busy_count--;
+ }
+ spa_hook_remove(&impl->output_port_listener);
+ spa_hook_remove(&impl->output_node_listener);
+ spa_hook_remove(&impl->output_global_listener);
+
+ spa_list_remove(&this->output_link);
+ pw_impl_port_emit_link_removed(this->output, this);
+
+ pw_impl_port_recalc_latency(this->output);
+
+ /* we don't clear output buffers when the link goes away. They will get
+ * cleared when the node goes to suspend */
+ pw_impl_port_release_mix(port, mix);
+
+ pw_work_queue_cancel(impl->work, &this->output_link, SPA_ID_INVALID);
+ this->output = NULL;
+}
+
+int pw_impl_link_prepare(struct pw_impl_link *this)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+
+ pw_log_debug("%p: prepared:%d preparing:%d in_active:%d out_active:%d",
+ this, this->prepared, this->preparing,
+ impl->inode->active, impl->onode->active);
+
+ if (!impl->inode->active || !impl->onode->active)
+ return 0;
+
+ if (this->preparing || this->prepared)
+ return 0;
+
+ this->preparing = true;
+
+ pw_work_queue_add(impl->work,
+ this, -EBUSY, (pw_work_func_t) check_states, this);
+
+ return 0;
+}
+
+static int
+do_deactivate_link(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct pw_impl_link *this = user_data;
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+
+ pw_log_trace("%p: disable %p and %p", this, &this->rt.in_mix, &this->rt.out_mix);
+
+ spa_list_remove(&this->rt.out_mix.rt_link);
+ spa_list_remove(&this->rt.in_mix.rt_link);
+
+ if (impl->inode != impl->onode) {
+ struct pw_node_activation_state *state;
+
+ spa_list_remove(&this->rt.target.link);
+ state = &this->rt.target.activation->state[0];
+ if (this->rt.target.active) {
+ state->required--;
+ this->rt.target.active = false;
+ }
+
+ pw_log_trace("%p: node:%p state:%p pending:%d/%d", this, impl->inode,
+ state, state->pending, state->required);
+ }
+
+ return 0;
+}
+
+int pw_impl_link_deactivate(struct pw_impl_link *this)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+
+ pw_log_debug("%p: deactivate activated:%d", this, impl->activated);
+
+ if (!impl->activated)
+ return 0;
+
+ pw_loop_invoke(this->output->node->data_loop,
+ do_deactivate_link, SPA_ID_INVALID, NULL, 0, true, this);
+
+ port_set_io(this, this->output, SPA_IO_Buffers, NULL, 0,
+ &this->rt.out_mix);
+ port_set_io(this, this->input, SPA_IO_Buffers, NULL, 0,
+ &this->rt.in_mix);
+
+ impl->io_set = false;
+ impl->activated = false;
+ pw_log_info("(%s) deactivated", this->name);
+ link_update_state(this, PW_LINK_STATE_PAUSED, 0, NULL);
+
+ return 0;
+}
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct pw_impl_link *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, 0);
+ if (resource == NULL)
+ goto error_resource;
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_LINK_CHANGE_MASK_ALL;
+ pw_link_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+error_resource:
+ pw_log_error("%p: can't create link resource: %m", this);
+ return -errno;
+}
+
+static void port_state_changed(struct pw_impl_link *this, struct pw_impl_port *port,
+ struct pw_impl_port *other, enum pw_impl_port_state old,
+ enum pw_impl_port_state state, const char *error)
+{
+ pw_log_debug("%p: port %p old:%d -> state:%d prepared:%d preparing:%d",
+ this, port, old, state, this->prepared, this->preparing);
+
+ switch (state) {
+ case PW_IMPL_PORT_STATE_ERROR:
+ link_update_state(this, PW_LINK_STATE_ERROR, -EIO, error ? strdup(error) : NULL);
+ break;
+ case PW_IMPL_PORT_STATE_INIT:
+ case PW_IMPL_PORT_STATE_CONFIGURE:
+ if (this->prepared || state < old) {
+ this->prepared = false;
+ link_update_state(this, PW_LINK_STATE_INIT, 0, NULL);
+ }
+ break;
+ case PW_IMPL_PORT_STATE_READY:
+ if (this->prepared || state < old) {
+ this->prepared = false;
+ link_update_state(this, PW_LINK_STATE_NEGOTIATING, 0, NULL);
+ }
+ break;
+ case PW_IMPL_PORT_STATE_PAUSED:
+ break;
+ }
+}
+
+static void port_param_changed(struct pw_impl_link *this, uint32_t id,
+ struct pw_impl_port *outport, struct pw_impl_port *inport)
+{
+ enum pw_impl_port_state target;
+
+ pw_log_debug("%p: outport %p input %p param %d (%s)", this,
+ outport, inport, id, spa_debug_type_find_name(spa_type_param, id));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ target = PW_IMPL_PORT_STATE_CONFIGURE;
+ break;
+ default:
+ return;
+ }
+ if (outport)
+ pw_impl_port_update_state(outport, target, 0, NULL);
+ if (inport)
+ pw_impl_port_update_state(inport, target, 0, NULL);
+
+ this->preparing = this->prepared = false;
+ link_update_state(this, PW_LINK_STATE_INIT, 0, NULL);
+ pw_impl_link_prepare(this);
+}
+
+static void input_port_param_changed(void *data, uint32_t id)
+{
+ struct impl *impl = data;
+ struct pw_impl_link *this = &impl->this;
+ port_param_changed(this, id, this->output, this->input);
+}
+
+static void input_port_state_changed(void *data, enum pw_impl_port_state old,
+ enum pw_impl_port_state state, const char *error)
+{
+ struct impl *impl = data;
+ struct pw_impl_link *this = &impl->this;
+ port_state_changed(this, this->input, this->output, old, state, error);
+}
+
+static void output_port_param_changed(void *data, uint32_t id)
+{
+ struct impl *impl = data;
+ struct pw_impl_link *this = &impl->this;
+ port_param_changed(this, id, this->output, this->input);
+}
+
+static void output_port_state_changed(void *data, enum pw_impl_port_state old,
+ enum pw_impl_port_state state, const char *error)
+{
+ struct impl *impl = data;
+ struct pw_impl_link *this = &impl->this;
+ port_state_changed(this, this->output, this->input, old, state, error);
+}
+
+static void input_port_latency_changed(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_link *this = &impl->this;
+ if (!this->feedback)
+ pw_impl_port_recalc_latency(this->output);
+}
+
+static void output_port_latency_changed(void *data)
+{
+ struct impl *impl = data;
+ struct pw_impl_link *this = &impl->this;
+ if (!this->feedback)
+ pw_impl_port_recalc_latency(this->input);
+}
+
+static const struct pw_impl_port_events input_port_events = {
+ PW_VERSION_IMPL_PORT_EVENTS,
+ .param_changed = input_port_param_changed,
+ .state_changed = input_port_state_changed,
+ .latency_changed = input_port_latency_changed,
+};
+
+static const struct pw_impl_port_events output_port_events = {
+ PW_VERSION_IMPL_PORT_EVENTS,
+ .param_changed = output_port_param_changed,
+ .state_changed = output_port_state_changed,
+ .latency_changed = output_port_latency_changed,
+};
+
+static void node_result(struct impl *impl, void *obj,
+ int seq, int res, uint32_t type, const void *result)
+{
+ if (SPA_RESULT_IS_ASYNC(seq))
+ pw_work_queue_complete(impl->work, obj, SPA_RESULT_ASYNC_SEQ(seq), res);
+}
+
+static void input_node_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ 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);
+ node_result(impl, &impl->this.input_link, seq, res, type, result);
+}
+
+static void output_node_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ 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);
+ node_result(impl, &impl->this.output_link, seq, res, type, result);
+}
+
+static void node_active_changed(void *data, bool active)
+{
+ struct impl *impl = data;
+ pw_impl_link_prepare(&impl->this);
+}
+
+static const struct pw_impl_node_events input_node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .result = input_node_result,
+ .active_changed = node_active_changed,
+};
+
+static const struct pw_impl_node_events output_node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .result = output_node_result,
+ .active_changed = node_active_changed,
+};
+
+static bool pw_impl_node_can_reach(struct pw_impl_node *output, struct pw_impl_node *input, int hop)
+{
+ struct pw_impl_port *p;
+ struct pw_impl_link *l;
+
+ output->loopchecked = true;
+
+ if (output == input)
+ return true;
+
+ if (hop == MAX_HOPS) {
+ pw_log_warn("exceeded hops (%d) %s -> %s", hop, output->name, input->name);
+ return false;
+ }
+
+ spa_list_for_each(p, &output->output_ports, link) {
+ spa_list_for_each(l, &p->links, output_link)
+ l->input->node->loopchecked = l->feedback;
+ }
+ spa_list_for_each(p, &output->output_ports, link) {
+ spa_list_for_each(l, &p->links, output_link) {
+ if (l->input->node->loopchecked)
+ continue;
+ if (pw_impl_node_can_reach(l->input->node, input, hop+1))
+ return true;
+ }
+ }
+ return false;
+}
+
+static void try_link_controls(struct impl *impl, struct pw_impl_port *output, struct pw_impl_port *input)
+{
+ struct pw_control *cin, *cout;
+ struct pw_impl_link *this = &impl->this;
+ uint32_t omix, imix;
+ int res;
+
+ imix = this->rt.in_mix.port.port_id;
+ omix = this->rt.out_mix.port.port_id;
+
+ pw_log_debug("%p: trying controls", impl);
+ spa_list_for_each(cout, &output->control_list[SPA_DIRECTION_OUTPUT], port_link) {
+ spa_list_for_each(cin, &input->control_list[SPA_DIRECTION_INPUT], port_link) {
+ if ((res = pw_control_add_link(cout, omix, cin, imix, &this->control)) < 0)
+ pw_log_error("%p: failed to link controls: %s",
+ this, spa_strerror(res));
+ break;
+ }
+ }
+ spa_list_for_each(cin, &output->control_list[SPA_DIRECTION_INPUT], port_link) {
+ spa_list_for_each(cout, &input->control_list[SPA_DIRECTION_OUTPUT], port_link) {
+ if ((res = pw_control_add_link(cout, imix, cin, omix, &this->notify)) < 0)
+ pw_log_error("%p: failed to link controls: %s",
+ this, spa_strerror(res));
+ break;
+ }
+ }
+}
+
+static void try_unlink_controls(struct impl *impl, struct pw_impl_port *output, struct pw_impl_port *input)
+{
+ struct pw_impl_link *this = &impl->this;
+ int res;
+
+ pw_log_debug("%p: unlinking controls", impl);
+ if (this->control.valid) {
+ if ((res = pw_control_remove_link(&this->control)) < 0)
+ pw_log_error("%p: failed to unlink controls: %s",
+ this, spa_strerror(res));
+ }
+ if (this->notify.valid) {
+ if ((res = pw_control_remove_link(&this->notify)) < 0)
+ pw_log_error("%p: failed to unlink controls: %s",
+ this, spa_strerror(res));
+ }
+}
+
+static int
+check_permission(struct pw_context *context,
+ struct pw_impl_port *output,
+ struct pw_impl_port *input,
+ struct pw_properties *properties)
+{
+ return 0;
+}
+
+static void permissions_changed(struct pw_impl_link *this, struct pw_impl_port *other,
+ struct pw_impl_client *client, uint32_t old, uint32_t new)
+{
+ uint32_t perm;
+
+ perm = pw_global_get_permissions(other->global, client);
+ old &= perm;
+ new &= perm;
+ pw_log_debug("%p: permissions changed %08x -> %08x", this, old, new);
+
+ if (check_permission(this->context, this->output, this->input, this->properties) < 0) {
+ pw_impl_link_destroy(this);
+ } else if (this->global != NULL) {
+ pw_global_update_permissions(this->global, client, old, new);
+ }
+}
+
+static void output_permissions_changed(void *data,
+ struct pw_impl_client *client, uint32_t old, uint32_t new)
+{
+ struct pw_impl_link *this = data;
+ permissions_changed(this, this->input, client, old, new);
+}
+
+static const struct pw_global_events output_global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .permissions_changed = output_permissions_changed,
+};
+
+static void input_permissions_changed(void *data,
+ struct pw_impl_client *client, uint32_t old, uint32_t new)
+{
+ struct pw_impl_link *this = data;
+ permissions_changed(this, this->output, client, old, new);
+}
+
+static const struct pw_global_events input_global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .permissions_changed = input_permissions_changed,
+};
+
+SPA_EXPORT
+struct pw_impl_link *pw_context_create_link(struct pw_context *context,
+ struct pw_impl_port *output,
+ struct pw_impl_port *input,
+ struct spa_pod *format_filter,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct impl *impl;
+ struct pw_impl_link *this;
+ struct pw_impl_node *input_node, *output_node;
+ int res;
+
+ if (output == input)
+ goto error_same_ports;
+
+ if (output->direction != PW_DIRECTION_OUTPUT ||
+ input->direction != PW_DIRECTION_INPUT)
+ goto error_wrong_direction;
+
+ if (pw_impl_link_find(output, input))
+ goto error_link_exists;
+
+ if (check_permission(context, output, input, properties) < 0)
+ goto error_link_not_allowed;
+
+ output_node = output->node;
+ input_node = input->node;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ goto error_no_mem;
+
+ impl = calloc(1, sizeof(struct impl) + user_data_size);
+ if (impl == NULL)
+ goto error_no_mem;
+
+ impl->input_busy_id = SPA_ID_INVALID;
+ impl->output_busy_id = SPA_ID_INVALID;
+
+ this = &impl->this;
+ this->feedback = pw_impl_node_can_reach(input_node, output_node, 0);
+ pw_properties_set(properties, PW_KEY_LINK_FEEDBACK, this->feedback ? "true" : NULL);
+
+ pw_log_debug("%p: new out-port:%p -> in-port:%p", this, output, input);
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ impl->work = pw_context_get_work_queue(context);
+
+ this->context = context;
+ this->properties = properties;
+ this->info.state = PW_LINK_STATE_INIT;
+
+ this->output = output;
+ this->input = input;
+
+ /* passive means that this link does not make the nodes active */
+ this->passive = pw_properties_get_bool(properties, PW_KEY_LINK_PASSIVE, false);
+
+ spa_hook_list_init(&this->listener_list);
+
+ impl->format_filter = format_filter;
+ this->info.format = NULL;
+ this->info.props = &this->properties->dict;
+
+ this->rt.out_mix.peer_id = input->global->id;
+ this->rt.in_mix.peer_id = output->global->id;
+
+ if ((res = pw_impl_port_init_mix(output, &this->rt.out_mix)) < 0)
+ goto error_output_mix;
+ if ((res = pw_impl_port_init_mix(input, &this->rt.in_mix)) < 0)
+ goto error_input_mix;
+
+ pw_impl_port_add_listener(input, &impl->input_port_listener, &input_port_events, impl);
+ pw_impl_node_add_listener(input_node, &impl->input_node_listener, &input_node_events, impl);
+ pw_global_add_listener(input->global, &impl->input_global_listener, &input_global_events, impl);
+ pw_impl_port_add_listener(output, &impl->output_port_listener, &output_port_events, impl);
+ pw_impl_node_add_listener(output_node, &impl->output_node_listener, &output_node_events, impl);
+ pw_global_add_listener(output->global, &impl->output_global_listener, &output_global_events, impl);
+
+ input_node->live = output_node->live;
+
+ pw_log_debug("%p: output node %p live %d, feedback %d",
+ this, output_node, output_node->live, this->feedback);
+
+ spa_list_append(&output->links, &this->output_link);
+ spa_list_append(&input->links, &this->input_link);
+
+ impl->io = SPA_IO_BUFFERS_INIT;
+
+ select_io(this);
+
+ if (this->feedback) {
+ impl->inode = output_node;
+ impl->onode = input_node;
+ }
+ else {
+ impl->onode = output_node;
+ impl->inode = input_node;
+ }
+
+ this->rt.target.signal_func = impl->inode->rt.target.signal_func;
+ this->rt.target.data = impl->inode->rt.target.data;
+
+ pw_log_debug("%p: constructed out:%p:%d.%d -> in:%p:%d.%d", impl,
+ output_node, output->port_id, this->rt.out_mix.port.port_id,
+ input_node, input->port_id, this->rt.in_mix.port.port_id);
+
+ if (asprintf(&this->name, "%d.%d -> %d.%d",
+ output_node->info.id, output->port_id,
+ input_node->info.id, input->port_id) < 0)
+ this->name = NULL;
+ pw_log_info("(%s) (%s) -> (%s)", this->name, output_node->name, input_node->name);
+
+ pw_impl_port_emit_link_added(output, this);
+ pw_impl_port_emit_link_added(input, this);
+
+ try_link_controls(impl, output, input);
+
+ pw_impl_port_recalc_latency(output);
+ pw_impl_port_recalc_latency(input);
+
+ pw_impl_node_emit_peer_added(impl->onode, impl->inode);
+
+ return this;
+
+error_same_ports:
+ res = -EINVAL;
+ pw_log_debug("can't link the same ports");
+ goto error_exit;
+error_wrong_direction:
+ res = -EINVAL;
+ pw_log_debug("ports have wrong direction");
+ goto error_exit;
+error_link_exists:
+ res = -EEXIST;
+ pw_log_debug("link already exists");
+ goto error_exit;
+error_link_not_allowed:
+ res = -EPERM;
+ pw_log_debug("link not allowed");
+ goto error_exit;
+error_no_mem:
+ res = -errno;
+ pw_log_debug("alloc failed: %m");
+ goto error_exit;
+error_output_mix:
+ pw_log_error("%p: can't get output mix %d (%s)", this, res, spa_strerror(res));
+ goto error_free;
+error_input_mix:
+ pw_log_error("%p: can't get input mix %d (%s)", this, res, spa_strerror(res));
+ pw_impl_port_release_mix(output, &this->rt.out_mix);
+ goto error_free;
+error_free:
+ free(impl);
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_link *link = data;
+ spa_hook_remove(&link->global_listener);
+ link->global = NULL;
+ pw_impl_link_destroy(link);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+SPA_EXPORT
+int pw_impl_link_register(struct pw_impl_link *link,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_OBJECT_PATH,
+ PW_KEY_MODULE_ID,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_LINK_OUTPUT_PORT,
+ PW_KEY_LINK_INPUT_PORT,
+ PW_KEY_LINK_OUTPUT_NODE,
+ PW_KEY_LINK_INPUT_NODE,
+ NULL
+ };
+
+ struct pw_context *context = link->context;
+ struct pw_impl_node *output_node, *input_node;
+
+ if (link->registered)
+ goto error_existed;
+
+ output_node = link->output->node;
+ input_node = link->input->node;
+
+ link->info.output_node_id = output_node->global->id;
+ link->info.output_port_id = link->output->global->id;
+ link->info.input_node_id = input_node->global->id;
+ link->info.input_port_id = link->input->global->id;
+
+ link->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ properties,
+ global_bind,
+ link);
+ if (link->global == NULL)
+ return -errno;
+
+ spa_list_append(&context->link_list, &link->link);
+ link->registered = true;
+
+ link->info.id = link->global->id;
+ pw_properties_setf(link->properties, PW_KEY_OBJECT_ID, "%d", link->info.id);
+ pw_properties_setf(link->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(link->global));
+ pw_properties_setf(link->properties, PW_KEY_LINK_OUTPUT_NODE, "%u", link->info.output_node_id);
+ pw_properties_setf(link->properties, PW_KEY_LINK_OUTPUT_PORT, "%u", link->info.output_port_id);
+ pw_properties_setf(link->properties, PW_KEY_LINK_INPUT_NODE, "%u", link->info.input_node_id);
+ pw_properties_setf(link->properties, PW_KEY_LINK_INPUT_PORT, "%u", link->info.input_port_id);
+ link->info.props = &link->properties->dict;
+
+ pw_global_update_keys(link->global, link->info.props, keys);
+
+ pw_impl_link_emit_initialized(link);
+
+ pw_global_add_listener(link->global, &link->global_listener, &global_events, link);
+ pw_global_register(link->global);
+
+ pw_impl_link_prepare(link);
+
+ return 0;
+
+error_existed:
+ pw_properties_free(properties);
+ return -EEXIST;
+}
+
+SPA_EXPORT
+void pw_impl_link_destroy(struct pw_impl_link *link)
+{
+ struct impl *impl = SPA_CONTAINER_OF(link, struct impl, this);
+
+ pw_log_debug("%p: destroy", impl);
+ pw_log_info("(%s) destroy", link->name);
+ pw_impl_link_emit_destroy(link);
+
+ pw_impl_link_deactivate(link);
+
+ if (link->registered)
+ spa_list_remove(&link->link);
+
+ pw_impl_node_emit_peer_removed(impl->onode, impl->inode);
+
+ try_unlink_controls(impl, link->output, link->input);
+
+ output_remove(link, link->output);
+ input_remove(link, link->input);
+
+ if (link->global) {
+ spa_hook_remove(&link->global_listener);
+ pw_global_destroy(link->global);
+ }
+
+ if (link->prepared)
+ pw_context_recalc_graph(link->context, "link destroy");
+
+ pw_log_debug("%p: free", impl);
+ pw_impl_link_emit_free(link);
+
+ pw_work_queue_cancel(impl->work, link, SPA_ID_INVALID);
+
+ spa_hook_list_clean(&link->listener_list);
+
+ pw_properties_free(link->properties);
+
+ free(link->name);
+ free(link->info.format);
+ free(impl);
+}
+
+SPA_EXPORT
+void pw_impl_link_add_listener(struct pw_impl_link *link,
+ struct spa_hook *listener,
+ const struct pw_impl_link_events *events,
+ void *data)
+{
+ pw_log_debug("%p: add listener %p", link, listener);
+ spa_hook_list_append(&link->listener_list, listener, events, data);
+}
+
+struct pw_impl_link *pw_impl_link_find(struct pw_impl_port *output_port, struct pw_impl_port *input_port)
+{
+ struct pw_impl_link *pl;
+
+ spa_list_for_each(pl, &output_port->links, output_link) {
+ if (pl->input == input_port)
+ return pl;
+ }
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_context *pw_impl_link_get_context(struct pw_impl_link *link)
+{
+ return link->context;
+}
+
+SPA_EXPORT
+void *pw_impl_link_get_user_data(struct pw_impl_link *link)
+{
+ return link->user_data;
+}
+
+SPA_EXPORT
+const struct pw_link_info *pw_impl_link_get_info(struct pw_impl_link *link)
+{
+ return &link->info;
+}
+
+SPA_EXPORT
+struct pw_global *pw_impl_link_get_global(struct pw_impl_link *link)
+{
+ return link->global;
+}
+
+SPA_EXPORT
+struct pw_impl_port *pw_impl_link_get_output(struct pw_impl_link *link)
+{
+ return link->output;
+}
+
+SPA_EXPORT
+struct pw_impl_port *pw_impl_link_get_input(struct pw_impl_link *link)
+{
+ return link->input;
+}
diff --git a/src/pipewire/impl-link.h b/src/pipewire/impl-link.h
new file mode 100644
index 0000000..5a3b73f
--- /dev/null
+++ b/src/pipewire/impl-link.h
@@ -0,0 +1,126 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_LINK_H
+#define PIPEWIRE_IMPL_LINK_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_impl_link Link Impl
+ *
+ * \brief PipeWire link object.
+ */
+
+/**
+ * \addtogroup pw_impl_link
+ * \{
+ */
+struct pw_impl_link;
+struct pw_impl_port;
+
+#include <pipewire/impl.h>
+
+/** link events added with \ref pw_impl_link_add_listener */
+struct pw_impl_link_events {
+#define PW_VERSION_IMPL_LINK_EVENTS 0
+ uint32_t version;
+
+ /** A link is destroyed */
+ void (*destroy) (void *data);
+
+ /** A link is freed */
+ void (*free) (void *data);
+
+ /** a Link is initialized */
+ void (*initialized) (void *data);
+
+ /** The info changed on a link */
+ void (*info_changed) (void *data, const struct pw_link_info *info);
+
+ /** The link state changed, \a error is only valid when the state is
+ * in error. */
+ void (*state_changed) (void *data, enum pw_link_state old,
+ enum pw_link_state state, const char *error);
+
+ /** A port is unlinked */
+ void (*port_unlinked) (void *data, struct pw_impl_port *port);
+};
+
+
+/** Make a new link between two ports
+ * \return a newly allocated link */
+struct pw_impl_link *
+pw_context_create_link(struct pw_context *context, /**< the context object */
+ struct pw_impl_port *output, /**< an output port */
+ struct pw_impl_port *input, /**< an input port */
+ struct spa_pod *format_filter, /**< an optional format filter */
+ struct pw_properties *properties /**< extra properties */,
+ size_t user_data_size /**< extra user data size */);
+
+/** Destroy a link */
+void pw_impl_link_destroy(struct pw_impl_link *link);
+
+/** Add an event listener to \a link */
+void pw_impl_link_add_listener(struct pw_impl_link *link,
+ struct spa_hook *listener,
+ const struct pw_impl_link_events *events,
+ void *data);
+
+/** Finish link configuration and register */
+int pw_impl_link_register(struct pw_impl_link *link, /**< the link to register */
+ struct pw_properties *properties /**< extra properties */);
+
+/** Get the context of a link */
+struct pw_context *pw_impl_link_get_context(struct pw_impl_link *link);
+
+/** Get the user_data of a link, the size of the memory is given when
+ * constructing the link */
+void *pw_impl_link_get_user_data(struct pw_impl_link *link);
+
+/** Get the link info */
+const struct pw_link_info *pw_impl_link_get_info(struct pw_impl_link *link);
+
+/** Get the global of the link */
+struct pw_global *pw_impl_link_get_global(struct pw_impl_link *link);
+
+/** Get the output port of the link */
+struct pw_impl_port *pw_impl_link_get_output(struct pw_impl_link *link);
+
+/** Get the input port of the link */
+struct pw_impl_port *pw_impl_link_get_input(struct pw_impl_link *link);
+
+/** Find the link between 2 ports */
+struct pw_impl_link *pw_impl_link_find(struct pw_impl_port *output, struct pw_impl_port *input);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_LINK_H */
diff --git a/src/pipewire/impl-metadata.c b/src/pipewire/impl-metadata.c
new file mode 100644
index 0000000..9f7aa41
--- /dev/null
+++ b/src/pipewire/impl-metadata.c
@@ -0,0 +1,627 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/debug/types.h>
+#include <spa/utils/string.h>
+
+#include "pipewire/impl.h"
+#include "pipewire/private.h"
+
+#include "pipewire/extensions/metadata.h"
+
+PW_LOG_TOPIC_EXTERN(log_metadata);
+#define PW_LOG_TOPIC_DEFAULT log_metadata
+
+#define pw_metadata_emit(hooks,method,version,...) \
+ spa_hook_list_call_simple(hooks, struct pw_metadata_events, \
+ method, version, ##__VA_ARGS__)
+
+#define pw_metadata_emit_property(hooks,...) pw_metadata_emit(hooks,property, 0, ##__VA_ARGS__)
+
+struct metadata {
+ struct spa_interface iface;
+ struct pw_array storage;
+ struct spa_hook_list hooks; /**< event listeners */
+};
+
+struct item {
+ uint32_t subject;
+ char *key;
+ char *type;
+ char *value;
+};
+
+static void clear_item(struct item *item)
+{
+ free(item->key);
+ free(item->type);
+ free(item->value);
+ spa_zero(*item);
+}
+
+static void set_item(struct item *item, uint32_t subject, const char *key, const char *type, const char *value)
+{
+ item->subject = subject;
+ item->key = strdup(key);
+ item->type = type ? strdup(type) : NULL;
+ item->value = strdup(value);
+}
+
+static int change_item(struct item *item, const char *type, const char *value)
+{
+ int changed = 0;
+ if (!spa_streq(item->type, type)) {
+ free((char*)item->type);
+ item->type = type ? strdup(type) : NULL;
+ changed++;
+ }
+ if (!spa_streq(item->value, value)) {
+ free((char*)item->value);
+ item->value = value ? strdup(value) : NULL;
+ changed++;
+ }
+ return changed;
+}
+
+static void emit_properties(struct metadata *this)
+{
+ struct item *item;
+ pw_array_for_each(item, &this->storage) {
+ pw_log_debug("metadata %p: %d %s %s %s",
+ this, item->subject, item->key, item->type, item->value);
+ pw_metadata_emit_property(&this->hooks,
+ item->subject,
+ item->key,
+ item->type,
+ item->value);
+ }
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_metadata_events *events,
+ void *data)
+{
+ struct metadata *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ pw_log_debug("metadata %p:", this);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_properties(this);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static struct item *find_item(struct pw_array *storage, uint32_t subject, const char *key)
+{
+ struct item *item;
+
+ pw_array_for_each(item, storage) {
+ if (item->subject == subject && (key == NULL || spa_streq(item->key, key)))
+ return item;
+ }
+ return NULL;
+}
+
+static int clear_subjects(struct metadata *this, struct pw_array *storage, uint32_t subject)
+{
+ struct item *item;
+ uint32_t removed = 0;
+
+ while (true) {
+ item = find_item(storage, subject, NULL);
+ if (item == NULL)
+ break;
+
+ pw_log_debug("%p: remove id:%d key:%s", this, subject, item->key);
+
+ clear_item(item);
+ pw_array_remove(storage, item);
+ removed++;
+ }
+ if (removed > 0)
+ pw_metadata_emit_property(&this->hooks, subject, NULL, NULL, NULL);
+
+ return 0;
+}
+
+static void clear_items(struct metadata *this)
+{
+ struct item *item;
+ struct pw_array tmp;
+
+ /* copy to tmp and reinitialize the storage so that the callbacks
+ * will operate on the new empty metadata. Otherwise, if a callbacks
+ * adds new metadata we just keep on emptying the metadata forever. */
+ tmp = this->storage;
+ pw_array_init(&this->storage, 4096);
+
+ pw_array_consume(item, &tmp)
+ clear_subjects(this, &tmp, item->subject);
+ pw_array_clear(&tmp);
+}
+
+static int impl_set_property(void *object,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct metadata *this = object;
+ struct item *item = NULL;
+ int changed = 0;
+
+ pw_log_debug("%p: id:%d key:%s type:%s value:%s", this, subject, key, type, value);
+
+ if (key == NULL)
+ return clear_subjects(this, &this->storage, subject);
+
+ item = find_item(&this->storage, subject, key);
+ if (value == NULL) {
+ if (item != NULL) {
+ clear_item(item);
+ pw_array_remove(&this->storage, item);
+ type = NULL;
+ changed++;
+ pw_log_info("%p: remove id:%d key:%s", this,
+ subject, key);
+ }
+ } else if (item == NULL) {
+ item = pw_array_add(&this->storage, sizeof(*item));
+ if (item == NULL)
+ return -errno;
+ set_item(item, subject, key, type, value);
+ changed++;
+ pw_log_info("%p: add id:%d key:%s type:%s value:%s", this,
+ subject, key, type, value);
+ } else {
+ if (type == NULL)
+ type = item->type;
+ changed = change_item(item, type, value);
+ if (changed)
+ pw_log_info("%p: change id:%d key:%s type:%s value:%s", this,
+ subject, key, type, value);
+ }
+
+ if (changed) {
+ pw_metadata_emit_property(&this->hooks,
+ subject, key, type, value);
+ }
+ return 0;
+}
+
+static int impl_clear(void *object)
+{
+ struct metadata *this = object;
+ clear_items(this);
+ return 0;
+}
+
+static const struct pw_metadata_methods impl_metadata = {
+ PW_VERSION_METADATA_METHODS,
+ .add_listener = impl_add_listener,
+ .set_property = impl_set_property,
+ .clear = impl_clear,
+};
+
+static struct pw_metadata *metadata_init(struct metadata *this)
+{
+ this->iface = SPA_INTERFACE_INIT(
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ &impl_metadata, this);
+ pw_array_init(&this->storage, 4096);
+ spa_hook_list_init(&this->hooks);
+ return (struct pw_metadata*)&this->iface;
+}
+
+static void metadata_reset(struct metadata *this)
+{
+ spa_hook_list_clean(&this->hooks);
+ clear_items(this);
+ pw_array_clear(&this->storage);
+}
+
+struct impl {
+ struct pw_impl_metadata this;
+
+ struct metadata def;
+};
+
+struct resource_data {
+ struct pw_impl_metadata *impl;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+ struct spa_hook metadata_listener;
+};
+
+
+static int metadata_property(void *data, uint32_t subject, const char *key,
+ const char *type, const char *value)
+{
+ struct pw_impl_metadata *this = data;
+ pw_impl_metadata_emit_property(this, subject, key, type, value);
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property,
+};
+
+SPA_EXPORT
+struct pw_impl_metadata *pw_context_create_metadata(struct pw_context *context,
+ const char *name, struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct impl *impl;
+ struct pw_impl_metadata *this;
+ int res;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ return NULL;
+
+ impl = calloc(1, sizeof(*impl) + user_data_size);
+ if (impl == NULL) {
+ res = -errno;
+ goto error_exit;
+ };
+ this = &impl->this;
+
+ this->context = context;
+ this->properties = properties;
+
+ if (name != NULL)
+ pw_properties_set(properties, PW_KEY_METADATA_NAME, name);
+
+ spa_hook_list_init(&this->listener_list);
+
+ pw_impl_metadata_set_implementation(this, metadata_init(&impl->def));
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(this, sizeof(*this), void);
+
+ pw_log_debug("%p: new", this);
+
+ return this;
+
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_impl_metadata_get_properties(struct pw_impl_metadata *metadata)
+{
+ return metadata->properties;
+}
+
+SPA_EXPORT
+int pw_impl_metadata_set_implementation(struct pw_impl_metadata *metadata,
+ struct pw_metadata *meta)
+{
+ struct impl *impl = SPA_CONTAINER_OF(metadata, struct impl, this);
+
+ if (metadata->metadata == meta)
+ return 0;
+
+ if (metadata->metadata)
+ spa_hook_remove(&metadata->metadata_listener);
+ if (meta == NULL)
+ meta = (struct pw_metadata*)&impl->def.iface;
+
+ metadata->metadata = meta;
+ pw_metadata_add_listener(meta, &metadata->metadata_listener,
+ &metadata_events, metadata);
+
+ return 0;
+}
+SPA_EXPORT
+struct pw_metadata *pw_impl_metadata_get_implementation(struct pw_impl_metadata *metadata)
+{
+ return metadata->metadata;
+}
+
+SPA_EXPORT
+void pw_impl_metadata_destroy(struct pw_impl_metadata *metadata)
+{
+ struct impl *impl = SPA_CONTAINER_OF(metadata, struct impl, this);
+
+ pw_log_debug("%p: destroy", metadata);
+ pw_impl_metadata_emit_destroy(metadata);
+
+ if (metadata->registered)
+ spa_list_remove(&metadata->link);
+
+ if (metadata->global) {
+ spa_hook_remove(&metadata->global_listener);
+ pw_global_destroy(metadata->global);
+ }
+
+ pw_impl_metadata_emit_free(metadata);
+ pw_log_debug("%p: free", metadata);
+
+ metadata_reset(&impl->def);
+
+ spa_hook_list_clean(&metadata->listener_list);
+
+ pw_properties_free(metadata->properties);
+
+ free(metadata);
+}
+
+#define pw_metadata_resource(r,m,v,...) \
+ pw_resource_call_res(r,struct pw_metadata_events,m,v,__VA_ARGS__)
+
+#define pw_metadata_resource_property(r,...) \
+ pw_metadata_resource(r,property,0,__VA_ARGS__)
+
+static int metadata_resource_property(void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct resource_data *d = data;
+ struct pw_resource *resource = d->resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+
+ if (pw_impl_client_check_permissions(client, subject, PW_PERM_R) >= 0)
+ pw_metadata_resource_property(d->resource, subject, key, type, value);
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_resource_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_resource_property,
+};
+
+static int metadata_set_property(void *object,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct resource_data *d = object;
+ struct pw_impl_metadata *impl = d->impl;
+ struct pw_resource *resource = d->resource;
+ struct pw_impl_client *client = pw_resource_get_client(resource);
+ int res;
+
+ if ((res = pw_impl_client_check_permissions(client, subject, PW_PERM_R)) < 0)
+ goto error;
+
+ pw_metadata_set_property(impl->metadata, subject, key, type, value);
+ return 0;
+
+error:
+ pw_resource_errorf(resource, res, "set property error for id %d: %s",
+ subject, spa_strerror(res));
+ return res;
+}
+
+static int metadata_clear(void *object)
+{
+ struct resource_data *d = object;
+ struct pw_impl_metadata *impl = d->impl;
+ pw_metadata_clear(impl->metadata);
+ return 0;
+}
+
+static const struct pw_metadata_methods metadata_methods = {
+ PW_VERSION_METADATA_METHODS,
+ .set_property = metadata_set_property,
+ .clear = metadata_clear,
+};
+
+static void global_unbind(void *data)
+{
+ struct resource_data *d = data;
+ if (d->resource) {
+ spa_hook_remove(&d->resource_listener);
+ spa_hook_remove(&d->object_listener);
+ spa_hook_remove(&d->metadata_listener);
+ }
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = global_unbind,
+};
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct pw_impl_metadata *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, sizeof(*data));
+ if (resource == NULL)
+ goto error_resource;
+
+ data = pw_resource_get_user_data(resource);
+ data->impl = this;
+ data->resource = resource;
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+ pw_global_add_resource(global, resource);
+
+ /* listen for when the resource goes away */
+ pw_resource_add_listener(resource,
+ &data->resource_listener,
+ &resource_events, data);
+
+ /* resource methods -> implementation */
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &metadata_methods, data);
+
+ /* implementation events -> resource */
+ pw_metadata_add_listener(this->metadata,
+ &data->metadata_listener,
+ &metadata_resource_events, data);
+
+ return 0;
+
+error_resource:
+ pw_log_error("%p: can't create metadata resource: %m", this);
+ return -errno;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_metadata *metadata = data;
+ spa_hook_remove(&metadata->global_listener);
+ metadata->global = NULL;
+ pw_impl_metadata_destroy(metadata);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+SPA_EXPORT
+int pw_impl_metadata_register(struct pw_impl_metadata *metadata,
+ struct pw_properties *properties)
+{
+ struct pw_context *context = metadata->context;
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_MODULE_ID,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_METADATA_NAME,
+ NULL
+ };
+
+ if (metadata->registered)
+ goto error_existed;
+
+ metadata->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Metadata,
+ PW_VERSION_METADATA,
+ properties,
+ global_bind,
+ metadata);
+ if (metadata->global == NULL)
+ return -errno;
+
+ spa_list_append(&context->metadata_list, &metadata->link);
+ metadata->registered = true;
+
+ pw_properties_setf(metadata->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(metadata->global));
+
+ pw_global_update_keys(metadata->global, &metadata->properties->dict, keys);
+
+ pw_global_add_listener(metadata->global, &metadata->global_listener, &global_events, metadata);
+ pw_global_register(metadata->global);
+
+ return 0;
+
+error_existed:
+ pw_properties_free(properties);
+ return -EEXIST;
+}
+
+SPA_EXPORT
+void *pw_impl_metadata_get_user_data(struct pw_impl_metadata *metadata)
+{
+ return metadata->user_data;
+}
+
+SPA_EXPORT
+struct pw_global *pw_impl_metadata_get_global(struct pw_impl_metadata *metadata)
+{
+ return metadata->global;
+}
+
+SPA_EXPORT
+void pw_impl_metadata_add_listener(struct pw_impl_metadata *metadata,
+ struct spa_hook *listener,
+ const struct pw_impl_metadata_events *events,
+ void *data)
+{
+ spa_hook_list_append(&metadata->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+int pw_impl_metadata_set_property(struct pw_impl_metadata *metadata,
+ uint32_t subject, const char *key, const char *type,
+ const char *value)
+{
+ return pw_metadata_set_property(metadata->metadata, subject, key, type, value);
+}
+
+SPA_EXPORT
+int pw_impl_metadata_set_propertyf(struct pw_impl_metadata *metadata,
+ uint32_t subject, const char *key, const char *type,
+ const char *fmt, ...)
+{
+ va_list args;
+ int n = 0, res;
+ size_t size = 0;
+ char *p = NULL;
+
+ va_start(args, fmt);
+ n = vsnprintf(p, size, fmt, args);
+ va_end(args);
+ if (n < 0)
+ return -errno;
+
+ size = (size_t) n + 1;
+ p = malloc(size);
+ if (p == NULL)
+ return -errno;
+
+ va_start(args, fmt);
+ n = vsnprintf(p, size, fmt, args);
+ va_end(args);
+
+ if (n < 0) {
+ free(p);
+ return -errno;
+ }
+ res = pw_impl_metadata_set_property(metadata, subject, key, type, p);
+ free(p);
+
+ return res;
+}
diff --git a/src/pipewire/impl-metadata.h b/src/pipewire/impl-metadata.h
new file mode 100644
index 0000000..4b81cac
--- /dev/null
+++ b/src/pipewire/impl-metadata.h
@@ -0,0 +1,114 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_METADATA_H
+#define PIPEWIRE_IMPL_METADATA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_impl_metadata Metadata Impl
+ *
+ * The metadata is used to store key/type/value pairs per object id.
+ */
+
+/**
+ * \addtogroup pw_impl_metadata
+ * \{
+ */
+struct pw_impl_metadata;
+
+#include <pipewire/context.h>
+#include <pipewire/impl-client.h>
+#include <pipewire/global.h>
+#include <pipewire/properties.h>
+#include <pipewire/resource.h>
+
+#include "pipewire/extensions/metadata.h"
+
+/** Metadata events, listen to them with \ref pw_impl_metadata_add_listener */
+struct pw_impl_metadata_events {
+#define PW_VERSION_IMPL_METADATA_EVENTS 0
+ uint32_t version;
+
+ /** the metadata is destroyed */
+ void (*destroy) (void *data);
+ /** the metadata is freed */
+ void (*free) (void *data);
+
+ /** a property changed */
+ int (*property) (void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value);
+};
+
+struct pw_impl_metadata *pw_context_create_metadata(struct pw_context *context,
+ const char *name, struct pw_properties *properties,
+ size_t user_data_size);
+
+/** Get the metadata properties */
+const struct pw_properties *pw_impl_metadata_get_properties(struct pw_impl_metadata *metadata);
+
+int pw_impl_metadata_register(struct pw_impl_metadata *metadata,
+ struct pw_properties *properties);
+
+void pw_impl_metadata_destroy(struct pw_impl_metadata *metadata);
+
+void *pw_impl_metadata_get_user_data(struct pw_impl_metadata *metadata);
+
+int pw_impl_metadata_set_implementation(struct pw_impl_metadata *metadata,
+ struct pw_metadata *impl);
+
+struct pw_metadata *pw_impl_metadata_get_implementation(struct pw_impl_metadata *metadata);
+
+/** Get the global of this metadata */
+struct pw_global *pw_impl_metadata_get_global(struct pw_impl_metadata *metadata);
+
+/** Add an event listener */
+void pw_impl_metadata_add_listener(struct pw_impl_metadata *metadata,
+ struct spa_hook *listener,
+ const struct pw_impl_metadata_events *events,
+ void *data);
+
+/** Set a property */
+int pw_impl_metadata_set_property(struct pw_impl_metadata *metadata,
+ uint32_t subject, const char *key, const char *type,
+ const char *value);
+
+int pw_impl_metadata_set_propertyf(struct pw_impl_metadata *metadata,
+ uint32_t subject, const char *key, const char *type,
+ const char *fmt, ...) SPA_PRINTF_FUNC(5,6);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_METADATA_H */
diff --git a/src/pipewire/impl-module.c b/src/pipewire/impl-module.c
new file mode 100644
index 0000000..282ba08
--- /dev/null
+++ b/src/pipewire/impl-module.c
@@ -0,0 +1,427 @@
+/* PipeWire
+ * Copyright © 2016 Axis Communications <dev-gstreamer@axis.com>
+ * @author Linus Svensson <linus.svensson@axis.com>
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <dlfcn.h>
+#include <dirent.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <errno.h>
+
+#include <spa/utils/string.h>
+
+#include "pipewire/impl.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_module);
+#define PW_LOG_TOPIC_DEFAULT log_module
+
+/** \cond */
+struct impl {
+ struct pw_impl_module this;
+ void *hnd;
+ uint32_t destroy_work_id;
+};
+
+#define pw_module_resource_info(r,...) pw_resource_call(r,struct pw_module_events,info,0,__VA_ARGS__)
+
+
+/** \endcond */
+
+static char *find_module(const char *path, const char *name, int level)
+{
+ char *filename;
+ struct dirent *entry;
+ struct stat s;
+ DIR *dir;
+ int res;
+
+ filename = spa_aprintf("%s/%s.so", path, name);
+ if (filename == NULL)
+ return NULL;
+
+ if (stat(filename, &s) == 0 && S_ISREG(s.st_mode)) {
+ /* found a regular file with name */
+ return filename;
+ }
+
+ free(filename);
+ filename = NULL;
+
+ /* now recurse down in subdirectories and look for it there */
+ if (level <= 0)
+ return NULL;
+
+ dir = opendir(path);
+ if (dir == NULL) {
+ res = -errno;
+ pw_log_warn("could not open %s: %m", path);
+ errno = -res;
+ return NULL;
+ }
+
+ while ((entry = readdir(dir))) {
+ char *newpath;
+
+ if (spa_streq(entry->d_name, ".") || spa_streq(entry->d_name, ".."))
+ continue;
+
+ newpath = spa_aprintf("%s/%s", path, entry->d_name);
+ if (newpath == NULL)
+ break;
+
+ if (stat(newpath, &s) == 0 && S_ISDIR(s.st_mode))
+ filename = find_module(newpath, name, level - 1);
+
+ free(newpath);
+
+ if (filename != NULL)
+ break;
+ }
+
+ closedir(dir);
+
+ return filename;
+}
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct pw_impl_module *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, 0);
+ if (resource == NULL)
+ goto error_resource;
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_MODULE_CHANGE_MASK_ALL;
+ pw_module_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+error_resource:
+ pw_log_error("%p: can't create module resource: %m", this);
+ return -errno;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_module *module = data;
+ spa_hook_remove(&module->global_listener);
+ module->global = NULL;
+ pw_impl_module_destroy(module);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+/** Load a module
+ *
+ * \param context a \ref pw_context
+ * \param name name of the module to load
+ * \param args A string with arguments for the module
+ * \param properties extra global properties
+ * \return A \ref pw_impl_module if the module could be loaded, or NULL on failure.
+ *
+ */
+SPA_EXPORT
+struct pw_impl_module *
+pw_context_load_module(struct pw_context *context,
+ const char *name, const char *args,
+ struct pw_properties *properties)
+{
+ struct pw_impl_module *this;
+ struct impl *impl;
+ void *hnd;
+ char *filename = NULL;
+ const char *module_dir;
+ int res;
+ pw_impl_module_init_func_t init_func;
+ const char *state = NULL, *p;
+ size_t len;
+ char path_part[PATH_MAX];
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_MODULE_NAME,
+ NULL
+ };
+
+ pw_log_info("%p: name:%s args:%s", context, name, args);
+
+ module_dir = getenv("PIPEWIRE_MODULE_DIR");
+ if (module_dir == NULL) {
+ module_dir = MODULEDIR;
+ pw_log_debug("moduledir set to: %s", module_dir);
+ }
+ else {
+ pw_log_debug("PIPEWIRE_MODULE_DIR set to: %s", module_dir);
+ }
+
+ while ((p = pw_split_walk(module_dir, ":", &len, &state))) {
+ if ((res = spa_scnprintf(path_part, sizeof(path_part), "%.*s", (int)len, p)) > 0) {
+ filename = find_module(path_part, name, 8);
+ if (filename != NULL) {
+ pw_log_debug("trying to load module: %s (%s) args(%s)", name, filename, args);
+
+ hnd = dlopen(filename, RTLD_NOW | RTLD_LOCAL);
+ if (hnd != NULL)
+ break;
+
+ pw_log_debug("open failed: %s", dlerror());
+ free(filename);
+ filename = NULL;
+ }
+ }
+ }
+
+ if (filename == NULL)
+ goto error_not_found;
+ if (hnd == NULL)
+ goto error_open_failed;
+
+ if ((init_func = dlsym(hnd, PIPEWIRE_SYMBOL_MODULE_INIT)) == NULL)
+ goto error_no_pw_module;
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL)
+ goto error_no_mem;
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL)
+ goto error_no_mem;
+
+ impl->hnd = hnd;
+ impl->destroy_work_id = SPA_ID_INVALID;
+ hnd = NULL;
+
+ this = &impl->this;
+ this->context = context;
+ this->properties = properties;
+ properties = NULL;
+
+ spa_hook_list_init(&this->listener_list);
+
+ pw_properties_set(this->properties, PW_KEY_MODULE_NAME, name);
+
+ this->info.name = name ? strdup(name) : NULL;
+ this->info.filename = filename;
+ filename = NULL;
+ this->info.args = args ? strdup(args) : NULL;
+
+ spa_list_prepend(&context->module_list, &this->link);
+
+ this->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Module,
+ PW_VERSION_MODULE,
+ NULL,
+ global_bind,
+ this);
+
+ if (this->global == NULL)
+ goto error_no_global;
+
+ this->info.id = this->global->id;
+ pw_properties_setf(this->properties, PW_KEY_OBJECT_ID, "%d", this->info.id);
+ pw_properties_setf(this->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+ this->info.props = &this->properties->dict;
+
+ pw_global_update_keys(this->global, &this->properties->dict, keys);
+
+ pw_impl_module_emit_initialized(this);
+
+ pw_global_add_listener(this->global, &this->global_listener, &global_events, this);
+
+ if ((res = init_func(this, args)) < 0)
+ goto error_init_failed;
+
+ pw_global_register(this->global);
+
+ pw_impl_module_emit_registered(this);
+
+ pw_log_debug("%p: loaded module: %s", this, this->info.name);
+
+ return this;
+
+error_not_found:
+ res = -ENOENT;
+ pw_log_info("No module \"%s\" was found", name);
+ goto error_cleanup;
+error_open_failed:
+ res = -EIO;
+ pw_log_error("Failed to open module: \"%s\" %s", filename, dlerror());
+ goto error_free_filename;
+error_no_pw_module:
+ res = -ENOSYS;
+ pw_log_error("\"%s\": is not a pipewire module", filename);
+ goto error_close;
+error_no_mem:
+ res = -errno;
+ pw_log_error("can't allocate module: %m");
+ goto error_close;
+error_no_global:
+ res = -errno;
+ pw_log_error("\"%s\": failed to create global: %m", this->info.filename);
+ goto error_free_module;
+error_init_failed:
+ pw_log_debug("\"%s\": failed to initialize: %s", this->info.filename, spa_strerror(res));
+ goto error_free_module;
+
+error_free_module:
+ pw_impl_module_destroy(this);
+error_close:
+ if (hnd)
+ dlclose(hnd);
+error_free_filename:
+ if (filename)
+ free(filename);
+error_cleanup:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+/** Destroy a module
+ * \param module the module to destroy
+ */
+SPA_EXPORT
+void pw_impl_module_destroy(struct pw_impl_module *module)
+{
+ struct impl *impl = SPA_CONTAINER_OF(module, struct impl, this);
+
+ pw_log_debug("%p: destroy %s", module, module->info.name);
+ pw_impl_module_emit_destroy(module);
+
+ spa_list_remove(&module->link);
+
+ if (module->global) {
+ spa_hook_remove(&module->global_listener);
+ pw_global_destroy(module->global);
+ }
+
+ pw_log_debug("%p: free", module);
+ pw_impl_module_emit_free(module);
+ free((char *) module->info.name);
+ free((char *) module->info.filename);
+ free((char *) module->info.args);
+
+ pw_properties_free(module->properties);
+
+ spa_hook_list_clean(&module->listener_list);
+
+ if (impl->destroy_work_id != SPA_ID_INVALID)
+ pw_work_queue_cancel(pw_context_get_work_queue(module->context),
+ module, SPA_ID_INVALID);
+
+ if (!pw_in_valgrind() && dlclose(impl->hnd) != 0)
+ pw_log_warn("%p: dlclose failed: %s", module, dlerror());
+ free(impl);
+}
+
+SPA_EXPORT
+struct pw_context *
+pw_impl_module_get_context(struct pw_impl_module *module)
+{
+ return module->context;
+}
+
+SPA_EXPORT
+struct pw_global * pw_impl_module_get_global(struct pw_impl_module *module)
+{
+ return module->global;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_impl_module_get_properties(struct pw_impl_module *module)
+{
+ return module->properties;
+}
+
+SPA_EXPORT
+int pw_impl_module_update_properties(struct pw_impl_module *module, const struct spa_dict *dict)
+{
+ struct pw_resource *resource;
+ int changed;
+
+ changed = pw_properties_update(module->properties, dict);
+ module->info.props = &module->properties->dict;
+
+ pw_log_debug("%p: updated %d properties", module, changed);
+
+ if (!changed)
+ return 0;
+
+ module->info.change_mask |= PW_MODULE_CHANGE_MASK_PROPS;
+ if (module->global)
+ spa_list_for_each(resource, &module->global->resource_list, link)
+ pw_module_resource_info(resource, &module->info);
+ module->info.change_mask = 0;
+
+ return changed;
+}
+
+SPA_EXPORT
+const struct pw_module_info *
+pw_impl_module_get_info(struct pw_impl_module *module)
+{
+ return &module->info;
+}
+
+SPA_EXPORT
+void pw_impl_module_add_listener(struct pw_impl_module *module,
+ struct spa_hook *listener,
+ const struct pw_impl_module_events *events,
+ void *data)
+{
+ spa_hook_list_append(&module->listener_list, listener, events, data);
+}
+
+static void do_destroy_module(void *obj, void *data, int res, uint32_t id)
+{
+ pw_impl_module_destroy(obj);
+}
+
+SPA_EXPORT
+void pw_impl_module_schedule_destroy(struct pw_impl_module *module)
+{
+ struct impl *impl = SPA_CONTAINER_OF(module, struct impl, this);
+
+ if (impl->destroy_work_id != SPA_ID_INVALID)
+ return;
+
+ impl->destroy_work_id = pw_work_queue_add(pw_context_get_work_queue(module->context),
+ module, 0, do_destroy_module, NULL);
+}
diff --git a/src/pipewire/impl-module.h b/src/pipewire/impl-module.h
new file mode 100644
index 0000000..0135498
--- /dev/null
+++ b/src/pipewire/impl-module.h
@@ -0,0 +1,120 @@
+/* PipeWire
+ * Copyright © 2016 Axis Communications <dev-gstreamer@axis.com>
+ * @author Linus Svensson <linus.svensson@axis.com>
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_MODULE_H
+#define PIPEWIRE_IMPL_MODULE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+
+#include <pipewire/context.h>
+
+#define PIPEWIRE_SYMBOL_MODULE_INIT "pipewire__module_init"
+#define PIPEWIRE_MODULE_PREFIX "libpipewire-"
+
+/** \defgroup pw_impl_module Module Impl
+ *
+ * A dynamically loadable module
+ */
+
+/**
+ * \addtogroup pw_impl_module
+ * \{
+ */
+struct pw_impl_module;
+
+/** Module init function signature
+ *
+ * \param module A \ref pw_impl_module
+ * \param args Arguments to the module
+ * \return 0 on success, < 0 otherwise with an errno style error
+ *
+ * A module should provide an init function with this signature. This function
+ * will be called when a module is loaded.
+ */
+typedef int (*pw_impl_module_init_func_t) (struct pw_impl_module *module, const char *args);
+
+/** Module events added with \ref pw_impl_module_add_listener */
+struct pw_impl_module_events {
+#define PW_VERSION_IMPL_MODULE_EVENTS 0
+ uint32_t version;
+
+ /** The module is destroyed */
+ void (*destroy) (void *data);
+ /** The module is freed */
+ void (*free) (void *data);
+ /** The module is initialized */
+ void (*initialized) (void *data);
+
+ /** The module is registered. This is a good time to register
+ * objects created from the module. */
+ void (*registered) (void *data);
+};
+
+struct pw_impl_module *
+pw_context_load_module(struct pw_context *context,
+ const char *name,
+ const char *args,
+ struct pw_properties *properties);
+
+/** Get the context of a module */
+struct pw_context * pw_impl_module_get_context(struct pw_impl_module *module);
+
+/** Get the global of a module */
+struct pw_global * pw_impl_module_get_global(struct pw_impl_module *module);
+
+/** Get the module properties */
+const struct pw_properties *pw_impl_module_get_properties(struct pw_impl_module *module);
+
+/** Update the module properties */
+int pw_impl_module_update_properties(struct pw_impl_module *module, const struct spa_dict *dict);
+
+/** Get the module info */
+const struct pw_module_info *pw_impl_module_get_info(struct pw_impl_module *module);
+
+/** Add an event listener to a module */
+void pw_impl_module_add_listener(struct pw_impl_module *module,
+ struct spa_hook *listener,
+ const struct pw_impl_module_events *events,
+ void *data);
+
+/** Destroy a module */
+void pw_impl_module_destroy(struct pw_impl_module *module);
+
+/** Schedule a destroy later on the main thread */
+void pw_impl_module_schedule_destroy(struct pw_impl_module *module);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_MODULE_H */
diff --git a/src/pipewire/impl-node.c b/src/pipewire/impl-node.c
new file mode 100644
index 0000000..6200e80
--- /dev/null
+++ b/src/pipewire/impl-node.c
@@ -0,0 +1,2316 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+
+#include <spa/support/system.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/node/utils.h>
+#include <spa/debug/types.h>
+#include <spa/utils/string.h>
+
+#include "pipewire/impl-node.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_node);
+#define PW_LOG_TOPIC_DEFAULT log_node
+
+#define DEFAULT_SYNC_TIMEOUT ((uint64_t)(5 * SPA_NSEC_PER_SEC))
+
+/** \cond */
+struct impl {
+ struct pw_impl_node this;
+
+ enum pw_node_state pending_state;
+ uint32_t pending_id;
+
+ struct pw_work_queue *work;
+
+ int last_error;
+
+ struct spa_list param_list;
+ struct spa_list pending_list;
+
+ unsigned int cache_params:1;
+ unsigned int pending_play:1;
+};
+
+#define pw_node_resource(r,m,v,...) pw_resource_call(r,struct pw_node_events,m,v,__VA_ARGS__)
+#define pw_node_resource_info(r,...) pw_node_resource(r,info,0,__VA_ARGS__)
+#define pw_node_resource_param(r,...) pw_node_resource(r,param,0,__VA_ARGS__)
+
+struct resource_data {
+ struct pw_impl_node *node;
+
+ struct pw_resource *resource;
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ uint32_t subscribe_ids[MAX_PARAMS];
+ uint32_t n_subscribe_ids;
+
+ /* for async replies */
+ int seq;
+ int end;
+ struct spa_hook listener;
+};
+
+/** \endcond */
+
+static void add_node(struct pw_impl_node *this, struct pw_impl_node *driver)
+{
+ struct pw_node_activation_state *dstate, *nstate;
+ struct pw_node_target *t;
+
+ if (this->exported)
+ return;
+
+ pw_log_trace("%p: add to driver %p %p %p", this, driver,
+ driver->rt.activation, this->rt.activation);
+
+ /* signal the driver */
+ this->rt.driver_target.activation = driver->rt.activation;
+ this->rt.driver_target.node = driver;
+ this->rt.driver_target.data = driver;
+ spa_list_append(&this->rt.target_list, &this->rt.driver_target.link);
+
+ spa_list_append(&driver->rt.target_list, &this->rt.target.link);
+ nstate = &this->rt.activation->state[0];
+ if (!this->rt.target.active) {
+ nstate->required++;
+ this->rt.target.active = true;
+ }
+
+ spa_list_for_each(t, &this->rt.target_list, link) {
+ dstate = &t->activation->state[0];
+ if (!t->active) {
+ dstate->required++;
+ t->active = true;
+ }
+ pw_log_trace("%p: driver state:%p pending:%d/%d, node state:%p pending:%d/%d",
+ this, dstate, dstate->pending, dstate->required,
+ nstate, nstate->pending, nstate->required);
+ }
+}
+
+static void remove_node(struct pw_impl_node *this)
+{
+ struct pw_node_activation_state *dstate, *nstate;
+ struct pw_node_target *t;
+
+ if (this->exported)
+ return;
+
+ pw_log_trace("%p: remove from driver %p %p %p",
+ this, this->rt.driver_target.data,
+ this->rt.driver_target.activation, this->rt.activation);
+
+ spa_list_remove(&this->rt.target.link);
+
+ nstate = &this->rt.activation->state[0];
+ if (this->rt.target.active) {
+ nstate->required--;
+ this->rt.target.active = false;
+ }
+
+ spa_list_for_each(t, &this->rt.target_list, link) {
+ dstate = &t->activation->state[0];
+ if (t->active) {
+ dstate->required--;
+ t->active = false;
+ }
+ pw_log_trace("%p: driver state:%p pending:%d/%d, node state:%p pending:%d/%d",
+ this, dstate, dstate->pending, dstate->required,
+ nstate, nstate->pending, nstate->required);
+ }
+ spa_list_remove(&this->rt.driver_target.link);
+
+ this->rt.driver_target.node = NULL;
+}
+
+static int
+do_node_add(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct pw_impl_node *this = user_data;
+ struct pw_impl_node *driver = this->driver_node;
+
+ this->added = true;
+ if (this->source.loop == NULL) {
+ struct spa_system *data_system = this->context->data_system;
+ uint64_t dummy;
+ int res;
+
+ /* clear the eventfd in case it was written to while the node was stopped */
+ res = spa_system_eventfd_read(data_system, this->source.fd, &dummy);
+ if (SPA_UNLIKELY(res != -EAGAIN && res != 0))
+ pw_log_warn("%p: read failed %m", this);
+
+ spa_loop_add_source(loop, &this->source);
+ add_node(this, driver);
+ }
+ return 0;
+}
+
+static int
+do_node_remove(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct pw_impl_node *this = user_data;
+ if (this->source.loop != NULL) {
+ spa_loop_remove_source(loop, &this->source);
+ remove_node(this);
+ }
+ this->added = false;
+ return 0;
+}
+
+static void node_deactivate(struct pw_impl_node *this)
+{
+ struct pw_impl_port *port;
+ struct pw_impl_link *link;
+
+ pw_log_debug("%p: deactivate", this);
+
+ /* make sure the node doesn't get woken up while not active */
+ pw_loop_invoke(this->data_loop, do_node_remove, 1, NULL, 0, true, this);
+
+ spa_list_for_each(port, &this->input_ports, link) {
+ spa_list_for_each(link, &port->links, input_link)
+ pw_impl_link_deactivate(link);
+ }
+ spa_list_for_each(port, &this->output_ports, link) {
+ spa_list_for_each(link, &port->links, output_link)
+ pw_impl_link_deactivate(link);
+ }
+}
+
+static int idle_node(struct pw_impl_node *this)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ int res = 0;
+
+ pw_log_debug("%p: idle node state:%s pending:%s pause-on-idle:%d", this,
+ pw_node_state_as_string(this->info.state),
+ pw_node_state_as_string(impl->pending_state),
+ this->pause_on_idle);
+
+ if (impl->pending_state <= PW_NODE_STATE_IDLE)
+ return 0;
+
+ if (!this->pause_on_idle)
+ return 0;
+
+ node_deactivate(this);
+
+ res = spa_node_send_command(this->node,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause));
+ if (res < 0)
+ pw_log_debug("%p: pause node error %s", this, spa_strerror(res));
+
+ return res;
+}
+
+static void node_activate(struct pw_impl_node *this)
+{
+ struct pw_impl_port *port;
+
+ pw_log_debug("%p: activate", this);
+ spa_list_for_each(port, &this->output_ports, link) {
+ struct pw_impl_link *link;
+ spa_list_for_each(link, &port->links, output_link)
+ pw_impl_link_activate(link);
+ }
+ spa_list_for_each(port, &this->input_ports, link) {
+ struct pw_impl_link *link;
+ spa_list_for_each(link, &port->links, input_link)
+ pw_impl_link_activate(link);
+ }
+}
+
+static int start_node(struct pw_impl_node *this)
+{
+ struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
+ int res = 0;
+
+ node_activate(this);
+
+ if (impl->pending_state >= PW_NODE_STATE_RUNNING)
+ return 0;
+
+ pw_log_debug("%p: start node driving:%d driver:%d added:%d", this,
+ this->driving, this->driver, this->added);
+
+ if (!(this->driving && this->driver)) {
+ impl->pending_play = true;
+ res = spa_node_send_command(this->node,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start));
+ } else {
+ /* driver nodes will wait until all other nodes are started before
+ * they are started */
+ res = EBUSY;
+ }
+
+ if (res < 0)
+ pw_log_error("(%s-%u) start node error %d: %s", this->name, this->info.id,
+ res, spa_strerror(res));
+
+ return res;
+}
+
+static void emit_info_changed(struct pw_impl_node *node, bool flags_changed)
+{
+ if (node->info.change_mask == 0 && !flags_changed)
+ return;
+
+ pw_impl_node_emit_info_changed(node, &node->info);
+
+ if (node->global && node->info.change_mask != 0) {
+ struct pw_resource *resource;
+ spa_list_for_each(resource, &node->global->resource_list, link)
+ pw_node_resource_info(resource, &node->info);
+ }
+
+ node->info.change_mask = 0;
+}
+
+static int resource_is_subscribed(struct pw_resource *resource, uint32_t id)
+{
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == id)
+ return 1;
+ }
+ return 0;
+}
+
+static int notify_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct pw_impl_node *node = data;
+ struct pw_resource *resource;
+
+ spa_list_for_each(resource, &node->global->resource_list, link) {
+ if (!resource_is_subscribed(resource, id))
+ continue;
+
+ pw_log_debug("%p: resource %p notify param %d", node, resource, id);
+ pw_node_resource_param(resource, seq, id, index, next, param);
+ }
+ return 0;
+}
+
+static void emit_params(struct pw_impl_node *node, uint32_t *changed_ids, uint32_t n_changed_ids)
+{
+ uint32_t i;
+ int res;
+
+ if (node->global == NULL)
+ return;
+
+ pw_log_debug("%p: emit %d params", node, n_changed_ids);
+
+ for (i = 0; i < n_changed_ids; i++) {
+ struct pw_resource *resource;
+ int subscribed = 0;
+
+ /* first check if anyone is subscribed */
+ spa_list_for_each(resource, &node->global->resource_list, link) {
+ if ((subscribed = resource_is_subscribed(resource, changed_ids[i])))
+ break;
+ }
+ if (!subscribed)
+ continue;
+
+ if ((res = pw_impl_node_for_each_param(node, 1, changed_ids[i], 0, UINT32_MAX,
+ NULL, notify_param, node)) < 0) {
+ pw_log_error("%p: error %d (%s)", node, res, spa_strerror(res));
+ }
+ }
+}
+
+static void node_update_state(struct pw_impl_node *node, enum pw_node_state state, int res, char *error)
+{
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+ enum pw_node_state old = node->info.state;
+
+ switch (state) {
+ case PW_NODE_STATE_RUNNING:
+ pw_log_debug("%p: start node driving:%d driver:%d added:%d", node,
+ node->driving, node->driver, node->added);
+
+ if (res >= 0) {
+ pw_loop_invoke(node->data_loop, do_node_add, 1, NULL, 0, true, node);
+ }
+ if (node->driving && node->driver) {
+ res = spa_node_send_command(node->node,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start));
+ if (res < 0) {
+ state = PW_NODE_STATE_ERROR;
+ error = spa_aprintf("Start error: %s", spa_strerror(res));
+ pw_loop_invoke(node->data_loop, do_node_remove, 1, NULL, 0, true, node);
+ }
+ }
+ break;
+ case PW_NODE_STATE_IDLE:
+ case PW_NODE_STATE_SUSPENDED:
+ case PW_NODE_STATE_ERROR:
+ if (state != PW_NODE_STATE_IDLE || node->pause_on_idle)
+ pw_loop_invoke(node->data_loop, do_node_remove, 1, NULL, 0, true, node);
+ break;
+ default:
+ break;
+ }
+
+ free((char*)node->info.error);
+ node->info.error = error;
+ node->info.state = state;
+ impl->pending_state = state;
+
+ pw_log_debug("%p: (%s) %s -> %s (%s)", node, node->name,
+ pw_node_state_as_string(old), pw_node_state_as_string(state), error);
+
+ if (old == state)
+ return;
+
+ if (state == PW_NODE_STATE_ERROR) {
+ pw_log_error("(%s-%u) %s -> error (%s)", node->name, node->info.id,
+ pw_node_state_as_string(old), error);
+ } else {
+ pw_log_info("(%s-%u) %s -> %s", node->name, node->info.id,
+ pw_node_state_as_string(old), pw_node_state_as_string(state));
+ }
+ pw_impl_node_emit_state_changed(node, old, state, error);
+
+ node->info.change_mask |= PW_NODE_CHANGE_MASK_STATE;
+ emit_info_changed(node, false);
+
+ if (state == PW_NODE_STATE_ERROR && node->global) {
+ struct pw_resource *resource;
+ spa_list_for_each(resource, &node->global->resource_list, link)
+ pw_resource_error(resource, res, error);
+ }
+ if (node->reconfigure) {
+ if (state == PW_NODE_STATE_SUSPENDED &&
+ node->pause_on_idle) {
+ node->reconfigure = false;
+ }
+ if (state == PW_NODE_STATE_RUNNING)
+ node->reconfigure = false;
+ }
+ if (old == PW_NODE_STATE_RUNNING &&
+ state == PW_NODE_STATE_IDLE &&
+ node->suspend_on_idle) {
+ pw_impl_node_set_state(node, PW_NODE_STATE_SUSPENDED);
+ }
+}
+
+static int suspend_node(struct pw_impl_node *this)
+{
+ int res = 0;
+ struct pw_impl_port *p;
+
+ pw_log_debug("%p: suspend node state:%s", this,
+ pw_node_state_as_string(this->info.state));
+
+ if (this->info.state > 0 && this->info.state <= PW_NODE_STATE_SUSPENDED)
+ return 0;
+
+ node_deactivate(this);
+
+ spa_list_for_each(p, &this->input_ports, link) {
+ if ((res = pw_impl_port_set_param(p, SPA_PARAM_Format, 0, NULL)) < 0)
+ pw_log_warn("%p: error unset format input: %s",
+ this, spa_strerror(res));
+ /* force CONFIGURE in case of async */
+ p->state = PW_IMPL_PORT_STATE_CONFIGURE;
+ }
+
+ spa_list_for_each(p, &this->output_ports, link) {
+ if ((res = pw_impl_port_set_param(p, SPA_PARAM_Format, 0, NULL)) < 0)
+ pw_log_warn("%p: error unset format output: %s",
+ this, spa_strerror(res));
+ /* force CONFIGURE in case of async */
+ p->state = PW_IMPL_PORT_STATE_CONFIGURE;
+ }
+
+ pw_log_debug("%p: suspend node driving:%d driver:%d added:%d", this,
+ this->driving, this->driver, this->added);
+
+ res = spa_node_send_command(this->node,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Suspend));
+ if (res == -ENOTSUP)
+ res = spa_node_send_command(this->node,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause));
+ if (res < 0 && res != -EIO)
+ pw_log_warn("%p: suspend node error %s", this, spa_strerror(res));
+
+ node_update_state(this, PW_NODE_STATE_SUSPENDED, 0, NULL);
+
+ return res;
+}
+
+static void
+clear_info(struct pw_impl_node *this)
+{
+ free(this->name);
+ free((char*)this->info.error);
+}
+
+static int reply_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct resource_data *d = data;
+ pw_log_debug("%p: resource %p reply param %d", d->node, d->resource, seq);
+ pw_node_resource_param(d->resource, seq, id, index, next, param);
+ return 0;
+}
+
+static int node_enum_params(void *object, int seq, uint32_t id,
+ uint32_t index, uint32_t num, const struct spa_pod *filter)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ struct pw_impl_node *node = data->node;
+ int res;
+
+ pw_log_debug("%p: resource %p enum params seq:%d id:%d (%s) index:%u num:%u",
+ node, resource, seq, id,
+ spa_debug_type_find_name(spa_type_param, id), index, num);
+
+ if ((res = pw_impl_node_for_each_param(node, seq, id, index, num,
+ filter, reply_param, data)) < 0) {
+ pw_resource_errorf(resource, res,
+ "enum params id:%d (%s) failed", id,
+ spa_debug_type_find_name(spa_type_param, id));
+ }
+ return 0;
+}
+
+static int node_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug("%p: resource %p subscribe param id:%d (%s)",
+ data->node, resource, ids[i],
+ spa_debug_type_find_name(spa_type_param, ids[i]));
+ node_enum_params(data, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static void remove_busy_resource(struct resource_data *d)
+{
+ if (d->end != -1) {
+ spa_hook_remove(&d->listener);
+ d->end = -1;
+ pw_impl_client_set_busy(d->resource->client, false);
+ }
+}
+
+static void result_node_sync(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct resource_data *d = data;
+
+ pw_log_debug("%p: sync result %d %d (%d/%d)", d->node, res, seq, d->seq, d->end);
+ if (seq == d->end)
+ remove_busy_resource(d);
+}
+
+static int node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ struct pw_impl_node *node = data->node;
+ struct pw_impl_client *client = resource->client;
+ int res;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .result = result_node_sync,
+ };
+
+ pw_log_debug("%p: resource %p set param id:%d (%s) %08x", node, resource,
+ id, spa_debug_type_find_name(spa_type_param, id), flags);
+
+ res = spa_node_set_param(node->node, id, flags, param);
+
+ if (res < 0) {
+ pw_resource_errorf(resource, res,
+ "set param id:%d (%s) flags:%08x failed", id,
+ spa_debug_type_find_name(spa_type_param, id), flags);
+
+ } else if (SPA_RESULT_IS_ASYNC(res)) {
+ pw_impl_client_set_busy(client, true);
+ if (data->end == -1)
+ spa_node_add_listener(node->node, &data->listener,
+ &node_events, data);
+ data->seq = res;
+ data->end = spa_node_sync(node->node, res);
+ }
+ return 0;
+}
+
+static int node_send_command(void *object, const struct spa_command *command)
+{
+ struct resource_data *data = object;
+ struct pw_impl_node *node = data->node;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Suspend:
+ suspend_node(node);
+ break;
+ default:
+ spa_node_send_command(node->node, command);
+ break;
+ }
+ return 0;
+}
+
+static const struct pw_node_methods node_methods = {
+ PW_VERSION_NODE_METHODS,
+ .subscribe_params = node_subscribe_params,
+ .enum_params = node_enum_params,
+ .set_param = node_set_param,
+ .send_command = node_send_command
+};
+
+static void resource_destroy(void *data)
+{
+ struct resource_data *d = data;
+ remove_busy_resource(d);
+ spa_hook_remove(&d->resource_listener);
+ spa_hook_remove(&d->object_listener);
+}
+
+static void resource_pong(void *data, int seq)
+{
+ struct resource_data *d = data;
+ struct pw_resource *resource = d->resource;
+ pw_log_debug("%p: resource %p: got pong %d", d->node,
+ resource, seq);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy,
+ .pong = resource_pong,
+};
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct pw_impl_node *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, sizeof(*data));
+ if (resource == NULL)
+ goto error_resource;
+
+ data = pw_resource_get_user_data(resource);
+ data->node = this;
+ data->resource = resource;
+ data->end = -1;
+
+ pw_resource_add_listener(resource,
+ &data->resource_listener,
+ &resource_events, data);
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &node_methods, data);
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_NODE_CHANGE_MASK_ALL;
+ pw_node_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+
+ return 0;
+
+error_resource:
+ pw_log_error("%p: can't create node resource: %m", this);
+ return -errno;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_node *this = data;
+ spa_hook_remove(&this->global_listener);
+ this->global = NULL;
+ pw_impl_node_destroy(this);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+static inline void insert_driver(struct pw_context *context, struct pw_impl_node *node)
+{
+ struct pw_impl_node *n, *t;
+
+ spa_list_for_each_safe(n, t, &context->driver_list, driver_link) {
+ if (n->priority_driver < node->priority_driver)
+ break;
+ }
+ spa_list_append(&n->driver_link, &node->driver_link);
+}
+
+static void update_io(struct pw_impl_node *node)
+{
+ pw_log_debug("%p: id:%d", node, node->info.id);
+
+ if (spa_node_set_io(node->node,
+ SPA_IO_Position,
+ &node->rt.activation->position,
+ sizeof(struct spa_io_position)) >= 0) {
+ pw_log_debug("%p: set position %p", node, &node->rt.activation->position);
+ node->rt.position = &node->rt.activation->position;
+
+ node->current_rate = node->rt.position->clock.rate;
+ node->current_quantum = node->rt.position->clock.duration;
+ } else if (node->driver) {
+ pw_log_warn("%p: can't set position on driver", node);
+ }
+ if (spa_node_set_io(node->node,
+ SPA_IO_Clock,
+ &node->rt.activation->position.clock,
+ sizeof(struct spa_io_clock)) >= 0) {
+ pw_log_debug("%p: set clock %p", node, &node->rt.activation->position.clock);
+ node->rt.clock = &node->rt.activation->position.clock;
+ }
+}
+
+SPA_EXPORT
+int pw_impl_node_register(struct pw_impl_node *this,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_OBJECT_PATH,
+ PW_KEY_MODULE_ID,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_DEVICE_ID,
+ PW_KEY_PRIORITY_SESSION,
+ PW_KEY_PRIORITY_DRIVER,
+ PW_KEY_APP_NAME,
+ PW_KEY_NODE_DESCRIPTION,
+ PW_KEY_NODE_NAME,
+ PW_KEY_NODE_NICK,
+ PW_KEY_NODE_SESSION,
+ PW_KEY_MEDIA_CLASS,
+ PW_KEY_MEDIA_TYPE,
+ PW_KEY_MEDIA_CATEGORY,
+ PW_KEY_MEDIA_ROLE,
+ NULL
+ };
+
+ struct pw_context *context = this->context;
+ struct pw_impl_port *port;
+
+ pw_log_debug("%p: register", this);
+
+ if (this->registered)
+ goto error_existed;
+
+ this->global = pw_global_new(context,
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ properties,
+ global_bind,
+ this);
+ if (this->global == NULL)
+ return -errno;
+
+ spa_list_append(&context->node_list, &this->link);
+ if (this->driver)
+ insert_driver(context, this);
+ this->registered = true;
+
+ this->rt.activation->position.clock.id = this->global->id;
+
+ this->info.id = this->global->id;
+ pw_properties_setf(this->properties, PW_KEY_OBJECT_ID, "%d", this->info.id);
+ pw_properties_setf(this->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(this->global));
+ this->info.props = &this->properties->dict;
+
+ pw_global_update_keys(this->global, &this->properties->dict, keys);
+
+ pw_impl_node_initialized(this);
+
+ pw_global_add_listener(this->global, &this->global_listener, &global_events, this);
+ pw_global_register(this->global);
+
+ if (this->node)
+ update_io(this);
+
+ spa_list_for_each(port, &this->input_ports, link)
+ pw_impl_port_register(port, NULL);
+ spa_list_for_each(port, &this->output_ports, link)
+ pw_impl_port_register(port, NULL);
+
+ if (this->active)
+ pw_context_recalc_graph(context, "register active node");
+
+ return 0;
+
+error_existed:
+ pw_properties_free(properties);
+ return -EEXIST;
+}
+
+SPA_EXPORT
+int pw_impl_node_initialized(struct pw_impl_node *this)
+{
+ pw_log_debug("%p initialized", this);
+ pw_impl_node_emit_initialized(this);
+ node_update_state(this, PW_NODE_STATE_SUSPENDED, 0, NULL);
+ return 0;
+}
+
+static int
+do_move_nodes(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct impl *impl = user_data;
+ struct pw_impl_node *driver = *(struct pw_impl_node **)data;
+ struct pw_impl_node *node = &impl->this;
+
+ pw_log_trace("%p: driver:%p->%p", node, node->driver_node, driver);
+
+ pw_log_trace("%p: set position %p", node, &driver->rt.activation->position);
+ node->rt.position = &driver->rt.activation->position;
+
+ node->current_rate = node->rt.position->clock.rate;
+ node->current_quantum = node->rt.position->clock.duration;
+
+ if (node->source.loop != NULL) {
+ remove_node(node);
+ add_node(node, driver);
+ }
+ return 0;
+}
+
+static void remove_segment_owner(struct pw_impl_node *driver, uint32_t node_id)
+{
+ struct pw_node_activation *a = driver->rt.activation;
+ ATOMIC_CAS(a->segment_owner[0], node_id, 0);
+ ATOMIC_CAS(a->segment_owner[1], node_id, 0);
+}
+
+SPA_EXPORT
+int pw_impl_node_set_driver(struct pw_impl_node *node, struct pw_impl_node *driver)
+{
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+ struct pw_impl_node *old = node->driver_node;
+ int res;
+ bool was_driving;
+
+ if (driver == NULL)
+ driver = node;
+
+ spa_list_remove(&node->follower_link);
+ spa_list_append(&driver->follower_list, &node->follower_link);
+
+ if (old == driver)
+ return 0;
+
+ remove_segment_owner(old, node->info.id);
+
+ if (old != node && old->driving && driver->info.state < PW_NODE_STATE_RUNNING) {
+ driver->current_rate = old->current_rate;
+ driver->current_quantum = old->current_quantum;
+ driver->current_pending = true;
+ pw_log_info("move quantum:%"PRIu64" rate:%d (%s-%d -> %s-%d)",
+ driver->current_quantum,
+ driver->current_rate.denom,
+ old->name, old->info.id,
+ driver->name, driver->info.id);
+ }
+ was_driving = node->driving;
+ node->driving = node->driver && driver == node;
+
+ /* When a node was driver (and is waiting for all nodes to complete
+ * the Start command) cancel the pending state and let the new driver
+ * calculate a new state so that the Start command is sent to the
+ * node */
+ if (was_driving && !node->driving)
+ impl->pending_state = node->info.state;
+
+ pw_log_debug("%p: driver %p driving:%u", node,
+ driver, node->driving);
+ pw_log_info("(%s-%u) -> change driver (%s-%d -> %s-%d)",
+ node->name, node->info.id,
+ old->name, old->info.id, driver->name, driver->info.id);
+
+ node->driver_node = driver;
+ node->moved = true;
+
+ if ((res = spa_node_set_io(node->node,
+ SPA_IO_Position,
+ &driver->rt.activation->position,
+ sizeof(struct spa_io_position))) < 0) {
+ pw_log_debug("%p: set position: %s", node, spa_strerror(res));
+ }
+
+ pw_loop_invoke(node->data_loop,
+ do_move_nodes, SPA_ID_INVALID, &driver, sizeof(struct pw_impl_node *),
+ true, impl);
+
+ pw_impl_node_emit_driver_changed(node, old, driver);
+
+ return 0;
+}
+
+static void check_properties(struct pw_impl_node *node)
+{
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+ struct pw_context *context = node->context;
+ const char *str, *recalc_reason = NULL;
+ struct spa_fraction frac;
+ uint32_t value;
+ bool driver;
+
+ if ((str = pw_properties_get(node->properties, PW_KEY_PRIORITY_DRIVER))) {
+ node->priority_driver = pw_properties_parse_int(str);
+ pw_log_debug("%p: priority driver %d", node, node->priority_driver);
+ }
+
+ if ((str = pw_properties_get(node->properties, PW_KEY_NODE_NAME)) &&
+ (node->name == NULL || !spa_streq(node->name, str))) {
+ free(node->name);
+ node->name = strdup(str);
+ pw_log_debug("%p: name '%s'", node, node->name);
+ }
+
+ node->pause_on_idle = pw_properties_get_bool(node->properties, PW_KEY_NODE_PAUSE_ON_IDLE, true);
+ node->suspend_on_idle = pw_properties_get_bool(node->properties, PW_KEY_NODE_SUSPEND_ON_IDLE, false);
+ node->transport_sync = pw_properties_get_bool(node->properties, PW_KEY_NODE_TRANSPORT_SYNC, false);
+ impl->cache_params = pw_properties_get_bool(node->properties, PW_KEY_NODE_CACHE_PARAMS, true);
+ driver = pw_properties_get_bool(node->properties, PW_KEY_NODE_DRIVER, false);
+
+ if (node->driver != driver) {
+ pw_log_debug("%p: driver %d -> %d", node, node->driver, driver);
+ node->driver = driver;
+ if (node->registered) {
+ if (driver)
+ insert_driver(context, node);
+ else
+ spa_list_remove(&node->driver_link);
+ }
+ recalc_reason = "driver changed";
+ }
+
+ /* not scheduled automatically so we add an additional required trigger */
+ if (pw_properties_get_bool(node->properties, PW_KEY_NODE_TRIGGER, false))
+ node->rt.activation->state[0].required++;
+
+ /* group defines what nodes are scheduled together */
+ if ((str = pw_properties_get(node->properties, PW_KEY_NODE_GROUP)) == NULL)
+ str = "";
+
+ if (!spa_streq(str, node->group)) {
+ pw_log_info("%p: group '%s'->'%s'", node, node->group, str);
+ snprintf(node->group, sizeof(node->group), "%s", str);
+ node->freewheel = spa_streq(node->group, "pipewire.freewheel");
+ recalc_reason = "group changed";
+ }
+
+
+ node->want_driver = pw_properties_get_bool(node->properties, PW_KEY_NODE_WANT_DRIVER, false);
+ node->always_process = pw_properties_get_bool(node->properties, PW_KEY_NODE_ALWAYS_PROCESS, false);
+
+ if (node->always_process)
+ node->want_driver = true;
+
+ if ((str = pw_properties_get(node->properties, PW_KEY_NODE_LATENCY))) {
+ if (sscanf(str, "%u/%u", &frac.num, &frac.denom) == 2 && frac.denom != 0) {
+ if (node->latency.num != frac.num || node->latency.denom != frac.denom) {
+ pw_log_info("(%s-%u) latency:%u/%u -> %u/%u", node->name,
+ node->info.id, node->latency.num,
+ node->latency.denom, frac.num, frac.denom);
+ node->latency = frac;
+ recalc_reason = "quantum changed";
+ }
+ }
+ }
+ if ((str = pw_properties_get(node->properties, PW_KEY_NODE_MAX_LATENCY))) {
+ if (sscanf(str, "%u/%u", &frac.num, &frac.denom) == 2 && frac.denom != 0) {
+ if (node->max_latency.num != frac.num || node->max_latency.denom != frac.denom) {
+ pw_log_info("(%s-%u) max-latency:%u/%u -> %u/%u", node->name,
+ node->info.id, node->max_latency.num,
+ node->max_latency.denom, frac.num, frac.denom);
+ node->max_latency = frac;
+ recalc_reason = "max quantum changed";
+ }
+ }
+ }
+ 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";
+ }
+ }
+
+ if ((str = pw_properties_get(node->properties, PW_KEY_NODE_RATE))) {
+ if (sscanf(str, "%u/%u", &frac.num, &frac.denom) == 2 && frac.denom != 0) {
+ if (node->rate.num != frac.num || node->rate.denom != frac.denom) {
+ pw_log_info("(%s-%u) rate:%u/%u -> %u/%u", node->name,
+ node->info.id, node->rate.num,
+ node->rate.denom, frac.num, frac.denom);
+ node->rate = frac;
+ recalc_reason = "node rate changed";
+ }
+ }
+ }
+ 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) &&
+ 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,
+ recalc_reason, node->active);
+
+ if (recalc_reason != NULL && node->active)
+ pw_context_recalc_graph(context, recalc_reason);
+}
+
+static const char *str_status(uint32_t status)
+{
+ switch (status) {
+ case PW_NODE_ACTIVATION_NOT_TRIGGERED:
+ return "not-triggered";
+ case PW_NODE_ACTIVATION_TRIGGERED:
+ return "triggered";
+ case PW_NODE_ACTIVATION_AWAKE:
+ return "awake";
+ case PW_NODE_ACTIVATION_FINISHED:
+ return "finished";
+ }
+ return "unknown";
+}
+
+static void dump_states(struct pw_impl_node *driver)
+{
+ struct pw_node_target *t;
+ struct pw_node_activation *na = driver->rt.activation;
+ struct spa_io_clock *cl = &na->position.clock;
+
+ spa_list_for_each(t, &driver->rt.target_list, link) {
+ struct pw_node_activation *a = t->activation;
+ struct pw_node_activation_state *state = &a->state[0];
+ if (t->node == NULL)
+ continue;
+ if (a->status == PW_NODE_ACTIVATION_TRIGGERED ||
+ a->status == PW_NODE_ACTIVATION_AWAKE) {
+ pw_log_info("(%s-%u) client too slow! rate:%u/%u pos:%"PRIu64" status:%s",
+ t->node->name, t->node->info.id,
+ (uint32_t)(cl->rate.num * cl->duration), cl->rate.denom,
+ cl->position, str_status(a->status));
+ }
+ pw_log_debug("(%s-%u) state:%p pending:%d/%d s:%"PRIu64" a:%"PRIu64" f:%"PRIu64
+ " waiting:%"PRIu64" process:%"PRIu64" status:%s sync:%d",
+ t->node->name, t->node->info.id, state,
+ state->pending, state->required,
+ a->signal_time,
+ a->awake_time,
+ a->finish_time,
+ a->awake_time - a->signal_time,
+ a->finish_time - a->awake_time,
+ str_status(a->status), a->pending_sync);
+ }
+}
+
+static inline int resume_node(struct pw_impl_node *this, int status)
+{
+ struct pw_node_target *t;
+ struct timespec ts;
+ struct pw_node_activation *activation = this->rt.activation;
+ struct spa_system *data_system = this->context->data_system;
+ uint64_t nsec;
+
+ spa_system_clock_gettime(data_system, CLOCK_MONOTONIC, &ts);
+ nsec = SPA_TIMESPEC_TO_NSEC(&ts);
+ activation->status = PW_NODE_ACTIVATION_FINISHED;
+ activation->finish_time = nsec;
+
+ pw_log_trace_fp("%p: trigger peers %"PRIu64, this, nsec);
+
+ spa_list_for_each(t, &this->rt.target_list, link) {
+ struct pw_node_activation *a = t->activation;
+ struct pw_node_activation_state *state = &a->state[0];
+
+ pw_log_trace_fp("%p: state:%p pending:%d/%d", t->node, state,
+ state->pending, state->required);
+
+ if (pw_node_activation_state_dec(state, 1)) {
+ a->status = PW_NODE_ACTIVATION_TRIGGERED;
+ a->signal_time = nsec;
+ t->signal_func(t->data);
+ }
+ }
+ return 0;
+}
+
+static inline void calculate_stats(struct pw_impl_node *this, struct pw_node_activation *a)
+{
+ if (SPA_LIKELY(a->signal_time > a->prev_signal_time)) {
+ uint64_t process_time = a->finish_time - a->signal_time;
+ uint64_t period_time = a->signal_time - a->prev_signal_time;
+ float load = (float) process_time / (float) period_time;
+ a->cpu_load[0] = (a->cpu_load[0] + load) / 2.0f;
+ a->cpu_load[1] = (a->cpu_load[1] * 7.0f + load) / 8.0f;
+ a->cpu_load[2] = (a->cpu_load[2] * 31.0f + load) / 32.0f;
+ }
+}
+
+static inline int process_node(void *data)
+{
+ struct pw_impl_node *this = data;
+ struct timespec ts;
+ struct pw_impl_port *p;
+ struct pw_node_activation *a = this->rt.activation;
+ struct spa_system *data_system = this->context->data_system;
+ int status;
+
+ spa_system_clock_gettime(data_system, CLOCK_MONOTONIC, &ts);
+ a->status = PW_NODE_ACTIVATION_AWAKE;
+ a->awake_time = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ pw_log_trace_fp("%p: process %"PRIu64, this, a->awake_time);
+
+ /* when transport sync is not supported, just clear the flag */
+ if (!this->transport_sync)
+ a->pending_sync = false;
+
+ if (this->added) {
+ spa_list_for_each(p, &this->rt.input_mix, rt.node_link)
+ spa_node_process(p->mix);
+
+ status = spa_node_process(this->node);
+
+ if (status & SPA_STATUS_HAVE_DATA) {
+ spa_list_for_each(p, &this->rt.output_mix, rt.node_link)
+ spa_node_process(p->mix);
+ }
+ } else {
+ /* This can happen when we deactivated the node but some links are
+ * still not shut down. We simply don't schedule the node and make
+ * sure we trigger the peers in resume_node below. */
+ pw_log_debug("%p: scheduling non-active node %s", this, this->name);
+ status = SPA_STATUS_HAVE_DATA;
+ }
+ a->state[0].status = status;
+
+ if (SPA_UNLIKELY(this == this->driver_node && !this->exported)) {
+ spa_system_clock_gettime(data_system, CLOCK_MONOTONIC, &ts);
+ a->status = PW_NODE_ACTIVATION_FINISHED;
+ a->signal_time = a->finish_time;
+ a->finish_time = SPA_TIMESPEC_TO_NSEC(&ts);
+
+ /* calculate CPU time */
+ calculate_stats(this, a);
+
+ pw_log_trace_fp("%p: graph completed wait:%"PRIu64" run:%"PRIu64
+ " busy:%"PRIu64" period:%"PRIu64" cpu:%f:%f:%f", this,
+ a->awake_time - a->signal_time,
+ a->finish_time - a->awake_time,
+ a->finish_time - a->signal_time,
+ a->signal_time - a->prev_signal_time,
+ a->cpu_load[0], a->cpu_load[1], a->cpu_load[2]);
+
+ pw_context_driver_emit_complete(this->context, this);
+
+ } else if (status == SPA_STATUS_OK) {
+ pw_log_trace_fp("%p: async continue", this);
+ } else {
+ resume_node(this, status);
+ }
+ if (status & SPA_STATUS_DRAINED) {
+ pw_context_driver_emit_drained(this->context, this);
+ }
+ return 0;
+}
+
+static void node_on_fd_events(struct spa_source *source)
+{
+ struct pw_impl_node *this = source->data;
+ struct spa_system *data_system = this->context->data_system;
+
+ if (SPA_UNLIKELY(source->rmask & (SPA_IO_ERR | SPA_IO_HUP))) {
+ pw_log_warn("%p: got socket error %08x", this, source->rmask);
+ return;
+ }
+
+ if (SPA_LIKELY(source->rmask & SPA_IO_IN)) {
+ uint64_t cmd;
+
+ if (SPA_UNLIKELY(spa_system_eventfd_read(data_system, this->source.fd, &cmd) < 0))
+ pw_log_warn("%p: read failed %m", this);
+ else if (SPA_UNLIKELY(cmd > 1))
+ pw_log_info("(%s-%u) client missed %"PRIu64" wakeups",
+ this->name, this->info.id, cmd - 1);
+
+ pw_log_trace_fp("%p: got process", this);
+ this->rt.target.signal_func(this->rt.target.data);
+ }
+}
+
+static void reset_segment(struct spa_io_segment *seg)
+{
+ spa_zero(*seg);
+ seg->rate = 1.0;
+}
+
+static void reset_position(struct pw_impl_node *this, struct spa_io_position *pos)
+{
+ uint32_t i;
+ struct settings *s = &this->context->settings;
+ uint32_t quantum = s->clock_force_quantum == 0 ? s->clock_quantum : s->clock_force_quantum;
+ uint32_t rate = s->clock_force_rate == 0 ? s->clock_rate : s->clock_force_rate;
+
+ this->current_rate = SPA_FRACTION(1, rate);
+ this->current_quantum = quantum;
+
+ pos->clock.rate = this->current_rate;
+ pos->clock.duration = this->current_quantum;
+ pos->video.flags = SPA_IO_VIDEO_SIZE_VALID;
+ pos->video.size = s->video_size;
+ pos->video.stride = pos->video.size.width * 16;
+ pos->video.framerate = s->video_rate;
+ pos->offset = INT64_MIN;
+
+ pos->n_segments = 1;
+ for (i = 0; i < SPA_IO_POSITION_MAX_SEGMENTS; i++)
+ reset_segment(&pos->segments[i]);
+}
+
+SPA_EXPORT
+struct pw_impl_node *pw_context_create_node(struct pw_context *context,
+ struct pw_properties *properties,
+ size_t user_data_size)
+{
+ struct impl *impl;
+ struct pw_impl_node *this;
+ size_t size;
+ struct spa_system *data_system = context->data_system;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl) + user_data_size);
+ if (impl == NULL) {
+ res = -errno;
+ goto error_exit;
+ }
+
+ spa_list_init(&impl->param_list);
+ spa_list_init(&impl->pending_list);
+
+ this = &impl->this;
+ this->context = context;
+ this->name = strdup("node");
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ if (properties == NULL)
+ properties = pw_properties_new(NULL, NULL);
+ if (properties == NULL) {
+ res = -errno;
+ goto error_clean;
+ }
+
+ this->properties = properties;
+
+ if ((res = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_clean;
+
+ pw_log_debug("%p: new fd:%d", this, res);
+
+ this->source.fd = res;
+ this->source.func = node_on_fd_events;
+ this->source.data = this;
+ this->source.mask = SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP;
+ this->source.rmask = 0;
+
+ size = sizeof(struct pw_node_activation);
+
+ this->activation = pw_mempool_alloc(this->context->pool,
+ PW_MEMBLOCK_FLAG_READWRITE |
+ PW_MEMBLOCK_FLAG_SEAL |
+ PW_MEMBLOCK_FLAG_MAP,
+ SPA_DATA_MemFd, size);
+ if (this->activation == NULL) {
+ res = -errno;
+ goto error_clean;
+ }
+
+ impl->work = pw_context_get_work_queue(this->context);
+ impl->pending_id = SPA_ID_INVALID;
+
+ this->data_loop = context->data_loop;
+
+ spa_list_init(&this->follower_list);
+
+ spa_hook_list_init(&this->listener_list);
+
+ this->info.state = PW_NODE_STATE_CREATING;
+ this->info.props = &this->properties->dict;
+ this->info.params = this->params;
+
+ spa_list_init(&this->input_ports);
+ pw_map_init(&this->input_port_map, 64, 64);
+ spa_list_init(&this->output_ports);
+ pw_map_init(&this->output_port_map, 64, 64);
+
+ spa_list_init(&this->rt.input_mix);
+ spa_list_init(&this->rt.output_mix);
+ spa_list_init(&this->rt.target_list);
+
+ this->rt.activation = this->activation->map->ptr;
+ this->rt.target.activation = this->rt.activation;
+ this->rt.target.node = this;
+ this->rt.target.signal_func = process_node;
+ this->rt.target.data = this;
+ this->rt.driver_target.signal_func = process_node;
+
+ reset_position(this, &this->rt.activation->position);
+ this->rt.activation->sync_timeout = DEFAULT_SYNC_TIMEOUT;
+ this->rt.activation->sync_left = 0;
+
+ this->rt.rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+ this->rt.rate_limit.burst = 1;
+
+ check_properties(this);
+
+ this->driver_node = this;
+ spa_list_append(&this->follower_list, &this->follower_link);
+ this->driving = true;
+
+ return this;
+
+error_clean:
+ if (this->activation)
+ pw_memblock_unref(this->activation);
+ if (this->source.fd != -1)
+ spa_system_close(this->context->data_system, this->source.fd);
+ free(impl);
+error_exit:
+ pw_properties_free(properties);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+const struct pw_node_info *pw_impl_node_get_info(struct pw_impl_node *node)
+{
+ return &node->info;
+}
+
+SPA_EXPORT
+void * pw_impl_node_get_user_data(struct pw_impl_node *node)
+{
+ return node->user_data;
+}
+
+SPA_EXPORT
+struct pw_context * pw_impl_node_get_context(struct pw_impl_node *node)
+{
+ return node->context;
+}
+
+SPA_EXPORT
+struct pw_global *pw_impl_node_get_global(struct pw_impl_node *node)
+{
+ return node->global;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_impl_node_get_properties(struct pw_impl_node *node)
+{
+ return node->properties;
+}
+
+static int update_properties(struct pw_impl_node *node, const struct spa_dict *dict, bool filter)
+{
+ static const char * const ignored[] = {
+ PW_KEY_OBJECT_ID,
+ PW_KEY_MODULE_ID,
+ PW_KEY_FACTORY_ID,
+ PW_KEY_CLIENT_ID,
+ PW_KEY_DEVICE_ID,
+ NULL
+ };
+
+ int changed;
+
+ changed = pw_properties_update_ignore(node->properties, dict, filter ? ignored : NULL);
+ node->info.props = &node->properties->dict;
+
+ pw_log_debug("%p: updated %d properties", node, changed);
+
+ if (changed) {
+ check_properties(node);
+ node->info.change_mask |= PW_NODE_CHANGE_MASK_PROPS;
+ }
+ return changed;
+}
+
+SPA_EXPORT
+int pw_impl_node_update_properties(struct pw_impl_node *node, const struct spa_dict *dict)
+{
+ int changed = update_properties(node, dict, false);
+ emit_info_changed(node, false);
+ return changed;
+}
+
+static void node_info(void *data, const struct spa_node_info *info)
+{
+ struct pw_impl_node *node = data;
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+ bool flags_changed = false;
+
+ node->info.max_input_ports = info->max_input_ports;
+ node->info.max_output_ports = info->max_output_ports;
+
+ pw_log_debug("%p: flags:%08"PRIx64" change_mask:%08"PRIx64" max_in:%u max_out:%u",
+ node, info->flags, info->change_mask, info->max_input_ports,
+ info->max_output_ports);
+
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_FLAGS) {
+ if (node->spa_flags != info->flags) {
+ flags_changed = node->spa_flags != 0;
+ pw_log_debug("%p: flags %"PRIu64"->%"PRIu64, node, node->spa_flags, info->flags);
+ node->spa_flags = info->flags;
+ }
+ }
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PROPS) {
+ update_properties(node, info->props, true);
+ }
+ if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ uint32_t i;
+
+ node->info.change_mask |= PW_NODE_CHANGE_MASK_PARAMS;
+ node->info.n_params = SPA_MIN(info->n_params, SPA_N_ELEMENTS(node->params));
+
+ for (i = 0; i < node->info.n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ pw_log_debug("%p: param %d id:%d (%s) %08x:%08x", node, i,
+ id, spa_debug_type_find_name(spa_type_param, id),
+ node->info.params[i].flags, info->params[i].flags);
+
+ node->info.params[i].id = info->params[i].id;
+ if (node->info.params[i].flags == info->params[i].flags)
+ continue;
+
+ pw_log_debug("%p: update param %d", node, id);
+ node->info.params[i] = info->params[i];
+ node->info.params[i].user = 0;
+
+ if (info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = id;
+ }
+ }
+ emit_info_changed(node, flags_changed);
+
+ if (n_changed_ids > 0)
+ emit_params(node, changed_ids, n_changed_ids);
+
+ if (flags_changed)
+ pw_context_recalc_graph(node->context, "node flags changed");
+}
+
+static void node_port_info(void *data, enum spa_direction direction, uint32_t port_id,
+ const struct spa_port_info *info)
+{
+ struct pw_impl_node *node = data;
+ struct pw_impl_port *port = pw_impl_node_find_port(node, direction, port_id);
+
+ if (info == NULL) {
+ if (port) {
+ pw_log_debug("%p: %s port %d removed", node,
+ pw_direction_as_string(direction), port_id);
+ pw_impl_port_destroy(port);
+ } else {
+ pw_log_warn("%p: %s port %d unknown", node,
+ pw_direction_as_string(direction), port_id);
+ }
+ } else if (port) {
+ pw_log_debug("%p: %s port %d changed", node,
+ pw_direction_as_string(direction), port_id);
+ pw_impl_port_update_info(port, info);
+ } else {
+ int res;
+
+ pw_log_debug("%p: %s port %d added", node,
+ pw_direction_as_string(direction), port_id);
+
+ if ((port = pw_context_create_port(node->context, direction, port_id, info,
+ node->port_user_data_size))) {
+ if ((res = pw_impl_port_add(port, node)) < 0) {
+ pw_log_error("%p: can't add port %p: %d, %s",
+ node, port, res, spa_strerror(res));
+ pw_impl_port_destroy(port);
+ }
+ }
+ }
+}
+
+static void node_result(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct pw_impl_node *node = data;
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+
+ pw_log_trace("%p: result seq:%d res:%d type:%u", node, seq, res, type);
+ if (res < 0)
+ impl->last_error = res;
+
+ if (SPA_RESULT_IS_ASYNC(seq))
+ pw_work_queue_complete(impl->work, &impl->this, SPA_RESULT_ASYNC_SEQ(seq), res);
+
+ pw_impl_node_emit_result(node, seq, res, type, result);
+}
+
+static void node_event(void *data, const struct spa_event *event)
+{
+ struct pw_impl_node *node = data;
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+
+ pw_log_trace("%p: event %d", node, SPA_EVENT_TYPE(event));
+
+ switch (SPA_NODE_EVENT_ID(event)) {
+ case SPA_NODE_EVENT_Error:
+ impl->last_error = -EFAULT;
+ node_update_state(node, PW_NODE_STATE_ERROR,
+ -EFAULT, strdup("Received error event"));
+ break;
+ case SPA_NODE_EVENT_RequestProcess:
+ pw_log_debug("request process");
+ if (!node->driving) {
+ pw_impl_node_send_command(node->driver_node,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_RequestProcess));
+ }
+ break;
+ default:
+ pw_log_debug("unhandled event %d", SPA_NODE_EVENT_ID(event));
+ break;
+ }
+ pw_impl_node_emit_event(node, event);
+}
+
+static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .info = node_info,
+ .port_info = node_port_info,
+ .result = node_result,
+ .event = node_event,
+};
+
+#define SYNC_CHECK 0
+#define SYNC_START 1
+#define SYNC_STOP 2
+
+static int check_updates(struct pw_impl_node *node, uint32_t *reposition_owner)
+{
+ int res = SYNC_CHECK;
+ struct pw_node_activation *a = node->rt.activation;
+ uint32_t command;
+
+ if (SPA_UNLIKELY(a->position.offset == INT64_MIN))
+ a->position.offset = a->position.clock.position;
+
+ command = ATOMIC_XCHG(a->command, PW_NODE_ACTIVATION_COMMAND_NONE);
+ *reposition_owner = ATOMIC_XCHG(a->reposition_owner, 0);
+
+ if (SPA_UNLIKELY(command != PW_NODE_ACTIVATION_COMMAND_NONE)) {
+ pw_log_debug("%p: update command:%u", node, command);
+ switch (command) {
+ case PW_NODE_ACTIVATION_COMMAND_STOP:
+ a->position.state = SPA_IO_POSITION_STATE_STOPPED;
+ res = SYNC_STOP;
+ break;
+ case PW_NODE_ACTIVATION_COMMAND_START:
+ a->position.state = SPA_IO_POSITION_STATE_STARTING;
+ a->sync_left = a->sync_timeout /
+ ((a->position.clock.duration * SPA_NSEC_PER_SEC) /
+ a->position.clock.rate.denom);
+ res = SYNC_START;
+ break;
+ }
+ }
+ return res;
+}
+
+static void do_reposition(struct pw_impl_node *driver, struct pw_impl_node *node)
+{
+ struct pw_node_activation *a = driver->rt.activation;
+ struct spa_io_segment *dst, *src;
+
+ src = &node->rt.activation->reposition;
+ dst = &a->position.segments[0];
+
+ pw_log_info("%p: update position:%"PRIu64, node, src->position);
+
+ dst->version = src->version;
+ dst->flags = src->flags;
+ dst->start = src->start;
+ dst->duration = src->duration;
+ dst->rate = src->rate;
+ dst->position = src->position;
+ if (src->bar.flags & SPA_IO_SEGMENT_BAR_FLAG_VALID)
+ dst->bar = src->bar;
+ if (src->video.flags & SPA_IO_SEGMENT_VIDEO_FLAG_VALID)
+ dst->video = src->video;
+
+ if (dst->start == 0)
+ dst->start = a->position.clock.position - a->position.offset;
+
+ switch (a->position.state) {
+ case SPA_IO_POSITION_STATE_RUNNING:
+ a->position.state = SPA_IO_POSITION_STATE_STARTING;
+ a->sync_left = a->sync_timeout /
+ ((a->position.clock.duration * SPA_NSEC_PER_SEC) /
+ a->position.clock.rate.denom);
+ break;
+ }
+}
+
+static void update_position(struct pw_impl_node *node, int all_ready)
+{
+ struct pw_node_activation *a = node->rt.activation;
+
+ if (a->position.state == SPA_IO_POSITION_STATE_STARTING) {
+ if (!all_ready && --a->sync_left == 0) {
+ pw_log_warn("(%s-%u) sync timeout, going to RUNNING",
+ node->name, node->info.id);
+ pw_context_driver_emit_timeout(node->context, node);
+ dump_states(node);
+ all_ready = true;
+ }
+ if (all_ready)
+ a->position.state = SPA_IO_POSITION_STATE_RUNNING;
+ }
+ if (a->position.state != SPA_IO_POSITION_STATE_RUNNING)
+ a->position.offset += a->position.clock.duration;
+}
+
+static int node_ready(void *data, int status)
+{
+ struct pw_impl_node *node = data, *reposition_node = NULL;
+ struct pw_impl_node *driver = node->driver_node;
+ struct pw_node_target *t;
+ struct pw_impl_port *p;
+
+ pw_log_trace_fp("%p: ready driver:%d exported:%d %p status:%d added:%d", node,
+ node->driver, node->exported, driver, status, node->added);
+
+ if (!node->added) {
+ /* This can happen when we are stopping a node and removed it from the
+ * graph but we still have not completed the Pause/Suspend command on
+ * the node. In that case, the node might still emit ready events,
+ * which we should simply ignore here. */
+ pw_log_info("%p: ready non-active node %s in state %d", node, node->name, node->info.state);
+ return -EIO;
+ }
+
+ if (SPA_UNLIKELY(node == driver)) {
+ struct pw_node_activation *a = node->rt.activation;
+ struct pw_node_activation_state *state = &a->state[0];
+ int sync_type, all_ready, update_sync, target_sync;
+ uint32_t owner[2], reposition_owner;
+ uint64_t min_timeout = UINT64_MAX;
+
+ if (SPA_UNLIKELY(state->pending > 0)) {
+ pw_context_driver_emit_incomplete(node->context, node);
+ if (ratelimit_test(&node->rt.rate_limit, a->signal_time, SPA_LOG_LEVEL_DEBUG)) {
+ pw_log_debug("(%s-%u) graph not finished: state:%p quantum:%"PRIu64
+ " pending %d/%d", node->name, node->info.id,
+ state, a->position.clock.duration,
+ state->pending, state->required);
+ dump_states(node);
+ }
+ node->rt.target.signal_func(node->rt.target.data);
+ }
+
+ if (node->current_pending) {
+ node->rt.position->clock.duration = node->current_quantum;
+ node->rt.position->clock.rate = node->current_rate;
+ node->current_pending = false;
+ }
+
+ sync_type = check_updates(node, &reposition_owner);
+ owner[0] = ATOMIC_LOAD(a->segment_owner[0]);
+ owner[1] = ATOMIC_LOAD(a->segment_owner[1]);
+again:
+ all_ready = sync_type == SYNC_CHECK;
+ update_sync = !all_ready;
+ target_sync = sync_type == SYNC_START ? true : false;
+
+ spa_list_for_each(t, &driver->rt.target_list, link) {
+ struct pw_node_activation *ta = t->activation;
+
+ ta->status = PW_NODE_ACTIVATION_NOT_TRIGGERED;
+ pw_node_activation_state_reset(&ta->state[0]);
+
+ if (SPA_LIKELY(t->node)) {
+ uint32_t id = t->node->info.id;
+
+ /* this is the node with reposition info */
+ if (SPA_UNLIKELY(id == reposition_owner))
+ reposition_node = t->node;
+
+ /* update extra segment info if it is the owner */
+ if (SPA_UNLIKELY(id == owner[0]))
+ a->position.segments[0].bar = ta->segment.bar;
+ if (SPA_UNLIKELY(id == owner[1]))
+ a->position.segments[0].video = ta->segment.video;
+
+ min_timeout = SPA_MIN(min_timeout, ta->sync_timeout);
+ }
+
+ if (SPA_UNLIKELY(update_sync)) {
+ ta->pending_sync = target_sync;
+ ta->pending_new_pos = target_sync;
+ } else {
+ all_ready &= ta->pending_sync == false;
+ }
+ }
+ a->prev_signal_time = a->signal_time;
+ a->sync_timeout = SPA_MIN(min_timeout, DEFAULT_SYNC_TIMEOUT);
+
+ if (SPA_UNLIKELY(reposition_node)) {
+ do_reposition(node, reposition_node);
+ sync_type = SYNC_START;
+ reposition_owner = 0;
+ reposition_node = NULL;
+ goto again;
+ }
+
+ update_position(node, all_ready);
+
+ pw_context_driver_emit_start(node->context, node);
+ }
+ if (SPA_UNLIKELY(node->driver && !node->driving))
+ return 0;
+
+ if (!node->driver) {
+ struct timespec ts;
+ struct pw_node_activation *a = node->rt.activation;
+ struct spa_system *data_system = node->context->data_system;
+
+ spa_system_clock_gettime(data_system, CLOCK_MONOTONIC, &ts);
+ a->status = PW_NODE_ACTIVATION_AWAKE;
+ a->signal_time = a->awake_time = SPA_TIMESPEC_TO_NSEC(&ts);
+ }
+
+ if (status & SPA_STATUS_HAVE_DATA) {
+ spa_list_for_each(p, &node->rt.output_mix, rt.node_link)
+ spa_node_process(p->mix);
+ }
+ return resume_node(node, status);
+}
+
+static int node_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_id)
+{
+ struct pw_impl_node *node = data;
+ struct pw_impl_port *p;
+
+ spa_list_for_each(p, &node->rt.input_mix, rt.node_link) {
+ if (p->port_id != port_id)
+ continue;
+ spa_node_port_reuse_buffer(p->mix, 0, buffer_id);
+ break;
+ }
+ return 0;
+}
+
+static void update_xrun_stats(struct pw_node_activation *a, uint64_t trigger, uint64_t delay)
+{
+ a->xrun_count++;
+ a->xrun_time = trigger;
+ a->xrun_delay = delay;
+ a->max_delay = SPA_MAX(a->max_delay, delay);
+}
+
+static int node_xrun(void *data, uint64_t trigger, uint64_t delay, struct spa_pod *info)
+{
+ struct pw_impl_node *this = data;
+ struct pw_node_activation *a = this->rt.activation;
+ struct pw_node_activation *da = this->rt.driver_target.activation;
+
+ update_xrun_stats(a, trigger, delay);
+ if (da && da != a)
+ update_xrun_stats(da, trigger, delay);
+
+ if (ratelimit_test(&this->rt.rate_limit, a->signal_time, SPA_LOG_LEVEL_INFO)) {
+ struct spa_fraction rate;
+ if (da) {
+ struct spa_io_clock *cl = &da->position.clock;
+ rate.num = cl->rate.num * cl->duration;
+ rate.denom = cl->rate.denom;
+ } else {
+ rate = SPA_FRACTION(0,0);
+ }
+ pw_log_info("(%s-%d) XRun! rate:%u/%u count:%u time:%"PRIu64
+ " delay:%"PRIu64" max:%"PRIu64,
+ this->name, this->info.id,
+ rate.num, rate.denom, a->xrun_count,
+ trigger, delay, a->max_delay);
+ }
+
+ pw_context_driver_emit_xrun(this->context, this);
+
+ return 0;
+}
+
+static const struct spa_node_callbacks node_callbacks = {
+ SPA_VERSION_NODE_CALLBACKS,
+ .ready = node_ready,
+ .reuse_buffer = node_reuse_buffer,
+ .xrun = node_xrun,
+};
+
+SPA_EXPORT
+int pw_impl_node_set_implementation(struct pw_impl_node *node,
+ struct spa_node *spa_node)
+{
+ int res;
+
+ pw_log_debug("%p: implementation %p", node, spa_node);
+
+ if (node->node) {
+ pw_log_error("%p: implementation existed %p", node, node->node);
+ return -EEXIST;
+ }
+
+ node->node = spa_node;
+ spa_node_set_callbacks(node->node, &node_callbacks, node);
+ res = spa_node_add_listener(node->node, &node->listener, &node_events, node);
+
+ if (node->registered)
+ update_io(node);
+
+ return res;
+}
+
+SPA_EXPORT
+struct spa_node *pw_impl_node_get_implementation(struct pw_impl_node *node)
+{
+ return node->node;
+}
+
+SPA_EXPORT
+void pw_impl_node_add_listener(struct pw_impl_node *node,
+ struct spa_hook *listener,
+ const struct pw_impl_node_events *events,
+ void *data)
+{
+ spa_hook_list_append(&node->listener_list, listener, events, data);
+}
+
+/** Destroy a node
+ * \param node a node to destroy
+ *
+ * Remove \a node. This will stop the transfer on the node and
+ * free the resources allocated by \a node.
+ */
+SPA_EXPORT
+void pw_impl_node_destroy(struct pw_impl_node *node)
+{
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+ struct pw_impl_port *port;
+ struct pw_impl_node *follower;
+ struct pw_context *context = node->context;
+ bool active, had_driver;
+
+ active = node->active;
+ node->active = false;
+
+ pw_log_debug("%p: destroy", impl);
+ pw_log_info("(%s-%u) destroy", node->name, node->info.id);
+
+ node_deactivate(node);
+
+ suspend_node(node);
+
+ pw_impl_node_emit_destroy(node);
+
+ pw_log_debug("%p: driver node %p", impl, node->driver_node);
+ had_driver = node != node->driver_node;
+
+ /* remove ourself as a follower from the driver node */
+ spa_list_remove(&node->follower_link);
+ remove_segment_owner(node->driver_node, node->info.id);
+
+ spa_list_consume(follower, &node->follower_list, follower_link) {
+ pw_log_debug("%p: reassign follower %p", impl, follower);
+ pw_impl_node_set_driver(follower, NULL);
+ }
+
+ if (node->registered) {
+ spa_list_remove(&node->link);
+ if (node->driver)
+ spa_list_remove(&node->driver_link);
+ }
+
+ if (node->node) {
+ spa_hook_remove(&node->listener);
+ spa_node_set_callbacks(node->node, NULL, NULL);
+ }
+
+ pw_log_debug("%p: destroy ports", node);
+ spa_list_consume(port, &node->input_ports, link)
+ pw_impl_port_destroy(port);
+ spa_list_consume(port, &node->output_ports, link)
+ pw_impl_port_destroy(port);
+
+ if (node->global) {
+ spa_hook_remove(&node->global_listener);
+ pw_global_destroy(node->global);
+ }
+
+ if (active || had_driver)
+ pw_context_recalc_graph(context,
+ "active node destroy");
+
+ pw_log_debug("%p: free", node);
+ pw_impl_node_emit_free(node);
+
+ spa_hook_list_clean(&node->listener_list);
+
+ pw_memblock_unref(node->activation);
+
+ pw_param_clear(&impl->param_list, SPA_ID_INVALID);
+ pw_param_clear(&impl->pending_list, SPA_ID_INVALID);
+
+ pw_map_clear(&node->input_port_map);
+ pw_map_clear(&node->output_port_map);
+
+ pw_work_queue_cancel(impl->work, node, SPA_ID_INVALID);
+
+ pw_properties_free(node->properties);
+
+ clear_info(node);
+
+ spa_system_close(context->data_system, node->source.fd);
+ free(impl);
+}
+
+SPA_EXPORT
+int pw_impl_node_for_each_port(struct pw_impl_node *node,
+ enum pw_direction direction,
+ int (*callback) (void *data, struct pw_impl_port *port),
+ void *data)
+{
+ struct spa_list *ports;
+ struct pw_impl_port *p, *t;
+ int res;
+
+ if (direction == PW_DIRECTION_INPUT)
+ ports = &node->input_ports;
+ else
+ ports = &node->output_ports;
+
+ spa_list_for_each_safe(p, t, ports, link)
+ if ((res = callback(data, p)) != 0)
+ return res;
+ return 0;
+}
+
+struct result_node_params_data {
+ struct impl *impl;
+ void *data;
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param);
+ int seq;
+ uint32_t count;
+ unsigned int cache:1;
+};
+
+static void result_node_params(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct result_node_params_data *d = data;
+ struct impl *impl = d->impl;
+ switch (type) {
+ case SPA_RESULT_TYPE_NODE_PARAMS:
+ {
+ const struct spa_result_node_params *r = result;
+ if (d->seq == seq) {
+ d->callback(d->data, seq, r->id, r->index, r->next, r->param);
+ if (d->cache) {
+ if (d->count++ == 0)
+ pw_param_add(&impl->pending_list, seq, r->id, NULL);
+ pw_param_add(&impl->pending_list, seq, r->id, r->param);
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+SPA_EXPORT
+int pw_impl_node_for_each_param(struct pw_impl_node *node,
+ int seq, uint32_t param_id,
+ uint32_t index, uint32_t max,
+ const struct spa_pod *filter,
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param),
+ void *data)
+{
+ int res;
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+ struct result_node_params_data user_data = { impl, data, callback, seq, 0, false };
+ struct spa_hook listener;
+ struct spa_param_info *pi;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .result = result_node_params,
+ };
+
+ pi = pw_param_info_find(node->info.params, node->info.n_params, param_id);
+ if (pi == NULL)
+ return -ENOENT;
+
+ if (max == 0)
+ max = UINT32_MAX;
+
+ pw_log_debug("%p: params id:%d (%s) index:%u max:%u cached:%d", node, param_id,
+ spa_debug_type_find_name(spa_type_param, param_id),
+ index, max, pi->user);
+
+ if (pi->user == 1) {
+ struct pw_param *p;
+ uint8_t buffer[4096];
+ struct spa_pod_dynamic_builder b;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ result.id = param_id;
+ result.next = 0;
+
+ spa_list_for_each(p, &impl->param_list, link) {
+ if (p->id != param_id)
+ continue;
+
+ result.index = result.next++;
+ if (result.index < index)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+
+ if (spa_pod_filter(&b.b, &result.param, p->param, filter) == 0) {
+ pw_log_debug("%p: %d param %u", node, seq, result.index);
+ result_node_params(&user_data, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == max)
+ break;
+ }
+ res = 0;
+ } else {
+ user_data.cache = impl->cache_params &&
+ (filter == NULL && index == 0 && max == UINT32_MAX);
+
+ spa_zero(listener);
+ spa_node_add_listener(node->node, &listener, &node_events, &user_data);
+ res = spa_node_enum_params(node->node, seq,
+ param_id, index, max,
+ filter);
+ spa_hook_remove(&listener);
+
+ if (user_data.cache) {
+ pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL);
+ pi->user = 1;
+ }
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_impl_node_set_param(struct pw_impl_node *node,
+ uint32_t id, uint32_t flags, const struct spa_pod *param)
+{
+ pw_log_debug("%p: set_param id:%d (%s) flags:%08x param:%p", node, id,
+ spa_debug_type_find_name(spa_type_param, id), flags, param);
+ return spa_node_set_param(node->node, id, flags, param);
+}
+
+SPA_EXPORT
+struct pw_impl_port *
+pw_impl_node_find_port(struct pw_impl_node *node, enum pw_direction direction, uint32_t port_id)
+{
+ struct pw_impl_port *port, *p;
+ struct pw_map *portmap;
+ struct spa_list *ports;
+
+ if (direction == PW_DIRECTION_INPUT) {
+ portmap = &node->input_port_map;
+ ports = &node->input_ports;
+ } else {
+ portmap = &node->output_port_map;
+ ports = &node->output_ports;
+ }
+
+ if (port_id != PW_ID_ANY)
+ port = pw_map_lookup(portmap, port_id);
+ else {
+ port = NULL;
+ /* try to find an unlinked port */
+ spa_list_for_each(p, ports, link) {
+ if (spa_list_is_empty(&p->links)) {
+ port = p;
+ break;
+ }
+ /* We can use this port if it can multiplex */
+ if (SPA_FLAG_IS_SET(p->mix_flags, PW_IMPL_PORT_MIX_FLAG_MULTI))
+ port = p;
+ }
+ }
+ pw_log_debug("%p: return %s port %d: %p", node,
+ pw_direction_as_string(direction), port_id, port);
+ return port;
+}
+
+SPA_EXPORT
+uint32_t pw_impl_node_get_free_port_id(struct pw_impl_node *node, enum pw_direction direction)
+{
+ uint32_t n_ports, max_ports;
+ struct pw_map *portmap;
+ uint32_t port_id;
+ bool dynamic;
+ int res;
+
+ if (direction == PW_DIRECTION_INPUT) {
+ max_ports = node->info.max_input_ports;
+ n_ports = node->info.n_input_ports;
+ portmap = &node->input_port_map;
+ dynamic = SPA_FLAG_IS_SET(node->spa_flags, SPA_NODE_FLAG_IN_DYNAMIC_PORTS);
+ } else {
+ max_ports = node->info.max_output_ports;
+ n_ports = node->info.n_output_ports;
+ portmap = &node->output_port_map;
+ dynamic = SPA_FLAG_IS_SET(node->spa_flags, SPA_NODE_FLAG_OUT_DYNAMIC_PORTS);
+ }
+ pw_log_debug("%p: direction %s n_ports:%u max_ports:%u",
+ node, pw_direction_as_string(direction), n_ports, max_ports);
+
+ if (!dynamic || n_ports >= max_ports) {
+ res = -ENOSPC;
+ goto error;
+ }
+
+ port_id = pw_map_insert_new(portmap, NULL);
+ if (port_id == SPA_ID_INVALID) {
+ res = -errno;
+ goto error;
+ }
+
+ pw_log_debug("%p: free port %d", node, port_id);
+
+ return port_id;
+
+error:
+ pw_log_warn("%p: no more port available: %s", node, spa_strerror(res));
+ errno = -res;
+ return SPA_ID_INVALID;
+}
+
+static void on_state_complete(void *obj, void *data, int res, uint32_t seq)
+{
+ struct pw_impl_node *node = obj;
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+ enum pw_node_state state = SPA_PTR_TO_INT(data);
+ char *error = NULL;
+
+ /* driver nodes added -EBUSY. This is then not an error */
+ if (res == -EBUSY)
+ res = 0;
+
+ impl->pending_id = SPA_ID_INVALID;
+ impl->pending_play = false;
+
+ pw_log_debug("%p: state complete res:%d seq:%d", node, res, seq);
+ if (impl->last_error < 0) {
+ res = impl->last_error;
+ impl->last_error = 0;
+ }
+ if (SPA_RESULT_IS_ERROR(res)) {
+ if (node->info.state == PW_NODE_STATE_SUSPENDED) {
+ state = PW_NODE_STATE_SUSPENDED;
+ res = 0;
+ } else {
+ error = spa_aprintf("error changing node state: %s", spa_strerror(res));
+ state = PW_NODE_STATE_ERROR;
+ }
+ }
+ node_update_state(node, state, res, error);
+}
+
+/** Set the node state
+ * \param node a \ref pw_impl_node
+ * \param state a \ref pw_node_state
+ * \return 0 on success < 0 on error
+ *
+ * Set the state of \a node to \a state.
+ */
+SPA_EXPORT
+int pw_impl_node_set_state(struct pw_impl_node *node, enum pw_node_state state)
+{
+ int res = 0;
+ struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this);
+ enum pw_node_state old = impl->pending_state;
+
+ pw_log_debug("%p: set state (%s) %s -> %s, active %d pause_on_idle:%d", node,
+ pw_node_state_as_string(node->info.state),
+ pw_node_state_as_string(old),
+ pw_node_state_as_string(state),
+ node->active,
+ node->pause_on_idle);
+
+ if (old != state)
+ pw_impl_node_emit_state_request(node, state);
+
+ switch (state) {
+ case PW_NODE_STATE_CREATING:
+ return -EIO;
+
+ case PW_NODE_STATE_SUSPENDED:
+ res = suspend_node(node);
+ break;
+
+ case PW_NODE_STATE_IDLE:
+ res = idle_node(node);
+ break;
+
+ case PW_NODE_STATE_RUNNING:
+ if (node->active)
+ res = start_node(node);
+ break;
+
+ case PW_NODE_STATE_ERROR:
+ break;
+ }
+ if (SPA_RESULT_IS_ERROR(res))
+ return res;
+
+ if (SPA_RESULT_IS_ASYNC(res)) {
+ res = spa_node_sync(node->node, res);
+ }
+
+ if (old != state) {
+ if (impl->pending_id != SPA_ID_INVALID) {
+ pw_log_debug("cancel state from %s to %s to %s",
+ pw_node_state_as_string(node->info.state),
+ pw_node_state_as_string(impl->pending_state),
+ pw_node_state_as_string(state));
+
+ if (impl->pending_state == PW_NODE_STATE_RUNNING &&
+ state < PW_NODE_STATE_RUNNING &&
+ impl->pending_play) {
+ impl->pending_play = false;
+ idle_node(node);
+ }
+ pw_work_queue_cancel(impl->work, node, impl->pending_id);
+ node->info.state = impl->pending_state;
+ }
+ /* driver nodes return EBUSY to add a -EBUSY to the work queue. This
+ * will wait until all previous items in the work queue are
+ * completed */
+ impl->pending_state = state;
+ impl->pending_id = pw_work_queue_add(impl->work,
+ node, res == EBUSY ? -EBUSY : res,
+ on_state_complete, SPA_INT_TO_PTR(state));
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_impl_node_set_active(struct pw_impl_node *node, bool active)
+{
+ bool old = node->active;
+
+ if (old != active) {
+ pw_log_debug("%p: %s registered:%d exported:%d", node,
+ active ? "activate" : "deactivate",
+ node->registered, node->exported);
+
+ node->active = active;
+ pw_impl_node_emit_active_changed(node, active);
+
+ if (node->registered)
+ pw_context_recalc_graph(node->context,
+ active ? "node activate" : "node deactivate");
+ else if (!active && node->exported)
+ pw_loop_invoke(node->data_loop, do_node_remove, 1, NULL, 0, true, node);
+ }
+ return 0;
+}
+
+SPA_EXPORT
+bool pw_impl_node_is_active(struct pw_impl_node *node)
+{
+ return node->active;
+}
+
+SPA_EXPORT
+int pw_impl_node_send_command(struct pw_impl_node *node, const struct spa_command *command)
+{
+ return spa_node_send_command(node->node, command);
+}
diff --git a/src/pipewire/impl-node.h b/src/pipewire/impl-node.h
new file mode 100644
index 0000000..978a6d2
--- /dev/null
+++ b/src/pipewire/impl-node.h
@@ -0,0 +1,190 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_NODE_H
+#define PIPEWIRE_IMPL_NODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_impl_node Node Impl
+ *
+ * The node object processes data. The node has a list of
+ * input and output ports (\ref pw_impl_port) on which it
+ * will receive and send out buffers respectively.
+ */
+/**
+ * \addtogroup pw_impl_node
+ * \{
+ */
+struct pw_impl_node;
+struct pw_impl_port;
+
+#include <spa/node/node.h>
+#include <spa/node/event.h>
+
+#include <pipewire/impl.h>
+
+/** Node events, listen to them with \ref pw_impl_node_add_listener */
+struct pw_impl_node_events {
+#define PW_VERSION_IMPL_NODE_EVENTS 0
+ uint32_t version;
+
+ /** the node is destroyed */
+ void (*destroy) (void *data);
+ /** the node is about to be freed */
+ void (*free) (void *data);
+ /** the node is initialized */
+ void (*initialized) (void *data);
+
+ /** a port is being initialized on the node */
+ void (*port_init) (void *data, struct pw_impl_port *port);
+ /** a port was added */
+ void (*port_added) (void *data, struct pw_impl_port *port);
+ /** a port was removed */
+ void (*port_removed) (void *data, struct pw_impl_port *port);
+
+ /** the node info changed */
+ void (*info_changed) (void *data, const struct pw_node_info *info);
+ /** a port on the node changed info */
+ void (*port_info_changed) (void *data, struct pw_impl_port *port,
+ const struct pw_port_info *info);
+ /** the node active state changed */
+ void (*active_changed) (void *data, bool active);
+
+ /** a new state is requested on the node */
+ void (*state_request) (void *data, enum pw_node_state state);
+ /** the state of the node changed */
+ void (*state_changed) (void *data, enum pw_node_state old,
+ enum pw_node_state state, const char *error);
+
+ /** a result was received */
+ void (*result) (void *data, int seq, int res, uint32_t type, const void *result);
+
+ /** an event is emitted */
+ void (*event) (void *data, const struct spa_event *event);
+
+ /** the driver of the node changed */
+ void (*driver_changed) (void *data, struct pw_impl_node *old, struct pw_impl_node *driver);
+
+ /** a peer was added */
+ void (*peer_added) (void *data, struct pw_impl_node *peer);
+ /** a peer was removed */
+ void (*peer_removed) (void *data, struct pw_impl_node *peer);
+};
+
+/** Create a new node */
+struct pw_impl_node *
+pw_context_create_node(struct pw_context *context, /**< the context */
+ struct pw_properties *properties, /**< extra properties */
+ size_t user_data_size /**< user data size */);
+
+/** Complete initialization of the node and register */
+int pw_impl_node_register(struct pw_impl_node *node, /**< node to register */
+ struct pw_properties *properties /**< extra properties */);
+
+/** Destroy a node */
+void pw_impl_node_destroy(struct pw_impl_node *node);
+
+/** Get the node info */
+const struct pw_node_info *pw_impl_node_get_info(struct pw_impl_node *node);
+
+/** Get node user_data. The size of the memory was given in \ref pw_context_create_node */
+void * pw_impl_node_get_user_data(struct pw_impl_node *node);
+
+/** Get the context of this node */
+struct pw_context *pw_impl_node_get_context(struct pw_impl_node *node);
+
+/** Get the global of this node */
+struct pw_global *pw_impl_node_get_global(struct pw_impl_node *node);
+
+/** Get the node properties */
+const struct pw_properties *pw_impl_node_get_properties(struct pw_impl_node *node);
+
+/** Update the node properties */
+int pw_impl_node_update_properties(struct pw_impl_node *node, const struct spa_dict *dict);
+
+/** Set the node implementation */
+int pw_impl_node_set_implementation(struct pw_impl_node *node, struct spa_node *spa_node);
+
+/** Get the node implementation */
+struct spa_node *pw_impl_node_get_implementation(struct pw_impl_node *node);
+
+/** Add an event listener */
+void pw_impl_node_add_listener(struct pw_impl_node *node,
+ struct spa_hook *listener,
+ const struct pw_impl_node_events *events,
+ void *data);
+
+/** Iterate the ports in the given direction. The callback should return
+ * 0 to fetch the next item, any other value stops the iteration and returns
+ * the value. When all callbacks return 0, this function returns 0 when all
+ * items are iterated. */
+int pw_impl_node_for_each_port(struct pw_impl_node *node,
+ enum pw_direction direction,
+ int (*callback) (void *data, struct pw_impl_port *port),
+ void *data);
+
+int pw_impl_node_for_each_param(struct pw_impl_node *node,
+ int seq, uint32_t param_id,
+ uint32_t index, uint32_t max,
+ const struct spa_pod *filter,
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param),
+ void *data);
+
+/** Find the port with direction and port_id or NULL when not found. Passing
+ * PW_ID_ANY for port_id will return any port, preferably an unlinked one. */
+struct pw_impl_port *
+pw_impl_node_find_port(struct pw_impl_node *node, enum pw_direction direction, uint32_t port_id);
+
+/** Get a free unused port_id from the node */
+uint32_t pw_impl_node_get_free_port_id(struct pw_impl_node *node, enum pw_direction direction);
+
+int pw_impl_node_initialized(struct pw_impl_node *node);
+
+/** Set a node active. This will start negotiation with all linked active
+ * nodes and start data transport */
+int pw_impl_node_set_active(struct pw_impl_node *node, bool active);
+
+/** Check if a node is active */
+bool pw_impl_node_is_active(struct pw_impl_node *node);
+
+/** Check if a node is active, Since 0.3.39 */
+int pw_impl_node_send_command(struct pw_impl_node *node, const struct spa_command *command);
+
+/** Set a param on the node, Since 0.3.65 */
+int pw_impl_node_set_param(struct pw_impl_node *node,
+ uint32_t id, uint32_t flags, const struct spa_pod *param);
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_NODE_H */
diff --git a/src/pipewire/impl-port.c b/src/pipewire/impl-port.c
new file mode 100644
index 0000000..ef8fa06
--- /dev/null
+++ b/src/pipewire/impl-port.c
@@ -0,0 +1,1614 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <float.h>
+
+#include <spa/pod/parser.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/node/utils.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/debug/types.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+
+#include "pipewire/impl.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_port);
+#define PW_LOG_TOPIC_DEFAULT log_port
+
+/** \cond */
+struct impl {
+ struct pw_impl_port this;
+ struct spa_node mix_node; /**< mix node implementation */
+
+ struct spa_list param_list;
+ struct spa_list pending_list;
+
+ unsigned int cache_params:1;
+};
+
+#define pw_port_resource(r,m,v,...) pw_resource_call(r,struct pw_port_events,m,v,__VA_ARGS__)
+#define pw_port_resource_info(r,...) pw_port_resource(r,info,0,__VA_ARGS__)
+#define pw_port_resource_param(r,...) pw_port_resource(r,param,0,__VA_ARGS__)
+
+struct resource_data {
+ struct pw_impl_port *port;
+ struct pw_resource *resource;
+
+ struct spa_hook resource_listener;
+ struct spa_hook object_listener;
+
+ uint32_t subscribe_ids[MAX_PARAMS];
+ uint32_t n_subscribe_ids;
+};
+
+/** \endcond */
+
+static void emit_info_changed(struct pw_impl_port *port)
+{
+ struct pw_resource *resource;
+
+ if (port->info.change_mask == 0)
+ return;
+
+ pw_impl_port_emit_info_changed(port, &port->info);
+ if (port->node)
+ pw_impl_node_emit_port_info_changed(port->node, port, &port->info);
+
+ if (port->global)
+ spa_list_for_each(resource, &port->global->resource_list, link)
+ pw_port_resource_info(resource, &port->info);
+
+ port->info.change_mask = 0;
+}
+
+static const char *port_state_as_string(enum pw_impl_port_state state)
+{
+ switch (state) {
+ case PW_IMPL_PORT_STATE_ERROR:
+ return "error";
+ case PW_IMPL_PORT_STATE_INIT:
+ return "init";
+ case PW_IMPL_PORT_STATE_CONFIGURE:
+ return "configure";
+ case PW_IMPL_PORT_STATE_READY:
+ return "ready";
+ case PW_IMPL_PORT_STATE_PAUSED:
+ return "paused";
+ }
+ return "invalid-state";
+}
+
+void pw_impl_port_update_state(struct pw_impl_port *port, enum pw_impl_port_state state, int res, char *error)
+{
+ enum pw_impl_port_state old = port->state;
+
+ port->state = state;
+ free((void*)port->error);
+ port->error = error;
+
+ if (old == state)
+ return;
+
+ pw_log(state == PW_IMPL_PORT_STATE_ERROR ?
+ SPA_LOG_LEVEL_ERROR : SPA_LOG_LEVEL_DEBUG,
+ "%p: state %s -> %s (%s)", port,
+ port_state_as_string(old), port_state_as_string(state), error);
+
+ pw_impl_port_emit_state_changed(port, old, state, error);
+
+ if (state == PW_IMPL_PORT_STATE_ERROR && port->global) {
+ struct pw_resource *resource;
+ spa_list_for_each(resource, &port->global->resource_list, link)
+ pw_resource_error(resource, res, error);
+ }
+}
+
+static int tee_process(void *object)
+{
+ struct impl *impl = object;
+ struct pw_impl_port *this = &impl->this;
+ struct pw_impl_port_mix *mix;
+ struct spa_io_buffers *io = &this->rt.io;
+
+ pw_log_trace_fp("%p: tee input %d %d", this, io->status, io->buffer_id);
+ spa_list_for_each(mix, &this->rt.mix_list, rt_link) {
+ pw_log_trace_fp("%p: port %d %p->%p %d", this,
+ mix->port.port_id, io, mix->io, mix->io->buffer_id);
+ *mix->io = *io;
+ }
+ io->status = SPA_STATUS_NEED_DATA;
+
+ return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
+}
+
+static int tee_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *impl = object;
+ struct pw_impl_port *this = &impl->this;
+
+ pw_log_trace_fp("%p: tee reuse buffer %d %d", this, port_id, buffer_id);
+ spa_node_port_reuse_buffer(this->node->node, this->port_id, buffer_id);
+
+ return 0;
+}
+
+static const struct spa_node_methods schedule_tee_node = {
+ SPA_VERSION_NODE_METHODS,
+ .process = tee_process,
+ .port_reuse_buffer = tee_reuse_buffer,
+};
+
+static int schedule_mix_input(void *object)
+{
+ struct impl *impl = object;
+ struct pw_impl_port *this = &impl->this;
+ struct spa_io_buffers *io = &this->rt.io;
+ struct pw_impl_port_mix *mix;
+
+ if (SPA_UNLIKELY(PW_IMPL_PORT_IS_CONTROL(this)))
+ return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
+
+ spa_list_for_each(mix, &this->rt.mix_list, rt_link) {
+ pw_log_trace_fp("%p: mix input %d %p->%p %d %d", this,
+ mix->port.port_id, mix->io, io, mix->io->status, mix->io->buffer_id);
+ *io = *mix->io;
+ mix->io->status = SPA_STATUS_NEED_DATA;
+ break;
+ }
+ return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
+}
+
+static int schedule_mix_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct impl *impl = object;
+ struct pw_impl_port *this = &impl->this;
+ struct pw_impl_port_mix *mix;
+
+ spa_list_for_each(mix, &this->rt.mix_list, rt_link) {
+ pw_log_trace_fp("%p: reuse buffer %d %d", this, port_id, buffer_id);
+ /* FIXME send reuse buffer to peer */
+ break;
+ }
+ return 0;
+}
+
+static const struct spa_node_methods schedule_mix_node = {
+ SPA_VERSION_NODE_METHODS,
+ .process = schedule_mix_input,
+ .port_reuse_buffer = schedule_mix_reuse_buffer,
+};
+
+SPA_EXPORT
+int pw_impl_port_init_mix(struct pw_impl_port *port, struct pw_impl_port_mix *mix)
+{
+ uint32_t port_id;
+ struct pw_impl_node *node = port->node;
+ int res = 0;
+
+ port_id = pw_map_insert_new(&port->mix_port_map, mix);
+ if (port_id == SPA_ID_INVALID)
+ return -errno;
+
+ if ((res = spa_node_add_port(port->mix, port->direction, port_id, NULL)) < 0 &&
+ res != -ENOTSUP)
+ goto error_remove_map;
+
+ mix->port.direction = port->direction;
+ mix->port.port_id = port_id;
+ mix->p = port;
+
+ if ((res = pw_impl_port_call_init_mix(port, mix)) < 0)
+ goto error_remove_port;
+
+ /* set the same format on the mixer as on the port if any */
+ {
+ uint32_t idx = 0;
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ struct spa_pod *param;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+
+ if (spa_node_port_enum_params_sync(port->mix,
+ pw_direction_reverse(port->direction), 0,
+ SPA_PARAM_Format, &idx, NULL, &param, &b.b) == 1) {
+ spa_node_port_set_param(port->mix,
+ port->direction, port_id,
+ SPA_PARAM_Format, 0, param);
+ }
+ spa_pod_dynamic_builder_clean(&b);
+ }
+
+ spa_list_append(&port->mix_list, &mix->link);
+ port->n_mix++;
+
+ pw_log_debug("%p: init mix n_mix:%d %d.%d io:%p: (%s)", port,
+ port->n_mix, port->port_id, mix->port.port_id,
+ mix->io, spa_strerror(res));
+
+ if (port->n_mix == 1) {
+ pw_log_debug("%p: setting port io", port);
+ spa_node_port_set_io(node->node,
+ port->direction, port->port_id,
+ SPA_IO_Buffers,
+ &port->rt.io, sizeof(port->rt.io));
+ }
+ return res;
+
+error_remove_port:
+ spa_node_remove_port(port->mix, port->direction, port_id);
+error_remove_map:
+ pw_map_remove(&port->mix_port_map, port_id);
+ return res;
+}
+
+SPA_EXPORT
+int pw_impl_port_release_mix(struct pw_impl_port *port, struct pw_impl_port_mix *mix)
+{
+ int res = 0;
+ uint32_t port_id = mix->port.port_id;
+ struct pw_impl_node *node = port->node;
+
+ pw_map_remove(&port->mix_port_map, port_id);
+ spa_list_remove(&mix->link);
+ port->n_mix--;
+
+ res = pw_impl_port_call_release_mix(port, mix);
+
+ if ((res = spa_node_remove_port(port->mix, port->direction, port_id)) < 0 &&
+ res != -ENOTSUP)
+ pw_log_warn("can't remove mix port %d: %s", port_id, spa_strerror(res));
+
+ pw_log_debug("%p: release mix %d %d.%d", port,
+ port->n_mix, port->port_id, mix->port.port_id);
+
+ if (port->n_mix == 0) {
+ pw_log_debug("%p: clearing port io", port);
+ spa_node_port_set_io(node->node,
+ port->direction, port->port_id,
+ SPA_IO_Buffers,
+ NULL, sizeof(port->rt.io));
+ }
+ return res;
+}
+
+static int update_properties(struct pw_impl_port *port, const struct spa_dict *dict, bool filter)
+{
+ static const char * const ignored[] = {
+ PW_KEY_OBJECT_ID,
+ PW_KEY_PORT_DIRECTION,
+ PW_KEY_PORT_CONTROL,
+ PW_KEY_NODE_ID,
+ PW_KEY_PORT_ID,
+ NULL
+ };
+
+ int changed;
+
+ changed = pw_properties_update_ignore(port->properties, dict, filter ? ignored : NULL);
+ port->info.props = &port->properties->dict;
+
+ if (changed) {
+ pw_log_debug("%p: updated %d properties", port, changed);
+ port->info.change_mask |= PW_PORT_CHANGE_MASK_PROPS;
+ }
+ return changed;
+}
+
+static int resource_is_subscribed(struct pw_resource *resource, uint32_t id)
+{
+ struct resource_data *data = pw_resource_get_user_data(resource);
+ uint32_t i;
+
+ for (i = 0; i < data->n_subscribe_ids; i++) {
+ if (data->subscribe_ids[i] == id)
+ return 1;
+ }
+ return 0;
+}
+
+static int notify_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct pw_impl_port *port = data;
+ struct pw_resource *resource;
+
+ spa_list_for_each(resource, &port->global->resource_list, link) {
+ if (!resource_is_subscribed(resource, id))
+ continue;
+
+ pw_log_debug("%p: resource %p notify param %d", port, resource, id);
+ pw_port_resource_param(resource, seq, id, index, next, param);
+ }
+ return 0;
+}
+
+static void emit_params(struct pw_impl_port *port, uint32_t *changed_ids, uint32_t n_changed_ids)
+{
+ uint32_t i;
+ int res;
+
+ if (port->global == NULL)
+ return;
+
+ pw_log_debug("%p: emit %d params", port, n_changed_ids);
+
+ for (i = 0; i < n_changed_ids; i++) {
+ struct pw_resource *resource;
+ int subscribed = 0;
+
+ pw_log_debug("%p: emit param %d/%d: %d", port, i, n_changed_ids,
+ changed_ids[i]);
+
+ pw_impl_port_emit_param_changed(port, changed_ids[i]);
+
+ /* first check if anyone is subscribed */
+ spa_list_for_each(resource, &port->global->resource_list, link) {
+ if ((subscribed = resource_is_subscribed(resource, changed_ids[i])))
+ break;
+ }
+ if (!subscribed)
+ continue;
+
+ if ((res = pw_impl_port_for_each_param(port, 1, changed_ids[i], 0, UINT32_MAX,
+ NULL, notify_param, port)) < 0) {
+ pw_log_error("%p: error %d (%s)", port, res, spa_strerror(res));
+ }
+ }
+}
+
+static int process_latency_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct pw_impl_port *this = data;
+ struct spa_latency_info latency;
+
+ if (id != SPA_PARAM_Latency)
+ return -EINVAL;
+
+ if (spa_latency_parse(param, &latency) < 0)
+ return 0;
+ if (spa_latency_info_compare(&this->latency[latency.direction], &latency) == 0)
+ return 0;
+
+ pw_log_debug("port %p: got %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this,
+ pw_direction_as_string(latency.direction),
+ latency.min_quantum, latency.max_quantum,
+ latency.min_rate, latency.max_rate,
+ latency.min_ns, latency.max_ns);
+
+ this->latency[latency.direction] = latency;
+ if (latency.direction == this->direction)
+ pw_impl_port_emit_latency_changed(this);
+
+ return 0;
+}
+
+static void update_info(struct pw_impl_port *port, const struct spa_port_info *info)
+{
+ uint32_t changed_ids[MAX_PARAMS], n_changed_ids = 0;
+
+ pw_log_debug("%p: %p flags:%08"PRIx64" change_mask:%08"PRIx64,
+ port, info, info->flags, info->change_mask);
+
+ if (info->change_mask & SPA_PORT_CHANGE_MASK_FLAGS) {
+ port->spa_flags = info->flags;
+ }
+ if (info->change_mask & SPA_PORT_CHANGE_MASK_PROPS) {
+ if (info->props) {
+ update_properties(port, info->props, true);
+ } else {
+ pw_log_warn("%p: port PROPS update but no properties", port);
+ }
+ }
+ if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ uint32_t i;
+
+ port->info.change_mask |= PW_PORT_CHANGE_MASK_PARAMS;
+ port->info.n_params = SPA_MIN(info->n_params, SPA_N_ELEMENTS(port->params));
+
+ for (i = 0; i < port->info.n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ pw_log_debug("%p: param %d id:%d (%s) %08x:%08x", port, i,
+ id, spa_debug_type_find_name(spa_type_param, id),
+ port->info.params[i].flags, info->params[i].flags);
+
+ port->info.params[i].id = info->params[i].id;
+ if (port->info.params[i].flags == info->params[i].flags)
+ continue;
+
+ pw_log_debug("%p: update param %d", port, id);
+ port->info.params[i] = info->params[i];
+ port->info.params[i].user = 0;
+
+ if (info->params[i].flags & SPA_PARAM_INFO_READ)
+ changed_ids[n_changed_ids++] = id;
+
+ switch (id) {
+ case SPA_PARAM_Latency:
+ port->have_latency_param =
+ SPA_FLAG_IS_SET(info->params[i].flags, SPA_PARAM_INFO_WRITE);
+ if (port->node != NULL)
+ pw_impl_port_for_each_param(port, 0, id, 0, UINT32_MAX,
+ NULL, process_latency_param, port);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (n_changed_ids > 0)
+ emit_params(port, changed_ids, n_changed_ids);
+}
+
+SPA_EXPORT
+struct pw_impl_port *pw_context_create_port(
+ struct pw_context *context,
+ enum pw_direction direction,
+ uint32_t port_id,
+ const struct spa_port_info *info,
+ size_t user_data_size)
+{
+ struct impl *impl;
+ struct pw_impl_port *this;
+ struct pw_properties *properties;
+ const struct spa_node_methods *mix_methods;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl) + user_data_size);
+ if (impl == NULL)
+ return NULL;
+
+ spa_list_init(&impl->param_list);
+ spa_list_init(&impl->pending_list);
+ impl->cache_params = true;
+
+ this = &impl->this;
+ pw_log_debug("%p: new %s %d", this,
+ pw_direction_as_string(direction), port_id);
+
+ if (info && info->change_mask & SPA_PORT_CHANGE_MASK_PROPS && info->props)
+ properties = pw_properties_new_dict(info->props);
+ else
+ properties = pw_properties_new(NULL, NULL);
+
+ if (properties == NULL) {
+ res = -errno;
+ goto error_no_mem;
+ }
+ pw_properties_setf(properties, PW_KEY_PORT_ID, "%u", port_id);
+
+ if (info) {
+ if (SPA_FLAG_IS_SET(info->flags, SPA_PORT_FLAG_PHYSICAL))
+ pw_properties_set(properties, PW_KEY_PORT_PHYSICAL, "true");
+ if (SPA_FLAG_IS_SET(info->flags, SPA_PORT_FLAG_TERMINAL))
+ pw_properties_set(properties, PW_KEY_PORT_TERMINAL, "true");
+ this->spa_flags = info->flags;
+ }
+
+ this->direction = direction;
+ this->port_id = port_id;
+ this->properties = properties;
+ this->state = PW_IMPL_PORT_STATE_INIT;
+ this->rt.io = SPA_IO_BUFFERS_INIT;
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ this->info.direction = direction;
+ this->info.params = this->params;
+ this->info.change_mask = PW_PORT_CHANGE_MASK_PROPS;
+ this->info.props = &this->properties->dict;
+
+ spa_list_init(&this->links);
+ spa_list_init(&this->mix_list);
+ spa_list_init(&this->rt.mix_list);
+ spa_list_init(&this->control_list[0]);
+ spa_list_init(&this->control_list[1]);
+
+ spa_hook_list_init(&this->listener_list);
+
+ if (this->direction == PW_DIRECTION_INPUT)
+ mix_methods = &schedule_mix_node;
+ else
+ mix_methods = &schedule_tee_node;
+
+ impl->mix_node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ mix_methods, impl);
+
+ pw_impl_port_set_mix(this, NULL, 0);
+
+ pw_map_init(&this->mix_port_map, 64, 64);
+
+ this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+ this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ if (info)
+ update_info(this, info);
+
+ return this;
+
+error_no_mem:
+ pw_log_warn("%p: new failed", impl);
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_impl_port_set_mix(struct pw_impl_port *port, struct spa_node *node, uint32_t flags)
+{
+ struct impl *impl = SPA_CONTAINER_OF(port, struct impl, this);
+ struct pw_impl_port_mix *mix;
+
+ if (node == NULL) {
+ node = &impl->mix_node;
+ flags = 0;
+ }
+
+ pw_log_debug("%p: mix node %p->%p", port, port->mix, node);
+
+ if (port->mix != NULL && port->mix != node) {
+ spa_list_for_each(mix, &port->mix_list, link)
+ spa_node_remove_port(port->mix, mix->port.direction, mix->port.port_id);
+
+ spa_node_port_set_io(port->mix,
+ pw_direction_reverse(port->direction), 0,
+ SPA_IO_Buffers, NULL, 0);
+ }
+ if (port->mix_handle != NULL) {
+ pw_unload_spa_handle(port->mix_handle);
+ port->mix_handle = NULL;
+ }
+
+ port->mix_flags = flags;
+ port->mix = node;
+
+ if (port->mix) {
+ spa_list_for_each(mix, &port->mix_list, link)
+ spa_node_add_port(port->mix, mix->port.direction, mix->port.port_id, NULL);
+
+ spa_node_port_set_io(port->mix,
+ pw_direction_reverse(port->direction), 0,
+ SPA_IO_Buffers,
+ &port->rt.io, sizeof(port->rt.io));
+ }
+ return 0;
+}
+
+static int setup_mixer(struct pw_impl_port *port, const struct spa_pod *param)
+{
+ uint32_t media_type, media_subtype;
+ int res;
+ const char *fallback_lib, *factory_name;
+ struct spa_handle *handle;
+ struct spa_dict_item items[2];
+ char quantum_limit[16];
+ void *iface;
+ struct pw_context *context = port->node->context;
+
+ if ((res = spa_format_parse(param, &media_type, &media_subtype)) < 0)
+ return res;
+
+ pw_log_debug("%p: %s/%s", port,
+ spa_debug_type_find_name(spa_type_media_type, media_type),
+ spa_debug_type_find_name(spa_type_media_subtype, media_subtype));
+
+ switch (media_type) {
+ case SPA_MEDIA_TYPE_audio:
+ switch (media_subtype) {
+ case SPA_MEDIA_SUBTYPE_dsp:
+ {
+ struct spa_audio_info_dsp info;
+ if ((res = spa_format_audio_dsp_parse(param, &info)) < 0)
+ return res;
+
+ if (info.format != SPA_AUDIO_FORMAT_DSP_F32)
+ return -ENOTSUP;
+
+ fallback_lib = "audiomixer/libspa-audiomixer";
+ factory_name = SPA_NAME_AUDIO_MIXER_DSP;
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_raw:
+ fallback_lib = "audiomixer/libspa-audiomixer";
+ factory_name = SPA_NAME_AUDIO_MIXER;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ break;
+
+ case SPA_MEDIA_TYPE_application:
+ switch (media_subtype) {
+ case SPA_MEDIA_SUBTYPE_control:
+ fallback_lib = "control/libspa-control";
+ factory_name = SPA_NAME_CONTROL_MIXER;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ break;
+
+ default:
+ return -ENOTSUP;
+ }
+
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LIBRARY_NAME, fallback_lib);
+ spa_scnprintf(quantum_limit, sizeof(quantum_limit), "%u",
+ context->settings.clock_quantum_limit);
+ items[1] = SPA_DICT_ITEM_INIT("clock.quantum-limit", quantum_limit);
+
+ handle = pw_context_load_spa_handle(context, factory_name,
+ &SPA_DICT_INIT_ARRAY(items));
+ if (handle == NULL)
+ return -errno;
+
+ if ((res = spa_handle_get_interface(handle,
+ SPA_TYPE_INTERFACE_Node, &iface)) < 0) {
+ pw_unload_spa_handle(handle);
+ return res;
+ }
+
+ pw_log_debug("mix node handle:%p iface:%p", handle, iface);
+ pw_impl_port_set_mix(port, (struct spa_node*)iface,
+ PW_IMPL_PORT_MIX_FLAG_MULTI |
+ PW_IMPL_PORT_MIX_FLAG_NEGOTIATE);
+ port->mix_handle = handle;
+
+ return 0;
+}
+
+SPA_EXPORT
+enum pw_direction pw_impl_port_get_direction(struct pw_impl_port *port)
+{
+ return port->direction;
+}
+
+SPA_EXPORT
+uint32_t pw_impl_port_get_id(struct pw_impl_port *port)
+{
+ return port->port_id;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_impl_port_get_properties(struct pw_impl_port *port)
+{
+ return port->properties;
+}
+
+SPA_EXPORT
+int pw_impl_port_update_properties(struct pw_impl_port *port, const struct spa_dict *dict)
+{
+ int changed = update_properties(port, dict, false);
+ emit_info_changed(port);
+ return changed;
+}
+
+void pw_impl_port_update_info(struct pw_impl_port *port, const struct spa_port_info *info)
+{
+ update_info(port, info);
+ emit_info_changed(port);
+}
+
+SPA_EXPORT
+struct pw_impl_node *pw_impl_port_get_node(struct pw_impl_port *port)
+{
+ return port->node;
+}
+
+SPA_EXPORT
+void pw_impl_port_add_listener(struct pw_impl_port *port,
+ struct spa_hook *listener,
+ const struct pw_impl_port_events *events,
+ void *data)
+{
+ spa_hook_list_append(&port->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+const struct pw_port_info *pw_impl_port_get_info(struct pw_impl_port *port)
+{
+ return &port->info;
+}
+
+SPA_EXPORT
+void * pw_impl_port_get_user_data(struct pw_impl_port *port)
+{
+ return port->user_data;
+}
+
+static int do_add_port(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct pw_impl_port *this = user_data;
+
+ pw_log_trace("%p: add port", this);
+ if (this->direction == PW_DIRECTION_INPUT)
+ spa_list_append(&this->node->rt.input_mix, &this->rt.node_link);
+ else
+ spa_list_append(&this->node->rt.output_mix, &this->rt.node_link);
+
+ return 0;
+}
+
+static int check_param_io(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct pw_impl_port *port = data;
+ struct pw_impl_node *node = port->node;
+ uint32_t pid, psize;
+
+ if (spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamIO, NULL,
+ SPA_PARAM_IO_id, SPA_POD_Id(&pid),
+ SPA_PARAM_IO_size, SPA_POD_Int(&psize)) < 0)
+ return 0;
+
+ pw_log_debug("%p: got io id:%d (%s)", port, pid,
+ spa_debug_type_find_name(spa_type_io, pid));
+
+ switch (pid) {
+ case SPA_IO_Control:
+ case SPA_IO_Notify:
+ pw_control_new(node->context, port, pid, psize, 0);
+ SPA_FLAG_SET(port->flags, PW_IMPL_PORT_FLAG_CONTROL);
+ break;
+ case SPA_IO_Buffers:
+ SPA_FLAG_SET(port->flags, PW_IMPL_PORT_FLAG_BUFFERS);
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+
+static int reply_param(void *data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct resource_data *d = data;
+ struct pw_resource *resource = d->resource;
+ pw_log_debug("%p: resource %p reply param %u %u %u", d->port,
+ resource, id, index, next);
+ pw_port_resource_param(resource, seq, id, index, next, param);
+ return 0;
+}
+
+static int port_enum_params(void *object, int seq, uint32_t id, uint32_t index, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ struct pw_impl_port *port = data->port;
+ int res;
+
+ pw_log_debug("%p: resource %p enum params seq:%d id:%d (%s) index:%u num:%u", port,
+ resource, seq, id, spa_debug_type_find_name(spa_type_param, id),
+ index, num);
+
+ if ((res = pw_impl_port_for_each_param(port, seq, id, index, num, filter,
+ reply_param, data)) < 0)
+ pw_resource_errorf(resource, res,
+ "enum params id:%d (%s) failed", id,
+ spa_debug_type_find_name(spa_type_param, id));
+ return res;
+}
+
+static int port_subscribe_params(void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct resource_data *data = object;
+ struct pw_resource *resource = data->resource;
+ uint32_t i;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(data->subscribe_ids));
+ data->n_subscribe_ids = n_ids;
+
+ for (i = 0; i < n_ids; i++) {
+ data->subscribe_ids[i] = ids[i];
+ pw_log_debug("%p: resource %p subscribe param id:%d (%s)", data->port,
+ resource, ids[i],
+ spa_debug_type_find_name(spa_type_param, ids[i]));
+ port_enum_params(data, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static const struct pw_port_methods port_methods = {
+ PW_VERSION_PORT_METHODS,
+ .subscribe_params = port_subscribe_params,
+ .enum_params = port_enum_params
+};
+
+static void resource_destroy(void *data)
+{
+ struct resource_data *d = data;
+ spa_hook_remove(&d->resource_listener);
+ spa_hook_remove(&d->object_listener);
+}
+
+static const struct pw_resource_events resource_events = {
+ PW_VERSION_RESOURCE_EVENTS,
+ .destroy = resource_destroy,
+};
+
+static int
+global_bind(void *object, struct pw_impl_client *client, uint32_t permissions,
+ uint32_t version, uint32_t id)
+{
+ struct pw_impl_port *this = object;
+ struct pw_global *global = this->global;
+ struct pw_resource *resource;
+ struct resource_data *data;
+ int res;
+
+ resource = pw_resource_new(client, id, permissions, global->type, version, sizeof(*data));
+ if (resource == NULL) {
+ res = -errno;
+ goto error_resource;
+ }
+
+ data = pw_resource_get_user_data(resource);
+ data->port = this;
+ data->resource = resource;
+
+ pw_resource_add_listener(resource,
+ &data->resource_listener,
+ &resource_events, data);
+ pw_resource_add_object_listener(resource,
+ &data->object_listener,
+ &port_methods, data);
+
+ pw_log_debug("%p: bound to %d", this, resource->id);
+ pw_global_add_resource(global, resource);
+
+ this->info.change_mask = PW_PORT_CHANGE_MASK_ALL;
+ pw_port_resource_info(resource, &this->info);
+ this->info.change_mask = 0;
+ return 0;
+
+error_resource:
+ pw_log_error("%p: can't create port resource: %m", this);
+ return res;
+}
+
+static void global_destroy(void *data)
+{
+ struct pw_impl_port *port = data;
+ spa_hook_remove(&port->global_listener);
+ port->global = NULL;
+ pw_impl_port_destroy(port);
+}
+
+static const struct pw_global_events global_events = {
+ PW_VERSION_GLOBAL_EVENTS,
+ .destroy = global_destroy,
+};
+
+int pw_impl_port_register(struct pw_impl_port *port,
+ struct pw_properties *properties)
+{
+ static const char * const keys[] = {
+ PW_KEY_OBJECT_SERIAL,
+ PW_KEY_OBJECT_PATH,
+ PW_KEY_FORMAT_DSP,
+ PW_KEY_NODE_ID,
+ PW_KEY_AUDIO_CHANNEL,
+ PW_KEY_PORT_ID,
+ PW_KEY_PORT_NAME,
+ PW_KEY_PORT_DIRECTION,
+ PW_KEY_PORT_MONITOR,
+ PW_KEY_PORT_PHYSICAL,
+ PW_KEY_PORT_TERMINAL,
+ PW_KEY_PORT_CONTROL,
+ PW_KEY_PORT_ALIAS,
+ PW_KEY_PORT_EXTRA,
+ NULL
+ };
+
+ struct pw_impl_node *node = port->node;
+
+ if (node == NULL || node->global == NULL)
+ return -EIO;
+
+ port->global = pw_global_new(node->context,
+ PW_TYPE_INTERFACE_Port,
+ PW_VERSION_PORT,
+ properties,
+ global_bind,
+ port);
+ if (port->global == NULL)
+ return -errno;
+
+ pw_global_add_listener(port->global, &port->global_listener, &global_events, port);
+
+ port->info.id = port->global->id;
+ pw_properties_setf(port->properties, PW_KEY_NODE_ID, "%d", node->global->id);
+ pw_properties_setf(port->properties, PW_KEY_OBJECT_ID, "%d", port->info.id);
+ pw_properties_setf(port->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64,
+ pw_global_get_serial(port->global));
+ port->info.props = &port->properties->dict;
+
+ pw_global_update_keys(port->global, &port->properties->dict, keys);
+
+ pw_impl_port_emit_initialized(port);
+
+ return pw_global_register(port->global);
+}
+
+SPA_EXPORT
+int pw_impl_port_add(struct pw_impl_port *port, struct pw_impl_node *node)
+{
+ uint32_t port_id = port->port_id;
+ struct spa_list *ports;
+ struct pw_map *portmap;
+ struct pw_impl_port *find;
+ bool control;
+ const char *str, *dir;
+ int res;
+
+ if (port->node != NULL)
+ return -EEXIST;
+
+ if (port->direction == PW_DIRECTION_INPUT) {
+ ports = &node->input_ports;
+ portmap = &node->input_port_map;
+ } else {
+ ports = &node->output_ports;
+ portmap = &node->output_port_map;
+ }
+
+ find = pw_map_lookup(portmap, port_id);
+ if (find != NULL)
+ return -EEXIST;
+
+ if ((res = pw_map_insert_at(portmap, port_id, port)) < 0)
+ return res;
+
+ port->node = node;
+
+ pw_impl_node_emit_port_init(node, port);
+
+ pw_impl_port_for_each_param(port, 0, SPA_PARAM_IO, 0, 0, NULL, check_param_io, port);
+ pw_impl_port_for_each_param(port, 0, SPA_PARAM_Latency, 0, 0, NULL, process_latency_param, port);
+
+ control = PW_IMPL_PORT_IS_CONTROL(port);
+ if (control) {
+ dir = port->direction == PW_DIRECTION_INPUT ? "control" : "notify";
+ pw_properties_set(port->properties, PW_KEY_PORT_CONTROL, "true");
+ }
+ else {
+ dir = port->direction == PW_DIRECTION_INPUT ? "in" : "out";
+ }
+ pw_properties_set(port->properties, PW_KEY_PORT_DIRECTION, dir);
+
+ if (pw_properties_get(port->properties, PW_KEY_PORT_NAME) == NULL) {
+ if ((str = pw_properties_get(port->properties, PW_KEY_AUDIO_CHANNEL)) != NULL &&
+ !spa_streq(str, "UNK")) {
+ pw_properties_setf(port->properties, PW_KEY_PORT_NAME, "%s_%s", dir, str);
+ }
+ else {
+ pw_properties_setf(port->properties, PW_KEY_PORT_NAME, "%s_%d", dir, port->port_id);
+ }
+ }
+ if (pw_properties_get(port->properties, PW_KEY_PORT_ALIAS) == NULL) {
+ const struct pw_properties *nprops;
+ const char *node_name;
+
+ nprops = pw_impl_node_get_properties(node);
+ if ((node_name = pw_properties_get(nprops, PW_KEY_NODE_NICK)) == NULL &&
+ (node_name = pw_properties_get(nprops, PW_KEY_NODE_DESCRIPTION)) == NULL &&
+ (node_name = pw_properties_get(nprops, PW_KEY_NODE_NAME)) == NULL)
+ node_name = "node";
+
+ pw_properties_setf(port->properties, PW_KEY_PORT_ALIAS, "%s:%s",
+ node_name,
+ pw_properties_get(port->properties, PW_KEY_PORT_NAME));
+ }
+
+ port->info.props = &port->properties->dict;
+
+ if (control) {
+ pw_log_debug("%p: setting node control", port);
+ } else {
+ pw_log_debug("%p: setting mixer io", port);
+ spa_node_port_set_io(port->mix,
+ pw_direction_reverse(port->direction), 0,
+ SPA_IO_Buffers,
+ &port->rt.io, sizeof(port->rt.io));
+ }
+
+ pw_log_debug("%p: %d add to node %p", port, port_id, node);
+
+ spa_list_append(ports, &port->link);
+
+ if (port->direction == PW_DIRECTION_INPUT) {
+ node->info.n_input_ports++;
+ node->info.change_mask |= PW_NODE_CHANGE_MASK_INPUT_PORTS;
+ } else {
+ node->info.n_output_ports++;
+ node->info.change_mask |= PW_NODE_CHANGE_MASK_OUTPUT_PORTS;
+ }
+
+ if (node->global)
+ pw_impl_port_register(port, NULL);
+
+ if (port->state <= PW_IMPL_PORT_STATE_INIT)
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_CONFIGURE, 0, NULL);
+
+ pw_impl_node_emit_port_added(node, port);
+ emit_info_changed(port);
+
+ return 0;
+}
+
+static int do_destroy_link(void *data, struct pw_impl_link *link)
+{
+ pw_impl_link_destroy(link);
+ return 0;
+}
+
+void pw_impl_port_unlink(struct pw_impl_port *port)
+{
+ pw_impl_port_for_each_link(port, do_destroy_link, port);
+}
+
+static int do_remove_port(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct pw_impl_port *this = user_data;
+
+ pw_log_trace("%p: remove port", this);
+ spa_list_remove(&this->rt.node_link);
+
+ return 0;
+}
+
+static void pw_impl_port_remove(struct pw_impl_port *port)
+{
+ struct pw_impl_node *node = port->node;
+ int res;
+
+ if (node == NULL)
+ return;
+
+ pw_log_debug("%p: remove added:%d", port, port->added);
+
+ if (port->added) {
+ pw_loop_invoke(node->data_loop, do_remove_port,
+ SPA_ID_INVALID, NULL, 0, true, port);
+ port->added = false;
+ }
+
+ if (SPA_FLAG_IS_SET(port->flags, PW_IMPL_PORT_FLAG_TO_REMOVE)) {
+ if ((res = spa_node_remove_port(node->node, port->direction, port->port_id)) < 0)
+ pw_log_warn("%p: can't remove: %s", port, spa_strerror(res));
+ }
+
+ if (port->direction == PW_DIRECTION_INPUT) {
+ if ((res = pw_map_insert_at(&node->input_port_map, port->port_id, NULL)) < 0)
+ pw_log_warn("%p: can't remove input port: %s", port, spa_strerror(res));
+ node->info.n_input_ports--;
+ } else {
+ if ((res = pw_map_insert_at(&node->output_port_map, port->port_id, NULL)) < 0)
+ pw_log_warn("%p: can't remove output port: %s", port, spa_strerror(res));
+ node->info.n_output_ports--;
+ }
+
+ pw_impl_port_set_mix(port, NULL, 0);
+
+ spa_list_remove(&port->link);
+ pw_impl_node_emit_port_removed(node, port);
+ port->node = NULL;
+}
+
+void pw_impl_port_destroy(struct pw_impl_port *port)
+{
+ struct impl *impl = SPA_CONTAINER_OF(port, struct impl, this);
+ struct pw_control *control;
+
+ pw_log_debug("%p: destroy", port);
+
+ port->destroying = true;
+ pw_impl_port_emit_destroy(port);
+
+ pw_impl_port_unlink(port);
+
+ pw_log_debug("%p: control destroy", port);
+ spa_list_consume(control, &port->control_list[0], port_link)
+ pw_control_destroy(control);
+ spa_list_consume(control, &port->control_list[1], port_link)
+ pw_control_destroy(control);
+
+ pw_impl_port_remove(port);
+
+ if (port->global) {
+ spa_hook_remove(&port->global_listener);
+ pw_global_destroy(port->global);
+ }
+
+ pw_log_debug("%p: free", port);
+ pw_impl_port_emit_free(port);
+
+ spa_hook_list_clean(&port->listener_list);
+
+ pw_buffers_clear(&port->buffers);
+ pw_buffers_clear(&port->mix_buffers);
+ free((void*)port->error);
+
+ pw_param_clear(&impl->param_list, SPA_ID_INVALID);
+ pw_param_clear(&impl->pending_list, SPA_ID_INVALID);
+
+ pw_map_clear(&port->mix_port_map);
+
+ pw_properties_free(port->properties);
+
+ free(port);
+}
+
+struct result_port_params_data {
+ struct impl *impl;
+ void *data;
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param);
+ int seq;
+ uint32_t count;
+ unsigned int cache:1;
+};
+
+static void result_port_params(void *data, int seq, int res, uint32_t type, const void *result)
+{
+ struct result_port_params_data *d = data;
+ struct impl *impl = d->impl;
+ switch (type) {
+ case SPA_RESULT_TYPE_NODE_PARAMS:
+ {
+ const struct spa_result_node_params *r = result;
+ if (d->seq == seq) {
+ d->callback(d->data, seq, r->id, r->index, r->next, r->param);
+ if (d->cache) {
+ if (d->count++ == 0)
+ pw_param_add(&impl->pending_list, seq, r->id, NULL);
+ pw_param_add(&impl->pending_list, seq, r->id, r->param);
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+int pw_impl_port_for_each_param(struct pw_impl_port *port,
+ int seq,
+ uint32_t param_id,
+ uint32_t index, uint32_t max,
+ const struct spa_pod *filter,
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param),
+ void *data)
+{
+ int res;
+ struct impl *impl = SPA_CONTAINER_OF(port, struct impl, this);
+ struct pw_impl_node *node = port->node;
+ struct result_port_params_data user_data = { impl, data, callback, seq, 0, false };
+ struct spa_hook listener;
+ struct spa_param_info *pi;
+ static const struct spa_node_events node_events = {
+ SPA_VERSION_NODE_EVENTS,
+ .result = result_port_params,
+ };
+
+ pi = pw_param_info_find(port->info.params, port->info.n_params, param_id);
+ if (pi == NULL)
+ return -ENOENT;
+
+ if (max == 0)
+ max = UINT32_MAX;
+
+ pw_log_debug("%p: params id:%d (%s) index:%u max:%u cached:%d", port, param_id,
+ spa_debug_type_find_name(spa_type_param, param_id),
+ index, max, pi->user);
+
+ if (pi->user == 1) {
+ struct pw_param *p;
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ result.id = param_id;
+ result.next = 0;
+
+ spa_list_for_each(p, &impl->param_list, link) {
+ if (p->id != param_id)
+ continue;
+
+ result.index = result.next++;
+ if (result.index < index)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+
+ if (spa_pod_filter(&b.b, &result.param, p->param, filter) >= 0) {
+ pw_log_debug("%p: %d param %u", port, seq, result.index);
+ result_port_params(&user_data, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == max)
+ break;
+ }
+ res = 0;
+ } else {
+ user_data.cache = impl->cache_params &&
+ (filter == NULL && index == 0 && max == UINT32_MAX);
+
+ spa_zero(listener);
+ spa_node_add_listener(node->node, &listener, &node_events, &user_data);
+ res = spa_node_port_enum_params(node->node, seq,
+ port->direction, port->port_id,
+ param_id, index, max,
+ filter);
+ spa_hook_remove(&listener);
+
+ if (user_data.cache) {
+ pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL);
+ pi->user = 1;
+ }
+ }
+
+ pw_log_debug("%p: res %d: (%s)", port, res, spa_strerror(res));
+ return res;
+}
+
+struct param_filter {
+ struct pw_impl_port *in_port;
+ struct pw_impl_port *out_port;
+ int seq;
+ uint32_t in_param_id;
+ uint32_t out_param_id;
+ int (*callback) (void *data, int seq, uint32_t id, uint32_t index,
+ uint32_t next, struct spa_pod *param);
+ void *data;
+ uint32_t n_params;
+};
+
+static int do_filter(void *data, int seq, uint32_t id, uint32_t index, uint32_t next, struct spa_pod *param)
+{
+ struct param_filter *f = data;
+ f->n_params++;
+ return pw_impl_port_for_each_param(f->out_port, seq, f->out_param_id, 0, 0, param, f->callback, f->data);
+}
+
+int pw_impl_port_for_each_filtered_param(struct pw_impl_port *in_port,
+ struct pw_impl_port *out_port,
+ int seq,
+ uint32_t in_param_id,
+ uint32_t out_param_id,
+ const struct spa_pod *filter,
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param),
+ void *data)
+{
+ int res;
+ struct param_filter fd = { in_port, out_port, seq, in_param_id, out_param_id, callback, data, 0 };
+
+ if ((res = pw_impl_port_for_each_param(in_port, seq, in_param_id, 0, 0, filter, do_filter, &fd)) < 0)
+ return res;
+
+ if (fd.n_params == 0)
+ res = do_filter(&fd, seq, 0, 0, 0, NULL);
+
+ return res;
+}
+
+int pw_impl_port_for_each_link(struct pw_impl_port *port,
+ int (*callback) (void *data, struct pw_impl_link *link),
+ void *data)
+{
+ struct pw_impl_link *l, *t;
+ int res = 0;
+
+ if (port->direction == PW_DIRECTION_OUTPUT) {
+ spa_list_for_each_safe(l, t, &port->links, output_link)
+ if ((res = callback(data, l)) != 0)
+ break;
+ } else {
+ spa_list_for_each_safe(l, t, &port->links, input_link)
+ if ((res = callback(data, l)) != 0)
+ break;
+ }
+ return res;
+}
+
+int pw_impl_port_recalc_latency(struct pw_impl_port *port)
+{
+ struct pw_impl_link *l;
+ struct spa_latency_info latency, *current;
+ struct pw_impl_port *other;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ bool changed;
+
+ if (port->destroying)
+ return 0;
+
+ /* given an output port, we calculate the total latency to the sinks or the input
+ * latency. */
+ spa_latency_info_combine_start(&latency, SPA_DIRECTION_REVERSE(port->direction));
+
+ if (port->direction == PW_DIRECTION_OUTPUT) {
+ spa_list_for_each(l, &port->links, output_link) {
+ other = l->input;
+ spa_latency_info_combine(&latency, &other->latency[other->direction]);
+ pw_log_debug("port %d: peer %d: latency %f-%f %d-%d %"PRIu64"-%"PRIu64,
+ port->info.id, other->info.id,
+ latency.min_quantum, latency.max_quantum,
+ latency.min_rate, latency.max_rate,
+ latency.min_ns, latency.max_ns);
+ }
+ } else {
+ spa_list_for_each(l, &port->links, input_link) {
+ other = l->output;
+ spa_latency_info_combine(&latency, &other->latency[other->direction]);
+ pw_log_debug("port %d: peer %d: latency %f-%f %d-%d %"PRIu64"-%"PRIu64,
+ port->info.id, other->info.id,
+ latency.min_quantum, latency.max_quantum,
+ latency.min_rate, latency.max_rate,
+ latency.min_ns, latency.max_ns);
+ }
+ }
+ spa_latency_info_combine_finish(&latency);
+
+ current = &port->latency[latency.direction];
+
+ changed = spa_latency_info_compare(current, &latency) != 0;
+
+ pw_log_info("port %d: %s %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64,
+ port->info.id, changed ? "set" : "keep",
+ pw_direction_as_string(latency.direction),
+ latency.min_quantum, latency.max_quantum,
+ latency.min_rate, latency.max_rate,
+ latency.min_ns, latency.max_ns);
+
+ if (!changed)
+ return 0;
+
+ *current = latency;
+
+ if (!port->have_latency_param)
+ return 0;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ param = spa_latency_build(&b, SPA_PARAM_Latency, &latency);
+ return pw_impl_port_set_param(port, SPA_PARAM_Latency, 0, param);
+}
+
+SPA_EXPORT
+int pw_impl_port_is_linked(struct pw_impl_port *port)
+{
+ return spa_list_is_empty(&port->links) ? 0 : 1;
+}
+
+SPA_EXPORT
+int pw_impl_port_set_param(struct pw_impl_port *port, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ int res;
+ struct pw_impl_node *node = port->node;
+
+ pw_log_debug("%p: %d set param %d %p", port, port->state, id, param);
+
+ /* set parameter on node */
+ res = spa_node_port_set_param(node->node,
+ port->direction, port->port_id,
+ id, flags, param);
+
+ pw_log_debug("%p: %d set param on node %d:%d id:%d (%s): %d (%s)", port, port->state,
+ port->direction, port->port_id, id,
+ spa_debug_type_find_name(spa_type_param, id),
+ res, spa_strerror(res));
+
+ /* set the parameters on all ports of the mixer node if possible */
+ if (res >= 0) {
+ struct pw_impl_port_mix *mix;
+
+ if (port->direction == PW_DIRECTION_INPUT &&
+ id == SPA_PARAM_Format && param != NULL &&
+ !SPA_FLAG_IS_SET(port->flags, PW_IMPL_PORT_FLAG_NO_MIXER)) {
+ setup_mixer(port, param);
+ }
+
+ spa_list_for_each(mix, &port->mix_list, link) {
+ spa_node_port_set_param(port->mix,
+ mix->port.direction, mix->port.port_id,
+ id, flags, param);
+ }
+ spa_node_port_set_param(port->mix,
+ pw_direction_reverse(port->direction), 0,
+ id, flags, param);
+ }
+
+ if (id == SPA_PARAM_Format) {
+ pw_log_debug("%p: %d %p %d", port, port->state, param, res);
+
+ if (port->added) {
+ pw_loop_invoke(node->data_loop, do_remove_port, SPA_ID_INVALID, NULL, 0, true, port);
+ port->added = false;
+ }
+ /* setting the format always destroys the negotiated buffers */
+ if (port->direction == PW_DIRECTION_OUTPUT) {
+ struct pw_impl_link *l;
+ /* remove all buffers shared with an output port peer */
+ spa_list_for_each(l, &port->links, output_link)
+ pw_impl_port_use_buffers(l->input, &l->rt.in_mix, 0, NULL, 0);
+ }
+ pw_buffers_clear(&port->buffers);
+ pw_buffers_clear(&port->mix_buffers);
+
+ if (param == NULL || res < 0) {
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_CONFIGURE, 0, NULL);
+ }
+ else if (spa_pod_is_fixated(param) <= 0) {
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_CONFIGURE, 0, NULL);
+ pw_impl_port_emit_param_changed(port, id);
+ }
+ else if (!SPA_RESULT_IS_ASYNC(res)) {
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_READY, 0, NULL);
+ }
+ }
+ return res;
+}
+
+static int negotiate_mixer_buffers(struct pw_impl_port *port, uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ int res;
+ struct pw_impl_node *node = port->node;
+
+ if (SPA_FLAG_IS_SET(port->mix_flags, PW_IMPL_PORT_MIX_FLAG_MIX_ONLY))
+ return 0;
+
+ if (SPA_FLAG_IS_SET(port->mix_flags, PW_IMPL_PORT_MIX_FLAG_NEGOTIATE)) {
+ int alloc_flags;
+
+ /* try dynamic data */
+ alloc_flags = PW_BUFFERS_FLAG_DYNAMIC;
+
+ pw_log_debug("%p: %d.%d negotiate %d buffers on node: %p",
+ port, port->direction, port->port_id, n_buffers, node->node);
+
+ if (port->added) {
+ pw_loop_invoke(node->data_loop, do_remove_port, SPA_ID_INVALID, NULL, 0, true, port);
+ port->added = false;
+ }
+
+ pw_buffers_clear(&port->mix_buffers);
+
+ if (n_buffers > 0) {
+ if ((res = pw_buffers_negotiate(node->context, alloc_flags,
+ port->mix, 0,
+ node->node, port->port_id,
+ &port->mix_buffers)) < 0) {
+ pw_log_warn("%p: can't negotiate buffers: %s",
+ port, spa_strerror(res));
+ return res;
+ }
+ buffers = port->mix_buffers.buffers;
+ n_buffers = port->mix_buffers.n_buffers;
+ flags = 0;
+ }
+ }
+
+ pw_log_debug("%p: %d.%d use %d buffers on node: %p",
+ port, port->direction, port->port_id, n_buffers, node->node);
+
+ res = spa_node_port_use_buffers(node->node,
+ port->direction, port->port_id,
+ flags, buffers, n_buffers);
+
+ if (SPA_RESULT_IS_OK(res)) {
+ spa_node_port_use_buffers(port->mix,
+ pw_direction_reverse(port->direction), 0,
+ 0, buffers, n_buffers);
+ }
+ if (!port->added && n_buffers > 0) {
+ pw_loop_invoke(node->data_loop, do_add_port, SPA_ID_INVALID, NULL, 0, false, port);
+ port->added = true;
+ }
+ return res;
+}
+
+
+SPA_EXPORT
+int pw_impl_port_use_buffers(struct pw_impl_port *port, struct pw_impl_port_mix *mix, uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ int res = 0, res2;
+
+ pw_log_debug("%p: %d:%d.%d: %d buffers flags:%d state:%d n_mix:%d", port,
+ port->direction, port->port_id, mix->id,
+ n_buffers, flags, port->state, port->n_mix);
+
+ if (n_buffers == 0 && port->state <= PW_IMPL_PORT_STATE_READY)
+ return 0;
+
+ if (n_buffers > 0 && port->state < PW_IMPL_PORT_STATE_READY)
+ return -EIO;
+
+ if (n_buffers == 0) {
+ mix->have_buffers = false;
+ if (port->n_mix == 1)
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_READY, 0, NULL);
+ }
+
+ /* first negotiate with the node, this makes it possible to let the
+ * node allocate buffer memory if needed */
+ if (port->state == PW_IMPL_PORT_STATE_READY) {
+ res = negotiate_mixer_buffers(port, flags, buffers, n_buffers);
+
+ if (res < 0) {
+ pw_log_error("%p: negotiate buffers on node: %d (%s)",
+ port, res, spa_strerror(res));
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_ERROR, res,
+ strdup("can't negotiate buffers on port"));
+ } else if (n_buffers > 0 && !SPA_RESULT_IS_ASYNC(res)) {
+ pw_impl_port_update_state(port, PW_IMPL_PORT_STATE_PAUSED, 0, NULL);
+ }
+ }
+
+ /* then use the buffers on the mixer */
+ if (!SPA_FLAG_IS_SET(port->mix_flags, PW_IMPL_PORT_MIX_FLAG_MIX_ONLY))
+ flags &= ~SPA_NODE_BUFFERS_FLAG_ALLOC;
+
+ res2 = spa_node_port_use_buffers(port->mix,
+ mix->port.direction, mix->port.port_id, flags,
+ buffers, n_buffers);
+ if (res2 < 0) {
+ if (res2 != -ENOTSUP && n_buffers > 0) {
+ pw_log_warn("%p: mix use buffers failed: %d (%s)",
+ port, res2, spa_strerror(res2));
+ return res2;
+ }
+ }
+ else if (SPA_RESULT_IS_ASYNC(res2))
+ res = res2;
+
+ return res;
+}
diff --git a/src/pipewire/impl-port.h b/src/pipewire/impl-port.h
new file mode 100644
index 0000000..c275104
--- /dev/null
+++ b/src/pipewire/impl-port.h
@@ -0,0 +1,144 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_PORT_H
+#define PIPEWIRE_IMPL_PORT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+
+/** \defgroup pw_impl_port Port Impl
+ *
+ * \brief A port can be used to link two nodes.
+ */
+
+/**
+ * \addtogroup pw_impl_port
+ * \{
+ */
+struct pw_impl_port;
+struct pw_impl_link;
+struct pw_control;
+
+#include <pipewire/impl.h>
+
+enum pw_impl_port_state {
+ PW_IMPL_PORT_STATE_ERROR = -1, /**< the port is in error */
+ PW_IMPL_PORT_STATE_INIT = 0, /**< the port is being created */
+ PW_IMPL_PORT_STATE_CONFIGURE = 1, /**< the port is ready for format negotiation */
+ PW_IMPL_PORT_STATE_READY = 2, /**< the port is ready for buffer allocation */
+ PW_IMPL_PORT_STATE_PAUSED = 3, /**< the port is paused */
+};
+
+/** Port events, use \ref pw_impl_port_add_listener */
+struct pw_impl_port_events {
+#define PW_VERSION_IMPL_PORT_EVENTS 2
+ uint32_t version;
+
+ /** The port is destroyed */
+ void (*destroy) (void *data);
+
+ /** The port is freed */
+ void (*free) (void *data);
+
+ /** The port is initialized */
+ void (*initialized) (void *data);
+
+ /** the port info changed */
+ void (*info_changed) (void *data, const struct pw_port_info *info);
+
+ /** a new link is added on this port */
+ void (*link_added) (void *data, struct pw_impl_link *link);
+
+ /** a link is removed from this port */
+ void (*link_removed) (void *data, struct pw_impl_link *link);
+
+ /** the state of the port changed */
+ void (*state_changed) (void *data, enum pw_impl_port_state old,
+ enum pw_impl_port_state state, const char *error);
+
+ /** a control was added to the port */
+ void (*control_added) (void *data, struct pw_control *control);
+
+ /** a control was removed from the port */
+ void (*control_removed) (void *data, struct pw_control *control);
+
+ /** a parameter changed, since version 1 */
+ void (*param_changed) (void *data, uint32_t id);
+
+ /** latency changed. Since version 2 */
+ void (*latency_changed) (void *data);
+};
+
+/** Create a new port
+ * \return a newly allocated port */
+struct pw_impl_port *
+pw_context_create_port(struct pw_context *context,
+ enum pw_direction direction,
+ uint32_t port_id,
+ const struct spa_port_info *info,
+ size_t user_data_size);
+
+/** Get the port direction */
+enum pw_direction pw_impl_port_get_direction(struct pw_impl_port *port);
+
+/** Get the port properties */
+const struct pw_properties *pw_impl_port_get_properties(struct pw_impl_port *port);
+
+/** Update the port properties */
+int pw_impl_port_update_properties(struct pw_impl_port *port, const struct spa_dict *dict);
+
+/** Get the port info */
+const struct pw_port_info *pw_impl_port_get_info(struct pw_impl_port *port);
+
+/** Get the port id */
+uint32_t pw_impl_port_get_id(struct pw_impl_port *port);
+
+/** Get the port parent node or NULL when not yet set */
+struct pw_impl_node *pw_impl_port_get_node(struct pw_impl_port *port);
+
+/** check is a port has links, return 0 if not, 1 if it is linked */
+int pw_impl_port_is_linked(struct pw_impl_port *port);
+
+/** Add a port to a node */
+int pw_impl_port_add(struct pw_impl_port *port, struct pw_impl_node *node);
+
+/** Add an event listener on the port */
+void pw_impl_port_add_listener(struct pw_impl_port *port,
+ struct spa_hook *listener,
+ const struct pw_impl_port_events *events,
+ void *data);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_PORT_H */
diff --git a/src/pipewire/impl.h b/src/pipewire/impl.h
new file mode 100644
index 0000000..8caa673
--- /dev/null
+++ b/src/pipewire/impl.h
@@ -0,0 +1,62 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_IMPL_H
+#define PIPEWIRE_IMPL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \addtogroup api_pw_impl
+ */
+
+struct pw_impl_client;
+struct pw_impl_module;
+struct pw_global;
+struct pw_node;
+struct pw_impl_port;
+struct pw_resource;
+
+#include <pipewire/pipewire.h>
+#include <pipewire/control.h>
+#include <pipewire/impl-core.h>
+#include <pipewire/impl-client.h>
+#include <pipewire/impl-device.h>
+#include <pipewire/impl-factory.h>
+#include <pipewire/global.h>
+#include <pipewire/impl-link.h>
+#include <pipewire/impl-metadata.h>
+#include <pipewire/impl-module.h>
+#include <pipewire/impl-node.h>
+#include <pipewire/impl-port.h>
+#include <pipewire/resource.h>
+#include <pipewire/work-queue.h>
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_IMPL_H */
diff --git a/src/pipewire/introspect.c b/src/pipewire/introspect.c
new file mode 100644
index 0000000..e52ef28
--- /dev/null
+++ b/src/pipewire/introspect.c
@@ -0,0 +1,590 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+
+#include <spa/pod/builder.h>
+
+#include "pipewire/pipewire.h"
+
+#include "pipewire/core.h"
+
+SPA_EXPORT
+const char *pw_node_state_as_string(enum pw_node_state state)
+{
+ switch (state) {
+ case PW_NODE_STATE_ERROR:
+ return "error";
+ case PW_NODE_STATE_CREATING:
+ return "creating";
+ case PW_NODE_STATE_SUSPENDED:
+ return "suspended";
+ case PW_NODE_STATE_IDLE:
+ return "idle";
+ case PW_NODE_STATE_RUNNING:
+ return "running";
+ }
+ return "invalid-state";
+}
+
+SPA_EXPORT
+const char *pw_direction_as_string(enum pw_direction direction)
+{
+ switch (direction) {
+ case PW_DIRECTION_INPUT:
+ return "input";
+ case PW_DIRECTION_OUTPUT:
+ return "output";
+ }
+ return "invalid";
+}
+
+SPA_EXPORT
+const char *pw_link_state_as_string(enum pw_link_state state)
+{
+ switch (state) {
+ case PW_LINK_STATE_ERROR:
+ return "error";
+ case PW_LINK_STATE_UNLINKED:
+ return "unlinked";
+ case PW_LINK_STATE_INIT:
+ return "init";
+ case PW_LINK_STATE_NEGOTIATING:
+ return "negotiating";
+ case PW_LINK_STATE_ALLOCATING:
+ return "allocating";
+ case PW_LINK_STATE_PAUSED:
+ return "paused";
+ case PW_LINK_STATE_ACTIVE:
+ return "active";
+ }
+ return "invalid-state";
+}
+
+static void pw_spa_dict_destroy(struct spa_dict *dict)
+{
+ const struct spa_dict_item *item;
+
+ spa_dict_for_each(item, dict) {
+ free((void *) item->key);
+ free((void *) item->value);
+ }
+ free((void*)dict->items);
+ free(dict);
+}
+
+static struct spa_dict *pw_spa_dict_copy(struct spa_dict *dict)
+{
+ struct spa_dict *copy;
+ struct spa_dict_item *items;
+ uint32_t i;
+
+ if (dict == NULL)
+ return NULL;
+
+ copy = calloc(1, sizeof(struct spa_dict));
+ if (copy == NULL)
+ goto no_mem;
+ copy->items = items = calloc(dict->n_items, sizeof(struct spa_dict_item));
+ if (copy->items == NULL)
+ goto no_items;
+ copy->n_items = dict->n_items;
+
+ for (i = 0; i < dict->n_items; i++) {
+ items[i].key = strdup(dict->items[i].key);
+ items[i].value = dict->items[i].value ? strdup(dict->items[i].value) : NULL;
+ }
+ return copy;
+
+ no_items:
+ free(copy);
+ no_mem:
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_core_info *pw_core_info_merge(struct pw_core_info *info,
+ const struct pw_core_info *update, bool reset)
+{
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ info = calloc(1, sizeof(struct pw_core_info));
+ if (info == NULL)
+ return NULL;
+
+ info->id = update->id;
+ info->cookie = update->cookie;
+ info->user_name = update->user_name ? strdup(update->user_name) : NULL;
+ info->host_name = update->host_name ? strdup(update->host_name) : NULL;
+ info->version = update->version ? strdup(update->version) : NULL;
+ info->name = update->name ? strdup(update->name) : NULL;
+ }
+ if (reset)
+ info->change_mask = 0;
+ info->change_mask |= update->change_mask;
+
+ if (update->change_mask & PW_CORE_CHANGE_MASK_PROPS) {
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ info->props = pw_spa_dict_copy(update->props);
+ }
+ return info;
+}
+
+SPA_EXPORT
+struct pw_core_info *pw_core_info_update(struct pw_core_info *info,
+ const struct pw_core_info *update)
+{
+ return pw_core_info_merge(info, update, true);
+}
+
+SPA_EXPORT
+void pw_core_info_free(struct pw_core_info *info)
+{
+ free((void *) info->user_name);
+ free((void *) info->host_name);
+ free((void *) info->version);
+ free((void *) info->name);
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ free(info);
+}
+
+SPA_EXPORT
+struct pw_node_info *pw_node_info_merge(struct pw_node_info *info,
+ const struct pw_node_info *update, bool reset)
+{
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ info = calloc(1, sizeof(struct pw_node_info));
+ if (info == NULL)
+ return NULL;
+
+ info->id = update->id;
+ info->max_input_ports = update->max_input_ports;
+ info->max_output_ports = update->max_output_ports;
+ }
+ if (reset)
+ info->change_mask = 0;
+ info->change_mask |= update->change_mask;
+
+ if (update->change_mask & PW_NODE_CHANGE_MASK_INPUT_PORTS) {
+ info->n_input_ports = update->n_input_ports;
+ }
+ if (update->change_mask & PW_NODE_CHANGE_MASK_OUTPUT_PORTS) {
+ info->n_output_ports = update->n_output_ports;
+ }
+ if (update->change_mask & PW_NODE_CHANGE_MASK_STATE) {
+ info->state = update->state;
+ free((void *) info->error);
+ info->error = update->error ? strdup(update->error) : NULL;
+ }
+ if (update->change_mask & PW_NODE_CHANGE_MASK_PROPS) {
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ info->props = pw_spa_dict_copy(update->props);
+ }
+ if (update->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ uint32_t i, n_params = update->n_params;
+ void *np;
+
+ np = pw_reallocarray(info->params, n_params, sizeof(struct spa_param_info));
+ if (np == NULL) {
+ free(info->params);
+ info->params = NULL;
+ info->n_params = n_params = 0;
+ }
+ info->params = np;
+
+ for (i = 0; i < SPA_MIN(info->n_params, n_params); i++) {
+ info->params[i].id = update->params[i].id;
+ if (reset)
+ info->params[i].user = 0;
+ if (info->params[i].flags != update->params[i].flags) {
+ info->params[i].flags = update->params[i].flags;
+ info->params[i].user++;
+ }
+ }
+ info->n_params = n_params;
+ for (; i < info->n_params; i++) {
+ info->params[i].id = update->params[i].id;
+ info->params[i].flags = update->params[i].flags;
+ info->params[i].user = 1;
+ }
+ }
+ return info;
+}
+
+SPA_EXPORT
+struct pw_node_info *pw_node_info_update(struct pw_node_info *info,
+ const struct pw_node_info *update)
+{
+ return pw_node_info_merge(info, update, true);
+}
+
+SPA_EXPORT
+void pw_node_info_free(struct pw_node_info *info)
+{
+ free((void *) info->error);
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ free((void *) info->params);
+ free(info);
+}
+
+SPA_EXPORT
+struct pw_port_info *pw_port_info_merge(struct pw_port_info *info,
+ const struct pw_port_info *update, bool reset)
+{
+
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ info = calloc(1, sizeof(struct pw_port_info));
+ if (info == NULL)
+ return NULL;
+
+ info->id = update->id;
+ info->direction = update->direction;
+ }
+ if (reset)
+ info->change_mask = 0;
+ info->change_mask |= update->change_mask;
+
+ if (update->change_mask & PW_PORT_CHANGE_MASK_PROPS) {
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ info->props = pw_spa_dict_copy(update->props);
+ }
+ if (update->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
+ uint32_t i, n_params = update->n_params;
+ void *np;
+
+ np = pw_reallocarray(info->params, n_params, sizeof(struct spa_param_info));
+ if (np == NULL) {
+ free(info->params);
+ info->params = NULL;
+ info->n_params = n_params = 0;
+ }
+ info->params = np;
+
+ for (i = 0; i < SPA_MIN(info->n_params, n_params); i++) {
+ info->params[i].id = update->params[i].id;
+ if (reset)
+ info->params[i].user = 0;
+ if (info->params[i].flags != update->params[i].flags) {
+ info->params[i].flags = update->params[i].flags;
+ info->params[i].user++;
+ }
+ }
+ info->n_params = n_params;
+ for (; i < info->n_params; i++) {
+ info->params[i].id = update->params[i].id;
+ info->params[i].flags = update->params[i].flags;
+ info->params[i].user = 1;
+ }
+ }
+ return info;
+}
+
+SPA_EXPORT
+struct pw_port_info *pw_port_info_update(struct pw_port_info *info,
+ const struct pw_port_info *update)
+{
+ return pw_port_info_merge(info, update, true);
+}
+
+SPA_EXPORT
+void pw_port_info_free(struct pw_port_info *info)
+{
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ free((void *) info->params);
+ free(info);
+}
+
+SPA_EXPORT
+struct pw_factory_info *pw_factory_info_merge(struct pw_factory_info *info,
+ const struct pw_factory_info *update, bool reset)
+{
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ info = calloc(1, sizeof(struct pw_factory_info));
+ if (info == NULL)
+ return NULL;
+
+ info->id = update->id;
+ info->name = update->name ? strdup(update->name) : NULL;
+ info->type = update->type ? strdup(update->type) : NULL;
+ info->version = update->version;
+ }
+ if (reset)
+ info->change_mask = 0;
+ info->change_mask |= update->change_mask;
+
+ if (update->change_mask & PW_FACTORY_CHANGE_MASK_PROPS) {
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ info->props = pw_spa_dict_copy(update->props);
+ }
+ return info;
+}
+
+SPA_EXPORT
+struct pw_factory_info *pw_factory_info_update(struct pw_factory_info *info,
+ const struct pw_factory_info *update)
+{
+ return pw_factory_info_merge(info, update, true);
+}
+
+SPA_EXPORT
+void pw_factory_info_free(struct pw_factory_info *info)
+{
+ free((void *) info->name);
+ free((void *) info->type);
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ free(info);
+}
+
+SPA_EXPORT
+struct pw_module_info *pw_module_info_merge(struct pw_module_info *info,
+ const struct pw_module_info *update, bool reset)
+{
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ info = calloc(1, sizeof(struct pw_module_info));
+ if (info == NULL)
+ return NULL;
+
+ info->id = update->id;
+ info->name = update->name ? strdup(update->name) : NULL;
+ info->filename = update->filename ? strdup(update->filename) : NULL;
+ info->args = update->args ? strdup(update->args) : NULL;
+ }
+ if (reset)
+ info->change_mask = 0;
+ info->change_mask |= update->change_mask;
+
+ if (update->change_mask & PW_MODULE_CHANGE_MASK_PROPS) {
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ info->props = pw_spa_dict_copy(update->props);
+ }
+ return info;
+}
+
+SPA_EXPORT
+struct pw_module_info *pw_module_info_update(struct pw_module_info *info,
+ const struct pw_module_info *update)
+{
+ return pw_module_info_merge(info, update, true);
+}
+
+SPA_EXPORT
+void pw_module_info_free(struct pw_module_info *info)
+{
+ free((void *) info->name);
+ free((void *) info->filename);
+ free((void *) info->args);
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ free(info);
+}
+
+SPA_EXPORT
+struct pw_device_info *pw_device_info_merge(struct pw_device_info *info,
+ const struct pw_device_info *update, bool reset)
+{
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ info = calloc(1, sizeof(struct pw_device_info));
+ if (info == NULL)
+ return NULL;
+
+ info->id = update->id;
+ }
+ if (reset)
+ info->change_mask = 0;
+ info->change_mask |= update->change_mask;
+
+ if (update->change_mask & PW_DEVICE_CHANGE_MASK_PROPS) {
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ info->props = pw_spa_dict_copy(update->props);
+ }
+ if (update->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
+ uint32_t i, n_params = update->n_params;
+ void *np;
+
+ np = pw_reallocarray(info->params, n_params, sizeof(struct spa_param_info));
+ if (np == NULL) {
+ free(info->params);
+ info->params = NULL;
+ info->n_params = n_params = 0;
+ }
+ info->params = np;
+
+ for (i = 0; i < SPA_MIN(info->n_params, n_params); i++) {
+ info->params[i].id = update->params[i].id;
+ if (reset)
+ info->params[i].user = 0;
+ if (info->params[i].flags != update->params[i].flags) {
+ info->params[i].flags = update->params[i].flags;
+ info->params[i].user++;
+ }
+ }
+ info->n_params = n_params;
+ for (; i < info->n_params; i++) {
+ info->params[i].id = update->params[i].id;
+ info->params[i].flags = update->params[i].flags;
+ info->params[i].user = 1;
+ }
+ }
+ return info;
+}
+
+SPA_EXPORT
+struct pw_device_info *pw_device_info_update(struct pw_device_info *info,
+ const struct pw_device_info *update)
+{
+ return pw_device_info_merge(info, update, true);
+}
+
+SPA_EXPORT
+void pw_device_info_free(struct pw_device_info *info)
+{
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ free((void *) info->params);
+ free(info);
+}
+
+SPA_EXPORT
+struct pw_client_info *pw_client_info_merge(struct pw_client_info *info,
+ const struct pw_client_info *update, bool reset)
+{
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ info = calloc(1, sizeof(struct pw_client_info));
+ if (info == NULL)
+ return NULL;
+
+ info->id = update->id;
+ }
+ if (reset)
+ info->change_mask = 0;
+ info->change_mask |= update->change_mask;
+
+ if (update->change_mask & PW_CLIENT_CHANGE_MASK_PROPS) {
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ info->props = pw_spa_dict_copy(update->props);
+ }
+ return info;
+}
+
+SPA_EXPORT
+struct pw_client_info *pw_client_info_update(struct pw_client_info *info,
+ const struct pw_client_info *update)
+{
+ return pw_client_info_merge(info, update, true);
+}
+
+SPA_EXPORT
+void pw_client_info_free(struct pw_client_info *info)
+{
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ free(info);
+}
+
+SPA_EXPORT
+struct pw_link_info *pw_link_info_merge(struct pw_link_info *info,
+ const struct pw_link_info *update, bool reset)
+{
+ if (update == NULL)
+ return info;
+
+ if (info == NULL) {
+ info = calloc(1, sizeof(struct pw_link_info));
+ if (info == NULL)
+ return NULL;
+
+ info->id = update->id;
+ info->output_node_id = update->output_node_id;
+ info->output_port_id = update->output_port_id;
+ info->input_node_id = update->input_node_id;
+ info->input_port_id = update->input_port_id;
+ }
+ if (reset)
+ info->change_mask = 0;
+ info->change_mask |= update->change_mask;
+
+ if (update->change_mask & PW_LINK_CHANGE_MASK_STATE) {
+ info->state = update->state;
+ free((void *) info->error);
+ info->error = update->error ? strdup(update->error) : NULL;
+ }
+ if (update->change_mask & PW_LINK_CHANGE_MASK_FORMAT) {
+ free(info->format);
+ info->format = update->format ? spa_pod_copy(update->format) : NULL;
+ }
+ if (update->change_mask & PW_LINK_CHANGE_MASK_PROPS) {
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ info->props = pw_spa_dict_copy(update->props);
+ }
+ return info;
+}
+
+SPA_EXPORT
+struct pw_link_info *pw_link_info_update(struct pw_link_info *info,
+ const struct pw_link_info *update)
+{
+ return pw_link_info_merge(info, update, true);
+}
+
+SPA_EXPORT
+void pw_link_info_free(struct pw_link_info *info)
+{
+ free((void *) info->error);
+ free(info->format);
+ if (info->props)
+ pw_spa_dict_destroy(info->props);
+ free(info);
+}
diff --git a/src/pipewire/keys.h b/src/pipewire/keys.h
new file mode 100644
index 0000000..b0faf61
--- /dev/null
+++ b/src/pipewire/keys.h
@@ -0,0 +1,356 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_KEYS_H
+#define PIPEWIRE_KEYS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <pipewire/utils.h>
+/**
+ * \defgroup pw_keys Key Names
+ *
+ * A collection of keys that are used to add extra information on objects.
+ *
+ * Keys that start with "pipewire." are in general set-once and then
+ * read-only. They are usually used for security sensitive information that
+ * needs to be fixed.
+ *
+ * Properties from other objects can also appear. This usually suggests some
+ * sort of parent/child or owner/owned relationship.
+ *
+ * \addtogroup pw_keys
+ * \{
+ */
+#define PW_KEY_PROTOCOL "pipewire.protocol" /**< protocol used for connection */
+#define PW_KEY_ACCESS "pipewire.access" /**< how the client access is controlled */
+#define PW_KEY_CLIENT_ACCESS "pipewire.client.access"/**< how the client wants to be access
+ * controlled */
+
+/** Various keys related to the identity of a client process and its security.
+ * Must be obtained from trusted sources by the protocol and placed as
+ * read-only properties. */
+#define PW_KEY_SEC_PID "pipewire.sec.pid" /**< Client pid, set by protocol */
+#define PW_KEY_SEC_UID "pipewire.sec.uid" /**< Client uid, set by protocol*/
+#define PW_KEY_SEC_GID "pipewire.sec.gid" /**< client gid, set by protocol*/
+#define PW_KEY_SEC_LABEL "pipewire.sec.label" /**< client security label, set by protocol*/
+
+#define PW_KEY_LIBRARY_NAME_SYSTEM "library.name.system" /**< name of the system library to use */
+#define PW_KEY_LIBRARY_NAME_LOOP "library.name.loop" /**< name of the loop library to use */
+#define PW_KEY_LIBRARY_NAME_DBUS "library.name.dbus" /**< name of the dbus library to use */
+
+/** object properties */
+#define PW_KEY_OBJECT_PATH "object.path" /**< unique path to construct the object */
+#define PW_KEY_OBJECT_ID "object.id" /**< a global object id */
+#define PW_KEY_OBJECT_SERIAL "object.serial" /**< a 64 bit object serial number. This is a number
+ * incremented for each object that is created.
+ * The lower 32 bits are guaranteed to never be
+ * SPA_ID_INVALID. */
+#define PW_KEY_OBJECT_LINGER "object.linger" /**< the object lives on even after the client
+ * that created it has been destroyed */
+#define PW_KEY_OBJECT_REGISTER "object.register" /**< If the object should be registered. */
+
+
+/* config */
+#define PW_KEY_CONFIG_PREFIX "config.prefix" /**< a config prefix directory */
+#define PW_KEY_CONFIG_NAME "config.name" /**< a config file name */
+#define PW_KEY_CONFIG_OVERRIDE_PREFIX "config.override.prefix" /**< a config override prefix directory */
+#define PW_KEY_CONFIG_OVERRIDE_NAME "config.override.name" /**< a config override file name */
+
+/* context */
+#define PW_KEY_CONTEXT_PROFILE_MODULES "context.profile.modules" /**< a context profile for modules, deprecated */
+#define PW_KEY_USER_NAME "context.user-name" /**< The user name that runs pipewire */
+#define PW_KEY_HOST_NAME "context.host-name" /**< The host name of the machine */
+
+/* core */
+#define PW_KEY_CORE_NAME "core.name" /**< The name of the core. Default is
+ * `pipewire-<username>-<pid>`, overwritten
+ * by env(PIPEWIRE_CORE) */
+#define PW_KEY_CORE_VERSION "core.version" /**< The version of the core. */
+#define PW_KEY_CORE_DAEMON "core.daemon" /**< If the core is listening for connections. */
+
+#define PW_KEY_CORE_ID "core.id" /**< the core id */
+#define PW_KEY_CORE_MONITORS "core.monitors" /**< the apis monitored by core. */
+
+/* cpu */
+#define PW_KEY_CPU_MAX_ALIGN "cpu.max-align" /**< maximum alignment needed to support
+ * all CPU optimizations */
+#define PW_KEY_CPU_CORES "cpu.cores" /**< number of cores */
+
+/* priorities */
+#define PW_KEY_PRIORITY_SESSION "priority.session" /**< priority in session manager */
+#define PW_KEY_PRIORITY_DRIVER "priority.driver" /**< priority to be a driver */
+
+/* remote keys */
+#define PW_KEY_REMOTE_NAME "remote.name" /**< The name of the remote to connect to,
+ * default pipewire-0, overwritten by
+ * env(PIPEWIRE_REMOTE) */
+#define PW_KEY_REMOTE_INTENTION "remote.intention" /**< The intention of the remote connection,
+ * "generic", "screencast" */
+
+/** application keys */
+#define PW_KEY_APP_NAME "application.name" /**< application name. Ex: "Totem Music Player" */
+#define PW_KEY_APP_ID "application.id" /**< a textual id for identifying an
+ * application logically. Ex: "org.gnome.Totem" */
+#define PW_KEY_APP_VERSION "application.version" /**< application version. Ex: "1.2.0" */
+#define PW_KEY_APP_ICON "application.icon" /**< aa base64 blob with PNG image data */
+#define PW_KEY_APP_ICON_NAME "application.icon-name" /**< an XDG icon name for the application.
+ * Ex: "totem" */
+#define PW_KEY_APP_LANGUAGE "application.language" /**< application language if applicable, in
+ * standard POSIX format. Ex: "en_GB" */
+
+#define PW_KEY_APP_PROCESS_ID "application.process.id" /**< process id (pid)*/
+#define PW_KEY_APP_PROCESS_BINARY "application.process.binary" /**< binary name */
+#define PW_KEY_APP_PROCESS_USER "application.process.user" /**< user name */
+#define PW_KEY_APP_PROCESS_HOST "application.process.host" /**< host name */
+#define PW_KEY_APP_PROCESS_MACHINE_ID "application.process.machine-id" /**< the D-Bus host id the
+ * application runs on */
+#define PW_KEY_APP_PROCESS_SESSION_ID "application.process.session-id" /**< login session of the
+ * application, on Unix the
+ * value of $XDG_SESSION_ID. */
+/** window system */
+#define PW_KEY_WINDOW_X11_DISPLAY "window.x11.display" /**< the X11 display string. Ex. ":0.0" */
+
+/** Client properties */
+#define PW_KEY_CLIENT_ID "client.id" /**< a client id */
+#define PW_KEY_CLIENT_NAME "client.name" /**< the client name */
+#define PW_KEY_CLIENT_API "client.api" /**< the client api used to access
+ * PipeWire */
+
+/** Node keys */
+#define PW_KEY_NODE_ID "node.id" /**< node id */
+#define PW_KEY_NODE_NAME "node.name" /**< node name */
+#define PW_KEY_NODE_NICK "node.nick" /**< short node name */
+#define PW_KEY_NODE_DESCRIPTION "node.description" /**< localized human readable node one-line
+ * description. Ex. "Foobar USB Headset" */
+#define PW_KEY_NODE_PLUGGED "node.plugged" /**< when the node was created. As a uint64 in
+ * nanoseconds. */
+
+#define PW_KEY_NODE_SESSION "node.session" /**< the session id this node is part of */
+#define PW_KEY_NODE_GROUP "node.group" /**< the group id this node is part of. Nodes
+ * in the same group are always scheduled
+ * with the same driver. */
+#define PW_KEY_NODE_EXCLUSIVE "node.exclusive" /**< node wants exclusive access to resources */
+#define PW_KEY_NODE_AUTOCONNECT "node.autoconnect" /**< node wants to be automatically connected
+ * to a compatible node */
+#define PW_KEY_NODE_LATENCY "node.latency" /**< the requested latency of the node as
+ * a fraction. Ex: 128/48000 */
+#define PW_KEY_NODE_MAX_LATENCY "node.max-latency" /**< the maximum supported latency of the
+ * node as a fraction. Ex: 1024/48000 */
+#define PW_KEY_NODE_LOCK_QUANTUM "node.lock-quantum" /**< don't change quantum when this node
+ * is active */
+#define PW_KEY_NODE_FORCE_QUANTUM "node.force-quantum" /**< force a quantum while the node is
+ * active */
+#define PW_KEY_NODE_RATE "node.rate" /**< the requested rate of the graph as
+ * a fraction. Ex: 1/48000 */
+#define PW_KEY_NODE_LOCK_RATE "node.lock-rate" /**< don't change rate when this node
+ * is active */
+#define PW_KEY_NODE_FORCE_RATE "node.force-rate" /**< force a rate while the node is
+ * active */
+
+#define PW_KEY_NODE_DONT_RECONNECT "node.dont-reconnect" /**< don't reconnect this node. The node is
+ * initially linked to target.object or the
+ * default node. If the target is removed,
+ * the node is destroyed */
+#define PW_KEY_NODE_ALWAYS_PROCESS "node.always-process" /**< process even when unlinked */
+#define PW_KEY_NODE_WANT_DRIVER "node.want-driver" /**< the node wants to be grouped with a driver
+ * node in order to schedule the graph. */
+#define PW_KEY_NODE_PAUSE_ON_IDLE "node.pause-on-idle" /**< pause the node when idle */
+#define PW_KEY_NODE_SUSPEND_ON_IDLE "node.suspend-on-idle" /**< suspend the node when idle */
+#define PW_KEY_NODE_CACHE_PARAMS "node.cache-params" /**< cache the node params */
+#define PW_KEY_NODE_TRANSPORT_SYNC "node.transport.sync" /**< the node handles transport sync */
+#define PW_KEY_NODE_DRIVER "node.driver" /**< node can drive the graph */
+#define PW_KEY_NODE_STREAM "node.stream" /**< node is a stream, the server side should
+ * add a converter */
+#define PW_KEY_NODE_VIRTUAL "node.virtual" /**< the node is some sort of virtual
+ * object */
+#define PW_KEY_NODE_PASSIVE "node.passive" /**< indicate that a node wants passive links
+ * on output/input/all ports when the value is
+ * "out"/"in"/"true" respectively */
+#define PW_KEY_NODE_LINK_GROUP "node.link-group" /**< the node is internally linked to
+ * nodes with the same link-group */
+#define PW_KEY_NODE_NETWORK "node.network" /**< the node is on a network */
+#define PW_KEY_NODE_TRIGGER "node.trigger" /**< the node is not scheduled automatically
+ * based on the dependencies in the graph
+ * but it will be triggered explicitly. */
+#define PW_KEY_NODE_CHANNELNAMES "node.channel-names" /**< names of node's
+ * channels (unrelated to positions) */
+#define PW_KEY_NODE_DEVICE_PORT_NAME_PREFIX "node.device-port-name-prefix" /** override
+ * port name prefix for device ports, like capture and playback
+ * or disable the prefix completely if an empty string is provided */
+
+/** Port keys */
+#define PW_KEY_PORT_ID "port.id" /**< port id */
+#define PW_KEY_PORT_NAME "port.name" /**< port name */
+#define PW_KEY_PORT_DIRECTION "port.direction" /**< the port direction, one of "in" or "out"
+ * or "control" and "notify" for control ports */
+#define PW_KEY_PORT_ALIAS "port.alias" /**< port alias */
+#define PW_KEY_PORT_PHYSICAL "port.physical" /**< if this is a physical port */
+#define PW_KEY_PORT_TERMINAL "port.terminal" /**< if this port consumes the data */
+#define PW_KEY_PORT_CONTROL "port.control" /**< if this port is a control port */
+#define PW_KEY_PORT_MONITOR "port.monitor" /**< if this port is a monitor port */
+#define PW_KEY_PORT_CACHE_PARAMS "port.cache-params" /**< cache the node port params */
+#define PW_KEY_PORT_EXTRA "port.extra" /**< api specific extra port info, API name
+ * should be prefixed. "jack:flags:56" */
+
+/** link properties */
+#define PW_KEY_LINK_ID "link.id" /**< a link id */
+#define PW_KEY_LINK_INPUT_NODE "link.input.node" /**< input node id of a link */
+#define PW_KEY_LINK_INPUT_PORT "link.input.port" /**< input port id of a link */
+#define PW_KEY_LINK_OUTPUT_NODE "link.output.node" /**< output node id of a link */
+#define PW_KEY_LINK_OUTPUT_PORT "link.output.port" /**< output port id of a link */
+#define PW_KEY_LINK_PASSIVE "link.passive" /**< indicate that a link is passive and
+ * does not cause the graph to be
+ * runnable. */
+#define PW_KEY_LINK_FEEDBACK "link.feedback" /**< indicate that a link is a feedback
+ * link and the target will receive data
+ * in the next cycle */
+
+/** device properties */
+#define PW_KEY_DEVICE_ID "device.id" /**< device id */
+#define PW_KEY_DEVICE_NAME "device.name" /**< device name */
+#define PW_KEY_DEVICE_PLUGGED "device.plugged" /**< when the device was created. As a uint64 in
+ * nanoseconds. */
+#define PW_KEY_DEVICE_NICK "device.nick" /**< a short device nickname */
+#define PW_KEY_DEVICE_STRING "device.string" /**< device string in the underlying layer's
+ * format. Ex. "surround51:0" */
+#define PW_KEY_DEVICE_API "device.api" /**< API this device is accessed with.
+ * Ex. "alsa", "v4l2" */
+#define PW_KEY_DEVICE_DESCRIPTION "device.description" /**< localized human readable device one-line
+ * description. Ex. "Foobar USB Headset" */
+#define PW_KEY_DEVICE_BUS_PATH "device.bus-path" /**< bus path to the device in the OS'
+ * format. Ex. "pci-0000:00:14.0-usb-0:3.2:1.0" */
+#define PW_KEY_DEVICE_SERIAL "device.serial" /**< Serial number if applicable */
+#define PW_KEY_DEVICE_VENDOR_ID "device.vendor.id" /**< vendor ID if applicable */
+#define PW_KEY_DEVICE_VENDOR_NAME "device.vendor.name" /**< vendor name if applicable */
+#define PW_KEY_DEVICE_PRODUCT_ID "device.product.id" /**< product ID if applicable */
+#define PW_KEY_DEVICE_PRODUCT_NAME "device.product.name" /**< product name if applicable */
+#define PW_KEY_DEVICE_CLASS "device.class" /**< device class */
+#define PW_KEY_DEVICE_FORM_FACTOR "device.form-factor" /**< form factor if applicable. One of
+ * "internal", "speaker", "handset", "tv",
+ * "webcam", "microphone", "headset",
+ * "headphone", "hands-free", "car", "hifi",
+ * "computer", "portable" */
+#define PW_KEY_DEVICE_BUS "device.bus" /**< bus of the device if applicable. One of
+ * "isa", "pci", "usb", "firewire",
+ * "bluetooth" */
+#define PW_KEY_DEVICE_SUBSYSTEM "device.subsystem" /**< device subsystem */
+#define PW_KEY_DEVICE_SYSFS_PATH "device.sysfs.path" /**< device sysfs path */
+#define PW_KEY_DEVICE_ICON "device.icon" /**< icon for the device. A base64 blob
+ * containing PNG image data */
+#define PW_KEY_DEVICE_ICON_NAME "device.icon-name" /**< an XDG icon name for the device.
+ * Ex. "sound-card-speakers-usb" */
+#define PW_KEY_DEVICE_INTENDED_ROLES "device.intended-roles" /**< intended use. A space separated list of
+ * roles (see PW_KEY_MEDIA_ROLE) this device
+ * is particularly well suited for, due to
+ * latency, quality or form factor. */
+#define PW_KEY_DEVICE_CACHE_PARAMS "device.cache-params" /**< cache the device spa params */
+
+/** module properties */
+#define PW_KEY_MODULE_ID "module.id" /**< the module id */
+#define PW_KEY_MODULE_NAME "module.name" /**< the name of the module */
+#define PW_KEY_MODULE_AUTHOR "module.author" /**< the author's name */
+#define PW_KEY_MODULE_DESCRIPTION "module.description" /**< a human readable one-line description
+ * of the module's purpose.*/
+#define PW_KEY_MODULE_USAGE "module.usage" /**< a human readable usage description of
+ * the module's arguments. */
+#define PW_KEY_MODULE_VERSION "module.version" /**< a version string for the module. */
+
+/** Factory properties */
+#define PW_KEY_FACTORY_ID "factory.id" /**< the factory id */
+#define PW_KEY_FACTORY_NAME "factory.name" /**< the name of the factory */
+#define PW_KEY_FACTORY_USAGE "factory.usage" /**< the usage of the factory */
+#define PW_KEY_FACTORY_TYPE_NAME "factory.type.name" /**< the name of the type created by a factory */
+#define PW_KEY_FACTORY_TYPE_VERSION "factory.type.version" /**< the version of the type created by a factory */
+
+/** Stream properties */
+#define PW_KEY_STREAM_IS_LIVE "stream.is-live" /**< Indicates that the stream is live. */
+#define PW_KEY_STREAM_LATENCY_MIN "stream.latency.min" /**< The minimum latency of the stream. */
+#define PW_KEY_STREAM_LATENCY_MAX "stream.latency.max" /**< The maximum latency of the stream */
+#define PW_KEY_STREAM_MONITOR "stream.monitor" /**< Indicates that the stream is monitoring
+ * and might select a less accurate but faster
+ * conversion algorithm. */
+#define PW_KEY_STREAM_DONT_REMIX "stream.dont-remix" /**< don't remix channels */
+#define PW_KEY_STREAM_CAPTURE_SINK "stream.capture.sink" /**< Try to capture the sink output instead of
+ * source output */
+
+/** Media */
+#define PW_KEY_MEDIA_TYPE "media.type" /**< Media type, one of
+ * Audio, Video, Midi */
+#define PW_KEY_MEDIA_CATEGORY "media.category" /**< Media Category:
+ * Playback, Capture, Duplex, Monitor, Manager */
+#define PW_KEY_MEDIA_ROLE "media.role" /**< Role: Movie, Music, Camera,
+ * Screen, Communication, Game,
+ * Notification, DSP, Production,
+ * Accessibility, Test */
+#define PW_KEY_MEDIA_CLASS "media.class" /**< class Ex: "Video/Source" */
+#define PW_KEY_MEDIA_NAME "media.name" /**< media name. Ex: "Pink Floyd: Time" */
+#define PW_KEY_MEDIA_TITLE "media.title" /**< title. Ex: "Time" */
+#define PW_KEY_MEDIA_ARTIST "media.artist" /**< artist. Ex: "Pink Floyd" */
+#define PW_KEY_MEDIA_COPYRIGHT "media.copyright" /**< copyright string */
+#define PW_KEY_MEDIA_SOFTWARE "media.software" /**< generator software */
+#define PW_KEY_MEDIA_LANGUAGE "media.language" /**< language in POSIX format. Ex: en_GB */
+#define PW_KEY_MEDIA_FILENAME "media.filename" /**< filename */
+#define PW_KEY_MEDIA_ICON "media.icon" /**< icon for the media, a base64 blob with
+ * PNG image data */
+#define PW_KEY_MEDIA_ICON_NAME "media.icon-name" /**< an XDG icon name for the media.
+ * Ex: "audio-x-mp3" */
+#define PW_KEY_MEDIA_COMMENT "media.comment" /**< extra comment */
+#define PW_KEY_MEDIA_DATE "media.date" /**< date of the media */
+#define PW_KEY_MEDIA_FORMAT "media.format" /**< format of the media */
+
+/** format related properties */
+#define PW_KEY_FORMAT_DSP "format.dsp" /**< a dsp format.
+ * Ex: "32 bit float mono audio" */
+/** audio related properties */
+#define PW_KEY_AUDIO_CHANNEL "audio.channel" /**< an audio channel. Ex: "FL" */
+#define PW_KEY_AUDIO_RATE "audio.rate" /**< an audio samplerate */
+#define PW_KEY_AUDIO_CHANNELS "audio.channels" /**< number of audio channels */
+#define PW_KEY_AUDIO_FORMAT "audio.format" /**< an audio format. Ex: "S16LE" */
+#define PW_KEY_AUDIO_ALLOWED_RATES "audio.allowed-rates" /**< a list of allowed samplerates
+ * ex. "[ 44100 48000 ]" */
+
+/** video related properties */
+#define PW_KEY_VIDEO_RATE "video.framerate" /**< a video framerate */
+#define PW_KEY_VIDEO_FORMAT "video.format" /**< a video format */
+#define PW_KEY_VIDEO_SIZE "video.size" /**< a video size as "<width>x<height" */
+
+#define PW_KEY_TARGET_OBJECT "target.object" /**< a target object to link to. This can be
+ * and object name or object.serial */
+
+#ifndef PW_REMOVE_DEPRECATED
+#define PW_KEY_PRIORITY_MASTER PW_DEPRECATED("priority.master") /**< deprecated, use priority.driver */
+#define PW_KEY_NODE_TARGET PW_DEPRECATED("node.target") /**< deprecated since 0.3.64, use target.object. */
+#endif /* PW_REMOVE_DEPRECATED */
+
+/** \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_KEYS_H */
diff --git a/src/pipewire/link.h b/src/pipewire/link.h
new file mode 100644
index 0000000..8130f4b
--- /dev/null
+++ b/src/pipewire/link.h
@@ -0,0 +1,148 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_LINK_H
+#define PIPEWIRE_LINK_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include <pipewire/proxy.h>
+
+/** \defgroup pw_link Link
+ *
+ * A link is the connection between 2 nodes (\ref pw_node). Nodes are
+ * linked together on ports.
+ *
+ * The link is responsible for negotiating the format and buffers for
+ * the nodes.
+ *
+ */
+
+/**
+ * \addtogroup pw_link
+ * \{
+ */
+
+#define PW_TYPE_INTERFACE_Link PW_TYPE_INFO_INTERFACE_BASE "Link"
+
+#define PW_VERSION_LINK 3
+struct pw_link;
+
+/** \enum pw_link_state The different link states */
+enum pw_link_state {
+ PW_LINK_STATE_ERROR = -2, /**< the link is in error */
+ PW_LINK_STATE_UNLINKED = -1, /**< the link is unlinked */
+ PW_LINK_STATE_INIT = 0, /**< the link is initialized */
+ PW_LINK_STATE_NEGOTIATING = 1, /**< the link is negotiating formats */
+ PW_LINK_STATE_ALLOCATING = 2, /**< the link is allocating buffers */
+ PW_LINK_STATE_PAUSED = 3, /**< the link is paused */
+ PW_LINK_STATE_ACTIVE = 4, /**< the link is active */
+};
+
+/** Convert a \ref pw_link_state to a readable string */
+const char * pw_link_state_as_string(enum pw_link_state state);
+/** The link information. Extra information can be added in later versions */
+struct pw_link_info {
+ uint32_t id; /**< id of the global */
+ uint32_t output_node_id; /**< server side output node id */
+ uint32_t output_port_id; /**< output port id */
+ uint32_t input_node_id; /**< server side input node id */
+ uint32_t input_port_id; /**< input port id */
+#define PW_LINK_CHANGE_MASK_STATE (1 << 0)
+#define PW_LINK_CHANGE_MASK_FORMAT (1 << 1)
+#define PW_LINK_CHANGE_MASK_PROPS (1 << 2)
+#define PW_LINK_CHANGE_MASK_ALL ((1 << 3)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ enum pw_link_state state; /**< the current state of the link */
+ const char *error; /**< an error reason if \a state is error */
+ struct spa_pod *format; /**< format over link */
+ struct spa_dict *props; /**< the properties of the link */
+};
+
+struct pw_link_info *
+pw_link_info_update(struct pw_link_info *info,
+ const struct pw_link_info *update);
+
+struct pw_link_info *
+pw_link_info_merge(struct pw_link_info *info,
+ const struct pw_link_info *update, bool reset);
+
+void
+pw_link_info_free(struct pw_link_info *info);
+
+
+#define PW_LINK_EVENT_INFO 0
+#define PW_LINK_EVENT_NUM 1
+
+/** Link events */
+struct pw_link_events {
+#define PW_VERSION_LINK_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify link info
+ *
+ * \param info info about the link
+ */
+ void (*info) (void *data, const struct pw_link_info *info);
+};
+
+#define PW_LINK_METHOD_ADD_LISTENER 0
+#define PW_LINK_METHOD_NUM 1
+
+/** Link methods */
+struct pw_link_methods {
+#define PW_VERSION_LINK_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_link_events *events,
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_LINK_H */
diff --git a/src/pipewire/log.c b/src/pipewire/log.c
new file mode 100644
index 0000000..2484109
--- /dev/null
+++ b/src/pipewire/log.c
@@ -0,0 +1,291 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <limits.h>
+#include <fnmatch.h>
+
+#include <spa/support/log-impl.h>
+
+#include <spa/pod/pod.h>
+#include <spa/debug/types.h>
+#include <spa/pod/iter.h>
+#include <spa/utils/list.h>
+
+#include <pipewire/log.h>
+#include <pipewire/private.h>
+
+SPA_LOG_IMPL(default_log);
+
+#define DEFAULT_LOG_LEVEL SPA_LOG_LEVEL_WARN
+
+SPA_EXPORT
+enum spa_log_level pw_log_level = DEFAULT_LOG_LEVEL;
+
+static struct spa_log *global_log = &default_log.log;
+
+SPA_EXPORT
+struct spa_log_topic *PW_LOG_TOPIC_DEFAULT;
+
+PW_LOG_TOPIC_STATIC(log_topic, "pw.log"); /* log topic for this file here */
+PW_LOG_TOPIC(log_buffers, "pw.buffers");
+PW_LOG_TOPIC(log_client, "pw.client");
+PW_LOG_TOPIC(log_conf, "pw.conf");
+PW_LOG_TOPIC(log_context, "pw.context");
+PW_LOG_TOPIC(log_core, "pw.core");
+PW_LOG_TOPIC(log_data_loop, "pw.data-loop");
+PW_LOG_TOPIC(log_device, "pw.device");
+PW_LOG_TOPIC(log_factory, "pw.factory");
+PW_LOG_TOPIC(log_filter, "pw.filter");
+PW_LOG_TOPIC(log_global, "pw.global");
+PW_LOG_TOPIC(log_link, "pw.link");
+PW_LOG_TOPIC(log_loop, "pw.loop");
+PW_LOG_TOPIC(log_main_loop, "pw.main-loop");
+PW_LOG_TOPIC(log_mem, "pw.mem");
+PW_LOG_TOPIC(log_metadata, "pw.metadata");
+PW_LOG_TOPIC(log_module, "pw.module");
+PW_LOG_TOPIC(log_node, "pw.node");
+PW_LOG_TOPIC(log_port, "pw.port");
+PW_LOG_TOPIC(log_properties, "pw.props");
+PW_LOG_TOPIC(log_protocol, "pw.protocol");
+PW_LOG_TOPIC(log_proxy, "pw.proxy");
+PW_LOG_TOPIC(log_resource, "pw.resource");
+PW_LOG_TOPIC(log_stream, "pw.stream");
+PW_LOG_TOPIC(log_thread_loop, "pw.thread-loop");
+PW_LOG_TOPIC(log_work_queue, "pw.work-queue");
+
+PW_LOG_TOPIC(PW_LOG_TOPIC_DEFAULT, "default");
+
+/** Set the global log interface
+ * \param log the global log to set
+ */
+SPA_EXPORT
+void pw_log_set(struct spa_log *log)
+{
+ global_log = log ? log : &default_log.log;
+ global_log->level = pw_log_level;
+}
+
+bool pw_log_is_default(void)
+{
+ return global_log == &default_log.log;
+}
+
+/** Get the global log interface
+ * \return the global log
+ */
+SPA_EXPORT
+struct spa_log *pw_log_get(void)
+{
+ return global_log;
+}
+
+/** Set the global log level
+ * \param level the new log level
+ */
+SPA_EXPORT
+void pw_log_set_level(enum spa_log_level level)
+{
+ pw_log_level = level;
+ global_log->level = level;
+}
+
+/** Log a message for the given topic
+ * \param level the log level
+ * \param topic the topic
+ * \param file the file this message originated from
+ * \param line the line number
+ * \param func the function
+ * \param fmt the printf style format
+ * \param ... printf style arguments to log
+ *
+ */
+SPA_EXPORT
+void
+pw_log_logt(enum spa_log_level level,
+ const struct spa_log_topic *topic,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt, ...)
+{
+ if (SPA_UNLIKELY(pw_log_topic_enabled(level, topic))) {
+ va_list args;
+ va_start(args, fmt);
+ spa_log_logtv(global_log, level, topic, file, line, func, fmt, args);
+ va_end(args);
+ }
+}
+
+/** Log a message for the given topic with va_list
+ * \param level the log level
+ * \param topic the topic
+ * \param file the file this message originated from
+ * \param line the line number
+ * \param func the function
+ * \param fmt the printf style format
+ * \param args a va_list of arguments
+ *
+ */
+SPA_EXPORT
+void
+pw_log_logtv(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)
+{
+ spa_log_logtv(global_log, level, topic, file, line, func, fmt, args);
+}
+
+
+/** Log a message for the default topic with va_list
+ * \param level the log level
+ * \param file the file this message originated from
+ * \param line the line number
+ * \param func the function
+ * \param fmt the printf style format
+ * \param args a va_list of arguments
+ *
+ */
+SPA_EXPORT
+void
+pw_log_logv(enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt,
+ va_list args)
+{
+ pw_log_logtv(level, PW_LOG_TOPIC_DEFAULT, file, line, func, fmt, args);
+}
+
+/** Log a message for the default topic
+ * \param level the log level
+ * \param file the file this message originated from
+ * \param line the line number
+ * \param func the function
+ * \param fmt the printf style format
+ * \param ... printf style arguments to log
+ *
+ */
+SPA_EXPORT
+void
+pw_log_log(enum spa_log_level level,
+ const char *file,
+ int line,
+ const char *func,
+ const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ pw_log_logtv(level, PW_LOG_TOPIC_DEFAULT, file, line, func, fmt, args);
+ va_end(args);
+}
+
+/** \fn void pw_log_error (const char *format, ...)
+ * Log an error message
+ * \param format a printf style format
+ * \param ... printf style arguments
+ */
+/** \fn void pw_log_warn (const char *format, ...)
+ * Log a warning message
+ * \param format a printf style format
+ * \param ... printf style arguments
+ */
+/** \fn void pw_log_info (const char *format, ...)
+ * Log an info message
+ * \param format a printf style format
+ * \param ... printf style arguments
+ */
+/** \fn void pw_log_debug (const char *format, ...)
+ * Log a debug message
+ * \param format a printf style format
+ * \param ... printf style arguments
+ */
+/** \fn void pw_log_trace (const char *format, ...)
+ * Log a trace message. Trace messages may be generated from
+ * \param format a printf style format
+ * \param ... printf style arguments
+ * realtime threads
+ */
+
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+void pw_log_log_object(enum spa_log_level level,
+ const struct spa_log_topic *topic, const char *file,
+ int line, const char *func, uint32_t flags, const void *object)
+{
+ struct spa_debug_log_ctx ctx = SPA_LOGF_DEBUG_INIT(global_log, level,
+ topic, file, line, func );
+ if (flags & PW_LOG_OBJECT_POD) {
+ const struct spa_pod *pod = object;
+ if (pod == NULL) {
+ pw_log_logt(level, topic, file, line, func, "NULL");
+ } else {
+ spa_debugc_pod(&ctx.ctx, 0, SPA_TYPE_ROOT, pod);
+ }
+ }
+}
+
+SPA_EXPORT
+void
+_pw_log_topic_new(struct spa_log_topic *topic)
+{
+ spa_log_topic_init(global_log, topic);
+}
+
+void
+pw_log_init(void)
+{
+ PW_LOG_TOPIC_INIT(PW_LOG_TOPIC_DEFAULT);
+ PW_LOG_TOPIC_INIT(log_buffers);
+ PW_LOG_TOPIC_INIT(log_client);
+ PW_LOG_TOPIC_INIT(log_conf);
+ PW_LOG_TOPIC_INIT(log_context);
+ PW_LOG_TOPIC_INIT(log_core);
+ PW_LOG_TOPIC_INIT(log_data_loop);
+ PW_LOG_TOPIC_INIT(log_device);
+ PW_LOG_TOPIC_INIT(log_factory);
+ PW_LOG_TOPIC_INIT(log_filter);
+ PW_LOG_TOPIC_INIT(log_global);
+ PW_LOG_TOPIC_INIT(log_link);
+ PW_LOG_TOPIC_INIT(log_loop);
+ PW_LOG_TOPIC_INIT(log_main_loop);
+ PW_LOG_TOPIC_INIT(log_mem);
+ PW_LOG_TOPIC_INIT(log_metadata);
+ PW_LOG_TOPIC_INIT(log_module);
+ PW_LOG_TOPIC_INIT(log_node);
+ PW_LOG_TOPIC_INIT(log_port);
+ PW_LOG_TOPIC_INIT(log_properties);
+ PW_LOG_TOPIC_INIT(log_protocol);
+ PW_LOG_TOPIC_INIT(log_proxy);
+ PW_LOG_TOPIC_INIT(log_resource);
+ PW_LOG_TOPIC_INIT(log_stream);
+ PW_LOG_TOPIC_INIT(log_thread_loop);
+ PW_LOG_TOPIC_INIT(log_topic);
+ PW_LOG_TOPIC_INIT(log_work_queue);
+}
diff --git a/src/pipewire/log.h b/src/pipewire/log.h
new file mode 100644
index 0000000..f91dc11
--- /dev/null
+++ b/src/pipewire/log.h
@@ -0,0 +1,182 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_LOG_H
+#define PIPEWIRE_LOG_H
+
+#include <spa/support/log.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_log Logging
+ *
+ * \brief Logging functions of PipeWire
+ *
+ * Logging is performed to stdout and stderr. Trace logging is performed
+ * in a lockfree ringbuffer and written out from the main thread as to not
+ * block the realtime threads.
+ */
+
+/**
+ * \addtogroup pw_log
+ * \{
+ */
+/** The global log level */
+extern enum spa_log_level pw_log_level;
+
+extern struct spa_log_topic *PW_LOG_TOPIC_DEFAULT;
+
+/** Configure a logging module. This is usually done automatically
+ * in pw_init() but you can install a custom logger before calling
+ * pw_init(). */
+void pw_log_set(struct spa_log *log);
+
+/** Get the log interface */
+struct spa_log *pw_log_get(void);
+
+/** Configure the logging level */
+void pw_log_set_level(enum spa_log_level level);
+
+/** Log a message for a topic */
+void
+pw_log_logt(enum spa_log_level level,
+ const struct spa_log_topic *topic,
+ const char *file,
+ int line, const char *func,
+ const char *fmt, ...) SPA_PRINTF_FUNC(6, 7);
+
+/** Log a message for a topic */
+void
+pw_log_logtv(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) SPA_PRINTF_FUNC(6, 0);
+
+
+
+/** Log a message for the default topic */
+void
+pw_log_log(enum spa_log_level level,
+ const char *file,
+ int line, const char *func,
+ const char *fmt, ...) SPA_PRINTF_FUNC(5, 6);
+
+/** Log a message for the default topic */
+void
+pw_log_logv(enum spa_log_level level,
+ const char *file,
+ int line, const char *func,
+ const char *fmt, va_list args) SPA_PRINTF_FUNC(5, 0);
+
+/** Initialize the log topic. The returned topic is owned by the pipewire
+ * context and the topic must not be modified or freed.
+ * Do not use this function directly, use one of PW_LOG_TOPIC_* instead.
+ *
+ * \see PW_LOG_TOPIC_STATIC
+ * \see PW_LOG_TOPIC_EXTERN
+ * \see PW_LOG_TOPIC
+ */
+void
+_pw_log_topic_new(struct spa_log_topic *topic);
+
+/**
+ * Declare a static log topic named \a var. The usual usage is:
+ * \code
+ * PW_LOG_TOPIC_STATIC(my_topic);
+ * #define PW_LOG_TOPIC_DEFAULT my_topic
+ *
+ * void foo() {
+ * pw_log_debug("bar");
+ * }
+ * \endcode
+ */
+#define PW_LOG_TOPIC_STATIC(var, topic) \
+ static struct spa_log_topic var##__LINE__ = SPA_LOG_TOPIC(0, topic); \
+ static struct spa_log_topic *(var) = &(var##__LINE__)
+
+/**
+ * Declare a static log topic named \a var.
+ * See \ref PW_LOG_TOPIC_STATIC for an example usage.
+ */
+#define PW_LOG_TOPIC_EXTERN(var) \
+ extern struct spa_log_topic *var
+
+/**
+ * Declare a static log topic named \a var.
+ * See \ref PW_LOG_TOPIC_STATIC for an example usage.
+ */
+#define PW_LOG_TOPIC(var, topic) \
+ struct spa_log_topic var##__LINE__ = SPA_LOG_TOPIC(0, topic); \
+ struct spa_log_topic *(var) = &(var##__LINE__)
+
+#define PW_LOG_TOPIC_INIT(var) \
+ spa_log_topic_init(pw_log_get(), var);
+
+/** Check if a loglevel is enabled */
+#define pw_log_level_enabled(lev) (pw_log_level >= (lev))
+#define pw_log_topic_enabled(lev,t) ((t) && (t)->has_custom_level ? (t)->level >= (lev) : pw_log_level_enabled((lev)))
+
+#define pw_logtv(lev,topic,fmt,ap) \
+({ \
+ if (SPA_UNLIKELY(pw_log_topic_enabled(lev,topic))) \
+ pw_log_logtv(lev,topic,__FILE__,__LINE__,__func__,fmt,ap); \
+})
+
+#define pw_logt(lev,topic,...) \
+({ \
+ if (SPA_UNLIKELY(pw_log_topic_enabled(lev,topic))) \
+ pw_log_logt(lev,topic,__FILE__,__LINE__,__func__,__VA_ARGS__); \
+})
+
+#define pw_log(lev,...) pw_logt(lev,PW_LOG_TOPIC_DEFAULT,__VA_ARGS__)
+
+#define pw_log_error(...) pw_log(SPA_LOG_LEVEL_ERROR,__VA_ARGS__)
+#define pw_log_warn(...) pw_log(SPA_LOG_LEVEL_WARN,__VA_ARGS__)
+#define pw_log_info(...) pw_log(SPA_LOG_LEVEL_INFO,__VA_ARGS__)
+#define pw_log_debug(...) pw_log(SPA_LOG_LEVEL_DEBUG,__VA_ARGS__)
+#define pw_log_trace(...) pw_log(SPA_LOG_LEVEL_TRACE,__VA_ARGS__)
+
+#define pw_logt_error(t,...) pw_logt(SPA_LOG_LEVEL_ERROR,t,__VA_ARGS__)
+#define pw_logt_warn(t,...) pw_logt(SPA_LOG_LEVEL_WARN,t,__VA_ARGS__)
+#define pw_logt_info(t,...) pw_logt(SPA_LOG_LEVEL_INFO,t,__VA_ARGS__)
+#define pw_logt_debug(t,...) pw_logt(SPA_LOG_LEVEL_DEBUG,t,__VA_ARGS__)
+#define pw_logt_trace(t,...) pw_logt(SPA_LOG_LEVEL_TRACE,t,__VA_ARGS__)
+
+#ifndef FASTPATH
+#define pw_log_trace_fp(...) pw_log(SPA_LOG_LEVEL_TRACE,__VA_ARGS__)
+#else
+#define pw_log_trace_fp(...)
+#endif
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* PIPEWIRE_LOG_H */
diff --git a/src/pipewire/loop.c b/src/pipewire/loop.c
new file mode 100644
index 0000000..da766a6
--- /dev/null
+++ b/src/pipewire/loop.c
@@ -0,0 +1,162 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include <spa/support/loop.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/loop.h>
+#include <pipewire/log.h>
+#include <pipewire/type.h>
+
+PW_LOG_TOPIC_EXTERN(log_loop);
+#define PW_LOG_TOPIC_DEFAULT log_loop
+
+/** \cond */
+
+struct impl {
+ struct pw_loop this;
+
+ struct spa_handle *system_handle;
+ struct spa_handle *loop_handle;
+};
+/** \endcond */
+
+/** Create a new loop
+ * \returns a newly allocated loop
+ */
+SPA_EXPORT
+struct pw_loop *pw_loop_new(const struct spa_dict *props)
+{
+ int res;
+ struct impl *impl;
+ struct pw_loop *this;
+ void *iface;
+ struct spa_support support[32];
+ uint32_t n_support;
+ const char *lib;
+
+ n_support = pw_get_support(support, 32);
+
+ impl = calloc(1, sizeof(struct impl));
+ if (impl == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ this = &impl->this;
+
+ if (props)
+ lib = spa_dict_lookup(props, PW_KEY_LIBRARY_NAME_SYSTEM);
+ else
+ lib = NULL;
+
+ impl->system_handle = pw_load_spa_handle(lib,
+ SPA_NAME_SUPPORT_SYSTEM,
+ props, n_support, support);
+ if (impl->system_handle == NULL) {
+ res = -errno;
+ pw_log_error("%p: can't make "SPA_NAME_SUPPORT_SYSTEM" handle: %m", this);
+ goto error_free;
+ }
+
+ if ((res = spa_handle_get_interface(impl->system_handle,
+ SPA_TYPE_INTERFACE_System,
+ &iface)) < 0) {
+ pw_log_error("%p: can't get System interface: %s", this, spa_strerror(res));
+ goto error_unload_system;
+ }
+ this->system = iface;
+
+ support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, iface);
+
+ if (props)
+ lib = spa_dict_lookup(props, PW_KEY_LIBRARY_NAME_LOOP);
+ else
+ lib = NULL;
+
+ impl->loop_handle = pw_load_spa_handle(lib,
+ SPA_NAME_SUPPORT_LOOP, props,
+ n_support, support);
+ if (impl->loop_handle == NULL) {
+ res = -errno;
+ pw_log_error("%p: can't make "SPA_NAME_SUPPORT_LOOP" handle: %m", this);
+ goto error_unload_system;
+ }
+
+ if ((res = spa_handle_get_interface(impl->loop_handle,
+ SPA_TYPE_INTERFACE_Loop,
+ &iface)) < 0) {
+ pw_log_error("%p: can't get Loop interface: %s",
+ this, spa_strerror(res));
+ goto error_unload_loop;
+ }
+ this->loop = iface;
+
+ if ((res = spa_handle_get_interface(impl->loop_handle,
+ SPA_TYPE_INTERFACE_LoopControl,
+ &iface)) < 0) {
+ pw_log_error("%p: can't get LoopControl interface: %s",
+ this, spa_strerror(res));
+ goto error_unload_loop;
+ }
+ this->control = iface;
+
+ if ((res = spa_handle_get_interface(impl->loop_handle,
+ SPA_TYPE_INTERFACE_LoopUtils,
+ &iface)) < 0) {
+ pw_log_error("%p: can't get LoopUtils interface: %s",
+ this, spa_strerror(res));
+ goto error_unload_loop;
+ }
+ this->utils = iface;
+
+ return this;
+
+error_unload_loop:
+ pw_unload_spa_handle(impl->loop_handle);
+error_unload_system:
+ pw_unload_spa_handle(impl->system_handle);
+error_free:
+ free(impl);
+error_cleanup:
+ errno = -res;
+ return NULL;
+}
+
+/** Destroy a loop
+ * \param loop a loop to destroy
+ */
+SPA_EXPORT
+void pw_loop_destroy(struct pw_loop *loop)
+{
+ struct impl *impl = SPA_CONTAINER_OF(loop, struct impl, this);
+
+ pw_unload_spa_handle(impl->loop_handle);
+ pw_unload_spa_handle(impl->system_handle);
+ free(impl);
+}
diff --git a/src/pipewire/loop.h b/src/pipewire/loop.h
new file mode 100644
index 0000000..42a630d
--- /dev/null
+++ b/src/pipewire/loop.h
@@ -0,0 +1,90 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_LOOP_H
+#define PIPEWIRE_LOOP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/support/loop.h>
+#include <spa/utils/dict.h>
+
+/** \defgroup pw_loop Loop
+ *
+ * PipeWire loop object provides an implementation of
+ * the spa loop interfaces. It can be used to implement various
+ * event loops.
+ */
+
+/**
+ * \addtogroup pw_loop
+ * \{
+ */
+
+struct pw_loop {
+ struct spa_system *system; /**< system utils */
+ struct spa_loop *loop; /**< wrapped loop */
+ struct spa_loop_control *control; /**< loop control */
+ struct spa_loop_utils *utils; /**< loop utils */
+};
+
+struct pw_loop *
+pw_loop_new(const struct spa_dict *props);
+
+void
+pw_loop_destroy(struct pw_loop *loop);
+
+#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_iterate(l,...) spa_loop_control_iterate((l)->control,__VA_ARGS__)
+#define pw_loop_leave(l) spa_loop_control_leave((l)->control)
+
+#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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_LOOP_H */
diff --git a/src/pipewire/main-loop.c b/src/pipewire/main-loop.c
new file mode 100644
index 0000000..4e8c8aa
--- /dev/null
+++ b/src/pipewire/main-loop.c
@@ -0,0 +1,157 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pipewire/log.h"
+#include "pipewire/main-loop.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_main_loop);
+#define PW_LOG_TOPIC_DEFAULT log_main_loop
+
+static int do_stop(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *data, size_t size, void *user_data)
+{
+ struct pw_main_loop *this = user_data;
+ pw_log_debug("%p: do stop", this);
+ this->running = false;
+ return 0;
+}
+
+static struct pw_main_loop *loop_new(struct pw_loop *loop, const struct spa_dict *props)
+{
+ struct pw_main_loop *this;
+ int res;
+
+ this = calloc(1, sizeof(struct pw_main_loop));
+ if (this == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ pw_log_debug("%p: new", this);
+
+ if (loop == NULL) {
+ loop = pw_loop_new(props);
+ this->created = true;
+ }
+ if (loop == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+ this->loop = loop;
+
+ spa_hook_list_init(&this->listener_list);
+
+ return this;
+
+error_free:
+ free(this);
+error_cleanup:
+ errno = -res;
+ return NULL;
+}
+
+/** Create a new main loop
+ * \return a newly allocated \ref pw_main_loop
+ *
+ */
+SPA_EXPORT
+struct pw_main_loop *pw_main_loop_new(const struct spa_dict *props)
+{
+ return loop_new(NULL, props);
+}
+
+/** Destroy a main loop
+ * \param loop the main loop to destroy
+ *
+ */
+SPA_EXPORT
+void pw_main_loop_destroy(struct pw_main_loop *loop)
+{
+ pw_log_debug("%p: destroy", loop);
+ pw_main_loop_emit_destroy(loop);
+
+ if (loop->created)
+ pw_loop_destroy(loop->loop);
+
+ spa_hook_list_clean(&loop->listener_list);
+
+ free(loop);
+}
+
+SPA_EXPORT
+void pw_main_loop_add_listener(struct pw_main_loop *loop,
+ struct spa_hook *listener,
+ const struct pw_main_loop_events *events,
+ void *data)
+{
+ spa_hook_list_append(&loop->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+struct pw_loop * pw_main_loop_get_loop(struct pw_main_loop *loop)
+{
+ return loop->loop;
+}
+
+/** Stop a main loop
+ * \param loop a \ref pw_main_loop to stop
+ *
+ * The call to \ref pw_main_loop_run() will return
+ *
+ */
+SPA_EXPORT
+int pw_main_loop_quit(struct pw_main_loop *loop)
+{
+ pw_log_debug("%p: quit", loop);
+ return pw_loop_invoke(loop->loop, do_stop, 1, NULL, 0, false, loop);
+}
+
+/** Start a main loop
+ * \param loop the main loop to start
+ *
+ * Start running \a loop. This function blocks until \ref pw_main_loop_quit()
+ * has been called
+ *
+ */
+SPA_EXPORT
+int pw_main_loop_run(struct pw_main_loop *loop)
+{
+ int res = 0;
+
+ pw_log_debug("%p: run", loop);
+
+ loop->running = true;
+ pw_loop_enter(loop->loop);
+ while (loop->running) {
+ if ((res = pw_loop_iterate(loop->loop, -1)) < 0) {
+ if (res == -EINTR)
+ continue;
+ pw_log_warn("%p: iterate error %d (%s)",
+ loop, res, spa_strerror(res));
+ }
+ }
+ pw_loop_leave(loop->loop);
+ return res;
+}
diff --git a/src/pipewire/main-loop.h b/src/pipewire/main-loop.h
new file mode 100644
index 0000000..2225bee
--- /dev/null
+++ b/src/pipewire/main-loop.h
@@ -0,0 +1,86 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_MAIN_LOOP_H
+#define PIPEWIRE_MAIN_LOOP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_main_loop Main Loop
+ *
+ * A main loop object
+ */
+
+/**
+ * \addtogroup pw_main_loop
+ * \{
+ */
+
+/** A main loop object */
+struct pw_main_loop;
+
+#include <pipewire/loop.h>
+
+/** Events of the main loop */
+struct pw_main_loop_events {
+#define PW_VERSION_MAIN_LOOP_EVENTS 0
+ uint32_t version;
+
+ /** Emitted when the main loop is destroyed */
+ void (*destroy) (void *data);
+};
+
+/** Create a new main loop. */
+struct pw_main_loop *
+pw_main_loop_new(const struct spa_dict *props);
+
+/** Add an event listener */
+void pw_main_loop_add_listener(struct pw_main_loop *loop,
+ struct spa_hook *listener,
+ const struct pw_main_loop_events *events,
+ void *data);
+
+/** Get the loop implementation */
+struct pw_loop * pw_main_loop_get_loop(struct pw_main_loop *loop);
+
+/** Destroy a loop */
+void pw_main_loop_destroy(struct pw_main_loop *loop);
+
+/** Run a main loop. This blocks until \ref pw_main_loop_quit is called */
+int pw_main_loop_run(struct pw_main_loop *loop);
+
+/** Quit a main loop */
+int pw_main_loop_quit(struct pw_main_loop *loop);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_MAIN_LOOP_H */
diff --git a/src/pipewire/map.h b/src/pipewire/map.h
new file mode 100644
index 0000000..9a46082
--- /dev/null
+++ b/src/pipewire/map.h
@@ -0,0 +1,252 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_MAP_H
+#define PIPEWIRE_MAP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <string.h>
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+#include <pipewire/array.h>
+
+/** \defgroup pw_map Map
+ *
+ * \brief A map that holds pointers to objects indexed by id
+ *
+ * The map is a sparse version of the \ref pw_array "pw_array" that manages the
+ * indices of elements for the caller. Adding items with
+ * pw_map_insert_new() returns the assigned index for that item; if items
+ * are removed the map re-uses indices to keep the array at the minimum
+ * required size.
+ *
+ * \code{.c}
+ * struct pw_map map = PW_MAP_INIT(4);
+ *
+ * idx1 = pw_map_insert_new(&map, ptr1);
+ * idx2 = pw_map_insert_new(&map, ptr2);
+ * // the map is now [ptr1, ptr2], size 2
+ * pw_map_remove(&map, idx1);
+ * // the map is now [<unused>, ptr2], size 2
+ * pw_map_insert_new(&map, ptr3);
+ * // the map is now [ptr3, ptr2], size 2
+ * \endcode
+ */
+
+/**
+ * \addtogroup pw_map
+ * \{
+ */
+
+/** \private
+ * An entry in the map. This is used internally only. Each element in the
+ * backing pw_array is a union pw_map_item. For real items, the data pointer
+ * points to the item. If an element has been removed, pw_map->free_list
+ * is the index of the most recently removed item. That item contains
+ * the index of the next removed item until item->next is SPA_ID_INVALID.
+ *
+ * The free list is prepended only, the last item to be removed will be the
+ * first item to get re-used on the next insert.
+ */
+union pw_map_item {
+ uintptr_t next; /* next free index */
+ void *data; /* data of this item, must be an even address */
+};
+
+/** A map. This struct should be treated as opaque by the caller. */
+struct pw_map {
+ struct pw_array items; /* an array with the map items */
+ uint32_t free_list; /* first free index */
+};
+
+/** \param extend the amount of bytes to grow the map with when needed */
+#define PW_MAP_INIT(extend) ((struct pw_map) { PW_ARRAY_INIT(extend), SPA_ID_INVALID })
+
+/**
+ * Get the number of currently allocated elements in the map.
+ * \note pw_map_get_size() returns the currently allocated number of
+ * elements in the map, not the number of actually set elements.
+ * \return the number of available elements before the map needs to grow
+ */
+#define pw_map_get_size(m) pw_array_get_len(&(m)->items, union pw_map_item)
+#define pw_map_get_item(m,id) pw_array_get_unchecked(&(m)->items,id,union pw_map_item)
+#define pw_map_item_is_free(item) ((item)->next & 0x1)
+#define pw_map_id_is_free(m,id) (pw_map_item_is_free(pw_map_get_item(m,id)))
+/** \return true if the id fits within the current map size */
+#define pw_map_check_id(m,id) ((id) < pw_map_get_size(m))
+/** \return true if there is a valid item at \a id */
+#define pw_map_has_item(m,id) (pw_map_check_id(m,id) && !pw_map_id_is_free(m, id))
+#define pw_map_lookup_unchecked(m,id) pw_map_get_item(m,id)->data
+
+/** Convert an id to a pointer that can be inserted into the map */
+#define PW_MAP_ID_TO_PTR(id) (SPA_UINT32_TO_PTR((id)<<1))
+/** Convert a pointer to an id that can be retrieved from the map */
+#define PW_MAP_PTR_TO_ID(p) (SPA_PTR_TO_UINT32(p)>>1)
+
+/** Initialize a map
+ * \param map the map to initialize
+ * \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_array_init(&map->items, extend * sizeof(union pw_map_item));
+ pw_array_ensure_size(&map->items, size * sizeof(union pw_map_item));
+ map->free_list = SPA_ID_INVALID;
+}
+
+/** 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_array_clear(&map->items);
+}
+
+/** 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_array_reset(&map->items);
+ map->free_list = SPA_ID_INVALID;
+}
+
+/** Insert data in the map. This function causes the map to grow if required.
+ * \param map the map to insert into
+ * \param data the item to add
+ * \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)
+{
+ union pw_map_item *start, *item;
+ uint32_t id;
+
+ if (map->free_list != SPA_ID_INVALID) {
+ start = (union pw_map_item *) map->items.data;
+ item = &start[map->free_list >> 1]; /* lsb always 1, see pw_map_remove */
+ map->free_list = item->next;
+ } else {
+ item = (union pw_map_item *) pw_array_add(&map->items, sizeof(union pw_map_item));
+ if (item == NULL)
+ return SPA_ID_INVALID;
+ start = (union pw_map_item *) map->items.data;
+ }
+ item->data = data;
+ id = (item - start);
+ return id;
+}
+
+/** Replace the data in the map at an index.
+ *
+ * \param map the map to insert into
+ * \param id the index to insert at, must be less or equal to pw_map_get_size()
+ * \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)
+{
+ size_t size = pw_map_get_size(map);
+ union pw_map_item *item;
+
+ if (id > size)
+ return -ENOSPC;
+ else if (id == size) {
+ item = (union pw_map_item *) pw_array_add(&map->items, sizeof(union pw_map_item));
+ if (item == NULL)
+ return -errno;
+ } else {
+ item = pw_map_get_item(map, id);
+ if (pw_map_item_is_free(item))
+ return -EINVAL;
+ }
+ item->data = data;
+ return 0;
+}
+
+/** Remove an item at index. The id may get re-used in the future.
+ *
+ * \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)
+{
+ if (pw_map_id_is_free(map, id))
+ return;
+
+ pw_map_get_item(map, id)->next = map->free_list;
+ map->free_list = (id << 1) | 1;
+}
+
+/** Find an item in the map
+ * \param map the map to use
+ * \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(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);
+ if (!pw_map_item_is_free(item))
+ return item->data;
+ }
+ return NULL;
+}
+
+/** Iterate all map items
+ * \param map the map to iterate
+ * \param func the function to call for each item, the item data and \a data is
+ * passed to the function. When \a func returns a non-zero result,
+ * iteration ends and the result is returned.
+ * \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(struct pw_map *map,
+ int (*func) (void *item_data, void *data), void *data)
+{
+ union pw_map_item *item;
+ int res = 0;
+
+ pw_array_for_each(item, &map->items) {
+ if (!pw_map_item_is_free(item))
+ if ((res = func(item->data, data)) != 0)
+ break;
+ }
+ return res;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_MAP_H */
diff --git a/src/pipewire/mem.c b/src/pipewire/mem.c
new file mode 100644
index 0000000..ae9e1e4
--- /dev/null
+++ b/src/pipewire/mem.c
@@ -0,0 +1,808 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <sys/syscall.h>
+
+#include <spa/utils/list.h>
+#include <spa/buffer/buffer.h>
+
+#include <pipewire/log.h>
+#include <pipewire/map.h>
+#include <pipewire/mem.h>
+
+PW_LOG_TOPIC_EXTERN(log_mem);
+#define PW_LOG_TOPIC_DEFAULT log_mem
+
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) && !defined(HAVE_MEMFD_CREATE)
+/*
+ * No glibc wrappers exist for memfd_create(2), so provide our own.
+ *
+ * Also define memfd fcntl sealing macros. While they are already
+ * defined in the kernel header file <linux/fcntl.h>, that file as
+ * a whole conflicts with the original glibc header <fnctl.h>.
+ */
+
+static inline int memfd_create(const char *name, unsigned int flags)
+{
+ return syscall(SYS_memfd_create, name, flags);
+}
+
+#define HAVE_MEMFD_CREATE 1
+#endif
+
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#define MAP_LOCKED 0
+#endif
+
+/* memfd_create(2) flags */
+
+#ifndef MFD_CLOEXEC
+#define MFD_CLOEXEC 0x0001U
+#endif
+
+#ifndef MFD_ALLOW_SEALING
+#define MFD_ALLOW_SEALING 0x0002U
+#endif
+
+/* fcntl() seals-related flags */
+
+#ifndef F_LINUX_SPECIFIC_BASE
+#define F_LINUX_SPECIFIC_BASE 1024
+#endif
+
+#ifndef F_ADD_SEALS
+#define F_ADD_SEALS (F_LINUX_SPECIFIC_BASE + 9)
+#define F_GET_SEALS (F_LINUX_SPECIFIC_BASE + 10)
+
+#define F_SEAL_SEAL 0x0001 /* prevent further seals from being set */
+#define F_SEAL_SHRINK 0x0002 /* prevent file from shrinking */
+#define F_SEAL_GROW 0x0004 /* prevent file from growing */
+#define F_SEAL_WRITE 0x0008 /* prevent writes */
+#endif
+
+#define pw_mempool_emit(p,m,v,...) spa_hook_list_call(&p->listener_list, struct pw_mempool_events, m, v, ##__VA_ARGS__)
+#define pw_mempool_emit_destroy(p) pw_mempool_emit(p, destroy, 0)
+#define pw_mempool_emit_added(p,b) pw_mempool_emit(p, added, 0, b)
+#define pw_mempool_emit_removed(p,b) pw_mempool_emit(p, removed, 0, b)
+
+struct mempool {
+ struct pw_mempool this;
+
+ struct spa_hook_list listener_list;
+
+ struct pw_map map; /* map memblock to id */
+ struct spa_list blocks; /* list of memblock */
+ uint32_t pagesize;
+};
+
+struct memblock {
+ struct pw_memblock this;
+ struct spa_list link; /* link in mempool */
+ struct spa_list mappings; /* list of struct mapping */
+ struct spa_list memmaps; /* list of struct memmap */
+};
+
+/* a mapped region of a block */
+struct mapping {
+ struct memblock *block;
+ int ref;
+ uint32_t offset;
+ uint32_t size;
+ unsigned int do_unmap:1;
+ struct spa_list link;
+ void *ptr;
+};
+
+/* a reference to a (part of a) mapped region */
+struct memmap {
+ struct pw_memmap this;
+ struct mapping *mapping;
+ struct spa_list link;
+};
+
+SPA_EXPORT
+struct pw_mempool *pw_mempool_new(struct pw_properties *props)
+{
+ struct mempool *impl;
+ struct pw_mempool *this;
+
+ impl = calloc(1, sizeof(struct mempool));
+ if (impl == NULL)
+ return NULL;
+
+ this = &impl->this;
+ this->props = props;
+
+ impl->pagesize = sysconf(_SC_PAGESIZE);
+
+ pw_log_debug("%p: new", this);
+
+ spa_hook_list_init(&impl->listener_list);
+ pw_map_init(&impl->map, 64, 64);
+ spa_list_init(&impl->blocks);
+
+ return this;
+}
+
+SPA_EXPORT
+void pw_mempool_clear(struct pw_mempool *pool)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+
+ pw_log_debug("%p: clear", pool);
+
+ spa_list_consume(b, &impl->blocks, link)
+ pw_memblock_free(&b->this);
+ pw_map_reset(&impl->map);
+}
+
+SPA_EXPORT
+void pw_mempool_destroy(struct pw_mempool *pool)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+
+ pw_log_debug("%p: destroy", pool);
+
+ pw_mempool_emit_destroy(impl);
+
+ pw_mempool_clear(pool);
+
+ spa_hook_list_clean(&impl->listener_list);
+
+ pw_map_clear(&impl->map);
+ pw_properties_free(pool->props);
+ free(impl);
+}
+
+SPA_EXPORT
+void pw_mempool_add_listener(struct pw_mempool *pool,
+ struct spa_hook *listener,
+ const struct pw_mempool_events *events,
+ void *data)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ spa_hook_list_append(&impl->listener_list, listener, events, data);
+}
+
+#if 0
+/** Map a memblock
+ * \param mem a memblock
+ * \return 0 on success, < 0 on error
+ */
+SPA_EXPORT
+int pw_memblock_map_old(struct pw_memblock *mem)
+{
+ if (mem->ptr != NULL)
+ return 0;
+
+ if (mem->flags & PW_MEMBLOCK_FLAG_MAP_READWRITE) {
+ int prot = 0;
+
+ if (mem->flags & PW_MEMBLOCK_FLAG_MAP_READ)
+ prot |= PROT_READ;
+ if (mem->flags & PW_MEMBLOCK_FLAG_MAP_WRITE)
+ prot |= PROT_WRITE;
+
+ if (mem->flags & PW_MEMBLOCK_FLAG_MAP_TWICE) {
+ void *ptr, *wrap;
+
+ mem->ptr =
+ mmap(NULL, mem->size << 1, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1,
+ 0);
+ if (mem->ptr == MAP_FAILED)
+ return -errno;
+
+ ptr =
+ mmap(mem->ptr, mem->size, prot, MAP_FIXED | MAP_SHARED, mem->fd,
+ mem->offset);
+ if (ptr != mem->ptr) {
+ munmap(mem->ptr, mem->size << 1);
+ return -ENOMEM;
+ }
+
+ wrap = SPA_PTROFF(mem->ptr, mem->size, void);
+
+ ptr =
+ mmap(wrap, mem->size, prot, MAP_FIXED | MAP_SHARED,
+ mem->fd, mem->offset);
+ if (ptr != wrap) {
+ munmap(mem->ptr, mem->size << 1);
+ return -ENOMEM;
+ }
+ } else {
+ mem->ptr = mmap(NULL, mem->size, prot, MAP_SHARED, mem->fd, 0);
+ if (mem->ptr == MAP_FAILED)
+ return -errno;
+ }
+ } else {
+ mem->ptr = NULL;
+ }
+
+ pw_log_debug("%p: map to %p", mem, mem->ptr);
+
+ return 0;
+}
+#endif
+
+static struct mapping * memblock_find_mapping(struct memblock *b,
+ uint32_t flags, uint32_t offset, uint32_t size)
+{
+ struct mapping *m;
+ struct pw_mempool *pool = b->this.pool;
+
+ spa_list_for_each(m, &b->mappings, link) {
+ pw_log_debug("%p: check %p offset:(%u <= %u) end:(%u >= %u)",
+ pool, m, m->offset, offset, m->offset + m->size,
+ offset + size);
+ if (m->offset <= offset && (m->offset + m->size) >= (offset + size)) {
+ pw_log_debug("%p: found %p id:%u fd:%d offs:%u size:%u ref:%d",
+ pool, &b->this, b->this.id, b->this.fd,
+ offset, size, b->this.ref);
+ return m;
+ }
+ }
+ return NULL;
+}
+
+static struct mapping * memblock_map(struct memblock *b,
+ enum pw_memmap_flags flags, uint32_t offset, uint32_t size)
+{
+ struct mempool *p = SPA_CONTAINER_OF(b->this.pool, struct mempool, this);
+ struct mapping *m;
+ void *ptr;
+ int prot = 0, fl = 0;
+
+ if (flags & PW_MEMMAP_FLAG_READ)
+ prot |= PROT_READ;
+ if (flags & PW_MEMMAP_FLAG_WRITE)
+ prot |= PROT_WRITE;
+
+ if (flags & PW_MEMMAP_FLAG_PRIVATE)
+ fl |= MAP_PRIVATE;
+ else
+ fl |= MAP_SHARED;
+
+ if (flags & PW_MEMMAP_FLAG_LOCKED)
+ fl |= MAP_LOCKED;
+
+ if (flags & PW_MEMMAP_FLAG_TWICE) {
+ pw_log_error("%p: implement me PW_MEMMAP_FLAG_TWICE", p);
+ errno = ENOTSUP;
+ return NULL;
+ }
+
+
+ ptr = mmap(NULL, size, prot, fl, b->this.fd, offset);
+ if (ptr == MAP_FAILED) {
+ pw_log_error("%p: Failed to mmap memory fd:%d offset:%u size:%u: %m",
+ p, b->this.fd, offset, size);
+ return NULL;
+ }
+
+ m = calloc(1, sizeof(struct mapping));
+ if (m == NULL) {
+ munmap(ptr, size);
+ return NULL;
+ }
+ m->ptr = ptr;
+ m->do_unmap = true;
+ m->block = b;
+ m->offset = offset;
+ m->size = size;
+ b->this.ref++;
+ spa_list_append(&b->mappings, &m->link);
+
+ pw_log_debug("%p: block:%p fd:%d map:%p ptr:%p (%u %u) block-ref:%d", p, &b->this,
+ b->this.fd, m, m->ptr, offset, size, b->this.ref);
+
+ return m;
+}
+
+static void mapping_free(struct mapping *m)
+{
+ struct memblock *b = m->block;
+ struct mempool *p = SPA_CONTAINER_OF(b->this.pool, struct mempool, this);
+
+ pw_log_debug("%p: mapping:%p block:%p fd:%d ptr:%p size:%u block-ref:%d",
+ p, m, b, b->this.fd, m->ptr, m->size, b->this.ref);
+
+ if (m->do_unmap)
+ munmap(m->ptr, m->size);
+ spa_list_remove(&m->link);
+ free(m);
+}
+
+static void mapping_unmap(struct mapping *m)
+{
+ struct memblock *b = m->block;
+ struct mempool *p = SPA_CONTAINER_OF(b->this.pool, struct mempool, this);
+ pw_log_debug("%p: mapping:%p block:%p fd:%d ptr:%p size:%u block-ref:%d",
+ p, m, b, b->this.fd, m->ptr, m->size, b->this.ref);
+ mapping_free(m);
+ pw_memblock_unref(&b->this);
+}
+
+SPA_EXPORT
+struct pw_memmap * pw_memblock_map(struct pw_memblock *block,
+ enum pw_memmap_flags flags, uint32_t offset, uint32_t size, uint32_t tag[5])
+{
+ struct memblock *b = SPA_CONTAINER_OF(block, struct memblock, this);
+ struct mempool *p = SPA_CONTAINER_OF(block->pool, struct mempool, this);
+ struct mapping *m;
+ struct memmap *mm;
+ struct pw_map_range range;
+
+ pw_map_range_init(&range, offset, size, p->pagesize);
+
+ m = memblock_find_mapping(b, flags, offset, size);
+ if (m == NULL)
+ m = memblock_map(b, flags, range.offset, range.size);
+ if (m == NULL)
+ return NULL;
+
+ mm = calloc(1, sizeof(struct memmap));
+ if (mm == NULL) {
+ if (m->ref == 0)
+ mapping_unmap(m);
+ return NULL;
+ }
+
+ m->ref++;
+ mm->mapping = m;
+ mm->this.block = block;
+ mm->this.flags = flags;
+ mm->this.offset = offset;
+ mm->this.size = size;
+ mm->this.ptr = SPA_PTROFF(m->ptr, range.start, void);
+
+ pw_log_debug("%p: map:%p block:%p fd:%d ptr:%p (%u %u) mapping:%p ref:%d", p,
+ &mm->this, b, b->this.fd, mm->this.ptr, offset, size, m, m->ref);
+
+ if (tag) {
+ memcpy(mm->this.tag, tag, sizeof(mm->this.tag));
+ pw_log_debug("%p: tag:%u:%u:%u:%u:%u", p,
+ tag[0], tag[1], tag[2], tag[3], tag[4]);
+ }
+
+ spa_list_append(&b->memmaps, &mm->link);
+
+ return &mm->this;
+}
+
+SPA_EXPORT
+struct pw_memmap * pw_mempool_map_id(struct pw_mempool *pool,
+ uint32_t id, enum pw_memmap_flags flags, uint32_t offset, uint32_t size, uint32_t tag[5])
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+
+ b = pw_map_lookup(&impl->map, id);
+ if (b == NULL) {
+ errno = ENOENT;
+ return NULL;
+ }
+ return pw_memblock_map(&b->this, flags, offset, size, tag);
+}
+
+SPA_EXPORT
+int pw_memmap_free(struct pw_memmap *map)
+{
+ struct memmap *mm;
+ struct mapping *m;
+ struct memblock *b;
+ struct mempool *p;
+
+ if (map == NULL)
+ return 0;
+
+ mm = SPA_CONTAINER_OF(map, struct memmap, this);
+ m = mm->mapping;
+ b = m->block;
+ p = SPA_CONTAINER_OF(b->this.pool, struct mempool, this);
+
+ pw_log_debug("%p: map:%p block:%p fd:%d ptr:%p mapping:%p ref:%d", p,
+ &mm->this, b, b->this.fd, mm->this.ptr, m, m->ref);
+
+ spa_list_remove(&mm->link);
+
+ if (--m->ref == 0)
+ mapping_unmap(m);
+
+ free(mm);
+
+ return 0;
+}
+
+static inline enum pw_memmap_flags block_flags_to_mem(enum pw_memblock_flags flags)
+{
+ enum pw_memmap_flags fl = 0;
+
+ if (flags & PW_MEMBLOCK_FLAG_READABLE)
+ fl |= PW_MEMMAP_FLAG_READ;
+ if (flags & PW_MEMBLOCK_FLAG_WRITABLE)
+ fl |= PW_MEMMAP_FLAG_WRITE;
+
+ return fl;
+}
+
+/** Create a new memblock
+ * \param pool the pool to use
+ * \param flags memblock flags
+ * \param type the requested memory type one of enum spa_data_type
+ * \param size size to allocate
+ * \return a memblock structure or NULL with errno on error
+ */
+SPA_EXPORT
+struct pw_memblock * pw_mempool_alloc(struct pw_mempool *pool, enum pw_memblock_flags flags,
+ uint32_t type, size_t size)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+ int res;
+
+ b = calloc(1, sizeof(struct memblock));
+ if (b == NULL)
+ return NULL;
+
+ b->this.ref = 1;
+ b->this.pool = pool;
+ b->this.flags = flags;
+ b->this.type = type;
+ b->this.size = size;
+ spa_list_init(&b->mappings);
+ spa_list_init(&b->memmaps);
+
+#ifdef HAVE_MEMFD_CREATE
+ char name[128];
+ snprintf(name, sizeof(name),
+ "pipewire-memfd:flags=0x%08x,type=%" PRIu32 ",size=%zu",
+ (unsigned int) flags, type, size);
+
+ b->this.fd = memfd_create(name, MFD_CLOEXEC | MFD_ALLOW_SEALING);
+ if (b->this.fd == -1) {
+ res = -errno;
+ pw_log_error("%p: Failed to create memfd: %m", pool);
+ goto error_free;
+ }
+#elif defined(__FreeBSD__) || defined(__MidnightBSD__)
+ b->this.fd = shm_open(SHM_ANON, O_CREAT | O_RDWR | O_CLOEXEC, 0);
+ if (b->this.fd == -1) {
+ res = -errno;
+ pw_log_error("%p: Failed to create SHM_ANON fd: %m", pool);
+ goto error_free;
+ }
+#else
+ char filename[128];
+ snprintf(filename, sizeof(filename),
+ "/dev/shm/pipewire-tmpfile:flags=0x%08x,type=%" PRIu32 ",size=%zu:XXXXXX",
+ (unsigned int) flags, type, size);
+
+ b->this.fd = mkostemp(filename, O_CLOEXEC);
+ if (b->this.fd == -1) {
+ res = -errno;
+ pw_log_error("%p: Failed to create temporary file: %m", pool);
+ goto error_free;
+ }
+ unlink(filename);
+#endif
+ pw_log_debug("%p: new fd:%d", pool, b->this.fd);
+
+ if (ftruncate(b->this.fd, size) < 0) {
+ res = -errno;
+ pw_log_warn("%p: Failed to truncate temporary file: %m", pool);
+ goto error_close;
+ }
+#ifdef HAVE_MEMFD_CREATE
+ if (flags & PW_MEMBLOCK_FLAG_SEAL) {
+ unsigned int seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL;
+ if (fcntl(b->this.fd, F_ADD_SEALS, seals) == -1) {
+ pw_log_warn("%p: Failed to add seals: %m", pool);
+ }
+ }
+#endif
+ if (flags & PW_MEMBLOCK_FLAG_MAP && size > 0) {
+ b->this.map = pw_memblock_map(&b->this,
+ block_flags_to_mem(flags), 0, size, NULL);
+ if (b->this.map == NULL) {
+ res = -errno;
+ pw_log_warn("%p: Failed to map: %m", pool);
+ goto error_close;
+ }
+ b->this.ref--;
+ }
+
+ b->this.id = pw_map_insert_new(&impl->map, b);
+ spa_list_append(&impl->blocks, &b->link);
+ pw_log_debug("%p: block:%p id:%d type:%u size:%zu", pool,
+ &b->this, b->this.id, type, size);
+
+ if (!SPA_FLAG_IS_SET(flags, PW_MEMBLOCK_FLAG_DONT_NOTIFY))
+ pw_mempool_emit_added(impl, &b->this);
+
+ return &b->this;
+
+error_close:
+ pw_log_debug("%p: close fd:%d", pool, b->this.fd);
+ close(b->this.fd);
+error_free:
+ free(b);
+ errno = -res;
+ return NULL;
+}
+
+static struct memblock * mempool_find_fd(struct pw_mempool *pool, int fd)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+
+ spa_list_for_each(b, &impl->blocks, link) {
+ if (fd == b->this.fd) {
+ pw_log_debug("%p: found %p id:%u fd:%d ref:%d",
+ pool, &b->this, b->this.id, fd, b->this.ref);
+ return b;
+ }
+ }
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_memblock * pw_mempool_import(struct pw_mempool *pool,
+ enum pw_memblock_flags flags, uint32_t type, int fd)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+
+ b = mempool_find_fd(pool, fd);
+ if (b != NULL) {
+ b->this.ref++;
+ return &b->this;
+ }
+
+ b = calloc(1, sizeof(struct memblock));
+ if (b == NULL)
+ return NULL;
+
+ spa_list_init(&b->memmaps);
+ spa_list_init(&b->mappings);
+
+ b->this.ref = 1;
+ b->this.pool = pool;
+ b->this.type = type;
+ b->this.fd = fd;
+ b->this.flags = flags;
+ b->this.id = pw_map_insert_new(&impl->map, b);
+ spa_list_append(&impl->blocks, &b->link);
+
+ pw_log_debug("%p: block:%p id:%u flags:%08x type:%u fd:%d",
+ pool, b, b->this.id, flags, type, fd);
+
+ if (!SPA_FLAG_IS_SET(flags, PW_MEMBLOCK_FLAG_DONT_NOTIFY))
+ pw_mempool_emit_added(impl, &b->this);
+
+ return &b->this;
+}
+
+SPA_EXPORT
+struct pw_memblock * pw_mempool_import_block(struct pw_mempool *pool,
+ struct pw_memblock *mem)
+{
+ pw_log_debug("%p: import block:%p type:%d fd:%d", pool,
+ mem, mem->type, mem->fd);
+ return pw_mempool_import(pool,
+ mem->flags | PW_MEMBLOCK_FLAG_DONT_CLOSE,
+ mem->type, mem->fd);
+}
+
+SPA_EXPORT
+struct pw_memmap * pw_mempool_import_map(struct pw_mempool *pool,
+ struct pw_mempool *other, void *data, uint32_t size, uint32_t tag[5])
+{
+ struct pw_memblock *old, *block;
+ struct memblock *b;
+ struct pw_memmap *map;
+ uint32_t offset;
+
+ old = pw_mempool_find_ptr(other, data);
+ if (old == NULL || old->map == NULL) {
+ errno = EFAULT;
+ return NULL;
+ }
+
+ block = pw_mempool_import_block(pool, old);
+ if (block == NULL)
+ return NULL;
+
+ if (block->ref == 1) {
+ struct mapping *m;
+
+ b = SPA_CONTAINER_OF(block, struct memblock, this);
+
+ m = calloc(1, sizeof(struct mapping));
+ if (m == NULL) {
+ pw_memblock_unref(block);
+ return NULL;
+ }
+ m->ptr = old->map->ptr;
+ m->block = b;
+ m->offset = old->map->offset;
+ m->size = old->map->size;
+ spa_list_append(&b->mappings, &m->link);
+ pw_log_debug("%p: mapping:%p block:%p offset:%u size:%u ref:%u",
+ pool, m, block, m->offset, m->size, block->ref);
+ } else {
+ block->ref--;
+ }
+
+ offset = SPA_PTRDIFF(data, old->map->ptr);
+
+ map = pw_memblock_map(block,
+ block_flags_to_mem(block->flags), offset, size, tag);
+ if (map == NULL)
+ return NULL;
+
+ pw_log_debug("%p: from pool:%p block:%p id:%u data:%p size:%u ref:%d",
+ pool, other, block, block->id, data, size, block->ref);
+
+ return map;
+}
+
+SPA_EXPORT
+int pw_mempool_remove_id(struct pw_mempool *pool, uint32_t id)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+
+ b = pw_map_lookup(&impl->map, id);
+ if (b == NULL)
+ return -ENOENT;
+
+ pw_log_debug("%p: block:%p id:%u fd:%d ref:%d",
+ pool, b, id, b->this.fd, b->this.ref);
+
+ b->this.id = SPA_ID_INVALID;
+ pw_map_remove(&impl->map, id);
+ pw_memblock_unref(&b->this);
+
+ return 0;
+}
+
+/** Free a memblock
+ * \param block a memblock
+ */
+SPA_EXPORT
+void pw_memblock_free(struct pw_memblock *block)
+{
+ struct memblock *b = SPA_CONTAINER_OF(block, struct memblock, this);
+ struct pw_mempool *pool = block->pool;
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memmap *mm;
+ struct mapping *m;
+
+ spa_return_if_fail(block != NULL);
+
+ pw_log_debug("%p: block:%p id:%d fd:%d ref:%d",
+ pool, block, block->id, block->fd, block->ref);
+
+ block->ref++;
+ if (block->map)
+ block->ref++;
+
+ if (block->id != SPA_ID_INVALID)
+ pw_map_remove(&impl->map, block->id);
+ spa_list_remove(&b->link);
+
+ if (!SPA_FLAG_IS_SET(block->flags, PW_MEMBLOCK_FLAG_DONT_NOTIFY))
+ pw_mempool_emit_removed(impl, block);
+
+ spa_list_consume(mm, &b->memmaps, link)
+ pw_memmap_free(&mm->this);
+
+ spa_list_consume(m, &b->mappings, link) {
+ pw_log_warn("%p: stray mapping:%p", pool, m);
+ mapping_free(m);
+ }
+
+ if (block->fd != -1 && !(block->flags & PW_MEMBLOCK_FLAG_DONT_CLOSE)) {
+ pw_log_debug("%p: close fd:%d", pool, block->fd);
+ close(block->fd);
+ }
+ free(b);
+}
+
+SPA_EXPORT
+struct pw_memblock * pw_mempool_find_ptr(struct pw_mempool *pool, const void *ptr)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+ struct mapping *m;
+
+ spa_list_for_each(b, &impl->blocks, link) {
+ spa_list_for_each(m, &b->mappings, link) {
+ if (ptr >= m->ptr && ptr < SPA_PTROFF(m->ptr, m->size, void)) {
+ pw_log_debug("%p: block:%p id:%u for %p", pool,
+ b, b->this.id, ptr);
+ return &b->this;
+ }
+ }
+ }
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_memblock * pw_mempool_find_id(struct pw_mempool *pool, uint32_t id)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+
+ b = pw_map_lookup(&impl->map, id);
+ pw_log_debug("%p: block:%p for %u", pool, b, id);
+ if (b == NULL)
+ return NULL;
+
+ return &b->this;
+}
+
+SPA_EXPORT
+struct pw_memblock * pw_mempool_find_fd(struct pw_mempool *pool, int fd)
+{
+ struct memblock *b;
+
+ b = mempool_find_fd(pool, fd);
+ if (b == NULL)
+ return NULL;
+
+ return &b->this;
+}
+
+SPA_EXPORT
+struct pw_memmap * pw_mempool_find_tag(struct pw_mempool *pool, uint32_t tag[5], size_t size)
+{
+ struct mempool *impl = SPA_CONTAINER_OF(pool, struct mempool, this);
+ struct memblock *b;
+ struct memmap *mm;
+
+ pw_log_debug("%p: find tag %u:%u:%u:%u:%u size:%zu", pool,
+ tag[0], tag[1], tag[2], tag[3], tag[4], size);
+
+ spa_list_for_each(b, &impl->blocks, link) {
+ spa_list_for_each(mm, &b->memmaps, link) {
+ if (memcmp(tag, mm->this.tag, size) == 0) {
+ pw_log_debug("%p: found %p", pool, mm);
+ return &mm->this;
+ }
+ }
+ }
+ return NULL;
+}
diff --git a/src/pipewire/mem.h b/src/pipewire/mem.h
new file mode 100644
index 0000000..4edfab6
--- /dev/null
+++ b/src/pipewire/mem.h
@@ -0,0 +1,212 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_MEM_H
+#define PIPEWIRE_MEM_H
+
+#include <pipewire/properties.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_memblock Memory Blocks
+ * Memory allocation and pools.
+ */
+
+/**
+ * \addtogroup pw_memblock
+ * \{
+ */
+
+/** Flags passed to \ref pw_mempool_alloc() */
+enum pw_memblock_flags {
+ PW_MEMBLOCK_FLAG_NONE = 0,
+ PW_MEMBLOCK_FLAG_READABLE = (1 << 0), /**< memory is readable */
+ PW_MEMBLOCK_FLAG_WRITABLE = (1 << 1), /**< memory is writable */
+ PW_MEMBLOCK_FLAG_SEAL = (1 << 2), /**< seal the fd */
+ PW_MEMBLOCK_FLAG_MAP = (1 << 3), /**< mmap the fd */
+ PW_MEMBLOCK_FLAG_DONT_CLOSE = (1 << 4), /**< don't close fd */
+ PW_MEMBLOCK_FLAG_DONT_NOTIFY = (1 << 5), /**< don't notify events */
+
+ PW_MEMBLOCK_FLAG_READWRITE = PW_MEMBLOCK_FLAG_READABLE | PW_MEMBLOCK_FLAG_WRITABLE,
+};
+
+enum pw_memmap_flags {
+ PW_MEMMAP_FLAG_NONE = 0,
+ PW_MEMMAP_FLAG_READ = (1 << 0), /**< map in read mode */
+ PW_MEMMAP_FLAG_WRITE = (1 << 1), /**< map in write mode */
+ PW_MEMMAP_FLAG_TWICE = (1 << 2), /**< map the same area twice after each other,
+ * creating a circular ringbuffer */
+ PW_MEMMAP_FLAG_PRIVATE = (1 << 3), /**< writes will be private */
+ PW_MEMMAP_FLAG_LOCKED = (1 << 4), /**< lock the memory into RAM */
+ PW_MEMMAP_FLAG_READWRITE = PW_MEMMAP_FLAG_READ | PW_MEMMAP_FLAG_WRITE,
+};
+
+struct pw_memchunk;
+
+/**
+ *
+ * A memory pool is a collection of pw_memblocks */
+struct pw_mempool {
+ struct pw_properties *props;
+};
+
+/**
+ * Memory block structure */
+struct pw_memblock {
+ struct pw_mempool *pool; /**< owner pool */
+ uint32_t id; /**< unique id */
+ int ref; /**< refcount */
+ uint32_t flags; /**< flags for the memory block on of enum pw_memblock_flags */
+ uint32_t type; /**< type of the fd, one of enum spa_data_type */
+ int fd; /**< fd */
+ uint32_t size; /**< size of memory */
+ struct pw_memmap *map; /**< optional map when PW_MEMBLOCK_FLAG_MAP was given */
+};
+
+/** a mapped region of a pw_memblock */
+struct pw_memmap {
+ struct pw_memblock *block; /**< owner memblock */
+ void *ptr; /**< mapped pointer */
+ uint32_t flags; /**< flags for the mapping on of enum pw_memmap_flags */
+ uint32_t offset; /**< offset in memblock */
+ uint32_t size; /**< size in memblock */
+ uint32_t tag[5]; /**< user tag */
+};
+
+struct pw_mempool_events {
+#define PW_VERSION_MEMPOOL_EVENTS 0
+ uint32_t version;
+
+ /** the pool is destroyed */
+ void (*destroy) (void *data);
+
+ /** a new memory block is added to the pool */
+ void (*added) (void *data, struct pw_memblock *block);
+
+ /** a memory block is removed from the pool */
+ void (*removed) (void *data, struct pw_memblock *block);
+};
+
+/** Create a new memory pool */
+struct pw_mempool *pw_mempool_new(struct pw_properties *props);
+
+/** Listen for events */
+void pw_mempool_add_listener(struct pw_mempool *pool,
+ struct spa_hook *listener,
+ const struct pw_mempool_events *events,
+ void *data);
+
+/** Clear a pool */
+void pw_mempool_clear(struct pw_mempool *pool);
+
+/** Clear and destroy a pool */
+void pw_mempool_destroy(struct pw_mempool *pool);
+
+
+/** Allocate a memory block from the pool */
+struct pw_memblock * pw_mempool_alloc(struct pw_mempool *pool,
+ enum pw_memblock_flags flags, uint32_t type, size_t size);
+
+/** Import a block from another pool */
+struct pw_memblock * pw_mempool_import_block(struct pw_mempool *pool,
+ struct pw_memblock *mem);
+
+/** Import an fd into the pool */
+struct pw_memblock * pw_mempool_import(struct pw_mempool *pool,
+ enum pw_memblock_flags flags, uint32_t type, int fd);
+
+/** Free a memblock regardless of the refcount and destroy all mappings */
+void pw_memblock_free(struct pw_memblock *mem);
+
+/** Unref a memblock */
+static inline void pw_memblock_unref(struct pw_memblock *mem)
+{
+ if (--mem->ref == 0)
+ pw_memblock_free(mem);
+}
+
+/** Remove a memblock for given \a id */
+int pw_mempool_remove_id(struct pw_mempool *pool, uint32_t id);
+
+/** Find memblock for given \a ptr */
+struct pw_memblock * pw_mempool_find_ptr(struct pw_mempool *pool, const void *ptr);
+
+/** Find memblock for given \a id */
+struct pw_memblock * pw_mempool_find_id(struct pw_mempool *pool, uint32_t id);
+
+/** Find memblock for given \a fd */
+struct pw_memblock * pw_mempool_find_fd(struct pw_mempool *pool, int fd);
+
+
+/** Map a region of a memory block */
+struct pw_memmap * pw_memblock_map(struct pw_memblock *block,
+ enum pw_memmap_flags flags, uint32_t offset, uint32_t size,
+ uint32_t tag[5]);
+
+/** Map a region of a memory block with \a id */
+struct pw_memmap * pw_mempool_map_id(struct pw_mempool *pool, uint32_t id,
+ enum pw_memmap_flags flags, uint32_t offset, uint32_t size,
+ uint32_t tag[5]);
+
+struct pw_memmap * pw_mempool_import_map(struct pw_mempool *pool,
+ struct pw_mempool *other, void *data, uint32_t size, uint32_t tag[5]);
+
+/** find a map with the given tag */
+struct pw_memmap * pw_mempool_find_tag(struct pw_mempool *pool, uint32_t tag[5], size_t size);
+
+/** Unmap a region */
+int pw_memmap_free(struct pw_memmap *map);
+
+
+/** parameters to map a memory range */
+struct pw_map_range {
+ uint32_t start; /** offset in first page with start of data */
+ uint32_t offset; /** page aligned offset to map */
+ uint32_t size; /** size to map */
+};
+
+#define PW_MAP_RANGE_INIT (struct pw_map_range){ 0, }
+
+/** 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,
+ uint32_t offset, uint32_t size,
+ uint32_t page_size)
+{
+ range->offset = SPA_ROUND_DOWN_N(offset, page_size);
+ range->start = offset - range->offset;
+ range->size = SPA_ROUND_UP_N(range->start + size, page_size);
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_MEM_H */
diff --git a/src/pipewire/meson.build b/src/pipewire/meson.build
new file mode 100644
index 0000000..b19631a
--- /dev/null
+++ b/src/pipewire/meson.build
@@ -0,0 +1,138 @@
+pipewire_headers = [
+ 'array.h',
+ 'buffers.h',
+ 'impl-core.h',
+ 'impl-client.h',
+ 'client.h',
+ 'conf.h',
+ 'context.h',
+ 'control.h',
+ 'core.h',
+ 'device.h',
+ 'impl-device.h',
+ 'data-loop.h',
+ 'factory.h',
+ 'impl-factory.h',
+ 'filter.h',
+ 'global.h',
+ 'keys.h',
+ 'impl.h',
+ 'i18n.h',
+ 'impl-link.h',
+ 'link.h',
+ 'log.h',
+ 'loop.h',
+ 'main-loop.h',
+ 'map.h',
+ 'mem.h',
+ 'impl-metadata.h',
+ 'impl-module.h',
+ 'module.h',
+ 'impl-node.h',
+ 'node.h',
+ 'permission.h',
+ 'pipewire.h',
+ 'impl-port.h',
+ 'port.h',
+ 'properties.h',
+ 'protocol.h',
+ 'proxy.h',
+ 'resource.h',
+ 'stream.h',
+ 'thread.h',
+ 'thread-loop.h',
+ 'type.h',
+ 'utils.h',
+ 'work-queue.h',
+]
+
+pipewire_sources = [
+ 'buffers.c',
+ 'impl-core.c',
+ 'impl-client.c',
+ 'conf.c',
+ 'context.c',
+ 'control.c',
+ 'core.c',
+ 'data-loop.c',
+ 'impl-device.c',
+ 'filter.c',
+ 'global.c',
+ 'introspect.c',
+ 'impl-link.c',
+ 'log.c',
+ 'loop.c',
+ 'main-loop.c',
+ 'mem.c',
+ 'impl-module.c',
+ 'impl-node.c',
+ 'impl-factory.c',
+ 'impl-metadata.c',
+ 'pipewire.c',
+ 'impl-port.c',
+ 'properties.c',
+ 'protocol.c',
+ 'proxy.c',
+ 'resource.c',
+ 'settings.c',
+ 'stream.c',
+ 'thread.c',
+ 'thread-loop.c',
+ 'utils.c',
+ 'work-queue.c',
+]
+
+configure_file(input : 'version.h.in',
+ output : 'version.h',
+ install_dir : get_option('includedir') / pipewire_headers_dir,
+ configuration : versiondata)
+
+
+install_headers(pipewire_headers, subdir : pipewire_headers_dir)
+
+libpipewire_c_args = [
+ '-DOLD_MEDIA_SESSION_WORKAROUND=1'
+]
+
+if host_machine.system() != 'freebsd' and host_machine.system() != 'midnightbsd'
+ libpipewire_c_args += [
+ '-D_POSIX_C_SOURCE'
+ ]
+endif
+
+libpipewire = shared_library(pipewire_name, pipewire_sources,
+ version : libversion,
+ soversion : soversion,
+ c_args : libpipewire_c_args,
+ include_directories : [pipewire_inc, configinc, includes_inc],
+ install : true,
+ dependencies : [spa_dep, dl_lib, mathlib, pthread_lib, libintl_dep, atomic_dep, ],
+)
+
+pipewire_dep = declare_dependency(link_with : libpipewire,
+ include_directories : [pipewire_inc, configinc],
+ dependencies : [pthread_lib, atomic_dep, spa_dep],
+ variables : {
+ 'moduledir' : meson.current_build_dir() / '..' / 'modules',
+ 'confdatadir' : meson.current_build_dir() / '..' / 'daemon',
+ }
+)
+
+pkgconfig.generate(libpipewire,
+ filebase : 'lib@0@'.format(pipewire_name),
+ requires : ['lib@0@'.format(spa_name)],
+ name : 'libpipewire',
+ subdirs : pipewire_name,
+ description : 'PipeWire Interface',
+ version : pipewire_version,
+ extra_cflags : '-D_REENTRANT',
+ variables : ['moduledir=${libdir}/@0@'.format(pipewire_name)],
+ uninstalled_variables : [
+ 'moduledir=${prefix}/src/modules',
+ 'confdatadir=${prefix}/src/daemon',
+ ],
+)
+
+meson.override_dependency('lib@0@'.format(pipewire_name), pipewire_dep)
+
+subdir('extensions')
diff --git a/src/pipewire/module.h b/src/pipewire/module.h
new file mode 100644
index 0000000..aba6de8
--- /dev/null
+++ b/src/pipewire/module.h
@@ -0,0 +1,121 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_MODULE_H
+#define PIPEWIRE_MODULE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+
+#include <pipewire/proxy.h>
+
+/** \defgroup pw_module Module
+ * Module interface
+ */
+
+/**
+ * \addtogroup pw_module
+ * \{
+ */
+#define PW_TYPE_INTERFACE_Module PW_TYPE_INFO_INTERFACE_BASE "Module"
+
+#define PW_VERSION_MODULE 3
+struct pw_module;
+
+/** The module information. Extra information can be added in later versions */
+struct pw_module_info {
+ uint32_t id; /**< id of the global */
+ const char *name; /**< name of the module */
+ const char *filename; /**< filename of the module */
+ const char *args; /**< arguments passed to the module */
+#define PW_MODULE_CHANGE_MASK_PROPS (1 << 0)
+#define PW_MODULE_CHANGE_MASK_ALL ((1 << 1)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ struct spa_dict *props; /**< extra properties */
+};
+
+/** Update and existing \ref pw_module_info with \a update with reset */
+struct pw_module_info *
+pw_module_info_update(struct pw_module_info *info,
+ const struct pw_module_info *update);
+/** Merge and existing \ref pw_module_info with \a update */
+struct pw_module_info *
+pw_module_info_merge(struct pw_module_info *info,
+ const struct pw_module_info *update, bool reset);
+/** Free a \ref pw_module_info */
+void pw_module_info_free(struct pw_module_info *info);
+
+#define PW_MODULE_EVENT_INFO 0
+#define PW_MODULE_EVENT_NUM 1
+
+/** Module events */
+struct pw_module_events {
+#define PW_VERSION_MODULE_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify module info
+ *
+ * \param info info about the module
+ */
+ void (*info) (void *data, const struct pw_module_info *info);
+};
+
+#define PW_MODULE_METHOD_ADD_LISTENER 0
+#define PW_MODULE_METHOD_NUM 1
+
+/** Module methods */
+struct pw_module_methods {
+#define PW_VERSION_MODULE_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_module_events *events,
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_MODULE_H */
diff --git a/src/pipewire/node.h b/src/pipewire/node.h
new file mode 100644
index 0000000..e5b8995
--- /dev/null
+++ b/src/pipewire/node.h
@@ -0,0 +1,216 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_NODE_H
+#define PIPEWIRE_NODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/node/command.h>
+#include <spa/param/param.h>
+
+#include <pipewire/proxy.h>
+
+/** \defgroup pw_node Node
+ * Node interface
+ */
+
+/**
+ * \addtogroup pw_node
+ * \{
+ */
+#define PW_TYPE_INTERFACE_Node PW_TYPE_INFO_INTERFACE_BASE "Node"
+
+#define PW_VERSION_NODE 3
+struct pw_node;
+
+/** \enum pw_node_state The different node states */
+enum pw_node_state {
+ PW_NODE_STATE_ERROR = -1, /**< error state */
+ PW_NODE_STATE_CREATING = 0, /**< the node is being created */
+ PW_NODE_STATE_SUSPENDED = 1, /**< the node is suspended, the device might
+ * be closed */
+ PW_NODE_STATE_IDLE = 2, /**< the node is running but there is no active
+ * port */
+ PW_NODE_STATE_RUNNING = 3, /**< the node is running */
+};
+
+/** Convert a \ref pw_node_state to a readable string */
+const char * pw_node_state_as_string(enum pw_node_state state);
+
+/** The node information. Extra information can be added in later versions */
+struct pw_node_info {
+ uint32_t id; /**< id of the global */
+ uint32_t max_input_ports; /**< maximum number of inputs */
+ uint32_t max_output_ports; /**< maximum number of outputs */
+#define PW_NODE_CHANGE_MASK_INPUT_PORTS (1 << 0)
+#define PW_NODE_CHANGE_MASK_OUTPUT_PORTS (1 << 1)
+#define PW_NODE_CHANGE_MASK_STATE (1 << 2)
+#define PW_NODE_CHANGE_MASK_PROPS (1 << 3)
+#define PW_NODE_CHANGE_MASK_PARAMS (1 << 4)
+#define PW_NODE_CHANGE_MASK_ALL ((1 << 5)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ uint32_t n_input_ports; /**< number of inputs */
+ uint32_t n_output_ports; /**< number of outputs */
+ enum pw_node_state state; /**< the current state of the node */
+ const char *error; /**< an error reason if \a state is error */
+ struct spa_dict *props; /**< the properties of the node */
+ struct spa_param_info *params; /**< parameters */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+struct pw_node_info *
+pw_node_info_update(struct pw_node_info *info,
+ const struct pw_node_info *update);
+
+struct pw_node_info *
+pw_node_info_merge(struct pw_node_info *info,
+ const struct pw_node_info *update, bool reset);
+
+void
+pw_node_info_free(struct pw_node_info *info);
+
+#define PW_NODE_EVENT_INFO 0
+#define PW_NODE_EVENT_PARAM 1
+#define PW_NODE_EVENT_NUM 2
+
+/** Node events */
+struct pw_node_events {
+#define PW_VERSION_NODE_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify node info
+ *
+ * \param info info about the node
+ */
+ void (*info) (void *data, const struct pw_node_info *info);
+ /**
+ * Notify a node param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param seq the sequence number of the request
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define PW_NODE_METHOD_ADD_LISTENER 0
+#define PW_NODE_METHOD_SUBSCRIBE_PARAMS 1
+#define PW_NODE_METHOD_ENUM_PARAMS 2
+#define PW_NODE_METHOD_SET_PARAM 3
+#define PW_NODE_METHOD_SEND_COMMAND 4
+#define PW_NODE_METHOD_NUM 5
+
+/** Node methods */
+struct pw_node_methods {
+#define PW_VERSION_NODE_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_node_events *events,
+ void *data);
+ /**
+ * Subscribe to parameter changes
+ *
+ * Automatically emit param events for the given ids when
+ * they are changed.
+ *
+ * \param ids an array of param ids
+ * \param n_ids the number of ids in \a ids
+ */
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+
+ /**
+ * Enumerate node parameters
+ *
+ * Start enumeration of node parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param seq a sequence number to place in the reply
+ * \param id the parameter id to enum or PW_ID_ANY for all
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ int (*enum_params) (void *object, int seq, uint32_t id,
+ uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+
+ /**
+ * Set a parameter on the node
+ *
+ * \param id the parameter id to set
+ * \param flags extra parameter flags
+ * \param param the parameter to set
+ */
+ int (*set_param) (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+
+ /**
+ * Send a command to the node
+ *
+ * \param command the command to send
+ */
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_NODE_H */
diff --git a/src/pipewire/permission.h b/src/pipewire/permission.h
new file mode 100644
index 0000000..1bcebc3
--- /dev/null
+++ b/src/pipewire/permission.h
@@ -0,0 +1,86 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PERMISSION_H
+#define PIPEWIRE_PERMISSION_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/defs.h>
+
+/** \defgroup pw_permission Permission
+ *
+ * Permissions are kept for a client and describe what the client is
+ * allowed to do with an object.
+ *
+ * See \ref page_core_api
+ */
+
+/**
+ * \addtogroup pw_permission
+ * \{
+ */
+
+#define PW_PERM_R 0400 /**< object can be seen and events can be received */
+#define PW_PERM_W 0200 /**< methods can be called that modify the object */
+#define PW_PERM_X 0100 /**< methods can be called on the object. The W flag must be
+ * present in order to call methods that modify the object. */
+#define PW_PERM_M 0010 /**< metadata can be set on object, Since 0.3.9 */
+
+#define PW_PERM_RWX (PW_PERM_R|PW_PERM_W|PW_PERM_X)
+#define PW_PERM_RWXM (PW_PERM_RWX|PW_PERM_M)
+
+#define PW_PERM_IS_R(p) (((p)&PW_PERM_R) == PW_PERM_R)
+#define PW_PERM_IS_W(p) (((p)&PW_PERM_W) == PW_PERM_W)
+#define PW_PERM_IS_X(p) (((p)&PW_PERM_X) == PW_PERM_X)
+#define PW_PERM_IS_M(p) (((p)&PW_PERM_M) == PW_PERM_M)
+
+#define PW_PERM_ALL PW_PERM_RWXM
+#define PW_PERM_INVALID (uint32_t)(0xffffffff)
+
+struct pw_permission {
+ uint32_t id; /**< id of object, PW_ID_ANY for default permission */
+ uint32_t permissions; /**< bitmask of above permissions */
+};
+
+#define PW_PERMISSION_INIT(id,p) ((struct pw_permission){ (id), (p) })
+
+#define PW_PERMISSION_FORMAT "%c%c%c%c"
+#define PW_PERMISSION_ARGS(permission) \
+ (permission) & PW_PERM_R ? 'r' : '-', \
+ (permission) & PW_PERM_W ? 'w' : '-', \
+ (permission) & PW_PERM_X ? 'x' : '-', \
+ (permission) & PW_PERM_M ? 'm' : '-'
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_PERMISSION_H */
diff --git a/src/pipewire/pipewire.c b/src/pipewire/pipewire.c
new file mode 100644
index 0000000..104b52b
--- /dev/null
+++ b/src/pipewire/pipewire.c
@@ -0,0 +1,871 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <unistd.h>
+#include <limits.h>
+#include <stdio.h>
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
+#include <sys/prctl.h>
+#endif
+#include <pwd.h>
+#include <errno.h>
+#include <dlfcn.h>
+#include <pthread.h>
+
+#include <locale.h>
+#include <libintl.h>
+
+#include <valgrind/valgrind.h>
+
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/support/cpu.h>
+#include <spa/support/i18n.h>
+
+#include "pipewire.h"
+#include "private.h"
+
+#define MAX_SUPPORT 32
+
+#define SUPPORTLIB "support/libspa-support"
+
+PW_LOG_TOPIC_EXTERN(log_context);
+#define PW_LOG_TOPIC_DEFAULT log_context
+
+static char *prgname;
+
+static struct spa_i18n *_pipewire_i18n = NULL;
+
+struct plugin {
+ struct spa_list link;
+ char *filename;
+ void *hnd;
+ spa_handle_factory_enum_func_t enum_func;
+ int ref;
+};
+
+struct handle {
+ struct spa_list link;
+ struct plugin *plugin;
+ char *factory_name;
+ int ref;
+ struct spa_handle handle SPA_ALIGNED(8);
+};
+
+struct registry {
+ struct spa_list plugins;
+ struct spa_list handles; /* all handles across all plugins by age (youngest first) */
+};
+
+struct support {
+ const char *plugin_dir;
+ const char *support_lib;
+ struct registry registry;
+ char *i18n_domain;
+ struct spa_interface i18n_iface;
+ struct spa_support support[MAX_SUPPORT];
+ uint32_t n_support;
+ uint32_t init_count;
+ unsigned int in_valgrind:1;
+ unsigned int no_color:1;
+ unsigned int no_config:1;
+ unsigned int do_dlclose:1;
+};
+
+static pthread_mutex_t init_lock = PTHREAD_MUTEX_INITIALIZER;
+static pthread_mutex_t support_lock = PTHREAD_MUTEX_INITIALIZER;
+static struct support global_support;
+
+static struct plugin *
+find_plugin(struct registry *registry, const char *filename)
+{
+ struct plugin *p;
+ spa_list_for_each(p, &registry->plugins, link) {
+ if (spa_streq(p->filename, filename))
+ return p;
+ }
+ return NULL;
+}
+
+static struct plugin *
+open_plugin(struct registry *registry,
+ const char *path, size_t len, const char *lib)
+{
+ struct plugin *plugin;
+ char filename[PATH_MAX];
+ void *hnd;
+ spa_handle_factory_enum_func_t enum_func;
+ int res;
+
+ if ((res = spa_scnprintf(filename, sizeof(filename), "%.*s/%s.so", (int)len, path, lib)) < 0)
+ goto error_out;
+
+ if ((plugin = find_plugin(registry, filename)) != NULL) {
+ plugin->ref++;
+ return plugin;
+ }
+
+ if ((hnd = dlopen(filename, RTLD_NOW)) == NULL) {
+ res = -ENOENT;
+ pw_log_debug("can't load %s: %s", filename, dlerror());
+ goto error_out;
+ }
+ if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
+ res = -ENOSYS;
+ pw_log_debug("can't find enum function: %s", dlerror());
+ goto error_dlclose;
+ }
+
+ if ((plugin = calloc(1, sizeof(struct plugin))) == NULL) {
+ res = -errno;
+ goto error_dlclose;
+ }
+
+ pw_log_debug("loaded plugin:'%s'", filename);
+ plugin->ref = 1;
+ plugin->filename = strdup(filename);
+ plugin->hnd = hnd;
+ plugin->enum_func = enum_func;
+
+ spa_list_append(&registry->plugins, &plugin->link);
+
+ return plugin;
+
+error_dlclose:
+ dlclose(hnd);
+error_out:
+ errno = -res;
+ return NULL;
+}
+
+static void
+unref_plugin(struct plugin *plugin)
+{
+ if (--plugin->ref == 0) {
+ spa_list_remove(&plugin->link);
+ pw_log_debug("unloaded plugin:'%s'", plugin->filename);
+ if (global_support.do_dlclose)
+ dlclose(plugin->hnd);
+ free(plugin->filename);
+ free(plugin);
+ }
+}
+
+static const struct spa_handle_factory *find_factory(struct plugin *plugin, const char *factory_name)
+{
+ int res = -ENOENT;
+ uint32_t index;
+ const struct spa_handle_factory *factory;
+
+ for (index = 0;;) {
+ if ((res = plugin->enum_func(&factory, &index)) <= 0) {
+ if (res == 0)
+ break;
+ goto out;
+ }
+ if (factory->version < 1) {
+ pw_log_warn("factory version %d < 1 not supported",
+ factory->version);
+ continue;
+ }
+ if (spa_streq(factory->name, factory_name))
+ return factory;
+ }
+ res = -ENOENT;
+out:
+ pw_log_debug("can't find factory %s: %s", factory_name, spa_strerror(res));
+ errno = -res;
+ return NULL;
+}
+
+static void unref_handle(struct handle *handle)
+{
+ if (--handle->ref == 0) {
+ spa_list_remove(&handle->link);
+ pw_log_debug("clear handle '%s'", handle->factory_name);
+ pthread_mutex_unlock(&support_lock);
+ spa_handle_clear(&handle->handle);
+ pthread_mutex_lock(&support_lock);
+ unref_plugin(handle->plugin);
+ free(handle->factory_name);
+ free(handle);
+ }
+}
+
+SPA_EXPORT
+uint32_t pw_get_support(struct spa_support *support, uint32_t max_support)
+{
+ uint32_t i, n = SPA_MIN(global_support.n_support, max_support);
+ for (i = 0; i < n; i++)
+ support[i] = global_support.support[i];
+ return n;
+}
+
+static struct spa_handle *load_spa_handle(const char *lib,
+ const char *factory_name,
+ const struct spa_dict *info,
+ uint32_t n_support,
+ const struct spa_support support[])
+{
+ struct support *sup = &global_support;
+ struct plugin *plugin;
+ struct handle *handle;
+ const struct spa_handle_factory *factory;
+ const char *state = NULL, *p;
+ int res;
+ size_t len;
+
+ if (factory_name == NULL) {
+ res = -EINVAL;
+ goto error_out;
+ }
+
+ if (lib == NULL)
+ lib = sup->support_lib;
+
+ pw_log_debug("load lib:'%s' factory-name:'%s'", lib, factory_name);
+
+ plugin = NULL;
+ res = -ENOENT;
+
+ if (sup->plugin_dir == NULL) {
+ pw_log_error("load lib: plugin directory undefined, set SPA_PLUGIN_DIR");
+ goto error_out;
+ }
+ while ((p = pw_split_walk(sup->plugin_dir, ":", &len, &state))) {
+ if ((plugin = open_plugin(&sup->registry, p, len, lib)) != NULL)
+ break;
+ res = -errno;
+ }
+ if (plugin == NULL)
+ goto error_out;
+
+ pthread_mutex_unlock(&support_lock);
+
+ factory = find_factory(plugin, factory_name);
+ if (factory == NULL) {
+ res = -errno;
+ goto error_unref_plugin;
+ }
+
+ handle = calloc(1, sizeof(struct handle) + spa_handle_factory_get_size(factory, info));
+ if (handle == NULL) {
+ res = -errno;
+ goto error_unref_plugin;
+ }
+
+ if ((res = spa_handle_factory_init(factory,
+ &handle->handle, info,
+ support, n_support)) < 0) {
+ pw_log_debug("can't make factory instance '%s': %d (%s)",
+ factory_name, res, spa_strerror(res));
+ goto error_free_handle;
+ }
+
+ pthread_mutex_lock(&support_lock);
+ handle->ref = 1;
+ handle->plugin = plugin;
+ handle->factory_name = strdup(factory_name);
+ spa_list_prepend(&sup->registry.handles, &handle->link);
+
+ return &handle->handle;
+
+error_free_handle:
+ free(handle);
+error_unref_plugin:
+ pthread_mutex_lock(&support_lock);
+ unref_plugin(plugin);
+error_out:
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+struct spa_handle *pw_load_spa_handle(const char *lib,
+ const char *factory_name,
+ const struct spa_dict *info,
+ uint32_t n_support,
+ const struct spa_support support[])
+{
+ struct spa_handle *handle;
+ pthread_mutex_lock(&support_lock);
+ handle = load_spa_handle(lib, factory_name, info, n_support, support);
+ pthread_mutex_unlock(&support_lock);
+ return handle;
+}
+
+static struct handle *find_handle(struct spa_handle *handle)
+{
+ struct registry *registry = &global_support.registry;
+ struct handle *h;
+
+ spa_list_for_each(h, &registry->handles, link) {
+ if (&h->handle == handle)
+ return h;
+ }
+
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_unload_spa_handle(struct spa_handle *handle)
+{
+ struct handle *h;
+ int res = 0;
+
+ pthread_mutex_lock(&support_lock);
+ if ((h = find_handle(handle)) == NULL)
+ res = -ENOENT;
+ else
+ unref_handle(h);
+ pthread_mutex_unlock(&support_lock);
+
+ return res;
+}
+
+static void *add_interface(struct support *support,
+ const char *factory_name,
+ const char *type,
+ const struct spa_dict *info)
+{
+ struct spa_handle *handle;
+ void *iface = NULL;
+ int res = -ENOENT;
+
+ handle = load_spa_handle(support->support_lib,
+ factory_name, info,
+ support->n_support, support->support);
+ if (handle == NULL)
+ return NULL;
+
+ pthread_mutex_unlock(&support_lock);
+ res = spa_handle_get_interface(handle, type, &iface);
+ pthread_mutex_lock(&support_lock);
+
+ if (res < 0 || iface == NULL) {
+ pw_log_error("can't get %s interface %d: %s", type, res,
+ spa_strerror(res));
+ return NULL;
+ }
+
+ support->support[support->n_support++] =
+ SPA_SUPPORT_INIT(type, iface);
+ return iface;
+}
+
+SPA_EXPORT
+int pw_set_domain(const char *domain)
+{
+ struct support *support = &global_support;
+ free(support->i18n_domain);
+ if (domain == NULL)
+ support->i18n_domain = NULL;
+ else if ((support->i18n_domain = strdup(domain)) == NULL)
+ return -errno;
+ return 0;
+}
+
+SPA_EXPORT
+const char *pw_get_domain(void)
+{
+ struct support *support = &global_support;
+ return support->i18n_domain;
+}
+
+static const char *i18n_text(void *object, const char *msgid)
+{
+ struct support *support = object;
+ return dgettext(support->i18n_domain, msgid);
+}
+
+static const char *i18n_ntext(void *object, const char *msgid, const char *msgid_plural,
+ unsigned long int n)
+{
+ struct support *support = object;
+ return dngettext(support->i18n_domain, msgid, msgid_plural, n);
+}
+
+static void init_i18n(struct support *support)
+{
+ bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+ pw_set_domain(GETTEXT_PACKAGE);
+}
+
+static void *add_i18n(struct support *support)
+{
+ static const struct spa_i18n_methods i18n_methods = {
+ SPA_VERSION_I18N_METHODS,
+ .text = i18n_text,
+ .ntext = i18n_ntext,
+ };
+
+ support->i18n_iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_I18N,
+ SPA_VERSION_I18N,
+ &i18n_methods, support);
+ _pipewire_i18n = (struct spa_i18n*) &support->i18n_iface;
+
+ support->support[support->n_support++] =
+ SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_I18N, _pipewire_i18n);
+
+ return 0;
+}
+
+SPA_EXPORT
+const char *pw_gettext(const char *msgid)
+{
+ return spa_i18n_text(_pipewire_i18n, msgid);
+}
+SPA_EXPORT
+const char *pw_ngettext(const char *msgid, const char *msgid_plural, unsigned long int n)
+{
+ return spa_i18n_ntext(_pipewire_i18n, msgid, msgid_plural, n);
+}
+
+#ifdef HAVE_SYSTEMD
+static struct spa_log *load_journal_logger(struct support *support,
+ const struct spa_dict *info)
+{
+ struct spa_handle *handle;
+ void *iface = NULL;
+ int res = -ENOENT;
+ uint32_t i;
+
+ /* is the journal even available? */
+ if (access("/run/systemd/journal/socket", F_OK) != 0)
+ return NULL;
+
+ handle = load_spa_handle("support/libspa-journal",
+ SPA_NAME_SUPPORT_LOG, info,
+ support->n_support, support->support);
+ if (handle == NULL)
+ return NULL;
+
+ pthread_mutex_unlock(&support_lock);
+ res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Log, &iface);
+ pthread_mutex_lock(&support_lock);
+
+ if (res < 0 || iface == NULL) {
+ pw_log_error("can't get log interface %d: %s", res,
+ spa_strerror(res));
+ return NULL;
+ }
+
+ /* look for an existing logger, and
+ * replace it with the journal logger */
+ for (i = 0; i < support->n_support; i++) {
+ if (spa_streq(support->support[i].type, SPA_TYPE_INTERFACE_Log)) {
+ support->support[i].data = iface;
+ break;
+ }
+ }
+ return (struct spa_log *) iface;
+}
+#endif
+
+static bool
+parse_log_level(const char *str, enum spa_log_level *l)
+{
+ uint32_t lvl;
+ if (strlen(str) == 1) {
+ switch(str[0]) {
+ case 'X': lvl = SPA_LOG_LEVEL_NONE; break;
+ case 'E': lvl = SPA_LOG_LEVEL_ERROR; break;
+ case 'W': lvl = SPA_LOG_LEVEL_WARN; break;
+ case 'I': lvl = SPA_LOG_LEVEL_INFO; break;
+ case 'D': lvl = SPA_LOG_LEVEL_DEBUG; break;
+ case 'T': lvl = SPA_LOG_LEVEL_TRACE; break;
+ default:
+ goto check_int;
+ }
+ } else {
+check_int:
+ if (!spa_atou32(str, &lvl, 0))
+ return false;
+ if (lvl > SPA_LOG_LEVEL_TRACE)
+ return false;
+ }
+ *l = lvl;
+ return true;
+}
+
+static char *
+parse_pw_debug_env(void)
+{
+ const char *str;
+ char **tokens;
+ int n_tokens;
+ size_t slen;
+ char json[1024] = {0};
+ char *pos = json;
+ char *end = pos + sizeof(json) - 1;
+ enum spa_log_level lvl;
+
+ str = getenv("PIPEWIRE_DEBUG");
+
+ if (!str || (slen = strlen(str)) == 0)
+ return NULL;
+
+ /* String format is PIPEWIRE_DEBUG=[<glob>:]<level>,...,
+ * converted into [{ conn.* = 0}, {glob = level}, {glob = level}, ....] ,
+ * with the connection namespace disabled by default.
+ */
+ pos += spa_scnprintf(pos, end - pos, "[ { conn.* = %d },", SPA_LOG_LEVEL_NONE);
+
+ tokens = pw_split_strv(str, ",", INT_MAX, &n_tokens);
+ if (n_tokens > 0) {
+ int i;
+ for (i = 0; i < n_tokens; i++) {
+ int n_tok;
+ char **tok;
+ char *pattern;
+
+ tok = pw_split_strv(tokens[i], ":", 2, &n_tok);
+ if (n_tok == 2 && parse_log_level(tok[1], &lvl)) {
+ pattern = tok[0];
+ pos += spa_scnprintf(pos, end - pos, "{ %s = %d },",
+ pattern, lvl);
+ } else if (n_tok == 1 && parse_log_level(tok[0], &lvl)) {
+ pw_log_set_level(lvl);
+ } else {
+ pw_log_warn("Ignoring invalid format in PIPEWIRE_DEBUG: '%s'",
+ tokens[i]);
+ }
+
+ pw_free_strv(tok);
+ }
+ }
+ pw_free_strv(tokens);
+ pos += spa_scnprintf(pos, end - pos, "]");
+ return strdup(json);
+}
+
+/** Initialize PipeWire
+ *
+ * \param argc pointer to argc
+ * \param argv pointer to argv
+ *
+ * Initialize the PipeWire system, parse and modify any parameters given
+ * by \a argc and \a argv and set up debugging.
+ *
+ * This function can be called multiple times.
+ */
+SPA_EXPORT
+void pw_init(int *argc, char **argv[])
+{
+ const char *str;
+ struct spa_dict_item items[6];
+ uint32_t n_items;
+ struct spa_dict info;
+ struct support *support = &global_support;
+ struct spa_log *log;
+ char level[32];
+
+ pthread_mutex_lock(&init_lock);
+ if (support->init_count > 0)
+ goto done;
+
+ pthread_mutex_lock(&support_lock);
+ support->in_valgrind = RUNNING_ON_VALGRIND;
+
+ support->do_dlclose = true;
+ if ((str = getenv("PIPEWIRE_DLCLOSE")) != NULL)
+ support->do_dlclose = pw_properties_parse_bool(str);
+
+ if (getenv("NO_COLOR") != NULL)
+ support->no_color = true;
+
+ if ((str = getenv("PIPEWIRE_NO_CONFIG")) != NULL)
+ support->no_config = pw_properties_parse_bool(str);
+
+ init_i18n(support);
+
+ if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
+ str = PLUGINDIR;
+ support->plugin_dir = str;
+
+ if ((str = getenv("SPA_SUPPORT_LIB")) == NULL)
+ str = SUPPORTLIB;
+ support->support_lib = str;
+
+ spa_list_init(&support->registry.plugins);
+ spa_list_init(&support->registry.handles);
+
+ if (pw_log_is_default()) {
+ char *patterns = NULL;
+
+ n_items = 0;
+ if (!support->no_color)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_COLORS, "true");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_TIMESTAMP, "true");
+ 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);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_LEVEL, level);
+ if ((str = getenv("PIPEWIRE_LOG")) != NULL)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_FILE, str);
+ if ((patterns = parse_pw_debug_env()) != NULL)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_PATTERNS, patterns);
+ info = SPA_DICT_INIT(items, n_items);
+
+ log = add_interface(support, SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log, &info);
+ if (log)
+ pw_log_set(log);
+
+#ifdef HAVE_SYSTEMD
+ if ((str = getenv("PIPEWIRE_LOG_SYSTEMD")) == NULL || spa_atob(str)) {
+ log = load_journal_logger(support, &info);
+ if (log)
+ pw_log_set(log);
+ }
+#endif
+ free(patterns);
+ } else {
+ support->support[support->n_support++] =
+ SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, pw_log_get());
+ }
+
+ pw_log_init();
+
+ n_items = 0;
+ if ((str = getenv("PIPEWIRE_CPU")))
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_CPU_FORCE, str);
+ if ((str = getenv("PIPEWIRE_VM")))
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_CPU_VM_TYPE, str);
+ info = SPA_DICT_INIT(items, n_items);
+
+ add_interface(support, SPA_NAME_SUPPORT_CPU, SPA_TYPE_INTERFACE_CPU, &info);
+
+ add_i18n(support);
+
+ pw_log_info("version %s", pw_get_library_version());
+ pthread_mutex_unlock(&support_lock);
+done:
+ support->init_count++;
+ pthread_mutex_unlock(&init_lock);
+}
+
+/** Deinitialize PipeWire
+ *
+ * Deinitialize the PipeWire system and free up all resources allocated
+ * by pw_init().
+ *
+ * Before 0.3.49 this function can only be called once after which the pipewire
+ * library can not be used again. This is usually called by test programs to
+ * check for memory leaks.
+ *
+ * Since 0.3.49 this function must be paired with an equal amount of pw_init()
+ * calls to deinitialize the PipeWire library. The PipeWire library can be
+ * used again after being deinitialized with a new pw_init() call.
+ */
+SPA_EXPORT
+void pw_deinit(void)
+{
+ struct support *support = &global_support;
+ struct registry *registry = &support->registry;
+ struct handle *h;
+
+ pthread_mutex_lock(&init_lock);
+ if (support->init_count == 0)
+ goto done;
+ if (--support->init_count > 0)
+ goto done;
+
+ pthread_mutex_lock(&support_lock);
+ pw_log_set(NULL);
+
+ spa_list_consume(h, &registry->handles, link)
+ unref_handle(h);
+
+ free(support->i18n_domain);
+ spa_zero(global_support);
+ pthread_mutex_unlock(&support_lock);
+done:
+ pthread_mutex_unlock(&init_lock);
+
+}
+
+/** Check if a debug category is enabled
+ *
+ * \param name the name of the category to check
+ * \return true if enabled
+ *
+ * Debugging categories can be enabled by using the PIPEWIRE_DEBUG
+ * environment variable
+ */
+SPA_EXPORT
+bool pw_debug_is_category_enabled(const char *name)
+{
+ struct spa_log_topic t = SPA_LOG_TOPIC(0, name);
+ PW_LOG_TOPIC_INIT(&t);
+ return t.has_custom_level;
+}
+
+/** Get the application name */
+SPA_EXPORT
+const char *pw_get_application_name(void)
+{
+ errno = ENOTSUP;
+ return NULL;
+}
+
+static void init_prgname(void)
+{
+ static char name[PATH_MAX];
+
+ spa_memzero(name, sizeof(name));
+#if defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__MidnightBSD_kernel__)
+ {
+ if (readlink("/proc/self/exe", name, sizeof(name)-1) > 0) {
+ prgname = strrchr(name, '/') + 1;
+ return;
+ }
+ }
+#endif
+#if defined __FreeBSD__ || defined(__MidnightBSD__)
+ {
+ ssize_t len;
+
+ if ((len = readlink("/proc/curproc/file", name, sizeof(name)-1)) > 0) {
+ prgname = strrchr(name, '/') + 1;
+ return;
+ }
+ }
+#endif
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
+ {
+ if (prctl(PR_GET_NAME, (unsigned long) name, 0, 0, 0) == 0) {
+ prgname = name;
+ return;
+ }
+ }
+#endif
+ snprintf(name, sizeof(name), "pid-%d", getpid());
+ prgname = name;
+}
+
+/** Get the program name */
+SPA_EXPORT
+const char *pw_get_prgname(void)
+{
+ static pthread_once_t prgname_is_initialized = PTHREAD_ONCE_INIT;
+
+ pthread_once(&prgname_is_initialized, init_prgname);
+ return prgname;
+}
+
+/** Get the user name */
+SPA_EXPORT
+const char *pw_get_user_name(void)
+{
+ struct passwd *pw;
+
+ if ((pw = getpwuid(getuid())))
+ return pw->pw_name;
+
+ return NULL;
+}
+
+/** Get the host name */
+SPA_EXPORT
+const char *pw_get_host_name(void)
+{
+ static char hname[256];
+
+ if (gethostname(hname, 256) < 0)
+ return NULL;
+
+ hname[255] = 0;
+ return hname;
+}
+
+SPA_EXPORT
+bool pw_in_valgrind(void)
+{
+ return global_support.in_valgrind;
+}
+
+SPA_EXPORT
+bool pw_check_option(const char *option, const char *value)
+{
+ if (spa_streq(option, "in-valgrind"))
+ return global_support.in_valgrind == spa_atob(value);
+ else if (spa_streq(option, "no-color"))
+ return global_support.no_color == spa_atob(value);
+ else if (spa_streq(option, "no-config"))
+ return global_support.no_config == spa_atob(value);
+ else if (spa_streq(option, "do-dlclose"))
+ return global_support.do_dlclose == spa_atob(value);
+ return false;
+}
+
+/** Get the client name
+ *
+ * Make a new PipeWire client name that can be used to construct a remote.
+ *
+ */
+SPA_EXPORT
+const char *pw_get_client_name(void)
+{
+ const char *cc;
+ static char cname[256];
+
+ if ((cc = pw_get_application_name()) || (cc = pw_get_prgname()))
+ return cc;
+ else if (snprintf(cname, sizeof(cname), "pipewire-pid-%zd", (size_t) getpid()) < 0)
+ return NULL;
+ return cname;
+}
+
+/** Reverse the direction */
+SPA_EXPORT
+enum pw_direction pw_direction_reverse(enum pw_direction direction)
+{
+ if (direction == PW_DIRECTION_INPUT)
+ return PW_DIRECTION_OUTPUT;
+ else if (direction == PW_DIRECTION_OUTPUT)
+ return PW_DIRECTION_INPUT;
+ return direction;
+}
+
+/** Get the currently running version */
+SPA_EXPORT
+const char* pw_get_library_version(void)
+{
+ return pw_get_headers_version();
+}
+
+static const struct spa_type_info type_info[] = {
+ { SPA_ID_INVALID, SPA_ID_INVALID, "spa_types", spa_types },
+ { 0, 0, NULL, NULL },
+};
+
+SPA_EXPORT
+const struct spa_type_info * pw_type_info(void)
+{
+ return type_info;
+}
diff --git a/src/pipewire/pipewire.h b/src/pipewire/pipewire.h
new file mode 100644
index 0000000..ed24fbd
--- /dev/null
+++ b/src/pipewire/pipewire.h
@@ -0,0 +1,123 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_H
+#define PIPEWIRE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/support/plugin.h>
+
+#include <pipewire/array.h>
+#include <pipewire/client.h>
+#include <pipewire/conf.h>
+#include <pipewire/context.h>
+#include <pipewire/device.h>
+#include <pipewire/buffers.h>
+#include <pipewire/core.h>
+#include <pipewire/factory.h>
+#include <pipewire/keys.h>
+#include <pipewire/log.h>
+#include <pipewire/loop.h>
+#include <pipewire/link.h>
+#include <pipewire/main-loop.h>
+#include <pipewire/map.h>
+#include <pipewire/mem.h>
+#include <pipewire/module.h>
+#include <pipewire/node.h>
+#include <pipewire/properties.h>
+#include <pipewire/proxy.h>
+#include <pipewire/permission.h>
+#include <pipewire/protocol.h>
+#include <pipewire/port.h>
+#include <pipewire/stream.h>
+#include <pipewire/filter.h>
+#include <pipewire/thread-loop.h>
+#include <pipewire/data-loop.h>
+#include <pipewire/type.h>
+#include <pipewire/utils.h>
+#include <pipewire/version.h>
+
+/** \defgroup pw_pipewire Initialization
+ * Initializing PipeWire and loading SPA modules.
+ */
+
+/**
+ * \addtogroup pw_pipewire
+ * \{
+ */
+void
+pw_init(int *argc, char **argv[]);
+
+void pw_deinit(void);
+
+bool
+pw_debug_is_category_enabled(const char *name);
+
+const char *
+pw_get_application_name(void);
+
+const char *
+pw_get_prgname(void);
+
+const char *
+pw_get_user_name(void);
+
+const char *
+pw_get_host_name(void);
+
+const char *
+pw_get_client_name(void);
+
+bool pw_in_valgrind(void);
+
+bool pw_check_option(const char *option, const char *value);
+
+enum pw_direction
+pw_direction_reverse(enum pw_direction direction);
+
+int pw_set_domain(const char *domain);
+const char *pw_get_domain(void);
+
+uint32_t pw_get_support(struct spa_support *support, uint32_t max_support);
+
+struct spa_handle *pw_load_spa_handle(const char *lib,
+ const char *factory_name,
+ const struct spa_dict *info,
+ uint32_t n_support,
+ const struct spa_support support[]);
+
+int pw_unload_spa_handle(struct spa_handle *handle);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_H */
diff --git a/src/pipewire/port.h b/src/pipewire/port.h
new file mode 100644
index 0000000..463af23
--- /dev/null
+++ b/src/pipewire/port.h
@@ -0,0 +1,179 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PORT_H
+#define PIPEWIRE_PORT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+#include <errno.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/hook.h>
+#include <spa/param/param.h>
+
+#include <pipewire/proxy.h>
+
+/** \defgroup pw_port Port
+ * Port interface
+ */
+
+/**
+ * \addtogroup pw_port
+ * \{
+ */
+
+#define PW_TYPE_INTERFACE_Port PW_TYPE_INFO_INTERFACE_BASE "Port"
+
+#define PW_VERSION_PORT 3
+struct pw_port;
+
+/** The direction of a port */
+#define pw_direction spa_direction
+#define PW_DIRECTION_INPUT SPA_DIRECTION_INPUT
+#define PW_DIRECTION_OUTPUT SPA_DIRECTION_OUTPUT
+
+/** Convert a \ref pw_direction to a readable string */
+const char * pw_direction_as_string(enum pw_direction direction);
+
+struct pw_port_info {
+ uint32_t id; /**< id of the global */
+ enum pw_direction direction; /**< port direction */
+#define PW_PORT_CHANGE_MASK_PROPS (1 << 0)
+#define PW_PORT_CHANGE_MASK_PARAMS (1 << 1)
+#define PW_PORT_CHANGE_MASK_ALL ((1 << 2)-1)
+ uint64_t change_mask; /**< bitfield of changed fields since last call */
+ struct spa_dict *props; /**< the properties of the port */
+ struct spa_param_info *params; /**< parameters */
+ uint32_t n_params; /**< number of items in \a params */
+};
+
+struct pw_port_info *
+pw_port_info_update(struct pw_port_info *info,
+ const struct pw_port_info *update);
+
+struct pw_port_info *
+pw_port_info_merge(struct pw_port_info *info,
+ const struct pw_port_info *update, bool reset);
+
+void
+pw_port_info_free(struct pw_port_info *info);
+
+#define PW_PORT_EVENT_INFO 0
+#define PW_PORT_EVENT_PARAM 1
+#define PW_PORT_EVENT_NUM 2
+
+/** Port events */
+struct pw_port_events {
+#define PW_VERSION_PORT_EVENTS 0
+ uint32_t version;
+ /**
+ * Notify port info
+ *
+ * \param info info about the port
+ */
+ void (*info) (void *data, const struct pw_port_info *info);
+ /**
+ * Notify a port param
+ *
+ * Event emitted as a result of the enum_params method.
+ *
+ * \param seq the sequence number of the request
+ * \param id the param id
+ * \param index the param index
+ * \param next the param index of the next param
+ * \param param the parameter
+ */
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+};
+
+#define PW_PORT_METHOD_ADD_LISTENER 0
+#define PW_PORT_METHOD_SUBSCRIBE_PARAMS 1
+#define PW_PORT_METHOD_ENUM_PARAMS 2
+#define PW_PORT_METHOD_NUM 3
+
+/** Port methods */
+struct pw_port_methods {
+#define PW_VERSION_PORT_METHODS 0
+ uint32_t version;
+
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_port_events *events,
+ void *data);
+ /**
+ * Subscribe to parameter changes
+ *
+ * Automatically emit param events for the given ids when
+ * they are changed.
+ *
+ * \param ids an array of param ids
+ * \param n_ids the number of ids in \a ids
+ */
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+
+ /**
+ * Enumerate port parameters
+ *
+ * Start enumeration of port parameters. For each param, a
+ * param event will be emitted.
+ *
+ * \param seq a sequence number returned in the reply
+ * \param id the parameter id to enumerate
+ * \param start the start index or 0 for the first param
+ * \param num the maximum number of params to retrieve
+ * \param filter a param filter or NULL
+ */
+ int (*enum_params) (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ 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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_PORT_H */
diff --git a/src/pipewire/private.h b/src/pipewire/private.h
new file mode 100644
index 0000000..6fa8d77
--- /dev/null
+++ b/src/pipewire/private.h
@@ -0,0 +1,1310 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PRIVATE_H
+#define PIPEWIRE_PRIVATE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <sys/socket.h>
+#include <sys/types.h> /* for pthread_t */
+
+#include "pipewire/impl.h"
+
+#include <spa/support/plugin.h>
+#include <spa/pod/builder.h>
+#include <spa/param/latency-utils.h>
+#include <spa/utils/result.h>
+#include <spa/utils/type-info.h>
+
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+struct ucred {
+};
+#endif
+
+#define MAX_RATES 32u
+#define CLOCK_MIN_QUANTUM 4u
+#define CLOCK_MAX_QUANTUM 65536u
+
+struct settings {
+ uint32_t log_level;
+ uint32_t clock_rate; /* default clock rate */
+ uint32_t clock_rates[MAX_RATES]; /* allowed clock rates */
+ uint32_t n_clock_rates; /* number of alternative clock rates */
+ uint32_t clock_quantum; /* default quantum */
+ uint32_t clock_min_quantum; /* min quantum */
+ uint32_t clock_max_quantum; /* max quantum */
+ uint32_t clock_quantum_limit; /* quantum limit */
+ struct spa_rectangle video_size;
+ struct spa_fraction video_rate;
+ uint32_t link_max_buffers;
+ unsigned int mem_warn_mlock:1;
+ unsigned int mem_allow_mlock:1;
+ unsigned int clock_power_of_two_quantum:1;
+ unsigned int check_quantum:1;
+ unsigned int check_rate:1;
+#define CLOCK_RATE_UPDATE_MODE_HARD 0
+#define CLOCK_RATE_UPDATE_MODE_SOFT 1
+ int clock_rate_update_mode;
+ uint32_t clock_force_rate; /* force a clock rate */
+ uint32_t clock_force_quantum; /* force a quantum */
+};
+
+struct ratelimit {
+ uint64_t interval;
+ uint64_t begin;
+ unsigned burst;
+ unsigned n_printed, n_missed;
+};
+
+static inline bool ratelimit_test(struct ratelimit *r, uint64_t now, enum spa_log_level level)
+{
+ if (r->begin + r->interval < now) {
+ if (r->n_missed)
+ pw_log(level, "%u events suppressed", r->n_missed);
+ r->begin = now;
+ r->n_printed = 0;
+ r->n_missed = 0;
+ } else if (r->n_printed >= r->burst) {
+ r->n_missed++;
+ return false;
+ }
+ r->n_printed++;
+ return true;
+}
+
+#define MAX_PARAMS 32
+
+struct pw_param {
+ uint32_t id;
+ int32_t seq;
+ struct spa_list link;
+ struct spa_pod *param;
+};
+
+static inline uint32_t pw_param_clear(struct spa_list *param_list, uint32_t id)
+{
+ struct pw_param *p, *t;
+ uint32_t count = 0;
+
+ spa_list_for_each_safe(p, t, param_list, link) {
+ if (id == SPA_ID_INVALID || p->id == id) {
+ spa_list_remove(&p->link);
+ free(p);
+ count++;
+ }
+ }
+ return count;
+}
+
+static inline struct pw_param *pw_param_add(struct spa_list *params, int32_t seq,
+ uint32_t id, const struct spa_pod *param)
+{
+ struct pw_param *p;
+
+ if (id == SPA_ID_INVALID) {
+ if (param == NULL || !spa_pod_is_object(param)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ id = SPA_POD_OBJECT_ID(param);
+ }
+
+ if ((p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0))) == NULL)
+ return NULL;
+
+ p->id = id;
+ p->seq = seq;
+ if (param != NULL) {
+ p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ } else {
+ pw_param_clear(params, id);
+ p->param = NULL;
+ }
+ spa_list_append(params, &p->link);
+ return p;
+}
+
+static inline void pw_param_update(struct spa_list *param_list, struct spa_list *pending_list,
+ uint32_t n_params, struct spa_param_info *params)
+{
+ struct pw_param *p, *t;
+ uint32_t i;
+
+ for (i = 0; i < n_params; i++) {
+ spa_list_for_each_safe(p, t, pending_list, link) {
+ if (p->id == params[i].id &&
+ p->seq != params[i].seq &&
+ p->param != NULL) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+ }
+ spa_list_consume(p, pending_list, link) {
+ spa_list_remove(&p->link);
+ if (p->param == NULL) {
+ pw_param_clear(param_list, p->id);
+ free(p);
+ } else {
+ spa_list_append(param_list, &p->link);
+ }
+ }
+}
+
+static inline struct spa_param_info *pw_param_info_find(struct spa_param_info info[],
+ uint32_t n_info, uint32_t id)
+{
+ uint32_t i;
+ for (i = 0; i < n_info; i++) {
+ if (info[i].id == id)
+ return &info[i];
+ }
+ return NULL;
+}
+
+#define pw_protocol_emit_destroy(p) spa_hook_list_call(&(p)->listener_list, struct pw_protocol_events, destroy, 0)
+
+struct pw_protocol {
+ struct spa_list link; /**< link in context protocol_list */
+ struct pw_context *context; /**< context for this protocol */
+
+ char *name; /**< type name of the protocol */
+
+ struct spa_list marshal_list; /**< list of marshallers for supported interfaces */
+ struct spa_list client_list; /**< list of current clients */
+ struct spa_list server_list; /**< list of current servers */
+ struct spa_hook_list listener_list; /**< event listeners */
+
+ const struct pw_protocol_implementation *implementation; /**< implementation of the protocol */
+
+ const void *extension; /**< extension API */
+
+ void *user_data; /**< user data for the implementation */
+};
+
+/** the permission function. It returns the allowed access permissions for \a global
+ * for \a client */
+typedef uint32_t (*pw_permission_func_t) (struct pw_global *global,
+ struct pw_impl_client *client, void *data);
+
+#define pw_impl_client_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_impl_client_events, m, v, ##__VA_ARGS__)
+
+#define pw_impl_client_emit_destroy(o) pw_impl_client_emit(o, destroy, 0)
+#define pw_impl_client_emit_free(o) pw_impl_client_emit(o, free, 0)
+#define pw_impl_client_emit_initialized(o) pw_impl_client_emit(o, initialized, 0)
+#define pw_impl_client_emit_info_changed(o,i) pw_impl_client_emit(o, info_changed, 0, i)
+#define pw_impl_client_emit_resource_added(o,r) pw_impl_client_emit(o, resource_added, 0, r)
+#define pw_impl_client_emit_resource_impl(o,r) pw_impl_client_emit(o, resource_impl, 0, r)
+#define pw_impl_client_emit_resource_removed(o,r) pw_impl_client_emit(o, resource_removed, 0, r)
+#define pw_impl_client_emit_busy_changed(o,b) pw_impl_client_emit(o, busy_changed, 0, b)
+
+enum spa_node0_event {
+ SPA_NODE0_EVENT_START = SPA_TYPE_VENDOR_PipeWire,
+ SPA_NODE0_EVENT_RequestClockUpdate,
+};
+
+enum spa_node0_command {
+ SPA_NODE0_COMMAND_START = SPA_TYPE_VENDOR_PipeWire,
+ SPA_NODE0_COMMAND_ClockUpdate,
+};
+
+struct protocol_compat_v2 {
+ /* v2 typemap */
+ struct pw_map types;
+ unsigned int send_types:1;
+};
+
+#define pw_impl_core_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct pw_impl_core_events, m, v, ##__VA_ARGS__)
+
+#define pw_impl_core_emit_destroy(s) pw_impl_core_emit(s, destroy, 0)
+#define pw_impl_core_emit_free(s) pw_impl_core_emit(s, free, 0)
+#define pw_impl_core_emit_initialized(s) pw_impl_core_emit(s, initialized, 0)
+
+struct pw_impl_core {
+ struct pw_context *context;
+ struct spa_list link; /**< link in context object core_impl list */
+ struct pw_global *global; /**< global object created for this core */
+ struct spa_hook global_listener;
+
+ struct pw_properties *properties; /**< core properties */
+ struct pw_core_info info; /**< core info */
+
+ struct spa_hook_list listener_list;
+ void *user_data; /**< extra user data */
+
+ unsigned int registered:1;
+};
+
+#define pw_impl_metadata_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct pw_impl_metadata_events, m, v, ##__VA_ARGS__)
+
+#define pw_impl_metadata_emit_destroy(s) pw_impl_metadata_emit(s, destroy, 0)
+#define pw_impl_metadata_emit_free(s) pw_impl_metadata_emit(s, free, 0)
+#define pw_impl_metadata_emit_property(s, ...) pw_impl_metadata_emit(s, property, 0, __VA_ARGS__)
+
+struct pw_impl_metadata {
+ struct pw_context *context; /**< the context */
+ struct spa_list link; /**< link in context metadata_list */
+ struct pw_global *global; /**< global for this metadata */
+ struct spa_hook global_listener;
+
+ struct pw_properties *properties; /**< properties of the metadata */
+
+ struct pw_metadata *metadata;
+ struct spa_hook metadata_listener;
+
+ struct spa_hook_list listener_list; /**< event listeners */
+ void *user_data;
+
+ unsigned int registered:1;
+};
+
+struct pw_impl_client {
+ struct pw_impl_core *core; /**< core object */
+ struct pw_context *context; /**< context object */
+
+ struct spa_list link; /**< link in context object client list */
+ struct pw_global *global; /**< global object created for this client */
+ struct spa_hook global_listener;
+
+ pw_permission_func_t permission_func; /**< get permissions of an object */
+ void *permission_data; /**< data passed to permission function */
+
+ struct pw_properties *properties; /**< Client properties */
+
+ struct pw_client_info info; /**< client info */
+
+ struct pw_mempool *pool; /**< client mempool */
+ struct pw_resource *core_resource; /**< core resource object */
+ struct pw_resource *client_resource; /**< client resource object */
+
+ struct pw_map objects; /**< list of resource objects */
+
+ struct spa_hook_list listener_list;
+
+ struct pw_protocol *protocol; /**< protocol in use */
+ int recv_seq; /**< last received sequence number */
+ int send_seq; /**< last sender sequence number */
+ uint64_t recv_generation; /**< last received registry generation */
+ uint64_t sent_generation; /**< last sent registry generation */
+
+ void *user_data; /**< extra user data */
+
+ struct ucred ucred; /**< ucred information */
+ unsigned int registered:1;
+ unsigned int ucred_valid:1; /**< if the ucred member is valid */
+ unsigned int busy:1;
+ unsigned int destroyed:1;
+
+ int refcount;
+
+ /* v2 compatibility data */
+ void *compat_v2;
+};
+
+#define pw_global_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_global_events, m, v, ##__VA_ARGS__)
+
+#define pw_global_emit_registering(g) pw_global_emit(g, registering, 0)
+#define pw_global_emit_destroy(g) pw_global_emit(g, destroy, 0)
+#define pw_global_emit_free(g) pw_global_emit(g, free, 0)
+#define pw_global_emit_permissions_changed(g,...) pw_global_emit(g, permissions_changed, 0, __VA_ARGS__)
+
+struct pw_global {
+ struct pw_context *context; /**< the context */
+
+ struct spa_list link; /**< link in context list of globals */
+ uint32_t id; /**< server id of the object */
+
+ struct pw_properties *properties; /**< properties of the global */
+
+ struct spa_hook_list listener_list;
+
+ const char *type; /**< type of interface */
+ uint32_t version; /**< version of interface */
+
+ pw_global_bind_func_t func; /**< bind function */
+ void *object; /**< object associated with the interface */
+ uint64_t serial; /**< increasing serial number */
+ uint64_t generation; /**< registry generation number */
+
+ struct spa_list resource_list; /**< The list of resources of this global */
+
+ unsigned int registered:1;
+ unsigned int destroyed:1;
+};
+
+#define pw_core_resource(r,m,v,...) pw_resource_call(r, struct pw_core_events, m, v, ##__VA_ARGS__)
+#define pw_core_resource_info(r,...) pw_core_resource(r,info,0,__VA_ARGS__)
+#define pw_core_resource_done(r,...) pw_core_resource(r,done,0,__VA_ARGS__)
+#define pw_core_resource_ping(r,...) pw_core_resource(r,ping,0,__VA_ARGS__)
+#define pw_core_resource_error(r,...) pw_core_resource(r,error,0,__VA_ARGS__)
+#define pw_core_resource_remove_id(r,...) pw_core_resource(r,remove_id,0,__VA_ARGS__)
+#define pw_core_resource_bound_id(r,...) pw_core_resource(r,bound_id,0,__VA_ARGS__)
+#define pw_core_resource_add_mem(r,...) pw_core_resource(r,add_mem,0,__VA_ARGS__)
+#define pw_core_resource_remove_mem(r,...) pw_core_resource(r,remove_mem,0,__VA_ARGS__)
+
+static inline SPA_PRINTF_FUNC(5,0) void
+pw_core_resource_errorv(struct pw_resource *resource, uint32_t id, int seq,
+ int res, const char *message, va_list args)
+{
+ char buffer[1024];
+ vsnprintf(buffer, sizeof(buffer), message, args);
+ buffer[1023] = '\0';
+ pw_log_debug("resource %p: id:%d seq:%d res:%d (%s) msg:\"%s\"",
+ resource, id, seq, res, spa_strerror(res), buffer);
+ if (resource)
+ pw_core_resource_error(resource, id, seq, res, buffer);
+ else
+ pw_log_error("id:%d seq:%d res:%d (%s) msg:\"%s\"",
+ id, seq, res, spa_strerror(res), buffer);
+}
+
+static inline SPA_PRINTF_FUNC(5,6) void
+pw_core_resource_errorf(struct pw_resource *resource, uint32_t id, int seq,
+ int res, const char *message, ...)
+{
+ va_list args;
+ va_start(args, message);
+ pw_core_resource_errorv(resource, id, seq, res, message, args);
+ va_end(args);
+}
+
+#define pw_context_driver_emit(c,m,v,...) spa_hook_list_call_simple(&c->driver_listener_list, struct pw_context_driver_events, m, v, ##__VA_ARGS__)
+#define pw_context_driver_emit_start(c,n) pw_context_driver_emit(c, start, 0, n)
+#define pw_context_driver_emit_xrun(c,n) pw_context_driver_emit(c, xrun, 0, n)
+#define pw_context_driver_emit_incomplete(c,n) pw_context_driver_emit(c, incomplete, 0, n)
+#define pw_context_driver_emit_timeout(c,n) pw_context_driver_emit(c, timeout, 0, n)
+#define pw_context_driver_emit_drained(c,n) pw_context_driver_emit(c, drained, 0, n)
+#define pw_context_driver_emit_complete(c,n) pw_context_driver_emit(c, complete, 0, n)
+
+struct pw_context_driver_events {
+#define PW_VERSION_CONTEXT_DRIVER_EVENTS 0
+ uint32_t version;
+
+ /** The driver graph is started */
+ void (*start) (void *data, struct pw_impl_node *node);
+ /** The driver under/overruns */
+ void (*xrun) (void *data, struct pw_impl_node *node);
+ /** The driver could not complete the graph */
+ void (*incomplete) (void *data, struct pw_impl_node *node);
+ /** The driver got a sync timeout */
+ void (*timeout) (void *data, struct pw_impl_node *node);
+ /** a node drained */
+ void (*drained) (void *data, struct pw_impl_node *node);
+ /** The driver completed the graph */
+ void (*complete) (void *data, struct pw_impl_node *node);
+};
+
+#define pw_registry_resource(r,m,v,...) pw_resource_call(r, struct pw_registry_events,m,v,##__VA_ARGS__)
+#define pw_registry_resource_global(r,...) pw_registry_resource(r,global,0,__VA_ARGS__)
+#define pw_registry_resource_global_remove(r,...) pw_registry_resource(r,global_remove,0,__VA_ARGS__)
+
+#define pw_context_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_context_events, m, v, ##__VA_ARGS__)
+#define pw_context_emit_destroy(c) pw_context_emit(c, destroy, 0)
+#define pw_context_emit_free(c) pw_context_emit(c, free, 0)
+#define pw_context_emit_info_changed(c,i) pw_context_emit(c, info_changed, 0, i)
+#define pw_context_emit_check_access(c,cl) pw_context_emit(c, check_access, 0, cl)
+#define pw_context_emit_global_added(c,g) pw_context_emit(c, global_added, 0, g)
+#define pw_context_emit_global_removed(c,g) pw_context_emit(c, global_removed, 0, g)
+
+struct pw_context {
+ struct pw_impl_core *core; /**< core object */
+
+ struct pw_properties *conf; /**< configuration of the context */
+ struct pw_properties *properties; /**< properties of the context */
+
+ struct settings defaults; /**< default parameters */
+ struct settings settings; /**< current parameters */
+
+ void *settings_impl; /**< settings metadata */
+
+ struct pw_mempool *pool; /**< global memory pool */
+
+ uint64_t stamp;
+ uint64_t serial;
+ uint64_t generation; /**< registry generation number */
+ struct pw_map globals; /**< map of globals */
+
+ struct spa_list core_impl_list; /**< list of core_imp */
+ struct spa_list protocol_list; /**< list of protocols */
+ struct spa_list core_list; /**< list of core connections */
+ struct spa_list registry_resource_list; /**< list of registry resources */
+ struct spa_list module_list; /**< list of modules */
+ struct spa_list device_list; /**< list of devices */
+ struct spa_list global_list; /**< list of globals */
+ struct spa_list client_list; /**< list of clients */
+ struct spa_list node_list; /**< list of nodes */
+ struct spa_list factory_list; /**< list of factories */
+ struct spa_list metadata_list; /**< list of metadata */
+ struct spa_list link_list; /**< list of links */
+ struct spa_list control_list[2]; /**< list of controls, indexed by direction */
+ struct spa_list export_list; /**< list of export types */
+ struct spa_list driver_list; /**< list of driver nodes */
+
+ struct spa_hook_list driver_listener_list;
+ struct spa_hook_list listener_list;
+
+ struct spa_thread_utils *thread_utils;
+ struct pw_loop *main_loop; /**< main loop for control */
+ struct pw_loop *data_loop; /**< data loop for data passing */
+ struct pw_data_loop *data_loop_impl;
+ struct spa_system *data_system; /**< data system for data passing */
+ struct pw_work_queue *work_queue; /**< work queue */
+
+ struct spa_support support[16]; /**< support for spa plugins */
+ uint32_t n_support; /**< number of support items */
+ struct pw_array factory_lib; /**< mapping of factory_name regexp to library */
+
+ struct pw_array objects; /**< objects */
+
+ struct pw_impl_client *current_client; /**< client currently executing code in mainloop */
+
+ long sc_pagesize;
+ unsigned int freewheeling:1;
+
+ void *user_data; /**< extra user data */
+};
+
+#define pw_data_loop_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_data_loop_events, m, v, ##__VA_ARGS__)
+#define pw_data_loop_emit_destroy(o) pw_data_loop_emit(o, destroy, 0)
+
+struct pw_data_loop {
+ struct pw_loop *loop;
+
+ struct spa_hook_list listener_list;
+
+ struct spa_thread_utils *thread_utils;
+
+ pthread_t thread;
+ unsigned int cancel:1;
+ unsigned int created:1;
+ unsigned int running:1;
+};
+
+#define pw_main_loop_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_main_loop_events, m, v, ##__VA_ARGS__)
+#define pw_main_loop_emit_destroy(o) pw_main_loop_emit(o, destroy, 0)
+
+struct pw_main_loop {
+ struct pw_loop *loop;
+
+ struct spa_hook_list listener_list;
+
+ unsigned int created:1;
+ unsigned int running:1;
+};
+
+#define pw_impl_device_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_impl_device_events, m, v, ##__VA_ARGS__)
+#define pw_impl_device_emit_destroy(m) pw_impl_device_emit(m, destroy, 0)
+#define pw_impl_device_emit_free(m) pw_impl_device_emit(m, free, 0)
+#define pw_impl_device_emit_initialized(m) pw_impl_device_emit(m, initialized, 0)
+#define pw_impl_device_emit_info_changed(n,i) pw_impl_device_emit(n, info_changed, 0, i)
+
+struct pw_impl_device {
+ struct pw_context *context; /**< the context object */
+ struct spa_list link; /**< link in the context device_list */
+ struct pw_global *global; /**< global object for this device */
+ struct spa_hook global_listener;
+
+ struct pw_properties *properties; /**< properties of the device */
+ struct pw_device_info info; /**< introspectable device info */
+ struct spa_param_info params[MAX_PARAMS];
+
+ char *name; /**< device name for debug */
+
+ struct spa_device *device; /**< device implementation */
+ struct spa_hook listener;
+ struct spa_hook_list listener_list;
+
+ struct spa_list object_list;
+
+ void *user_data; /**< device user_data */
+
+ unsigned int registered:1;
+};
+
+#define pw_impl_module_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_impl_module_events, m, v, ##__VA_ARGS__)
+#define pw_impl_module_emit_destroy(m) pw_impl_module_emit(m, destroy, 0)
+#define pw_impl_module_emit_free(m) pw_impl_module_emit(m, free, 0)
+#define pw_impl_module_emit_initialized(m) pw_impl_module_emit(m, initialized, 0)
+#define pw_impl_module_emit_registered(m) pw_impl_module_emit(m, registered, 0)
+
+struct pw_impl_module {
+ struct pw_context *context; /**< the context object */
+ struct spa_list link; /**< link in the context module_list */
+ struct pw_global *global; /**< global object for this module */
+ struct spa_hook global_listener;
+
+ struct pw_properties *properties; /**< properties of the module */
+ struct pw_module_info info; /**< introspectable module info */
+
+ struct spa_hook_list listener_list;
+
+ void *user_data; /**< module user_data */
+};
+
+struct pw_node_activation_state {
+ int status; /**< current status, the result of spa_node_process() */
+ int32_t required; /**< required number of signals */
+ int32_t pending; /**< number of pending signals */
+};
+
+static inline void pw_node_activation_state_reset(struct pw_node_activation_state *state)
+{
+ state->pending = state->required;
+}
+
+#define pw_node_activation_state_dec(s,c) (__atomic_sub_fetch(&(s)->pending, c, __ATOMIC_SEQ_CST) == 0)
+
+struct pw_node_target {
+ struct spa_list link;
+ struct pw_impl_node *node;
+ struct pw_node_activation *activation;
+ int (*signal_func) (void *data);
+ void *data;
+ unsigned int active:1;
+};
+
+struct pw_node_activation {
+#define PW_NODE_ACTIVATION_NOT_TRIGGERED 0
+#define PW_NODE_ACTIVATION_TRIGGERED 1
+#define PW_NODE_ACTIVATION_AWAKE 2
+#define PW_NODE_ACTIVATION_FINISHED 3
+ uint32_t status;
+
+ unsigned int version:1;
+ unsigned int pending_sync:1; /* a sync is pending */
+ unsigned int pending_new_pos:1; /* a new position is pending */
+
+ 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;
+
+ /* updates */
+ struct spa_io_segment reposition; /* reposition info, used when driver reposition_owner
+ * has this node id */
+ struct spa_io_segment segment; /* update for the extra segment info fields.
+ * used when driver segment_owner has this node id */
+
+ /* for drivers, shared with all nodes */
+ uint32_t segment_owner[32]; /* id of owners for each segment info struct.
+ * nodes that want to update segment info need to
+ * CAS their node id in this array. */
+ struct spa_io_position position; /* contains current position and segment info.
+ * extra info is updated by nodes that have set
+ * themselves as owner in the segment structs */
+
+ uint64_t sync_timeout; /* sync timeout in nanoseconds
+ * position goes to RUNNING without waiting any
+ * longer for sync clients. */
+ uint64_t sync_left; /* number of cycles before timeout */
+
+
+ float cpu_load[3]; /* averaged over short, medium, long time */
+ uint32_t xrun_count; /* number of xruns */
+ uint64_t xrun_time; /* time of last xrun in microseconds */
+ uint64_t xrun_delay; /* delay of last xrun in microseconds */
+ uint64_t max_delay; /* max of all xruns in microseconds */
+
+#define PW_NODE_ACTIVATION_COMMAND_NONE 0
+#define PW_NODE_ACTIVATION_COMMAND_START 1
+#define PW_NODE_ACTIVATION_COMMAND_STOP 2
+ uint32_t command; /* next command */
+ uint32_t reposition_owner; /* owner id with new reposition info, last one
+ * to update wins */
+};
+
+#define ATOMIC_CAS(v,ov,nv) \
+({ \
+ __typeof__(v) __ov = (ov); \
+ __atomic_compare_exchange_n(&(v), &__ov, (nv), \
+ 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); \
+})
+
+#define ATOMIC_DEC(s) __atomic_sub_fetch(&(s), 1, __ATOMIC_SEQ_CST)
+#define ATOMIC_INC(s) __atomic_add_fetch(&(s), 1, __ATOMIC_SEQ_CST)
+#define ATOMIC_LOAD(s) __atomic_load_n(&(s), __ATOMIC_SEQ_CST)
+#define ATOMIC_STORE(s,v) __atomic_store_n(&(s), (v), __ATOMIC_SEQ_CST)
+#define ATOMIC_XCHG(s,v) __atomic_exchange_n(&(s), (v), __ATOMIC_SEQ_CST)
+
+#define SEQ_WRITE(s) ATOMIC_INC(s)
+#define SEQ_WRITE_SUCCESS(s1,s2) ((s1) + 1 == (s2) && ((s2) & 1) == 0)
+
+#define SEQ_READ(s) ATOMIC_LOAD(s)
+#define SEQ_READ_SUCCESS(s1,s2) ((s1) == (s2) && ((s2) & 1) == 0)
+
+#define pw_impl_node_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_impl_node_events, m, v, ##__VA_ARGS__)
+#define pw_impl_node_emit_destroy(n) pw_impl_node_emit(n, destroy, 0)
+#define pw_impl_node_emit_free(n) pw_impl_node_emit(n, free, 0)
+#define pw_impl_node_emit_initialized(n) pw_impl_node_emit(n, initialized, 0)
+#define pw_impl_node_emit_port_init(n,p) pw_impl_node_emit(n, port_init, 0, p)
+#define pw_impl_node_emit_port_added(n,p) pw_impl_node_emit(n, port_added, 0, p)
+#define pw_impl_node_emit_port_removed(n,p) pw_impl_node_emit(n, port_removed, 0, p)
+#define pw_impl_node_emit_info_changed(n,i) pw_impl_node_emit(n, info_changed, 0, i)
+#define pw_impl_node_emit_port_info_changed(n,p,i) pw_impl_node_emit(n, port_info_changed, 0, p, i)
+#define pw_impl_node_emit_active_changed(n,a) pw_impl_node_emit(n, active_changed, 0, a)
+#define pw_impl_node_emit_state_request(n,s) pw_impl_node_emit(n, state_request, 0, s)
+#define pw_impl_node_emit_state_changed(n,o,s,e) pw_impl_node_emit(n, state_changed, 0, o, s, e)
+#define pw_impl_node_emit_async_complete(n,s,r) pw_impl_node_emit(n, async_complete, 0, s, r)
+#define pw_impl_node_emit_result(n,s,r,t,result) pw_impl_node_emit(n, result, 0, s, r, t, result)
+#define pw_impl_node_emit_event(n,e) pw_impl_node_emit(n, event, 0, e)
+#define pw_impl_node_emit_driver_changed(n,o,d) pw_impl_node_emit(n, driver_changed, 0, o, d)
+#define pw_impl_node_emit_peer_added(n,p) pw_impl_node_emit(n, peer_added, 0, p)
+#define pw_impl_node_emit_peer_removed(n,p) pw_impl_node_emit(n, peer_removed, 0, p)
+
+struct pw_impl_node {
+ struct pw_context *context; /**< context object */
+ struct spa_list link; /**< link in context node_list */
+ struct pw_global *global; /**< global for this node */
+ struct spa_hook global_listener;
+
+ struct pw_properties *properties; /**< properties of the node */
+
+ struct pw_node_info info; /**< introspectable node info */
+ struct spa_param_info params[MAX_PARAMS];
+
+ char *name; /** for debug */
+
+ uint32_t priority_driver; /** priority for being driver */
+ char group[128]; /** group to schedule this node in */
+ uint64_t spa_flags;
+
+ unsigned int registered:1;
+ unsigned int active:1; /**< if the node is active */
+ unsigned int live:1; /**< if the node is live */
+ unsigned int driver:1; /**< if the node can drive the graph */
+ unsigned int exported:1; /**< if the node is exported */
+ unsigned int remote:1; /**< if the node is implemented remotely */
+ unsigned int driving:1; /**< a driving node is one of the driver nodes that
+ * is selected to drive the graph */
+ unsigned int visited:1; /**< for sorting */
+ unsigned int want_driver:1; /**< this node wants to be assigned to a driver */
+ unsigned int passive:1; /**< driver graph only has passive links */
+ unsigned int freewheel:1; /**< if this is the freewheel driver */
+ unsigned int loopchecked:1; /**< for feedback loop checking */
+ unsigned int always_process:1; /**< this node wants to always be processing, even when idle */
+ unsigned int lock_quantum:1; /**< don't change graph quantum */
+ unsigned int lock_rate:1; /**< don't change graph rate */
+ unsigned int transport_sync:1; /**< supports transport sync */
+ unsigned int current_pending:1; /**< a quantum/rate update is pending */
+ unsigned int moved:1; /**< the node was moved drivers */
+ unsigned int added:1; /**< the node was add to graph */
+ unsigned int pause_on_idle:1; /**< Pause processing when IDLE */
+ unsigned int suspend_on_idle:1;
+ unsigned int reconfigure:1;
+
+ uint32_t port_user_data_size; /**< extra size for port user data */
+
+ struct spa_list driver_link;
+ struct pw_impl_node *driver_node;
+ struct spa_list follower_list;
+ struct spa_list follower_link;
+
+ struct spa_list sort_link; /**< link used to sort nodes */
+
+ struct spa_node *node; /**< SPA node implementation */
+ struct spa_hook listener;
+
+ struct spa_list input_ports; /**< list of input ports */
+ struct pw_map input_port_map; /**< map from port_id to port */
+ struct spa_list output_ports; /**< list of output ports */
+ struct pw_map output_port_map; /**< map from port_id to port */
+
+ struct spa_hook_list listener_list;
+
+ struct pw_loop *data_loop; /**< the data loop for this node */
+
+ struct spa_fraction latency; /**< requested latency */
+ struct spa_fraction max_latency; /**< maximum latency */
+ struct spa_fraction rate; /**< requested rate */
+ uint32_t force_quantum; /**< forced quantum */
+ uint32_t force_rate; /**< forced rate */
+ uint32_t stamp; /**< stamp of last update */
+ struct spa_source source; /**< source to remotely trigger this node */
+ struct pw_memblock *activation;
+ struct {
+ struct spa_io_clock *clock; /**< io area of the clock or NULL */
+ struct spa_io_position *position;
+ struct pw_node_activation *activation;
+
+ struct spa_list target_list; /* list of targets to signal after
+ * this node */
+ struct pw_node_target driver_target; /* driver target that we signal */
+ struct spa_list input_mix; /* our input ports (and mixers) */
+ struct spa_list output_mix; /* output ports (and mixers) */
+
+ struct pw_node_target target; /* our target that is signaled by the
+ driver */
+ struct spa_list driver_link; /* our link in driver */
+
+ struct ratelimit rate_limit;
+ } rt;
+ struct spa_fraction current_rate;
+ uint64_t current_quantum;
+
+ void *user_data; /**< extra user data */
+};
+
+struct pw_impl_port_mix {
+ struct spa_list link;
+ struct spa_list rt_link;
+ struct pw_impl_port *p;
+ struct {
+ enum spa_direction direction;
+ uint32_t port_id;
+ } port;
+ struct spa_io_buffers *io;
+ uint32_t id;
+ uint32_t peer_id;
+ unsigned int have_buffers:1;
+};
+
+struct pw_impl_port_implementation {
+#define PW_VERSION_PORT_IMPLEMENTATION 0
+ uint32_t version;
+
+ int (*init_mix) (void *data, struct pw_impl_port_mix *mix);
+ int (*release_mix) (void *data, struct pw_impl_port_mix *mix);
+};
+
+#define pw_impl_port_call(p,m,v,...) \
+({ \
+ int _res = 0; \
+ spa_callbacks_call_res(&(p)->impl, \
+ struct pw_impl_port_implementation, \
+ _res, m, v, ## __VA_ARGS__); \
+ _res; \
+})
+
+#define pw_impl_port_call_init_mix(p,m) pw_impl_port_call(p,init_mix,0,m)
+#define pw_impl_port_call_release_mix(p,m) pw_impl_port_call(p,release_mix,0,m)
+
+#define pw_impl_port_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_impl_port_events, m, v, ##__VA_ARGS__)
+#define pw_impl_port_emit_destroy(p) pw_impl_port_emit(p, destroy, 0)
+#define pw_impl_port_emit_free(p) pw_impl_port_emit(p, free, 0)
+#define pw_impl_port_emit_initialized(p) pw_impl_port_emit(p, initialized, 0)
+#define pw_impl_port_emit_info_changed(p,i) pw_impl_port_emit(p, info_changed, 0, i)
+#define pw_impl_port_emit_link_added(p,l) pw_impl_port_emit(p, link_added, 0, l)
+#define pw_impl_port_emit_link_removed(p,l) pw_impl_port_emit(p, link_removed, 0, l)
+#define pw_impl_port_emit_state_changed(p,o,s,e) pw_impl_port_emit(p, state_changed, 0, o, s, e)
+#define pw_impl_port_emit_control_added(p,c) pw_impl_port_emit(p, control_added, 0, c)
+#define pw_impl_port_emit_control_removed(p,c) pw_impl_port_emit(p, control_removed, 0, c)
+#define pw_impl_port_emit_param_changed(p,i) pw_impl_port_emit(p, param_changed, 1, i)
+#define pw_impl_port_emit_latency_changed(p) pw_impl_port_emit(p, latency_changed, 2)
+
+#define PW_IMPL_PORT_IS_CONTROL(port) SPA_FLAG_MASK((port)->flags, \
+ PW_IMPL_PORT_FLAG_BUFFERS|PW_IMPL_PORT_FLAG_CONTROL,\
+ PW_IMPL_PORT_FLAG_CONTROL)
+struct pw_impl_port {
+ struct spa_list link; /**< link in node port_list */
+
+ struct pw_impl_node *node; /**< owner node */
+ struct pw_global *global; /**< global for this port */
+ struct spa_hook global_listener;
+
+#define PW_IMPL_PORT_FLAG_TO_REMOVE (1<<0) /**< if the port should be removed from the
+ * implementation when destroyed */
+#define PW_IMPL_PORT_FLAG_BUFFERS (1<<1) /**< port has data */
+#define PW_IMPL_PORT_FLAG_CONTROL (1<<2) /**< port has control */
+#define PW_IMPL_PORT_FLAG_NO_MIXER (1<<3) /**< don't try to add mixer to port */
+ uint32_t flags;
+ uint64_t spa_flags;
+
+ enum pw_direction direction; /**< port direction */
+ uint32_t port_id; /**< port id */
+
+ enum pw_impl_port_state state; /**< state of the port */
+ const char *error; /**< error state */
+
+ struct pw_properties *properties; /**< properties of the port */
+ struct pw_port_info info;
+ struct spa_param_info params[MAX_PARAMS];
+
+ struct pw_buffers buffers; /**< buffers managed by this port, only on
+ * output ports, shared with all links */
+
+ struct spa_list links; /**< list of \ref pw_impl_link */
+
+ struct spa_list control_list[2];/**< list of \ref pw_control indexed by direction */
+
+ struct spa_hook_list listener_list;
+
+ struct spa_callbacks impl;
+
+ struct spa_node *mix; /**< port buffer mix/split */
+#define PW_IMPL_PORT_MIX_FLAG_MULTI (1<<0) /**< multi input or output */
+#define PW_IMPL_PORT_MIX_FLAG_MIX_ONLY (1<<1) /**< only negotiate mix ports */
+#define PW_IMPL_PORT_MIX_FLAG_NEGOTIATE (1<<2) /**< negotiate buffers */
+ uint32_t mix_flags; /**< flags for the mixing */
+ struct spa_handle *mix_handle; /**< mix plugin handle */
+ struct pw_buffers mix_buffers; /**< buffers between mixer and node */
+
+ struct spa_list mix_list; /**< list of \ref pw_impl_port_mix */
+ struct pw_map mix_port_map; /**< map from port_id from mixer */
+ uint32_t n_mix;
+
+ struct {
+ struct spa_io_buffers io; /**< io area of the port */
+ struct spa_io_clock clock; /**< io area of the clock */
+ struct spa_list mix_list;
+ struct spa_list node_link;
+ } rt; /**< data only accessed from the data thread */
+ unsigned int added:1;
+ unsigned int destroying:1;
+ int busy_count;
+
+ struct spa_latency_info latency[2]; /**< latencies */
+ unsigned int have_latency_param:1;
+
+ void *owner_data; /**< extra owner data */
+ void *user_data; /**< extra user data */
+};
+
+struct pw_control_link {
+ struct spa_list out_link;
+ struct spa_list in_link;
+ struct pw_control *output;
+ struct pw_control *input;
+ uint32_t out_port;
+ uint32_t in_port;
+ unsigned int valid:1;
+};
+
+#define pw_impl_link_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_impl_link_events, m, v, ##__VA_ARGS__)
+#define pw_impl_link_emit_destroy(l) pw_impl_link_emit(l, destroy, 0)
+#define pw_impl_link_emit_free(l) pw_impl_link_emit(l, free, 0)
+#define pw_impl_link_emit_initialized(l) pw_impl_link_emit(l, initialized, 0)
+#define pw_impl_link_emit_info_changed(l,i) pw_impl_link_emit(l, info_changed, 0, i)
+#define pw_impl_link_emit_state_changed(l,...) pw_impl_link_emit(l, state_changed, 0, __VA_ARGS__)
+#define pw_impl_link_emit_port_unlinked(l,p) pw_impl_link_emit(l, port_unlinked, 0, p)
+
+struct pw_impl_link {
+ struct pw_context *context; /**< context object */
+ struct spa_list link; /**< link in context link_list */
+ struct pw_global *global; /**< global for this link */
+ struct spa_hook global_listener;
+
+ char *name;
+
+ struct pw_link_info info; /**< introspectable link info */
+ struct pw_properties *properties; /**< extra link properties */
+
+ struct spa_io_buffers *io; /**< link io area */
+
+ struct pw_impl_port *output; /**< output port */
+ struct spa_list output_link; /**< link in output port links */
+ struct pw_impl_port *input; /**< input port */
+ struct spa_list input_link; /**< link in input port links */
+
+ struct spa_hook_list listener_list;
+
+ struct pw_control_link control;
+ struct pw_control_link notify;
+
+ struct {
+ struct pw_impl_port_mix out_mix; /**< port added to the output mixer */
+ struct pw_impl_port_mix in_mix; /**< port added to the input mixer */
+ struct pw_node_target target; /**< target to trigger the input node */
+ } rt;
+
+ void *user_data;
+
+ unsigned int registered:1;
+ unsigned int feedback:1;
+ unsigned int preparing:1;
+ unsigned int prepared:1;
+ unsigned int passive:1;
+};
+
+#define pw_resource_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_resource_events, m, v, ##__VA_ARGS__)
+
+#define pw_resource_emit_destroy(o) pw_resource_emit(o, destroy, 0)
+#define pw_resource_emit_pong(o,s) pw_resource_emit(o, pong, 0, s)
+#define pw_resource_emit_error(o,s,r,m) pw_resource_emit(o, error, 0, s, r, m)
+
+struct pw_resource {
+ struct spa_interface impl; /**< object implementation */
+
+ struct pw_context *context; /**< the context object */
+ struct pw_global *global; /**< global of resource */
+ struct spa_list link; /**< link in global resource_list */
+
+ struct pw_impl_client *client; /**< owner client */
+
+ uint32_t id; /**< per client unique id, index in client objects */
+ uint32_t permissions; /**< resource permissions */
+ const char *type; /**< type of the client interface */
+ uint32_t version; /**< version of the client interface */
+ uint32_t bound_id; /**< global id we are bound to */
+ int refcount;
+
+ unsigned int removed:1; /**< resource was removed from server */
+ unsigned int destroyed:1; /**< resource was destroyed */
+
+ struct spa_hook_list listener_list;
+ struct spa_hook_list object_listener_list;
+
+ const struct pw_protocol_marshal *marshal;
+
+ void *user_data; /**< extra user data */
+};
+
+#define pw_proxy_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_proxy_events, m, v, ##__VA_ARGS__)
+#define pw_proxy_emit_destroy(p) pw_proxy_emit(p, destroy, 0)
+#define pw_proxy_emit_bound(p,g) pw_proxy_emit(p, bound, 0, g)
+#define pw_proxy_emit_removed(p) pw_proxy_emit(p, removed, 0)
+#define pw_proxy_emit_done(p,s) pw_proxy_emit(p, done, 0, s)
+#define pw_proxy_emit_error(p,s,r,m) pw_proxy_emit(p, error, 0, s, r, m)
+
+struct pw_proxy {
+ struct spa_interface impl; /**< object implementation */
+
+ struct pw_core *core; /**< the owner core of this proxy */
+
+ uint32_t id; /**< client side id */
+ const char *type; /**< type of the interface */
+ uint32_t version; /**< client side version */
+ uint32_t bound_id; /**< global id we are bound to */
+ int refcount;
+ unsigned int zombie:1; /**< proxy is removed locally and waiting to
+ * be removed from server */
+ unsigned int removed:1; /**< proxy was removed from server */
+ unsigned int destroyed:1; /**< proxy was destroyed by client */
+ unsigned int in_map:1; /**< proxy is in core object map */
+
+ struct spa_hook_list listener_list;
+ struct spa_hook_list object_listener_list;
+
+ const struct pw_protocol_marshal *marshal; /**< protocol specific marshal functions */
+
+ void *user_data; /**< extra user data */
+};
+
+struct pw_core {
+ struct pw_proxy proxy;
+
+ struct pw_context *context; /**< context */
+ struct spa_list link; /**< link in context core_list */
+ struct pw_properties *properties; /**< extra properties */
+
+ struct pw_mempool *pool; /**< memory pool */
+ struct pw_core *core; /**< proxy for the core object */
+ struct spa_hook core_listener;
+ struct spa_hook proxy_core_listener;
+
+ struct pw_map objects; /**< map of client side proxy objects
+ * indexed with the client id */
+ struct pw_client *client; /**< proxy for the client object */
+
+ struct spa_list stream_list; /**< list of \ref pw_stream objects */
+ struct spa_list filter_list; /**< list of \ref pw_stream objects */
+
+ struct pw_protocol_client *conn; /**< the protocol client connection */
+ int recv_seq; /**< last received sequence number */
+ int send_seq; /**< last protocol result code */
+ uint64_t recv_generation; /**< last received registry generation */
+
+ unsigned int removed:1;
+ unsigned int destroyed:1;
+
+ void *user_data; /**< extra user data */
+};
+
+#define pw_stream_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct pw_stream_events, m, v, ##__VA_ARGS__)
+#define pw_stream_emit_destroy(s) pw_stream_emit(s, destroy, 0)
+#define pw_stream_emit_state_changed(s,o,n,e) pw_stream_emit(s, state_changed,0,o,n,e)
+#define pw_stream_emit_io_changed(s,i,a,t) pw_stream_emit(s, io_changed,0,i,a,t)
+#define pw_stream_emit_param_changed(s,i,p) pw_stream_emit(s, param_changed,0,i,p)
+#define pw_stream_emit_add_buffer(s,b) pw_stream_emit(s, add_buffer, 0, b)
+#define pw_stream_emit_remove_buffer(s,b) pw_stream_emit(s, remove_buffer, 0, b)
+#define pw_stream_emit_process(s) pw_stream_emit(s, process, 0)
+#define pw_stream_emit_drained(s) pw_stream_emit(s, drained,0)
+#define pw_stream_emit_control_info(s,i,c) pw_stream_emit(s, control_info, 0, i, c)
+#define pw_stream_emit_command(s,c) pw_stream_emit(s, command,1,c)
+#define pw_stream_emit_trigger_done(s) pw_stream_emit(s, trigger_done,2)
+
+
+struct pw_stream {
+ struct pw_core *core; /**< the owner core */
+ struct spa_hook core_listener;
+
+ struct spa_list link; /**< link in the core */
+
+ char *name; /**< the name of the stream */
+ struct pw_properties *properties; /**< properties of the stream */
+
+ uint32_t node_id; /**< node id for remote node, available from
+ * CONFIGURE state and higher */
+ enum pw_stream_state state; /**< stream state */
+ char *error; /**< error reason when state is in error */
+
+ struct spa_hook_list listener_list;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+
+ struct spa_hook node_listener;
+
+ struct spa_list controls;
+};
+
+#define pw_filter_emit(s,m,v,...) spa_hook_list_call(&(s)->listener_list, struct pw_filter_events, m, v, ##__VA_ARGS__)
+#define pw_filter_emit_destroy(s) pw_filter_emit(s, destroy, 0)
+#define pw_filter_emit_state_changed(s,o,n,e) pw_filter_emit(s, state_changed,0,o,n,e)
+#define pw_filter_emit_io_changed(s,p,i,d,t) pw_filter_emit(s, io_changed,0,p,i,d,t)
+#define pw_filter_emit_param_changed(s,p,i,f) pw_filter_emit(s, param_changed,0,p,i,f)
+#define pw_filter_emit_add_buffer(s,p,b) pw_filter_emit(s, add_buffer, 0, p, b)
+#define pw_filter_emit_remove_buffer(s,p,b) pw_filter_emit(s, remove_buffer, 0, p, b)
+#define pw_filter_emit_process(s,p) pw_filter_emit(s, process, 0, p)
+#define pw_filter_emit_drained(s) pw_filter_emit(s, drained, 0)
+#define pw_filter_emit_command(s,c) pw_filter_emit(s, command, 1, c)
+
+
+struct pw_filter {
+ struct pw_core *core; /**< the owner core proxy */
+ struct spa_hook core_listener;
+
+ struct spa_list link; /**< link in the core proxy */
+
+ char *name; /**< the name of the filter */
+ struct pw_properties *properties; /**< properties of the filter */
+
+ uint32_t node_id; /**< node id for remote node, available from
+ * CONFIGURE state and higher */
+ enum pw_filter_state state; /**< filter state */
+ char *error; /**< error reason when state is in error */
+
+ struct spa_hook_list listener_list;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+
+ struct spa_list controls;
+};
+
+#define pw_impl_factory_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, struct pw_impl_factory_events, m, v, ##__VA_ARGS__)
+
+#define pw_impl_factory_emit_destroy(s) pw_impl_factory_emit(s, destroy, 0)
+#define pw_impl_factory_emit_free(s) pw_impl_factory_emit(s, free, 0)
+#define pw_impl_factory_emit_initialized(s) pw_impl_factory_emit(s, initialized, 0)
+
+struct pw_impl_factory {
+ struct pw_context *context; /**< the context */
+ struct spa_list link; /**< link in context factory_list */
+ struct pw_global *global; /**< global for this factory */
+ struct spa_hook global_listener;
+
+ struct pw_factory_info info; /**< introspectable factory info */
+ struct pw_properties *properties; /**< properties of the factory */
+
+ struct spa_hook_list listener_list; /**< event listeners */
+
+ struct spa_callbacks impl;
+
+ void *user_data;
+
+ unsigned int registered:1;
+};
+
+#define pw_control_emit(c,m,v,...) spa_hook_list_call(&c->listener_list, struct pw_control_events, m, v, ##__VA_ARGS__)
+#define pw_control_emit_destroy(c) pw_control_emit(c, destroy, 0)
+#define pw_control_emit_free(c) pw_control_emit(c, free, 0)
+#define pw_control_emit_linked(c,o) pw_control_emit(c, linked, 0, o)
+#define pw_control_emit_unlinked(c,o) pw_control_emit(c, unlinked, 0, o)
+
+struct pw_control {
+ struct spa_list link; /**< link in context control_list */
+ struct pw_context *context; /**< the context */
+
+ struct pw_impl_port *port; /**< owner port or NULL */
+ struct spa_list port_link; /**< link in port control_list */
+
+ enum spa_direction direction; /**< the direction */
+ struct spa_list links; /**< list of pw_control_link */
+
+ uint32_t id;
+ int32_t size;
+
+ struct spa_hook_list listener_list;
+
+ void *user_data;
+};
+
+/** Find a good format between 2 ports */
+int pw_context_find_format(struct pw_context *context,
+ struct pw_impl_port *output,
+ struct pw_impl_port *input,
+ struct pw_properties *props,
+ uint32_t n_format_filters,
+ struct spa_pod **format_filters,
+ struct spa_pod **format,
+ struct spa_pod_builder *builder,
+ char **error);
+
+int pw_context_debug_port_params(struct pw_context *context,
+ struct spa_node *node, enum spa_direction direction,
+ uint32_t port_id, uint32_t id, int err, const char *debug, ...);
+
+const struct pw_export_type *pw_context_find_export_type(struct pw_context *context, const char *type);
+
+int pw_proxy_init(struct pw_proxy *proxy, const char *type, uint32_t version);
+
+void pw_proxy_remove(struct pw_proxy *proxy);
+
+int pw_context_recalc_graph(struct pw_context *context, const char *reason);
+
+void pw_impl_port_update_info(struct pw_impl_port *port, const struct spa_port_info *info);
+
+int pw_impl_port_register(struct pw_impl_port *port,
+ struct pw_properties *properties);
+
+/** Get the user data of a port, the size of the memory was given \ref in pw_context_create_port */
+void * pw_impl_port_get_user_data(struct pw_impl_port *port);
+
+int pw_impl_port_set_mix(struct pw_impl_port *port, struct spa_node *node, uint32_t flags);
+
+int pw_impl_port_init_mix(struct pw_impl_port *port, struct pw_impl_port_mix *mix);
+int pw_impl_port_release_mix(struct pw_impl_port *port, struct pw_impl_port_mix *mix);
+
+void pw_impl_port_update_state(struct pw_impl_port *port, enum pw_impl_port_state state, int res, char *error);
+
+/** Unlink a port */
+void pw_impl_port_unlink(struct pw_impl_port *port);
+
+/** Destroy a port */
+void pw_impl_port_destroy(struct pw_impl_port *port);
+
+/** Iterate the params of the given port. The callback should return
+ * 1 to fetch the next item, 0 to stop iteration or <0 on error.
+ * The function returns 0 on success or the error returned by the callback. */
+int pw_impl_port_for_each_param(struct pw_impl_port *port,
+ int seq, uint32_t param_id,
+ uint32_t index, uint32_t max,
+ const struct spa_pod *filter,
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param),
+ void *data);
+
+int pw_impl_port_for_each_filtered_param(struct pw_impl_port *in_port,
+ struct pw_impl_port *out_port,
+ int seq,
+ uint32_t in_param_id,
+ uint32_t out_param_id,
+ const struct spa_pod *filter,
+ int (*callback) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param),
+ void *data);
+
+/** Iterate the links of the port. The callback should return
+ * 0 to fetch the next item, any other value stops the iteration and returns
+ * the value. When all callbacks return 0, this function returns 0 when all
+ * items are iterated. */
+int pw_impl_port_for_each_link(struct pw_impl_port *port,
+ int (*callback) (void *data, struct pw_impl_link *link),
+ void *data);
+
+/** Set a param on a port, use SPA_ID_INVALID for mix_id to set
+ * the param on all mix ports */
+int pw_impl_port_set_param(struct pw_impl_port *port,
+ uint32_t id, uint32_t flags, const struct spa_pod *param);
+
+/** Use buffers on a port */
+int pw_impl_port_use_buffers(struct pw_impl_port *port, struct pw_impl_port_mix *mix, uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers);
+
+int pw_impl_port_recalc_latency(struct pw_impl_port *port);
+
+/** Change the state of the node */
+int pw_impl_node_set_state(struct pw_impl_node *node, enum pw_node_state state);
+
+
+int pw_impl_node_update_ports(struct pw_impl_node *node);
+
+int pw_impl_node_set_driver(struct pw_impl_node *node, struct pw_impl_node *driver);
+
+/** Prepare a link
+ * Starts the negotiation of formats and buffers on \a link */
+int pw_impl_link_prepare(struct pw_impl_link *link);
+/** starts streaming on a link */
+int pw_impl_link_activate(struct pw_impl_link *link);
+
+/** Deactivate a link */
+int pw_impl_link_deactivate(struct pw_impl_link *link);
+
+struct pw_control *
+pw_control_new(struct pw_context *context,
+ struct pw_impl_port *owner, /**< can be NULL */
+ uint32_t id, uint32_t size,
+ size_t user_data_size /**< extra user data */);
+
+int pw_control_add_link(struct pw_control *control, uint32_t cmix,
+ struct pw_control *other, uint32_t omix,
+ struct pw_control_link *link);
+
+int pw_control_remove_link(struct pw_control_link *link);
+
+void pw_control_destroy(struct pw_control *control);
+
+void pw_impl_client_unref(struct pw_impl_client *client);
+
+#define PW_LOG_OBJECT_POD (1<<0)
+void pw_log_log_object(enum spa_log_level level, const struct spa_log_topic *topic,
+ const char *file, int line, const char *func, uint32_t flags,
+ const void *object);
+
+#define pw_log_object(lev,t,fl,obj) \
+({ \
+ if (SPA_UNLIKELY(pw_log_topic_enabled(lev,t))) \
+ pw_log_log_object(lev,t,__FILE__,__LINE__, \
+ __func__,(fl),(obj)); \
+})
+
+#define pw_log_pod(lev,pod) pw_log_object(lev,PW_LOG_TOPIC_DEFAULT,PW_LOG_OBJECT_POD,pod)
+#define pw_log_format(lev,pod) pw_log_object(lev,PW_LOG_TOPIC_DEFAULT,PW_LOG_OBJECT_POD,pod)
+
+bool pw_log_is_default(void);
+
+void pw_log_init(void);
+void pw_log_deinit(void);
+
+void pw_settings_init(struct pw_context *context);
+int pw_settings_expose(struct pw_context *context);
+void pw_settings_clean(struct pw_context *context);
+
+pthread_attr_t *pw_thread_fill_attr(const struct spa_dict *props, pthread_attr_t *attr);
+
+/** \endcond */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_PRIVATE_H */
diff --git a/src/pipewire/properties.c b/src/pipewire/properties.c
new file mode 100644
index 0000000..be52a7c
--- /dev/null
+++ b/src/pipewire/properties.c
@@ -0,0 +1,733 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+#include "pipewire/array.h"
+#include "pipewire/log.h"
+#include "pipewire/utils.h"
+#include "pipewire/properties.h"
+
+PW_LOG_TOPIC_EXTERN(log_properties);
+#define PW_LOG_TOPIC_DEFAULT log_properties
+
+/** \cond */
+struct properties {
+ struct pw_properties this;
+
+ struct pw_array items;
+};
+/** \endcond */
+
+static int add_func(struct pw_properties *this, char *key, char *value)
+{
+ struct spa_dict_item *item;
+ struct properties *impl = SPA_CONTAINER_OF(this, struct properties, this);
+
+ item = pw_array_add(&impl->items, sizeof(struct spa_dict_item));
+ if (item == NULL) {
+ free(key);
+ free(value);
+ return -errno;
+ }
+
+ item->key = key;
+ item->value = value;
+
+ this->dict.items = impl->items.data;
+ this->dict.n_items++;
+ return 0;
+}
+
+static void clear_item(struct spa_dict_item *item)
+{
+ free((char *) item->key);
+ free((char *) item->value);
+}
+
+static int find_index(const struct pw_properties *this, const char *key)
+{
+ const struct spa_dict_item *item;
+ item = spa_dict_lookup_item(&this->dict, key);
+ if (item == NULL)
+ return -1;
+ return item - this->dict.items;
+}
+
+static struct properties *properties_new(int prealloc)
+{
+ struct properties *impl;
+
+ impl = calloc(1, sizeof(struct properties));
+ if (impl == NULL)
+ return NULL;
+
+ pw_array_init(&impl->items, 16);
+ pw_array_ensure_size(&impl->items, sizeof(struct spa_dict_item) * prealloc);
+
+ return impl;
+}
+
+/** Make a new properties object
+ *
+ * \param key a first key
+ * \param ... value and more keys NULL terminated
+ * \return a newly allocated properties object
+ */
+SPA_EXPORT
+struct pw_properties *pw_properties_new(const char *key, ...)
+{
+ struct properties *impl;
+ va_list varargs;
+ const char *value;
+
+ impl = properties_new(16);
+ if (impl == NULL)
+ return NULL;
+
+ va_start(varargs, key);
+ while (key != NULL) {
+ value = va_arg(varargs, char *);
+ if (value && key[0])
+ add_func(&impl->this, strdup(key), strdup(value));
+ key = va_arg(varargs, char *);
+ }
+ va_end(varargs);
+
+ return &impl->this;
+}
+
+/** Make a new properties object from the given dictionary
+ *
+ * \param dict a dictionary. keys and values are copied
+ * \return a new properties object
+ */
+SPA_EXPORT
+struct pw_properties *pw_properties_new_dict(const struct spa_dict *dict)
+{
+ uint32_t i;
+ struct properties *impl;
+
+ impl = properties_new(SPA_ROUND_UP_N(dict->n_items, 16));
+ if (impl == NULL)
+ return NULL;
+
+ for (i = 0; i < dict->n_items; i++) {
+ const struct spa_dict_item *it = &dict->items[i];
+ if (it->key != NULL && it->key[0] && it->value != NULL)
+ add_func(&impl->this, strdup(it->key),
+ strdup(it->value));
+ }
+
+ return &impl->this;
+}
+
+/** Update the properties from the given string, overwriting any
+ * existing keys with the new values from \a str.
+ *
+ * \a str should be a whitespace separated list of key=value
+ * strings or a json object, see pw_properties_new_string().
+ *
+ * \return The number of properties added or updated
+ */
+SPA_EXPORT
+int pw_properties_update_string(struct pw_properties *props, const char *str, size_t size)
+{
+ struct properties *impl = SPA_CONTAINER_OF(props, struct properties, this);
+ struct spa_json it[2];
+ char key[1024], *val;
+ int count = 0;
+
+ 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;
+
+ 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);
+
+ if ((val = malloc(len+1)) != NULL)
+ spa_json_parse_stringn(value, len, val, len+1);
+ }
+ count += pw_properties_set(&impl->this, key, val);
+ free(val);
+ }
+ return count;
+}
+
+/** Make a new properties object from the given str
+ *
+ * \a object should be a whitespace separated list of key=value
+ * strings or a json object.
+ *
+ * \param object a property description
+ * \return a new properties object
+ */
+SPA_EXPORT
+struct pw_properties *
+pw_properties_new_string(const char *object)
+{
+ struct properties *impl;
+ int res;
+
+ impl = properties_new(16);
+ if (impl == NULL)
+ return NULL;
+
+ if ((res = pw_properties_update_string(&impl->this, object, strlen(object))) < 0)
+ goto error;
+
+ return &impl->this;
+error:
+ pw_properties_free(&impl->this);
+ errno = -res;
+ return NULL;
+}
+
+/** Copy a properties object
+ *
+ * \param properties properties to copy
+ * \return a new properties object
+ */
+SPA_EXPORT
+struct pw_properties *pw_properties_copy(const struct pw_properties *properties)
+{
+ return pw_properties_new_dict(&properties->dict);
+}
+
+/** Copy multiple keys from one property to another
+ *
+ * \param props properties to copy to
+ * \param dict properties to copy from
+ * \param keys a NULL terminated list of keys to copy
+ * \return the number of keys changed in \a dest
+ */
+SPA_EXPORT
+int pw_properties_update_keys(struct pw_properties *props,
+ const struct spa_dict *dict, const char * const keys[])
+{
+ int i, changed = 0;
+ const char *str;
+
+ for (i = 0; keys[i]; i++) {
+ if ((str = spa_dict_lookup(dict, keys[i])) != NULL)
+ changed += pw_properties_set(props, keys[i], str);
+ }
+ return changed;
+}
+
+static bool has_key(const char * const keys[], const char *key)
+{
+ int i;
+ for (i = 0; keys[i]; i++) {
+ if (spa_streq(keys[i], key))
+ return true;
+ }
+ return false;
+}
+
+SPA_EXPORT
+int pw_properties_update_ignore(struct pw_properties *props,
+ const struct spa_dict *dict, const char * const ignore[])
+{
+ const struct spa_dict_item *it;
+ int changed = 0;
+
+ spa_dict_for_each(it, dict) {
+ if (ignore == NULL || !has_key(ignore, it->key))
+ changed += pw_properties_set(props, it->key, it->value);
+ }
+ return changed;
+}
+
+/** Clear a properties object
+ *
+ * \param properties properties to clear
+ */
+SPA_EXPORT
+void pw_properties_clear(struct pw_properties *properties)
+{
+ struct properties *impl = SPA_CONTAINER_OF(properties, struct properties, this);
+ struct spa_dict_item *item;
+
+ pw_array_for_each(item, &impl->items)
+ clear_item(item);
+ pw_array_reset(&impl->items);
+ properties->dict.n_items = 0;
+}
+
+/** Update properties
+ *
+ * \param props properties to update
+ * \param dict new properties
+ * \return the number of changed properties
+ *
+ * The properties in \a props are updated with \a dict. Keys in \a dict
+ * with NULL values are removed from \a props.
+ */
+SPA_EXPORT
+int pw_properties_update(struct pw_properties *props,
+ const struct spa_dict *dict)
+{
+ const struct spa_dict_item *it;
+ int changed = 0;
+
+ spa_dict_for_each(it, dict)
+ changed += pw_properties_set(props, it->key, it->value);
+
+ return changed;
+}
+
+/** Add properties
+ *
+ * \param props properties to add
+ * \param dict new properties
+ * \return the number of added properties
+ *
+ * The properties from \a dict that are not yet in \a props are added.
+ */
+SPA_EXPORT
+int pw_properties_add(struct pw_properties *props,
+ const struct spa_dict *dict)
+{
+ uint32_t i;
+ int added = 0;
+
+ for (i = 0; i < dict->n_items; i++) {
+ if (pw_properties_get(props, dict->items[i].key) == NULL)
+ added += pw_properties_set(props, dict->items[i].key, dict->items[i].value);
+ }
+ return added;
+}
+
+/** Add keys
+ *
+ * \param props properties to add
+ * \param dict new properties
+ * \param keys a NULL terminated list of keys to add
+ * \return the number of added properties
+ *
+ * The properties with \a keys from \a dict that are not yet
+ * in \a props are added.
+ */
+SPA_EXPORT
+int pw_properties_add_keys(struct pw_properties *props,
+ const struct spa_dict *dict, const char * const keys[])
+{
+ uint32_t i;
+ int added = 0;
+ const char *str;
+
+ for (i = 0; keys[i]; i++) {
+ if ((str = spa_dict_lookup(dict, keys[i])) == NULL)
+ continue;
+ if (pw_properties_get(props, keys[i]) == NULL)
+ added += pw_properties_set(props, keys[i], str);
+ }
+ return added;
+}
+
+/** Free a properties object
+ *
+ * \param properties the properties to free
+ */
+SPA_EXPORT
+void pw_properties_free(struct pw_properties *properties)
+{
+ struct properties *impl;
+
+ if (properties == NULL)
+ return;
+
+ impl = SPA_CONTAINER_OF(properties, struct properties, this);
+ pw_properties_clear(properties);
+ pw_array_clear(&impl->items);
+ free(impl);
+}
+
+static int do_replace(struct pw_properties *properties, const char *key, char *value, bool copy)
+{
+ struct properties *impl = SPA_CONTAINER_OF(properties, struct properties, this);
+ int index;
+
+ if (key == NULL || key[0] == 0)
+ goto exit_noupdate;
+
+ index = find_index(properties, key);
+
+ if (index == -1) {
+ if (value == NULL)
+ return 0;
+ add_func(properties, strdup(key), copy ? strdup(value) : value);
+ SPA_FLAG_CLEAR(properties->dict.flags, SPA_DICT_FLAG_SORTED);
+ } else {
+ struct spa_dict_item *item =
+ pw_array_get_unchecked(&impl->items, index, struct spa_dict_item);
+
+ if (value && spa_streq(item->value, value))
+ goto exit_noupdate;
+
+ if (value == NULL) {
+ struct spa_dict_item *last = pw_array_get_unchecked(&impl->items,
+ pw_array_get_len(&impl->items, struct spa_dict_item) - 1,
+ struct spa_dict_item);
+ clear_item(item);
+ item->key = last->key;
+ item->value = last->value;
+ impl->items.size -= sizeof(struct spa_dict_item);
+ properties->dict.n_items--;
+ SPA_FLAG_CLEAR(properties->dict.flags, SPA_DICT_FLAG_SORTED);
+ } else {
+ free((char *) item->value);
+ item->value = copy ? strdup(value) : value;
+ }
+ }
+ return 1;
+exit_noupdate:
+ if (!copy)
+ free(value);
+ return 0;
+}
+
+/** Set a property value
+ *
+ * \param properties the properties to change
+ * \param key a key
+ * \param value a value or NULL to remove the key
+ * \return 1 if the properties were changed. 0 if nothing was changed because
+ * the property already existed with the same value or because the key to remove
+ * did not exist.
+ *
+ * Set the property in \a properties with \a key to \a value. Any previous value
+ * of \a key will be overwritten. When \a value is NULL, the key will be
+ * removed.
+ */
+SPA_EXPORT
+int pw_properties_set(struct pw_properties *properties, const char *key, const char *value)
+{
+ return do_replace(properties, key, (char*)value, true);
+}
+
+SPA_EXPORT
+int pw_properties_setva(struct pw_properties *properties,
+ const char *key, const char *format, va_list args)
+{
+ char *value = NULL;
+ if (format != NULL) {
+ if (vasprintf(&value, format, args) < 0)
+ return -errno;
+ }
+ return do_replace(properties, key, value, false);
+}
+
+/** Set a property value by format
+ *
+ * \param properties a \ref pw_properties
+ * \param key a key
+ * \param format a value
+ * \param ... extra arguments
+ * \return 1 if the property was changed. 0 if nothing was changed because
+ * the property already existed with the same value or because the key to remove
+ * did not exist.
+ *
+ * Set the property in \a properties with \a key to the value in printf style \a format
+ * Any previous value of \a key will be overwritten.
+ */
+SPA_EXPORT
+int pw_properties_setf(struct pw_properties *properties, const char *key, const char *format, ...)
+{
+ int res;
+ va_list varargs;
+
+ va_start(varargs, format);
+ res = pw_properties_setva(properties, key, format, varargs);
+ va_end(varargs);
+
+ return res;
+}
+
+/** Get a property
+ *
+ * \param properties a \ref pw_properties
+ * \param key a key
+ * \return the property for \a key or NULL when the key was not found
+ *
+ * Get the property in \a properties with \a key.
+ */
+SPA_EXPORT
+const char *pw_properties_get(const struct pw_properties *properties, const char *key)
+{
+ struct properties *impl = SPA_CONTAINER_OF(properties, struct properties, this);
+ int index = find_index(properties, key);
+
+ if (index == -1)
+ return NULL;
+
+ return pw_array_get_unchecked(&impl->items, index, struct spa_dict_item)->value;
+}
+
+/** Fetch a property as uint32_t.
+ *
+ * \param properties a \ref pw_properties
+ * \param key a key
+ * \param value set to the value of the property on success, otherwise left
+ * unmodified
+ * \return 0 on success or a negative errno otherwise
+ * \retval -ENOENT The property does not exist
+ * \retval -EINVAL The property is not in the expected format
+ */
+SPA_EXPORT
+int pw_properties_fetch_uint32(const struct pw_properties *properties, const char *key,
+ uint32_t *value)
+{
+ const char *str = pw_properties_get(properties, key);
+ bool success;
+
+ if (!str)
+ return -ENOENT;
+
+ success = spa_atou32(str, value, 0);
+ if (SPA_UNLIKELY(!success))
+ pw_log_warn("Failed to parse \"%s\"=\"%s\" as int32", key, str);
+
+ return success ? 0 : -EINVAL;
+}
+
+/** Fetch a property as int32_t
+ *
+ * \param properties a \ref pw_properties
+ * \param key a key
+ * \param value set to the value of the property on success, otherwise left
+ * unmodified
+ * \return 0 on success or a negative errno otherwise
+ * \retval -ENOENT The property does not exist
+ * \retval -EINVAL The property is not in the expected format
+ */
+SPA_EXPORT
+int pw_properties_fetch_int32(const struct pw_properties *properties, const char *key,
+ int32_t *value)
+{
+ const char *str = pw_properties_get(properties, key);
+ bool success;
+
+ if (!str)
+ return -ENOENT;
+
+ success = spa_atoi32(str, value, 0);
+ if (SPA_UNLIKELY(!success))
+ pw_log_warn("Failed to parse \"%s\"=\"%s\" as int32", key, str);
+
+ return success ? 0 : -EINVAL;
+}
+
+/** Fetch a property as uint64_t.
+ *
+ * \param properties a \ref pw_properties
+ * \param key a key
+ * \param value set to the value of the property on success, otherwise left
+ * unmodified
+ * \return 0 on success or a negative errno otherwise
+ * \retval -ENOENT The property does not exist
+ * \retval -EINVAL The property is not in the expected format
+ */
+SPA_EXPORT
+int pw_properties_fetch_uint64(const struct pw_properties *properties, const char *key,
+ uint64_t *value)
+{
+ const char *str = pw_properties_get(properties, key);
+ bool success;
+
+ if (!str)
+ return -ENOENT;
+
+ success = spa_atou64(str, value, 0);
+ if (SPA_UNLIKELY(!success))
+ pw_log_warn("Failed to parse \"%s\"=\"%s\" as uint64", key, str);
+
+ return success ? 0 : -EINVAL;
+}
+
+/** Fetch a property as int64_t
+ *
+ * \param properties a \ref pw_properties
+ * \param key a key
+ * \param value set to the value of the property on success, otherwise left
+ * unmodified
+ * \return 0 on success or a negative errno otherwise
+ * \retval -ENOENT The property does not exist
+ * \retval -EINVAL The property is not in the expected format
+ */
+SPA_EXPORT
+int pw_properties_fetch_int64(const struct pw_properties *properties, const char *key,
+ int64_t *value)
+{
+ const char *str = pw_properties_get(properties, key);
+ bool success;
+
+ if (!str)
+ return -ENOENT;
+
+ success = spa_atoi64(str, value, 0);
+ if (SPA_UNLIKELY(!success))
+ pw_log_warn("Failed to parse \"%s\"=\"%s\" as int64", key, str);
+
+ return success ? 0 : -EINVAL;
+}
+
+/** Fetch a property as boolean value
+ *
+ * \param properties a \ref pw_properties
+ * \param key a key
+ * \param value set to the value of the property on success, otherwise left
+ * unmodified
+ * \return 0 on success or a negative errno otherwise
+ * \retval -ENOENT The property does not exist
+ * \retval -EINVAL The property is not in the expected format
+ */
+SPA_EXPORT
+int pw_properties_fetch_bool(const struct pw_properties *properties, const char *key,
+ bool *value)
+{
+ const char *str = pw_properties_get(properties, key);
+
+ if (!str)
+ return -ENOENT;
+
+ *value = spa_atob(str);
+ return 0;
+}
+
+/** Iterate property values
+ *
+ * \param properties a \ref pw_properties
+ * \param state state
+ * \return The next key or NULL when there are no more keys to iterate.
+ *
+ * Iterate over \a properties, returning each key in turn. \a state should point
+ * to a pointer holding NULL to get the first element and will be updated
+ * after each iteration. When NULL is returned, all elements have been
+ * iterated.
+ */
+SPA_EXPORT
+const char *pw_properties_iterate(const struct pw_properties *properties, void **state)
+{
+ struct properties *impl = SPA_CONTAINER_OF(properties, struct properties, this);
+ uint32_t index;
+
+ if (*state == NULL)
+ index = 0;
+ else
+ index = SPA_PTR_TO_INT(*state);
+
+ if (!pw_array_check_index(&impl->items, index, struct spa_dict_item))
+ return NULL;
+
+ *state = SPA_INT_TO_PTR(index + 1);
+
+ return pw_array_get_unchecked(&impl->items, index, struct spa_dict_item)->key;
+}
+
+static int encode_string(FILE *f, const char *val)
+{
+ int len = 0;
+ len += fprintf(f, "\"");
+ while (*val) {
+ switch (*val) {
+ case '\n':
+ len += fprintf(f, "\\n");
+ break;
+ case '\r':
+ len += fprintf(f, "\\r");
+ break;
+ case '\b':
+ len += fprintf(f, "\\b");
+ break;
+ case '\t':
+ len += fprintf(f, "\\t");
+ break;
+ case '\f':
+ len += fprintf(f, "\\f");
+ break;
+ case '\\':
+ case '"':
+ len += fprintf(f, "\\%c", *val);
+ break;
+ default:
+ if (*val > 0 && *val < 0x20)
+ len += fprintf(f, "\\u%04x", *val);
+ else
+ len += fprintf(f, "%c", *val);
+ break;
+ }
+ val++;
+ }
+ len += fprintf(f, "\"");
+ return len-1;
+}
+
+SPA_EXPORT
+int pw_properties_serialize_dict(FILE *f, const struct spa_dict *dict, uint32_t flags)
+{
+ const struct spa_dict_item *it;
+ int count = 0;
+ char key[1024];
+
+ spa_dict_for_each(it, dict) {
+ size_t len = it->value ? strlen(it->value) : 0;
+
+ if (spa_json_encode_string(key, sizeof(key)-1, it->key) >= (int)sizeof(key)-1)
+ continue;
+
+ fprintf(f, "%s%s %s: ",
+ count == 0 ? "" : ",",
+ flags & PW_PROPERTIES_FLAG_NL ? "\n" : "",
+ key);
+
+ if (it->value == NULL) {
+ fprintf(f, "null");
+ } else if (spa_json_is_null(it->value, len) ||
+ spa_json_is_float(it->value, len) ||
+ spa_json_is_bool(it->value, len) ||
+ spa_json_is_container(it->value, len) ||
+ spa_json_is_string(it->value, len)) {
+ fprintf(f, "%s", it->value);
+ } else {
+ encode_string(f, it->value);
+ }
+ count++;
+ }
+ return count;
+}
diff --git a/src/pipewire/properties.h b/src/pipewire/properties.h
new file mode 100644
index 0000000..e91fee5
--- /dev/null
+++ b/src/pipewire/properties.h
@@ -0,0 +1,199 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PROPERTIES_H
+#define PIPEWIRE_PROPERTIES_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+
+#include <spa/utils/dict.h>
+#include <spa/utils/string.h>
+
+/** \defgroup pw_properties Properties
+ *
+ * Properties are used to pass around arbitrary key/value pairs.
+ * Both keys and values are strings which keeps things simple.
+ * Encoding of arbitrary values should be done by using a string
+ * serialization such as base64 for binary blobs.
+ */
+
+/**
+ * \addtogroup pw_properties
+ * \{
+ */
+struct pw_properties {
+ struct spa_dict dict; /**< dictionary of key/values */
+ uint32_t flags; /**< extra flags */
+};
+
+struct pw_properties *
+pw_properties_new(const char *key, ...) SPA_SENTINEL;
+
+struct pw_properties *
+pw_properties_new_dict(const struct spa_dict *dict);
+
+struct pw_properties *
+pw_properties_new_string(const char *args);
+
+struct pw_properties *
+pw_properties_copy(const struct pw_properties *properties);
+
+int pw_properties_update_keys(struct pw_properties *props,
+ const struct spa_dict *dict, const char * const keys[]);
+int pw_properties_update_ignore(struct pw_properties *props,
+ const struct spa_dict *dict, const char * const ignore[]);
+
+/* Update props with all key/value pairs from dict */
+int pw_properties_update(struct pw_properties *props,
+ const struct spa_dict *dict);
+/* Update props with all key/value pairs from str */
+int pw_properties_update_string(struct pw_properties *props,
+ const char *str, size_t size);
+
+int pw_properties_add(struct pw_properties *oldprops,
+ const struct spa_dict *dict);
+int pw_properties_add_keys(struct pw_properties *oldprops,
+ const struct spa_dict *dict, const char * const keys[]);
+
+void pw_properties_clear(struct pw_properties *properties);
+
+void
+pw_properties_free(struct pw_properties *properties);
+
+int
+pw_properties_set(struct pw_properties *properties, const char *key, const char *value);
+
+int
+pw_properties_setf(struct pw_properties *properties,
+ const char *key, const char *format, ...) SPA_PRINTF_FUNC(3, 4);
+int
+pw_properties_setva(struct pw_properties *properties,
+ const char *key, const char *format, va_list args) SPA_PRINTF_FUNC(3,0);
+const char *
+pw_properties_get(const struct pw_properties *properties, const char *key);
+
+int
+pw_properties_fetch_uint32(const struct pw_properties *properties, const char *key, uint32_t *value);
+
+int
+pw_properties_fetch_int32(const struct pw_properties *properties, const char *key, int32_t *value);
+
+int
+pw_properties_fetch_uint64(const struct pw_properties *properties, const char *key, uint64_t *value);
+
+int
+pw_properties_fetch_int64(const struct pw_properties *properties, const char *key, int64_t *value);
+
+int
+pw_properties_fetch_bool(const struct pw_properties *properties, const char *key, bool *value);
+
+static inline uint32_t
+pw_properties_get_uint32(const struct pw_properties *properties, const char *key, uint32_t deflt)
+{
+ uint32_t val = deflt;
+ pw_properties_fetch_uint32(properties, key, &val);
+ return val;
+}
+
+static inline int32_t
+pw_properties_get_int32(const struct pw_properties *properties, const char *key, int32_t deflt)
+{
+ int32_t val = deflt;
+ pw_properties_fetch_int32(properties, key, &val);
+ return val;
+}
+
+static inline uint64_t
+pw_properties_get_uint64(const struct pw_properties *properties, const char *key, uint64_t deflt)
+{
+ uint64_t val = deflt;
+ pw_properties_fetch_uint64(properties, key, &val);
+ return val;
+}
+
+static inline int64_t
+pw_properties_get_int64(const struct pw_properties *properties, const char *key, int64_t deflt)
+{
+ int64_t val = deflt;
+ pw_properties_fetch_int64(properties, key, &val);
+ return val;
+}
+
+
+static inline bool
+pw_properties_get_bool(const struct pw_properties *properties, const char *key, bool deflt)
+{
+ bool val = deflt;
+ pw_properties_fetch_bool(properties, key, &val);
+ return val;
+}
+
+const char *
+pw_properties_iterate(const struct pw_properties *properties, void **state);
+
+#define PW_PROPERTIES_FLAG_NL (1<<0)
+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) {
+ return spa_atob(value);
+}
+
+static inline 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) {
+ int64_t v;
+ return spa_atoi64(value, &v, 0) ? v : 0;
+}
+
+static inline 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) {
+ float v;
+ return spa_atof(value, &v) ? v : 0.0f;
+}
+
+static inline double pw_properties_parse_double(const char *value) {
+ double v;
+ return spa_atod(value, &v) ? v : 0.0;
+}
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_PROPERTIES_H */
diff --git a/src/pipewire/protocol.c b/src/pipewire/protocol.c
new file mode 100644
index 0000000..3092f2c
--- /dev/null
+++ b/src/pipewire/protocol.c
@@ -0,0 +1,188 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+
+#include <spa/debug/types.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/protocol.h>
+#include <pipewire/private.h>
+#include <pipewire/type.h>
+
+PW_LOG_TOPIC_EXTERN(log_protocol);
+#define PW_LOG_TOPIC_DEFAULT log_protocol
+
+/** \cond */
+struct impl {
+ struct pw_protocol this;
+};
+
+struct marshal {
+ struct spa_list link;
+ const struct pw_protocol_marshal *marshal;
+};
+/** \endcond */
+
+SPA_EXPORT
+struct pw_protocol *pw_protocol_new(struct pw_context *context,
+ const char *name,
+ size_t user_data_size)
+{
+ struct pw_protocol *protocol;
+
+ protocol = calloc(1, sizeof(struct impl) + user_data_size);
+ if (protocol == NULL)
+ return NULL;
+
+ protocol->context = context;
+ protocol->name = strdup(name);
+
+ spa_list_init(&protocol->marshal_list);
+ spa_list_init(&protocol->server_list);
+ spa_list_init(&protocol->client_list);
+ spa_hook_list_init(&protocol->listener_list);
+
+ if (user_data_size > 0)
+ protocol->user_data = SPA_PTROFF(protocol, sizeof(struct impl), void);
+
+ spa_list_append(&context->protocol_list, &protocol->link);
+
+ pw_log_debug("%p: Created protocol %s", protocol, name);
+
+ return protocol;
+}
+
+SPA_EXPORT
+struct pw_context *pw_protocol_get_context(struct pw_protocol *protocol)
+{
+ return protocol->context;
+}
+
+SPA_EXPORT
+void *pw_protocol_get_user_data(struct pw_protocol *protocol)
+{
+ return protocol->user_data;
+}
+
+SPA_EXPORT
+const struct pw_protocol_implementation *
+pw_protocol_get_implementation(struct pw_protocol *protocol)
+{
+ return protocol->implementation;
+}
+
+SPA_EXPORT
+const void *
+pw_protocol_get_extension(struct pw_protocol *protocol)
+{
+ return protocol->extension;
+}
+
+SPA_EXPORT
+void pw_protocol_destroy(struct pw_protocol *protocol)
+{
+ struct impl *impl = SPA_CONTAINER_OF(protocol, struct impl, this);
+ struct marshal *marshal, *t1;
+ struct pw_protocol_server *server;
+ struct pw_protocol_client *client;
+
+ pw_log_debug("%p: destroy", protocol);
+ pw_protocol_emit_destroy(protocol);
+
+ spa_hook_list_clean(&protocol->listener_list);
+
+ spa_list_remove(&protocol->link);
+
+ spa_list_consume(server, &protocol->server_list, link)
+ pw_protocol_server_destroy(server);
+
+ spa_list_consume(client, &protocol->client_list, link)
+ pw_protocol_client_destroy(client);
+
+ spa_list_for_each_safe(marshal, t1, &protocol->marshal_list, link)
+ free(marshal);
+
+ free(protocol->name);
+
+ free(impl);
+}
+
+SPA_EXPORT
+void pw_protocol_add_listener(struct pw_protocol *protocol,
+ struct spa_hook *listener,
+ const struct pw_protocol_events *events,
+ void *data)
+{
+ spa_hook_list_append(&protocol->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+int
+pw_protocol_add_marshal(struct pw_protocol *protocol,
+ const struct pw_protocol_marshal *marshal)
+{
+ struct marshal *impl;
+
+ impl = calloc(1, sizeof(struct marshal));
+ if (impl == NULL)
+ return -errno;
+
+ impl->marshal = marshal;
+
+ spa_list_append(&protocol->marshal_list, &impl->link);
+
+ pw_log_debug("%p: Add marshal %s/%d to protocol %s", protocol,
+ marshal->type, marshal->version, protocol->name);
+
+ return 0;
+}
+
+SPA_EXPORT
+const struct pw_protocol_marshal *
+pw_protocol_get_marshal(struct pw_protocol *protocol, const char *type, uint32_t version, uint32_t flags)
+{
+ struct marshal *impl;
+
+ spa_list_for_each(impl, &protocol->marshal_list, link) {
+ if (spa_streq(impl->marshal->type, type) &&
+ (impl->marshal->flags & flags) == flags)
+ return impl->marshal;
+ }
+ pw_log_debug("%p: No marshal %s/%d for protocol %s", protocol,
+ type, version, protocol->name);
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_protocol *pw_context_find_protocol(struct pw_context *context, const char *name)
+{
+ struct pw_protocol *protocol;
+
+ spa_list_for_each(protocol, &context->protocol_list, link) {
+ if (spa_streq(protocol->name, name))
+ return protocol;
+ }
+ return NULL;
+}
diff --git a/src/pipewire/protocol.h b/src/pipewire/protocol.h
new file mode 100644
index 0000000..bb97273
--- /dev/null
+++ b/src/pipewire/protocol.h
@@ -0,0 +1,162 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PROTOCOL_H
+#define PIPEWIRE_PROTOCOL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/list.h>
+
+/** \defgroup pw_protocol Protocol
+ *
+ * \brief Manages protocols and their implementation
+ */
+
+/**
+ * \addtogroup pw_protocol
+ * \{
+ */
+
+struct pw_protocol;
+
+#include <pipewire/context.h>
+#include <pipewire/properties.h>
+#include <pipewire/utils.h>
+
+#define PW_TYPE_INFO_Protocol "PipeWire:Protocol"
+#define PW_TYPE_INFO_PROTOCOL_BASE PW_TYPE_INFO_Protocol ":"
+
+struct pw_protocol_client {
+ struct spa_list link; /**< link in protocol client_list */
+ struct pw_protocol *protocol; /**< the owner protocol */
+
+ struct pw_core *core;
+
+ int (*connect) (struct pw_protocol_client *client,
+ const struct spa_dict *props,
+ void (*done_callback) (void *data, int result),
+ void *data);
+ int (*connect_fd) (struct pw_protocol_client *client, int fd, bool close);
+ int (*steal_fd) (struct pw_protocol_client *client);
+ void (*disconnect) (struct pw_protocol_client *client);
+ void (*destroy) (struct pw_protocol_client *client);
+ int (*set_paused) (struct pw_protocol_client *client, bool paused);
+};
+
+#define pw_protocol_client_connect(c,p,cb,d) ((c)->connect(c,p,cb,d))
+#define pw_protocol_client_connect_fd(c,fd,cl) ((c)->connect_fd(c,fd,cl))
+#define pw_protocol_client_steal_fd(c) ((c)->steal_fd(c))
+#define pw_protocol_client_disconnect(c) ((c)->disconnect(c))
+#define pw_protocol_client_destroy(c) ((c)->destroy(c))
+#define pw_protocol_client_set_paused(c,p) ((c)->set_paused(c,p))
+
+struct pw_protocol_server {
+ struct spa_list link; /**< link in protocol server_list */
+ struct pw_protocol *protocol; /**< the owner protocol */
+
+ struct pw_impl_core *core;
+
+ struct spa_list client_list; /**< list of clients of this protocol */
+
+ void (*destroy) (struct pw_protocol_server *listen);
+};
+
+#define pw_protocol_server_destroy(l) ((l)->destroy(l))
+
+struct pw_protocol_marshal {
+ const char *type; /**< interface type */
+ uint32_t version; /**< version */
+#define PW_PROTOCOL_MARSHAL_FLAG_IMPL (1 << 0) /**< marshal for implementations */
+ uint32_t flags; /**< version */
+ uint32_t n_client_methods; /**< number of client methods */
+ uint32_t n_server_methods; /**< number of server methods */
+ const void *client_marshal;
+ const void *server_demarshal;
+ const void *server_marshal;
+ const void *client_demarshal;
+};
+
+struct pw_protocol_implementation {
+#define PW_VERSION_PROTOCOL_IMPLEMENTATION 0
+ uint32_t version;
+
+ struct pw_protocol_client * (*new_client) (struct pw_protocol *protocol,
+ struct pw_core *core,
+ const struct spa_dict *props);
+ struct pw_protocol_server * (*add_server) (struct pw_protocol *protocol,
+ struct pw_impl_core *core,
+ const struct spa_dict *props);
+};
+
+struct pw_protocol_events {
+#define PW_VERSION_PROTOCOL_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+};
+
+#define pw_protocol_new_client(p,...) (pw_protocol_get_implementation(p)->new_client(p,__VA_ARGS__))
+#define pw_protocol_add_server(p,...) (pw_protocol_get_implementation(p)->add_server(p,__VA_ARGS__))
+#define pw_protocol_ext(p,type,method,...) (((type*)pw_protocol_get_extension(p))->method( __VA_ARGS__))
+
+struct pw_protocol *pw_protocol_new(struct pw_context *context, const char *name, size_t user_data_size);
+
+void pw_protocol_destroy(struct pw_protocol *protocol);
+
+struct pw_context *pw_protocol_get_context(struct pw_protocol *protocol);
+
+void *pw_protocol_get_user_data(struct pw_protocol *protocol);
+
+const struct pw_protocol_implementation *
+pw_protocol_get_implementation(struct pw_protocol *protocol);
+
+const void *
+pw_protocol_get_extension(struct pw_protocol *protocol);
+
+
+void pw_protocol_add_listener(struct pw_protocol *protocol,
+ struct spa_hook *listener,
+ const struct pw_protocol_events *events,
+ void *data);
+
+int pw_protocol_add_marshal(struct pw_protocol *protocol,
+ const struct pw_protocol_marshal *marshal);
+
+const struct pw_protocol_marshal *
+pw_protocol_get_marshal(struct pw_protocol *protocol, const char *type, uint32_t version, uint32_t flags);
+
+struct pw_protocol * pw_context_find_protocol(struct pw_context *context, const char *name);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_PROTOCOL_H */
diff --git a/src/pipewire/proxy.c b/src/pipewire/proxy.c
new file mode 100644
index 0000000..b3eff72
--- /dev/null
+++ b/src/pipewire/proxy.c
@@ -0,0 +1,374 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <pipewire/log.h>
+#include <pipewire/proxy.h>
+#include <pipewire/core.h>
+#include <pipewire/private.h>
+#include <pipewire/type.h>
+
+#include <spa/debug/types.h>
+
+PW_LOG_TOPIC_EXTERN(log_proxy);
+#define PW_LOG_TOPIC_DEFAULT log_proxy
+
+/** \cond */
+struct proxy {
+ struct pw_proxy this;
+};
+/** \endcond */
+
+int pw_proxy_init(struct pw_proxy *proxy, const char *type, uint32_t version)
+{
+ int res;
+
+ proxy->refcount = 1;
+ proxy->type = type;
+ proxy->version = version;
+ proxy->bound_id = SPA_ID_INVALID;
+
+ proxy->id = pw_map_insert_new(&proxy->core->objects, proxy);
+ if (proxy->id == SPA_ID_INVALID) {
+ res = -errno;
+ pw_log_error("%p: can't allocate new id: %m", proxy);
+ goto error;
+ }
+
+ spa_hook_list_init(&proxy->listener_list);
+ spa_hook_list_init(&proxy->object_listener_list);
+
+ if ((res = pw_proxy_install_marshal(proxy, false)) < 0) {
+ pw_log_error("%p: no marshal for type %s/%d: %s", proxy,
+ type, version, spa_strerror(res));
+ goto error_clean;
+ }
+ proxy->in_map = true;
+ return 0;
+
+error_clean:
+ pw_map_remove(&proxy->core->objects, proxy->id);
+error:
+ return res;
+}
+
+/** Create a proxy object with a given id and type
+ *
+ * \param factory another proxy object that serves as a factory
+ * \param type Type of the proxy object
+ * \param version Interface version
+ * \param user_data_size size of user_data
+ * \return A newly allocated proxy object or NULL on failure
+ *
+ * This function creates a new proxy object with the supplied id and type. The
+ * proxy object will have an id assigned from the client id space.
+ *
+ * \sa pw_core
+ */
+SPA_EXPORT
+struct pw_proxy *pw_proxy_new(struct pw_proxy *factory,
+ const char *type, uint32_t version,
+ size_t user_data_size)
+{
+ struct proxy *impl;
+ struct pw_proxy *this;
+ int res;
+
+ impl = calloc(1, sizeof(struct proxy) + user_data_size);
+ if (impl == NULL)
+ return NULL;
+
+ this = &impl->this;
+ this->core = factory->core;
+
+ if ((res = pw_proxy_init(this, type, version)) < 0)
+ goto error_init;
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(impl, sizeof(struct proxy), void);
+
+ pw_log_debug("%p: new %u type %s/%d core-proxy:%p, marshal:%p",
+ this, this->id, type, version, this->core, this->marshal);
+ return this;
+
+error_init:
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_proxy_install_marshal(struct pw_proxy *this, bool implementor)
+{
+ struct pw_core *core = this->core;
+ const struct pw_protocol_marshal *marshal;
+
+ if (core == NULL)
+ return -EIO;
+
+ marshal = pw_protocol_get_marshal(core->conn->protocol,
+ this->type, this->version,
+ implementor ? PW_PROTOCOL_MARSHAL_FLAG_IMPL : 0);
+ if (marshal == NULL)
+ return -EPROTO;
+
+ this->marshal = marshal;
+ this->type = marshal->type;
+
+ this->impl = SPA_INTERFACE_INIT(
+ this->type,
+ this->marshal->version,
+ this->marshal->client_marshal, this);
+ return 0;
+}
+
+SPA_EXPORT
+void *pw_proxy_get_user_data(struct pw_proxy *proxy)
+{
+ return proxy->user_data;
+}
+
+SPA_EXPORT
+uint32_t pw_proxy_get_id(struct pw_proxy *proxy)
+{
+ return proxy->id;
+}
+
+SPA_EXPORT
+int pw_proxy_set_bound_id(struct pw_proxy *proxy, uint32_t global_id)
+{
+ proxy->bound_id = global_id;
+ pw_log_debug("%p: id:%d bound:%d", proxy, proxy->id, global_id);
+ pw_proxy_emit_bound(proxy, global_id);
+ return 0;
+}
+
+SPA_EXPORT
+uint32_t pw_proxy_get_bound_id(struct pw_proxy *proxy)
+{
+ return proxy->bound_id;
+}
+
+SPA_EXPORT
+const char *pw_proxy_get_type(struct pw_proxy *proxy, uint32_t *version)
+{
+ if (version)
+ *version = proxy->version;
+ return proxy->type;
+}
+
+SPA_EXPORT
+struct pw_core *pw_proxy_get_core(struct pw_proxy *proxy)
+{
+ return proxy->core;
+}
+
+SPA_EXPORT
+struct pw_protocol *pw_proxy_get_protocol(struct pw_proxy *proxy)
+{
+ if (proxy->core == NULL || proxy->core->conn == NULL)
+ return NULL;
+ return proxy->core->conn->protocol;
+}
+
+SPA_EXPORT
+void pw_proxy_add_listener(struct pw_proxy *proxy,
+ struct spa_hook *listener,
+ const struct pw_proxy_events *events,
+ void *data)
+{
+ spa_hook_list_append(&proxy->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+void pw_proxy_add_object_listener(struct pw_proxy *proxy,
+ struct spa_hook *listener,
+ const void *funcs,
+ void *data)
+{
+ spa_hook_list_append(&proxy->object_listener_list, listener, funcs, data);
+}
+
+static inline void remove_from_map(struct pw_proxy *proxy)
+{
+ if (proxy->in_map) {
+ if (proxy->core)
+ pw_map_remove(&proxy->core->objects, proxy->id);
+ proxy->in_map = false;
+ }
+}
+
+/** Destroy a proxy object
+ *
+ * \param proxy Proxy object to destroy
+ *
+ * \note This is normally called by \ref pw_core when the server
+ * decides to destroy the server side object
+ */
+SPA_EXPORT
+void pw_proxy_destroy(struct pw_proxy *proxy)
+{
+ pw_log_debug("%p: destroy id:%u removed:%u zombie:%u ref:%d", proxy,
+ proxy->id, proxy->removed, proxy->zombie, proxy->refcount);
+
+ assert(!proxy->destroyed);
+ proxy->destroyed = true;
+
+ if (!proxy->removed) {
+ /* if the server did not remove this proxy, schedule a
+ * destroy if we can */
+ if (proxy->core && !proxy->core->removed) {
+ pw_core_destroy(proxy->core, proxy);
+ proxy->refcount++;
+ } else {
+ proxy->removed = true;
+ }
+ }
+ if (proxy->removed)
+ remove_from_map(proxy);
+
+ if (!proxy->zombie) {
+ /* mark zombie and emit destroyed. No more
+ * events will be emitted on zombie objects */
+ proxy->zombie = true;
+ pw_proxy_emit_destroy(proxy);
+ }
+
+ pw_proxy_unref(proxy);
+}
+
+/** called when cleaning up or when the server removed the resource. Can
+ * be called multiple times */
+void pw_proxy_remove(struct pw_proxy *proxy)
+{
+ assert(proxy->refcount > 0);
+
+ pw_log_debug("%p: remove id:%u removed:%u destroyed:%u zombie:%u ref:%d", proxy,
+ proxy->id, proxy->removed, proxy->destroyed, proxy->zombie,
+ proxy->refcount);
+
+ if (!proxy->destroyed)
+ proxy->refcount++;
+
+ if (!proxy->removed) {
+ /* mark removed and emit the removed signal only once and
+ * only when not already destroyed */
+ proxy->removed = true;
+ if (!proxy->destroyed)
+ pw_proxy_emit_removed(proxy);
+ }
+ if (proxy->destroyed)
+ remove_from_map(proxy);
+
+ pw_proxy_unref(proxy);
+}
+
+SPA_EXPORT
+void pw_proxy_unref(struct pw_proxy *proxy)
+{
+ assert(proxy->refcount > 0);
+ if (--proxy->refcount > 0)
+ return;
+
+ pw_log_debug("%p: free %u", proxy, proxy->id);
+ /** client must explicitly destroy all proxies */
+ assert(proxy->destroyed);
+
+#if DEBUG_LISTENERS
+ {
+ struct spa_hook *h;
+ spa_list_for_each(h, &proxy->object_listener_list.list, link) {
+ pw_log_warn("%p: proxy %u: leaked object listener %p",
+ proxy, proxy->id, h);
+ break;
+ }
+ spa_list_for_each(h, &proxy->listener_list.list, link) {
+ pw_log_warn("%p: proxy %u: leaked listener %p",
+ proxy, proxy->id, h);
+ break;
+ }
+ }
+#endif
+ free(proxy);
+}
+
+SPA_EXPORT
+void pw_proxy_ref(struct pw_proxy *proxy)
+{
+ assert(proxy->refcount > 0);
+ proxy->refcount++;
+}
+
+SPA_EXPORT
+int pw_proxy_sync(struct pw_proxy *proxy, int seq)
+{
+ int res = -EIO;
+ struct pw_core *core = proxy->core;
+
+ if (core && !core->removed) {
+ res = pw_core_sync(core, proxy->id, seq);
+ pw_log_debug("%p: %u seq:%d sync %u", proxy, proxy->id, seq, res);
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_proxy_errorf(struct pw_proxy *proxy, int res, const char *error, ...)
+{
+ va_list ap;
+ int r = -EIO;
+ struct pw_core *core = proxy->core;
+
+ va_start(ap, error);
+ if (core && !core->removed)
+ r = pw_core_errorv(core, proxy->id,
+ core->recv_seq, res, error, ap);
+ va_end(ap);
+ return r;
+}
+
+SPA_EXPORT
+int pw_proxy_error(struct pw_proxy *proxy, int res, const char *error)
+{
+ int r = -EIO;
+ struct pw_core *core = proxy->core;
+
+ if (core && !core->removed)
+ r = pw_core_error(core, proxy->id,
+ core->recv_seq, res, error);
+ return r;
+}
+
+SPA_EXPORT
+struct spa_hook_list *pw_proxy_get_object_listeners(struct pw_proxy *proxy)
+{
+ return &proxy->object_listener_list;
+}
+
+SPA_EXPORT
+const struct pw_protocol_marshal *pw_proxy_get_marshal(struct pw_proxy *proxy)
+{
+ return proxy->marshal;
+}
diff --git a/src/pipewire/proxy.h b/src/pipewire/proxy.h
new file mode 100644
index 0000000..1e15dcc
--- /dev/null
+++ b/src/pipewire/proxy.h
@@ -0,0 +1,219 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_PROXY_H
+#define PIPEWIRE_PROXY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+
+/** \page page_proxy Proxy
+ *
+ * \section sec_page_proxy_overview Overview
+ *
+ * The proxy object is a client side representation of a resource
+ * that lives on a remote PipeWire instance.
+ *
+ * It is used to communicate with the remote object.
+ *
+ * \section sec_page_proxy_core Core proxy
+ *
+ * A proxy for a remote core object can be obtained by making
+ * a remote connection with \ref pw_context_connect.
+ * See \ref pw_proxy
+ *
+ * Some methods on proxy object allow creation of more proxy objects or
+ * create a binding between a local proxy and global resource.
+ *
+ * \section sec_page_proxy_create Create
+ *
+ * A client first creates a new proxy object with pw_proxy_new(). A
+ * type must be provided for this object.
+ *
+ * The protocol of the context will usually install an interface to
+ * translate method calls and events to the wire format.
+ *
+ * The creator of the proxy will usually also install an event
+ * implementation of the particular object type.
+ *
+ * \section sec_page_proxy_bind Bind
+ *
+ * To actually use the proxy object, one needs to create a server
+ * side resource for it. This can be done by, for example, binding
+ * to a global object or by calling a method that creates and binds
+ * to a new remote object. In all cases, the local id is passed to
+ * the server and is used to create a resource with the same id.
+ *
+ * \section sec_page_proxy_methods Methods
+ *
+ * To call a method on the proxy use the interface methods. Calling
+ * any interface method will result in a request to the server to
+ * perform the requested action on the corresponding resource.
+ *
+ * \section sec_page_proxy_events Events
+ *
+ * Events send from the server to the proxy will be demarshalled by
+ * the protocol and will then result in a call to the installed
+ * implementation of the proxy.
+ *
+ * \section sec_page_proxy_destroy Destroy
+ *
+ * Use pw_proxy_destroy() to destroy the client side object. This
+ * is usually done automatically when the server removes the resource
+ * associated to the proxy.
+ */
+
+/** \defgroup pw_proxy Proxy
+ *
+ * \brief Represents an object on the client side.
+ *
+ * A pw_proxy acts as a client side proxy to an object existing in a remote
+ * pipewire instance. The proxy is responsible for converting interface functions
+ * invoked by the client to PipeWire messages. Events will call the handlers
+ * set in listener.
+ *
+ * See \ref page_proxy
+ */
+
+/**
+ * \addtogroup pw_proxy
+ * \{
+ */
+struct pw_proxy;
+
+#include <pipewire/protocol.h>
+
+/** Proxy events, use \ref pw_proxy_add_listener */
+struct pw_proxy_events {
+#define PW_VERSION_PROXY_EVENTS 0
+ uint32_t version;
+
+ /** The proxy is destroyed */
+ void (*destroy) (void *data);
+
+ /** a proxy is bound to a global id */
+ void (*bound) (void *data, uint32_t global_id);
+
+ /** a proxy is removed from the server. Use pw_proxy_destroy to
+ * free the proxy. */
+ void (*removed) (void *data);
+
+ /** a reply to a sync method completed */
+ void (*done) (void *data, int seq);
+
+ /** an error occurred on the proxy */
+ void (*error) (void *data, int seq, int res, const char *message);
+};
+
+/* Make a new proxy object. The id can be used to bind to a remote object and
+ * can be retrieved with \ref pw_proxy_get_id . */
+struct pw_proxy *
+pw_proxy_new(struct pw_proxy *factory,
+ const char *type, /* interface type */
+ uint32_t version, /* interface version */
+ size_t user_data_size /* size of user data */);
+
+/** Add an event listener to proxy */
+void pw_proxy_add_listener(struct pw_proxy *proxy,
+ struct spa_hook *listener,
+ const struct pw_proxy_events *events,
+ void *data);
+
+/** Add a listener for the events received from the remote object. The
+ * events depend on the type of the remote object type. */
+void pw_proxy_add_object_listener(struct pw_proxy *proxy, /**< the proxy */
+ struct spa_hook *listener, /**< listener */
+ const void *funcs, /**< proxied functions */
+ void *data /**< data passed to events */);
+
+/** destroy a proxy */
+void pw_proxy_destroy(struct pw_proxy *proxy);
+
+void pw_proxy_ref(struct pw_proxy *proxy);
+void pw_proxy_unref(struct pw_proxy *proxy);
+
+/** Get the user_data. The size was given in \ref pw_proxy_new */
+void *pw_proxy_get_user_data(struct pw_proxy *proxy);
+
+/** Get the local id of the proxy */
+uint32_t pw_proxy_get_id(struct pw_proxy *proxy);
+
+/** Get the type and version of the proxy */
+const char *pw_proxy_get_type(struct pw_proxy *proxy, uint32_t *version);
+
+/** Get the protocol used for the proxy */
+struct pw_protocol *pw_proxy_get_protocol(struct pw_proxy *proxy);
+
+/** Generate an sync method for a proxy. This will generate a done event
+ * with the same seq number of the reply. */
+int pw_proxy_sync(struct pw_proxy *proxy, int seq);
+
+/** Set the global id this proxy is bound to. This is usually used internally
+ * and will also emit the bound event */
+int pw_proxy_set_bound_id(struct pw_proxy *proxy, uint32_t global_id);
+/** Get the global id bound to this proxy of SPA_ID_INVALID when not bound
+ * to a global */
+uint32_t pw_proxy_get_bound_id(struct pw_proxy *proxy);
+
+/** Generate an error for a proxy */
+int pw_proxy_error(struct pw_proxy *proxy, int res, const char *error);
+int pw_proxy_errorf(struct pw_proxy *proxy, int res, const char *error, ...) SPA_PRINTF_FUNC(3, 4);
+
+/** Get the listener of proxy */
+struct spa_hook_list *pw_proxy_get_object_listeners(struct pw_proxy *proxy);
+
+/** Get the marshal functions for the proxy */
+const struct pw_protocol_marshal *pw_proxy_get_marshal(struct pw_proxy *proxy);
+
+/** Install a marshal function on a proxy */
+int pw_proxy_install_marshal(struct pw_proxy *proxy, bool implementor);
+
+#define pw_proxy_notify(p,type,event,version,...) \
+ spa_hook_list_call(pw_proxy_get_object_listeners(p), \
+ type, event, version, ## __VA_ARGS__)
+
+#define pw_proxy_call(p,type,method,version,...) \
+ spa_interface_call((struct spa_interface*)p, \
+ type, method, version, ##__VA_ARGS__)
+
+#define pw_proxy_call_res(p,type,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_interface_call_res((struct spa_interface*)p, \
+ type, _res, method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_PROXY_H */
diff --git a/src/pipewire/resource.c b/src/pipewire/resource.c
new file mode 100644
index 0000000..0a6231c
--- /dev/null
+++ b/src/pipewire/resource.c
@@ -0,0 +1,351 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <assert.h>
+
+#include "pipewire/private.h"
+#include "pipewire/protocol.h"
+#include "pipewire/resource.h"
+#include "pipewire/type.h"
+
+#include <spa/debug/types.h>
+
+PW_LOG_TOPIC_EXTERN(log_resource);
+#define PW_LOG_TOPIC_DEFAULT log_resource
+
+/** \cond */
+struct impl {
+ struct pw_resource this;
+};
+/** \endcond */
+
+SPA_EXPORT
+struct pw_resource *pw_resource_new(struct pw_impl_client *client,
+ uint32_t id,
+ uint32_t permissions,
+ const char *type,
+ uint32_t version,
+ size_t user_data_size)
+{
+ struct impl *impl;
+ struct pw_resource *this;
+ int res;
+
+ impl = calloc(1, sizeof(struct impl) + user_data_size);
+ if (impl == NULL)
+ return NULL;
+
+ this = &impl->this;
+ this->refcount = 1;
+ this->context = client->context;
+ this->client = client;
+ this->permissions = permissions;
+ this->type = type;
+ this->version = version;
+ this->bound_id = SPA_ID_INVALID;
+
+ spa_hook_list_init(&this->listener_list);
+ spa_hook_list_init(&this->object_listener_list);
+
+ if (id == SPA_ID_INVALID) {
+ res = -EINVAL;
+ goto error_clean;
+ }
+
+ if ((res = pw_map_insert_at(&client->objects, id, this)) < 0) {
+ pw_log_error("%p: can't add id %u for client %p: %s",
+ this, id, client, spa_strerror(res));
+ goto error_clean;
+ }
+ this->id = id;
+
+ if ((res = pw_resource_install_marshal(this, false)) < 0) {
+ pw_log_error("%p: no marshal for type %s/%d: %s", this,
+ type, version, spa_strerror(res));
+ goto error_clean;
+ }
+
+
+ if (user_data_size > 0)
+ this->user_data = SPA_PTROFF(impl, sizeof(struct impl), void);
+
+ pw_log_debug("%p: new %u type %s/%d client:%p marshal:%p",
+ this, id, type, version, client, this->marshal);
+
+ pw_impl_client_emit_resource_added(client, this);
+
+ return this;
+
+error_clean:
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_resource_install_marshal(struct pw_resource *this, bool implementor)
+{
+ struct pw_impl_client *client = this->client;
+ const struct pw_protocol_marshal *marshal;
+
+ marshal = pw_protocol_get_marshal(client->protocol,
+ this->type, this->version,
+ implementor ? PW_PROTOCOL_MARSHAL_FLAG_IMPL : 0);
+ if (marshal == NULL)
+ return -EPROTO;
+
+ this->marshal = marshal;
+ this->type = marshal->type;
+
+ this->impl = SPA_INTERFACE_INIT(
+ this->type,
+ this->marshal->version,
+ this->marshal->server_marshal, this);
+ return 0;
+}
+
+SPA_EXPORT
+struct pw_impl_client *pw_resource_get_client(struct pw_resource *resource)
+{
+ return resource->client;
+}
+
+SPA_EXPORT
+uint32_t pw_resource_get_id(struct pw_resource *resource)
+{
+ return resource->id;
+}
+
+SPA_EXPORT
+uint32_t pw_resource_get_permissions(struct pw_resource *resource)
+{
+ return resource->permissions;
+}
+
+SPA_EXPORT
+const char *pw_resource_get_type(struct pw_resource *resource, uint32_t *version)
+{
+ if (version)
+ *version = resource->version;
+ return resource->type;
+}
+
+SPA_EXPORT
+struct pw_protocol *pw_resource_get_protocol(struct pw_resource *resource)
+{
+ return resource->client->protocol;
+}
+
+SPA_EXPORT
+void *pw_resource_get_user_data(struct pw_resource *resource)
+{
+ return resource->user_data;
+}
+
+SPA_EXPORT
+void pw_resource_add_listener(struct pw_resource *resource,
+ struct spa_hook *listener,
+ const struct pw_resource_events *events,
+ void *data)
+{
+ spa_hook_list_append(&resource->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+void pw_resource_add_object_listener(struct pw_resource *resource,
+ struct spa_hook *listener,
+ const void *funcs,
+ void *data)
+{
+ spa_hook_list_append(&resource->object_listener_list, listener, funcs, data);
+}
+
+SPA_EXPORT
+struct spa_hook_list *pw_resource_get_object_listeners(struct pw_resource *resource)
+{
+ return &resource->object_listener_list;
+}
+
+SPA_EXPORT
+const struct pw_protocol_marshal *pw_resource_get_marshal(struct pw_resource *resource)
+{
+ return resource->marshal;
+}
+
+SPA_EXPORT
+int pw_resource_ping(struct pw_resource *resource, int seq)
+{
+ int res = -EIO;
+ struct pw_impl_client *client = resource->client;
+
+ if (client->core_resource != NULL) {
+ pw_core_resource_ping(client->core_resource, resource->id, seq);
+ res = client->send_seq;
+ pw_log_debug("%p: %u seq:%d ping %d", resource, resource->id, seq, res);
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_resource_set_bound_id(struct pw_resource *resource, uint32_t global_id)
+{
+ struct pw_impl_client *client = resource->client;
+
+ resource->bound_id = global_id;
+ if (client->core_resource != NULL) {
+ pw_log_debug("%p: %u global_id:%u", resource, resource->id, global_id);
+ pw_core_resource_bound_id(client->core_resource, resource->id, global_id);
+ }
+ return 0;
+}
+
+SPA_EXPORT
+uint32_t pw_resource_get_bound_id(struct pw_resource *resource)
+{
+ return resource->bound_id;
+}
+
+static void SPA_PRINTF_FUNC(4, 0)
+pw_resource_errorv_id(struct pw_resource *resource, uint32_t id, int res, const char *error, va_list ap)
+{
+ struct pw_impl_client *client;
+
+ if (resource) {
+ client = resource->client;
+ if (client->core_resource != NULL)
+ pw_core_resource_errorv(client->core_resource,
+ id, client->recv_seq, res, error, ap);
+ } else {
+ pw_logtv(SPA_LOG_LEVEL_ERROR, PW_LOG_TOPIC_DEFAULT, error, ap);
+ }
+}
+
+SPA_EXPORT
+void pw_resource_errorf(struct pw_resource *resource, int res, const char *error, ...)
+{
+ va_list ap;
+ va_start(ap, error);
+ if (resource)
+ pw_resource_errorv_id(resource, resource->id, res, error, ap);
+ else
+ pw_logtv(SPA_LOG_LEVEL_ERROR, PW_LOG_TOPIC_DEFAULT, error, ap);
+ va_end(ap);
+}
+
+SPA_EXPORT
+void pw_resource_errorf_id(struct pw_resource *resource, uint32_t id, int res, const char *error, ...)
+{
+ va_list ap;
+ va_start(ap, error);
+ if (resource)
+ pw_resource_errorv_id(resource, id, res, error, ap);
+ else
+ pw_logtv(SPA_LOG_LEVEL_ERROR, PW_LOG_TOPIC_DEFAULT, error, ap);
+ va_end(ap);
+}
+
+SPA_EXPORT
+void pw_resource_error(struct pw_resource *resource, int res, const char *error)
+{
+ struct pw_impl_client *client;
+ if (resource) {
+ client = resource->client;
+ if (client->core_resource != NULL)
+ pw_core_resource_error(client->core_resource,
+ resource->id, client->recv_seq, res, error);
+ } else {
+ pw_log_error("%s: %s", error, spa_strerror(res));
+ }
+}
+
+SPA_EXPORT
+void pw_resource_ref(struct pw_resource *resource)
+{
+ assert(resource->refcount > 0);
+ resource->refcount++;
+}
+
+SPA_EXPORT
+void pw_resource_unref(struct pw_resource *resource)
+{
+ assert(resource->refcount > 0);
+ if (--resource->refcount > 0)
+ return;
+
+ pw_log_debug("%p: free %u", resource, resource->id);
+ assert(resource->destroyed);
+
+#if DEBUG_LISTENERS
+ {
+ struct spa_hook *h;
+ spa_list_for_each(h, &resource->object_listener_list.list, link) {
+ pw_log_warn("%p: resource %u: leaked object listener %p",
+ resource, resource->id, h);
+ break;
+ }
+ spa_list_for_each(h, &resource->listener_list.list, link) {
+ pw_log_warn("%p: resource %u: leaked listener %p",
+ resource, resource->id, h);
+ break;
+ }
+ }
+#endif
+ spa_hook_list_clean(&resource->listener_list);
+ spa_hook_list_clean(&resource->object_listener_list);
+
+ free(resource);
+}
+
+SPA_EXPORT
+void pw_resource_destroy(struct pw_resource *resource)
+{
+ struct pw_impl_client *client = resource->client;
+
+ pw_log_debug("%p: destroy %u", resource, resource->id);
+ assert(!resource->destroyed);
+ resource->destroyed = true;
+
+ if (resource->global) {
+ spa_list_remove(&resource->link);
+ resource->global = NULL;
+ }
+
+ pw_resource_emit_destroy(resource);
+
+ pw_map_insert_at(&client->objects, resource->id, NULL);
+ pw_impl_client_emit_resource_removed(client, resource);
+
+ if (client->core_resource && !resource->removed)
+ pw_core_resource_remove_id(client->core_resource, resource->id);
+
+ pw_resource_unref(resource);
+}
+
+SPA_EXPORT
+void pw_resource_remove(struct pw_resource *resource)
+{
+ resource->removed = true;
+ pw_resource_destroy(resource);
+}
diff --git a/src/pipewire/resource.h b/src/pipewire/resource.h
new file mode 100644
index 0000000..24d458c
--- /dev/null
+++ b/src/pipewire/resource.h
@@ -0,0 +1,174 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_RESOURCE_H
+#define PIPEWIRE_RESOURCE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/hook.h>
+
+/** \defgroup pw_resource Resource
+ *
+ * \brief Client owned objects
+ *
+ * Resources represent objects owned by a \ref pw_impl_client. They are
+ * the result of binding to a global resource or by calling API that
+ * creates client owned objects.
+ *
+ * The client usually has a proxy object associated with the resource
+ * that it can use to communicate with the resource. See \ref page_proxy.
+ *
+ * Resources are destroyed when the client or the bound object is
+ * destroyed.
+ *
+ */
+
+
+/**
+ * \addtogroup pw_resource
+ * \{
+ */
+struct pw_resource;
+
+#include <pipewire/impl-client.h>
+
+/** Resource events */
+struct pw_resource_events {
+#define PW_VERSION_RESOURCE_EVENTS 0
+ uint32_t version;
+
+ /** The resource is destroyed */
+ void (*destroy) (void *data);
+
+ /** a reply to a ping event completed */
+ void (*pong) (void *data, int seq);
+
+ /** an error occurred on the resource */
+ void (*error) (void *data, int seq, int res, const char *message);
+};
+
+/** Make a new resource for client */
+struct pw_resource *
+pw_resource_new(struct pw_impl_client *client, /**< the client owning the resource */
+ uint32_t id, /**< the remote per client id */
+ uint32_t permissions, /**< permissions on this resource */
+ const char *type, /**< interface of the resource */
+ uint32_t version, /**< requested interface version */
+ size_t user_data_size /**< extra user data size */);
+
+/** Destroy a resource */
+void pw_resource_destroy(struct pw_resource *resource);
+
+/** Remove a resource, like pw_resource_destroy but without sending a
+ * remove_id message to the client */
+void pw_resource_remove(struct pw_resource *resource);
+
+/** Get the client owning this resource */
+struct pw_impl_client *pw_resource_get_client(struct pw_resource *resource);
+
+/** Get the unique id of this resource */
+uint32_t pw_resource_get_id(struct pw_resource *resource);
+
+/** Get the permissions of this resource */
+uint32_t pw_resource_get_permissions(struct pw_resource *resource);
+
+/** Get the type and optionally the version of this resource */
+const char *pw_resource_get_type(struct pw_resource *resource, uint32_t *version);
+
+/** Get the protocol used for this resource */
+struct pw_protocol *pw_resource_get_protocol(struct pw_resource *resource);
+
+/** Get the user data for the resource, the size was given in \ref pw_resource_new */
+void *pw_resource_get_user_data(struct pw_resource *resource);
+
+/** Add an event listener */
+void pw_resource_add_listener(struct pw_resource *resource,
+ struct spa_hook *listener,
+ const struct pw_resource_events *events,
+ void *data);
+
+/** Set the resource implementation. */
+void pw_resource_add_object_listener(struct pw_resource *resource,
+ struct spa_hook *listener,
+ const void *funcs,
+ void *data);
+
+/** Generate an ping event for a resource. This will generate a pong event
+ * with the same \a sequence number in the return value. */
+int pw_resource_ping(struct pw_resource *resource, int seq);
+
+/** ref/unref a resource, Since 0.3.52 */
+void pw_resource_ref(struct pw_resource *resource);
+void pw_resource_unref(struct pw_resource *resource);
+
+/** Notify global id this resource is bound to */
+int pw_resource_set_bound_id(struct pw_resource *resource, uint32_t global_id);
+
+/** Get the global id this resource is bound to or SPA_ID_INVALID when not bound */
+uint32_t pw_resource_get_bound_id(struct pw_resource *resource);
+
+/** Generate an error for a resource */
+void pw_resource_error(struct pw_resource *resource, int res, const char *error);
+void pw_resource_errorf(struct pw_resource *resource, int res, const char *error, ...) SPA_PRINTF_FUNC(3, 4);
+void pw_resource_errorf_id(struct pw_resource *resource, uint32_t id, int res, const char *error, ...) SPA_PRINTF_FUNC(4, 5);
+
+/** Get the list of object listeners from a resource */
+struct spa_hook_list *pw_resource_get_object_listeners(struct pw_resource *resource);
+
+/** Get the marshal functions for the resource */
+const struct pw_protocol_marshal *pw_resource_get_marshal(struct pw_resource *resource);
+
+/** install a marshal function on a resource */
+int pw_resource_install_marshal(struct pw_resource *resource, bool implementor);
+
+#define pw_resource_notify(r,type,event,version,...) \
+ spa_hook_list_call(pw_resource_get_object_listeners(r), \
+ type, event, version, ## __VA_ARGS__)
+
+#define pw_resource_call(r,type,method,version,...) \
+ spa_interface_call((struct spa_interface*)r, \
+ type, method, version, ##__VA_ARGS__)
+
+#define pw_resource_call_res(r,type,method,version,...) \
+({ \
+ int _res = -ENOTSUP; \
+ spa_interface_call_res((struct spa_interface*)r, \
+ type, _res, method, version, ##__VA_ARGS__); \
+ _res; \
+})
+
+
+/**
+ * \}
+ */
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_RESOURCE_H */
diff --git a/src/pipewire/settings.c b/src/pipewire/settings.c
new file mode 100644
index 0000000..c512e96
--- /dev/null
+++ b/src/pipewire/settings.c
@@ -0,0 +1,337 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/pod/filter.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/private.h>
+#include "pipewire/array.h"
+#include "pipewire/core.h"
+
+#include <pipewire/extensions/metadata.h>
+
+#define NAME "settings"
+
+#define DEFAULT_CLOCK_RATE 48000u
+#define DEFAULT_CLOCK_RATES "[ 48000 ]"
+#define DEFAULT_CLOCK_QUANTUM 1024u
+#define DEFAULT_CLOCK_MIN_QUANTUM 32u
+#define DEFAULT_CLOCK_MAX_QUANTUM 2048u
+#define DEFAULT_CLOCK_QUANTUM_LIMIT 8192u
+#define DEFAULT_CLOCK_POWER_OF_TWO_QUANTUM true
+#define DEFAULT_VIDEO_WIDTH 640
+#define DEFAULT_VIDEO_HEIGHT 480
+#define DEFAULT_VIDEO_RATE_NUM 25u
+#define DEFAULT_VIDEO_RATE_DENOM 1u
+#define DEFAULT_LINK_MAX_BUFFERS 64u
+#define DEFAULT_MEM_WARN_MLOCK false
+#define DEFAULT_MEM_ALLOW_MLOCK true
+#define DEFAULT_CHECK_QUANTUM false
+#define DEFAULT_CHECK_RATE false
+
+struct impl {
+ struct pw_context *context;
+ struct pw_impl_metadata *metadata;
+
+ struct spa_hook metadata_listener;
+};
+
+static void metadata_destroy(void *data)
+{
+ struct impl *impl = data;
+ spa_hook_remove(&impl->metadata_listener);
+ impl->metadata = NULL;
+}
+
+static uint32_t get_default_int(struct pw_properties *properties, const char *name, uint32_t def)
+{
+ uint32_t val;
+ const char *str;
+ if ((str = pw_properties_get(properties, name)) != NULL)
+ val = atoi(str);
+ else {
+ val = def;
+ pw_properties_setf(properties, name, "%d", val);
+ }
+ return val;
+}
+
+static bool get_default_bool(struct pw_properties *properties, const char *name, bool def)
+{
+ bool val;
+ const char *str;
+ if ((str = pw_properties_get(properties, name)) != NULL)
+ val = pw_properties_parse_bool(str);
+ else {
+ val = def;
+ pw_properties_set(properties, name, val ? "true" : "false");
+ }
+ return val;
+}
+
+static bool uint32_array_contains(uint32_t *vals, uint32_t n_vals, uint32_t val)
+{
+ uint32_t i;
+ for (i = 0; i < n_vals; i++)
+ if (vals[i] == val)
+ return true;
+ return false;
+}
+
+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];
+ 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));
+
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ count < max) {
+ if (spa_atou32(v, &r, 0))
+ vals[count++] = r;
+ }
+ if (!uint32_array_contains(vals, count, def))
+ count = 0;
+ return count;
+}
+
+static uint32_t parse_clock_rate(struct pw_properties *properties, const char *name,
+ uint32_t *rates, const char *def_rates, uint32_t def)
+{
+ const char *str;
+ uint32_t count = 0;
+
+ if ((str = pw_properties_get(properties, name)) == NULL)
+ str = def_rates;
+
+ count = parse_uint32_array(str, rates, MAX_RATES, def);
+ if (count == 0)
+ count = parse_uint32_array(def_rates, rates, MAX_RATES, def);
+ if (count == 0)
+ goto fallback;
+
+ return count;
+fallback:
+ rates[0] = def;
+ pw_properties_setf(properties, name, "[ %u ]", def);
+ return 1;
+}
+
+static int metadata_property(void *data, uint32_t subject, const char *key,
+ const char *type, const char *value)
+{
+ struct impl *impl = data;
+ struct pw_context *context = impl->context;
+ struct settings *d = &context->defaults;
+ struct settings *s = &context->settings;
+ uint32_t v;
+ bool recalc = false;
+
+ if (subject != PW_ID_CORE)
+ return 0;
+
+ if (spa_streq(key, "log.level")) {
+ v = value ? atoi(value) : 3;
+ pw_log_set_level(v);
+ } else if (spa_streq(key, "clock.rate")) {
+ v = value ? atoi(value) : 0;
+ s->clock_rate = v == 0 ? d->clock_rate : v;
+ recalc = true;
+ } else if (spa_streq(key, "clock.allowed-rates")) {
+ s->n_clock_rates = parse_uint32_array(value,
+ s->clock_rates, MAX_RATES, s->clock_rate);
+ if (s->n_clock_rates == 0) {
+ s->n_clock_rates = d->n_clock_rates;
+ memcpy(s->clock_rates, d->clock_rates, MAX_RATES * sizeof(uint32_t));
+ }
+ recalc = true;
+ } else if (spa_streq(key, "clock.quantum")) {
+ v = value ? atoi(value) : 0;
+ s->clock_quantum = v == 0 ? d->clock_quantum : v;
+ recalc = true;
+ } else if (spa_streq(key, "clock.min-quantum")) {
+ v = value ? atoi(value) : 0;
+ s->clock_min_quantum = v == 0 ? d->clock_min_quantum : v;
+ recalc = true;
+ } else if (spa_streq(key, "clock.max-quantum")) {
+ v = value ? atoi(value) : 0;
+ s->clock_max_quantum = v == 0 ? d->clock_max_quantum : v;
+ recalc = true;
+ } else if (spa_streq(key, "clock.force-rate")) {
+ v = value ? atoi(value) : 0;
+ if (v != 0 && s->check_rate &&
+ !uint32_array_contains(s->clock_rates, s->n_clock_rates, v)) {
+ pw_log_info("invalid %s: %d not in allowed rates", key, v);
+ } else {
+ s->clock_force_rate = v;
+ recalc = true;
+ }
+ } else if (spa_streq(key, "clock.force-quantum")) {
+ v = value ? atoi(value) : 0;
+ if (v != 0 && s->check_quantum &&
+ (v < s->clock_min_quantum || v > s->clock_max_quantum)) {
+ pw_log_info("invalid %s: %d not in (%d-%d)", key, v,
+ s->clock_min_quantum, s->clock_max_quantum);
+ } else {
+ s->clock_force_quantum = v;
+ recalc = true;
+ }
+ }
+ if (recalc)
+ pw_context_recalc_graph(context, "settings changed");
+
+ return 0;
+}
+
+static const struct pw_impl_metadata_events metadata_events = {
+ PW_VERSION_IMPL_METADATA_EVENTS,
+ .destroy = metadata_destroy,
+ .property = metadata_property,
+};
+
+void pw_settings_init(struct pw_context *this)
+{
+ struct pw_properties *p = this->properties;
+ struct settings *d = &this->defaults;
+
+ d->clock_rate = get_default_int(p, "default.clock.rate", DEFAULT_CLOCK_RATE);
+ d->n_clock_rates = parse_clock_rate(p, "default.clock.allowed-rates", d->clock_rates,
+ DEFAULT_CLOCK_RATES, d->clock_rate);
+ d->clock_quantum = get_default_int(p, "default.clock.quantum", DEFAULT_CLOCK_QUANTUM);
+ d->clock_min_quantum = get_default_int(p, "default.clock.min-quantum", DEFAULT_CLOCK_MIN_QUANTUM);
+ d->clock_max_quantum = get_default_int(p, "default.clock.max-quantum", DEFAULT_CLOCK_MAX_QUANTUM);
+ d->clock_quantum_limit = get_default_int(p, "default.clock.quantum-limit", DEFAULT_CLOCK_QUANTUM_LIMIT);
+ d->video_size.width = get_default_int(p, "default.video.width", DEFAULT_VIDEO_WIDTH);
+ d->video_size.height = get_default_int(p, "default.video.height", DEFAULT_VIDEO_HEIGHT);
+ d->video_rate.num = get_default_int(p, "default.video.rate.num", DEFAULT_VIDEO_RATE_NUM);
+ d->video_rate.denom = get_default_int(p, "default.video.rate.denom", DEFAULT_VIDEO_RATE_DENOM);
+
+ d->log_level = get_default_int(p, "log.level", pw_log_level);
+ d->clock_power_of_two_quantum = get_default_bool(p, "clock.power-of-two-quantum",
+ DEFAULT_CLOCK_POWER_OF_TWO_QUANTUM);
+ d->link_max_buffers = get_default_int(p, "link.max-buffers", DEFAULT_LINK_MAX_BUFFERS);
+ d->mem_warn_mlock = get_default_bool(p, "mem.warn-mlock", DEFAULT_MEM_WARN_MLOCK);
+ d->mem_allow_mlock = get_default_bool(p, "mem.allow-mlock", DEFAULT_MEM_ALLOW_MLOCK);
+
+ d->check_quantum = get_default_bool(p, "settings.check-quantum", DEFAULT_CHECK_QUANTUM);
+ d->check_rate = get_default_bool(p, "settings.check-rate", DEFAULT_CHECK_RATE);
+
+ d->clock_quantum_limit = SPA_CLAMP(d->clock_quantum_limit,
+ CLOCK_MIN_QUANTUM, CLOCK_MAX_QUANTUM);
+ d->clock_max_quantum = SPA_CLAMP(d->clock_max_quantum,
+ CLOCK_MIN_QUANTUM, d->clock_quantum_limit);
+ d->clock_min_quantum = SPA_CLAMP(d->clock_min_quantum,
+ CLOCK_MIN_QUANTUM, d->clock_max_quantum);
+ d->clock_quantum = SPA_CLAMP(d->clock_quantum,
+ d->clock_min_quantum, d->clock_max_quantum);
+}
+
+static void expose_settings(struct pw_context *context, struct pw_impl_metadata *metadata)
+{
+ struct settings *s = &context->settings;
+ uint32_t i, o;
+ char rates[MAX_RATES*16] = "";
+
+ pw_impl_metadata_set_propertyf(metadata,
+ PW_ID_CORE, "log.level", "", "%d", s->log_level);
+ pw_impl_metadata_set_propertyf(metadata,
+ PW_ID_CORE, "clock.rate", "", "%d", s->clock_rate);
+ for (i = 0, o = 0; i < s->n_clock_rates; i++) {
+ int r = snprintf(rates+o, sizeof(rates)-o, "%s%d", i == 0 ? "" : ", ",
+ s->clock_rates[i]);
+ if (r < 0 || o + r >= (int)sizeof(rates)) {
+ snprintf(rates, sizeof(rates), "%d", s->clock_rate);
+ break;
+ }
+ o += r;
+ }
+ if (s->n_clock_rates == 0)
+ snprintf(rates, sizeof(rates), "%d", s->clock_rate);
+
+ pw_impl_metadata_set_propertyf(metadata,
+ PW_ID_CORE, "clock.allowed-rates", "", "[ %s ]", rates);
+ pw_impl_metadata_set_propertyf(metadata,
+ PW_ID_CORE, "clock.quantum", "", "%d", s->clock_quantum);
+ pw_impl_metadata_set_propertyf(metadata,
+ PW_ID_CORE, "clock.min-quantum", "", "%d", s->clock_min_quantum);
+ pw_impl_metadata_set_propertyf(metadata,
+ PW_ID_CORE, "clock.max-quantum", "", "%d", s->clock_max_quantum);
+ pw_impl_metadata_set_propertyf(metadata,
+ PW_ID_CORE, "clock.force-quantum", "", "%d", s->clock_force_quantum);
+ pw_impl_metadata_set_propertyf(metadata,
+ PW_ID_CORE, "clock.force-rate", "", "%d", s->clock_force_rate);
+}
+
+int pw_settings_expose(struct pw_context *context)
+{
+ struct impl *impl;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL)
+ return -errno;
+
+ impl->context = context;
+ impl->metadata = pw_context_create_metadata(context, "settings", NULL, 0);
+ if (impl->metadata == NULL)
+ goto error_free;
+
+ expose_settings(context, impl->metadata);
+
+ pw_impl_metadata_add_listener(impl->metadata,
+ &impl->metadata_listener,
+ &metadata_events, impl);
+
+ pw_impl_metadata_register(impl->metadata, NULL);
+
+ context->settings_impl = impl;
+
+ return 0;
+
+error_free:
+ free(impl);
+ return -errno;
+}
+
+void pw_settings_clean(struct pw_context *context)
+{
+ struct impl *impl = context->settings_impl;
+
+ if (impl == NULL)
+ return;
+
+ context->settings_impl = NULL;
+ if (impl->metadata != NULL)
+ pw_impl_metadata_destroy(impl->metadata);
+ free(impl);
+}
diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c
new file mode 100644
index 0000000..da49b66
--- /dev/null
+++ b/src/pipewire/stream.c
@@ -0,0 +1,2414 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <math.h>
+#include <sys/mman.h>
+#include <time.h>
+
+#include <spa/buffer/alloc.h>
+#include <spa/param/props.h>
+#include <spa/param/format-utils.h>
+#include <spa/node/io.h>
+#include <spa/node/utils.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/dynamic.h>
+#include <spa/debug/types.h>
+
+#define PW_ENABLE_DEPRECATED
+
+#include "pipewire/pipewire.h"
+#include "pipewire/stream.h"
+#include "pipewire/private.h"
+
+PW_LOG_TOPIC_EXTERN(log_stream);
+#define PW_LOG_TOPIC_DEFAULT log_stream
+
+#define MAX_BUFFERS 64
+
+#define MASK_BUFFERS (MAX_BUFFERS-1)
+
+static bool mlock_warned = false;
+
+static uint32_t mappable_dataTypes = (1<<SPA_DATA_MemFd);
+
+struct buffer {
+ struct pw_buffer this;
+ uint32_t id;
+#define BUFFER_FLAG_MAPPED (1 << 0)
+#define BUFFER_FLAG_QUEUED (1 << 1)
+#define BUFFER_FLAG_ADDED (1 << 2)
+ uint32_t flags;
+ struct spa_meta_busy *busy;
+};
+
+struct queue {
+ uint32_t ids[MAX_BUFFERS];
+ struct spa_ringbuffer ring;
+ uint64_t incount;
+ uint64_t outcount;
+};
+
+struct data {
+ struct pw_context *context;
+ struct spa_hook stream_listener;
+};
+
+struct param {
+ uint32_t id;
+#define PARAM_FLAG_LOCKED (1 << 0)
+ uint32_t flags;
+ struct spa_list link;
+ struct spa_pod *param;
+};
+
+struct control {
+ uint32_t id;
+ uint32_t type;
+ uint32_t container;
+ struct spa_list link;
+ struct pw_stream_control control;
+ struct spa_pod *info;
+ unsigned int emitted:1;
+ float values[64];
+};
+
+struct stream {
+ struct pw_stream this;
+
+ const char *path;
+
+ struct pw_context *context;
+ struct spa_hook context_listener;
+
+ enum spa_direction direction;
+ enum pw_stream_flags flags;
+
+ struct pw_impl_node *node;
+
+ struct spa_node impl_node;
+ struct spa_node_methods node_methods;
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+ struct spa_io_buffers *io;
+ struct spa_io_rate_match *rate_match;
+ uint32_t rate_queued;
+ struct {
+ struct spa_io_position *position;
+ } rt;
+
+ uint32_t port_change_mask_all;
+ struct spa_port_info port_info;
+ struct pw_properties *port_props;
+#define PORT_EnumFormat 0
+#define PORT_Meta 1
+#define PORT_IO 2
+#define PORT_Format 3
+#define PORT_Buffers 4
+#define PORT_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info port_params[N_PORT_PARAMS];
+
+ struct spa_list param_list;
+
+ uint32_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
+ struct spa_param_info params[N_NODE_PARAMS];
+
+ uint32_t media_type;
+ uint32_t media_subtype;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+
+ struct queue dequeued;
+ struct queue queued;
+
+ struct data data;
+ uintptr_t seq;
+ struct pw_time time;
+ uint64_t base_pos;
+ uint32_t clock_id;
+ struct spa_latency_info latency;
+ uint64_t quantum;
+
+ struct spa_callbacks rt_callbacks;
+
+ unsigned int disconnecting:1;
+ unsigned int disconnect_core:1;
+ unsigned int draining:1;
+ unsigned int drained:1;
+ unsigned int allow_mlock:1;
+ unsigned int warn_mlock:1;
+ unsigned int process_rt:1;
+ unsigned int driving:1;
+ unsigned int using_trigger:1;
+ unsigned int trigger:1;
+ int in_set_control;
+};
+
+static int get_param_index(uint32_t id)
+{
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ return NODE_PropInfo;
+ case SPA_PARAM_Props:
+ return NODE_Props;
+ case SPA_PARAM_EnumFormat:
+ return NODE_EnumFormat;
+ case SPA_PARAM_Format:
+ return NODE_Format;
+ default:
+ return -1;
+ }
+}
+
+static int get_port_param_index(uint32_t id)
+{
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ return PORT_EnumFormat;
+ case SPA_PARAM_Meta:
+ return PORT_Meta;
+ case SPA_PARAM_IO:
+ return PORT_IO;
+ case SPA_PARAM_Format:
+ return PORT_Format;
+ case SPA_PARAM_Buffers:
+ return PORT_Buffers;
+ case SPA_PARAM_Latency:
+ return PORT_Latency;
+ default:
+ return -1;
+ }
+}
+
+static void fix_datatype(const struct spa_pod *param)
+{
+ const struct spa_pod_prop *pod_param;
+ const struct spa_pod *vals;
+ uint32_t dataType, n_vals, choice;
+
+ pod_param = spa_pod_find_prop(param, NULL, SPA_PARAM_BUFFERS_dataType);
+ if (pod_param == NULL)
+ return;
+
+ vals = spa_pod_get_values(&pod_param->value, &n_vals, &choice);
+ if (n_vals == 0)
+ return;
+
+ if (spa_pod_get_int(&vals[0], (int32_t*)&dataType) < 0)
+ return;
+
+ pw_log_debug("dataType: %u", dataType);
+ if (dataType & (1u << SPA_DATA_MemPtr)) {
+ SPA_POD_VALUE(struct spa_pod_int, &vals[0]) =
+ dataType | mappable_dataTypes;
+ pw_log_debug("Change dataType: %u -> %u", dataType,
+ SPA_POD_VALUE(struct spa_pod_int, &vals[0]));
+ }
+}
+
+static struct param *add_param(struct stream *impl,
+ uint32_t id, uint32_t flags, const struct spa_pod *param)
+{
+ struct param *p;
+ int idx;
+
+ if (param == NULL || !spa_pod_is_object(param)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ if (id == SPA_ID_INVALID)
+ id = SPA_POD_OBJECT_ID(param);
+
+ p = malloc(sizeof(struct param) + SPA_POD_SIZE(param));
+ if (p == NULL)
+ return NULL;
+
+ if (id == SPA_PARAM_Buffers &&
+ SPA_FLAG_IS_SET(impl->flags, PW_STREAM_FLAG_MAP_BUFFERS) &&
+ impl->direction == SPA_DIRECTION_INPUT)
+ fix_datatype(param);
+
+ p->id = id;
+ p->flags = flags;
+ p->param = SPA_PTROFF(p, sizeof(struct param), struct spa_pod);
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ SPA_POD_OBJECT_ID(p->param) = id;
+
+ spa_list_append(&impl->param_list, &p->link);
+
+ if ((idx = get_param_index(id)) != -1) {
+ impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ impl->params[idx].flags |= SPA_PARAM_INFO_READ;
+ impl->params[idx].user++;
+ }
+ if ((idx = get_port_param_index(id)) != -1) {
+ impl->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ impl->port_params[idx].flags |= SPA_PARAM_INFO_READ;
+ impl->port_params[idx].user++;
+ }
+ return p;
+}
+
+static void clear_params(struct stream *impl, uint32_t id)
+{
+ struct param *p, *t;
+
+ spa_list_for_each_safe(p, t, &impl->param_list, link) {
+ if (id == SPA_ID_INVALID ||
+ (p->id == id && !(p->flags & PARAM_FLAG_LOCKED))) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+}
+
+static int update_params(struct stream *impl, uint32_t id,
+ const struct spa_pod **params, uint32_t n_params)
+{
+ uint32_t i;
+ int res = 0;
+
+ if (id != SPA_ID_INVALID) {
+ clear_params(impl, id);
+ } else {
+ for (i = 0; i < n_params; i++) {
+ if (params[i] == NULL || !spa_pod_is_object(params[i]))
+ continue;
+ clear_params(impl, SPA_POD_OBJECT_ID(params[i]));
+ }
+ }
+ for (i = 0; i < n_params; i++) {
+ if (add_param(impl, id, 0, params[i]) == NULL) {
+ res = -errno;
+ break;
+ }
+ }
+ return res;
+}
+
+
+static inline int queue_push(struct stream *stream, struct queue *queue, struct buffer *buffer)
+{
+ uint32_t index;
+
+ if (SPA_FLAG_IS_SET(buffer->flags, BUFFER_FLAG_QUEUED) ||
+ buffer->id >= stream->n_buffers)
+ return -EINVAL;
+
+ SPA_FLAG_SET(buffer->flags, BUFFER_FLAG_QUEUED);
+ queue->incount += buffer->this.size;
+
+ spa_ringbuffer_get_write_index(&queue->ring, &index);
+ queue->ids[index & MASK_BUFFERS] = buffer->id;
+ spa_ringbuffer_write_update(&queue->ring, index + 1);
+
+ return 0;
+}
+
+static inline bool queue_is_empty(struct stream *stream, struct queue *queue)
+{
+ uint32_t index;
+ return spa_ringbuffer_get_read_index(&queue->ring, &index) < 1;
+}
+
+static inline struct buffer *queue_pop(struct stream *stream, struct queue *queue)
+{
+ uint32_t index, id;
+ struct buffer *buffer;
+
+ if (spa_ringbuffer_get_read_index(&queue->ring, &index) < 1) {
+ errno = EPIPE;
+ return NULL;
+ }
+
+ id = queue->ids[index & MASK_BUFFERS];
+ spa_ringbuffer_read_update(&queue->ring, index + 1);
+
+ buffer = &stream->buffers[id];
+ queue->outcount += buffer->this.size;
+ SPA_FLAG_CLEAR(buffer->flags, BUFFER_FLAG_QUEUED);
+
+ return buffer;
+}
+static inline void clear_queue(struct stream *stream, struct queue *queue)
+{
+ spa_ringbuffer_init(&queue->ring);
+ queue->incount = queue->outcount;
+}
+
+static bool stream_set_state(struct pw_stream *stream, enum pw_stream_state state, const char *error)
+{
+ enum pw_stream_state old = stream->state;
+ bool res = old != state;
+
+ if (res) {
+ free(stream->error);
+ stream->error = error ? strdup(error) : NULL;
+
+ pw_log_debug("%p: update state from %s -> %s (%s)", stream,
+ pw_stream_state_as_string(old),
+ pw_stream_state_as_string(state), stream->error);
+
+ if (state == PW_STREAM_STATE_ERROR)
+ pw_log_error("%p: error %s", stream, error);
+
+ stream->state = state;
+ pw_stream_emit_state_changed(stream, old, state, error);
+ }
+ return res;
+}
+
+static struct buffer *get_buffer(struct pw_stream *stream, uint32_t id)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ if (id < impl->n_buffers)
+ return &impl->buffers[id];
+
+ errno = EINVAL;
+ return NULL;
+}
+
+static inline uint32_t update_requested(struct stream *impl)
+{
+ uint32_t index, id, res = 0;
+ struct buffer *buffer;
+ struct spa_io_rate_match *r = impl->rate_match;
+
+ if (spa_ringbuffer_get_read_index(&impl->dequeued.ring, &index) < 1)
+ return 0;
+
+ id = impl->dequeued.ids[index & MASK_BUFFERS];
+ buffer = &impl->buffers[id];
+ if (r) {
+ buffer->this.requested = r->size;
+ res = r->size > 0 ? 1 : 0;
+ } else {
+ buffer->this.requested = impl->quantum;
+ res = 1;
+ }
+ pw_log_trace_fp("%p: update buffer:%u size:%"PRIu64, impl, id, buffer->this.requested);
+ return res;
+}
+
+static int
+do_call_process(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *impl = user_data;
+ struct pw_stream *stream = &impl->this;
+ pw_log_trace_fp("%p: do process", stream);
+ pw_stream_emit_process(stream);
+ return 0;
+}
+
+static inline void call_process(struct stream *impl)
+{
+ pw_log_trace_fp("%p: call process rt:%u", impl, impl->process_rt);
+ if (impl->direction == SPA_DIRECTION_OUTPUT && update_requested(impl) <= 0)
+ return;
+ if (impl->process_rt)
+ spa_callbacks_call(&impl->rt_callbacks, struct pw_stream_events, process, 0);
+ else
+ pw_loop_invoke(impl->context->main_loop,
+ do_call_process, 1, NULL, 0, false, impl);
+}
+
+static int
+do_call_drained(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *impl = user_data;
+ struct pw_stream *stream = &impl->this;
+ pw_log_trace_fp("%p: drained", stream);
+ pw_stream_emit_drained(stream);
+ return 0;
+}
+
+static void call_drained(struct stream *impl)
+{
+ pw_loop_invoke(impl->context->main_loop,
+ do_call_drained, 1, NULL, 0, false, impl);
+}
+
+static int
+do_call_trigger_done(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *impl = user_data;
+ struct pw_stream *stream = &impl->this;
+ pw_log_trace_fp("%p: trigger_done", stream);
+ pw_stream_emit_trigger_done(stream);
+ return 0;
+}
+
+static void call_trigger_done(struct stream *impl)
+{
+ pw_loop_invoke(impl->context->main_loop,
+ do_call_trigger_done, 1, NULL, 0, false, impl);
+}
+
+static int
+do_set_position(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *impl = user_data;
+ impl->rt.position = impl->position;
+ return 0;
+}
+
+static int impl_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct stream *impl = object;
+ struct pw_stream *stream = &impl->this;
+
+ pw_log_debug("%p: set io id %d (%s) %p %zd", impl, id,
+ spa_debug_type_find_name(spa_type_io, id), data, size);
+
+ switch(id) {
+ case SPA_IO_Clock:
+ if (data && size >= sizeof(struct spa_io_clock))
+ impl->clock = data;
+ else
+ impl->clock = NULL;
+ break;
+ case SPA_IO_Position:
+ if (data && size >= sizeof(struct spa_io_position))
+ impl->position = data;
+ else
+ impl->position = NULL;
+
+ pw_loop_invoke(impl->context->data_loop,
+ do_set_position, 1, NULL, 0, true, impl);
+ break;
+ default:
+ break;
+ }
+ impl->driving = impl->clock && impl->position && impl->position->clock.id == impl->clock->id;
+ pw_stream_emit_io_changed(stream, id, data, size);
+
+ return 0;
+}
+
+static int enum_params(void *object, bool is_port, int seq, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct stream *d = object;
+ struct spa_result_node_params result;
+ uint8_t buffer[1024];
+ struct spa_pod_dynamic_builder b;
+ uint32_t count = 0;
+ struct param *p;
+ bool found = false;
+
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = 0;
+
+ pw_log_debug("%p: param id %d (%s) start:%d num:%d", d, id,
+ spa_debug_type_find_name(spa_type_param, id),
+ start, num);
+
+ spa_list_for_each(p, &d->param_list, link) {
+ struct spa_pod *param;
+
+ param = p->param;
+ if (param == NULL || p->id != id)
+ continue;
+
+ found = true;
+
+ result.index = result.next++;
+ if (result.index < start)
+ continue;
+
+ spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096);
+ if (spa_pod_filter(&b.b, &result.param, param, filter) == 0) {
+ spa_node_emit_result(&d->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+ count++;
+ }
+ spa_pod_dynamic_builder_clean(&b);
+
+ if (count == num)
+ break;
+ }
+ return found ? 0 : -ENOENT;
+}
+
+static int impl_enum_params(void *object, int seq, uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ return enum_params(object, false, seq, id, start, num, filter);
+}
+
+static int impl_set_param(void *object, uint32_t id, uint32_t flags, const struct spa_pod *param)
+{
+ struct stream *impl = object;
+ struct pw_stream *stream = &impl->this;
+
+ if (id != SPA_PARAM_Props)
+ return -ENOTSUP;
+
+ if (impl->in_set_control == 0)
+ pw_stream_emit_param_changed(stream, id, param);
+
+ return 0;
+}
+
+static inline void copy_position(struct stream *impl, int64_t queued)
+{
+ struct spa_io_position *p = impl->rt.position;
+
+ SEQ_WRITE(impl->seq);
+ if (SPA_LIKELY(p != NULL)) {
+ impl->time.now = p->clock.nsec;
+ impl->time.rate = p->clock.rate;
+ if (SPA_UNLIKELY(impl->clock_id != p->clock.id)) {
+ impl->base_pos = p->clock.position - impl->time.ticks;
+ impl->clock_id = p->clock.id;
+ }
+ impl->time.ticks = p->clock.position - impl->base_pos;
+ impl->time.delay = 0;
+ impl->time.queued = queued;
+ impl->quantum = p->clock.duration;
+ }
+ if (SPA_LIKELY(impl->rate_match != NULL))
+ impl->rate_queued = impl->rate_match->delay;
+ SEQ_WRITE(impl->seq);
+}
+
+static int impl_send_command(void *object, const struct spa_command *command)
+{
+ struct stream *impl = object;
+ struct pw_stream *stream = &impl->this;
+ uint32_t id = SPA_NODE_COMMAND_ID(command);
+
+ pw_log_info("%p: command %s", impl,
+ spa_debug_type_find_name(spa_type_node_command_id, id));
+
+ switch (id) {
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Flush:
+ case SPA_NODE_COMMAND_Pause:
+ pw_loop_invoke(impl->context->main_loop,
+ NULL, 0, NULL, 0, false, impl);
+ if (stream->state == PW_STREAM_STATE_STREAMING) {
+
+ pw_log_debug("%p: pause", stream);
+ stream_set_state(stream, PW_STREAM_STATE_PAUSED, NULL);
+ }
+ break;
+ case SPA_NODE_COMMAND_Start:
+ if (stream->state == PW_STREAM_STATE_PAUSED) {
+ pw_log_debug("%p: start %d", stream, impl->direction);
+
+ if (impl->direction == SPA_DIRECTION_INPUT) {
+ if (impl->io != NULL)
+ impl->io->status = SPA_STATUS_NEED_DATA;
+ }
+ else if (!impl->process_rt && !impl->driving) {
+ copy_position(impl, impl->queued.incount);
+ call_process(impl);
+ }
+
+ stream_set_state(stream, PW_STREAM_STATE_STREAMING, NULL);
+ }
+ break;
+ default:
+ break;
+ }
+ pw_stream_emit_command(stream, command);
+ return 0;
+}
+
+static void emit_node_info(struct stream *d, bool full)
+{
+ uint32_t i;
+ uint64_t old = full ? d->info.change_mask : 0;
+ if (full)
+ d->info.change_mask = d->change_mask_all;
+ if (d->info.change_mask != 0) {
+ if (d->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < d->info.n_params; i++) {
+ if (d->params[i].user > 0) {
+ d->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ d->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_info(&d->hooks, &d->info);
+ }
+ d->info.change_mask = old;
+}
+
+static void emit_port_info(struct stream *d, bool full)
+{
+ uint32_t i;
+ uint64_t old = full ? d->port_info.change_mask : 0;
+ if (full)
+ d->port_info.change_mask = d->port_change_mask_all;
+ if (d->port_info.change_mask != 0) {
+ if (d->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < d->port_info.n_params; i++) {
+ if (d->port_params[i].user > 0) {
+ d->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ d->port_params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_port_info(&d->hooks, d->direction, 0, &d->port_info);
+ }
+ d->port_info.change_mask = old;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct stream *d = object;
+ struct spa_hook_list save;
+
+ spa_hook_list_isolate(&d->hooks, &save, listener, events, data);
+
+ emit_node_info(d, true);
+ emit_port_info(d, true);
+
+ spa_hook_list_join(&d->hooks, &save);
+
+ return 0;
+}
+
+static int impl_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks, void *data)
+{
+ struct stream *d = object;
+
+ d->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_port_set_io(void *object, enum spa_direction direction, uint32_t port_id,
+ uint32_t id, void *data, size_t size)
+{
+ struct stream *impl = object;
+ struct pw_stream *stream = &impl->this;
+
+ pw_log_debug("%p: id:%d (%s) %p %zd", impl, id,
+ spa_debug_type_find_name(spa_type_io, id), data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ if (data && size >= sizeof(struct spa_io_buffers))
+ impl->io = data;
+ else
+ impl->io = NULL;
+ break;
+ case SPA_IO_RateMatch:
+ if (data && size >= sizeof(struct spa_io_rate_match))
+ impl->rate_match = data;
+ else
+ impl->rate_match = NULL;
+ break;
+ }
+ pw_stream_emit_io_changed(stream, id, data, size);
+
+ return 0;
+}
+
+static int impl_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)
+{
+ return enum_params(object, true, seq, id, start, num, filter);
+}
+
+static int map_data(struct stream *impl, struct spa_data *data, int prot)
+{
+ void *ptr;
+ struct pw_map_range range;
+
+ pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize);
+
+ ptr = mmap(NULL, range.size, prot, MAP_SHARED, data->fd, range.offset);
+ if (ptr == MAP_FAILED) {
+ pw_log_error("%p: failed to mmap buffer mem: %m", impl);
+ return -errno;
+ }
+
+ data->data = SPA_PTROFF(ptr, range.start, void);
+ pw_log_debug("%p: fd %"PRIi64" mapped %d %d %p", impl, data->fd,
+ range.offset, range.size, data->data);
+
+ if (impl->allow_mlock && mlock(data->data, data->maxsize) < 0) {
+ if (errno != ENOMEM || !mlock_warned) {
+ pw_log(impl->warn_mlock ? SPA_LOG_LEVEL_WARN : SPA_LOG_LEVEL_DEBUG,
+ "%p: Failed to mlock memory %p %u: %s", impl,
+ data->data, data->maxsize,
+ errno == ENOMEM ?
+ "This is not a problem but for best performance, "
+ "consider increasing RLIMIT_MEMLOCK" : strerror(errno));
+ mlock_warned |= errno == ENOMEM;
+ }
+ }
+ return 0;
+}
+
+static int unmap_data(struct stream *impl, struct spa_data *data)
+{
+ struct pw_map_range range;
+
+ pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize);
+
+ if (munmap(SPA_PTROFF(data->data, -range.start, void), range.size) < 0)
+ pw_log_warn("%p: failed to unmap: %m", impl);
+
+ pw_log_debug("%p: fd %"PRIi64" unmapped", impl, data->fd);
+ return 0;
+}
+
+static void clear_buffers(struct pw_stream *stream)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ uint32_t i, j;
+
+ pw_log_debug("%p: clear buffers %d", stream, impl->n_buffers);
+
+ for (i = 0; i < impl->n_buffers; i++) {
+ struct buffer *b = &impl->buffers[i];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_ADDED))
+ pw_stream_emit_remove_buffer(stream, &b->this);
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) {
+ for (j = 0; j < b->this.buffer->n_datas; j++) {
+ struct spa_data *d = &b->this.buffer->datas[j];
+ pw_log_debug("%p: clear buffer %d mem",
+ stream, b->id);
+ unmap_data(impl, d);
+ }
+ }
+ }
+ impl->n_buffers = 0;
+ if (impl->direction == SPA_DIRECTION_INPUT) {
+ struct buffer *b;
+
+ while ((b = queue_pop(impl, &impl->dequeued))) {
+ if (b->busy)
+ ATOMIC_DEC(b->busy->count);
+ }
+ } else
+ clear_queue(impl, &impl->dequeued);
+ clear_queue(impl, &impl->queued);
+}
+
+static int parse_latency(struct pw_stream *stream, const struct spa_pod *param)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ struct spa_latency_info info;
+ int res;
+
+ if (param == NULL)
+ return 0;
+
+ if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+
+ pw_log_info("stream %p: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, stream,
+ 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 (info.direction == impl->direction)
+ return 0;
+
+ impl->latency = info;
+ return 0;
+}
+
+static int impl_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 stream *impl = object;
+ struct pw_stream *stream = &impl->this;
+ int res;
+
+ pw_log_debug("%p: port:%d.%d id:%d (%s) param:%p disconnecting:%d", impl,
+ direction, port_id, id,
+ spa_debug_type_find_name(spa_type_param, id), param,
+ impl->disconnecting);
+
+ if (impl->disconnecting && param != NULL)
+ return -EIO;
+
+ if (param)
+ pw_log_pod(SPA_LOG_LEVEL_DEBUG, param);
+
+ if ((res = update_params(impl, id, &param, param ? 1 : 0)) < 0)
+ return res;
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ clear_buffers(stream);
+ break;
+ case SPA_PARAM_Latency:
+ parse_latency(stream, param);
+ break;
+ default:
+ break;
+ }
+
+ pw_stream_emit_param_changed(stream, id, param);
+
+ if (stream->state == PW_STREAM_STATE_ERROR)
+ return -EIO;
+
+ emit_node_info(impl, false);
+ emit_port_info(impl, false);
+
+ return 0;
+}
+
+static int impl_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 stream *impl = object;
+ struct pw_stream *stream = &impl->this;
+ uint32_t i, j, impl_flags = impl->flags;
+ int prot, res;
+ int size = 0;
+
+ pw_log_debug("%p: port:%d.%d buffers:%u disconnecting:%d", impl,
+ direction, port_id, n_buffers, impl->disconnecting);
+
+ if (impl->disconnecting && n_buffers > 0)
+ return -EIO;
+
+ prot = PROT_READ | (direction == SPA_DIRECTION_OUTPUT ? PROT_WRITE : 0);
+
+ clear_buffers(stream);
+
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ int buf_size = 0;
+ struct buffer *b = &impl->buffers[i];
+
+ b->flags = 0;
+ b->id = i;
+
+ if (SPA_FLAG_IS_SET(impl_flags, PW_STREAM_FLAG_MAP_BUFFERS)) {
+ for (j = 0; j < buffers[i]->n_datas; j++) {
+ struct spa_data *d = &buffers[i]->datas[j];
+ if ((mappable_dataTypes & (1<<d->type)) > 0) {
+ if ((res = map_data(impl, d, prot)) < 0)
+ return res;
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED);
+ }
+ else if (d->type == SPA_DATA_MemPtr && d->data == NULL) {
+ pw_log_error("%p: invalid buffer mem", stream);
+ return -EINVAL;
+ }
+ buf_size += d->maxsize;
+ }
+
+ if (size > 0 && buf_size != size) {
+ pw_log_error("%p: invalid buffer size %d", stream, buf_size);
+ return -EINVAL;
+ } else
+ size = buf_size;
+ }
+ pw_log_debug("%p: got buffer id:%d datas:%d, mapped size %d", stream, i,
+ buffers[i]->n_datas, size);
+ }
+ impl->n_buffers = n_buffers;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &impl->buffers[i];
+
+ b->this.buffer = buffers[i];
+ b->busy = spa_buffer_find_meta_data(buffers[i], SPA_META_Busy, sizeof(*b->busy));
+
+ if (impl->direction == SPA_DIRECTION_OUTPUT) {
+ pw_log_trace("%p: recycle buffer %d", stream, b->id);
+ queue_push(impl, &impl->dequeued, b);
+ }
+
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_ADDED);
+
+ pw_stream_emit_add_buffer(stream, &b->this);
+ }
+ return 0;
+}
+
+static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct stream *d = object;
+ pw_log_trace("%p: recycle buffer %d", d, buffer_id);
+ if (buffer_id < d->n_buffers)
+ queue_push(d, &d->queued, &d->buffers[buffer_id]);
+ return 0;
+}
+
+static int impl_node_process_input(void *object)
+{
+ struct stream *impl = object;
+ struct pw_stream *stream = &impl->this;
+ struct spa_io_buffers *io = impl->io;
+ struct buffer *b;
+
+ if (io == NULL)
+ return -EIO;
+
+ pw_log_trace_fp("%p: process in status:%d id:%d ticks:%"PRIu64" delay:%"PRIi64,
+ stream, io->status, io->buffer_id, impl->time.ticks, impl->time.delay);
+
+ if (io->status == SPA_STATUS_HAVE_DATA &&
+ (b = get_buffer(stream, io->buffer_id)) != NULL) {
+ /* push new buffer */
+ pw_log_trace_fp("%p: push %d %p", stream, b->id, io);
+ if (queue_push(impl, &impl->dequeued, b) == 0) {
+ copy_position(impl, impl->dequeued.incount);
+ if (b->busy)
+ ATOMIC_INC(b->busy->count);
+ call_process(impl);
+ }
+ }
+ if (io->status != SPA_STATUS_NEED_DATA || io->buffer_id == SPA_ID_INVALID) {
+ /* pop buffer to recycle */
+ if ((b = queue_pop(impl, &impl->queued))) {
+ pw_log_trace_fp("%p: recycle buffer %d", stream, b->id);
+ io->buffer_id = b->id;
+ } else {
+ pw_log_trace_fp("%p: no buffers to recycle", stream);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+ io->status = SPA_STATUS_NEED_DATA;
+ }
+ if (impl->driving && impl->using_trigger)
+ call_trigger_done(impl);
+
+ return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA;
+}
+
+static int impl_node_process_output(void *object)
+{
+ struct stream *impl = object;
+ struct pw_stream *stream = &impl->this;
+ struct spa_io_buffers *io = impl->io;
+ struct buffer *b;
+ int res;
+ bool ask_more;
+
+ if (io == NULL)
+ return -EIO;
+
+again:
+ pw_log_trace_fp("%p: process out status:%d id:%d", stream,
+ io->status, io->buffer_id);
+
+ ask_more = false;
+ if ((res = io->status) != SPA_STATUS_HAVE_DATA) {
+ /* recycle old buffer */
+ if ((b = get_buffer(stream, io->buffer_id)) != NULL) {
+ pw_log_trace_fp("%p: recycle buffer %d", stream, b->id);
+ queue_push(impl, &impl->dequeued, b);
+ }
+
+ /* pop new buffer */
+ if ((b = queue_pop(impl, &impl->queued)) != NULL) {
+ impl->drained = false;
+ io->buffer_id = b->id;
+ res = io->status = SPA_STATUS_HAVE_DATA;
+ pw_log_trace_fp("%p: pop %d %p", stream, b->id, io);
+ /* we have a buffer, if we are not rt and don't follow
+ * any rate matching and there are no more
+ * buffers queued and there is a buffer to dequeue, ask for
+ * more buffers so that we have one in the next round.
+ * If we are using rate matching we need to wait until the
+ * rate matching node (audioconvert) has been scheduled to
+ * update the values. */
+ ask_more = !impl->process_rt && impl->rate_match == NULL &&
+ queue_is_empty(impl, &impl->queued) &&
+ !queue_is_empty(impl, &impl->dequeued);
+ } else if (impl->draining || impl->drained) {
+ impl->draining = true;
+ impl->drained = true;
+ io->buffer_id = SPA_ID_INVALID;
+ res = io->status = SPA_STATUS_DRAINED;
+ pw_log_trace_fp("%p: draining", stream);
+ } else {
+ io->buffer_id = SPA_ID_INVALID;
+ res = io->status = SPA_STATUS_NEED_DATA;
+ pw_log_trace_fp("%p: no more buffers %p", stream, io);
+ ask_more = true;
+ }
+ } else {
+ ask_more = !impl->process_rt &&
+ queue_is_empty(impl, &impl->queued) &&
+ !queue_is_empty(impl, &impl->dequeued);
+ }
+
+ copy_position(impl, impl->queued.outcount);
+
+ if (!impl->draining && !impl->driving) {
+ /* we're not draining, not a driver check if we need to get
+ * more buffers */
+ if (ask_more) {
+ call_process(impl);
+ /* realtime, we can try again now if there is something.
+ * non-realtime, we will have to try in the next round */
+ if (impl->process_rt &&
+ (impl->draining || !queue_is_empty(impl, &impl->queued)))
+ goto again;
+ }
+ }
+
+ pw_log_trace_fp("%p: res %d", stream, res);
+
+ if (impl->driving && impl->using_trigger && res != SPA_STATUS_HAVE_DATA)
+ call_trigger_done(impl);
+
+ return res;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_add_listener,
+ .set_callbacks = impl_set_callbacks,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+ .set_io = impl_set_io,
+ .send_command = impl_send_command,
+ .port_set_io = impl_port_set_io,
+ .port_enum_params = impl_port_enum_params,
+ .port_set_param = impl_port_set_param,
+ .port_use_buffers = impl_port_use_buffers,
+ .port_reuse_buffer = impl_port_reuse_buffer,
+};
+
+static void proxy_removed(void *_data)
+{
+ struct pw_stream *stream = _data;
+ pw_log_debug("%p: removed", stream);
+ spa_hook_remove(&stream->proxy_listener);
+ stream->node_id = SPA_ID_INVALID;
+ stream_set_state(stream, PW_STREAM_STATE_UNCONNECTED, NULL);
+}
+
+static void proxy_destroy(void *_data)
+{
+ struct pw_stream *stream = _data;
+ pw_log_debug("%p: destroy", stream);
+ proxy_removed(_data);
+}
+
+static void proxy_error(void *_data, int seq, int res, const char *message)
+{
+ struct pw_stream *stream = _data;
+ /* 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() */
+ pw_stream_emit_state_changed(stream, stream->state,
+ PW_STREAM_STATE_ERROR, message);
+}
+
+static void proxy_bound(void *data, uint32_t global_id)
+{
+ struct pw_stream *stream = data;
+ stream->node_id = global_id;
+ stream_set_state(stream, PW_STREAM_STATE_PAUSED, NULL);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = proxy_removed,
+ .destroy = proxy_destroy,
+ .error = proxy_error,
+ .bound = proxy_bound,
+};
+
+static struct control *find_control(struct pw_stream *stream, uint32_t id)
+{
+ struct control *c;
+ spa_list_for_each(c, &stream->controls, link) {
+ if (c->id == id)
+ return c;
+ }
+ return NULL;
+}
+
+static int node_event_param(void *object, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ struct spa_pod *param)
+{
+ struct pw_stream *stream = object;
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ struct control *c;
+ const struct spa_pod *type, *pod;
+ uint32_t iid, choice, n_vals, container = SPA_ID_INVALID;
+ float *vals, bool_range[3] = { 1.0f, 0.0f, 1.0f }, dbl[3];
+
+ if (spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_id, SPA_POD_Id(&iid)) < 0)
+ return -EINVAL;
+
+ c = find_control(stream, iid);
+ if (c != NULL)
+ return 0;
+
+ c = calloc(1, sizeof(*c) + SPA_POD_SIZE(param));
+ c->info = SPA_PTROFF(c, sizeof(*c), struct spa_pod);
+ memcpy(c->info, param, SPA_POD_SIZE(param));
+ c->control.n_values = 0;
+ c->control.max_values = 0;
+ c->control.values = c->values;
+
+ if (spa_pod_parse_object(c->info,
+ SPA_TYPE_OBJECT_PropInfo, NULL,
+ SPA_PROP_INFO_description, SPA_POD_OPT_String(&c->control.name),
+ SPA_PROP_INFO_type, SPA_POD_PodChoice(&type),
+ SPA_PROP_INFO_container, SPA_POD_OPT_Id(&container)) < 0) {
+ free(c);
+ return -EINVAL;
+ }
+
+ pod = spa_pod_get_values(type, &n_vals, &choice);
+
+ c->type = SPA_POD_TYPE(pod);
+ if (spa_pod_is_float(pod))
+ vals = SPA_POD_BODY(pod);
+ else if (spa_pod_is_double(pod)) {
+ double *v = SPA_POD_BODY(pod);
+ dbl[0] = v[0];
+ if (n_vals > 1)
+ dbl[1] = v[1];
+ if (n_vals > 2)
+ dbl[2] = v[2];
+ vals = dbl;
+ }
+ else if (spa_pod_is_bool(pod) && n_vals > 0) {
+ choice = SPA_CHOICE_Range;
+ vals = bool_range;
+ vals[0] = SPA_POD_VALUE(struct spa_pod_bool, pod);
+ n_vals = 3;
+ }
+ else {
+ free(c);
+ return -ENOTSUP;
+ }
+
+ c->container = container != SPA_ID_INVALID ? container : c->type;
+
+ switch (choice) {
+ case SPA_CHOICE_None:
+ if (n_vals < 1) {
+ free(c);
+ return -EINVAL;
+ }
+ c->control.n_values = 1;
+ c->control.max_values = 1;
+ c->control.values[0] = c->control.def = c->control.min = c->control.max = vals[0];
+ break;
+ case SPA_CHOICE_Range:
+ if (n_vals < 3) {
+ free(c);
+ return -EINVAL;
+ }
+ c->control.n_values = 1;
+ c->control.max_values = 1;
+ c->control.values[0] = vals[0];
+ c->control.def = vals[0];
+ c->control.min = vals[1];
+ c->control.max = vals[2];
+ break;
+ default:
+ free(c);
+ return -ENOTSUP;
+ }
+
+ c->id = iid;
+ spa_list_append(&stream->controls, &c->link);
+ pw_log_debug("%p: add control %d (%s) container:%d (def:%f min:%f max:%f)",
+ stream, c->id, c->control.name, c->container,
+ c->control.def, c->control.min, c->control.max);
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) param;
+ float value_f;
+ double value_d;
+ bool value_b;
+ float *values;
+ uint32_t i, n_values;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ struct control *c;
+
+ c = find_control(stream, prop->key);
+ if (c == NULL)
+ continue;
+
+ switch (c->container) {
+ case SPA_TYPE_Float:
+ if (spa_pod_get_float(&prop->value, &value_f) < 0)
+ continue;
+ n_values = 1;
+ values = &value_f;
+ break;
+ case SPA_TYPE_Double:
+ if (spa_pod_get_double(&prop->value, &value_d) < 0)
+ continue;
+ n_values = 1;
+ value_f = value_d;
+ values = &value_f;
+ break;
+ case SPA_TYPE_Bool:
+ if (spa_pod_get_bool(&prop->value, &value_b) < 0)
+ continue;
+ value_f = value_b ? 1.0f : 0.0f;
+ n_values = 1;
+ values = &value_f;
+ break;
+ case SPA_TYPE_Array:
+ if ((values = spa_pod_get_array(&prop->value, &n_values)) == NULL ||
+ !spa_pod_is_float(SPA_POD_ARRAY_CHILD(&prop->value)))
+ continue;
+ break;
+ default:
+ continue;
+ }
+
+ if (c->emitted && c->control.n_values == n_values &&
+ memcmp(c->control.values, values, sizeof(float) * n_values) == 0)
+ continue;
+
+ memcpy(c->control.values, values, sizeof(float) * n_values);
+ c->control.n_values = n_values;
+ c->emitted = true;
+
+ pw_log_debug("%p: control %d (%s) changed %d:", stream,
+ prop->key, c->control.name, n_values);
+ for (i = 0; i < n_values; i++)
+ pw_log_debug("%p: value %d %f", stream, i, values[i]);
+
+ pw_stream_emit_control_info(stream, prop->key, &c->control);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ return 0;
+}
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct pw_stream *stream = data;
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ uint32_t i;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ switch (info->params[i].id) {
+ case SPA_PARAM_PropInfo:
+ case SPA_PARAM_Props:
+ pw_impl_node_for_each_param(impl->node,
+ 0, info->params[i].id,
+ 0, UINT32_MAX,
+ NULL,
+ node_event_param,
+ stream);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
+
+static const struct pw_impl_node_events node_events = {
+ PW_VERSION_IMPL_NODE_EVENTS,
+ .info_changed = node_event_info,
+};
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct pw_stream *stream = data;
+
+ pw_log_debug("%p: error id:%u seq:%d res:%d (%s): %s", stream,
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE) {
+ stream_set_state(stream, PW_STREAM_STATE_UNCONNECTED, message);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+};
+
+static void context_drained(void *data, struct pw_impl_node *node)
+{
+ struct stream *impl = data;
+ if (impl->node != node)
+ return;
+ if (impl->draining && impl->drained) {
+ impl->draining = false;
+ if (impl->io != NULL)
+ impl->io->status = SPA_STATUS_NEED_DATA;
+ call_drained(impl);
+ }
+}
+
+static const struct pw_context_driver_events context_events = {
+ PW_VERSION_CONTEXT_DRIVER_EVENTS,
+ .drained = context_drained,
+};
+
+struct match {
+ struct pw_stream *stream;
+ int count;
+};
+#define MATCH_INIT(s) ((struct match){ .stream = (s) })
+
+static int execute_match(void *data, const char *location, const char *action,
+ const char *val, size_t len)
+{
+ struct match *match = data;
+ struct pw_stream *this = match->stream;
+ if (spa_streq(action, "update-props"))
+ match->count += pw_properties_update_string(this->properties, val, len);
+ return 1;
+}
+
+static struct stream *
+stream_new(struct pw_context *context, const char *name,
+ struct pw_properties *props, const struct pw_properties *extra)
+{
+ struct stream *impl;
+ struct pw_stream *this;
+ const char *str;
+ struct match match;
+ int res;
+
+ impl = calloc(1, sizeof(struct stream));
+ if (impl == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+ impl->port_props = pw_properties_new(NULL, NULL);
+ if (impl->port_props == NULL) {
+ res = -errno;
+ goto error_properties;
+ }
+
+ this = &impl->this;
+ pw_log_debug("%p: new \"%s\"", impl, name);
+
+ if (props == NULL) {
+ props = pw_properties_new(PW_KEY_MEDIA_NAME, name, NULL);
+ } else if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL) {
+ pw_properties_set(props, PW_KEY_MEDIA_NAME, name);
+ }
+ if (props == NULL) {
+ res = -errno;
+ goto error_properties;
+ }
+ spa_hook_list_init(&impl->hooks);
+ this->properties = props;
+
+ pw_context_conf_update_props(context, "stream.properties", props);
+
+ match = MATCH_INIT(this);
+ pw_context_conf_section_match_rules(context, "stream.rules",
+ &this->properties->dict, execute_match, &match);
+
+ if ((str = getenv("PIPEWIRE_PROPS")) != NULL)
+ pw_properties_update_string(props, str, strlen(str));
+ if ((str = getenv("PIPEWIRE_QUANTUM")) != NULL) {
+ struct spa_fraction q;
+ if (sscanf(str, "%u/%u", &q.num, &q.denom) == 2 && q.denom != 0) {
+ pw_properties_setf(props, PW_KEY_NODE_RATE,
+ "1/%u", q.denom);
+ pw_properties_setf(props, PW_KEY_NODE_LATENCY,
+ "%u/%u", q.num, q.denom);
+ }
+ }
+ if ((str = getenv("PIPEWIRE_LATENCY")) != NULL)
+ pw_properties_set(props, PW_KEY_NODE_LATENCY, str);
+ if ((str = getenv("PIPEWIRE_RATE")) != NULL)
+ pw_properties_set(props, PW_KEY_NODE_RATE, str);
+
+ if (pw_properties_get(props, PW_KEY_STREAM_IS_LIVE) == NULL)
+ pw_properties_set(props, PW_KEY_STREAM_IS_LIVE, "true");
+ if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL && extra) {
+ str = pw_properties_get(extra, PW_KEY_APP_NAME);
+ if (str == NULL)
+ str = pw_properties_get(extra, PW_KEY_APP_PROCESS_BINARY);
+ if (str == NULL)
+ str = name;
+ pw_properties_set(props, PW_KEY_NODE_NAME, str);
+ }
+
+ this->name = name ? strdup(name) : NULL;
+ this->node_id = SPA_ID_INVALID;
+
+ spa_ringbuffer_init(&impl->dequeued.ring);
+ spa_ringbuffer_init(&impl->queued.ring);
+ spa_list_init(&impl->param_list);
+
+ spa_hook_list_init(&this->listener_list);
+ spa_list_init(&this->controls);
+
+ this->state = PW_STREAM_STATE_UNCONNECTED;
+
+ impl->context = context;
+ impl->allow_mlock = context->settings.mem_allow_mlock;
+ impl->warn_mlock = context->settings.mem_warn_mlock;
+
+ spa_hook_list_append(&impl->context->driver_listener_list,
+ &impl->context_listener,
+ &context_events, impl);
+ return impl;
+
+error_properties:
+ pw_properties_free(impl->port_props);
+ free(impl);
+error_cleanup:
+ pw_properties_free(props);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+struct pw_stream * pw_stream_new(struct pw_core *core, const char *name,
+ struct pw_properties *props)
+{
+ struct stream *impl;
+ struct pw_stream *this;
+ struct pw_context *context = core->context;
+
+ impl = stream_new(context, name, props, core->properties);
+ if (impl == NULL)
+ return NULL;
+
+ this = &impl->this;
+ this->core = core;
+ spa_list_append(&core->stream_list, &this->link);
+ pw_core_add_listener(core,
+ &this->core_listener, &core_events, this);
+
+ return this;
+}
+
+SPA_EXPORT
+struct pw_stream *
+pw_stream_new_simple(struct pw_loop *loop,
+ const char *name,
+ struct pw_properties *props,
+ const struct pw_stream_events *events,
+ void *data)
+{
+ struct pw_stream *this;
+ struct stream *impl;
+ struct pw_context *context;
+ int res;
+
+ if (props == NULL)
+ props = pw_properties_new(NULL, NULL);
+ if (props == NULL)
+ return NULL;
+
+ context = pw_context_new(loop, NULL, 0);
+ if (context == NULL) {
+ res = -errno;
+ goto error_cleanup;
+ }
+
+ impl = stream_new(context, name, props, NULL);
+ if (impl == NULL) {
+ res = -errno;
+ props = NULL;
+ goto error_cleanup;
+ }
+
+ this = &impl->this;
+ impl->data.context = context;
+ pw_stream_add_listener(this, &impl->data.stream_listener, events, data);
+
+ return this;
+
+error_cleanup:
+ if (context)
+ pw_context_destroy(context);
+ pw_properties_free(props);
+ errno = -res;
+ return NULL;
+}
+
+SPA_EXPORT
+const char *pw_stream_state_as_string(enum pw_stream_state state)
+{
+ switch (state) {
+ case PW_STREAM_STATE_ERROR:
+ return "error";
+ case PW_STREAM_STATE_UNCONNECTED:
+ return "unconnected";
+ case PW_STREAM_STATE_CONNECTING:
+ return "connecting";
+ case PW_STREAM_STATE_PAUSED:
+ return "paused";
+ case PW_STREAM_STATE_STREAMING:
+ return "streaming";
+ }
+ return "invalid-state";
+}
+
+SPA_EXPORT
+void pw_stream_destroy(struct pw_stream *stream)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ struct control *c;
+
+ pw_log_debug("%p: destroy", stream);
+
+ pw_stream_emit_destroy(stream);
+
+ if (!impl->disconnecting)
+ pw_stream_disconnect(stream);
+
+ if (stream->core) {
+ spa_hook_remove(&stream->core_listener);
+ spa_list_remove(&stream->link);
+ stream->core = NULL;
+ }
+
+ clear_params(impl, SPA_ID_INVALID);
+
+ pw_log_debug("%p: free", stream);
+ free(stream->error);
+
+ pw_properties_free(stream->properties);
+
+ free(stream->name);
+
+ spa_list_consume(c, &stream->controls, link) {
+ spa_list_remove(&c->link);
+ free(c);
+ }
+
+ spa_hook_list_clean(&impl->hooks);
+ spa_hook_list_clean(&stream->listener_list);
+
+ spa_hook_remove(&impl->context_listener);
+
+ if (impl->data.context)
+ pw_context_destroy(impl->data.context);
+
+ pw_properties_free(impl->port_props);
+ free(impl);
+}
+
+static void hook_removed(struct spa_hook *hook)
+{
+ struct stream *impl = hook->priv;
+ spa_zero(impl->rt_callbacks);
+ hook->priv = NULL;
+ hook->removed = NULL;
+}
+
+SPA_EXPORT
+void pw_stream_add_listener(struct pw_stream *stream,
+ struct spa_hook *listener,
+ const struct pw_stream_events *events,
+ void *data)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ spa_hook_list_append(&stream->listener_list, listener, events, data);
+
+ if (events->process && impl->rt_callbacks.funcs == NULL) {
+ impl->rt_callbacks = SPA_CALLBACKS_INIT(events, data);
+ listener->removed = hook_removed;
+ listener->priv = impl;
+ }
+}
+
+SPA_EXPORT
+enum pw_stream_state pw_stream_get_state(struct pw_stream *stream, const char **error)
+{
+ if (error)
+ *error = stream->error;
+ return stream->state;
+}
+
+SPA_EXPORT
+const char *pw_stream_get_name(struct pw_stream *stream)
+{
+ return stream->name;
+}
+
+SPA_EXPORT
+const struct pw_properties *pw_stream_get_properties(struct pw_stream *stream)
+{
+ return stream->properties;
+}
+
+SPA_EXPORT
+int pw_stream_update_properties(struct pw_stream *stream, const struct spa_dict *dict)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ int changed, res = 0;
+ struct match match;
+
+ changed = pw_properties_update(stream->properties, dict);
+ if (!changed)
+ return 0;
+
+ match = MATCH_INIT(stream);
+ pw_context_conf_section_match_rules(impl->context, "stream.rules",
+ &stream->properties->dict, execute_match, &match);
+
+ if (impl->node)
+ res = pw_impl_node_update_properties(impl->node,
+ match.count == 0 ?
+ dict :
+ &stream->properties->dict);
+
+ return res;
+}
+
+SPA_EXPORT
+struct pw_core *pw_stream_get_core(struct pw_stream *stream)
+{
+ return stream->core;
+}
+
+static void add_params(struct stream *impl)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ add_param(impl, SPA_PARAM_IO, PARAM_FLAG_LOCKED,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, SPA_PARAM_IO,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))));
+
+ add_param(impl, SPA_PARAM_Meta, PARAM_FLAG_LOCKED,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Busy),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_busy))));
+}
+
+static int find_format(struct stream *impl, enum pw_direction direction,
+ uint32_t *media_type, uint32_t *media_subtype)
+{
+ uint32_t state = 0;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ int res;
+ struct spa_pod *format;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ if (spa_node_port_enum_params_sync(&impl->impl_node,
+ impl->direction, 0,
+ SPA_PARAM_EnumFormat, &state,
+ NULL, &format, &b) != 1) {
+ pw_log_warn("%p: no format given", impl);
+ return 0;
+ }
+
+ if ((res = spa_format_parse(format, media_type, media_subtype)) < 0)
+ return res;
+
+ pw_log_debug("%p: %s/%s", impl,
+ spa_debug_type_find_name(spa_type_media_type, *media_type),
+ spa_debug_type_find_name(spa_type_media_subtype, *media_subtype));
+ return 0;
+}
+
+static const char *get_media_class(struct stream *impl)
+{
+ switch (impl->media_type) {
+ case SPA_MEDIA_TYPE_audio:
+ return "Audio";
+ case SPA_MEDIA_TYPE_video:
+ return "Video";
+ case SPA_MEDIA_TYPE_application:
+ switch(impl->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_control:
+ return "Midi";
+ }
+ return "Data";
+ case SPA_MEDIA_TYPE_stream:
+ switch(impl->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_midi:
+ return "Midi";
+ }
+ return "Data";
+ default:
+ return "Unknown";
+ }
+}
+
+SPA_EXPORT
+int
+pw_stream_connect(struct pw_stream *stream,
+ enum pw_direction direction,
+ uint32_t target_id,
+ enum pw_stream_flags flags,
+ const struct spa_pod **params,
+ uint32_t n_params)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ struct pw_impl_factory *factory;
+ struct pw_properties *props = NULL;
+ const char *str;
+ uint32_t i;
+ int res;
+
+ pw_log_debug("%p: connect target:%d", stream, target_id);
+ impl->direction =
+ direction == PW_DIRECTION_INPUT ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT;
+ impl->flags = flags;
+ impl->node_methods = impl_node;
+
+ if (impl->direction == SPA_DIRECTION_INPUT)
+ impl->node_methods.process = impl_node_process_input;
+ else
+ impl->node_methods.process = impl_node_process_output;
+
+ impl->process_rt = SPA_FLAG_IS_SET(flags, PW_STREAM_FLAG_RT_PROCESS);
+
+ impl->impl_node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl->node_methods, impl);
+
+ impl->change_mask_all =
+ SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+
+ impl->info = SPA_NODE_INFO_INIT();
+ if (impl->direction == SPA_DIRECTION_INPUT) {
+ impl->info.max_input_ports = 1;
+ impl->info.max_output_ports = 0;
+ } else {
+ impl->info.max_input_ports = 0;
+ impl->info.max_output_ports = 1;
+ }
+ /* we're always RT safe, if the stream was marked RT_PROCESS,
+ * the callback must be RT safe */
+ impl->info.flags = SPA_NODE_FLAG_RT;
+ /* if the callback was not marked RT_PROCESS, we will offload
+ * the process callback in the main thread and we are ASYNC */
+ if (!impl->process_rt)
+ impl->info.flags |= SPA_NODE_FLAG_ASYNC;
+ impl->info.props = &stream->properties->dict;
+ impl->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, 0);
+ 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->info.params = impl->params;
+ impl->info.n_params = N_NODE_PARAMS;
+ impl->info.change_mask = impl->change_mask_all;
+
+ impl->port_change_mask_all =
+ SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+
+ impl->port_info = SPA_PORT_INFO_INIT();
+ impl->port_info.change_mask = impl->port_change_mask_all;
+ impl->port_info.flags = 0;
+ if (SPA_FLAG_IS_SET(flags, PW_STREAM_FLAG_ALLOC_BUFFERS))
+ impl->port_info.flags |= SPA_PORT_FLAG_CAN_ALLOC_BUFFERS;
+ impl->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, 0);
+ impl->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, 0);
+ impl->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, 0);
+ impl->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ impl->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ impl->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_WRITE);
+ impl->port_info.props = &impl->port_props->dict;
+ impl->port_info.params = impl->port_params;
+ impl->port_info.n_params = N_PORT_PARAMS;
+
+ clear_params(impl, SPA_ID_INVALID);
+ for (i = 0; i < n_params; i++)
+ add_param(impl, SPA_ID_INVALID, 0, params[i]);
+
+ add_params(impl);
+
+ if ((res = find_format(impl, direction, &impl->media_type, &impl->media_subtype)) < 0)
+ return res;
+
+ impl->disconnecting = false;
+ stream_set_state(stream, PW_STREAM_STATE_CONNECTING, NULL);
+
+ if ((str = getenv("PIPEWIRE_NODE")) != NULL)
+ pw_properties_set(stream->properties, PW_KEY_TARGET_OBJECT, str);
+ else if (target_id != PW_ID_ANY)
+ /* XXX this is deprecated but still used by the portal and its apps */
+ pw_properties_setf(stream->properties, PW_KEY_NODE_TARGET, "%d", target_id);
+
+ if ((flags & PW_STREAM_FLAG_AUTOCONNECT) &&
+ pw_properties_get(stream->properties, PW_KEY_NODE_AUTOCONNECT) == NULL) {
+ str = getenv("PIPEWIRE_AUTOCONNECT");
+ pw_properties_set(stream->properties, PW_KEY_NODE_AUTOCONNECT, str ? str : "true");
+ }
+ if (flags & PW_STREAM_FLAG_DRIVER)
+ pw_properties_set(stream->properties, PW_KEY_NODE_DRIVER, "true");
+ if ((pw_properties_get(stream->properties, PW_KEY_NODE_WANT_DRIVER) == NULL))
+ pw_properties_set(stream->properties, PW_KEY_NODE_WANT_DRIVER, "true");
+
+ if (flags & PW_STREAM_FLAG_EXCLUSIVE)
+ pw_properties_set(stream->properties, PW_KEY_NODE_EXCLUSIVE, "true");
+ if (flags & PW_STREAM_FLAG_DONT_RECONNECT)
+ pw_properties_set(stream->properties, PW_KEY_NODE_DONT_RECONNECT, "true");
+ if (flags & PW_STREAM_FLAG_TRIGGER) {
+ pw_properties_set(stream->properties, PW_KEY_NODE_TRIGGER, "true");
+ impl->trigger = true;
+ }
+
+ if ((str = pw_properties_get(stream->properties, "mem.warn-mlock")) != NULL)
+ impl->warn_mlock = pw_properties_parse_bool(str);
+
+ if ((pw_properties_get(stream->properties, PW_KEY_MEDIA_CLASS) == NULL)) {
+ const char *media_type = pw_properties_get(stream->properties, PW_KEY_MEDIA_TYPE);
+ pw_properties_setf(stream->properties, PW_KEY_MEDIA_CLASS, "Stream/%s/%s",
+ direction == PW_DIRECTION_INPUT ? "Input" : "Output",
+ media_type ? media_type : get_media_class(impl));
+ }
+
+ if ((str = pw_properties_get(stream->properties, PW_KEY_FORMAT_DSP)) != NULL)
+ 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");
+
+ impl->port_info.props = &impl->port_props->dict;
+
+ if (stream->core == NULL) {
+ stream->core = pw_context_connect(impl->context,
+ pw_properties_copy(stream->properties), 0);
+ if (stream->core == NULL) {
+ res = -errno;
+ goto error_connect;
+ }
+ spa_list_append(&stream->core->stream_list, &stream->link);
+ pw_core_add_listener(stream->core,
+ &stream->core_listener, &core_events, stream);
+ impl->disconnect_core = true;
+ }
+
+ pw_log_debug("%p: creating node", stream);
+ props = pw_properties_copy(stream->properties);
+ if (props == NULL) {
+ res = -errno;
+ goto error_node;
+ }
+
+ if ((str = pw_properties_get(props, PW_KEY_STREAM_MONITOR)) &&
+ pw_properties_parse_bool(str)) {
+ pw_properties_set(props, "resample.peaks", "true");
+ pw_properties_set(props, "channelmix.normalize", "true");
+ }
+
+ if (impl->media_type == SPA_MEDIA_TYPE_audio) {
+ factory = pw_context_find_factory(impl->context, "adapter");
+ if (factory == NULL) {
+ pw_log_error("%p: no adapter factory found", stream);
+ res = -ENOENT;
+ goto error_node;
+ }
+ pw_properties_setf(props, "adapt.follower.spa-node", "pointer:%p",
+ &impl->impl_node);
+ pw_properties_set(props, "object.register", "false");
+ impl->node = pw_impl_factory_create_object(factory,
+ NULL,
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ props,
+ 0);
+ props = NULL;
+ if (impl->node == NULL) {
+ res = -errno;
+ goto error_node;
+ }
+ } else {
+ impl->node = pw_context_create_node(impl->context, props, 0);
+ props = NULL;
+ if (impl->node == NULL) {
+ res = -errno;
+ goto error_node;
+ }
+ pw_impl_node_set_implementation(impl->node, &impl->impl_node);
+ }
+ pw_impl_node_set_active(impl->node,
+ !SPA_FLAG_IS_SET(impl->flags, PW_STREAM_FLAG_INACTIVE));
+
+ pw_log_debug("%p: export node %p", stream, impl->node);
+ stream->proxy = pw_core_export(stream->core,
+ PW_TYPE_INTERFACE_Node, NULL, impl->node, 0);
+ if (stream->proxy == NULL) {
+ res = -errno;
+ goto error_proxy;
+ }
+
+ pw_proxy_add_listener(stream->proxy, &stream->proxy_listener, &proxy_events, stream);
+
+ pw_impl_node_add_listener(impl->node, &stream->node_listener, &node_events, stream);
+
+ return 0;
+
+error_connect:
+ pw_log_error("%p: can't connect: %s", stream, spa_strerror(res));
+ goto exit_cleanup;
+error_node:
+ pw_log_error("%p: can't make node: %s", stream, spa_strerror(res));
+ goto exit_cleanup;
+error_proxy:
+ pw_log_error("%p: can't make proxy: %s", stream, spa_strerror(res));
+ goto exit_cleanup;
+
+exit_cleanup:
+ pw_properties_free(props);
+ return res;
+}
+
+SPA_EXPORT
+uint32_t pw_stream_get_node_id(struct pw_stream *stream)
+{
+ return stream->node_id;
+}
+
+SPA_EXPORT
+int pw_stream_disconnect(struct pw_stream *stream)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+
+ pw_log_debug("%p: disconnect", stream);
+
+ if (impl->disconnecting)
+ return 0;
+
+ impl->disconnecting = true;
+
+ if (impl->node)
+ pw_impl_node_set_active(impl->node, false);
+
+ if (stream->proxy) {
+ pw_proxy_destroy(stream->proxy);
+ stream->proxy = NULL;
+ }
+
+ if (impl->node) {
+ pw_impl_node_destroy(impl->node);
+ impl->node = NULL;
+ }
+ if (impl->disconnect_core) {
+ impl->disconnect_core = false;
+ spa_hook_remove(&stream->core_listener);
+ spa_list_remove(&stream->link);
+ pw_core_disconnect(stream->core);
+ stream->core = NULL;
+ }
+ return 0;
+}
+
+SPA_EXPORT
+int pw_stream_set_error(struct pw_stream *stream,
+ int res, const char *error, ...)
+{
+ if (res < 0) {
+ va_list args;
+ char *value;
+ int r;
+
+ va_start(args, error);
+ r = vasprintf(&value, error, args);
+ va_end(args);
+ if (r < 0)
+ return -errno;
+
+ if (stream->proxy)
+ pw_proxy_error(stream->proxy, res, value);
+ stream_set_state(stream, PW_STREAM_STATE_ERROR, value);
+
+ free(value);
+ }
+ return res;
+}
+
+SPA_EXPORT
+int pw_stream_update_params(struct pw_stream *stream,
+ const struct spa_pod **params,
+ uint32_t n_params)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ int res;
+
+ pw_log_debug("%p: update params", stream);
+ if ((res = update_params(impl, SPA_ID_INVALID, params, n_params)) < 0)
+ return res;
+
+ emit_node_info(impl, false);
+ emit_port_info(impl, false);
+
+ return res;
+}
+
+SPA_EXPORT
+int pw_stream_set_control(struct pw_stream *stream, uint32_t id, uint32_t n_values, float *values, ...)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ va_list varargs;
+ char buf[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+ struct spa_pod_frame f[1];
+ struct spa_pod *pod;
+ struct control *c;
+
+ if (impl->node == NULL)
+ return -EIO;
+
+ va_start(varargs, values);
+
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+ while (1) {
+ pw_log_debug("%p: set control %d %d %f", stream, id, n_values, values[0]);
+
+ if ((c = find_control(stream, id))) {
+ spa_pod_builder_prop(&b, id, 0);
+ switch (c->container) {
+ case SPA_TYPE_Float:
+ spa_pod_builder_float(&b, values[0]);
+ break;
+ case SPA_TYPE_Double:
+ spa_pod_builder_double(&b, values[0]);
+ break;
+ case SPA_TYPE_Bool:
+ spa_pod_builder_bool(&b, values[0] < 0.5 ? false : true);
+ break;
+ case SPA_TYPE_Array:
+ spa_pod_builder_array(&b,
+ sizeof(float), SPA_TYPE_Float,
+ n_values, values);
+ break;
+ default:
+ spa_pod_builder_none(&b);
+ break;
+ }
+ } else {
+ pw_log_warn("%p: unknown control with id %d", stream, id);
+ }
+ if ((id = va_arg(varargs, uint32_t)) == 0)
+ break;
+ n_values = va_arg(varargs, uint32_t);
+ values = va_arg(varargs, float *);
+ }
+ pod = spa_pod_builder_pop(&b, &f[0]);
+
+ va_end(varargs);
+
+ impl->in_set_control++;
+ pw_impl_node_set_param(impl->node, SPA_PARAM_Props, 0, pod);
+ impl->in_set_control--;
+
+ return 0;
+}
+
+SPA_EXPORT
+const struct pw_stream_control *pw_stream_get_control(struct pw_stream *stream, uint32_t id)
+{
+ struct control *c;
+
+ if (id == 0)
+ return NULL;
+
+ if ((c = find_control(stream, id)))
+ return &c->control;
+
+ return NULL;
+}
+
+SPA_EXPORT
+int pw_stream_set_active(struct pw_stream *stream, bool active)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ pw_log_debug("%p: active:%d", stream, active);
+ if (impl->node)
+ pw_impl_node_set_active(impl->node, active);
+
+ if (!active || impl->drained)
+ impl->drained = impl->draining = false;
+ return 0;
+}
+
+struct old_time {
+ int64_t now;
+ struct spa_fraction rate;
+ uint64_t ticks;
+ int64_t delay;
+ uint64_t queued;
+};
+
+SPA_EXPORT
+int pw_stream_get_time(struct pw_stream *stream, struct pw_time *time)
+{
+ return pw_stream_get_time_n(stream, time, sizeof(struct old_time));
+}
+
+SPA_EXPORT
+int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ uintptr_t seq1, seq2;
+ uint32_t buffered, quantum, index;
+
+ do {
+ seq1 = SEQ_READ(impl->seq);
+ memcpy(time, &impl->time, SPA_MIN(size, sizeof(struct pw_time)));
+ buffered = impl->rate_queued;
+ quantum = impl->quantum;
+ seq2 = SEQ_READ(impl->seq);
+ } while (!SEQ_READ_SUCCESS(seq1, seq2));
+
+ if (impl->direction == SPA_DIRECTION_INPUT)
+ time->queued = (int64_t)(time->queued - impl->dequeued.outcount);
+ else
+ time->queued = (int64_t)(impl->queued.incount - time->queued);
+
+ time->delay += ((impl->latency.min_quantum + impl->latency.max_quantum) / 2) * 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;
+
+ if (size >= offsetof(struct pw_time, queued_buffers))
+ time->buffered = buffered;
+ if (size >= offsetof(struct pw_time, avail_buffers))
+ time->queued_buffers = spa_ringbuffer_get_read_index(&impl->queued.ring, &index);
+ if (size >= sizeof(struct pw_time))
+ time->avail_buffers = spa_ringbuffer_get_read_index(&impl->dequeued.ring, &index);
+
+ pw_log_trace_fp("%p: %"PRIi64" %"PRIi64" %"PRIu64" %d/%d %"PRIu64" %"
+ PRIu64" %"PRIu64" %"PRIu64" %"PRIu64, stream,
+ time->now, time->delay, time->ticks,
+ time->rate.num, time->rate.denom, time->queued,
+ impl->dequeued.outcount, impl->dequeued.incount,
+ impl->queued.outcount, impl->queued.incount);
+ return 0;
+}
+
+static int
+do_trigger_deprecated(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *impl = user_data;
+ int res = impl->node_methods.process(impl);
+ return spa_node_call_ready(&impl->callbacks, res);
+}
+
+SPA_EXPORT
+struct pw_buffer *pw_stream_dequeue_buffer(struct pw_stream *stream)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ struct buffer *b;
+ int res;
+
+ if ((b = queue_pop(impl, &impl->dequeued)) == NULL) {
+ res = -errno;
+ pw_log_trace_fp("%p: no more buffers: %m", stream);
+ errno = -res;
+ return NULL;
+ }
+ pw_log_trace_fp("%p: dequeue buffer %d size:%"PRIu64, stream, b->id, b->this.size);
+
+ if (b->busy && impl->direction == SPA_DIRECTION_OUTPUT) {
+ if (ATOMIC_INC(b->busy->count) > 1) {
+ ATOMIC_DEC(b->busy->count);
+ queue_push(impl, &impl->dequeued, b);
+ pw_log_trace_fp("%p: buffer busy", stream);
+ errno = EBUSY;
+ return NULL;
+ }
+ }
+ return &b->this;
+}
+
+SPA_EXPORT
+int pw_stream_queue_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);
+ int res;
+
+ if (b->busy)
+ ATOMIC_DEC(b->busy->count);
+
+ pw_log_trace_fp("%p: queue buffer %d", stream, b->id);
+ if ((res = queue_push(impl, &impl->queued, b)) < 0)
+ return res;
+
+ if (impl->direction == SPA_DIRECTION_OUTPUT &&
+ impl->driving && !impl->using_trigger) {
+ pw_log_debug("deprecated: use pw_stream_trigger_process() to drive the stream.");
+ res = pw_loop_invoke(impl->context->data_loop,
+ do_trigger_deprecated, 1, NULL, 0, false, impl);
+ }
+ return res;
+}
+
+static int
+do_flush(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *impl = user_data;
+ struct buffer *b;
+
+ pw_log_trace_fp("%p: flush", impl);
+ do {
+ b = queue_pop(impl, &impl->queued);
+ if (b != NULL)
+ queue_push(impl, &impl->dequeued, b);
+ }
+ while (b);
+
+ impl->queued.outcount = impl->dequeued.incount =
+ impl->dequeued.outcount = impl->queued.incount = 0;
+
+ return 0;
+}
+static int
+do_drain(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *impl = user_data;
+ pw_log_trace_fp("%p", impl);
+ impl->draining = true;
+ impl->drained = false;
+ return 0;
+}
+
+SPA_EXPORT
+int pw_stream_flush(struct pw_stream *stream, bool drain)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ pw_loop_invoke(impl->context->data_loop,
+ drain ? do_drain : do_flush, 1, NULL, 0, true, impl);
+ if (!drain && impl->node != NULL)
+ spa_node_send_command(impl->node->node,
+ &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Flush));
+ return 0;
+}
+
+SPA_EXPORT
+bool pw_stream_is_driving(struct pw_stream *stream)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ return impl->driving;
+}
+
+static int
+do_trigger_process(struct spa_loop *loop,
+ bool async, uint32_t seq, const void *data, size_t size, void *user_data)
+{
+ struct stream *impl = user_data;
+ int res;
+ if (impl->direction == SPA_DIRECTION_OUTPUT) {
+ if (impl->process_rt)
+ call_process(impl);
+ res = impl->node_methods.process(impl);
+ } else {
+ res = SPA_STATUS_NEED_DATA;
+ }
+ return spa_node_call_ready(&impl->callbacks, res);
+}
+
+static int trigger_request_process(struct stream *impl)
+{
+ 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));
+ return 0;
+}
+
+SPA_EXPORT
+int pw_stream_trigger_process(struct pw_stream *stream)
+{
+ struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this);
+ int res = 0;
+
+ pw_log_trace_fp("%p", impl);
+
+ /* flag to check for old or new behaviour */
+ impl->using_trigger = true;
+
+ if (!impl->driving && !impl->trigger) {
+ res = trigger_request_process(impl);
+ } else {
+ if (!impl->process_rt)
+ call_process(impl);
+
+ res = pw_loop_invoke(impl->context->data_loop,
+ do_trigger_process, 1, NULL, 0, false, impl);
+ }
+ return res;
+}
diff --git a/src/pipewire/stream.h b/src/pipewire/stream.h
new file mode 100644
index 0000000..bf08c44
--- /dev/null
+++ b/src/pipewire/stream.h
@@ -0,0 +1,529 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_STREAM_H
+#define PIPEWIRE_STREAM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \page page_streams Streams
+ *
+ * \section sec_overview Overview
+ *
+ * \ref pw_stream "Streams" are used to exchange data with the
+ * PipeWire server. A stream is a wrapper around a proxy for a pw_client_node
+ * with an adapter. This means the stream will automatically do conversion
+ * to the type required by the server.
+ *
+ * Streams can be used to:
+ *
+ * \li Consume a stream from PipeWire. This is a PW_DIRECTION_INPUT stream.
+ * \li Produce a stream to PipeWire. This is a PW_DIRECTION_OUTPUT stream
+ *
+ * You can connect the stream port to a specific server port or let PipeWire
+ * choose a port for you.
+ *
+ * For more complicated nodes such as filters or ports with multiple
+ * inputs and/or outputs you will need to use the pw_filter or make
+ * a pw_node yourself and export it with \ref pw_core_export.
+ *
+ * Streams can also be used to:
+ *
+ * \li Implement a Sink in PipeWire. This is a PW_DIRECTION_INPUT stream.
+ * \li Implement a Source in PipeWire. This is a PW_DIRECTION_OUTPUT stream
+ *
+ * In this case, the PW_KEY_MEDIA_CLASS property needs to be set to
+ * "Audio/Sink" or "Audio/Source" respectively.
+ *
+ * \section sec_create Create
+ *
+ * Make a new stream with \ref pw_stream_new(). You will need to specify
+ * a name for the stream and extra properties. The basic set of properties
+ * each stream must provide is filled in automatically.
+ *
+ * Once the stream is created, the state_changed event should be used to
+ * track the state of the stream.
+ *
+ * \section sec_connect Connect
+ *
+ * The stream is initially unconnected. To connect the stream, use
+ * \ref pw_stream_connect(). Pass the desired direction as an argument.
+ *
+ * The direction is:
+
+ * \li PW_DIRECTION_INPUT for a stream that *consumes* data. This can be a
+ * stream that captures from a Source or a when the stream is used to
+ * implement a Sink.
+ *
+ * \li PW_DIRECTION_OUTPUT for a stream that *produces* data. This can be a
+ * stream that plays to a Sink or when the stream is used to implement
+ * a Source.
+ *
+ * \subsection ssec_stream_target Stream target
+ *
+ * To make the newly connected stream automatically connect to an existing
+ * PipeWire node, use the \ref PW_STREAM_FLAG_AUTOCONNECT and set the
+ * PW_KEY_OBJECT_SERIAL or the PW_KEY_NODE_NAME value of the target node
+ * in the PW_KEY_TARGET_OBJECT property before connecting.
+ *
+ * \subsection ssec_stream_formats Stream formats
+ *
+ * An array of possible formats that this stream can consume or provide
+ * must be specified.
+ *
+ * \section sec_format Format negotiation
+ *
+ * After connecting the stream, the server will want to configure some
+ * parameters on the stream. You will be notified of these changes
+ * with the param_changed event.
+ *
+ * When a format param change is emitted, the client should now prepare
+ * itself to deal with the format and complete the negotiation procedure
+ * with a call to \ref pw_stream_update_params().
+ *
+ * As arguments to \ref pw_stream_update_params() an array of spa_param
+ * structures must be given. They contain parameters such as buffer size,
+ * number of buffers, required metadata and other parameters for the
+ * media buffers.
+ *
+ * \section sec_buffers Buffer negotiation
+ *
+ * After completing the format negotiation, PipeWire will allocate and
+ * notify the stream of the buffers that will be used to exchange data
+ * between client and server.
+ *
+ * With the add_buffer event, a stream will be notified of a new buffer
+ * that can be used for data transport. You can attach user_data to these
+ * buffers. The buffers can only be used with the stream that emitted
+ * the add_buffer event.
+ *
+ * After the buffers are negotiated, the stream will transition to the
+ * \ref PW_STREAM_STATE_PAUSED state.
+ *
+ * \section sec_streaming Streaming
+ *
+ * From the \ref PW_STREAM_STATE_PAUSED state, the stream can be set to
+ * the \ref PW_STREAM_STATE_STREAMING state by the PipeWire server when
+ * data transport is started.
+ *
+ * Depending on how the stream was connected it will need to Produce or
+ * Consume data for/from PipeWire as explained in the following
+ * subsections.
+ *
+ * \subsection ssec_consume Consume data
+ *
+ * The process event is emitted for each new buffer that can be
+ * consumed.
+ *
+ * \ref pw_stream_dequeue_buffer() should be used to get the data and
+ * metadata of the buffer.
+ *
+ * The buffer is owned by the stream and stays alive until the
+ * remove_buffer event is emitted or the stream is destroyed.
+ *
+ * When the buffer has been processed, call \ref pw_stream_queue_buffer()
+ * to let PipeWire reuse the buffer.
+ *
+ * \subsection ssec_produce Produce data
+ *
+ * \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
+ * remove_buffer event is emitted or the stream is destroyed.
+ *
+ * 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.
+ *
+ * \section sec_stream_disconnect Disconnect
+ *
+ * Use \ref pw_stream_disconnect() to disconnect a stream after use.
+ *
+ * \section sec_stream_configuration Configuration
+ *
+ * \subsection ssec_config_properties Stream Properties
+ *
+ * \subsection ssec_config_rules Stream Rules
+ *
+ * \section sec_stream_environment Environment Variables
+ *
+ */
+/** \defgroup pw_stream Stream
+ *
+ * \brief PipeWire stream objects
+ *
+ * The stream object provides a convenient way to send and
+ * receive data streams from/to PipeWire.
+ *
+ * See also \ref page_streams and \ref api_pw_core
+ */
+
+/**
+ * \addtogroup pw_stream
+ * \{
+ */
+struct pw_stream;
+
+#include <spa/buffer/buffer.h>
+#include <spa/param/param.h>
+#include <spa/pod/command.h>
+
+/** \enum pw_stream_state The state of a stream */
+enum pw_stream_state {
+ PW_STREAM_STATE_ERROR = -1, /**< the stream is in error */
+ PW_STREAM_STATE_UNCONNECTED = 0, /**< unconnected */
+ PW_STREAM_STATE_CONNECTING = 1, /**< connection is in progress */
+ PW_STREAM_STATE_PAUSED = 2, /**< paused */
+ PW_STREAM_STATE_STREAMING = 3 /**< streaming */
+};
+
+/** a buffer structure obtained from pw_stream_dequeue_buffer(). The size of this
+ * structure can grow as more field are added in the future */
+struct pw_buffer {
+ struct spa_buffer *buffer; /**< the spa buffer */
+ void *user_data; /**< user data attached to the buffer */
+ uint64_t size; /**< This field is set by the user and the sum of
+ * all queued buffer is returned in the time info.
+ * For audio, it is advised to use the number of
+ * samples in the buffer for this field. */
+ uint64_t requested; /**< For playback streams, this field contains the
+ * suggested amount of data to provide. For audio
+ * streams this will be the amount of samples
+ * required by the resampler. This field is 0
+ * when no suggestion is provided. Since 0.3.49 */
+};
+
+struct pw_stream_control {
+ const char *name; /**< name of the control */
+ uint32_t flags; /**< extra flags (unused) */
+ float def; /**< default value */
+ float min; /**< min value */
+ float max; /**< max value */
+ float *values; /**< array of values */
+ uint32_t n_values; /**< number of values in array */
+ uint32_t max_values; /**< max values that can be set on this control */
+};
+
+/** A time structure.
+ *
+ * Use pw_stream_get_time_n() to get an updated time snapshot of the stream.
+ * The time snapshot can give information about the time in the driver of the
+ * graph, the delay to the edge of the graph and the internal queuing in the
+ * stream.
+ *
+ * pw_time.ticks gives a monotonic increasing counter of the time in the graph
+ * driver. I can be used to generate a timetime to schedule samples as well
+ * as detect discontinuities in the timeline caused by xruns.
+ *
+ * pw_time.delay is expressed as pw_time.rate, the time domain of the graph. This
+ * value, and pw_time.ticks, were captured at pw_time.now and can be extrapolated
+ * to the current time like this:
+ *
+ * struct timespec ts;
+ * clock_gettime(CLOCK_MONOTONIC, &ts);
+ * int64_t diff = SPA_TIMESPEC_TO_NSEC(&ts) - pw_time.now;
+ * int64_t elapsed = (pw_time.rate.denom * diff) / (pw_time.rate.num * SPA_NSEC_PER_SEC);
+ *
+ * pw_time.delay contains the total delay that a signal will travel through the
+ * graph. This includes the delay caused by filters in the graph as well as delays
+ * caused by the hardware. The delay is usually quite stable and should only change when
+ * the topology, quantum or samplerate of the graph changes.
+ *
+ * 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.
+ *
+ * pw_time.queued is the sum of all the pw_buffer.size fields of the buffers that are
+ * currently queued in the stream but not yet processed. The application can choose
+ * the units of this value, for example, time, samples or bytes (below expressed
+ * as app.rate).
+ *
+ * pw_time.buffered is format dependent, for audio/raw it contains the number of samples
+ * that are buffered inside the resampler/converter.
+ *
+ * The total delay of data in a stream is the sum of the queued and buffered data
+ * (not yet processed data) and the delay to the edge of the graph, usually a
+ * playback or capture device.
+ *
+ * For an audio playback stream, if you were to queue a buffer, the total delay
+ * in milliseconds for the first sample in the newly queued buffer to be played
+ * by the hardware can be calculated as:
+ *
+ * (pw_time.buffered * 1000 / stream.samplerate) +
+ * (pw_time.queued * 1000 / app.rate) +
+ * ((pw_time.delay - elapsed) * 1000 * pw_time.rate.num / pw_time.rate.denom)
+ *
+ * The current extrapolated time (in ms) in the source or sink can be calculated as:
+ *
+ * (pw_time.ticks + elapsed) * 1000 * pw_time.rate.num / pw_time.rate.denom
+ *
+ *
+ * stream time domain graph time domain
+ * /-----------------------\/-----------------------------\
+ *
+ * queue +-+ +-+ +-----------+ +--------+
+ * ----> | | | |->| converter | -> graph -> | kernel | -> speaker
+ * <---- +-+ +-+ +-----------+ +--------+
+ * dequeue buffers \-------------------/\--------/
+ * graph internal
+ * latency latency
+ * \--------/\-------------/\-----------------------------/
+ * queued buffered delay
+ */
+struct pw_time {
+ int64_t now; /**< the monotonic time in nanoseconds. This is the time
+ * when this time report was updated. It is usually
+ * updated every graph cycle. You can use the current
+ * monotonic time to calculate the elapsed time between
+ * this report and the current state and calculate
+ * updated ticks and delay values. */
+ struct spa_fraction rate; /**< the rate of \a ticks and delay. This is usually
+ * expressed in 1/<samplerate>. */
+ uint64_t ticks; /**< the ticks at \a now. This is the current time that
+ * the remote end is reading/writing. This is monotonicaly
+ * increasing. */
+ int64_t delay; /**< delay to device. This is the time it will take for
+ * the next output sample of the stream to be presented by
+ * the playback device or the time a sample traveled
+ * from the capture device. This delay includes the
+ * delay introduced by all filters on the path between
+ * the stream and the device. The delay is normally
+ * constant in a graph and can change when the topology
+ * of the graph or the quantum changes. This delay does
+ * not include the delay caused by queued buffers. */
+ uint64_t queued; /**< data queued in the stream, this is the sum
+ * of the size fields in the pw_buffer that are
+ * currently queued */
+ uint64_t buffered; /**< for audio/raw streams, this contains the extra
+ * number of samples buffered in the resampler.
+ * Since 0.3.50. */
+ uint32_t queued_buffers; /**< The number of buffers that are queued. Since 0.3.50 */
+ uint32_t avail_buffers; /**< The number of buffers that can be dequeued. Since 0.3.50 */
+};
+
+#include <pipewire/port.h>
+
+/** Events for a stream. These events are always called from the mainloop
+ * unless explicitly documented otherwise. */
+struct pw_stream_events {
+#define PW_VERSION_STREAM_EVENTS 2
+ uint32_t version;
+
+ void (*destroy) (void *data);
+ /** when the stream state changes */
+ void (*state_changed) (void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error);
+
+ /** Notify information about a control. */
+ void (*control_info) (void *data, uint32_t id, const struct pw_stream_control *control);
+
+ /** when io changed on the stream. */
+ void (*io_changed) (void *data, uint32_t id, void *area, uint32_t size);
+ /** when a parameter changed */
+ void (*param_changed) (void *data, uint32_t id, const struct spa_pod *param);
+
+ /** when a new buffer was created for this stream */
+ void (*add_buffer) (void *data, struct pw_buffer *buffer);
+ /** when a buffer was destroyed for this stream */
+ void (*remove_buffer) (void *data, struct pw_buffer *buffer);
+
+ /** when a buffer can be queued (for playback streams) or
+ * dequeued (for capture streams). 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. */
+ void (*process) (void *data);
+
+ /** The stream is drained */
+ void (*drained) (void *data);
+
+ /** A command notify, Since 0.3.39:1 */
+ void (*command) (void *data, const struct spa_command *command);
+
+ /** a trigger_process completed. Since version 0.3.40:2 */
+ void (*trigger_done) (void *data);
+};
+
+/** Convert a stream state to a readable string */
+const char * pw_stream_state_as_string(enum pw_stream_state state);
+
+/** \enum pw_stream_flags Extra flags that can be used in \ref pw_stream_connect() */
+enum pw_stream_flags {
+ PW_STREAM_FLAG_NONE = 0, /**< no flags */
+ PW_STREAM_FLAG_AUTOCONNECT = (1 << 0), /**< try to automatically connect
+ * this stream */
+ PW_STREAM_FLAG_INACTIVE = (1 << 1), /**< start the stream inactive,
+ * pw_stream_set_active() needs to be
+ * called explicitly */
+ PW_STREAM_FLAG_MAP_BUFFERS = (1 << 2), /**< mmap the buffers except DmaBuf */
+ PW_STREAM_FLAG_DRIVER = (1 << 3), /**< be a driver */
+ PW_STREAM_FLAG_RT_PROCESS = (1 << 4), /**< call process from the realtime
+ * thread. You MUST use RT safe functions
+ * in the process callback. */
+ PW_STREAM_FLAG_NO_CONVERT = (1 << 5), /**< don't convert format */
+ PW_STREAM_FLAG_EXCLUSIVE = (1 << 6), /**< require exclusive access to the
+ * device */
+ PW_STREAM_FLAG_DONT_RECONNECT = (1 << 7), /**< don't try to reconnect this stream
+ * when the sink/source is removed */
+ PW_STREAM_FLAG_ALLOC_BUFFERS = (1 << 8), /**< the application will allocate buffer
+ * memory. In the add_buffer event, the
+ * data of the buffer should be set */
+ PW_STREAM_FLAG_TRIGGER = (1 << 9), /**< the output stream will not be scheduled
+ * automatically but _trigger_process()
+ * needs to be called. This can be used
+ * when the output of the stream depends
+ * on input from other streams. */
+};
+
+/** Create a new unconneced \ref pw_stream
+ * \return a newly allocated \ref pw_stream */
+struct pw_stream *
+pw_stream_new(struct pw_core *core, /**< a \ref pw_core */
+ const char *name, /**< a stream media name */
+ struct pw_properties *props /**< stream properties, ownership is taken */);
+
+struct pw_stream *
+pw_stream_new_simple(struct pw_loop *loop, /**< a \ref pw_loop to use */
+ const char *name, /**< a stream media name */
+ struct pw_properties *props,/**< stream properties, ownership is taken */
+ const struct pw_stream_events *events, /**< stream events */
+ void *data /**< data passed to events */);
+
+/** Destroy a stream */
+void pw_stream_destroy(struct pw_stream *stream);
+
+void pw_stream_add_listener(struct pw_stream *stream,
+ struct spa_hook *listener,
+ const struct pw_stream_events *events,
+ void *data);
+
+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);
+
+struct pw_core *pw_stream_get_core(struct pw_stream *stream);
+
+const struct pw_properties *pw_stream_get_properties(struct pw_stream *stream);
+
+int pw_stream_update_properties(struct pw_stream *stream, const struct spa_dict *dict);
+
+/** Connect a stream for input or output on \a port_path.
+ * \return 0 on success < 0 on error.
+ *
+ * You should connect to the process event and use pw_stream_dequeue_buffer()
+ * to get the latest metadata and data. */
+int
+pw_stream_connect(struct pw_stream *stream, /**< a \ref pw_stream */
+ enum pw_direction direction, /**< the stream direction */
+ uint32_t target_id, /**< should have the value PW_ID_ANY.
+ * To select a specific target
+ * node, specify the
+ * PW_KEY_OBJECT_SERIAL or the
+ * PW_KEY_NODE_NAME value of the target
+ * node in the PW_KEY_TARGET_OBJECT
+ * property of the stream.
+ * Specifying target nodes by
+ * their id is deprecated.
+ */
+ enum pw_stream_flags flags, /**< stream flags */
+ const struct spa_pod **params, /**< an array with params. The params
+ * should ideally contain supported
+ * formats. */
+ uint32_t n_params /**< number of items in \a params */);
+
+/** Get the node ID of the stream.
+ * \return node ID. */
+uint32_t
+pw_stream_get_node_id(struct pw_stream *stream);
+
+/** Disconnect \a stream */
+int pw_stream_disconnect(struct pw_stream *stream);
+
+/** Set the stream in error state */
+int pw_stream_set_error(struct pw_stream *stream, /**< a \ref pw_stream */
+ int res, /**< a result code */
+ const char *error, /**< an error message */
+ ...) SPA_PRINTF_FUNC(3, 4);
+
+/** Complete the negotiation process with result code \a res
+ *
+ * This function should be called after notification of the format.
+
+ * When \a res indicates success, \a params contain the parameters for the
+ * allocation state. */
+int
+pw_stream_update_params(struct pw_stream *stream, /**< a \ref pw_stream */
+ const struct spa_pod **params, /**< an array of params. The params should
+ * ideally contain parameters for doing
+ * buffer allocation. */
+ uint32_t n_params /**< number of elements in \a params */);
+
+/** Get control values */
+const struct pw_stream_control *pw_stream_get_control(struct pw_stream *stream, uint32_t id);
+
+/** 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 */
+int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size);
+
+/** 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. */
+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. */
+struct pw_buffer *pw_stream_dequeue_buffer(struct pw_stream *stream);
+
+/** Submit a buffer for playback or recycle a buffer for capture. */
+int pw_stream_queue_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);
+
+/** Flush a stream. When \a drain is true, the drained callback will
+ * be called when all data is played or recorded */
+int pw_stream_flush(struct pw_stream *stream, bool drain);
+
+/** Check if the stream is driving. The stream needs to have the
+ * PW_STREAM_FLAG_DRIVER set. When the stream is driving,
+ * pw_stream_trigger_process() needs to be called when data is
+ * available (output) or needed (input). Since 0.3.34 */
+bool pw_stream_is_driving(struct pw_stream *stream);
+
+/** Trigger a push/pull on the stream. One iteration of the graph will
+ * scheduled and process() will be called. Since 0.3.34 */
+int pw_stream_trigger_process(struct pw_stream *stream);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_STREAM_H */
diff --git a/src/pipewire/thread-loop.c b/src/pipewire/thread-loop.c
new file mode 100644
index 0000000..2fdd50e
--- /dev/null
+++ b/src/pipewire/thread-loop.c
@@ -0,0 +1,469 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pthread.h>
+#include <errno.h>
+#include <sys/time.h>
+
+#include <spa/support/thread.h>
+#include <spa/utils/result.h>
+
+#include "log.h"
+#include "thread.h"
+#include "thread-loop.h"
+
+PW_LOG_TOPIC_EXTERN(log_thread_loop);
+#define PW_LOG_TOPIC_DEFAULT log_thread_loop
+
+
+#define pw_thread_loop_events_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_thread_loop_events, m, v, ##__VA_ARGS__)
+#define pw_thread_loop_events_destroy(o) pw_thread_loop_events_emit(o, destroy, 0)
+
+/** \cond */
+struct pw_thread_loop {
+ struct pw_loop *loop;
+ char name[16];
+
+ struct spa_hook_list listener_list;
+
+ pthread_mutex_t lock;
+ pthread_cond_t cond;
+ pthread_cond_t accept_cond;
+
+ pthread_t thread;
+
+ struct spa_hook hook;
+
+ struct spa_source *event;
+
+ int n_waiting;
+ int n_waiting_for_accept;
+ unsigned int created:1;
+ unsigned int running:1;
+};
+/** \endcond */
+
+static void before(void *data)
+{
+ struct pw_thread_loop *this = data;
+ pthread_mutex_unlock(&this->lock);
+}
+
+static void after(void *data)
+{
+ struct pw_thread_loop *this = data;
+ pthread_mutex_lock(&this->lock);
+}
+
+static const struct spa_loop_control_hooks impl_hooks = {
+ SPA_VERSION_LOOP_CONTROL_HOOKS,
+ before,
+ after,
+};
+
+static void do_stop(void *data, uint64_t count)
+{
+ struct pw_thread_loop *this = data;
+ pw_log_debug("stopping");
+ this->running = false;
+}
+
+#define CHECK(expression,label) \
+do { \
+ if ((errno = (expression)) != 0) { \
+ res = -errno; \
+ pw_log_error(#expression ": %s", strerror(errno)); \
+ goto label; \
+ } \
+} while(false);
+
+static struct pw_thread_loop *loop_new(struct pw_loop *loop,
+ const char *name,
+ const struct spa_dict *props)
+{
+ struct pw_thread_loop *this;
+ pthread_mutexattr_t attr;
+ pthread_condattr_t cattr;
+ int res;
+
+ this = calloc(1, sizeof(struct pw_thread_loop));
+ if (this == NULL)
+ return NULL;
+
+ pw_log_debug("%p: new name:%s", this, name);
+
+ if (loop == NULL) {
+ loop = pw_loop_new(props);
+ this->created = true;
+ }
+ if (loop == NULL) {
+ res = -errno;
+ goto clean_this;
+ }
+ this->loop = loop;
+ snprintf(this->name, sizeof(this->name), "%s", name ? name : "pw-thread-loop");
+
+ spa_hook_list_init(&this->listener_list);
+
+ CHECK(pthread_mutexattr_init(&attr), clean_this);
+ CHECK(pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE), clean_this);
+ CHECK(pthread_mutex_init(&this->lock, &attr), clean_this);
+
+ CHECK(pthread_condattr_init(&cattr), clean_lock);
+ CHECK(pthread_condattr_setclock(&cattr, CLOCK_REALTIME), clean_lock);
+
+ CHECK(pthread_cond_init(&this->cond, &cattr), clean_lock);
+ CHECK(pthread_cond_init(&this->accept_cond, &cattr), clean_cond);
+
+ if ((this->event = pw_loop_add_event(this->loop, do_stop, this)) == NULL) {
+ res = -errno;
+ goto clean_acceptcond;
+ }
+
+ pw_loop_add_hook(loop, &this->hook, &impl_hooks, this);
+
+ return this;
+
+clean_acceptcond:
+ pthread_cond_destroy(&this->accept_cond);
+clean_cond:
+ pthread_cond_destroy(&this->cond);
+clean_lock:
+ pthread_mutex_destroy(&this->lock);
+clean_this:
+ if (this->created && this->loop)
+ pw_loop_destroy(this->loop);
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+/** Create a new \ref pw_thread_loop
+ *
+ * \param name the name of the thread or NULL
+ * \param props a dict of properties for the thread loop
+ * \return a newly allocated \ref pw_thread_loop
+ *
+ * Make a new \ref pw_thread_loop that will run in
+ * a thread with \a name.
+ *
+ * After this function you should probably call pw_thread_loop_start() to
+ * actually start the thread
+ *
+ */
+SPA_EXPORT
+struct pw_thread_loop *pw_thread_loop_new(const char *name,
+ const struct spa_dict *props)
+{
+ return loop_new(NULL, name, props);
+}
+
+/** Create a new \ref pw_thread_loop
+ *
+ * \param loop the loop to wrap
+ * \param name the name of the thread or NULL
+ * \param props a dict of properties for the thread loop
+ * \return a newly allocated \ref pw_thread_loop
+ *
+ * Make a new \ref pw_thread_loop that will run \a loop in
+ * a thread with \a name.
+ *
+ * After this function you should probably call pw_thread_loop_start() to
+ * actually start the thread
+ *
+ */
+SPA_EXPORT
+struct pw_thread_loop *pw_thread_loop_new_full(struct pw_loop *loop,
+ const char *name, const struct spa_dict *props)
+{
+ return loop_new(loop, name, props);
+}
+
+/** Destroy a threaded loop */
+SPA_EXPORT
+void pw_thread_loop_destroy(struct pw_thread_loop *loop)
+{
+ pw_thread_loop_events_destroy(loop);
+
+ pw_thread_loop_stop(loop);
+
+ spa_hook_remove(&loop->hook);
+
+ spa_hook_list_clean(&loop->listener_list);
+
+ pw_loop_destroy_source(loop->loop, loop->event);
+
+ if (loop->created)
+ pw_loop_destroy(loop->loop);
+
+ pthread_cond_destroy(&loop->accept_cond);
+ pthread_cond_destroy(&loop->cond);
+ pthread_mutex_destroy(&loop->lock);
+
+ free(loop);
+}
+
+SPA_EXPORT
+void pw_thread_loop_add_listener(struct pw_thread_loop *loop,
+ struct spa_hook *listener,
+ const struct pw_thread_loop_events *events,
+ void *data)
+{
+ spa_hook_list_append(&loop->listener_list, listener, events, data);
+}
+
+SPA_EXPORT
+struct pw_loop *
+pw_thread_loop_get_loop(struct pw_thread_loop *loop)
+{
+ return loop->loop;
+}
+
+static void *do_loop(void *user_data)
+{
+ struct pw_thread_loop *this = user_data;
+ int res;
+
+ pthread_mutex_lock(&this->lock);
+ pw_log_debug("%p: enter thread", this);
+ pw_loop_enter(this->loop);
+
+ while (this->running) {
+ if ((res = pw_loop_iterate(this->loop, -1)) < 0) {
+ if (res == -EINTR)
+ continue;
+ pw_log_warn("%p: iterate error %d (%s)",
+ this, res, spa_strerror(res));
+ }
+ }
+ pw_log_debug("%p: leave thread", this);
+ pw_loop_leave(this->loop);
+ pthread_mutex_unlock(&this->lock);
+
+ return NULL;
+}
+
+/** Start the thread to handle \a loop
+ *
+ * \param loop a \ref pw_thread_loop
+ * \return 0 on success
+ *
+ */
+SPA_EXPORT
+int pw_thread_loop_start(struct pw_thread_loop *loop)
+{
+ int err;
+
+ if (!loop->running) {
+ struct spa_thread *thr;
+ struct spa_dict_item items[1];
+
+ loop->running = true;
+
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_THREAD_NAME, loop->name);
+ thr = pw_thread_utils_create(&SPA_DICT_INIT_ARRAY(items), do_loop, loop);
+ if (thr == NULL)
+ goto error;
+
+ loop->thread = (pthread_t)thr;
+ }
+ return 0;
+
+error:
+ err = errno;
+ pw_log_warn("%p: can't create thread: %s", loop,
+ strerror(err));
+ loop->running = false;
+ return -err;
+}
+
+/** Quit the loop and stop its thread
+ *
+ * \param loop a \ref pw_thread_loop
+ *
+ */
+SPA_EXPORT
+void pw_thread_loop_stop(struct pw_thread_loop *loop)
+{
+ pw_log_debug("%p stopping %d", loop, loop->running);
+ if (loop->running) {
+ pw_log_debug("%p signal", loop);
+ pw_loop_signal_event(loop->loop, loop->event);
+ pw_log_debug("%p join", loop);
+ pthread_join(loop->thread, NULL);
+ pw_log_debug("%p joined", loop);
+ loop->running = false;
+ }
+ pw_log_debug("%p stopped", loop);
+}
+
+/** Lock the mutex associated with \a loop
+ *
+ * \param loop a \ref pw_thread_loop
+ *
+ */
+SPA_EXPORT
+void pw_thread_loop_lock(struct pw_thread_loop *loop)
+{
+ pthread_mutex_lock(&loop->lock);
+ pw_log_trace("%p", loop);
+}
+
+/** Unlock the mutex associated with \a loop
+ *
+ * \param loop a \ref pw_thread_loop
+ *
+ */
+SPA_EXPORT
+void pw_thread_loop_unlock(struct pw_thread_loop *loop)
+{
+ pw_log_trace("%p", loop);
+ pthread_mutex_unlock(&loop->lock);
+}
+
+/** Signal the thread
+ *
+ * \param loop a \ref pw_thread_loop to signal
+ * \param wait_for_accept if we need to wait for accept
+ *
+ * Signal the thread of \a loop. If \a wait_for_accept is true,
+ * this function waits until \ref pw_thread_loop_accept() is called.
+ *
+ */
+SPA_EXPORT
+void pw_thread_loop_signal(struct pw_thread_loop *loop, bool wait_for_accept)
+{
+ pw_log_trace("%p, waiting:%d accept:%d",
+ loop, loop->n_waiting, wait_for_accept);
+ if (loop->n_waiting > 0)
+ pthread_cond_broadcast(&loop->cond);
+
+ if (wait_for_accept) {
+ loop->n_waiting_for_accept++;
+
+ while (loop->n_waiting_for_accept > 0)
+ pthread_cond_wait(&loop->accept_cond, &loop->lock);
+ }
+}
+
+/** Wait for the loop thread to call \ref pw_thread_loop_signal()
+ *
+ * \param loop a \ref pw_thread_loop to signal
+ *
+ */
+SPA_EXPORT
+void pw_thread_loop_wait(struct pw_thread_loop *loop)
+{
+ pw_log_trace("%p, waiting %d", loop, loop->n_waiting);
+ loop->n_waiting++;
+ pthread_cond_wait(&loop->cond, &loop->lock);
+ loop->n_waiting--;
+ pw_log_trace("%p, waiting done %d", loop, loop->n_waiting);
+}
+
+/** Wait for the loop thread to call \ref pw_thread_loop_signal()
+ * or time out.
+ *
+ * \param loop a \ref pw_thread_loop to signal
+ * \param wait_max_sec the maximum number of seconds to wait for a \ref pw_thread_loop_signal()
+ * \return 0 on success or ETIMEDOUT on timeout or a negative errno value.
+ *
+ */
+SPA_EXPORT
+int pw_thread_loop_timed_wait(struct pw_thread_loop *loop, int wait_max_sec)
+{
+ struct timespec timeout;
+ int ret = 0;
+ if ((ret = pw_thread_loop_get_time(loop,
+ &timeout, wait_max_sec * SPA_NSEC_PER_SEC)) < 0)
+ return ret;
+ ret = pw_thread_loop_timed_wait_full(loop, &timeout);
+ return ret == -ETIMEDOUT ? ETIMEDOUT : ret;
+}
+
+/** Get the current time of the loop + timeout. This can be used in
+ * pw_thread_loop_timed_wait_full().
+ *
+ * \param loop a \ref pw_thread_loop
+ * \param abstime the result struct timesspec
+ * \param timeout the time in nanoseconds to add to \a tp
+ * \return 0 on success or a negative errno value on error.
+ *
+ */
+SPA_EXPORT
+int pw_thread_loop_get_time(struct pw_thread_loop *loop, struct timespec *abstime, int64_t timeout)
+{
+ if (clock_gettime(CLOCK_REALTIME, abstime) < 0)
+ return -errno;
+
+ abstime->tv_sec += timeout / SPA_NSEC_PER_SEC;
+ abstime->tv_nsec += timeout % SPA_NSEC_PER_SEC;
+ if (abstime->tv_nsec >= SPA_NSEC_PER_SEC) {
+ abstime->tv_sec++;
+ abstime->tv_nsec -= SPA_NSEC_PER_SEC;
+ }
+ return 0;
+}
+
+/** Wait for the loop thread to call \ref pw_thread_loop_signal()
+ * or time out.
+ *
+ * \param loop a \ref pw_thread_loop to signal
+ * \param abstime the absolute time to wait for a \ref pw_thread_loop_signal()
+ * \return 0 on success or -ETIMEDOUT on timeout or a negative error value
+ *
+ */
+SPA_EXPORT
+int pw_thread_loop_timed_wait_full(struct pw_thread_loop *loop, struct timespec *abstime)
+{
+ int ret;
+ loop->n_waiting++;
+ ret = pthread_cond_timedwait(&loop->cond, &loop->lock, abstime);
+ loop->n_waiting--;
+ return -ret;
+}
+
+/** Signal the loop thread waiting for accept with \ref pw_thread_loop_signal()
+ *
+ * \param loop a \ref pw_thread_loop to signal
+ *
+ */
+SPA_EXPORT
+void pw_thread_loop_accept(struct pw_thread_loop *loop)
+{
+ loop->n_waiting_for_accept--;
+ pthread_cond_signal(&loop->accept_cond);
+}
+
+/** Check if we are inside the thread of the loop
+ *
+ * \param loop a \ref pw_thread_loop to signal
+ * \return true when called inside the thread of \a loop.
+ *
+ */
+SPA_EXPORT
+bool pw_thread_loop_in_thread(struct pw_thread_loop *loop)
+{
+ return loop->running && pthread_self() == loop->thread;
+}
diff --git a/src/pipewire/thread-loop.h b/src/pipewire/thread-loop.h
new file mode 100644
index 0000000..13e8532
--- /dev/null
+++ b/src/pipewire/thread-loop.h
@@ -0,0 +1,175 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_THREAD_LOOP_H
+#define PIPEWIRE_THREAD_LOOP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <pipewire/loop.h>
+
+/** \page page_thread_loop Thread Loop
+ *
+ * \section sec_thread_loop_overview Overview
+ *
+ * The threaded loop implementation is a special wrapper around the
+ * regular \ref pw_loop implementation.
+ *
+ * The added feature in the threaded loop is that it spawns a new thread
+ * that runs the wrapped loop. This allows a synchronous application to use
+ * the asynchronous API without risking to stall the PipeWire library.
+ *
+ * \section sec_thread_loop_create Creation
+ *
+ * A \ref pw_thread_loop object is created using pw_thread_loop_new().
+ * The \ref pw_loop to wrap must be given as an argument along with the name
+ * for the thread that will be spawned.
+ *
+ * After allocating the object, the thread must be started with
+ * pw_thread_loop_start()
+ *
+ * \section sec_thread_loop_destruction Destruction
+ *
+ * When the PipeWire connection has been terminated, the thread must be
+ * stopped and the resources freed. Stopping the thread is done using
+ * pw_thread_loop_stop(), which must be called without the lock (see
+ * below) held. When that function returns, the thread is stopped and the
+ * \ref pw_thread_loop object can be freed using pw_thread_loop_destroy().
+ *
+ * \section sec_thread_loop_locking Locking
+ *
+ * Since the PipeWire API doesn't allow concurrent accesses to objects,
+ * a locking scheme must be used to guarantee safe usage. The threaded
+ * loop API provides such a scheme through the functions
+ * pw_thread_loop_lock() and pw_thread_loop_unlock().
+ *
+ * The lock is recursive, so it's safe to use it multiple times from the same
+ * thread. Just make sure you call pw_thread_loop_unlock() the same
+ * number of times you called pw_thread_loop_lock().
+ *
+ * The lock needs to be held whenever you call any PipeWire function that
+ * uses an object associated with this loop. Make sure you do not hold
+ * on to the lock more than necessary though, as the threaded loop stops
+ * while the lock is held.
+ *
+ * \section sec_thread_loop_events Events and Callbacks
+ *
+ * All events and callbacks are called with the thread lock held.
+ *
+ */
+/** \defgroup pw_thread_loop Thread Loop
+ *
+ * The threaded loop object runs a \ref pw_loop in a separate thread
+ * and ensures proper locking is done.
+ *
+ * All of the loop callbacks will be executed with the loop
+ * lock held.
+ *
+ * See also \ref page_thread_loop
+ */
+
+/**
+ * \addtogroup pw_thread_loop
+ * \{
+ */
+struct pw_thread_loop;
+
+/** Thread loop events */
+struct pw_thread_loop_events {
+#define PW_VERSION_THREAD_LOOP_EVENTS 0
+ uint32_t version;
+
+ /** the loop is destroyed */
+ void (*destroy) (void *data);
+};
+
+/** Make a new thread loop with the given name and optional properties. */
+struct pw_thread_loop *
+pw_thread_loop_new(const char *name, const struct spa_dict *props);
+
+/** Make a new thread loop with the given loop, name and optional properties.
+ * When \a loop is NULL, a new loop will be created. */
+struct pw_thread_loop *
+pw_thread_loop_new_full(struct pw_loop *loop, const char *name, const struct spa_dict *props);
+
+/** Destroy a thread loop */
+void pw_thread_loop_destroy(struct pw_thread_loop *loop);
+
+/** Add an event listener */
+void pw_thread_loop_add_listener(struct pw_thread_loop *loop,
+ struct spa_hook *listener,
+ const struct pw_thread_loop_events *events,
+ void *data);
+
+/** Get the loop implementation of the thread loop */
+struct pw_loop * pw_thread_loop_get_loop(struct pw_thread_loop *loop);
+
+/** Start the thread loop */
+int pw_thread_loop_start(struct pw_thread_loop *loop);
+
+/** Stop the thread loop */
+void pw_thread_loop_stop(struct pw_thread_loop *loop);
+
+/** Lock the loop. This ensures exclusive ownership of the loop */
+void pw_thread_loop_lock(struct pw_thread_loop *loop);
+
+/** Unlock the loop */
+void pw_thread_loop_unlock(struct pw_thread_loop *loop);
+
+/** Release the lock and wait until some thread calls \ref pw_thread_loop_signal */
+void pw_thread_loop_wait(struct pw_thread_loop *loop);
+
+/** Release the lock and wait a maximum of 'wait_max_sec' seconds
+ * until some thread calls \ref pw_thread_loop_signal or time out */
+int pw_thread_loop_timed_wait(struct pw_thread_loop *loop, int wait_max_sec);
+
+/** Get a struct timespec suitable for \ref pw_thread_loop_timed_wait_full.
+ * Since: 0.3.7 */
+int pw_thread_loop_get_time(struct pw_thread_loop *loop, struct timespec *abstime, int64_t timeout);
+
+/** Release the lock and wait up to \a abstime until some thread calls
+ * \ref pw_thread_loop_signal. Use \ref pw_thread_loop_get_time to make a timeout.
+ * Since: 0.3.7 */
+int pw_thread_loop_timed_wait_full(struct pw_thread_loop *loop, struct timespec *abstime);
+
+/** Signal all threads waiting with \ref pw_thread_loop_wait */
+void pw_thread_loop_signal(struct pw_thread_loop *loop, bool wait_for_accept);
+
+/** Signal all threads executing \ref pw_thread_loop_signal with wait_for_accept */
+void pw_thread_loop_accept(struct pw_thread_loop *loop);
+
+/** Check if inside the thread */
+bool pw_thread_loop_in_thread(struct pw_thread_loop *loop);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_THREAD_LOOP_H */
diff --git a/src/pipewire/thread.c b/src/pipewire/thread.c
new file mode 100644
index 0000000..02b8b9e
--- /dev/null
+++ b/src/pipewire/thread.c
@@ -0,0 +1,160 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <pthread.h>
+
+#include <spa/utils/dict.h>
+#include <spa/utils/defs.h>
+#include <spa/utils/list.h>
+
+#include <pipewire/log.h>
+
+#include "thread.h"
+
+#define CHECK(expression,label) \
+do { \
+ if ((errno = (expression)) != 0) { \
+ res = -errno; \
+ pw_log_error(#expression ": %s", strerror(errno)); \
+ goto label; \
+ } \
+} while(false);
+
+SPA_EXPORT
+pthread_attr_t *pw_thread_fill_attr(const struct spa_dict *props, pthread_attr_t *attr)
+{
+ const char *str;
+ int res;
+
+ if (props == NULL)
+ return NULL;
+
+ pthread_attr_init(attr);
+ if ((str = spa_dict_lookup(props, SPA_KEY_THREAD_STACK_SIZE)) != NULL)
+ CHECK(pthread_attr_setstacksize(attr, atoi(str)), error);
+ return attr;
+error:
+ errno = -res;
+ return NULL;
+}
+
+#if defined(__FreeBSD__) || defined(__MidnightBSD__)
+#include <sys/param.h>
+#if __FreeBSD_version < 1202000 || defined(__MidnightBSD__)
+int pthread_setname_np(pthread_t thread, const char *name)
+{
+ pthread_set_name_np(thread, name);
+ return 0;
+}
+#endif
+#endif
+
+static struct spa_thread *impl_create(void *object,
+ const struct spa_dict *props,
+ void *(*start)(void*), void *arg)
+{
+ pthread_t pt;
+ pthread_attr_t *attr = NULL, attributes;
+ const char *str;
+ int err;
+
+ attr = pw_thread_fill_attr(props, &attributes);
+
+ err = pthread_create(&pt, attr, start, arg);
+
+ if (attr)
+ pthread_attr_destroy(attr);
+
+ if (err != 0) {
+ errno = err;
+ return NULL;
+ }
+ if (props) {
+ if ((str = spa_dict_lookup(props, SPA_KEY_THREAD_NAME)) != NULL &&
+ (err = pthread_setname_np(pt, str)) != 0)
+ pw_log_warn("pthread_setname error: %s", strerror(err));
+ }
+ return (struct spa_thread*)pt;
+}
+
+static int impl_join(void *object, struct spa_thread *thread, void **retval)
+{
+ pthread_t pt = (pthread_t)thread;
+ return pthread_join(pt, retval);
+}
+
+static int impl_get_rt_range(void *object, const struct spa_dict *props,
+ int *min, int *max)
+{
+ if (min)
+ *min = sched_get_priority_min(SCHED_OTHER);
+ if (max)
+ *max = sched_get_priority_max(SCHED_OTHER);
+ return 0;
+}
+static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority)
+{
+ pw_log_warn("acquire_rt thread:%p prio:%d not implemented", thread, priority);
+ return -ENOTSUP;
+}
+
+static int impl_drop_rt(void *object, struct spa_thread *thread)
+{
+ pw_log_warn("drop_rt thread:%p not implemented", thread);
+ return -ENOTSUP;
+}
+
+static struct {
+ struct spa_thread_utils utils;
+ struct spa_thread_utils_methods methods;
+} default_impl = {
+ { { SPA_TYPE_INTERFACE_ThreadUtils,
+ SPA_VERSION_THREAD_UTILS,
+ SPA_CALLBACKS_INIT(&default_impl.methods,
+ &default_impl) } },
+ { SPA_VERSION_THREAD_UTILS_METHODS,
+ .create = impl_create,
+ .join = impl_join,
+ .get_rt_range = impl_get_rt_range,
+ .acquire_rt = impl_acquire_rt,
+ .drop_rt = impl_drop_rt,
+ }
+};
+
+static struct spa_thread_utils *global_impl = &default_impl.utils;
+
+SPA_EXPORT
+void pw_thread_utils_set(struct spa_thread_utils *impl)
+{
+ pw_log_warn("pw_thread_utils_set is deprecated and does nothing anymore");
+}
+
+SPA_EXPORT
+struct spa_thread_utils *pw_thread_utils_get(void)
+{
+ return global_impl;
+}
diff --git a/src/pipewire/thread.h b/src/pipewire/thread.h
new file mode 100644
index 0000000..ba84a5d
--- /dev/null
+++ b/src/pipewire/thread.h
@@ -0,0 +1,65 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_THREAD_H
+#define PIPEWIRE_THREAD_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <string.h>
+#include <errno.h>
+
+#include <spa/support/thread.h>
+
+/** \defgroup pw_thread Thread
+ *
+ * \brief functions to manipulate threads
+ */
+
+/**
+ * \addtogroup pw_thread
+ * \{
+ */
+
+SPA_DEPRECATED
+void pw_thread_utils_set(struct spa_thread_utils *impl);
+struct spa_thread_utils *pw_thread_utils_get(void);
+
+#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__)
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_THREAD_H */
diff --git a/src/pipewire/type.h b/src/pipewire/type.h
new file mode 100644
index 0000000..3572531
--- /dev/null
+++ b/src/pipewire/type.h
@@ -0,0 +1,65 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_TYPE_H
+#define PIPEWIRE_TYPE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <spa/utils/type.h>
+
+/** \defgroup pw_type Type info
+ * Type information
+ */
+
+/**
+ * \addtogroup pw_type
+ * \{
+ */
+
+enum {
+ PW_TYPE_FIRST = SPA_TYPE_VENDOR_PipeWire,
+};
+
+#define PW_TYPE_INFO_BASE "PipeWire:"
+
+#define PW_TYPE_INFO_Object PW_TYPE_INFO_BASE "Object"
+#define PW_TYPE_INFO_OBJECT_BASE PW_TYPE_INFO_Object ":"
+
+#define PW_TYPE_INFO_Interface PW_TYPE_INFO_BASE "Interface"
+#define PW_TYPE_INFO_INTERFACE_BASE PW_TYPE_INFO_Interface ":"
+
+const struct spa_type_info * pw_type_info(void);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_TYPE_H */
diff --git a/src/pipewire/utils.c b/src/pipewire/utils.c
new file mode 100644
index 0000000..1d02714
--- /dev/null
+++ b/src/pipewire/utils.c
@@ -0,0 +1,214 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#ifdef HAVE_SYS_RANDOM_H
+#include <sys/random.h>
+#endif
+#include <string.h>
+
+#include <pipewire/array.h>
+#include <pipewire/log.h>
+#include <pipewire/utils.h>
+
+/** Split a string based on delimiters
+ * \param str a string to split
+ * \param delimiter delimiter characters to split on
+ * \param[out] len the length of the current string
+ * \param[in,out] state a state variable
+ * \return a string or NULL when the end is reached
+ *
+ * Repeatedly call this function to split \a str into all substrings
+ * delimited by \a delimiter. \a state should be set to NULL on the first
+ * invocation and passed to the function until NULL is returned.
+ */
+SPA_EXPORT
+const char *pw_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;
+}
+
+/** Split a string based on delimiters
+ * \param str a string to split
+ * \param delimiter delimiter characters to split on
+ * \param max_tokens the max number of tokens to split
+ * \param[out] n_tokens the number of tokens
+ * \return a NULL terminated array of strings that should be
+ * freed with \ref pw_free_strv.
+ */
+SPA_EXPORT
+char **pw_split_strv(const char *str, const char *delimiter, int max_tokens, int *n_tokens)
+{
+ const char *state = NULL, *s = NULL;
+ struct pw_array arr;
+ size_t len;
+ int n = 0;
+
+ pw_array_init(&arr, 16);
+
+ s = pw_split_walk(str, delimiter, &len, &state);
+ while (s && n + 1 < max_tokens) {
+ pw_array_add_ptr(&arr, strndup(s, len));
+ s = pw_split_walk(str, delimiter, &len, &state);
+ n++;
+ }
+ if (s) {
+ pw_array_add_ptr(&arr, strdup(s));
+ n++;
+ }
+ pw_array_add_ptr(&arr, NULL);
+
+ *n_tokens = n;
+
+ return arr.data;
+}
+
+/** Split a string in-place based on delimiters
+ * \param str a string to split
+ * \param delimiter delimiter characters to split on
+ * \param max_tokens the max number of tokens to split
+ * \param[out] tokens an array to hold up to \a max_tokens of strings
+ * \return the number of tokens in \a tokens
+ *
+ * \a str will be modified in-place so that \a tokens will contain zero terminated
+ * strings split at \a delimiter characters.
+ */
+SPA_EXPORT
+int pw_split_ip(char *str, const char *delimiter, int max_tokens, char *tokens[])
+{
+ const char *state = NULL;
+ char *s, *t;
+ size_t len, l2;
+ int n = 0;
+
+ s = (char *)pw_split_walk(str, delimiter, &len, &state);
+ while (s && n + 1 < max_tokens) {
+ t = (char*)pw_split_walk(str, delimiter, &l2, &state);
+ s[len] = '\0';
+ tokens[n++] = s;
+ s = t;
+ len = l2;
+ }
+ if (s)
+ tokens[n++] = s;
+ return n;
+}
+
+/** Free a NULL terminated array of strings
+ * \param str a NULL terminated array of string
+ *
+ * Free all the strings in the array and the array
+ */
+SPA_EXPORT
+void pw_free_strv(char **str)
+{
+ int i;
+
+ if (str == NULL)
+ return;
+
+ for (i = 0; str[i]; i++)
+ free(str[i]);
+ free(str);
+}
+
+/** Strip all whitespace before and after a string
+ * \param str a string to strip
+ * \param whitespace characters to strip
+ * \return the stripped part of \a str
+ *
+ * Strip whitespace before and after \a str. \a str will be
+ * modified.
+ */
+SPA_EXPORT
+char *pw_strip(char *str, const char *whitespace)
+{
+ char *e, *l = NULL;
+
+ str += strspn(str, whitespace);
+
+ for (e = str; *e; e++)
+ if (!strchr(whitespace, *e))
+ l = e;
+
+ if (l)
+ *(l + 1) = '\0';
+ else
+ *str = '\0';
+
+ return str;
+}
+
+/** Fill a buffer with random data
+ * \param buf a buffer to fill
+ * \param buflen the number of bytes to fill
+ * \param flags optional flags
+ * \return the number of bytes filled
+ *
+ * Fill \a buf with \a buflen random bytes.
+ */
+SPA_EXPORT
+ssize_t pw_getrandom(void *buf, size_t buflen, unsigned int flags)
+{
+ ssize_t bytes;
+ int read_errno;
+
+#ifdef HAVE_GETRANDOM
+ bytes = getrandom(buf, buflen, flags);
+ if (!(bytes == -1 && errno == ENOSYS))
+ return bytes;
+#endif
+
+ int fd = open("/dev/urandom", O_CLOEXEC);
+ if (fd < 0)
+ return -1;
+ bytes = read(fd, buf, buflen);
+ read_errno = errno;
+ close(fd);
+ errno = read_errno;
+ return bytes;
+}
+
+SPA_EXPORT
+void* pw_reallocarray(void *ptr, size_t nmemb, size_t size)
+{
+#ifdef HAVE_REALLOCARRAY
+ return reallocarray(ptr, nmemb, size);
+#else
+ return realloc(ptr, nmemb * size);
+#endif
+}
diff --git a/src/pipewire/utils.h b/src/pipewire/utils.h
new file mode 100644
index 0000000..d8d45e0
--- /dev/null
+++ b/src/pipewire/utils.h
@@ -0,0 +1,111 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_UTILS_H
+#define PIPEWIRE_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/un.h>
+#ifndef _POSIX_C_SOURCE
+# include <sys/mount.h>
+#endif
+
+#include <spa/utils/defs.h>
+#include <spa/pod/pod.h>
+
+/** \defgroup pw_utils Utilities
+ *
+ * Various utility functions
+ */
+
+/**
+ * \addtogroup pw_utils
+ * \{
+ */
+
+/** a function to destroy an item */
+typedef void (*pw_destroy_t) (void *object);
+
+const char *
+pw_split_walk(const char *str, const char *delimiter, size_t *len, const char **state);
+
+char **
+pw_split_strv(const char *str, const char *delimiter, int max_tokens, int *n_tokens);
+
+int
+pw_split_ip(char *str, const char *delimiter, int max_tokens, char *tokens[]);
+
+void
+pw_free_strv(char **str);
+
+char *
+pw_strip(char *str, const char *whitespace);
+
+#if !defined(strndupa)
+# define strndupa(s, n) \
+ ({ \
+ const char *__old = (s); \
+ size_t __len = strnlen(__old, (n)); \
+ char *__new = (char *) __builtin_alloca(__len + 1); \
+ memcpy(__new, __old, __len); \
+ __new[__len] = '\0'; \
+ __new; \
+ })
+#endif
+
+#if !defined(strdupa)
+# define strdupa(s) \
+ ({ \
+ const char *__old = (s); \
+ size_t __len = strlen(__old) + 1; \
+ char *__new = (char *) alloca(__len); \
+ (char *) memcpy(__new, __old, __len); \
+ })
+#endif
+
+SPA_WARN_UNUSED_RESULT
+ssize_t pw_getrandom(void *buf, size_t buflen, unsigned int flags);
+
+void* pw_reallocarray(void *ptr, size_t nmemb, size_t size);
+
+#ifdef PW_ENABLE_DEPRECATED
+#define PW_DEPRECATED(v) (v)
+#else
+#define PW_DEPRECATED(v) ({ __typeof__(v) _v SPA_DEPRECATED = (v); (void)_v; (v); })
+#endif /* PW_ENABLE_DEPRECATED */
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PIPEWIRE_UTILS_H */
diff --git a/src/pipewire/version.h.in b/src/pipewire/version.h.in
new file mode 100644
index 0000000..55604d0
--- /dev/null
+++ b/src/pipewire/version.h.in
@@ -0,0 +1,68 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_VERSION_H
+#define PIPEWIRE_VERSION_H
+
+/* WARNING: Make sure to edit the real source file version.h.in! */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Return the version of the header files. Keep in mind that this is
+a macro and not a function, so it is impossible to get the pointer of
+it. */
+#define pw_get_headers_version() ("@PIPEWIRE_VERSION_MAJOR@.@PIPEWIRE_VERSION_MINOR@.@PIPEWIRE_VERSION_MICRO@")
+
+/** Return the version of the library the current application is
+ * linked to. */
+const char* pw_get_library_version(void);
+
+/** The current API version. Versions prior to 0.2.0 have
+ * PW_API_VERSION undefined. Please note that this is only ever
+ * increased on incompatible API changes! */
+#define PW_API_VERSION @PIPEWIRE_API_VERSION@
+
+/** The major version of PipeWire. \since 0.2.0 */
+#define PW_MAJOR @PIPEWIRE_VERSION_MAJOR@
+
+/** The minor version of PipeWire. \since 0.2.0 */
+#define PW_MINOR @PIPEWIRE_VERSION_MINOR@
+
+/** The micro version of PipeWire. \since 0.2.0 */
+#define PW_MICRO @PIPEWIRE_VERSION_MICRO@
+
+/** Evaluates to TRUE if the PipeWire library version is equal or
+ * newer than the specified. \since 0.2.0 */
+#define PW_CHECK_VERSION(major,minor,micro) \
+ ((PW_MAJOR > (major)) || \
+ (PW_MAJOR == (major) && PW_MINOR > (minor)) || \
+ (PW_MAJOR == (major) && PW_MINOR == (minor) && PW_MICRO >= (micro)))
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_VERSION_H */
diff --git a/src/pipewire/work-queue.c b/src/pipewire/work-queue.c
new file mode 100644
index 0000000..190d29f
--- /dev/null
+++ b/src/pipewire/work-queue.c
@@ -0,0 +1,269 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <spa/utils/type.h>
+#include <spa/utils/result.h>
+
+#include "pipewire/log.h"
+#include "pipewire/work-queue.h"
+
+PW_LOG_TOPIC_EXTERN(log_work_queue);
+#define PW_LOG_TOPIC_DEFAULT log_work_queue
+
+/** \cond */
+struct work_item {
+ void *obj;
+ uint32_t id;
+ uint32_t seq;
+ pw_work_func_t func;
+ void *data;
+ struct spa_list link;
+ int res;
+};
+
+struct pw_work_queue {
+ struct pw_loop *loop;
+
+ struct spa_source *wakeup;
+
+ struct spa_list work_list;
+ struct spa_list free_list;
+ uint32_t counter;
+ uint32_t n_queued;
+};
+/** \endcond */
+
+static void process_work_queue(void *data, uint64_t count)
+{
+ struct pw_work_queue *this = data;
+ struct work_item *item, *tmp;
+
+ spa_list_for_each_safe(item, tmp, &this->work_list, link) {
+ if (item->seq != SPA_ID_INVALID) {
+ pw_log_debug("%p: n_queued:%d waiting for item %p seq:%d id:%u", this,
+ this->n_queued, item->obj, item->seq, item->id);
+ continue;
+ }
+
+ if (item->res == -EBUSY &&
+ item != spa_list_first(&this->work_list, struct work_item, link)) {
+ pw_log_debug("%p: n_queued:%d sync item %p not head id:%u", this,
+ this->n_queued, item->obj, item->id);
+ continue;
+ }
+
+ spa_list_remove(&item->link);
+ this->n_queued--;
+
+ if (item->func) {
+ pw_log_debug("%p: n_queued:%d process work item %p seq:%d res:%d id:%u",
+ this, this->n_queued, item->obj, item->seq, item->res,
+ item->id);
+ item->func(item->obj, item->data, item->res, item->id);
+ }
+ spa_list_append(&this->free_list, &item->link);
+ }
+}
+
+/** Create a new \ref pw_work_queue
+ *
+ * \param loop the loop to use
+ * \return a newly allocated work queue
+ *
+ */
+struct pw_work_queue *pw_work_queue_new(struct pw_loop *loop)
+{
+ struct pw_work_queue *this;
+ int res;
+
+ this = calloc(1, sizeof(struct pw_work_queue));
+ if (this == NULL)
+ return NULL;
+
+ pw_log_debug("%p: new", this);
+
+ this->loop = loop;
+
+ this->wakeup = pw_loop_add_event(this->loop, process_work_queue, this);
+ if (this->wakeup == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ spa_list_init(&this->work_list);
+ spa_list_init(&this->free_list);
+
+ return this;
+
+error_free:
+ free(this);
+ errno = -res;
+ return NULL;
+}
+
+/** Destroy a work queue
+ * \param queue the work queue to destroy
+ *
+ */
+void pw_work_queue_destroy(struct pw_work_queue *queue)
+{
+ struct work_item *item, *tmp;
+
+ pw_log_debug("%p: destroy", queue);
+
+ pw_loop_destroy_source(queue->loop, queue->wakeup);
+
+ spa_list_for_each_safe(item, tmp, &queue->work_list, link) {
+ pw_log_debug("%p: cancel work item %p seq:%d res:%d id:%u",
+ queue, item->obj, item->seq, item->res, item->id);
+ free(item);
+ }
+ spa_list_for_each_safe(item, tmp, &queue->free_list, link)
+ free(item);
+
+ free(queue);
+}
+
+/** Add an item to the work queue
+ *
+ * \param queue the work queue
+ * \param obj the object owning the work item
+ * \param res a result code
+ * \param func a work function
+ * \param data passed to \a func
+ *
+ */
+SPA_EXPORT
+uint32_t
+pw_work_queue_add(struct pw_work_queue *queue, void *obj, int res, pw_work_func_t func, void *data)
+{
+ struct work_item *item;
+ bool have_work = false;
+
+ if (!spa_list_is_empty(&queue->free_list)) {
+ item = spa_list_first(&queue->free_list, struct work_item, link);
+ spa_list_remove(&item->link);
+ } else {
+ item = malloc(sizeof(struct work_item));
+ if (item == NULL)
+ return SPA_ID_INVALID;
+ }
+ item->id = ++queue->counter;
+ if (item->id == SPA_ID_INVALID)
+ item->id = ++queue->counter;
+
+ item->obj = obj;
+ item->func = func;
+ item->data = data;
+
+ if (SPA_RESULT_IS_ASYNC(res)) {
+ item->seq = SPA_RESULT_ASYNC_SEQ(res);
+ item->res = res;
+ pw_log_debug("%p: defer async %d for object %p id:%d",
+ queue, item->seq, obj, item->id);
+ } else if (res == -EBUSY) {
+ pw_log_debug("%p: wait sync object %p id:%u",
+ queue, obj, item->id);
+ item->seq = SPA_ID_INVALID;
+ item->res = res;
+ have_work = true;
+ } else {
+ item->seq = SPA_ID_INVALID;
+ item->res = res;
+ have_work = true;
+ pw_log_debug("%p: defer object %p id:%u", queue, obj, item->id);
+ }
+ spa_list_append(&queue->work_list, &item->link);
+ queue->n_queued++;
+
+ if (have_work)
+ pw_loop_signal_event(queue->loop, queue->wakeup);
+
+ return item->id;
+}
+
+/** Cancel a work item
+ * \param queue the work queue
+ * \param obj the owner object
+ * \param id the wotk id to cancel
+ *
+ */
+SPA_EXPORT
+int pw_work_queue_cancel(struct pw_work_queue *queue, void *obj, uint32_t id)
+{
+ bool have_work = false;
+ struct work_item *item;
+
+ spa_list_for_each(item, &queue->work_list, link) {
+ if ((id == SPA_ID_INVALID || item->id == id) && (obj == NULL || item->obj == obj)) {
+ pw_log_debug("%p: cancel defer %d for object %p id:%u", queue,
+ item->seq, item->obj, id);
+ item->seq = SPA_ID_INVALID;
+ item->func = NULL;
+ have_work = true;
+ }
+ }
+ if (!have_work) {
+ pw_log_debug("%p: no deferred found for object %p id:%u", queue, obj, id);
+ return -EINVAL;
+ }
+
+ pw_loop_signal_event(queue->loop, queue->wakeup);
+ return 0;
+}
+
+/** Complete a work item
+ * \param queue the work queue
+ * \param obj the owner object
+ * \param seq the sequence number that completed
+ * \param res 0 if the item was found, < 0 on error
+ *
+ */
+SPA_EXPORT
+int pw_work_queue_complete(struct pw_work_queue *queue, void *obj, uint32_t seq, int res)
+{
+ struct work_item *item;
+ bool have_work = false;
+
+ spa_list_for_each(item, &queue->work_list, link) {
+ if (item->obj == obj && item->seq == seq) {
+ pw_log_debug("%p: found deferred %d for object %p res:%d id:%u",
+ queue, seq, obj, res, item->id);
+ item->seq = SPA_ID_INVALID;
+ item->res = res;
+ have_work = true;
+ }
+ }
+ if (!have_work) {
+ pw_log_trace("%p: no deferred %d found for object %p", queue, seq, obj);
+ return -EINVAL;
+ }
+
+ pw_loop_signal_event(queue->loop, queue->wakeup);
+ return 0;
+}
diff --git a/src/pipewire/work-queue.h b/src/pipewire/work-queue.h
new file mode 100644
index 0000000..299f8f1
--- /dev/null
+++ b/src/pipewire/work-queue.h
@@ -0,0 +1,71 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PIPEWIRE_WORK_QUEUE_H
+#define PIPEWIRE_WORK_QUEUE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** \defgroup pw_work_queue Work Queue
+ * Queued processing of work items.
+ */
+
+/**
+ * \addtogroup pw_work_queue
+ * \{
+ */
+struct pw_work_queue;
+
+#include <pipewire/loop.h>
+
+typedef void (*pw_work_func_t) (void *obj, void *data, int res, uint32_t id);
+
+struct pw_work_queue *
+pw_work_queue_new(struct pw_loop *loop);
+
+void
+pw_work_queue_destroy(struct pw_work_queue *queue);
+
+uint32_t
+pw_work_queue_add(struct pw_work_queue *queue,
+ void *obj, int res,
+ pw_work_func_t func, void *data);
+
+int
+pw_work_queue_cancel(struct pw_work_queue *queue, void *obj, uint32_t id);
+
+int
+pw_work_queue_complete(struct pw_work_queue *queue, void *obj, uint32_t seq, int res);
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PIPEWIRE_WORK_QUEUE_H */
diff --git a/src/tests/meson.build b/src/tests/meson.build
new file mode 100644
index 0000000..f7c54f2
--- /dev/null
+++ b/src/tests/meson.build
@@ -0,0 +1,52 @@
+test_apps = [
+ 'test-endpoint',
+ 'test-interfaces',
+ # 'test-remote',
+ 'test-stream',
+ 'test-filter',
+]
+
+foreach a : test_apps
+ test('pw-' + a,
+ executable('pw-' + a, a + '.c',
+ dependencies : [pipewire_dep],
+ include_directories: [includes_inc],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir),
+ env : [
+ 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')),
+ 'PIPEWIRE_CONFIG_DIR=@0@'.format(pipewire_dep.get_variable('confdatadir')),
+ 'PIPEWIRE_MODULE_DIR=@0@'.format(pipewire_dep.get_variable('moduledir')),
+ ])
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'pw-' + a)
+ configure_file(
+ input: installed_tests_template,
+ output: 'pw-' + a + '.test',
+ install_dir: installed_tests_metadir,
+ configuration: test_conf
+ )
+ endif
+endforeach
+
+
+if have_cpp
+ test_cpp = executable('pw-test-cpp', 'test-cpp.cpp',
+ dependencies : [pipewire_dep],
+ install : installed_tests_enabled,
+ install_dir : installed_tests_execdir)
+ test('pw-test-cpp', test_cpp)
+
+ if installed_tests_enabled
+ test_conf = configuration_data()
+ test_conf.set('exec', installed_tests_execdir / 'pw-test-cpp')
+ configure_file(
+ input: installed_tests_template,
+ output: 'pw-test-cpp.test',
+ install_dir: installed_tests_metadir,
+ configuration: test_conf
+ )
+ endif
+endif
diff --git a/src/tests/test-cpp.cpp b/src/tests/test-cpp.cpp
new file mode 100644
index 0000000..e905724
--- /dev/null
+++ b/src/tests/test-cpp.cpp
@@ -0,0 +1,35 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+#include <pipewire/extensions/client-node.h>
+#include <pipewire/extensions/protocol-native.h>
+
+
+int main(int argc, char *argv[])
+{
+ pw_init(&argc, &argv);
+ return 0;
+}
diff --git a/src/tests/test-endpoint.c b/src/tests/test-endpoint.c
new file mode 100644
index 0000000..dd516a6
--- /dev/null
+++ b/src/tests/test-endpoint.c
@@ -0,0 +1,469 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/session-manager.h>
+
+#include <spa/utils/string.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/filter.h>
+
+#include <valgrind/valgrind.h>
+
+struct props
+{
+ float volume;
+ bool mute;
+};
+
+struct endpoint
+{
+ struct spa_interface iface;
+ struct spa_hook_list hooks;
+ struct pw_properties *properties;
+ struct pw_endpoint_info info;
+ uint32_t n_subscribe_ids;
+ uint32_t subscribe_ids[2];
+ struct props props;
+};
+
+#define pw_endpoint_emit(hooks,method,version,...) \
+ spa_hook_list_call_simple(hooks, struct pw_endpoint_events, \
+ method, version, ##__VA_ARGS__)
+
+#define pw_endpoint_emit_info(hooks,...) pw_endpoint_emit(hooks, info, 0, ##__VA_ARGS__)
+#define pw_endpoint_emit_param(hooks,...) pw_endpoint_emit(hooks, param, 0, ##__VA_ARGS__)
+
+static int
+endpoint_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct pw_endpoint_events *events,
+ void *data)
+{
+ struct endpoint *self = object;
+ struct spa_hook_list save;
+
+ spa_hook_list_isolate(&self->hooks, &save, listener, events, data);
+ pw_endpoint_emit_info(&self->hooks, &self->info);
+ spa_hook_list_join(&self->hooks, &save);
+ return 0;
+}
+
+static int
+endpoint_enum_params (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct endpoint *self = object;
+ struct spa_pod *param, *result;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ uint32_t count = 0, index, next = start;
+
+ next = start;
+ next:
+ index = next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ struct props *p = &self->props;
+
+ switch (index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume),
+ SPA_PROP_INFO_description, SPA_POD_String("volume"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 1.0));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute),
+ SPA_PROP_INFO_description, SPA_POD_String("mute"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->mute));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &self->props;
+
+ switch (index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_volume, SPA_POD_Float(p->volume),
+ SPA_PROP_mute, SPA_POD_Bool(p->mute));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result, param, filter) < 0)
+ goto next;
+
+ pw_endpoint_emit_param(&self->hooks, seq, id, index, next, result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int
+endpoint_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
+{
+ struct endpoint *self = object;
+
+ n_ids = SPA_MIN(n_ids, SPA_N_ELEMENTS(self->subscribe_ids));
+ self->n_subscribe_ids = n_ids;
+
+ for (uint32_t i = 0; i < n_ids; i++) {
+ self->subscribe_ids[i] = ids[i];
+ endpoint_enum_params(object, 1, ids[i], 0, UINT32_MAX, NULL);
+ }
+ return 0;
+}
+
+static int
+endpoint_set_param (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct endpoint *self = object;
+
+ if (id == SPA_PARAM_Props) {
+ struct props *p = &self->props;
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume),
+ SPA_PROP_mute, SPA_POD_OPT_Bool(&p->mute));
+ }
+ else {
+ spa_assert_not_reached();
+ return -ENOENT;
+ }
+
+ for (uint32_t i = 0; i < self->n_subscribe_ids; i++) {
+ if (id == self->subscribe_ids[i])
+ endpoint_enum_params (self, 1, id, 0, UINT32_MAX, NULL);
+ }
+
+ return 0;
+}
+
+static int
+endpoint_create_link (void *object, const struct spa_dict *props)
+{
+ spa_assert_not_reached();
+ return -ENOTSUP;
+}
+
+static const struct pw_endpoint_methods endpoint_methods = {
+ PW_VERSION_ENDPOINT_METHODS,
+ .add_listener = endpoint_add_listener,
+ .subscribe_params = endpoint_subscribe_params,
+ .enum_params = endpoint_enum_params,
+ .set_param = endpoint_set_param,
+ .create_link = endpoint_create_link,
+};
+
+static struct spa_param_info param_info[] = {
+ SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
+ SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
+};
+
+static void
+endpoint_init(struct endpoint * self)
+{
+ self->iface = SPA_INTERFACE_INIT (
+ PW_TYPE_INTERFACE_Endpoint,
+ PW_VERSION_ENDPOINT,
+ &endpoint_methods, self);
+ spa_hook_list_init (&self->hooks);
+
+ self->info.version = PW_VERSION_ENDPOINT_INFO;
+ self->info.change_mask = PW_ENDPOINT_CHANGE_MASK_ALL;
+ self->info.name = "test-endpoint";
+ self->info.media_class = "Audio/Sink";
+ self->info.direction = PW_DIRECTION_OUTPUT;
+ self->info.n_streams = 0;
+ self->info.session_id = SPA_ID_INVALID;
+
+ self->properties = pw_properties_new(
+ PW_KEY_ENDPOINT_NAME, self->info.name,
+ PW_KEY_MEDIA_CLASS, self->info.media_class,
+ NULL);
+ self->info.props = &self->properties->dict;
+
+ self->info.params = param_info;
+ self->info.n_params = SPA_N_ELEMENTS (param_info);
+
+ self->props.volume = 0.9;
+ self->props.mute = false;
+}
+
+static void
+endpoint_clear(struct endpoint * self)
+{
+ spa_hook_list_clean(&self->hooks);
+ pw_properties_free(self->properties);
+}
+
+struct test_endpoint_data
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct endpoint endpoint;
+ struct pw_proxy *export_proxy;
+ struct pw_proxy *bound_proxy;
+ struct spa_hook object_listener;
+ struct spa_hook proxy_listener;
+
+ struct props props;
+ bool info_received;
+ int params_received;
+};
+
+static void
+endpoint_event_info(void *data, const struct pw_endpoint_info *info)
+{
+ struct test_endpoint_data *d = data;
+ const char *val;
+
+ spa_assert_se(info);
+ spa_assert_se(info->version == PW_VERSION_ENDPOINT_INFO);
+ spa_assert_se(info->id == pw_proxy_get_bound_id(d->bound_proxy));
+ spa_assert_se(info->id == pw_proxy_get_bound_id(d->export_proxy));
+ spa_assert_se(info->change_mask == PW_ENDPOINT_CHANGE_MASK_ALL);
+ spa_assert_se(spa_streq(info->name, "test-endpoint"));
+ spa_assert_se(spa_streq(info->media_class, "Audio/Sink"));
+ spa_assert_se(info->direction == PW_DIRECTION_OUTPUT);
+ spa_assert_se(info->n_streams == 0);
+ spa_assert_se(info->session_id == SPA_ID_INVALID);
+ spa_assert_se(info->n_params == SPA_N_ELEMENTS (param_info));
+ spa_assert_se(info->n_params == 2);
+ spa_assert_se(info->params[0].id == param_info[0].id);
+ spa_assert_se(info->params[0].flags == param_info[0].flags);
+ spa_assert_se(info->params[1].id == param_info[1].id);
+ spa_assert_se(info->params[1].flags == param_info[1].flags);
+ spa_assert_se(info->props != NULL);
+ val = spa_dict_lookup(info->props, PW_KEY_ENDPOINT_NAME);
+ spa_assert_se(val && spa_streq(val, "test-endpoint"));
+ val = spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS);
+ spa_assert_se(val && spa_streq(val, "Audio/Sink"));
+
+ d->info_received = true;
+ pw_main_loop_quit(d->loop);
+}
+
+static void
+endpoint_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct test_endpoint_data *d = data;
+
+ if (id == SPA_PARAM_Props) {
+ struct props *p = &d->props;
+ spa_assert_se(param);
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, &id,
+ SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume),
+ SPA_PROP_mute, SPA_POD_OPT_Bool(&p->mute));
+ spa_assert_se(id == SPA_PARAM_Props);
+ }
+
+ d->params_received++;
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_endpoint_events endpoint_events = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = endpoint_event_info,
+ .param = endpoint_event_param,
+};
+
+static void
+endpoint_proxy_destroy(void *data)
+{
+ struct test_endpoint_data *d = data;
+ d->bound_proxy = NULL;
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = endpoint_proxy_destroy,
+};
+
+static void
+test_endpoint_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct test_endpoint_data *d = data;
+ const char *val;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Endpoint))
+ return;
+
+ d->bound_proxy = pw_registry_bind(d->registry, id, type,
+ PW_VERSION_ENDPOINT, 0);
+ spa_assert_se(d->bound_proxy != NULL);
+
+ spa_assert_se(props != NULL);
+ val = spa_dict_lookup(props, PW_KEY_ENDPOINT_NAME);
+ spa_assert_se(val && spa_streq(val, "test-endpoint"));
+ val = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+ spa_assert_se(val && spa_streq(val, "Audio/Sink"));
+
+ pw_endpoint_add_listener(d->bound_proxy, &d->object_listener,
+ &endpoint_events, d);
+ pw_proxy_add_listener(d->bound_proxy, &d->proxy_listener,
+ &proxy_events, d);
+}
+
+static void
+test_endpoint_global_remove(void *data, uint32_t id)
+{
+ struct test_endpoint_data *d = data;
+ if (d->bound_proxy && id == pw_proxy_get_bound_id(d->bound_proxy))
+ pw_proxy_destroy(d->bound_proxy);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = test_endpoint_global,
+ .global_remove = test_endpoint_global_remove,
+};
+
+static void test_endpoint(void)
+{
+ struct test_endpoint_data d;
+ uint32_t ids[] = { SPA_PARAM_Props };
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, 1024);
+
+ d.loop = pw_main_loop_new(NULL);
+ d.context = pw_context_new(pw_main_loop_get_loop(d.loop), NULL, 0);
+ spa_assert_se(d.context != NULL);
+
+ d.core = pw_context_connect_self(d.context, NULL, 0);
+ spa_assert_se(d.core != NULL);
+
+ d.registry = pw_core_get_registry(d.core, PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(d.registry,
+ &d.registry_listener,
+ &registry_events, &d);
+
+ /* export and expect to get a global on the registry, along with info */
+ d.info_received = false;
+ endpoint_init(&d.endpoint);
+ d.export_proxy = pw_core_export(d.core, PW_TYPE_INTERFACE_Endpoint,
+ d.endpoint.info.props, &d.endpoint.iface, 0);
+ spa_assert_se(d.export_proxy != NULL);
+ pw_main_loop_run(d.loop);
+ spa_assert_se(d.bound_proxy);
+ spa_assert_se(d.info_received == true);
+
+ /* request params */
+ d.params_received = 0;
+ d.props.volume = 0.0;
+ d.props.mute = true;
+ pw_endpoint_subscribe_params(d.bound_proxy, ids, SPA_N_ELEMENTS(ids));
+ pw_main_loop_run(d.loop);
+ spa_assert_se(d.params_received == 1);
+ spa_assert_se(d.props.volume > 0.89 && d.props.volume < 0.91);
+ spa_assert_se(d.props.mute == false);
+
+ /* set param from the client */
+ d.params_received = 0;
+ pw_endpoint_set_param(d.bound_proxy, SPA_PARAM_Props, 0,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+ SPA_PROP_volume, SPA_POD_Float(0.5)));
+ pw_main_loop_run(d.loop);
+ spa_assert_se(d.params_received == 1);
+ spa_assert_se(d.props.volume > 0.49 && d.props.volume < 0.51);
+ spa_assert_se(d.props.mute == false);
+
+ /* set param from the impl */
+ d.params_received = 0;
+ pw_endpoint_set_param(&d.endpoint.iface, SPA_PARAM_Props, 0,
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+ SPA_PROP_volume, SPA_POD_Float(0.2),
+ SPA_PROP_mute, SPA_POD_Bool(true)));
+ pw_main_loop_run(d.loop);
+ spa_assert_se(d.params_received == 1);
+ spa_assert_se(d.props.volume > 0.19 && d.props.volume < 0.21);
+ spa_assert_se(d.props.mute == true);
+
+ /* stop exporting and expect to see that reflected on the registry */
+ pw_proxy_destroy(d.export_proxy);
+ pw_main_loop_run(d.loop);
+ spa_assert_se(!d.bound_proxy);
+
+ endpoint_clear(&d.endpoint);
+ pw_proxy_destroy((struct pw_proxy*)d.registry);
+ pw_context_destroy(d.context);
+ pw_main_loop_destroy(d.loop);
+}
+
+int main(int argc, char *argv[])
+{
+ /* FIXME: This test has a leak and a use of uninitialized buffer
+ * that needs to be debugged and fixed (or excluded). Meanwhile -
+ * skip it from valgrind so we can at least use the others. */
+ if (RUNNING_ON_VALGRIND)
+ return 77;
+
+ pw_init(&argc, &argv);
+
+ alarm(5); /* watchdog; terminate after 5 seconds */
+ test_endpoint();
+
+ return 0;
+}
diff --git a/src/tests/test-filter.c b/src/tests/test-filter.c
new file mode 100644
index 0000000..93f0037
--- /dev/null
+++ b/src/tests/test-filter.c
@@ -0,0 +1,375 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+#include <pipewire/main-loop.h>
+#include <pipewire/filter.h>
+
+#include <spa/utils/string.h>
+
+#define TEST_FUNC(a,b,func) \
+do { \
+ a.func = b.func; \
+ spa_assert_se(SPA_PTRDIFF(&a.func, &a) == SPA_PTRDIFF(&b.func, &b)); \
+} while(0)
+
+static void test_abi(void)
+{
+ static const struct {
+ uint32_t version;
+ void (*destroy) (void *data);
+ void (*state_changed) (void *data, enum pw_filter_state old,
+ enum pw_filter_state state, const char *error);
+ void (*io_changed) (void *data, void *port_data, uint32_t id, void *area, uint32_t size);
+ void (*param_changed) (void *data, void *port_data, uint32_t id, const struct spa_pod *param);
+ void (*add_buffer) (void *data, void *port_data, struct pw_buffer *buffer);
+ void (*remove_buffer) (void *data, void *port_data, struct pw_buffer *buffer);
+ void (*process) (void *data, struct spa_io_position *position);
+ void (*drained) (void *data);
+ void (*command) (void *data, const struct spa_command *command);
+ } test = { PW_VERSION_FILTER_EVENTS, NULL };
+
+ struct pw_filter_events ev;
+
+ TEST_FUNC(ev, test, destroy);
+ TEST_FUNC(ev, test, state_changed);
+ TEST_FUNC(ev, test, io_changed);
+ TEST_FUNC(ev, test, param_changed);
+ TEST_FUNC(ev, test, add_buffer);
+ TEST_FUNC(ev, test, remove_buffer);
+ TEST_FUNC(ev, test, process);
+ TEST_FUNC(ev, test, drained);
+ TEST_FUNC(ev, test, command);
+
+ spa_assert_se(PW_VERSION_FILTER_EVENTS == 1);
+ spa_assert_se(sizeof(ev) == sizeof(test));
+
+ spa_assert_se(PW_FILTER_STATE_ERROR == -1);
+ spa_assert_se(PW_FILTER_STATE_UNCONNECTED == 0);
+ spa_assert_se(PW_FILTER_STATE_CONNECTING == 1);
+ spa_assert_se(PW_FILTER_STATE_PAUSED == 2);
+ spa_assert_se(PW_FILTER_STATE_STREAMING == 3);
+
+ spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_ERROR) != NULL);
+ spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_UNCONNECTED) != NULL);
+ spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_CONNECTING) != NULL);
+ spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_PAUSED) != NULL);
+ spa_assert_se(pw_filter_state_as_string(PW_FILTER_STATE_STREAMING) != NULL);
+}
+
+static void filter_destroy_error(void *data)
+{
+ spa_assert_not_reached();
+}
+static void filter_state_changed_error(void *data, enum pw_filter_state old,
+ enum pw_filter_state state, const char *error)
+{
+ spa_assert_not_reached();
+}
+static void filter_io_changed_error(void *data, void *port_data, uint32_t id, void *area, uint32_t size)
+{
+ spa_assert_not_reached();
+}
+static void filter_param_changed_error(void *data, void *port_data, uint32_t id, const struct spa_pod *format)
+{
+ spa_assert_not_reached();
+}
+static void filter_add_buffer_error(void *data, void *port_data, struct pw_buffer *buffer)
+{
+ spa_assert_not_reached();
+}
+static void filter_remove_buffer_error(void *data, void *port_data, struct pw_buffer *buffer)
+{
+ spa_assert_not_reached();
+}
+static void filter_process_error(void *data, struct spa_io_position *position)
+{
+ spa_assert_not_reached();
+}
+static void filter_drained_error(void *data)
+{
+ spa_assert_not_reached();
+}
+
+static const struct pw_filter_events filter_events_error =
+{
+ PW_VERSION_FILTER_EVENTS,
+ .destroy = filter_destroy_error,
+ .state_changed = filter_state_changed_error,
+ .io_changed = filter_io_changed_error,
+ .param_changed = filter_param_changed_error,
+ .add_buffer = filter_add_buffer_error,
+ .remove_buffer = filter_remove_buffer_error,
+ .process = filter_process_error,
+ .drained = filter_drained_error
+};
+
+static int destroy_count = 0;
+static void filter_destroy_count(void *data)
+{
+ destroy_count++;
+}
+static void test_create(void)
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct pw_filter *filter;
+ struct pw_filter_events filter_events = filter_events_error;
+ struct spa_hook listener = { 0, };
+ const char *error = NULL;
+
+ loop = pw_main_loop_new(NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
+ spa_assert_se(context != NULL);
+ core = pw_context_connect_self(context, NULL, 0);
+ spa_assert_se(core != NULL);
+ filter = pw_filter_new(core, "test", NULL);
+ spa_assert_se(filter != NULL);
+ pw_filter_add_listener(filter, &listener, &filter_events, filter);
+
+ /* check state */
+ spa_assert_se(pw_filter_get_state(filter, &error) == PW_FILTER_STATE_UNCONNECTED);
+ spa_assert_se(error == NULL);
+ /* check name */
+ spa_assert_se(spa_streq(pw_filter_get_name(filter), "test"));
+
+ /* check id, only when connected */
+ spa_assert_se(pw_filter_get_node_id(filter) == SPA_ID_INVALID);
+
+ /* check destroy */
+ destroy_count = 0;
+ filter_events.destroy = filter_destroy_count;
+ pw_filter_destroy(filter);
+ spa_assert_se(destroy_count == 1);
+
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+}
+
+static void test_properties(void)
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ const struct pw_properties *props;
+ struct pw_filter *filter;
+ struct pw_filter_events filter_events = filter_events_error;
+ struct spa_hook listener = { { NULL }, };
+ struct spa_dict_item items[3];
+
+ loop = pw_main_loop_new(NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
+ spa_assert_se(context != NULL);
+ core = pw_context_connect_self(context, NULL, 0);
+ spa_assert_se(core != NULL);
+ filter = pw_filter_new(core, "test",
+ pw_properties_new("foo", "bar",
+ "biz", "fuzz",
+ NULL));
+ spa_assert_se(filter != NULL);
+ pw_filter_add_listener(filter, &listener, &filter_events, filter);
+
+ props = pw_filter_get_properties(filter, NULL);
+ spa_assert_se(props != NULL);
+ spa_assert_se(spa_streq(pw_properties_get(props, "foo"), "bar"));
+ spa_assert_se(spa_streq(pw_properties_get(props, "biz"), "fuzz"));
+ spa_assert_se(pw_properties_get(props, "buzz") == NULL);
+
+ /* remove foo */
+ items[0] = SPA_DICT_ITEM_INIT("foo", NULL);
+ /* change biz */
+ items[1] = SPA_DICT_ITEM_INIT("biz", "buzz");
+ /* add buzz */
+ items[2] = SPA_DICT_ITEM_INIT("buzz", "frizz");
+ pw_filter_update_properties(filter, NULL, &SPA_DICT_INIT(items, 3));
+
+ spa_assert_se(props == pw_filter_get_properties(filter, NULL));
+ spa_assert_se(pw_properties_get(props, "foo") == NULL);
+ spa_assert_se(spa_streq(pw_properties_get(props, "biz"), "buzz"));
+ spa_assert_se(spa_streq(pw_properties_get(props, "buzz"), "frizz"));
+
+ /* check destroy */
+ destroy_count = 0;
+ filter_events.destroy = filter_destroy_count;
+ pw_context_destroy(context);
+ spa_assert_se(destroy_count == 1);
+
+ pw_main_loop_destroy(loop);
+}
+
+struct roundtrip_data
+{
+ struct pw_main_loop *loop;
+ int pending;
+ int done;
+};
+
+static void core_event_done(void *object, uint32_t id, int seq)
+{
+ struct roundtrip_data *data = object;
+ if (id == PW_ID_CORE && seq == data->pending) {
+ data->done = 1;
+ printf("done %d\n", seq);
+ pw_main_loop_quit(data->loop);
+ }
+}
+
+static int roundtrip(struct pw_core *core, struct pw_main_loop *loop)
+{
+ struct spa_hook core_listener;
+ struct roundtrip_data data = { .loop = loop };
+ const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = core_event_done,
+ };
+ spa_zero(core_listener);
+ pw_core_add_listener(core, &core_listener,
+ &core_events, &data);
+
+ data.pending = pw_core_sync(core, PW_ID_CORE, 0);
+ printf("sync %d\n", data.pending);
+
+ while (!data.done) {
+ pw_main_loop_run(loop);
+ }
+ spa_hook_remove(&core_listener);
+ return 0;
+}
+
+static int node_count = 0;
+static int port_count = 0;
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ printf("object: id:%u type:%s/%d\n", id, type, version);
+ if (spa_streq(type, PW_TYPE_INTERFACE_Port))
+ port_count++;
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Node))
+ node_count++;
+
+}
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ printf("object: id:%u\n", id);
+}
+
+struct port {
+ struct pw_filter *filter;
+};
+
+static void test_create_port(void)
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct pw_registry *registry;
+ struct pw_filter *filter;
+ struct spa_hook registry_listener = { 0, };
+ static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+ };
+ int res;
+ struct port *port;
+ enum pw_filter_state state;
+
+ loop = pw_main_loop_new(NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
+ spa_assert_se(context != NULL);
+ core = pw_context_connect_self(context, NULL, 0);
+ spa_assert_se(core != NULL);
+ filter = pw_filter_new(core, "test", NULL);
+ spa_assert_se(filter != NULL);
+
+ registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0);
+ spa_assert_se(registry != NULL);
+ pw_registry_add_listener(registry, &registry_listener,
+ &registry_events, NULL);
+
+ state = pw_filter_get_state(filter, NULL);
+ printf("state %s\n", pw_filter_state_as_string(state));
+ res = pw_filter_connect(filter, PW_FILTER_FLAG_RT_PROCESS, NULL, 0);
+ spa_assert_se(res >= 0);
+
+ printf("wait connect\n");
+ while (true) {
+ state = pw_filter_get_state(filter, NULL);
+ printf("state %s\n", pw_filter_state_as_string(state));
+ spa_assert_se(state != PW_FILTER_STATE_ERROR);
+
+ if (state == PW_FILTER_STATE_PAUSED)
+ break;
+
+ roundtrip(core, loop);
+ }
+ spa_assert_se(node_count == 1);
+
+ printf("add port\n");
+ /* make an audio DSP output port */
+ port = pw_filter_add_port(filter,
+ PW_DIRECTION_OUTPUT,
+ PW_FILTER_PORT_FLAG_MAP_BUFFERS,
+ sizeof(struct port),
+ pw_properties_new(
+ PW_KEY_FORMAT_DSP, "32 bit float mono audio",
+ PW_KEY_PORT_NAME, "output",
+ NULL),
+ NULL, 0);
+
+ printf("wait port\n");
+ roundtrip(core, loop);
+
+ spa_assert_se(port_count == 1);
+ printf("port added\n");
+
+ printf("remove port\n");
+ pw_filter_remove_port(port);
+ roundtrip(core, loop);
+
+ printf("destroy\n");
+ /* check destroy */
+ pw_filter_destroy(filter);
+
+ pw_proxy_destroy((struct pw_proxy*)registry);
+
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+}
+
+int main(int argc, char *argv[])
+{
+ pw_init(&argc, &argv);
+
+ test_abi();
+ test_create();
+ test_properties();
+ test_create_port();
+
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tests/test-interfaces.c b/src/tests/test-interfaces.c
new file mode 100644
index 0000000..86f772f
--- /dev/null
+++ b/src/tests/test-interfaces.c
@@ -0,0 +1,382 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+
+#define TEST_FUNC(a,b,func) \
+do { \
+ a.func = b.func; \
+ spa_assert_se(SPA_PTRDIFF(&a.func, &a) == SPA_PTRDIFF(&b.func, &b)); \
+} while(0)
+
+static void test_core_abi(void)
+{
+ static const struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_core_events *events,
+ void *data);
+ int (*hello) (void *object, uint32_t version);
+ int (*sync) (void *object, uint32_t id, int seq);
+ int (*pong) (void *object, uint32_t id, int seq);
+ int (*error) (void *object, uint32_t id, int seq, int res, const char *error);
+ struct pw_registry * (*get_registry) (void *object,
+ uint32_t version, size_t user_data_size);
+ void * (*create_object) (void *object,
+ const char *factory_name,
+ const char *type,
+ uint32_t version,
+ const struct spa_dict *props,
+ size_t user_data_size);
+ int (*destroy) (void *object, void *proxy);
+ } methods = { PW_VERSION_CORE_METHODS, };
+ static const struct {
+ uint32_t version;
+ void (*info) (void *data, const struct pw_core_info *info);
+ void (*done) (void *data, uint32_t id, int seq);
+ void (*ping) (void *data, uint32_t id, int seq);
+ void (*error) (void *data, uint32_t id, int seq, int res, const char *error);
+ void (*remove_id) (void *data, uint32_t id);
+ void (*bound_id) (void *data, uint32_t id, uint32_t global_id);
+ void (*add_mem) (void *data, uint32_t id, uint32_t type, int fd, uint32_t flags);
+ void (*remove_mem) (void *data, uint32_t id);
+ } events = { PW_VERSION_CORE_EVENTS, };
+
+ struct pw_core_events e;
+ struct pw_core_methods m;
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ TEST_FUNC(m, methods, hello);
+ TEST_FUNC(m, methods, sync);
+ TEST_FUNC(m, methods, pong);
+ TEST_FUNC(m, methods, error);
+ TEST_FUNC(m, methods, get_registry);
+ TEST_FUNC(m, methods, create_object);
+ TEST_FUNC(m, methods, destroy);
+ spa_assert_se(PW_VERSION_CORE_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, info);
+ TEST_FUNC(e, events, done);
+ TEST_FUNC(e, events, ping);
+ TEST_FUNC(e, events, error);
+ TEST_FUNC(e, events, remove_id);
+ TEST_FUNC(e, events, bound_id);
+ TEST_FUNC(e, events, add_mem);
+ TEST_FUNC(e, events, remove_mem);
+ spa_assert_se(PW_VERSION_CORE_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+static void test_registry_abi(void)
+{
+ struct pw_registry_methods m;
+ struct pw_registry_events e;
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_registry_events *events,
+ void *data);
+ void * (*bind) (void *object, uint32_t id, const char *type, uint32_t version,
+ size_t user_data_size);
+ int (*destroy) (void *object, uint32_t id);
+ } methods = { PW_VERSION_REGISTRY_METHODS, };
+ struct {
+ uint32_t version;
+ void (*global) (void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props);
+ void (*global_remove) (void *data, uint32_t id);
+ } events = { PW_VERSION_REGISTRY_EVENTS, };
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ TEST_FUNC(m, methods, bind);
+ TEST_FUNC(m, methods, destroy);
+ spa_assert_se(PW_VERSION_REGISTRY_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, global);
+ TEST_FUNC(e, events, global_remove);
+ spa_assert_se(PW_VERSION_REGISTRY_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+static void test_module_abi(void)
+{
+ struct pw_module_methods m;
+ struct pw_module_events e;
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_module_events *events,
+ void *data);
+ } methods = { PW_VERSION_MODULE_METHODS, };
+ struct {
+ uint32_t version;
+ void (*info) (void *data, const struct pw_module_info *info);
+ } events = { PW_VERSION_MODULE_EVENTS, };
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ spa_assert_se(PW_VERSION_MODULE_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, info);
+ spa_assert_se(PW_VERSION_MODULE_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+static void test_device_abi(void)
+{
+ struct pw_device_methods m;
+ struct pw_device_events e;
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_device_events *events,
+ void *data);
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+ int (*enum_params) (void *object, int seq, uint32_t id,
+ uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+ int (*set_param) (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ } methods = { PW_VERSION_DEVICE_METHODS, };
+ struct {
+ uint32_t version;
+ void (*info) (void *data, const struct pw_device_info *info);
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+ } events = { PW_VERSION_DEVICE_EVENTS, };
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ TEST_FUNC(m, methods, subscribe_params);
+ TEST_FUNC(m, methods, enum_params);
+ TEST_FUNC(m, methods, set_param);
+ spa_assert_se(PW_VERSION_DEVICE_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, info);
+ TEST_FUNC(e, events, param);
+ spa_assert_se(PW_VERSION_DEVICE_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+static void test_node_abi(void)
+{
+ struct pw_node_methods m;
+ struct pw_node_events e;
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_node_events *events,
+ void *data);
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+ int (*enum_params) (void *object, int seq, uint32_t id,
+ uint32_t start, uint32_t num, const struct spa_pod *filter);
+ int (*set_param) (void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ int (*send_command) (void *object, const struct spa_command *command);
+ } methods = { PW_VERSION_NODE_METHODS, };
+ struct {
+ uint32_t version;
+ void (*info) (void *data, const struct pw_node_info *info);
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+ } events = { PW_VERSION_NODE_EVENTS, };
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ TEST_FUNC(m, methods, subscribe_params);
+ TEST_FUNC(m, methods, enum_params);
+ TEST_FUNC(m, methods, set_param);
+ TEST_FUNC(m, methods, send_command);
+ spa_assert_se(PW_VERSION_NODE_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, info);
+ TEST_FUNC(e, events, param);
+ spa_assert_se(PW_VERSION_NODE_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+static void test_port_abi(void)
+{
+ struct pw_port_methods m;
+ struct pw_port_events e;
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_port_events *events,
+ void *data);
+ int (*subscribe_params) (void *object, uint32_t *ids, uint32_t n_ids);
+ int (*enum_params) (void *object, int seq, uint32_t id,
+ uint32_t start, uint32_t num, const struct spa_pod *filter);
+ } methods = { PW_VERSION_PORT_METHODS, };
+ struct {
+ uint32_t version;
+ void (*info) (void *data, const struct pw_port_info *info);
+ void (*param) (void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param);
+ } events = { PW_VERSION_PORT_EVENTS, };
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ TEST_FUNC(m, methods, enum_params);
+ spa_assert_se(PW_VERSION_PORT_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, info);
+ TEST_FUNC(e, events, param);
+ spa_assert_se(PW_VERSION_PORT_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+static void test_factory_abi(void)
+{
+ struct pw_factory_methods m;
+ struct pw_factory_events e;
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_factory_events *events,
+ void *data);
+ } methods = { PW_VERSION_FACTORY_METHODS, };
+ struct {
+ uint32_t version;
+ void (*info) (void *data, const struct pw_factory_info *info);
+ } events = { PW_VERSION_FACTORY_EVENTS, };
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ spa_assert_se(PW_VERSION_FACTORY_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, info);
+ spa_assert_se(PW_VERSION_FACTORY_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+static void test_client_abi(void)
+{
+ struct pw_client_methods m;
+ struct pw_client_events e;
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_client_events *events,
+ void *data);
+ int (*error) (void *object, uint32_t id, int res, const char *error);
+ int (*update_properties) (void *object, const struct spa_dict *props);
+ int (*get_permissions) (void *object, uint32_t index, uint32_t num);
+ int (*update_permissions) (void *object, uint32_t n_permissions,
+ const struct pw_permission *permissions);
+ } methods = { PW_VERSION_CLIENT_METHODS, };
+ struct {
+ uint32_t version;
+ void (*info) (void *data, const struct pw_client_info *info);
+ void (*permissions) (void *data, uint32_t index,
+ uint32_t n_permissions, const struct pw_permission *permissions);
+ } events = { PW_VERSION_CLIENT_EVENTS, };
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ TEST_FUNC(m, methods, error);
+ TEST_FUNC(m, methods, update_properties);
+ TEST_FUNC(m, methods, get_permissions);
+ TEST_FUNC(m, methods, update_permissions);
+ spa_assert_se(PW_VERSION_CLIENT_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, info);
+ TEST_FUNC(e, events, permissions);
+ spa_assert_se(PW_VERSION_CLIENT_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+static void test_link_abi(void)
+{
+ struct pw_link_methods m;
+ struct pw_link_events e;
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct pw_link_events *events,
+ void *data);
+ } methods = { PW_VERSION_LINK_METHODS, };
+ struct {
+ uint32_t version;
+ void (*info) (void *data, const struct pw_link_info *info);
+ } events = { PW_VERSION_LINK_EVENTS, };
+
+ TEST_FUNC(m, methods, version);
+ TEST_FUNC(m, methods, add_listener);
+ spa_assert_se(PW_VERSION_LINK_METHODS == 0);
+ spa_assert_se(sizeof(m) == sizeof(methods));
+
+ TEST_FUNC(e, events, version);
+ TEST_FUNC(e, events, info);
+ spa_assert_se(PW_VERSION_LINK_EVENTS == 0);
+ spa_assert_se(sizeof(e) == sizeof(events));
+}
+
+int main(int argc, char *argv[])
+{
+ pw_init(&argc, &argv);
+
+ test_core_abi();
+ test_registry_abi();
+ test_module_abi();
+ test_device_abi();
+ test_node_abi();
+ test_port_abi();
+ test_factory_abi();
+ test_client_abi();
+ test_link_abi();
+
+ return 0;
+}
diff --git a/src/tests/test-stream.c b/src/tests/test-stream.c
new file mode 100644
index 0000000..1e54fed
--- /dev/null
+++ b/src/tests/test-stream.c
@@ -0,0 +1,257 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <pipewire/pipewire.h>
+#include <pipewire/main-loop.h>
+#include <pipewire/stream.h>
+
+#include <spa/utils/string.h>
+
+#define TEST_FUNC(a,b,func) \
+do { \
+ a.func = b.func; \
+ spa_assert_se(SPA_PTRDIFF(&a.func, &a) == SPA_PTRDIFF(&b.func, &b)); \
+} while(0)
+
+static void test_abi(void)
+{
+ static const struct {
+ uint32_t version;
+ void (*destroy) (void *data);
+ void (*state_changed) (void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error);
+ void (*control_info) (void *data, uint32_t id, const struct pw_stream_control *control);
+ void (*io_changed) (void *data, uint32_t id, void *area, uint32_t size);
+ void (*param_changed) (void *data, uint32_t id, const struct spa_pod *param);
+ void (*add_buffer) (void *data, struct pw_buffer *buffer);
+ void (*remove_buffer) (void *data, struct pw_buffer *buffer);
+ void (*process) (void *data);
+ void (*drained) (void *data);
+ void (*command) (void *data, const struct spa_command *command);
+ void (*trigger_done) (void *data);
+ } test = { PW_VERSION_STREAM_EVENTS, NULL };
+
+ struct pw_stream_events ev;
+
+ TEST_FUNC(ev, test, destroy);
+ TEST_FUNC(ev, test, state_changed);
+ TEST_FUNC(ev, test, control_info);
+ TEST_FUNC(ev, test, io_changed);
+ TEST_FUNC(ev, test, param_changed);
+ TEST_FUNC(ev, test, add_buffer);
+ TEST_FUNC(ev, test, remove_buffer);
+ TEST_FUNC(ev, test, process);
+ TEST_FUNC(ev, test, drained);
+ TEST_FUNC(ev, test, command);
+ TEST_FUNC(ev, test, trigger_done);
+
+#if defined(__x86_64__) && defined(__LP64__)
+ spa_assert_se(sizeof(struct pw_buffer) == 32);
+ spa_assert_se(sizeof(struct pw_time) == 56);
+#else
+ fprintf(stderr, "%zd\n", sizeof(struct pw_buffer));
+ fprintf(stderr, "%zd\n", sizeof(struct pw_time));
+#endif
+
+ spa_assert_se(PW_VERSION_STREAM_EVENTS == 2);
+ spa_assert_se(sizeof(ev) == sizeof(test));
+
+ spa_assert_se(PW_STREAM_STATE_ERROR == -1);
+ spa_assert_se(PW_STREAM_STATE_UNCONNECTED == 0);
+ spa_assert_se(PW_STREAM_STATE_CONNECTING == 1);
+ spa_assert_se(PW_STREAM_STATE_PAUSED == 2);
+ spa_assert_se(PW_STREAM_STATE_STREAMING == 3);
+
+ spa_assert_se(pw_stream_state_as_string(PW_STREAM_STATE_ERROR) != NULL);
+ spa_assert_se(pw_stream_state_as_string(PW_STREAM_STATE_UNCONNECTED) != NULL);
+ spa_assert_se(pw_stream_state_as_string(PW_STREAM_STATE_CONNECTING) != NULL);
+ spa_assert_se(pw_stream_state_as_string(PW_STREAM_STATE_PAUSED) != NULL);
+ spa_assert_se(pw_stream_state_as_string(PW_STREAM_STATE_STREAMING) != NULL);
+}
+
+static void stream_destroy_error(void *data)
+{
+ spa_assert_not_reached();
+}
+static void stream_state_changed_error(void *data, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ spa_assert_not_reached();
+}
+static void stream_io_changed_error(void *data, uint32_t id, void *area, uint32_t size)
+{
+ spa_assert_not_reached();
+}
+static void stream_param_changed_error(void *data, uint32_t id, const struct spa_pod *format)
+{
+ spa_assert_not_reached();
+}
+static void stream_add_buffer_error(void *data, struct pw_buffer *buffer)
+{
+ spa_assert_not_reached();
+}
+static void stream_remove_buffer_error(void *data, struct pw_buffer *buffer)
+{
+ spa_assert_not_reached();
+}
+static void stream_process_error(void *data)
+{
+ spa_assert_not_reached();
+}
+static void stream_drained_error(void *data)
+{
+ spa_assert_not_reached();
+}
+
+static const struct pw_stream_events stream_events_error =
+{
+ PW_VERSION_STREAM_EVENTS,
+ .destroy = stream_destroy_error,
+ .state_changed = stream_state_changed_error,
+ .io_changed = stream_io_changed_error,
+ .param_changed = stream_param_changed_error,
+ .add_buffer = stream_add_buffer_error,
+ .remove_buffer = stream_remove_buffer_error,
+ .process = stream_process_error,
+ .drained = stream_drained_error
+};
+
+static int destroy_count = 0;
+static void stream_destroy_count(void *data)
+{
+ destroy_count++;
+}
+static void test_create(void)
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct pw_stream *stream;
+ struct pw_stream_events stream_events = stream_events_error;
+ struct spa_hook listener = { 0, };
+ const char *error = NULL;
+ struct pw_time tm;
+
+ loop = pw_main_loop_new(NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
+ spa_assert_se(context != NULL);
+ core = pw_context_connect_self(context, NULL, 0);
+ spa_assert_se(core != NULL);
+ stream = pw_stream_new(core, "test", NULL);
+ spa_assert_se(stream != NULL);
+ pw_stream_add_listener(stream, &listener, &stream_events, stream);
+
+ /* check state */
+ spa_assert_se(pw_stream_get_state(stream, &error) == PW_STREAM_STATE_UNCONNECTED);
+ spa_assert_se(error == NULL);
+ /* check name */
+ spa_assert_se(spa_streq(pw_stream_get_name(stream), "test"));
+
+ /* check id, only when connected */
+ spa_assert_se(pw_stream_get_node_id(stream) == SPA_ID_INVALID);
+
+ spa_assert_se(pw_stream_get_time_n(stream, &tm, sizeof(tm)) == 0);
+ spa_assert_se(tm.now == 0);
+ spa_assert_se(tm.rate.num == 0);
+ spa_assert_se(tm.rate.denom == 0);
+ spa_assert_se(tm.ticks == 0);
+ spa_assert_se(tm.delay == 0);
+ spa_assert_se(tm.queued == 0);
+ spa_assert_se(tm.buffered == 0);
+
+ spa_assert_se(pw_stream_dequeue_buffer(stream) == NULL);
+
+ /* check destroy */
+ destroy_count = 0;
+ stream_events.destroy = stream_destroy_count;
+ pw_stream_destroy(stream);
+ spa_assert_se(destroy_count == 1);
+
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+}
+
+static void test_properties(void)
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ const struct pw_properties *props;
+ struct pw_stream *stream;
+ struct pw_stream_events stream_events = stream_events_error;
+ struct spa_hook listener = { { NULL }, };
+ struct spa_dict_item items[3];
+
+ loop = pw_main_loop_new(NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 12);
+ spa_assert_se(context != NULL);
+ core = pw_context_connect_self(context, NULL, 0);
+ spa_assert_se(core != NULL);
+ stream = pw_stream_new(core, "test",
+ pw_properties_new("foo", "bar",
+ "biz", "fuzz",
+ NULL));
+ spa_assert_se(stream != NULL);
+ pw_stream_add_listener(stream, &listener, &stream_events, stream);
+
+ props = pw_stream_get_properties(stream);
+ spa_assert_se(props != NULL);
+ spa_assert_se(spa_streq(pw_properties_get(props, "foo"), "bar"));
+ spa_assert_se(spa_streq(pw_properties_get(props, "biz"), "fuzz"));
+ spa_assert_se(pw_properties_get(props, "buzz") == NULL);
+
+ /* remove foo */
+ items[0] = SPA_DICT_ITEM_INIT("foo", NULL);
+ /* change biz */
+ items[1] = SPA_DICT_ITEM_INIT("biz", "buzz");
+ /* add buzz */
+ items[2] = SPA_DICT_ITEM_INIT("buzz", "frizz");
+ pw_stream_update_properties(stream, &SPA_DICT_INIT(items, 3));
+
+ spa_assert_se(props == pw_stream_get_properties(stream));
+ spa_assert_se(pw_properties_get(props, "foo") == NULL);
+ spa_assert_se(spa_streq(pw_properties_get(props, "biz"), "buzz"));
+ spa_assert_se(spa_streq(pw_properties_get(props, "buzz"), "frizz"));
+
+ /* check destroy */
+ destroy_count = 0;
+ stream_events.destroy = stream_destroy_count;
+ pw_context_destroy(context);
+ spa_assert_se(destroy_count == 1);
+
+ pw_main_loop_destroy(loop);
+}
+
+int main(int argc, char *argv[])
+{
+ pw_init(&argc, &argv);
+
+ test_abi();
+ test_create();
+ test_properties();
+
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/dsffile.c b/src/tools/dsffile.c
new file mode 100644
index 0000000..9c6ce0c
--- /dev/null
+++ b/src/tools/dsffile.c
@@ -0,0 +1,266 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <math.h>
+
+#include <spa/utils/string.h>
+
+#include "dsffile.h"
+
+struct dsf_file {
+ uint8_t *data;
+ size_t size;
+
+ int mode;
+ int fd;
+
+ struct dsf_file_info info;
+
+ uint8_t *p;
+ size_t offset;
+};
+
+static inline uint32_t parse_le32(const uint8_t *in)
+{
+ return in[0] | (in[1] << 8) | (in[2] << 16) | (in[3] << 24);
+}
+
+static inline uint64_t parse_le64(const uint8_t *in)
+{
+ uint64_t res = in[0];
+ res |= ((uint64_t)in[1]) << 8;
+ res |= ((uint64_t)in[2]) << 16;
+ res |= ((uint64_t)in[3]) << 24;
+ res |= ((uint64_t)in[4]) << 32;
+ res |= ((uint64_t)in[5]) << 40;
+ res |= ((uint64_t)in[6]) << 48;
+ res |= ((uint64_t)in[7]) << 56;
+ return res;
+}
+
+static inline int f_avail(struct dsf_file *f)
+{
+ if (f->p < f->data + f->size)
+ return f->size + f->data - f->p;
+ return 0;
+}
+
+static int read_DSD(struct dsf_file *f)
+{
+ uint64_t size;
+
+ if (f_avail(f) < 28 ||
+ memcmp(f->p, "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;
+ return 0;
+}
+
+static int read_fmt(struct dsf_file *f)
+{
+ uint64_t size;
+
+ if (f_avail(f) < 52 ||
+ memcmp(f->p, "fmt ", 4) != 0)
+ return -EINVAL;
+
+ size = parse_le64(f->p + 4); /* size of this chunk */
+ if (parse_le32(f->p + 12) != 1) /* version */
+ return -EINVAL;
+ if (parse_le32(f->p + 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;
+ return 0;
+}
+
+static int read_data(struct dsf_file *f)
+{
+ uint64_t size;
+
+ if (f_avail(f) < 12 ||
+ memcmp(f->p, "data", 4) != 0)
+ return -EINVAL;
+
+ size = parse_le64(f->p + 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;
+ }
+
+ f->p = f->data;
+
+ if ((res = read_DSD(f)) < 0)
+ goto exit_unmap;
+ if ((res = read_fmt(f)) < 0)
+ goto exit_unmap;
+ if ((res = read_data(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);
+exit:
+ return res;
+}
+
+struct dsf_file *
+dsf_file_open(const char *filename, const char *mode, struct dsf_file_info *info)
+{
+ int res;
+ struct dsf_file *f;
+
+ f = calloc(1, sizeof(struct dsf_file));
+ if (f == NULL)
+ return NULL;
+
+ if (spa_streq(mode, "r")) {
+ if ((res = open_read(f, filename, info)) < 0)
+ goto exit_free;
+ } else {
+ res = -EINVAL;
+ goto exit_free;
+ }
+ return f;
+
+exit_free:
+ free(f);
+ errno = -res;
+ return NULL;
+}
+
+static const uint8_t bitrev[256] = {
+ 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
+ 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
+ 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
+ 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
+ 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
+ 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
+ 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
+ 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
+ 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
+ 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
+ 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
+ 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
+ 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
+ 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
+ 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
+ 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff,
+};
+
+ssize_t
+dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_layout *layout)
+{
+ uint8_t *d = data;
+ int step = SPA_ABS(layout->interleave);
+ bool rev = layout->lsb != f->info.lsb;
+ size_t total, block, offset, pos, scale;
+
+ block = f->offset / f->info.blocksize;
+ offset = block * f->info.blocksize * f->info.channels;
+ pos = f->offset % f->info.blocksize;
+ scale = SPA_CLAMP(f->info.rate / (44100u * 64u), 1u, 4u);
+
+ samples *= step;
+ samples *= scale;
+
+ for (total = 0; total < samples && offset + pos < f->info.length; total++) {
+ const uint8_t *s = f->p + offset + pos;
+ uint32_t i;
+
+ for (i = 0; i < layout->channels; i++) {
+ const uint8_t *c = &s[f->info.blocksize * i];
+ int j;
+
+ if (layout->interleave > 0) {
+ for (j = 0; j < step; j++)
+ *d++ = rev ? bitrev[c[j]] : c[j];
+ } else {
+ for (j = step-1; j >= 0; j--)
+ *d++ = rev ? bitrev[c[j]] : c[j];
+ }
+ }
+ pos += step;
+ if (pos == f->info.blocksize) {
+ pos = 0;
+ offset += f->info.blocksize * f->info.channels;
+ }
+ }
+ f->offset += total * step;
+
+ return total;
+}
+
+int dsf_file_close(struct dsf_file *f)
+{
+ if (f->mode == 1) {
+ munmap(f->data, f->size);
+ } else
+ return -EINVAL;
+
+ close(f->fd);
+ free(f);
+ return 0;
+}
diff --git a/src/tools/dsffile.h b/src/tools/dsffile.h
new file mode 100644
index 0000000..616dbb1
--- /dev/null
+++ b/src/tools/dsffile.h
@@ -0,0 +1,52 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include <spa/utils/defs.h>
+
+struct dsf_file;
+
+struct dsf_file_info {
+ uint32_t channel_type;
+ uint32_t channels;
+ uint32_t rate;
+ bool lsb;
+ uint64_t samples;
+ uint64_t length;
+ uint32_t blocksize;
+};
+
+struct dsf_layout {
+ int32_t interleave;
+ uint32_t channels;
+ bool lsb;
+};
+
+struct dsf_file * dsf_file_open(const char *filename, const char *mode, struct dsf_file_info *info);
+
+ssize_t dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_layout *layout);
+
+int dsf_file_close(struct dsf_file *f);
+
diff --git a/src/tools/meson.build b/src/tools/meson.build
new file mode 100644
index 0000000..6623e7a
--- /dev/null
+++ b/src/tools/meson.build
@@ -0,0 +1,88 @@
+tools_sources = [
+ [ 'pw-mon', [ 'pw-mon.c' ] ],
+ [ 'pw-dot', [ 'pw-dot.c' ] ],
+ [ 'pw-dump', [ 'pw-dump.c' ] ],
+ [ 'pw-profiler', [ 'pw-profiler.c' ] ],
+ [ 'pw-mididump', [ 'pw-mididump.c', 'midifile.c' ] ],
+ [ 'pw-metadata', [ 'pw-metadata.c' ] ],
+ [ 'pw-loopback', [ 'pw-loopback.c' ] ],
+ [ 'pw-link', [ 'pw-link.c' ] ],
+]
+
+foreach t : tools_sources
+ executable(t.get(0),
+ t.get(1),
+ install: true,
+ dependencies : [pipewire_dep, mathlib],
+ )
+endforeach
+
+executable('pw-cli',
+ 'pw-cli.c',
+ install: true,
+ dependencies: [pipewire_dep, readline_dep]
+)
+
+if ncurses_dep.found()
+ executable('pw-top',
+ 'pw-top.c',
+ install: true,
+ dependencies : [pipewire_dep, ncurses_dep],
+ )
+endif
+
+build_pw_cat = false
+build_pw_cat_with_ffmpeg = false
+pwcat_deps = [ sndfile_dep ]
+
+if get_option('pw-cat').allowed() and sndfile_dep.found()
+ build_pw_cat = true
+
+ if pw_cat_ffmpeg.allowed() and avcodec_dep.found() and avformat_dep.found()
+ pwcat_deps += avcodec_dep
+ pwcat_deps += avformat_dep
+ build_pw_cat_with_ffmpeg = true
+ endif
+
+ pwcat_sources = [
+ 'pw-cat.c',
+ 'midifile.c',
+ 'dsffile.c',
+ ]
+
+ pwcat_aliases = [
+ 'pw-play',
+ 'pw-record',
+ 'pw-midiplay',
+ 'pw-midirecord',
+ 'pw-dsdplay',
+ ]
+
+ executable('pw-cat',
+ pwcat_sources,
+ install: true,
+ dependencies : [pwcat_deps, pipewire_dep, mathlib],
+ )
+
+ foreach alias : pwcat_aliases
+ dst = pipewire_bindir / alias
+ cmd = 'ln -fs @0@ $DESTDIR@1@'.format('pw-cat', dst)
+ meson.add_install_script('sh', '-c', cmd)
+ endforeach
+elif not sndfile_dep.found() and get_option('pw-cat').enabled()
+ error('pw-cat is enabled but required dependency `sndfile` was not found.')
+endif
+summary({'Build pw-cat tool': build_pw_cat}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool')
+if build_pw_cat
+ summary({'Build pw-cat with FFmpeg integration': build_pw_cat_with_ffmpeg}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool')
+endif
+
+if dbus_dep.found()
+ executable('pw-reserve',
+ 'reserve.h',
+ 'reserve.c',
+ 'pw-reserve.c',
+ install: true,
+ dependencies : [dbus_dep, pipewire_dep],
+ )
+endif
diff --git a/src/tools/midifile.c b/src/tools/midifile.c
new file mode 100644
index 0000000..5276ade
--- /dev/null
+++ b/src/tools/midifile.c
@@ -0,0 +1,740 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <math.h>
+
+#include <spa/utils/string.h>
+
+#include "midifile.h"
+
+#define DEFAULT_TEMPO 500000 /* 500ms per quarter note (120 BPM) is the default */
+
+struct midi_track {
+ uint16_t id;
+
+ uint8_t *data;
+ uint32_t size;
+
+ 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;
+
+ struct midi_file_info info;
+ uint32_t length;
+ uint32_t tempo;
+
+ uint8_t *p;
+ int64_t tick;
+ double tick_sec;
+ double tick_start;
+
+ struct midi_track tracks[64];
+};
+
+static inline uint16_t parse_be16(const uint8_t *in)
+{
+ return (in[0] << 8) | in[1];
+}
+
+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)
+{
+ if (mf->p < mf->data + mf->size)
+ return mf->size + mf->data - mf->p;
+ return 0;
+}
+
+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;
+ tr->eof = true;
+ return 0;
+}
+
+static int read_mthd(struct midi_file *mf)
+{
+ if (mf_avail(mf) < 14 ||
+ memcmp(mf->p, "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->p += 14;
+ return 0;
+}
+
+static int read_mtrk(struct midi_file *mf, struct midi_track *track)
+{
+ if (mf_avail(mf) < 8 ||
+ memcmp(mf->p, "MTrk", 4) != 0)
+ return -EINVAL;
+
+ 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)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int parse_varlen(struct midi_file *mf, struct midi_track *tr, uint32_t *result)
+{
+ uint32_t value = 0;
+
+ while (tr_avail(tr) > 0) {
+ uint8_t b = *tr->p++;
+ value = (value << 7) | (b & 0x7f);
+ if ((b & 0x80) == 0)
+ break;
+ }
+ *result = value;
+ return 0;
+}
+
+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;
+ }
+
+ mf->p = mf->data;
+
+ if ((res = read_mthd(mf)) < 0)
+ goto exit_unmap;
+
+ 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;
+
+ if ((res = parse_varlen(mf, tr, &delta_time)) < 0)
+ goto exit_unmap;
+
+ tr->tick = delta_time;
+ tr->id = i;
+ }
+ mf->mode = 1;
+ *info = mf->info;
+ return 0;
+
+exit_unmap:
+ munmap(mf->data, mf->size);
+exit_close:
+ close(mf->fd);
+exit:
+ return res;
+}
+
+static inline int write_n(int fd, const void *buf, int count)
+{
+ return write(fd, buf, count) == (ssize_t)count ? count : -errno;
+}
+
+static inline int write_be16(int fd, uint16_t val)
+{
+ uint8_t buf[2] = { val >> 8, val };
+ return write_n(fd, buf, 2);
+}
+
+static inline int write_be32(int fd, uint32_t val)
+{
+ uint8_t buf[4] = { val >> 24, val >> 16, val >> 8, val };
+ return write_n(fd, buf, 4);
+}
+
+#define CHECK_RES(expr) if ((res = (expr)) < 0) return res
+
+static int write_headers(struct midi_file *mf)
+{
+ struct midi_track *tr = &mf->tracks[0];
+ int res;
+
+ lseek(mf->fd, 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->fd, "MTrk", 4));
+ CHECK_RES(write_be32(mf->fd, tr->size));
+
+ return 0;
+}
+
+static int open_write(struct midi_file *mf, const char *filename, struct midi_file_info *info)
+{
+ int res;
+
+ if (info->format != 0)
+ return -EINVAL;
+ if (info->ntracks == 0)
+ info->ntracks = 1;
+ else if (info->ntracks != 1)
+ return -EINVAL;
+ if (info->division == 0)
+ info->division = 96;
+
+ if ((mf->fd = open(filename, O_WRONLY | O_CREAT, 0660)) < 0) {
+ res = -errno;
+ goto exit;
+ }
+ mf->mode = 2;
+ mf->tempo = DEFAULT_TEMPO;
+ mf->info = *info;
+
+ res = write_headers(mf);
+exit:
+ return res;
+}
+
+struct midi_file *
+midi_file_open(const char *filename, const char *mode, struct midi_file_info *info)
+{
+ int res;
+ struct midi_file *mf;
+
+ mf = calloc(1, sizeof(struct midi_file));
+ if (mf == NULL)
+ return NULL;
+
+ if (spa_streq(mode, "r")) {
+ if ((res = open_read(mf, filename, info)) < 0)
+ goto exit_free;
+ } else if (spa_streq(mode, "w")) {
+ if ((res = open_write(mf, filename, info)) < 0)
+ goto exit_free;
+ } else {
+ res = -EINVAL;
+ goto exit_free;
+ }
+ return mf;
+
+exit_free:
+ free(mf);
+ errno = -res;
+ return NULL;
+}
+
+int midi_file_close(struct midi_file *mf)
+{
+ int res;
+
+ if (mf->mode == 1) {
+ munmap(mf->data, mf->size);
+ } else if (mf->mode == 2) {
+ uint8_t buf[4] = { 0x00, 0xff, 0x2f, 0x00 };
+ CHECK_RES(write_n(mf->fd, buf, 4));
+ mf->tracks[0].size += 4;
+ CHECK_RES(write_headers(mf));
+ } else
+ return -EINVAL;
+
+ close(mf->fd);
+ free(mf);
+ return 0;
+}
+
+static int peek_next(struct midi_file *mf, struct midi_event *ev)
+{
+ struct midi_track *tr, *found = NULL;
+ uint16_t i;
+
+ for (i = 0; i < mf->info.ntracks; i++) {
+ tr = &mf->tracks[i];
+ if (tr_avail(tr) == 0)
+ continue;
+ if (found == NULL || tr->tick < found->tick)
+ found = tr;
+ }
+ if (found == NULL)
+ return 0;
+
+ ev->track = found->id;
+ ev->sec = mf->tick_sec + ((found->tick - mf->tick_start) * (double)mf->tempo) / (1000000.0 * mf->info.division);
+ return 1;
+}
+
+int midi_file_next_time(struct midi_file *mf, double *sec)
+{
+ struct midi_event ev;
+ int res;
+
+ if ((res = peek_next(mf, &ev)) <= 0)
+ return res;
+
+ *sec = ev.sec;
+ return 1;
+}
+
+int midi_file_read_event(struct midi_file *mf, struct midi_event *event)
+{
+ struct midi_track *tr;
+ uint32_t delta_time, size;
+ uint8_t status, meta;
+ int res, running;
+
+ if ((res = peek_next(mf, event)) <= 0)
+ return res;
+
+ tr = &mf->tracks[event->track];
+ status = *tr->p;
+
+ running = (status & 0x80) == 0;
+ if (running) {
+ status = tr->event[0];
+ event->data = tr->event;
+ } else {
+ event->data = tr->p++;
+ tr->event[0] = status;
+ }
+
+ switch (status) {
+ case 0xc0 ... 0xdf:
+ size = 2;
+ break;
+
+ case 0x80 ... 0xbf:
+ case 0xe0 ... 0xef:
+ size = 3;
+ break;
+
+ case 0xff:
+ meta = *tr->p++;
+
+ if ((res = parse_varlen(mf, tr, &size)) < 0)
+ return res;
+
+ event->meta.offset = tr->p - event->data;
+ event->meta.size = size;
+
+ 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];
+ break;
+ }
+ size += tr->p - event->data;
+ break;
+
+ case 0xf0:
+ case 0xf7:
+ if ((res = parse_varlen(mf, tr, &size)) < 0)
+ return res;
+ size += tr->p - event->data;
+ 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 ((res = parse_varlen(mf, tr, &delta_time)) < 0)
+ return res;
+
+ tr->tick += delta_time;
+ return 1;
+}
+
+static int write_varlen(struct midi_file *mf, struct midi_track *tr, uint32_t value)
+{
+ uint64_t buffer;
+ uint8_t b;
+ int res;
+
+ buffer = value & 0x7f;
+ while ((value >>= 7)) {
+ buffer <<= 8;
+ buffer |= ((value & 0x7f) | 0x80);
+ }
+ do {
+ b = buffer & 0xff;
+ CHECK_RES(write_n(mf->fd, &b, 1));
+ tr->size++;
+ buffer >>= 8;
+ } while (b & 0x80);
+
+ return 0;
+}
+
+int midi_file_write_event(struct midi_file *mf, const struct midi_event *event)
+{
+ struct midi_track *tr;
+ uint32_t tick;
+ int res;
+
+ 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);
+
+ tr = &mf->tracks[event->track];
+
+ tick = event->sec * (1000000.0 * mf->info.division) / (double)mf->tempo;
+
+ 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;
+
+ return 0;
+}
+
+static const char * const event_names[] = {
+ "Text", "Copyright", "Sequence/Track Name",
+ "Instrument", "Lyric", "Marker", "Cue Point",
+ "Program Name", "Device (Port) Name"
+};
+
+static const char * const note_names[] = {
+ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
+};
+
+static const char * const controller_names[128] = {
+ [0] = "Bank Select (coarse)",
+ [1] = "Modulation Wheel (coarse)",
+ [2] = "Breath controller (coarse)",
+ [4] = "Foot Pedal (coarse)",
+ [5] = "Portamento Time (coarse)",
+ [6] = "Data Entry (coarse)",
+ [7] = "Volume (coarse)",
+ [8] = "Balance (coarse)",
+ [10] = "Pan position (coarse)",
+ [11] = "Expression (coarse)",
+ [12] = "Effect Control 1 (coarse)",
+ [13] = "Effect Control 2 (coarse)",
+ [16] = "General Purpose Slider 1",
+ [17] = "General Purpose Slider 2",
+ [18] = "General Purpose Slider 3",
+ [19] = "General Purpose Slider 4",
+ [32] = "Bank Select (fine)",
+ [33] = "Modulation Wheel (fine)",
+ [34] = "Breath (fine)",
+ [36] = "Foot Pedal (fine)",
+ [37] = "Portamento Time (fine)",
+ [38] = "Data Entry (fine)",
+ [39] = "Volume (fine)",
+ [40] = "Balance (fine)",
+ [42] = "Pan position (fine)",
+ [43] = "Expression (fine)",
+ [44] = "Effect Control 1 (fine)",
+ [45] = "Effect Control 2 (fine)",
+ [64] = "Hold Pedal (on/off)",
+ [65] = "Portamento (on/off)",
+ [66] = "Sustenuto Pedal (on/off)",
+ [67] = "Soft Pedal (on/off)",
+ [68] = "Legato Pedal (on/off)",
+ [69] = "Hold 2 Pedal (on/off)",
+ [70] = "Sound Variation",
+ [71] = "Sound Timbre",
+ [72] = "Sound Release Time",
+ [73] = "Sound Attack Time",
+ [74] = "Sound Brightness",
+ [75] = "Sound Control 6",
+ [76] = "Sound Control 7",
+ [77] = "Sound Control 8",
+ [78] = "Sound Control 9",
+ [79] = "Sound Control 10",
+ [80] = "General Purpose Button 1 (on/off)",
+ [81] = "General Purpose Button 2 (on/off)",
+ [82] = "General Purpose Button 3 (on/off)",
+ [83] = "General Purpose Button 4 (on/off)",
+ [91] = "Effects Level",
+ [92] = "Tremulo Level",
+ [93] = "Chorus Level",
+ [94] = "Celeste Level",
+ [95] = "Phaser Level",
+ [96] = "Data Button increment",
+ [97] = "Data Button decrement",
+ [98] = "Non-registered Parameter (fine)",
+ [99] = "Non-registered Parameter (coarse)",
+ [100] = "Registered Parameter (fine)",
+ [101] = "Registered Parameter (coarse)",
+ [120] = "All Sound Off",
+ [121] = "All Controllers Off",
+ [122] = "Local Keyboard (on/off)",
+ [123] = "All Notes Off",
+ [124] = "Omni Mode Off",
+ [125] = "Omni Mode On",
+ [126] = "Mono Operation",
+ [127] = "Poly Operation",
+};
+
+static const char * const program_names[] = {
+ "Acoustic Grand", "Bright Acoustic", "Electric Grand", "Honky-Tonk",
+ "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet",
+ "Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba",
+ "Xylophone", "Tubular Bells", "Dulcimer", "Drawbar Organ", "Percussive Organ",
+ "Rock Organ", "Church Organ", "Reed Organ", "Accoridan", "Harmonica",
+ "Tango Accordion", "Nylon String Guitar", "Steel String Guitar",
+ "Electric Jazz Guitar", "Electric Clean Guitar", "Electric Muted Guitar",
+ "Overdriven Guitar", "Distortion Guitar", "Guitar Harmonics",
+ "Acoustic Bass", "Electric Bass (fingered)", "Electric Bass (picked)",
+ "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2",
+ "Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings", "Pizzicato Strings",
+ "Orchestral Strings", "Timpani", "String Ensemble 1", "String Ensemble 2",
+ "SynthStrings 1", "SynthStrings 2", "Choir Aahs", "Voice Oohs", "Synth Voice",
+ "Orchestra Hit", "Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn",
+ "Brass Section", "SynthBrass 1", "SynthBrass 2", "Soprano Sax", "Alto Sax",
+ "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet",
+ "Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Skakuhachi",
+ "Whistle", "Ocarina", "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)",
+ "Lead 4 (chiff)", "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)",
+ "Lead 8 (bass+lead)", "Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)",
+ "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)",
+ "Pad 8 (sweep)", "FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)",
+ "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)",
+ "FX 8 (sci-fi)", "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bagpipe",
+ "Fiddle", "Shanai", "Tinkle Bell", "Agogo", "Steel Drums", "Woodblock",
+ "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal", "Guitar Fret Noise",
+ "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter",
+ "Applause", "Gunshot"
+};
+
+static const char * const smpte_rates[] = {
+ "24 fps",
+ "25 fps",
+ "30 fps (drop frame)",
+ "30 fps (non drop frame)"
+};
+
+static const char * const major_keys[] = {
+ "Unknown major", "Fb", "Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F",
+ "C", "G", "D", "A", "E", "B", "F#", "C#", "G#", "Unknown major"
+};
+
+static const char * const minor_keys[] = {
+ "Unknown minor", "Dbm", "Abm", "Ebm", "Bbm", "Fm", "Cm", "Gm", "Dm",
+ "Am", "Em", "Bm", "F#m", "C#m", "G#m", "D#m", "A#m", "E#m", "Unknown minor"
+};
+
+static const char *controller_name(uint8_t ctrl)
+{
+ if (ctrl > 127 ||
+ controller_names[ctrl] == NULL)
+ return "Unknown";
+ return controller_names[ctrl];
+}
+
+static void dump_mem(FILE *out, const char *label, uint8_t *data, uint32_t size)
+{
+ fprintf(out, "%s: ", label);
+ while (size--)
+ fprintf(out, "%02x ", *data++);
+}
+
+int midi_file_dump_event(FILE *out, const struct midi_event *ev)
+{
+ fprintf(out, "track:%2d sec:%f ", ev->track, ev->sec);
+
+ switch (ev->data[0]) {
+ case 0x80 ... 0x8f:
+ fprintf(out, "Note Off (channel %2d): note %3s%d, velocity %3d",
+ (ev->data[0] & 0x0f) + 1,
+ note_names[ev->data[1] % 12], ev->data[1] / 12 -1,
+ ev->data[2]);
+ break;
+ case 0x90 ... 0x9f:
+ fprintf(out, "Note On (channel %2d): note %3s%d, velocity %3d",
+ (ev->data[0] & 0x0f) + 1,
+ note_names[ev->data[1] % 12], ev->data[1] / 12 -1,
+ ev->data[2]);
+ break;
+ case 0xa0 ... 0xaf:
+ fprintf(out, "Aftertouch (channel %2d): note %3s%d, pressure %3d",
+ (ev->data[0] & 0x0f) + 1,
+ note_names[ev->data[1] % 12], ev->data[1] / 12 -1,
+ ev->data[2]);
+ break;
+ case 0xb0 ... 0xbf:
+ fprintf(out, "Controller (channel %2d): controller %3d (%s), value %3d",
+ (ev->data[0] & 0x0f) + 1, ev->data[1],
+ controller_name(ev->data[1]), ev->data[2]);
+ break;
+ case 0xc0 ... 0xcf:
+ fprintf(out, "Program (channel %2d): program %3d (%s)",
+ (ev->data[0] & 0x0f) + 1, ev->data[1],
+ program_names[ev->data[1]]);
+ break;
+ case 0xd0 ... 0xdf:
+ fprintf(out, "Channel Pressure (channel %2d): pressure %3d",
+ (ev->data[0] & 0x0f) + 1, ev->data[1]);
+ break;
+ case 0xe0 ... 0xef:
+ fprintf(out, "Pitch Bend (channel %2d): value %d", (ev->data[0] & 0x0f) + 1,
+ ((int)ev->data[2] << 7 | ev->data[1]) - 0x2000);
+ break;
+ case 0xf0:
+ case 0xf7:
+ dump_mem(out, "SysEx", ev->data, ev->size);
+ break;
+ case 0xf1:
+ fprintf(out, "MIDI Time Code Quarter Frame: type %d values %d",
+ ev->data[0] >> 4, ev->data[0] & 0xf);
+ break;
+ case 0xf2:
+ fprintf(out, "Song Position Pointer: value %d",
+ ((int)ev->data[1] << 7 | ev->data[0]));
+ break;
+ case 0xf3:
+ fprintf(out, "Song Select: value %d", (ev->data[0] & 0x7f));
+ break;
+ case 0xf6:
+ fprintf(out, "Tune Request");
+ break;
+ case 0xf8:
+ fprintf(out, "Timing Clock");
+ break;
+ case 0xfa:
+ fprintf(out, "Start Sequence");
+ break;
+ case 0xfb:
+ fprintf(out, "Continue Sequence");
+ break;
+ case 0xfc:
+ fprintf(out, "Stop Sequence");
+ break;
+ case 0xfe:
+ fprintf(out, "Active Sensing");
+ break;
+ case 0xff:
+ fprintf(out, "Meta: ");
+ switch (ev->data[1]) {
+ case 0x00:
+ fprintf(out, "Sequence Number %3d %3d", ev->data[3], ev->data[4]);
+ break;
+ case 0x01 ... 0x09:
+ fprintf(out, "%s: %s", event_names[ev->data[1] - 1], &ev->data[ev->meta.offset]);
+ break;
+ case 0x20:
+ fprintf(out, "Channel Prefix: %03d", ev->data[3]);
+ break;
+ case 0x21:
+ fprintf(out, "Midi Port: %03d", ev->data[3]);
+ break;
+ case 0x2f:
+ fprintf(out, "End Of Track");
+ break;
+ case 0x51:
+ fprintf(out, "Tempo: %d microseconds per quarter note, %.2f BPM",
+ ev->meta.parsed.tempo.uspqn,
+ 60000000.0 / (double)ev->meta.parsed.tempo.uspqn);
+ 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]);
+ 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]);
+ break;
+ case 0x59:
+ {
+ int sf = ev->data[3];
+ fprintf(out, "Key Signature: %d %s: %s", abs(sf),
+ sf > 0 ? "sharps" : "flats",
+ ev->data[4] == 0 ?
+ major_keys[SPA_CLAMP(sf + 9, 0, 18)] :
+ minor_keys[SPA_CLAMP(sf + 9, 0, 18)]);
+ break;
+ }
+ case 0x7f:
+ dump_mem(out, "Sequencer", ev->data, ev->size);
+ break;
+ default:
+ dump_mem(out, "Invalid", ev->data, ev->size);
+ }
+ break;
+ default:
+ dump_mem(out, "Unknown", ev->data, ev->size);
+ break;
+ }
+ fprintf(out, "\n");
+ return 0;
+}
diff --git a/src/tools/midifile.h b/src/tools/midifile.h
new file mode 100644
index 0000000..6c69df4
--- /dev/null
+++ b/src/tools/midifile.h
@@ -0,0 +1,64 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include <spa/utils/defs.h>
+
+struct midi_file;
+
+struct midi_event {
+ uint32_t track;
+ double sec;
+ uint8_t *data;
+ uint32_t size;
+ struct {
+ uint32_t offset;
+ uint32_t size;
+ union {
+ struct {
+ uint32_t uspqn; /* microseconds per quarter note */
+ } tempo;
+ } parsed;
+ } meta;
+};
+
+struct midi_file_info {
+ uint16_t format;
+ uint16_t ntracks;
+ uint16_t division;
+};
+
+struct midi_file *
+midi_file_open(const char *filename, const char *mode, struct midi_file_info *info);
+
+int midi_file_close(struct midi_file *mf);
+
+int midi_file_next_time(struct midi_file *mf, double *sec);
+
+int midi_file_read_event(struct midi_file *mf, struct midi_event *event);
+
+int midi_file_write_event(struct midi_file *mf, const struct midi_event *event);
+
+int midi_file_dump_event(FILE *out, const struct midi_event *event);
diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c
new file mode 100644
index 0000000..068fac1
--- /dev/null
+++ b/src/tools/pw-cat.c
@@ -0,0 +1,1971 @@
+/* PipeWire - pw-cat
+ *
+ * Copyright © 2020 Konsulko Group
+
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <time.h>
+#include <math.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <assert.h>
+#include <ctype.h>
+#include <locale.h>
+
+#include <sndfile.h>
+
+#include <spa/param/audio/layout.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/props.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/i18n.h>
+#include <pipewire/extensions/metadata.h>
+
+#include "config.h"
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+#include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#endif
+
+#include "midifile.h"
+#include "dsffile.h"
+
+#define DEFAULT_MEDIA_TYPE "Audio"
+#define DEFAULT_MIDI_MEDIA_TYPE "Midi"
+#define DEFAULT_MEDIA_CATEGORY_PLAYBACK "Playback"
+#define DEFAULT_MEDIA_CATEGORY_RECORD "Capture"
+#define DEFAULT_MEDIA_ROLE "Music"
+#define DEFAULT_TARGET "auto"
+#define DEFAULT_LATENCY_PLAY "100ms"
+#define DEFAULT_LATENCY_REC "none"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_FORMAT "s16"
+#define DEFAULT_VOLUME 1.0
+#define DEFAULT_QUALITY 4
+
+enum mode {
+ mode_none,
+ mode_playback,
+ mode_record
+};
+
+enum unit {
+ unit_none,
+ unit_samples,
+ unit_sec,
+ unit_msec,
+ unit_usec,
+ unit_nsec,
+};
+
+struct data;
+
+typedef int (*fill_fn)(struct data *d, void *dest, unsigned int n_frames);
+
+struct channelmap {
+ int n_channels;
+ int channels[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_source *timer;
+
+ enum mode mode;
+ bool verbose;
+#define TYPE_PCM 0
+#define TYPE_MIDI 1
+#define TYPE_DSD 2
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+#define TYPE_ENCODED 3
+#endif
+ int data_type;
+ const char *remote_name;
+ const char *media_type;
+ const char *media_category;
+ const char *media_role;
+ const char *channel_map;
+ const char *format;
+ const char *target;
+ const char *latency;
+ struct pw_properties *props;
+
+ const char *filename;
+ SNDFILE *file;
+
+ unsigned int bitrate;
+ unsigned int rate;
+ int channels;
+ struct channelmap channelmap;
+ unsigned int stride;
+ enum unit latency_unit;
+ unsigned int latency_value;
+ int quality;
+
+ enum spa_audio_format spa_format;
+
+ float volume;
+ bool volume_is_set;
+
+ fill_fn fill;
+
+ struct spa_io_position *position;
+ bool drained;
+ uint64_t clock_time;
+
+ struct {
+ struct midi_file *file;
+ struct midi_file_info info;
+ } midi;
+ struct {
+ struct dsf_file *file;
+ struct dsf_file_info info;
+ struct dsf_layout layout;
+ } dsf;
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ FILE *encoded_file;
+ AVFormatContext *fmt_context;
+ AVStream *astream;
+ AVCodecContext *ctx;
+ enum AVSampleFormat sfmt;
+#endif
+};
+
+#define STR_FMTS "(ulaw|alaw|u8|s8|s16|s32|f32|f64)"
+
+static const struct format_info {
+ const char *name;
+ int sf_format;
+ uint32_t spa_format;
+ uint32_t width;
+} format_info[] = {
+ { "ulaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ULAW, 1 },
+ { "alaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ALAW, 1 },
+ { "s8", SF_FORMAT_PCM_S8, SPA_AUDIO_FORMAT_S8, 1 },
+ { "u8", SF_FORMAT_PCM_U8, SPA_AUDIO_FORMAT_U8, 1 },
+ { "s16", SF_FORMAT_PCM_16, SPA_AUDIO_FORMAT_S16, 2 },
+ { "s24", SF_FORMAT_PCM_24, SPA_AUDIO_FORMAT_S24, 3 },
+ { "s32", SF_FORMAT_PCM_32, SPA_AUDIO_FORMAT_S32, 4 },
+ { "f32", SF_FORMAT_FLOAT, SPA_AUDIO_FORMAT_F32, 4 },
+ { "f64", SF_FORMAT_DOUBLE, SPA_AUDIO_FORMAT_F32, 8 },
+};
+
+static const struct format_info *format_info_by_name(const char *str)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+ if (spa_streq(str, i->name))
+ return i;
+ return NULL;
+}
+
+static const struct format_info *format_info_by_sf_format(int format)
+{
+ int sub_type = (format & SF_FORMAT_SUBMASK);
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+ if (i->sf_format == sub_type)
+ return i;
+ return NULL;
+}
+
+static int sf_playback_fill_x8(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ rn = sf_read_raw(d->file, dest, n_frames * d->stride);
+ return (int)rn / d->stride;
+}
+
+static int sf_playback_fill_s16(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(short) == sizeof(int16_t));
+ rn = sf_readf_short(d->file, dest, n_frames);
+ return (int)rn;
+}
+
+static int sf_playback_fill_s32(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(int) == sizeof(int32_t));
+ rn = sf_readf_int(d->file, dest, n_frames);
+ return (int)rn;
+}
+
+static int sf_playback_fill_f32(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(float) == 4);
+ rn = sf_readf_float(d->file, dest, n_frames);
+ return (int)rn;
+}
+
+static int sf_playback_fill_f64(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(double) == 8);
+ rn = sf_readf_double(d->file, dest, n_frames);
+ return (int)rn;
+}
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+static int encoded_playback_fill(struct data *d, void *dest, unsigned int n_frames)
+{
+ int ret, size = 0;
+ uint8_t buffer[16384] = { 0 };
+
+ ret = fread(buffer, 1, 16384, d->encoded_file);
+ if (ret > 0) {
+ memcpy(dest, buffer, ret);
+ size = ret;
+ }
+
+ return (int)size;
+}
+
+static int avcodec_ctx_to_info(struct data *data, AVCodecContext *ctx, struct spa_audio_info *info)
+{
+ int32_t profile;
+
+ switch (ctx->codec_id) {
+ case AV_CODEC_ID_VORBIS:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_vorbis;
+ info->info.vorbis.rate = data->rate;
+ info->info.vorbis.channels = data->channels;
+ break;
+ case AV_CODEC_ID_MP3:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_mp3;
+ info->info.mp3.rate = data->rate;
+ info->info.mp3.channels = data->channels;
+ break;
+ case AV_CODEC_ID_AAC:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_aac;
+ info->info.aac.rate = data->rate;
+ info->info.aac.channels = data->channels;
+ info->info.aac.bitrate = data->bitrate;
+ info->info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_RAW;
+ break;
+ case AV_CODEC_ID_WMAV1:
+ case AV_CODEC_ID_WMAV2:
+ case AV_CODEC_ID_WMAPRO:
+ case AV_CODEC_ID_WMAVOICE:
+ case AV_CODEC_ID_WMALOSSLESS:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_wma;
+ switch (ctx->codec_tag) {
+ /* TODO see if these hex constants can be replaced by named constants from FFmpeg */
+ case 0x161:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA9;
+ break;
+ case 0x162:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA9_PRO;
+ break;
+ case 0x163:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS;
+ break;
+ case 0x166:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA10;
+ break;
+ case 0x167:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS;
+ break;
+ default:
+ fprintf(stderr, "error: invalid WMA profile\n");
+ return -EINVAL;
+ }
+ info->info.wma.rate = data->rate;
+ info->info.wma.channels = data->channels;
+ info->info.wma.bitrate = data->bitrate;
+ info->info.wma.block_align = ctx->block_align;
+ info->info.wma.profile = profile;
+ break;
+ case AV_CODEC_ID_FLAC:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_flac;
+ info->info.flac.rate = data->rate;
+ info->info.flac.channels = data->channels;
+ break;
+ case AV_CODEC_ID_ALAC:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_alac;
+ info->info.alac.rate = data->rate;
+ info->info.alac.channels = data->channels;
+ break;
+ case AV_CODEC_ID_APE:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_ape;
+ info->info.ape.rate = data->rate;
+ info->info.ape.channels = data->channels;
+ break;
+ case AV_CODEC_ID_RA_144:
+ case AV_CODEC_ID_RA_288:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_ra;
+ info->info.ra.rate = data->rate;
+ info->info.ra.channels = data->channels;
+ break;
+ case AV_CODEC_ID_AMR_NB:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_amr;
+ info->info.amr.rate = data->rate;
+ info->info.amr.channels = data->channels;
+ info->info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_NB;
+ break;
+ case AV_CODEC_ID_AMR_WB:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_amr;
+ info->info.amr.rate = data->rate;
+ info->info.amr.channels = data->channels;
+ info->info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_WB;
+ break;
+ default:
+ fprintf(stderr, "Unsupported encoded media subtype\n");
+ return -EINVAL;
+ }
+ return 0;
+}
+#endif
+
+static inline fill_fn
+playback_fill_fn(uint32_t fmt)
+{
+ switch (fmt) {
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_ULAW:
+ case SPA_AUDIO_FORMAT_ALAW:
+ return sf_playback_fill_x8;
+ case SPA_AUDIO_FORMAT_S16_LE:
+ case SPA_AUDIO_FORMAT_S16_BE:
+ /* sndfile check */
+ if (sizeof(int16_t) != sizeof(short))
+ return NULL;
+ return sf_playback_fill_s16;
+ case SPA_AUDIO_FORMAT_S32_LE:
+ case SPA_AUDIO_FORMAT_S32_BE:
+ /* sndfile check */
+ if (sizeof(int32_t) != sizeof(int))
+ return NULL;
+ return sf_playback_fill_s32;
+ case SPA_AUDIO_FORMAT_F32_LE:
+ case SPA_AUDIO_FORMAT_F32_BE:
+ /* sndfile check */
+ if (sizeof(float) != 4)
+ return NULL;
+ return sf_playback_fill_f32;
+ case SPA_AUDIO_FORMAT_F64_LE:
+ case SPA_AUDIO_FORMAT_F64_BE:
+ if (sizeof(double) != 8)
+ return NULL;
+ return sf_playback_fill_f64;
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ case SPA_AUDIO_FORMAT_ENCODED:
+ return encoded_playback_fill;
+#endif
+ default:
+ break;
+ }
+ return NULL;
+}
+
+static int sf_record_fill_x8(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ rn = sf_write_raw(d->file, src, n_frames * d->stride);
+ return (int)rn / d->stride;
+}
+
+static int sf_record_fill_s16(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(short) == sizeof(int16_t));
+ rn = sf_writef_short(d->file, src, n_frames);
+ return (int)rn;
+}
+
+static int sf_record_fill_s32(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(int) == sizeof(int32_t));
+ rn = sf_writef_int(d->file, src, n_frames);
+ return (int)rn;
+}
+
+static int sf_record_fill_f32(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(float) == 4);
+ rn = sf_writef_float(d->file, src, n_frames);
+ return (int)rn;
+}
+
+static int sf_record_fill_f64(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(double) == 8);
+ rn = sf_writef_double(d->file, src, n_frames);
+ return (int)rn;
+}
+
+static inline fill_fn
+record_fill_fn(uint32_t fmt)
+{
+ switch (fmt) {
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_ULAW:
+ case SPA_AUDIO_FORMAT_ALAW:
+ return sf_record_fill_x8;
+ case SPA_AUDIO_FORMAT_S16_LE:
+ case SPA_AUDIO_FORMAT_S16_BE:
+ /* sndfile check */
+ if (sizeof(int16_t) != sizeof(short))
+ return NULL;
+ return sf_record_fill_s16;
+ case SPA_AUDIO_FORMAT_S32_LE:
+ case SPA_AUDIO_FORMAT_S32_BE:
+ /* sndfile check */
+ if (sizeof(int32_t) != sizeof(int))
+ return NULL;
+ return sf_record_fill_s32;
+ case SPA_AUDIO_FORMAT_F32_LE:
+ case SPA_AUDIO_FORMAT_F32_BE:
+ /* sndfile check */
+ if (sizeof(float) != 4)
+ return NULL;
+ return sf_record_fill_f32;
+ case SPA_AUDIO_FORMAT_F64_LE:
+ case SPA_AUDIO_FORMAT_F64_BE:
+ /* sndfile check */
+ if (sizeof(double) != 8)
+ return NULL;
+ return sf_record_fill_f64;
+ default:
+ break;
+ }
+ return NULL;
+}
+
+static int channelmap_from_sf(struct channelmap *map)
+{
+ static const enum spa_audio_channel table[] = {
+ [SF_CHANNEL_MAP_MONO] = SPA_AUDIO_CHANNEL_MONO,
+ [SF_CHANNEL_MAP_LEFT] = SPA_AUDIO_CHANNEL_FL, /* libsndfile distinguishes left and front-left, which we don't */
+ [SF_CHANNEL_MAP_RIGHT] = SPA_AUDIO_CHANNEL_FR,
+ [SF_CHANNEL_MAP_CENTER] = SPA_AUDIO_CHANNEL_FC,
+ [SF_CHANNEL_MAP_FRONT_LEFT] = SPA_AUDIO_CHANNEL_FL,
+ [SF_CHANNEL_MAP_FRONT_RIGHT] = SPA_AUDIO_CHANNEL_FR,
+ [SF_CHANNEL_MAP_FRONT_CENTER] = SPA_AUDIO_CHANNEL_FC,
+ [SF_CHANNEL_MAP_REAR_CENTER] = SPA_AUDIO_CHANNEL_RC,
+ [SF_CHANNEL_MAP_REAR_LEFT] = SPA_AUDIO_CHANNEL_RL,
+ [SF_CHANNEL_MAP_REAR_RIGHT] = SPA_AUDIO_CHANNEL_RR,
+ [SF_CHANNEL_MAP_LFE] = SPA_AUDIO_CHANNEL_LFE,
+ [SF_CHANNEL_MAP_FRONT_LEFT_OF_CENTER] = SPA_AUDIO_CHANNEL_FLC,
+ [SF_CHANNEL_MAP_FRONT_RIGHT_OF_CENTER] = SPA_AUDIO_CHANNEL_FRC,
+ [SF_CHANNEL_MAP_SIDE_LEFT] = SPA_AUDIO_CHANNEL_SL,
+ [SF_CHANNEL_MAP_SIDE_RIGHT] = SPA_AUDIO_CHANNEL_SR,
+ [SF_CHANNEL_MAP_TOP_CENTER] = SPA_AUDIO_CHANNEL_TC,
+ [SF_CHANNEL_MAP_TOP_FRONT_LEFT] = SPA_AUDIO_CHANNEL_TFL,
+ [SF_CHANNEL_MAP_TOP_FRONT_RIGHT] = SPA_AUDIO_CHANNEL_TFR,
+ [SF_CHANNEL_MAP_TOP_FRONT_CENTER] = SPA_AUDIO_CHANNEL_TFC,
+ [SF_CHANNEL_MAP_TOP_REAR_LEFT] = SPA_AUDIO_CHANNEL_TRL,
+ [SF_CHANNEL_MAP_TOP_REAR_RIGHT] = SPA_AUDIO_CHANNEL_TRR,
+ [SF_CHANNEL_MAP_TOP_REAR_CENTER] = SPA_AUDIO_CHANNEL_TRC
+ };
+ int i;
+
+ for (i = 0; i < map->n_channels; i++) {
+ if (map->channels[i] >= 0 && map->channels[i] < (int) SPA_N_ELEMENTS(table))
+ map->channels[i] = table[map->channels[i]];
+ else
+ map->channels[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+ }
+ return 0;
+}
+struct mapping {
+ const char *name;
+ unsigned int channels;
+ unsigned int values[32];
+};
+
+static const struct mapping maps[] =
+{
+ { "mono", SPA_AUDIO_LAYOUT_Mono },
+ { "stereo", SPA_AUDIO_LAYOUT_Stereo },
+ { "surround-21", SPA_AUDIO_LAYOUT_2_1 },
+ { "quad", SPA_AUDIO_LAYOUT_Quad },
+ { "surround-22", SPA_AUDIO_LAYOUT_2_2 },
+ { "surround-40", SPA_AUDIO_LAYOUT_4_0 },
+ { "surround-31", SPA_AUDIO_LAYOUT_3_1 },
+ { "surround-41", SPA_AUDIO_LAYOUT_4_1 },
+ { "surround-50", SPA_AUDIO_LAYOUT_5_0 },
+ { "surround-51", SPA_AUDIO_LAYOUT_5_1 },
+ { "surround-51r", SPA_AUDIO_LAYOUT_5_1R },
+ { "surround-70", SPA_AUDIO_LAYOUT_7_0 },
+ { "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;
+ char **ch;
+
+ SPA_FOR_EACH_ELEMENT_VAR(maps, m) {
+ if (spa_streq(m->name, channel_map)) {
+ map->n_channels = m->channels;
+ spa_memcpy(map->channels, &m->values,
+ map->n_channels * sizeof(unsigned int));
+ return 0;
+ }
+ }
+
+ 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;
+ }
+ pw_free_strv(ch);
+ return 0;
+}
+
+static int channelmap_default(struct channelmap *map, int n_channels)
+{
+ switch(n_channels) {
+ case 1:
+ parse_channelmap("mono", map);
+ break;
+ case 2:
+ parse_channelmap("stereo", map);
+ break;
+ case 3:
+ parse_channelmap("surround-21", map);
+ break;
+ case 4:
+ parse_channelmap("quad", map);
+ break;
+ case 5:
+ parse_channelmap("surround-50", map);
+ break;
+ case 6:
+ parse_channelmap("surround-51", map);
+ break;
+ case 7:
+ parse_channelmap("surround-70", map);
+ break;
+ case 8:
+ parse_channelmap("surround-71", map);
+ break;
+ default:
+ n_channels = 0;
+ break;
+ }
+ map->n_channels = n_channels;
+ return 0;
+}
+
+static void channelmap_print(struct channelmap *map)
+{
+ int 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 ? "," : "");
+ }
+}
+
+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",
+ info->id, info->name);
+}
+
+static void on_core_error(void *userdata, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *data = userdata;
+
+ fprintf(stderr, "remote error: id=%"PRIu32" seq:%d res:%d (%s): %s\n",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(data->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .info = on_core_info,
+ .error = on_core_error,
+};
+
+static void
+on_state_changed(void *userdata, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct data *data = userdata;
+ int ret;
+
+ if (data->verbose)
+ printf("stream state changed %s -> %s\n",
+ pw_stream_state_as_string(old),
+ pw_stream_state_as_string(state));
+
+ switch (state) {
+ case PW_STREAM_STATE_STREAMING:
+ if (!data->volume_is_set) {
+ ret = pw_stream_set_control(data->stream,
+ SPA_PROP_volume, 1, &data->volume,
+ 0);
+ if (data->verbose)
+ printf("stream set volume to %.3f - %s\n", data->volume,
+ ret == 0 ? "success" : "FAILED");
+
+ data->volume_is_set = true;
+ }
+ if (data->verbose) {
+ 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",
+ pw_stream_get_node_id(data->stream));
+ }
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ if (data->verbose) {
+ struct timespec timeout = {0, 0}, interval = {0, 0};
+ struct pw_loop *l = pw_main_loop_get_loop(data->loop);
+ pw_loop_update_timer(l, data->timer, &timeout, &interval, false);
+ }
+ break;
+ case PW_STREAM_STATE_ERROR:
+ printf("stream node %"PRIu32" error: %s\n",
+ pw_stream_get_node_id(data->stream),
+ error);
+ pw_main_loop_quit(data->loop);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+on_io_changed(void *userdata, uint32_t id, void *data, uint32_t size)
+{
+ struct data *d = userdata;
+
+ switch (id) {
+ case SPA_IO_Position:
+ d->position = data;
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = userdata;
+ struct spa_audio_info info = { 0 };
+ int err;
+
+ if (data->verbose)
+ printf("stream param change: %s\n",
+ spa_debug_type_find_name(spa_type_param, id));
+
+ if (id != SPA_PARAM_Format || param == NULL)
+ return;
+
+ if ((err = spa_format_parse(param, &info.media_type, &info.media_subtype)) < 0)
+ return;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_dsd)
+ return;
+
+ if (spa_format_audio_dsd_parse(param, &info.info.dsd) < 0)
+ return;
+
+ data->dsf.layout.interleave = info.info.dsd.interleave,
+ data->dsf.layout.channels = info.info.dsd.channels;
+ data->dsf.layout.lsb = info.info.dsd.bitorder == SPA_PARAM_BITORDER_lsb;
+
+ 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",
+ data->dsf.layout.channels,
+ data->dsf.layout.lsb ? "lsb" : "msb",
+ data->dsf.layout.interleave,
+ data->stride);
+ }
+}
+
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ struct spa_data *d;
+ int n_frames, n_fill_frames;
+ uint8_t *p;
+ bool have_data;
+ uint32_t offset, size;
+
+ if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL)
+ return;
+
+ buf = b->buffer;
+ d = &buf->datas[0];
+
+ have_data = false;
+
+ if ((p = d->data) == NULL)
+ return;
+
+ if (data->mode == mode_playback) {
+ n_frames = d->maxsize / data->stride;
+ n_frames = SPA_MIN(n_frames, (int)b->requested);
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ n_fill_frames = data->fill(data, p, n_frames);
+
+ if (n_fill_frames > 0 || n_frames == 0) {
+ d->chunk->offset = 0;
+ if (data->data_type == TYPE_ENCODED) {
+ d->chunk->stride = 0;
+ // encoded_playback_fill returns number of bytes
+ // read and not number of frames like other
+ // functions for raw audio.
+ d->chunk->size = n_fill_frames;
+ b->size = n_fill_frames;
+ } else {
+ d->chunk->stride = data->stride;
+ d->chunk->size = n_fill_frames * data->stride;
+ b->size = n_frames;
+ }
+ have_data = true;
+ } else if (n_fill_frames < 0) {
+ fprintf(stderr, "fill error %d\n", n_fill_frames);
+ } else {
+ if (data->verbose)
+ printf("drain start\n");
+ }
+#else
+ n_fill_frames = data->fill(data, p, n_frames);
+
+ if (n_fill_frames > 0 || n_frames == 0) {
+ d->chunk->offset = 0;
+ d->chunk->stride = data->stride;
+ d->chunk->size = n_fill_frames * data->stride;
+ have_data = true;
+ b->size = n_frames;
+ } else if (n_fill_frames < 0) {
+ fprintf(stderr, "fill error %d\n", n_fill_frames);
+ } else {
+ if (data->verbose)
+ printf("drain start\n");
+ }
+#endif
+ } else {
+ offset = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offset);
+
+ p += offset;
+
+ n_frames = size / data->stride;
+
+ n_fill_frames = data->fill(data, p, n_frames);
+
+ have_data = true;
+ }
+
+ if (have_data) {
+ pw_stream_queue_buffer(data->stream, b);
+ return;
+ }
+
+ if (data->mode == mode_playback)
+ pw_stream_flush(data->stream, true);
+}
+
+static void on_drained(void *userdata)
+{
+ struct data *data = userdata;
+
+ if (data->verbose)
+ printf("stream drained\n");
+
+ data->drained = true;
+ pw_main_loop_quit(data->loop);
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = on_state_changed,
+ .io_changed = on_io_changed,
+ .param_changed = on_param_changed,
+ .process = on_process,
+ .drained = on_drained
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+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
+ " delay:%"PRIi64" queued:%"PRIu64
+ " buffered:%"PRIi64" buffers:%u avail:%u\n",
+ time.now,
+ time.rate.num, time.rate.denom,
+ time.ticks, time.delay, time.queued, time.buffered,
+ time.queued_buffers, time.avail_buffers);
+}
+
+enum {
+ OPT_VERSION = 1000,
+ OPT_MEDIA_TYPE,
+ OPT_MEDIA_CATEGORY,
+ OPT_MEDIA_ROLE,
+ OPT_TARGET,
+ OPT_LATENCY,
+ OPT_RATE,
+ OPT_CHANNELS,
+ OPT_CHANNELMAP,
+ OPT_FORMAT,
+ OPT_VOLUME,
+};
+
+static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, OPT_VERSION},
+ { "verbose", no_argument, NULL, 'v' },
+
+ { "record", no_argument, NULL, 'r' },
+ { "playback", no_argument, NULL, 'p' },
+ { "midi", no_argument, NULL, 'm' },
+
+ { "remote", required_argument, NULL, 'R' },
+
+ { "media-type", required_argument, NULL, OPT_MEDIA_TYPE },
+ { "media-category", required_argument, NULL, OPT_MEDIA_CATEGORY },
+ { "media-role", required_argument, NULL, OPT_MEDIA_ROLE },
+ { "target", required_argument, NULL, OPT_TARGET },
+ { "latency", required_argument, NULL, OPT_LATENCY },
+ { "properties", required_argument, NULL, 'P' },
+
+ { "rate", required_argument, NULL, OPT_RATE },
+ { "channels", required_argument, NULL, OPT_CHANNELS },
+ { "channel-map", required_argument, NULL, OPT_CHANNELMAP },
+ { "format", required_argument, NULL, OPT_FORMAT },
+ { "volume", required_argument, NULL, OPT_VOLUME },
+ { "quality", required_argument, NULL, 'q' },
+
+ { 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] [<file>|-]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -v, --verbose Enable verbose operations\n"
+ "\n"), name);
+
+ fprintf(fp,
+ _(" -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"),
+ DEFAULT_MEDIA_TYPE,
+ DEFAULT_MEDIA_CATEGORY_PLAYBACK,
+ DEFAULT_MEDIA_ROLE,
+ DEFAULT_TARGET, DEFAULT_LATENCY_PLAY);
+
+ fprintf(fp,
+ _(" --rate Sample rate (req. for rec) (default %u)\n"
+ " --channels Number of channels (req. for rec) (default %u)\n"
+ " --channel-map Channel map\n"
+ " one of: \"stereo\", \"surround-51\",... or\n"
+ " comma separated list of channel names: eg. \"FL,FR\"\n"
+ " --format Sample format %s (req. for rec) (default %s)\n"
+ " --volume Stream volume 0-1.0 (default %.3f)\n"
+ " -q --quality Resampler quality (0 - 15) (default %d)\n"
+ "\n"),
+ DEFAULT_RATE,
+ DEFAULT_CHANNELS,
+ STR_FMTS, DEFAULT_FORMAT,
+ DEFAULT_VOLUME,
+ DEFAULT_QUALITY);
+
+ if (spa_streq(name, "pw-cat")) {
+ fputs(
+ _(" -p, --playback Playback mode\n"
+ " -r, --record Recording mode\n"
+ " -m, --midi Midi mode\n"
+ " -d, --dsd DSD mode\n"
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ " -o, --encoded Encoded mode\n"
+#endif
+ "\n"), fp);
+ }
+}
+
+static int midi_play(struct data *d, void *src, unsigned int n_frames)
+{
+ int res;
+ struct spa_pod_builder b;
+ struct spa_pod_frame f;
+ uint32_t first_frame, last_frame;
+ bool have_data = false;
+
+ spa_zero(b);
+ spa_pod_builder_init(&b, src, n_frames);
+
+ spa_pod_builder_push_sequence(&b, &f, 0);
+
+ first_frame = d->clock_time;
+ last_frame = first_frame + d->position->clock.duration;
+ d->clock_time = last_frame;
+
+ while (1) {
+ uint32_t frame;
+ struct midi_event ev;
+
+ res = midi_file_next_time(d->midi.file, &ev.sec);
+ if (res <= 0) {
+ if (have_data)
+ break;
+ return res;
+ }
+
+ frame = ev.sec * d->position->clock.rate.denom;
+ if (frame < first_frame)
+ frame = 0;
+ else if (frame < last_frame)
+ frame -= first_frame;
+ else
+ break;
+
+ midi_file_read_event(d->midi.file, &ev);
+
+ if (d->verbose)
+ midi_file_dump_event(stdout, &ev);
+
+ if (ev.data[0] == 0xff)
+ 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);
+
+ return b.state.offset;
+}
+
+static int midi_record(struct data *d, void *src, unsigned int n_frames)
+{
+ struct spa_pod *pod;
+ struct spa_pod_control *c;
+ uint32_t frame;
+
+ frame = d->clock_time;
+ d->clock_time += d->position->clock.duration;
+
+ if ((pod = spa_pod_from_data(src, n_frames, 0, n_frames)) == NULL)
+ return 0;
+ if (!spa_pod_is_sequence(pod))
+ return 0;
+
+ SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) {
+ struct midi_event ev;
+
+ if (c->type != SPA_CONTROL_Midi)
+ 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);
+
+ if (d->verbose)
+ midi_file_dump_event(stdout, &ev);
+
+ midi_file_write_event(d->midi.file, &ev);
+ }
+ return 0;
+}
+
+static int setup_midifile(struct data *data)
+{
+ if (data->mode == mode_record) {
+ spa_zero(data->midi.info);
+ data->midi.info.format = 0;
+ data->midi.info.ntracks = 1;
+ data->midi.info.division = 0;
+ }
+
+ data->midi.file = midi_file_open(data->filename,
+ data->mode == mode_playback ? "r" : "w",
+ &data->midi.info);
+ if (data->midi.file == NULL) {
+ fprintf(stderr, "midifile: can't read midi file '%s': %m\n", data->filename);
+ return -errno;
+ }
+
+ if (data->verbose)
+ printf("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);
+
+ data->fill = data->mode == mode_playback ? midi_play : midi_record;
+ data->stride = 1;
+
+ return 0;
+}
+
+struct dsd_layout_info {
+ uint32_t type;
+ struct spa_audio_layout_info info;
+};
+static const struct dsd_layout_info dsd_layouts[] = {
+ { 1, { SPA_AUDIO_LAYOUT_Mono, }, },
+ { 2, { SPA_AUDIO_LAYOUT_Stereo, }, },
+ { 3, { SPA_AUDIO_LAYOUT_2FC }, },
+ { 4, { SPA_AUDIO_LAYOUT_Quad }, },
+ { 5, { SPA_AUDIO_LAYOUT_3_1 }, },
+ { 6, { SPA_AUDIO_LAYOUT_5_0R }, },
+ { 7, { SPA_AUDIO_LAYOUT_5_1R }, },
+};
+
+static int dsf_play(struct data *d, void *src, unsigned int n_frames)
+{
+ return dsf_file_read(d->dsf.file, src, n_frames, &d->dsf.layout);
+}
+
+static int setup_dsffile(struct data *data)
+{
+ if (data->mode == mode_record)
+ return -ENOTSUP;
+
+ data->dsf.file = dsf_file_open(data->filename, "r", &data->dsf.info);
+ if (data->dsf.file == NULL) {
+ fprintf(stderr, "dsffile: can't read dsf file '%s': %m\n", data->filename);
+ return -errno;
+ }
+
+ if (data->verbose)
+ printf("dsffile: opened file \"%s\" channels:%d rate:%d samples:%"PRIu64" bitorder:%s\n",
+ data->filename,
+ data->dsf.info.channels, data->dsf.info.rate,
+ data->dsf.info.samples,
+ data->dsf.info.lsb ? "lsb" : "msb");
+
+ data->fill = dsf_play;
+
+ return 0;
+}
+
+static int stdout_record(struct data *d, void *src, unsigned int n_frames)
+{
+ return fwrite(src, d->stride, n_frames, stdout);
+}
+
+static int stdin_play(struct data *d, void *src, unsigned int n_frames)
+{
+ return fread(src, d->stride, n_frames, stdin);
+}
+
+static int setup_pipe(struct data *data)
+{
+ const struct format_info *info;
+
+ if (data->format == NULL)
+ data->format = DEFAULT_FORMAT;
+ if (data->channels == 0)
+ data->channels = DEFAULT_CHANNELS;
+ if (data->rate == 0)
+ data->rate = DEFAULT_RATE;
+ if (data->channelmap.n_channels == 0)
+ channelmap_default(&data->channelmap, data->channels);
+
+ info = format_info_by_name(data->format);
+ if (info == NULL)
+ return -EINVAL;
+
+ data->spa_format = info->spa_format;
+ data->stride = info->width * data->channels;
+ data->fill = data->mode == mode_playback ? stdin_play : stdout_record;
+
+ if (data->verbose)
+ printf("PIPE: rate=%u channels=%u fmt=%s samplesize=%u stride=%u\n",
+ data->rate, data->channels,
+ info->name, info->width, data->stride);
+
+ return 0;
+}
+
+static int fill_properties(struct data *data)
+{
+ static const char * const table[] = {
+ [SF_STR_TITLE] = PW_KEY_MEDIA_TITLE,
+ [SF_STR_COPYRIGHT] = PW_KEY_MEDIA_COPYRIGHT,
+ [SF_STR_SOFTWARE] = PW_KEY_MEDIA_SOFTWARE,
+ [SF_STR_ARTIST] = PW_KEY_MEDIA_ARTIST,
+ [SF_STR_COMMENT] = PW_KEY_MEDIA_COMMENT,
+ [SF_STR_DATE] = PW_KEY_MEDIA_DATE
+ };
+
+ SF_INFO sfi;
+ SF_FORMAT_INFO fi;
+ int res;
+ unsigned c;
+ const char *s, *t;
+
+ for (c = 0; c < SPA_N_ELEMENTS(table); c++) {
+ if (table[c] == NULL)
+ continue;
+
+ if ((s = sf_get_string(data->file, c)) == NULL ||
+ *s == '\0')
+ continue;
+
+ pw_properties_set(data->props, table[c], s);
+ }
+
+ spa_zero(sfi);
+ if ((res = sf_command(data->file, SFC_GET_CURRENT_SF_INFO, &sfi, sizeof(sfi)))) {
+ pw_log_error("sndfile: %s", sf_error_number(res));
+ return -EIO;
+ }
+
+ spa_zero(fi);
+ fi.format = sfi.format;
+ if (sf_command(data->file, SFC_GET_FORMAT_INFO, &fi, sizeof(fi)) == 0 && fi.name)
+ pw_properties_set(data->props, PW_KEY_MEDIA_FORMAT, fi.name);
+
+ s = pw_properties_get(data->props, PW_KEY_MEDIA_TITLE);
+ t = pw_properties_get(data->props, PW_KEY_MEDIA_ARTIST);
+ if (s && t)
+ pw_properties_setf(data->props, PW_KEY_MEDIA_NAME,
+ "'%s' / '%s'", s, t);
+
+ return 0;
+}
+static void format_from_filename(SF_INFO *info, const char *filename)
+{
+ int i, count = 0;
+ int format = -1;
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+ info->format |= SF_ENDIAN_BIG;
+#else
+ info->format |= SF_ENDIAN_LITTLE;
+#endif
+
+ 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(filename, fi.extension)) {
+ format = fi.format;
+ break;
+ }
+ }
+ if (format == -1)
+ format = SF_FORMAT_WAV;
+ if (format == SF_FORMAT_WAV && info->channels > 2)
+ format = SF_FORMAT_WAVEX;
+
+ info->format |= format;
+
+ if (format == SF_FORMAT_OGG || format == SF_FORMAT_FLAC)
+ info->format = (info->format & ~SF_FORMAT_ENDMASK) | SF_ENDIAN_FILE;
+ if (format == SF_FORMAT_OGG)
+ info->format = (info->format & ~SF_FORMAT_SUBMASK) | SF_FORMAT_VORBIS;
+}
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+static int setup_encodedfile(struct data *data)
+{
+ int ret;
+ int bits_per_sample;
+ int num_channels;
+ char path[256] = { 0 };
+
+ /* We do not support record with encoded media */
+ if (data->mode == mode_record) {
+ return -EINVAL;
+ }
+
+ strcpy(path, "file:");
+ strcat(path, data->filename);
+
+ data->fmt_context = NULL;
+ ret = avformat_open_input(&data->fmt_context, path, NULL, NULL);
+ if (ret < 0) {
+ fprintf(stderr, "Failed to open input\n");
+ return -EINVAL;
+ }
+
+ avformat_find_stream_info (data->fmt_context, NULL);
+
+ data->ctx = avcodec_alloc_context3(NULL);
+ if (!data->ctx) {
+ fprintf(stderr, "Could not allocate audio codec context\n");
+ avformat_close_input(&data->fmt_context);
+ return -EINVAL;
+ }
+
+ // We expect only one stream with audio
+ data->astream = data->fmt_context->streams[0];
+ avcodec_parameters_to_context (data->ctx, data->astream->codecpar);
+
+ if (data->ctx->codec_type != AVMEDIA_TYPE_AUDIO) {
+ fprintf(stderr, "Not an audio file\n");
+ avformat_close_input(&data->fmt_context);
+ return -EINVAL;
+ }
+
+ printf("Number of streams: %d Codec id: %x\n", data->fmt_context->nb_streams,
+ data->ctx->codec_id);
+
+ /* FFmpeg 5.1 (which contains libavcodec 59.37.100) introduced
+ * a new channel layout API and deprecated the old one. */
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 37, 100)
+ num_channels = data->ctx->ch_layout.nb_channels;
+#else
+ num_channels = data->ctx->channels;
+#endif
+
+ data->rate = data->ctx->sample_rate;
+ data->channels = num_channels;
+ data->sfmt = data->ctx->sample_fmt;
+ data->stride = 1; // Don't care
+
+ bits_per_sample = av_get_bits_per_sample(data->ctx->codec_id);
+ data->bitrate = bits_per_sample ?
+ data->ctx->sample_rate * num_channels * bits_per_sample : data->ctx->bit_rate;
+
+ data->spa_format = SPA_AUDIO_FORMAT_ENCODED;
+ data->fill = playback_fill_fn(data->spa_format);
+
+ if (data->verbose)
+ printf("Opened file \"%s\" sample format %08x channels:%d rate:%d bitrate: %d\n",
+ data->filename, data->ctx->sample_fmt, data->channels,
+ data->rate, data->bitrate);
+
+ if (data->fill == NULL) {
+ fprintf(stderr, "Unhandled encoded format %d\n", data->spa_format);
+ avformat_close_input(&data->fmt_context);
+ return -EINVAL;
+ }
+
+ avformat_close_input(&data->fmt_context);
+
+ data->encoded_file = fopen(data->filename, "rb");
+ if (!data->encoded_file) {
+ fprintf(stderr, "Failed to open file\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+#endif
+
+static int setup_sndfile(struct data *data)
+{
+ const struct format_info *fi = NULL;
+ SF_INFO info;
+
+ spa_zero(info);
+ /* for record, you fill in the info first */
+ if (data->mode == mode_record) {
+ if (data->format == NULL)
+ data->format = DEFAULT_FORMAT;
+ if (data->channels == 0)
+ data->channels = DEFAULT_CHANNELS;
+ if (data->rate == 0)
+ data->rate = DEFAULT_RATE;
+ if (data->channelmap.n_channels == 0)
+ channelmap_default(&data->channelmap, data->channels);
+
+ if ((fi = format_info_by_name(data->format)) == NULL) {
+ fprintf(stderr, "error: unknown format \"%s\"\n", data->format);
+ return -EINVAL;
+ }
+ memset(&info, 0, sizeof(info));
+ info.samplerate = data->rate;
+ info.channels = data->channels;
+ info.format = fi->sf_format;
+ format_from_filename(&info, data->filename);
+ }
+
+ data->file = sf_open(data->filename,
+ data->mode == mode_playback ? SFM_READ : SFM_WRITE,
+ &info);
+ if (!data->file) {
+ fprintf(stderr, "sndfile: failed to open audio file \"%s\": %s\n",
+ data->filename, sf_strerror(NULL));
+ return -EIO;
+ }
+
+ if (data->verbose)
+ printf("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) {
+ fprintf(stderr, "sndfile: given channels (%u) don't match file channels (%d)\n",
+ data->channels, info.channels);
+ return -EINVAL;
+ }
+
+ data->rate = info.samplerate;
+ data->channels = info.channels;
+
+ if (data->mode == mode_playback) {
+ if (data->channelmap.n_channels == 0) {
+ bool def = false;
+
+ if (sf_command(data->file, SFC_GET_CHANNEL_MAP_INFO,
+ data->channelmap.channels,
+ sizeof(data->channelmap.channels[0]) * data->channels)) {
+ data->channelmap.n_channels = data->channels;
+ if (channelmap_from_sf(&data->channelmap) < 0)
+ data->channelmap.n_channels = 0;
+ }
+ if (data->channelmap.n_channels == 0) {
+ channelmap_default(&data->channelmap, data->channels);
+ def = true;
+ }
+ if (data->verbose) {
+ printf("sndfile: using %s channel map: ", def ? "default" : "file");
+ channelmap_print(&data->channelmap);
+ printf("\n");
+ }
+ }
+ fill_properties(data);
+
+ /* try native format first, else decode to float */
+ if ((fi = format_info_by_sf_format(info.format)) == NULL)
+ fi = format_info_by_sf_format(SF_FORMAT_FLOAT);
+
+ }
+ if (fi == NULL)
+ return -EIO;
+
+ if (data->verbose)
+ printf("PCM: fmt:%s rate:%u channels:%u width:%u\n",
+ fi->name, data->rate, data->channels, fi->width);
+
+ /* we read and write S24 as S32 with sndfile */
+ if (fi->spa_format == SPA_AUDIO_FORMAT_S24)
+ fi = format_info_by_sf_format(SF_FORMAT_PCM_32);
+
+ data->spa_format = fi->spa_format;
+ data->stride = fi->width * data->channels;
+ data->fill = data->mode == mode_playback ?
+ playback_fill_fn(data->spa_format) :
+ record_fill_fn(data->spa_format);
+
+ if (data->fill == NULL) {
+ fprintf(stderr, "PCM: unhandled format %d\n", data->spa_format);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int setup_properties(struct data *data)
+{
+ const char *s;
+ unsigned int nom = 0;
+
+ if (data->quality >= 0)
+ pw_properties_setf(data->props, "resample.quality", "%d", data->quality);
+
+ if (data->rate)
+ pw_properties_setf(data->props, PW_KEY_NODE_RATE, "1/%u", data->rate);
+
+ data->latency_unit = unit_none;
+
+ s = data->latency;
+ while (*s && isdigit(*s))
+ s++;
+ if (!*s)
+ data->latency_unit = unit_samples;
+ else if (spa_streq(s, "none"))
+ data->latency_unit = unit_none;
+ else if (spa_streq(s, "s") || spa_streq(s, "sec") || spa_streq(s, "secs"))
+ data->latency_unit = unit_sec;
+ else if (spa_streq(s, "ms") || spa_streq(s, "msec") || spa_streq(s, "msecs"))
+ data->latency_unit = unit_msec;
+ else if (spa_streq(s, "us") || spa_streq(s, "usec") || spa_streq(s, "usecs"))
+ data->latency_unit = unit_usec;
+ else if (spa_streq(s, "ns") || spa_streq(s, "nsec") || spa_streq(s, "nsecs"))
+ data->latency_unit = unit_nsec;
+ else {
+ fprintf(stderr, "error: bad latency value %s (bad unit)\n", data->latency);
+ return -EINVAL;
+ }
+ data->latency_value = atoi(data->latency);
+ if (!data->latency_value && data->latency_unit != unit_none) {
+ fprintf(stderr, "error: bad latency value %s (is zero)\n", data->latency);
+ return -EINVAL;
+ }
+
+ switch (data->latency_unit) {
+ case unit_sec:
+ nom = data->latency_value * data->rate;
+ break;
+ case unit_msec:
+ nom = nearbyint((data->latency_value * data->rate) / 1000.0);
+ break;
+ case unit_usec:
+ nom = nearbyint((data->latency_value * data->rate) / 1000000.0);
+ break;
+ case unit_nsec:
+ nom = nearbyint((data->latency_value * data->rate) / 1000000000.0);
+ break;
+ case unit_samples:
+ nom = data->latency_value;
+ break;
+ default:
+ nom = 0;
+ break;
+ }
+
+ if (data->verbose)
+ printf("rate:%d latency:%u (%.3fs)\n",
+ data->rate, nom, data->rate ? (double)nom/data->rate : 0.0f);
+ if (nom)
+ pw_properties_setf(data->props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data->rate);
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ struct pw_loop *l;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const char *prog;
+ int exit_code = EXIT_FAILURE, c, ret;
+ enum pw_stream_flags flags = 0;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ flags |= PW_STREAM_FLAG_AUTOCONNECT;
+
+ prog = argv[0];
+ if ((prog = strrchr(argv[0], '/')) != NULL)
+ prog++;
+ else
+ prog = argv[0];
+
+ /* prime the mode from the program name */
+ if (spa_streq(prog, "pw-play")) {
+ data.mode = mode_playback;
+ data.data_type = TYPE_PCM;
+ } else if (spa_streq(prog, "pw-record")) {
+ data.mode = mode_record;
+ data.data_type = TYPE_PCM;
+ } else if (spa_streq(prog, "pw-midiplay")) {
+ data.mode = mode_playback;
+ data.data_type = TYPE_MIDI;
+ } else if (spa_streq(prog, "pw-midirecord")) {
+ data.mode = mode_record;
+ data.data_type = TYPE_MIDI;
+ } else if (spa_streq(prog, "pw-dsdplay")) {
+ data.mode = mode_playback;
+ data.data_type = TYPE_DSD;
+ } else
+ data.mode = mode_none;
+
+ /* negative means no volume adjustment */
+ data.volume = -1.0;
+ data.quality = -1;
+ data.props = pw_properties_new(
+ PW_KEY_APP_NAME, prog,
+ PW_KEY_NODE_NAME, prog,
+ NULL);
+
+ if (data.props == NULL) {
+ fprintf(stderr, "error: pw_properties_new() failed: %m\n");
+ goto error_no_props;
+ }
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ while ((c = getopt_long(argc, argv, "hvprmdoR:q:P:", long_options, NULL)) != -1) {
+#else
+ while ((c = getopt_long(argc, argv, "hvprmdR:q:P:", long_options, NULL)) != -1) {
+#endif
+
+ switch (c) {
+
+ case 'h':
+ show_usage(prog, false);
+ return EXIT_SUCCESS;
+
+ case OPT_VERSION:
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ prog,
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+
+ case 'v':
+ data.verbose = true;
+ break;
+
+ case 'p':
+ data.mode = mode_playback;
+ break;
+
+ case 'r':
+ data.mode = mode_record;
+ break;
+
+ case 'm':
+ data.data_type = TYPE_MIDI;
+ break;
+
+ case 'd':
+ data.data_type = TYPE_DSD;
+ break;
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ case 'o':
+ data.data_type = TYPE_ENCODED;
+ break;
+#endif
+
+ case 'R':
+ data.remote_name = optarg;
+ break;
+
+ case 'q':
+ data.quality = atoi(optarg);
+ break;
+
+ case OPT_MEDIA_TYPE:
+ data.media_type = optarg;
+ break;
+
+ case OPT_MEDIA_CATEGORY:
+ data.media_category = optarg;
+ break;
+
+ case OPT_MEDIA_ROLE:
+ data.media_role = optarg;
+ break;
+
+ case 'P':
+ pw_properties_update_string(data.props, optarg, strlen(optarg));
+ break;
+
+ case OPT_TARGET:
+ data.target = optarg;
+ if (spa_streq(data.target, "0")) {
+ data.target = NULL;
+ flags &= ~PW_STREAM_FLAG_AUTOCONNECT;
+ }
+ break;
+
+ case OPT_LATENCY:
+ data.latency = optarg;
+ break;
+
+ case OPT_RATE:
+ ret = atoi(optarg);
+ if (ret <= 0) {
+ fprintf(stderr, "error: bad rate %d\n", ret);
+ goto error_usage;
+ }
+ data.rate = (unsigned int)ret;
+ break;
+
+ case OPT_CHANNELS:
+ ret = atoi(optarg);
+ if (ret <= 0) {
+ fprintf(stderr, "error: bad channels %d\n", ret);
+ goto error_usage;
+ }
+ data.channels = (unsigned int)ret;
+ break;
+
+ case OPT_CHANNELMAP:
+ data.channel_map = optarg;
+ break;
+
+ case OPT_FORMAT:
+ data.format = optarg;
+ break;
+
+ case OPT_VOLUME:
+ data.volume = atof(optarg);
+ break;
+ default:
+ goto error_usage;
+ }
+ }
+
+ if (data.mode == mode_none) {
+ fprintf(stderr, "error: one of the playback/record options must be provided\n");
+ goto error_usage;
+ }
+
+ if (!data.media_type) {
+ switch (data.data_type) {
+ case TYPE_MIDI:
+ data.media_type = DEFAULT_MIDI_MEDIA_TYPE;
+ break;
+ default:
+ data.media_type = DEFAULT_MEDIA_TYPE;
+ break;
+ }
+ }
+ if (!data.media_category)
+ data.media_category = data.mode == mode_playback ?
+ DEFAULT_MEDIA_CATEGORY_PLAYBACK :
+ DEFAULT_MEDIA_CATEGORY_RECORD;
+ if (!data.media_role)
+ data.media_role = DEFAULT_MEDIA_ROLE;
+
+ if (!data.latency)
+ data.latency = data.mode == mode_playback ?
+ DEFAULT_LATENCY_PLAY :
+ DEFAULT_LATENCY_REC;
+ if (data.channel_map != NULL) {
+ if (parse_channelmap(data.channel_map, &data.channelmap) < 0) {
+ fprintf(stderr, "error: can parse channel-map \"%s\"\n", data.channel_map);
+ goto error_usage;
+
+ } else {
+ if (data.channels > 0 && data.channelmap.n_channels != data.channels) {
+ fprintf(stderr, "error: channels and channel-map incompatible\n");
+ goto error_usage;
+ }
+ data.channels = data.channelmap.n_channels;
+ }
+ }
+ if (data.volume < 0)
+ data.volume = DEFAULT_VOLUME;
+
+ if (optind >= argc) {
+ fprintf(stderr, "error: filename or - argument missing\n");
+ goto error_usage;
+ }
+ data.filename = argv[optind++];
+
+ pw_properties_set(data.props, PW_KEY_MEDIA_TYPE, data.media_type);
+ pw_properties_set(data.props, PW_KEY_MEDIA_CATEGORY, data.media_category);
+ pw_properties_set(data.props, PW_KEY_MEDIA_ROLE, data.media_role);
+ pw_properties_set(data.props, PW_KEY_MEDIA_FILENAME, data.filename);
+ pw_properties_set(data.props, PW_KEY_MEDIA_NAME, data.filename);
+ pw_properties_set(data.props, PW_KEY_TARGET_OBJECT, data.target);
+
+ /* make a main loop. If you already have another main loop, you can add
+ * the fd of this pipewire mainloop to it. */
+ data.loop = pw_main_loop_new(NULL);
+ if (!data.loop) {
+ fprintf(stderr, "error: pw_main_loop_new() failed: %m\n");
+ goto error_no_main_loop;
+ }
+
+ l = pw_main_loop_get_loop(data.loop);
+ 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);
+ if (!data.context) {
+ fprintf(stderr, "error: pw_context_new() failed: %m\n");
+ goto error_no_context;
+ }
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, data.remote_name,
+ NULL),
+ 0);
+ if (!data.core) {
+ fprintf(stderr, "error: pw_context_connect() failed: %m\n");
+ goto error_ctx_connect_failed;
+ }
+ pw_core_add_listener(data.core, &data.core_listener, &core_events, &data);
+
+ if (spa_streq(data.filename, "-")) {
+ ret = setup_pipe(&data);
+ } else {
+ switch (data.data_type) {
+ case TYPE_PCM:
+ ret = setup_sndfile(&data);
+ break;
+ case TYPE_MIDI:
+ ret = setup_midifile(&data);
+ break;
+ case TYPE_DSD:
+ ret = setup_dsffile(&data);
+ break;
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ case TYPE_ENCODED:
+ ret = setup_encodedfile(&data);
+ break;
+#endif
+ default:
+ ret = -ENOTSUP;
+ break;
+ }
+ }
+ if (ret < 0) {
+ fprintf(stderr, "error: open failed: %s\n", spa_strerror(ret));
+ switch (ret) {
+ case -EIO:
+ goto error_bad_file;
+ case -EINVAL:
+ default:
+ goto error_usage;
+ }
+ }
+ ret = setup_properties(&data);
+
+ switch (data.data_type) {
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ case TYPE_ENCODED:
+ {
+ struct spa_audio_info info;
+
+ spa_zero(info);
+ info.media_type = SPA_MEDIA_TYPE_audio;
+
+ ret = avcodec_ctx_to_info(&data, data.ctx, &info);
+ if (ret < 0) {
+ if (data.encoded_file) {
+ fclose(data.encoded_file);
+ }
+ goto error_bad_file;
+ }
+ params[0] = spa_format_audio_build(&b, SPA_PARAM_EnumFormat, &info);
+ break;
+ }
+#endif
+ case TYPE_PCM:
+ {
+ struct spa_audio_info_raw info;
+ info = SPA_AUDIO_INFO_RAW_INIT(
+ .flags = data.channelmap.n_channels ? 0 : SPA_AUDIO_FLAG_UNPOSITIONED,
+ .format = data.spa_format,
+ .rate = data.rate,
+ .channels = data.channels);
+
+ if (data.channelmap.n_channels)
+ memcpy(info.position, data.channelmap.channels, data.channels * sizeof(int));
+
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info);
+ break;
+ }
+ case TYPE_MIDI:
+ params[0] = 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));
+
+ pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "8 bit raw midi");
+ break;
+ case TYPE_DSD:
+ {
+ struct spa_audio_info_dsd info;
+
+ spa_zero(info);
+ info.channels = data.dsf.info.channels;
+ info.rate = data.dsf.info.rate / 8;
+
+ SPA_FOR_EACH_ELEMENT_VAR(dsd_layouts, i) {
+ if (i->type != data.dsf.info.channel_type)
+ continue;
+ info.channels = i->info.n_channels;
+ memcpy(info.position, i->info.position,
+ info.channels * sizeof(uint32_t));
+ }
+ params[0] = spa_format_audio_dsd_build(&b, SPA_PARAM_EnumFormat, &info);
+ break;
+ }
+ }
+
+ data.stream = pw_stream_new(data.core, prog, data.props);
+ data.props = NULL;
+
+ if (data.stream == NULL) {
+ fprintf(stderr, "error: failed to create stream: %m\n");
+ goto error_no_stream;
+ }
+ pw_stream_add_listener(data.stream, &data.stream_listener, &stream_events, &data);
+
+ if (data.verbose)
+ printf("connecting %s stream; target=%s\n",
+ data.mode == mode_playback ? "playback" : "record",
+ data.target);
+
+ if (data.verbose)
+ data.timer = pw_loop_add_timer(l, do_print_delay, &data);
+
+ ret = pw_stream_connect(data.stream,
+ data.mode == mode_playback ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ flags |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, 1);
+ if (ret < 0) {
+ fprintf(stderr, "error: failed connect: %s\n", spa_strerror(ret));
+ goto error_connect_fail;
+ }
+
+ if (data.verbose) {
+ const struct pw_properties *props;
+ void *pstate;
+ const char *key, *val;
+
+ if ((props = pw_stream_get_properties(data.stream)) != NULL) {
+ printf("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);
+ }
+ }
+ }
+
+ /* and wait while we let things run */
+ pw_main_loop_run(data.loop);
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ if (data.encoded_file)
+ fclose(data.encoded_file);
+#endif
+
+ /* we're returning OK only if got to the point to drain */
+ if (data.drained)
+ exit_code = EXIT_SUCCESS;
+
+error_connect_fail:
+ if (data.stream) {
+ spa_hook_remove(&data.stream_listener);
+ pw_stream_destroy(data.stream);
+ }
+error_no_stream:
+error_bad_file:
+ spa_hook_remove(&data.core_listener);
+ pw_core_disconnect(data.core);
+error_ctx_connect_failed:
+ pw_context_destroy(data.context);
+error_no_context:
+ pw_main_loop_destroy(data.loop);
+error_no_props:
+error_no_main_loop:
+ pw_properties_free(data.props);
+ if (data.file)
+ sf_close(data.file);
+ if (data.midi.file)
+ midi_file_close(data.midi.file);
+ pw_deinit();
+ return exit_code;
+
+error_usage:
+ show_usage(prog, true);
+ return EXIT_FAILURE;
+}
diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c
new file mode 100644
index 0000000..53a3384
--- /dev/null
+++ b/src/tools/pw-cli.c
@@ -0,0 +1,2375 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <unistd.h>
+#include <errno.h>
+#include <stdio.h>
+#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
+#include <readline/readline.h>
+#include <readline/history.h>
+#endif
+#include <locale.h>
+
+#if !defined(FNM_EXTMATCH)
+#define FNM_EXTMATCH 0
+#endif
+
+#define spa_debug(fmt,...) printf(fmt"\n", ## __VA_ARGS__)
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/debug/pod.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/json-pod.h>
+#include <spa/pod/builder.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include <pipewire/extensions/session-manager.h>
+
+static const char WHITESPACE[] = " \t";
+static char prompt[64];
+
+struct remote_data;
+struct proxy_data;
+
+typedef void (*info_func_t) (struct proxy_data *pd);
+
+struct class {
+ const char *type;
+ uint32_t version;
+ const void *events;
+ pw_destroy_t destroy;
+ info_func_t info;
+ const char *name_key;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct spa_list remotes;
+ struct remote_data *current;
+
+ struct pw_map vars;
+ unsigned int interactive:1;
+ unsigned int monitoring:1;
+ unsigned int quit:1;
+};
+
+struct global {
+ struct remote_data *rd;
+ uint32_t id;
+ uint32_t permissions;
+ uint32_t version;
+ char *type;
+ const struct class *class;
+ struct pw_proxy *proxy;
+ bool info_pending;
+ struct pw_properties *properties;
+};
+
+struct remote_data {
+ struct spa_list link;
+ struct data *data;
+
+ char *name;
+ uint32_t id;
+
+ int prompt_pending;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook proxy_core_listener;
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_map globals;
+};
+
+
+struct proxy_data {
+ struct remote_data *rd;
+ struct global *global;
+ struct pw_proxy *proxy;
+ void *info;
+ const struct class *class;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+};
+
+struct command {
+ const char *name;
+ const char *alias;
+ const char *description;
+ bool (*func) (struct data *data, const char *cmd, char *args, char **error);
+};
+
+static struct spa_dict * global_props(struct global *global);
+static struct global * obj_global(struct remote_data *rd, uint32_t id);
+static int children_of(struct remote_data *rd, uint32_t parent_id,
+ const char *child_type, uint32_t **children);
+
+static void print_properties(struct spa_dict *props, char mark, bool header)
+{
+ const struct spa_dict_item *item;
+
+ if (header)
+ printf("%c\tproperties:\n", mark);
+ if (props == NULL || props->n_items == 0) {
+ if (header)
+ printf("\t\tnone\n");
+ return;
+ }
+
+ spa_dict_for_each(item, props) {
+ printf("%c\t\t%s = \"%s\"\n", mark, item->key, item->value);
+ }
+}
+
+static void print_params(struct spa_param_info *params, uint32_t n_params, char mark, bool header)
+{
+ uint32_t i;
+
+ if (header)
+ printf("%c\tparams: (%u)\n", mark, n_params);
+ if (params == NULL || n_params == 0) {
+ if (header)
+ printf("\t\tnone\n");
+ return;
+ }
+ for (i = 0; i < n_params; i++) {
+ const struct spa_type_info *type_info = spa_type_param;
+
+ printf("%c\t %d (%s) %c%c\n",
+ params[i].user > 0 ? mark : ' ', params[i].id,
+ spa_debug_type_find_name(type_info, params[i].id),
+ params[i].flags & SPA_PARAM_INFO_READ ? 'r' : '-',
+ params[i].flags & SPA_PARAM_INFO_WRITE ? 'w' : '-');
+ params[i].user = 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;
+}
+
+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_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);
+static bool do_list_remotes(struct data *data, const char *cmd, char *args, char **error);
+static bool do_switch_remote(struct data *data, const char *cmd, char *args, char **error);
+static bool do_info(struct data *data, const char *cmd, char *args, char **error);
+static bool do_create_device(struct data *data, const char *cmd, char *args, char **error);
+static bool do_create_node(struct data *data, const char *cmd, char *args, char **error);
+static bool do_destroy(struct data *data, const char *cmd, char *args, char **error);
+static bool do_create_link(struct data *data, const char *cmd, char *args, char **error);
+static bool do_export_node(struct data *data, const char *cmd, char *args, char **error);
+static bool do_enum_params(struct data *data, const char *cmd, char *args, char **error);
+static bool do_set_param(struct data *data, const char *cmd, char *args, char **error);
+static bool do_permissions(struct data *data, const char *cmd, char *args, char **error);
+static bool do_get_permissions(struct data *data, const char *cmd, char *args, char **error);
+static bool do_send_command(struct data *data, const char *cmd, char *args, char **error);
+static bool do_quit(struct data *data, const char *cmd, char *args, char **error);
+
+#define DUMP_NAMES "Core|Module|Device|Node|Port|Factory|Client|Link|Session|Endpoint|EndpointStream"
+
+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 },
+ { "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 },
+ { "switch-remote", "sr", "Switch between current remotes. [<remote-var>]", do_switch_remote },
+ { "list-objects", "ls", "List objects or current remote. [<interface>]", do_list_objects },
+ { "info", "i", "Get info about an object. <object-id>|all", do_info },
+ { "create-device", "cd", "Create a device from a factory. <factory-name> [<properties>]", do_create_device },
+ { "create-node", "cn", "Create a node from a factory. <factory-name> [<properties>]", do_create_node },
+ { "destroy", "d", "Destroy a global object. <object-id>", do_destroy },
+ { "create-link", "cl", "Create a link between nodes. <node-id> <port-id> <node-id> <port-id> [<properties>]", do_create_link },
+ { "export-node", "en", "Export a local node to the current remote. <node-id> [remote-var]", do_export_node },
+ { "enum-params", "e", "Enumerate params of an object <object-id> <param-id>", do_enum_params },
+ { "set-param", "s", "Set param of an object <object-id> <param-id> <param-json>", do_set_param },
+ { "permissions", "sp", "Set permissions for a client <client-id> <object> <permission>", do_permissions },
+ { "get-permissions", "gp", "Get permissions of a client <client-id>", do_get_permissions },
+ { "send-command", "c", "Send a command <object-id>", do_send_command },
+ { "quit", "q", "Quit", do_quit },
+};
+
+static bool do_quit(struct data *data, const char *cmd, char *args, char **error)
+{
+ pw_main_loop_quit(data->loop);
+ data->quit = true;
+ return true;
+}
+
+static bool do_help(struct data *data, const char *cmd, char *args, char **error)
+{
+ printf("Available commands:\n");
+ SPA_FOR_EACH_ELEMENT_VAR(command_list, c) {
+ char cmd[256];
+ snprintf(cmd, sizeof(cmd), "%s | %s", c->name, c->alias);
+ printf("\t%-20.20s\t%s\n", cmd, c->description);
+ }
+ return true;
+}
+
+static bool do_load_module(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct pw_impl_module *module;
+ char *a[2];
+ int n;
+ uint32_t id;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <module-name> [<module-arguments>]", cmd);
+ return false;
+ }
+
+ module = pw_context_load_module(data->context, a[0], n == 2 ? a[1] : NULL, NULL);
+ if (module == NULL) {
+ *error = spa_aprintf("Could not load module");
+ return false;
+ }
+
+ id = pw_map_insert_new(&data->vars, module);
+ if (data->interactive)
+ printf("%d = @module:%d\n", id, pw_global_get_id(pw_impl_module_get_global(module)));
+
+ return true;
+}
+
+static void on_core_info(void *_data, const struct pw_core_info *info)
+{
+ struct remote_data *rd = _data;
+ free(rd->name);
+ rd->name = info->name ? strdup(info->name) : NULL;
+ if (rd->data->interactive)
+ printf("remote %d is named '%s'\n", rd->id, rd->name);
+}
+
+static void set_prompt(struct remote_data *rd)
+{
+ snprintf(prompt, sizeof(prompt), "%s>> ", rd->name);
+#ifdef HAVE_READLINE
+ rl_set_prompt(prompt);
+#else
+ printf("%s", prompt);
+ fflush(stdout);
+#endif
+}
+
+static void on_core_done(void *_data, uint32_t id, int seq)
+{
+ struct remote_data *rd = _data;
+ struct data *d = rd->data;
+
+ if (seq == rd->prompt_pending) {
+ if (d->interactive) {
+ set_prompt(rd);
+ rd->data->monitoring = true;
+ } else {
+ pw_main_loop_quit(d->loop);
+ }
+ }
+}
+
+static bool global_matches(struct global *g, const char *pattern)
+{
+ const char *str;
+
+ if (g->properties == NULL)
+ return false;
+
+ if (strstr(g->type, pattern) != NULL)
+ return true;
+ if ((str = pw_properties_get(g->properties, PW_KEY_OBJECT_PATH)) != NULL &&
+ fnmatch(pattern, str, FNM_EXTMATCH) == 0)
+ return true;
+ if ((str = pw_properties_get(g->properties, PW_KEY_OBJECT_SERIAL)) != NULL &&
+ spa_streq(pattern, str))
+ return true;
+ if (g->class != NULL && g->class->name_key != NULL &&
+ (str = pw_properties_get(g->properties, g->class->name_key)) != NULL &&
+ fnmatch(pattern, str, FNM_EXTMATCH) == 0)
+ return true;
+
+ return false;
+}
+
+static int print_global(void *obj, void *data)
+{
+ struct global *global = obj;
+ const char *filter = data;
+
+ if (global == NULL)
+ return 0;
+
+ if (filter && !global_matches(global, filter))
+ return 0;
+
+ printf("\tid %d, type %s/%d\n", global->id,
+ global->type, global->version);
+ if (global->properties)
+ print_properties(&global->properties->dict, ' ', false);
+
+ return 0;
+}
+
+
+static bool bind_global(struct remote_data *rd, struct global *global, char **error);
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct remote_data *rd = data;
+ struct global *global;
+ size_t size;
+ char *error;
+ bool ret;
+
+ global = calloc(1, sizeof(struct global));
+ global->rd = rd;
+ global->id = id;
+ global->permissions = permissions;
+ global->type = strdup(type);
+ global->version = version;
+ global->properties = props ? pw_properties_new_dict(props) : NULL;
+
+ if (rd->data->monitoring) {
+ printf("remote %d added global: ", rd->id);
+ print_global(global, NULL);
+ }
+
+ size = pw_map_get_size(&rd->globals);
+ while (id > size)
+ pw_map_insert_at(&rd->globals, size++, NULL);
+ pw_map_insert_at(&rd->globals, id, global);
+
+ /* immediately bind the object always */
+ ret = bind_global(rd, global, &error);
+ if (!ret) {
+ if (rd->data->interactive)
+ fprintf(stderr, "Error: \"%s\"\n", error);
+ free(error);
+ }
+}
+
+static int destroy_global(void *obj, void *data)
+{
+ struct global *global = obj;
+
+ if (global == NULL)
+ return 0;
+
+ if (global->proxy)
+ pw_proxy_destroy(global->proxy);
+ pw_map_insert_at(&global->rd->globals, global->id, NULL);
+ pw_properties_free(global->properties);
+ free(global->type);
+ free(global);
+ return 0;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct remote_data *rd = data;
+ struct global *global;
+
+ global = pw_map_lookup(&rd->globals, id);
+ if (global == NULL) {
+ fprintf(stderr, "remote %d removed unknown global %d\n", rd->id, id);
+ return;
+ }
+
+ if (rd->data->monitoring) {
+ printf("remote %d removed global: ", rd->id);
+ print_global(global, NULL);
+ }
+
+ destroy_global(global, rd);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static struct global *find_global(struct remote_data *rd, const char *pattern)
+{
+ uint32_t id;
+ union pw_map_item *item;
+
+ if (spa_atou32(pattern, &id, 0))
+ return pw_map_lookup(&rd->globals, id);
+
+ pw_array_for_each(item, &rd->globals.items) {
+ struct global *g = item->data;
+ if (pw_map_item_is_free(item) || g == NULL)
+ continue;
+ if (global_matches(g, pattern))
+ return g;
+ }
+ return NULL;
+}
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct remote_data *rd = _data;
+ struct data *data = rd->data;
+
+ pw_log_error("remote %p: error id:%u seq:%d res:%d (%s): %s", rd,
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(data->loop);
+}
+
+static const struct pw_core_events remote_core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .info = on_core_info,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void on_core_destroy(void *_data)
+{
+ struct remote_data *rd = _data;
+ struct data *data = rd->data;
+
+ spa_list_remove(&rd->link);
+
+ spa_hook_remove(&rd->core_listener);
+ spa_hook_remove(&rd->proxy_core_listener);
+
+ pw_map_remove(&data->vars, rd->id);
+ pw_map_for_each(&rd->globals, destroy_global, rd);
+ pw_map_clear(&rd->globals);
+
+ if (data->current == rd)
+ data->current = NULL;
+ free(rd->name);
+}
+
+static const struct pw_proxy_events proxy_core_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = on_core_destroy,
+};
+
+static void remote_data_free(struct remote_data *rd)
+{
+ spa_hook_remove(&rd->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)rd->registry);
+ pw_core_disconnect(rd->core);
+}
+
+static bool do_connect(struct data *data, const char *cmd, char *args, char **error)
+{
+ char *a[1];
+ int n;
+ struct pw_properties *props = NULL;
+ struct pw_core *core;
+ struct remote_data *rd;
+
+ n = args ? pw_split_ip(args, WHITESPACE, 1, a) : 0;
+ if (n == 1) {
+ props = pw_properties_new(PW_KEY_REMOTE_NAME, a[0], NULL);
+ }
+ core = pw_context_connect(data->context, props, sizeof(struct remote_data));
+ if (core == NULL) {
+ *error = spa_aprintf("failed to connect: %m");
+ return false;
+ }
+
+ rd = pw_proxy_get_user_data((struct pw_proxy*)core);
+ rd->core = core;
+ rd->data = data;
+ pw_map_init(&rd->globals, 64, 16);
+ rd->id = pw_map_insert_new(&data->vars, rd);
+ spa_list_append(&data->remotes, &rd->link);
+
+ if (rd->data->interactive)
+ printf("%d = @remote:%p\n", rd->id, rd->core);
+
+ data->current = rd;
+
+ pw_core_add_listener(rd->core,
+ &rd->core_listener,
+ &remote_core_events, rd);
+ pw_proxy_add_listener((struct pw_proxy*)rd->core,
+ &rd->proxy_core_listener,
+ &proxy_core_events, rd);
+ rd->registry = pw_core_get_registry(rd->core, PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(rd->registry,
+ &rd->registry_listener,
+ &registry_events, rd);
+ rd->prompt_pending = pw_core_sync(rd->core, 0, 0);
+
+ return true;
+}
+
+static bool do_disconnect(struct data *data, const char *cmd, char *args, char **error)
+{
+ char *a[1];
+ int n;
+ uint32_t idx;
+ struct remote_data *rd = data->current;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n >= 1) {
+ idx = atoi(a[0]);
+ rd = pw_map_lookup(&data->vars, idx);
+ if (rd == NULL)
+ goto no_remote;
+
+ }
+ if (rd)
+ remote_data_free(rd);
+
+ if (data->current == NULL) {
+ if (spa_list_is_empty(&data->remotes)) {
+ return true;
+ }
+ data->current = spa_list_last(&data->remotes, struct remote_data, link);
+ }
+
+ return true;
+
+ no_remote:
+ *error = spa_aprintf("Remote %d does not exist", idx);
+ return false;
+}
+
+static bool do_list_remotes(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd;
+
+ spa_list_for_each(rd, &data->remotes, link)
+ printf("\t%d = @remote:%p '%s'\n", rd->id, rd->core, rd->name);
+
+ return true;
+}
+
+static bool do_switch_remote(struct data *data, const char *cmd, char *args, char **error)
+{
+ char *a[1];
+ int n, idx = 0;
+ struct remote_data *rd;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n == 1)
+ idx = atoi(a[0]);
+
+ rd = pw_map_lookup(&data->vars, idx);
+ if (rd == NULL)
+ goto no_remote;
+
+ spa_list_remove(&rd->link);
+ spa_list_append(&data->remotes, &rd->link);
+ data->current = rd;
+
+ return true;
+
+ no_remote:
+ *error = spa_aprintf("Remote %d does not exist", idx);
+ return false;
+}
+
+#define MARK_CHANGE(f) ((((info)->change_mask & (f))) ? '*' : ' ')
+
+static void info_global(struct proxy_data *pd)
+{
+ struct global *global = pd->global;
+
+ if (global == NULL)
+ return;
+
+ printf("\tid: %d\n", global->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(global->permissions));
+ printf("\ttype: %s/%d\n", global->type, global->version);
+}
+
+static void info_core(struct proxy_data *pd)
+{
+ struct pw_core_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tcookie: %u\n", info->cookie);
+ printf("\tuser-name: \"%s\"\n", info->user_name);
+ printf("\thost-name: \"%s\"\n", info->host_name);
+ printf("\tversion: \"%s\"\n", info->version);
+ printf("\tname: \"%s\"\n", info->name);
+ print_properties(info->props, MARK_CHANGE(PW_CORE_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_module(struct proxy_data *pd)
+{
+ struct pw_module_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tname: \"%s\"\n", info->name);
+ printf("\tfilename: \"%s\"\n", info->filename);
+ printf("\targs: \"%s\"\n", info->args);
+ print_properties(info->props, MARK_CHANGE(PW_MODULE_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_node(struct proxy_data *pd)
+{
+ struct pw_node_info *info = pd->info;
+
+ info_global(pd);
+ printf("%c\tinput ports: %u/%u\n", MARK_CHANGE(PW_NODE_CHANGE_MASK_INPUT_PORTS),
+ info->n_input_ports, info->max_input_ports);
+ printf("%c\toutput ports: %u/%u\n", MARK_CHANGE(PW_NODE_CHANGE_MASK_OUTPUT_PORTS),
+ info->n_output_ports, info->max_output_ports);
+ printf("%c\tstate: \"%s\"", MARK_CHANGE(PW_NODE_CHANGE_MASK_STATE),
+ pw_node_state_as_string(info->state));
+ if (info->state == PW_NODE_STATE_ERROR && info->error)
+ printf(" \"%s\"\n", info->error);
+ else
+ printf("\n");
+ print_properties(info->props, MARK_CHANGE(PW_NODE_CHANGE_MASK_PROPS), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(PW_NODE_CHANGE_MASK_PARAMS), true);
+ info->change_mask = 0;
+}
+
+static void info_port(struct proxy_data *pd)
+{
+ struct pw_port_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tdirection: \"%s\"\n", pw_direction_as_string(info->direction));
+ print_properties(info->props, MARK_CHANGE(PW_PORT_CHANGE_MASK_PROPS), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(PW_PORT_CHANGE_MASK_PARAMS), true);
+ info->change_mask = 0;
+}
+
+static void info_factory(struct proxy_data *pd)
+{
+ struct pw_factory_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tname: \"%s\"\n", info->name);
+ printf("\tobject-type: %s/%d\n", info->type, info->version);
+ print_properties(info->props, MARK_CHANGE(PW_FACTORY_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_client(struct proxy_data *pd)
+{
+ struct pw_client_info *info = pd->info;
+
+ info_global(pd);
+ print_properties(info->props, MARK_CHANGE(PW_CLIENT_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_link(struct proxy_data *pd)
+{
+ struct pw_link_info *info = pd->info;
+
+ info_global(pd);
+ printf("\toutput-node-id: %u\n", info->output_node_id);
+ printf("\toutput-port-id: %u\n", info->output_port_id);
+ printf("\tinput-node-id: %u\n", info->input_node_id);
+ printf("\tinput-port-id: %u\n", info->input_port_id);
+
+ printf("%c\tstate: \"%s\"", MARK_CHANGE(PW_LINK_CHANGE_MASK_STATE),
+ pw_link_state_as_string(info->state));
+ if (info->state == PW_LINK_STATE_ERROR && info->error)
+ printf(" \"%s\"\n", info->error);
+ else
+ printf("\n");
+ printf("%c\tformat:\n", MARK_CHANGE(PW_LINK_CHANGE_MASK_FORMAT));
+ if (info->format)
+ spa_debug_pod(2, NULL, info->format);
+ else
+ printf("\t\tnone\n");
+ print_properties(info->props, MARK_CHANGE(PW_LINK_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_device(struct proxy_data *pd)
+{
+ struct pw_device_info *info = pd->info;
+
+ info_global(pd);
+ print_properties(info->props, MARK_CHANGE(PW_DEVICE_CHANGE_MASK_PROPS), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(PW_DEVICE_CHANGE_MASK_PARAMS), true);
+ info->change_mask = 0;
+}
+
+static void info_session(struct proxy_data *pd)
+{
+ struct pw_session_info *info = pd->info;
+
+ info_global(pd);
+ print_properties(info->props, MARK_CHANGE(0), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(1), true);
+ info->change_mask = 0;
+}
+
+static void info_endpoint(struct proxy_data *pd)
+{
+ struct pw_endpoint_info *info = pd->info;
+ const char *direction;
+
+ info_global(pd);
+ printf("\tname: %s\n", info->name);
+ printf("\tmedia-class: %s\n", info->media_class);
+ switch(info->direction) {
+ case PW_DIRECTION_OUTPUT:
+ direction = "source";
+ break;
+ case PW_DIRECTION_INPUT:
+ direction = "sink";
+ break;
+ default:
+ direction = "invalid";
+ break;
+ }
+ printf("\tdirection: %s\n", direction);
+ printf("\tflags: 0x%x\n", info->flags);
+ printf("%c\tstreams: %u\n", MARK_CHANGE(0), info->n_streams);
+ printf("%c\tsession: %u\n", MARK_CHANGE(1), info->session_id);
+ print_properties(info->props, MARK_CHANGE(2), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(3), true);
+ info->change_mask = 0;
+}
+
+static void info_endpoint_stream(struct proxy_data *pd)
+{
+ struct pw_endpoint_stream_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tid: %u\n", info->id);
+ printf("\tendpoint-id: %u\n", info->endpoint_id);
+ printf("\tname: %s\n", info->name);
+ print_properties(info->props, MARK_CHANGE(1), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(2), true);
+ info->change_mask = 0;
+}
+
+static void core_event_info(void *data, const struct pw_core_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d core %d changed\n", rd->id, info->id);
+ pd->info = pw_core_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_core(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .info = core_event_info
+};
+
+
+static void module_event_info(void *data, const struct pw_module_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d module %d changed\n", rd->id, info->id);
+ pd->info = pw_module_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_module(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info
+};
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d node %d changed\n", rd->id, info->id);
+ pd->info = pw_node_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_node(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static void event_param(void *_data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct proxy_data *data = _data;
+ struct remote_data *rd = data->rd;
+
+ if (rd->data->interactive)
+ printf("remote %d object %d param %d index %d\n",
+ rd->id, data->global->id, id, index);
+
+ spa_debug_pod(2, NULL, param);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = event_param
+};
+
+
+static void port_event_info(void *data, const struct pw_port_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d port %d changed\n", rd->id, info->id);
+ pd->info = pw_port_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_port(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+ .param = event_param
+};
+
+static void factory_event_info(void *data, const struct pw_factory_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d factory %d changed\n", rd->id, info->id);
+ pd->info = pw_factory_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_factory(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_factory_events factory_events = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = factory_event_info
+};
+
+static void client_event_info(void *data, const struct pw_client_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d client %d changed\n", rd->id, info->id);
+ pd->info = pw_client_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_client(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static void client_event_permissions(void *_data, uint32_t index,
+ uint32_t n_permissions, const struct pw_permission *permissions)
+{
+ struct proxy_data *data = _data;
+ struct remote_data *rd = data->rd;
+ uint32_t i;
+
+ printf("remote %d node %d index %d\n",
+ rd->id, data->global->id, index);
+
+ for (i = 0; i < n_permissions; i++) {
+ if (permissions[i].id == PW_ID_ANY)
+ printf(" default:");
+ else
+ printf(" %u:", permissions[i].id);
+ printf(" "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(permissions[i].permissions));
+ }
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info,
+ .permissions = client_event_permissions
+};
+
+static void link_event_info(void *data, const struct pw_link_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d link %d changed\n", rd->id, info->id);
+ pd->info = pw_link_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_link(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_link_events link_events = {
+ PW_VERSION_LINK_EVENTS,
+ .info = link_event_info
+};
+
+
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d device %d changed\n", rd->id, info->id);
+ pd->info = pw_device_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_device(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = event_param
+};
+
+static void session_info_free(struct pw_session_info *info)
+{
+ free(info->params);
+ pw_properties_free ((struct pw_properties *)info->props);
+ free(info);
+}
+
+static void session_event_info(void *data,
+ const struct pw_session_info *update)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ struct pw_session_info *info = pd->info;
+
+ if (!info) {
+ info = pd->info = calloc(1, sizeof(*info));
+ info->id = update->id;
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free(info->params);
+ info->params = malloc(info->n_params * sizeof(struct spa_param_info));
+ memcpy(info->params, update->params,
+ info->n_params * sizeof(struct spa_param_info));
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) {
+ pw_properties_free ((struct pw_properties *)info->props);
+ info->props =
+ (struct spa_dict *) pw_properties_new_dict (update->props);
+ }
+
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_session(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_session_events session_events = {
+ PW_VERSION_SESSION_EVENTS,
+ .info = session_event_info,
+ .param = event_param
+};
+
+static void endpoint_info_free(struct pw_endpoint_info *info)
+{
+ free(info->name);
+ free(info->media_class);
+ free(info->params);
+ pw_properties_free ((struct pw_properties *)info->props);
+ free(info);
+}
+
+static void endpoint_event_info(void *data,
+ const struct pw_endpoint_info *update)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ struct pw_endpoint_info *info = pd->info;
+
+ if (!info) {
+ info = pd->info = calloc(1, sizeof(*info));
+ info->id = update->id;
+ info->name = update->name ? strdup(update->name) : NULL;
+ info->media_class = update->media_class ? strdup(update->media_class) : NULL;
+ info->direction = update->direction;
+ info->flags = update->flags;
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_STREAMS)
+ info->n_streams = update->n_streams;
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_SESSION)
+ info->session_id = update->session_id;
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free(info->params);
+ info->params = malloc(info->n_params * sizeof(struct spa_param_info));
+ memcpy(info->params, update->params,
+ info->n_params * sizeof(struct spa_param_info));
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) {
+ pw_properties_free ((struct pw_properties *)info->props);
+ info->props =
+ (struct spa_dict *) pw_properties_new_dict (update->props);
+ }
+
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_endpoint(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_endpoint_events endpoint_events = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = endpoint_event_info,
+ .param = event_param
+};
+
+static void endpoint_stream_info_free(struct pw_endpoint_stream_info *info)
+{
+ free(info->name);
+ free(info->params);
+ pw_properties_free ((struct pw_properties *)info->props);
+ free(info);
+}
+
+static void endpoint_stream_event_info(void *data,
+ const struct pw_endpoint_stream_info *update)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ struct pw_endpoint_stream_info *info = pd->info;
+
+ if (!info) {
+ info = pd->info = calloc(1, sizeof(*info));
+ info->id = update->id;
+ info->endpoint_id = update->endpoint_id;
+ info->name = update->name ? strdup(update->name) : NULL;
+ }
+ if (update->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free(info->params);
+ info->params = malloc(info->n_params * sizeof(struct spa_param_info));
+ memcpy(info->params, update->params,
+ info->n_params * sizeof(struct spa_param_info));
+ }
+ if (update->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS) {
+ pw_properties_free ((struct pw_properties *)info->props);
+ info->props =
+ (struct spa_dict *) pw_properties_new_dict (update->props);
+ }
+
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_endpoint_stream(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_endpoint_stream_events endpoint_stream_events = {
+ PW_VERSION_ENDPOINT_STREAM_EVENTS,
+ .info = endpoint_stream_event_info,
+ .param = event_param
+};
+
+static void
+removed_proxy (void *data)
+{
+ struct proxy_data *pd = data;
+ pw_proxy_destroy(pd->proxy);
+}
+
+static void
+destroy_proxy (void *data)
+{
+ struct proxy_data *pd = data;
+
+ spa_hook_remove(&pd->proxy_listener);
+ spa_hook_remove(&pd->object_listener);
+
+ if (pd->global)
+ pd->global->proxy = NULL;
+
+ if (pd->info == NULL)
+ return;
+
+ if (pd->class->destroy)
+ pd->class->destroy(pd->info);
+ pd->info = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = removed_proxy,
+ .destroy = destroy_proxy,
+};
+
+static bool do_list_objects(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ pw_map_for_each(&rd->globals, print_global, args);
+ return true;
+}
+
+static const struct class core_class = {
+ .type = PW_TYPE_INTERFACE_Core,
+ .version = PW_VERSION_CORE,
+ .events = &core_events,
+ .destroy = (pw_destroy_t) pw_core_info_free,
+ .info = info_core,
+ .name_key = PW_KEY_CORE_NAME,
+};
+static const struct class module_class = {
+ .type = PW_TYPE_INTERFACE_Module,
+ .version = PW_VERSION_MODULE,
+ .events = &module_events,
+ .destroy = (pw_destroy_t) pw_module_info_free,
+ .info = info_module,
+ .name_key = PW_KEY_MODULE_NAME,
+};
+
+static const struct class factory_class = {
+ .type = PW_TYPE_INTERFACE_Factory,
+ .version = PW_VERSION_FACTORY,
+ .events = &factory_events,
+ .destroy = (pw_destroy_t) pw_factory_info_free,
+ .info = info_factory,
+ .name_key = PW_KEY_FACTORY_NAME,
+};
+
+static const struct class client_class = {
+ .type = PW_TYPE_INTERFACE_Client,
+ .version = PW_VERSION_CLIENT,
+ .events = &client_events,
+ .destroy = (pw_destroy_t) pw_client_info_free,
+ .info = info_client,
+ .name_key = PW_KEY_APP_NAME,
+};
+static const struct class device_class = {
+ .type = PW_TYPE_INTERFACE_Device,
+ .version = PW_VERSION_DEVICE,
+ .events = &device_events,
+ .destroy = (pw_destroy_t) pw_device_info_free,
+ .info = info_device,
+ .name_key = PW_KEY_DEVICE_NAME,
+};
+static const struct class node_class = {
+ .type = PW_TYPE_INTERFACE_Node,
+ .version = PW_VERSION_NODE,
+ .events = &node_events,
+ .destroy = (pw_destroy_t) pw_node_info_free,
+ .info = info_node,
+ .name_key = PW_KEY_NODE_NAME,
+};
+static const struct class port_class = {
+ .type = PW_TYPE_INTERFACE_Port,
+ .version = PW_VERSION_PORT,
+ .events = &port_events,
+ .destroy = (pw_destroy_t) pw_port_info_free,
+ .info = info_port,
+ .name_key = PW_KEY_PORT_NAME,
+};
+static const struct class link_class = {
+ .type = PW_TYPE_INTERFACE_Link,
+ .version = PW_VERSION_LINK,
+ .events = &link_events,
+ .destroy = (pw_destroy_t) pw_link_info_free,
+ .info = info_link,
+};
+static const struct class session_class = {
+ .type = PW_TYPE_INTERFACE_Session,
+ .version = PW_VERSION_SESSION,
+ .events = &session_events,
+ .destroy = (pw_destroy_t) session_info_free,
+ .info = info_session,
+};
+static const struct class endpoint_class = {
+ .type = PW_TYPE_INTERFACE_Endpoint,
+ .version = PW_VERSION_ENDPOINT,
+ .events = &endpoint_events,
+ .destroy = (pw_destroy_t) endpoint_info_free,
+ .info = info_endpoint,
+};
+static const struct class endpoint_stream_class = {
+ .type = PW_TYPE_INTERFACE_EndpointStream,
+ .version = PW_VERSION_ENDPOINT_STREAM,
+ .events = &endpoint_stream_events,
+ .destroy = (pw_destroy_t) endpoint_stream_info_free,
+ .info = info_endpoint_stream,
+};
+static const struct class metadata_class = {
+ .type = PW_TYPE_INTERFACE_Metadata,
+ .version = PW_VERSION_METADATA,
+ .name_key = PW_KEY_METADATA_NAME,
+};
+
+static const struct class *classes[] =
+{
+ &core_class,
+ &module_class,
+ &factory_class,
+ &client_class,
+ &device_class,
+ &node_class,
+ &port_class,
+ &link_class,
+ &session_class,
+ &endpoint_class,
+ &endpoint_stream_class,
+ &metadata_class,
+};
+
+static const struct class *find_class(const char *type, uint32_t version)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(classes, c) {
+ if (spa_streq((*c)->type, type) &&
+ (*c)->version <= version)
+ return *c;
+ }
+ return NULL;
+}
+
+static bool bind_global(struct remote_data *rd, struct global *global, char **error)
+{
+ const struct class *class;
+ struct proxy_data *pd;
+ struct pw_proxy *proxy;
+
+ class = find_class(global->type, global->version);
+ if (class == NULL) {
+ *error = spa_aprintf("unsupported type %s", global->type);
+ return false;
+ }
+ global->class = class;
+
+ proxy = pw_registry_bind(rd->registry,
+ global->id,
+ global->type,
+ class->version,
+ sizeof(struct proxy_data));
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->rd = rd;
+ pd->global = global;
+ pd->proxy = proxy;
+ pd->class = class;
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, class->events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+
+ global->proxy = proxy;
+
+ rd->prompt_pending = pw_core_sync(rd->core, 0, 0);
+
+ return true;
+}
+
+static bool do_global_info(struct global *global, char **error)
+{
+ struct remote_data *rd = global->rd;
+ struct proxy_data *pd;
+
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ global->info_pending = true;
+ } else {
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (pd->class->info)
+ pd->class->info(pd);
+ }
+ return true;
+}
+static int do_global_info_all(void *obj, void *data)
+{
+ struct global *global = obj;
+ char *error;
+
+ if (global == NULL)
+ return 0;
+
+ if (!do_global_info(global, &error)) {
+ fprintf(stderr, "info: %s\n", error);
+ free(error);
+ }
+ return 0;
+}
+
+static bool do_info(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[1];
+ int n;
+ struct global *global;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <object-id>|all", cmd);
+ return false;
+ }
+ if (spa_streq(a[0], "all")) {
+ pw_map_for_each(&rd->globals, do_global_info_all, NULL);
+ }
+ else {
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ return do_global_info(global, error);
+ }
+ return true;
+}
+
+static bool do_create_device(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[2];
+ int n;
+ uint32_t id;
+ struct pw_proxy *proxy;
+ struct pw_properties *props = NULL;
+ struct proxy_data *pd;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <factory-name> [<properties>]", cmd);
+ return false;
+ }
+ if (n == 2)
+ props = pw_properties_new_string(a[1]);
+
+ proxy = pw_core_create_object(rd->core, a[0],
+ PW_TYPE_INTERFACE_Device,
+ PW_VERSION_DEVICE,
+ props ? &props->dict : NULL,
+ sizeof(struct proxy_data));
+
+ pw_properties_free(props);
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->rd = rd;
+ pd->proxy = proxy;
+ pd->class = &device_class;
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, &device_events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+
+ id = pw_map_insert_new(&data->vars, proxy);
+ if (rd->data->interactive)
+ printf("%d = @proxy:%d\n", id, pw_proxy_get_id(proxy));
+
+ return true;
+}
+
+static bool do_create_node(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[2];
+ int n;
+ uint32_t id;
+ struct pw_proxy *proxy;
+ struct pw_properties *props = NULL;
+ struct proxy_data *pd;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <factory-name> [<properties>]", cmd);
+ return false;
+ }
+ if (n == 2)
+ props = pw_properties_new_string(a[1]);
+
+ proxy = pw_core_create_object(rd->core, a[0],
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ props ? &props->dict : NULL,
+ sizeof(struct proxy_data));
+
+ pw_properties_free(props);
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->rd = rd;
+ pd->proxy = proxy;
+ pd->class = &node_class;
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, &node_events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+
+ id = pw_map_insert_new(&data->vars, proxy);
+ if (rd->data->interactive)
+ printf("%d = @proxy:%d\n", id, pw_proxy_get_id(proxy));
+
+ return true;
+}
+
+static bool do_destroy(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[1];
+ int n;
+ struct global *global;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <object-id>", cmd);
+ return false;
+ }
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ pw_registry_destroy(rd->registry, global->id);
+
+ return true;
+}
+
+static struct global *
+obj_global_port(struct remote_data *rd, struct global *global, const char *port_direction, const char *port_id)
+{
+ struct global *global_port_found = NULL;
+ uint32_t *ports = NULL;
+ int port_count;
+
+ port_count = children_of(rd, global->id, PW_TYPE_INTERFACE_Port, &ports);
+
+ if (port_count <= 0)
+ return NULL;
+
+ for (int i = 0; i < port_count; i++) {
+ struct global *global_port = obj_global(rd, ports[i]);
+
+ if (!global_port)
+ continue;
+
+ struct spa_dict *props_port = global_props(global_port);
+
+ if (spa_streq(spa_dict_lookup(props_port, "port.direction"), port_direction)
+ && spa_streq(spa_dict_lookup(props_port, "port.id"), port_id)) {
+ global_port_found = global_port;
+ break;
+ }
+ }
+
+ free(ports);
+ return global_port_found;
+}
+
+static void create_link_with_properties(struct data *data, struct pw_properties *props)
+{
+ struct remote_data *rd = data->current;
+ uint32_t id;
+ struct pw_proxy *proxy;
+ struct proxy_data *pd;
+
+ proxy = (struct pw_proxy*)pw_core_create_object(rd->core,
+ "link-factory",
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ props ? &props->dict : NULL,
+ sizeof(struct proxy_data));
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->rd = rd;
+ pd->proxy = proxy;
+ pd->class = &link_class;
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, &link_events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+
+ id = pw_map_insert_new(&data->vars, proxy);
+ if (rd->data->interactive)
+ printf("%d = @proxy:%d\n", id, pw_proxy_get_id((struct pw_proxy*)proxy));
+}
+
+static bool do_create_link(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[5];
+ int n;
+ struct pw_properties *props = NULL;
+
+ n = pw_split_ip(args, WHITESPACE, 5, a);
+ if (n < 4) {
+ *error = spa_aprintf("%s <node-id> <port> <node-id> <port> [<properties>]", cmd);
+ return false;
+ }
+ if (n == 5)
+ props = pw_properties_new_string(a[4]);
+ else
+ props = pw_properties_new(NULL, NULL);
+
+ if (!spa_streq(a[0], "-"))
+ pw_properties_set(props, PW_KEY_LINK_OUTPUT_NODE, a[0]);
+ if (!spa_streq(a[1], "-"))
+ pw_properties_set(props, PW_KEY_LINK_OUTPUT_PORT, a[1]);
+ if (!spa_streq(a[2], "-"))
+ pw_properties_set(props, PW_KEY_LINK_INPUT_NODE, a[2]);
+ if (!spa_streq(a[3], "-"))
+ pw_properties_set(props, PW_KEY_LINK_INPUT_PORT, a[3]);
+
+ if (spa_streq(a[1], "*") && spa_streq(a[3], "*")) {
+ struct global *global_out, *global_in;
+ struct proxy_data *pd_out, *pd_in;
+ uint32_t n_output_ports, n_input_ports;
+
+ global_out = find_global(rd, a[0]);
+ if (global_out == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ global_in = find_global(rd, a[2]);
+ if (global_in == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[2]);
+ return false;
+ }
+
+ pd_out = pw_proxy_get_user_data(global_out->proxy);
+ pd_in = pw_proxy_get_user_data(global_in->proxy);
+
+ n_output_ports = ((struct pw_node_info *)pd_out->info)->n_output_ports;
+ n_input_ports = ((struct pw_node_info *)pd_in->info)->n_input_ports;
+
+ if (n_output_ports != n_input_ports) {
+ *error = spa_aprintf("%s: Number of ports don't match (%u != %u)", cmd, n_output_ports, n_input_ports);
+ return false;
+ }
+
+ for (uint32_t i = 0; i < n_output_ports; i++) {
+ char port_id[4];
+ struct global *global_port_out, *global_port_in;
+
+ snprintf(port_id, 4, "%d", i);
+
+ global_port_out = obj_global_port(rd, global_out, "out", port_id);
+ global_port_in = obj_global_port(rd, global_in, "in", port_id);
+
+ if (!global_port_out || !global_port_in)
+ continue;
+
+ pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%d", global_port_out->id);
+ pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%d", global_port_in->id);
+
+ create_link_with_properties(data, props);
+ }
+ } else
+ create_link_with_properties(data, props);
+
+ pw_properties_free(props);
+
+ return true;
+}
+
+static bool do_export_node(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ struct pw_global *global;
+ struct pw_node *node;
+ struct pw_proxy *proxy;
+ char *a[2];
+ int n, idx;
+ uint32_t id;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <node-id> [<remote-var>]", cmd);
+ return false;
+ }
+ if (n == 2) {
+ idx = atoi(a[1]);
+ rd = pw_map_lookup(&data->vars, idx);
+ if (rd == NULL)
+ goto no_remote;
+ }
+
+ global = pw_context_find_global(data->context, atoi(a[0]));
+ if (global == NULL) {
+ *error = spa_aprintf("object %d does not exist", atoi(a[0]));
+ return false;
+ }
+ if (!pw_global_is_type(global, PW_TYPE_INTERFACE_Node)) {
+ *error = spa_aprintf("object %d is not a node", atoi(a[0]));
+ return false;
+ }
+ node = pw_global_get_object(global);
+ proxy = pw_core_export(rd->core, PW_TYPE_INTERFACE_Node, NULL, node, 0);
+
+ id = pw_map_insert_new(&data->vars, proxy);
+ if (rd->data->interactive)
+ printf("%d = @proxy:%d\n", id, pw_proxy_get_id((struct pw_proxy*)proxy));
+
+ return true;
+
+ no_remote:
+ *error = spa_aprintf("Remote %d does not exist", idx);
+ return false;
+}
+
+static bool do_enum_params(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[2];
+ int n;
+ uint32_t param_id;
+ const struct spa_type_info *ti;
+ struct global *global;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 2) {
+ *error = spa_aprintf("%s <object-id> <param-id>", cmd);
+ return false;
+ }
+
+ ti = spa_debug_type_find_short(spa_type_param, a[1]);
+ if (ti == NULL) {
+ *error = spa_aprintf("%s: unknown param type: %s", cmd, a[1]);
+ return false;
+ }
+ param_id = ti->type;
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Node))
+ pw_node_enum_params((struct pw_node*)global->proxy, 0,
+ param_id, 0, 0, NULL);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Port))
+ pw_port_enum_params((struct pw_port*)global->proxy, 0,
+ param_id, 0, 0, NULL);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device))
+ pw_device_enum_params((struct pw_device*)global->proxy, 0,
+ param_id, 0, 0, NULL);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Endpoint))
+ pw_endpoint_enum_params((struct pw_endpoint*)global->proxy, 0,
+ param_id, 0, 0, NULL);
+ else {
+ *error = spa_aprintf("enum-params not implemented on object %d type:%s",
+ atoi(a[0]), global->type);
+ return false;
+ }
+ return true;
+}
+
+static bool do_set_param(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[3];
+ int res, n;
+ uint32_t param_id;
+ struct global *global;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_type_info *ti;
+ struct spa_pod *pod;
+
+ n = pw_split_ip(args, WHITESPACE, 3, a);
+ if (n < 3) {
+ *error = spa_aprintf("%s <object-id> <param-id> <param-json>", cmd);
+ return false;
+ }
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+
+ ti = spa_debug_type_find_short(spa_type_param, a[1]);
+ if (ti == NULL) {
+ *error = spa_aprintf("%s: unknown param type: %s", cmd, a[1]);
+ return false;
+ }
+ if ((res = spa_json_to_pod(&b, 0, ti, a[2], strlen(a[2]))) < 0) {
+ *error = spa_aprintf("%s: can't make pod: %s", cmd, spa_strerror(res));
+ return false;
+ }
+ if ((pod = spa_pod_builder_deref(&b, 0)) == NULL) {
+ *error = spa_aprintf("%s: can't make pod", cmd);
+ return false;
+ }
+ spa_debug_pod(0, NULL, pod);
+
+ param_id = ti->type;
+
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Node))
+ pw_node_set_param((struct pw_node*)global->proxy,
+ param_id, 0, pod);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device))
+ pw_device_set_param((struct pw_device*)global->proxy,
+ param_id, 0, pod);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Endpoint))
+ pw_endpoint_set_param((struct pw_endpoint*)global->proxy,
+ param_id, 0, pod);
+ else {
+ *error = spa_aprintf("set-param not implemented on object %d type:%s",
+ atoi(a[0]), global->type);
+ return false;
+ }
+ return true;
+}
+
+static bool do_permissions(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[3];
+ int n;
+ uint32_t p;
+ struct global *global;
+ struct pw_permission permissions[1];
+
+ n = pw_split_ip(args, WHITESPACE, 3, a);
+ if (n < 3) {
+ *error = spa_aprintf("%s <client-id> <object> <permission>", cmd);
+ return false;
+ }
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (!spa_streq(global->type, PW_TYPE_INTERFACE_Client)) {
+ *error = spa_aprintf("object %d is not a client", atoi(a[0]));
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+
+ p = strtol(a[2], NULL, 0);
+ if (rd->data->interactive)
+ printf("setting permissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(p));
+
+ permissions[0] = PW_PERMISSION_INIT(atoi(a[1]), p);
+ pw_client_update_permissions((struct pw_client*)global->proxy,
+ 1, permissions);
+
+ return true;
+}
+
+static bool do_get_permissions(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[3];
+ int n;
+ struct global *global;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <client-id>", cmd);
+ return false;
+ }
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (!spa_streq(global->type, PW_TYPE_INTERFACE_Client)) {
+ *error = spa_aprintf("object %d is not a client", atoi(a[0]));
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+ pw_client_get_permissions((struct pw_client*)global->proxy,
+ 0, UINT32_MAX);
+
+ return true;
+}
+
+static bool do_send_command(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[3];
+ int res, n;
+ struct global *global;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_type_info *ti;
+ struct spa_pod *pod;
+
+ n = pw_split_ip(args, WHITESPACE, 3, a);
+ if (n < 3) {
+ *error = spa_aprintf("%s <object-id> <command-id> <command-json>", cmd);
+ return false;
+ }
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Node)) {
+ ti = spa_debug_type_find_short(spa_type_node_command_id, a[1]);
+ } else {
+ *error = spa_aprintf("send-command not implemented on object %d type:%s",
+ atoi(a[0]), global->type);
+ return false;
+ }
+
+ if (ti == NULL) {
+ *error = spa_aprintf("%s: unknown node command type: %s", cmd, a[1]);
+ return false;
+ }
+ if ((res = spa_json_to_pod(&b, 0, ti, a[2], strlen(a[2]))) < 0) {
+ *error = spa_aprintf("%s: can't make pod: %s", cmd, spa_strerror(res));
+ return false;
+ }
+ if ((pod = spa_pod_builder_deref(&b, 0)) == NULL) {
+ *error = spa_aprintf("%s: can't make pod", cmd);
+ return false;
+ }
+ spa_debug_pod(0, NULL, pod);
+
+ pw_node_send_command((struct pw_node*)global->proxy, (struct spa_command*)pod);
+ return true;
+}
+
+static struct global *
+obj_global(struct remote_data *rd, uint32_t id)
+{
+ struct global *global;
+ struct proxy_data *pd;
+
+ if (!rd)
+ return NULL;
+
+ global = pw_map_lookup(&rd->globals, id);
+ if (!global)
+ return NULL;
+
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (!pd || !pd->info)
+ return NULL;
+
+ return global;
+}
+
+static struct spa_dict *
+global_props(struct global *global)
+{
+ struct proxy_data *pd;
+
+ if (!global)
+ return NULL;
+
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (!pd || !pd->info)
+ return NULL;
+
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Core))
+ return ((struct pw_core_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Module))
+ return ((struct pw_module_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Device))
+ return ((struct pw_device_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Node))
+ return ((struct pw_node_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Port))
+ return ((struct pw_port_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Factory))
+ return ((struct pw_factory_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Client))
+ return ((struct pw_client_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Link))
+ return ((struct pw_link_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Session))
+ return ((struct pw_session_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Endpoint))
+ return ((struct pw_endpoint_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_EndpointStream))
+ return ((struct pw_endpoint_stream_info *)pd->info)->props;
+
+ return NULL;
+}
+
+static const char *
+global_lookup(struct global *global, const char *key)
+{
+ struct spa_dict *dict;
+
+ dict = global_props(global);
+ if (!dict)
+ return NULL;
+ return spa_dict_lookup(dict, key);
+}
+
+
+static int
+children_of(struct remote_data *rd, uint32_t parent_id,
+ const char *child_type, uint32_t **children)
+{
+ const char *parent_type;
+ union pw_map_item *item;
+ struct global *global;
+ struct proxy_data *pd;
+ const char *parent_key = NULL, *child_key = NULL;
+ const char *parent_value = NULL, *child_value = NULL;
+ int pass, i, count;
+
+ if (!rd || !children)
+ return -1;
+
+ /* get the device info */
+ global = obj_global(rd, parent_id);
+ if (!global)
+ return -1;
+ parent_type = global->type;
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (!pd || !pd->info)
+ return -1;
+
+ /* supported combinations */
+ if (spa_streq(parent_type, PW_TYPE_INTERFACE_Device) &&
+ spa_streq(child_type, PW_TYPE_INTERFACE_Node)) {
+ parent_key = PW_KEY_OBJECT_ID;
+ child_key = PW_KEY_DEVICE_ID;
+ } else if (spa_streq(parent_type, PW_TYPE_INTERFACE_Node) &&
+ spa_streq(child_type, PW_TYPE_INTERFACE_Port)) {
+ parent_key = PW_KEY_OBJECT_ID;
+ child_key = PW_KEY_NODE_ID;
+ } else if (spa_streq(parent_type, PW_TYPE_INTERFACE_Module) &&
+ spa_streq(child_type, PW_TYPE_INTERFACE_Factory)) {
+ parent_key = PW_KEY_OBJECT_ID;
+ child_key = PW_KEY_MODULE_ID;
+ } else if (spa_streq(parent_type, PW_TYPE_INTERFACE_Factory) &&
+ spa_streq(child_type, PW_TYPE_INTERFACE_Device)) {
+ parent_key = PW_KEY_OBJECT_ID;
+ child_key = PW_KEY_FACTORY_ID;
+ } else
+ return -1;
+
+ /* get the parent key value */
+ if (parent_key) {
+ parent_value = global_lookup(global, parent_key);
+ if (!parent_value)
+ return -1;
+ }
+
+ count = 0;
+ *children = NULL;
+ i = 0;
+ for (pass = 1; pass <= 2; pass++) {
+ if (pass == 2) {
+ count = i;
+ if (!count)
+ return 0;
+
+ *children = malloc(sizeof(uint32_t) * count);
+ if (!*children)
+ return -1;
+ }
+ i = 0;
+ pw_array_for_each(item, &rd->globals.items) {
+ if (pw_map_item_is_free(item) || item->data == NULL)
+ continue;
+
+ global = item->data;
+
+ if (!spa_streq(global->type, child_type))
+ continue;
+
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (!pd || !pd->info)
+ return -1;
+
+ if (child_key) {
+ /* get the device path */
+ child_value = global_lookup(global, child_key);
+ if (!child_value)
+ continue;
+ }
+
+ /* match? */
+ if (!spa_streq(parent_value, child_value))
+ continue;
+
+ if (*children)
+ (*children)[i] = global->id;
+ i++;
+
+ }
+ }
+ 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];
+ int n;
+ char *p, *cmd, *args;
+
+ if ((p = strchr(buf, '#')))
+ *p = '\0';
+
+ p = pw_strip(buf, "\n\r \t");
+
+ if (*p == '\0')
+ return true;
+
+ n = pw_split_ip(p, WHITESPACE, 2, a);
+ if (n < 1)
+ return true;
+
+ cmd = a[0];
+ args = n > 1 ? a[1] : "";
+
+ SPA_FOR_EACH_ELEMENT_VAR(command_list, c) {
+ if (spa_streq(c->name, cmd) ||
+ spa_streq(c->alias, cmd)) {
+ return c->func(data, cmd, args, error);
+ }
+ }
+ *error = spa_aprintf("Command \"%s\" does not exist. Type 'help' for usage.", cmd);
+ return false;
+}
+
+/* We need a global variable, readline doesn't have a closure arg */
+static struct data *input_dataptr;
+
+static void input_process_line(char *line)
+{
+ struct data *d = input_dataptr;
+ char *error;
+
+ if (!line)
+ line = strdup("quit");
+
+ if (line[0] != '\0') {
+#ifdef HAVE_READLINE
+ add_history(line);
+#endif
+ if (!parse(d, line, &error)) {
+ fprintf(stderr, "Error: \"%s\"\n", error);
+ free(error);
+ }
+ }
+ free(line);
+}
+
+static void do_input(void *data, int fd, uint32_t mask)
+{
+ struct data *d = data;
+
+ if (mask & SPA_IO_IN) {
+ input_dataptr = d;
+#ifdef HAVE_READLINE
+ rl_callback_read_char();
+#else
+ {
+ char *line = NULL;
+ size_t s = 0;
+
+ if (getline(&line, &s, stdin) < 0) {
+ free(line);
+ line = NULL;
+ }
+ input_process_line(line);
+ }
+#endif
+
+ if (d->current == NULL)
+ pw_main_loop_quit(d->loop);
+ else {
+ struct remote_data *rd = d->current;
+ if (rd->core)
+ rd->prompt_pending = pw_core_sync(rd->core, 0, 0);
+ }
+ }
+}
+
+#ifdef HAVE_READLINE
+static char *
+readline_match_command(const char *text, int state)
+{
+ static size_t idx;
+ static int len;
+
+ if (!state) {
+ idx = 0;
+ len = strlen(text);
+ }
+
+ while (idx < SPA_N_ELEMENTS(command_list)) {
+ const char *name = command_list[idx].name;
+ const char *alias = command_list[idx].alias;
+
+ idx++;
+ if (spa_strneq(name, text, len) || spa_strneq(alias, text, len))
+ return strdup(name);
+ }
+
+ return NULL;
+}
+
+static char **
+readline_command_completion(const char *text, int start, int end)
+{
+ char **matches = NULL;
+
+ /* Only try to complete the first word in a line */
+ if (start == 0)
+ matches = rl_completion_matches(text, readline_match_command);
+
+ /* Don't fall back to filename completion */
+ rl_attempted_completion_over = true;
+
+ return matches;
+}
+
+static void readline_init(void)
+{
+ rl_attempted_completion_function = readline_command_completion;
+ rl_callback_handler_install(">> ", input_process_line);
+}
+
+static void readline_cleanup(void)
+{
+ rl_callback_handler_remove();
+}
+#endif
+
+static void do_quit_on_signal(void *data, int signal_number)
+{
+ struct data *d = data;
+ d->quit = true;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, _("%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"),
+ name);
+
+ do_help(data, "help", "", NULL);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ char *opt_remote = NULL;
+ char *error;
+ bool daemon = false, monitor = false;
+ struct remote_data *rd;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "daemon", no_argument, NULL, 'd' },
+ { "remote", required_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0}
+ };
+ int c, i;
+
+ setlinebuf(stdout);
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVmdr:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(&data, argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'd':
+ daemon = true;
+ break;
+ case 'm':
+ monitor = true;
+ break;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "Broken installation: %m\n");
+ return -1;
+ }
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit_on_signal, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit_on_signal, &data);
+
+ spa_list_init(&data.remotes);
+ pw_map_init(&data.vars, 64, 16);
+
+ data.context = pw_context_new(l,
+ pw_properties_new(
+ PW_KEY_CORE_DAEMON, daemon ? "true" : NULL,
+ NULL),
+ 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "Can't create context: %m\n");
+ return -1;
+ }
+
+ pw_context_load_module(data.context, "libpipewire-module-link-factory", NULL, NULL);
+
+ if (!do_connect(&data, "connect", opt_remote, &error)) {
+ fprintf(stderr, "Error: \"%s\"\n", error);
+ return -1;
+ }
+
+ if (optind == argc) {
+ data.interactive = true;
+
+ printf("Welcome to PipeWire version %s. Type 'help' for usage.\n",
+ pw_get_library_version());
+
+#ifdef HAVE_READLINE
+ readline_init();
+#endif
+
+ pw_loop_add_io(l, STDIN_FILENO, SPA_IO_IN|SPA_IO_HUP, false, do_input, &data);
+
+ pw_main_loop_run(data.loop);
+
+#ifdef HAVE_READLINE
+ readline_cleanup();
+#endif
+ } else {
+ char buf[4096], *p, *error;
+
+ p = buf;
+ for (i = optind; i < argc; i++) {
+ p = stpcpy(p, argv[i]);
+ p = stpcpy(p, " ");
+ }
+
+ pw_main_loop_run(data.loop);
+
+ if (!parse(&data, buf, &error)) {
+ fprintf(stderr, "Error: \"%s\"\n", error);
+ free(error);
+ }
+ data.current->prompt_pending = pw_core_sync(data.current->core, 0, 0);
+ while (!data.quit && data.current) {
+ pw_main_loop_run(data.loop);
+ if (!monitor)
+ break;
+ }
+ }
+ spa_list_consume(rd, &data.remotes, link)
+ remote_data_free(rd);
+
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_map_clear(&data.vars);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-dot.c b/src/tools/pw-dot.c
new file mode 100644
index 0000000..30eb780
--- /dev/null
+++ b/src/tools/pw-dot.c
@@ -0,0 +1,1169 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+
+#define GLOBAL_ID_NONE UINT32_MAX
+#define DEFAULT_DOT_PATH "pw.dot"
+
+struct global;
+
+typedef void (*draw_t)(struct global *g);
+typedef void *(*info_update_t) (void *info, const void *update);
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list globals;
+ char *dot_str;
+ const char *dot_rankdir;
+ bool dot_orthoedges;
+
+ bool show_all;
+ bool show_smart;
+ bool show_detail;
+};
+
+struct global {
+ struct spa_list link;
+
+ struct data *data;
+ struct pw_proxy *proxy;
+
+ uint32_t id;
+#define INTERFACE_Port 0
+#define INTERFACE_Node 1
+#define INTERFACE_Link 2
+#define INTERFACE_Client 3
+#define INTERFACE_Device 4
+#define INTERFACE_Module 5
+#define INTERFACE_Factory 6
+ uint32_t type;
+ void *info;
+
+ pw_destroy_t info_destroy;
+ info_update_t info_update;
+ draw_t draw;
+
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+};
+
+static char *dot_str_new(void)
+{
+ return strdup("");
+}
+
+static void dot_str_clear(char **str)
+{
+ if (str && *str) {
+ free(*str);
+ *str = NULL;
+ }
+}
+
+static SPA_PRINTF_FUNC(2,0) void dot_str_vadd(char **str, const char *fmt, va_list varargs)
+{
+ char *res = NULL;
+ char *fmt2 = NULL;
+
+ spa_return_if_fail(str != NULL);
+ spa_return_if_fail(fmt != NULL);
+
+ if (asprintf(&fmt2, "%s%s", *str, fmt) < 0) {
+ spa_assert_not_reached();
+ return;
+ }
+
+ if (vasprintf(&res, fmt2, varargs) < 0) {
+ free (fmt2);
+ spa_assert_not_reached();
+ return;
+ }
+ free (fmt2);
+
+ free(*str);
+ *str = res;
+}
+
+static SPA_PRINTF_FUNC(2,3) void dot_str_add(char **str, const char *fmt, ...)
+{
+ va_list varargs;
+ va_start(varargs, fmt);
+ dot_str_vadd(str, fmt, varargs);
+ va_end(varargs);
+}
+
+static void draw_dict(char **str, const char *title,
+ const struct spa_dict *props)
+{
+ const struct spa_dict_item *item;
+
+ dot_str_add(str, "%s:\\l", title);
+ if (props == NULL || props->n_items == 0) {
+ dot_str_add(str, "- none\\l");
+ return;
+ }
+
+ spa_dict_for_each(item, props) {
+ if (item->value)
+ dot_str_add(str, "- %s: %s\\l", item->key, item->value);
+ else
+ dot_str_add(str, "- %s: (null)\\l", item->key);
+ }
+}
+
+static SPA_PRINTF_FUNC(6,0) void draw_vlabel(char **str, const char *name, uint32_t id, bool detail,
+ const struct spa_dict *props, const char *fmt, va_list varargs)
+{
+ /* draw the label header */
+ dot_str_add(str, "%s_%u [label=\"", name, id);
+
+ /* draw the label body */
+ dot_str_vadd(str, fmt, varargs);
+
+ if (detail)
+ draw_dict(str, "properties", props);
+
+ /*draw the label footer */
+ dot_str_add(str, "%s", "\"];\n");
+}
+
+static SPA_PRINTF_FUNC(6,7) void draw_label(char **str, const char *name, uint32_t id, bool detail,
+ const struct spa_dict *props, const char *fmt, ...)
+{
+ va_list varargs;
+ va_start(varargs, fmt);
+ draw_vlabel(str, name, id, detail, props, fmt, varargs);
+ va_end(varargs);
+}
+
+static void draw_port(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Port);
+
+ struct pw_port_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ /* draw the box */
+ dot_str_add(dot_str,
+ "port_%u [shape=box style=filled fillcolor=%s];\n",
+ g->id,
+ info->direction == PW_DIRECTION_INPUT ? "lightslateblue" : "lightcoral"
+ );
+
+ /* draw the label */
+ draw_label(dot_str,
+ "port", g->id, g->data->show_detail, info->props,
+ "port_id: %u\\lname: %s\\ldirection: %s\\l",
+ g->id,
+ spa_dict_lookup(info->props, PW_KEY_PORT_NAME),
+ pw_direction_as_string(info->direction)
+ );
+}
+
+
+static void draw_node(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Node);
+
+ struct pw_node_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ const char *client_id_str, *factory_id_str;
+ uint32_t client_id, factory_id;
+
+ client_id_str = spa_dict_lookup(info->props, PW_KEY_CLIENT_ID);
+ factory_id_str = spa_dict_lookup(info->props, PW_KEY_FACTORY_ID);
+ client_id = client_id_str ? (uint32_t)atoi(client_id_str) : GLOBAL_ID_NONE;
+ factory_id = factory_id_str ? (uint32_t)atoi(factory_id_str) : GLOBAL_ID_NONE;
+
+ /* draw the node header */
+ dot_str_add(dot_str, "subgraph cluster_node_%u {\n", g->id);
+ dot_str_add(dot_str, "bgcolor=palegreen;\n");
+
+ /* draw the label header */
+ dot_str_add(dot_str, "label=\"");
+
+ /* draw the label body */
+ dot_str_add(dot_str, "node_id: %u\\lname: %s\\lmedia_class: %s\\l",
+ g->id,
+ spa_dict_lookup(info->props, PW_KEY_NODE_NAME),
+ spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS));
+
+ if (g->data->show_detail)
+ draw_dict(dot_str, "properties", info->props);
+
+ /*draw the label footer */
+ dot_str_add(dot_str, "%s", "\"\n");
+
+ /* draw all node ports */
+ struct global *p;
+ const char *prop_node_id;
+ spa_list_for_each(p, &g->data->globals, link) {
+ struct pw_port_info *pinfo;
+ if (p->info == NULL)
+ continue;
+ if (p->type != INTERFACE_Port)
+ continue;
+ pinfo = p->info;
+ prop_node_id = spa_dict_lookup(pinfo->props, PW_KEY_NODE_ID);
+ if (!prop_node_id || (uint32_t)atoi(prop_node_id) != g->id)
+ continue;
+ if (p->draw)
+ p->draw(p);
+ }
+
+ /* draw the client/factory box if all option is enabled */
+ if (g->data->show_all) {
+ dot_str_add(dot_str, "node_%u [shape=box style=filled fillcolor=white];\n", g->id);
+ dot_str_add(dot_str, "node_%u [label=\"client_id: %u\\lfactory_id: %u\\l\"];\n", g->id, client_id, factory_id);
+ }
+
+ /* draw the node footer */
+ dot_str_add(dot_str, "}\n");
+
+ /* draw the client/factory arrows if all option is enabled */
+ if (g->data->show_all) {
+ dot_str_add(dot_str, "node_%u -> client_%u [style=dashed];\n", g->id, client_id);
+ dot_str_add(dot_str, "node_%u -> factory_%u [style=dashed];\n", g->id, factory_id);
+ }
+}
+
+static void draw_link(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Link);
+
+ struct pw_link_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ /* draw the box */
+ dot_str_add(dot_str, "link_%u [shape=box style=filled fillcolor=lightblue];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "link", g->id, g->data->show_detail, info->props,
+ "link_id: %u\\loutput_node_id: %u\\linput_node_id: %u\\loutput_port_id: %u\\linput_port_id: %u\\lstate: %s\\l",
+ g->id,
+ info->output_node_id,
+ info->input_node_id,
+ info->output_port_id,
+ info->input_port_id,
+ pw_link_state_as_string(info->state)
+ );
+
+ /* draw the arrows */
+ dot_str_add(dot_str, "port_%u -> link_%u -> port_%u;\n", info->output_port_id, g->id, info->input_port_id);
+}
+
+static void draw_client(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Client);
+
+ struct pw_client_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ /* draw the box */
+ dot_str_add(dot_str, "client_%u [shape=box style=filled fillcolor=tan1];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "client", g->id, g->data->show_detail, info->props,
+ "client_id: %u\\lname: %s\\lpid: %s\\l",
+ g->id,
+ spa_dict_lookup(info->props, PW_KEY_APP_NAME),
+ spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_ID)
+ );
+}
+
+static void draw_device(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Device);
+
+ struct pw_device_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ const char *client_id_str = spa_dict_lookup(info->props, PW_KEY_CLIENT_ID);
+ const char *factory_id_str = spa_dict_lookup(info->props, PW_KEY_FACTORY_ID);
+ uint32_t client_id = client_id_str ? (uint32_t)atoi(client_id_str) : GLOBAL_ID_NONE;
+ uint32_t factory_id = factory_id_str ? (uint32_t)atoi(factory_id_str) : GLOBAL_ID_NONE;
+
+ /* draw the box */
+ dot_str_add(dot_str, "device_%u [shape=box style=filled fillcolor=lightpink];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "device", g->id, g->data->show_detail, info->props,
+ "device_id: %u\\lname: %s\\lmedia_class: %s\\lapi: %s\\lpath: %s\\l",
+ g->id,
+ spa_dict_lookup(info->props, PW_KEY_DEVICE_NAME),
+ spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS),
+ spa_dict_lookup(info->props, PW_KEY_DEVICE_API),
+ spa_dict_lookup(info->props, PW_KEY_OBJECT_PATH)
+ );
+
+ /* draw the arrows */
+ dot_str_add(dot_str, "device_%u -> client_%u [style=dashed];\n", g->id, client_id);
+ dot_str_add(dot_str, "device_%u -> factory_%u [style=dashed];\n", g->id, factory_id);
+}
+
+static void draw_factory(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Factory);
+
+ struct pw_factory_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ const char *module_id_str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID);
+ uint32_t module_id = module_id_str ? (uint32_t)atoi(module_id_str) : GLOBAL_ID_NONE;
+
+ /* draw the box */
+ dot_str_add(dot_str, "factory_%u [shape=box style=filled fillcolor=lightyellow];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "factory", g->id, g->data->show_detail, info->props,
+ "factory_id: %u\\lname: %s\\lmodule_id: %u\\l",
+ g->id, info->name, module_id
+ );
+
+ /* draw the arrow */
+ dot_str_add(dot_str, "factory_%u -> module_%u [style=dashed];\n", g->id, module_id);
+}
+
+static void draw_module(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Module);
+
+ struct pw_module_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ /* draw the box */
+ dot_str_add(dot_str, "module_%u [shape=box style=filled fillcolor=lightgrey];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "module", g->id, g->data->show_detail, info->props,
+ "module_id: %u\\lname: %s\\l",
+ g->id, info->name
+ );
+}
+
+static bool is_node_id_link_referenced(uint32_t id, struct spa_list *globals)
+{
+ struct global *g;
+ struct pw_link_info *info;
+ spa_list_for_each(g, globals, link) {
+ if (g->info == NULL)
+ continue;
+ if (g->type != INTERFACE_Link)
+ continue;
+ info = g->info;
+ if (info->input_node_id == id || info->output_node_id == id)
+ return true;
+ }
+ return false;
+}
+
+static bool is_module_id_factory_referenced(uint32_t id, struct spa_list *globals)
+{
+ struct global *g;
+ struct pw_factory_info *info;
+ const char *module_id_str;
+ spa_list_for_each(g, globals, link) {
+ if (g->info == NULL)
+ continue;
+ if (g->type != INTERFACE_Factory)
+ continue;
+ info = g->info;
+ module_id_str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID);
+ if (module_id_str && (uint32_t)atoi(module_id_str) == id)
+ return true;
+ }
+ return false;
+}
+
+static bool is_global_referenced(struct global *g)
+{
+ switch (g->type) {
+ case INTERFACE_Node:
+ return is_node_id_link_referenced(g->id, &g->data->globals);
+ case INTERFACE_Module:
+ return is_module_id_factory_referenced(g->id, &g->data->globals);
+ default:
+ break;
+ }
+
+ return true;
+}
+
+static int draw_graph(struct data *d, const char *path)
+{
+ FILE *fp;
+ struct global *g;
+
+ /* draw the header */
+ dot_str_add(&d->dot_str, "digraph pipewire {\n");
+
+ if (d->dot_rankdir) {
+ /* set rank direction, if provided */
+ dot_str_add(&d->dot_str, "rankdir = \"%s\";\n", d->dot_rankdir);
+ }
+
+ if (d->dot_orthoedges) {
+ /* enable orthogonal edges */
+ dot_str_add(&d->dot_str, "splines = ortho;\n");
+ }
+
+ /* iterate the globals */
+ spa_list_for_each(g, &d->globals, link) {
+ /* skip null and non-info globals */
+ if (g->info == NULL)
+ continue;
+
+ /* always skip ports since they are drawn by the nodes */
+ if (g->type == INTERFACE_Port)
+ continue;
+
+ /* skip clients, devices, factories and modules if all option is disabled */
+ if (!d->show_all) {
+ switch (g->type) {
+ case INTERFACE_Client:
+ case INTERFACE_Device:
+ case INTERFACE_Factory:
+ case INTERFACE_Module:
+ continue;
+ default:
+ break;
+ }
+ }
+
+ /* skip not referenced globals if smart option is enabled */
+ if (d->show_smart && !is_global_referenced(g))
+ continue;
+
+ /* draw the global */
+ if (g->draw)
+ g->draw(g);
+ }
+
+ /* draw the footer */
+ dot_str_add(&d->dot_str, "}\n");
+
+ if (spa_streq(path, "-")) {
+ /* wire the dot graph into to stdout */
+ fputs(d->dot_str, stdout);
+ } else {
+ /* open the file */
+ fp = fopen(path, "we");
+ if (fp == NULL) {
+ printf("open error: could not open %s for writing\n", path);
+ return -1;
+ }
+
+ /* wire the dot graph into the file */
+ fputs(d->dot_str, fp);
+ fclose(fp);
+ }
+ return 0;
+}
+
+static void global_event_info(struct global *g, const void *info)
+{
+ if (g->info_update)
+ g->info = g->info_update(g->info, info);
+}
+
+static void port_event_info(void *data, const struct pw_port_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+};
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+};
+
+static void link_event_info(void *data, const struct pw_link_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_link_events link_events = {
+ PW_VERSION_LINK_EVENTS,
+ .info = link_event_info
+};
+
+static void client_event_info(void *data, const struct pw_client_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info
+};
+
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info
+};
+
+static void factory_event_info(void *data, const struct pw_factory_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_factory_events factory_events = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = factory_event_info
+};
+
+static void module_event_info(void *data, const struct pw_module_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info
+};
+
+static void removed_proxy(void *data)
+{
+ struct global *g = data;
+ pw_proxy_destroy(g->proxy);
+}
+
+static void destroy_proxy(void *data)
+{
+ struct global *g = data;
+ spa_hook_remove(&g->object_listener);
+ spa_hook_remove(&g->proxy_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = removed_proxy,
+ .destroy = destroy_proxy,
+};
+
+static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct pw_proxy *proxy;
+ uint32_t client_version;
+ uint32_t object_type;
+ const void *events;
+ pw_destroy_t info_destroy;
+ info_update_t info_update;
+ draw_t draw;
+ struct global *g;
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ events = &port_events;
+ info_destroy = (pw_destroy_t)pw_port_info_free;
+ info_update = (info_update_t)pw_port_info_update;
+ draw = draw_port;
+ client_version = PW_VERSION_PORT;
+ object_type = INTERFACE_Port;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ events = &node_events;
+ info_destroy = (pw_destroy_t)pw_node_info_free;
+ info_update = (info_update_t)pw_node_info_update;
+ draw = draw_node;
+ client_version = PW_VERSION_NODE;
+ object_type = INTERFACE_Node;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ events = &link_events;
+ info_destroy = (pw_destroy_t)pw_link_info_free;
+ info_update = (info_update_t)pw_link_info_update;
+ draw = draw_link;
+ client_version = PW_VERSION_LINK;
+ object_type = INTERFACE_Link;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Client)) {
+ events = &client_events;
+ info_destroy = (pw_destroy_t)pw_client_info_free;
+ info_update = (info_update_t)pw_client_info_update;
+ draw = draw_client;
+ client_version = PW_VERSION_CLIENT;
+ object_type = INTERFACE_Client;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Device)) {
+ events = &device_events;
+ info_destroy = (pw_destroy_t)pw_device_info_free;
+ info_update = (info_update_t)pw_device_info_update;
+ draw = draw_device;
+ client_version = PW_VERSION_DEVICE;
+ object_type = INTERFACE_Device;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Factory)) {
+ events = &factory_events;
+ info_destroy = (pw_destroy_t)pw_factory_info_free;
+ info_update = (info_update_t)pw_factory_info_update;
+ draw = draw_factory;
+ client_version = PW_VERSION_FACTORY;
+ object_type = INTERFACE_Factory;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Module)) {
+ events = &module_events;
+ info_destroy = (pw_destroy_t)pw_module_info_free;
+ info_update = (info_update_t)pw_module_info_update;
+ draw = draw_module;
+ client_version = PW_VERSION_MODULE;
+ object_type = INTERFACE_Module;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Core)) {
+ /* sync to notify we are done with globals */
+ pw_core_sync(d->core, 0, 0);
+ return;
+ }
+ else {
+ return;
+ }
+
+ proxy = pw_registry_bind(d->registry, id, type, client_version, 0);
+ if (proxy == NULL)
+ return;
+
+ /* set the global data */
+ g = calloc(1, sizeof(struct global));
+ g->data = d;
+ g->proxy = proxy;
+
+ g->id = id;
+ g->type = object_type;
+ g->info = NULL;
+
+ g->info_destroy = info_destroy;
+ g->info_update = info_update;
+ g->draw = draw;
+
+ pw_proxy_add_object_listener(proxy, &g->object_listener, events, g);
+ pw_proxy_add_listener(proxy, &g->proxy_listener, &proxy_events, g);
+
+ /* add the global to the list */
+ spa_list_insert(&d->globals, &g->link);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *d = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static int get_data_from_pipewire(struct data *data, const char *opt_remote)
+{
+ struct pw_loop *l;
+ struct global *g;
+
+ data->loop = pw_main_loop_new(NULL);
+ if (data->loop == NULL) {
+ fprintf(stderr, "can't create main loop: %m\n");
+ return -1;
+ }
+
+ l = pw_main_loop_get_loop(data->loop);
+ pw_loop_add_signal(l, SIGINT, do_quit, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit, &data);
+
+ data->context = pw_context_new(l, NULL, 0);
+ if (data->context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ pw_main_loop_destroy(data->loop);
+ return -1;
+ }
+
+ data->core = pw_context_connect(data->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data->core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ pw_context_destroy(data->context);
+ pw_main_loop_destroy(data->loop);
+ return -1;
+ }
+
+ pw_core_add_listener(data->core,
+ &data->core_listener,
+ &core_events, data);
+
+ data->registry = pw_core_get_registry(data->core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data->registry,
+ &data->registry_listener,
+ &registry_events, data);
+
+ pw_main_loop_run(data->loop);
+
+ spa_hook_remove(&data->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data->registry);
+ spa_list_for_each(g, &data->globals, link)
+ pw_proxy_destroy(g->proxy);
+ spa_hook_remove(&data->core_listener);
+ pw_context_destroy(data->context);
+ pw_main_loop_destroy(data->loop);
+
+ return 0;
+}
+
+static void handle_json_obj(struct data *data, struct pw_properties *obj)
+{
+ struct global *g;
+ struct pw_properties *info, *props;
+ const char *str;
+
+ str = pw_properties_get(obj, "type");
+ if (!str) {
+ fprintf(stderr, "invalid object without type\n");
+ return;
+ }
+
+ g = calloc(1, sizeof (struct global));
+ g->data = data;
+
+ if (spa_streq(str, PW_TYPE_INTERFACE_Port)) {
+ g->info_destroy = (pw_destroy_t)pw_port_info_free;
+ g->draw = draw_port;
+ g->type = INTERFACE_Port;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Node)) {
+ g->info_destroy = (pw_destroy_t)pw_node_info_free;
+ g->draw = draw_node;
+ g->type = INTERFACE_Node;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Link)) {
+ g->info_destroy = (pw_destroy_t)pw_link_info_free;
+ g->draw = draw_link;
+ g->type = INTERFACE_Link;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Client)) {
+ g->info_destroy = (pw_destroy_t)pw_client_info_free;
+ g->draw = draw_client;
+ g->type = INTERFACE_Client;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Device)) {
+ g->info_destroy = (pw_destroy_t)pw_device_info_free;
+ g->draw = draw_device;
+ g->type = INTERFACE_Device;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Factory)) {
+ g->info_destroy = (pw_destroy_t)pw_factory_info_free;
+ g->draw = draw_factory;
+ g->type = INTERFACE_Factory;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Module)) {
+ g->info_destroy = (pw_destroy_t)pw_module_info_free;
+ g->draw = draw_module;
+ g->type = INTERFACE_Module;
+ }
+ else {
+ free(g);
+ return;
+ }
+
+ g->id = pw_properties_get_uint32(obj, "id", 0);
+
+ str = pw_properties_get(obj, "info");
+ info = pw_properties_new_string(str);
+
+ str = pw_properties_get(info, "props");
+ props = str ? pw_properties_new_string(str) : NULL;
+
+ switch (g->type) {
+ case INTERFACE_Port: {
+ struct pw_port_info pinfo = {0};
+ pinfo.id = g->id;
+ str = pw_properties_get(info, "direction");
+ pinfo.direction = spa_streq(str, "output") ?
+ PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
+ pinfo.props = props ? &props->dict : NULL;
+ pinfo.change_mask = PW_PORT_CHANGE_MASK_PROPS;
+ g->info = pw_port_info_update(NULL, &pinfo);
+ break;
+ }
+ case INTERFACE_Node: {
+ struct pw_node_info ninfo = {0};
+ ninfo.id = g->id;
+ ninfo.max_input_ports =
+ pw_properties_get_uint32(info, "max-input-ports", 0);
+ ninfo.max_output_ports =
+ pw_properties_get_uint32(info, "max-output-ports", 0);
+ ninfo.n_input_ports =
+ pw_properties_get_uint32(info, "n-input-ports", 0);
+ ninfo.n_output_ports =
+ pw_properties_get_uint32(info, "n-output-ports", 0);
+
+ str = pw_properties_get(info, "state");
+ if (spa_streq(str, "running"))
+ ninfo.state = PW_NODE_STATE_RUNNING;
+ else if (spa_streq(str, "idle"))
+ ninfo.state = PW_NODE_STATE_IDLE;
+ else if (spa_streq(str, "suspended"))
+ ninfo.state = PW_NODE_STATE_SUSPENDED;
+ else if (spa_streq(str, "creating"))
+ ninfo.state = PW_NODE_STATE_CREATING;
+ else
+ ninfo.state = PW_NODE_STATE_ERROR;
+ ninfo.error = pw_properties_get(info, "error");
+
+ ninfo.props = props ? &props->dict : NULL;
+ ninfo.change_mask = PW_NODE_CHANGE_MASK_INPUT_PORTS |
+ PW_NODE_CHANGE_MASK_OUTPUT_PORTS |
+ PW_NODE_CHANGE_MASK_STATE |
+ PW_NODE_CHANGE_MASK_PROPS;
+ g->info = pw_node_info_update(NULL, &ninfo);
+ break;
+ }
+ case INTERFACE_Link: {
+ struct pw_link_info linfo = {0};
+ linfo.id = g->id;
+ linfo.output_node_id =
+ pw_properties_get_uint32(info, "output-node-id", 0);
+ linfo.output_port_id =
+ pw_properties_get_uint32(info, "output-port-id", 0);
+ linfo.input_node_id =
+ pw_properties_get_uint32(info, "input-node-id", 0);
+ linfo.input_port_id =
+ pw_properties_get_uint32(info, "input-port-id", 0);
+
+ str = pw_properties_get(info, "state");
+ if (spa_streq(str, "active"))
+ linfo.state = PW_LINK_STATE_ACTIVE;
+ else if (spa_streq(str, "paused"))
+ linfo.state = PW_LINK_STATE_PAUSED;
+ else if (spa_streq(str, "allocating"))
+ linfo.state = PW_LINK_STATE_ALLOCATING;
+ else if (spa_streq(str, "negotiating"))
+ linfo.state = PW_LINK_STATE_NEGOTIATING;
+ else if (spa_streq(str, "init"))
+ linfo.state = PW_LINK_STATE_INIT;
+ else if (spa_streq(str, "unlinked"))
+ linfo.state = PW_LINK_STATE_UNLINKED;
+ else
+ linfo.state = PW_LINK_STATE_ERROR;
+ linfo.error = pw_properties_get(info, "error");
+
+ linfo.props = props ? &props->dict : NULL;
+ linfo.change_mask = PW_LINK_CHANGE_MASK_STATE |
+ PW_LINK_CHANGE_MASK_PROPS;
+ g->info = pw_link_info_update(NULL, &linfo);
+ break;
+ }
+ case INTERFACE_Client: {
+ struct pw_client_info cinfo = {0};
+ cinfo.id = g->id;
+ cinfo.props = props ? &props->dict : NULL;
+ cinfo.change_mask = PW_CLIENT_CHANGE_MASK_PROPS;
+ g->info = pw_client_info_update(NULL, &cinfo);
+ break;
+ }
+ case INTERFACE_Device: {
+ struct pw_device_info dinfo = {0};
+ dinfo.id = g->id;
+ dinfo.props = props ? &props->dict : NULL;
+ dinfo.change_mask = PW_DEVICE_CHANGE_MASK_PROPS;
+ g->info = pw_device_info_update(NULL, &dinfo);
+ break;
+ }
+ case INTERFACE_Factory: {
+ struct pw_factory_info finfo = {0};
+ finfo.id = g->id;
+ finfo.name = pw_properties_get(info, "name");
+ finfo.type = pw_properties_get(info, "type");
+ finfo.version = pw_properties_get_uint32(info, "version", 0);
+ finfo.props = props ? &props->dict : NULL;
+ finfo.change_mask = PW_FACTORY_CHANGE_MASK_PROPS;
+ g->info = pw_factory_info_update(NULL, &finfo);
+ break;
+ }
+ case INTERFACE_Module: {
+ struct pw_module_info minfo = {0};
+ minfo.id = g->id;
+ minfo.name = pw_properties_get(info, "name");
+ minfo.filename = pw_properties_get(info, "filename");
+ minfo.args = pw_properties_get(info, "args");
+ minfo.props = props ? &props->dict : NULL;
+ minfo.change_mask = PW_MODULE_CHANGE_MASK_PROPS;
+ g->info = pw_module_info_update(NULL, &minfo);
+ break;
+ }
+ default:
+ break;
+ }
+
+ pw_properties_free(info);
+ pw_properties_free(props);
+
+ /* add the global to the list */
+ spa_list_insert(&data->globals, &g->link);
+}
+
+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];
+ const char *value;
+
+ if ((fd = open(json_path, O_CLOEXEC | O_RDONLY)) < 0) {
+ fprintf(stderr, "error opening file '%s': %m\n", json_path);
+ return -1;
+ }
+ if (fstat(fd, &sbuf) < 0) {
+ fprintf(stderr, "error statting file '%s': %m\n", json_path);
+ close(fd);
+ return -1;
+ }
+ if ((json = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) {
+ fprintf(stderr, "error mmapping file '%s': %m\n", json_path);
+ close(fd);
+ return -1;
+ }
+
+ close(fd);
+ spa_json_init(&it[0], json, sbuf.st_size);
+
+ if (spa_json_enter_array(&it[0], &it[1]) <= 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)) {
+ struct pw_properties *obj;
+ obj = pw_properties_new(NULL, NULL);
+ len = spa_json_container_len(&it[1], value, len);
+ pw_properties_update_string(obj, value, len);
+ handle_json_obj(data, obj);
+ pw_properties_free(obj);
+ }
+
+ munmap(json, sbuf.st_size);
+ return 0;
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -a, --all Show all object types\n"
+ " -s, --smart Show linked objects only\n"
+ " -d, --detail Show all object properties\n"
+ " -r, --remote Remote daemon name\n"
+ " -o, --output Output file (Default %s)\n"
+ " -L, --lr Use left-right rank direction\n"
+ " -9, --90 Use orthogonal edges\n"
+ " -j, --json Read objects from pw-dump JSON file\n",
+ name,
+ DEFAULT_DOT_PATH);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct global *g;
+ const char *opt_remote = NULL;
+ const char *dot_path = DEFAULT_DOT_PATH;
+ const char *json_path = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "all", no_argument, NULL, 'a' },
+ { "smart", no_argument, NULL, 's' },
+ { "detail", no_argument, NULL, 'd' },
+ { "remote", required_argument, NULL, 'r' },
+ { "output", required_argument, NULL, 'o' },
+ { "lr", no_argument, NULL, 'L' },
+ { "90", no_argument, NULL, '9' },
+ { "json", required_argument, NULL, 'j' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVasdr:o:L9j:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h' :
+ show_help(argv[0], false);
+ return 0;
+ case 'V' :
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'a' :
+ data.show_all = true;
+ fprintf(stderr, "all option enabled\n");
+ break;
+ case 's' :
+ data.show_smart = true;
+ fprintf(stderr, "smart option enabled\n");
+ break;
+ case 'd' :
+ data.show_detail = true;
+ fprintf(stderr, "detail option enabled\n");
+ break;
+ case 'r' :
+ opt_remote = optarg;
+ fprintf(stderr, "set remote to %s\n", opt_remote);
+ break;
+ case 'o' :
+ dot_path = optarg;
+ fprintf(stderr, "set output file %s\n", dot_path);
+ break;
+ case 'L' :
+ data.dot_rankdir = "LR";
+ fprintf(stderr, "set rank direction to LR\n");
+ break;
+ case '9' :
+ data.dot_orthoedges = true;
+ fprintf(stderr, "orthogonal edges enabled\n");
+ break;
+ case 'j' :
+ json_path = optarg;
+ fprintf(stderr, "Using JSON file %s as input\n", json_path);
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ if (!(data.dot_str = dot_str_new()))
+ return -1;
+
+ spa_list_init(&data.globals);
+
+ if (!json_path && get_data_from_pipewire(&data, opt_remote) < 0)
+ return -1;
+ else if (json_path && get_data_from_json(&data, json_path) < 0)
+ return -1;
+
+ draw_graph(&data, dot_path);
+
+ dot_str_clear(&data.dot_str);
+ spa_list_consume(g, &data.globals, link) {
+ if (g->info && g->info_destroy)
+ g->info_destroy(g->info);
+ spa_list_remove(&g->link);
+ free(g);
+ }
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-dump.c b/src/tools/pw-dump.c
new file mode 100644
index 0000000..ecc2c55
--- /dev/null
+++ b/src/tools/pw-dump.c
@@ -0,0 +1,1664 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+#include <fnmatch.h>
+#include <locale.h>
+
+#if !defined(FNM_EXTMATCH)
+#define FNM_EXTMATCH 0
+#endif
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/pod/iter.h>
+#include <spa/debug/types.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ansi.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/metadata.h>
+
+#define INDENT 2
+
+static bool colors = false;
+
+#define NORMAL (colors ? SPA_ANSI_RESET : "")
+#define LITERAL (colors ? SPA_ANSI_BRIGHT_MAGENTA : "")
+#define NUMBER (colors ? SPA_ANSI_BRIGHT_CYAN : "")
+#define STRING (colors ? SPA_ANSI_BRIGHT_GREEN : "")
+#define KEY (colors ? SPA_ANSI_BRIGHT_BLUE : "")
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core_info *info;
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ int sync_seq;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list object_list;
+
+ const char *pattern;
+
+ FILE *out;
+ int level;
+#define STATE_KEY (1<<0)
+#define STATE_COMMA (1<<1)
+#define STATE_FIRST (1<<2)
+#define STATE_MASK 0xffff0000
+#define STATE_SIMPLE (1<<16)
+ uint32_t state;
+
+ unsigned int monitor:1;
+};
+
+struct param {
+ uint32_t id;
+ int32_t seq;
+ struct spa_list link;
+ struct spa_pod *param;
+};
+
+struct object;
+
+struct class {
+ const char *type;
+ uint32_t version;
+ const void *events;
+ void (*destroy) (struct object *object);
+ void (*dump) (struct object *object);
+ const char *name_key;
+};
+
+struct object {
+ struct spa_list link;
+
+ struct data *data;
+
+ uint32_t id;
+ uint32_t permissions;
+ char *type;
+ uint32_t version;
+ struct pw_properties *props;
+
+ const struct class *class;
+ void *info;
+ struct spa_param_info *params;
+ uint32_t n_params;
+
+ int changed;
+ struct spa_list param_list;
+ struct spa_list pending_list;
+ struct spa_list data_list;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+};
+
+static void core_sync(struct data *d)
+{
+ d->sync_seq = pw_core_sync(d->core, PW_ID_CORE, d->sync_seq);
+ pw_log_debug("sync start %u", d->sync_seq);
+}
+
+static uint32_t clear_params(struct spa_list *param_list, uint32_t id)
+{
+ struct param *p, *t;
+ uint32_t count = 0;
+
+ spa_list_for_each_safe(p, t, param_list, link) {
+ if (id == SPA_ID_INVALID || p->id == id) {
+ spa_list_remove(&p->link);
+ free(p);
+ count++;
+ }
+ }
+ return count;
+}
+
+static struct param *add_param(struct spa_list *params, int seq,
+ uint32_t id, const struct spa_pod *param)
+{
+ struct param *p;
+
+ if (id == SPA_ID_INVALID) {
+ if (param == NULL || !spa_pod_is_object(param)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ id = SPA_POD_OBJECT_ID(param);
+ }
+
+ p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0));
+ if (p == NULL)
+ return NULL;
+
+ p->id = id;
+ p->seq = seq;
+ if (param != NULL) {
+ p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ } else {
+ clear_params(params, id);
+ p->param = NULL;
+ }
+ spa_list_append(params, &p->link);
+
+ return p;
+}
+
+static struct object *find_object(struct data *d, uint32_t id)
+{
+ struct object *o;
+ spa_list_for_each(o, &d->object_list, link) {
+ if (o->id == id)
+ return o;
+ }
+ return NULL;
+}
+
+static void object_update_params(struct spa_list *param_list, struct spa_list *pending_list,
+ uint32_t n_params, struct spa_param_info *params)
+{
+ struct param *p, *t;
+ uint32_t i;
+
+ for (i = 0; i < n_params; i++) {
+ spa_list_for_each_safe(p, t, pending_list, link) {
+ if (p->id == params[i].id &&
+ p->seq != params[i].seq &&
+ p->param != NULL) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+ }
+
+ spa_list_consume(p, pending_list, link) {
+ spa_list_remove(&p->link);
+ if (p->param == NULL) {
+ clear_params(param_list, p->id);
+ free(p);
+ } else {
+ spa_list_append(param_list, &p->link);
+ }
+ }
+}
+
+static void object_destroy(struct object *o)
+{
+ spa_list_remove(&o->link);
+ if (o->proxy)
+ pw_proxy_destroy(o->proxy);
+ pw_properties_free(o->props);
+ clear_params(&o->param_list, SPA_ID_INVALID);
+ clear_params(&o->pending_list, SPA_ID_INVALID);
+ free(o->type);
+ free(o);
+}
+
+static void put_key(struct data *d, const char *key);
+
+static SPA_PRINTF_FUNC(3,4) void put_fmt(struct data *d, const char *key, const char *fmt, ...)
+{
+ va_list va;
+ if (key)
+ put_key(d, key);
+ fprintf(d->out, "%s%s%*s",
+ d->state & STATE_COMMA ? "," : "",
+ d->state & (STATE_MASK | STATE_KEY) ? " " : d->state & STATE_FIRST ? "" : "\n",
+ d->state & (STATE_MASK | STATE_KEY) ? 0 : d->level, "");
+ va_start(va, fmt);
+ vfprintf(d->out, fmt, va);
+ va_end(va);
+ d->state = (d->state & STATE_MASK) + STATE_COMMA;
+}
+
+static void put_key(struct data *d, const char *key)
+{
+ int size = (strlen(key) + 1) * 4;
+ char *str = alloca(size);
+ spa_json_encode_string(str, size, key);
+ put_fmt(d, NULL, "%s%s%s:", KEY, str, NORMAL);
+ d->state = (d->state & STATE_MASK) + STATE_KEY;
+}
+
+static void put_begin(struct data *d, const char *key, const char *type, uint32_t flags)
+{
+ put_fmt(d, key, "%s", type);
+ d->level += INDENT;
+ d->state = (d->state & STATE_MASK) + (flags & STATE_SIMPLE);
+}
+
+static void put_end(struct data *d, const char *type, uint32_t flags)
+{
+ d->level -= INDENT;
+ d->state = d->state & STATE_MASK;
+ put_fmt(d, NULL, "%s", type);
+ d->state = (d->state & STATE_MASK) + STATE_COMMA - (flags & STATE_SIMPLE);
+}
+
+static void put_encoded_string(struct data *d, const char *key, const char *val)
+{
+ put_fmt(d, key, "%s%s%s", STRING, val, NORMAL);
+}
+static void put_string(struct data *d, const char *key, const char *val)
+{
+ int size = (strlen(val) + 1) * 4;
+ char *str = alloca(size);
+ spa_json_encode_string(str, size, val);
+ put_encoded_string(d, key, str);
+}
+
+static void put_literal(struct data *d, const char *key, const char *val)
+{
+ put_fmt(d, key, "%s%s%s", LITERAL, val, NORMAL);
+}
+
+static void put_int(struct data *d, const char *key, int64_t val)
+{
+ put_fmt(d, key, "%s%"PRIi64"%s", NUMBER, val, NORMAL);
+}
+
+static void put_double(struct data *d, const char *key, double val)
+{
+ char buf[128];
+ put_fmt(d, key, "%s%s%s", NUMBER,
+ spa_json_format_float(buf, sizeof(buf), val), NORMAL);
+}
+
+static void put_value(struct data *d, const char *key, const char *val)
+{
+ int64_t li;
+ float fv;
+
+ if (val == NULL)
+ put_literal(d, key, "null");
+ else if (spa_streq(val, "true") || spa_streq(val, "false"))
+ put_literal(d, key, val);
+ else if (spa_atoi64(val, &li, 10))
+ put_int(d, key, li);
+ else if (spa_json_parse_float(val, strlen(val), &fv))
+ put_double(d, key, fv);
+ else
+ put_string(d, key, val);
+}
+
+static void put_dict(struct data *d, const char *key, struct spa_dict *dict)
+{
+ const struct spa_dict_item *it;
+ spa_dict_qsort(dict);
+ put_begin(d, key, "{", 0);
+ spa_dict_for_each(it, dict)
+ put_value(d, it->key, it->value);
+ put_end(d, "}", 0);
+}
+
+static void put_pod_value(struct data *d, const char *key, const struct spa_type_info *info,
+ uint32_t type, void *body, uint32_t size)
+{
+ if (key)
+ put_key(d, key);
+ switch (type) {
+ case SPA_TYPE_Bool:
+ put_value(d, NULL, *(int32_t*)body ? "true" : "false");
+ break;
+ case SPA_TYPE_Id:
+ {
+ const char *str;
+ char fallback[32];
+ uint32_t id = *(uint32_t*)body;
+ str = spa_debug_type_find_short_name(info, *(uint32_t*)body);
+ if (str == NULL) {
+ snprintf(fallback, sizeof(fallback), "id-%08x", id);
+ str = fallback;
+ }
+ put_value(d, NULL, str);
+ break;
+ }
+ case SPA_TYPE_Int:
+ put_int(d, NULL, *(int32_t*)body);
+ break;
+ case SPA_TYPE_Fd:
+ case SPA_TYPE_Long:
+ put_int(d, NULL, *(int64_t*)body);
+ break;
+ case SPA_TYPE_Float:
+ put_double(d, NULL, *(float*)body);
+ break;
+ case SPA_TYPE_Double:
+ put_double(d, NULL, *(double*)body);
+ break;
+ case SPA_TYPE_String:
+ put_string(d, NULL, (const char*)body);
+ break;
+ case SPA_TYPE_Rectangle:
+ {
+ struct spa_rectangle *r = (struct spa_rectangle *)body;
+ put_begin(d, NULL, "{", STATE_SIMPLE);
+ put_int(d, "width", r->width);
+ put_int(d, "height", r->height);
+ put_end(d, "}", STATE_SIMPLE);
+ break;
+ }
+ case SPA_TYPE_Fraction:
+ {
+ struct spa_fraction *f = (struct spa_fraction *)body;
+ put_begin(d, NULL, "{", STATE_SIMPLE);
+ put_int(d, "num", f->num);
+ put_int(d, "denom", f->denom);
+ put_end(d, "}", STATE_SIMPLE);
+ break;
+ }
+ case SPA_TYPE_Array:
+ {
+ struct spa_pod_array_body *b = (struct spa_pod_array_body *)body;
+ void *p;
+ info = info && info->values ? info->values: info;
+ put_begin(d, NULL, "[", STATE_SIMPLE);
+ SPA_POD_ARRAY_BODY_FOREACH(b, size, p)
+ put_pod_value(d, NULL, info, b->child.type, p, b->child.size);
+ put_end(d, "]", STATE_SIMPLE);
+ break;
+ }
+ case SPA_TYPE_Choice:
+ {
+ struct spa_pod_choice_body *b = (struct spa_pod_choice_body *)body;
+ int index = 0;
+
+ if (b->type == SPA_CHOICE_None) {
+ put_pod_value(d, NULL, info, b->child.type,
+ SPA_POD_CONTENTS(struct spa_pod, &b->child),
+ b->child.size);
+ } else {
+ static const char * const range_labels[] = { "default", "min", "max", NULL };
+ static const char * const step_labels[] = { "default", "min", "max", "step", NULL };
+ static const char * const enum_labels[] = { "default", "alt%u" };
+ static const char * const flags_labels[] = { "default", "flag%u" };
+
+ const char * const *labels;
+ const char *label;
+ char buffer[64];
+ int max_labels, flags = 0;
+ void *p;
+
+ switch (b->type) {
+ case SPA_CHOICE_Range:
+ labels = range_labels;
+ max_labels = 3;
+ flags |= STATE_SIMPLE;
+ break;
+ case SPA_CHOICE_Step:
+ labels = step_labels;
+ max_labels = 4;
+ flags |= STATE_SIMPLE;
+ break;
+ case SPA_CHOICE_Enum:
+ labels = enum_labels;
+ max_labels = 1;
+ break;
+ case SPA_CHOICE_Flags:
+ labels = flags_labels;
+ max_labels = 1;
+ break;
+ default:
+ labels = NULL;
+ break;
+ }
+ if (labels == NULL)
+ break;
+
+ put_begin(d, NULL, "{", flags);
+ SPA_POD_CHOICE_BODY_FOREACH(b, size, p) {
+ if ((label = labels[SPA_CLAMP(index, 0, max_labels)]) == NULL)
+ break;
+ snprintf(buffer, sizeof(buffer), label, index);
+ put_pod_value(d, buffer, info, b->child.type, p, b->child.size);
+ index++;
+ }
+ put_end(d, "}", flags);
+ }
+ break;
+ }
+ case SPA_TYPE_Object:
+ {
+ put_begin(d, NULL, "{", 0);
+ struct spa_pod_object_body *b = (struct spa_pod_object_body *)body;
+ struct spa_pod_prop *p;
+ const struct spa_type_info *ti, *ii;
+
+ ti = spa_debug_type_find(info, b->type);
+ ii = ti ? spa_debug_type_find(ti->values, 0) : NULL;
+ ii = ii ? spa_debug_type_find(ii->values, b->id) : NULL;
+
+ info = ti ? ti->values : info;
+
+ SPA_POD_OBJECT_BODY_FOREACH(b, size, p) {
+ char fallback[32];
+ const char *name;
+
+ ii = spa_debug_type_find(info, p->key);
+ name = ii ? spa_debug_type_short_name(ii->name) : NULL;
+ if (name == NULL) {
+ snprintf(fallback, sizeof(fallback), "id-%08x", p->key);
+ name = fallback;
+ }
+ put_pod_value(d, name,
+ ii ? ii->values : NULL,
+ p->value.type,
+ SPA_POD_CONTENTS(struct spa_pod_prop, p),
+ p->value.size);
+ }
+ put_end(d, "}", 0);
+ break;
+ }
+ case SPA_TYPE_Struct:
+ {
+ struct spa_pod *b = (struct spa_pod *)body, *p;
+ put_begin(d, NULL, "[", 0);
+ SPA_POD_FOREACH(b, size, p)
+ put_pod_value(d, NULL, info, p->type, SPA_POD_BODY(p), p->size);
+ put_end(d, "]", 0);
+ break;
+ }
+ case SPA_TYPE_None:
+ put_value(d, NULL, NULL);
+ break;
+ }
+}
+static void put_pod(struct data *d, const char *key, const struct spa_pod *pod)
+{
+ if (pod == NULL) {
+ put_value(d, key, NULL);
+ } else {
+ put_pod_value(d, key, SPA_TYPE_ROOT,
+ SPA_POD_TYPE(pod),
+ SPA_POD_BODY(pod),
+ SPA_POD_BODY_SIZE(pod));
+ }
+}
+
+static void put_params(struct data *d, const char *key,
+ struct spa_param_info *params, uint32_t n_params,
+ struct spa_list *list)
+{
+ uint32_t i;
+
+ put_begin(d, key, "{", 0);
+ for (i = 0; i < n_params; i++) {
+ struct spa_param_info *pi = &params[i];
+ struct param *p;
+ uint32_t flags;
+
+ flags = pi->flags & SPA_PARAM_INFO_READ ? 0 : STATE_SIMPLE;
+
+ put_begin(d, spa_debug_type_find_short_name(spa_type_param, pi->id),
+ "[", flags);
+ spa_list_for_each(p, list, link) {
+ if (p->id == pi->id)
+ put_pod(d, NULL, p->param);
+ }
+ put_end(d, "]", flags);
+ }
+ put_end(d, "}", 0);
+}
+
+struct flags_info {
+ const char *name;
+ uint64_t mask;
+};
+
+static void put_flags(struct data *d, const char *key,
+ uint64_t flags, const struct flags_info *info)
+{
+ uint32_t i;
+ put_begin(d, key, "[", STATE_SIMPLE);
+ for (i = 0; info[i].name != NULL; i++) {
+ if (info[i].mask & flags)
+ put_string(d, NULL, info[i].name);
+ }
+ put_end(d, "]", STATE_SIMPLE);
+}
+
+/* core */
+static void core_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_CORE_CHANGE_MASK_PROPS },
+ { NULL, 0 },
+ };
+
+ struct data *d = o->data;
+ struct pw_core_info *i = d->info;
+
+ put_begin(d, "info", "{", 0);
+ put_int(d, "cookie", i->cookie);
+ put_value(d, "user-name", i->user_name);
+ put_value(d, "host-name", i->host_name);
+ put_value(d, "version", i->version);
+ put_value(d, "name", i->name);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static const struct class core_class = {
+ .type = PW_TYPE_INTERFACE_Core,
+ .version = PW_VERSION_CORE,
+ .dump = core_dump,
+ .name_key = PW_KEY_CORE_NAME,
+};
+
+/* client */
+static void client_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_CLIENT_CHANGE_MASK_PROPS },
+ { NULL, 0 },
+ };
+
+ struct data *d = o->data;
+ struct pw_client_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static void client_event_info(void *data, const struct pw_client_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_client_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info,
+};
+
+static void client_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_client_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class client_class = {
+ .type = PW_TYPE_INTERFACE_Client,
+ .version = PW_VERSION_CLIENT,
+ .events = &client_events,
+ .destroy = client_destroy,
+ .dump = client_dump,
+ .name_key = PW_KEY_APP_NAME,
+};
+
+/* module */
+static void module_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_MODULE_CHANGE_MASK_PROPS },
+ { NULL, 0 },
+ };
+
+ struct data *d = o->data;
+ struct pw_module_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ put_value(d, "name", i->name);
+ put_value(d, "filename", i->filename);
+ put_value(d, "args", i->args);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static void module_event_info(void *data, const struct pw_module_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_module_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_MODULE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info,
+};
+
+static void module_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_module_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class module_class = {
+ .type = PW_TYPE_INTERFACE_Module,
+ .version = PW_VERSION_MODULE,
+ .events = &module_events,
+ .destroy = module_destroy,
+ .dump = module_dump,
+ .name_key = PW_KEY_MODULE_NAME,
+};
+
+/* factory */
+static void factory_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_FACTORY_CHANGE_MASK_PROPS },
+ { NULL, 0 },
+ };
+
+ struct data *d = o->data;
+ struct pw_factory_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ put_value(d, "name", i->name);
+ put_value(d, "type", i->type);
+ put_int(d, "version", i->version);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static void factory_event_info(void *data, const struct pw_factory_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_factory_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_FACTORY_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static const struct pw_factory_events factory_events = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = factory_event_info,
+};
+
+static void factory_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_factory_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class factory_class = {
+ .type = PW_TYPE_INTERFACE_Factory,
+ .version = PW_VERSION_FACTORY,
+ .events = &factory_events,
+ .destroy = factory_destroy,
+ .dump = factory_dump,
+ .name_key = PW_KEY_FACTORY_NAME,
+};
+
+/* device */
+static void device_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_DEVICE_CHANGE_MASK_PROPS },
+ { "params", PW_DEVICE_CHANGE_MASK_PARAMS },
+ { NULL, 0 },
+ };
+
+ struct data *d = o->data;
+ struct pw_device_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_params(d, "params", i->params, i->n_params, &o->param_list);
+ put_end(d, "}", 0);
+}
+
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+ int res;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_device_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ o->params = info->params;
+ o->n_params = info->n_params;
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ changed++;
+ add_param(&o->pending_list, 0, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_device_enum_params((struct pw_device*)o->proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static void device_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+ add_param(&o->pending_list, seq, id, param);
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = device_event_param,
+};
+
+static void device_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_device_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class device_class = {
+ .type = PW_TYPE_INTERFACE_Device,
+ .version = PW_VERSION_DEVICE,
+ .events = &device_events,
+ .destroy = device_destroy,
+ .dump = device_dump,
+ .name_key = PW_KEY_DEVICE_NAME,
+};
+
+/* node */
+static void node_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "input-ports", PW_NODE_CHANGE_MASK_INPUT_PORTS },
+ { "output-ports", PW_NODE_CHANGE_MASK_OUTPUT_PORTS },
+ { "state", PW_NODE_CHANGE_MASK_STATE },
+ { "props", PW_NODE_CHANGE_MASK_PROPS },
+ { "params", PW_NODE_CHANGE_MASK_PARAMS },
+ { NULL, 0 },
+ };
+
+ struct data *d = o->data;
+ struct pw_node_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ put_int(d, "max-input-ports", i->max_input_ports);
+ put_int(d, "max-output-ports", i->max_output_ports);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_int(d, "n-input-ports", i->n_input_ports);
+ put_int(d, "n-output-ports", i->n_output_ports);
+ put_value(d, "state", pw_node_state_as_string(i->state));
+ put_value(d, "error", i->error);
+ put_dict(d, "props", i->props);
+ put_params(d, "params", i->params, i->n_params, &o->param_list);
+ put_end(d, "}", 0);
+}
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+ int res;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_node_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ o->params = info->params;
+ o->n_params = info->n_params;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_STATE)
+ changed++;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ changed++;
+ add_param(&o->pending_list, 0, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_node_enum_params((struct pw_node*)o->proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static void node_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+ add_param(&o->pending_list, seq, id, param);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = node_event_param,
+};
+
+static void node_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_node_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class node_class = {
+ .type = PW_TYPE_INTERFACE_Node,
+ .version = PW_VERSION_NODE,
+ .events = &node_events,
+ .destroy = node_destroy,
+ .dump = node_dump,
+ .name_key = PW_KEY_NODE_NAME,
+};
+
+/* port */
+static void port_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_PORT_CHANGE_MASK_PROPS },
+ { "params", PW_PORT_CHANGE_MASK_PARAMS },
+ { NULL, },
+ };
+
+ struct data *d = o->data;
+ struct pw_port_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ put_value(d, "direction", pw_direction_as_string(i->direction));
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_params(d, "params", i->params, i->n_params, &o->param_list);
+ put_end(d, "}", 0);
+}
+
+static void port_event_info(void *data, const struct pw_port_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+ int res;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_port_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ o->params = info->params;
+ o->n_params = info->n_params;
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ changed++;
+ add_param(&o->pending_list, 0, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_port_enum_params((struct pw_port*)o->proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static void port_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+ add_param(&o->pending_list, seq, id, param);
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+ .param = port_event_param,
+};
+
+static void port_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_port_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class port_class = {
+ .type = PW_TYPE_INTERFACE_Port,
+ .version = PW_VERSION_PORT,
+ .events = &port_events,
+ .destroy = port_destroy,
+ .dump = port_dump,
+ .name_key = PW_KEY_PORT_NAME,
+};
+
+/* link */
+static void link_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "state", PW_LINK_CHANGE_MASK_STATE },
+ { "format", PW_LINK_CHANGE_MASK_FORMAT },
+ { "props", PW_LINK_CHANGE_MASK_PROPS },
+ { NULL, },
+ };
+
+ struct data *d = o->data;
+ struct pw_link_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ put_int(d, "output-node-id", i->output_node_id);
+ put_int(d, "output-port-id", i->output_port_id);
+ put_int(d, "input-node-id", i->input_node_id);
+ put_int(d, "input-port-id", i->input_port_id);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_value(d, "state", pw_link_state_as_string(i->state));
+ put_value(d, "error", i->error);
+ put_pod(d, "format", i->format);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static void link_event_info(void *data, const struct pw_link_info *info)
+{
+ struct object *o = data;
+ uint32_t changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_link_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_LINK_CHANGE_MASK_STATE)
+ changed++;
+
+ if (info->change_mask & PW_LINK_CHANGE_MASK_FORMAT)
+ changed++;
+
+ if (info->change_mask & PW_LINK_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static const struct pw_link_events link_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = link_event_info,
+};
+
+static void link_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_link_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class link_class = {
+ .type = PW_TYPE_INTERFACE_Link,
+ .version = PW_VERSION_LINK,
+ .events = &link_events,
+ .destroy = link_destroy,
+ .dump = link_dump,
+};
+
+static void json_dump_val(struct data *d, const char *key, struct spa_json *it, const char *value, int len)
+{
+ struct spa_json sub;
+ if (spa_json_is_array(value, len)) {
+ put_begin(d, key, "[", STATE_SIMPLE);
+ spa_json_enter(it, &sub);
+ while ((len = spa_json_next(&sub, &value)) > 0) {
+ json_dump_val(d, NULL, &sub, value, len);
+ }
+ put_end(d, "]", STATE_SIMPLE);
+ } else if (spa_json_is_object(value, len)) {
+ 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;
+ 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));
+ } else {
+ put_value(d, key, strndupa(value, len));
+ }
+}
+
+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)
+ json_dump_val(d, key, &it[0], val, len);
+}
+
+/* metadata */
+
+struct metadata_entry {
+ struct spa_list link;
+ uint32_t changed;
+ uint32_t subject;
+ char *key;
+ char *value;
+ char *type;
+};
+
+static void metadata_dump(struct object *o)
+{
+ struct data *d = o->data;
+ struct metadata_entry *e;
+ put_dict(d, "props", &o->props->dict);
+ put_begin(d, "metadata", "[", 0);
+ spa_list_for_each(e, &o->data_list, link) {
+ if (e->changed == 0)
+ continue;
+ put_begin(d, NULL, "{", STATE_SIMPLE);
+ put_int(d, "subject", e->subject);
+ put_value(d, "key", e->key);
+ put_value(d, "type", e->type);
+ if (e->type != NULL && spa_streq(e->type, "Spa:String:JSON"))
+ json_dump(d, "value", e->value);
+ else
+ put_value(d, "value", e->value);
+ put_end(d, "}", STATE_SIMPLE);
+ e->changed = 0;
+ }
+ put_end(d, "]", 0);
+}
+
+static struct metadata_entry *metadata_find(struct object *o, uint32_t subject, const char *key)
+{
+ struct metadata_entry *e;
+ spa_list_for_each(e, &o->data_list, link) {
+ if ((e->subject == subject) &&
+ (key == NULL || spa_streq(e->key, key)))
+ return e;
+ }
+ return NULL;
+}
+
+static int metadata_property(void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct object *o = data;
+ struct metadata_entry *e;
+
+ while ((e = metadata_find(o, subject, key)) != NULL) {
+ spa_list_remove(&e->link);
+ free(e);
+ }
+ if (key != NULL && value != NULL) {
+ size_t size = strlen(key) + 1;
+ size += strlen(value) + 1;
+ size += type ? strlen(type) + 1 : 0;
+
+ e = calloc(1, sizeof(*e) + size);
+ if (e == NULL)
+ return -errno;
+
+ e->subject = subject;
+ e->key = SPA_PTROFF(e, sizeof(*e), void);
+ strcpy(e->key, key);
+ e->value = SPA_PTROFF(e->key, strlen(e->key) + 1, void);
+ strcpy(e->value, value);
+ if (type) {
+ e->type = SPA_PTROFF(e->value, strlen(e->value) + 1, void);
+ strcpy(e->type, type);
+ } else {
+ e->type = NULL;
+ }
+ spa_list_append(&o->data_list, &e->link);
+ e->changed++;
+ }
+ o->changed++;
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property,
+};
+
+static void metadata_destroy(struct object *o)
+{
+ struct metadata_entry *e;
+ spa_list_consume(e, &o->data_list, link) {
+ spa_list_remove(&e->link);
+ free(e);
+ }
+}
+
+static const struct class metadata_class = {
+ .type = PW_TYPE_INTERFACE_Metadata,
+ .version = PW_VERSION_METADATA,
+ .events = &metadata_events,
+ .destroy = metadata_destroy,
+ .dump = metadata_dump,
+ .name_key = PW_KEY_METADATA_NAME,
+};
+
+static const struct class *classes[] =
+{
+ &core_class,
+ &module_class,
+ &factory_class,
+ &client_class,
+ &device_class,
+ &node_class,
+ &port_class,
+ &link_class,
+ &metadata_class,
+};
+
+static const struct class *find_class(const char *type, uint32_t version)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(classes, c) {
+ if (spa_streq((*c)->type, type) &&
+ (*c)->version <= version)
+ return *c;
+ }
+ return NULL;
+}
+
+static void
+destroy_removed(void *data)
+{
+ struct object *o = data;
+ pw_proxy_destroy(o->proxy);
+}
+
+static void
+destroy_proxy(void *data)
+{
+ struct object *o = data;
+
+ spa_hook_remove(&o->proxy_listener);
+ if (o->class != NULL) {
+ if (o->class->events)
+ spa_hook_remove(&o->object_listener);
+ if (o->class->destroy)
+ o->class->destroy(o);
+ }
+ o->proxy = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = destroy_removed,
+ .destroy = destroy_proxy,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct object *o;
+
+ o = calloc(1, sizeof(*o));
+ if (o == NULL) {
+ pw_log_error("can't alloc object for %u %s/%d: %m", id, type, version);
+ return;
+ }
+ o->data = d;
+ o->id = id;
+ o->permissions = permissions;
+ o->type = strdup(type);
+ o->version = version;
+ o->props = props ? pw_properties_new_dict(props) : NULL;
+ spa_list_init(&o->param_list);
+ spa_list_init(&o->pending_list);
+ spa_list_init(&o->data_list);
+
+ o->class = find_class(type, version);
+ if (o->class != NULL) {
+ o->proxy = pw_registry_bind(d->registry,
+ id, type, o->class->version, 0);
+ if (o->proxy == NULL)
+ goto bind_failed;
+
+ pw_proxy_add_listener(o->proxy,
+ &o->proxy_listener,
+ &proxy_events, o);
+
+ if (o->class->events)
+ pw_proxy_add_object_listener(o->proxy,
+ &o->object_listener,
+ o->class->events, o);
+ else
+ o->changed++;
+ } else {
+ o->changed++;
+ }
+ spa_list_append(&d->object_list, &o->link);
+
+ core_sync(d);
+ return;
+
+bind_failed:
+ pw_log_error("can't bind object for %u %s/%d: %m", id, type, version);
+ pw_properties_free(o->props);
+ free(o);
+ return;
+}
+
+static bool object_matches(struct object *o, const char *pattern)
+{
+ uint32_t id;
+ const char *str;
+
+ if (spa_atou32(pattern, &id, 0) && o->id == id)
+ return true;
+
+ if (o->props == NULL)
+ return false;
+
+ if (strstr(o->type, pattern) != NULL)
+ return true;
+ if ((str = pw_properties_get(o->props, PW_KEY_OBJECT_PATH)) != NULL &&
+ fnmatch(pattern, str, FNM_EXTMATCH) == 0)
+ return true;
+ if ((str = pw_properties_get(o->props, PW_KEY_OBJECT_SERIAL)) != NULL &&
+ spa_streq(pattern, str))
+ return true;
+ if (o->class != NULL && o->class->name_key != NULL &&
+ (str = pw_properties_get(o->props, o->class->name_key)) != NULL &&
+ fnmatch(pattern, str, FNM_EXTMATCH) == 0)
+ return true;
+ return false;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct data *d = data;
+ struct object *o;
+
+ if ((o = find_object(d, id)) == NULL)
+ return;
+
+ d->state = STATE_FIRST;
+ if (d->pattern != NULL && !object_matches(o, d->pattern))
+ return;
+ if (d->state == STATE_FIRST)
+ put_begin(d, NULL, "[", 0);
+ put_begin(d, NULL, "{", 0);
+ put_int(d, "id", o->id);
+ if (o->class && o->class->dump)
+ put_value(d, "info", NULL);
+ else if (o->props)
+ put_value(d, "props", NULL);
+ put_end(d, "}", 0);
+ if (d->state != STATE_FIRST)
+ put_end(d, "]\n", 0);
+
+ object_destroy(o);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void dump_objects(struct data *d)
+{
+ static const struct flags_info fl[] = {
+ { "r", PW_PERM_R },
+ { "w", PW_PERM_W },
+ { "x", PW_PERM_X },
+ { "m", PW_PERM_M },
+ { NULL, },
+ };
+
+ struct object *o;
+
+ d->state = STATE_FIRST;
+ spa_list_for_each(o, &d->object_list, link) {
+ if (d->pattern != NULL && !object_matches(o, d->pattern))
+ continue;
+ if (o->changed == 0)
+ continue;
+ if (d->state == STATE_FIRST)
+ put_begin(d, NULL, "[", 0);
+ put_begin(d, NULL, "{", 0);
+ put_int(d, "id", o->id);
+ put_value(d, "type", o->type);
+ put_int(d, "version", o->version);
+ put_flags(d, "permissions", o->permissions, fl);
+ if (o->class && o->class->dump)
+ o->class->dump(o);
+ else if (o->props)
+ put_dict(d, "props", &o->props->dict);
+ put_end(d, "}", 0);
+ o->changed = 0;
+ }
+ if (d->state != STATE_FIRST)
+ put_end(d, "]\n", 0);
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *d = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(d->loop);
+}
+
+static void on_core_info(void *data, const struct pw_core_info *info)
+{
+ struct data *d = data;
+ d->info = pw_core_info_update(d->info, info);
+}
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ struct object *o;
+
+ if (id == PW_ID_CORE) {
+ if (d->sync_seq != seq)
+ return;
+
+ pw_log_debug("sync end %u/%u", d->sync_seq, seq);
+
+ spa_list_for_each(o, &d->object_list, link)
+ object_update_params(&o->param_list, &o->pending_list,
+ o->n_params, o->params);
+
+ dump_objects(d);
+ if (!d->monitor)
+ pw_main_loop_quit(d->loop);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .info = on_core_info,
+ .error = on_core_error,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options] [<id>]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -m, --monitor monitor changes\n"
+ " -N, --no-colors disable color output\n"
+ " -C, --color[=WHEN] whether to enable color support. WHEN is `never`, `always`, or `auto`\n",
+ name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct object *o;
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "no-colors", no_argument, NULL, 'N' },
+ { "color", optional_argument, NULL, 'C' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ data.out = stdout;
+ if (isatty(fileno(data.out)) && getenv("NO_COLOR") == NULL)
+ colors = true;
+ setlinebuf(data.out);
+
+ while ((c = getopt_long(argc, argv, "hVr:mNC", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h' :
+ show_help(&data, argv[0], false);
+ return 0;
+ case 'V' :
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r' :
+ opt_remote = optarg;
+ break;
+ case 'm' :
+ data.monitor = true;
+ break;
+ case 'N' :
+ colors = false;
+ break;
+ case 'C' :
+ if (optarg == NULL || !strcmp(optarg, "auto"))
+ break; /* nothing to do, tty detection was done
+ before parsing options */
+ else if (!strcmp(optarg, "never"))
+ colors = false;
+ else if (!strcmp(optarg, "always"))
+ colors = true;
+ else {
+ fprintf(stderr, "Unknown color: %s\n", optarg);
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+
+ if (optind < argc)
+ data.pattern = argv[optind++];
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create main loop: %m\n");
+ return -1;
+ }
+
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit, &data);
+
+ data.context = pw_context_new(l, NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ return -1;
+ }
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ return -1;
+ }
+
+ spa_list_init(&data.object_list);
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ pw_main_loop_run(data.loop);
+
+ spa_list_consume(o, &data.object_list, link)
+ object_destroy(o);
+ if (data.info)
+ pw_core_info_free(data.info);
+
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-link.c b/src/tools/pw-link.c
new file mode 100644
index 0000000..8696eb2
--- /dev/null
+++ b/src/tools/pw-link.c
@@ -0,0 +1,912 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <math.h>
+#include <getopt.h>
+#include <regex.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/defs.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+
+struct object {
+ struct spa_list link;
+
+ uint32_t id;
+#define OBJECT_ANY 0
+#define OBJECT_NODE 1
+#define OBJECT_PORT 2
+#define OBJECT_LINK 3
+ uint32_t type;
+ struct pw_properties *props;
+ uint32_t extra[2];
+};
+
+struct data {
+ struct pw_main_loop *loop;
+
+ const char *opt_remote;
+#define MODE_LIST_OUTPUT (1<<0)
+#define MODE_LIST_INPUT (1<<1)
+#define MODE_LIST_PORTS (MODE_LIST_OUTPUT|MODE_LIST_INPUT)
+#define MODE_LIST_LINKS (1<<2)
+#define MODE_LIST (MODE_LIST_PORTS|MODE_LIST_LINKS)
+#define MODE_MONITOR (1<<3)
+#define MODE_DISCONNECT (1<<4)
+ uint32_t opt_mode;
+ bool opt_id;
+ bool opt_verbose;
+ const char *opt_output;
+ const char *opt_input;
+ struct pw_properties *props;
+
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list objects;
+
+ int sync;
+ int link_res;
+ bool monitoring;
+ bool list_inputs;
+ bool list_outputs;
+ const char *prefix;
+
+ regex_t out_port_regex, *out_regex;
+ regex_t in_port_regex, *in_regex;
+};
+
+static void link_proxy_error(void *data, int seq, int res, const char *message)
+{
+ struct data *d = data;
+ d->link_res = res;
+}
+
+static const struct pw_proxy_events link_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .error = link_proxy_error,
+};
+
+static void core_sync(struct data *data)
+{
+ data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync);
+}
+
+static int create_link(struct data *data)
+{
+ struct pw_proxy *proxy;
+ struct spa_hook listener;
+
+ data->link_res = 0;
+
+ proxy = pw_core_create_object(data->core,
+ "link-factory",
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ &data->props->dict, 0);
+ if (proxy == NULL)
+ return -errno;
+
+ spa_zero(listener);
+ pw_proxy_add_listener(proxy, &listener, &link_proxy_events, data);
+
+ core_sync(data);
+ pw_main_loop_run(data->loop);
+
+ spa_hook_remove(&listener);
+
+ pw_proxy_destroy(proxy);
+
+ return data->link_res;
+}
+
+static struct object *find_object(struct data *data, uint32_t type, uint32_t id)
+{
+ struct object *o;
+ spa_list_for_each(o, &data->objects, link)
+ if ((type == OBJECT_ANY || o->type == type) && o->id == id)
+ return o;
+ return NULL;
+}
+
+static struct object *find_node_port(struct data *data, struct object *node, enum pw_direction direction, const char *port_id)
+{
+ struct object *o;
+
+ spa_list_for_each(o, &data->objects, link) {
+ const char *o_port_id;
+ if (o->type != OBJECT_PORT)
+ continue;
+ if (o->extra[1] != node->id)
+ continue;
+ if (o->extra[0] != direction)
+ continue;
+ if ((o_port_id = pw_properties_get(o->props, PW_KEY_PORT_ID)) == NULL)
+ continue;
+ if (spa_streq(o_port_id, port_id))
+ return o;
+ }
+
+ return NULL;
+}
+
+static char *node_name(char *buffer, int size, struct object *n)
+{
+ const char *name;
+ buffer[0] = '\0';
+ if ((name = pw_properties_get(n->props, PW_KEY_NODE_NAME)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s", name);
+ return buffer;
+}
+
+static char *node_path(char *buffer, int size, struct object *n)
+{
+ const char *name;
+ buffer[0] = '\0';
+ if ((name = pw_properties_get(n->props, PW_KEY_OBJECT_PATH)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s", name);
+ return buffer;
+}
+
+static char *port_name(char *buffer, int size, struct object *n, struct object *p)
+{
+ const char *name1, *name2;
+ buffer[0] = '\0';
+ if ((name1 = pw_properties_get(n->props, PW_KEY_NODE_NAME)) == NULL)
+ return buffer;
+ if ((name2 = pw_properties_get(p->props, PW_KEY_PORT_NAME)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s:%s", name1, name2);
+ return buffer;
+}
+
+static char *port_path(char *buffer, int size, struct object *n, struct object *p)
+{
+ const char *name;
+ buffer[0] = '\0';
+ if ((name = pw_properties_get(p->props, PW_KEY_OBJECT_PATH)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s", name);
+ return buffer;
+}
+
+static char *port_alias(char *buffer, int size, struct object *n, struct object *p)
+{
+ const char *name;
+ buffer[0] = '\0';
+ if ((name = pw_properties_get(p->props, PW_KEY_PORT_ALIAS)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s", name);
+ return buffer;
+}
+
+static void print_port(struct data *data, const char *prefix, struct object *n,
+ struct object *p, bool verbose)
+{
+ char buffer[1024], id[64] = "", *prefix2 = "";
+ if (data->opt_id) {
+ snprintf(id, sizeof(id), "%4d ", p->id);
+ prefix2 = " ";
+ }
+
+ printf("%s%s%s%s\n", data->prefix, prefix,
+ id, port_name(buffer, sizeof(buffer), n, p));
+ if (verbose) {
+ port_path(buffer, sizeof(buffer), n, p);
+ if (buffer[0] != '\0')
+ printf("%s %s%s%s\n", data->prefix, prefix2, prefix, buffer);
+ port_alias(buffer, sizeof(buffer), n, p);
+ if (buffer[0] != '\0')
+ printf("%s %s%s%s\n", data->prefix, prefix2, prefix, buffer);
+ }
+}
+
+static void print_port_id(struct data *data, const char *prefix, uint32_t peer)
+{
+ struct object *n, *p;
+ if ((p = find_object(data, OBJECT_PORT, peer)) == NULL)
+ return;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ return;
+ print_port(data, prefix, n, p, false);
+}
+
+static void do_list_port_links(struct data *data, struct object *node, struct object *port)
+{
+ struct object *o;
+ bool first = false;
+
+ if ((data->opt_mode & MODE_LIST_PORTS) == 0)
+ first = true;
+
+ spa_list_for_each(o, &data->objects, link) {
+ uint32_t peer;
+ char prefix[64], id[16] = "";
+
+ if (data->opt_id)
+ snprintf(id, sizeof(id), "%4d ", o->id);
+
+ if (o->type != OBJECT_LINK)
+ continue;
+
+ if (port->extra[0] == PW_DIRECTION_OUTPUT &&
+ o->extra[0] == port->id) {
+ peer = o->extra[1];
+ snprintf(prefix, sizeof(prefix), "%s |-> ", id);
+ }
+ else if (port->extra[0] == PW_DIRECTION_INPUT &&
+ o->extra[1] == port->id) {
+ peer = o->extra[0];
+ snprintf(prefix, sizeof(prefix), "%s |<- ", id);
+ }
+ else
+ continue;
+
+ if (first) {
+ print_port(data, "", node, port, data->opt_verbose);
+ first = false;
+ }
+ print_port_id(data, prefix, peer);
+ }
+}
+
+static int node_matches(struct data *data, struct object *n, const char *name)
+{
+ char buffer[1024];
+ uint32_t id = atoi(name);
+ if (n->id == id)
+ return 1;
+ if (spa_streq(node_name(buffer, sizeof(buffer), n), name))
+ return 1;
+ if (spa_streq(node_path(buffer, sizeof(buffer), n), name))
+ return 1;
+ return 0;
+}
+
+static int port_matches(struct data *data, struct object *n, struct object *p, const char *name)
+{
+ char buffer[1024];
+ uint32_t id = atoi(name);
+ if (p->id == id)
+ return 1;
+ if (spa_streq(port_name(buffer, sizeof(buffer), n, p), name))
+ return 1;
+ if (spa_streq(port_path(buffer, sizeof(buffer), n, p), name))
+ return 1;
+ if (spa_streq(port_alias(buffer, sizeof(buffer), n, p), name))
+ return 1;
+ return 0;
+}
+
+static int port_regex(struct data *data, struct object *n, struct object *p, regex_t *regex)
+{
+ char buffer[1024];
+ if (regexec(regex, port_name(buffer, sizeof(buffer), n, p), 0, NULL, 0) == 0)
+ return 1;
+ return 0;
+}
+
+static void do_list_ports(struct data *data, struct object *node,
+ enum pw_direction direction, regex_t *regex)
+{
+ struct object *o;
+ spa_list_for_each(o, &data->objects, link) {
+ if (o->type != OBJECT_PORT)
+ continue;
+ if (o->extra[1] != node->id)
+ continue;
+ if (o->extra[0] != direction)
+ continue;
+
+ if (regex && !port_regex(data, node, o, regex))
+ continue;
+
+ if (data->opt_mode & MODE_LIST_PORTS)
+ print_port(data, "", node, o, data->opt_verbose);
+ if (data->opt_mode & MODE_LIST_LINKS)
+ do_list_port_links(data, node, o);
+ }
+}
+
+static void do_list(struct data *data)
+{
+ struct object *n;
+
+ spa_list_for_each(n, &data->objects, link) {
+ if (n->type != OBJECT_NODE)
+ continue;
+ if (data->list_outputs)
+ do_list_ports(data, n, PW_DIRECTION_OUTPUT, data->out_regex);
+ if (data->list_inputs)
+ do_list_ports(data, n, PW_DIRECTION_INPUT, data->in_regex);
+ }
+}
+
+static int do_link_ports(struct data *data)
+{
+ uint32_t in_port = 0, out_port = 0;
+ struct object *n, *p;
+ struct object *in_node = NULL, *out_node = NULL;
+
+ spa_list_for_each(n, &data->objects, link) {
+ if (n->type != OBJECT_NODE)
+ continue;
+
+ if (out_node == NULL && node_matches(data, n, data->opt_output)) {
+ out_node = n;
+ continue;
+ } else if (in_node == NULL && node_matches(data, n, data->opt_input)) {
+ in_node = n;
+ continue;
+ }
+
+ spa_list_for_each(p, &data->objects, link) {
+ if (p->type != OBJECT_PORT)
+ continue;
+ if (p->extra[1] != n->id)
+ continue;
+
+ if (out_port == 0 && p->extra[0] == PW_DIRECTION_OUTPUT &&
+ port_matches(data, n, p, data->opt_output))
+ out_port = p->id;
+ else if (in_port == 0 && p->extra[0] == PW_DIRECTION_INPUT &&
+ port_matches(data, n, p, data->opt_input))
+ in_port = p->id;
+ }
+ }
+
+ if (in_node && out_node) {
+ int i, ret;
+ char port_id[32];
+ bool all_links_exist = true;
+
+ for (i=0;; i++) {
+ snprintf(port_id, sizeof(port_id), "%d", i);
+
+ struct object *port_out = find_node_port(data, out_node, PW_DIRECTION_OUTPUT, port_id);
+ struct object *port_in = find_node_port(data, in_node, PW_DIRECTION_INPUT, port_id);
+
+ if (!port_out && !port_in) {
+ fprintf(stderr, "Input & output port do not exist\n");
+ goto no_port;
+ } else if (!port_in) {
+ fprintf(stderr, "Input port does not exist\n");
+ goto no_port;
+ } else if (!port_out) {
+ fprintf(stderr, "Output port does not exist\n");
+ goto no_port;
+ }
+
+ pw_properties_setf(data->props, PW_KEY_LINK_OUTPUT_PORT, "%u", port_out->id);
+ pw_properties_setf(data->props, PW_KEY_LINK_INPUT_PORT, "%u", port_in->id);
+
+ if ((ret = create_link(data)) < 0 && ret != -EEXIST)
+ return ret;
+
+ if (ret >= 0)
+ all_links_exist = false;
+ }
+ return (all_links_exist ? -EEXIST : 0);
+ }
+
+ if (in_port == 0 || out_port == 0)
+ return -ENOENT;
+
+ pw_properties_setf(data->props, PW_KEY_LINK_OUTPUT_PORT, "%u", out_port);
+ pw_properties_setf(data->props, PW_KEY_LINK_INPUT_PORT, "%u", in_port);
+
+ return create_link(data);
+
+no_port:
+ return -ENOENT;
+}
+
+static int do_unlink_ports(struct data *data)
+{
+ struct object *l, *n, *p;
+ bool found_any = false;
+ struct object *in_node = NULL, *out_node = NULL;
+
+ if (data->opt_input != NULL) {
+ /* 2 args, check if they are node names */
+ spa_list_for_each(n, &data->objects, link) {
+ if (n->type != OBJECT_NODE)
+ continue;
+
+ if (out_node == NULL && node_matches(data, n, data->opt_output)) {
+ out_node = n;
+ continue;
+ } else if (in_node == NULL && node_matches(data, n, data->opt_input)) {
+ in_node = n;
+ continue;
+ }
+ }
+ }
+
+ spa_list_for_each(l, &data->objects, link) {
+ if (l->type != OBJECT_LINK)
+ continue;
+
+ if (data->opt_input == NULL) {
+ /* 1 arg, check link id */
+ if (l->id != (uint32_t)atoi(data->opt_output))
+ continue;
+ } else if (out_node && in_node) {
+ /* 2 args, check nodes */
+ if ((p = find_object(data, OBJECT_PORT, l->extra[0])) == NULL)
+ continue;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ continue;
+ if (n->id != out_node->id)
+ continue;
+
+ if ((p = find_object(data, OBJECT_PORT, l->extra[1])) == NULL)
+ continue;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ continue;
+ if (n->id != in_node->id)
+ continue;
+ } else {
+ /* 2 args, check port names */
+ if ((p = find_object(data, OBJECT_PORT, l->extra[0])) == NULL)
+ continue;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ continue;
+ if (!port_matches(data, n, p, data->opt_output))
+ continue;
+
+ if ((p = find_object(data, OBJECT_PORT, l->extra[1])) == NULL)
+ continue;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ continue;
+ if (!port_matches(data, n, p, data->opt_input))
+ continue;
+ }
+ pw_registry_destroy(data->registry, l->id);
+ found_any = true;
+ }
+ if (!found_any)
+ return -ENOENT;
+
+ core_sync(data);
+ pw_main_loop_run(data->loop);
+
+ return 0;
+}
+
+static int do_monitor_port(struct data *data, struct object *port)
+{
+ regex_t *regex = NULL;
+ bool do_print = false;
+ struct object *node;
+
+ if (port->extra[0] == PW_DIRECTION_OUTPUT && data->list_outputs) {
+ regex = data->out_regex;
+ do_print = true;
+ }
+ if (port->extra[0] == PW_DIRECTION_INPUT && data->list_inputs) {
+ regex = data->in_regex;
+ do_print = true;
+ }
+ if (!do_print)
+ return 0;
+
+ if ((node = find_object(data, OBJECT_NODE, port->extra[1])) == NULL)
+ return -ENOENT;
+
+ if (regex && !port_regex(data, node, port, regex))
+ return 0;
+
+ print_port(data, "", node, port, data->opt_verbose);
+ return 0;
+}
+
+static int do_monitor_link(struct data *data, struct object *link)
+{
+ char buffer1[1024], buffer2[1024], id[64] = "";
+ struct object *n1, *n2, *p1, *p2;
+
+ if (!(data->opt_mode & MODE_LIST_LINKS))
+ return 0;
+
+ if ((p1 = find_object(data, OBJECT_PORT, link->extra[0])) == NULL)
+ return -ENOENT;
+ if ((n1 = find_object(data, OBJECT_NODE, p1->extra[1])) == NULL)
+ return -ENOENT;
+ if (data->out_regex && !port_regex(data, n1, p1, data->out_regex))
+ return 0;
+
+ if ((p2 = find_object(data, OBJECT_PORT, link->extra[1])) == NULL)
+ return -ENOENT;
+ if ((n2 = find_object(data, OBJECT_NODE, p2->extra[1])) == NULL)
+ return -ENOENT;
+ if (data->in_regex && !port_regex(data, n2, p2, data->in_regex))
+ return 0;
+
+ if (data->opt_id)
+ snprintf(id, sizeof(id), "%4d ", link->id);
+
+ printf("%s%s%s -> %s\n", data->prefix, id,
+ port_name(buffer1, sizeof(buffer1), n1, p1),
+ port_name(buffer2, sizeof(buffer2), n2, p2));
+ return 0;
+}
+
+static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ uint32_t t, extra[2];
+ struct object *obj;
+ const char *str;
+
+ if (props == NULL)
+ return;
+
+ spa_zero(extra);
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ t = OBJECT_NODE;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ t = OBJECT_PORT;
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) == NULL)
+ return;
+ if (spa_streq(str, "in"))
+ extra[0] = PW_DIRECTION_INPUT;
+ else if (spa_streq(str, "out"))
+ extra[0] = PW_DIRECTION_OUTPUT;
+ else
+ return;
+ if ((str = spa_dict_lookup(props, PW_KEY_NODE_ID)) == NULL)
+ return;
+ extra[1] = atoi(str);
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ t = OBJECT_LINK;
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT)) == NULL)
+ return;
+ extra[0] = atoi(str);
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT)) == NULL)
+ return;
+ extra[1] = atoi(str);
+ } else
+ return;
+
+ obj = calloc(1, sizeof(*obj));
+ obj->type = t;
+ obj->id = id;
+ obj->props = pw_properties_new_dict(props);
+ memcpy(obj->extra, extra, sizeof(extra));
+ spa_list_append(&d->objects, &obj->link);
+
+ if (d->monitoring) {
+ d->prefix = "+ ";
+ switch (obj->type) {
+ case OBJECT_PORT:
+ do_monitor_port(d, obj);
+ break;
+ case OBJECT_LINK:
+ do_monitor_link(d, obj);
+ break;
+ }
+ }
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct data *d = data;
+ struct object *obj;
+
+ if ((obj = find_object(d, OBJECT_ANY, id)) == NULL)
+ return;
+
+ if (d->monitoring) {
+ d->prefix = "- ";
+ switch (obj->type) {
+ case OBJECT_PORT:
+ do_monitor_port(d, obj);
+ break;
+ case OBJECT_LINK:
+ do_monitor_link(d, obj);
+ break;
+ }
+ }
+
+ spa_list_remove(&obj->link);
+ pw_properties_free(obj->props);
+ free(obj);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ if (d->sync == seq)
+ pw_main_loop_quit(d->loop);
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *d = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%1$s : PipeWire port and link manager.\n"
+ "Generic: %1$s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote=NAME Remote daemon name\n"
+ "List: %1$s [options] [out-pattern] [in-pattern]\n"
+ " -o, --output List output ports\n"
+ " -i, --input List input ports\n"
+ " -l, --links List links\n"
+ " -m, --monitor Monitor links and ports\n"
+ " -I, --id List IDs\n"
+ " -v, --verbose Verbose port properties\n"
+ "Connect: %1$s [options] output input\n"
+ " -L, --linger Linger (default, unless -m is used)\n"
+ " -P, --passive Passive link\n"
+ " -p, --props=PROPS Properties as JSON object\n"
+ "Disconnect: %1$s -d [options] output input\n"
+ " %1$s -d [options] link-id\n"
+ " -d, --disconnect Disconnect ports\n",
+ name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ int res = 0, c;
+ regex_t out_port_regex;
+ regex_t in_port_regex;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "output", no_argument, NULL, 'o' },
+ { "input", no_argument, NULL, 'i' },
+ { "links", no_argument, NULL, 'l' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "id", no_argument, NULL, 'I' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "linger", no_argument, NULL, 'L' },
+ { "passive", no_argument, NULL, 'P' },
+ { "props", required_argument, NULL, 'p' },
+ { "disconnect", no_argument, NULL, 'd' },
+ { NULL, 0, NULL, 0}
+ };
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+ spa_list_init(&data.objects);
+
+ setlinebuf(stdout);
+
+ data.props = pw_properties_new(NULL, NULL);
+ if (data.props == NULL) {
+ fprintf(stderr, "can't create properties: %m\n");
+ return -1;
+ }
+
+ while ((c = getopt_long(argc, argv, "hVr:oilmIvLPp:d", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(&data, argv[0], NULL);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ data.opt_remote = optarg;
+ break;
+ case 'o':
+ data.opt_mode |= MODE_LIST_OUTPUT;
+ break;
+ case 'i':
+ data.opt_mode |= MODE_LIST_INPUT;
+ break;
+ case 'l':
+ data.opt_mode |= MODE_LIST_LINKS;
+ break;
+ case 'm':
+ data.opt_mode |= MODE_MONITOR;
+ break;
+ case 'I':
+ data.opt_id = true;
+ break;
+ case 'v':
+ data.opt_verbose = true;
+ break;
+ case 'L':
+ pw_properties_set(data.props, PW_KEY_OBJECT_LINGER, "true");
+ break;
+ case 'P':
+ pw_properties_set(data.props, PW_KEY_LINK_PASSIVE, "true");
+ break;
+ case 'p':
+ pw_properties_update_string(data.props, optarg, strlen(optarg));
+ break;
+ case 'd':
+ data.opt_mode |= MODE_DISCONNECT;
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+ if (argc == 1)
+ show_help(&data, argv[0], true);
+
+ if (data.opt_id && (data.opt_mode & MODE_LIST) == 0) {
+ fprintf(stderr, "-I option needs one or more of -l, -i or -o\n");
+ return -1;
+ }
+
+ if ((data.opt_mode & MODE_MONITOR) == 0)
+ pw_properties_set(data.props, PW_KEY_OBJECT_LINGER, "true");
+
+ if (optind < argc)
+ data.opt_output = argv[optind++];
+ if (optind < argc)
+ data.opt_input = argv[optind++];
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create mainloop: %m\n");
+ return -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);
+
+ data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ return -1;
+ }
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, data.opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ data.prefix = (data.opt_mode & MODE_MONITOR) ? "= " : "";
+
+ core_sync(&data);
+ pw_main_loop_run(data.loop);
+
+ if ((data.opt_mode & (MODE_LIST_PORTS|MODE_LIST_LINKS)) == MODE_LIST_LINKS)
+ data.list_inputs = data.list_outputs = true;
+ if ((data.opt_mode & MODE_LIST_INPUT) == MODE_LIST_INPUT)
+ data.list_inputs = true;
+ if ((data.opt_mode & MODE_LIST_OUTPUT) == MODE_LIST_OUTPUT)
+ data.list_outputs = true;
+
+ if (data.opt_output) {
+ if (regcomp(&out_port_regex, data.opt_output, REG_EXTENDED | REG_NOSUB) == 0)
+ data.out_regex = &out_port_regex;
+ }
+ if (data.opt_input) {
+ if (regcomp(&in_port_regex, data.opt_input, REG_EXTENDED | REG_NOSUB) == 0)
+ data.in_regex = &in_port_regex;
+ }
+
+ if (data.opt_mode & (MODE_LIST)) {
+ do_list(&data);
+ } else if (data.opt_mode & MODE_DISCONNECT) {
+ if (data.opt_output == NULL) {
+ fprintf(stderr, "missing link-id or output and input port names to disconnect\n");
+ return -1;
+ }
+ if ((res = do_unlink_ports(&data)) < 0) {
+ fprintf(stderr, "failed to unlink ports: %s\n", spa_strerror(res));
+ return -1;
+ }
+ } else {
+ if (data.opt_output == NULL ||
+ data.opt_input == NULL) {
+ fprintf(stderr, "missing output and input port names to connect\n");
+ return -1;
+ }
+ if ((res = do_link_ports(&data)) < 0) {
+ fprintf(stderr, "failed to link ports: %s\n", spa_strerror(res));
+ return -1;
+ }
+ }
+
+ if (data.opt_mode & MODE_MONITOR) {
+ data.monitoring = true;
+ pw_main_loop_run(data.loop);
+ data.monitoring = false;
+ }
+
+ if (data.out_regex)
+ regfree(data.out_regex);
+ if (data.in_regex)
+ regfree(data.in_regex);
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_core_disconnect(data.core);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/tools/pw-loopback.c b/src/tools/pw-loopback.c
new file mode 100644
index 0000000..5f39be6
--- /dev/null
+++ b/src/tools/pw-loopback.c
@@ -0,0 +1,284 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_CHANNEL_MAP "[ FL, FR ]"
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ const char *opt_node_name;
+ const char *opt_group_name;
+ const char *opt_channel_map;
+
+ uint32_t channels;
+ uint32_t latency;
+ float delay;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void module_destroy(void *data)
+{
+ struct data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -n, --name Node name (default '%s')\n"
+ " -g, --group Node group (default '%s')\n"
+ " -c, --channels Number of channels (default %d)\n"
+ " -m, --channel-map Channel map (default '%s')\n"
+ " -l, --latency Desired latency in ms\n"
+ " -d, --delay Desired delay in float s\n"
+ " -C --capture Capture source to connect to (name or serial)\n"
+ " --capture-props Capture stream properties\n"
+ " -P --playback Playback sink to connect to (name or serial)\n"
+ " --playback-props Playback stream properties\n",
+ name,
+ data->opt_node_name,
+ data->opt_group_name,
+ data->channels,
+ data->opt_channel_map);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ char cname[256], value[256];
+ char *args;
+ size_t size;
+ FILE *f;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "group", required_argument, NULL, 'g' },
+ { "name", required_argument, NULL, 'n' },
+ { "channels", required_argument, NULL, 'c' },
+ { "latency", required_argument, NULL, 'l' },
+ { "delay", required_argument, NULL, 'd' },
+ { "capture", required_argument, NULL, 'C' },
+ { "playback", required_argument, NULL, 'P' },
+ { "capture-props", required_argument, NULL, 'i' },
+ { "playback-props", required_argument, NULL, 'o' },
+ { NULL, 0, NULL, 0}
+ };
+ int c, res = -1;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ data.channels = DEFAULT_CHANNELS;
+ data.opt_channel_map = DEFAULT_CHANNEL_MAP;
+ data.opt_group_name = pw_get_client_name();
+ if (snprintf(cname, sizeof(cname), "%s-%zd", argv[0], (size_t) getpid()) > 0)
+ data.opt_group_name = cname;
+ data.opt_node_name = data.opt_group_name;
+
+ data.capture_props = pw_properties_new(NULL, NULL);
+ data.playback_props = pw_properties_new(NULL, NULL);
+ if (data.capture_props == NULL || data.playback_props == NULL) {
+ fprintf(stderr, "can't create properties: %m\n");
+ goto exit;
+ }
+
+ while ((c = getopt_long(argc, argv, "hVr:n:g:c:m:l:d:C:P:i:o:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(&data, argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ case 'n':
+ data.opt_node_name = optarg;
+ break;
+ case 'g':
+ data.opt_group_name = optarg;
+ break;
+ case 'c':
+ data.channels = atoi(optarg);
+ break;
+ case 'm':
+ data.opt_channel_map = optarg;
+ break;
+ case 'l':
+ data.latency = atoi(optarg) * DEFAULT_RATE / SPA_MSEC_PER_SEC;
+ break;
+ case 'd':
+ data.delay = atof(optarg);
+ break;
+ case 'C':
+ pw_properties_set(data.capture_props, PW_KEY_TARGET_OBJECT, optarg);
+ break;
+ case 'P':
+ pw_properties_set(data.playback_props, PW_KEY_TARGET_OBJECT, optarg);
+ break;
+ case 'i':
+ pw_properties_update_string(data.capture_props, optarg, strlen(optarg));
+ break;
+ case 'o':
+ pw_properties_update_string(data.playback_props, optarg, strlen(optarg));
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create main loop: %m\n");
+ goto exit;
+ }
+
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit, &data);
+
+ data.context = pw_context_new(l, NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ goto exit;
+ }
+
+
+ if ((f = open_memstream(&args, &size)) == NULL) {
+ fprintf(stderr, "can't open memstream: %m\n");
+ goto exit;
+ }
+
+ fprintf(f, "{");
+
+ if (opt_remote != NULL)
+ fprintf(f, " remote.name = \"%s\"", opt_remote);
+ if (data.latency != 0)
+ fprintf(f, " node.latency = %u/%u", data.latency, DEFAULT_RATE);
+ if (data.delay != 0.0f)
+ fprintf(f, " target.delay.sec = %s",
+ spa_json_format_float(value, sizeof(value), data.delay));
+ if (data.channels != 0)
+ fprintf(f, " audio.channels = %u", data.channels);
+ if (data.opt_channel_map != NULL)
+ fprintf(f, " audio.position = %s", data.opt_channel_map);
+ if (data.opt_node_name != NULL)
+ fprintf(f, " node.name = %s", data.opt_node_name);
+
+ if (data.opt_group_name != NULL) {
+ pw_properties_set(data.capture_props, PW_KEY_NODE_GROUP, data.opt_group_name);
+ pw_properties_set(data.playback_props, PW_KEY_NODE_GROUP, data.opt_group_name);
+ }
+
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data.capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data.playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ pw_log_info("loading module with %s", args);
+
+ data.module = pw_context_load_module(data.context,
+ "libpipewire-module-loopback", args,
+ NULL);
+ free(args);
+
+ if (data.module == NULL) {
+ fprintf(stderr, "can't load module: %m\n");
+ goto exit;
+ }
+
+ pw_impl_module_add_listener(data.module,
+ &data.module_listener, &module_events, &data);
+
+ pw_main_loop_run(data.loop);
+
+ res = 0;
+exit:
+ if (data.module)
+ pw_impl_module_destroy(data.module);
+ if (data.context)
+ pw_context_destroy(data.context);
+ if (data.loop)
+ pw_main_loop_destroy(data.loop);
+ pw_properties_free(data.capture_props);
+ pw_properties_free(data.playback_props);
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/tools/pw-metadata.c b/src/tools/pw-metadata.c
new file mode 100644
index 0000000..768bd91
--- /dev/null
+++ b/src/tools/pw-metadata.c
@@ -0,0 +1,297 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <math.h>
+#include <getopt.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/defs.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+#include <pipewire/extensions/metadata.h>
+
+struct data {
+ struct pw_main_loop *loop;
+
+ const char *opt_remote;
+ const char *opt_name;
+ bool opt_monitor;
+ bool opt_delete;
+ uint32_t opt_id;
+ const char *opt_key;
+ const char *opt_value;
+ const char *opt_type;
+
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_metadata *metadata;
+ struct spa_hook metadata_listener;
+
+ int sync;
+};
+
+
+static int metadata_property(void *data, uint32_t id,
+ const char *key, const char *type, const char *value)
+{
+ struct data *d = data;
+
+ if ((d->opt_id == SPA_ID_INVALID || d->opt_id == id) &&
+ (d->opt_key == NULL || spa_streq(d->opt_key, key))) {
+ if (key == NULL) {
+ printf("remove: id:%u all keys\n", id);
+ } else if (value == NULL) {
+ printf("remove: id:%u key:'%s'\n", id, key);
+ } else {
+ printf("update: id:%u key:'%s' value:'%s' type:'%s'\n", id, key, value, type);
+ }
+ }
+
+ 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)
+{
+ struct data *d = data;
+ const char *str;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Metadata))
+ return;
+
+ if (props != NULL &&
+ (str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) != NULL &&
+ !spa_streq(str, d->opt_name))
+ return;
+
+ if (d->metadata != NULL) {
+ pw_log_warn("Multiple metadata: ignoring metadata %d", id);
+ return;
+ }
+
+ printf("Found \"%s\" metadata %d\n", d->opt_name, id);
+ d->metadata = pw_registry_bind(d->registry,
+ id, type, PW_VERSION_METADATA, 0);
+
+ if (d->opt_delete) {
+ if (d->opt_id != SPA_ID_INVALID) {
+ if (d->opt_key != NULL)
+ printf("delete property: id:%u key:%s\n", d->opt_id, d->opt_key);
+ else
+ printf("delete properties: id:%u\n", d->opt_id);
+ pw_metadata_set_property(d->metadata, d->opt_id, d->opt_key, NULL, NULL);
+ } else {
+ printf("delete all properties\n");
+ pw_metadata_clear(d->metadata);
+ }
+ } else if (d->opt_id != SPA_ID_INVALID && d->opt_key != NULL && d->opt_value != NULL) {
+ printf("set property: id:%u key:%s value:%s type:%s\n",
+ d->opt_id, d->opt_key, d->opt_value, d->opt_type);
+ pw_metadata_set_property(d->metadata, d->opt_id, d->opt_key, d->opt_type, d->opt_value);
+ } else {
+ pw_metadata_add_listener(d->metadata,
+ &d->metadata_listener,
+ &metadata_events, d);
+ }
+
+ d->sync = pw_core_sync(d->core, PW_ID_CORE, d->sync);
+}
+
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ if (d->sync == seq && !d->opt_monitor)
+ pw_main_loop_quit(d->loop);
+}
+
+static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *d = data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options] [ id [ key [ value [ type ] ] ] ]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -m, --monitor Monitor metadata\n"
+ " -d, --delete Delete metadata\n"
+ " -n, --name Metadata name (default: \"%s\")\n",
+ name, data->opt_name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ int res = 0, c;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "delete", no_argument, NULL, 'd' },
+ { "name", required_argument, NULL, 'n' },
+ { NULL, 0, NULL, 0}
+ };
+
+ setlinebuf(stdout);
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ data.opt_name = "default";
+
+ while ((c = getopt_long(argc, argv, "hVr:mdn:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(&data, argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ data.opt_remote = optarg;
+ break;
+ case 'm':
+ data.opt_monitor = true;
+ break;
+ case 'd':
+ data.opt_delete = true;
+ break;
+ case 'n':
+ data.opt_name = optarg;
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+
+ data.opt_id = SPA_ID_INVALID;
+ if (optind < argc)
+ data.opt_id = atoi(argv[optind++]);
+ if (optind < argc)
+ data.opt_key = argv[optind++];
+ if (optind < argc)
+ data.opt_value = argv[optind++];
+ if (optind < argc)
+ data.opt_type = argv[optind++];
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create mainloop: %m\n");
+ return -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);
+
+ data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ return -1;
+ }
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, data.opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ data.sync = pw_core_sync(data.core, PW_ID_CORE, data.sync);
+
+ pw_main_loop_run(data.loop);
+
+ if (data.metadata)
+ pw_proxy_destroy((struct pw_proxy*)data.metadata);
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_core_disconnect(data.core);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/tools/pw-mididump.c b/src/tools/pw-mididump.c
new file mode 100644
index 0000000..d6b803f
--- /dev/null
+++ b/src/tools/pw-mididump.c
@@ -0,0 +1,233 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <math.h>
+#include <getopt.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/defs.h>
+#include <spa/control/control.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/props.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+
+#include "midifile.h"
+
+struct data;
+
+struct port {
+ struct data *data;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ const char *opt_remote;
+ struct pw_filter *filter;
+ struct port *in_port;
+ int64_t clock_time;
+};
+
+static int dump_file(const char *filename)
+{
+ struct midi_file *file;
+ struct midi_file_info info;
+ struct midi_event ev;
+
+ file = midi_file_open(filename, "r", &info);
+ if (file == NULL) {
+ fprintf(stderr, "error opening %s: %m\n", filename);
+ return -1;
+ }
+
+ printf("opened %s\n", filename);
+
+ while (midi_file_read_event(file, &ev) == 1) {
+ midi_file_dump_event(stdout, &ev);
+ }
+ midi_file_close(file);
+
+ return 0;
+}
+
+static void on_process(void *_data, struct spa_io_position *position)
+{
+ struct data *data = _data;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ struct spa_data *d;
+ struct spa_pod *pod;
+ struct spa_pod_control *c;
+ uint64_t frame;
+
+ frame = data->clock_time;
+ data->clock_time += position->clock.duration;
+
+ b = pw_filter_dequeue_buffer(data->in_port);
+ if (b == NULL)
+ return;
+
+ buf = b->buffer;
+ d = &buf->datas[0];
+
+ if (d->data == NULL)
+ return;
+
+ if ((pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size)) == NULL)
+ return;
+ if (!spa_pod_is_sequence(pod))
+ return;
+
+ SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) {
+ struct midi_event ev;
+
+ if (c->type != SPA_CONTROL_Midi)
+ 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);
+
+ printf("%4d: ", c->offset);
+ midi_file_dump_event(stdout, &ev);
+ }
+
+ pw_filter_queue_buffer(data->in_port, b);
+}
+
+static const struct pw_filter_events filter_events = {
+ PW_VERSION_FILTER_EVENTS,
+ .process = on_process,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+static int dump_filter(struct data *data)
+{
+ data->loop = pw_main_loop_new(NULL);
+ if (data->loop == NULL)
+ return -errno;
+
+ 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);
+
+ data->filter = pw_filter_new_simple(
+ pw_main_loop_get_loop(data->loop),
+ "midi-dump",
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, data->opt_remote,
+ PW_KEY_MEDIA_TYPE, "Midi",
+ PW_KEY_MEDIA_CATEGORY, "Filter",
+ PW_KEY_MEDIA_ROLE, "DSP",
+ NULL),
+ &filter_events,
+ data);
+
+ data->in_port = pw_filter_add_port(data->filter,
+ PW_DIRECTION_INPUT,
+ PW_FILTER_PORT_FLAG_MAP_BUFFERS,
+ sizeof(struct port),
+ pw_properties_new(
+ PW_KEY_FORMAT_DSP, "8 bit raw midi",
+ PW_KEY_PORT_NAME, "input",
+ NULL),
+ NULL, 0);
+
+ if (pw_filter_connect(data->filter, PW_FILTER_FLAG_RT_PROCESS, NULL, 0) < 0) {
+ fprintf(stderr, "can't connect\n");
+ return -1;
+ }
+
+ pw_main_loop_run(data->loop);
+
+ pw_filter_destroy(data->filter);
+ pw_main_loop_destroy(data->loop);
+
+ return 0;
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options] [FILE]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n",
+ name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ int res = 0, c;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0}
+ };
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ setlinebuf(stdout);
+
+ while ((c = getopt_long(argc, argv, "hVr:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ data.opt_remote = optarg;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ if (optind < argc) {
+ res = dump_file(argv[optind]);
+ } else {
+ res = dump_filter(&data);
+ }
+ pw_deinit();
+ return res;
+}
diff --git a/src/tools/pw-mon.c b/src/tools/pw-mon.c
new file mode 100644
index 0000000..2eeb4dd
--- /dev/null
+++ b/src/tools/pw-mon.c
@@ -0,0 +1,875 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/ansi.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/format.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+
+struct proxy_data;
+
+typedef void (*print_func_t) (struct proxy_data *data);
+
+static struct pprefix {
+ const char *prefix;
+ const char *suffix;
+} pprefix[2] = {
+ { .prefix = " ", .suffix = "" },
+ { .prefix = "*", .suffix = "" },
+};
+
+#define with_prefix(use_prefix_) \
+ for (bool once_ = !!printf("%s", (pprefix[!!(use_prefix_)]).prefix); \
+ once_; \
+ once_ = false, printf("%s", (pprefix[!!(use_prefix_)]).suffix))
+
+
+struct param {
+ struct spa_list link;
+ uint32_t id;
+ int seq;
+ struct spa_pod *param;
+ unsigned int changed:1;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list pending_list;
+ struct spa_list global_list;
+};
+
+struct proxy_data {
+ struct data *data;
+ bool first;
+ struct pw_proxy *proxy;
+ uint32_t id;
+ uint32_t permissions;
+ uint32_t version;
+ char *type;
+ void *info;
+ pw_destroy_t destroy;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+ int pending_seq;
+ struct spa_list global_link;
+ struct spa_list pending_link;
+ print_func_t print_func;
+ struct spa_list param_list;
+};
+
+static void add_pending(struct proxy_data *pd)
+{
+ struct data *d = pd->data;
+
+ if (pd->pending_seq == 0) {
+ spa_list_append(&d->pending_list, &pd->pending_link);
+ }
+ pd->pending_seq = pw_core_sync(d->core, 0, pd->pending_seq);
+}
+
+static void remove_pending(struct proxy_data *pd)
+{
+ if (pd->pending_seq != 0) {
+ spa_list_remove(&pd->pending_link);
+ pd->pending_seq = 0;
+ }
+}
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ struct proxy_data *pd, *t;
+
+ spa_list_for_each_safe(pd, t, &d->pending_list, pending_link) {
+ if (pd->pending_seq == seq) {
+ remove_pending(pd);
+ pd->print_func(pd);
+ }
+ }
+}
+
+static void clear_params(struct proxy_data *data)
+{
+ struct param *p;
+ spa_list_consume(p, &data->param_list, link) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+}
+
+static void remove_params(struct proxy_data *data, uint32_t id, int seq)
+{
+ struct param *p, *t;
+
+ spa_list_for_each_safe(p, t, &data->param_list, link) {
+ if (p->id == id && seq != p->seq) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+}
+
+static void event_param(void *_data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct proxy_data *data = _data;
+ struct param *p;
+
+ /* remove all params with the same id and older seq */
+ remove_params(data, id, seq);
+
+ /* add new param */
+ p = malloc(sizeof(struct param) + SPA_POD_SIZE(param));
+ if (p == NULL) {
+ pw_log_error("can't add param: %m");
+ return;
+ }
+
+ p->id = id;
+ p->seq = seq;
+ p->param = SPA_PTROFF(p, sizeof(struct param), struct spa_pod);
+ p->changed = true;
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ spa_list_append(&data->param_list, &p->link);
+}
+
+static void print_params(struct proxy_data *data, bool use_prefix)
+{
+ struct param *p;
+
+ with_prefix(use_prefix) {
+ printf("\tparams:\n");
+ }
+
+ spa_list_for_each(p, &data->param_list, link) {
+ with_prefix(p->changed) {
+ printf("\t id:%u (%s)\n",
+ p->id,
+ spa_debug_type_find_name(spa_type_param, p->id));
+ if (spa_pod_is_object_type(p->param, SPA_TYPE_OBJECT_Format))
+ spa_debug_format(10, NULL, p->param);
+ else
+ spa_debug_pod(10, NULL, p->param);
+ }
+ p->changed = false;
+ }
+}
+
+static void print_properties(const struct spa_dict *props, bool use_prefix)
+{
+ const struct spa_dict_item *item;
+
+ with_prefix(use_prefix) {
+ printf("\tproperties:\n");
+ if (props == NULL || props->n_items == 0) {
+ printf("\t\tnone\n");
+ return;
+ }
+ }
+
+ spa_dict_for_each(item, props) {
+ with_prefix(use_prefix) {
+ if (item->value)
+ printf("\t\t%s = \"%s\"\n", item->key, item->value);
+ else
+ printf("\t\t%s = (null)\n", item->key);
+ }
+ }
+}
+
+#define MARK_CHANGE(f) (!!(print_mark && ((info)->change_mask & (f))))
+
+static void on_core_info(void *data, const struct pw_core_info *info)
+{
+ bool print_all = true, print_mark = true;
+
+ printf("\ttype: %s\n", PW_TYPE_INTERFACE_Core);
+ printf("\tcookie: %u\n", info->cookie);
+ printf("\tuser-name: \"%s\"\n", info->user_name);
+ printf("\thost-name: \"%s\"\n", info->host_name);
+ printf("\tversion: \"%s\"\n", info->version);
+ printf("\tname: \"%s\"\n", info->name);
+ if (print_all) {
+ print_properties(info->props, MARK_CHANGE(PW_CORE_CHANGE_MASK_PROPS));
+ }
+}
+
+static void module_event_info(void *_data, const struct pw_module_info *info)
+{
+ struct proxy_data *data = _data;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->info == NULL) {
+ printf("added:\n");
+ print_mark = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ info = data->info = pw_module_info_update(data->info, info);
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+ printf("\tname: \"%s\"\n", info->name);
+ printf("\tfilename: \"%s\"\n", info->filename);
+ printf("\targs: \"%s\"\n", info->args);
+ if (print_all) {
+ print_properties(info->props, MARK_CHANGE(PW_MODULE_CHANGE_MASK_PROPS));
+ }
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info,
+};
+
+static void print_node(struct proxy_data *data)
+{
+ struct pw_node_info *info = data->info;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->first) {
+ printf("added:\n");
+ print_mark = false;
+ data->first = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+ if (print_all) {
+ print_params(data, MARK_CHANGE(PW_NODE_CHANGE_MASK_PARAMS));
+ with_prefix(MARK_CHANGE(PW_NODE_CHANGE_MASK_INPUT_PORTS)) {
+ printf("\tinput ports: %u/%u\n",
+ info->n_input_ports, info->max_input_ports);
+ }
+ with_prefix(MARK_CHANGE(PW_NODE_CHANGE_MASK_OUTPUT_PORTS)) {
+ printf("\toutput ports: %u/%u\n",
+ info->n_output_ports, info->max_output_ports);
+ }
+ with_prefix(MARK_CHANGE(PW_NODE_CHANGE_MASK_STATE)) {
+ printf("\tstate: \"%s\"",
+ pw_node_state_as_string(info->state));
+ }
+ if (info->state == PW_NODE_STATE_ERROR && info->error)
+ printf(" \"%s\"\n", info->error);
+ else
+ printf("\n");
+ print_properties(info->props, MARK_CHANGE(PW_NODE_CHANGE_MASK_PROPS));
+ }
+}
+
+static void node_event_info(void *_data, const struct pw_node_info *info)
+{
+ struct proxy_data *data = _data;
+ uint32_t i;
+
+ info = data->info = pw_node_info_update(data->info, info);
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if (info->params[i].user == 0)
+ continue;
+ remove_params(data, info->params[i].id, 0);
+ if (!SPA_FLAG_IS_SET(info->params[i].flags, SPA_PARAM_INFO_READ))
+ continue;
+ pw_node_enum_params((struct pw_node*)data->proxy,
+ 0, info->params[i].id, 0, 0, NULL);
+ info->params[i].user = 0;
+ }
+ add_pending(data);
+ }
+
+ if (data->pending_seq == 0)
+ data->print_func(data);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = event_param
+};
+
+static void print_port(struct proxy_data *data)
+{
+ struct pw_port_info *info = data->info;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->first) {
+ printf("added:\n");
+ print_mark = false;
+ data->first = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ printf("\tdirection: \"%s\"\n", pw_direction_as_string(info->direction));
+ if (print_all) {
+ print_params(data, MARK_CHANGE(PW_PORT_CHANGE_MASK_PARAMS));
+ print_properties(info->props, MARK_CHANGE(PW_PORT_CHANGE_MASK_PROPS));
+ }
+}
+
+static void port_event_info(void *_data, const struct pw_port_info *info)
+{
+ struct proxy_data *data = _data;
+ uint32_t i;
+
+ info = data->info = pw_port_info_update(data->info, info);
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if (info->params[i].user == 0)
+ continue;
+ remove_params(data, info->params[i].id, 0);
+ if (!SPA_FLAG_IS_SET(info->params[i].flags, SPA_PARAM_INFO_READ))
+ continue;
+ pw_port_enum_params((struct pw_port*)data->proxy,
+ 0, info->params[i].id, 0, 0, NULL);
+ info->params[i].user = 0;
+ }
+ add_pending(data);
+ }
+
+ if (data->pending_seq == 0)
+ data->print_func(data);
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+ .param = event_param
+};
+
+static void factory_event_info(void *_data, const struct pw_factory_info *info)
+{
+ struct proxy_data *data = _data;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->info == NULL) {
+ printf("added:\n");
+ print_mark = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ info = data->info = pw_factory_info_update(data->info, info);
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ printf("\tname: \"%s\"\n", info->name);
+ printf("\tobject-type: %s/%d\n", info->type, info->version);
+ if (print_all) {
+ print_properties(info->props, MARK_CHANGE(PW_FACTORY_CHANGE_MASK_PROPS));
+ }
+}
+
+static const struct pw_factory_events factory_events = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = factory_event_info
+};
+
+static void client_event_info(void *_data, const struct pw_client_info *info)
+{
+ struct proxy_data *data = _data;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->info == NULL) {
+ printf("added:\n");
+ print_mark = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ info = data->info = pw_client_info_update(data->info, info);
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ if (print_all) {
+ print_properties(info->props, MARK_CHANGE(PW_CLIENT_CHANGE_MASK_PROPS));
+ }
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info
+};
+
+static void link_event_info(void *_data, const struct pw_link_info *info)
+{
+ struct proxy_data *data = _data;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->info == NULL) {
+ printf("added:\n");
+ print_mark = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ info = data->info = pw_link_info_update(data->info, info);
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ printf("\toutput-node-id: %u\n", info->output_node_id);
+ printf("\toutput-port-id: %u\n", info->output_port_id);
+ printf("\tinput-node-id: %u\n", info->input_node_id);
+ printf("\tinput-port-id: %u\n", info->input_port_id);
+ if (print_all) {
+ with_prefix(MARK_CHANGE(PW_LINK_CHANGE_MASK_STATE)) {
+ printf("\tstate: \"%s\"",
+ pw_link_state_as_string(info->state));
+ }
+ if (info->state == PW_LINK_STATE_ERROR && info->error)
+ printf(" \"%s\"\n", info->error);
+ else
+ printf("\n");
+ with_prefix(MARK_CHANGE(PW_LINK_CHANGE_MASK_FORMAT)) {
+ printf("\tformat:\n");
+ if (info->format)
+ spa_debug_format(2, NULL, info->format);
+ else
+ printf("\t\tnone\n");
+ }
+ print_properties(info->props, MARK_CHANGE(PW_LINK_CHANGE_MASK_PROPS));
+ }
+}
+
+static const struct pw_link_events link_events = {
+ PW_VERSION_LINK_EVENTS,
+ .info = link_event_info
+};
+
+static void print_device(struct proxy_data *data)
+{
+ struct pw_device_info *info = data->info;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->first) {
+ printf("added:\n");
+ print_mark = false;
+ data->first = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ if (print_all) {
+ print_params(data, MARK_CHANGE(PW_DEVICE_CHANGE_MASK_PARAMS));
+ print_properties(info->props, MARK_CHANGE(PW_DEVICE_CHANGE_MASK_PROPS));
+ }
+}
+
+
+static void device_event_info(void *_data, const struct pw_device_info *info)
+{
+ struct proxy_data *data = _data;
+ uint32_t i;
+
+ info = data->info = pw_device_info_update(data->info, info);
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if (info->params[i].user == 0)
+ continue;
+ remove_params(data, info->params[i].id, 0);
+ if (!SPA_FLAG_IS_SET(info->params[i].flags, SPA_PARAM_INFO_READ))
+ continue;
+ pw_device_enum_params((struct pw_device*)data->proxy,
+ 0, info->params[i].id, 0, 0, NULL);
+ info->params[i].user = 0;
+ }
+ add_pending(data);
+ }
+ if (data->pending_seq == 0)
+ data->print_func(data);
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = event_param
+};
+
+static void
+removed_proxy (void *data)
+{
+ struct proxy_data *pd = data;
+ pw_proxy_destroy(pd->proxy);
+}
+
+static void
+destroy_proxy (void *data)
+{
+ struct proxy_data *pd = data;
+
+ spa_list_remove(&pd->global_link);
+
+ spa_hook_remove(&pd->object_listener);
+ spa_hook_remove(&pd->proxy_listener);
+
+ clear_params(pd);
+ remove_pending(pd);
+ free(pd->type);
+
+ if (pd->info == NULL)
+ return;
+ if (pd->destroy)
+ pd->destroy(pd->info);
+ pd->info = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = removed_proxy,
+ .destroy = destroy_proxy,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct pw_proxy *proxy;
+ uint32_t client_version;
+ const void *events;
+ struct proxy_data *pd;
+ pw_destroy_t destroy;
+ print_func_t print_func = NULL;
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ events = &node_events;
+ client_version = PW_VERSION_NODE;
+ destroy = (pw_destroy_t) pw_node_info_free;
+ print_func = print_node;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ events = &port_events;
+ client_version = PW_VERSION_PORT;
+ destroy = (pw_destroy_t) pw_port_info_free;
+ print_func = print_port;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Module)) {
+ events = &module_events;
+ client_version = PW_VERSION_MODULE;
+ destroy = (pw_destroy_t) pw_module_info_free;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Device)) {
+ events = &device_events;
+ client_version = PW_VERSION_DEVICE;
+ destroy = (pw_destroy_t) pw_device_info_free;
+ print_func = print_device;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Factory)) {
+ events = &factory_events;
+ client_version = PW_VERSION_FACTORY;
+ destroy = (pw_destroy_t) pw_factory_info_free;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Client)) {
+ events = &client_events;
+ client_version = PW_VERSION_CLIENT;
+ destroy = (pw_destroy_t) pw_client_info_free;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ events = &link_events;
+ client_version = PW_VERSION_LINK;
+ destroy = (pw_destroy_t) pw_link_info_free;
+ } else {
+ printf("added:\n");
+ printf("\tid: %u\n", id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(permissions));
+ printf("\ttype: %s (version %d)\n", type, version);
+ print_properties(props, false);
+ return;
+ }
+
+ proxy = pw_registry_bind(d->registry, id, type,
+ client_version,
+ sizeof(struct proxy_data));
+ if (proxy == NULL)
+ goto no_mem;
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->data = d;
+ pd->first = true;
+ pd->proxy = proxy;
+ pd->id = id;
+ pd->permissions = permissions;
+ pd->version = version;
+ pd->type = strdup(type);
+ pd->destroy = destroy;
+ pd->pending_seq = 0;
+ pd->print_func = print_func;
+ spa_list_init(&pd->param_list);
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+ spa_list_append(&d->global_list, &pd->global_link);
+
+ return;
+
+no_mem:
+ fprintf(stderr, "failed to create proxy");
+ return;
+}
+
+static struct proxy_data *find_proxy(struct data *d, uint32_t id)
+{
+ struct proxy_data *pd;
+ spa_list_for_each(pd, &d->global_list, global_link) {
+ if (pd->id == id)
+ return pd;
+ }
+ return NULL;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct data *d = data;
+ struct proxy_data *pd;
+
+ printf("removed:\n");
+ printf("\tid: %u\n", id);
+
+ pd = find_proxy(d, id);
+ if (pd == NULL)
+ return;
+ if (pd->proxy)
+ pw_proxy_destroy(pd->proxy);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *data = _data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(data->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .info = on_core_info,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -N, --no-colors disable color output\n"
+ " -C, --color[=WHEN] whether to enable color support. WHEN is `never`, `always`, or `auto`\n",
+ name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "no-colors", no_argument, NULL, 'N' },
+ { "color", optional_argument, NULL, 'C' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+ bool colors = false;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ setlinebuf(stdout);
+
+ if (isatty(STDERR_FILENO) && getenv("NO_COLOR") == NULL)
+ colors = true;
+
+ while ((c = getopt_long(argc, argv, "hVr:NC", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ case 'N' :
+ colors = false;
+ break;
+ case 'C' :
+ if (optarg == NULL || !strcmp(optarg, "auto"))
+ break; /* nothing to do, tty detection was done
+ before parsing options */
+ else if (!strcmp(optarg, "never"))
+ colors = false;
+ else if (!strcmp(optarg, "always"))
+ colors = true;
+ else {
+ fprintf(stderr, "Invalid color: %s\n", optarg);
+ show_help(argv[0], true);
+ return -1;
+ }
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ if (colors) {
+ pprefix[1].prefix = SPA_ANSI_RED "*";
+ pprefix[1].suffix = SPA_ANSI_RESET;
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create main loop: %m\n");
+ return -1;
+ }
+
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit, &data);
+
+ data.context = pw_context_new(l, NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ return -1;
+ }
+
+ spa_list_init(&data.pending_list);
+ spa_list_init(&data.global_list);
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ pw_main_loop_run(data.loop);
+
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-profiler.c b/src/tools/pw-profiler.c
new file mode 100644
index 0000000..7ee85bb
--- /dev/null
+++ b/src/tools/pw-profiler.c
@@ -0,0 +1,665 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+#define MAX_NAME 128
+#define MAX_FOLLOWERS 64
+#define DEFAULT_FILENAME "profiler.log"
+
+struct follower {
+ uint32_t id;
+ char name[MAX_NAME];
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ const char *filename;
+ FILE *output;
+
+ int64_t count;
+ int64_t start_status;
+ int64_t last_status;
+
+ struct pw_proxy *profiler;
+ struct spa_hook profiler_listener;
+ int check_profiler;
+
+ uint32_t driver_id;
+
+ int n_followers;
+ struct follower followers[MAX_FOLLOWERS];
+};
+
+struct measurement {
+ int64_t period;
+ int64_t prev_signal;
+ int64_t signal;
+ int64_t awake;
+ int64_t finish;
+ int32_t status;
+};
+
+struct point {
+ int64_t count;
+ float cpu_load[3];
+ struct spa_io_clock clock;
+ struct measurement driver;
+ struct measurement follower[MAX_FOLLOWERS];
+};
+
+static int process_info(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ return 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]));
+}
+
+static int process_clock(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ return 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)),
+ SPA_POD_Long(&point->clock.nsec),
+ SPA_POD_Fraction(&point->clock.rate),
+ SPA_POD_Long(&point->clock.position),
+ 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));
+}
+
+static int process_driver_block(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ char *name = NULL;
+ uint32_t driver_id = 0;
+ struct measurement driver;
+ int res;
+
+ spa_zero(driver);
+ if ((res = spa_pod_parse_struct(pod,
+ SPA_POD_Int(&driver_id),
+ SPA_POD_String(&name),
+ SPA_POD_Long(&driver.prev_signal),
+ SPA_POD_Long(&driver.signal),
+ SPA_POD_Long(&driver.awake),
+ SPA_POD_Long(&driver.finish),
+ SPA_POD_Int(&driver.status))) < 0)
+ return res;
+
+ if (d->driver_id == 0) {
+ d->driver_id = driver_id;
+ printf("logging driver %u\n", driver_id);
+ }
+ else if (d->driver_id != driver_id)
+ return -1;
+
+ point->driver = driver;
+ return 0;
+}
+
+static int find_follower(struct data *d, uint32_t id, const char *name)
+{
+ int i;
+ for (i = 0; i < d->n_followers; i++) {
+ if (d->followers[i].id == id && spa_streq(d->followers[i].name, name))
+ return i;
+ }
+ return -1;
+}
+
+static int add_follower(struct data *d, uint32_t id, const char *name)
+{
+ int idx = d->n_followers;
+
+ if (idx == MAX_FOLLOWERS)
+ return -1;
+
+ d->n_followers++;
+
+ 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);
+
+ return idx;
+}
+
+static int process_follower_block(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ uint32_t id = 0;
+ const char *name = NULL;
+ struct measurement m;
+ int res, idx;
+
+ spa_zero(m);
+ if ((res = spa_pod_parse_struct(pod,
+ SPA_POD_Int(&id),
+ SPA_POD_String(&name),
+ SPA_POD_Long(&m.prev_signal),
+ SPA_POD_Long(&m.signal),
+ SPA_POD_Long(&m.awake),
+ SPA_POD_Long(&m.finish),
+ SPA_POD_Int(&m.status))) < 0)
+ return res;
+
+ if ((idx = find_follower(d, id, name)) < 0) {
+ if ((idx = add_follower(d, id, name)) < 0) {
+ pw_log_warn("too many followers");
+ return -ENOSPC;
+ }
+ }
+ point->follower[idx] = m;
+ return 0;
+}
+
+static void dump_point(struct data *d, struct point *point)
+{
+ int i;
+ int64_t d1, d2;
+ int64_t delay, period_usecs;
+
+#define CLOCK_AS_USEC(cl,val) (val * (float)SPA_USEC_PER_SEC / (cl)->rate.denom)
+#define CLOCK_AS_SUSEC(cl,val) (val * (float)SPA_USEC_PER_SEC / ((cl)->rate.denom * (cl)->rate_diff))
+
+ delay = CLOCK_AS_USEC(&point->clock, point->clock.delay);
+ period_usecs = CLOCK_AS_SUSEC(&point->clock, point->clock.duration);
+
+ d1 = (point->driver.signal - point->driver.prev_signal) / 1000;
+ d2 = (point->driver.finish - point->driver.signal) / 1000;
+
+ if (d1 > period_usecs * 1.3 ||
+ d2 > period_usecs * 1.3)
+ d1 = d2 = period_usecs * 1.4;
+
+ /* 4 columns for the driver */
+ fprintf(d->output, "%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t",
+ d1 > 0 ? d1 : 0, d2 > 0 ? d2 : 0, delay, period_usecs);
+
+ for (i = 0; i < MAX_FOLLOWERS; i++) {
+ /* 8 columns for each follower */
+ if (point->follower[i].status == 0) {
+ fprintf(d->output, " \t \t \t \t \t \t \t \t");
+ } else {
+ int64_t d4 = (point->follower[i].signal - point->driver.signal) / 1000;
+ int64_t d5 = (point->follower[i].awake - point->driver.signal) / 1000;
+ int64_t d6 = (point->follower[i].finish - point->driver.signal) / 1000;
+
+ fprintf(d->output, "%u\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%d\t0\t",
+ d->followers[i].id,
+ d4 > 0 ? d4 : 0,
+ d5 > 0 ? d5 : 0,
+ d6 > 0 ? d6 : 0,
+ (d5 > 0 && d4 > 0 && d5 > d4) ? d5 - d4 : 0,
+ (d6 > 0 && d5 > 0 && d6 > d5) ? d6 - d5 : 0,
+ point->follower[i].status);
+ }
+ }
+ fprintf(d->output, "\n");
+ if (d->count == 0) {
+ d->start_status = point->clock.nsec;
+ 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",
+ 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;
+ }
+ d->count++;
+}
+
+static void dump_scripts(struct data *d)
+{
+ FILE *out;
+ int i;
+
+ if (d->driver_id == 0)
+ return;
+
+ printf("\ndumping scripts for %d followers\n", d->n_followers);
+
+ out = fopen("Timing1.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing1.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing1.svg\n"
+ "set terminal svg\n"
+ "set multiplot\n"
+ "set grid\n"
+ "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"
+ "unset multiplot\n"
+ "unset output\n", d->filename);
+ fclose(out);
+ }
+
+ out = fopen("Timing2.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing2.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing2.svg\n"
+ "set terminal svg\n"
+ "set grid\n"
+ "set title \"Driver end date\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot \"%s\" using 2 title \"Driver end date\" with lines \n"
+ "unset output\n", d->filename);
+ fclose(out);
+ }
+
+ out = fopen("Timing3.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing3.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing3.svg\n"
+ "set terminal svg\n"
+ "set multiplot\n"
+ "set grid\n"
+ "set title \"Clients end date\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot "
+ "\"%s\" using 1 title \"Audio period\" with lines%s",
+ d->filename,
+ d->n_followers > 0 ? ", " : "");
+
+ for (i = 0; i < d->n_followers; i++) {
+ fprintf(out,
+ "\"%s\" using %d title \"%s/%u\" with lines%s",
+ d->filename, 4 + (i * 8) + 4,
+ d->followers[i].name, d->followers[i].id,
+ i+1 < d->n_followers ? ", " : "");
+ }
+ fprintf(out,
+ "\nunset multiplot\n"
+ "unset output\n");
+ fclose(out);
+ }
+
+ out = fopen("Timing4.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing4.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing4.svg\n"
+ "set terminal svg\n"
+ "set multiplot\n"
+ "set grid\n"
+ "set title \"Clients scheduling latency\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot ");
+
+ for (i = 0; i < d->n_followers; i++) {
+ fprintf(out,
+ "\"%s\" using %d title \"%s/%u\" with lines%s",
+ d->filename, 4 + (i * 8) + 5,
+ d->followers[i].name, d->followers[i].id,
+ i+1 < d->n_followers ? ", " : "");
+ }
+ fprintf(out,
+ "\nunset multiplot\n"
+ "unset output\n");
+ fclose(out);
+ }
+
+ out = fopen("Timing5.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing5.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing5.svg\n"
+ "set terminal svg\n"
+ "set multiplot\n"
+ "set grid\n"
+ "set title \"Clients duration\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot ");
+
+ for (i = 0; i < d->n_followers; i++) {
+ fprintf(out,
+ "\"%s\" using %d title \"%s/%u\" with lines%s",
+ d->filename, 4 + (i * 8) + 6,
+ d->followers[i].name, d->followers[i].id,
+ i+1 < d->n_followers ? ", " : "");
+ }
+ fprintf(out,
+ "\nunset multiplot\n"
+ "unset output\n");
+ fclose(out);
+ }
+ out = fopen("Timings.html", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timings.html: %m");
+ } else {
+ fprintf(out,
+ "<?xml version='1.0' encoding='utf-8'?>\n"
+ "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n"
+ "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"
+ "<html xmlns='http://www.w3.org/1999/xhtml' lang='en'>\n"
+ " <head>\n"
+ " <title>PipeWire profiling</title>\n"
+ " <!-- assuming that images are 600px wide -->\n"
+ " <style media='all' type='text/css'>\n"
+ " .center { margin-left:auto ; margin-right: auto; width: 650px; height: 550px }\n"
+ " </style>\n"
+ " </head>\n"
+ " <body>\n"
+ " <h2 style='text-align:center'>PipeWire profiling</h2>\n"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing1.svg'>Timing1</object></div>"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing2.svg'>Timing2</object></div>"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing3.svg'>Timing3</object></div>"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing4.svg'>Timing4</object></div>"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing5.svg'>Timing5</object></div>"
+ " </body>\n"
+ "</html>\n");
+ fclose(out);
+ }
+
+ out = fopen("generate_timings.sh", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open generate_timings.sh: %m");
+ } else {
+ fprintf(out,
+ "gnuplot Timing1.plot\n"
+ "gnuplot Timing2.plot\n"
+ "gnuplot Timing3.plot\n"
+ "gnuplot Timing4.plot\n"
+ "gnuplot Timing5.plot\n");
+ fclose(out);
+ }
+ printf("run 'sh generate_timings.sh' and load Timings.html in a browser\n");
+}
+
+static void profiler_profile(void *data, const struct spa_pod *pod)
+{
+ struct data *d = data;
+ struct spa_pod *o;
+ struct spa_pod_prop *p;
+ struct point point;
+
+ SPA_POD_STRUCT_FOREACH(pod, o) {
+ int res = 0;
+ if (!spa_pod_is_object_type(o, SPA_TYPE_OBJECT_Profiler))
+ continue;
+
+ spa_zero(point);
+ SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)o, p) {
+ switch(p->key) {
+ case SPA_PROFILER_info:
+ res = process_info(d, &p->value, &point);
+ break;
+ case SPA_PROFILER_clock:
+ res = process_clock(d, &p->value, &point);
+ break;
+ case SPA_PROFILER_driverBlock:
+ res = process_driver_block(d, &p->value, &point);
+ break;
+ case SPA_PROFILER_followerBlock:
+ process_follower_block(d, &p->value, &point);
+ break;
+ default:
+ break;
+ }
+ if (res < 0)
+ break;
+ }
+ if (res < 0)
+ continue;
+
+ dump_point(d, &point);
+ }
+}
+
+static const struct pw_profiler_events profiler_events = {
+ PW_VERSION_PROFILER_EVENTS,
+ .profile = profiler_profile,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct pw_proxy *proxy;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Profiler))
+ return;
+
+ if (d->profiler != NULL) {
+ fprintf(stderr, "Ignoring profiler %d: already attached\n", id);
+ return;
+ }
+
+ proxy = pw_registry_bind(d->registry, id, type, PW_VERSION_PROFILER, 0);
+ if (proxy == NULL)
+ goto error_proxy;
+
+ printf("Attaching to Profiler id:%d\n", id);
+ d->profiler = proxy;
+ pw_proxy_add_object_listener(proxy, &d->profiler_listener, &profiler_events, d);
+
+ return;
+
+error_proxy:
+ pw_log_error("failed to create proxy: %m");
+ return;
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+};
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *data = _data;
+
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(data->loop);
+}
+
+static void on_core_done(void *_data, uint32_t id, int seq)
+{
+ struct data *d = _data;
+
+ if (seq == d->check_profiler) {
+ if (d->profiler == NULL) {
+ pw_log_error("no Profiler Interface found, please load one in the server");
+ pw_main_loop_quit(d->loop);
+ }
+ }
+}
+
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+ .done = on_core_done,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -o, --output Profiler output name (default \"%s\")\n",
+ name,
+ DEFAULT_FILENAME);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ const char *opt_output = DEFAULT_FILENAME;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "output", required_argument, NULL, 'o' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVr:o:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'o':
+ opt_output = optarg;
+ break;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "Can't create data loop: %m\n");
+ return -1;
+ }
+
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit, &data);
+
+ data.context = pw_context_new(l, NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "Can't create context: %m\n");
+ return -1;
+ }
+
+ pw_context_load_module(data.context, PW_EXTENSION_MODULE_PROFILER, NULL, NULL);
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "Can't connect: %m\n");
+ return -1;
+ }
+
+ 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;
+ }
+
+ printf("Logging to %s\n", data.filename);
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ data.check_profiler = pw_core_sync(data.core, 0, 0);
+
+ pw_main_loop_run(data.loop);
+
+ if (data.profiler) {
+ spa_hook_remove(&data.profiler_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.profiler);
+ }
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ fclose(data.output);
+
+ dump_scripts(&data);
+
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-reserve.c b/src/tools/pw-reserve.c
new file mode 100644
index 0000000..45a3c45
--- /dev/null
+++ b/src/tools/pw-reserve.c
@@ -0,0 +1,253 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <getopt.h>
+#include <signal.h>
+#include <locale.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/utils/result.h>
+#include <spa/support/dbus.h>
+
+#include "pipewire/pipewire.h"
+#include "pipewire/log.h"
+
+#include "reserve.h"
+
+struct impl {
+ struct pw_main_loop *mainloop;
+ struct pw_loop *loop;
+ struct pw_context *context;
+
+ struct spa_dbus *dbus;
+ struct spa_dbus_connection *dbus_connection;
+ DBusConnection *conn;
+
+ struct rd_device *device;
+};
+
+static void reserve_acquired(void *data, struct rd_device *d)
+{
+ printf("reserve acquired\n");
+}
+
+static void reserve_release(void *data, struct rd_device *d, int forced)
+{
+ struct impl *impl = data;
+ printf("reserve release\n");
+ rd_device_complete_release(impl->device, true);
+}
+
+static void reserve_busy(void *data, struct rd_device *d, const char *name, int32_t prio)
+{
+ printf("reserve busy %s, prio %d\n", name, prio);
+}
+
+static void reserve_available(void *data, struct rd_device *d, const char *name)
+{
+ printf("reserve available %s\n", name);
+}
+
+static const struct rd_device_callbacks reserve_callbacks = {
+ .acquired = reserve_acquired,
+ .release = reserve_release,
+ .busy = reserve_busy,
+ .available = reserve_available,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct impl *impl = data;
+ pw_main_loop_quit(impl->mainloop);
+}
+
+#define DEFAULT_APPNAME "pw-reserve"
+#define DEFAULT_PRIORITY 0
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -n, --name Name to reserve (Audio0, Midi0, Video0, ..)\n"
+ " -a, --appname Application Name (default %s)\n"
+ " -p, --priority Priority (default %d)\n"
+ " -m, --monitor Monitor only, don't try to acquire\n"
+ " -r, --release Request release when busy\n",
+ name, DEFAULT_APPNAME, DEFAULT_PRIORITY);
+}
+
+int main(int argc, char *argv[])
+{
+ struct impl impl = { 0, };
+ const struct spa_support *support;
+ uint32_t n_support;
+ const char *opt_name = NULL;
+ const char *opt_appname = DEFAULT_APPNAME;
+ bool opt_monitor = false;
+ bool opt_release = false;
+ int opt_priority= DEFAULT_PRIORITY;
+
+ int res = 0, c;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "name", required_argument, NULL, 'n' },
+ { "app", required_argument, NULL, 'a' },
+ { "priority", required_argument, NULL, 'p' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "release", no_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0}
+ };
+
+ setlinebuf(stdout);
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVn:a:p:mr", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'n':
+ opt_name = optarg;
+ break;
+ case 'a':
+ opt_appname = optarg;
+ break;
+ case 'p':
+ opt_priority = atoi(optarg);
+ break;
+ case 'm':
+ opt_monitor = true;
+ break;
+ case 'r':
+ opt_release = true;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+ if (opt_name == NULL) {
+ fprintf(stderr, "name must be given\n");
+ return -1;
+ }
+
+ impl.mainloop = pw_main_loop_new(NULL);
+ if (impl.mainloop == NULL) {
+ fprintf(stderr, "can't create mainloop: %m\n");
+ res = -errno;
+ goto exit;
+ }
+ impl.loop = pw_main_loop_get_loop(impl.mainloop);
+
+ pw_loop_add_signal(impl.loop, SIGINT, do_quit, &impl);
+ pw_loop_add_signal(impl.loop, SIGTERM, do_quit, &impl);
+
+ impl.context = pw_context_new(impl.loop, NULL, 0);
+ if (impl.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ res = -errno;
+ goto exit;
+ }
+
+ support = pw_context_get_support(impl.context, &n_support);
+
+ impl.dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ if (impl.dbus)
+ impl.dbus_connection = spa_dbus_get_connection(impl.dbus, SPA_DBUS_TYPE_SESSION);
+ if (impl.dbus_connection == NULL) {
+ fprintf(stderr, "no dbus connection: %m\n");
+ res = -errno;
+ goto exit;
+ }
+ impl.conn = spa_dbus_connection_get(impl.dbus_connection);
+ if (impl.conn == NULL) {
+ fprintf(stderr, "no dbus connection: %m\n");
+ res = -errno;
+ goto exit;
+ }
+
+ /* XXX: we don't handle dbus reconnection yet, so ref the handle instead */
+ dbus_connection_ref(impl.conn);
+
+ impl.device = rd_device_new(impl.conn,
+ opt_name,
+ opt_appname,
+ opt_priority,
+ &reserve_callbacks, &impl);
+
+ if (!opt_monitor) {
+ res = rd_device_acquire(impl.device);
+ if (res == -EBUSY) {
+ printf("device %s is busy\n", opt_name);
+ if (opt_release) {
+ printf("doing RequestRelease on %s\n", opt_name);
+ res = rd_device_request_release(impl.device);
+ } else {
+ printf("use -r to attempt to release\n");
+ }
+ } else if (res < 0) {
+ printf("Device %s can not be acquired: %s\n", opt_name,
+ spa_strerror(res));
+ }
+ }
+
+ if (res >= 0)
+ pw_main_loop_run(impl.mainloop);
+
+ if (!opt_monitor) {
+ if (opt_release) {
+ printf("doing Release on %s\n", opt_name);
+ rd_device_release(impl.device);
+ }
+ }
+
+exit:
+ if (impl.conn)
+ dbus_connection_unref(impl.conn);
+ if (impl.dbus)
+ spa_dbus_connection_destroy(impl.dbus_connection);
+ if (impl.context)
+ pw_context_destroy(impl.context);
+ if (impl.mainloop)
+ pw_main_loop_destroy(impl.mainloop);
+
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c
new file mode 100644
index 0000000..a20bdf9
--- /dev/null
+++ b/src/tools/pw-top.c
@@ -0,0 +1,862 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+#include <locale.h>
+#include <ncurses.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+#include <spa/param/format-utils.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/video/format-utils.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+#define MAX_FORMAT 16
+#define MAX_NAME 128
+
+struct driver {
+ int64_t count;
+ float cpu_load[3];
+ struct spa_io_clock clock;
+ uint32_t xrun_count;
+};
+
+struct measurement {
+ int32_t index;
+ int32_t status;
+ int64_t quantum;
+ int64_t prev_signal;
+ int64_t signal;
+ int64_t awake;
+ int64_t finish;
+ struct spa_fraction latency;
+};
+
+struct node {
+ struct spa_list link;
+ struct data *data;
+ uint32_t id;
+ char name[MAX_NAME+1];
+ enum pw_node_state state;
+ struct measurement measurement;
+ struct driver info;
+ struct node *driver;
+ uint32_t errors;
+ int32_t last_error_status;
+ uint32_t generation;
+ char format[MAX_FORMAT+1];
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+ unsigned int inactive:1;
+ struct spa_hook object_listener;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_proxy *profiler;
+ struct spa_hook profiler_listener;
+ int check_profiler;
+
+ struct spa_source *timer;
+
+ int n_nodes;
+ struct spa_list node_list;
+ uint32_t generation;
+ unsigned pending_refresh:1;
+
+ WINDOW *win;
+};
+
+struct point {
+ struct node *driver;
+ struct driver info;
+};
+
+static int process_info(struct data *d, const struct spa_pod *pod, struct driver *info)
+{
+ return spa_pod_parse_struct(pod,
+ SPA_POD_Long(&info->count),
+ SPA_POD_Float(&info->cpu_load[0]),
+ SPA_POD_Float(&info->cpu_load[1]),
+ SPA_POD_Float(&info->cpu_load[2]),
+ SPA_POD_Int(&info->xrun_count));
+}
+
+static int process_clock(struct data *d, const struct spa_pod *pod, struct driver *info)
+{
+ return spa_pod_parse_struct(pod,
+ SPA_POD_Int(&info->clock.flags),
+ SPA_POD_Int(&info->clock.id),
+ SPA_POD_Stringn(info->clock.name, sizeof(info->clock.name)),
+ SPA_POD_Long(&info->clock.nsec),
+ SPA_POD_Fraction(&info->clock.rate),
+ SPA_POD_Long(&info->clock.position),
+ SPA_POD_Long(&info->clock.duration),
+ SPA_POD_Long(&info->clock.delay),
+ SPA_POD_Double(&info->clock.rate_diff),
+ SPA_POD_Long(&info->clock.next_nsec));
+}
+
+static struct node *find_node(struct data *d, uint32_t id)
+{
+ struct node *n;
+ spa_list_for_each(n, &d->node_list, link) {
+ if (n->id == id)
+ return n;
+ }
+ return NULL;
+}
+
+static void on_node_removed(void *data)
+{
+ struct node *n = data;
+ pw_proxy_destroy(n->proxy);
+}
+
+static void on_node_destroy(void *data)
+{
+ struct node *n = data;
+ n->proxy = NULL;
+ spa_hook_remove(&n->proxy_listener);
+ spa_hook_remove(&n->object_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = on_node_removed,
+ .destroy = on_node_destroy,
+};
+
+static void do_refresh(struct data *d);
+
+static void node_info(void *data, const struct pw_node_info *info)
+{
+ struct node *n = data;
+
+ if (n->state != info->state) {
+ n->state = info->state;
+ do_refresh(n->data);
+ }
+}
+
+static void node_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct node *n = data;
+
+ if (param == NULL) {
+ spa_zero(n->format);
+ goto done;
+ }
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ {
+ uint32_t media_type, media_subtype;
+
+ spa_format_parse(param, &media_type, &media_subtype);
+
+ switch(media_type) {
+ case SPA_MEDIA_TYPE_audio:
+ switch(media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ {
+ 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),
+ info.channels, info.rate);
+ }
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_dsd:
+ {
+ struct spa_audio_info_dsd info = { 0 };
+ if (spa_format_audio_dsd_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "DSD%d %d ",
+ 8 * info.rate / 44100, info.channels);
+
+ }
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_iec958:
+ {
+ struct spa_audio_info_iec958 info = { 0 };
+ if (spa_format_audio_iec958_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "IEC958 %s %d",
+ spa_debug_type_find_short_name(
+ spa_type_audio_iec958_codec, info.codec),
+ info.rate);
+
+ }
+ break;
+ }
+ }
+ break;
+ case SPA_MEDIA_TYPE_video:
+ switch(media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ {
+ struct spa_video_info_raw info = { 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),
+ info.size.width, info.size.height);
+ }
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ {
+ struct spa_video_info_mjpg info = { 0 };
+ if (spa_format_video_mjpg_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "MJPG %dx%d",
+ info.size.width, info.size.height);
+ }
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_h264:
+ {
+ struct spa_video_info_h264 info = { 0 };
+ if (spa_format_video_h264_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "H264 %dx%d",
+ info.size.width, info.size.height);
+ }
+ break;
+ }
+ }
+ break;
+ case SPA_MEDIA_TYPE_application:
+ switch(media_subtype) {
+ case SPA_MEDIA_SUBTYPE_control:
+ snprintf(n->format, sizeof(n->format), "%s", "CONTROL");
+ break;
+ }
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+done:
+ do_refresh(n->data);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE,
+ .info = node_info,
+ .param = node_param,
+};
+
+static struct node *add_node(struct data *d, uint32_t id, const char *name)
+{
+ struct node *n;
+
+ if ((n = calloc(1, sizeof(*n))) == NULL)
+ return NULL;
+
+ if (name)
+ strncpy(n->name, name, MAX_NAME);
+ else
+ snprintf(n->name, sizeof(n->name), "%u", id);
+ n->data = d;
+ n->id = id;
+ n->driver = n;
+ n->proxy = pw_registry_bind(d->registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
+ if (n->proxy) {
+ uint32_t ids[1] = { SPA_PARAM_Format };
+
+ pw_proxy_add_listener(n->proxy,
+ &n->proxy_listener, &proxy_events, n);
+ pw_proxy_add_object_listener(n->proxy,
+ &n->object_listener, &node_events, n);
+
+ pw_node_subscribe_params((struct pw_node*)n->proxy,
+ ids, 1);
+ }
+ spa_list_append(&d->node_list, &n->link);
+ d->n_nodes++;
+ d->pending_refresh = true;
+
+ return n;
+}
+
+static void remove_node(struct data *d, struct node *n)
+{
+ if (n->proxy)
+ pw_proxy_destroy(n->proxy);
+ spa_list_remove(&n->link);
+ d->n_nodes--;
+ d->pending_refresh = true;
+ free(n);
+}
+
+static int process_driver_block(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ char *name = NULL;
+ uint32_t id = 0;
+ struct measurement m;
+ struct node *n;
+ int res;
+
+ spa_zero(m);
+ if ((res = spa_pod_parse_struct(pod,
+ SPA_POD_Int(&id),
+ SPA_POD_String(&name),
+ SPA_POD_Long(&m.prev_signal),
+ SPA_POD_Long(&m.signal),
+ SPA_POD_Long(&m.awake),
+ SPA_POD_Long(&m.finish),
+ SPA_POD_Int(&m.status),
+ SPA_POD_Fraction(&m.latency))) < 0)
+ return res;
+
+ if ((n = find_node(d, id)) == NULL)
+ return -ENOENT;
+
+ n->driver = n;
+ n->measurement = m;
+ n->info = point->info;
+ point->driver = n;
+ n->generation = d->generation;
+
+ if (m.status != 3) {
+ n->errors++;
+ if (n->last_error_status == -1)
+ n->last_error_status = m.status;
+ }
+ return 0;
+}
+
+static int process_follower_block(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ uint32_t id = 0;
+ const char *name = NULL;
+ struct measurement m;
+ struct node *n;
+ int res;
+
+ spa_zero(m);
+ if ((res = spa_pod_parse_struct(pod,
+ SPA_POD_Int(&id),
+ SPA_POD_String(&name),
+ SPA_POD_Long(&m.prev_signal),
+ SPA_POD_Long(&m.signal),
+ SPA_POD_Long(&m.awake),
+ SPA_POD_Long(&m.finish),
+ SPA_POD_Int(&m.status),
+ SPA_POD_Fraction(&m.latency))) < 0)
+ return res;
+
+ if ((n = find_node(d, id)) == NULL)
+ return -ENOENT;
+
+ n->measurement = m;
+ if (n->driver != point->driver) {
+ n->driver = point->driver;
+ d->pending_refresh = true;
+ }
+ n->generation = d->generation;
+ if (m.status != 3) {
+ n->errors++;
+ if (n->last_error_status == -1)
+ n->last_error_status = m.status;
+ }
+ return 0;
+}
+
+static const char *print_time(char *buf, bool active, size_t len, uint64_t val)
+{
+ if (val == (uint64_t)-1 || !active)
+ snprintf(buf, len, " --- ");
+ else if (val == (uint64_t)-2)
+ snprintf(buf, len, " +++ ");
+ else if (val < 1000000llu)
+ snprintf(buf, len, "%5.1fus", val/1000.f);
+ else if (val < 1000000000llu)
+ snprintf(buf, len, "%5.1fms", val/1000000.f);
+ else
+ snprintf(buf, len, "%5.1fs", val/1000000000.f);
+ return buf;
+}
+
+static const char *print_perc(char *buf, bool active, size_t len, uint64_t val, float quantum)
+{
+ if (val == (uint64_t)-1 || !active) {
+ snprintf(buf, len, " --- ");
+ } else if (val == (uint64_t)-2) {
+ snprintf(buf, len, " +++ ");
+ } else {
+ float frac = val / 1000000000.f;
+ snprintf(buf, len, "%5.2f", quantum == 0.0f ? 0.0f : frac/quantum);
+ }
+ return buf;
+}
+
+static const char *state_as_string(enum pw_node_state state)
+{
+ switch (state) {
+ case PW_NODE_STATE_ERROR:
+ return "E";
+ case PW_NODE_STATE_CREATING:
+ return "C";
+ case PW_NODE_STATE_SUSPENDED:
+ return "S";
+ case PW_NODE_STATE_IDLE:
+ return "I";
+ case PW_NODE_STATE_RUNNING:
+ return "R";
+ }
+ return "!";
+}
+
+static void print_node(struct data *d, struct driver *i, struct node *n, int y)
+{
+ char buf1[64];
+ char buf2[64];
+ char buf3[64];
+ char buf4[64];
+ uint64_t waiting, busy;
+ float quantum;
+ struct spa_fraction frac;
+ bool active;
+
+ active = n->state == PW_NODE_STATE_RUNNING || n->state == PW_NODE_STATE_IDLE;
+
+ if (!active)
+ frac = SPA_FRACTION(0, 0);
+ else if (n->driver == n)
+ frac = SPA_FRACTION((uint32_t)(i->clock.duration * i->clock.rate.num), i->clock.rate.denom);
+ else
+ frac = SPA_FRACTION(n->measurement.latency.num, n->measurement.latency.denom);
+
+ if (i->clock.rate.denom)
+ quantum = (float)i->clock.duration * i->clock.rate.num / (float)i->clock.rate.denom;
+ else
+ quantum = 0.0;
+
+ if (n->measurement.awake >= n->measurement.signal)
+ waiting = n->measurement.awake - n->measurement.signal;
+ else if (n->measurement.signal > n->measurement.prev_signal)
+ waiting = -2;
+ else
+ waiting = -1;
+
+ if (n->measurement.finish >= n->measurement.awake)
+ busy = n->measurement.finish - n->measurement.awake;
+ else if (n->measurement.awake > n->measurement.prev_signal)
+ busy = -2;
+ else
+ busy = -1;
+
+ mvwprintw(d->win, y, 0, "%s %4.1u %6.1u %6.1u %s %s %s %s %3.1u %16.16s %s%s",
+ state_as_string(n->state),
+ n->id,
+ frac.num, frac.denom,
+ print_time(buf1, active, 64, waiting),
+ print_time(buf2, active, 64, busy),
+ print_perc(buf3, active, 64, waiting, quantum),
+ print_perc(buf4, active, 64, busy, quantum),
+ i->xrun_count + n->errors,
+ active ? n->format : "",
+ n->driver == n ? "" : " + ",
+ n->name);
+}
+
+static void clear_node(struct node *n)
+{
+ n->driver = n;
+ spa_zero(n->measurement);
+ spa_zero(n->info);
+ n->errors = 0;
+ n->last_error_status = 0;
+}
+
+static void do_refresh(struct data *d)
+{
+ struct node *n, *t, *f;
+ int y = 1;
+
+ wclear(d->win);
+ wattron(d->win, A_REVERSE);
+ wprintw(d->win, "%-*.*s", COLS, COLS, "S ID QUANT RATE WAIT BUSY W/Q B/Q ERR FORMAT NAME ");
+ wattroff(d->win, A_REVERSE);
+ wprintw(d->win, "\n");
+
+ spa_list_for_each_safe(n, t, &d->node_list, link) {
+ if (n->driver != n)
+ continue;
+
+ print_node(d, &n->info, n, y++);
+ if(y > LINES)
+ break;
+
+ spa_list_for_each(f, &d->node_list, link) {
+ if (d->generation > f->generation + 22)
+ clear_node(f);
+
+ if (f->driver != n || f == n)
+ continue;
+
+ print_node(d, &n->info, f, y++);
+ if(y > LINES)
+ break;
+
+ }
+ }
+
+ // Clear from last line to the end of the window to hide text wrapping from the last node
+ wmove(d->win, y, 0);
+ wclrtobot(d->win);
+
+ wrefresh(d->win);
+ d->pending_refresh = false;
+}
+
+static void do_timeout(void *data, uint64_t expirations)
+{
+ struct data *d = data;
+ d->generation++;
+ do_refresh(d);
+}
+
+static void profiler_profile(void *data, const struct spa_pod *pod)
+{
+ struct data *d = data;
+ struct spa_pod *o;
+ struct spa_pod_prop *p;
+ struct point point;
+
+ SPA_POD_STRUCT_FOREACH(pod, o) {
+ int res = 0;
+ if (!spa_pod_is_object_type(o, SPA_TYPE_OBJECT_Profiler))
+ continue;
+
+ spa_zero(point);
+ SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)o, p) {
+ switch(p->key) {
+ case SPA_PROFILER_info:
+ res = process_info(d, &p->value, &point.info);
+ break;
+ case SPA_PROFILER_clock:
+ res = process_clock(d, &p->value, &point.info);
+ break;
+ case SPA_PROFILER_driverBlock:
+ res = process_driver_block(d, &p->value, &point);
+ break;
+ case SPA_PROFILER_followerBlock:
+ process_follower_block(d, &p->value, &point);
+ break;
+ default:
+ break;
+ }
+ if (res < 0)
+ break;
+ }
+ if (res < 0)
+ continue;
+ }
+ if (d->pending_refresh)
+ do_refresh(d);
+}
+
+static const struct pw_profiler_events profiler_events = {
+ PW_VERSION_PROFILER_EVENTS,
+ .profile = profiler_profile,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct pw_proxy *proxy;
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ const char *str;
+
+ if ((str = spa_dict_lookup(props, PW_KEY_NODE_NAME)) == NULL &&
+ (str = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION)) == NULL) {
+ str = spa_dict_lookup(props, PW_KEY_APP_NAME);
+ }
+
+ if (add_node(d, id, str) == NULL) {
+ pw_log_warn("can add node %u: %m", id);
+ }
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Profiler)) {
+ if (d->profiler != NULL) {
+ printf("Ignoring profiler %d: already attached\n", id);
+ return;
+ }
+
+ proxy = pw_registry_bind(d->registry, id, type, PW_VERSION_PROFILER, 0);
+ if (proxy == NULL)
+ goto error_proxy;
+
+ d->profiler = proxy;
+ pw_proxy_add_object_listener(proxy, &d->profiler_listener, &profiler_events, d);
+ }
+ if (d->pending_refresh)
+ do_refresh(d);
+ return;
+
+error_proxy:
+ pw_log_error("failed to create proxy: %m");
+ return;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct data *d = data;
+ struct node *n;
+ if ((n = find_node(d, id)) != NULL)
+ remove_node(d, n);
+ if (d->pending_refresh)
+ do_refresh(d);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *data = _data;
+
+ if (id == PW_ID_CORE) {
+ switch (res) {
+ case -EPIPE:
+ pw_main_loop_quit(data->loop);
+ break;
+ default:
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ break;
+ }
+ } else {
+ pw_log_info("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ }
+}
+
+static void on_core_done(void *_data, uint32_t id, int seq)
+{
+ struct data *d = _data;
+
+ if (seq == d->check_profiler) {
+ if (d->profiler == NULL) {
+ pw_log_error("no Profiler Interface found, please load one in the server");
+ pw_main_loop_quit(d->loop);
+ } else {
+ do_refresh(d);
+ }
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+ .done = on_core_done,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n",
+ name);
+}
+
+static void terminal_start(void)
+{
+ initscr();
+ cbreak();
+ noecho();
+ refresh();
+}
+
+static void terminal_stop(void)
+{
+ endwin();
+}
+
+static void do_handle_io(void *data, int fd, uint32_t mask)
+{
+ struct data *d = data;
+
+ if (mask & SPA_IO_IN) {
+ int ch = getch();
+
+ switch(ch) {
+ case 'q':
+ pw_main_loop_quit(d->loop);
+ break;
+ default:
+ do_refresh(d);
+ break;
+ }
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+ struct timespec value, interval;
+ struct node *n;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ spa_list_init(&data.node_list);
+
+ while ((c = getopt_long(argc, argv, "hVr:o:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "Can't create data loop: %m\n");
+ return -1;
+ }
+
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit, &data);
+
+ data.context = pw_context_new(l, NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "Can't create context: %m\n");
+ return -1;
+ }
+
+ pw_context_load_module(data.context, PW_EXTENSION_MODULE_PROFILER, NULL, NULL);
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "Can't connect: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ data.check_profiler = pw_core_sync(data.core, 0, 0);
+
+ terminal_start();
+
+ data.win = newwin(LINES, COLS, 0, 0);
+
+ data.timer = pw_loop_add_timer(l, do_timeout, &data);
+ value.tv_sec = 1;
+ value.tv_nsec = 0;
+ interval.tv_sec = 1;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(l, data.timer, &value, &interval, false);
+
+ pw_loop_add_io(l, fileno(stdin), SPA_IO_IN, false, do_handle_io, &data);
+
+ pw_main_loop_run(data.loop);
+
+ terminal_stop();
+
+ spa_list_consume(n, &data.node_list, link)
+ remove_node(&data, n);
+
+ if (data.profiler) {
+ spa_hook_remove(&data.profiler_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.profiler);
+ }
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/reserve.c b/src/tools/reserve.c
new file mode 100644
index 0000000..2329a14
--- /dev/null
+++ b/src/tools/reserve.c
@@ -0,0 +1,527 @@
+/* DBus device reservation API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef NAME
+#define NAME "reserve"
+#endif
+
+#include "reserve.h"
+
+#include <spa/utils/string.h>
+#include <pipewire/log.h>
+
+#define SERVICE_PREFIX "org.freedesktop.ReserveDevice1."
+#define OBJECT_PREFIX "/org/freedesktop/ReserveDevice1/"
+
+static const char introspection[] =
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE
+ "<node>"
+ " <!-- If you are looking for documentation make sure to check out\n"
+ " http://git.0pointer.de/?p=reserve.git;a=blob;f=reserve.txt -->\n"
+ " <interface name=\"org.freedesktop.ReserveDevice1\">"
+ " <method name=\"RequestRelease\">"
+ " <arg name=\"priority\" type=\"i\" direction=\"in\"/>"
+ " <arg name=\"result\" type=\"b\" direction=\"out\"/>"
+ " </method>"
+ " <property name=\"Priority\" type=\"i\" access=\"read\"/>"
+ " <property name=\"ApplicationName\" type=\"s\" access=\"read\"/>"
+ " <property name=\"ApplicationDeviceName\" type=\"s\" access=\"read\"/>"
+ " </interface>"
+ " <interface name=\"org.freedesktop.DBus.Properties\">"
+ " <method name=\"Get\">"
+ " <arg name=\"interface\" direction=\"in\" type=\"s\"/>"
+ " <arg name=\"property\" direction=\"in\" type=\"s\"/>"
+ " <arg name=\"value\" direction=\"out\" type=\"v\"/>"
+ " </method>"
+ " </interface>"
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">"
+ " <method name=\"Introspect\">"
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>"
+ " </method>"
+ " </interface>"
+ "</node>";
+
+struct rd_device {
+ DBusConnection *connection;
+
+ int32_t priority;
+ char *service_name;
+ char *object_path;
+ char *application_name;
+ char *application_device_name;
+
+ const struct rd_device_callbacks *callbacks;
+ void *data;
+
+ DBusMessage *reply;
+
+ unsigned int filtering:1;
+ unsigned int registered:1;
+ unsigned int acquiring:1;
+ unsigned int owning:1;
+};
+
+static dbus_bool_t add_variant(DBusMessage *m, int type, const void *data)
+{
+ DBusMessageIter iter, sub;
+ char t[2];
+
+ t[0] = (char) type;
+ t[1] = 0;
+
+ dbus_message_iter_init_append(m, &iter);
+
+ if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, t, &sub))
+ return false;
+
+ if (!dbus_message_iter_append_basic(&sub, type, data))
+ return false;
+
+ if (!dbus_message_iter_close_container(&iter, &sub))
+ return false;
+
+ return true;
+}
+
+static DBusHandlerResult object_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct rd_device *d = userdata;
+ DBusError error;
+ DBusMessage *reply = NULL;
+
+ dbus_error_init(&error);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.ReserveDevice1",
+ "RequestRelease")) {
+ int32_t priority;
+
+ if (!dbus_message_get_args(m, &error,
+ DBUS_TYPE_INT32, &priority,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ pw_log_debug("%p: request release priority:%d", d, priority);
+
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (d->reply)
+ rd_device_complete_release(d, false);
+ d->reply = reply;
+
+ if (priority > d->priority && d->callbacks->release)
+ d->callbacks->release(d->data, d, 0);
+ else
+ rd_device_complete_release(d, false);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+ } else if (dbus_message_is_method_call(
+ m,
+ "org.freedesktop.DBus.Properties",
+ "Get")) {
+
+ const char *interface, *property;
+
+ if (!dbus_message_get_args( m, &error,
+ DBUS_TYPE_STRING, &interface,
+ DBUS_TYPE_STRING, &property,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ if (spa_streq(interface, "org.freedesktop.ReserveDevice1")) {
+ const char *empty = "";
+
+ if (spa_streq(property, "ApplicationName") && d->application_name) {
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (!add_variant(reply,
+ DBUS_TYPE_STRING,
+ d->application_name ? (const char**) &d->application_name : &empty))
+ goto oom;
+
+ } else if (spa_streq(property, "ApplicationDeviceName")) {
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (!add_variant(reply,
+ DBUS_TYPE_STRING,
+ d->application_device_name ? (const char**) &d->application_device_name : &empty))
+ goto oom;
+
+ } else if (spa_streq(property, "Priority")) {
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (!add_variant(reply,
+ DBUS_TYPE_INT32, &d->priority))
+ goto oom;
+ } else {
+ if (!(reply = dbus_message_new_error_printf(m,
+ DBUS_ERROR_UNKNOWN_METHOD,
+ "Unknown property %s", property)))
+ goto oom;
+ }
+
+ if (!dbus_connection_send(c, reply, NULL))
+ goto oom;
+
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+ } else if (dbus_message_is_method_call(
+ m,
+ "org.freedesktop.DBus.Introspectable",
+ "Introspect")) {
+ const char *i = introspection;
+
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (!dbus_message_append_args(reply,
+ DBUS_TYPE_STRING, &i,
+ DBUS_TYPE_INVALID))
+ goto oom;
+
+ if (!dbus_connection_send(c, reply, NULL))
+ goto oom;
+
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+invalid:
+ if (!(reply = dbus_message_new_error(m,
+ DBUS_ERROR_INVALID_ARGS,
+ "Invalid arguments")))
+ goto oom;
+
+ if (!dbus_connection_send(c, reply, NULL))
+ goto oom;
+
+ dbus_message_unref(reply);
+
+ dbus_error_free(&error);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+oom:
+ if (reply)
+ dbus_message_unref(reply);
+
+ dbus_error_free(&error);
+
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+}
+
+static const struct DBusObjectPathVTable vtable ={
+ .message_function = object_handler
+};
+
+static DBusHandlerResult filter_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct rd_device *d = userdata;
+ DBusError error;
+ const char *name;
+
+ dbus_error_init(&error);
+
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameAcquired")) {
+ if (!dbus_message_get_args( m, &error,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ if (!spa_streq(name, d->service_name))
+ goto invalid;
+
+ pw_log_debug("%p: acquired %s, %s", d, name, d->service_name);
+
+ d->owning = true;
+
+ if (!d->registered) {
+ if (!(dbus_connection_register_object_path(d->connection,
+ d->object_path,
+ &vtable,
+ d)))
+ goto invalid;
+
+ if (!spa_streq(name, d->service_name))
+ goto invalid;
+
+ d->registered = true;
+
+ if (d->callbacks->acquired)
+ d->callbacks->acquired(d->data, d);
+ }
+ } else if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameLost")) {
+ if (!dbus_message_get_args( m, &error,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ if (!spa_streq(name, d->service_name))
+ goto invalid;
+
+ pw_log_debug("%p: lost %s", d, name);
+
+ d->owning = false;
+
+ if (d->registered) {
+ dbus_connection_unregister_object_path(d->connection,
+ d->object_path);
+ d->registered = false;
+ }
+ }
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+ const char *old, *new;
+ if (!dbus_message_get_args( m, &error,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old,
+ DBUS_TYPE_STRING, &new,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ if (!spa_streq(name, d->service_name) || d->owning)
+ goto invalid;
+
+ pw_log_debug("%p: changed %s: %s -> %s", d, name, old, new);
+
+ if (old == NULL || *old == 0) {
+ if (d->callbacks->busy && !d->acquiring)
+ d->callbacks->busy(d->data, d, name, 0);
+ } else {
+ if (d->callbacks->available)
+ d->callbacks->available(d->data, d, name);
+ }
+ }
+
+invalid:
+ dbus_error_free(&error);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+struct rd_device *
+rd_device_new(DBusConnection *connection, const char *device_name, const char *application_name,
+ int32_t priority, const struct rd_device_callbacks *callbacks, void *data)
+{
+ struct rd_device *d;
+ int res;
+
+ d = calloc(1, sizeof(struct rd_device));
+ if (d == NULL)
+ return NULL;
+
+ d->connection = connection;
+ d->priority = priority;
+ d->callbacks = callbacks;
+ d->data = data;
+
+ d->application_name = strdup(application_name);
+
+ d->object_path = spa_aprintf(OBJECT_PREFIX "%s", device_name);
+ if (d->object_path == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+ d->service_name = spa_aprintf(SERVICE_PREFIX "%s", device_name);
+ if (d->service_name == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ if (!dbus_connection_add_filter(d->connection,
+ filter_handler,
+ d,
+ NULL)) {
+ res = -ENOMEM;
+ goto error_free;
+ }
+ dbus_bus_add_match(d->connection,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameLost'", NULL);
+ dbus_bus_add_match(d->connection,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameAcquired'", NULL);
+ dbus_bus_add_match(d->connection,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged'", NULL);
+
+ dbus_connection_ref(d->connection);
+
+ pw_log_debug("%p: new device %s", d, device_name);
+
+ return d;
+
+error_free:
+ free(d->service_name);
+ free(d->object_path);
+ free(d);
+ errno = -res;
+ return NULL;
+}
+
+int rd_device_acquire(struct rd_device *d)
+{
+ int res;
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ pw_log_debug("%p: reserve %s", d, d->service_name);
+
+ d->acquiring = true;
+
+ if ((res = dbus_bus_request_name(d->connection,
+ d->service_name,
+ (d->priority < INT32_MAX ? DBUS_NAME_FLAG_ALLOW_REPLACEMENT : 0),
+ &error)) < 0) {
+ pw_log_warn("%p: reserve failed: %s", d, error.message);
+ dbus_error_free(&error);
+ return -EIO;
+ }
+
+ pw_log_debug("%p: reserve result: %d", d, res);
+
+ if (res == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER ||
+ res == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER)
+ return 0;
+
+ if (res == DBUS_REQUEST_NAME_REPLY_EXISTS ||
+ res == DBUS_REQUEST_NAME_REPLY_IN_QUEUE)
+ return -EBUSY;
+
+ return -EIO;
+}
+
+int rd_device_request_release(struct rd_device *d)
+{
+ DBusMessage *m = NULL;
+
+ if (d->priority <= INT32_MIN)
+ return -EBUSY;
+
+ if ((m = dbus_message_new_method_call(d->service_name,
+ d->object_path,
+ "org.freedesktop.ReserveDevice1",
+ "RequestRelease")) == NULL) {
+ return -ENOMEM;
+ }
+ if (!dbus_message_append_args(m,
+ DBUS_TYPE_INT32, &d->priority,
+ DBUS_TYPE_INVALID)) {
+ dbus_message_unref(m);
+ return -ENOMEM;
+ }
+ if (!dbus_connection_send(d->connection, m, NULL)) {
+ return -EIO;
+ }
+ return 0;
+}
+
+int rd_device_complete_release(struct rd_device *d, int res)
+{
+ dbus_bool_t ret = res != 0;
+
+ if (d->reply == NULL)
+ return -EINVAL;
+
+ pw_log_debug("%p: complete release %d", d, res);
+
+ if (!dbus_message_append_args(d->reply,
+ DBUS_TYPE_BOOLEAN, &ret,
+ DBUS_TYPE_INVALID)) {
+ res = -ENOMEM;
+ goto exit;
+ }
+
+ if (!dbus_connection_send(d->connection, d->reply, NULL)) {
+ res = -EIO;
+ goto exit;
+ }
+ res = 0;
+exit:
+ dbus_message_unref(d->reply);
+ d->reply = NULL;
+ return res;
+}
+
+void rd_device_release(struct rd_device *d)
+{
+ pw_log_debug("%p: release %d", d, d->owning);
+
+ if (d->owning) {
+ DBusError error;
+ dbus_error_init(&error);
+
+ dbus_bus_release_name(d->connection,
+ d->service_name, &error);
+ dbus_error_free(&error);
+ }
+ d->acquiring = false;
+}
+
+void rd_device_destroy(struct rd_device *d)
+{
+ dbus_connection_remove_filter(d->connection,
+ filter_handler, d);
+
+ if (d->registered)
+ dbus_connection_unregister_object_path(d->connection,
+ d->object_path);
+
+ rd_device_release(d);
+
+ free(d->service_name);
+ free(d->object_path);
+ free(d->application_name);
+ free(d->application_device_name);
+ if (d->reply)
+ dbus_message_unref(d->reply);
+
+ dbus_connection_unref(d->connection);
+
+ free(d);
+}
+
+int rd_device_set_application_device_name(struct rd_device *d, const char *name)
+{
+ char *t;
+
+ if (!d)
+ return -EINVAL;
+
+ if (!(t = strdup(name)))
+ return -ENOMEM;
+
+ free(d->application_device_name);
+ d->application_device_name = t;
+
+ return 0;
+}
diff --git a/src/tools/reserve.h b/src/tools/reserve.h
new file mode 100644
index 0000000..c31e2d0
--- /dev/null
+++ b/src/tools/reserve.h
@@ -0,0 +1,82 @@
+/* DBus device reservation API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef DEVICE_RESERVE_H
+#define DEVICE_RESERVE_H
+
+#include <dbus/dbus.h>
+#include <inttypes.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rd_device;
+
+struct rd_device_callbacks {
+ /** the device is acquired by us */
+ void (*acquired) (void *data, struct rd_device *d);
+ /** request a release of the device */
+ void (*release) (void *data, struct rd_device *d, int forced);
+ /** the device is busy by someone else */
+ void (*busy) (void *data, struct rd_device *d, const char *name, int32_t priority);
+ /** the device is made available by someone else */
+ void (*available) (void *data, struct rd_device *d, const char *name);
+};
+
+/* create a new device and start watching */
+struct rd_device *
+rd_device_new(DBusConnection *connection, /**< Bus to watch */
+ const char *device_name, /**< The device to lock, e.g. "Audio0" */
+ const char *application_name, /**< A human readable name of the application,
+ * e.g. "PipeWire Server" */
+ int32_t priority, /**< The priority for this application.
+ * If unsure use 0 */
+ const struct rd_device_callbacks *callbacks, /**< Called when device name is acquired/released */
+ void *data);
+
+/** try to acquire the device */
+int rd_device_acquire(struct rd_device *d);
+
+/** request the owner to release the device */
+int rd_device_request_release(struct rd_device *d);
+
+/** complete the release of the device */
+int rd_device_complete_release(struct rd_device *d, int res);
+
+/** release a device */
+void rd_device_release(struct rd_device *d);
+
+/** destroy a device */
+void rd_device_destroy(struct rd_device *d);
+
+/* Set the application device name for an rd_device object. Returns 0
+ * on success, a negative errno style return value on error. */
+int rd_device_set_application_device_name(struct rd_device *d, const char *name);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/subprojects/media-session.wrap b/subprojects/media-session.wrap
new file mode 100644
index 0000000..f6b4e62
--- /dev/null
+++ b/subprojects/media-session.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://gitlab.freedesktop.org/pipewire/media-session.git
+revision = head
+
diff --git a/subprojects/wireplumber.wrap b/subprojects/wireplumber.wrap
new file mode 100644
index 0000000..0153b2a
--- /dev/null
+++ b/subprojects/wireplumber.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://gitlab.freedesktop.org/pipewire/wireplumber.git
+revision = head
diff --git a/template.test.in b/template.test.in
new file mode 100644
index 0000000..f90c96e
--- /dev/null
+++ b/template.test.in
@@ -0,0 +1,3 @@
+[Test]
+Type=session
+Exec=@exec@
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..1ce7d00
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,156 @@
+pwtest_sources = [
+ 'pwtest.h',
+ 'pwtest-implementation.h',
+ 'pwtest.c',
+ 'pwtest-compat.c',
+]
+
+pwtest_deps = [
+ pipewire_dep,
+ mathlib,
+ dl_lib,
+ cap_lib,
+ epoll_shim_dep
+]
+
+pwtest_c_args = [
+ '-DBUILD_ROOT="@0@"'.format(meson.project_build_root()),
+ '-DSOURCE_ROOT="@0@"'.format(meson.project_source_root()),
+]
+
+pwtest_inc = [
+ pipewire_inc,
+ configinc,
+ includes_inc,
+]
+
+pwtest_lib = static_library(
+ 'pwtest',
+ pwtest_sources,
+ c_args: pwtest_c_args,
+ dependencies: pwtest_deps,
+ include_directories: pwtest_inc,
+)
+
+test('test-pwtest',
+ executable('test-pwtest',
+ 'test-pwtest.c',
+ include_directories: pwtest_inc,
+ dependencies: [ spa_dep ],
+ link_with: pwtest_lib)
+)
+
+# Compilation only, this is the example file for how pwtest works and most
+# of its tests will fail.
+executable('test-example',
+ 'test-example.c',
+ include_directories: pwtest_inc,
+ dependencies: [ spa_dep ],
+ link_with: pwtest_lib)
+
+test('test-pw-utils',
+ executable('test-pw-utils',
+ 'test-properties.c',
+ 'test-array.c',
+ 'test-map.c',
+ 'test-utils.c',
+ include_directories: pwtest_inc,
+ dependencies: [ spa_dep ],
+ link_with: pwtest_lib)
+)
+
+test('test-lib',
+ executable('test-lib',
+ 'test-lib.c',
+ include_directories: pwtest_inc,
+ dependencies: [ spa_dep ],
+ link_with: pwtest_lib)
+)
+
+test('test-client',
+ executable('test-client',
+ 'test-client.c',
+ include_directories: pwtest_inc,
+ dependencies: [ spa_dep ],
+ link_with: pwtest_lib)
+)
+
+test('test-loop',
+ executable('test-loop',
+ 'test-loop.c',
+ include_directories: pwtest_inc,
+ dependencies: [ spa_dep ],
+ link_with: pwtest_lib)
+)
+
+test('test-context',
+ executable('test-context',
+ 'test-context.c',
+ 'test-config.c',
+ include_directories: pwtest_inc,
+ dependencies: [spa_dep, spa_support_dep, spa_dbus_dep],
+ link_with: [pwtest_lib,
+ pipewire_module_protocol_native,
+ pipewire_module_client_node,
+ pipewire_module_client_device,
+ pipewire_module_adapter,
+ pipewire_module_metadata,
+ pipewire_module_session_manager])
+)
+
+test('test-support',
+ executable('test-support',
+ 'test-support.c',
+ 'test-logger.c',
+ include_directories: pwtest_inc,
+ dependencies: [spa_dep, systemd_dep, spa_support_dep, spa_journal_dep],
+ link_with: [pwtest_lib])
+)
+test('test-spa',
+ executable('test-spa',
+ 'test-spa-buffer.c',
+ 'test-spa-json.c',
+ 'test-spa-utils.c',
+ 'test-spa-log.c',
+ 'test-spa-node.c',
+ 'test-spa-pod.c',
+ include_directories: pwtest_inc,
+ dependencies: [ spa_dep ],
+ link_with: pwtest_lib)
+)
+
+openal_info = find_program('openal-info', required: false)
+if openal_info.found()
+ cdata.set_quoted('OPENAL_INFO_PATH', openal_info.full_path())
+endif
+summary({'openal-info': openal_info.found()}, bool_yn: true, section: 'Functional test programs')
+
+pactl = find_program('pactl', required: false)
+if pactl.found()
+ cdata.set_quoted('PACTL_PATH', pactl.full_path())
+endif
+summary({'pactl': pactl.found()}, bool_yn: true, section: 'Functional test programs')
+
+if default_sm == 'media-session' or default_sm == 'wireplumber'
+ test('test-functional',
+ executable('test-functional',
+ 'test-functional.c',
+ include_directories: pwtest_inc,
+ dependencies: [ spa_dep ],
+ link_with: pwtest_lib)
+ )
+endif
+
+valgrind = find_program('valgrind', required: false)
+summary({'valgrind (test setup)': valgrind.found()}, bool_yn: true, section: 'Optional programs')
+if valgrind.found()
+ valgrind_env = environment({'PIPEWIRE_DEBUG': 'D'})
+ add_test_setup('valgrind',
+ exe_wrapper : [ valgrind,
+ '--leak-check=full',
+ '--gen-suppressions=all',
+ '--error-exitcode=3',
+ ],
+ env : valgrind_env,
+ timeout_multiplier : 3)
+endif
diff --git a/test/pwtest-compat.c b/test/pwtest-compat.c
new file mode 100644
index 0000000..fa89756
--- /dev/null
+++ b/test/pwtest-compat.c
@@ -0,0 +1,55 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#ifndef HAVE_SIGABBREV_NP
+#include <stddef.h>
+#include <signal.h>
+
+/* glibc >= 2.32 */
+static inline const char *sigabbrev_np(int sig)
+{
+#define SIGABBREV(a_) case SIG##a_: return #a_
+ switch(sig) {
+ SIGABBREV(INT);
+ SIGABBREV(ABRT);
+ SIGABBREV(BUS);
+ SIGABBREV(SEGV);
+ SIGABBREV(ALRM);
+ SIGABBREV(CHLD);
+ SIGABBREV(HUP);
+ SIGABBREV(PIPE);
+ SIGABBREV(CONT);
+ SIGABBREV(STOP);
+ SIGABBREV(ILL);
+ SIGABBREV(KILL);
+ SIGABBREV(TERM);
+ }
+#undef SIGABBREV
+
+ return NULL;
+}
+
+#endif /* HAVE_SIGABBREV_NP */
diff --git a/test/pwtest-implementation.h b/test/pwtest-implementation.h
new file mode 100644
index 0000000..1525d20
--- /dev/null
+++ b/test/pwtest-implementation.h
@@ -0,0 +1,137 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PWTEST_IMPLEMENTATION_H
+#define PWTEST_IMPLEMENTATION_H
+
+#include <math.h>
+
+/* This header should never be included on its own, it merely exists to make
+ * the user-visible pwtest.h header more readable */
+
+void
+_pwtest_fail_condition(int exitstatus, const char *file, int line, const char *func,
+ const char *condition, const char *message, ...);
+void
+_pwtest_fail_comparison_int(const char *file, int line, const char *func,
+ const char *operator, int a, int b,
+ const char *astr, const char *bstr);
+void
+_pwtest_fail_comparison_double(const char *file, int line, const char *func,
+ const char *operator, double a, double b,
+ const char *astr, const char *bstr);
+void
+_pwtest_fail_comparison_ptr(const char *file, int line, const char *func,
+ const char *comparison);
+
+void
+_pwtest_fail_comparison_str(const char *file, int line, const char *func,
+ const char *comparison, const char *a, const char *b);
+
+void
+_pwtest_fail_comparison_bool(const char *file, int line, const char *func,
+ const char *operator, bool a, bool b,
+ const char *astr, const char *bstr);
+
+void
+_pwtest_fail_errno(const char *file, int line, const char *func,
+ int expected, int err_no);
+
+#define pwtest_errno_check(r_, errno_) \
+ do { \
+ int _r = r_; \
+ int _e = errno_; \
+ if (_e == 0) { \
+ if (_r == -1) \
+ _pwtest_fail_errno(__FILE__, __LINE__, __func__, _e, errno); \
+ } else { \
+ if (_r != -1 || errno != _e) \
+ _pwtest_fail_errno(__FILE__, __LINE__, __func__, _e, errno); \
+ } \
+ } while(0)
+
+#define pwtest_neg_errno_check(r_, errno_) \
+ do { \
+ int _r = r_; \
+ int _e = errno_; \
+ if (_e == 0) { \
+ if (_r < 0) \
+ _pwtest_fail_errno(__FILE__, __LINE__, __func__, _e, -_r); \
+ } else { \
+ if (_r >= 0 || _r != _e) \
+ _pwtest_fail_errno(__FILE__, __LINE__, __func__, -_e, _r >= 0 ? 0 : -_r); \
+ } \
+ } while(0)
+
+#define pwtest_comparison_bool_(a_, op_, b_) \
+ do { \
+ bool _a = !!(a_); \
+ bool _b = !!(b_); \
+ if (!(_a op_ _b)) \
+ _pwtest_fail_comparison_bool(__FILE__, __LINE__, __func__,\
+ #op_, _a, _b, #a_, #b_); \
+ } while(0)
+
+#define pwtest_comparison_int_(a_, op_, b_) \
+ do { \
+ __typeof__(a_) _a = a_; \
+ __typeof__(b_) _b = b_; \
+ if (trunc(_a) != _a || trunc(_b) != _b) \
+ pwtest_error_with_msg("pwtest_int_* used for non-integer value\n"); \
+ if (!((_a) op_ (_b))) \
+ _pwtest_fail_comparison_int(__FILE__, __LINE__, __func__,\
+ #op_, _a, _b, #a_, #b_); \
+ } while(0)
+
+#define pwtest_comparison_ptr_(a_, op_, b_) \
+ do { \
+ __typeof__(a_) _a = a_; \
+ __typeof__(b_) _b = b_; \
+ if (!((_a) op_ (_b))) \
+ _pwtest_fail_comparison_ptr(__FILE__, __LINE__, __func__,\
+ #a_ " " #op_ " " #b_); \
+ } while(0)
+
+#define pwtest_comparison_double_(a_, op_, b_) \
+ do { \
+ const double EPSILON = 1.0/256; \
+ __typeof__(a_) _a = a_; \
+ __typeof__(b_) _b = b_; \
+ if (!((_a) op_ (_b)) && fabs((_a) - (_b)) > EPSILON) \
+ _pwtest_fail_comparison_double(__FILE__, __LINE__, __func__,\
+ #op_, _a, _b, #a_, #b_); \
+ } while(0)
+
+void _pwtest_add(struct pwtest_context *ctx,
+ struct pwtest_suite *suite,
+ const char *funcname, const void *func,
+ ...) SPA_SENTINEL;
+
+struct pwtest_suite_decl {
+ const char *name;
+ enum pwtest_result (*setup)(struct pwtest_context *, struct pwtest_suite *);
+};
+
+
+#endif /* PWTEST_IMPLEMENTATION_H */
diff --git a/test/pwtest.c b/test/pwtest.c
new file mode 100644
index 0000000..968805b
--- /dev/null
+++ b/test/pwtest.c
@@ -0,0 +1,1486 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <assert.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <ftw.h>
+#include <getopt.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+#ifdef HAVE_PIDFD_OPEN
+#include <sys/syscall.h>
+#endif
+#ifdef HAVE_LIBCAP
+#include <sys/capability.h>
+#endif
+#include <sys/epoll.h>
+#include <sys/ptrace.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/timerfd.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include <valgrind/valgrind.h>
+
+#include "spa/utils/ansi.h"
+#include "spa/utils/string.h"
+#include "spa/utils/defs.h"
+#include "spa/utils/list.h"
+#include "spa/support/plugin.h"
+
+#include "pipewire/array.h"
+#include "pipewire/utils.h"
+#include "pipewire/properties.h"
+
+#include "pwtest.h"
+
+#include "pwtest-compat.c"
+
+#define pwtest_log(...) dprintf(testlog_fd, __VA_ARGS__)
+#define pwtest_vlog(format_, args_) vdprintf(testlog_fd, format_, args_)
+
+static bool verbose = false;
+
+/** the global context object */
+static struct pwtest_context *ctx;
+
+/**
+ * The various pwtest_assert() etc. functions write to this fd, collected
+ * separately in the log.
+ */
+static int testlog_fd = STDOUT_FILENO;
+
+enum pwtest_logfds {
+ FD_STDOUT,
+ FD_STDERR,
+ FD_LOG,
+ FD_DAEMON,
+ _FD_LAST,
+};
+
+struct pwtest_test {
+ struct spa_list link;
+ const char *name;
+ enum pwtest_result (*func)(struct pwtest_test *test);
+
+ int iteration;
+
+ /* env vars changed by pwtest. These will be restored after the test
+ * run to get close to the original environment. */
+ struct pw_properties *env;
+
+ /* Arguments during pwtest_add() */
+ struct {
+ int signal;
+ struct {
+ int min, max;
+ } range;
+ struct pw_properties *props;
+ struct pw_properties *env;
+ bool pw_daemon;
+ } args;
+
+ /* Results */
+ enum pwtest_result result;
+ int sig_or_errno;
+ struct pw_array logs[_FD_LAST];
+};
+
+struct pwtest_suite {
+ struct spa_list link;
+ const struct pwtest_suite_decl *decl;
+ enum pwtest_result result;
+
+ struct spa_list tests;
+};
+
+struct pwtest_context {
+ struct spa_list suites;
+ unsigned int timeout;
+ bool no_fork;
+ bool terminate;
+ struct spa_list cleanup_pids;
+
+ const char *test_filter;
+ bool has_iteration_filter;
+ int iteration_filter;
+ char *xdg_dir;
+};
+
+struct cleanup_pid {
+ struct spa_list link;
+ pid_t pid;
+};
+
+struct pwtest_context *pwtest_get_context(struct pwtest_test *t)
+{
+ return ctx;
+}
+
+int pwtest_get_iteration(struct pwtest_test *t)
+{
+ return t->iteration;
+}
+
+struct pw_properties *pwtest_get_props(struct pwtest_test *t)
+{
+ return t->args.props;
+}
+
+static void replace_env(struct pwtest_test *t, const char *prop, const char *value)
+{
+ const char *oldval = getenv(prop);
+
+ pw_properties_set(t->env, prop, oldval ? oldval : "pwtest-null");
+ if (value)
+ setenv(prop, value, 1);
+ else
+ unsetenv(prop);
+}
+
+static void restore_env(struct pwtest_test *t)
+{
+ const char *env;
+ void *state = NULL;
+
+ while ((env = pw_properties_iterate(t->env, &state))) {
+ const char *value = pw_properties_get(t->env, env);
+ if (spa_streq(value, "pwtest-null"))
+ unsetenv(env);
+ else
+ setenv(env, value, 1);
+ }
+}
+
+static int add_cleanup_pid(struct pwtest_context *ctx, pid_t pid)
+{
+ struct cleanup_pid *cpid;
+
+ if (pid == 0)
+ return -EINVAL;
+
+ cpid = calloc(1, sizeof(struct cleanup_pid));
+ if (cpid == NULL)
+ return -errno;
+
+ cpid->pid = pid;
+ spa_list_append(&ctx->cleanup_pids, &cpid->link);
+
+ return 0;
+}
+
+static void remove_cleanup_pid(struct pwtest_context *ctx, pid_t pid)
+{
+ struct cleanup_pid *cpid, *t;
+
+ spa_list_for_each_safe(cpid, t, &ctx->cleanup_pids, link) {
+ if (cpid->pid == pid) {
+ spa_list_remove(&cpid->link);
+ free(cpid);
+ }
+ }
+}
+
+static void terminate_cleanup_pids(struct pwtest_context *ctx)
+{
+ struct cleanup_pid *cpid;
+ spa_list_for_each(cpid, &ctx->cleanup_pids, link) {
+ /* Don't free here, to be signal-safe */
+ if (cpid->pid != 0) {
+ kill(cpid->pid, SIGTERM);
+ cpid->pid = 0;
+ }
+ }
+}
+
+static void free_cleanup_pids(struct pwtest_context *ctx)
+{
+ struct cleanup_pid *cpid;
+ spa_list_consume(cpid, &ctx->cleanup_pids, link) {
+ spa_list_remove(&cpid->link);
+ free(cpid);
+ }
+}
+
+static void pwtest_backtrace(pid_t p)
+{
+#ifdef HAVE_GSTACK
+ char pid[11];
+ pid_t parent, child;
+ int status;
+
+ if (RUNNING_ON_VALGRIND)
+ return;
+
+ parent = p == 0 ? getpid() : p;
+ child = fork();
+ if (child == 0) {
+ assert(testlog_fd > 0);
+ /* gstack writes the backtrace to stdout, we re-route to our
+ * custom fd */
+ dup2(testlog_fd, STDOUT_FILENO);
+
+ spa_scnprintf(pid, sizeof(pid), "%d", (uint32_t)parent);
+ execlp("gstack", "gstack", pid, NULL);
+ exit(errno);
+ }
+
+ /* parent */
+ waitpid(child, &status, 0);
+#endif
+}
+
+SPA_PRINTF_FUNC(6, 7)
+SPA_NORETURN
+void _pwtest_fail_condition(int exitstatus,
+ const char *file, int line, const char *func,
+ const char *condition, const char *message, ...)
+{
+ pwtest_log("FAILED: %s\n", condition);
+
+ if (message) {
+ va_list args;
+ va_start(args, message);
+ pwtest_vlog(message, args);
+ va_end(args);
+ pwtest_log("\n");
+ }
+
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(exitstatus);
+}
+
+SPA_NORETURN
+void _pwtest_fail_comparison_bool(const char *file, int line, const char *func,
+ const char *operator, bool a, bool b,
+ const char *astr, const char *bstr)
+{
+ pwtest_log("FAILED COMPARISON: %s %s %s\n", astr, operator, bstr);
+ pwtest_log("Resolved to: %s %s %s\n", a ? "true" : "false", operator, b ? "true" : "false");
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+SPA_NORETURN
+void _pwtest_fail_errno(const char *file, int line, const char *func,
+ int expected, int err_no)
+{
+ pwtest_log("FAILED ERRNO CHECK: expected %d (%s), got %d (%s)\n",
+ expected, strerror(expected), err_no, strerror(err_no));
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+
+SPA_NORETURN
+void _pwtest_fail_comparison_int(const char *file, int line, const char *func,
+ const char *operator, int a, int b,
+ const char *astr, const char *bstr)
+{
+ pwtest_log("FAILED COMPARISON: %s %s %s\n", astr, operator, bstr);
+ pwtest_log("Resolved to: %d %s %d\n", a, operator, b);
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+SPA_NORETURN
+void _pwtest_fail_comparison_double(const char *file, int line, const char *func,
+ const char *operator, double a, double b,
+ const char *astr, const char *bstr)
+{
+ pwtest_log("FAILED COMPARISON: %s %s %s\n", astr, operator, bstr);
+ pwtest_log("Resolved to: %.3f %s %.3f\n", a, operator, b);
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+SPA_NORETURN
+void _pwtest_fail_comparison_ptr(const char *file, int line, const char *func,
+ const char *comparison)
+{
+ pwtest_log("FAILED COMPARISON: %s\n", comparison);
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+SPA_NORETURN
+void _pwtest_fail_comparison_str(const char *file, int line, const char *func,
+ const char *comparison, const char *a, const char *b)
+{
+ pwtest_log("FAILED COMPARISON: %s, expanded (\"%s\" vs \"%s\")\n", comparison, a, b);
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+struct pwtest_spa_plugin *
+pwtest_spa_plugin_new(void)
+{
+ return calloc(1, sizeof(struct pwtest_spa_plugin));
+}
+
+void
+pwtest_spa_plugin_destroy(struct pwtest_spa_plugin *plugin)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(plugin->handles, hnd) {
+ if (*hnd) {
+ spa_handle_clear(*hnd);
+ free(*hnd);
+ }
+ }
+ SPA_FOR_EACH_ELEMENT_VAR(plugin->dlls, dll) {
+ if (*dll)
+ dlclose(*dll);
+ }
+ free(plugin);
+}
+
+int
+pwtest_spa_plugin_try_load_interface(struct pwtest_spa_plugin *plugin,
+ void **iface_return,
+ const char *libname,
+ const char *factory_name,
+ const char *interface_name,
+ const struct spa_dict *info)
+{
+ char *libdir = getenv("SPA_PLUGIN_DIR");
+ char path[PATH_MAX];
+ void *hnd, *iface;
+ spa_handle_factory_enum_func_t enum_func;
+ const struct spa_handle_factory *factory;
+ uint32_t index = 0;
+ int r;
+ bool found = false;
+ struct spa_handle *handle;
+
+ spa_assert_se(libdir != NULL);
+ spa_scnprintf(path, sizeof(path), "%s/%s.so", libdir, libname);
+
+ hnd = dlopen(path, RTLD_NOW);
+ if (hnd == NULL)
+ return -ENOENT;
+
+ enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME);
+ pwtest_ptr_notnull(enum_func);
+
+ while ((r = enum_func(&factory, &index)) > 0) {
+ pwtest_int_ge(factory->version, 1U);
+ if (spa_streq(factory->name, factory_name)) {
+ found = true;
+ break;
+ }
+ }
+ pwtest_neg_errno_ok(r);
+ if (!found) {
+ dlclose(hnd);
+ return -EINVAL;
+ }
+
+ handle = calloc(1, spa_handle_factory_get_size(factory, info));
+ pwtest_ptr_notnull(handle);
+
+ r = spa_handle_factory_init(factory, handle, info, plugin->support, plugin->nsupport);
+ pwtest_neg_errno_ok(r);
+ if ((r = spa_handle_get_interface(handle, interface_name, &iface)) != 0) {
+ spa_handle_clear(handle);
+ free(handle);
+ dlclose(hnd);
+ return -ENOSYS;
+ }
+
+ plugin->dlls[plugin->ndlls++] = hnd;
+ plugin->handles[plugin->nhandles++] = handle;
+ plugin->support[plugin->nsupport++] = SPA_SUPPORT_INIT(interface_name, iface);
+
+ *iface_return = iface;
+ return 0;
+}
+
+void *
+pwtest_spa_plugin_load_interface(struct pwtest_spa_plugin *plugin,
+ const char *libname,
+ const char *factory_name,
+ const char *interface_name,
+ const struct spa_dict *info)
+{
+ void *iface;
+ int r = pwtest_spa_plugin_try_load_interface(plugin, &iface, libname,
+ factory_name, interface_name, info);
+ pwtest_neg_errno_ok(r);
+ return iface;
+}
+
+void
+pwtest_mkstemp(char path[PATH_MAX])
+{
+ const char *tmpdir = getenv("TMPDIR");
+ int r;
+
+ if (tmpdir == NULL)
+ pwtest_error_with_msg("tmpdir is unset");
+
+ spa_scnprintf(path, PATH_MAX, "%s/%s", tmpdir, "tmp.XXXXXX");
+ r = mkstemp(path);
+ if (r == -1)
+ pwtest_error_with_msg("Unable to create temporary file: %s", strerror(errno));
+}
+
+int
+pwtest_spawn(const char *file, char *const argv[])
+{
+ int r;
+ int status = -1;
+ pid_t pid;
+ const int fail_code = 121;
+
+ pid = fork();
+ if (pid == 0) {
+ /* child process */
+ execvp(file, (char **)argv);
+ exit(fail_code);
+ } else if (pid < 0)
+ pwtest_error_with_msg("Unable to fork: %s", strerror(errno));
+
+ add_cleanup_pid(ctx, pid);
+ r = waitpid(pid, &status, 0);
+ remove_cleanup_pid(ctx, pid);
+ if (r <= 0)
+ pwtest_error_with_msg("waitpid failed: %s", strerror(errno));
+
+ if (WEXITSTATUS(status) == fail_code)
+ pwtest_error_with_msg("exec %s failed", file);
+
+ return status;
+}
+
+void _pwtest_add(struct pwtest_context *ctx, struct pwtest_suite *suite,
+ const char *funcname, const void *func, ...)
+{
+ struct pwtest_test *t;
+ va_list args;
+
+ if (ctx->test_filter != NULL && fnmatch(ctx->test_filter, funcname, 0) != 0)
+ return;
+
+ t = calloc(1, sizeof *t);
+ t->result = PWTEST_SYSTEM_ERROR;
+ t->name = funcname;
+ t->func = func;
+ t->args.range.min = 0;
+ t->args.range.max = 1;
+ t->args.env = pw_properties_new("PWTEST", "1", NULL);
+ t->env = pw_properties_new(NULL, NULL);
+ for (size_t i = 0; i < SPA_N_ELEMENTS(t->logs); i++)
+ pw_array_init(&t->logs[i], 1024);
+
+ va_start(args, func);
+ while (true) {
+ const char *key, *value;
+
+ enum pwtest_arg arg = va_arg(args, enum pwtest_arg);
+ if (!arg)
+ break;
+ switch (arg) {
+ case PWTEST_NOARG:
+ break;
+ case PWTEST_ARG_SIGNAL:
+ if (RUNNING_ON_VALGRIND)
+ t->result = PWTEST_SKIP;
+ t->args.signal = va_arg(args, int);
+ break;
+ case PWTEST_ARG_RANGE:
+ t->args.range.min = va_arg(args, int);
+ t->args.range.max = va_arg(args, int);
+ break;
+ case PWTEST_ARG_PROP:
+ key = va_arg(args, const char *);
+ value = va_arg(args, const char *);
+ if (t->args.props == NULL) {
+ t->args.props = pw_properties_new(key, value, NULL);
+ } else {
+ pw_properties_set(t->args.props, key, value);
+ }
+ break;
+ case PWTEST_ARG_ENV:
+ key = va_arg(args, const char *);
+ value = va_arg(args, const char *);
+ pw_properties_set(t->args.env, key, value);
+ break;
+ case PWTEST_ARG_DAEMON:
+ if (RUNNING_ON_VALGRIND)
+ t->result = PWTEST_SKIP;
+ t->args.pw_daemon = true;
+ break;
+ }
+ }
+ va_end(args);
+
+ spa_list_append(&suite->tests, &t->link);
+}
+
+extern const struct pwtest_suite_decl __start_pwtest_suite_section;
+extern const struct pwtest_suite_decl __stop_pwtest_suite_section;
+
+static void add_suite(struct pwtest_context *ctx,
+ const struct pwtest_suite_decl *decl)
+{
+ struct pwtest_suite *c = calloc(1, sizeof *c);
+
+ c->decl = decl;
+ spa_list_init(&c->tests);
+ spa_list_append(&ctx->suites, &c->link);
+}
+
+static void free_test(struct pwtest_test *t)
+{
+ spa_list_remove(&t->link);
+ for (size_t i = 0; i < SPA_N_ELEMENTS(t->logs); i++)
+ pw_array_clear(&t->logs[i]);
+ pw_properties_free(t->args.props);
+ pw_properties_free(t->args.env);
+ pw_properties_free(t->env);
+ free(t);
+}
+
+static void free_suite(struct pwtest_suite *c)
+{
+ struct pwtest_test *t, *tmp;
+
+ spa_list_for_each_safe(t, tmp, &c->tests, link)
+ free_test(t);
+
+ spa_list_remove(&c->link);
+ free(c);
+}
+
+static void find_suites(struct pwtest_context *ctx, const char *suite_filter)
+{
+ const struct pwtest_suite_decl *c;
+
+ for (c = &__start_pwtest_suite_section; c < &__stop_pwtest_suite_section; c++) {
+ if (suite_filter == NULL || fnmatch(suite_filter, c->name, 0) == 0)
+ add_suite(ctx, c);
+ }
+}
+
+static void add_tests(struct pwtest_context *ctx)
+{
+ struct pwtest_suite *c;
+
+ spa_list_for_each(c, &ctx->suites, link) {
+ c->result = c->decl->setup(ctx, c);
+ spa_assert_se(c->result >= PWTEST_PASS && c->result <= PWTEST_SYSTEM_ERROR);
+ }
+}
+
+static int remove_file(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf)
+{
+ char *tmpdir = getenv("TMPDIR");
+ int r;
+
+ /* Safety check: bail out if somehow we left TMPDIR */
+ spa_assert_se(tmpdir != NULL);
+ spa_assert_se(spa_strneq(fpath, tmpdir, strlen(tmpdir)));
+
+ r = remove(fpath);
+ if (r)
+ fprintf(stderr, "Failed to remove %s: %m", fpath);
+
+ return r;
+}
+
+static void remove_xdg_runtime_dir(const char *xdg_dir)
+{
+ char *tmpdir = getenv("TMPDIR");
+ char path[PATH_MAX];
+ int r;
+
+ if (xdg_dir == NULL)
+ return;
+
+ /* Safety checks, we really don't want to recursively remove a
+ * random directory due to a bug */
+ spa_assert_se(tmpdir != NULL);
+ spa_assert_se(spa_strneq(xdg_dir, tmpdir, strlen(tmpdir)));
+ r = spa_scnprintf(path, sizeof(path), "%s/pwtest.dir", xdg_dir);
+ spa_assert_se((size_t)r == strlen(xdg_dir) + 11);
+ if (access(path, F_OK) != 0) {
+ fprintf(stderr, "XDG_RUNTIME_DIR changed, cannot clean up\n");
+ return;
+ }
+
+ nftw(xdg_dir, remove_file, 16, FTW_DEPTH | FTW_PHYS);
+}
+
+static void cleanup(struct pwtest_context *ctx)
+{
+ struct pwtest_suite *c, *tmp;
+
+ terminate_cleanup_pids(ctx);
+ free_cleanup_pids(ctx);
+
+ spa_list_for_each_safe(c, tmp, &ctx->suites, link) {
+ free_suite(c);
+ }
+
+ remove_xdg_runtime_dir(ctx->xdg_dir);
+ free(ctx->xdg_dir);
+}
+
+static void sighandler(int signal)
+{
+ struct sigaction act;
+ sigemptyset(&act.sa_mask);
+ act.sa_flags = 0;
+ act.sa_handler = SIG_DFL;
+ sigaction(signal, &act, NULL);
+
+ pwtest_backtrace(0);
+ terminate_cleanup_pids(ctx);
+ raise(signal);
+}
+
+static inline void log_append(struct pw_array *buffer, int fd)
+{
+ int r = 0;
+ const int sz = 65536;
+
+ while (true) {
+ r = pw_array_ensure_size(buffer, sz);
+ spa_assert_se(r == 0);
+ r = read(fd, pw_array_end(buffer), sz);
+ if (r <= 0)
+ break;
+ /* We've read directly into the array's buffer, we just add
+ * now to update the array */
+ pw_array_add(buffer, r);
+ }
+}
+
+static bool collect_child(struct pwtest_test *t, pid_t pid)
+{
+ int r;
+ int status;
+
+ r = waitpid(pid, &status, WNOHANG);
+ if (r <= 0)
+ return false;
+
+ if (WIFEXITED(status)) {
+ t->result = WEXITSTATUS(status);
+ switch (t->result) {
+ case PWTEST_PASS:
+ case PWTEST_SKIP:
+ case PWTEST_FAIL:
+ case PWTEST_TIMEOUT:
+ case PWTEST_SYSTEM_ERROR:
+ break;
+ default:
+ spa_assert_se(!"Invalid test result");
+ break;
+ }
+ return true;
+ }
+
+ if (WIFSIGNALED(status)) {
+ t->sig_or_errno = WTERMSIG(status);
+ t->result = (t->sig_or_errno == t->args.signal) ? PWTEST_PASS : PWTEST_FAIL;
+ } else {
+ t->result = PWTEST_FAIL;
+ }
+ return true;
+}
+
+static pid_t start_pwdaemon(struct pwtest_test *t, int stderr_fd, int log_fd)
+{
+ static unsigned int count;
+ const char *daemon = BUILD_ROOT "/src/daemon/pipewire-uninstalled";
+ pid_t pid;
+ char pw_remote[64];
+ int status;
+ int r;
+
+ spa_scnprintf(pw_remote, sizeof(pw_remote), "pwtest-pw-%u\n", count++);
+ replace_env(t, "PIPEWIRE_REMOTE", pw_remote);
+
+ pid = fork();
+ if (pid == 0) {
+ /* child */
+ setpgid(0, 0);
+
+ setenv("PIPEWIRE_CORE", pw_remote, 1);
+ setenv("PIPEWIRE_DEBUG", "4", 0);
+ setenv("WIREPLUMBER_DEBUG", "4", 0);
+
+ r = dup2(stderr_fd, STDERR_FILENO);
+ spa_assert_se(r != -1);
+ r = dup2(stderr_fd, STDOUT_FILENO);
+ spa_assert_se(r != -1);
+
+ execl(daemon, daemon, (char*)NULL);
+ return -errno;
+
+ } else if (pid < 0) {
+ return pid;
+ }
+
+ add_cleanup_pid(ctx, -pid);
+
+ /* parent */
+ sleep(1); /* FIXME how to wait for pw to be ready? */
+ if (waitpid(pid, &status, WNOHANG) > 0) {
+ if (WIFEXITED(status)) {
+ dprintf(log_fd, "pipewire daemon exited with %d before test started\n", WEXITSTATUS(status));
+ return -ESRCH;
+ } else if (WIFSIGNALED(status)) {
+ dprintf(log_fd, "pipewire daemon terminated with %d (SIG%s) before test started\n", WTERMSIG(status),
+ sigabbrev_np(WTERMSIG(status)));
+ return -EHOSTDOWN;
+ }
+ }
+
+ return pid;
+}
+
+static void make_xdg_runtime_test_dir(char dir[PATH_MAX], const char *prefix)
+{
+ static size_t counter;
+ int r;
+
+ r = spa_scnprintf(dir, PATH_MAX, "%s/%zd", prefix, counter++);
+ spa_assert_se(r >= (int)(strlen(prefix) + 2));
+ r = mkdir(dir, 0777);
+ if (r == -1) {
+ fprintf(stderr, "Failed to make XDG_RUNTIME_DIR %s (%m)\n", dir);
+ spa_assert_se(r != -1);
+ }
+}
+
+static void set_test_env(struct pwtest_context *ctx, struct pwtest_test *t)
+{
+ char xdg_runtime_dir[PATH_MAX];
+
+ make_xdg_runtime_test_dir(xdg_runtime_dir, ctx->xdg_dir);
+ replace_env(t, "XDG_RUNTIME_DIR", xdg_runtime_dir);
+ replace_env(t, "TMPDIR", xdg_runtime_dir);
+
+ replace_env(t, "SPA_PLUGIN_DIR", BUILD_ROOT "/spa/plugins");
+ replace_env(t, "SPA_DATA_DIR", SOURCE_ROOT "/spa/plugins");
+ replace_env(t, "PIPEWIRE_CONFIG_DIR", BUILD_ROOT "/src/daemon");
+ replace_env(t, "PIPEWIRE_MODULE_DIR", BUILD_ROOT "/src/modules");
+ replace_env(t, "ACP_PATHS_DIR", SOURCE_ROOT "/spa/plugins/alsa/mixer/paths");
+ replace_env(t, "ACP_PROFILES_DIR", SOURCE_ROOT "/spa/plugins/alsa/mixer/profile-sets");
+ replace_env(t, "PIPEWIRE_LOG_SYSTEMD", "false");
+}
+
+static void close_pipes(int fds[_FD_LAST])
+{
+ for (int i = 0; i < _FD_LAST; i++) {
+ if (fds[i] >= 0)
+ close(fds[i]);
+ fds[i] = -1;
+ }
+}
+
+static int init_pipes(int read_fds[_FD_LAST], int write_fds[_FD_LAST])
+{
+ int r;
+ int i;
+ int pipe_max_size = 4194304;
+
+ for (i = 0; i < _FD_LAST; i++) {
+ read_fds[i] = -1;
+ write_fds[i] = -1;
+ }
+
+#ifdef __linux__
+ {
+ FILE *f;
+ f = fopen("/proc/sys/fs/pipe-max-size", "re");
+ if (f) {
+ if (fscanf(f, "%d", &r) == 1)
+ pipe_max_size = SPA_MIN(r, pipe_max_size);
+ fclose(f);
+ }
+ }
+#endif
+
+ for (i = 0; i < _FD_LAST; i++) {
+ int pipe[2];
+
+ r = pipe2(pipe, O_CLOEXEC | O_NONBLOCK);
+ if (r < 0)
+ goto error;
+ read_fds[i] = pipe[0];
+ write_fds[i] = pipe[1];
+#ifdef __linux__
+ /* Max pipe buffers, to avoid scrambling if reading lags.
+ * Can't use blocking write fds, since reading too slow
+ * then affects execution.
+ */
+ fcntl(write_fds[i], F_SETPIPE_SZ, pipe_max_size);
+#endif
+ }
+
+ return 0;
+error:
+ r = -errno;
+ close_pipes(read_fds);
+ close_pipes(write_fds);
+ return r;
+}
+
+static void start_test_nofork(struct pwtest_test *t)
+{
+ const char *env;
+ void *state = NULL;
+
+ /* This is going to mess with future tests */
+ while ((env = pw_properties_iterate(t->args.env, &state)))
+ replace_env(t, env, pw_properties_get(t->args.env, env));
+
+ /* The actual test function */
+ t->result = t->func(t);
+}
+
+static int start_test_forked(struct pwtest_test *t, int read_fds[_FD_LAST], int write_fds[_FD_LAST])
+{
+ pid_t pid;
+ enum pwtest_result result;
+ struct sigaction act;
+ const char *env;
+ void *state = NULL;
+ int r;
+
+ pid = fork();
+ if (pid < 0) {
+ r = -errno;
+ close_pipes(read_fds);
+ close_pipes(write_fds);
+ return r;
+ }
+
+ if (pid > 0) { /* parent */
+ close_pipes(write_fds);
+ return pid;
+ }
+
+ /* child */
+
+ close_pipes(read_fds);
+
+ /* Reset cleanup pid list */
+ free_cleanup_pids(ctx);
+
+ /* Catch any crashers so we can insert a backtrace */
+ sigemptyset(&act.sa_mask);
+ act.sa_flags = 0;
+ act.sa_handler = sighandler;
+ sigaction(SIGSEGV, &act, NULL);
+ sigaction(SIGBUS, &act, NULL);
+ sigaction(SIGSEGV, &act, NULL);
+ sigaction(SIGABRT, &act, NULL);
+ /* SIGALARM is used for our timeout */
+ sigaction(SIGALRM, &act, NULL);
+
+ r = dup2(write_fds[FD_STDERR], STDERR_FILENO);
+ spa_assert_se(r != -1);
+ setlinebuf(stderr);
+ r = dup2(write_fds[FD_STDOUT], STDOUT_FILENO);
+ spa_assert_se(r != -1);
+ setlinebuf(stdout);
+
+ /* For convenience in the tests, let this be a global variable. */
+ testlog_fd = write_fds[FD_LOG];
+
+ while ((env = pw_properties_iterate(t->args.env, &state)))
+ setenv(env, pw_properties_get(t->args.env, env), 1);
+
+ /* The actual test function */
+ result = t->func(t);
+
+ for (int i = 0; i < _FD_LAST; i++)
+ fsync(write_fds[i]);
+
+ exit(result);
+}
+
+static int monitor_test_forked(struct pwtest_test *t, pid_t pid, int read_fds[_FD_LAST])
+{
+ int pidfd = -1, timerfd = -1, epollfd = -1;
+ struct epoll_event ev[10];
+ size_t nevents = 0;
+ int r;
+
+#ifdef HAVE_PIDFD_OPEN
+ pidfd = syscall(SYS_pidfd_open, pid, 0);
+#else
+ errno = ENOSYS;
+#endif
+ /* If we don't have pidfd, we use a timerfd to ping us every 20ms */
+ if (pidfd < 0 && errno == ENOSYS) {
+ pidfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
+ if (pidfd == -1)
+ goto error;
+ r = timerfd_settime(pidfd, 0,
+ &((struct itimerspec ){
+ .it_interval.tv_nsec = 20 * 1000 * 1000,
+ .it_value.tv_nsec = 20 * 1000 * 1000,
+ }), NULL);
+ if (r < 0)
+ goto error;
+ }
+
+ /* Each test has an epollfd with:
+ * - a timerfd so we can kill() it if it hangs
+ * - a pidfd so we get notified when the test exits
+ * - a pipe for stdout and a pipe for stderr
+ * - a pipe for logging (the various pwtest functions)
+ * - a pipe for the daemon's stdout
+ */
+ timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
+ if (timerfd < 0)
+ goto error;
+ timerfd_settime(timerfd, 0, &((struct itimerspec ){ .it_value.tv_sec = ctx->timeout}), NULL);
+
+ epollfd = epoll_create(1);
+ if (epollfd < 0)
+ goto error;
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = pidfd };
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = read_fds[FD_STDOUT] };
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = read_fds[FD_STDERR] };
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = read_fds[FD_LOG] };
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = timerfd };
+ if (t->args.pw_daemon)
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = read_fds[FD_DAEMON] };
+
+ for (size_t i = 0; i < nevents; i++) {
+ r = epoll_ctl(epollfd, EPOLL_CTL_ADD, ev[i].data.fd, &ev[i]);
+ if (r < 0)
+ goto error;
+ }
+
+ while (true) {
+ struct epoll_event e;
+
+ r = epoll_wait(epollfd, &e, 1, (ctx->timeout * 2) * 1000);
+ if (r == 0)
+ break;
+ if (r == -1) {
+ goto error;
+ }
+
+ if (e.data.fd == pidfd) {
+ uint64_t buf;
+ int ignore SPA_UNUSED;
+ ignore = read(pidfd, &buf, sizeof(buf)); /* for timerfd fallback */
+ if (collect_child(t, pid))
+ break;
+ } else if (e.data.fd == timerfd) {
+ /* SIGALARM so we get the backtrace */
+ kill(pid, SIGALRM);
+ t->result = PWTEST_TIMEOUT;
+ waitpid(pid, NULL, 0);
+ break;
+ } else {
+ for (int i = 0; i < _FD_LAST; i++) {
+ if (e.data.fd == read_fds[i]) {
+ log_append(&t->logs[i], e.data.fd);
+ }
+
+ }
+ }
+ }
+ errno = 0;
+error:
+ r = errno;
+ close(epollfd);
+ close(timerfd);
+ close(pidfd);
+
+ return -r;
+}
+
+static void run_test(struct pwtest_context *ctx, struct pwtest_suite *c, struct pwtest_test *t)
+{
+ pid_t pid;
+ pid_t pw_daemon = 0;
+ int read_fds[_FD_LAST], write_fds[_FD_LAST];
+ int r;
+ const char *tmpdir;
+
+ if (t->result == PWTEST_SKIP) {
+ char *buf = pw_array_add(&t->logs[FD_LOG], 64);
+ spa_scnprintf(buf, 64, "pwtest: test skipped by pwtest\n");
+ return;
+ }
+
+ t->result = PWTEST_SYSTEM_ERROR;
+
+ r = init_pipes(read_fds, write_fds);
+ if (r < 0) {
+ t->sig_or_errno = r;
+ return;
+ }
+
+ set_test_env(ctx, t);
+ tmpdir = getenv("TMPDIR");
+ spa_assert_se(tmpdir != NULL);
+ r = chdir(tmpdir);
+ if (r < 0) {
+ char *buf = pw_array_add(&t->logs[FD_LOG], 256);
+ spa_scnprintf(buf, 256, "pwtest: failed to chdir to '%s'\n", tmpdir);
+ t->sig_or_errno = -errno;
+ goto error;
+ }
+
+ if (t->args.pw_daemon) {
+ pw_daemon = start_pwdaemon(t, write_fds[FD_DAEMON], write_fds[FD_LOG]);
+ if (pw_daemon < 0) {
+ errno = -pw_daemon;
+ goto error;
+ }
+ } else {
+ replace_env(t, "PIPEWIRE_REMOTE", "test-has-no-daemon");
+ }
+
+ if (ctx->no_fork) {
+ start_test_nofork(t);
+ } else {
+ pid = start_test_forked(t, read_fds, write_fds);
+ if (pid < 0) {
+ errno = -r;
+ goto error;
+ }
+ add_cleanup_pid(ctx, pid);
+
+ r = monitor_test_forked(t, pid, read_fds);
+ if (r < 0) {
+ errno = -r;
+ goto error;
+ }
+ remove_cleanup_pid(ctx, pid);
+ }
+
+ errno = 0;
+error:
+ if (errno)
+ t->sig_or_errno = -errno;
+
+ if (ctx->terminate) {
+ char *buf = pw_array_add(&t->logs[FD_LOG], 64);
+ spa_scnprintf(buf, 64, "pwtest: tests terminated by signal\n");
+ t->result = PWTEST_SYSTEM_ERROR;
+ }
+
+ for (size_t i = 0; i < SPA_N_ELEMENTS(read_fds); i++) {
+ log_append(&t->logs[i], read_fds[i]);
+ }
+
+ if (pw_daemon > 0) {
+ int status;
+
+ kill(-pw_daemon, SIGTERM);
+ remove_cleanup_pid(ctx, -pw_daemon);
+
+ /* blocking read. the other end closes when done */
+ close_pipes(write_fds);
+ fcntl(read_fds[FD_DAEMON], F_SETFL, O_CLOEXEC);
+ do {
+ log_append(&t->logs[FD_DAEMON], read_fds[FD_DAEMON]);
+ } while ((r = waitpid(pw_daemon, &status, WNOHANG)) == 0);
+
+ if (r > 0) {
+ /* write_fds are closed in the parent process, so we append directly */
+ char *buf = pw_array_add(&t->logs[FD_DAEMON], 64);
+
+ if (WIFEXITED(status)) {
+ spa_scnprintf(buf, 64, "pwtest: pipewire daemon exited with status %d\n",
+ WEXITSTATUS(status));
+ } else if (WIFSIGNALED(status)) {
+ spa_scnprintf(buf, 64, "pwtest: pipewire daemon crashed with signal %d (SIG%s)\n",
+ WTERMSIG(status), sigabbrev_np(WTERMSIG(status)));
+ }
+ }
+ }
+
+ for (size_t i = 0; i < SPA_N_ELEMENTS(t->logs); i++) {
+ char *e = pw_array_add(&t->logs[i], 1);
+ spa_assert_se(e);
+ *e = '\0';
+ }
+
+ close_pipes(read_fds);
+ close_pipes(write_fds);
+
+ restore_env(t);
+}
+
+static inline void print_lines(FILE *fp, const char *log, const char *prefix)
+{
+ const char *state = NULL;
+ const char *s;
+ size_t len;
+
+ while (true) {
+ if ((s = pw_split_walk(log, "\n", &len, &state)) == NULL)
+ break;
+ fprintf(fp, "%s%.*s\n", prefix, (int)len, s);
+ }
+}
+
+static void log_test_result(struct pwtest_test *t)
+{
+ const struct status *s;
+ const struct status {
+ const char *status;
+ const char *color;
+ } statuses[] = {
+ { "PASS", SPA_ANSI_BOLD_GREEN },
+ { "FAIL", SPA_ANSI_BOLD_RED },
+ { "SKIP", SPA_ANSI_BOLD_YELLOW },
+ { "TIMEOUT", SPA_ANSI_BOLD_CYAN },
+ { "ERROR", SPA_ANSI_BOLD_MAGENTA },
+ };
+
+ spa_assert_se(t->result >= PWTEST_PASS);
+ spa_assert_se(t->result <= PWTEST_SYSTEM_ERROR);
+ s = &statuses[t->result - PWTEST_PASS];
+
+ fprintf(stderr, " status: %s%s%s\n",
+ isatty(STDERR_FILENO) ? s->color : "",
+ s->status,
+ isatty(STDERR_FILENO) ? "\x1B[0m" : "");
+
+ switch (t->result) {
+ case PWTEST_PASS:
+ case PWTEST_SKIP:
+ if (!verbose)
+ return;
+ break;
+ default:
+ break;
+ }
+
+ if (t->sig_or_errno > 0)
+ fprintf(stderr, " signal: %d # SIG%s \n", t->sig_or_errno,
+ sigabbrev_np(t->sig_or_errno));
+ else if (t->sig_or_errno < 0)
+ fprintf(stderr, " errno: %d # %s\n", -t->sig_or_errno,
+ strerror(-t->sig_or_errno));
+ if (t->logs[FD_LOG].size) {
+ fprintf(stderr, " log: |\n");
+ print_lines(stderr, t->logs[FD_LOG].data, " ");
+ }
+ if (t->logs[FD_STDOUT].size) {
+ fprintf(stderr, " stdout: |\n");
+ print_lines(stderr, t->logs[FD_STDOUT].data, " ");
+ }
+ if (t->logs[FD_STDERR].size) {
+ fprintf(stderr, " stderr: |\n");
+ print_lines(stderr, t->logs[FD_STDERR].data, " ");
+ }
+ if (t->logs[FD_DAEMON].size) {
+ fprintf(stderr, " daemon: |\n");
+ print_lines(stderr, t->logs[FD_DAEMON].data, " ");
+ }
+}
+
+static char* make_xdg_runtime_dir(void)
+{
+ time_t t = time(NULL);
+ struct tm *tm = localtime(&t);
+ char *dir;
+ char *tmpdir = getenv("TMPDIR");
+ char path[PATH_MAX];
+ FILE *fp;
+
+ if (!tmpdir)
+ tmpdir = "/tmp";
+
+ int r = asprintf(&dir, "%s/pwtest-%02d:%02d-XXXXXX", tmpdir, tm->tm_hour, tm->tm_min);
+ spa_assert_se((size_t)r == strlen(tmpdir) + 20); /* rough estimate */
+ spa_assert_se(mkdtemp(dir) != NULL);
+
+ /* Marker file to avoid removing a random directory during cleanup */
+ r = spa_scnprintf(path, sizeof(path), "%s/pwtest.dir", dir);
+ spa_assert_se((size_t)r == strlen(dir) + 11);
+ fp = fopen(path, "we");
+ spa_assert_se(fp);
+ fprintf(fp, "pwtest\n");
+ fclose(fp);
+
+ return dir;
+}
+
+static int run_tests(struct pwtest_context *ctx)
+{
+ int r = EXIT_SUCCESS;
+ struct pwtest_suite *c;
+ struct pwtest_test *t;
+
+ fprintf(stderr, "pwtest:\n");
+ spa_list_for_each(c, &ctx->suites, link) {
+ if (c->result != PWTEST_PASS)
+ continue;
+
+ fprintf(stderr, "- suite: \"%s\"\n", c->decl->name);
+ fprintf(stderr, " tests:\n");
+ spa_list_for_each(t, &c->tests, link) {
+ int min = t->args.range.min,
+ max = t->args.range.max;
+ bool have_range = min != 0 || max != 1;
+
+ for (int iteration = min; iteration < max; iteration++) {
+ if (ctx->has_iteration_filter &&
+ ctx->iteration_filter != iteration)
+ continue;
+
+ fprintf(stderr, " - name: \"%s\"\n", t->name);
+ if (have_range)
+ fprintf(stderr, " iteration: %d # %d - %d\n",
+ iteration, min, max);
+ t->iteration = iteration;
+ run_test(ctx, c, t);
+ log_test_result(t);
+
+ switch (t->result) {
+ case PWTEST_PASS:
+ case PWTEST_SKIP:
+ break;
+ default:
+ r = EXIT_FAILURE;
+ break;
+ }
+
+ if (ctx->terminate) {
+ r = EXIT_FAILURE;
+ return r;
+ }
+ }
+ }
+ }
+ return r;
+}
+
+static void list_tests(struct pwtest_context *ctx)
+{
+ struct pwtest_suite *c;
+ struct pwtest_test *t;
+
+ fprintf(stderr, "pwtest:\n");
+ spa_list_for_each(c, &ctx->suites, link) {
+ fprintf(stderr, "- suite: \"%s\"\n", c->decl->name);
+ fprintf(stderr, " tests:\n");
+ spa_list_for_each(t, &c->tests, link) {
+ fprintf(stderr, " - { name: \"%s\" }\n", t->name);
+ }
+ }
+}
+
+static bool is_debugger_attached(void)
+{
+ bool rc = false;
+#ifdef HAVE_LIBCAP
+ int status;
+ int pid = fork();
+
+ if (pid == -1)
+ return 0;
+
+ if (pid == 0) {
+ int ppid = getppid();
+ cap_t caps = cap_get_pid(ppid);
+ cap_flag_value_t cap_val;
+
+ if (cap_get_flag(caps, CAP_SYS_PTRACE, CAP_EFFECTIVE, &cap_val) == -1 ||
+ cap_val != CAP_SET)
+ _exit(false);
+
+ if (ptrace(PTRACE_ATTACH, ppid, NULL, 0) == 0) {
+ waitpid(ppid, NULL, 0);
+ ptrace(PTRACE_CONT, ppid, NULL, 0);
+ ptrace(PTRACE_DETACH, ppid, NULL, 0);
+ rc = false;
+ } else {
+ rc = true;
+ }
+ _exit(rc);
+ } else {
+ waitpid(pid, &status, 0);
+ rc = WEXITSTATUS(status);
+ }
+
+#endif
+ return !!rc;
+}
+
+static void usage(FILE *fp, const char *progname)
+{
+ fprintf(fp, "Usage: %s [OPTIONS]\n"
+ " -h, --help Show this help\n"
+ " --verbose Verbose output\n"
+ " --list List all available suites and tests\n"
+ " --timeout=N Set the test timeout to N seconds (default: 15)\n"
+ " --filter-test=glob Run only tests matching the given glob\n"
+ " --filter-suites=glob Run only suites matching the given glob\n"
+ " --filter-iteration=N Run only iteration N\n"
+ " --no-fork Do not fork for the test (see note below)\n"
+ "\n"
+ "Using --no-fork allows for easy debugging of tests but should only be used.\n"
+ "used with --filter-test. A test that modifies the process state will affect\n"
+ "subsequent tests and invalidate test results.\n",
+ progname);
+}
+
+static void sigterm_handler(int signo)
+{
+ terminate_cleanup_pids(ctx);
+ ctx->terminate = true;
+ if (ctx->no_fork) {
+ signal(SIGTERM, SIG_DFL);
+ signal(SIGINT, SIG_DFL);
+ raise(signo);
+ }
+}
+
+int main(int argc, char **argv)
+{
+ int r = EXIT_SUCCESS;
+ enum {
+ OPT_TIMEOUT = 10,
+ OPT_LIST,
+ OPT_VERBOSE,
+ OPT_FILTER_TEST,
+ OPT_FILTER_SUITE,
+ OPT_FILTER_ITERATION,
+ OPT_NOFORK,
+ };
+ static const struct option opts[] = {
+ { "help", no_argument, 0, 'h' },
+ { "timeout", required_argument, 0, OPT_TIMEOUT },
+ { "list", no_argument, 0, OPT_LIST },
+ { "filter-test", required_argument, 0, OPT_FILTER_TEST },
+ { "filter-suite", required_argument, 0, OPT_FILTER_SUITE },
+ { "filter-iteration", required_argument, 0, OPT_FILTER_ITERATION },
+ { "list", no_argument, 0, OPT_LIST },
+ { "verbose", no_argument, 0, OPT_VERBOSE },
+ { "no-fork", no_argument, 0, OPT_NOFORK },
+ { NULL },
+ };
+ struct pwtest_context test_ctx = {
+ .suites = SPA_LIST_INIT(&test_ctx.suites),
+ .timeout = 15,
+ .has_iteration_filter = false,
+ };
+ enum {
+ MODE_TEST,
+ MODE_LIST,
+ } mode = MODE_TEST;
+ const char *suite_filter = NULL;
+
+ spa_list_init(&test_ctx.cleanup_pids);
+
+ ctx = &test_ctx;
+
+ while (1) {
+ int c;
+ int option_index = 0;
+
+ c = getopt_long(argc, argv, "h", opts, &option_index);
+ if (c == -1)
+ break;
+ switch(c) {
+ case 'h':
+ usage(stdout, argv[0]);
+ exit(EXIT_SUCCESS);
+ case OPT_TIMEOUT:
+ ctx->timeout = atoi(optarg);
+ break;
+ case OPT_LIST:
+ mode = MODE_LIST;
+ break;
+ case OPT_VERBOSE:
+ verbose = true;
+ break;
+ case OPT_FILTER_TEST:
+ ctx->test_filter = optarg;
+ break;
+ case OPT_FILTER_SUITE:
+ suite_filter= optarg;
+ break;
+ case OPT_FILTER_ITERATION:
+ ctx->has_iteration_filter = spa_atoi32(optarg, &ctx->iteration_filter, 10);
+ break;
+ case OPT_NOFORK:
+ ctx->no_fork = true;
+ break;
+ default:
+ usage(stderr, argv[0]);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ if (RUNNING_ON_VALGRIND || is_debugger_attached())
+ ctx->no_fork = true;
+
+ find_suites(ctx, suite_filter);
+ add_tests(ctx);
+
+ if (getenv("TMPDIR") == NULL)
+ setenv("TMPDIR", "/tmp", 1);
+
+ ctx->xdg_dir = make_xdg_runtime_dir();
+
+ switch (mode) {
+ case MODE_LIST:
+ list_tests(ctx);
+ break;
+ case MODE_TEST:
+ setrlimit(RLIMIT_CORE, &((struct rlimit){0, 0}));
+ signal(SIGTERM, sigterm_handler);
+ signal(SIGINT, sigterm_handler);
+ r = run_tests(ctx);
+ break;
+ }
+
+ cleanup(ctx);
+
+ return r;
+}
diff --git a/test/pwtest.h b/test/pwtest.h
new file mode 100644
index 0000000..6d3070b
--- /dev/null
+++ b/test/pwtest.h
@@ -0,0 +1,577 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#ifndef PWTEST_H
+#define PWTEST_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <limits.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <math.h>
+
+#include <spa/utils/string.h>
+#include <spa/utils/dict.h>
+#include "spa/support/plugin.h"
+
+/**
+ * \defgroup pwtest Test Suite
+ * \brief `pwtest` is a test runner framework for PipeWire.
+ *
+ * It's modelled after other
+ * test suites like [check](https://libcheck.github.io/check/)
+ * and draws a lot of inspiration from the [libinput test
+ * suite](https://wayland.freedesktop.org/libinput/doc/latest/).
+ *
+ * `pwtest` captures logs from the tests (and the pipewire daemon, if
+ * applicable) and collects the output into YAML files printed to `stderr`.
+ *
+ * ## Tests
+ *
+ * A `pwtest` test is declared with the `PWTEST()` macro and must return one of
+ * the `pwtest` status codes. Those codes are:
+ * - \ref PWTEST_PASS for a successful test
+ * - \ref PWTEST_FAIL for a test case failure. Usually you should not return this
+ * value but rely on the `pwtest` macros to handle this case.
+ * - \ref PWTEST_SKIP to skip the current test
+ * - \ref PWTEST_SYSTEM_ERROR in case of an error that would cause the test to not run properly. This is not a test case failure but some required precondition not being met.
+ *
+ * ```c
+ * #include "pwtest.h"
+ *
+ * PWTEST(some_test)
+ * {
+ * int var = 10;
+ * const char *str = "foo";
+ *
+ * if (access("/", R_OK))
+ * pwtest_error_with_message("Oh dear, no root directory?");
+ *
+ * if (today_is_monday)
+ * return PWTEST_SKIP;
+ *
+ * pwtest_int_lt(var, 20);
+ * pwtest_ptr_notnull(&var);
+ * pwtest_str_ne(str, "bar");
+ *
+ * return PWTEST_PASS;
+ * }
+ * ...
+ * ```
+ *
+ * `pwtest` provides comparison macros for most basic data types with the `lt`,
+ * `le`, `eq`, `gt`, `ge` suffixes (`<, <=, ==, >, >=`). Tests usually should not
+ * return `PWTEST_FAIL` directly, use the `pwtest_fail()` macros if .
+ *
+ * By default, a test runs in a forked process, any changes to the
+ * process'environment, etc. are discarded in the next test.
+ *
+ * ## Suites
+ *
+ * Tests are grouped into suites and declared with the PWTEST_SUITE() macro.
+ * Each test must be added with the required arguments, it is acceptable to
+ * add the same test multiple times with different arguments.
+ *
+ * ```c
+ * ...
+ * PWTEST_SUITE(misc)
+ * {
+ * if (today_is_monday)
+ * return PWTEST_SKIP;
+ *
+ * // simple test
+ * pwtest_add(some_test, PWTEST_NOARG);
+ * // starts with its own pipewire daemon instance
+ * pwtest_add(some_test, PWTEST_ARG_DAEMON);
+ *
+ * return PWTEST_SUCCESS;
+ * }
+ * ```
+ * For a list of potential arguments, see \ref pwtest_arg and the
+ * `test-examples.c` file in the source directory.
+ *
+ * Suites are auto-discovered, they do not have to be manually added to a test run.
+ *
+ * ## Running tests
+ *
+ * The `pwtest` framework is built into each test binary, so just execute the
+ * matching binary. See the `--help` output for the full argument list.
+ *
+ * The most useful arguments when running the test suite:
+ * - `--verbose` to enable logs even when tests pass or are skipped
+ * - `--filter-test=glob`, `--filter-suite=glob` uses an `fnmatch()` glob to limit which tests or suites are run
+ * - `--no-fork` - see "Debugging test-case failures"
+ *
+ * ## Debugging test-case failures
+ *
+ * To debug a single test, disable forking and run the test through gdb:
+ *
+ * ```
+ * $ gdb path/to/test
+ * (gdb) break test_function_name
+ * Breakpoint 1 at 0xfffffffffffff: file ../test/test-file.c, line 123
+ * (gdb) r --no-fork --filter-test=test_function_name
+ * ```
+ * Disabling forking makes it easy to debug but should always be used with
+ * `--filter-test`. Any test that modifies its environment will affect
+ * subsequent tests and may invalidate the test results.
+ *
+ * Where a test has multiple iterations, use `--filter-iteration` to only run
+ * one single iteration.
+ */
+
+/**
+ * \addtogroup pwtest
+ * \{
+ */
+
+
+/** \struct pwtest_context */
+struct pwtest_context;
+/** \struct pwtest_suite */
+struct pwtest_suite;
+/** \struct pwtest_test */
+struct pwtest_test;
+
+#include "pwtest-implementation.h"
+
+/**
+ * Result returned from tests or suites.
+ */
+enum pwtest_result {
+ PWTEST_PASS = 75, /**< test successful */
+ PWTEST_FAIL = 76, /**< test failed. Should not be returned directly,
+ Use the pwtest_ macros instead */
+ PWTEST_SKIP = 77, /**< test was skipped */
+ PWTEST_TIMEOUT = 78, /**< test aborted after timeout */
+ PWTEST_SYSTEM_ERROR = 79, /**< unrelated error occurred */
+};
+
+/**
+ * If the test was added with a range (see \ref PWTEST_ARG_RANGE), this
+ * function returns the current iteration within that range. Otherwise, this
+ * function returns zero.
+ */
+int pwtest_get_iteration(struct pwtest_test *t);
+
+/**
+ * If the test had properties set (see \ref PWTEST_ARG_PROP), this function
+ * returns the \ref pw_properties. Otherwise, this function returns NULL.
+ */
+struct pw_properties *pwtest_get_props(struct pwtest_test *t);
+
+struct pwtest_context *pwtest_get_context(struct pwtest_test *t);
+
+/** Fail the current test */
+#define pwtest_fail() \
+ _pwtest_fail_condition(PWTEST_FAIL, __FILE__, __LINE__, __func__, "aborting", "")
+
+/** Same as above but more expressive in the code */
+#define pwtest_fail_if_reached() \
+ _pwtest_fail_condition(PWTEST_FAIL, __FILE__, __LINE__, __func__, "This line is supposed to be unreachable", "")
+
+/** Fail the current test with the given message */
+#define pwtest_fail_with_msg(...) \
+ _pwtest_fail_condition(PWTEST_FAIL, __FILE__, __LINE__, __func__, \
+ "aborting", __VA_ARGS__)
+
+/** Error out of the current test with the given message */
+#define pwtest_error_with_msg(...) \
+ _pwtest_fail_condition(PWTEST_SYSTEM_ERROR, __FILE__, __LINE__, __func__, \
+ "error", __VA_ARGS__)
+
+/** Assert r is not -1 and if it is, print the errno */
+#define pwtest_errno_ok(r_) \
+ pwtest_errno_check(r_, 0);
+
+/** Assert r is -1 and the errno is the given one */
+#define pwtest_errno(r_, errno_) \
+ pwtest_errno_check(r_, errno_);
+
+/** Assert r is not < 0 and if it is assume it's a negative errno */
+#define pwtest_neg_errno_ok(r_) \
+ pwtest_neg_errno_check(r_, 0);
+
+/** Assert r is < 0 and the given negative errno */
+#define pwtest_neg_errno(r_, errno_) \
+ pwtest_neg_errno_check(r_, errno_);
+
+/** Assert boolean (a == b) */
+#define pwtest_bool_eq(a_, b_) \
+ pwtest_comparison_bool_(a_, ==, b_)
+
+/** Assert boolean (a != b) */
+#define pwtest_bool_ne(a_, b_) \
+ pwtest_comparison_bool_(a_, !=, b_)
+
+/** Assert cond to be true. Convenience wrapper for readability */
+#define pwtest_bool_true(cond_) \
+ pwtest_comparison_bool_(cond_, ==, true)
+
+/** Assert cond to be false. Convenience wrapper for readability */
+#define pwtest_bool_false(cond_) \
+ pwtest_comparison_bool_(cond_, ==, false)
+
+/** Assert a == b */
+#define pwtest_int_eq(a_, b_) \
+ pwtest_comparison_int_(a_, ==, b_)
+
+/** Assert a != b */
+#define pwtest_int_ne(a_, b_) \
+ pwtest_comparison_int_(a_, !=, b_)
+
+/** Assert a < b */
+#define pwtest_int_lt(a_, b_) \
+ pwtest_comparison_int_(a_, <, b_)
+
+/** Assert a <= b */
+#define pwtest_int_le(a_, b_) \
+ pwtest_comparison_int_(a_, <=, b_)
+
+/** Assert a >= b */
+#define pwtest_int_ge(a_, b_) \
+ pwtest_comparison_int_(a_, >=, b_)
+
+/** Assert a > b */
+#define pwtest_int_gt(a_, b_) \
+ pwtest_comparison_int_(a_, >, b_)
+
+/** Assert ptr1 == ptr2 */
+#define pwtest_ptr_eq(a_, b_) \
+ pwtest_comparison_ptr_(a_, ==, b_)
+
+/** Assert ptr1 != ptr2 */
+#define pwtest_ptr_ne(a_, b_) \
+ pwtest_comparison_ptr_(a_, !=, b_)
+
+/** Assert ptr == NULL */
+#define pwtest_ptr_null(a_) \
+ pwtest_comparison_ptr_(a_, ==, NULL)
+
+/** Assert ptr != NULL */
+#define pwtest_ptr_notnull(a_) \
+ pwtest_comparison_ptr_(a_, !=, NULL)
+
+/** Assert a == b for a (hardcoded) epsilon */
+#define pwtest_double_eq(a_, b_)\
+ pwtest_comparison_double_((a_), ==, (b_))
+
+/** Assert a != b for a (hardcoded) epsilon */
+#define pwtest_double_ne(a_, b_)\
+ pwtest_comparison_double_((a_), !=, (b_))
+
+/** Assert a < b for a (hardcoded) epsilon */
+#define pwtest_double_lt(a_, b_)\
+ pwtest_comparison_double_((a_), <, (b_))
+
+/** Assert a <= b for a (hardcoded) epsilon */
+#define pwtest_double_le(a_, b_)\
+ pwtest_comparison_double_((a_), <=, (b_))
+
+/** Assert a >= b for a (hardcoded) epsilon */
+#define pwtest_double_ge(a_, b_)\
+ pwtest_comparison_double_((a_), >=, (b_))
+
+/** Assert a > b for a (hardcoded) epsilon */
+#define pwtest_double_gt(a_, b_)\
+ pwtest_comparison_double_((a_), >, (b_))
+
+#define pwtest_int(a_, op_, b_) \
+ pwtest_comparison_int_(a_, op_, b_)
+
+
+/** Assert str1 is equal to str2 */
+#define pwtest_str_eq(a_, b_) \
+ do { \
+ const char *_a = a_; \
+ const char *_b = b_; \
+ if (!spa_streq(_a, _b)) \
+ _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \
+ #a_ " equals " #b_, _a, _b); \
+ } while(0)
+
+/** Assert str1 is equal to str2 for l characters */
+#define pwtest_str_eq_n(a_, b_, l_) \
+ do { \
+ const char *_a = a_; \
+ const char *_b = b_; \
+ if (!spa_strneq(_a, _b, l_)) \
+ _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \
+ #a_ " equals " #b_ ", len: " #l_, _a, _b); \
+ } while(0)
+
+/** Assert str1 is not equal to str2 */
+#define pwtest_str_ne(a_, b_) \
+ do { \
+ const char *_a = a_; \
+ const char *_b = b_; \
+ if (spa_streq(_a, _b)) \
+ _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \
+ #a_ " not equal to " #b_, _a, _b); \
+ } while(0)
+
+/** Assert str1 is not equal to str2 for l characters */
+#define pwtest_str_ne_n(a_, b_, l_) \
+ do { \
+ __typeof__(a_) _a = a_; \
+ __typeof__(b_) _b = b_; \
+ if (spa_strneq(_a, _b, l_)) \
+ _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \
+ #a_ " not equal to " #b_ ", len: " #l_, _a, _b); \
+ } while(0)
+
+
+/** Assert haystack contains needle */
+#define pwtest_str_contains(haystack_, needle_) \
+ do { \
+ const char *_h = haystack_; \
+ const char *_n = needle_; \
+ if (!strstr(_h, _n)) \
+ _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \
+ #haystack_ " contains " #needle_, _h, _n); \
+ } while(0)
+
+
+/* Needs to be a #define NULL for SPA_SENTINEL */
+enum pwtest_arg {
+ PWTEST_NOARG = 0,
+ /**
+ * The next argument is an int specifying the numerical signal number.
+ * The test is expected to raise that signal. The test fails if none
+ * or any other signal is raised.
+ *
+ * Example:
+ * ```c
+ * pwtest_add(mytest, PWTEST_ARG_SIGNAL, SIGABRT);
+ * ```
+ */
+ PWTEST_ARG_SIGNAL,
+ /**
+ * The next two int arguments are the minimum (inclusive) and
+ * maximum (exclusive) range for this test.
+ *
+ * Example:
+ * ```c
+ * pwtest_add(mytest, PWTEST_ARG_RANGE, -50, 50);
+ * ```
+ * Use pwtest_get_iteration() in the test function to obtain the current iteration.
+ */
+ PWTEST_ARG_RANGE,
+ /**
+ * The next two const char * arguments are the key and value
+ * for a property entry. This argument may be specified multiple times
+ * to add multiple properties.
+ *
+ * Use pwtest_get_props() to get the properties within the test function.
+ *
+ * Example:
+ * ```c
+ * pwtest_add(mytest,
+ * PWTEST_ARG_PROP, "key1", "value1",
+ * PWTEST_ARG_PROP, "key2", "value2");
+ * ```
+ */
+ PWTEST_ARG_PROP,
+ /**
+ * The next two const char * arguments are the key and value
+ * for the environment variable to be set in the test. This argument
+ * may be specified multiple times to add multiple environment
+ * variables.
+ *
+ * Example:
+ * ```c
+ * pwtest_add(mytest,
+ * PWTEST_ARG_ENV, "env1", "value1",
+ * PWTEST_ARG_ENV, "env2", "value2");
+ * ```
+ *
+ * These environment variables are only set for the test itself, a
+ * a pipewire daemon started with \ref PWTEST_ARG_DAEMON does not share
+ * those variables.
+ *
+ */
+ PWTEST_ARG_ENV,
+ /**
+ * Takes no extra arguments. If provided, the test case will start a
+ * pipewire daemon and stop the daemon when finished.
+ *
+ * The `PIPEWIRE_REMOTE` environment variable will be set in the
+ * test to point to this daemon.
+ *
+ * Example:
+ * ```c
+ * pwtest_add(mytest, PWTEST_ARG_DAEMON);
+ * ```
+ *
+ * Environment variables specified with \ref PWTEST_ARG_ENV are
+ * **not** available to the daemon, only to the test itself.
+ */
+ PWTEST_ARG_DAEMON,
+};
+/**
+ * Add function \a func_ to the current test suite.
+ *
+ * This macro should be used within PWTEST_SUITE() to register the test in that suite, for example:
+ *
+ * ```c
+ * PWTEST_SUITE(mysuite)
+ * {
+ * pwtest_add(test1, PWTEST_NOARG);
+ * pwtest_add(test2, PWTEST_ARG_DAEMON);
+ * pwtest_add(test3, PWTEST_ARG_RANGE, 0, 100, PWTEST_ARG_DAEMON);
+ * }
+ *
+ * ```
+ *
+ * If the test matches the given filters and the suite is executed, the test
+ * will be executed with the parameters given to pwtest_add().
+ *
+ * Arguments take a argument-dependent number of extra parameters, see
+ * see the \ref pwtest_arg documentation for details.
+ */
+#define pwtest_add(func_, ...) \
+ _pwtest_add(ctx, suite, #func_, func_, __VA_ARGS__, NULL)
+
+
+/**
+ * Declare a test case. To execute the test, add the test case name with pwtest_add().
+ *
+ * This macro expands so each test has a struct \ref pwtest_test variable
+ * named `current_test` available.
+ *
+ * ```c
+ * PWTEST(mytest)
+ * {
+ * struct pwtest_test *t = current_test;
+ *
+ * ... do stuff ...
+ *
+ * return PWTEST_PASS;
+ * }
+ *
+ * PWTEST_SUITE(mysuite)
+ * {
+ * pwtest_add(mytest);
+ *
+ * return PWTEST_PASS;
+ * }
+ * ```
+ */
+#define PWTEST(tname) \
+ static enum pwtest_result tname(struct pwtest_test *current_test)
+
+/**
+ * Initialize a test suite. A test suite is a group of related
+ * tests that filters and other conditions may apply to.
+ *
+ * Test suites are automatically discovered at build-time.
+ */
+#define PWTEST_SUITE(cname) \
+ static enum pwtest_result (cname##__setup)(struct pwtest_context *ctx, struct pwtest_suite *suite); \
+ __attribute__((used)) \
+ __attribute__((retain)) \
+ __attribute__((section("pwtest_suite_section"))) \
+ __attribute__((aligned(__alignof__(struct pwtest_suite_decl)))) \
+ static const struct pwtest_suite_decl _test_suite = { \
+ .name = #cname, \
+ .setup = cname##__setup, \
+ }; \
+ static enum pwtest_result (cname##__setup)(struct pwtest_context *ctx, struct pwtest_suite *suite)
+
+struct pwtest_spa_plugin {
+#define PWTEST_PLUGIN_MAX 32
+ size_t nsupport;
+ struct spa_support support[PWTEST_PLUGIN_MAX];
+
+ size_t ndlls;
+ void *dlls[PWTEST_PLUGIN_MAX];
+
+ size_t nhandles;
+ struct spa_handle *handles[PWTEST_PLUGIN_MAX];
+};
+
+struct pwtest_spa_plugin* pwtest_spa_plugin_new(void);
+void pwtest_spa_plugin_destroy(struct pwtest_spa_plugin *plugin);
+
+/**
+ * Identical to pwtest_spa_plugin_try_load_interface() but returns the
+ * interface and fails if the interface is NULL.
+ */
+void*
+pwtest_spa_plugin_load_interface(struct pwtest_spa_plugin *plugin,
+ const char *libname,
+ const char *factory_name,
+ const char *interface_name,
+ const struct spa_dict *info);
+
+/**
+ * Load \a interface_name from the factory in \a libname.
+ * If successful, the interface is returned and added to \a plugin's
+ * support items, i.e. subsequent loads of an interface will be able to
+ * make use of previously loaded ones.
+ *
+ * \return 0 on success or a negative errno on error
+ * \retval -ENOENT \a libname does not exist
+ * \retval -EINVAL \a factory_name does not exist in \a libname
+ * \retval -ENOSYS \a interface_name does not exist in \a factory_name
+ */
+int
+pwtest_spa_plugin_try_load_interface(struct pwtest_spa_plugin *plugin,
+ void **iface_return,
+ const char *libname,
+ const char *factory_name,
+ const char *interface_name,
+ const struct spa_dict *info);
+
+
+
+/**
+ * Create a temporary file and copy its full path to \a path. Fails the test
+ * with \ref PWTEST_SYSTEM_ERROR on error.
+ *
+ * This file does not need to be removed by the test, the pwtest framework
+ * will take care of it on exit.
+ */
+void pwtest_mkstemp(char path[PATH_MAX]);
+
+/**
+ * Run a command and wait for it to return.
+ */
+int pwtest_spawn(const char *file, char *const argv[]);
+
+
+/**
+ * \}
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PWTEST_H */
diff --git a/test/test-array.c b/test/test-array.c
new file mode 100644
index 0000000..1d9ea24
--- /dev/null
+++ b/test/test-array.c
@@ -0,0 +1,146 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#include "config.h"
+
+#include "pwtest.h"
+#include "pipewire/array.h"
+
+PWTEST(array_test_abi)
+{
+ /* array */
+#if defined(__x86_64__) && defined(__LP64__)
+ pwtest_int_eq(sizeof(struct pw_array), 32U);
+ return PWTEST_PASS;
+#else
+ fprintf(stderr, "Unknown arch: pw_array is size %zd\n", sizeof(struct pw_array));
+ return PWTEST_SKIP;
+#endif
+}
+
+PWTEST(array_test)
+{
+ struct pw_array arr;
+ uint32_t *ptr;
+ uint32_t vals[] = { 0, 100, 0x8a, 0 };
+ size_t i;
+
+ pw_array_init(&arr, 64);
+ pwtest_int_eq(SPA_N_ELEMENTS(vals), 4U);
+
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 0U);
+ pwtest_bool_false(pw_array_check_index(&arr, 0, uint32_t));
+ pwtest_ptr_eq(pw_array_first(&arr), pw_array_end(&arr));
+ pw_array_for_each(ptr, &arr)
+ pwtest_fail_if_reached();
+
+ for (i = 0; i < 4; i++) {
+ ptr = (uint32_t*)pw_array_add(&arr, sizeof(uint32_t));
+ *ptr = vals[i];
+ }
+
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 4U);
+ pwtest_bool_true(pw_array_check_index(&arr, 2, uint32_t));
+ pwtest_bool_true(pw_array_check_index(&arr, 3, uint32_t));
+ pwtest_bool_false(pw_array_check_index(&arr, 4, uint32_t));
+
+ i = 0;
+ pw_array_for_each(ptr, &arr) {
+ pwtest_int_eq(*ptr, vals[i++]);
+ }
+
+ /* remove second */
+ ptr = pw_array_get_unchecked(&arr, 2, uint32_t);
+ pwtest_ptr_notnull(ptr);
+ pw_array_remove(&arr, ptr);
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 3U);
+ pwtest_bool_false(pw_array_check_index(&arr, 3, uint32_t));
+ ptr = pw_array_get_unchecked(&arr, 2, uint32_t);
+ pwtest_ptr_notnull(ptr);
+ pwtest_int_eq(*ptr, vals[3]);
+
+ /* remove first */
+ ptr = pw_array_get_unchecked(&arr, 0, uint32_t);
+ pwtest_ptr_notnull(ptr);
+ pw_array_remove(&arr, ptr);
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 2U);
+ ptr = pw_array_get_unchecked(&arr, 0, uint32_t);
+ pwtest_ptr_notnull(ptr);
+ pwtest_int_eq(*ptr, vals[1]);
+
+ /* iterate */
+ ptr = (uint32_t*)pw_array_first(&arr);
+ pwtest_bool_true(pw_array_check(&arr, ptr));
+ pwtest_int_eq(*ptr, vals[1]);
+ ptr++;
+ pwtest_bool_true(pw_array_check(&arr, ptr));
+ pwtest_int_eq(*ptr, vals[3]);
+ ptr++;
+ pwtest_bool_false(pw_array_check(&arr, ptr));
+
+ pw_array_reset(&arr);
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 0U);
+
+ pw_array_clear(&arr);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(array_clear)
+{
+ struct pw_array arr;
+ uint32_t *ptr;
+ uint32_t vals[] = { 0, 100, 0x8a, 0 };
+ size_t i;
+
+ pw_array_init(&arr, 64);
+
+ for (i = 0; i < 4; i++) {
+ ptr = (uint32_t*)pw_array_add(&arr, sizeof(uint32_t));
+ *ptr = vals[i];
+ }
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 4U);
+ pw_array_clear(&arr);
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 0U);
+
+ for (i = 0; i < 4; i++) {
+ ptr = (uint32_t*)pw_array_add(&arr, sizeof(uint32_t));
+ *ptr = vals[i];
+ }
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 4U);
+ pw_array_clear(&arr);
+ pwtest_int_eq(pw_array_get_len(&arr, uint32_t), 0U);
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(pw_array)
+{
+ pwtest_add(array_test_abi, PWTEST_NOARG);
+ pwtest_add(array_test, PWTEST_NOARG);
+ pwtest_add(array_clear, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-client.c b/test/test-client.c
new file mode 100644
index 0000000..f912dc9
--- /dev/null
+++ b/test/test-client.c
@@ -0,0 +1,70 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pwtest.h"
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl-client.h>
+
+#define TEST_FUNC(a,b,func) \
+do { \
+ a.func = b.func; \
+ pwtest_ptr_eq(SPA_PTRDIFF(&a.func, &a), SPA_PTRDIFF(&b.func, &b)); \
+} while(0)
+
+PWTEST(client_abi)
+{
+ static const struct {
+ uint32_t version;
+ void (*destroy) (void *data);
+ void (*free) (void *data);
+ void (*initialized) (void *data);
+ void (*info_changed) (void *data, const struct pw_client_info *info);
+ void (*resource_added) (void *data, struct pw_resource *resource);
+ void (*resource_removed) (void *data, struct pw_resource *resource);
+ void (*busy_changed) (void *data, bool busy);
+ } test = { PW_VERSION_IMPL_CLIENT_EVENTS, NULL };
+
+ struct pw_impl_client_events ev;
+
+ TEST_FUNC(ev, test, destroy);
+ TEST_FUNC(ev, test, free);
+ TEST_FUNC(ev, test, initialized);
+ TEST_FUNC(ev, test, info_changed);
+ TEST_FUNC(ev, test, resource_added);
+ TEST_FUNC(ev, test, resource_removed);
+ TEST_FUNC(ev, test, busy_changed);
+
+ pwtest_int_eq(PW_VERSION_IMPL_CLIENT_EVENTS, 0);
+ pwtest_int_eq(sizeof(ev), sizeof(test));
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(client)
+{
+ pwtest_add(client_abi, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-config.c b/test/test-config.c
new file mode 100644
index 0000000..9ebbc26
--- /dev/null
+++ b/test/test-config.c
@@ -0,0 +1,102 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pwtest.h"
+
+#include <pipewire/conf.h>
+
+PWTEST(config_load_abspath)
+{
+ char path[PATH_MAX];
+ int r;
+ FILE *fp;
+ struct pw_properties *props;
+ char *basename;
+
+ pwtest_mkstemp(path);
+ fp = fopen(path, "we");
+ fputs("data = x", fp);
+ fclose(fp);
+
+ /* Load with NULL prefix and abs path */
+ props = pw_properties_new("ignore", "me", NULL);
+ r = pw_conf_load_conf(NULL, path, props);
+ pwtest_neg_errno_ok(r);
+ pwtest_str_eq(pw_properties_get(props, "data"), "x");
+ pw_properties_free(props);
+
+#if 0
+ /* Load with non-NULL abs prefix and abs path */
+ props = pw_properties_new("ignore", "me", NULL);
+ r = pw_conf_load_conf("/dummy", path, props);
+ pwtest_neg_errno_ok(r);
+ pwtest_str_eq(pw_properties_get(props, "data"), "x");
+ pw_properties_free(props);
+
+ /* Load with non-NULL relative prefix and abs path */
+ props = pw_properties_new("ignore", "me", NULL);
+ r = pw_conf_load_conf("dummy", path, props);
+ pwtest_neg_errno_ok(r);
+ pwtest_str_eq(pw_properties_get(props, "data"), "x");
+ pw_properties_free(props);
+#endif
+
+ /* Load with non-NULL abs prefix and relative path */
+ basename = rindex(path, '/'); /* basename(3) and dirname(3) are terrible */
+ pwtest_ptr_notnull(basename);
+ *basename = '\0';
+ basename++;
+
+ props = pw_properties_new("ignore", "me", NULL);
+ r = pw_conf_load_conf(path, basename, props);
+ pwtest_neg_errno_ok(r);
+ pwtest_str_eq(pw_properties_get(props, "data"), "x");
+ pw_properties_free(props);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(config_load_nullname)
+{
+ struct pw_properties *props = pw_properties_new("ignore", "me", NULL);
+ int r;
+
+ r = pw_conf_load_conf(NULL, NULL, props);
+ pwtest_neg_errno(r, -EINVAL);
+
+ r = pw_conf_load_conf("/dummy", NULL, props);
+ pwtest_neg_errno(r, -EINVAL);
+
+ pw_properties_free(props);
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(context)
+{
+ pwtest_add(config_load_abspath, PWTEST_NOARG);
+ pwtest_add(config_load_nullname, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-context.c b/test/test-context.c
new file mode 100644
index 0000000..3625d35
--- /dev/null
+++ b/test/test-context.c
@@ -0,0 +1,277 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pwtest.h"
+
+#include <spa/utils/string.h>
+#include <spa/support/dbus.h>
+#include <spa/support/cpu.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/global.h>
+
+#define TEST_FUNC(a,b,func) \
+do { \
+ a.func = b.func; \
+ pwtest_ptr_eq(SPA_PTRDIFF(&a.func, &a), SPA_PTRDIFF(&b.func, &b)); \
+} while(0)
+
+PWTEST(context_abi)
+{
+ struct pw_context_events ev;
+ struct {
+ uint32_t version;
+ void (*destroy) (void *data);
+ void (*free) (void *data);
+ void (*check_access) (void *data, struct pw_impl_client *client);
+ void (*global_added) (void *data, struct pw_global *global);
+ void (*global_removed) (void *data, struct pw_global *global);
+ } test = { PW_VERSION_CONTEXT_EVENTS, NULL };
+
+ pw_init(0, NULL);
+
+ TEST_FUNC(ev, test, destroy);
+ TEST_FUNC(ev, test, free);
+ TEST_FUNC(ev, test, check_access);
+ TEST_FUNC(ev, test, global_added);
+ TEST_FUNC(ev, test, global_removed);
+
+ pwtest_int_eq(PW_VERSION_CONTEXT_EVENTS, 0);
+ pwtest_int_eq(sizeof(ev), sizeof(test));
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+static void context_destroy_error(void *data)
+{
+ pwtest_fail_if_reached();
+}
+static void context_free_error(void *data)
+{
+ pwtest_fail_if_reached();
+}
+static void context_check_access_error(void *data, struct pw_impl_client *client)
+{
+ pwtest_fail_if_reached();
+}
+static void context_global_added_error(void *data, struct pw_global *global)
+{
+ pwtest_fail_if_reached();
+}
+static void context_global_removed_error(void *data, struct pw_global *global)
+{
+ pwtest_fail_if_reached();
+}
+
+static const struct pw_context_events context_events_error =
+{
+ PW_VERSION_CONTEXT_EVENTS,
+ .destroy = context_destroy_error,
+ .free = context_free_error,
+ .check_access = context_check_access_error,
+ .global_added = context_global_added_error,
+ .global_removed = context_global_removed_error,
+};
+
+static int destroy_count = 0;
+static void context_destroy_count(void *data)
+{
+ destroy_count++;
+}
+static int free_count = 0;
+static void context_free_count(void *data)
+{
+ free_count++;
+}
+static int global_removed_count = 0;
+static void context_global_removed_count(void *data, struct pw_global *global)
+{
+ global_removed_count++;
+}
+static int context_foreach_count = 0;
+static int context_foreach(void *data, struct pw_global *global)
+{
+ context_foreach_count++;
+ return 0;
+}
+static int context_foreach_error(void *data, struct pw_global *global)
+{
+ context_foreach_count++;
+ return -1;
+}
+PWTEST(context_create)
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct spa_hook listener = { { NULL }, };
+ struct pw_context_events context_events = context_events_error;
+ int res;
+
+ pw_init(0, NULL);
+
+ loop = pw_main_loop_new(NULL);
+ pwtest_ptr_notnull(loop);
+
+ context = pw_context_new(pw_main_loop_get_loop(loop),
+ pw_properties_new(
+ PW_KEY_CONFIG_NAME, "null",
+ NULL), 12);
+ pwtest_ptr_notnull(context);
+ pw_context_add_listener(context, &listener, &context_events, context);
+
+ /* check main loop */
+ pwtest_ptr_eq(pw_context_get_main_loop(context), pw_main_loop_get_loop(loop));
+ /* check user data */
+ pwtest_ptr_notnull(pw_context_get_user_data(context));
+
+ /* iterate globals */
+ pwtest_int_eq(context_foreach_count, 0);
+ res = pw_context_for_each_global(context, context_foreach, context);
+ pwtest_int_eq(res, 0);
+ pwtest_int_eq(context_foreach_count, 2);
+ res = pw_context_for_each_global(context, context_foreach_error, context);
+ pwtest_int_eq(res, -1);
+ pwtest_int_eq(context_foreach_count, 3);
+
+ /* check destroy */
+ context_events.destroy = context_destroy_count;
+ context_events.free = context_free_count;
+ context_events.global_removed = context_global_removed_count;
+
+ pwtest_int_eq(destroy_count, 0);
+ pwtest_int_eq(free_count, 0);
+ pwtest_int_eq(global_removed_count, 0);
+ pw_context_destroy(context);
+ pwtest_int_eq(destroy_count, 1);
+ pwtest_int_eq(free_count, 1);
+ pwtest_int_eq(global_removed_count, 2);
+ pw_main_loop_destroy(loop);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+PWTEST(context_properties)
+{
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ const struct pw_properties *props;
+ struct spa_hook listener = { { NULL }, };
+ struct pw_context_events context_events = context_events_error;
+ struct spa_dict_item items[3];
+
+ pw_init(0, NULL);
+
+ loop = pw_main_loop_new(NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop),
+ pw_properties_new("foo", "bar",
+ "biz", "fuzz",
+ NULL),
+ 0);
+ pwtest_ptr_notnull(context);
+ pwtest_ptr_null(pw_context_get_user_data(context));
+ pw_context_add_listener(context, &listener, &context_events, context);
+
+ props = pw_context_get_properties(context);
+ pwtest_ptr_notnull(props);
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "biz"), "fuzz");
+ pwtest_str_eq(pw_properties_get(props, "buzz"), NULL);
+
+ /* remove foo */
+ items[0] = SPA_DICT_ITEM_INIT("foo", NULL);
+ /* change biz */
+ items[1] = SPA_DICT_ITEM_INIT("biz", "buzz");
+ /* add buzz */
+ items[2] = SPA_DICT_ITEM_INIT("buzz", "frizz");
+ pw_context_update_properties(context, &SPA_DICT_INIT(items, 3));
+
+ pwtest_ptr_eq(props, pw_context_get_properties(context));
+ pwtest_str_eq(pw_properties_get(props, "foo"), NULL);
+ pwtest_str_eq(pw_properties_get(props, "biz"), "buzz");
+ pwtest_str_eq(pw_properties_get(props, "buzz"), "frizz");
+
+ spa_hook_remove(&listener);
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+PWTEST(context_support)
+{
+ static const char * const types[] = {
+ SPA_TYPE_INTERFACE_DataSystem,
+ SPA_TYPE_INTERFACE_DataLoop,
+ SPA_TYPE_INTERFACE_System,
+ SPA_TYPE_INTERFACE_Loop,
+ SPA_TYPE_INTERFACE_LoopUtils,
+ SPA_TYPE_INTERFACE_Log,
+#ifdef HAVE_DBUS
+ SPA_TYPE_INTERFACE_DBus,
+#endif
+ SPA_TYPE_INTERFACE_CPU
+ };
+
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ const struct spa_support *support;
+ uint32_t n_support;
+ size_t i;
+
+ pw_init(0, NULL);
+
+ loop = pw_main_loop_new(NULL);
+ context = pw_context_new(pw_main_loop_get_loop(loop), NULL, 0);
+
+ support = pw_context_get_support(context, &n_support);
+ pwtest_ptr_notnull(support);
+ pwtest_int_gt(n_support, 0U);
+
+ for (i = 0; i < SPA_N_ELEMENTS(types); i++) {
+ pwtest_ptr_notnull(spa_support_find(support, n_support, types[i]));
+ }
+
+ pw_context_destroy(context);
+ pw_main_loop_destroy(loop);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(context)
+{
+ pwtest_add(context_abi, PWTEST_NOARG);
+ pwtest_add(context_create, PWTEST_NOARG);
+ pwtest_add(context_properties, PWTEST_NOARG);
+ pwtest_add(context_support, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-example.c b/test/test-example.c
new file mode 100644
index 0000000..05f9779
--- /dev/null
+++ b/test/test-example.c
@@ -0,0 +1,265 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+
+#include "config.h"
+
+#include <signal.h>
+#include <unistd.h>
+
+#include <pipewire/pipewire.h>
+
+#include "pwtest.h"
+
+/* The simplest test example (test passes) */
+PWTEST(successful_test)
+{
+ int x = 10, y = 20, z = 10;
+ bool t = true, f = false;
+ const char *a = "foo", *b = "bar", *c = "baz";
+
+ pwtest_int_lt(x, y);
+ pwtest_int_le(x, y);
+ pwtest_int_gt(y, x);
+ pwtest_int_ge(y, x);
+ pwtest_int_eq(x, z);
+ pwtest_int_ne(y, z);
+
+ pwtest_bool_true(t);
+ pwtest_bool_false(f);
+ pwtest_bool_eq(t, !f);
+ pwtest_bool_ne(t, f);
+
+ pwtest_str_eq(a, a);
+ pwtest_str_ne(a, b);
+ pwtest_str_eq_n(b, c, 2);
+ pwtest_str_ne_n(b, c, 3);
+
+ return PWTEST_PASS;
+}
+
+/* Demo failure of an integer comparison (test will fail) */
+PWTEST(failing_test_int)
+{
+ int x = 10, y = 20;
+ pwtest_int_gt(x, y);
+ return PWTEST_PASS;
+}
+
+/* Demo failure of a bool comparison (test will fail) */
+PWTEST(failing_test_bool)
+{
+ bool oops = true;
+ pwtest_bool_false(oops);
+ return PWTEST_PASS;
+}
+
+/* Demo failure of a string comparison (test will fail) */
+PWTEST(failing_test_string)
+{
+ const char *what = "yes";
+ pwtest_str_eq(what, "no");
+ return PWTEST_PASS;
+}
+
+/* Demo custom failure (test will fail) */
+PWTEST(general_fail_test)
+{
+ /* pwtest_fail(); */
+ pwtest_fail_with_msg("Some condition wasn't met");
+ return PWTEST_PASS;
+}
+
+/* Demo failure (test will fail) */
+PWTEST(failing_test_if_reached)
+{
+ pwtest_fail_if_reached();
+ return PWTEST_SYSTEM_ERROR;
+}
+
+/* Demo a system error (test will fail) */
+PWTEST(system_error_test)
+{
+ return PWTEST_SYSTEM_ERROR;
+}
+
+/* Demo signal handling of SIGSEGV (test will fail) */
+PWTEST(catch_segfault_test)
+{
+ int *x = NULL;
+ *x = 20;
+ return PWTEST_PASS;
+}
+
+/* Demo signal handling of abort (test will fail) */
+PWTEST(catch_abort_signal_test)
+{
+ abort();
+ return PWTEST_PASS;
+}
+
+/* Demo a timeout (test will fail with default timeout of 30) */
+PWTEST(timeout_test)
+{
+ /* run with --timeout=1 to make this less annoying */
+ sleep(60);
+ return PWTEST_PASS;
+}
+
+/* Demo a ranged test (test passes, skips the last 2) */
+PWTEST(ranged_test)
+{
+ struct pwtest_test *t = current_test;
+ int iteration = pwtest_get_iteration(t);
+
+ pwtest_int_lt(iteration, 10); /* see pwtest_add */
+
+ printf("Ranged test iteration %d\n", iteration);
+ if (iteration >= 8)
+ return PWTEST_SKIP;
+
+ return PWTEST_PASS;
+}
+
+/* Demo the properties passed to tests (test passes) */
+PWTEST(property_test)
+{
+ struct pwtest_test *t = current_test;
+ struct pw_properties *p = pwtest_get_props(t);
+
+ pwtest_ptr_notnull(p);
+ pwtest_str_eq(pw_properties_get(p, "myprop"), "somevalue");
+ pwtest_str_eq(pw_properties_get(p, "prop2"), "other");
+
+ return PWTEST_PASS;
+}
+
+/* Demo the environment passed to tests (test passes) */
+PWTEST(env_test)
+{
+ pwtest_str_eq(getenv("myenv"), "envval");
+ pwtest_str_eq(getenv("env2"), "val");
+
+ /* Set by pwtest */
+ pwtest_str_eq(getenv("PWTEST"), "1");
+
+ return PWTEST_PASS;
+}
+
+/* Demo the environment passed to tests (test passes) */
+PWTEST(env_reset_test)
+{
+ /* If run after env_test even with --no-fork this test should
+ * succeed */
+ pwtest_str_eq(getenv("myenv"), NULL);
+ pwtest_str_eq(getenv("env2"), NULL);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(default_env_test)
+{
+ /* This one is set automatically */
+ pwtest_str_eq(getenv("PWTEST"), "1");
+ /* Default value */
+ pwtest_str_eq(getenv("PIPEWIRE_REMOTE"), "test-has-no-daemon");
+
+ return PWTEST_PASS;
+}
+
+PWTEST(daemon_test)
+{
+ struct pw_context *ctx;
+ struct pw_core *core;
+ struct pw_loop *loop;
+
+ pwtest_str_eq_n(getenv("PIPEWIRE_REMOTE"), "pwtest-pw-", 10);
+
+ pw_init(0, NULL);
+ loop = pw_loop_new(NULL);
+ ctx = pw_context_new(loop, NULL, 0);
+ pwtest_ptr_notnull(ctx);
+ core = pw_context_connect(ctx, NULL, 0);
+ pwtest_ptr_notnull(core);
+
+ pw_loop_iterate(loop, -1);
+ pw_core_disconnect(core);
+ pw_context_destroy(ctx);
+ pw_loop_destroy(loop);
+
+ return PWTEST_PASS;
+}
+
+/* If not started with a daemon, we can't connect to a daemon (test will fail) */
+PWTEST(daemon_test_without_daemon)
+{
+ struct pw_context *ctx;
+ struct pw_core *core;
+ struct pw_loop *loop;
+
+ pw_init(0, NULL);
+ loop = pw_loop_new(NULL);
+ ctx = pw_context_new(loop, NULL, 0);
+ pwtest_ptr_notnull(ctx);
+ core = pw_context_connect(ctx, NULL, 0);
+
+ pwtest_ptr_notnull(core); /* Expect this to fail because we don't have a daemon */
+
+ pw_loop_iterate(loop, -1);
+ pw_core_disconnect(core);
+ pw_context_destroy(ctx);
+ pw_loop_destroy(loop);
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(example_tests)
+{
+ pwtest_add(successful_test, PWTEST_NOARG);
+ pwtest_add(failing_test_int, PWTEST_NOARG);
+ pwtest_add(failing_test_bool, PWTEST_NOARG);
+ pwtest_add(failing_test_string, PWTEST_NOARG);
+ pwtest_add(failing_test_if_reached, PWTEST_NOARG);
+ pwtest_add(general_fail_test, PWTEST_NOARG);
+ pwtest_add(system_error_test, PWTEST_NOARG);
+ pwtest_add(catch_segfault_test, PWTEST_NOARG);
+ pwtest_add(catch_abort_signal_test, PWTEST_ARG_SIGNAL, SIGABRT);
+ pwtest_add(ranged_test, PWTEST_ARG_RANGE, 0, 10);
+ pwtest_add(property_test,
+ PWTEST_ARG_PROP, "myprop", "somevalue",
+ PWTEST_ARG_PROP, "prop2", "other");
+ pwtest_add(env_test,
+ PWTEST_ARG_ENV, "env2", "val",
+ PWTEST_ARG_ENV, "myenv", "envval");
+ pwtest_add(env_reset_test, PWTEST_NOARG);
+ pwtest_add(default_env_test, PWTEST_NOARG);
+ pwtest_add(daemon_test, PWTEST_ARG_DAEMON);
+ pwtest_add(daemon_test_without_daemon, PWTEST_NOARG);
+
+ /* Run this one last so it doesn't matter if we forget --timeout */
+ pwtest_add(timeout_test, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-functional.c b/test/test-functional.c
new file mode 100644
index 0000000..ae837b4
--- /dev/null
+++ b/test/test-functional.c
@@ -0,0 +1,58 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#include "config.h"
+
+#include "pwtest.h"
+
+PWTEST(openal_info_test)
+{
+#ifdef OPENAL_INFO_PATH
+ int status = pwtest_spawn(OPENAL_INFO_PATH, (char *[]){ "openal-info", NULL });
+ pwtest_int_eq(WEXITSTATUS(status), 0);
+ return PWTEST_PASS;
+#else
+ return PWTEST_SKIP;
+#endif
+}
+
+PWTEST(pactl_test)
+{
+#ifdef PACTL_PATH
+ int status = pwtest_spawn(PACTL_PATH, (char *[]){ "pactl", "info", NULL });
+ pwtest_int_eq(WEXITSTATUS(status), 0);
+ return PWTEST_PASS;
+#else
+ return PWTEST_SKIP;
+#endif
+}
+
+PWTEST_SUITE(pw_array)
+{
+ pwtest_add(pactl_test, PWTEST_ARG_DAEMON);
+ pwtest_add(openal_info_test, PWTEST_ARG_DAEMON);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-lib.c b/test/test-lib.c
new file mode 100644
index 0000000..c2d73a4
--- /dev/null
+++ b/test/test-lib.c
@@ -0,0 +1,69 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#include "config.h"
+
+#include "pwtest.h"
+
+#include "pipewire/pipewire.h"
+
+PWTEST(library_version)
+{
+ const char *libversion, *headerversion;
+ char version_expected[64];
+
+ pw_init(0, NULL);
+ libversion = pw_get_library_version();
+ headerversion = pw_get_headers_version();
+
+ spa_scnprintf(version_expected, sizeof(version_expected),
+ "%d.%d.%d", PW_MAJOR, PW_MINOR, PW_MICRO);
+
+ pwtest_str_eq(headerversion, version_expected);
+ pwtest_str_eq(libversion, version_expected);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+PWTEST(init_deinit)
+{
+ pw_init(0, NULL);
+ pw_deinit();
+ pw_init(0, NULL);
+ pw_init(0, NULL);
+ pw_deinit();
+ pw_deinit();
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(properties)
+{
+ pwtest_add(library_version, PWTEST_NOARG);
+ pwtest_add(init_deinit, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-logger.c b/test/test-logger.c
new file mode 100644
index 0000000..b40445a
--- /dev/null
+++ b/test/test-logger.c
@@ -0,0 +1,656 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pwtest.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <spa/utils/ansi.h>
+#include <spa/utils/names.h>
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <pipewire/pipewire.h>
+
+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-journal.h>
+#endif
+
+PWTEST(logger_truncate_long_lines)
+{
+ struct pwtest_spa_plugin *plugin;
+ void *iface;
+ char fname[PATH_MAX];
+ struct spa_dict_item item;
+ struct spa_dict info;
+ char buffer[1024];
+ FILE *fp;
+ bool mark_line_found = false;
+
+ pw_init(0, NULL);
+
+ pwtest_mkstemp(fname);
+ item = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_FILE, fname);
+ info = SPA_DICT_INIT(&item, 1);
+ plugin = pwtest_spa_plugin_new();
+ iface = pwtest_spa_plugin_load_interface(plugin, "support/libspa-support",
+ SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log,
+ &info);
+ pwtest_ptr_notnull(iface);
+
+ /* Print a line expected to be truncated */
+ spa_log_error(iface, "MARK: %1100s", "foo");
+
+ fp = fopen(fname, "re");
+ while (fgets(buffer, sizeof(buffer), fp) != NULL) {
+ if (strstr(buffer, "MARK:")) {
+ const char *suffix = ".. (truncated)\n";
+ int len = strlen(buffer);
+ pwtest_str_eq(buffer + len - strlen(suffix), suffix);
+ mark_line_found = true;
+ break;
+ }
+ }
+
+ fclose(fp);
+
+ pwtest_bool_true(mark_line_found);
+ pwtest_spa_plugin_destroy(plugin);
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+PWTEST(logger_no_ansi)
+{
+ struct pwtest_spa_plugin *plugin;
+ void *iface;
+ char fname[PATH_MAX];
+ struct spa_dict_item items[2];
+ struct spa_dict info;
+ char buffer[1024];
+ FILE *fp;
+ bool mark_line_found = false;
+
+ pw_init(0, NULL);
+
+ pwtest_mkstemp(fname);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_FILE, fname);
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_COLORS, "true");
+ info = SPA_DICT_INIT(items, 2);
+ plugin = pwtest_spa_plugin_new();
+ iface = pwtest_spa_plugin_load_interface(plugin, "support/libspa-support",
+ SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log,
+ &info);
+ pwtest_ptr_notnull(iface);
+
+ /* Print a line usually containing a color sequence, but we're not a
+ * tty so expect none despite colors being enabled */
+ spa_log_error(iface, "MARK\n");
+
+ fp = fopen(fname, "re");
+ while (fgets(buffer, sizeof(buffer), fp) != NULL) {
+ if (strstr(buffer, "MARK")) {
+ mark_line_found = true;
+ pwtest_ptr_null(strstr(buffer, SPA_ANSI_RESET));
+ pwtest_ptr_null(strstr(buffer, SPA_ANSI_RED));
+ pwtest_ptr_null(strstr(buffer, SPA_ANSI_BRIGHT_RED));
+ pwtest_ptr_null(strstr(buffer, SPA_ANSI_BOLD_RED));
+ }
+ }
+
+ fclose(fp);
+
+ pwtest_bool_true(mark_line_found);
+ pwtest_spa_plugin_destroy(plugin);
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+static void
+test_log_levels(enum spa_log_level level)
+{
+ char fname[PATH_MAX];
+ char buffer[1024];
+ FILE *fp;
+ bool above_level_found = false;
+ bool below_level_found = false;
+ bool current_level_found = false;
+ char *oldenv = getenv("PIPEWIRE_LOG");
+
+ pwtest_mkstemp(fname);
+ setenv("PIPEWIRE_LOG", fname, 1);
+
+ pw_init(0, NULL);
+
+ /* current level is whatever the iteration is. Log one line
+ * with our level, one with a level above (should never show up)
+ * and one with a level below (should show up).
+ */
+ if (level > SPA_LOG_LEVEL_NONE)
+ pw_log(level, "CURRENT");
+ if (level > SPA_LOG_LEVEL_ERROR)
+ pw_log(level - 1, "BELOW");
+ if (level < SPA_LOG_LEVEL_TRACE)
+ pw_log(level + 1, "ABOVE");
+
+ fp = fopen(fname, "re");
+ while (fgets(buffer, sizeof(buffer), fp) != NULL) {
+ if (strstr(buffer, "CURRENT"))
+ current_level_found = true;
+ else if (strstr(buffer, "ABOVE"))
+ above_level_found = true;
+ else if (strstr(buffer, "BELOW"))
+ below_level_found = true;
+ }
+
+ fclose(fp);
+
+ /* Anything on a higher level than ours should never show up */
+ pwtest_bool_false(above_level_found);
+ if (level == SPA_LOG_LEVEL_NONE) {
+ pwtest_bool_false(current_level_found);
+ pwtest_bool_false(below_level_found);
+ } else if (level == SPA_LOG_LEVEL_ERROR) {
+ pwtest_bool_true(current_level_found);
+ pwtest_bool_false(below_level_found);
+ } else {
+ pwtest_bool_true(current_level_found);
+ pwtest_bool_true(below_level_found);
+ }
+ pw_deinit();
+
+ if (oldenv)
+ setenv("PIPEWIRE_LOG", oldenv, 1);
+ else
+ unsetenv("PIPEWIRE_LOG");
+}
+
+PWTEST(logger_levels)
+{
+ enum spa_log_level level = pwtest_get_iteration(current_test);
+ enum spa_log_level default_level = pw_log_level;
+ struct spa_log *default_logger = pw_log_get();
+ char *oldenv = getenv("PIPEWIRE_DEBUG");
+
+ if (oldenv)
+ oldenv = strdup(oldenv);
+ unsetenv("PIPEWIRE_DEBUG");
+
+ pw_log_set_level(level);
+
+ test_log_levels(level);
+
+ if (oldenv) {
+ setenv("PIPEWIRE_DEBUG", oldenv, 1);
+ free(oldenv);
+ }
+
+ pw_log_set(default_logger);
+ pw_log_set_level(default_level);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(logger_debug_env)
+{
+ enum spa_log_level level = pwtest_get_iteration(current_test);
+ enum spa_log_level default_level = pw_log_level;
+ struct spa_log *default_logger = pw_log_get();
+ char lvl[2] = {0};
+ char *oldenv = getenv("PIPEWIRE_DEBUG");
+
+ if (oldenv)
+ oldenv = strdup(oldenv);
+
+ spa_scnprintf(lvl, sizeof(lvl), "%d", level);
+ setenv("PIPEWIRE_DEBUG", lvl, 1);
+
+ /* Disable logging, let PIPEWIRE_DEBUG set the level */
+ pw_log_set_level(SPA_LOG_LEVEL_NONE);
+
+ test_log_levels(level);
+
+ if (oldenv) {
+ setenv("PIPEWIRE_DEBUG", oldenv, 1);
+ free(oldenv);
+ } else {
+ unsetenv("PIPEWIRE_DEBUG");
+ }
+
+ pw_log_set(default_logger);
+ pw_log_set_level(default_level);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(logger_debug_env_alpha)
+{
+ enum spa_log_level level = pwtest_get_iteration(current_test);
+ enum spa_log_level default_level = pw_log_level;
+ struct spa_log *default_logger = pw_log_get();
+ char *lvl = NULL;
+ char *oldenv = getenv("PIPEWIRE_DEBUG");
+
+ if (oldenv)
+ oldenv = strdup(oldenv);
+
+ switch(level) {
+ case SPA_LOG_LEVEL_NONE: lvl = "X"; break;
+ case SPA_LOG_LEVEL_ERROR: lvl = "E"; break;
+ case SPA_LOG_LEVEL_WARN: lvl = "W"; break;
+ case SPA_LOG_LEVEL_INFO: lvl = "I"; break;
+ case SPA_LOG_LEVEL_DEBUG: lvl = "D"; break;
+ case SPA_LOG_LEVEL_TRACE: lvl = "T"; break;
+ default:
+ pwtest_fail_if_reached();
+ break;
+ }
+ setenv("PIPEWIRE_DEBUG", lvl, 1);
+
+ /* Disable logging, let PIPEWIRE_DEBUG set the level */
+ pw_log_set_level(SPA_LOG_LEVEL_NONE);
+
+ test_log_levels(level);
+
+ if (oldenv) {
+ setenv("PIPEWIRE_DEBUG", oldenv, 1);
+ free(oldenv);
+ } else {
+ unsetenv("PIPEWIRE_DEBUG");
+ }
+
+ pw_log_set(default_logger);
+ pw_log_set_level(default_level);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(logger_debug_env_topic_all)
+{
+ enum spa_log_level level = pwtest_get_iteration(current_test);
+ enum spa_log_level default_level = pw_log_level;
+ struct spa_log *default_logger = pw_log_get();
+ char *oldenv = getenv("PIPEWIRE_DEBUG");
+ char lvlstr[32];
+ char *lvl = "X";
+
+ if (oldenv)
+ oldenv = strdup(oldenv);
+
+ switch(level) {
+ case SPA_LOG_LEVEL_NONE: lvl = "X"; break;
+ case SPA_LOG_LEVEL_ERROR: lvl = "E"; break;
+ case SPA_LOG_LEVEL_WARN: lvl = "W"; break;
+ case SPA_LOG_LEVEL_INFO: lvl = "I"; break;
+ case SPA_LOG_LEVEL_DEBUG: lvl = "D"; break;
+ case SPA_LOG_LEVEL_TRACE: lvl = "T"; break;
+ default:
+ pwtest_fail_if_reached();
+ break;
+ }
+
+ /* Check that the * glob works to enable all topics */
+ spa_scnprintf(lvlstr, sizeof(lvlstr), "*:%s", lvl);
+ setenv("PIPEWIRE_DEBUG", lvlstr, 1);
+
+ /* Disable logging, let PIPEWIRE_DEBUG set the level */
+ pw_log_set_level(SPA_LOG_LEVEL_NONE);
+
+ test_log_levels(level);
+
+ if (oldenv) {
+ setenv("PIPEWIRE_DEBUG", oldenv, 1);
+ free(oldenv);
+ } else {
+ unsetenv("PIPEWIRE_DEBUG");
+ }
+
+ pw_log_set(default_logger);
+ pw_log_set_level(default_level);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(logger_debug_env_invalid)
+{
+ enum spa_log_level default_level = pw_log_level;
+ struct spa_log *default_logger = pw_log_get();
+ char *oldenv = getenv("PIPEWIRE_DEBUG");
+ char fname[PATH_MAX];
+ char buf[1024] = {0};
+ int fd;
+ int rc;
+ bool error_message_found = false;
+ long unsigned int which = pwtest_get_iteration(current_test);
+ const char *envvars[] = {
+ "invalid value",
+ "*:5,some invalid value",
+ "*:W,foo.bar:3,invalid:",
+ "*:W,2,foo.bar:Q",
+ "*:W,7,foo.bar:D",
+ "*:W,Q,foo.bar:5",
+ "*:W,D,foo.bar:8",
+ };
+
+ pwtest_int_lt(which, SPA_N_ELEMENTS(envvars));
+
+ if (oldenv)
+ oldenv = strdup(oldenv);
+
+ /* The error message during pw_init() will go to stderr because no
+ * logger has been set up yet. Intercept that in our temp file */
+ pwtest_mkstemp(fname);
+ fd = open(fname, O_RDWR);
+ pwtest_errno_ok(fd);
+ rc = dup2(fd, STDERR_FILENO);
+ setlinebuf(stderr);
+ pwtest_errno_ok(rc);
+
+ setenv("PIPEWIRE_DEBUG", envvars[which], 1);
+ pw_init(0, NULL);
+
+ fsync(STDERR_FILENO);
+ lseek(fd, SEEK_SET, 0);
+ while ((rc = read(fd, buf, sizeof(buf) - 1) > 0)) {
+ if (strstr(buf, "Ignoring invalid format in PIPEWIRE_DEBUG")) {
+ error_message_found = true;
+ break;
+ }
+ }
+ pwtest_errno_ok(rc);
+ close(fd);
+ pwtest_bool_true(error_message_found);
+
+ if (oldenv) {
+ setenv("PIPEWIRE_DEBUG", oldenv, 1);
+ free(oldenv);
+ } else {
+ unsetenv("PIPEWIRE_DEBUG");
+ }
+
+ pw_log_set(default_logger);
+ pw_log_set_level(default_level);
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+PWTEST(logger_topics)
+{
+ struct pwtest_spa_plugin *plugin;
+ void *iface;
+ char fname[PATH_MAX];
+ struct spa_dict_item items[2];
+ struct spa_dict info;
+ char buffer[1024];
+ FILE *fp;
+ bool mark_line_found = false;
+ struct spa_log_topic topic = {
+ .version = 0,
+ .topic = "my topic",
+ .level = SPA_LOG_LEVEL_DEBUG,
+ };
+
+ pw_init(0, NULL);
+
+ pwtest_mkstemp(fname);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_FILE, fname);
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_COLORS, "true");
+ info = SPA_DICT_INIT(items, 2);
+ plugin = pwtest_spa_plugin_new();
+ iface = pwtest_spa_plugin_load_interface(plugin, "support/libspa-support",
+ SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log,
+ &info);
+ pwtest_ptr_notnull(iface);
+
+ spa_logt_info(iface, &topic, "MARK\n");
+
+ fp = fopen(fname, "re");
+ while (fgets(buffer, sizeof(buffer), fp) != NULL) {
+ if (strstr(buffer, "MARK")) {
+ mark_line_found = true;
+ pwtest_str_contains(buffer, "my topic");
+ }
+ }
+
+ pwtest_bool_true(mark_line_found);
+ pwtest_spa_plugin_destroy(plugin);
+
+ fclose(fp);
+
+ return PWTEST_PASS;
+}
+
+#ifdef HAVE_SYSTEMD
+static enum pwtest_result
+find_in_journal(sd_journal *journal, const char *needle, char *out, size_t out_sz)
+{
+ int rc;
+ int i;
+
+ /* We give ourselves up to a second for our message to appear */
+ for (i = 0; i < 10; i++) {
+ int activity = sd_journal_wait(journal, 100000);
+
+ pwtest_neg_errno_ok(activity);
+ switch (activity) {
+ case SD_JOURNAL_NOP:
+ break;
+ case SD_JOURNAL_INVALIDATE:
+ case SD_JOURNAL_APPEND:
+ while ((rc = sd_journal_next(journal)) > 0) {
+ char buffer[1024] = {0};
+ const char *d;
+ size_t l;
+ int r = sd_journal_get_data(journal, "MESSAGE", (const void **)&d, &l);
+ if (r == -ENOENT || r == -E2BIG || r == -EBADMSG)
+ continue;
+
+ pwtest_neg_errno_ok(r);
+ spa_scnprintf(buffer, sizeof(buffer), "%.*s", (int) l, d);
+ if (strstr(buffer, needle)) {
+ spa_scnprintf(out, out_sz, "%s", buffer);
+ return PWTEST_PASS;
+ }
+ }
+ pwtest_neg_errno_ok(rc);
+ break;
+ default:
+ break;
+ }
+ }
+ return PWTEST_FAIL;
+}
+#endif
+
+PWTEST(logger_journal)
+{
+ enum pwtest_result result = PWTEST_SKIP;
+#ifdef HAVE_SYSTEMD
+ struct pwtest_spa_plugin *plugin;
+ void *iface;
+ struct spa_dict_item items[2];
+ struct spa_dict info;
+ struct spa_log_topic topic = {
+ .version = 0,
+ .topic = "pwtest journal",
+ .level = SPA_LOG_LEVEL_DEBUG,
+ };
+ char buffer[1024] = {0};
+ sd_journal *journal;
+ int rc;
+ char token[64];
+
+ pw_init(0, NULL);
+
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_LEVEL, "4"); /* debug */
+ info = SPA_DICT_INIT(items, 1);
+ plugin = pwtest_spa_plugin_new();
+ iface = pwtest_spa_plugin_load_interface(plugin, "support/libspa-journal",
+ SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log,
+ &info);
+ pwtest_ptr_notnull(iface);
+
+ rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY|SD_JOURNAL_CURRENT_USER);
+ pwtest_neg_errno_ok(rc);
+
+ sd_journal_seek_head(journal);
+ if (sd_journal_next(journal) == 0) { /* No entries? We don't have a journal */
+ goto cleanup;
+ }
+
+ sd_journal_seek_tail(journal);
+ sd_journal_next(journal);
+
+ spa_scnprintf(token, sizeof(token), "MARK %s:%d", __func__, __LINE__);
+ spa_logt_info(iface, &topic, "%s", token);
+
+ result = find_in_journal(journal, token, buffer, sizeof(buffer));
+ pwtest_int_eq((int)result, PWTEST_PASS);
+ pwtest_str_contains(buffer, "pwtest journal");
+
+cleanup:
+ sd_journal_close(journal);
+ pwtest_spa_plugin_destroy(plugin);
+ pw_deinit();
+#endif
+ return result;
+}
+
+PWTEST(logger_journal_chain)
+{
+ enum pwtest_result result = PWTEST_SKIP;
+#ifdef HAVE_SYSTEMD
+ struct pwtest_spa_plugin *plugin;
+ void *iface_log;
+ void *iface;
+ char fname[PATH_MAX];
+ char buffer[1024];
+ FILE *fp;
+ struct spa_dict_item items[2];
+ struct spa_dict info;
+ bool mark_line_found = false;
+ struct spa_log_topic topic = {
+ .version = 0,
+ .topic = "pwtest journal",
+ .level = SPA_LOG_LEVEL_DEBUG,
+ };
+ sd_journal *journal;
+ int rc;
+ char token[64];
+
+ pw_init(0, NULL);
+ pwtest_mkstemp(fname);
+
+ /* Load a normal logger interface first, writing to fname */
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_FILE, fname);
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_LEVEL, "4"); /* debug */
+ info = SPA_DICT_INIT(items, 2);
+ plugin = pwtest_spa_plugin_new();
+ iface_log = pwtest_spa_plugin_load_interface(plugin, "support/libspa-support",
+ SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log,
+ &info);
+ pwtest_ptr_notnull(iface_log);
+
+ /* Load the journal logger, it should chain to the above */
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_LEVEL, "4"); /* debug */
+ info = SPA_DICT_INIT(&items[1], 1);
+ iface = pwtest_spa_plugin_load_interface(plugin, "support/libspa-journal",
+ SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log,
+ &info);
+ pwtest_ptr_notnull(iface);
+
+ rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY);
+ pwtest_neg_errno_ok(rc);
+ sd_journal_seek_head(journal);
+ if (sd_journal_next(journal) == 0) { /* No entries? We don't have a journal */
+ goto cleanup;
+ }
+
+ sd_journal_seek_tail(journal);
+ sd_journal_next(journal);
+
+ spa_scnprintf(token, sizeof(token), "MARK %s:%d", __func__, __LINE__);
+
+ spa_logt_info(iface, &topic, "%s", token);
+ result = find_in_journal(journal, token, buffer, sizeof(buffer));
+ pwtest_int_eq((int)result, PWTEST_PASS);
+ pwtest_str_contains(buffer, "pwtest journal");
+
+ /* Now check that the line is in the chained file logger too */
+ spa_memzero(buffer, sizeof(buffer));
+ mark_line_found = false;
+ fp = fopen(fname, "re");
+ while (fgets(buffer, sizeof(buffer), fp) != NULL) {
+ if (strstr(buffer, token)) {
+ mark_line_found = true;
+ pwtest_ptr_null(strstr(buffer, SPA_ANSI_RESET));
+ pwtest_ptr_null(strstr(buffer, SPA_ANSI_RED));
+ pwtest_ptr_null(strstr(buffer, SPA_ANSI_BRIGHT_RED));
+ pwtest_ptr_null(strstr(buffer, SPA_ANSI_BOLD_RED));
+ }
+ }
+
+ fclose(fp);
+
+ result = PWTEST_PASS;
+ pwtest_bool_true(mark_line_found);
+
+cleanup:
+ sd_journal_close(journal);
+ pwtest_spa_plugin_destroy(plugin);
+ pw_deinit();
+
+#endif
+ return result;
+}
+
+PWTEST_SUITE(logger)
+{
+ pwtest_add(logger_truncate_long_lines, PWTEST_NOARG);
+ pwtest_add(logger_no_ansi, PWTEST_NOARG);
+ pwtest_add(logger_levels,
+ PWTEST_ARG_RANGE, SPA_LOG_LEVEL_NONE, SPA_LOG_LEVEL_TRACE + 1,
+ PWTEST_NOARG);
+ pwtest_add(logger_debug_env,
+ PWTEST_ARG_RANGE, SPA_LOG_LEVEL_NONE, SPA_LOG_LEVEL_TRACE + 1,
+ PWTEST_NOARG);
+ pwtest_add(logger_debug_env_alpha,
+ PWTEST_ARG_RANGE, SPA_LOG_LEVEL_NONE, SPA_LOG_LEVEL_TRACE + 1,
+ PWTEST_NOARG);
+ pwtest_add(logger_debug_env_topic_all,
+ PWTEST_ARG_RANGE, SPA_LOG_LEVEL_NONE, SPA_LOG_LEVEL_TRACE + 1,
+ PWTEST_NOARG);
+ pwtest_add(logger_debug_env_invalid,
+ PWTEST_ARG_RANGE, 0, 7, /* see the test */
+ PWTEST_NOARG);
+ pwtest_add(logger_topics, PWTEST_NOARG);
+ pwtest_add(logger_journal, PWTEST_NOARG);
+ pwtest_add(logger_journal_chain, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-loop.c b/test/test-loop.c
new file mode 100644
index 0000000..bfdb1e4
--- /dev/null
+++ b/test/test-loop.c
@@ -0,0 +1,463 @@
+/* PipeWire
+ *
+ * Copyright © 2022 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/eventfd.h>
+
+#include "pwtest.h"
+
+#include <pipewire/pipewire.h>
+
+struct obj {
+ int x;
+ struct spa_source source;
+};
+
+struct data {
+ struct pw_main_loop *ml;
+ struct pw_loop *l;
+ struct obj *a, *b;
+ int count;
+};
+
+static inline void write_eventfd(int evfd)
+{
+ uint64_t value = 1;
+ ssize_t r = write(evfd, &value, sizeof(value));
+ pwtest_errno_ok(r);
+ pwtest_int_eq(r, (ssize_t) sizeof(value));
+}
+
+static inline void read_eventfd(int evfd)
+{
+ uint64_t value = 0;
+ ssize_t r = read(evfd, &value, sizeof(value));
+ pwtest_errno_ok(r);
+ pwtest_int_eq(r, (ssize_t) sizeof(value));
+}
+
+static void on_event(struct spa_source *source)
+{
+ struct data *d = source->data;
+
+ pw_loop_remove_source(d->l, &d->a->source);
+ pw_loop_remove_source(d->l, &d->b->source);
+ close(d->a->source.fd);
+ close(d->b->source.fd);
+ free(d->a);
+ free(d->b);
+
+ pw_main_loop_quit(d->ml);
+}
+
+PWTEST(pwtest_loop_destroy2)
+{
+ struct data data;
+
+ pw_init(0, NULL);
+
+ spa_zero(data);
+ data.ml = pw_main_loop_new(NULL);
+ pwtest_ptr_notnull(data.ml);
+
+ data.l = pw_main_loop_get_loop(data.ml);
+ pwtest_ptr_notnull(data.l);
+
+ data.a = calloc(1, sizeof(*data.a));
+ data.b = calloc(1, sizeof(*data.b));
+
+ data.a->source.func = on_event;
+ data.a->source.fd = eventfd(0, 0);
+ data.a->source.mask = SPA_IO_IN;
+ data.a->source.data = &data;
+ data.b->source.func = on_event;
+ data.b->source.fd = eventfd(0, 0);
+ data.b->source.mask = SPA_IO_IN;
+ data.b->source.data = &data;
+
+ pw_loop_add_source(data.l, &data.a->source);
+ pw_loop_add_source(data.l, &data.b->source);
+
+ write_eventfd(data.a->source.fd);
+ write_eventfd(data.b->source.fd);
+
+ pw_main_loop_run(data.ml);
+ pw_main_loop_destroy(data.ml);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+static void
+on_event_recurse1(struct spa_source *source)
+{
+ static bool first = true;
+ struct data *d = source->data;
+
+ ++d->count;
+ pwtest_int_lt(d->count, 3);
+
+ read_eventfd(source->fd);
+
+ if (first) {
+ first = false;
+ pw_loop_enter(d->l);
+ pw_loop_iterate(d->l, -1);
+ pw_loop_leave(d->l);
+ }
+ pw_main_loop_quit(d->ml);
+}
+
+PWTEST(pwtest_loop_recurse1)
+{
+ struct data data;
+
+ pw_init(0, NULL);
+
+ spa_zero(data);
+ data.ml = pw_main_loop_new(NULL);
+ pwtest_ptr_notnull(data.ml);
+
+ data.l = pw_main_loop_get_loop(data.ml);
+ pwtest_ptr_notnull(data.l);
+
+ data.a = calloc(1, sizeof(*data.a));
+ data.b = calloc(1, sizeof(*data.b));
+
+ data.a->source.func = on_event_recurse1;
+ data.a->source.fd = eventfd(0, 0);
+ data.a->source.mask = SPA_IO_IN;
+ data.a->source.data = &data;
+ data.b->source.func = on_event_recurse1;
+ data.b->source.fd = eventfd(0, 0);
+ data.b->source.mask = SPA_IO_IN;
+ data.b->source.data = &data;
+
+ pw_loop_add_source(data.l, &data.a->source);
+ pw_loop_add_source(data.l, &data.b->source);
+
+ write_eventfd(data.a->source.fd);
+ write_eventfd(data.b->source.fd);
+
+ pw_main_loop_run(data.ml);
+ pw_main_loop_destroy(data.ml);
+
+ pw_deinit();
+
+ free(data.a);
+ free(data.b);
+
+ return PWTEST_PASS;
+}
+
+static void
+on_event_recurse2(struct spa_source *source)
+{
+ static bool first = true;
+ struct data *d = source->data;
+
+ ++d->count;
+ pwtest_int_lt(d->count, 3);
+
+ read_eventfd(source->fd);
+
+ if (first) {
+ first = false;
+ pw_loop_enter(d->l);
+ pw_loop_iterate(d->l, -1);
+ pw_loop_leave(d->l);
+ } else {
+ pw_loop_remove_source(d->l, &d->a->source);
+ pw_loop_remove_source(d->l, &d->b->source);
+ close(d->a->source.fd);
+ close(d->b->source.fd);
+ free(d->a);
+ free(d->b);
+ }
+ pw_main_loop_quit(d->ml);
+}
+
+PWTEST(pwtest_loop_recurse2)
+{
+ struct data data;
+
+ pw_init(0, NULL);
+
+ spa_zero(data);
+ data.ml = pw_main_loop_new(NULL);
+ pwtest_ptr_notnull(data.ml);
+
+ data.l = pw_main_loop_get_loop(data.ml);
+ pwtest_ptr_notnull(data.l);
+
+ data.a = calloc(1, sizeof(*data.a));
+ data.b = calloc(1, sizeof(*data.b));
+
+ data.a->source.func = on_event_recurse2;
+ data.a->source.fd = eventfd(0, 0);
+ data.a->source.mask = SPA_IO_IN;
+ data.a->source.data = &data;
+ data.b->source.func = on_event_recurse2;
+ data.b->source.fd = eventfd(0, 0);
+ data.b->source.mask = SPA_IO_IN;
+ data.b->source.data = &data;
+
+ pw_loop_add_source(data.l, &data.a->source);
+ pw_loop_add_source(data.l, &data.b->source);
+
+ write_eventfd(data.a->source.fd);
+ write_eventfd(data.b->source.fd);
+
+ pw_main_loop_run(data.ml);
+ pw_main_loop_destroy(data.ml);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+static void
+on_event_fail_if_called(void *data, int fd, uint32_t mask)
+{
+ pwtest_fail_if_reached();
+}
+
+struct dmsbd_data {
+ struct pw_loop *l;
+ struct pw_main_loop *ml;
+ struct spa_source *source;
+ struct spa_hook hook;
+};
+
+static void dmsbd_after(void *data)
+{
+ struct dmsbd_data *d = data;
+
+ pw_loop_destroy_source(d->l, d->source);
+ pw_main_loop_quit(d->ml);
+}
+
+static const struct spa_loop_control_hooks dmsbd_hooks = {
+ SPA_VERSION_LOOP_CONTROL_HOOKS,
+ .after = dmsbd_after,
+};
+
+PWTEST(destroy_managed_source_before_dispatch)
+{
+ pw_init(NULL, NULL);
+
+ struct dmsbd_data data = {0};
+
+ data.ml = pw_main_loop_new(NULL);
+ pwtest_ptr_notnull(data.ml);
+
+ data.l = pw_main_loop_get_loop(data.ml);
+ pwtest_ptr_notnull(data.l);
+
+ data.source = pw_loop_add_io(data.l, eventfd(0, 0), SPA_IO_IN, true, on_event_fail_if_called, NULL);
+ pwtest_ptr_notnull(data.source);
+
+ pw_loop_add_hook(data.l, &data.hook, &dmsbd_hooks, &data);
+
+ write_eventfd(data.source->fd);
+
+ pw_main_loop_run(data.ml);
+ pw_main_loop_destroy(data.ml);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+struct dmsbd_recurse_data {
+ struct pw_loop *l;
+ struct pw_main_loop *ml;
+ struct spa_source *a, *b;
+ struct spa_hook hook;
+ bool first;
+};
+
+static void dmsbd_recurse_on_event(void *data, int fd, uint32_t mask)
+{
+ struct dmsbd_recurse_data *d = data;
+
+ read_eventfd(fd);
+
+ pw_loop_enter(d->l);
+ pw_loop_iterate(d->l, 0);
+ pw_loop_leave(d->l);
+
+ pw_main_loop_quit(d->ml);
+}
+
+static void dmswp_recurse_before(void *data)
+{
+ struct dmsbd_recurse_data *d = data;
+
+ if (d->first) {
+ write_eventfd(d->a->fd);
+ write_eventfd(d->b->fd);
+ }
+}
+
+static void dmsbd_recurse_after(void *data)
+{
+ struct dmsbd_recurse_data *d = data;
+
+ if (d->first) {
+ pw_loop_destroy_source(d->l, d->b);
+
+ d->first = false;
+ }
+}
+
+static const struct spa_loop_control_hooks dmsbd_recurse_hooks = {
+ SPA_VERSION_LOOP_CONTROL_HOOKS,
+ .before = dmswp_recurse_before,
+ .after = dmsbd_recurse_after,
+};
+
+PWTEST(destroy_managed_source_before_dispatch_recurse)
+{
+ pw_init(NULL, NULL);
+
+ struct dmsbd_recurse_data data = {
+ .first = true,
+ };
+
+ data.ml = pw_main_loop_new(NULL);
+ pwtest_ptr_notnull(data.ml);
+
+ data.l = pw_main_loop_get_loop(data.ml);
+ pwtest_ptr_notnull(data.l);
+
+ data.l = pw_main_loop_get_loop(data.ml);
+ pwtest_ptr_notnull(data.l);
+
+ data.a = pw_loop_add_io(data.l, eventfd(0, 0), SPA_IO_IN, true, dmsbd_recurse_on_event, &data);
+ data.b = pw_loop_add_io(data.l, eventfd(0, 0), SPA_IO_IN, true, on_event_fail_if_called, NULL);
+ pwtest_ptr_notnull(data.a);
+ pwtest_ptr_notnull(data.b);
+
+ pw_loop_add_hook(data.l, &data.hook, &dmsbd_recurse_hooks, &data);
+
+ pw_main_loop_run(data.ml);
+ pw_main_loop_destroy(data.ml);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+struct ctwd_data {
+ struct spa_source source;
+ int handler_running_barrier;
+};
+
+static void ctwd_event_handler(struct spa_source *source)
+{
+ struct ctwd_data *data = source->data;
+
+ write_eventfd(data->handler_running_barrier);
+
+ for (;;)
+ pause(); /* the purpose of this is to block the loop */
+}
+
+static int ctwd_add_source(struct spa_loop *loop, bool async, uint32_t seq,
+ const void *d, size_t size, void *user_data)
+{
+ struct ctwd_data *data = user_data;
+
+ pwtest_neg_errno_ok(spa_loop_add_source(loop, &data->source));
+
+ return 0;
+}
+
+PWTEST(cancel_thread_while_dispatching)
+{
+ static const struct spa_dict_item data_loop_props_items[] = {
+ { "loop.cancel", "true" },
+ };
+ static const struct spa_dict data_loop_props = SPA_DICT_INIT_ARRAY(data_loop_props_items);
+
+ struct ctwd_data data = {
+ .source = {
+ .data = &data,
+ .func = ctwd_event_handler,
+ .mask = SPA_IO_IN,
+ .fd = eventfd(0, 0),
+ },
+ .handler_running_barrier = eventfd(0, 0),
+ };
+
+ pw_init(NULL, NULL);
+
+ struct pw_data_loop *dl = pw_data_loop_new(&data_loop_props);
+ pwtest_ptr_notnull(dl);
+
+ struct pw_loop *l = pw_data_loop_get_loop(dl);
+ pwtest_ptr_notnull(l);
+
+ pwtest_neg_errno_ok(pw_data_loop_start(dl));
+
+ pw_loop_invoke(l, ctwd_add_source, 0, NULL, 0, true, &data);
+ pwtest_ptr_notnull(data.source.loop);
+
+ write_eventfd(data.source.fd);
+ read_eventfd(data.handler_running_barrier);
+
+ pwtest_neg_errno_ok(pw_data_loop_stop(dl));
+
+ /* these are the important checks */
+ pwtest_ptr_null(data.source.priv);
+ pwtest_int_eq(data.source.rmask, UINT32_C(0));
+
+ pw_loop_remove_source(l, &data.source);
+
+ pw_data_loop_destroy(dl);
+
+ close(data.source.fd);
+ close(data.handler_running_barrier);
+
+ pw_deinit();
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(support)
+{
+ pwtest_add(pwtest_loop_destroy2, PWTEST_NOARG);
+ pwtest_add(pwtest_loop_recurse1, PWTEST_NOARG);
+ pwtest_add(pwtest_loop_recurse2, PWTEST_NOARG);
+ pwtest_add(destroy_managed_source_before_dispatch, PWTEST_NOARG);
+ pwtest_add(destroy_managed_source_before_dispatch_recurse, PWTEST_NOARG);
+ pwtest_add(cancel_thread_while_dispatching, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-map.c b/test/test-map.c
new file mode 100644
index 0000000..b6d7681
--- /dev/null
+++ b/test/test-map.c
@@ -0,0 +1,242 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pwtest.h"
+
+#include <pipewire/map.h>
+
+
+PWTEST(map_add_remove)
+{
+ struct pw_map map = PW_MAP_INIT(2);
+ int a, b, c;
+ void *p1 = &a, *p2 = &b, *p3 = &c;
+ uint32_t idx1, idx2, idx3;
+
+ idx1 = pw_map_insert_new(&map, p1);
+ idx2 = pw_map_insert_new(&map, p2);
+ idx3 = pw_map_insert_new(&map, p3);
+
+ /* This is implementation-defined behavior and
+ * may change in the future */
+ pwtest_int_eq(idx1, 0U);
+ pwtest_int_eq(idx2, 1U);
+ pwtest_int_eq(idx3, 2U);
+
+ /* public API */
+ pwtest_ptr_eq(p1, pw_map_lookup(&map, idx1));
+ pwtest_ptr_eq(p2, pw_map_lookup(&map, idx2));
+ pwtest_ptr_eq(p3, pw_map_lookup(&map, idx3));
+
+ pw_map_remove(&map, idx1);
+ pwtest_ptr_null(pw_map_lookup(&map, idx1));
+ pwtest_ptr_eq(p2, pw_map_lookup(&map, idx2));
+ pwtest_ptr_eq(p3, pw_map_lookup(&map, idx3));
+
+ pw_map_remove(&map, idx2);
+ pwtest_ptr_null(pw_map_lookup(&map, idx1));
+ pwtest_ptr_null(pw_map_lookup(&map, idx2));
+ pwtest_ptr_eq(p3, pw_map_lookup(&map, idx3));
+
+ pw_map_remove(&map, idx3);
+ pwtest_ptr_null(pw_map_lookup(&map, idx1));
+ pwtest_ptr_null(pw_map_lookup(&map, idx2));
+ pwtest_ptr_null(pw_map_lookup(&map, idx3));
+
+ idx1 = pw_map_insert_new(&map, p1);
+ idx2 = pw_map_insert_new(&map, p2);
+ idx3 = pw_map_insert_new(&map, p3);
+
+ /* This is implementation-defined behavior and
+ * may change in the future */
+ pwtest_int_eq(idx3, 0U);
+ pwtest_int_eq(idx2, 1U);
+ pwtest_int_eq(idx1, 2U);
+
+ pw_map_clear(&map);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(map_insert)
+{
+ struct pw_map map = PW_MAP_INIT(2);
+ int a, b, c, d;
+ void *p1 = &a, *p2 = &b, *p3 = &c, *p4 = &d;
+ uint32_t idx1, idx2, idx3;
+ int rc;
+ size_t sz;
+
+ idx1 = pw_map_insert_new(&map, p1);
+ idx2 = pw_map_insert_new(&map, p2);
+ idx3 = pw_map_insert_new(&map, p3);
+
+ pwtest_ptr_eq(p1, pw_map_lookup(&map, idx1));
+ pwtest_ptr_eq(p2, pw_map_lookup(&map, idx2));
+ pwtest_ptr_eq(p3, pw_map_lookup(&map, idx3));
+ sz = pw_map_get_size(&map);
+ pwtest_int_eq(sz, 3U);
+
+ /* overwrite */
+ rc = pw_map_insert_at(&map, idx1, p4);
+ pwtest_neg_errno_ok(rc);
+ pwtest_ptr_eq(p4, pw_map_lookup(&map, idx1));
+ sz = pw_map_get_size(&map);
+ pwtest_int_eq(sz, 3U);
+
+ /* overwrite */
+ rc = pw_map_insert_at(&map, idx2, p4);
+ pwtest_neg_errno_ok(rc);
+ pwtest_ptr_eq(p4, pw_map_lookup(&map, idx2));
+ sz = pw_map_get_size(&map);
+ pwtest_int_eq(sz, 3U);
+
+ /* out of bounds */
+ rc = pw_map_insert_at(&map, 10000, p4);
+ pwtest_neg_errno(rc, -ENOSPC);
+
+ /* if id is the map size, the item is appended */
+ rc = pw_map_insert_at(&map, idx3 + 1, &p4);
+ pwtest_neg_errno_ok(rc);
+ sz = pw_map_get_size(&map);
+ pwtest_int_eq(sz, 4U);
+
+ pw_map_clear(&map);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(map_size)
+{
+ struct pw_map map = PW_MAP_INIT(2);
+ int a, b, c;
+ void *p1 = &a, *p2 = &b, *p3 = &c;
+ uint32_t idx1;
+ size_t sz;
+
+ idx1 = pw_map_insert_new(&map, p1);
+ sz = pw_map_get_size(&map);
+ pwtest_int_eq(sz, 1U);
+ pw_map_insert_new(&map, p2);
+ sz = pw_map_get_size(&map);
+ pwtest_int_eq(sz, 2U);
+ pw_map_insert_new(&map, p3);
+ sz = pw_map_get_size(&map);
+ pwtest_int_eq(sz, 3U);
+
+ /* Removing does not alter the size */
+ pw_map_remove(&map, idx1);
+ sz = pw_map_get_size(&map);
+ pwtest_int_eq(sz, 3U);
+
+ pw_map_clear(&map);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(map_double_remove)
+{
+ struct pw_map map = PW_MAP_INIT(2);
+ int a, b, c;
+ void *p1 = &a, *p2 = &b, *p3 = &c;
+
+ uint32_t idx1, idx2, idx3;
+
+ idx1 = pw_map_insert_new(&map, p1);
+ idx2 = pw_map_insert_new(&map, p2);
+ idx3 = pw_map_insert_new(&map, p3);
+
+ pw_map_remove(&map, idx1); /* idx1 in the free list */
+ pw_map_remove(&map, idx2); /* idx1 and 2 in the free list */
+ pw_map_remove(&map, idx2); /* should be a noop */
+ idx1 = pw_map_insert_new(&map, p1);
+ idx2 = pw_map_insert_new(&map, p2);
+
+ pwtest_ptr_eq(p1, pw_map_lookup(&map, idx1));
+ pwtest_ptr_eq(p2, pw_map_lookup(&map, idx2));
+ pwtest_ptr_eq(p3, pw_map_lookup(&map, idx3));
+
+ pw_map_clear(&map);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(map_insert_at_free)
+{
+ struct pw_map map = PW_MAP_INIT(2);
+ int data[3] = {1, 2, 3};
+ int new_data = 4;
+ int *ptr[3] = {&data[0], &data[1], &data[3]};
+ int idx[3];
+ int rc;
+
+ /* Test cases, for an item at idx:
+ * 1. remove at idx, then reinsert
+ * 2. remove at idx, remove another item, reinsert
+ * 3. remove another item, remove at idx, reinsert
+ * 4. remove another item, remove at index, remove another item, reinsert
+ *
+ * The indices are the respective 2 bits from the iteration counter,
+ * we use index 3 to indicate skipping that step to handle cases 1-3.
+ */
+ int iteration = pwtest_get_iteration(current_test);
+ int item_idx = iteration & 0x3;
+ int before_idx = (iteration >> 2) & 0x3;
+ int after_idx = (iteration >> 4) & 0x3;
+ const int SKIP = 3;
+
+ if (item_idx == SKIP)
+ return PWTEST_PASS;
+
+ idx[0] = pw_map_insert_new(&map, ptr[0]);
+ idx[1] = pw_map_insert_new(&map, ptr[1]);
+ idx[2] = pw_map_insert_new(&map, ptr[2]);
+
+ if (before_idx != SKIP) {
+ before_idx = idx[before_idx];
+ pw_map_remove(&map, before_idx);
+ }
+ pw_map_remove(&map, item_idx);
+ if (after_idx != SKIP) {
+ after_idx = idx[after_idx];
+ pw_map_remove(&map, after_idx);
+ }
+
+ rc = pw_map_insert_at(&map, item_idx, &new_data);
+ pwtest_neg_errno(rc, -EINVAL);
+ pw_map_clear(&map);
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(pw_map)
+{
+ pwtest_add(map_add_remove, PWTEST_NOARG);
+ pwtest_add(map_insert, PWTEST_NOARG);
+ pwtest_add(map_size, PWTEST_NOARG);
+ pwtest_add(map_double_remove, PWTEST_NOARG);
+ pwtest_add(map_insert_at_free, PWTEST_ARG_RANGE, 0, 63);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-properties.c b/test/test-properties.c
new file mode 100644
index 0000000..89e66bb
--- /dev/null
+++ b/test/test-properties.c
@@ -0,0 +1,629 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+
+#include "config.h"
+
+#include "pwtest.h"
+
+#include "pipewire/properties.h"
+
+PWTEST(properties_abi)
+{
+#if defined(__x86_64__) && defined(__LP64__)
+ pwtest_int_eq(sizeof(struct pw_properties), 24U);
+ return PWTEST_PASS;
+#else
+ fprintf(stderr, "%zd\n", sizeof(struct pw_properties));
+ return PWTEST_SKIP;
+#endif
+}
+
+PWTEST(properties_empty)
+{
+ struct pw_properties *props, *copy;
+ void *state = NULL;
+
+ props = pw_properties_new(NULL, NULL);
+ pwtest_ptr_notnull(props);
+ pwtest_int_eq(props->flags, 0U);
+
+ pwtest_int_eq(props->dict.n_items, 0U);
+ pwtest_ptr_null(pw_properties_get(props, NULL));
+ pwtest_ptr_null(pw_properties_get(props, "unknown"));
+ pwtest_ptr_null(pw_properties_iterate(props, &state));
+
+ pw_properties_clear(props);
+ pwtest_int_eq(props->dict.n_items, 0U);
+ pwtest_ptr_null(pw_properties_get(props, NULL));
+ pwtest_ptr_null(pw_properties_get(props, ""));
+ pwtest_ptr_null(pw_properties_get(props, "unknown"));
+ pwtest_ptr_null(pw_properties_iterate(props, &state));
+
+ copy = pw_properties_copy(props);
+ pwtest_ptr_notnull(copy);
+ pw_properties_free(props);
+
+ pwtest_int_eq(copy->dict.n_items, 0U);
+ pwtest_ptr_null(pw_properties_get(copy, NULL));
+ pwtest_ptr_null(pw_properties_get(copy, ""));
+ pwtest_ptr_null(pw_properties_get(copy, "unknown"));
+ pwtest_ptr_null(pw_properties_iterate(copy, &state));
+
+ pw_properties_free(copy);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_set)
+{
+ struct pw_properties *props, *copy;
+ void *state = NULL;
+ const char *str;
+
+ props = pw_properties_new(NULL, NULL);
+ pwtest_ptr_notnull(props);
+ pwtest_int_eq(props->flags, 0U);
+
+ pwtest_int_eq(pw_properties_set(props, "foo", "bar"), 1);
+ pwtest_int_eq(props->dict.n_items, 1U);
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_int_eq(pw_properties_set(props, "foo", "bar"), 0);
+ pwtest_int_eq(props->dict.n_items, 1U);
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_int_eq(pw_properties_set(props, "foo", "fuz"), 1);
+ pwtest_int_eq(props->dict.n_items, 1U);
+ pwtest_str_eq(pw_properties_get(props, "foo"), "fuz");
+ pwtest_int_eq(pw_properties_set(props, "bar", "foo"), 1);
+ pwtest_int_eq(props->dict.n_items, 2U);
+ pwtest_str_eq(pw_properties_get(props, "bar"), "foo");
+ pwtest_int_eq(pw_properties_set(props, "him", "too"), 1);
+ pwtest_int_eq(props->dict.n_items, 3U);
+ pwtest_str_eq(pw_properties_get(props, "him"), "too");
+ pwtest_int_eq(pw_properties_set(props, "him", NULL), 1);
+ pwtest_int_eq(props->dict.n_items, 2U);
+ pwtest_ptr_null(pw_properties_get(props, "him"));
+ pwtest_int_eq(pw_properties_set(props, "him", NULL), 0);
+ pwtest_int_eq(props->dict.n_items, 2U);
+ pwtest_ptr_null(pw_properties_get(props, "him"));
+
+ pwtest_int_eq(pw_properties_set(props, "", "invalid"), 0);
+ pwtest_int_eq(pw_properties_set(props, NULL, "invalid"), 0);
+
+ str = pw_properties_iterate(props, &state);
+ pwtest_str_ne(str, NULL);
+ pwtest_bool_true((spa_streq(str, "foo") || spa_streq(str, "bar")));
+ str = pw_properties_iterate(props, &state);
+ pwtest_str_ne(str, NULL);
+ pwtest_bool_true((spa_streq(str, "foo") || spa_streq(str, "bar")));
+ str = pw_properties_iterate(props, &state);
+ pwtest_ptr_null(str);
+
+ pwtest_int_eq(pw_properties_set(props, "foo", NULL), 1);
+ pwtest_int_eq(props->dict.n_items, 1U);
+ pwtest_int_eq(pw_properties_set(props, "bar", NULL), 1);
+ pwtest_int_eq(props->dict.n_items, 0U);
+
+ pwtest_int_eq(pw_properties_set(props, "foo", "bar"), 1);
+ pwtest_int_eq(pw_properties_set(props, "bar", "foo"), 1);
+ pwtest_int_eq(pw_properties_set(props, "him", "too"), 1);
+ pwtest_int_eq(props->dict.n_items, 3U);
+
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "foo");
+ pwtest_str_eq(pw_properties_get(props, "him"), "too");
+
+ pw_properties_clear(props);
+ pwtest_int_eq(props->dict.n_items, 0U);
+
+ pwtest_int_eq(pw_properties_set(props, "foo", "bar"), 1);
+ pwtest_int_eq(pw_properties_set(props, "bar", "foo"), 1);
+ pwtest_int_eq(pw_properties_set(props, "him", "too"), 1);
+ pwtest_int_eq(props->dict.n_items, 3U);
+
+ copy = pw_properties_copy(props);
+ pwtest_ptr_notnull(copy);
+ pwtest_int_eq(copy->dict.n_items, 3U);
+ pwtest_str_eq(pw_properties_get(copy, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(copy, "bar"), "foo");
+ pwtest_str_eq(pw_properties_get(copy, "him"), "too");
+
+ pwtest_int_eq(pw_properties_set(copy, "bar", NULL), 1);
+ pwtest_int_eq(pw_properties_set(copy, "foo", NULL), 1);
+ pwtest_int_eq(copy->dict.n_items, 1U);
+ pwtest_str_eq(pw_properties_get(copy, "him"), "too");
+
+ pwtest_int_eq(props->dict.n_items, 3U);
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "foo");
+ pwtest_str_eq(pw_properties_get(props, "him"), "too");
+
+ pw_properties_free(props);
+ pw_properties_free(copy);
+
+ return PWTEST_PASS;
+
+}
+
+PWTEST(properties_new)
+{
+ struct pw_properties *p;
+
+ /* Basic initialization */
+ p = pw_properties_new("k1", "v1", "k2", "v2", NULL);
+ pwtest_ptr_notnull(p);
+ pwtest_str_eq(pw_properties_get(p, "k1"), "v1");
+ pwtest_str_eq(pw_properties_get(p, "k2"), "v2");
+ pwtest_ptr_null(pw_properties_get(p, "k3"));
+ pw_properties_free(p);
+
+ /* Empty initialization should be fine */
+ p = pw_properties_new(NULL, NULL);
+ pwtest_ptr_notnull(p);
+ pwtest_ptr_null(pw_properties_get(p, "k1"));
+ pw_properties_free(p); /* sefault/valgrind only check */
+
+ p = pw_properties_new_string("k1=v1 k2 = v2\tk3\t=\tv3\nk4\n=\nv4");
+ pwtest_str_eq(pw_properties_get(p, "k1"), "v1");
+ pwtest_str_eq(pw_properties_get(p, "k2"), "v2");
+ pwtest_str_eq(pw_properties_get(p, "k3"), "v3");
+ pwtest_str_eq(pw_properties_get(p, "k4"), "v4");
+ pw_properties_free(p);
+
+ p = pw_properties_new("foo", "bar", "bar", "baz", "", "invalid", "him", "too", NULL);
+ pwtest_ptr_notnull(p);
+ pwtest_int_eq(p->flags, 0U);
+ pwtest_int_eq(p->dict.n_items, 3U);
+ pw_properties_free(p);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_new_string)
+{
+ struct pw_properties *props;
+
+ props = pw_properties_new_string("foo=bar bar=baz \"#ignore\"=ignore him=too empty=\"\" =gg");
+ pwtest_ptr_notnull(props);
+ pwtest_int_eq(props->flags, 0U);
+ pwtest_int_eq(props->dict.n_items, 5U);
+
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "baz");
+ pwtest_str_eq(pw_properties_get(props, "#ignore"), "ignore");
+ pwtest_str_eq(pw_properties_get(props, "him"), "too");
+ pwtest_str_eq(pw_properties_get(props, "empty"), "");
+
+ pw_properties_free(props);
+
+ props = pw_properties_new_string("foo=bar bar=baz");
+ pwtest_ptr_notnull(props);
+ pwtest_int_eq(props->flags, 0U);
+ pwtest_int_eq(props->dict.n_items, 2U);
+
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "baz");
+
+ pw_properties_free(props);
+
+ props = pw_properties_new_string("foo=bar bar=\"baz");
+ pwtest_ptr_notnull(props);
+ pwtest_int_eq(props->flags, 0U);
+ pwtest_int_eq(props->dict.n_items, 2U);
+
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "baz");
+
+ pw_properties_free(props);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_free)
+{
+ struct pw_properties *p;
+
+ pw_properties_free(NULL);
+
+ p = pw_properties_new("k1", "v1", "k2", "v2", NULL);
+ pw_properties_clear(p);
+ pw_properties_free(p);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_set_with_alloc)
+{
+ struct pw_properties *p;
+ int nadded;
+
+ /* Set a lot of properties to force reallocation */
+ p = pw_properties_new(NULL, NULL);
+ pwtest_ptr_notnull(p);
+ for (size_t i = 0; i < 1000; i++) {
+ char kbuf[6], vbuf[6];
+ spa_scnprintf(kbuf, sizeof(kbuf), "k%zd", i);
+ spa_scnprintf(vbuf, sizeof(vbuf), "v%zd", i);
+ /* New key, expect 1 */
+ pwtest_int_eq(pw_properties_set(p, kbuf, vbuf), 1);
+ /* No change, expect 0 */
+ pwtest_int_eq(pw_properties_set(p, kbuf, vbuf), 0);
+ pwtest_str_eq(pw_properties_get(p, kbuf), vbuf);
+
+ /* Different value now, expect 1 */
+ pwtest_int_eq(pw_properties_set(p, kbuf, "foo"), 1);
+ pwtest_str_eq(pw_properties_get(p, kbuf), "foo");
+ }
+ pw_properties_free(p);
+
+ /* Adding a NULL value is ignored */
+ p = pw_properties_new(NULL, NULL);
+ nadded = pw_properties_set(p, "key", NULL);
+ pwtest_int_eq(nadded, 0);
+ pw_properties_free(p);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_setf)
+{
+ struct pw_properties *p;
+
+ p = pw_properties_new(NULL, NULL);
+ pwtest_int_eq(pw_properties_setf(p, "foo", "%d.%08x", 657, 0x89342), 1);
+ pwtest_int_eq(p->dict.n_items, 1U);
+ pwtest_str_eq(pw_properties_get(p, "foo"), "657.00089342");
+
+ pwtest_int_eq(pw_properties_setf(p, "", "%f", 189.45f), 0);
+ pwtest_int_eq(pw_properties_setf(p, NULL, "%f", 189.45f), 0);
+ pwtest_int_eq(p->dict.n_items, 1U);
+ pw_properties_free(p);
+
+ /* Set a lot of properties to force reallocation */
+ p = pw_properties_new(NULL, NULL);
+ pwtest_ptr_notnull(p);
+ for (size_t i = 0; i < 1000; i++) {
+ char kbuf[6], vbuf[6];
+ spa_scnprintf(kbuf, sizeof(kbuf), "k%zd", i);
+ spa_scnprintf(vbuf, sizeof(vbuf), "v%zd", i);
+ /* New key, expect 1 */
+ pwtest_int_eq(pw_properties_setf(p, kbuf, "v%zd", i), 1);
+ /* No change, expect 0 */
+ pwtest_int_eq(pw_properties_setf(p, kbuf, "v%zd", i), 0);
+ pwtest_str_eq(pw_properties_get(p, kbuf), vbuf);
+
+ /* Different value now, expect 1 */
+ pwtest_int_eq(pw_properties_set(p, kbuf, "foo"), 1);
+ pwtest_str_eq(pw_properties_get(p, kbuf), "foo");
+ }
+ pw_properties_free(p);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_parse_bool)
+{
+ pwtest_bool_true(pw_properties_parse_bool("true"));
+ pwtest_bool_true(pw_properties_parse_bool("1"));
+ /* only a literal "true" is true */
+ pwtest_bool_false(pw_properties_parse_bool("TRUE"));
+ pwtest_bool_false(pw_properties_parse_bool("True"));
+
+ pwtest_bool_false(pw_properties_parse_bool("false"));
+ pwtest_bool_false(pw_properties_parse_bool("0"));
+ pwtest_bool_false(pw_properties_parse_bool("10"));
+ pwtest_bool_false(pw_properties_parse_bool("-1"));
+ pwtest_bool_false(pw_properties_parse_bool("1x"));
+ pwtest_bool_false(pw_properties_parse_bool("a"));
+
+ return PWTEST_PASS;
+}
+
+
+PWTEST(properties_parse_int)
+{
+ struct test {
+ const char *value;
+ int expected;
+ } tests[] = {
+ { "1", 1 },
+ { "0", 0 },
+ { "-1", -1 },
+ { "+1", +1 },
+ { "0xff", 0xff },
+ { "077", 077 },
+ /* parsing errors */
+ { "x", 0 },
+ { "xk", 0 },
+ { "1k", 0 },
+ { "abc", 0 },
+ { "foo", 0 },
+ { NULL, 0 },
+ { "", 0 },
+ };
+
+ for (size_t i = 0; i < SPA_N_ELEMENTS(tests); i++) {
+ struct test *t = &tests[i];
+ pwtest_int_eq(pw_properties_parse_int(t->value), t->expected);
+ pwtest_int_eq(pw_properties_parse_int64(t->value), t->expected);
+ pwtest_int_eq(pw_properties_parse_uint64(t->value), (uint64_t)t->expected);
+ }
+
+ pwtest_int_eq(pw_properties_parse_int("0xffffffffff"), 0); /* >32 bit */
+ pwtest_int_eq(pw_properties_parse_int64("0xffffffffff"), 0xffffffffff);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_parse_float)
+{
+ struct test {
+ const char *value;
+ double expected;
+ } tests[] = {
+ { "1.0", 1.0 },
+ { "0.0", 0.0 },
+ { "-1.0", -1.0 },
+ { "1.234", 1.234 },
+ /* parsing errors */
+ { "1,0", 0 },
+ { "x", 0 },
+ { "xk", 0 },
+ { "1k", 0 },
+ { "abc", 0 },
+ { "foo", 0 },
+ { NULL, 0 },
+ { "", 0 },
+ };
+
+ for (size_t i = 0; i < SPA_N_ELEMENTS(tests); i++) {
+ struct test *t = &tests[i];
+ pwtest_double_eq(pw_properties_parse_float(t->value), (float)t->expected);
+ pwtest_double_eq(pw_properties_parse_double(t->value), t->expected);
+ }
+
+ return PWTEST_PASS;
+}
+
+
+PWTEST(properties_copy)
+{
+ struct pw_properties *p1, *p2;
+
+ p1 = pw_properties_new("k1", "v1", "k2", "v2", NULL);
+ p2 = pw_properties_copy(p1);
+ pwtest_str_eq(pw_properties_get(p2, "k1"), "v1");
+ pwtest_str_eq(pw_properties_get(p2, "k2"), "v2");
+ pwtest_ptr_null(pw_properties_get(p2, "k3"));
+
+ /* Change in p2, esure p1 remains the same */
+ pw_properties_set(p2, "k1", "changed");
+ pwtest_str_eq(pw_properties_get(p1, "k1"), "v1");
+ pwtest_str_eq(pw_properties_get(p2, "k1"), "changed");
+
+ /* Add to p1, then to p2, check addition is separate */
+ pw_properties_set(p1, "k3", "v3");
+ pwtest_ptr_null(pw_properties_get(p2, "k3"));
+ pw_properties_set(p2, "k3", "new");
+ pwtest_str_eq(pw_properties_get(p2, "k3"), "new");
+
+ pw_properties_free(p1);
+ pw_properties_free(p2);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_update_string)
+{
+ struct pw_properties *p;
+ const char str[] = "k1 = new1 k3 = new3";
+ int nadded;
+
+ p = pw_properties_new("k1", "v1", "k2", "v2", NULL);
+ nadded = pw_properties_update_string(p, str, sizeof(str));
+ pwtest_int_eq(nadded, 2);
+ pwtest_str_eq(pw_properties_get(p, "k1"), "new1");
+ pwtest_str_eq(pw_properties_get(p, "k2"), "v2");
+ pwtest_str_eq(pw_properties_get(p, "k3"), "new3");
+ pw_properties_free(p);
+
+ /* Updating an empty property should be fine */
+ p = pw_properties_new(NULL, NULL);
+ nadded = pw_properties_update_string(p, str, sizeof(str));
+ pwtest_int_eq(nadded, 2);
+ pwtest_str_eq(pw_properties_get(p, "k1"), "new1");
+ pwtest_ptr_null(pw_properties_get(p, "k2"));
+ pwtest_str_eq(pw_properties_get(p, "k3"), "new3");
+ pw_properties_free(p);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_serialize_dict_stack_overflow)
+{
+ char *long_value = NULL;
+ struct spa_dict_item items[2];
+ struct spa_dict dict;
+ const int sz = 8 * 1024 * 1024;
+ char tmpfile[PATH_MAX];
+ FILE *fp;
+ int r;
+
+ /* Alloc a property value long enough to trigger a stack overflow
+ * in any variadic arrays (see * e994949d576e93f8c22)
+ */
+ long_value = calloc(1, sz);
+ if (long_value == 0)
+ return PWTEST_SKIP;
+
+ memset(long_value, 'a', sz - 1);
+ items[0] = SPA_DICT_ITEM_INIT("longval", long_value);
+ items[1] = SPA_DICT_ITEM_INIT(long_value, "longval");
+ dict = SPA_DICT_INIT(items, 2);
+
+ pwtest_mkstemp(tmpfile);
+ fp = fopen(tmpfile, "we");
+ pwtest_ptr_notnull(fp);
+ r = pw_properties_serialize_dict(fp, &dict, 0);
+ pwtest_int_eq(r, 1);
+
+ fclose(fp);
+ free(long_value);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_new_dict)
+{
+ struct pw_properties *props;
+ struct spa_dict_item items[5];
+
+ items[0] = SPA_DICT_ITEM_INIT("foo", "bar");
+ items[1] = SPA_DICT_ITEM_INIT("bar", "baz");
+ items[3] = SPA_DICT_ITEM_INIT("", "invalid");
+ items[4] = SPA_DICT_ITEM_INIT(NULL, "invalid");
+ items[2] = SPA_DICT_ITEM_INIT("him", "too");
+
+ props = pw_properties_new_dict(&SPA_DICT_INIT_ARRAY(items));
+ pwtest_ptr_notnull(props);
+ pwtest_int_eq(props->flags, 0U);
+ pwtest_int_eq(props->dict.n_items, 3U);
+
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "baz");
+ pwtest_str_eq(pw_properties_get(props, "him"), "too");
+
+ pw_properties_free(props);
+
+ return PWTEST_PASS;
+}
+
+
+PWTEST(properties_new_json)
+{
+ struct pw_properties *props;
+
+ props = pw_properties_new_string("{ \"foo\": \"bar\\n\\t\", \"bar\": 1.8, \"empty\": [ \"foo\", \"bar\" ], \"\": \"gg\"");
+ pwtest_ptr_notnull(props);
+ pwtest_int_eq(props->flags, 0U);
+ pwtest_int_eq(props->dict.n_items, 3U);
+
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar\n\t");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "1.8");
+ pwtest_str_eq(pw_properties_get(props, "empty"),
+ "[ \"foo\", \"bar\" ]");
+
+ pw_properties_free(props);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(properties_update)
+{
+ struct pw_properties *props;
+ struct spa_dict_item items[5];
+
+ props = pw_properties_new(NULL, NULL);
+ pwtest_ptr_notnull(props);
+ pwtest_int_eq(props->flags, 0U);
+ pwtest_int_eq(props->dict.n_items, 0U);
+
+ items[0] = SPA_DICT_ITEM_INIT("foo", "bar");
+ items[1] = SPA_DICT_ITEM_INIT("bar", "baz");
+ items[3] = SPA_DICT_ITEM_INIT("", "invalid");
+ items[4] = SPA_DICT_ITEM_INIT(NULL, "invalid");
+ items[2] = SPA_DICT_ITEM_INIT("him", "too");
+ pwtest_int_eq(pw_properties_update(props, &SPA_DICT_INIT_ARRAY(items)),
+ 3);
+ pwtest_int_eq(props->dict.n_items, 3U);
+
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "baz");
+ pwtest_str_eq(pw_properties_get(props, "him"), "too");
+
+ items[0] = SPA_DICT_ITEM_INIT("foo", "bar");
+ items[1] = SPA_DICT_ITEM_INIT("bar", "baz");
+ pwtest_int_eq(pw_properties_update(props, &SPA_DICT_INIT(items, 2)),
+ 0);
+ pwtest_int_eq(props->dict.n_items, 3U);
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "baz");
+ pwtest_str_eq(pw_properties_get(props, "him"), "too");
+
+ items[0] = SPA_DICT_ITEM_INIT("bar", "bear");
+ items[1] = SPA_DICT_ITEM_INIT("him", "too");
+ pwtest_int_eq(pw_properties_update(props, &SPA_DICT_INIT(items, 2)),
+ 1);
+ pwtest_int_eq(props->dict.n_items, 3U);
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "bear");
+ pwtest_str_eq(pw_properties_get(props, "him"), "too");
+
+ items[0] = SPA_DICT_ITEM_INIT("bar", "bear");
+ items[1] = SPA_DICT_ITEM_INIT("him", NULL);
+ pwtest_int_eq(pw_properties_update(props, &SPA_DICT_INIT(items, 2)),
+ 1);
+ pwtest_int_eq(props->dict.n_items, 2U);
+ pwtest_str_eq(pw_properties_get(props, "foo"), "bar");
+ pwtest_str_eq(pw_properties_get(props, "bar"), "bear");
+ pwtest_ptr_null(pw_properties_get(props, "him"));
+
+ items[0] = SPA_DICT_ITEM_INIT("foo", NULL);
+ items[1] = SPA_DICT_ITEM_INIT("bar", "beer");
+ items[2] = SPA_DICT_ITEM_INIT("him", "her");
+ pwtest_int_eq(pw_properties_update(props, &SPA_DICT_INIT(items, 3)),
+ 3);
+ pwtest_int_eq(props->dict.n_items, 2U);
+ pwtest_ptr_null(pw_properties_get(props, "foo"));
+ pwtest_str_eq(pw_properties_get(props, "bar"), "beer");
+ pwtest_str_eq(pw_properties_get(props, "him"), "her");
+
+ pw_properties_free(props);
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(properties)
+{
+ pwtest_add(properties_abi, PWTEST_NOARG);
+ pwtest_add(properties_empty, PWTEST_NOARG);
+ pwtest_add(properties_new, PWTEST_NOARG);
+ pwtest_add(properties_new_string, PWTEST_NOARG);
+ pwtest_add(properties_free, PWTEST_NOARG);
+ pwtest_add(properties_set, PWTEST_NOARG);
+ pwtest_add(properties_set_with_alloc, PWTEST_NOARG);
+ pwtest_add(properties_setf, PWTEST_NOARG);
+ pwtest_add(properties_parse_bool, PWTEST_NOARG);
+ pwtest_add(properties_parse_int, PWTEST_NOARG);
+ pwtest_add(properties_parse_float, PWTEST_NOARG);
+ pwtest_add(properties_copy, PWTEST_NOARG);
+ pwtest_add(properties_update_string, PWTEST_NOARG);
+ pwtest_add(properties_serialize_dict_stack_overflow, PWTEST_NOARG);
+ pwtest_add(properties_new_dict, PWTEST_NOARG);
+ pwtest_add(properties_new_json, PWTEST_NOARG);
+ pwtest_add(properties_update, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-pwtest.c b/test/test-pwtest.c
new file mode 100644
index 0000000..9242d4c
--- /dev/null
+++ b/test/test-pwtest.c
@@ -0,0 +1,55 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include "pwtest.h"
+
+#include <signal.h>
+
+#include "pwtest-compat.c"
+
+PWTEST(compat_sigabbrev_np)
+{
+#ifndef HAVE_SIGABBREV_NP
+ pwtest_str_eq(sigabbrev_np(SIGABRT), "ABRT");
+ pwtest_str_eq(sigabbrev_np(SIGSEGV), "SEGV");
+ pwtest_str_eq(sigabbrev_np(SIGSTOP), "STOP");
+ pwtest_str_eq(sigabbrev_np(SIGCHLD), "CHLD");
+ pwtest_str_eq(sigabbrev_np(SIGTERM), "TERM");
+ pwtest_str_eq(sigabbrev_np(SIGKILL), "KILL");
+ pwtest_str_eq(sigabbrev_np(12345), NULL);
+
+ return PWTEST_PASS;
+#else
+ return PWTEST_SKIP;
+#endif
+}
+
+PWTEST_SUITE(pwtest)
+{
+ pwtest_add(compat_sigabbrev_np, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-spa-buffer.c b/test/test-spa-buffer.c
new file mode 100644
index 0000000..89bfabd
--- /dev/null
+++ b/test/test-spa-buffer.c
@@ -0,0 +1,150 @@
+/* Simple Plugin API
+ * Copyright © 2018 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pwtest.h"
+
+#include <spa/buffer/alloc.h>
+#include <spa/buffer/buffer.h>
+#include <spa/buffer/meta.h>
+
+PWTEST(buffer_abi_types)
+{
+ /* buffer */
+ pwtest_int_eq(SPA_DATA_Invalid, 0);
+ pwtest_int_eq(SPA_DATA_MemPtr, 1);
+ pwtest_int_eq(SPA_DATA_MemFd, 2);
+ pwtest_int_eq(SPA_DATA_DmaBuf, 3);
+ pwtest_int_eq(SPA_DATA_MemId, 4);
+ pwtest_int_eq(_SPA_DATA_LAST, 5);
+
+ /* meta */
+ pwtest_int_eq(SPA_META_Invalid, 0);
+ pwtest_int_eq(SPA_META_Header, 1);
+ pwtest_int_eq(SPA_META_VideoCrop, 2);
+ pwtest_int_eq(SPA_META_VideoDamage, 3);
+ pwtest_int_eq(SPA_META_Bitmap, 4);
+ pwtest_int_eq(SPA_META_Cursor, 5);
+ pwtest_int_eq(SPA_META_Control, 6);
+ pwtest_int_eq(SPA_META_Busy, 7);
+ pwtest_int_eq(SPA_META_VideoTransform, 8);
+ pwtest_int_eq(_SPA_META_LAST, 9);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(buffer_abi_sizes)
+{
+#if defined(__x86_64__) && defined(__LP64__)
+ pwtest_int_eq(sizeof(struct spa_chunk), 16U);
+ pwtest_int_eq(sizeof(struct spa_data), 40U);
+ pwtest_int_eq(sizeof(struct spa_buffer), 24U);
+
+ pwtest_int_eq(sizeof(struct spa_meta), 16U);
+ pwtest_int_eq(sizeof(struct spa_meta_header), 32U);
+ pwtest_int_eq(sizeof(struct spa_meta_region), 16U);
+ pwtest_int_eq(sizeof(struct spa_meta_bitmap), 20U);
+ pwtest_int_eq(sizeof(struct spa_meta_cursor), 28U);
+ pwtest_int_eq(sizeof(struct spa_meta_videotransform), 4U);
+
+ return PWTEST_PASS;
+#else
+ fprintf(stderr, "%zd\n", sizeof(struct spa_chunk));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_data));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_buffer));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_meta));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_meta_header));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_meta_region));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_meta_bitmap));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_meta_cursor));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_meta_videotransform));
+ return PWTEST_SKIP;
+#endif
+}
+
+PWTEST(buffer_alloc)
+{
+ struct spa_buffer **buffers;
+ struct spa_meta metas[4];
+ struct spa_data datas[2];
+ uint32_t aligns[2];
+ uint32_t i, j;
+
+ metas[0].type = SPA_META_Header;
+ metas[0].size = sizeof(struct spa_meta_header);
+ metas[1].type = SPA_META_VideoDamage;
+ metas[1].size = sizeof(struct spa_meta_region) * 16;
+#define CURSOR_META_SIZE(w,h,bpp) (sizeof(struct spa_meta_cursor) + \
+ sizeof(struct spa_meta_bitmap) + w * h * bpp)
+ metas[2].type = SPA_META_Cursor;
+ metas[2].size = CURSOR_META_SIZE(64,64,4);
+ metas[3].type = 101;
+ metas[3].size = 11;
+
+ datas[0].maxsize = 4000;
+ datas[1].maxsize = 2011;
+
+ aligns[0] = 32;
+ aligns[1] = 16;
+
+ buffers = spa_buffer_alloc_array(16, 0,
+ SPA_N_ELEMENTS(metas), metas,
+ SPA_N_ELEMENTS(datas), datas, aligns);
+
+ fprintf(stderr, "buffers %p\n", buffers);
+
+ for (i = 0; i < 16; i++) {
+ struct spa_buffer *b = buffers[i];
+ fprintf(stderr, "buffer %d %p\n", i, b);
+
+ pwtest_int_eq(b->n_metas, SPA_N_ELEMENTS(metas));
+ pwtest_int_eq(b->n_datas, SPA_N_ELEMENTS(datas));
+
+ for (j = 0; j < SPA_N_ELEMENTS(metas); j++) {
+ pwtest_int_eq(b->metas[j].type, metas[j].type);
+ pwtest_int_eq(b->metas[j].size, metas[j].size);
+ fprintf(stderr, " meta %d %p\n", j, b->metas[j].data);
+ pwtest_bool_true(SPA_IS_ALIGNED(b->metas[j].data, 8));
+ }
+
+ for (j = 0; j < SPA_N_ELEMENTS(datas); j++) {
+ pwtest_int_eq(b->datas[j].maxsize, datas[j].maxsize);
+ fprintf(stderr, " data %d %p %p\n", j, b->datas[j].chunk, b->datas[j].data);
+ pwtest_bool_true(SPA_IS_ALIGNED(b->datas[j].chunk, 8));
+ pwtest_bool_true(SPA_IS_ALIGNED(b->datas[j].data, aligns[j]));
+ }
+ }
+ free(buffers);
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(spa_buffer)
+{
+ pwtest_add(buffer_abi_types, PWTEST_NOARG);
+ pwtest_add(buffer_abi_sizes, PWTEST_NOARG);
+ pwtest_add(buffer_alloc, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
+
diff --git a/test/test-spa-json.c b/test/test-spa-json.c
new file mode 100644
index 0000000..2a57039
--- /dev/null
+++ b/test/test-spa-json.c
@@ -0,0 +1,333 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2020 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <locale.h>
+
+#include "pwtest.h"
+
+#include <spa/utils/defs.h>
+#include <spa/utils/json.h>
+#include <spa/utils/string.h>
+
+PWTEST(json_abi)
+{
+#if defined(__x86_64__) && defined(__LP64__)
+ pwtest_int_eq(sizeof(struct spa_json), 32U);
+ return PWTEST_PASS;
+#else
+ fprintf(stderr, "%zd\n", sizeof(struct spa_json));
+ return PWTEST_SKIP;
+#endif
+}
+
+#define TYPE_OBJECT 0
+#define TYPE_ARRAY 1
+#define TYPE_STRING 2
+#define TYPE_BOOL 3
+#define TYPE_NULL 4
+#define TYPE_TRUE 5
+#define TYPE_FALSE 6
+#define TYPE_FLOAT 7
+
+static void check_type(int type, const char *value, int len)
+{
+ pwtest_bool_eq(spa_json_is_object(value, len), (type == TYPE_OBJECT));
+ pwtest_bool_eq(spa_json_is_array(value, len), (type == TYPE_ARRAY));
+ pwtest_bool_eq(spa_json_is_string(value, len), (type == TYPE_STRING));
+ pwtest_bool_eq(spa_json_is_bool(value, len),
+ (type == TYPE_BOOL || type == TYPE_TRUE || type == TYPE_FALSE));
+ pwtest_bool_eq(spa_json_is_null(value, len), (type == TYPE_NULL));
+ pwtest_bool_eq(spa_json_is_true(value, len), (type == TYPE_TRUE || type == TYPE_BOOL));
+ pwtest_bool_eq(spa_json_is_false(value, len), (type == TYPE_FALSE || type == TYPE_BOOL));
+ pwtest_bool_eq(spa_json_is_float(value, len), (type == TYPE_FLOAT));
+}
+
+static void expect_type(struct spa_json *it, int type)
+{
+ const char *value;
+ int len;
+ pwtest_int_gt((len = spa_json_next(it, &value)), 0);
+ check_type(type, value, len);
+}
+
+static void expect_string(struct spa_json *it, const char *str)
+{
+ const char *value;
+ int len;
+ char *s;
+ pwtest_int_gt((len = spa_json_next(it, &value)), 0);
+ check_type(TYPE_STRING, value, len);
+ s = alloca(len+1);
+ spa_json_parse_stringn(value, len, s, len+1);
+ pwtest_str_eq(s, str);
+}
+static void expect_float(struct spa_json *it, float val)
+{
+ const char *value;
+ int len;
+ float f = 0.0f;
+ pwtest_int_gt((len = spa_json_next(it, &value)), 0);
+ check_type(TYPE_FLOAT, value, len);
+ pwtest_int_gt(spa_json_parse_float(value, len, &f), 0);
+ pwtest_double_eq(f, val);
+}
+
+PWTEST(json_parse)
+{
+ struct spa_json it[5];
+ const char *json = " { "
+ "\"foo\": \"bar\","
+ "\"foo\\\" \": true, "
+ "\"foo \\n\\r\\t\": false,"
+ " \" arr\": [ true, false, null, 5, 5.7, \"str]\"],"
+ "\"foo 2\": null,"
+ "\"foo 3\": 1,"
+ " \"obj\": { \"ba } z\": false, \"empty\": [], \"foo\": { }, \"1.9\", 1.9 },"
+ "\"foo 4\" : 1.8, "
+ "\"foo 5\": -1.8 , "
+ "\"foo 6\": +2.8 ,"
+ " } ", *value;
+
+ spa_json_init(&it[0], json, strlen(json));
+
+ expect_type(&it[0], TYPE_OBJECT);
+ spa_json_enter(&it[0], &it[1]);
+ expect_string(&it[1], "foo");
+ expect_string(&it[1], "bar");
+ expect_string(&it[1], "foo\" ");
+ expect_type(&it[1], TYPE_TRUE);
+ expect_string(&it[1], "foo \n\r\t");
+ expect_type(&it[1], TYPE_FALSE);
+ expect_string(&it[1], " arr");
+ expect_type(&it[1], TYPE_ARRAY);
+ spa_json_enter(&it[1], &it[2]);
+ expect_string(&it[1], "foo 2");
+ expect_type(&it[1], TYPE_NULL);
+ expect_string(&it[1], "foo 3");
+ expect_float(&it[1], 1.f);
+ expect_string(&it[1], "obj");
+ expect_type(&it[1], TYPE_OBJECT);
+ spa_json_enter(&it[1], &it[3]);
+ expect_string(&it[1], "foo 4");
+ expect_float(&it[1], 1.8f);
+ expect_string(&it[1], "foo 5");
+ expect_float(&it[1], -1.8f);
+ expect_string(&it[1], "foo 6");
+ expect_float(&it[1], +2.8f);
+ /* in the array */
+ expect_type(&it[2], TYPE_TRUE);
+ expect_type(&it[2], TYPE_FALSE);
+ expect_type(&it[2], TYPE_NULL);
+ expect_float(&it[2], 5.f);
+ expect_float(&it[2], 5.7f);
+ expect_string(&it[2], "str]");
+ /* in the object */
+ expect_string(&it[3], "ba } z");
+ expect_type(&it[3], TYPE_FALSE);
+ expect_string(&it[3], "empty");
+ expect_type(&it[3], TYPE_ARRAY);
+ spa_json_enter(&it[3], &it[4]);
+ pwtest_int_eq(spa_json_next(&it[4], &value), 0);
+ expect_string(&it[3], "foo");
+ expect_type(&it[3], TYPE_OBJECT);
+ spa_json_enter(&it[3], &it[4]);
+ expect_string(&it[3], "1.9");
+ expect_float(&it[3], 1.9f);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(json_encode)
+{
+ char dst[128];
+ char dst4[4];
+ char dst6[6];
+ char result[1024];
+ pwtest_int_eq(spa_json_encode_string(dst, sizeof(dst), "test"), 6);
+ pwtest_str_eq(dst, "\"test\"");
+ pwtest_int_eq(spa_json_encode_string(dst4, sizeof(dst4), "test"), 6);
+ pwtest_int_eq(strncmp(dst4, "\"tes", 4), 0);
+ pwtest_int_eq(spa_json_encode_string(dst6, sizeof(dst6), "test"), 6);
+ pwtest_int_eq(strncmp(dst6, "\"test\"", 6), 0);
+ pwtest_int_eq(spa_json_encode_string(dst, sizeof(dst), "test\"\n\r \t\b\f\'"), 20);
+ pwtest_str_eq(dst, "\"test\\\"\\n\\r \\t\\b\\f'\"");
+ pwtest_int_eq(spa_json_encode_string(dst, sizeof(dst), "\x04\x05\x1f\x20\x01\x7f\x90"), 29);
+ pwtest_str_eq(dst, "\"\\u0004\\u0005\\u001f \\u0001\x7f\x90\"");
+ pwtest_int_eq(spa_json_parse_stringn(dst, sizeof(dst), result, sizeof(result)), 1);
+ pwtest_str_eq(result, "\x04\x05\x1f\x20\x01\x7f\x90");
+ strcpy(dst, "\"\\u03b2a\"");
+ pwtest_int_eq(spa_json_parse_stringn(dst, sizeof(dst), result, sizeof(result)), 1);
+ pwtest_str_eq(result, "\316\262a");
+ strcpy(dst, "\"\\u 03b2a \"");
+ pwtest_int_eq(spa_json_parse_stringn(dst, sizeof(dst), result, sizeof(result)), 1);
+ pwtest_str_eq(result, "u 03b2a ");
+
+ return PWTEST_PASS;
+}
+
+static void test_array(char *str, char **vals)
+{
+ struct spa_json it[2];
+ char val[256];
+ int i;
+
+ 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));
+ for (i = 0; vals[i]; i++) {
+ pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
+ pwtest_str_eq(val, vals[i]);
+ }
+}
+
+PWTEST(json_array)
+{
+ test_array("FL,FR", (char *[]){ "FL", "FR", NULL });
+ test_array(" FL , FR ", (char *[]){ "FL", "FR", NULL });
+ test_array("[ FL , FR ]", (char *[]){ "FL", "FR", NULL });
+ test_array("[FL FR]", (char *[]){ "FL", "FR", NULL });
+ test_array("FL FR", (char *[]){ "FL", "FR", NULL });
+ test_array("[ FL FR ]", (char *[]){ "FL", "FR", NULL });
+
+ return PWTEST_PASS;
+}
+
+PWTEST(json_overflow)
+{
+ struct spa_json it[2];
+ char val[3];
+ const char *str = "[ F, FR, FRC ]";
+
+ spa_json_init(&it[0], str, strlen(str));
+ pwtest_int_gt(spa_json_enter_array(&it[0], &it[1]), 0);
+
+ pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
+ pwtest_str_eq(val, "F");
+ pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
+ pwtest_str_eq(val, "FR");
+ pwtest_int_lt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(json_float)
+{
+ struct {
+ const char *str;
+ double val;
+ } val[] = {
+ { "0.0", 0.0f },
+ { ".0", 0.0f },
+ { ".0E0", 0.0E0f },
+ { "1.0", 1.0f },
+ { "1.011", 1.011f },
+ { "176543.123456", 176543.123456f },
+ { "-176543.123456", -176543.123456f },
+ { "-5678.5432E10", -5678.5432E10f },
+ { "-5678.5432e10", -5678.5432e10f },
+ { "-5678.5432e-10", -5678.5432e-10f },
+ { "5678.5432e+10", 5678.5432e+10f },
+ { "00.000100", 00.000100f },
+ { "-0.000100", -0.000100f },
+ };
+ float v;
+ unsigned i;
+ char buf1[128], buf2[128], *b1 = buf1, *b2 = buf2;
+
+ pwtest_int_eq(spa_json_parse_float("", 0, &v), 0);
+
+ setlocale(LC_NUMERIC, "C");
+ for (i = 0; i < SPA_N_ELEMENTS(val); i++) {
+ pwtest_int_gt(spa_json_parse_float(val[i].str, strlen(val[i].str), &v), 0);
+ pwtest_double_eq(v, val[i].val);
+ }
+ setlocale(LC_NUMERIC, "fr_FR");
+ for (i = 0; i < SPA_N_ELEMENTS(val); i++) {
+ pwtest_int_gt(spa_json_parse_float(val[i].str, strlen(val[i].str), &v), 0);
+ pwtest_double_eq(v, val[i].val);
+ }
+ pwtest_ptr_eq(spa_json_format_float(buf1, sizeof(buf1), 0.0f), b1);
+ pwtest_str_eq(buf1, "0.000000");
+ pwtest_ptr_eq(spa_json_format_float(buf1, sizeof(buf1), NAN), b1);
+ pwtest_str_eq(buf1, "0.000000");
+ pwtest_ptr_eq(spa_json_format_float(buf1, sizeof(buf1), INFINITY), b1);
+ pwtest_ptr_eq(spa_json_format_float(buf2, sizeof(buf2), FLT_MAX), b2);
+ pwtest_str_eq(buf1, buf2);
+ pwtest_ptr_eq(spa_json_format_float(buf1, sizeof(buf1), -INFINITY), b1);
+ pwtest_ptr_eq(spa_json_format_float(buf2, sizeof(buf2), FLT_MIN), b2);
+ pwtest_str_eq(buf1, buf2);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(json_float_check)
+{
+ struct {
+ const char *str;
+ int res;
+ } val[] = {
+ { "0.0", 1 },
+ { ".0", 1 },
+ { "+.0E0", 1 },
+ { "-.0e0", 1 },
+
+ { "0,0", 0 },
+ { "0.0.5", 0 },
+ { "0x0", 0 },
+ { "0x0.0", 0 },
+ { "E10", 0 },
+ { "e20", 0 },
+ { " 0.0", 0 },
+ { "0.0 ", 0 },
+ { " 0.0 ", 0 },
+ };
+ unsigned i;
+ float v;
+
+ for (i = 0; i < SPA_N_ELEMENTS(val); i++) {
+ pwtest_int_eq(spa_json_parse_float(val[i].str, strlen(val[i].str), &v), val[i].res);
+ }
+ return PWTEST_PASS;
+}
+
+PWTEST(json_int)
+{
+ int v;
+ pwtest_int_eq(spa_json_parse_int("", 0, &v), 0);
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(spa_json)
+{
+ pwtest_add(json_abi, PWTEST_NOARG);
+ pwtest_add(json_parse, PWTEST_NOARG);
+ pwtest_add(json_encode, PWTEST_NOARG);
+ pwtest_add(json_array, PWTEST_NOARG);
+ pwtest_add(json_overflow, PWTEST_NOARG);
+ pwtest_add(json_float, PWTEST_NOARG);
+ pwtest_add(json_float_check, PWTEST_NOARG);
+ pwtest_add(json_int, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-spa-log.c b/test/test-spa-log.c
new file mode 100644
index 0000000..a90ed73
--- /dev/null
+++ b/test/test-spa-log.c
@@ -0,0 +1,213 @@
+/* Simple Plugin API
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/support/log.h>
+
+#include "pwtest.h"
+
+#define OTHER_ARGS const char *file, int line, const char *func, const char *fmt
+
+struct data {
+ bool invoked;
+ const char *func;
+ const char *msg;
+ const struct spa_log_topic *topic;
+};
+
+
+static void impl_log_log(void *object, enum spa_log_level level, OTHER_ARGS, ...) {
+ struct data *data = object;
+ *data = (struct data) {
+ .func = __func__,
+ .invoked = true,
+ .msg = fmt,
+ .topic = NULL,
+ };
+};
+
+static void impl_log_logv(void *object, enum spa_log_level level, OTHER_ARGS, va_list args) {
+ struct data *data = object;
+ *data = (struct data) {
+ .func = __func__,
+ .invoked = true,
+ .msg = fmt,
+ .topic = NULL,
+ };
+};
+
+static void impl_log_logt(void *object, enum spa_log_level level, const struct spa_log_topic *topic, OTHER_ARGS, ...) {
+ struct data *data = object;
+ *data = (struct data) {
+ .func = __func__,
+ .invoked = true,
+ .msg = fmt,
+ .topic = topic,
+ };
+};
+
+static void impl_log_logtv(void *object, enum spa_log_level level, const struct spa_log_topic *topic, OTHER_ARGS, va_list args) {
+ struct data *data = object;
+ *data = (struct data) {
+ .func = __func__,
+ .invoked = true,
+ .msg = fmt,
+ .topic = topic,
+ };
+};
+
+PWTEST(utils_log_logt)
+{
+ struct spa_log_methods impl_log = {
+ SPA_VERSION_LOG_METHODS,
+ .log = impl_log_log,
+ .logv = impl_log_logv,
+ .logt = impl_log_logt,
+ .logtv = impl_log_logtv,
+ };
+ struct spa_log log;
+ struct data data;
+ struct spa_log_topic topic = {
+ .version = 0,
+ .topic = "log topic",
+ .level = SPA_LOG_LEVEL_DEBUG,
+ };
+
+ log.level = SPA_LOG_LEVEL_DEBUG;
+ log.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Log, 0, &impl_log, &data);
+
+ impl_log.version = 0;
+
+ /* impl_log is v0 so we expect the non-topic function to be called */
+ spa_log_debug(&log, "call v0");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.func, "impl_log_log");
+ pwtest_str_eq(data.msg, "call v0");
+ pwtest_ptr_null(data.topic);
+ data.invoked = false;
+
+ /* impl_log is v0 so we expect the topic to be ignored */
+ spa_logt_debug(&log, &topic, "call v0 logt");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.func, "impl_log_log");
+ pwtest_str_eq(data.msg, "call v0 logt");
+ pwtest_ptr_null(data.topic);
+ data.invoked = false;
+
+ impl_log.version = SPA_VERSION_LOG_METHODS;
+
+ /* impl_log is v1 so we expect logt to be called */
+ spa_log_debug(&log, "call v1");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.func, "impl_log_logt");
+ pwtest_str_eq(data.msg, "call v1");
+ pwtest_ptr_null(data.topic);
+ data.invoked = false;
+
+ /* impl_log is v1 so we expect the topic to be passed through */
+ spa_logt_debug(&log, &topic, "call v1 logt");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.func, "impl_log_logt");
+ pwtest_str_eq(data.msg, "call v1 logt");
+ pwtest_ptr_eq(data.topic, &topic);
+ data.invoked = false;
+
+ /* simulated:
+ * impl_log is v1 but we have an old caller that uses v0, this goes
+ * through to the non-topic log function */
+ spa_interface_call(&log.iface, struct spa_log_methods, log, 0,
+ SPA_LOG_LEVEL_DEBUG, "file", 123, "function", "call from v0");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.func, "impl_log_log");
+ pwtest_str_eq(data.msg, "call from v0");
+ pwtest_ptr_null(data.topic);
+ data.invoked = false;
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_log_logt_levels)
+{
+ struct spa_log_methods impl_log = {
+ SPA_VERSION_LOG_METHODS,
+ .log = impl_log_log,
+ .logv = impl_log_logv,
+ .logt = impl_log_logt,
+ .logtv = impl_log_logtv,
+ };
+ struct spa_log log;
+ struct data data;
+ struct spa_log_topic topic = {
+ .version = 0,
+ .topic = "log topic",
+ .level = SPA_LOG_LEVEL_INFO,
+ .has_custom_level = true,
+ };
+
+ log.level = SPA_LOG_LEVEL_DEBUG;
+ log.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Log, 0, &impl_log, &data);
+
+ /* Topic is NULL for spa_log_*, so expect this to be invoked */
+ spa_log_debug(&log, "spa_log_debug");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.msg, "spa_log_debug");
+ pwtest_ptr_null(data.topic);
+ data.invoked = false;
+
+ spa_log_info(&log, "spa_log_info");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.msg, "spa_log_info");
+ pwtest_ptr_null(data.topic);
+ data.invoked = false;
+
+ spa_log_warn(&log, "spa_log_warn");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.msg, "spa_log_warn");
+ pwtest_ptr_null(data.topic);
+ data.invoked = false;
+
+ spa_logt_debug(&log, &topic, "spa_logt_debug");
+ pwtest_bool_false(data.invoked);
+ data.invoked = false;
+
+ spa_logt_info(&log, &topic, "spa_logt_info");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.msg, "spa_logt_info");
+ pwtest_ptr_eq(data.topic, &topic);
+ data.invoked = false;
+
+ spa_logt_warn(&log, &topic, "spa_logt_warn");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.msg, "spa_logt_warn");
+ pwtest_ptr_eq(data.topic, &topic);
+ data.invoked = false;
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(spa_log)
+{
+ pwtest_add(utils_log_logt, PWTEST_NOARG);
+ pwtest_add(utils_log_logt_levels, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-spa-node.c b/test/test-spa-node.c
new file mode 100644
index 0000000..196bd03
--- /dev/null
+++ b/test/test-spa-node.c
@@ -0,0 +1,251 @@
+/* Simple Plugin API
+ *
+ * Copyright © 2019 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/utils/defs.h>
+#include <spa/node/node.h>
+#include <spa/node/io.h>
+#include <spa/node/command.h>
+#include <spa/node/event.h>
+
+#include "pwtest.h"
+
+PWTEST(node_io_abi_sizes)
+{
+#if defined(__x86_64__) && defined(__LP64__)
+ pwtest_int_eq(sizeof(struct spa_io_buffers), 8U);
+ pwtest_int_eq(sizeof(struct spa_io_memory), 16U);
+ pwtest_int_eq(sizeof(struct spa_io_range), 16U);
+ pwtest_int_eq(sizeof(struct spa_io_clock), 160U);
+ pwtest_int_eq(sizeof(struct spa_io_latency), 24U);
+ pwtest_int_eq(sizeof(struct spa_io_sequence), 16U);
+ pwtest_int_eq(sizeof(struct spa_io_segment_bar), 64U);
+ pwtest_int_eq(sizeof(struct spa_io_segment_video), 80U);
+ pwtest_int_eq(sizeof(struct spa_io_segment), 184U);
+
+ pwtest_int_eq(sizeof(struct spa_io_position), 1688U);
+ pwtest_int_eq(sizeof(struct spa_io_rate_match), 48U);
+
+ spa_assert_se(sizeof(struct spa_node_info) == 48);
+ spa_assert_se(sizeof(struct spa_port_info) == 48);
+
+ spa_assert_se(sizeof(struct spa_result_node_error) == 8);
+ spa_assert_se(sizeof(struct spa_result_node_params) == 24);
+
+ return PWTEST_PASS;
+#else
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_buffers));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_memory));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_range));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_clock));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_latency));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_sequence));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_segment_bar));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_segment_video));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_segment));
+
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_position));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_io_rate_match));
+
+ fprintf(stderr, "%zd\n", sizeof(struct spa_node_info));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_port_info));
+
+ fprintf(stderr, "%zd\n", sizeof(struct spa_result_node_error));
+ fprintf(stderr, "%zd\n", sizeof(struct spa_result_node_params));
+
+ return PWTEST_SKIP;
+#endif
+
+}
+
+PWTEST(node_io_abi)
+{
+ /* io */
+ pwtest_int_eq(SPA_IO_Invalid, 0);
+ pwtest_int_eq(SPA_IO_Buffers, 1);
+ pwtest_int_eq(SPA_IO_Range, 2);
+ pwtest_int_eq(SPA_IO_Clock, 3);
+ pwtest_int_eq(SPA_IO_Latency, 4);
+ pwtest_int_eq(SPA_IO_Control, 5);
+ pwtest_int_eq(SPA_IO_Notify, 6);
+ pwtest_int_eq(SPA_IO_Position, 7);
+ pwtest_int_eq(SPA_IO_RateMatch, 8);
+ pwtest_int_eq(SPA_IO_Memory, 9);
+
+ /* position state */
+ pwtest_int_eq(SPA_IO_POSITION_STATE_STOPPED, 0);
+ pwtest_int_eq(SPA_IO_POSITION_STATE_STARTING, 1);
+ pwtest_int_eq(SPA_IO_POSITION_STATE_RUNNING, 2);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(node_command_abi)
+{
+ pwtest_int_eq(SPA_NODE_COMMAND_Suspend, 0);
+ pwtest_int_eq(SPA_NODE_COMMAND_Pause, 1);
+ pwtest_int_eq(SPA_NODE_COMMAND_Start, 2);
+ pwtest_int_eq(SPA_NODE_COMMAND_Enable, 3);
+ pwtest_int_eq(SPA_NODE_COMMAND_Disable, 4);
+ pwtest_int_eq(SPA_NODE_COMMAND_Flush, 5);
+ pwtest_int_eq(SPA_NODE_COMMAND_Drain, 6);
+ pwtest_int_eq(SPA_NODE_COMMAND_Marker, 7);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(node_event_abi)
+{
+ pwtest_int_eq(SPA_NODE_EVENT_Error, 0);
+ pwtest_int_eq(SPA_NODE_EVENT_Buffering, 1);
+ pwtest_int_eq(SPA_NODE_EVENT_RequestRefresh, 2);
+
+ return PWTEST_PASS;
+}
+
+#define TEST_FUNC(a,b,func, id) \
+do { \
+ off_t diff = SPA_PTRDIFF(&a.func, &a); \
+ a.func = b.func; \
+ pwtest_ptr_eq(diff, SPA_PTRDIFF(&b.func, &b)); \
+ pwtest_bool_true(diff == 0 || (diff-1)/sizeof(void*) == id); \
+} while(0)
+
+PWTEST(node_node_abi)
+{
+ struct spa_node_events e;
+ struct spa_node_callbacks c;
+ struct spa_node_methods m;
+ struct {
+ uint32_t version;
+ void (*info) (void *data, const struct spa_node_info *info);
+ void (*port_info) (void *data,
+ enum spa_direction direction, uint32_t port,
+ const struct spa_port_info *info);
+ void (*result) (void *data, int seq, int res,
+ uint32_t type, const void *result);
+ void (*event) (void *data, const struct spa_event *event);
+ } events = { SPA_VERSION_NODE_EVENTS, };
+ struct {
+ uint32_t version;
+ int (*ready) (void *data, int state);
+ int (*reuse_buffer) (void *data,
+ uint32_t port_id,
+ uint32_t buffer_id);
+ int (*xrun) (void *data, uint64_t trigger, uint64_t delay,
+ struct spa_pod *info);
+ } callbacks = { SPA_VERSION_NODE_CALLBACKS, };
+ struct {
+ uint32_t version;
+ int (*add_listener) (void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data);
+ int (*set_callbacks) (void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data);
+ int (*sync) (void *object, int seq);
+ int (*enum_params) (void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t max,
+ const struct spa_pod *filter);
+ int (*set_param) (void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ int (*set_io) (void *object,
+ uint32_t id, void *data, size_t size);
+ int (*send_command) (void *object, const struct spa_command *command);
+ int (*add_port) (void *object,
+ enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props);
+ int (*remove_port) (void *object,
+ enum spa_direction direction, uint32_t port_id);
+ int (*port_enum_params) (void *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);
+ int (*port_set_param) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param);
+ int (*port_use_buffers) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers);
+ int (*port_set_io) (void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size);
+ int (*port_reuse_buffer) (void *object, uint32_t port_id, uint32_t buffer_id);
+ int (*process) (void *object);
+ } methods = { SPA_VERSION_NODE_METHODS, 0 };
+
+ TEST_FUNC(e, events, version, 0);
+ TEST_FUNC(e, events, info, SPA_NODE_EVENT_INFO);
+ TEST_FUNC(e, events, port_info, SPA_NODE_EVENT_PORT_INFO);
+ TEST_FUNC(e, events, result, SPA_NODE_EVENT_RESULT);
+ TEST_FUNC(e, events, event, SPA_NODE_EVENT_EVENT);
+ pwtest_int_eq(SPA_NODE_EVENT_NUM, 4);
+ pwtest_int_eq(sizeof(e), sizeof(events));
+
+ TEST_FUNC(c, callbacks, version, 0);
+ TEST_FUNC(c, callbacks, ready, SPA_NODE_CALLBACK_READY);
+ TEST_FUNC(c, callbacks, reuse_buffer, SPA_NODE_CALLBACK_REUSE_BUFFER);
+ TEST_FUNC(c, callbacks, xrun, SPA_NODE_CALLBACK_XRUN);
+ pwtest_int_eq(SPA_NODE_CALLBACK_NUM, 3);
+ pwtest_int_eq(sizeof(c), sizeof(callbacks));
+
+ TEST_FUNC(m, methods, version, 0);
+ TEST_FUNC(m, methods, add_listener, SPA_NODE_METHOD_ADD_LISTENER);
+ TEST_FUNC(m, methods, set_callbacks, SPA_NODE_METHOD_SET_CALLBACKS);
+ TEST_FUNC(m, methods, sync, SPA_NODE_METHOD_SYNC);
+ TEST_FUNC(m, methods, enum_params, SPA_NODE_METHOD_ENUM_PARAMS);
+ TEST_FUNC(m, methods, set_param, SPA_NODE_METHOD_SET_PARAM);
+ TEST_FUNC(m, methods, set_io, SPA_NODE_METHOD_SET_IO);
+ TEST_FUNC(m, methods, send_command, SPA_NODE_METHOD_SEND_COMMAND);
+ TEST_FUNC(m, methods, add_port, SPA_NODE_METHOD_ADD_PORT);
+ TEST_FUNC(m, methods, remove_port, SPA_NODE_METHOD_REMOVE_PORT);
+ TEST_FUNC(m, methods, port_enum_params, SPA_NODE_METHOD_PORT_ENUM_PARAMS);
+ TEST_FUNC(m, methods, port_use_buffers, SPA_NODE_METHOD_PORT_USE_BUFFERS);
+ TEST_FUNC(m, methods, port_set_io, SPA_NODE_METHOD_PORT_SET_IO);
+ TEST_FUNC(m, methods, port_reuse_buffer, SPA_NODE_METHOD_PORT_REUSE_BUFFER);
+ TEST_FUNC(m, methods, process, SPA_NODE_METHOD_PROCESS);
+ pwtest_int_eq(SPA_NODE_METHOD_NUM, 15);
+ pwtest_int_eq(sizeof(m), sizeof(methods));
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(spa_node)
+{
+ pwtest_add(node_io_abi_sizes, PWTEST_NOARG);
+ pwtest_add(node_io_abi, PWTEST_NOARG);
+ pwtest_add(node_command_abi, PWTEST_NOARG);
+ pwtest_add(node_event_abi, PWTEST_NOARG);
+ pwtest_add(node_node_abi, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-spa-pod.c b/test/test-spa-pod.c
new file mode 100644
index 0000000..24c3303
--- /dev/null
+++ b/test/test-spa-pod.c
@@ -0,0 +1,1706 @@
+/* Simple Plugin API
+ * Copyright © 2019 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <spa/pod/pod.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/command.h>
+#include <spa/pod/event.h>
+#include <spa/pod/iter.h>
+#include <spa/pod/parser.h>
+#include <spa/pod/vararg.h>
+#include <spa/debug/pod.h>
+#include <spa/param/format.h>
+#include <spa/param/video/raw.h>
+#include <spa/utils/string.h>
+
+#include "pwtest.h"
+
+PWTEST(pod_abi_sizes)
+{
+#if defined(__x86_64__) && defined(__LP64__)
+ spa_assert_se(sizeof(struct spa_pod) == 8);
+ spa_assert_se(sizeof(struct spa_pod_bool) == 16);
+ spa_assert_se(sizeof(struct spa_pod_id) == 16);
+ spa_assert_se(sizeof(struct spa_pod_int) == 16);
+ spa_assert_se(sizeof(struct spa_pod_long) == 16);
+ spa_assert_se(sizeof(struct spa_pod_float) == 16);
+ spa_assert_se(sizeof(struct spa_pod_double) == 16);
+ spa_assert_se(sizeof(struct spa_pod_string) == 8);
+ spa_assert_se(sizeof(struct spa_pod_bytes) == 8);
+ spa_assert_se(sizeof(struct spa_pod_rectangle) == 16);
+ spa_assert_se(sizeof(struct spa_pod_fraction) == 16);
+ spa_assert_se(sizeof(struct spa_pod_bitmap) == 8);
+ spa_assert_se(sizeof(struct spa_pod_array_body) == 8);
+ spa_assert_se(sizeof(struct spa_pod_array) == 16);
+
+ spa_assert_se(sizeof(struct spa_pod_choice_body) == 16);
+ spa_assert_se(sizeof(struct spa_pod_choice) == 24);
+ spa_assert_se(sizeof(struct spa_pod_struct) == 8);
+ spa_assert_se(sizeof(struct spa_pod_object_body) == 8);
+ spa_assert_se(sizeof(struct spa_pod_object) == 16);
+ spa_assert_se(sizeof(struct spa_pod_pointer_body) == 16);
+ spa_assert_se(sizeof(struct spa_pod_pointer) == 24);
+ spa_assert_se(sizeof(struct spa_pod_fd) == 16);
+ spa_assert_se(sizeof(struct spa_pod_prop) == 16);
+ spa_assert_se(sizeof(struct spa_pod_control) == 16);
+ spa_assert_se(sizeof(struct spa_pod_sequence_body) == 8);
+ spa_assert_se(sizeof(struct spa_pod_sequence) == 16);
+
+ /* builder */
+ spa_assert_se(sizeof(struct spa_pod_frame) == 24);
+ spa_assert_se(sizeof(struct spa_pod_builder_state) == 16);
+ spa_assert_se(sizeof(struct spa_pod_builder) == 48);
+
+ /* command */
+ spa_assert_se(sizeof(struct spa_command_body) == 8);
+ spa_assert_se(sizeof(struct spa_command) == 16);
+
+ /* event */
+ spa_assert_se(sizeof(struct spa_event_body) == 8);
+ spa_assert_se(sizeof(struct spa_event) == 16);
+
+ /* parser */
+ spa_assert_se(sizeof(struct spa_pod_parser_state) == 16);
+ spa_assert_se(sizeof(struct spa_pod_parser) == 32);
+
+ return PWTEST_PASS;
+#endif
+ return PWTEST_SKIP;
+}
+
+PWTEST(pod_abi)
+{
+ spa_assert_se(SPA_CHOICE_None == 0);
+ spa_assert_se(SPA_CHOICE_Range == 1);
+ spa_assert_se(SPA_CHOICE_Step == 2);
+ spa_assert_se(SPA_CHOICE_Enum == 3);
+ spa_assert_se(SPA_CHOICE_Flags == 4);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_init)
+{
+ {
+ struct spa_pod pod = SPA_POD_INIT(sizeof(int64_t), SPA_TYPE_Long);
+ int32_t val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == sizeof(int64_t) + 8);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == sizeof(int64_t));
+ spa_assert_se(SPA_POD_CONTENTS_SIZE(struct spa_pod, &pod) == sizeof(int64_t));
+ spa_assert_se(spa_pod_is_long(&pod));
+
+ pod = SPA_POD_INIT(sizeof(int32_t), SPA_TYPE_Int);
+ spa_assert_se(SPA_POD_SIZE(&pod) == sizeof(int32_t) + 8);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Int);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == sizeof(int32_t));
+ spa_assert_se(SPA_POD_CONTENTS_SIZE(struct spa_pod, &pod) == sizeof(int32_t));
+ spa_assert_se(spa_pod_is_int(&pod));
+
+ /** too small */
+ pod = SPA_POD_INIT(0, SPA_TYPE_Int);
+ spa_assert_se(!spa_pod_is_int(&pod));
+ spa_assert_se(spa_pod_get_int(&pod, &val) < 0);
+ }
+ {
+ struct spa_pod pod = SPA_POD_INIT_None();
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 8);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_None);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 0);
+ spa_assert_se(SPA_POD_CONTENTS_SIZE(struct spa_pod, &pod) == 0);
+ spa_assert_se(spa_pod_is_none(&pod));
+ }
+ {
+ struct spa_pod_bool pod = SPA_POD_INIT_Bool(true);
+ bool val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 12);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Bool);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 4);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_bool, &pod) == true);
+ spa_assert_se(spa_pod_is_bool(&pod.pod));
+ spa_assert_se(spa_pod_get_bool(&pod.pod, &val) == 0);
+ spa_assert_se(val == true);
+
+ pod = SPA_POD_INIT_Bool(false);
+ spa_assert_se(SPA_POD_SIZE(&pod) == 12);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Bool);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 4);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_bool, &pod) == false);
+ spa_assert_se(spa_pod_is_bool(&pod.pod));
+ spa_assert_se(spa_pod_get_bool(&pod.pod, &val) == 0);
+ spa_assert_se(val == false);
+
+ pod.pod = SPA_POD_INIT(0, SPA_TYPE_Bool);
+ spa_assert_se(!spa_pod_is_bool(&pod.pod));
+ spa_assert_se(spa_pod_get_bool(&pod.pod, &val) < 0);
+ }
+ {
+ struct spa_pod_id pod = SPA_POD_INIT_Id(SPA_TYPE_Int);
+ uint32_t val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 12);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Id);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 4);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_id, &pod) == SPA_TYPE_Int);
+ spa_assert_se(spa_pod_is_id(&pod.pod));
+ spa_assert_se(spa_pod_get_id(&pod.pod, &val) == 0);
+ spa_assert_se(val == SPA_TYPE_Int);
+
+ pod = SPA_POD_INIT_Id(SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_SIZE(&pod) == 12);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Id);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 4);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_id, &pod) == SPA_TYPE_Long);
+ spa_assert_se(spa_pod_is_id(&pod.pod));
+ spa_assert_se(spa_pod_get_id(&pod.pod, &val) == 0);
+ spa_assert_se(val == SPA_TYPE_Long);
+
+ pod.pod = SPA_POD_INIT(0, SPA_TYPE_Id);
+ spa_assert_se(!spa_pod_is_id(&pod.pod));
+ spa_assert_se(spa_pod_get_id(&pod.pod, &val) < 0);
+ }
+ {
+ struct spa_pod_int pod = SPA_POD_INIT_Int(23);
+ int32_t val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 12);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Int);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 4);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_int, &pod) == 23);
+ spa_assert_se(spa_pod_is_int(&pod.pod));
+ spa_assert_se(spa_pod_get_int(&pod.pod, &val) == 0);
+ spa_assert_se(val == 23);
+
+ pod = SPA_POD_INIT_Int(-123);
+ spa_assert_se(SPA_POD_SIZE(&pod) == 12);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Int);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 4);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_int, &pod) == -123);
+ spa_assert_se(spa_pod_is_int(&pod.pod));
+ spa_assert_se(spa_pod_get_int(&pod.pod, &val) == 0);
+ spa_assert_se(val == -123);
+
+ pod.pod = SPA_POD_INIT(0, SPA_TYPE_Int);
+ spa_assert_se(!spa_pod_is_int(&pod.pod));
+ spa_assert_se(spa_pod_get_int(&pod.pod, &val) < 0);
+ }
+ {
+ struct spa_pod_long pod = SPA_POD_INIT_Long(-23);
+ int64_t val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 16);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 8);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_long, &pod) == -23);
+ spa_assert_se(spa_pod_is_long(&pod.pod));
+ spa_assert_se(spa_pod_get_long(&pod.pod, &val) == 0);
+ spa_assert_se(val == -23);
+
+ pod = SPA_POD_INIT_Long(123);
+ spa_assert_se(SPA_POD_SIZE(&pod) == 16);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 8);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_long, &pod) == 123);
+ spa_assert_se(spa_pod_is_long(&pod.pod));
+ spa_assert_se(spa_pod_get_long(&pod.pod, &val) == 0);
+ spa_assert_se(val == 123);
+
+ pod.pod = SPA_POD_INIT(0, SPA_TYPE_Long);
+ spa_assert_se(!spa_pod_is_long(&pod.pod));
+ spa_assert_se(spa_pod_get_long(&pod.pod, &val) < 0);
+ }
+ {
+ struct spa_pod_float pod = SPA_POD_INIT_Float(0.67f);
+ float val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 12);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Float);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 4);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_float, &pod) == 0.67f);
+ spa_assert_se(spa_pod_is_float(&pod.pod));
+ spa_assert_se(spa_pod_get_float(&pod.pod, &val) == 0);
+ spa_assert_se(val == 0.67f);
+
+ pod = SPA_POD_INIT_Float(-134.8f);
+ spa_assert_se(SPA_POD_SIZE(&pod) == 12);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Float);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 4);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_float, &pod) == -134.8f);
+ spa_assert_se(spa_pod_is_float(&pod.pod));
+ spa_assert_se(spa_pod_get_float(&pod.pod, &val) == 0);
+ spa_assert_se(val == -134.8f);
+
+ pod.pod = SPA_POD_INIT(0, SPA_TYPE_Float);
+ spa_assert_se(!spa_pod_is_float(&pod.pod));
+ spa_assert_se(spa_pod_get_float(&pod.pod, &val) < 0);
+ }
+ {
+ struct spa_pod_double pod = SPA_POD_INIT_Double(0.67);
+ double val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 16);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Double);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 8);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_double, &pod) == 0.67);
+ spa_assert_se(spa_pod_is_double(&pod.pod));
+ spa_assert_se(spa_pod_get_double(&pod.pod, &val) == 0);
+ spa_assert_se(val == 0.67);
+
+ pod = SPA_POD_INIT_Double(-134.8);
+ spa_assert_se(SPA_POD_SIZE(&pod) == 16);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Double);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 8);
+ spa_assert_se(SPA_POD_VALUE(struct spa_pod_double, &pod) == -134.8);
+ spa_assert_se(spa_pod_is_double(&pod.pod));
+ spa_assert_se(spa_pod_get_double(&pod.pod, &val) == 0);
+ spa_assert_se(val == -134.8);
+
+ pod.pod = SPA_POD_INIT(0, SPA_TYPE_Double);
+ spa_assert_se(!spa_pod_is_double(&pod.pod));
+ spa_assert_se(spa_pod_get_double(&pod.pod, &val) < 0);
+ }
+ {
+ struct {
+ struct spa_pod_string pod;
+ char str[9];
+ } pod;
+ char val[12];
+
+ pod.pod = SPA_POD_INIT_String(9);
+ strncpy(pod.str, "test", 9);
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 17);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_String);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 9);
+ spa_assert_se(spa_pod_is_string(&pod.pod.pod));
+ spa_assert_se(spa_pod_copy_string(&pod.pod.pod, sizeof(val), val) == 0);
+ spa_assert_se(spa_streq(pod.str, val));
+
+ pod.pod = SPA_POD_INIT_String(6);
+ memcpy(pod.str, "test123456789", 9);
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 14);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_String);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 6);
+ spa_assert_se(!spa_pod_is_string(&pod.pod.pod));
+ spa_assert_se(spa_pod_copy_string(&pod.pod.pod, sizeof(val), val) < 0);
+ }
+ {
+ struct spa_pod_rectangle pod = SPA_POD_INIT_Rectangle(SPA_RECTANGLE(320,240));
+ struct spa_rectangle val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 16);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Rectangle);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 8);
+ spa_assert_se(memcmp(&SPA_POD_VALUE(struct spa_pod_rectangle, &pod),
+ &SPA_RECTANGLE(320,240), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(spa_pod_is_rectangle(&pod.pod));
+ spa_assert_se(spa_pod_get_rectangle(&pod.pod, &val) == 0);
+ spa_assert_se(memcmp(&val, &SPA_RECTANGLE(320,240), sizeof(struct spa_rectangle)) == 0);
+
+ pod.pod = SPA_POD_INIT(0, SPA_TYPE_Rectangle);
+ spa_assert_se(!spa_pod_is_rectangle(&pod.pod));
+ spa_assert_se(spa_pod_get_rectangle(&pod.pod, &val) < 0);
+ }
+ {
+ struct spa_pod_fraction pod = SPA_POD_INIT_Fraction(SPA_FRACTION(25,1));
+ struct spa_fraction val;
+
+ spa_assert_se(SPA_POD_SIZE(&pod) == 16);
+ spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_Fraction);
+ spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 8);
+ spa_assert_se(memcmp(&SPA_POD_VALUE(struct spa_pod_fraction, &pod),
+ &SPA_FRACTION(25,1), sizeof(struct spa_fraction)) == 0);
+ spa_assert_se(spa_pod_is_fraction(&pod.pod));
+ spa_assert_se(spa_pod_get_fraction(&pod.pod, &val) == 0);
+ spa_assert_se(memcmp(&val, &SPA_FRACTION(25,1), sizeof(struct spa_fraction)) == 0);
+
+ pod.pod = SPA_POD_INIT(0, SPA_TYPE_Fraction);
+ spa_assert_se(!spa_pod_is_fraction(&pod.pod));
+ spa_assert_se(spa_pod_get_fraction(&pod.pod, &val) < 0);
+ }
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_build)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ struct spa_pod *array, *choice, *head, *pod, *it;
+ const struct spa_pod_prop *prop;
+ struct spa_pod_control *control;
+ int64_t longs[] = { 5, 7, 11, 13, 17 }, *al;
+ uint32_t i, len, yl, *ai;
+ union {
+ bool b;
+ uint32_t I;
+ int32_t i;
+ int64_t l;
+ float f;
+ double d;
+ const char *s;
+ const void *y;
+ const void *p;
+ int64_t h;
+ struct spa_rectangle R;
+ struct spa_fraction F;
+ } val;
+ struct spa_pod_frame f;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_assert_se(b.data == buffer);
+ spa_assert_se(b.size == sizeof(buffer));
+ spa_assert_se(b.state.offset == 0);
+ spa_assert_se(b.state.flags == 0);
+
+ spa_assert_se(spa_pod_builder_none(&b) == 0);
+ spa_assert_se(b.state.offset == 8);
+ spa_assert_se(spa_pod_builder_bool(&b, true) == 0);
+ spa_assert_se(b.state.offset == 24);
+ spa_assert_se(spa_pod_builder_id(&b, SPA_TYPE_Object) == 0);
+ spa_assert_se(b.state.offset == 40);
+ spa_assert_se(spa_pod_builder_int(&b, 21) == 0);
+ spa_assert_se(b.state.offset == 56);
+ spa_assert_se(spa_pod_builder_float(&b, 0.8f) == 0);
+ spa_assert_se(b.state.offset == 72);
+ spa_assert_se(spa_pod_builder_double(&b, -1.56) == 0);
+ spa_assert_se(b.state.offset == 88);
+ spa_assert_se(spa_pod_builder_string(&b, "test") == 0);
+ spa_assert_se(b.state.offset == 104);
+ spa_assert_se(spa_pod_builder_bytes(&b, "PipeWire", 8) == 0);
+ spa_assert_se(b.state.offset == 120);
+ spa_assert_se(spa_pod_builder_pointer(&b, SPA_TYPE_Object, &b) == 0);
+ spa_assert_se(b.state.offset == 144);
+ spa_assert_se(spa_pod_builder_fd(&b, 4) == 0);
+ spa_assert_se(b.state.offset == 160);
+ spa_assert_se(spa_pod_builder_rectangle(&b, 320, 240) == 0);
+ spa_assert_se(b.state.offset == 176);
+ spa_assert_se(spa_pod_builder_fraction(&b, 25, 1) == 0);
+
+ spa_assert_se(b.state.offset == 192);
+ spa_assert_se(spa_pod_builder_push_array(&b, &f) == 0);
+ spa_assert_se(f.offset == 192);
+ spa_assert_se(b.state.flags == (SPA_POD_BUILDER_FLAG_BODY | SPA_POD_BUILDER_FLAG_FIRST));
+ spa_assert_se(b.state.offset == 200);
+ spa_assert_se(spa_pod_builder_int(&b, 1) == 0);
+ spa_assert_se(b.state.flags == SPA_POD_BUILDER_FLAG_BODY);
+ spa_assert_se(b.state.offset == 212);
+ spa_assert_se(spa_pod_builder_int(&b, 2) == 0);
+ spa_assert_se(b.state.offset == 216);
+ spa_assert_se(spa_pod_builder_int(&b, 3) == 0);
+ array = spa_pod_builder_pop(&b, &f);
+ spa_assert_se(f.pod.size == 20);
+ spa_assert_se(array != NULL);
+ spa_assert_se(SPA_POD_BODY_SIZE(array) == 8 + 12);
+ spa_assert_se(b.state.flags == 0);
+
+ spa_assert_se(b.state.offset == 224);
+ spa_assert_se(spa_pod_builder_array(&b,
+ sizeof(int64_t), SPA_TYPE_Long,
+ SPA_N_ELEMENTS(longs), longs) == 0);
+ spa_assert_se(b.state.flags == 0);
+
+ spa_assert_se(b.state.offset == 280);
+ spa_assert_se(spa_pod_builder_push_choice(&b, &f, SPA_CHOICE_Enum, 0) == 0);
+ spa_assert_se(b.state.flags == (SPA_POD_BUILDER_FLAG_BODY | SPA_POD_BUILDER_FLAG_FIRST));
+ spa_assert_se(b.state.offset == 296);
+ spa_assert_se(spa_pod_builder_long(&b, 1) == 0);
+ spa_assert_se(b.state.flags == SPA_POD_BUILDER_FLAG_BODY);
+ spa_assert_se(b.state.offset == 312);
+ spa_assert_se(spa_pod_builder_long(&b, 2) == 0);
+ spa_assert_se(b.state.offset == 320);
+ spa_assert_se(spa_pod_builder_long(&b, 3) == 0);
+ choice = spa_pod_builder_pop(&b, &f);
+ spa_assert_se(choice != NULL);
+ spa_assert_se(b.state.flags == 0);
+
+ spa_assert_se(b.state.offset == 328);
+ spa_assert_se(spa_pod_builder_push_struct(&b, &f) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 336);
+ spa_assert_se(spa_pod_builder_int(&b, 21) == 0);
+ spa_assert_se(b.state.offset == 352);
+ spa_assert_se(spa_pod_builder_float(&b, 0.8f) == 0);
+ spa_assert_se(b.state.offset == 368);
+ spa_assert_se(spa_pod_builder_double(&b, -1.56) == 0);
+ spa_assert_se(spa_pod_builder_pop(&b, &f) != NULL);
+
+ spa_assert_se(b.state.offset == 384);
+ spa_assert_se(spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_Props, 0) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 400);
+ spa_assert_se(spa_pod_builder_prop(&b, 1, 0) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 408);
+ spa_assert_se(spa_pod_builder_int(&b, 21) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 424);
+ spa_assert_se(spa_pod_builder_prop(&b, 2, 0) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 432);
+ spa_assert_se(spa_pod_builder_long(&b, 42) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 448);
+ spa_assert_se(spa_pod_builder_prop(&b, 3, 0) == 0);
+ spa_assert_se(b.state.offset == 456);
+ spa_assert_se(spa_pod_builder_string(&b, "test123") == 0);
+ spa_assert_se(spa_pod_builder_pop(&b, &f) != NULL);
+ spa_assert_se(b.state.flags == 0);
+
+ spa_assert_se(b.state.offset == 472);
+ spa_assert_se(spa_pod_builder_push_sequence(&b, &f, 0) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 488);
+ spa_assert_se(spa_pod_builder_control(&b, 0, 0) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 496);
+ spa_assert_se(spa_pod_builder_float(&b, 0.667f) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 512);
+ spa_assert_se(spa_pod_builder_control(&b, 12, 0) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(b.state.offset == 520);
+ spa_assert_se(spa_pod_builder_double(&b, 1.22) == 0);
+ spa_assert_se(b.state.flags == 0);
+ spa_assert_se(spa_pod_builder_pop(&b, &f) != NULL);
+ spa_assert_se(b.state.flags == 0);
+
+ spa_assert_se(b.state.offset == 536);
+
+ len = b.state.offset;
+ pod = head = (struct spa_pod *)buffer;
+
+ spa_assert_se(spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_none(pod));
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_bool(pod));
+ spa_assert_se(spa_pod_get_bool(pod, &val.b) == 0);
+ spa_assert_se(val.b == true);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_id(pod));
+ spa_assert_se(spa_pod_get_id(pod, &val.I) == 0);
+ spa_assert_se(val.I == SPA_TYPE_Object);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_int(pod));
+ spa_assert_se(spa_pod_get_int(pod, &val.i) == 0);
+ spa_assert_se(val.i == 21);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_float(pod));
+ spa_assert_se(spa_pod_get_float(pod, &val.f) == 0);
+ spa_assert_se(val.f == 0.8f);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_double(pod));
+ spa_assert_se(spa_pod_get_double(pod, &val.d) == 0);
+ spa_assert_se(val.d == -1.56);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_string(pod));
+ spa_assert_se(spa_pod_get_string(pod, &val.s) == 0);
+ spa_assert_se(spa_streq(val.s, "test"));
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_bytes(pod));
+ spa_assert_se(spa_pod_get_bytes(pod, &val.y, &yl) == 0);
+ spa_assert_se(yl == 8);
+ spa_assert_se(memcmp(val.y, "PipeWire", yl) == 0);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_pointer(pod));
+ spa_assert_se(spa_pod_get_pointer(pod, &yl, &val.p) == 0);
+ spa_assert_se(yl == SPA_TYPE_Object);
+ spa_assert_se(val.p == &b);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_fd(pod));
+ spa_assert_se(spa_pod_get_fd(pod, &val.l) == 0);
+ spa_assert_se(val.l == 4);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_rectangle(pod));
+ spa_assert_se(spa_pod_get_rectangle(pod, &val.R) == 0);
+ spa_assert_se(memcmp(&val.R, &SPA_RECTANGLE(320,240), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_fraction(pod));
+ spa_assert_se(spa_pod_get_fraction(pod, &val.F) == 0);
+ spa_assert_se(memcmp(&val.F, &SPA_FRACTION(25,1), sizeof(struct spa_fraction)) == 0);
+
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_array(pod));
+ spa_assert_se(SPA_POD_ARRAY_VALUE_TYPE(pod) == SPA_TYPE_Int);
+ spa_assert_se(SPA_POD_ARRAY_VALUE_SIZE(pod) == sizeof(int32_t));
+ spa_assert_se(SPA_POD_ARRAY_N_VALUES(pod) == 3);
+ ai = SPA_POD_ARRAY_VALUES(pod);
+ spa_assert_se(ai != NULL);
+ spa_assert_se(SPA_POD_ARRAY_CHILD(pod)->type == SPA_TYPE_Int);
+ spa_assert_se(SPA_POD_ARRAY_CHILD(pod)->size == sizeof(int32_t));
+ spa_assert_se(ai[0] == 1);
+ spa_assert_se(ai[1] == 2);
+ spa_assert_se(ai[2] == 3);
+ i = 1;
+ SPA_POD_ARRAY_FOREACH((struct spa_pod_array*)pod, ai) {
+ spa_assert_se(*ai == i);
+ i++;
+ }
+
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_array(pod));
+ spa_assert_se(SPA_POD_ARRAY_VALUE_TYPE(pod) == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_ARRAY_VALUE_SIZE(pod) == sizeof(int64_t));
+ spa_assert_se(SPA_POD_ARRAY_N_VALUES(pod) == SPA_N_ELEMENTS(longs));
+ al = SPA_POD_ARRAY_VALUES(pod);
+ spa_assert_se(al != NULL);
+ spa_assert_se(SPA_POD_ARRAY_CHILD(pod)->type == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_ARRAY_CHILD(pod)->size == sizeof(int64_t));
+ for (i = 0; i < SPA_N_ELEMENTS(longs); i++)
+ spa_assert_se(al[i] == longs[i]);
+ i = 0;
+ SPA_POD_ARRAY_FOREACH((struct spa_pod_array*)pod, al) {
+ spa_assert_se(*al == longs[i++]);
+ }
+
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_choice(pod));
+ spa_assert_se(SPA_POD_CHOICE_TYPE(pod) == SPA_CHOICE_Enum);
+ spa_assert_se(SPA_POD_CHOICE_FLAGS(pod) == 0);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_TYPE(pod) == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_SIZE(pod) == sizeof(int64_t));
+ spa_assert_se(SPA_POD_CHOICE_N_VALUES(pod) == 3);
+ al = SPA_POD_CHOICE_VALUES(pod);
+ spa_assert_se(al != NULL);
+ spa_assert_se(SPA_POD_CHOICE_CHILD(pod)->type == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_CHOICE_CHILD(pod)->size == sizeof(int64_t));
+ spa_assert_se(al[0] == 1);
+ spa_assert_se(al[1] == 2);
+ spa_assert_se(al[2] == 3);
+ i = 1;
+ SPA_POD_CHOICE_FOREACH((struct spa_pod_choice*)pod, al) {
+ spa_assert_se(*al == i);
+ i++;
+ }
+
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_struct(pod));
+ i = 0;
+ SPA_POD_STRUCT_FOREACH(pod, it) {
+ switch (i++) {
+ case 0:
+ spa_assert_se(spa_pod_is_int(it));
+ spa_assert_se(spa_pod_get_int(it, &val.i) == 0 && val.i == 21);
+ break;
+ case 1:
+ spa_assert_se(spa_pod_is_float(it));
+ spa_assert_se(spa_pod_get_float(it, &val.f) == 0 && val.f == 0.8f);
+ break;
+ case 2:
+ spa_assert_se(spa_pod_is_double(it));
+ spa_assert_se(spa_pod_get_double(it, &val.d) == 0 && val.d == -1.56);
+ break;
+ default:
+ spa_assert_not_reached();
+ break;
+ }
+ }
+
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_object(pod));
+ spa_assert_se(spa_pod_is_object_type(pod, SPA_TYPE_OBJECT_Props));
+ spa_assert_se(spa_pod_is_object_id(pod, 0));
+ i = 0;
+ SPA_POD_OBJECT_FOREACH((const struct spa_pod_object*)pod, prop) {
+ switch (i++) {
+ case 0:
+ spa_assert_se(prop->key == 1);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 20);
+ spa_assert_se(spa_pod_get_int(&prop->value, &val.i) == 0 && val.i == 21);
+ break;
+ case 1:
+ spa_assert_se(prop->key == 2);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 24);
+ spa_assert_se(spa_pod_get_long(&prop->value, &val.l) == 0 && val.l == 42);
+ break;
+ case 2:
+ spa_assert_se(prop->key == 3);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 24);
+ spa_assert_se(spa_pod_get_string(&prop->value, &val.s) == 0 &&
+ spa_streq(val.s, "test123"));
+ break;
+ default:
+ spa_assert_not_reached();
+ break;
+ }
+ }
+ prop = spa_pod_find_prop(pod, NULL, 3);
+ spa_assert_se(prop != NULL);
+ spa_assert_se(prop->key == 3);
+ spa_assert_se(spa_pod_get_string(&prop->value, &val.s) == 0 &&
+ spa_streq(val.s, "test123"));
+ prop = spa_pod_find_prop(pod, prop, 1);
+ spa_assert_se(prop != NULL);
+ spa_assert_se(prop->key == 1);
+ spa_assert_se(spa_pod_get_int(&prop->value, &val.i) == 0 && val.i == 21);
+ prop = spa_pod_find_prop(pod, prop, 2);
+ spa_assert_se(prop != NULL);
+ spa_assert_se(prop->key == 2);
+ spa_assert_se(spa_pod_get_long(&prop->value, &val.l) == 0 && val.l == 42);
+ prop = spa_pod_find_prop(pod, prop, 5);
+ spa_assert_se(prop == NULL);
+
+ prop = spa_pod_find_prop(pod, NULL, 3);
+ spa_assert_se(prop != NULL);
+ spa_assert_se(prop->key == 3);
+ spa_assert_se(spa_pod_get_string(&prop->value, &val.s) == 0 &&
+ spa_streq(val.s, "test123"));
+
+ spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod));
+ spa_assert_se(spa_pod_is_sequence(pod));
+
+ i = 0;
+ SPA_POD_SEQUENCE_FOREACH((const struct spa_pod_sequence*)pod, control) {
+ switch (i++) {
+ case 0:
+ spa_assert_se(control->offset == 0);
+ spa_assert_se(SPA_POD_CONTROL_SIZE(control) == 20);
+ spa_assert_se(spa_pod_get_float(&control->value, &val.f) == 0 && val.f == 0.667f);
+ break;
+ case 1:
+ spa_assert_se(control->offset == 12);
+ spa_assert_se(SPA_POD_CONTROL_SIZE(control) == 24);
+ spa_assert_se(spa_pod_get_double(&control->value, &val.d) == 0 && val.d == 1.22);
+ break;
+ default:
+ spa_assert_not_reached();
+ break;
+ }
+ }
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_empty)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ struct spa_pod *array, *a2, *choice, *ch2;
+ struct spa_pod_frame f;
+ uint32_t n_vals, ch;
+
+ memset(buffer, 0xab, sizeof(buffer));
+
+ /* create empty arrays */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_assert_se(spa_pod_builder_push_array(&b, &f) == 0);
+ spa_assert_se(spa_pod_builder_child(&b, sizeof(uint32_t), SPA_TYPE_Id) == 0);
+ array = spa_pod_builder_pop(&b, &f);
+ spa_assert_se(array != NULL);
+ spa_debug_mem(0, array, 16);
+ spa_assert_se(spa_pod_is_array(array));
+ a2 = spa_pod_get_array(array, &n_vals);
+ spa_assert_se(a2 != NULL);
+ spa_assert_se(n_vals == 0);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_assert_se(spa_pod_builder_push_array(&b, &f) == 0);
+ array = spa_pod_builder_pop(&b, &f);
+ spa_assert_se(array != NULL);
+ spa_assert_se(spa_pod_is_array(array));
+ a2 = spa_pod_get_array(array, &n_vals);
+ spa_assert_se(a2 != NULL);
+ spa_assert_se(n_vals == 0);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_assert_se(spa_pod_builder_push_array(&b, &f) == 0);
+ spa_assert_se(spa_pod_builder_none(&b) == 0);
+ array = spa_pod_builder_pop(&b, &f);
+ spa_assert_se(array != NULL);
+ spa_assert_se(spa_pod_is_array(array));
+ a2 = spa_pod_get_array(array, &n_vals);
+ spa_assert_se(a2 != NULL);
+ spa_assert_se(n_vals == 0);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_assert_se(spa_pod_builder_array(&b, 4, SPA_TYPE_Id, 0, NULL) == 0);
+ array = (struct spa_pod*)buffer;
+ spa_assert_se(spa_pod_is_array(array));
+ a2 = spa_pod_get_array(array, &n_vals);
+ spa_assert_se(a2 != NULL);
+ spa_assert_se(n_vals == 0);
+
+ /* create empty choice */
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_assert_se(spa_pod_builder_push_choice(&b, &f, 0, 0) == 0);
+ spa_assert_se(spa_pod_builder_child(&b, sizeof(uint32_t), SPA_TYPE_Id) == 0);
+ choice = spa_pod_builder_pop(&b, &f);
+ spa_assert_se(choice != NULL);
+ spa_debug_mem(0, choice, 32);
+ spa_assert_se(spa_pod_is_choice(choice));
+ ch2 = spa_pod_get_values(choice, &n_vals, &ch);
+ spa_assert_se(ch2 != NULL);
+ spa_assert_se(n_vals == 0);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_assert_se(spa_pod_builder_push_choice(&b, &f, 0, 0) == 0);
+ choice = spa_pod_builder_pop(&b, &f);
+ spa_assert_se(choice != NULL);
+ spa_assert_se(spa_pod_is_choice(choice));
+ ch2 = spa_pod_get_values(choice, &n_vals, &ch);
+ spa_assert_se(ch2 != NULL);
+ spa_assert_se(n_vals == 0);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_assert_se(spa_pod_builder_push_choice(&b, &f, 0, 0) == 0);
+ spa_assert_se(spa_pod_builder_none(&b) == 0);
+ choice = spa_pod_builder_pop(&b, &f);
+ spa_assert_se(choice != NULL);
+ spa_assert_se(spa_pod_is_choice(choice));
+ ch2 = spa_pod_get_values(choice, &n_vals, &ch);
+ spa_assert_se(ch2 != NULL);
+ spa_assert_se(n_vals == 0);
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_varargs)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ struct spa_pod *pod;
+ struct spa_pod_prop *prop;
+ uint32_t i, *aI;
+ union {
+ bool b;
+ uint32_t I;
+ int32_t i;
+ int64_t l;
+ float f;
+ double d;
+ const char *s;
+ const void *y;
+ const void *p;
+ int64_t h;
+ struct spa_rectangle R;
+ struct spa_fraction F;
+ } val;
+ uint32_t media_type, media_subtype, format;
+ int32_t views;
+ struct spa_rectangle *aR, size;
+ struct spa_fraction *aF, framerate;
+ struct spa_pod *Vformat, *Vsize, *Vframerate;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ pod = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, 0,
+ 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(3,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_I420,
+ SPA_VIDEO_FORMAT_YUY2),
+ SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
+ &SPA_RECTANGLE(320,242),
+ &SPA_RECTANGLE(1,1),
+ &SPA_RECTANGLE(INT32_MAX,INT32_MAX)),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
+ &SPA_FRACTION(25,1),
+ &SPA_FRACTION(0,1),
+ &SPA_FRACTION(INT32_MAX,1)));
+
+ i = 0;
+ SPA_POD_OBJECT_FOREACH((const struct spa_pod_object*)pod, prop) {
+ switch (i++) {
+ case 0:
+ spa_assert_se(prop->key == SPA_FORMAT_mediaType);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 20);
+ spa_assert_se(spa_pod_get_id(&prop->value, &val.I) == 0 && val.I == SPA_MEDIA_TYPE_video);
+ break;
+ case 1:
+ spa_assert_se(prop->key == SPA_FORMAT_mediaSubtype);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 20);
+ spa_assert_se(spa_pod_get_id(&prop->value, &val.I) == 0 && val.I == SPA_MEDIA_SUBTYPE_raw);
+ break;
+ case 2:
+ spa_assert_se(prop->key == SPA_FORMAT_VIDEO_format);
+ spa_assert_se(spa_pod_is_choice(&prop->value));
+ spa_assert_se(SPA_POD_CHOICE_TYPE(&prop->value) == SPA_CHOICE_Enum);
+ spa_assert_se(SPA_POD_CHOICE_N_VALUES(&prop->value) == 3);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_TYPE(&prop->value) == SPA_TYPE_Id);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_SIZE(&prop->value) == sizeof(uint32_t));
+ aI = SPA_POD_CHOICE_VALUES(&prop->value);
+ spa_assert_se(aI != NULL);
+ spa_assert_se(aI[0] == SPA_VIDEO_FORMAT_I420);
+ spa_assert_se(aI[1] == SPA_VIDEO_FORMAT_I420);
+ spa_assert_se(aI[2] == SPA_VIDEO_FORMAT_YUY2);
+ break;
+ case 3:
+ spa_assert_se(prop->key == SPA_FORMAT_VIDEO_size);
+ spa_assert_se(spa_pod_is_choice(&prop->value));
+ spa_assert_se(SPA_POD_CHOICE_TYPE(&prop->value) == SPA_CHOICE_Range);
+ spa_assert_se(SPA_POD_CHOICE_N_VALUES(&prop->value) == 3);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_TYPE(&prop->value) == SPA_TYPE_Rectangle);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_SIZE(&prop->value) == sizeof(struct spa_rectangle));
+ aR = SPA_POD_CHOICE_VALUES(&prop->value);
+ spa_assert_se(aR != NULL);
+ spa_assert_se(memcmp(&aR[0], &SPA_RECTANGLE(320,242), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(memcmp(&aR[1], &SPA_RECTANGLE(1,1), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(memcmp(&aR[2], &SPA_RECTANGLE(INT32_MAX,INT32_MAX), sizeof(struct spa_rectangle)) == 0);
+ break;
+ case 4:
+ spa_assert_se(prop->key == SPA_FORMAT_VIDEO_framerate);
+ spa_assert_se(spa_pod_is_choice(&prop->value));
+ spa_assert_se(SPA_POD_CHOICE_TYPE(&prop->value) == SPA_CHOICE_Range);
+ spa_assert_se(SPA_POD_CHOICE_N_VALUES(&prop->value) == 3);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_TYPE(&prop->value) == SPA_TYPE_Fraction);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_SIZE(&prop->value) == sizeof(struct spa_fraction));
+ aF = SPA_POD_CHOICE_VALUES(&prop->value);
+ spa_assert_se(aF != NULL);
+ spa_assert_se(memcmp(&aF[0], &SPA_FRACTION(25,1), sizeof(struct spa_fraction)) == 0);
+ spa_assert_se(memcmp(&aF[1], &SPA_FRACTION(0,1), sizeof(struct spa_fraction)) == 0);
+ spa_assert_se(memcmp(&aF[2], &SPA_FRACTION(INT32_MAX,1), sizeof(struct spa_fraction)) == 0);
+ break;
+ default:
+ spa_assert_not_reached();
+ break;
+ }
+ }
+
+ spa_assert_se(spa_pod_parse_object(pod,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&media_subtype),
+ SPA_FORMAT_VIDEO_format, SPA_POD_PodChoice(&Vformat),
+ SPA_FORMAT_VIDEO_size, SPA_POD_PodChoice(&Vsize),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_PodChoice(&Vframerate)) == 5);
+
+ spa_assert_se(media_type == SPA_MEDIA_TYPE_video);
+ spa_assert_se(media_subtype == SPA_MEDIA_SUBTYPE_raw);
+
+ spa_assert_se(spa_pod_is_choice(Vformat));
+ spa_assert_se(SPA_POD_CHOICE_TYPE(Vformat) == SPA_CHOICE_Enum);
+ spa_assert_se(SPA_POD_CHOICE_N_VALUES(Vformat) == 3);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_TYPE(Vformat) == SPA_TYPE_Id);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_SIZE(Vformat) == sizeof(uint32_t));
+ aI = SPA_POD_CHOICE_VALUES(Vformat);
+ spa_assert_se(aI != NULL);
+ spa_assert_se(aI[0] == SPA_VIDEO_FORMAT_I420);
+ spa_assert_se(aI[1] == SPA_VIDEO_FORMAT_I420);
+ spa_assert_se(aI[2] == SPA_VIDEO_FORMAT_YUY2);
+
+ spa_assert_se(spa_pod_is_choice(Vsize));
+ spa_assert_se(SPA_POD_CHOICE_TYPE(Vsize) == SPA_CHOICE_Range);
+ spa_assert_se(SPA_POD_CHOICE_N_VALUES(Vsize) == 3);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_TYPE(Vsize) == SPA_TYPE_Rectangle);
+ spa_assert_se(SPA_POD_CHOICE_VALUE_SIZE(Vsize) == sizeof(struct spa_rectangle));
+ aR = SPA_POD_CHOICE_VALUES(Vsize);
+ spa_assert_se(aR != NULL);
+ spa_assert_se(memcmp(&aR[0], &SPA_RECTANGLE(320,242), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(memcmp(&aR[1], &SPA_RECTANGLE(1,1), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(memcmp(&aR[2], &SPA_RECTANGLE(INT32_MAX,INT32_MAX), sizeof(struct spa_rectangle)) == 0);
+
+ spa_assert_se(spa_pod_is_choice(Vframerate));
+
+ spa_assert_se(spa_pod_parse_object(pod,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&media_subtype),
+ SPA_FORMAT_VIDEO_views, SPA_POD_Int(&views),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(&format),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&framerate)) == -ESRCH);
+
+ spa_assert_se(spa_pod_parse_object(pod,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&media_subtype),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(&format),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&framerate)) == -EPROTO);
+
+ spa_debug_pod(0, NULL, pod);
+ spa_pod_fixate(pod);
+
+ spa_assert_se(spa_pod_parse_object(pod,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&media_subtype),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(&format),
+ SPA_FORMAT_VIDEO_views, SPA_POD_OPT_Int(&views),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&framerate)) == 5);
+
+ spa_assert_se(media_type == SPA_MEDIA_TYPE_video);
+ spa_assert_se(media_subtype == SPA_MEDIA_SUBTYPE_raw);
+ spa_assert_se(format == SPA_VIDEO_FORMAT_I420);
+ spa_assert_se(memcmp(&size, &SPA_RECTANGLE(320,242), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(memcmp(&framerate, &SPA_FRACTION(25,1), sizeof(struct spa_fraction)) == 0);
+
+ spa_debug_pod(0, NULL, pod);
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_varargs2)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ struct spa_pod *pod;
+ struct spa_pod_prop *prop;
+ uint32_t i, j;
+ struct {
+ bool b;
+ uint32_t I;
+ int32_t i;
+ int64_t l;
+ float f;
+ double d;
+ const char *s;
+ uint32_t yl;
+ const void *y;
+ uint32_t ptype;
+ const void *p;
+ uint32_t asize, atype, anvals;
+ const void *a;
+ int64_t h;
+ struct spa_rectangle R;
+ struct spa_fraction F;
+ struct spa_pod *P;
+ } val;
+ uint8_t bytes[] = { 0x56, 0x00, 0x12, 0xf3, 0xba };
+ int64_t longs[] = { 1002, 5383, 28944, 1237748 }, *al;
+ struct spa_pod_int pi = SPA_POD_INIT_Int(77);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ pod = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ 1, SPA_POD_Bool(true),
+ 2, SPA_POD_Id(SPA_TYPE_Id),
+ 3, SPA_POD_Int(3),
+ 4, SPA_POD_Long(4LL),
+ 5, SPA_POD_Float(0.453f),
+ 6, SPA_POD_Double(0.871),
+ 7, SPA_POD_String("test"),
+ 8, SPA_POD_Bytes(bytes, sizeof(bytes)),
+ 9, SPA_POD_Rectangle(&SPA_RECTANGLE(3,4)),
+ 10, SPA_POD_Fraction(&SPA_FRACTION(24,1)),
+ 11, SPA_POD_Array(sizeof(int64_t), SPA_TYPE_Long, SPA_N_ELEMENTS(longs), longs),
+ 12, SPA_POD_Pointer(SPA_TYPE_Object, &b),
+ 13, SPA_POD_Fd(3),
+ 14, SPA_POD_Pod(&pi));
+
+ spa_debug_pod(0, NULL, pod);
+
+ i = 0;
+ SPA_POD_OBJECT_FOREACH((const struct spa_pod_object*)pod, prop) {
+ switch (i++) {
+ case 0:
+ spa_assert_se(prop->key == 1);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 20);
+ spa_assert_se(spa_pod_get_bool(&prop->value, &val.b) == 0 && val.b == true);
+ break;
+ case 1:
+ spa_assert_se(prop->key == 2);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 20);
+ spa_assert_se(spa_pod_get_id(&prop->value, &val.I) == 0 && val.I == SPA_TYPE_Id);
+ break;
+ case 2:
+ spa_assert_se(prop->key == 3);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 20);
+ spa_assert_se(spa_pod_get_int(&prop->value, &val.i) == 0 && val.i == 3);
+ break;
+ case 3:
+ spa_assert_se(prop->key == 4);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 24);
+ spa_assert_se(spa_pod_get_long(&prop->value, &val.l) == 0 && val.l == 4);
+ break;
+ case 4:
+ spa_assert_se(prop->key == 5);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 20);
+ spa_assert_se(spa_pod_get_float(&prop->value, &val.f) == 0 && val.f == 0.453f);
+ break;
+ case 5:
+ spa_assert_se(prop->key == 6);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 24);
+ spa_assert_se(spa_pod_get_double(&prop->value, &val.d) == 0 && val.d == 0.871);
+ break;
+ case 6:
+ spa_assert_se(prop->key == 7);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 21);
+ spa_assert_se(spa_pod_get_string(&prop->value, &val.s) == 0);
+ spa_assert_se(spa_streq(val.s, "test"));
+ break;
+ case 7:
+ spa_assert_se(prop->key == 8);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 21);
+ spa_assert_se(spa_pod_get_bytes(&prop->value, &val.y, &val.yl) == 0);
+ spa_assert_se(val.yl == sizeof(bytes));
+ spa_assert_se(memcmp(val.y, bytes, val.yl) == 0);
+ break;
+ case 8:
+ spa_assert_se(prop->key == 9);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 24);
+ spa_assert_se(spa_pod_get_rectangle(&prop->value, &val.R) == 0);
+ spa_assert_se(memcmp(&val.R, &SPA_RECTANGLE(3,4), sizeof(struct spa_rectangle)) == 0);
+ break;
+ case 9:
+ spa_assert_se(prop->key == 10);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 24);
+ spa_assert_se(spa_pod_get_fraction(&prop->value, &val.F) == 0);
+ spa_assert_se(memcmp(&val.F, &SPA_FRACTION(24,1), sizeof(struct spa_fraction)) == 0);
+ break;
+ case 10:
+ spa_assert_se(prop->key == 11);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 56);
+ spa_assert_se(spa_pod_is_array(&prop->value));
+ spa_assert_se(SPA_POD_ARRAY_VALUE_TYPE(&prop->value) == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_ARRAY_VALUE_SIZE(&prop->value) == sizeof(int64_t));
+ spa_assert_se(SPA_POD_ARRAY_N_VALUES(&prop->value) == SPA_N_ELEMENTS(longs));
+ al = SPA_POD_ARRAY_VALUES(&prop->value);
+ spa_assert_se(al != NULL);
+ spa_assert_se(SPA_POD_ARRAY_CHILD(&prop->value)->type == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_ARRAY_CHILD(&prop->value)->size == sizeof(int64_t));
+ for (j = 0; j < SPA_N_ELEMENTS(longs); j++)
+ spa_assert_se(al[j] == longs[j]);
+ break;
+ case 11:
+ spa_assert_se(prop->key == 12);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == (sizeof(struct spa_pod_prop) +
+ sizeof(struct spa_pod_pointer_body)));
+ spa_assert_se(spa_pod_get_pointer(&prop->value, &val.ptype, &val.p) == 0);
+ spa_assert_se(val.ptype == SPA_TYPE_Object);
+ spa_assert_se(val.p == &b);
+ break;
+ case 12:
+ spa_assert_se(prop->key == 13);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 24);
+ spa_assert_se(spa_pod_get_fd(&prop->value, &val.h) == 0);
+ spa_assert_se(val.h == 3);
+ break;
+ case 13:
+ spa_assert_se(prop->key == 14);
+ spa_assert_se(SPA_POD_PROP_SIZE(prop) == 20);
+ spa_assert_se(spa_pod_get_int(&prop->value, &val.i) == 0);
+ spa_assert_se(val.i == 77);
+ break;
+ default:
+ spa_assert_not_reached();
+ break;
+ }
+ }
+ spa_assert_se(spa_pod_parse_object(pod, SPA_TYPE_OBJECT_Format, NULL) == -EPROTO);
+ spa_assert_se(spa_pod_parse_object(pod, SPA_TYPE_OBJECT_Props, NULL) == 0);
+
+ spa_zero(val);
+ spa_assert_se(spa_pod_parse_object(pod,
+ SPA_TYPE_OBJECT_Props, NULL,
+ 1, SPA_POD_Bool(&val.b),
+ 2, SPA_POD_Id(&val.I),
+ 3, SPA_POD_Int(&val.i),
+ 4, SPA_POD_Long(&val.l),
+ 5, SPA_POD_Float(&val.f),
+ 6, SPA_POD_Double(&val.d),
+ 7, SPA_POD_String(&val.s),
+ 8, SPA_POD_Bytes(&val.y, &val.yl),
+ 9, SPA_POD_Rectangle(&val.R),
+ 10, SPA_POD_Fraction(&val.F),
+ 11, SPA_POD_Array(&val.asize, &val.atype, &val.anvals, &val.a),
+ 12, SPA_POD_Pointer(&val.ptype, &val.p),
+ 13, SPA_POD_Fd(&val.h),
+ 14, SPA_POD_Pod(&val.P)) == 14);
+
+ spa_assert_se(val.b == true);
+ spa_assert_se(val.I == SPA_TYPE_Id);
+ spa_assert_se(val.i == 3);
+ spa_assert_se(val.l == 4);
+ spa_assert_se(val.f == 0.453f);
+ spa_assert_se(val.d == 0.871);
+ spa_assert_se(spa_streq(val.s, "test"));
+ spa_assert_se(val.yl == sizeof(bytes));
+ spa_assert_se(memcmp(val.y, bytes, sizeof(bytes)) == 0);
+ spa_assert_se(memcmp(&val.R, &SPA_RECTANGLE(3, 4), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(memcmp(&val.F, &SPA_FRACTION(24, 1), sizeof(struct spa_fraction)) == 0);
+ spa_assert_se(val.asize == sizeof(int64_t));
+ spa_assert_se(val.atype == SPA_TYPE_Long);
+ spa_assert_se(val.anvals == SPA_N_ELEMENTS(longs));
+ spa_assert_se(memcmp(val.a, longs, val.anvals * val.asize) == 0);
+ spa_assert_se(val.ptype == SPA_TYPE_Object);
+ spa_assert_se(val.p == &b);
+ spa_assert_se(val.h == 3);
+ spa_assert_se(memcmp(val.P, &pi, sizeof(pi)) == 0);
+
+ spa_zero(val);
+ spa_assert_se(spa_pod_parse_object(pod,
+ SPA_TYPE_OBJECT_Props, NULL,
+ 0, SPA_POD_OPT_Bool(&val.b),
+ 0, SPA_POD_OPT_Id(&val.I),
+ 0, SPA_POD_OPT_Int(&val.i),
+ 0, SPA_POD_OPT_Long(&val.l),
+ 0, SPA_POD_OPT_Float(&val.f),
+ 0, SPA_POD_OPT_Double(&val.d),
+ 0, SPA_POD_OPT_String(&val.s),
+ 0, SPA_POD_OPT_Bytes(&val.y, &val.yl),
+ 0, SPA_POD_OPT_Rectangle(&val.R),
+ 0, SPA_POD_OPT_Fraction(&val.F),
+ 0, SPA_POD_OPT_Array(&val.asize, &val.atype, &val.anvals, &val.a),
+ 0, SPA_POD_OPT_Pointer(&val.ptype, &val.p),
+ 0, SPA_POD_OPT_Fd(&val.h),
+ 0, SPA_POD_OPT_Pod(&val.P)) == 0);
+
+ for (i = 1; i < 15; i++) {
+ spa_zero(val);
+ spa_assert_se(spa_pod_parse_object(pod,
+ SPA_TYPE_OBJECT_Props, NULL,
+ i, SPA_POD_OPT_Bool(&val.b),
+ i, SPA_POD_OPT_Id(&val.I),
+ i, SPA_POD_OPT_Int(&val.i),
+ i, SPA_POD_OPT_Long(&val.l),
+ i, SPA_POD_OPT_Float(&val.f),
+ i, SPA_POD_OPT_Double(&val.d),
+ i, SPA_POD_OPT_String(&val.s),
+ i, SPA_POD_OPT_Bytes(&val.y, &val.yl),
+ i, SPA_POD_OPT_Rectangle(&val.R),
+ i, SPA_POD_OPT_Fraction(&val.F),
+ i, SPA_POD_OPT_Array(&val.asize, &val.atype, &val.anvals, &val.a),
+ i, SPA_POD_OPT_Pointer(&val.ptype, &val.p),
+ i, SPA_POD_OPT_Fd(&val.h),
+ i, SPA_POD_OPT_Pod(&val.P)) == 2);
+ }
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_parser)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ struct spa_pod_parser p;
+ struct spa_pod_frame f;
+ struct spa_pod *pod;
+ struct {
+ bool b;
+ uint32_t I;
+ int32_t i;
+ int64_t l;
+ float f;
+ double d;
+ const char *s;
+ uint32_t yl;
+ const void *y;
+ uint32_t ptype;
+ const void *p;
+ uint32_t asize, atype, anvals;
+ const void *a;
+ int64_t h;
+ struct spa_rectangle R;
+ struct spa_fraction F;
+ struct spa_pod *P;
+ } val;
+ uint8_t bytes[] = { 0x56, 0x00, 0x12, 0xf3, 0xba };
+ int64_t longs[] = { 1002, 5383, 28944, 1237748 };
+ struct spa_pod_int pi = SPA_POD_INIT_Int(77);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ pod = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, 0,
+ 1, SPA_POD_Bool(true),
+ 2, SPA_POD_Id(SPA_TYPE_Id),
+ 3, SPA_POD_Int(3),
+ 4, SPA_POD_Long(4LL),
+ 5, SPA_POD_Float(0.453f),
+ 6, SPA_POD_Double(0.871),
+ 7, SPA_POD_String("test"),
+ 8, SPA_POD_Bytes(bytes, sizeof(bytes)),
+ 9, SPA_POD_Rectangle(&SPA_RECTANGLE(3,4)),
+ 10, SPA_POD_Fraction(&SPA_FRACTION(24,1)),
+ 11, SPA_POD_Array(sizeof(int64_t), SPA_TYPE_Long, SPA_N_ELEMENTS(longs), longs),
+ 12, SPA_POD_Pointer(SPA_TYPE_Object, &b),
+ 13, SPA_POD_Fd(3),
+ 14, SPA_POD_Pod(&pi));
+
+ spa_debug_pod(0, NULL, pod);
+
+ spa_pod_parser_pod(&p, pod);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_bool(&p, &val.b) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_id(&p, &val.I) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_int(&p, &val.i) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_long(&p, &val.l) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_float(&p, &val.f) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_double(&p, &val.d) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_string(&p, &val.s) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_bytes(&p, &val.y, &val.yl) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_rectangle(&p, &val.R) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_fraction(&p, &val.F) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_pointer(&p, &val.ptype, &val.p) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_fd(&p, &val.h) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_pod(&p, &val.P) == 0);
+ spa_assert_se(p.state.offset == 392);
+ spa_assert_se(spa_pod_is_object(val.P));
+
+ spa_pod_parser_pod(&p, val.P);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_push_struct(&p, &f) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_push_object(&p, &f, SPA_TYPE_OBJECT_Format, NULL) == -EPROTO);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_push_object(&p, &f, SPA_TYPE_OBJECT_Props, NULL) == 0);
+ spa_assert_se(p.state.offset == 392);
+ spa_assert_se(spa_pod_parser_frame(&p, &f) == val.P);
+
+ spa_zero(val);
+ spa_assert_se(spa_pod_parser_get(&p,
+ 1, SPA_POD_OPT_Bool(&val.b),
+ 2, SPA_POD_OPT_Id(&val.I),
+ 3, SPA_POD_OPT_Int(&val.i),
+ 4, SPA_POD_OPT_Long(&val.l),
+ 5, SPA_POD_OPT_Float(&val.f),
+ 6, SPA_POD_OPT_Double(&val.d),
+ 7, SPA_POD_OPT_String(&val.s),
+ 8, SPA_POD_OPT_Bytes(&val.y, &val.yl),
+ 9, SPA_POD_OPT_Rectangle(&val.R),
+ 10, SPA_POD_OPT_Fraction(&val.F),
+ 11, SPA_POD_OPT_Array(&val.asize, &val.atype, &val.anvals, &val.a),
+ 12, SPA_POD_OPT_Pointer(&val.ptype, &val.p),
+ 13, SPA_POD_OPT_Fd(&val.h),
+ 14, SPA_POD_OPT_Pod(&val.P), 0) == 14);
+ spa_pod_parser_pop(&p, &f);
+
+ spa_assert_se(val.b == true);
+ spa_assert_se(val.I == SPA_TYPE_Id);
+ spa_assert_se(val.i == 3);
+ spa_assert_se(val.l == 4);
+ spa_assert_se(val.f == 0.453f);
+ spa_assert_se(val.d == 0.871);
+ spa_assert_se(spa_streq(val.s, "test"));
+ spa_assert_se(val.yl == sizeof(bytes));
+ spa_assert_se(memcmp(val.y, bytes, sizeof(bytes)) == 0);
+ spa_assert_se(memcmp(&val.R, &SPA_RECTANGLE(3, 4), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(memcmp(&val.F, &SPA_FRACTION(24, 1), sizeof(struct spa_fraction)) == 0);
+ spa_assert_se(val.asize == sizeof(int64_t));
+ spa_assert_se(val.atype == SPA_TYPE_Long);
+ spa_assert_se(val.anvals == SPA_N_ELEMENTS(longs));
+ spa_assert_se(memcmp(val.a, longs, val.anvals * val.asize) == 0);
+ spa_assert_se(val.ptype == SPA_TYPE_Object);
+ spa_assert_se(val.p == &b);
+ spa_assert_se(val.h == 3);
+ spa_assert_se(memcmp(val.P, &pi, sizeof(pi)) == 0);
+
+ spa_assert_se(p.state.offset == 392);
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_parser2)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b;
+ struct spa_pod_parser p;
+ struct spa_pod_frame f;
+ struct spa_pod *pod;
+ struct {
+ bool b;
+ uint32_t I;
+ int32_t i;
+ int64_t l;
+ float f;
+ double d;
+ const char *s;
+ uint32_t yl;
+ const void *y;
+ uint32_t ptype;
+ const void *p;
+ uint32_t asize, atype, anvals;
+ const void *a;
+ int64_t h;
+ struct spa_rectangle R;
+ struct spa_fraction F;
+ struct spa_pod *P;
+ } val;
+ uint8_t bytes[] = { 0x56, 0x00, 0x12, 0xf3, 0xba };
+ int64_t longs[] = { 1002, 5383, 28944, 1237748 };
+ struct spa_pod_int pi = SPA_POD_INIT_Int(77);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ pod = spa_pod_builder_add_struct(&b,
+ SPA_POD_Bool(true),
+ SPA_POD_Id(SPA_TYPE_Id),
+ SPA_POD_Int(3),
+ SPA_POD_Long(4LL),
+ SPA_POD_Float(0.453f),
+ SPA_POD_Double(0.871),
+ SPA_POD_String("test"),
+ SPA_POD_Bytes(bytes, sizeof(bytes)),
+ SPA_POD_Rectangle(&SPA_RECTANGLE(3,4)),
+ SPA_POD_Fraction(&SPA_FRACTION(24,1)),
+ SPA_POD_Array(sizeof(int64_t), SPA_TYPE_Long, SPA_N_ELEMENTS(longs), longs),
+ SPA_POD_Pointer(SPA_TYPE_Object, &b),
+ SPA_POD_Fd(3),
+ SPA_POD_Pod(&pi));
+
+ spa_debug_pod(0, NULL, pod);
+
+ spa_pod_parser_pod(&p, pod);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_bool(&p, &val.b) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_id(&p, &val.I) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_int(&p, &val.i) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_long(&p, &val.l) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_float(&p, &val.f) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_double(&p, &val.d) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_string(&p, &val.s) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_bytes(&p, &val.y, &val.yl) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_rectangle(&p, &val.R) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_fraction(&p, &val.F) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_pointer(&p, &val.ptype, &val.p) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_fd(&p, &val.h) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_get_pod(&p, &val.P) == 0);
+ spa_assert_se(p.state.offset == 272);
+ spa_assert_se(spa_pod_is_struct(val.P));
+
+ spa_pod_parser_pod(&p, val.P);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_push_object(&p, &f, SPA_TYPE_OBJECT_Format, NULL) == -EINVAL);
+ spa_assert_se(p.state.offset == 0);
+ spa_assert_se(spa_pod_parser_push_struct(&p, &f) == 0);
+ spa_assert_se(f.pod.type == SPA_TYPE_Struct);
+ spa_assert_se(f.pod.size == 264);
+ spa_assert_se(f.offset == 0);
+ spa_assert_se(p.state.frame == &f);
+ spa_assert_se(spa_pod_parser_frame(&p, &f) == val.P);
+ spa_assert_se(p.state.offset == 8);
+ spa_assert_se(spa_pod_parser_get_bool(&p, &val.b) == 0 && val.b == true);
+ spa_assert_se(p.state.offset == 24);
+ spa_assert_se(spa_pod_parser_get_id(&p, &val.I) == 0 && val.I == SPA_TYPE_Id);
+ spa_assert_se(p.state.offset == 40);
+ spa_assert_se(spa_pod_parser_get_int(&p, &val.i) == 0 && val.i == 3);
+ spa_assert_se(p.state.offset == 56);
+ spa_assert_se(spa_pod_parser_get_long(&p, &val.l) == 0 && val.l == 4);
+ spa_assert_se(p.state.offset == 72);
+ spa_assert_se(spa_pod_parser_get_float(&p, &val.f) == 0 && val.f == 0.453f);
+ spa_assert_se(p.state.offset == 88);
+ spa_assert_se(spa_pod_parser_get_double(&p, &val.d) == 0 && val.d == 0.871);
+ spa_assert_se(p.state.offset == 104);
+ spa_assert_se(spa_pod_parser_get_string(&p, &val.s) == 0 && spa_streq(val.s, "test"));
+ spa_assert_se(p.state.offset == 120);
+ spa_assert_se(spa_pod_parser_get_bytes(&p, &val.y, &val.yl) == 0);
+ spa_assert_se(val.yl == sizeof(bytes));
+ spa_assert_se(memcmp(bytes, val.y, sizeof(bytes)) == 0);
+ spa_assert_se(p.state.offset == 136);
+ spa_assert_se(spa_pod_parser_get_rectangle(&p, &val.R) == 0);
+ spa_assert_se(memcmp(&val.R, &SPA_RECTANGLE(3,4), sizeof(struct spa_rectangle)) == 0);
+ spa_assert_se(p.state.offset == 152);
+ spa_assert_se(spa_pod_parser_get_fraction(&p, &val.F) == 0);
+ spa_assert_se(memcmp(&val.F, &SPA_FRACTION(24,1), sizeof(struct spa_fraction)) == 0);
+ spa_assert_se(p.state.offset == 168);
+ val.P = spa_pod_parser_next(&p);
+ spa_assert_se(val.P != NULL);
+ spa_assert_se(spa_pod_is_array(val.P));
+ spa_assert_se(p.state.offset == 216);
+ spa_assert_se(SPA_POD_ARRAY_VALUE_TYPE(val.P) == SPA_TYPE_Long);
+ spa_assert_se(SPA_POD_ARRAY_VALUE_SIZE(val.P) == sizeof(int64_t));
+ spa_assert_se(SPA_POD_ARRAY_N_VALUES(val.P) == SPA_N_ELEMENTS(longs));
+ spa_assert_se(spa_pod_parser_get_pointer(&p, &val.ptype, &val.p) == 0);
+ spa_assert_se(val.ptype == SPA_TYPE_Object);
+ spa_assert_se(val.p == &b);
+ spa_assert_se(p.state.offset == 240);
+ spa_assert_se(spa_pod_parser_get_fd(&p, &val.h) == 0);
+ spa_assert_se(val.h == 3);
+ spa_assert_se(p.state.offset == 256);
+ spa_assert_se(spa_pod_parser_get_pod(&p, &val.P) == 0);
+ spa_assert_se(p.state.offset == 272);
+ spa_assert_se(spa_pod_is_int(val.P));
+ spa_pod_parser_pop(&p, &f);
+ spa_assert_se(p.state.offset == 272);
+ spa_assert_se(p.state.frame == NULL);
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_static)
+{
+ struct _test_format {
+ struct spa_pod_object fmt;
+
+ struct {
+ struct spa_pod_prop prop_media_type SPA_ALIGNED(8);
+ uint32_t media_type;
+
+ struct spa_pod_prop prop_media_subtype SPA_ALIGNED(8);
+ uint32_t media_subtype;
+
+ struct spa_pod_prop prop_format SPA_ALIGNED(8);
+ struct {
+ struct spa_pod_choice_body choice;
+ uint32_t def_format;
+ uint32_t enum_format[2];
+ } format_vals;
+
+ struct spa_pod_prop prop_size SPA_ALIGNED(8);
+ struct {
+ struct spa_pod_choice_body choice;
+ struct spa_rectangle def_size;
+ struct spa_rectangle min_size;
+ struct spa_rectangle max_size;
+ } size_vals;
+
+ struct spa_pod_prop prop_framerate SPA_ALIGNED(8);
+ struct {
+ struct spa_pod_choice_body choice;
+ struct spa_fraction def_framerate;
+ struct spa_fraction min_framerate;
+ struct spa_fraction max_framerate;
+ } framerate_vals;
+ } props;
+ } test_format = {
+ SPA_POD_INIT_Object(sizeof(test_format.props) + sizeof(struct spa_pod_object_body),
+ SPA_TYPE_OBJECT_Format, 0),
+ {
+ SPA_POD_INIT_Prop(SPA_FORMAT_mediaType, 0,
+ sizeof(test_format.props.media_type), SPA_TYPE_Id),
+ SPA_MEDIA_TYPE_video,
+
+ SPA_POD_INIT_Prop(SPA_FORMAT_mediaSubtype, 0,
+ sizeof(test_format.props.media_subtype), SPA_TYPE_Id),
+ SPA_MEDIA_SUBTYPE_raw,
+
+ SPA_POD_INIT_Prop(SPA_FORMAT_VIDEO_format, 0,
+ sizeof(test_format.props.format_vals), SPA_TYPE_Choice),
+ {
+ SPA_POD_INIT_CHOICE_BODY(SPA_CHOICE_Enum, 0,
+ sizeof(uint32_t), SPA_TYPE_Id),
+ SPA_VIDEO_FORMAT_I420,
+ { SPA_VIDEO_FORMAT_I420, SPA_VIDEO_FORMAT_YUY2 }
+ },
+ SPA_POD_INIT_Prop(SPA_FORMAT_VIDEO_size, 0,
+ sizeof(test_format.props.size_vals), SPA_TYPE_Choice),
+
+ {
+ SPA_POD_INIT_CHOICE_BODY(SPA_CHOICE_Range, 0,
+ sizeof(struct spa_rectangle), SPA_TYPE_Rectangle),
+ SPA_RECTANGLE(320,243),
+ SPA_RECTANGLE(1,1), SPA_RECTANGLE(INT32_MAX, INT32_MAX)
+ },
+ SPA_POD_INIT_Prop(SPA_FORMAT_VIDEO_framerate, 0,
+ sizeof(test_format.props.framerate_vals), SPA_TYPE_Choice),
+ {
+ SPA_POD_INIT_CHOICE_BODY(SPA_CHOICE_Range, 0,
+ sizeof(struct spa_fraction), SPA_TYPE_Fraction),
+ SPA_FRACTION(25,1),
+ SPA_FRACTION(0,1), SPA_FRACTION(INT32_MAX,1)
+ }
+ }
+ };
+ struct {
+ uint32_t media_type;
+ uint32_t media_subtype;
+ uint32_t format;
+ struct spa_rectangle size;
+ struct spa_fraction framerate;
+ } vals;
+ int res;
+
+ spa_debug_pod(0, NULL, &test_format.fmt.pod);
+
+ spa_zero(vals);
+ res = spa_pod_parse_object(&test_format.fmt.pod,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&vals.media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&vals.media_subtype),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(&vals.format),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&vals.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&vals.framerate));
+
+ spa_assert_se(res == -EPROTO);
+ spa_assert_se(vals.media_type == SPA_MEDIA_TYPE_video);
+ spa_assert_se(vals.media_subtype == SPA_MEDIA_SUBTYPE_raw);
+ spa_assert_se(vals.format == 0);
+ spa_assert_se(vals.size.width == 0 && vals.size.height == 0);
+ spa_assert_se(vals.framerate.num == 0 && vals.framerate.denom == 0);
+
+ spa_pod_fixate(&test_format.fmt.pod);
+
+ spa_zero(vals);
+ res = spa_pod_parse_object(&test_format.fmt.pod,
+ SPA_TYPE_OBJECT_Format, NULL,
+ SPA_FORMAT_mediaType, SPA_POD_Id(&vals.media_type),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(&vals.media_subtype),
+ SPA_FORMAT_VIDEO_format, SPA_POD_Id(&vals.format),
+ SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&vals.size),
+ SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&vals.framerate));
+
+ spa_assert_se(res == 5);
+ spa_assert_se(vals.media_type == SPA_MEDIA_TYPE_video);
+ spa_assert_se(vals.media_subtype == SPA_MEDIA_SUBTYPE_raw);
+ spa_assert_se(vals.format == SPA_VIDEO_FORMAT_I420);
+ spa_assert_se(vals.size.width == 320 && vals.size.height == 243);
+ spa_assert_se(vals.framerate.num == 25 && vals.framerate.denom == 1);
+ return PWTEST_PASS;
+}
+
+PWTEST(pod_overflow)
+{
+ static const char * const labels[] = {
+ "640x480p59", "720x480i29", "720x480p59", "720x576i25", "720x576p50",
+ "1280x720p24", "1280x720p25", "1280x720p30", "1280x720p50", "1280x720p60",
+ "1920x1080p24", "1920x1080p25", "1920x1080p30", "1920x1080i25", "1920x1080p50",
+ "1920x1080i30", "1920x1080p60", "640x350p85", "640x400p85", "720x400p85",
+ "640x480p72", "640x480p75", "640x480p85", "800x600p56", "800x600p60",
+ "800x600p72", "800x600p75", "800x600p85", "800x600p119", "848x480p60",
+ "1024x768i43", "1024x768p60", "1024x768p70", "1024x768p75", "1024x768p84",
+ "1024x768p119", "1152x864p75", "1280x768p59", "1280x768p59", "1280x768p74",
+ "1280x768p84", "1280x768p119", "1280x800p59", "1280x800p59", "1280x800p74",
+ "1280x800p84", "1280x800p119", "1280x960p60", "1280x960p85", "1280x960p119",
+ "1280x1024p60", "1280x1024p75", "1280x1024p85", "1280x1024p119", "1360x768p60",
+ "1360x768p119", "1366x768p59", "1366x768p60", "1400x1050p59", "1400x1050p59",
+ "1400x1050p74", "1400x1050p84", "1400x1050p119", "1440x900p59", "1440x900p59",
+ "1440x900p74", "1440x900p84", "1440x900p119", "1600x900p60", "1600x1200p60",
+ "1600x1200p65", "1600x1200p70", "1600x1200p75", "1600x1200p85", "1600x1200p119",
+ "1680x1050p59", "1680x1050p59", "1680x1050p74", "1680x1050p84", "1680x1050p119",
+ "1792x1344p59", "1792x1344p74", "1792x1344p119", "1856x1392p59", "1856x1392p75",
+ "1856x1392p119", "1920x1200p59", "1920x1200p59", "1920x1200p74", "1920x1200p84",
+ "1920x1200p119", "1920x1440p60", "1920x1440p75", "1920x1440p119", "2048x1152p60",
+ "2560x1600p59", "2560x1600p59", "2560x1600p74", "2560x1600p84", "2560x1600p119",
+ "3840x2160p24", "3840x2160p25", "3840x2160p30", "3840x2160p50", "3840x2160p60",
+ "4096x2160p24", "4096x2160p25", "4096x2160p30", "4096x2160p50", "4096x2160p59",
+ "4096x2160p60", NULL };
+
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_builder_state state;
+ struct spa_pod_frame f[2];
+ uint32_t idx;
+ struct spa_pod *pod;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_id, SPA_POD_Id(32567359),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_ENUM_Int(1, 0),
+ SPA_PROP_INFO_description, SPA_POD_String("DV Timings"),
+ 0);
+
+ spa_pod_builder_get_state(&b, &state),
+
+ spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+ spa_pod_builder_push_struct(&b, &f[1]);
+
+ for (idx = 0; labels[idx]; idx++) {
+ spa_pod_builder_int(&b, idx);
+ spa_pod_builder_string(&b, labels[idx]);
+ }
+ spa_assert_se(b.state.offset > sizeof(buffer));
+ pod = spa_pod_builder_pop(&b, &f[1]);
+ spa_assert_se(pod == NULL);
+ spa_pod_builder_reset(&b, &state);
+
+ spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+ spa_pod_builder_push_struct(&b, &f[1]);
+ pod = spa_pod_builder_pop(&b, &f[1]);
+
+ spa_assert_se(b.state.offset < sizeof(buffer));
+ pod = spa_pod_builder_pop(&b, &f[0]);
+ spa_assert_se(pod != NULL);
+
+ spa_debug_pod(0, NULL, pod);
+ return PWTEST_PASS;
+}
+
+static int handle_overflow(void *data, uint32_t size)
+{
+ uint32_t *d = data;
+ (*d)++;
+ return -ENOSPC;
+}
+
+static struct spa_pod_builder_callbacks overflow_cb = {
+ SPA_VERSION_POD_BUILDER_CALLBACKS,
+ .overflow = handle_overflow
+};
+
+PWTEST(pod_overflow2)
+{
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_builder_state state;
+ struct spa_pod_frame f[2];
+ uint32_t idx, overflow_count = 0;
+ struct spa_pod *pod;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_set_callbacks(&b, &overflow_cb, &overflow_count);
+
+ spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo);
+ spa_pod_builder_add(&b,
+ SPA_PROP_INFO_id, SPA_POD_Id(32567359),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_ENUM_Int(1, 0),
+ SPA_PROP_INFO_description, SPA_POD_String("DV Timings"),
+ 0);
+
+ spa_pod_builder_get_state(&b, &state),
+
+ spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0);
+ spa_pod_builder_push_struct(&b, &f[1]);
+
+ for (idx = 0; idx < 512; idx++) {
+ spa_pod_builder_int(&b, idx);
+ spa_pod_builder_string(&b, "foo");
+ }
+ spa_assert_se(b.state.offset > sizeof(buffer));
+ pod = spa_pod_builder_pop(&b, &f[1]);
+ spa_assert_se(pod == NULL);
+ spa_assert_se(overflow_count == 1);
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(spa_pod)
+{
+ pwtest_add(pod_abi_sizes, PWTEST_NOARG);
+ pwtest_add(pod_abi, PWTEST_NOARG);
+ pwtest_add(pod_init, PWTEST_NOARG);
+ pwtest_add(pod_empty, PWTEST_NOARG);
+ pwtest_add(pod_build, PWTEST_NOARG);
+ pwtest_add(pod_varargs, PWTEST_NOARG);
+ pwtest_add(pod_varargs2, PWTEST_NOARG);
+ pwtest_add(pod_parser, PWTEST_NOARG);
+ pwtest_add(pod_parser2, PWTEST_NOARG);
+ pwtest_add(pod_static, PWTEST_NOARG);
+ pwtest_add(pod_overflow, PWTEST_NOARG);
+ pwtest_add(pod_overflow2, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-spa-utils.c b/test/test-spa-utils.c
new file mode 100644
index 0000000..4184f43
--- /dev/null
+++ b/test/test-spa-utils.c
@@ -0,0 +1,1043 @@
+/* Simple Plugin API
+ * Copyright © 2018 Collabora Ltd.
+ * @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <locale.h>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#include <valgrind/valgrind.h>
+
+#include <spa/utils/defs.h>
+#include <spa/utils/result.h>
+#include <spa/utils/dict.h>
+#include <spa/utils/list.h>
+#include <spa/utils/hook.h>
+#include <spa/utils/ringbuffer.h>
+#include <spa/utils/string.h>
+#include <spa/utils/type.h>
+#include <spa/utils/ansi.h>
+
+#include "pwtest.h"
+
+PWTEST(utils_abi_sizes)
+{
+#if defined(__x86_64__) && defined(__LP64__)
+ /* dict */
+ pwtest_int_eq(sizeof(struct spa_dict_item), 16U);
+ pwtest_int_eq(sizeof(struct spa_dict), 16U);
+
+ /* hook */
+ pwtest_int_eq(sizeof(struct spa_hook_list), sizeof(struct spa_list));
+ pwtest_int_eq(sizeof(struct spa_hook), 48U);
+
+ /* list */
+ pwtest_int_eq(sizeof(struct spa_list), 16U);
+
+ return PWTEST_PASS;
+#endif
+
+ return PWTEST_SKIP;
+}
+
+PWTEST(utils_abi)
+{
+ /* defs */
+ pwtest_int_eq(SPA_DIRECTION_INPUT, 0);
+ pwtest_int_eq(SPA_DIRECTION_OUTPUT, 1);
+
+ pwtest_int_eq(sizeof(struct spa_rectangle), 8U);
+ pwtest_int_eq(sizeof(struct spa_point), 8U);
+ pwtest_int_eq(sizeof(struct spa_region), 16U);
+ pwtest_int_eq(sizeof(struct spa_fraction), 8U);
+
+ {
+ struct spa_rectangle r = SPA_RECTANGLE(12, 14);
+ pwtest_int_eq(r.width, 12U);
+ pwtest_int_eq(r.height, 14U);
+ }
+ {
+ struct spa_point p = SPA_POINT(8, 34);
+ pwtest_int_eq(p.x, 8);
+ pwtest_int_eq(p.y, 34);
+ }
+ {
+ struct spa_region r = SPA_REGION(4, 5, 12, 13);
+ pwtest_int_eq(r.position.x, 4);
+ pwtest_int_eq(r.position.y, 5);
+ pwtest_int_eq(r.size.width, 12U);
+ pwtest_int_eq(r.size.height, 13U);
+ }
+ {
+ struct spa_fraction f = SPA_FRACTION(56, 125);
+ pwtest_int_eq(f.num, 56U);
+ pwtest_int_eq(f.denom, 125U);
+ }
+
+ /* ringbuffer */
+ pwtest_int_eq(sizeof(struct spa_ringbuffer), 8U);
+
+ /* type */
+ pwtest_int_eq(SPA_TYPE_START, 0);
+ pwtest_int_eq(SPA_TYPE_None, 1);
+ pwtest_int_eq(SPA_TYPE_Bool, 2);
+ pwtest_int_eq(SPA_TYPE_Id, 3);
+ pwtest_int_eq(SPA_TYPE_Int, 4);
+ pwtest_int_eq(SPA_TYPE_Long, 5);
+ pwtest_int_eq(SPA_TYPE_Float, 6);
+ pwtest_int_eq(SPA_TYPE_Double, 7);
+ pwtest_int_eq(SPA_TYPE_String, 8);
+ pwtest_int_eq(SPA_TYPE_Bytes, 9);
+ pwtest_int_eq(SPA_TYPE_Rectangle, 10);
+ pwtest_int_eq(SPA_TYPE_Fraction, 11);
+ pwtest_int_eq(SPA_TYPE_Bitmap, 12);
+ pwtest_int_eq(SPA_TYPE_Array, 13);
+ pwtest_int_eq(SPA_TYPE_Struct, 14);
+ pwtest_int_eq(SPA_TYPE_Object, 15);
+ pwtest_int_eq(SPA_TYPE_Sequence, 16);
+ pwtest_int_eq(SPA_TYPE_Pointer, 17);
+ pwtest_int_eq(SPA_TYPE_Fd, 18);
+ pwtest_int_eq(SPA_TYPE_Choice, 19);
+ pwtest_int_eq(SPA_TYPE_Pod, 20);
+ pwtest_int_eq(_SPA_TYPE_LAST, 21);
+
+ pwtest_int_eq(SPA_TYPE_EVENT_START, 0x20000);
+ pwtest_int_eq(SPA_TYPE_EVENT_Device, 0x20001);
+ pwtest_int_eq(SPA_TYPE_EVENT_Node, 0x20002);
+ pwtest_int_eq(_SPA_TYPE_EVENT_LAST, 0x20003);
+
+ pwtest_int_eq(SPA_TYPE_COMMAND_START, 0x30000);
+ pwtest_int_eq(SPA_TYPE_COMMAND_Device, 0x30001);
+ pwtest_int_eq(SPA_TYPE_COMMAND_Node, 0x30002);
+ pwtest_int_eq(_SPA_TYPE_COMMAND_LAST, 0x30003);
+
+ pwtest_int_eq(SPA_TYPE_OBJECT_START, 0x40000);
+ pwtest_int_eq(SPA_TYPE_OBJECT_PropInfo, 0x40001);
+ pwtest_int_eq(SPA_TYPE_OBJECT_Props, 0x40002);
+ pwtest_int_eq(SPA_TYPE_OBJECT_Format, 0x40003);
+ pwtest_int_eq(SPA_TYPE_OBJECT_ParamBuffers, 0x40004);
+ pwtest_int_eq(SPA_TYPE_OBJECT_ParamMeta, 0x40005);
+ pwtest_int_eq(SPA_TYPE_OBJECT_ParamIO, 0x40006);
+ pwtest_int_eq(SPA_TYPE_OBJECT_ParamProfile, 0x40007);
+ pwtest_int_eq(SPA_TYPE_OBJECT_ParamPortConfig, 0x40008);
+ pwtest_int_eq(SPA_TYPE_OBJECT_ParamRoute, 0x40009);
+ pwtest_int_eq(SPA_TYPE_OBJECT_Profiler, 0x4000a);
+ pwtest_int_eq(SPA_TYPE_OBJECT_ParamLatency, 0x4000b);
+ pwtest_int_eq(SPA_TYPE_OBJECT_ParamProcessLatency, 0x4000c);
+ pwtest_int_eq(_SPA_TYPE_OBJECT_LAST, 0x4000d);
+
+ pwtest_int_eq(SPA_TYPE_VENDOR_PipeWire, 0x02000000);
+ pwtest_int_eq(SPA_TYPE_VENDOR_Other, 0x7f000000);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_macros)
+{
+ uint8_t ptr[64];
+ uint16_t i16[14];
+ uint32_t i32[10];
+ uint64_t i64[12];
+ unsigned char c[16];
+
+ pwtest_int_eq(SPA_MIN(1, 2), 1);
+ pwtest_int_eq(SPA_MIN(1, -2), -2);
+ pwtest_int_eq(SPA_MAX(1, 2), 2);
+ pwtest_int_eq(SPA_MAX(1, -2), 1);
+ pwtest_int_eq(SPA_CLAMP(23, 1, 16), 16);
+ pwtest_int_eq(SPA_CLAMP(-1, 1, 16), 1);
+ pwtest_int_eq(SPA_CLAMP(8, 1, 16), 8);
+
+ /* SPA_MEMBER exists for backwards compatibility but should no
+ * longer be used, let's make sure it does what we expect it to */
+ pwtest_ptr_eq(SPA_MEMBER(ptr, 4, void), SPA_PTROFF(ptr, 4, void));
+ pwtest_ptr_eq(SPA_MEMBER(ptr, 32, void), SPA_PTROFF(ptr, 32, void));
+ pwtest_ptr_eq(SPA_MEMBER(ptr, 0, void), SPA_PTROFF(ptr, 0, void));
+ pwtest_ptr_eq(SPA_MEMBER_ALIGN(ptr, 0, 4, void), SPA_PTROFF_ALIGN(ptr, 0, 4, void));
+ pwtest_ptr_eq(SPA_MEMBER_ALIGN(ptr, 4, 32, void), SPA_PTROFF_ALIGN(ptr, 4, 32, void));
+
+ pwtest_int_eq(SPA_N_ELEMENTS(ptr), 64U);
+ pwtest_int_eq(SPA_N_ELEMENTS(i32), 10U);
+ pwtest_int_eq(SPA_N_ELEMENTS(i64), 12U);
+ pwtest_int_eq(SPA_N_ELEMENTS(i16), 14U);
+ pwtest_int_eq(SPA_N_ELEMENTS(c), 16U);
+
+#define check_traversal(array_) \
+ { \
+ int count = 0; \
+ SPA_FOR_EACH_ELEMENT_VAR(array_, it) \
+ *it = count++; \
+ for (size_t i = 0; i < SPA_N_ELEMENTS(array_); i++) \
+ pwtest_int_eq(array_[i], i); \
+ }
+ check_traversal(ptr);
+ check_traversal(i64);
+ check_traversal(i32);
+ check_traversal(i16);
+ check_traversal(c);
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_result)
+{
+ int res;
+ pwtest_int_eq(SPA_RESULT_IS_OK(0), true);
+ pwtest_int_eq(SPA_RESULT_IS_OK(1), true);
+ pwtest_int_eq(SPA_RESULT_IS_ERROR(0), false);
+ pwtest_int_eq(SPA_RESULT_IS_ERROR(1), false);
+ pwtest_int_eq(SPA_RESULT_IS_ERROR(-1), true);
+ pwtest_int_eq(SPA_RESULT_IS_ASYNC(-1), false);
+ pwtest_int_eq(SPA_RESULT_IS_ASYNC(0), false);
+ res = SPA_RESULT_RETURN_ASYNC(11);
+ pwtest_int_eq(SPA_RESULT_IS_ASYNC(res), true);
+ pwtest_int_eq(SPA_RESULT_IS_ERROR(res), false);
+ pwtest_int_eq(SPA_RESULT_IS_OK(res), true);
+ pwtest_int_eq(SPA_RESULT_ASYNC_SEQ(res), 11);
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_dict)
+{
+ struct spa_dict_item items[5] = {
+ SPA_DICT_ITEM_INIT("key", "value"),
+ SPA_DICT_ITEM_INIT("pipe", "wire"),
+ SPA_DICT_ITEM_INIT("test", "Works!"),
+ SPA_DICT_ITEM_INIT("123", ""),
+ SPA_DICT_ITEM_INIT("SPA", "Simple Plugin API"),
+ };
+ struct spa_dict dict = SPA_DICT_INIT_ARRAY (items);
+ const struct spa_dict_item *it;
+ int i = 0;
+
+ pwtest_int_eq(dict.n_items, 5U);
+ pwtest_str_eq(spa_dict_lookup(&dict, "pipe"), "wire");
+ pwtest_str_eq(spa_dict_lookup(&dict, "123"), "");
+ pwtest_str_eq(spa_dict_lookup(&dict, "key"), "value");
+ pwtest_str_eq(spa_dict_lookup(&dict, "SPA"), "Simple Plugin API");
+ pwtest_str_eq(spa_dict_lookup(&dict, "test"), "Works!");
+ pwtest_ptr_null(spa_dict_lookup(&dict, "nonexistent"));
+
+ pwtest_ptr_eq(spa_dict_lookup_item(&dict, "123"), &items[3]);
+ pwtest_ptr_null(spa_dict_lookup_item(&dict, "foobar"));
+
+ spa_dict_for_each(it, &dict) {
+ pwtest_ptr_eq(it, &items[i++]);
+ }
+ pwtest_int_eq(i, 5);
+ return PWTEST_PASS;
+}
+
+struct string_list {
+ char string[20];
+ struct spa_list node;
+};
+
+PWTEST(utils_list)
+{
+ struct string_list list;
+ struct spa_list *head = &list.node;
+ struct string_list *e;
+ int i;
+
+ spa_list_init(head);
+ pwtest_bool_true(spa_list_is_empty(head));
+
+ e = malloc(sizeof(struct string_list));
+ strcpy(e->string, "test");
+ spa_list_insert(head, &e->node);
+ pwtest_bool_false(spa_list_is_empty(head));
+ pwtest_ptr_eq(spa_list_first(head, struct string_list, node), e);
+ pwtest_ptr_eq(spa_list_last(head, struct string_list, node), e);
+
+ e = malloc(sizeof(struct string_list));
+ strcpy(e->string, "pipewire!");
+ spa_list_append(head, &e->node);
+ pwtest_bool_false(spa_list_is_empty(head));
+ pwtest_ptr_eq(spa_list_last(head, struct string_list, node), e);
+
+ e = malloc(sizeof(struct string_list));
+ strcpy(e->string, "First element");
+ spa_list_prepend(head, &e->node);
+ pwtest_bool_false(spa_list_is_empty(head));
+ pwtest_ptr_eq(spa_list_first(head, struct string_list, node), e);
+
+ i = 0;
+ spa_list_for_each(e, head, node) {
+ switch (i++) {
+ case 0:
+ pwtest_str_eq(e->string, "First element");
+ break;
+ case 1:
+ pwtest_str_eq(e->string, "test");
+ break;
+ case 2:
+ pwtest_str_eq(e->string, "pipewire!");
+ break;
+ default:
+ pwtest_fail_if_reached();
+ break;
+ }
+ }
+
+ i = 0;
+ spa_list_consume(e, head, node) {
+ spa_list_remove(&e->node);
+ free(e);
+ i++;
+ }
+ pwtest_int_eq(i, 3);
+ pwtest_bool_true(spa_list_is_empty(head));
+
+ return PWTEST_PASS;
+}
+
+
+struct my_hook {
+ int version;
+ void (*invoke) (void *);
+};
+
+struct my_hook_data {
+ bool cb1;
+ bool cb2;
+ bool cb3;
+};
+
+static void test_hook_callback_1(void *data)
+{
+ ((struct my_hook_data *) data)->cb1 = true;
+}
+
+static void test_hook_callback_2(void *data)
+{
+ ((struct my_hook_data *) data)->cb2 = true;
+}
+
+static void test_hook_callback_3(void *data)
+{
+ ((struct my_hook_data *) data)->cb3 = true;
+}
+
+static void test_hook_callback_4(void *data)
+{
+ pwtest_fail_if_reached();
+}
+
+static int hook_free_count = 0;
+
+static void hook_removed_cb(struct spa_hook *h)
+{
+ free(h);
+ hook_free_count++;
+}
+
+PWTEST(utils_hook)
+{
+ const int HOOK_VERSION = 2;
+ struct spa_hook_list hl;
+ struct my_hook callbacks[4] = {
+ {2, test_hook_callback_1},
+ {3, test_hook_callback_2},
+ {2, test_hook_callback_3},
+ /* version 1 should not be called */
+ {1, test_hook_callback_4}
+ };
+ struct my_hook_data data = {0};
+ struct spa_hook *h;
+ int count = 0;
+
+ spa_hook_list_init(&hl);
+
+ h = malloc(sizeof(struct spa_hook));
+ spa_hook_list_append(&hl, h, &callbacks[1], &data);
+ h->removed = hook_removed_cb;
+
+ h = malloc(sizeof(struct spa_hook));
+ spa_hook_list_append(&hl, h, &callbacks[2], &data);
+ h->removed = hook_removed_cb;
+
+ /* iterate with the simple API */
+ spa_hook_list_call_simple(&hl, struct my_hook, invoke, HOOK_VERSION);
+ pwtest_bool_eq(data.cb1, false);
+ pwtest_bool_eq(data.cb2, true);
+ pwtest_bool_eq(data.cb3, true);
+
+ /* reset cb* booleans to false */
+ memset(&data, 0, sizeof(struct my_hook_data));
+
+ h = malloc(sizeof(struct spa_hook));
+ spa_hook_list_prepend(&hl, h, &callbacks[0], &data);
+ h->removed = hook_removed_cb;
+
+ /* call only the first hook - this should be callback_1 */
+ count = spa_hook_list_call_once(&hl, struct my_hook, invoke, HOOK_VERSION);
+ pwtest_int_eq(count, 1);
+ pwtest_bool_eq(data.cb1, true);
+ pwtest_bool_eq(data.cb2, false);
+ pwtest_bool_eq(data.cb3, false);
+
+ /* reset cb* booleans to false */
+ memset(&data, 0, sizeof(struct my_hook_data));
+
+ /* add callback_4 - this is version 1, so it shouldn't be executed */
+ h = malloc(sizeof(struct spa_hook));
+ spa_hook_list_append(&hl, h, &callbacks[3], &data);
+ h->removed = hook_removed_cb;
+
+ count = spa_hook_list_call(&hl, struct my_hook, invoke, HOOK_VERSION);
+ pwtest_int_eq(count, 3);
+ pwtest_bool_eq(data.cb1, true);
+ pwtest_bool_eq(data.cb2, true);
+ pwtest_bool_eq(data.cb3, true);
+
+ count = 0;
+ hook_free_count = 0;
+ spa_list_consume(h, &hl.list, link) {
+ spa_hook_remove(h);
+ count++;
+ }
+ pwtest_int_eq(count, 4);
+ pwtest_int_eq(hook_free_count, 4);
+
+ /* remove a zeroed hook */
+ struct spa_hook hook;
+ spa_zero(hook);
+ spa_hook_remove(&hook);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_ringbuffer)
+{
+ struct spa_ringbuffer rb;
+ char buffer[20];
+ char readbuf[20];
+ uint32_t idx;
+ int32_t fill;
+
+ spa_ringbuffer_init(&rb);
+ fill = spa_ringbuffer_get_write_index(&rb, &idx);
+ pwtest_int_eq(idx, 0U);
+ pwtest_int_eq(fill, 0);
+
+ spa_ringbuffer_write_data(&rb, buffer, 20, idx, "hello pipewire", 14);
+ spa_ringbuffer_write_update(&rb, idx + 14);
+
+ fill = spa_ringbuffer_get_write_index(&rb, &idx);
+ pwtest_int_eq(idx, 14U);
+ pwtest_int_eq(fill, 14);
+ fill = spa_ringbuffer_get_read_index(&rb, &idx);
+ pwtest_int_eq(idx, 0U);
+ pwtest_int_eq(fill, 14);
+
+ spa_ringbuffer_read_data(&rb, buffer, 20, idx, readbuf, 6);
+ spa_ringbuffer_read_update(&rb, idx + 6);
+ pwtest_int_eq(memcmp(readbuf, "hello ", 6), 0);
+
+ fill = spa_ringbuffer_get_read_index(&rb, &idx);
+ pwtest_int_eq(idx, 6U);
+ pwtest_int_eq(fill, 8);
+ fill = spa_ringbuffer_get_write_index(&rb, &idx);
+ pwtest_int_eq(idx, 14U);
+ pwtest_int_eq(fill, 8);
+
+ spa_ringbuffer_write_data(&rb, buffer, 20, idx, " rocks !!!", 10);
+ spa_ringbuffer_write_update(&rb, idx + 10);
+
+ fill = spa_ringbuffer_get_write_index(&rb, &idx);
+ pwtest_int_eq(idx, 24U);
+ pwtest_int_eq(fill, 18);
+ fill = spa_ringbuffer_get_read_index(&rb, &idx);
+ pwtest_int_eq(idx, 6U);
+ pwtest_int_eq(fill, 18);
+
+ spa_ringbuffer_read_data(&rb, buffer, 20, idx, readbuf, 18);
+ spa_ringbuffer_read_update(&rb, idx + 18);
+ pwtest_str_eq_n(readbuf, "pipewire rocks !!!", 18);
+
+ fill = spa_ringbuffer_get_read_index(&rb, &idx);
+ pwtest_int_eq(idx, 24U);
+ pwtest_int_eq(fill, 0);
+ fill = spa_ringbuffer_get_write_index(&rb, &idx);
+ pwtest_int_eq(idx, 24U);
+ pwtest_int_eq(fill, 0);
+
+ /* actual buffer must have wrapped around */
+ pwtest_str_eq_n(buffer, " !!!o pipewire rocks", 20);
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_strtol)
+{
+ int32_t v = 0xabcd;
+
+ pwtest_bool_true(spa_atoi32("0", &v, 0)); pwtest_int_eq(v, 0);
+ pwtest_bool_true(spa_atoi32("0", &v, 16)); pwtest_int_eq(v, 0);
+ pwtest_bool_true(spa_atoi32("0", &v, 32)); pwtest_int_eq(v, 0);
+ pwtest_bool_true(spa_atoi32("-1", &v, 0)); pwtest_int_eq(v, -1);
+ pwtest_bool_true(spa_atoi32("-1234", &v, 0)); pwtest_int_eq(v, -1234);
+ pwtest_bool_true(spa_atoi32("-2147483648", &v, 0)); pwtest_int_eq(v, -2147483648);
+ pwtest_bool_true(spa_atoi32("+1", &v, 0)); pwtest_int_eq(v, 1);
+ pwtest_bool_true(spa_atoi32("+1234", &v, 0)); pwtest_int_eq(v, 1234);
+ pwtest_bool_true(spa_atoi32("+2147483647", &v, 0)); pwtest_int_eq(v, 2147483647);
+ pwtest_bool_true(spa_atoi32("65535", &v, 0)); pwtest_int_eq(v, 0xffff);
+ pwtest_bool_true(spa_atoi32("65535", &v, 10)); pwtest_int_eq(v, 0xffff);
+ pwtest_bool_true(spa_atoi32("65535", &v, 16)); pwtest_int_eq(v, 0x65535);
+ pwtest_bool_true(spa_atoi32("0xff", &v, 0)); pwtest_int_eq(v, 0xff);
+ pwtest_bool_true(spa_atoi32("0xff", &v, 16)); pwtest_int_eq(v, 0xff);
+
+ v = 0xabcd;
+ pwtest_bool_false(spa_atoi32("0xff", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("fabc", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("fabc", &v, 0)); pwtest_int_eq(v, 0xabcd);
+
+ pwtest_bool_false(spa_atoi32("124bogus", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("124bogus", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("124bogus", &v, 16)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("0xbogus", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("bogus", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("bogus", &v, 16)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("", &v, 16)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32(" ", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32(" ", &v, 0)); pwtest_int_eq(v, 0xabcd);
+
+ pwtest_bool_false(spa_atoi32("-2147483649", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("2147483648", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("9223372036854775807", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("-9223372036854775808", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32("9223372036854775808999", &v, 0)); pwtest_int_eq(v, 0xabcd);
+
+ pwtest_bool_false(spa_atoi32(NULL, &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32(NULL, &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi32(NULL, &v, 16)); pwtest_int_eq(v, 0xabcd);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_strtoul)
+{
+ uint32_t v = 0xabcd;
+
+ pwtest_bool_true(spa_atou32("0", &v, 0)); pwtest_int_eq(v, 0U);
+ pwtest_bool_true(spa_atou32("0", &v, 16)); pwtest_int_eq(v, 0U);
+ pwtest_bool_true(spa_atou32("0", &v, 32)); pwtest_int_eq(v, 0U);
+ pwtest_bool_true(spa_atou32("+1", &v, 0)); pwtest_int_eq(v, 1U);
+ pwtest_bool_true(spa_atou32("+1234", &v, 0)); pwtest_int_eq(v, 1234U);
+ pwtest_bool_true(spa_atou32("+4294967295", &v, 0)); pwtest_int_eq(v, 4294967295U);
+ pwtest_bool_true(spa_atou32("4294967295", &v, 0)); pwtest_int_eq(v, 4294967295U);
+ pwtest_bool_true(spa_atou32("65535", &v, 0)); pwtest_int_eq(v, 0xffffU);
+ pwtest_bool_true(spa_atou32("65535", &v, 10)); pwtest_int_eq(v, 0xffffU);
+ pwtest_bool_true(spa_atou32("65535", &v, 16)); pwtest_int_eq(v, 0x65535U);
+ pwtest_bool_true(spa_atou32("0xff", &v, 0)); pwtest_int_eq(v, 0xffU);
+ pwtest_bool_true(spa_atou32("0xff", &v, 16)); pwtest_int_eq(v, 0xffU);
+
+ v = 0xabcd;
+ pwtest_bool_false(spa_atou32("-1", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("-1234", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("-2147483648", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("0xff", &v, 10)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("fabc", &v, 10)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("fabc", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+
+ pwtest_bool_false(spa_atou32("124bogus", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("124bogus", &v, 10)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("124bogus", &v, 16)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("0xbogus", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("bogus", &v, 10)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("bogus", &v, 16)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("", &v, 10)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("", &v, 16)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32(" ", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32(" ", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+
+ pwtest_bool_false(spa_atou32("-2147483649", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("4294967296", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("9223372036854775807", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("-9223372036854775808", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32("9223372036854775808999", &v, 0)); pwtest_int_eq(v, 0xabcdU);
+
+ pwtest_bool_false(spa_atou32(NULL, &v, 0)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32(NULL, &v, 10)); pwtest_int_eq(v, 0xabcdU);
+ pwtest_bool_false(spa_atou32(NULL, &v, 16)); pwtest_int_eq(v, 0xabcdU);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_strtoll)
+{
+ int64_t v = 0xabcd;
+
+ pwtest_bool_true(spa_atoi64("0", &v, 0)); pwtest_int_eq(v, 0);
+ pwtest_bool_true(spa_atoi64("0", &v, 16)); pwtest_int_eq(v, 0);
+ pwtest_bool_true(spa_atoi64("0", &v, 32)); pwtest_int_eq(v, 0);
+ pwtest_bool_true(spa_atoi64("-1", &v, 0)); pwtest_int_eq(v, -1);
+ pwtest_bool_true(spa_atoi64("-1234", &v, 0)); pwtest_int_eq(v, -1234);
+ pwtest_bool_true(spa_atoi64("-2147483648", &v, 0)); pwtest_int_eq(v, -2147483648);
+ pwtest_bool_true(spa_atoi64("+1", &v, 0)); pwtest_int_eq(v, 1);
+ pwtest_bool_true(spa_atoi64("+1234", &v, 0)); pwtest_int_eq(v, 1234);
+ pwtest_bool_true(spa_atoi64("+2147483647", &v, 0)); pwtest_int_eq(v, 2147483647);
+ pwtest_bool_true(spa_atoi64("65535", &v, 0)); pwtest_int_eq(v, 0xffff);
+ pwtest_bool_true(spa_atoi64("65535", &v, 10)); pwtest_int_eq(v, 0xffff);
+ pwtest_bool_true(spa_atoi64("65535", &v, 16)); pwtest_int_eq(v, 0x65535);
+ pwtest_bool_true(spa_atoi64("0xff", &v, 0)); pwtest_int_eq(v, 0xff);
+ pwtest_bool_true(spa_atoi64("0xff", &v, 16)); pwtest_int_eq(v, 0xff);
+ pwtest_bool_true(spa_atoi64("9223372036854775807", &v, 0)); pwtest_int_eq(v, 0x7fffffffffffffff);
+ pwtest_bool_true(spa_atoi64("-9223372036854775808", &v, 0)); pwtest_int_eq((uint64_t)v, 0x8000000000000000);
+
+ v = 0xabcd;
+ pwtest_bool_false(spa_atoi64("0xff", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("fabc", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("fabc", &v, 0)); pwtest_int_eq(v, 0xabcd);
+
+ pwtest_bool_false(spa_atoi64("124bogus", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("124bogus", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("124bogus", &v, 16)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("0xbogus", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("bogus", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("bogus", &v, 16)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("", &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64("", &v, 16)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64(" ", &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64(" ", &v, 0)); pwtest_int_eq(v, 0xabcd);
+
+ pwtest_bool_false(spa_atoi64("9223372036854775808999", &v, 0)); pwtest_int_eq(v, 0xabcd);
+
+ pwtest_bool_false(spa_atoi64(NULL, &v, 0)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64(NULL, &v, 10)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atoi64(NULL, &v, 16)); pwtest_int_eq(v, 0xabcd);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_strtof)
+{
+ float v = 0xabcd;
+
+ setlocale(LC_NUMERIC, "C"); /* For decimal number parsing */
+
+ pwtest_bool_true(spa_atof("0", &v)); pwtest_double_eq(v, 0.0f);
+ pwtest_bool_true(spa_atof("0.00", &v)); pwtest_double_eq(v, 0.0f);
+ pwtest_bool_true(spa_atof("1", &v)); pwtest_double_eq(v, 1.0f);
+ pwtest_bool_true(spa_atof("-1", &v)); pwtest_double_eq(v, -1.0f);
+ pwtest_bool_true(spa_atof("0x1", &v)); pwtest_double_eq(v, 1.0f);
+
+ v = 0xabcd;
+ pwtest_bool_false(spa_atof("0,00", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atof("fabc", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atof("1.bogus", &v));pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atof("1.0a", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atof(" ", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atof(" ", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atof("", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atof(NULL, &v)); pwtest_int_eq(v, 0xabcd);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_strtod)
+{
+ double v = 0xabcd;
+
+ pwtest_bool_true(spa_atod("0", &v)); pwtest_double_eq(v, 0.0);
+ pwtest_bool_true(spa_atod("0.00", &v)); pwtest_double_eq(v, 0.0);
+ pwtest_bool_true(spa_atod("1", &v)); pwtest_double_eq(v, 1.0);
+ pwtest_bool_true(spa_atod("-1", &v)); pwtest_double_eq(v, -1.0);
+ pwtest_bool_true(spa_atod("0x1", &v)); pwtest_double_eq(v, 1.0);
+
+ v = 0xabcd;
+ pwtest_bool_false(spa_atod("0,00", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atod("fabc", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atod("1.bogus", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atod("1.0a", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atod(" ", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atod(" ", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atod("", &v)); pwtest_int_eq(v, 0xabcd);
+ pwtest_bool_false(spa_atod(NULL, &v)); pwtest_int_eq(v, 0xabcd);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_streq)
+{
+ pwtest_bool_true(spa_streq(NULL, NULL));
+ pwtest_bool_true(spa_streq("", ""));
+ pwtest_bool_true(spa_streq("a", "a"));
+ pwtest_bool_true(spa_streq("abc", "abc"));
+ pwtest_bool_false(spa_streq(NULL, "abc"));
+ pwtest_bool_false(spa_streq("abc", NULL));
+
+ pwtest_bool_true(spa_strneq("abc", "aaa", 1));
+ pwtest_bool_true(spa_strneq("abc", "abc", 7));
+ pwtest_bool_false(spa_strneq("abc", "aaa", 2));
+ pwtest_bool_false(spa_strneq("abc", NULL, 7));
+ pwtest_bool_false(spa_strneq(NULL, "abc", 7));
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_strendswith)
+{
+ pwtest_bool_true(spa_strendswith("foo", "o"));
+ pwtest_bool_true(spa_strendswith("foobar", "bar"));
+
+ pwtest_bool_false(spa_strendswith(NULL, "bar"));
+ pwtest_bool_false(spa_strendswith("foo", "f"));
+ pwtest_bool_false(spa_strendswith("foo", "fo"));
+ pwtest_bool_false(spa_strendswith("foo", "foobar"));
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_strendswith_null_suffix)
+{
+ spa_strendswith("foo", NULL);
+
+ return PWTEST_FAIL;
+}
+
+PWTEST(utils_atob)
+{
+ pwtest_bool_true(spa_atob("true"));
+ pwtest_bool_true(spa_atob("1"));
+ pwtest_bool_false(spa_atob("0"));
+ pwtest_bool_false(spa_atob("-1"));
+ pwtest_bool_false(spa_atob("10"));
+ pwtest_bool_false(spa_atob("11"));
+ pwtest_bool_false(spa_atob("t"));
+ pwtest_bool_false(spa_atob("yes"));
+ pwtest_bool_false(spa_atob("no"));
+ pwtest_bool_false(spa_atob(NULL));
+ pwtest_bool_false(spa_atob("True")); /* lower-case required */
+ pwtest_bool_false(spa_atob("TRUE"));
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_ansi)
+{
+ /* Visual test only */
+ printf("%sBOLD%s\n", SPA_ANSI_BOLD, SPA_ANSI_RESET);
+ printf("%sUNDERLINE%s\n", SPA_ANSI_UNDERLINE, SPA_ANSI_RESET);
+ printf("%sITALIC%s\n", SPA_ANSI_ITALIC, SPA_ANSI_RESET);
+
+ printf("%sBLACK%s\n", SPA_ANSI_BLACK, SPA_ANSI_RESET);
+ printf("%sBRIGHT_BLACK%s\n", SPA_ANSI_BRIGHT_BLACK, SPA_ANSI_RESET);
+ printf("%sDARK_BLACK%s\n", SPA_ANSI_DARK_BLACK, SPA_ANSI_RESET);
+ printf("%sBOLD_BLACK%s\n", SPA_ANSI_BOLD_BLACK, SPA_ANSI_RESET);
+
+ printf("%sRED%s\n", SPA_ANSI_RED, SPA_ANSI_RESET);
+ printf("%sBRIGHT_RED%s\n", SPA_ANSI_BRIGHT_RED, SPA_ANSI_RESET);
+ printf("%sDARK_RED%s\n", SPA_ANSI_DARK_RED, SPA_ANSI_RESET);
+ printf("%sBOLD_RED%s\n", SPA_ANSI_BOLD_RED, SPA_ANSI_RESET);
+
+ printf("%sGREEN%s\n", SPA_ANSI_GREEN, SPA_ANSI_RESET);
+ printf("%sBRIGHT_GREEN%s\n", SPA_ANSI_BRIGHT_GREEN, SPA_ANSI_RESET);
+ printf("%sDARK_GREEN%s\n", SPA_ANSI_DARK_GREEN, SPA_ANSI_RESET);
+ printf("%sBOLD_GREEN%s\n", SPA_ANSI_BOLD_GREEN, SPA_ANSI_RESET);
+
+ printf("%sYELLOW%s\n", SPA_ANSI_YELLOW, SPA_ANSI_RESET);
+ printf("%sBRIGHT_YELLOW%s\n", SPA_ANSI_BRIGHT_YELLOW, SPA_ANSI_RESET);
+ printf("%sDARK_YELLOW%s\n", SPA_ANSI_DARK_YELLOW, SPA_ANSI_RESET);
+ printf("%sBOLD_YELLOW%s\n", SPA_ANSI_BOLD_YELLOW, SPA_ANSI_RESET);
+
+ printf("%sBLUE%s\n", SPA_ANSI_BLUE, SPA_ANSI_RESET);
+ printf("%sBRIGHT_BLUE%s\n", SPA_ANSI_BRIGHT_BLUE, SPA_ANSI_RESET);
+ printf("%sDARK_BLUE%s\n", SPA_ANSI_DARK_BLUE, SPA_ANSI_RESET);
+ printf("%sBOLD_BLUE%s\n", SPA_ANSI_BOLD_BLUE, SPA_ANSI_RESET);
+
+ printf("%sMAGENTA%s\n", SPA_ANSI_MAGENTA, SPA_ANSI_RESET);
+ printf("%sBRIGHT_MAGENTA%s\n", SPA_ANSI_BRIGHT_MAGENTA, SPA_ANSI_RESET);
+ printf("%sDARK_MAGENTA%s\n", SPA_ANSI_DARK_MAGENTA, SPA_ANSI_RESET);
+ printf("%sBOLD_MAGENTA%s\n", SPA_ANSI_BOLD_MAGENTA, SPA_ANSI_RESET);
+
+ printf("%sCYAN%s\n", SPA_ANSI_CYAN, SPA_ANSI_RESET);
+ printf("%sBRIGHT_CYAN%s\n", SPA_ANSI_BRIGHT_CYAN, SPA_ANSI_RESET);
+ printf("%sDARK_CYAN%s\n", SPA_ANSI_DARK_CYAN, SPA_ANSI_RESET);
+ printf("%sBOLD_CYAN%s\n", SPA_ANSI_BOLD_CYAN, SPA_ANSI_RESET);
+
+ printf("%sWHITE%s\n", SPA_ANSI_WHITE, SPA_ANSI_RESET);
+ printf("%sBRIGHT_WHITE%s\n", SPA_ANSI_BRIGHT_WHITE, SPA_ANSI_RESET);
+ printf("%sDARK_WHITE%s\n", SPA_ANSI_DARK_WHITE, SPA_ANSI_RESET);
+ printf("%sBOLD_WHITE%s\n", SPA_ANSI_BOLD_WHITE, SPA_ANSI_RESET);
+
+
+ /* Background colors */
+
+ printf("%sBG_BLACK%s\n", SPA_ANSI_BG_BLACK, SPA_ANSI_RESET);
+ printf("%sBG_BRIGHT_BLACK%s\n", SPA_ANSI_BG_BRIGHT_BLACK, SPA_ANSI_RESET);
+
+ printf("%sBG_RED%s\n", SPA_ANSI_BG_RED, SPA_ANSI_RESET);
+ printf("%sBG_BRIGHT_RED%s\n", SPA_ANSI_BG_BRIGHT_RED, SPA_ANSI_RESET);
+
+ printf("%sBG_GREEN%s\n", SPA_ANSI_BG_GREEN, SPA_ANSI_RESET);
+ printf("%sBG_BRIGHT_GREEN%s\n", SPA_ANSI_BG_BRIGHT_GREEN, SPA_ANSI_RESET);
+
+ printf("%sBG_YELLOW%s\n", SPA_ANSI_BG_YELLOW, SPA_ANSI_RESET);
+ printf("%sBG_BRIGHT_YELLOW%s\n", SPA_ANSI_BG_BRIGHT_YELLOW, SPA_ANSI_RESET);
+
+ printf("%sBG_BLUE%s\n", SPA_ANSI_BG_BLUE, SPA_ANSI_RESET);
+ printf("%sBG_BRIGHT_BLUE%s\n", SPA_ANSI_BG_BRIGHT_BLUE, SPA_ANSI_RESET);
+
+ printf("%sBG_MAGENTA%s\n", SPA_ANSI_BG_MAGENTA, SPA_ANSI_RESET);
+ printf("%sBG_BRIGHT_MAGENTA%s\n", SPA_ANSI_BG_BRIGHT_MAGENTA, SPA_ANSI_RESET);
+
+ printf("%sBG_CYAN%s\n", SPA_ANSI_BG_CYAN, SPA_ANSI_RESET);
+ printf("%sBG_BRIGHT_CYAN%s\n", SPA_ANSI_BG_BRIGHT_CYAN, SPA_ANSI_RESET);
+
+ printf("%sBG_WHITE%s\n", SPA_ANSI_BG_WHITE, SPA_ANSI_RESET);
+ printf("%sBG_BRIGHT_WHITE%s\n", SPA_ANSI_BG_BRIGHT_WHITE, SPA_ANSI_RESET);
+
+ /* A combo */
+ printf("normal%s%s%sBG_BLUE,ITALIC,BOLD_YELLOW%snormal\n", SPA_ANSI_BG_BLUE,
+ SPA_ANSI_ITALIC, SPA_ANSI_BOLD_YELLOW, SPA_ANSI_RESET);
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_snprintf)
+{
+ char dest[8];
+ int len;
+
+ /* Basic printf */
+ pwtest_int_eq(spa_scnprintf(dest, sizeof(dest), "foo%d%s", 10, "2"), 6);
+ pwtest_str_eq(dest, "foo102");
+ /* Print a few strings, make sure dest is truncated and return value
+ * is the length of the returned string */
+ pwtest_int_eq(spa_scnprintf(dest, sizeof(dest), "1234567"), 7);
+ pwtest_str_eq(dest, "1234567");
+ pwtest_int_eq(spa_scnprintf(dest, sizeof(dest), "12345678"), 7);
+ pwtest_str_eq(dest, "1234567");
+ pwtest_int_eq(spa_scnprintf(dest, sizeof(dest), "123456789"), 7);
+ pwtest_str_eq(dest, "1234567");
+ /* Same as above, but with printf %s expansion */
+ pwtest_int_eq(spa_scnprintf(dest, sizeof(dest), "%s", "1234567"), 7);
+ pwtest_str_eq(dest, "1234567");
+ pwtest_int_eq(spa_scnprintf(dest, sizeof(dest), "%s", "12345678"), 7);
+ pwtest_str_eq(dest, "1234567");
+ pwtest_int_eq(spa_scnprintf(dest, sizeof(dest), "%s", "123456789"), 7);
+ pwtest_str_eq(dest, "1234567");
+
+ pwtest_int_eq(spa_scnprintf(dest, 2, "1234567"), 1);
+ pwtest_str_eq(dest, "1");
+ pwtest_int_eq(spa_scnprintf(dest, 1, "1234567"), 0);
+ pwtest_str_eq(dest, "");
+
+ /* The "append until buffer is full" use-case */
+ len = 0;
+ while ((size_t)len < sizeof(dest) - 1)
+ len += spa_scnprintf(dest + len, sizeof(dest) - len, "123");
+ /* and once more for good measure, this should print 0 characters */
+ len = spa_scnprintf(dest + len, sizeof(dest) - len, "abc");
+ pwtest_int_eq(len, 0);
+ pwtest_str_eq(dest, "1231231");
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_snprintf_abort_neg_size)
+{
+ size_t size = pwtest_get_iteration(current_test);
+ char dest[8];
+
+ if (RUNNING_ON_VALGRIND)
+ return PWTEST_SKIP;
+
+ spa_scnprintf(dest, size, "1234"); /* expected to abort() */
+
+ return PWTEST_FAIL;
+}
+
+struct cbtest_data {
+ bool invoked;
+ const char *data;
+};
+
+static void cbtest_func(void *object, const char *msg)
+{
+ struct cbtest_data *data = object;
+ data->invoked = true;
+ data->data = msg;
+}
+
+PWTEST(utils_callback)
+{
+ struct cbtest_methods {
+ uint32_t version;
+ void (*func_v0)(void *object, const char *msg);
+ void (*func_v1)(void *object, const char *msg);
+ } methods = { 0, cbtest_func, cbtest_func };
+ struct cbtest {
+ struct spa_interface iface;
+ } cbtest;
+ struct cbtest_data data;
+
+ /* Interface version doesn't matter for this test */
+ cbtest.iface = SPA_INTERFACE_INIT("cbtest type", 0, &methods, &data);
+
+ /* Methods are version 0 */
+ methods.version = 0;
+ data.invoked = false;
+ spa_interface_call(&cbtest.iface,
+ struct cbtest_methods,
+ func_v0, 0, "cbtest v0");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.data, "cbtest v0");
+
+ /* v1 call should be silently filtered */
+ data.invoked = false;
+ spa_interface_call(&cbtest.iface,
+ struct cbtest_methods,
+ func_v1, 1, "cbtest v1");
+ pwtest_bool_false(data.invoked);
+
+ /* Methods are version 1 */
+ methods.version = 1;
+ data.invoked = false;
+ spa_interface_call(&cbtest.iface,
+ struct cbtest_methods,
+ func_v0, 0, "cbtest v0");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.data, "cbtest v0");
+
+ /* v1 call expected to be called */
+ data.invoked = false;
+ spa_interface_call(&cbtest.iface,
+ struct cbtest_methods,
+ func_v1, 1, "cbtest v1");
+ pwtest_bool_true(data.invoked);
+ pwtest_str_eq(data.data, "cbtest v1");
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_callback_func_is_null)
+{
+ struct cbtest_methods {
+ uint32_t version;
+ void (*func_v0)(void *object, const char *msg);
+ void (*func_v1)(void *object, const char *msg);
+ } methods = { 0, NULL, NULL };
+ struct cbtest {
+ struct spa_interface iface;
+ } cbtest;
+ struct cbtest_data data;
+
+ /* Interface version doesn't matter for this test */
+ cbtest.iface = SPA_INTERFACE_INIT("cbtest type", 0, &methods, &data);
+
+ /* Methods are version 0 */
+ methods.version = 0;
+
+ /* func_v0 and func_v1 are NULL so this shouldn't crash */
+ data.invoked = false;
+ spa_interface_call(&cbtest.iface,
+ struct cbtest_methods,
+ func_v0, 0, "cbtest v0");
+ pwtest_bool_false(data.invoked);
+ spa_interface_call(&cbtest.iface,
+ struct cbtest_methods,
+ func_v1, 0, "cbtest v1");
+ pwtest_bool_false(data.invoked);
+
+ /* func_v1 is NULL so this shouldn't crash, though the call should get
+ * filtered anyway due to version mismatch */
+ spa_interface_call(&cbtest.iface,
+ struct cbtest_methods,
+ func_v1, 1, "cbtest v1");
+ pwtest_bool_false(data.invoked);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_callback_version)
+{
+ struct cbtest_methods {
+ uint32_t version;
+ void (*func_v0)(void *object, const char *msg);
+ } methods = { 0, cbtest_func };
+ struct cbtest {
+ struct spa_interface iface;
+ } cbtest;
+ struct cbtest_data data;
+
+ /* Interface version doesn't matter for this test */
+ cbtest.iface = SPA_INTERFACE_INIT("cbtest type", 0, &methods, &data);
+
+ /* Methods are version 0 */
+ methods.version = 0;
+ pwtest_bool_true(spa_interface_callback_version_min(&cbtest.iface,
+ struct cbtest_methods,
+ 0));
+ pwtest_bool_false(spa_interface_callback_version_min(&cbtest.iface,
+ struct cbtest_methods,
+ 1));
+ /* Methods are version 1 */
+ methods.version = 1;
+ pwtest_bool_true(spa_interface_callback_version_min(&cbtest.iface,
+ struct cbtest_methods,
+ 0));
+ pwtest_bool_true(spa_interface_callback_version_min(&cbtest.iface,
+ struct cbtest_methods,
+ 1));
+ pwtest_bool_false(spa_interface_callback_version_min(&cbtest.iface,
+ struct cbtest_methods,
+ 2));
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(spa_utils)
+{
+ pwtest_add(utils_abi_sizes, PWTEST_NOARG);
+ pwtest_add(utils_abi, PWTEST_NOARG);
+ pwtest_add(utils_macros, PWTEST_NOARG);
+ pwtest_add(utils_result, PWTEST_NOARG);
+ pwtest_add(utils_dict, PWTEST_NOARG);
+ pwtest_add(utils_list, PWTEST_NOARG);
+ pwtest_add(utils_hook, PWTEST_NOARG);
+ pwtest_add(utils_ringbuffer, PWTEST_NOARG);
+ pwtest_add(utils_strtol, PWTEST_NOARG);
+ pwtest_add(utils_strtoul, PWTEST_NOARG);
+ pwtest_add(utils_strtoll, PWTEST_NOARG);
+ pwtest_add(utils_strtof, PWTEST_NOARG);
+ pwtest_add(utils_strtod, PWTEST_NOARG);
+ pwtest_add(utils_streq, PWTEST_NOARG);
+ pwtest_add(utils_strendswith, PWTEST_NOARG);
+ pwtest_add(utils_strendswith_null_suffix,
+ PWTEST_ARG_SIGNAL, SIGABRT);
+ pwtest_add(utils_snprintf, PWTEST_NOARG);
+ pwtest_add(utils_snprintf_abort_neg_size,
+ PWTEST_ARG_SIGNAL, SIGABRT,
+ PWTEST_ARG_RANGE, -2, 0);
+ pwtest_add(utils_atob, PWTEST_NOARG);
+ pwtest_add(utils_ansi, PWTEST_NOARG);
+ pwtest_add(utils_callback, PWTEST_NOARG);
+ pwtest_add(utils_callback_func_is_null, PWTEST_NOARG);
+ pwtest_add(utils_callback_version, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-support.c b/test/test-support.c
new file mode 100644
index 0000000..e8a18f0
--- /dev/null
+++ b/test/test-support.c
@@ -0,0 +1,88 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pwtest.h"
+
+#include <spa/utils/names.h>
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+
+PWTEST(pwtest_load_nonexisting)
+{
+ struct pwtest_spa_plugin *plugin;
+ void *iface;
+
+ plugin = pwtest_spa_plugin_new();
+
+ pwtest_neg_errno_check(
+ pwtest_spa_plugin_try_load_interface(plugin, &iface,
+ "support/does_not_exist",
+ SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log,
+ NULL),
+ -ENOENT);
+
+ pwtest_neg_errno_check(
+ pwtest_spa_plugin_try_load_interface(plugin, &iface,
+ "support/libspa-support",
+ "foo.bar", SPA_TYPE_INTERFACE_Log,
+ NULL),
+ -EINVAL);
+
+ pwtest_neg_errno_check(
+ pwtest_spa_plugin_try_load_interface(plugin, &iface,
+ "support/libspa-support",
+ SPA_NAME_SUPPORT_LOG,
+ "foo", NULL),
+ -ENOSYS);
+
+ pwtest_spa_plugin_destroy(plugin);
+
+ return PWTEST_PASS;
+}
+
+PWTEST(pwtest_load_plugin)
+{
+ struct pwtest_spa_plugin *plugin;
+ void *iface;
+
+ plugin = pwtest_spa_plugin_new();
+
+ pwtest_neg_errno_ok(
+ pwtest_spa_plugin_try_load_interface(plugin, &iface,
+ "support/libspa-support",
+ SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log,
+ NULL)
+ );
+
+ pwtest_spa_plugin_destroy(plugin);
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(support)
+{
+ pwtest_add(pwtest_load_nonexisting, PWTEST_NOARG);
+ pwtest_add(pwtest_load_plugin, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}
diff --git a/test/test-utils.c b/test/test-utils.c
new file mode 100644
index 0000000..84a2fcb
--- /dev/null
+++ b/test/test-utils.c
@@ -0,0 +1,253 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pwtest.h"
+#include <limits.h>
+
+#include <pipewire/utils.h>
+
+#include <spa/utils/string.h>
+
+static void test_destroy(void *object)
+{
+ pwtest_fail_if_reached();
+}
+
+PWTEST(utils_abi)
+{
+ pw_destroy_t f = test_destroy;
+
+ pwtest_ptr_eq(f, &test_destroy);
+
+ return PWTEST_PASS;
+}
+
+static void test__pw_split_walk(void)
+{
+ const struct test_case {
+ const char * const input;
+ const char * const delim;
+ const char * const * const expected;
+ } test_cases[] = {
+ {
+ .input = "a \n test string \n \r ",
+ .delim = " \r\n",
+ .expected = (const char *[]) {
+ "a",
+ "test",
+ "string",
+ NULL
+ },
+ },
+ {
+ .input = "::field1::field2:: field3:::::",
+ .delim = ":",
+ .expected = (const char *[]) {
+ "field1",
+ "field2",
+ " field3",
+ NULL
+ },
+ },
+ {
+ .input = ",,,,,,,,,,,,",
+ .delim = ",",
+ .expected = (const char *[]) {
+ NULL
+ },
+ },
+ {
+ .input = ",;,,,'''':::':::,,,,;",
+ .delim = ",:';",
+ .expected = (const char *[]) {
+ NULL
+ },
+ },
+ {
+ .input = "aaa:bbb,ccc##ddd/#,eee?fff...",
+ .delim = ":,#/?",
+ .expected = (const char *[]) {
+ "aaa",
+ "bbb",
+ "ccc",
+ "ddd",
+ "eee",
+ "fff...",
+ NULL
+ },
+ },
+ {
+ .input = "line 1\na different line\nthe third line\n",
+ .delim = "\n",
+ .expected = (const char *[]) {
+ "line 1",
+ "a different line",
+ "the third line",
+ NULL
+ },
+ },
+ {
+ .input = "no delimiters",
+ .delim = ",:/;",
+ .expected = (const char *[]) {
+ "no delimiters",
+ NULL
+ },
+ },
+ {
+ .input = "delimiter at the end,;",
+ .delim = ",;",
+ .expected = (const char *[]) {
+ "delimiter at the end",
+ NULL
+ },
+ },
+ {
+ .input = "/delimiter on both ends,",
+ .delim = "/,",
+ .expected = (const char *[]) {
+ "delimiter on both ends",
+ NULL
+ },
+ },
+ {
+ .input = ",delimiter at the beginning",
+ .delim = ",",
+ .expected = (const char *[]) {
+ "delimiter at the beginning",
+ NULL
+ },
+ },
+ {
+ .input = "/usr/lib/pipewire-0.3/libpipewire.so",
+ .delim = "/",
+ .expected = (const char *[]) {
+ "usr",
+ "lib",
+ "pipewire-0.3",
+ "libpipewire.so",
+ NULL
+ }
+ },
+ {
+ .input = "/home/x/.ladspa:/usr/lib/ladspa:/usr/local/lib/ladspa",
+ .delim = ":",
+ .expected = (const char *[]) {
+ "/home/x/.ladspa",
+ "/usr/lib/ladspa",
+ "/usr/local/lib/ladspa",
+ NULL
+ }
+ },
+ {
+ .input = "\n field1 \t\n field2 \t \t field3",
+ .delim = " \n\t",
+ .expected = (const char *[]) {
+ "field1",
+ "field2",
+ "field3",
+ NULL
+ }
+ },
+ };
+
+ SPA_FOR_EACH_ELEMENT_VAR(test_cases, tc) {
+ const char *str = tc->input, *s;
+ const char *state = NULL;
+ size_t j = 0, len;
+
+ while ((s = pw_split_walk(str, tc->delim, &len, &state)) != NULL && tc->expected[j] != NULL) {
+ pwtest_int_eq(strlen(tc->expected[j]), len);
+ pwtest_str_eq_n(s, tc->expected[j], len);
+
+ j += 1;
+ }
+
+ pwtest_ptr_null(s);
+ pwtest_ptr_null(tc->expected[j]);
+ }
+}
+
+static void test__pw_split_strv(void)
+{
+ const char *test1 = "a \n test string \n \r ";
+ const char *del = "\n\r ";
+ const char *test2 = "a:";
+ const char *del2 = ":";
+ int n_tokens;
+ char **res;
+
+ res = pw_split_strv(test1, del, INT_MAX, &n_tokens);
+ pwtest_ptr_notnull(res);
+ pwtest_int_eq(n_tokens, 3);
+ pwtest_str_eq(res[0], "a");
+ pwtest_str_eq(res[1], "test");
+ pwtest_str_eq(res[2], "string");
+ pwtest_ptr_null(res[3]);
+ pw_free_strv(res);
+
+ res = pw_split_strv(test1, del, 2, &n_tokens);
+ pwtest_ptr_notnull(res);
+ pwtest_int_eq(n_tokens, 2);
+ pwtest_str_eq(res[0], "a");
+ pwtest_str_eq(res[1], "test string \n \r ");
+ pwtest_ptr_null(res[2]);
+ pw_free_strv(res);
+
+ res = pw_split_strv(test2, del2, 2, &n_tokens);
+ pwtest_ptr_notnull(res);
+ pwtest_int_eq(n_tokens, 1);
+ pwtest_str_eq(res[0], "a");
+ pwtest_ptr_null(res[1]);
+ pw_free_strv(res);
+}
+
+PWTEST(utils_split)
+{
+ test__pw_split_walk();
+ test__pw_split_strv();
+
+ return PWTEST_PASS;
+}
+
+PWTEST(utils_strip)
+{
+ char test1[] = " \n\r \n a test string \n \r ";
+ char test2[] = " \n\r \n \n \r ";
+ char test3[] = "a test string";
+ spa_assert_se(spa_streq(pw_strip(test1, "\n\r "), "a test string"));
+ spa_assert_se(spa_streq(pw_strip(test2, "\n\r "), ""));
+ spa_assert_se(spa_streq(pw_strip(test3, "\n\r "), "a test string"));
+
+ return PWTEST_PASS;
+}
+
+PWTEST_SUITE(utils)
+{
+ pwtest_add(utils_abi, PWTEST_NOARG);
+ pwtest_add(utils_split, PWTEST_NOARG);
+ pwtest_add(utils_strip, PWTEST_NOARG);
+
+ return PWTEST_PASS;
+}